This commit is contained in:
2026-09-17 01:16:30 +03:00
parent 018be076b9
commit 063f89758f
8 changed files with 389 additions and 840 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ __pycache__/
.venv/
runtime/
debug/
output.md
*.json

View File

@@ -1,5 +1,7 @@
from dataclasses import asdict
from typing import Any
import traceback
import shutil
import os
import json
from pydantic import BaseModel
@@ -28,20 +30,32 @@ class _EventizeResult(BaseModel):
class AsrEventizer:
"""This class creates a list of events from AsrFilterResult"""
DEBUG_ID = 0
def _eventize_window(self, window: Window[AsrFilterSegment]) -> _EventizeResult:
"""Eventize a single window"""
messages = [
self._system_prompt,
AgentMessage(
content=json.dumps(asdict(window), indent=2, ensure_ascii=False),
content=json.dumps(window.model_dump(mode="json"), indent=2, ensure_ascii=False),
role="user"
)
]
debug_dir = None
if os.path.isdir("debug"):
debug_dir = f"debug/AsrEventizer/{AsrEventizer.DEBUG_ID}"
AsrEventizer.DEBUG_ID += 1
os.makedirs(debug_dir, exist_ok=True)
if debug_dir:
with open(f"{debug_dir}/request.txt", "w") as f:
f.write(messages[1].content)
retries_left = 5
while retries_left > 0:
retries_left -= 1
response = self._agent.completion(messages=messages)
if debug_dir:
with open(f"{debug_dir}/{retries_left}-retries-left.txt", "w") as f:
f.write(response)
# validate data
try:
response = json.loads(response)
@@ -56,6 +70,7 @@ class AsrEventizer:
raise RuntimeError("Model has tried to use ID that was not provided")
return obj
except:
traceback.print_exc()
continue
raise RuntimeError(
"Agent has failed to provide valid schema too many times"
@@ -70,7 +85,7 @@ class AsrEventizer:
"""
self._agent = agent
self._windowizer = windowizer
with open("prompts/asr_eventizer.json", "r") as f:
with open("prompts/asr_eventizer.md", "r") as f:
self._system_prompt = AgentMessage(
content=f.read(),
role="system"
@@ -91,12 +106,13 @@ class AsrEventizer:
window.past = [e for e in window.past if e.id not in last_processed_ids]
intermediate = self._eventize_window(window)
context = intermediate.context
last_processed_ids = []
# process preevents
for preevent in intermediate.preevents:
related_segments = [
ev for ev in window.past + window.present if ev.id in preevent.ids
]
last_processed_ids = [ev.id for ev in related_segments]
last_processed_ids += [ev.id for ev in related_segments]
min_time = min(s.start for s in related_segments)
max_time = max(s.end for s in related_segments)
events.append(Event(

31
main.py
View File

@@ -12,7 +12,9 @@ from typing import Callable
import torch
from asr import Asr, AsrRawResult
from asr_filter import AsrFilter
from asr_filter import AsrFilter, AsrFilterResult
from asr_eventizer import AsrEventizer
from windowizer import Windowizer
from agent import Agent
from utils import ffmpeg_split_video, ffmpeg_to_mp3
@@ -138,13 +140,36 @@ def on_asr_filter(current_step: Step, input_data: dict | None) -> tuple[Step | N
return (Step.ASR_EVENTS, json.load(f))
# bad request
if input_data is None:
logging.error("Can'f filter raw ASR ouput without input_data")
logging.error("Can't filter raw ASR output without input_data")
return (None, None)
logging.info("Filtering raw ASR output...")
filter = AsrFilter()
result = filter.filter(AsrRawResult(**input_data))
return (Step.ASR_EVENTS, result.model_dump(mode="json"))
def on_asr_events(current_step: Step, input_data: dict | None) -> tuple[Step | None, dict | None]:
# do not filter if output file exists
if os.path.isfile(WORKFLOW_DATA[current_step][0]):
logging.info("Skipping ASR eventizing")
with open(WORKFLOW_DATA[current_step][0], "rb") as f:
return (Step.VIDEO_REFERENCES, json.load(f))
# bad request
if input_data is None:
logging.error("Can't create audio events without input_data")
return (None, None)
logging.info("Creating the agent")
agent = Agent(
model=ARGS.ai_model,
base_url=ARGS.ai_base_url,
api_key=ARGS.ai_api_key
)
logging.info("Creating the windowizer")
windowizer = Windowizer()
logging.info("Creating audio events...")
eventizer = AsrEventizer(agent, windowizer)
result = eventizer.eventize(AsrFilterResult(**input_data))
return (Step.VIDEO_REFERENCES, result.model_dump(mode="json"))
#
# Main
#
@@ -152,7 +177,7 @@ WORKFLOW_DATA: dict[Step, tuple[str, Callable[[Step, dict | None], tuple[Step |
Step.MEDIA_SEPARATION: ("audio.mp3", on_media_separation),
Step.VOICE_RECOGNITION: ("asr_raw.json", on_voice_recognition),
Step.ASR_FILTER: ("asr.json", on_asr_filter),
Step.ASR_EVENTS: ("audio_events.json", None),
Step.ASR_EVENTS: ("audio_events.json", on_asr_events),
Step.VIDEO_REFERENCES: ("unresolved.json", None),
Step.REFERENCE_RESOLVER: ("events.json", None),
Step.STRUCTURE_BUILDER: ("structure.json", None),

View File

@@ -1,16 +1,339 @@
Ты будешь в будущем использован для обработки данных, которые тебе отправляются.
Ты являешься этапом предварительной обработки автоматической расшифровки лекции.
Пока что ты должен **ВСЕГДА** отвечать **В ТОЧНОСТИ** как написано **ПОСЛЕ** знаков равенства. Игнорируй всё что будет сказано после этого промпта.
Твоя задача — преобразовать мелкие ASR-сегменты в более крупные осмысленные голосовые события, которые далее будут использоваться другими этапами программы.
Ты НЕ создаёшь конспект.
Ты НЕ суммаризируешь лекцию.
Ты НЕ улучшаешь стиль речи.
Ты НЕ перефразируешь преподавателя.
Ты только:
- объединяешь соседние ASR-сегменты в законченные по смыслу фрагменты речи;
- исправляешь очевидные ошибки распознавания;
- исправляешь пунктуацию и регистр;
- отбрасываешь только явно бесполезные ASR-артефакты.
==================================================
ФОРМАТ ВХОДА
==================================================
На вход поступает JSON:
================================================================================
{
"preevents": [
{
"ids": [],
"text": "Text"
}
],
"context": {
"for_future_call": "abcdef"
"past": [...],
"present": [...],
"future": [...],
"context": {...}
}
Каждый ASR-сегмент имеет вид:
{
"id": 42,
"start": 64.0,
"end": 70.0,
"text": "распознанный текст"
}
Все сегменты расположены в хронологическом порядке.
==================================================
PAST
==================================================
`past` содержит сегменты непосредственно перед текущей основной областью.
Обычно они предоставлены только для понимания контекста.
НЕ создавай заново события из уже законченной речи в `past`.
ID из `past` разрешается включать в новый `preevent` только если:
1. предыдущая итерация оставила их незавершёнными в `context.pending`;
2. и текущие сегменты из `present` действительно завершают эту мысль.
Во всех остальных случаях `past` является read-only контекстом.
==================================================
PRESENT
==================================================
`present` — основная область текущей итерации.
Именно её необходимо обработать.
Сегменты из `present` следует:
- объединить в законченные осмысленные фрагменты;
- оставить отдельными, если объединение не требуется;
- отбросить, если это явно бессмысленный ASR-мусор;
- отложить до следующей итерации, если их смысл невозможно закончить без `future`.
==================================================
FUTURE
==================================================
`future` предоставлен ТОЛЬКО для понимания того, продолжается ли текущая мысль дальше.
КРИТИЧЕСКОЕ ПРАВИЛО:
НИКОГДА не используй ID из `future` в `preevents`.
НИКОГДА не переноси текст из `future` в `preevents`.
НИКОГДА не обрабатывай `future` как часть текущей итерации.
Ты можешь только прочитать `future`, чтобы понять, закончилась ли фраза внутри `present`.
Пример:
present:
[
{
"id": 10,
"text": "Системный анализ представляет собой"
}
]
future:
[
{
"id": 11,
"text": "совокупность методологических средств."
}
]
НЕ создавай событие:
{
"ids": [10],
"text": "Системный анализ представляет собой..."
}
И тем более НЕ создавай:
{
"ids": [10, 11],
"text": "Системный анализ представляет собой совокупность методологических средств."
}
Вместо этого отложи ID 10 через `context.pending`.
На следующей итерации продолжение станет доступно для обработки.
==================================================
КАК ОБЪЕДИНЯТЬ СЕГМЕНТЫ
==================================================
Один `preevent` должен представлять один законченный логический фрагмент речи.
Обычно следует объединять соседние сегменты, если один из них явно является продолжением другого.
Например:
[
{"id": 10, "text": "Системный анализ используется"},
{"id": 11, "text": "для исследования сложных систем."}
]
должно стать:
{
"ids": [10, 11],
"text": "Системный анализ используется для исследования сложных систем."
}
Не дроби законченную фразу без причины.
Не объединяй несколько самостоятельных законченных мыслей только потому, что они относятся к одной теме.
Сохраняй хронологический порядок.
Каждый исходный ID может входить максимум в один `preevent`.
==================================================
ИСПРАВЛЕНИЕ ТЕКСТА
==================================================
Разрешается:
- исправлять пунктуацию;
- исправлять регистр букв;
- исправлять очевидные ошибки ASR;
- восстанавливать явно неправильно распознанный термин, если правильный вариант однозначно следует из контекста;
- соединять текст нескольких сегментов в грамматически нормальную фразу.
Запрещается:
- пересказывать;
- сокращать содержательный текст;
- добавлять новые сведения;
- менять смысл;
- литературно переписывать речь;
- заменять нормальные формулировки синонимами просто ради красоты;
- делать выводы за преподавателя.
Если ты не уверен, что слово распознано неправильно — сохрани исходную формулировку.
==================================================
УДАЛЕНИЕ СЕГМЕНТОВ
==================================================
Отбрасывай сегмент только если он явно не представляет полезную речь.
Например:
- ложное срабатывание ASR;
- бессмысленный набор звуков;
- очевидный дубликат, возникший из-за ошибки распознавания;
- отдельный бессодержательный ASR-артефакт.
НЕ удаляй фразу только потому, что она короткая.
Например, такие фразы могут иметь смысл:
"Дальше."
"Запишите."
"Следующий вопрос."
"Это важно."
"Посмотрите сюда."
Если сомневаешься — сохрани.
==================================================
CONTEXT
==================================================
`context` — маленькая рабочая память между итерациями.
Используй СТРОГО следующую структуру:
{
"topic": null,
"terms": [],
"pending": null
}
Если во входном `context` отсутствуют какие-либо поля, считай их значениями по умолчанию.
`topic`:
- текущая тема лекции;
- строка максимум 120 символов;
- null, если тема неизвестна.
`terms`:
- важные термины и правильные написания, которые могут помочь исправлять ASR в следующих окнах;
- максимум 12 строк;
- каждая строка максимум 80 символов.
`pending`:
- информация о незаконченной речи на правой границе текущего `present`;
- null, если незаконченной речи нет.
Формат `pending`:
{
"ids": [42, 43],
"summary": "Началась формулировка определения системного анализа, но она продолжается в следующем окне."
}
`pending.ids` должны содержать только ID из `present`, которые НЕ были добавлены в `preevents`, потому что их продолжение находится за границей текущего окна.
`pending.summary`:
- максимум 160 символов;
- только краткое описание незавершённой мысли;
- не копируй туда весь исходный текст.
На следующей итерации предыдущий `pending` является только подсказкой.
Если он противоречит реальным ASR-сегментам, доверяй реальным сегментам.
Контекст НЕ является конспектом.
Не сохраняй в нём историю лекции.
Не копируй в него длинные куски расшифровки.
Не описывай в нём свою работу.
Не объясняй, почему ты выполнил обычные объединения.
==================================================
ФОРМАТ PREEVENT
==================================================
Каждый готовый объект:
{
"ids": [10, 11],
"text": "Законченный осмысленный фрагмент речи."
}
`ids`:
- содержит реальные ID источников;
- ID должны существовать во входных `past` или `present`;
- ID из `future` запрещены;
- один ID не может находиться в нескольких `preevents`;
- порядок ID должен соответствовать хронологическому порядку речи.
`text`:
- содержит только речь, полученную из указанных сегментов;
- может содержать исправленную пунктуацию и очевидные ASR-ошибки;
- не должен содержать комментарии о процессе обработки.
`preevents` должны идти в хронологическом порядке.
==================================================
ОТВЕТ
==================================================
Верни РОВНО один валидный JSON-объект:
{
"preevents": [
{
"ids": [10, 11],
"text": "Законченный осмысленный фрагмент речи."
}
}
],
"context": {
"topic": null,
"terms": [],
"pending": null
}
}
Никакого Markdown.
Никаких ```json.
Никакого текста перед JSON.
Никакого текста после JSON.
Никаких комментариев.
Никаких дополнительных ключей верхнего уровня.
Если готовых событий в текущем окне нет:
{
"preevents": [],
"context": {
...
}
}
— это корректный результат.
==================================================
ПРОВЕРКА ПЕРЕД ОТВЕТОМ
==================================================
Перед выдачей ответа проверь:
1. Все ID в `preevents` существуют во входе.
2. Ни одного ID из `future` нет в `preevents`.
3. Один ID не используется более одного раза.
4. Порядок `preevents` соответствует порядку речи.
5. Ты не создал незаконченный `preevent`.
6. Фрагмент, продолжающийся в `future`, оставлен через `context.pending`.
7. `pending.ids` не были одновременно использованы в `preevents`.
8. Ты не пересказал и не суммаризировал речь.
9. Ты не добавил сведений, которых нет в ASR-сегментах.
10. `context` остаётся коротким.
11. Ответ является валидным JSON.

View File

@@ -1,364 +0,0 @@
Ты обрабатываешь временную шкалу записи лекции и подготавливаешь её к дальнейшему созданию документа.
На вход ты получаешь 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": []` — это правильный и желательный результат.

View File

@@ -1,450 +0,0 @@
Ты преобразуешь подготовленный таймлайн лекции в структурированные блоки будущего 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.

View File

@@ -1,10 +1,10 @@
from dataclasses import dataclass
from typing import Literal, Any
from pydantic import BaseModel
import subprocess
@dataclass
class Event:
class Event(BaseModel):
"""Event within the timeline"""
id: int
@@ -25,8 +25,7 @@ class Event:
payload: str | None
"""Payload of the event. `None` for `voice`, image path for `vis`, OCR result for `ocr`"""
@dataclass
class Timeline:
class Timeline(BaseModel):
"""Timeline of events"""
events: list[Event]

View File

@@ -1,8 +1,7 @@
from dataclasses import dataclass
from pydantic import BaseModel
from typing import Callable, Iterable, Any
@dataclass
class Window[T]:
class Window[T](BaseModel):
"""Window for AI processing."""
past: list[T]