
Опенинг
Lapka — локальный аниме‑плеер. Вставляю в неё ссылку на страницу аниме или одной серии, а она сама находит серии, сезоны и озвучки и собирает их в одну очередь. Знакомый, которому я дал её потрогать, сказал так:
Прикольно: нода крутится, парсит, а воспроизводит будто локальные файлы.
Код Lapka писала модель Claude Fable 5.1, и техническая сторона за ней. На мне продукт: как Lapka ведёт себя и как выглядит. Код открыт на GitHub, а здесь речь пойдёт о решениях, из которых Lapka сложилась. Часть из них предложила модель.
Серия 1. Одна фраза
Всё сложилось довольно спонтанно. Я смотрел аниме на сайтах, а незадолго до этого из интереса собрал себе PIP‑Player, локальный плеер для сериалов, ради браузерного режима «картинка в картинке». Он складывал серии в очередь и помнил выбранную аудиодорожку от серии к серии, но работал только с файлами, и серии приходилось сначала скачивать. А на сайтах я устал каждый раз переключать серии мышкой и выбирать, из какого плеера смотреть. Муторно, а хочется просто включить и смотреть. Как дизайнер я умею выключать в себе критика и просто пользоваться, а тут решил поэкспериментировать.
Эксперимент начался с вопроса: «зачем скачивать, если можно не скачивать?». Смотреть PIP‑Player уже умел, оставалось научить его брать аниме из ссылки, и под это я завёл на его основе отдельный проект, Lapka. Из этой мысли получилась базовая пользовательская история:
Вставил ссылку, вижу сериал со всеми сериями, выбрал озвучку, смотрю.
С этой фразы и началась работа над проектом. В начале каждой сессии сверяюсь, думает ли модель в ту же сторону. Новую фичу сначала прошу разобрать: как всё устроено сейчас и что с этим можно сделать. Свои идеи подкидываю вопросами, как это могло бы встроиться в то, что уже есть. Проверить устройство в коде я не могу, поэтому предложение модели оцениваю как предложение разработчика в продуктовой команде. Если понятно, зачем оно и что изменится для пользователя, его можно брать. А если не взлетело, откатываю.

Проще всего в этой фразе звучит начало, «вставил ссылку». Чтобы за ним появился сериал со всеми сериями, Lapka должна сама разобраться в странице, на которую ведёт ссылка, а страницы с аниме устроены очень по‑разному.
Серия 2. Страница без инструкции
На одной странице серии идут списком ссылок, на другой спрятаны в выпадающее меню, на третьей их нет совсем, и весь сериал живёт внутри встроенного плеера. С озвучками так же: кнопки над плеером, вкладки, пункты меню внутри него. За годы на таких страницах наслоилось столько UI‑решений, что пользоваться ими неудобно.
Граница у Lapka простая. Она видит ту же страницу, что открыл бы браузер, и берёт с неё только то, что та показывает любому посетителю. Наружу она ничего не отдаёт, всё собранное остаётся на вашем компьютере, и подключиться к ней можно только с него. А если серии показываются только после входа на сайт или плеер открывается только в браузере, Lapka дальше не идёт. Такие сайты называю закрытыми дверями. DRM она не обходит: такое видео в ней просто не заиграет.
Сама Lapka — маленький сервер на Node.js на вашем компьютере. Она читает страницу, находит видео и пропускает его через себя в браузер, отсюда и «будто локальные файлы». Страницу с аниме она разбирает по правилам, которые модель написала заранее, и в момент просмотра никакой модели рядом нет. Поэтому разбор не зависит от внешних сервисов, а результат зависит только от того, что отдал сайт.
Был прямой путь: писать отдельный парсер под каждую страницу. Он быстро дал бы результат, но любое изменение страницы ломало бы просмотр, и со временем продукт превратился бы в коллекцию исключений.
По продуктовой привычке мне ближе подход дизайн‑систем, где любой экран собирается из описанных паттернов. Lapka так же разбирает страницу по паттернам: много однотипных ссылок с растущими номерами для неё список серий, а группа кнопок, от которых меняется плеер, переключатель озвучек. Отсюда главное правило проекта: общий механизм должен работать сам. Здесь мне пришлось притормозить модель. Её первые подходы тянули к тому, чтобы быстро сделать что‑то работающее, а мне нужна была архитектура, которая выдержит доработки. Поэтому мы шли по модулям, и на первых шагах модель перепроверяла себя, не отступила ли от выбранной архитектуры. До настоящих сайтов всё проверялось на своём сгенерированном аниме‑сайте. Каждая маленькая страница в нём повторяет одно устройство: серии ссылками, серии списком, плеер во фрейме или озвучки внутри него. Получилось что‑то вроде библиотеки компонентов, только из страниц.
Главное правило проверилось на первом же живом сайте: у него нашёлся открытый API, который отдаёт всё напрямую. Опереться на него было бы проще всего, но мы взяли его как эталон. Lapka читала страницу общим механизмом, а по API сверяли, то ли она нашла. Что общий механизм держит, я понял, когда друзья начали вставлять в Lapka аниме с сайтов, о которых я даже не слышал, и всё равно получали сериал с сезонами и озвучками. А сезоны и фильмы франшизы собрались в одну очередь легко, на эту фичу я изначально не рассчитывал.

