Initial commit
This commit is contained in:
257
README.md
Normal file
257
README.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# 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` и происходит переход на следующий шаг.
|
||||
-
|
||||
Reference in New Issue
Block a user