# 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` (текст сегмента). Затем должен идти ключ `custom`, где указываются значения, специфичные для движка (они будут использованы только на следующем шаге, а затем будут удалены). ```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` и происходит переход на следующий шаг. -