Серия 3. Озвучка как дорожка
В ментальной модели зрителя озвучка — это аудиодорожка, которая держится от серии к серии. Технически же на сайтах это чаще всего отдельное видео со своим звуком, и одну и ту же озвучку разные плееры могут подписать по‑разному. Модель предложила разделить выбор: зритель выбирает только озвучку, плеер под неё выбирает Lapka, а названия озвучек она приводит к одному виду. Так пропадает и выбор плеера, который утомлял меня на сайтах. Как устроены озвучки, я знал и до Lapka, поэтому предложение совпало с тем, как задачу видел я. Договариваться ни о чём не пришлось, и заработало всё с первого раза.
При выборе озвучки видно, в каком качестве она доступна, а под названием стоит число источников. Одна и та же озвучка бывает сразу у нескольких плееров. Это число я оставил специально, мне с самого начала хотелось, чтобы устройство Lapka немного просвечивало наружу. По ней понятно, что у Lapka есть куда отступить, если источник отвалится посреди серии. Я на это натыкался сам: видео встало, но почти сразу серия пошла дальше уже из другого источника.

Дорожка должна держаться и при переходе между частями франшизы. Выбранную озвучку Lapka запоминает и следующую часть начинает с неё же. Если в следующей части её нет, Lapka продолжает в доступной и коротко называет, в какой. Так бывает часто: у фильмов и спешлов между сезонами набор озвучек отличается, в очереди на картинке выше у одного из фильмов есть только субтитры, а у второго сезона уже восемнадцать озвучек. Как вести себя на этих стыках, решила модель, а я оценил это уже потом, когда смотрел сам.
Отличать основные сезоны от побочных Lapka не пытается. Умная подсказка, которая ошиблась, раздражает сильнее, чем её отсутствие.

То, что Lapka устроена как со слов зрителя, пригодилось и там, где я этого не планировал. Историю вставленных ссылок после релиза предложили двое знакомых, и добавить её было легко: сериал, его части, озвучка и место остановки уже были. Теперь история нужна и мне самому, в ней видно, где я остановился и в какой озвучке, и клик открывает аниме с нужной серии.
Серия 4. Минута тишины
Однажды мне показалось, что Lapka сломалась: ссылка на большую франшизу открывалась около минуты, и всё это время экран оставался чёрным. Саму задержку модель нашла и убрала, а мне этот случай показал, что не так с экраном ожидания. Он у Lapka был, но появлялся, когда разбор уже подходил к концу, и даже тогда по нему нельзя было понять, что происходит.
С точки зрения UX ожидания это нарушение первой эвристики Нильсена, видимости состояния системы: пустой экран выглядит как лаги, даже когда Lapka вовсю работает. Первым делом экран ожидания стал появляться сразу, как только вставлена ссылка. Дальше нужно было решить, что на нём показывать. Обычный значок загрузки ставят, когда настоящий ход работы показать нельзя. Lapka работает на том же компьютере, что и браузер, и может показывать разбор по ходу. Поэтому на экране ожидания показан сам разбор, в виде командной строки, под стиль логотипа из символов.
Первая версия этого журнала выглядела странной небольшой областью посреди экрана, которая увеличивалась по мере появления строк. Модель переписала блок целиком: видны статусы основных этапов разбора, под ними командной строкой отображён сам разбор, и все элементы сразу в своих размерах.

