diff --git a/prompts/video_references.md b/prompts/video_references.md index 7ab04ac..79742dc 100644 --- a/prompts/video_references.md +++ b/prompts/video_references.md @@ -39,6 +39,8 @@ `ids` должны содержать только ID событий из `present`, во время которых нужно искать содержимое. ID указывай в хронологическом порядке и без повторов. +Если нужный фрагмент пересекает границу окна, укажи только его ID из `present`; +если таких ID нет, не создавай ссылку. `text` — короткая конкретная инструкция следующему этапу: diff --git a/structure_builder.py b/structure_builder.py index d0b68ff..b1ef01e 100644 --- a/structure_builder.py +++ b/structure_builder.py @@ -1,4 +1,5 @@ import json +import logging import traceback from typing import Annotated, Literal @@ -180,6 +181,49 @@ class StructureBuilder: "pending.event_ids must reference events from the current window" ) + @staticmethod + def _keep_window_references( + result: _BuildResult, + window: Window[Event], + ) -> _BuildResult: + """Drop impossible image references and repair technical pending IDs.""" + visual_ids = { + event.id + for event in window.past + window.present + if event.type == "vis" + } + elements: list[StructureElement] = [] + for element in result.elements: + if isinstance(element, ImageElement) and element.event_id not in visual_ids: + logging.warning("Ignoring image outside the current window: id=%s", element.event_id) + continue + elements.append(element) + + context = result.context + if context.pending is not None: + window_ids = { + event.id + for event in window.past + window.present + window.future + } + pending_ids = [ + event_id + for event_id in context.pending.event_ids + if event_id in window_ids + ] + if pending_ids != context.pending.event_ids: + logging.warning( + "Trimming pending IDs to the current window: %s -> %s", + context.pending.event_ids, + pending_ids, + ) + pending = ( + context.pending.model_copy(update={"event_ids": pending_ids}) + if pending_ids else None + ) + context = context.model_copy(update={"pending": pending}) + + return result.model_copy(update={"elements": elements, "context": context}) + def _build_window(self, window: Window[Event]) -> _BuildResult: """Build document elements from a single timeline window.""" messages = [ @@ -211,6 +255,7 @@ class StructureBuilder: try: result = _BuildResult.model_validate_json(response) + result = self._keep_window_references(result, window) self._validate_result(result, window) return result except (ValidationError, ValueError, TypeError): diff --git a/structure_refiner.py b/structure_refiner.py index d57d3bf..0bca185 100644 --- a/structure_refiner.py +++ b/structure_refiner.py @@ -1,4 +1,5 @@ import json +import logging import traceback from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator @@ -58,6 +59,25 @@ class StructureRefiner: if isinstance(element, ImageElement) and element.event_id not in input_image_ids: raise ValueError("image.event_id must come from present") + @staticmethod + def _keep_present_images( + result: _RefineResult, + window: Window[StructureElement], + ) -> _RefineResult: + """Discard image elements invented from context windows.""" + input_image_ids = { + element.event_id + for element in window.present + if isinstance(element, ImageElement) + } + elements: list[StructureElement] = [] + for element in result.elements: + if isinstance(element, ImageElement) and element.event_id not in input_image_ids: + logging.warning("Ignoring image outside present window: id=%s", element.event_id) + continue + elements.append(element) + return result.model_copy(update={"elements": elements}) + def _refine_window(self, window: Window[StructureElement]) -> _RefineResult: messages = [ self._system_prompt, @@ -91,6 +111,7 @@ class StructureRefiner: try: result = _RefineResult.model_validate_json(response) + result = self._keep_present_images(result, window) self._validate_result(result, window) return result except (ValidationError, ValueError, TypeError): diff --git a/video_references.py b/video_references.py index 38df2fd..f54b8c0 100644 --- a/video_references.py +++ b/video_references.py @@ -1,4 +1,5 @@ import json +import logging import traceback from typing import Literal @@ -99,6 +100,47 @@ class VideoReferenceBuilder: raise ValueError("Duplicate reference in a single window") seen.add(key) + @staticmethod + def _keep_present_references( + result: _WindowResult, + window: Window[VideoReferenceEvent], + ) -> _WindowResult: + """Keep the usable part of references that cross a window boundary.""" + present_positions = { + event.id: position + for position, event in enumerate(window.present) + } + references: list[UnresolvedReference] = [] + seen: set[tuple[tuple[int, ...], str]] = set() + + for reference in result.unresolved: + ids = sorted( + {event_id for event_id in reference.ids if event_id in present_positions}, + key=present_positions.__getitem__, + ) + if not ids: + logging.warning( + "Ignoring reference outside present window: ids=%s", + reference.ids, + ) + continue + if ids != reference.ids: + logging.warning( + "Trimming reference to present window: ids=%s -> %s", + reference.ids, + ids, + ) + + normalized = reference.model_copy(update={"ids": ids}) + key = (tuple(ids), normalized.type) + if key in seen: + logging.warning("Ignoring duplicate reference: ids=%s", ids) + continue + seen.add(key) + references.append(normalized) + + return result.model_copy(update={"unresolved": references}) + def _build_window( self, window: Window[VideoReferenceEvent], @@ -135,6 +177,7 @@ class VideoReferenceBuilder: try: result = _WindowResult.model_validate_json(response) + result = self._keep_present_references(result, window) self._validate_result(result, window) return result except (ValidationError, ValueError, TypeError):