diff --git a/README.md b/README.md index 77fa117..5ee525c 100644 --- a/README.md +++ b/README.md @@ -417,6 +417,8 @@ Telegram ботом, которому отправили видео). 9. **Собрать финальный Markdown файл** - Для сборки Markdown файла используются элементы, полученные на предыдщем шаге, и хранимые в файле `structure_refined.json` + - Сборка выполняется локально, без использования ИИ. Ссылки на изображения + разрешаются по `image.event_id` через `events.json`. - В итоге создаётся файл `output.md`, который может включать в себя ссылки на изображения из директории `images/` (относительно директории промежуточных данных) diff --git a/main.py b/main.py index 38ae47c..19792a5 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,7 @@ from asr_filter import AsrFilter, AsrFilterResult from asr_eventizer import AsrEventizer from structure_builder import Structure, StructureBuilder from structure_refiner import StructureRefiner +from markdown_builder import MarkdownBuilder from windowizer import Windowizer from agent import Agent @@ -272,10 +273,23 @@ def on_structure_refiner(current_step: Step, input_data: dict | None) -> tuple[S result = refiner.refine(Structure(**input_data)) return (Step.MARKDOWN_BUILDER, result.model_dump(mode="json")) +def on_markdown_builder(current_step: Step, input_data: dict | None) -> tuple[Step | None, str | None]: + if os.path.isfile(WORKFLOW_DATA[current_step][0]): + logging.info("Skipping Markdown builder") + return (None, None) + if input_data is None: + logging.error("Can't build Markdown without input_data") + return (None, None) + with open("events.json", "rb") as f: + timeline = Timeline(**json.load(f)) + logging.info("Building Markdown...") + builder = MarkdownBuilder(timeline) + return (None, builder.build(Structure(**input_data))) + # # Main # -WORKFLOW_DATA: dict[Step, tuple[str, Callable[[Step, dict | None], tuple[Step | None, dict | None]] | None]] = { +WORKFLOW_DATA: dict[Step, tuple[str, Callable[[Step, dict | None], tuple[Step | None, dict | str | None]] | None]] = { 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), @@ -284,7 +298,7 @@ WORKFLOW_DATA: dict[Step, tuple[str, Callable[[Step, dict | None], tuple[Step | Step.REFERENCE_RESOLVER: ("events.json", on_reference_resolver), Step.STRUCTURE_BUILDER: ("structure.json", on_structure_builder), Step.STRUCTURE_REFINER: ("structure_refined.json", on_structure_refiner), - Step.MARKDOWN_BUILDER: ("output.md", None) + Step.MARKDOWN_BUILDER: ("output.md", on_markdown_builder) } """Information about workflow. @@ -314,9 +328,12 @@ def main() -> None: logging.info(f"Executing step {step_to_do}") step_to_do, ret = func(step_to_do, intermediate_result) if ret is not None: - intermediate_result = ret - with open(output_file_path, "w") as f: - json.dump(ret, f, ensure_ascii=False, indent=4) + with open(output_file_path, "w", encoding="utf-8") as f: + if isinstance(ret, str): + f.write(ret) + else: + intermediate_result = ret + json.dump(ret, f, ensure_ascii=False, indent=4) logging.info(f"Intermediate results are saved {output_file_path}") # final report logging.info(f"Workflow was interrupted at step {step_to_do}") diff --git a/markdown_builder.py b/markdown_builder.py new file mode 100644 index 0000000..765d2ea --- /dev/null +++ b/markdown_builder.py @@ -0,0 +1,59 @@ +from structure_builder import ( + DefinitionElement, + HeadingElement, + ImageElement, + ImportantElement, + OrderedListElement, + ParagraphElement, + Structure, + StructureElement, + UnorderedListElement, +) +from utils import Event, Timeline + + +class MarkdownBuilder: + """Render a document structure to Markdown without using AI.""" + + def __init__(self, timeline: Timeline) -> None: + self._events: dict[int, Event] = {} + for event in timeline.events: + if event.id in self._events: + raise ValueError(f"Duplicate event ID: {event.id}") + self._events[event.id] = event + + def _render_image(self, element: ImageElement) -> str: + event = self._events.get(element.event_id) + if event is None or event.type != "vis" or not event.payload: + raise ValueError( + f"Image element references invalid visual event: {element.event_id}" + ) + alt = event.text.replace("[", "").replace("]", "").replace("\n", " ") + return f"![{alt}](<{event.payload}>)" + + def _render_element(self, element: StructureElement) -> str: + if isinstance(element, HeadingElement): + return f"{'#' * element.level} {element.text}" + if isinstance(element, ParagraphElement): + return element.text + if isinstance(element, UnorderedListElement): + return "\n".join(f"- {item}" for item in element.items) + if isinstance(element, OrderedListElement): + return "\n".join( + f"{index}. {item}" + for index, item in enumerate(element.items, start=1) + ) + if isinstance(element, DefinitionElement): + return f"**{element.term}.** {element.text}" + if isinstance(element, ImportantElement): + text = f"**Важно:** {element.text}" + return "\n".join(f"> {line}" for line in text.splitlines()) + if isinstance(element, ImageElement): + return self._render_image(element) + raise TypeError(f"Unsupported structure element: {type(element).__name__}") + + def build(self, structure: Structure) -> str: + blocks = [self._render_element(element) for element in structure.elements] + if not blocks: + return "" + return "\n\n".join(blocks) + "\n"