Вид у журнала технический, но пишет он понятными словами и от лица самой Lapka: “Нашла 12 серий”, «Открыла 3 плеера, потоки есть». Строки появляются по ходу разбора, поэтому, если они идут медленно, видно, что Lapka не зависла, и понятно, чем она занята. Обычно разбор проходит быстро, и журнал не успеваешь разглядеть, но когда он затягивается, мне спокойнее его видеть.
Если разбор не удался, журнал пока просто уступает место уведомлению, что у Lapka не получилось. Как показать, что именно помешало, я ещё не решил, и для UX ожидания это открытый вопрос.
Серия 5. Поведение пикселей
Большие вещи вроде архитектуры, разбора страниц и видео в браузере модель вела сама, а я сверял направление. Воспроизведение с локальных файлов на видео с сайтов она переложила между делом, и возвращаться к этому не пришлось. С интерфейсом так не выходит. Учесть с первого раза все зависимости элемента и сделать его поведение консистентным модель не может, и выправлять нюансы интерфейса — та ещё головная боль. То, что модель делает в интерфейсе сама, обычно выходит дефолтным. Когда я просил что‑то конкретное, а получал дефолт, мы останавливались на этом месте, и я подробно описывал поведение пикселей. Больной темой стали кривые анимаций: мне нужны были эксперименты, подвинуть кривую и посмотреть, а модель правила всё подряд и ломала больше, чем показывала. В итоге выправили. А однажды модель перенесла моноширинный шрифт кнопок и полей на описания, и это тоже пришлось исправлять.
Поиск в истории ссылок модель поставила сверху, где он обычно и стоит. Я опустил его вниз. Блок истории раскрывается от кнопки в подвале очереди, поэтому низ у него всегда на одном месте, а верх зависит от того, сколько записей накопилось. Поиск сверху прыгал бы вместе с хедером блока, а внизу он остаётся там же, где кнопка. История не пытается занять всю высоту экрана, когда записей мало, а если список большой и занимает всю высоту, хедер и поиск не скроллятся.
Отдельный навык — откат. Если попросить вернуть как было, модель может вернуть половину: откатить то, на что горит, и оставить тихие, непрошенные изменения. С объёмом задачи модель ошибалась и в других случаях. На просьбу подумать над блоком приходила точечная заплатка, а на просьбу поменять только текст она заодно подправляла соседей. Из этих промахов выросло правило двух режимов, закрытой рамки и открытого глагола. Правилом я пользуюсь и в других проектах, так что оставлю его под спойлером, вдруг кому пригодится.
Правило двух режимов для задач по интерфейсу
## Задачи по интерфейсу: два режима У каждого пункта задачи свой режим. В одном сообщении пункты могут быть в разных режимах, каждый выполняется по-своему. Закрытая рамка: «только текст», «остальное не трогай», «верни как было». - Делай ровно названное. Соседние элементы не трогай, улучшений по пути нет. - «Верни как было» = верни прежнее состояние целиком: вид, размеры, цвета, отступы, поведение. Не только то, что бросается в глаза. Открытый глагол: «подумай», «стилизуй», «расширь», «чтобы смотрелось». - Это задача на дизайн. Прочитай блок целиком: разметку, стили, поведение. - Реши задачу для всего блока и перепиши его целиком. - Проверь результат своим снимком экрана до отчёта. Всегда: - Открытое не толкуй в меньшую сторону, закрытое не толкуй в большую. - Заплатка на заплатку запрещена. Правка не держится — перепиши блок целиком. - У открытого пункта несколько прочтений с разным результатом — спроси одной фразой до работы, не выбирай молча меньшее. - В отчёте по каждому пункту только то, что сделано.
Бывает и наоборот, когда я вижу только симптом. Настройка вида субтитров просто не работала, и почему, я сам бы не понял. Модель нашла причину: Chrome на macOS навязывает субтитрам системный стиль. Она предложила два пути, рисовать субтитры своим слоем поверх видео или убрать настройку, и я выбрал свой слой. Как это устроено внутри, я не вникал, но настройка заработала.

Кое‑что ловится только на экране. Многоточие в кнопке «Выбрать другую…» читалось как обрезанный текст, и кнопка стала «Выбрать другую папку», а значок Shift в подсказках клавиш не читался вовсе и стал словом.
Спешл. Жмяк
У меня слабость к фановым штучкам внутри функциональности. Они ничему не мешают, а продукт получает характер.
Особенно мне нравится жмяк кнопки настроек, хотя с первого раза он не вышел. Я написал «поставить, чтобы жмякала как логотип», и модель выдала обычный скейл при нажатии, безлико и скучно. Пришлось описывать, что я вкладываю в слово «жмякать»: на кнопке лапа из логотипа, и при нажатии она должна проиграть ту же анимацию, с которой жмякает логотип. Теперь нажал на лапку, лапка жмякнула, и движение логотипа стало ответом на клик. Если в системе включён reduced motion, лапка не жмякает ни в логотипе, ни на кнопке. Фан фаном, а системную настройку он не отменяет.

