Initial commit
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
__pycache__/
|
||||
.venv/
|
||||
runtime/
|
||||
|
||||
*.json
|
||||
|
||||
*.mkv
|
||||
*.mp4
|
||||
*.avi
|
||||
|
||||
*.wav
|
||||
*.ogg
|
||||
*.mp3
|
||||
*.m4a
|
||||
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` и происходит переход на следующий шаг.
|
||||
-
|
||||
33
agent.py
Normal file
33
agent.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import httpx2
|
||||
from openai import OpenAI
|
||||
|
||||
from utils import AgentMessage
|
||||
|
||||
class Agent:
|
||||
"""Perform operations with timeline events using OpenAI-compatible API"""
|
||||
def __init__(self, *, model: str, base_url: str | None, api_key: str, **kwargs) -> None:
|
||||
self._client = OpenAI(
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
http_client=httpx2.Client(verify=False),
|
||||
**kwargs
|
||||
)
|
||||
self._model = model
|
||||
|
||||
|
||||
def completion(self, messages: list[AgentMessage], **kwargs) -> str:
|
||||
"""Generate a completion for specified messages."""
|
||||
messages_raw = []
|
||||
for m in messages:
|
||||
messages_raw.append(
|
||||
{
|
||||
"role": m.role,
|
||||
"content": m.content
|
||||
}
|
||||
)
|
||||
response = self._client.chat.completions.create(
|
||||
model=self._model,
|
||||
messages=messages_raw,
|
||||
**kwargs
|
||||
)
|
||||
return response.choices[0].message.content # type: ignore
|
||||
358
main.py
Normal file
358
main.py
Normal file
@@ -0,0 +1,358 @@
|
||||
"""Application entry point"""
|
||||
|
||||
import traceback
|
||||
import argparse
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
from dataclasses import asdict
|
||||
|
||||
import torch
|
||||
|
||||
from transcriber import Transcriber
|
||||
from agent import Agent
|
||||
from utils import TimelineEvent, AnalysisWindow, AgentMessage, TimelineProcessingResult
|
||||
|
||||
def check_cuda() -> None:
|
||||
"""Checks if CUDA is available."""
|
||||
if not torch.cuda.is_available():
|
||||
raise RuntimeError(
|
||||
"CUDA is unavaiable! Refusing to start, it's pointless."
|
||||
)
|
||||
|
||||
def setup_arguments() -> argparse.Namespace:
|
||||
"""Parses CLI arguments and returns namespace. It may terminate the
|
||||
application on invalid arguments.
|
||||
|
||||
Returns:
|
||||
- argparse namespace
|
||||
"""
|
||||
voice_models = Transcriber.get_models_list()
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="sumka",
|
||||
description="Summarizes large video/audio files into convenient format",
|
||||
)
|
||||
parser.add_argument("filename", type=str)
|
||||
parser.add_argument(
|
||||
"--voice-model",
|
||||
choices=voice_models,
|
||||
default="turbo" if "turbo" in voice_models else voice_models[-1]
|
||||
)
|
||||
parser.add_argument(
|
||||
"--voice-language",
|
||||
choices=["ru", "en"],
|
||||
default="ru"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-payload-size",
|
||||
type=int,
|
||||
default=1024
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-context-size",
|
||||
type=int,
|
||||
default=128
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ai-model",
|
||||
type=str,
|
||||
default="GigaChat-3-Pro"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ai-base-url",
|
||||
type=str,
|
||||
default="https://api.giga.chat/v1"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ai-api-key",
|
||||
type=str,
|
||||
default="gdsfgds"
|
||||
)
|
||||
parser.add_argument("-v", action='store_true')
|
||||
return parser.parse_args()
|
||||
|
||||
#
|
||||
# GENERIC
|
||||
#
|
||||
def make_windows(events: list[TimelineEvent], context_symbols: int, payload_symbols: int) -> list[AnalysisWindow]:
|
||||
result: list[AnalysisWindow] = []
|
||||
window_start = 0
|
||||
while window_start < len(events):
|
||||
window = AnalysisWindow([], [], [])
|
||||
result.append(window)
|
||||
# build the window itself
|
||||
window_end = window_start + 1
|
||||
total_size = 0
|
||||
for event in events[window_start:]:
|
||||
window.modifiable.append(event)
|
||||
total_size += len(event.payload)
|
||||
if total_size >= payload_symbols:
|
||||
break
|
||||
window_end += 1
|
||||
# build readonly events before the window
|
||||
total_size = 0
|
||||
for event in reversed(events[:window_start]):
|
||||
window.before_readonly.insert(0, event)
|
||||
total_size += len(event.payload)
|
||||
if total_size >= context_symbols:
|
||||
break
|
||||
# build readonly event after the window
|
||||
total_size = 0
|
||||
for event in events[window_end:]:
|
||||
window.after_readonly.append(event)
|
||||
total_size += len(event.payload)
|
||||
if total_size >= context_symbols:
|
||||
break
|
||||
# prepare for the next window
|
||||
window_start = window_end
|
||||
return [r for r in result if len(r.modifiable)]
|
||||
|
||||
def execute_timeline_request(res: TimelineProcessingResult, request: dict):
|
||||
req = request["req"]
|
||||
if req == "modify":
|
||||
id = request["id"]
|
||||
payload = request["payload"]
|
||||
valid = [e for e in res.events if e.id == id]
|
||||
if not len(valid):
|
||||
raise RuntimeError(f"AI tries to modify nonexistent event with ID `{id}`")
|
||||
valid[0].payload = payload
|
||||
logging.info(f"Updated `{id}`'s payload to `{payload}`")
|
||||
else:
|
||||
print(json.dumps(request, indent=2, ensure_ascii=False))
|
||||
|
||||
def build_document_structure(timeline: TimelineProcessingResult, args: argparse.Namespace):
|
||||
results_path = "document_structure.json"
|
||||
result = []
|
||||
# create the agent
|
||||
agent = Agent(
|
||||
model=args.ai_model,
|
||||
base_url=args.ai_base_url,
|
||||
api_key=args.ai_api_key
|
||||
)
|
||||
# create the system prompt
|
||||
with open("prompts/build_structure.md", "r") as f:
|
||||
system_prompt = AgentMessage(
|
||||
f.read(),
|
||||
"system"
|
||||
)
|
||||
windows = make_windows(timeline.events, args.window_context_size * 2, args.window_payload_size * 2)
|
||||
# context
|
||||
context = {}
|
||||
# process each window
|
||||
for window_id, window in enumerate(windows):
|
||||
# prepare request body
|
||||
req = {
|
||||
"before_readonly": [e.get_ai_dict() for e in window.before_readonly],
|
||||
"content": [e.get_ai_dict() for e in window.modifiable],
|
||||
"after_readonly": [e.get_ai_dict() for e in window.after_readonly],
|
||||
"document_context": context
|
||||
}
|
||||
msg = AgentMessage(
|
||||
content=json.dumps(req, indent=2, ensure_ascii=False),
|
||||
role="user"
|
||||
)
|
||||
# process the window
|
||||
success = False
|
||||
attempt = 1
|
||||
while not success:
|
||||
logging.info(f"Processing a window #{window_id + 1} (attempt #{attempt})...")
|
||||
# try to parse as JSON
|
||||
try:
|
||||
# call the AI
|
||||
response = json.loads(
|
||||
agent.completion(messages=[system_prompt, msg]))
|
||||
# process each request separately
|
||||
for block in response["blocks"]:
|
||||
result.append(block)
|
||||
context = response["new_document_context"]
|
||||
success = True
|
||||
except:
|
||||
attempt += 1
|
||||
logging.error("Failed, retrying")
|
||||
logging.debug(traceback.format_exc())
|
||||
with open(results_path, "w") as f:
|
||||
json.dump(
|
||||
result,
|
||||
f,
|
||||
indent=4,
|
||||
ensure_ascii=False
|
||||
)
|
||||
return result
|
||||
|
||||
#
|
||||
# AUDIO
|
||||
#
|
||||
def transcribe_audio(audio_path: str,
|
||||
args: argparse.Namespace) -> list[TimelineEvent]:
|
||||
"""Transcribes audio.
|
||||
|
||||
Args:
|
||||
- audio_path - path to the audio file
|
||||
- result_path - path to the resulting JSON file
|
||||
- args - arguments as returned by argsparse
|
||||
|
||||
Returns:
|
||||
- timeline events produced by ASR
|
||||
"""
|
||||
# check if file exists and just load it if it does
|
||||
result_path = "asr_events.json"
|
||||
if os.path.isfile(result_path):
|
||||
try:
|
||||
logging.info(
|
||||
f"Trying to load transcription data from {result_path}"
|
||||
)
|
||||
with open(result_path, "rb") as f:
|
||||
j = [TimelineEvent(**e) for e in json.load(f)]
|
||||
logging.info(f"Loaded transcription data from {result_path}")
|
||||
return j
|
||||
except:
|
||||
logging.debug(
|
||||
f"Could not load transcription data from {result_path}"
|
||||
)
|
||||
# actually transcribe
|
||||
logging.info(f"Transcribing {audio_path}...")
|
||||
logging.debug(f"Creating transcriber (using model `{args.voice_model}`)")
|
||||
t = Transcriber(args.voice_model)
|
||||
logging.debug(f"Creating the transcription...")
|
||||
events = t.transcribe(
|
||||
audio_path,
|
||||
language=args.voice_language
|
||||
)
|
||||
logging.debug(f"Saving to {result_path}")
|
||||
with open(result_path, "w") as f:
|
||||
f.write(json.dumps([asdict(e) for e in events], indent=4, ensure_ascii=False))
|
||||
logging.info(f"Done transcribing, timeline events produced: {len(events)}")
|
||||
return events
|
||||
|
||||
def prepare_audio_windows(events: list[TimelineEvent],
|
||||
args: argparse.Namespace) -> list[AnalysisWindow]:
|
||||
"""Prepare list of windows which should be processed by LLM.
|
||||
|
||||
Args:
|
||||
- events - return value of `transcribe_audio`
|
||||
- args - arguments as returned by argsparse
|
||||
|
||||
Returns:
|
||||
- list of windows for LLM
|
||||
"""
|
||||
return make_windows(
|
||||
events,
|
||||
args.window_context_size,
|
||||
args.window_payload_size
|
||||
)
|
||||
|
||||
def process_audio_windows(windows: list[AnalysisWindow], args: argparse.Namespace) -> TimelineProcessingResult:
|
||||
# check if already processed
|
||||
results_path = "asr_proc_events.json"
|
||||
if os.path.isfile(results_path):
|
||||
try:
|
||||
logging.info(f"Loading processing result from {results_path}")
|
||||
with open(results_path, "rb") as f:
|
||||
j = json.load(f)
|
||||
result = TimelineProcessingResult(
|
||||
events=[TimelineEvent(**e) for e in j["events"]],
|
||||
desired_events=[TimelineEvent(**e) for e in j["desired_events"]]
|
||||
)
|
||||
return result
|
||||
except:
|
||||
logging.error(traceback.print_exc())
|
||||
# create the agent
|
||||
agent = Agent(
|
||||
model=args.ai_model,
|
||||
base_url=args.ai_base_url,
|
||||
api_key=args.ai_api_key
|
||||
)
|
||||
# create the system prompt
|
||||
with open("prompts/audio_window.md", "r") as f:
|
||||
system_prompt = AgentMessage(
|
||||
f.read(),
|
||||
"system"
|
||||
)
|
||||
# prepare the processing result
|
||||
all_events = [w.modifiable for w in windows]
|
||||
processing_result = TimelineProcessingResult(
|
||||
events=[item for sublist in all_events for item in sublist],
|
||||
desired_events=[]
|
||||
)
|
||||
# context for AI to remember previous iterations
|
||||
previous_self_context = {
|
||||
"_comment": "Use this object as you data storage for next iterations"
|
||||
}
|
||||
# process each window
|
||||
for window_id, window in enumerate(windows):
|
||||
# prepare request body
|
||||
req = {
|
||||
"before_readonly": [e.get_ai_dict() for e in window.before_readonly],
|
||||
"modifiable": [e.get_ai_dict() for e in window.modifiable],
|
||||
"after_readonly": [e.get_ai_dict() for e in window.after_readonly],
|
||||
"ai_custom_context": previous_self_context
|
||||
}
|
||||
msg = AgentMessage(
|
||||
content=json.dumps(req, indent=2, ensure_ascii=False),
|
||||
role="user"
|
||||
)
|
||||
# process the window
|
||||
success = False
|
||||
attempt = 1
|
||||
while not success:
|
||||
logging.info(f"Processing a window #{window_id + 1} (attempt #{attempt})...")
|
||||
# try to parse as JSON
|
||||
try:
|
||||
# call the AI
|
||||
response = json.loads(
|
||||
agent.completion(messages=[system_prompt, msg]))
|
||||
# process each request separately
|
||||
requests = response["requests"]
|
||||
for r in requests:
|
||||
execute_timeline_request(processing_result, r)
|
||||
previous_self_context = response["new_context"]
|
||||
success = True
|
||||
except:
|
||||
attempt += 1
|
||||
logging.error("Failed, retrying")
|
||||
logging.debug(traceback.format_exc())
|
||||
with open(results_path, "w") as f:
|
||||
json.dump(
|
||||
{
|
||||
"events": [asdict(e) for e in processing_result.events],
|
||||
"desired_events": [asdict(e) for e in processing_result.desired_events],
|
||||
},
|
||||
f,
|
||||
indent=4,
|
||||
ensure_ascii=False
|
||||
)
|
||||
return processing_result
|
||||
|
||||
def main() -> None:
|
||||
"""Application entry point"""
|
||||
check_cuda()
|
||||
args = setup_arguments()
|
||||
# setup the logger
|
||||
logging.basicConfig(level=logging.DEBUG if args.v else logging.INFO)
|
||||
# transcribe
|
||||
audio_events = transcribe_audio(
|
||||
args.filename,
|
||||
args
|
||||
)
|
||||
# prepare audio windows
|
||||
audio_windows = prepare_audio_windows(
|
||||
audio_events,
|
||||
args
|
||||
)
|
||||
# process audio window
|
||||
processing_result = process_audio_windows(
|
||||
audio_windows,
|
||||
args
|
||||
)
|
||||
# build the document
|
||||
build_document_structure(processing_result, args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except SystemExit:
|
||||
raise
|
||||
except:
|
||||
traceback.print_exc()
|
||||
364
prompts/audio_window.md
Normal file
364
prompts/audio_window.md
Normal file
@@ -0,0 +1,364 @@
|
||||
Ты обрабатываешь временную шкалу записи лекции и подготавливаешь её к дальнейшему созданию документа.
|
||||
|
||||
На вход ты получаешь JSON:
|
||||
|
||||
{
|
||||
"before_readonly": [...],
|
||||
"modifiable": [...],
|
||||
"after_readonly": [...],
|
||||
"ai_custom_context": {...}
|
||||
}
|
||||
|
||||
События расположены в хронологическом порядке.
|
||||
|
||||
ТВОЯ ЗАДАЧА
|
||||
|
||||
1. Исправлять ТОЛЬКО явные ошибки распознавания речи в событиях из `modifiable`.
|
||||
2. Находить места, где для понимания лекции понадобится изображение с экрана (`vis`).
|
||||
3. Находить места, где понадобится извлечь текст, формулу, таблицу, код или другие письменные данные с экрана (`ocr`).
|
||||
4. Поддерживать ОЧЕНЬ КОРОТКИЙ контекст, полезный для обработки следующих окон.
|
||||
|
||||
Ты НЕ создаёшь конспект.
|
||||
Ты НЕ переписываешь речь литературно.
|
||||
Ты НЕ улучшаешь стиль преподавателя.
|
||||
Ты НЕ обязан что-либо изменить в каждом событии.
|
||||
Если изменение не нужно — ничего не делай.
|
||||
|
||||
=== СОБЫТИЯ ===
|
||||
|
||||
Обычное ASR-событие:
|
||||
|
||||
{
|
||||
"id": "asr_123",
|
||||
"timestamp": 100.0,
|
||||
"duration": 4.0,
|
||||
"payload": "распознанная речь"
|
||||
}
|
||||
|
||||
`id`, `timestamp`, `duration`, `links` и другие структурные поля являются данными приложения.
|
||||
|
||||
Никогда не изменяй:
|
||||
- id
|
||||
- timestamp
|
||||
- duration
|
||||
- links
|
||||
|
||||
=== READONLY ===
|
||||
|
||||
`before_readonly` и `after_readonly` предоставлены ТОЛЬКО для понимания контекста.
|
||||
|
||||
НИКОГДА не создавай `modify` или `delete` для событий из:
|
||||
- `before_readonly`
|
||||
- `after_readonly`
|
||||
|
||||
`modify` и `delete` могут ссылаться ТОЛЬКО на ID из `modifiable`.
|
||||
|
||||
=== MODIFY ===
|
||||
|
||||
Формат:
|
||||
|
||||
{
|
||||
"req": "modify",
|
||||
"id": "asr_123",
|
||||
"payload": "исправленный текст"
|
||||
}
|
||||
|
||||
Создавай `modify` ТОЛЬКО при содержательном изменении текста.
|
||||
|
||||
ВАЖНО:
|
||||
НЕ создавай `modify`, если старый и новый текст отличаются только:
|
||||
- пробелом в начале;
|
||||
- пробелом в конце;
|
||||
- количеством пробелов;
|
||||
- переносами строк;
|
||||
- другими незначительными изменениями whitespace.
|
||||
|
||||
Например:
|
||||
|
||||
БЫЛО:
|
||||
" для изучения сложных систем."
|
||||
|
||||
СТАЛО:
|
||||
"для изучения сложных систем."
|
||||
|
||||
Это НЕ является причиной для `modify`.
|
||||
|
||||
Не создавай такой запрос.
|
||||
|
||||
Также НЕ создавай `modify` только ради:
|
||||
- косметического изменения пробелов;
|
||||
- изменения регистра без необходимости;
|
||||
- несущественного перефразирования;
|
||||
- замены правильной фразы на синоним.
|
||||
|
||||
Исправляй:
|
||||
- явные ошибки ASR;
|
||||
- очевидно неправильно распознанные слова;
|
||||
- слова, восстановление которых однозначно следует из контекста;
|
||||
- важную пунктуацию, если она существенно влияет на смысл.
|
||||
|
||||
Если ты не уверен, что распознавание ошибочно — оставь исходный текст.
|
||||
|
||||
Сохраняй смысл и стиль речи преподавателя.
|
||||
|
||||
=== DELETE ===
|
||||
|
||||
Формат:
|
||||
|
||||
{
|
||||
"req": "delete",
|
||||
"id": "asr_123"
|
||||
}
|
||||
|
||||
Используй `delete` редко.
|
||||
|
||||
Удалять можно только очевидный мусор:
|
||||
- ложное распознавание;
|
||||
- случайный дубль;
|
||||
- бессмысленный ASR-артефакт.
|
||||
|
||||
Не удаляй короткую фразу только потому, что она короткая.
|
||||
|
||||
Например:
|
||||
|
||||
"Следующий слайд."
|
||||
"Записываем."
|
||||
"Посмотрите сюда."
|
||||
"Дальше."
|
||||
"Это будет на экзамене."
|
||||
|
||||
могут быть очень важны для анализа видео.
|
||||
|
||||
=== VIS ===
|
||||
|
||||
Если для понимания или будущего документа полезно увидеть изображение с видео, создай:
|
||||
|
||||
{
|
||||
"req": "add",
|
||||
"type": "vis",
|
||||
"links": ["asr_123"],
|
||||
"anchor": "during",
|
||||
"payload": "краткое описание того, что нужно найти на экране"
|
||||
}
|
||||
|
||||
`payload` должен описывать, ЧТО именно downstream-модель должна искать.
|
||||
|
||||
Хорошо:
|
||||
"График зависимости, на который указывает преподаватель."
|
||||
|
||||
Плохо:
|
||||
"Нужна картинка."
|
||||
|
||||
Используй `vis` для:
|
||||
- графиков;
|
||||
- схем;
|
||||
- диаграмм;
|
||||
- рисунков;
|
||||
- изображений;
|
||||
- интерфейсов;
|
||||
- визуально значимых слайдов;
|
||||
- объектов, расположение которых важно для понимания.
|
||||
|
||||
=== OCR ===
|
||||
|
||||
Если на экране ожидается важная письменная информация, создай:
|
||||
|
||||
{
|
||||
"req": "add",
|
||||
"type": "ocr",
|
||||
"links": ["asr_123"],
|
||||
"anchor": "during",
|
||||
"payload": "краткое описание текста, формулы или других данных, которые нужно найти"
|
||||
}
|
||||
|
||||
Используй `ocr` для:
|
||||
- определений;
|
||||
- формул;
|
||||
- таблиц;
|
||||
- списков;
|
||||
- чисел;
|
||||
- кода;
|
||||
- подписей;
|
||||
- больших фрагментов текста на слайдах.
|
||||
|
||||
Хорошо:
|
||||
"Формула метода, которую преподаватель просит записать."
|
||||
|
||||
Плохо:
|
||||
"Текст со слайда."
|
||||
|
||||
Не создавай одновременно `vis` и `ocr` без необходимости.
|
||||
|
||||
=== LINKS ===
|
||||
|
||||
Каждый `add` обязан иметь `links`.
|
||||
|
||||
`links` должны содержать ID событий, из-за которых возник запрос.
|
||||
|
||||
Хотя бы один ID в `links` ОБЯЗАН принадлежать `modifiable`.
|
||||
|
||||
Используй минимально необходимое количество ID.
|
||||
|
||||
Никогда не придумывай ID.
|
||||
|
||||
=== ANCHOR ===
|
||||
|
||||
Допустимые значения:
|
||||
|
||||
"before"
|
||||
"during"
|
||||
"after"
|
||||
|
||||
Используй:
|
||||
|
||||
"before" — искомая информация была непосредственно перед связанной речью.
|
||||
|
||||
"during" — информация показывается во время связанной речи.
|
||||
|
||||
"after" — информация ожидается сразу после связанной речи.
|
||||
|
||||
Примеры:
|
||||
|
||||
"Как видно на этом графике..."
|
||||
→ "during"
|
||||
|
||||
"На предыдущем слайде..."
|
||||
→ "before"
|
||||
|
||||
"Записываем следующий слайд."
|
||||
→ "after"
|
||||
|
||||
НЕ создавай timestamp для новых событий.
|
||||
Приложение само вычислит его по `links` и `anchor`.
|
||||
|
||||
=== ВАЖНО: МАЛО РЕЧИ НЕ ОЗНАЧАЕТ МАЛО ИНФОРМАЦИИ ===
|
||||
|
||||
Преподаватель может сказать:
|
||||
|
||||
"Записываем следующий слайд."
|
||||
|
||||
а затем несколько минут молчать.
|
||||
|
||||
Это может означать, что значительная часть содержания лекции находится только на экране.
|
||||
|
||||
Создавай `vis` или `ocr`, когда речь явно указывает на важную визуальную информацию, даже если сама речь почти ничего не содержит.
|
||||
|
||||
=== AI CUSTOM CONTEXT ===
|
||||
|
||||
`ai_custom_context` — это НЕ конспект и НЕ история лекции.
|
||||
|
||||
Это очень маленькая рабочая память для следующих окон.
|
||||
|
||||
Возвращай контекст строго в формате:
|
||||
|
||||
{
|
||||
"topic": null,
|
||||
"subtopic": null,
|
||||
"summary": "",
|
||||
"terms": {},
|
||||
"pending": []
|
||||
}
|
||||
|
||||
Правила:
|
||||
|
||||
`topic`
|
||||
- максимум 80 символов;
|
||||
- только текущая широкая тема;
|
||||
- null, если неизвестно.
|
||||
|
||||
`subtopic`
|
||||
- максимум 120 символов;
|
||||
- только текущая локальная тема;
|
||||
- null, если неизвестно.
|
||||
|
||||
`summary`
|
||||
- максимум 250 символов;
|
||||
- максимум 2 коротких предложения;
|
||||
- только информация, которая реально поможет понять СЛЕДУЮЩЕЕ окно;
|
||||
- не пересказывай текущий фрагмент подробно.
|
||||
|
||||
`terms`
|
||||
- максимум 8 элементов;
|
||||
- хранит только термины, имена, сокращения или правильные написания, полезные для дальнейшего исправления ASR;
|
||||
- значение каждого элемента максимум 100 символов.
|
||||
|
||||
`pending`
|
||||
- максимум 3 элемента;
|
||||
- каждый максимум 120 символов;
|
||||
- только неразрешённые ссылки или вопросы, которые могут проясниться в следующем окне.
|
||||
|
||||
КРИТИЧЕСКИ ВАЖНО:
|
||||
|
||||
Не копируй транскрипцию в контекст.
|
||||
Не храни подробное содержание уже обработанных фрагментов.
|
||||
Не пиши объяснения своей работы.
|
||||
Не пиши длинный пересказ лекции.
|
||||
Не накапливай историю бесконечно.
|
||||
Удаляй устаревшие сведения.
|
||||
Если старый контекст уже достаточен — можешь вернуть его почти без изменений.
|
||||
|
||||
Контекст должен быть минимальным.
|
||||
|
||||
=== OUTPUT ===
|
||||
|
||||
Верни РОВНО один JSON-объект:
|
||||
|
||||
{
|
||||
"requests": [],
|
||||
"new_context": {
|
||||
"topic": null,
|
||||
"subtopic": null,
|
||||
"summary": "",
|
||||
"terms": {},
|
||||
"pending": []
|
||||
}
|
||||
}
|
||||
|
||||
Никакого Markdown.
|
||||
Никаких ```json.
|
||||
Никакого текста до JSON.
|
||||
Никакого текста после JSON.
|
||||
Никаких комментариев.
|
||||
|
||||
В `requests` допускаются ТОЛЬКО:
|
||||
|
||||
{
|
||||
"req": "modify",
|
||||
"id": "...",
|
||||
"payload": "..."
|
||||
}
|
||||
|
||||
{
|
||||
"req": "delete",
|
||||
"id": "..."
|
||||
}
|
||||
|
||||
{
|
||||
"req": "add",
|
||||
"type": "vis",
|
||||
"links": ["..."],
|
||||
"anchor": "before|during|after",
|
||||
"payload": "..."
|
||||
}
|
||||
|
||||
{
|
||||
"req": "add",
|
||||
"type": "ocr",
|
||||
"links": ["..."],
|
||||
"anchor": "before|during|after",
|
||||
"payload": "..."
|
||||
}
|
||||
|
||||
=== ПРОВЕРКА ПЕРЕД ОТВЕТОМ ===
|
||||
|
||||
Перед выдачей ответа проверь:
|
||||
|
||||
1. Все `modify` и `delete` относятся только к `modifiable`.
|
||||
2. Ты не создал `modify` только ради изменения пробелов.
|
||||
3. Каждый `add` содержит хотя бы один `links` из `modifiable`.
|
||||
4. Все ID действительно существуют во входе.
|
||||
5. `type` равен только `vis` или `ocr`.
|
||||
6. `anchor` равен только `before`, `during` или `after`.
|
||||
7. Контекст укладывается в указанные лимиты.
|
||||
8. Ответ является валидным JSON.
|
||||
9. Если никаких изменений не требуется, `"requests": []` — это правильный и желательный результат.
|
||||
450
prompts/build_structure.md
Normal file
450
prompts/build_structure.md
Normal file
@@ -0,0 +1,450 @@
|
||||
Ты преобразуешь подготовленный таймлайн лекции в структурированные блоки будущего Markdown-документа.
|
||||
|
||||
Ты НЕ пишешь Markdown напрямую.
|
||||
Ты создаёшь логическую структуру документа в JSON.
|
||||
|
||||
На вход ты получаешь объект:
|
||||
|
||||
{
|
||||
"before_readonly": [...],
|
||||
"content": [...],
|
||||
"after_readonly": [...],
|
||||
"document_context": {
|
||||
"current_section": null,
|
||||
"current_subsection": null,
|
||||
"recent_structure": [],
|
||||
"pending": null
|
||||
}
|
||||
}
|
||||
|
||||
Все события расположены в хронологическом порядке.
|
||||
|
||||
Событие имеет примерно такую структуру:
|
||||
|
||||
{
|
||||
"id": "asr_123",
|
||||
"timestamp": 100.0,
|
||||
"duration": 4.0,
|
||||
"payload": "текст события",
|
||||
"links": []
|
||||
}
|
||||
|
||||
==================================================
|
||||
ЗАДАЧА
|
||||
==================================================
|
||||
|
||||
Преобразуй лекционную речь из `content` в удобные структурированные блоки учебного документа.
|
||||
|
||||
Ты должен:
|
||||
|
||||
- объединять короткие события в законченные мысли;
|
||||
- исправлять структуру устной речи;
|
||||
- удалять бессодержательные повторы;
|
||||
- удалять слова-паразиты и речевой шум;
|
||||
- объединять повторные формулировки одной мысли;
|
||||
- выделять смысловые разделы и подразделы;
|
||||
- создавать понятные заголовки;
|
||||
- превращать перечисления в списки;
|
||||
- выделять определения;
|
||||
- выделять действительно важные замечания;
|
||||
- сохранять полезные примеры и объяснения;
|
||||
- сохранять существенные организационные сведения, если они полезны читателю документа;
|
||||
- удалять переклички, случайные реплики, обсуждение подключения к конференции и другой технический шум.
|
||||
|
||||
Ты можешь значительно перерабатывать форму речи, но НЕ должен менять её смысл.
|
||||
|
||||
Ты НЕ обязан сохранять формулировки преподавателя дословно.
|
||||
|
||||
Не придумывай сведения, которых нет во входных событиях.
|
||||
|
||||
Если смысл нельзя уверенно восстановить, не выдумывай его.
|
||||
|
||||
==================================================
|
||||
РОЛИ ЧАСТЕЙ ОКНА
|
||||
==================================================
|
||||
|
||||
`content` — основная часть окна, из которой необходимо строить документ.
|
||||
|
||||
`before_readonly` — события непосредственно перед `content`.
|
||||
Они нужны для понимания начала текущей мысли.
|
||||
|
||||
`after_readonly` — события непосредственно после `content`.
|
||||
Они нужны в первую очередь для определения того, заканчивается ли мысль внутри `content` или продолжается дальше.
|
||||
|
||||
Ты можешь использовать `before_readonly` и `after_readonly` для понимания контекста.
|
||||
|
||||
Однако:
|
||||
|
||||
- готовый блок может использовать `source_ids` из `before_readonly` и `content`;
|
||||
- готовый блок НЕ должен использовать `source_ids` из `after_readonly`;
|
||||
- `after_readonly` не является частью материала, который нужно обработать на этой итерации.
|
||||
|
||||
==================================================
|
||||
ГРАНИЦЫ ОКНА
|
||||
==================================================
|
||||
|
||||
Главное правило:
|
||||
|
||||
ФИНАЛИЗИРУЙ смысловой блок только тогда, когда соответствующая мысль заканчивается внутри `content`.
|
||||
|
||||
Пример:
|
||||
|
||||
before_readonly:
|
||||
"Первой задачей анализа является..."
|
||||
|
||||
content:
|
||||
"определение тенденций и показателей состояния объекта."
|
||||
|
||||
→ мысль закончена внутри `content`;
|
||||
→ готовый блок создавать МОЖНО;
|
||||
→ `source_ids` могут включать событие из `before_readonly`.
|
||||
|
||||
Другой пример:
|
||||
|
||||
content:
|
||||
"Второй принцип заключается в том, что..."
|
||||
|
||||
after_readonly:
|
||||
"необходимо учитывать влияние внешней среды..."
|
||||
|
||||
→ мысль продолжается за пределами `content`;
|
||||
→ НЕ создавай незаконченный блок;
|
||||
→ сохрани сведения о незавершённой мысли в `document_context.pending`.
|
||||
|
||||
Никогда специально не обрывай предложение или логическую мысль на границе окна.
|
||||
|
||||
==================================================
|
||||
DOCUMENT_CONTEXT
|
||||
==================================================
|
||||
|
||||
`document_context` — компактное состояние построения документа, передаваемое между окнами.
|
||||
|
||||
Формат:
|
||||
|
||||
{
|
||||
"current_section": null,
|
||||
"current_subsection": null,
|
||||
"recent_structure": [],
|
||||
"pending": null
|
||||
}
|
||||
|
||||
`current_section`
|
||||
- текущий крупный раздел документа;
|
||||
- строка или null.
|
||||
|
||||
`current_subsection`
|
||||
- текущий подраздел;
|
||||
- строка или null.
|
||||
|
||||
`recent_structure`
|
||||
- последние существенные заголовки документа;
|
||||
- максимум 6 элементов;
|
||||
- это НЕ содержание лекции, а только структура документа.
|
||||
|
||||
`pending`
|
||||
- описание незаконченной мысли с предыдущего окна;
|
||||
- null, если незаконченной мысли нет.
|
||||
|
||||
==================================================
|
||||
PENDING
|
||||
==================================================
|
||||
|
||||
`document_context.pending` — это краткая подсказка о мысли, которая началась в предыдущем окне, но тогда не могла быть завершена.
|
||||
|
||||
Пример:
|
||||
|
||||
{
|
||||
"type": "paragraph",
|
||||
"source_ids": ["asr_200", "asr_201"],
|
||||
"summary": "Началось объяснение второго принципа системного анализа; формулировка не закончена."
|
||||
}
|
||||
|
||||
ВАЖНО:
|
||||
|
||||
`pending` НЕ является источником истины.
|
||||
|
||||
Реальные события имеют приоритет над `pending`.
|
||||
|
||||
События, указанные в `pending.source_ids`, обычно должны находиться в `before_readonly` текущего окна.
|
||||
|
||||
Используй `pending` только как подсказку, помогающую понять:
|
||||
- какая мысль осталась незавершённой;
|
||||
- какие события из `before_readonly` относятся к ней.
|
||||
|
||||
Если `pending` противоречит реальным событиям, доверяй реальным событиям.
|
||||
|
||||
Если незавершённая ранее мысль теперь закончилась внутри `content`:
|
||||
- создай нормальный готовый блок;
|
||||
- включи нужные события из `before_readonly` и `content` в `source_ids`;
|
||||
- верни `"pending": null`, если в конце нового `content` не началась другая незавершённая мысль.
|
||||
|
||||
Если в конце текущего `content` снова начинается мысль, продолжающаяся в `after_readonly`, создай новый `pending`.
|
||||
|
||||
`pending.summary` должен быть очень коротким.
|
||||
|
||||
Не копируй туда весь текст.
|
||||
Не пиши подробный пересказ.
|
||||
Не используй `pending` для уже законченных мыслей.
|
||||
|
||||
==================================================
|
||||
ТИПЫ ГОТОВЫХ БЛОКОВ
|
||||
==================================================
|
||||
|
||||
Допустимы только следующие типы.
|
||||
|
||||
1. Заголовок:
|
||||
|
||||
{
|
||||
"type": "heading",
|
||||
"level": 2,
|
||||
"text": "Задачи анализа",
|
||||
"source_ids": ["asr_100", "asr_101"]
|
||||
}
|
||||
|
||||
Допустимые уровни:
|
||||
- 2 — крупный раздел;
|
||||
- 3 — подраздел;
|
||||
- 4 — небольшой локальный подраздел.
|
||||
|
||||
Не используй level 1.
|
||||
|
||||
Не создавай заголовок для каждого небольшого смыслового перехода.
|
||||
|
||||
2. Обычный абзац:
|
||||
|
||||
{
|
||||
"type": "paragraph",
|
||||
"text": "Анализ является начальной составной частью исследования.",
|
||||
"source_ids": ["asr_102", "asr_103"]
|
||||
}
|
||||
|
||||
Абзац должен представлять законченную мысль.
|
||||
|
||||
Не создавай огромные абзацы, объединяющие много разных идей.
|
||||
|
||||
3. Маркированный список:
|
||||
|
||||
{
|
||||
"type": "list",
|
||||
"items": [
|
||||
"Первый пункт.",
|
||||
"Второй пункт.",
|
||||
"Третий пункт."
|
||||
],
|
||||
"source_ids": ["asr_110", "asr_111", "asr_112"]
|
||||
}
|
||||
|
||||
Используй для нескольких однородных элементов, если порядок несущественен.
|
||||
|
||||
4. Нумерованный список:
|
||||
|
||||
{
|
||||
"type": "numbered_list",
|
||||
"items": [
|
||||
"Первый принцип.",
|
||||
"Второй принцип.",
|
||||
"Третий принцип."
|
||||
],
|
||||
"source_ids": ["asr_120", "asr_121", "asr_122"]
|
||||
}
|
||||
|
||||
Используй, если порядок имеет смысл или преподаватель явно использует нумерацию:
|
||||
|
||||
"первое"
|
||||
"второе"
|
||||
"третье"
|
||||
|
||||
"первый принцип"
|
||||
"второй принцип"
|
||||
|
||||
и т. п.
|
||||
|
||||
5. Определение:
|
||||
|
||||
{
|
||||
"type": "definition",
|
||||
"term": "Системный анализ",
|
||||
"text": "Совокупность методологических средств...",
|
||||
"source_ids": ["asr_130", "asr_131"]
|
||||
}
|
||||
|
||||
Используй только тогда, когда материал действительно представляет определение термина или понятия.
|
||||
|
||||
6. Важное замечание:
|
||||
|
||||
{
|
||||
"type": "important",
|
||||
"text": "Цели структурных частей системы не должны противоречить общей цели системы.",
|
||||
"source_ids": ["asr_140", "asr_141"]
|
||||
}
|
||||
|
||||
Используй только для действительно важных положений, на которых делается смысловой акцент.
|
||||
|
||||
Не превращай каждый абзац в `important`.
|
||||
|
||||
7. Примечание:
|
||||
|
||||
{
|
||||
"type": "note",
|
||||
"text": "К этой теме преподаватель планирует вернуться на следующем занятии.",
|
||||
"source_ids": ["asr_150"]
|
||||
}
|
||||
|
||||
Используй редко.
|
||||
|
||||
==================================================
|
||||
SOURCE_IDS
|
||||
==================================================
|
||||
|
||||
Каждый готовый блок ОБЯЗАН иметь `source_ids`.
|
||||
|
||||
`source_ids` показывают, из каких событий получено содержание блока.
|
||||
|
||||
Разрешены ID только из:
|
||||
- `before_readonly`;
|
||||
- `content`.
|
||||
|
||||
Не используй ID из `after_readonly` в готовых блоках.
|
||||
|
||||
Не придумывай ID.
|
||||
|
||||
Добавляй только те ID, которые реально внесли содержательный вклад в блок.
|
||||
|
||||
Для `pending.source_ids` также используй реальные события, относящиеся к незавершённой мысли.
|
||||
|
||||
==================================================
|
||||
СТИЛЬ ДОКУМЕНТА
|
||||
==================================================
|
||||
|
||||
Будущий документ должен быть удобным учебным конспектом, а не дословной стенограммой.
|
||||
|
||||
Предпочитай:
|
||||
|
||||
- законченные предложения;
|
||||
- ясные формулировки;
|
||||
- компактные абзацы;
|
||||
- структурированные списки;
|
||||
- логичную иерархию заголовков;
|
||||
- сохранение специальной терминологии лекции;
|
||||
- сохранение полезных примеров.
|
||||
|
||||
Удаляй:
|
||||
|
||||
- многократные повторения;
|
||||
- самокоррекции преподавателя, если итоговый смысл понятен;
|
||||
- слова-паразиты;
|
||||
- бессодержательные паузы и реплики;
|
||||
- перекличку;
|
||||
- технические проблемы;
|
||||
- обсуждение подключения;
|
||||
- материал, не имеющий ценности для будущего документа.
|
||||
|
||||
Но не удаляй содержательную информацию только потому, что преподаватель сформулировал её разговорно.
|
||||
|
||||
Если одна и та же мысль произнесена несколько раз, обычно создай одну наиболее полную и ясную формулировку.
|
||||
|
||||
==================================================
|
||||
TIMESTAMP, DURATION, LINKS
|
||||
==================================================
|
||||
|
||||
`timestamp` и `duration` помогают понимать порядок событий и временные разрывы.
|
||||
|
||||
Не выводи их в готовые блоки.
|
||||
|
||||
`links` содержат дополнительную информацию о связи событий.
|
||||
|
||||
Используй их для понимания происхождения и связи материала, но не придумывай на их основе отсутствующее содержание.
|
||||
|
||||
==================================================
|
||||
ОБНОВЛЕНИЕ DOCUMENT_CONTEXT
|
||||
==================================================
|
||||
|
||||
После обработки верни новое состояние:
|
||||
|
||||
{
|
||||
"current_section": null,
|
||||
"current_subsection": null,
|
||||
"recent_structure": [],
|
||||
"pending": null
|
||||
}
|
||||
|
||||
Обновляй:
|
||||
|
||||
`current_section`
|
||||
- в соответствии с последним актуальным крупным разделом.
|
||||
|
||||
`current_subsection`
|
||||
- в соответствии с последним актуальным подразделом.
|
||||
|
||||
`recent_structure`
|
||||
- максимум 6 последних существенных заголовков.
|
||||
|
||||
`pending`
|
||||
- новая незавершённая мысль в конце `content`;
|
||||
- либо null.
|
||||
|
||||
Не храни в `document_context` подробный конспект лекции.
|
||||
|
||||
Не помещай туда готовые абзацы документа.
|
||||
|
||||
Контекст должен оставаться очень маленьким.
|
||||
|
||||
==================================================
|
||||
OUTPUT
|
||||
==================================================
|
||||
|
||||
Верни ровно один валидный JSON:
|
||||
|
||||
{
|
||||
"blocks": [
|
||||
...
|
||||
],
|
||||
"new_document_context": {
|
||||
"current_section": null,
|
||||
"current_subsection": null,
|
||||
"recent_structure": [],
|
||||
"pending": null
|
||||
}
|
||||
}
|
||||
|
||||
Никакого Markdown.
|
||||
|
||||
Никаких ```json.
|
||||
|
||||
Никакого текста до JSON.
|
||||
|
||||
Никакого текста после JSON.
|
||||
|
||||
Не добавляй комментарии.
|
||||
|
||||
Не добавляй неизвестные типы блоков.
|
||||
|
||||
Если в текущем `content` нет материала, достойного добавления в документ:
|
||||
|
||||
{
|
||||
"blocks": [],
|
||||
"new_document_context": {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
— это нормальный результат.
|
||||
|
||||
==================================================
|
||||
ПРОВЕРКА ПЕРЕД ОТВЕТОМ
|
||||
==================================================
|
||||
|
||||
Перед ответом проверь:
|
||||
|
||||
1. Каждый готовый блок имеет `source_ids`.
|
||||
2. Все `source_ids` существуют во входе.
|
||||
3. Готовые блоки используют ID только из `before_readonly` и `content`.
|
||||
4. Ни один готовый блок не зависит от продолжения в `after_readonly`.
|
||||
5. Если мысль продолжается за пределами `content`, она помещена в `pending`, а не обрезана.
|
||||
6. Если старый `pending` был завершён, он превращён в нормальный блок.
|
||||
7. `pending` не используется как источник истины вместо реальных событий.
|
||||
8. Повторяющийся материал не продублирован без причины.
|
||||
9. Перекличка и технический шум не попали в учебный документ.
|
||||
10. Не придуманы факты, отсутствующие во входных событиях.
|
||||
11. `recent_structure` содержит не более 6 элементов.
|
||||
12. Ответ является валидным JSON.
|
||||
60
transcriber.py
Normal file
60
transcriber.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from typing import Any
|
||||
|
||||
import whisper
|
||||
|
||||
from utils import TimelineEvent
|
||||
|
||||
class Transcriber:
|
||||
"""This class performs transcription of the audio file."""
|
||||
@staticmethod
|
||||
def get_models_list() -> list[str]:
|
||||
"""Returns list of allowed `model` values for constructor."""
|
||||
return whisper.available_models()
|
||||
|
||||
def __init__(self, model: str, **kwargs) -> None:
|
||||
"""Create the transcriber instance.
|
||||
|
||||
Args:
|
||||
- model - model to use (call `get_models_list` to get the list of
|
||||
available models)
|
||||
- **kwargs are passed to `whisper.load_model(...)`
|
||||
"""
|
||||
allowed_models = self.get_models_list()
|
||||
if model not in allowed_models:
|
||||
raise ValueError(
|
||||
f"Model `{model}` is not available. "
|
||||
f"Use one of {', '.join(f'`{s}`' for s in allowed_models)}"
|
||||
)
|
||||
self._model = whisper.load_model(model, **kwargs)
|
||||
|
||||
def transcribe(self, path: str, **kwargs) -> list[TimelineEvent]:
|
||||
"""Transcribe audiofile. The operation will take a lot of time for large
|
||||
files.
|
||||
|
||||
Args:
|
||||
- path - path to the file to transcribe.
|
||||
- **kwargs - passed to `transcribe()`
|
||||
|
||||
Returns:
|
||||
- list of timeline events you should use
|
||||
"""
|
||||
raw_segments: list[dict]
|
||||
raw_segments = self._model.transcribe(path, **kwargs)["segments"] # type: ignore
|
||||
result: list[TimelineEvent] = []
|
||||
seg_id: int = 0
|
||||
for raw_segment in raw_segments:
|
||||
ev = TimelineEvent(
|
||||
id = f"asr_{seg_id}",
|
||||
timestamp=float(raw_segment["start"]),
|
||||
duration=float(raw_segment["end"]) - float(raw_segment["start"]),
|
||||
payload=raw_segment["text"],
|
||||
custom={
|
||||
"whisper_temperature": float(raw_segment["temperature"]),
|
||||
"whisper_avg_logprob": float(raw_segment["avg_logprob"]),
|
||||
"whisper_no_speech_prob": float(raw_segment["no_speech_prob"])
|
||||
},
|
||||
links=[]
|
||||
)
|
||||
result.append(ev)
|
||||
seg_id += 1
|
||||
return result
|
||||
62
utils.py
Normal file
62
utils.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
@dataclass
|
||||
class TimelineEvent:
|
||||
id: str
|
||||
"""ID of the event in format `asr_18`"""
|
||||
|
||||
timestamp: float
|
||||
"""When did the event happen"""
|
||||
|
||||
duration: float
|
||||
"""How long did the event last (zero if it does not make sence)"""
|
||||
|
||||
payload: str
|
||||
"""Payload of the event (text for `asr`, description for `vis`, OCR result for `ocr`)"""
|
||||
|
||||
custom: dict
|
||||
"""Custom data"""
|
||||
|
||||
links: list[str]
|
||||
"""ID of related timeline events, empty list for None"""
|
||||
|
||||
def get_ai_dict(self) -> dict:
|
||||
"""Returns dict that is sanitized for AI."""
|
||||
d = {
|
||||
"id": self.id,
|
||||
"timestamp": self.timestamp,
|
||||
"payload": self.payload
|
||||
}
|
||||
if self.duration:
|
||||
d["duration"] = self.duration
|
||||
if self.links:
|
||||
d["links"] = self.links
|
||||
return d
|
||||
|
||||
@dataclass
|
||||
class AnalysisWindow:
|
||||
before_readonly: list[TimelineEvent]
|
||||
"""Timeline events for context (before current window)"""
|
||||
|
||||
modifiable: list[TimelineEvent]
|
||||
"""Timeline events that are to be analyzed"""
|
||||
|
||||
after_readonly: list[TimelineEvent]
|
||||
"""Timeline events for context (after current window)"""
|
||||
|
||||
@dataclass
|
||||
class AgentMessage:
|
||||
content: str
|
||||
"""Content of the message"""
|
||||
|
||||
role: Literal["system", "assistant", "user"]
|
||||
"""Who sent the message"""
|
||||
|
||||
@dataclass
|
||||
class TimelineProcessingResult:
|
||||
events: list[TimelineEvent]
|
||||
"""List of events"""
|
||||
|
||||
desired_events: list[TimelineEvent]
|
||||
"""List of events desired for existance"""
|
||||
Reference in New Issue
Block a user