# 2026-linux-sumka Сервис для построения полноценного файла коспекта (главным образом Markdown) из записи в одном из видов: 1. **Только звук.** Создаст файл лекции, основываясь только на звуке. 2. **Видео со звуком.** Создаст файл лекции на основе звука, и дополнит информацию, используя кадры из видео. Для запросов к ИИ используется OpenAI-compatible API, поэтому использовать можно разные модели. Разработка велась в связке с [GigaChat](https://giga.chat) и с локальной `ollama` через посредство `OpenWebUI`. В репозитории хранятся промтпы для абстрактной языковой модели, и, вероятно, их лучше поменять в проде. *К тому же, промпты написаны на русском языке, а иностранные модели лучше работают со служебными промптами на английском языке.* ## Установка (Docker) TODO ## Установка (Python) Для разработки и тестирования используется `Python 3.13.1`. ```bash # Клонировать репозиторий и перейти в него git clone https://git.tyukalov.su/nikita/2026-linux-sumka cd 2026-linux-sumka # Создать venv (либо ваш способ, если шарите) python3 -m venv .venv # Активировать виртуальное окружение . .venv/bin/activate # Установить зависимости pip install -r requirements.txt # Запуск вот так (см. далее) python main.py ``` ## Запуск TODO ## Архитектура Предполагается, что приложение будет запускаться сторонним приложением всякий раз, когда требуется произвести конвертацию медиафайла в конспект (например, Telegram ботом, которому отправили видео). **Сервис не сохраняет никаких данных между перезапусками**. Всё, что сохраняется - это промежуточные результаты. Например, если сервис сгенерировал файл `asr_events.json`, а после этого его принудительно завершили, то при следующем запуске он будет использовать этот файл, чтобы не повторять дорогие операции. **Поэтому при автоматизации рекомендуется на каждый новый запрос создавать новую промежуточную директорию, а старые директории удалять, когда они становятся не нужны.** Алгоритм работы сервиса следующий: 1. **Проверить, является входной файл звуком или видео со звуком** - Звуковой файл - сразу начать работу - Видеофайл со звуком - разделить файл на звук и видео, используя `ffmpeg`, и перейти к обработке звука - Видеофайл без звука - не поддерживается - Ни одна из категорий - ошибка 2. **Выполнить распознание речи** - Используется `Whisper` - Базовые настройки распознания должны быть доступны для изменения через CLI, релевантные аргументы должны начинаться с `asr-` - В результате распознания речи в промежуточной директории должен появиться файл `asr_raw.json`. В корне обязательно должен быть ключ, где указано название движка распознания (поддерживается только `whisper`). Каждый сегмент обязан содержать ключи `start` (время начала сегмента), `end` (время конца сегмента), `text` (текст сегмента). Затем должен идти ключ `engine`, где указываются значения, специфичные для движка (они будут использованы только на следующем шаге, а затем будут удалены). ```json { "engine": "whisper", "segments": [ { "start": 0.0, "end": 3.0, "text": "текст речи с 0.0 по 3.0", "engine": { "temperature": 0.4, "something": -5.6 } }, { "start": 3.0, "end": 7.0, "text": "текст речи с 3.0 по 7.0", "engine": { "temperature": 0.1, "something": 542 } } ] } ``` 3. **Выполнить фильтрацию полученных сегментов** - Смысл этого шага - отсеять сегменты которые можно по имеющимся данным отнести к лишним. - Этот шаг формально существует, но на самом деле в текущей реализации на нем просто выбрасываются лишние данные из `asr_raw.json` и сегментам присваиваются идентификаторы. - В итоге должен получится файл `asr.json` в таком формате: ```json { "segments": [ { "id": 0, "start": 0.0, "end": 3.0, "text": "текст речи с 0.0 по 3.0" }, { "id": 1, "start": 3.0, "end": 7.0, "text": "текст речи с 3.0 по 7.0" } ] } ``` 4. **Построить список событий** - Дальнейшие шаги требуют представления звука в виде списка событий - Для построения списка событий используется ИИ - Расшифровка скармливается ИИ постепенно, запрос состоит из двух сообщений, которые модели предлагается продолжить. **Первое сообщение** - системный промпт, где для модели разъясняется, что от неё требуется, а также описывается формат данных. **Второе сообщение** - сам запрос к модели. Он имеет следующий формат: ```json { "past": [ { "id": 40, "start": 58.0, "end": 60.0, "text": "фаышв" } ], "present": [ { "id": 41, "start": 60.0, "end": 64.0, "text": "ащфзо" }, { "id": 42, "start": 64.0, "end": 70.0, "text": "зпщыфвол" }, ], "future": [ { "id": 43, "start": 70.0, "end": 73.0, "text": "полытаы" } ], "context": {} } ``` - `present` - это события, которые попадают в скользящее окно нефиксированного размера. Размер окна в текущем запросе определяется переменными `asr_mmax_size` и `asr_mmin_segments`. Окно должно содержать как можно больше сегментов, но сумма длин всех `text`, входящих в окно, не должна превышать `asr_mmax_size`; в то же время количество сегментов в окне не должно быть меньше, чем `asr_mmin_segments`, и это требование - самое приоритетное. - `past` и `future` - это события, которые идут до и после текущего окна. Их размеры определяются аналогично `present`, но используются переменные `asr_cmax_size` и `asr_cmin_segments`. - `context` - это контекст, формат которого модель определяет сама. Его значение сохраняется между запросами к модели. В системном промпте рекомендуется строго задать формат данных. - Задача модели после получения промтпа - объединить сегменты распознания речи в так называемые *события*, которые будут использованы в дальнейшнем. Модели на этом этапе разрешается удалять сегменты, логически объединять их содержимое, но строго запрещается изменять смысл, формулировки. Также модели разрешается исправлять пунктуацию или очевидные ошибки распознания. - Модель должна отвечать в следующем виде: ```json { "preevents": [ { "ids": [0, 1], "text": "объединенный текст для ID 0 и 1" }, { "ids": [2, 3, 4, 5], "text": "объединенный текст для ID 2, 3, 4 и 5" }, { "ids": [8], "text": "заметьте, модель выбросила сегменты 6 и 7, а этот сегмент сохранила без объединения с другими" } ], "context": { "for_future_call": "Сегменты 6, 7 выброшены, потому что..." } } ``` - `preevents` - список недособытий. Время намеренно не должно быть использовано, так как оно должно вычисляться программно на основе значения `ids` - `context` - данные, которые модель хочет передать следующей итерации; формат определяется моделью или промптом - **Важно, что модели следует явно запретить объединять сегменты, если объединение затрагивает `future`. В промпте следует явно указать, что в такой ситуации (когда нужно объединить что-то между `present` и `future`), модель должна сделать для себя пометку в контексте, и при следующей итерации объединить сегменты из `past` и `present`. Если не ограничить модель в этом, то начнется сущий кошмар.** - Нужно после каждой итерации проверять, что указанные нейросетью ID действительно существуют, и что они входят в `past` или `present`. В следующей итерации модели больше нельзя передавать данные, которые были объединены, либо которые хоть раз попадали в `past`. - По мере получения объектов `preevent` нужно составлять файл `audio_events.json`: ```json { "events": [ { "id": 0, "type": "voice", "timestamp": 0.0, "duration": 7.0, "text": "Здесь уже должны быть осмысленные законченные фразы", "payload": null }, { "id": 1, "type": "voice", "timestamp": 7.0, "duration": 3.0, "text": "Обратите внимание, нет связи между ID здесь и раньше", "payload": null } ] } ``` - `id` присваивается независимо от того, что наблюдалось ранее. Здесь `id` - это просто число, увеличивающееся с ростом времени. - `type` на этом шаге может быть только `voice`. В этот общий список могут добавляться события других типов на более поздних шагах. - `timestamp` - начало события - `duration` - длительность события - `text` - текст - `payload` - для `voice` может быть только `null` 5. **Построить список ссылок на видео** - Если видео нет, то шаг пропускается. Вместо него файл `audio_events.json` копируется в `events.json` и происходит переход на 7 шаг. - Используя оконный метод, описанный выше, модели отправляются сформированные события, а она формирует список ссылок на видео, которые нужно разрешить. Окна передаются по такому же алгоритму, как уже было описано выше для шага №4, но данные передаются вот такими объектами: ```json { "id": 4, "timestamp": 3.0, "duration": 5.0, "text": "Осмысленный текст, полученный на 4-ом шаге." } ``` То есть для каждого объекта, входящего в `past`, `present` или `future`, передаются только данные, описанные выше. Промпт должен налагать дополнительные ограничения. Во-первых, непосредственно обрабатывать можно только соыбтия из списка `present`, а события, перечисляемые в `past` и `future` используются только для контекста - работать с ними нельзя. Ключ `context` работать точно так же, как на 4-ом шаге - модель может использовать его по своему усмотрению, но формат рекомендуется жестко сформулировать в промпте. - При построении списка ссылок модель модель возвращать данные в следующем формате: ```json { "unresolved": [ { "ids": [0, 1, 2], "type": "vis", "text": "Для типа `vis`: описание того что должно быть на видео в этот момент" }, { "ids": [19, 20, 21, 22, 23], "type": "ocr", "text": "Для типа `ocr`: текст, который нужно распознать с экрана (здесь должно быть названо, например, какой термин)" }, { "ids": [0, 1, 2], "type": "vis", "text": "Для типа `vis`: описание того что должно быть на видео в этот момент" } ], "context": { "for_future_call": "Сегменты 6, 7 выброшены, потому что..." } } ``` - `unresolved` - список событий, которые нужно будет разрешить на следующем шаге. Каждый объект содержит в точности три ключа: `ids` (идентификаторы событий, на протяжении которых нужно искать то, что описано), `type` (тип того, что нужно найти, см. далее), `text` (описание того, что нужно найти). Тип может быть либо `vis` (нужно определить, соответствует ли кадр из видео описанию `text`, и, если соответствует, то вставить его в итоговый файл лекции), либо `ocr` (нужно распознать текст, наводка на который даётся в `text`). - `context` - контекст, который должен быть передан модели при следующем вызове. - После выполнения этого шага модель должна сформировать файл `unresolved.json` 6. **Разрешить ссылки на видео** - После этого шага должен быть получен файл `events.json`, используя который будет построена структура итогового документа. - TODO, пока что это не реализовано - Итоговый файл `events.json`, должен иметь следующую структуру. ID никак не связаны с предудущими шагами. ```json { "events": [ { "id": 0, "type": "voice", "timestamp": 0.0, "duration": 7.0, "text": "Здесь уже должны быть осмысленные законченные фразы", "payload": null }, { "id": 1, "type": "vis", "timestamp": 4.0, "duration": 0.0, "text": "Описание содержимого изображения, путь к которому хранится в `payload`", "payload": "runtime/23865928/images/4123.jpg" }, { "id": 2, "type": "ocr", "timestamp": 6.0, "duration": 0.0, "text": "Описание текста, который должен был быть распознан", "payload": "Текст, распознанный с кадра из видео, без перефразирований" }, { "id": 3, "type": "voice", "timestamp": 7.0, "duration": 3.0, "text": "Для типа `voice` ключ `payload` всегда `null`", "payload": null } ] } ``` 7. **Построить структуру итогового документа** - После этого шага должен быть получен файл `structure.json` следующего вида: ```json { "elements": [ { "type": "heading", "level": 2, "text": "Заголовок второго уровня" }, { "type": "paragraph", "text": "Текст параграфа." }, { "type": "heading", "level": 3, "text": "Заголовок третьего уровня" }, { "type": "unordered", "items": [ "Элемент неупорядоченного списка", "Ещё один элемент неупорядоченного списка" ] }, { "type": "ordered", "items": [ "1-ый элемент упорядоченного списка", "2-ой элемент упорядоченного списка" ] }, { "type": "definition", "term": "Кошка", "text": "Домашнее животное, которое мяукает и царапается" }, { "type": "important", "text": "Важное замечание, которое нужно выделить на уровне структуры документа" }, { "type": "image", "event_id": 54 } ] } ``` - Для получения файла `structure.json` модели передаются соыбытия из `events.json`, используя метод окна (описан ранее). 8. **Собрать финальный Markdown файл** - Для сборки Markdown файла используются элементы, полученные на предыдщем шаге, и хранимые в файле `structure.json` - В итоге создаётся файл `output.md`, который может включать в себя ссылки на изображения из директории `images/` (относительно директории промежуточных данных)