Сам логотип тоже вышел из эксперимента. Рисование символами для меня тема новая, а для модели давно знакомая, поэтому я мог только направлять модель: после каждой правки открываю страницу, вижу, что не так, пробуем исправить. Переделок было много, и в итоге лапу и надпись Lapka я выбирал по отдельности, каждую из своих вариантов. Я рад, что эксперимент дал проекту символ и мне не пришлось выдумывать его самому.
Ещё мне нравится сам стиль. Он получился искажённым дефолтом: на первый взгляд похож на то, что сейчас по умолчанию выдают интерфейсы, собранные с ИИ, только здесь этот дефолт доведён до системы и получил свой характер. При этом Lapka подражает уже ушедшему подходу к интерфейсам. Настройки открываются одним листом, где видно всё сразу, как кнопки в кабине пилота, а сегодня такое скорее разложили бы по вкладкам. Те, кто пробовал пользоваться Lapka, в настройках разобрались. Их правда немного, выводов из этого не сделаешь, но подход показал жизнеспособность.

Тот же характер держат и мелкие элементы интерфейса. Фавикон во вкладке браузера живёт вместе с плеером: пока ничего не открыто, там лапка, во время просмотра треугольник, на паузе пауза, а пока Lapka думает, песочные часы. Части франшизы в очереди подписаны блоками, похожими на закладки.

Десять языков интерфейса тоже из разряда хотелок: раз мультиязычность заложена, останавливаться на двух языках не хотелось. Переводы делала модель. Русский я вычитал сам, английский читается нормально, а за немецкий, французский, испанский, итальянский, португальский, польский, турецкий и китайский поручиться не могу, носитель наверняка найдёт, где перевод кривой. Текстов в интерфейсе немного, так что если кто‑то знает один из этих языков и захочет поправить его в репозитории, буду рад.
Серия 6. В гостях
Выносить Lapka на свой домен я не хотел. Общий сервис гнал бы видео всех зрителей через один адрес и из личного инструмента превратился бы в посредника между сайтами и зрителями, с аккаунтами и платной моделью, чтобы окупать сервер. Когда у каждого своя Lapka, посредника нет. Через неё идёт видео одного зрителя, и страницы она открывает с его же компьютера, как браузер.
Вести себя на компьютере как вежливый гость Lapka умела с первого дня. Правилом это стало, когда я представил за компьютером не себя. Тогда всё, что Lapka делает на машине, должно быть предсказуемым. Переделывать ничего не пришлось, хватило обрамить. В сеть она ходит только туда, куда ведёт ссылка, и просит ровно то, что страница и так показывает, а в закрытые двери не ломится: постучалась, закрыто, ушла. Наружу ничего не проксирует, аккаунтов и облака у неё нет, а за новой версией обращается, только когда в настройках нажимают «Проверить обновление». История ссылок живёт целиком внутри Lapka, вместе с обложками, и открывается без запросов к сайтам. Иначе каждое открытие гоняло бы Lapka по всем ссылкам сразу. История не проверяет, жива ли ссылка. Собирать для неё ничего не пришлось, обложки Lapka сохраняет при просмотре, а остальное уже лежало рядом.
Защиту самого сервера модель предложила сама в первый же день и объяснила мне в чате, что к чему. Спорить было не с чем. Он слушает только 127.0.0.1 и сверяет Host, чтобы чужое доменное имя, переназначенное на локальный адрес, ничего не получило. Он проверяет Sec‑Fetch‑Site и Origin, а всё, что меняет состояние, принимает только с собственным заголовком Lapka. Чужая страница в браузере может разве что открыть Lapka, а прочитать её ответ или отдать ей команду не сможет.
На компьютере у Lapka есть сама программа, служебные файлы в системной папке настроек и папка, куда она складывает всё собранное. Место этой папки можно сменить в настройках. В первой версии Lapka с подачи модели переименовывала выбранную папку в Lapka, и я назвал это вандальным поведением: человек показал, где можно поселиться, и свою папку отдавать не собирался. Теперь Lapka создаёт внутри выбранной папку с именем Lapka, если та уже так не называется, и ничего не переименовывает. Всё, что она туда сложила, можно удалить по частям из настроек.
Закрытая вкладка сервер не останавливает: Lapka работает, пока его не выключат. Кнопка выключения и обновление появились после первого тестера. Обновиться он попробовал так же, как ставил, клонировать репозиторий заново, и git отказал, потому что папка уже есть. Так появилось обновление по кнопке (только чтобы её получить, тестеру пришлось обновиться вручную), а следом npx, через него Lapka сама запускается в последней версии. Кнопка обновляет копию, скачанную из репозитория, не задевая настроек.

