Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lecture-pipeline

Автоматичний конвеєр для записів онлайн-пар: записав лекцію в OBS — і більше нічого не робиш.

Відео їде на власний VPS, транскрибується локально через faster-whisper, локальна LLM сама визначає дисципліну й тему (жодного ручного іменування файлів), запис лягає на YouTube як unlisted у плейліст свого предмета, а сирий транскрипт повертається у сховище нотаток.

Жодних платних API: класифікацією займається Ollama на тому ж сервері, на голому CPU.

OBS  →  watch-тека на ПК
  │
  │  sync-lectures.ps1 -Mode Push        (планувальник: 2 рази на день + при вході)
  │  scp -p через VPN, звірка SHA256, тоді видалення локальної копії
  ▼
VPS  ~/lectures/incoming/
  │
  │  run.sh → process.py
  ├─ ffmpeg          → wav 16 кГц моно
  ├─ faster-whisper  → транскрипт з таймкодами   (окремий процес, див. нижче)
  ├─ Ollama + LLM    → дисципліна із закритого списку + тема
  ├─ YouTube API     → unlisted, звірка через videos.list
  ├─ плейліст        → відео в плейліст своєї дисципліни
  └─ відео           → в архів на 7 днів, а не одразу в кошик
  ▼
VPS  ~/lectures/outgoing/<предмет>/<назва>.md
  │
  │  sync-lectures.ps1 -Mode Pull, звірка SHA256, тоді видалення з VPS
  ▼
Сховище нотаток:  raw/lectures/<предмет>/<назва>.md

Назва формується як <Дисципліна> — <Тема> (ДД.ММ.РРРР) і використовується одночасно як заголовок на YouTube, ім'я файлу транскрипту й назву підпапки.


Що для цього потрібно

VPS Linux, 4 ядра, 6 ГБ RAM, ~50 ГБ вільно. GPU не потрібен
ПК Windows з OBS, PowerShell 5.1+, OpenSSH-клієнт
Мережа бажано VPN-тунель між ПК і VPS (WireGuard тощо)
Google акаунт із верифікованим YouTube-каналом
root на VPS не потрібен — усе ставиться в домашню теку користувача

Про пам'ять варто сказати чесно: 6 ГБ — це не «з запасом», а «рівно впритул». whisper large-v3 в int8 займає ~2.9 ГБ, 7B-модель у Ollama — ще ~4.7 ГБ. Разом вони не влазять, і саме тому транскрипція винесена в окремий процес (див. «Чому саме так»).


Встановлення

1. VPS

# робочі теки
mkdir -p ~/lectures/{incoming,outgoing,archive,work,_needs-review,logs,bin,secrets}
chmod 700 ~/lectures/secrets

