Конспект YouTube по клику. Расширение Chrome запускает сборку структурированного конспекта видео на вашем сервере: сервер скачивает субтитры, чистит транскрипт и отдаёт конспект потоком. Расширение — тонкий клиент: кнопка под плеером, живой превью по мере генерации, экспорт в Markdown.
Self-host: аккаунтов, оплаты и базы данных нет. Сервер не хранит ничего. Готовый конспект кэшируется в браузере и сохраняется как .md.
backend/— сервер Node на Hono (TypeScript). Скачивание субтитров и саммари через LLM целиком на сервере.extension/— расширение Chrome MV3. Адрес сервера и пароль доступа читает изconfig.json, в интерфейсе настроек сервера нет.
Конспект собирается в четыре шага: yt-dlp скачивает субтитры, cleanTranscript.ts чистит транскрипт, LLM пишет конспект, сервер стримит текст в расширение по SSE.
- Node.js 20+.
- yt-dlp в PATH (либо рядом с сервером, либо путь через
YTDLP_BIN). - Python для запуска не нужен. Он нужен только чтобы прогонять тесты чистки транскрипта — сверка
cleanTranscript.tsс эталономclean_transcript.py. Без Python тест сверяется с зафиксированным текстом.
Три сценария. Выбирайте по тому, где будет считаться конспект и с каких устройств нужен доступ.
| Сценарий | Сервер | Доступ | Кому подходит |
|---|---|---|---|
| Локально | ваш компьютер | localhost |
попробовать; один компьютер |
| Свой VPS + SSH-туннель | сервер | localhost через ssh |
нужен работающий сервер, но открывать его в интернет не хочется |
| Публичный адрес | сервер | https://ваш-домен |
несколько устройств; нужен домен и HTTPS |
Docker:
cd backend
cp .env.example .env
# заполните LLM_API_KEY и SHARED_TOKEN
docker compose up -d --build
curl http://127.0.0.1:3000/health # {"ok":true,"version":"1"}Без Docker:
cd backend
cp .env.example .env
# заполните LLM_API_KEY и SHARED_TOKEN
npm install
npm run build
npm startyt-dlp должен быть в PATH: pip install yt-dlp либо бинарник из релизов yt-dlp. Путь можно задать явно через YTDLP_BIN.
В config.json расширения оставьте baseUrl равным http://localhost:3000.
Порт бэкенда остаётся привязанным к 127.0.0.1 сервера, то есть снаружи закрыт. Ваш компьютер достаёт до него по ssh. Ни портов открывать, ни сертификаты выпускать не нужно: шифрование даёт сам ssh.
На сервере:
git clone https://github.com/vonavikon/conspect.git
cd conspect/backend
cp .env.example .env
# заполните LLM_API_KEY и SHARED_TOKEN
docker compose up -d --build
curl http://127.0.0.1:3000/healthНа своём компьютере:
ssh -N -L 3000:localhost:3000 user@your-serverПока эта команда работает, http://localhost:3000 на вашем компьютере — это бэкенд на сервере. baseUrl в config.json расширения остаётся http://localhost:3000.
Чтобы туннель поднимался сам при входе в систему и переживал обрывы связи, возьмите готовые скрипты: scripts/tunnel/ (Windows, Linux, macOS).
Если порт 3000 на сервере занят чем-то другим, поменяйте проброс в docker-compose.yml ("127.0.0.1:3010:3000") и в команде ssh — внутри контейнера порт остаётся прежним.
Нужен, если пользуетесь с нескольких устройств и туннель на каждом заводить не хочется. Готовьтесь к тому, что сервис увидят сканеры: адрес станет публичным, а защита — один токен.
- В
docker-compose.ymlзамените127.0.0.1:3000:3000на3000:3000. - Поставьте reverse-proxy с HTTPS (Caddy, nginx) на свой домен и проксируйте на
127.0.0.1:3000. Без HTTPS токен и транскрипты идут открытым текстом: их прочитает любой на маршруте, от Wi-Fi в кафе до провайдера. - Пропишите домен в
host_permissionsвmanifest.jsonи пересоберите расширение. Как вариант, вместо этого укажите вCORS_ORIGINorigin расширенияchrome-extension://<id>(id виден наchrome://extensions). - Откройте в файрволе только порт reverse-proxy (443), не порт бэкенда.
Сертификат Let's Encrypt требует домена: на голый IP публичные CA сертификаты не выдают, а ACME-проверка хочет либо домен (DNS-01), либо свободные порты 80/443 (HTTP-01, TLS-ALPN).
Перед публикацией прочитайте «Известные ограничения» ниже: на публичном адресе вся защита сводится к одному токену, а лимиты ограничивают ущерб от его утечки, но не исключают его.
Подключение к серверу задаётся файлом extension/config.json. Расширение читает его при первом запуске; настроек сервера в интерфейсе нет.
cd extension
npm install
# сгенерирует токен, выведет его и запишет config.json
node scripts/configure.mjs --base-url https://conspect.example.com --gen-token
npm run buildТокен, который напечатал скрипт, впишите в backend/.env строкой SHARED_TOKEN=<токен> (если ещё не делали). Он должен совпадать в обоих местах.
Либо создайте config.json рядом с build.mjs вручную:
{ "baseUrl": "https://conspect.example.com", "sharedToken": "тот же токен, что в backend/.env" }Откройте chrome://extensions, включите режим разработчика, «Загрузить распакованное», выберите extension/dist.
GET /health— проверка доступности.POST /digest/stream— SSE-стрим конспекта. ЗаголовокAuthorization: Bearer <SHARED_TOKEN>. Тело{"url": "..."}.
События SSE: meta (название, канал, длительность), delta (кусок конспекта), done (готово, число токенов), error (причина), ping (heartbeat каждые 10 секунд).
Подойдёт любой OpenAI-совместимый эндпоинт. Сервер не держит списка моделей, имя модели указывается в .env.
| Провайдер | LLM_BASE_URL |
Пример LLM_MODEL |
|---|---|---|
| bothub.chat | https://openai.bothub.chat/v1 |
deepseek-v4-flash |
| OpenAI | https://api.openai.com/v1 |
gpt-4o |
| Google Gemini | https://generativelanguage.googleapis.com/v1beta/openai |
gemini-2.5-flash |
| Groq | https://api.groq.com/openai/v1 |
llama-3.3-70b-versatile |
| OpenRouter | https://openrouter.ai/api/v1 |
openai/gpt-4o |
Все переменные читаются из .env (образец — .env.example).
| Переменная | Назначение | По умолчанию |
|---|---|---|
LLM_API_KEY |
ключ OpenAI-совместимого API | обязательна |
LLM_BASE_URL |
базовый URL LLM API | https://openai.bothub.chat/v1 |
LLM_MODEL |
имя модели | deepseek-v4-flash |
MAX_TOKENS |
лимит токенов ответа LLM | 8192 |
LLM_TIMEOUT_SEC |
таймаут запроса к LLM | 300 |
MAX_DURATION_MIN |
максимум длины видео в минутах | 180 |
YTDLP_PROBE_TIMEOUT_SEC |
таймаут проверки видео | 90 |
YTDLP_DOWNLOAD_TIMEOUT_SEC |
таймаут скачивания субтитров | 180 |
PORT |
порт сервера | 3000 |
HOST |
интерфейс, на котором слушает сервер (в Docker перекрывается на 0.0.0.0 из docker-compose.yml) |
127.0.0.1 |
CORS_ORIGIN |
разрешённые origin через запятую, пусто — запрет | пусто |
SHARED_TOKEN |
общий секрет между сервером и расширением | обязателен, от 24 символов |
MAX_CONCURRENT_DIGESTS |
максимум одновременных генераций | 2 |
RATE_LIMIT_PER_MIN |
запусков в минуту | 10 |
YTDLP_AUTO_UPDATE |
автообновление yt-dlp при старте (1 — включено) |
0 |
SHARED_TOKEN — единственная защита: сервер не различает пользователей. Любой, кто узнал адрес публичного сервера без токена, потратит ваш LLM-ключ. Задайте длинную случайную строку (от 24 символов).
Сравнение токена — константное по времени (crypto.timingSafeEqual).
Rate limit ограничивает ущерб от утечки токена, но не отменяет его: сервер держит максимум одновременных генераций (MAX_CONCURRENT_DIGESTS) и запусков в минуту (RATE_LIMIT_PER_MIN). Лимиты общие, потому что сервер знает один токен, а не пользователей. Токен держите в секрете.
Возьмите для сервера отдельный ключ LLM-провайдера и поставьте на него лимит расходов, если провайдер это умеет. Это единственная мера, которая ограничивает убыток, когда токен уже утёк.
- Rate limit общий, не по адресам. Счётчики глобальные, но считают только запросы с верным токеном: без него запрос отбивается на 401 раньше проверки лимита и квоту не тратит, SSE-соединение тоже не открывается. Пока токен известен только вам, ограничение незаметно. Если токен утёк, чужие запуски съедают общую квоту и ваш LLM-бюджет — лечится сменой токена, а не настройкой лимитов.
- Сервер не терминирует TLS. HTTPS — задача reverse-proxy перед ним.
- Токен нельзя отозвать частично. Он один. После смены обновите
config.jsonна каждом устройстве (достаточно заменить файл вdistи перезагрузить расширение). - yt-dlp просит JS-рантайм. В логах видно
No supported JavaScript runtime could be found ... has been deprecated. Субтитры при этом скачиваются, проверено. Если YouTube начнёт отдавать меньше данных, поставьтеdenoрядом с yt-dlp (в системе или в образе). - Конспекты живут в браузере. Сервер не хранит ничего, поэтому при переустановке браузера или переходе на другое устройство история не переезжает. Нужное сохраняйте в
.md.
MIT.