Филлер. Память, которой нет
Это филлер, к основному сюжету он отношения не имеет. Хотя в аниме филлеры иногда оказываются лучшими сериями.
Разработчик обычно открывает под задачу отдельную сессию. Цели известны заранее, и когда они достигнуты, сессию можно закрыть. У меня так не выходит, потому что конечная точка заранее не видна. Когда я делал историю ссылок, в ней нужно было показать, где я остановился, и выяснилось, что это Lapka запоминает ненадёжно: при смене озвучки место остановки могло потеряться, а последние секунды просмотра пропадали, когда закрывалась вкладка. Сначала пришлось это исправить, и только потом вернуться к самой истории. Поэтому я работаю в одной сессии, пока идёт поток. Такая сессия живёт долго и рано или поздно упирается в сжатие контекста, когда вся переписка сворачивается в пересказ и детали договорённостей в нём теряются.
Всё, что должно пережить сжатие, лежит в заметках. Есть файл с правилами работы, который модель читает в начале каждой сессии: роли, принципы, границы, что нельзя делать без апрува. Рядом лежит короткая память самой модели о том, как со мной работать. После сжатия модель обязана перечитать и то и другое целиком, а не опираться на свой пересказ.
Горький опыт научил меня ещё одному. Точку сжатия я стараюсь выбирать вместе с моделью. Предупреждаю заранее и прошу записать себе всё, что нельзя забыть. А после сжатия сначала прошу её вспомнить проект, наш подход и то, как мы работаем в чате, и только потом даю задачу.
Сами правила появлялись после ошибок. Модель шатало из стороны в сторону: то она убегала на несколько шагов вперёд, вплоть до того, что сама меняла номер версии, то спрашивала разрешения на каждое движение, и теперь шаг — это одна цельная работа, после которой модель показывает результат и прямо задаёт свои вопросы. Однажды я около часа перезапускал Lapka и не видел изменений. Модель правила её на своём рабочем порту, а я смотрел на другой. В другой раз оказалось, что после своих проверок модель оставляла запущенными браузеры без окна, их никто не подчищал, и они тормозили компьютер. С тех пор после изменений модель закрывает за собой всё временное и перезапускает Lapka на основном порту, где смотрю я.
Каждое такое правило записывается в заметки, так что модель знает о прошлых ошибках и после сжатия, и в новой сессии. Записывает их обычно сама модель, но решение, что в них окажется, за мной. Модель может перекосить: раз за разом она одинаково искажает задачу, как было с заплатками, которые она снова и снова ставила там, где я просил подумать над блоком целиком. Тогда и правило она сформулирует с обратным перекосом, а после сжатия будет ему следовать.
Как вывести модель из перекоса
Остановите работу над задачей и попросите модель разобрать в чате свои ошибки: что она сделала не так и какое правило из заметок при этом нарушила.
Проверьте, что разбор опирается на файлы. Модель должна открыть заметки и правила и сверяться с ними, а не пересказывать их по памяти и не отделываться одной цитатой. Иногда на это уходит несколько заходов подряд.
Не ждите от разбора точности: он тоже может выйти перекошенным. Важно, чтобы модель снова начала обращаться к файлам.
Пока идёт разбор, не давайте модели править заметки.
Сожмите контекст.
После сжатия первым делом попросите модель перечитать заметки и правила целиком, с диска, и только потом возвращайтесь к задаче.
В пересказе после сжатия остаётся разбор, а сами перекошенные ответы уходят. Перекос слабеет, а договорённости и понимание задачи сохраняются, тогда как новая сессия начала бы с нуля.
Как я работаю с моделью на других проектах, писал раньше: про то, как остаться автором, когда руки — ИИ, и про то, как дизайнеру понять, что его вайб‑код не мусор.
Эндинг. Что вышло
Эксперимент перерос в небольшой продукт. Lapka стала для меня обычным способом смотреть аниме, и знакомые, которым я её показал, смотрят через неё тоже. Законченной я её не считаю: пока ею пользуются, в ней найдётся что поправить и что добавить.
Выложил её в npm, это мой первый пакет там. Понадобится Node.js 22 или новее:
npx -y lapka
Если терминал непривычен, в репозитории github.com/MythHand/Lapka есть инструкция по шагам для macOS, Windows и Linux, а на macOS и Windows скачанную оттуда Lapka можно запускать двойным кликом по лончеру.

Вместо превью
Что будет с Lapka дальше, загадывать не буду: ни очередь из целой франшизы, ни историю ссылок я заранее не планировал. Продолжу развивать её в своём темпе.