# код
git clone https://github.com/<user>/lecture-pipeline.git /tmp/lp
cp /tmp/lp/vps/bin/* ~/lectures/bin/
chmod +x ~/lectures/bin/*.py ~/lectures/bin/*.sh

# оточення
python3 -m venv ~/lectures/venv
~/lectures/venv/bin/pip install -r /tmp/lp/requirements.txt

2. Ollama без root

Офіційний інсталятор просить sudo. Якщо його немає — розпакуй тарбол у домашню теку:

TAG=$(curl -fsSL https://api.github.com/repos/ollama/ollama/releases/latest \
      | grep -m1 '"tag_name"' | cut -d'"' -f4)
curl -fsSL -o /tmp/ollama.tar.zst \
  "https://github.com/ollama/ollama/releases/download/$TAG/ollama-linux-amd64.tar.zst"
# звір контрольну суму зі sha256sum.txt того ж релізу — не пропускай цей крок
tar --zstd -xf /tmp/ollama.tar.zst -C ~/.local

~/lectures/bin/ollama-serve.sh &          # слухає лише 127.0.0.1
~/.local/bin/ollama pull qwen2.5:7b-instruct-q4_K_M

Автозапуск — через @reboot у crontab (див. vps/crontab.example). systemd --user тут не годиться: без loginctl enable-linger, а це root, користувацькі юніти гинуть при виході з сесії.

3. Google OAuth

  1. Google Cloud Console → новий проєкт → увімкнути YouTube Data API v3.
  2. Google Auth Platform → налаштувати consent screen (тип External).
  3. Clients → Create client → тип Desktop app → зберегти client_secret.json. Секрет показується один раз і завантажити його потім уже не можна.
  4. Скласти secrets/client_secret.json у форматі:
    {"installed": {"client_id": "...", "client_secret": "...",
                   "auth_uri": "https://accounts.google.com/o/oauth2/auth",
                   "token_uri": "https://oauth2.googleapis.com/token",
                   "redirect_uris": ["http://localhost"]}}
  5. На машині з браузером: python windows/auth_bootstrap.py → пройти згоду → отриманий secrets/token.json перекласти на VPS із правами 600.

Обов'язково переведи застосунок у Production (Audience → Publish app). Поки він у статусі Testing, Google відкликає refresh token кожні 7 днів, і вся автоматика перетворюється на щотижневий ручний ритуал. Щоб кнопка розблокувалась, треба заповнити home page / privacy policy / terms і додати домен у Authorized domains — підійде будь-який безкоштовний динамічний піддомен. Сторінки при цьому Google не завантажує, перевіряє лише формат URL і наявність домену в списку.

4. Windows

git clone https://github.com/<user>/lecture-pipeline.git
cd lecture-pipeline\windows
Copy-Item config.example.ps1 config.ps1    # і підстав свої значення
.\register-task.ps1

Далі — увімкнути wake timers з-під адміністратора, інакше ПК не прокинеться:

powercfg /SETACVALUEINDEX SCHEME_CURRENT SUB_SLEEP RTCWAKE 1
powercfg /SETDCVALUEINDEX SCHEME_CURRENT SUB_SLEEP RTCWAKE 1
powercfg /SETACTIVE SCHEME_CURRENT
powercfg /waketimers      # має показати задачу Lectures Sync

5. Свої дисципліни

Відредагуй словник SUBJECTS у vps/bin/classify.py. Це і є той закритий список, з якого модель обирає. Тримай у ньому лише ті предмети, записи яких справді потраплятимуть у конвеєр — кожен зайвий пункт це ще один шанс на промах.


Чому саме так

Класифікація закритим списком, а не вільною генерацією

Модель не вигадує назву дисципліни, а вибирає код зі списку або повертає НЕВІДОМО. Прив'язка «день тижня + номер пари → предмет» свідомо не використовується: вона ламається на першому ж перенесенні, святі чи зміні розкладу.

Порядок полів у JSON-схемі виявився вирішальним

Граматика структурованого виводу змушує модель генерувати поля саме в тому порядку, в якому вони описані у схемі. Якщо код дисципліни стоїть першим, мала модель мусить вгадати його нульовим токеном — ще до того, як «побачила» зміст.

Заміри на однакових фікстурах, qwen2.5-3b:

Схема Влучань
subject першим 3 / 7
keywords → topic → kind → subject 5 / 7
те саме + коди рядками замість чисел 6 / 7
те саме на 7B 7 / 7

Тобто «спершу подумай вголос, потім відповідай» працює і на рівні JSON-схеми. Заміна числових ID на семантичні коди (МАТАНАЛІЗ замість 4) додала ще: модель правильно виписувала ключові слова «числові ряди, ознака збіжності», але не могла згадати, що математичний аналіз — це «4».

Ще одна дрібниця з великими наслідками: обмежуй довжину рядкових полів у схемі (maxLength) і став num_predict. Без цього мала модель одного разу згенерувала 10 600 токенів замість 40 і намертво зациклилась.

Транскрипція окремим процесом

whisper large-v3 в int8 тримає ~2.9 ГБ, Ollama слідом просить ~4.7 ГБ, а на сервері всього 6 ГБ. del model + gc.collect() не повертають пам'ять ОС — вона лишається в malloc-аренах. Гарантію дає лише вихід процесу, тому transcribe_worker.py запускається окремо і одразу вмирає.

Нічого не видаляється без підтвердження наступного кроку

  • Локальне відео — лише після збігу SHA256 з обох боків.
  • Файл заливається як <name>.uploading і перейменовується вже на сервері, тож процесор ніколи не побачить недокачаний файл.
  • Відео на сервері — не одразу після заливки, а через тиждень, і лише якщо YouTube підтвердив uploadStatus=processed.

Останнє — не перестраховка, а наслідок реального інциденту. YouTube приймає файл асинхронно: uploaded означає тільки «прийнято». Відхилення (задовге відео, Content ID, збій обробки) приходить згодом — коли видаляти вже пізно. Так була втрачена 72-хвилинна лекція. Тепер відео лежить в archive/, і перед видаленням стан перепитується в YouTube; якщо той не підтвердив — файл лишається, а в лог падає ERROR.

Стан за sha256, а не за іменем файлу

Той самий запис, перезалитий під іншим іменем, розпізнається як дублікат і не обробляється вдруге. Паралельні запуски виключені через flock.


Заміряна продуктивність

4 ядра CPU, без GPU, на реальній 72-хвилинній лекції:

Крок Час
whisper large-v3, int8 51 хв (0.71× реального часу)
класифікація 7B ~2.5 хв разом із завантаженням моделі
заливка 50 МБ секунди
разом на півторагодинну пару приблизно година

Короткі кліпи дають оманливо кращі цифри (на 54-секундному виходило 0.3×) — орієнтуйся на замір з повної пари.

Квота YouTube API: 10 000 одиниць на добу, заливка ≈ 1600 → не більше ~6 відео на день. Додавання в плейліст коштує 50, перевірка стану — 1.


Граблі, на які варто не наступати

longUploadsStatus: eligible — це НЕ дозвіл. Означає «канал може увімкнути довгі завантаження, але ще не увімкнув». Дозвіл — рівно allowed. Без нього YouTube мовчки відхилить усе довше за 15 хвилин, причому вже після того, як прийме файл. Конвеєр перевіряє цей прапорець перед заливкою і відмовляється працювати, якщо там не allowed.

unlisted — це не «приватно». Хто має пряме посилання, той дивиться без авторизації. Якщо ділитися ні з ким не плануєш — став private у yt_upload.py.

Дата береться з mtime файлу. Для записів OBS це і є час запису. Але у файлу, завантаженого звідкись, mtime — дата скачування. Тому scp викликається з -p: без нього mtime став би часом копіювання, і запис, залитий за logon-тригером наступного ранку, отримав би дату наступного дня.

Плейлісти коштують ширшого дозволу. playlistItems.insert вимагає скоупа youtube (повне керування акаунтом), який дозволяє й видаляти відео. Вужчого скоупа саме під плейлісти в API немає. Якщо це неприйнятно — прибери крок із плейлістом і лишайся на youtube.upload + youtube.readonly.

Не пиши в каталог сховища, синхронізований E2E-плагіном. Якщо нотатки їдуть на той самий сервер через Remotely Save чи подібне, там усе зашифроване на боці клієнта — запис повз клієнт зіпсує сховище. Конвеєр використовує окремий канал і того каталогу не торкається.


Тести

# класифікатор на фікстурах: 5 дисциплін + 2 негативні кейси
LECTURE_KEEP_ALIVE=5m venv/bin/python tests/test_classifier.py tests/fixtures

Негативні кейси (кулінарія, урок англійської) — не формальність. Саме вони ловлять найнеприємнішу поведінку: коли модель за будь-яку ціну намагається підігнати зміст під щось зі списку. Обов'язково перевіряй і їх, а не лише happy path.

Якщо класифікатор не впевнений, файл лягає в _needs-review/ разом із транскриптом, відео не заливається і не видаляється, і чекає ручного розбору.


Правові й етичні застереження

Це інструмент для особистого архіву власних записів. Перш ніж ним користуватись:

  • Запис чужої лекції — це чужий об'єкт авторського права. У багатьох закладів є прямі правила щодо запису занять; спитай викладача.
  • У записі можуть звучати голоси та імена інших студентів, які на це не погоджувались.
  • unlisted обмежує поширення, але не робить відео приватним.
  • Ніколи не став таким записам public.

Автори не несуть відповідальності за те, як ви скористаєтесь цим кодом.


Ліцензія

MIT — див. LICENSE.

About

Автоматичний конвеєр записів лекцій: OBS -> VPS -> faster-whisper -> локальна LLM-класифікація -> YouTube (unlisted) -> транскрипт у сховище нотаток. Без платних API.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages