diff --git a/.gitignore b/.gitignore index 80f5996..34450c9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__/ *.log .agents/ .codex/ +.codex_qa/ .venv/ venv/ output/ diff --git a/nodes.py b/nodes.py index f7e95fe..8704c88 100644 --- a/nodes.py +++ b/nodes.py @@ -125,11 +125,12 @@ MODEL_TYPE = "DG_MODEL_CONFIG" MEDIA_TYPE = "DG_MEDIA_CONTEXT" CONTEXT_TYPE = "DG_CONTEXT" TARGET_PROFILE_TYPE = "DG_TARGET_PROFILE_CONFIG" +H3_REFERENCE_POLICY_TYPE = "DG_H3_REFERENCE_POLICY_CONFIG" GROUNDING_GUARD_TYPE = "DG_GROUNDING_GUARD_CONFIG" GROUNDING_TELEMETRY_SCHEMA_VERSION = "dg-denoising-telemetry/1" DIRECTOR_RUNTIME_SCHEMA_VERSION = "dg-director-runtime/1" DIRECTOR_CACHE_MODE_CHOICES = ["reuse", "refresh", "off"] -DIRECTOR_EXECUTION_CONTRACT_REVISION = "branch-stable-native-reuse-v16" +DIRECTOR_EXECUTION_CONTRACT_REVISION = "h3-reference-policy-v18" DEFAULT_GGUF_MODEL_PATH = r"Z:\models\LLM\diffusiongemma-26B-A4B-it-GGUF\diffusiongemma-26B-A4B-it-Q4_K_M.gguf" DEFAULT_GGUF_CLI_PATH = r"C:\tools\llama.cpp-diffusiongemma\build-clang-cuda\bin\llama-diffusion-cli.exe" @@ -179,6 +180,10 @@ DEFAULT_MASTER_PROMPT = """You are a local ComfyUI prompt architect. Treat the u Use the user's text, any backend-consumable pixels, and any supplied visual_description to produce final, practical prompts for generation models. If the active backend is metadata-only for image/video, do not pretend you saw pixels; use only the user's text, the visual_description, and factual media metadata. Preserve requested subjects, exact text, composition, motion, style, camera behavior, visual capture language, audio mode, audio guidance, and constraints. In image_identity_video_control workflows, treat a request to replace the video subject with the image subject as a strict identity-and-appearance transfer: face, hair, body type, wardrobe, accessories, styling, and distinguishing traits come from the reference image, while the video contributes pose, action, blocking, depth/canny/edge structure, camera choreography, timing, composition, and scene geometry. Do not copy the control-video subject's wardrobe, hair, face, body type, accessories, or styling into the final prompt unless the user explicitly asks for those video-subject traits. For LTX source-video work, explicitly observe camera choreography before writing the final prompt: first-frame and ending framing, shot size, camera height/angle, camera support or stability, camera path, zoom/dolly/truck/boom/pan/tilt/roll/orbit/rotation, subject-camera relationship, focus behavior, depth/parallax, and visible lens/capture changes. Transcribe observed camera work precisely instead of reducing it to generic phrases like camera movement, zoom out, or pan left. Visual capture language includes exposure level, overexposed or blown highlights, underexposure, flash/direct light, bloom, halation, lens blur, bokeh, depth of field, focal length impression, perspective compression, lens distortion, motion blur, grain/noise, white balance, contrast, dynamic range, and camera angle. Do not describe invisible intent. Faithful mode preserves explicitly requested camera energy. For LTX targets only, that preservation is bounded by the LTX Target Profile's Camera Capability: Stable / base model simplifies unsupported risky camera movement, while Advanced / controlled camera preserves the prior fast or compound choreography policy. Other targets do not inherit this LTX-only capability gate and must follow their own model-specific camera contract. Every prompt field must contain the finished prompt text or finished prompt JSON, never instructions about how to write a prompt. Return only valid JSON with ltx_prompt, ideogram_prompt, minimax_h3_prompt, negative_prompt, scene_segments, and metadata.""" +MINIMAX_H3_MASTER_PROMPT = """You are a local ComfyUI MiniMax H3 prompt architect. Treat the user's text as a content brief: it only needs to say what the video should be about. Infer useful generator-ready subject, environment, lighting, camera, motion, pacing, composition, style, and sound detail when it is missing while preserving every explicit user constraint. Preserve an explicitly requested or evidenced static hold. A conditioned still frame fixes its camera geometry at that instant; it does not by itself prove that the camera remains locked afterward. + +Use the user's text, any backend-consumable pixels, and any supplied visual_description to produce one final, practical MiniMax H3 prompt. If the active backend is metadata-only, do not pretend you saw pixels; use only the user's text, the visual_description, and factual media metadata. Preserve requested subjects, exact text, composition, motion, style, camera behavior, visual capture language, audio guidance, and constraints. Visual capture language includes exposure level, blown highlights, underexposure, flash or direct light, bloom, halation, lens blur, bokeh, depth of field, focal length impression, perspective compression, lens distortion, motion blur, grain or noise, white balance, contrast, dynamic range, and camera angle. Do not describe invisible intent. The finished prompt must never contain instructions about how to write a prompt. Return only the JSON object requested by the model-facing contract below.""" + @dataclass class RuntimeConfig: @@ -236,6 +241,15 @@ class TargetProfileConfig: ltx_camera_capability: str = "stable" +@dataclass +class H3ReferencePolicyConfig: + """Host-owned rules for turning connected Ref2VA media into a manifest.""" + + layout: str = "auto" + custom_manifest: str = "" + expected_subject_count: int = 0 + + @dataclass class BackendRunOptions: """Per-call controls used only by the Grounding Guard path. @@ -1423,7 +1437,11 @@ def _normalize_minimax_h3_prompt(text: str, minimax_h3_mode: str = "t2va") -> st # required heading. The line-anchored alias is unambiguous and only # changes the field label; its generated music guidance is preserved. music_alias_pattern = re.compile(r"(?im)^\s*on[ _-]*diegetic[ _-]*music\s*:") + canonical_music_pattern = re.compile( + r"(?im)^\s*non[ _-]*diegetic[ _-]*music\s*:" + ) music_alias_matches = list(music_alias_pattern.finditer(prompt)) + canonical_music_matches = list(canonical_music_pattern.finditer(prompt)) preceding_heading_positions: list[int] = [] preceding_headings_are_canonical = True for field_name in _MINIMAX_H3_REF_FIELDS[:-1]: @@ -1432,14 +1450,53 @@ def _normalize_minimax_h3_prompt(text: str, minimax_h3_mode: str = "t2va") -> st preceding_headings_are_canonical = False break preceding_heading_positions.append(field_matches[0].start()) - if ( + preceding_structure_is_canonical = bool( preceding_headings_are_canonical and preceding_heading_positions == sorted(preceding_heading_positions) + ) + # A common constrained-decoding miss is a duplicated, empty final music + # heading. The first section is complete, so dropping only the empty EOF + # duplicate is lossless and avoids sending the same packet through two + # expensive model repair attempts. + if ( + preceding_structure_is_canonical + and len(canonical_music_matches) == 2 + and canonical_music_matches[0].start() > preceding_heading_positions[-1] + and canonical_music_matches[1].start() > canonical_music_matches[0].start() + and prompt[ + canonical_music_matches[0].end() : canonical_music_matches[1].start() + ].strip() + and not prompt[canonical_music_matches[1].end() :].strip() + ): + prompt = prompt[: canonical_music_matches[1].start()].rstrip() + canonical_music_matches = list(canonical_music_pattern.finditer(prompt)) + + alias_has_nonempty_body = bool( + len(music_alias_matches) == 1 + and ( + prompt[music_alias_matches[0].end() :].strip() + if not canonical_music_matches + else music_alias_matches[0].start() < canonical_music_matches[0].start() + and prompt[ + music_alias_matches[0].end() : canonical_music_matches[0].start() + ].strip() + ) + ) + empty_canonical_footer_after_alias = bool( + len(music_alias_matches) == 1 + and len(canonical_music_matches) == 1 + and music_alias_matches[0].start() < canonical_music_matches[0].start() + and not prompt[canonical_music_matches[0].end() :].strip() + ) + if ( + preceding_structure_is_canonical and len(music_alias_matches) == 1 and music_alias_matches[0].start() > preceding_heading_positions[-1] - and prompt[music_alias_matches[0].end() :].strip() - and not re.search(r"(?im)^\s*non[ _-]*diegetic[ _-]*music\s*:", prompt) + and alias_has_nonempty_body + and (not canonical_music_matches or empty_canonical_footer_after_alias) ): + if empty_canonical_footer_after_alias: + prompt = prompt[: canonical_music_matches[0].start()].rstrip() prompt = music_alias_pattern.sub("non_diegetic_music:", prompt, count=1) prompt = re.sub( r"<\s*(subject|picture|image|video|audio)\s+(\d+)\s*>", @@ -1718,6 +1775,72 @@ def _repair_minimax_h3_ref_cut_timestamps( return repaired +def _repair_minimax_h3_timed_cut_openers(timeline: str) -> str: + """Canonicalize only timestamp-adjacent H3 cut transport syntax. + + The model-authored shot body remains byte-for-byte intact. When a later + shot starts directly with action or camera motion, a neutral cut prefix is + inserted so native H3 receives the delimiter its parser requires. + """ + + repaired = str(timeline or "") + # The live T2VA failure omitted the comma only before an otherwise + # unmistakable smash-cut opener (``At 00:06.000 the smash cuts to``). + # Restore punctuation only for that exact transition family; ordinary + # timestamp-adjacent prose remains untouched and will still fail closed. + repaired = re.sub( + r"(\[Shot (?:[2-9]|[1-9]\d+)\]\s+At\s+\d{2,}:\d{2}\.\d{3})(?!,)" + r"(?=\s+(?:the\s+)?(?:a\s+)?smash(?:[- ]+cut|[- ]?cuts?)\s+to\b)", + r"\1,", + repaired, + flags=re.IGNORECASE, + ) + timestamp_prefix = ( + r"(\[Shot (?:[2-9]|[1-9]\d+)\]\s+At\s+\d{2,}:\d{2}\.\d{3},\s*)" + ) + repaired = re.sub( + timestamp_prefix + + r"the\s+camera\s+smash(?:[- ]+cut|[- ]?cuts?)\s+to\s*:?\s*", + r"\1the camera hard cuts to ", + repaired, + flags=re.IGNORECASE, + ) + repaired = re.sub( + timestamp_prefix + + r"(?:the\s+)?(?:a\s+)?smash(?:[- ]+cut|[- ]?cuts?)\s+to\s*:?\s*", + r"\1the camera hard cuts to ", + repaired, + flags=re.IGNORECASE, + ) + repaired = re.sub( + timestamp_prefix + + r"the\s+camera\s+(?:quick|clean)\s+cuts?\s+to\s+", + r"\1the camera cuts to ", + repaired, + flags=re.IGNORECASE, + ) + repaired = re.sub( + timestamp_prefix + r"(?:a\s+)?hard\s+cut\s+to\s*:?\s*", + r"\1the camera hard cuts to ", + repaired, + flags=re.IGNORECASE, + ) + repaired = re.sub( + timestamp_prefix + r"(?:a\s+)?(?:quick|clean)?\s*cut\s+to\s*:?\s*", + r"\1the camera cuts to ", + repaired, + flags=re.IGNORECASE, + ) + return re.sub( + r"(\[Shot (?:[2-9]|[1-9]\d+)\]\s+At\s+\d{2,}:\d{2}\.\d{3},)" + r"(?!\s*the\s+(?:camera|shot|scene)\s+(?:(?:hard\s+)?cuts|transitions|changes|switches|cross-dissolves|fades|wipes)\s+(?:to|into)\b)" + r"(\s*)", + r"\1\2the shot cuts to: ", + repaired, + flags=re.IGNORECASE, + ) + + def _repair_minimax_h3_ref_detailed_timeline(detailed: str, duration_seconds: float = 0.0) -> str: range_pattern = re.compile( r"\[Shot (?P\d+)\]\s*\(?\s*" @@ -1858,25 +1981,7 @@ def _repair_minimax_h3_ref_detailed_timeline(detailed: str, duration_seconds: fl detailed, flags=re.IGNORECASE, ) - detailed = re.sub( - r"(\[Shot (?:[2-9]|[1-9]\d+)\]\s+At\s+\d{2,}:\d{2}\.\d{3},\s*)the\s+camera\s+(?:quick|clean)\s+cuts?\s+to\s+", - r"\1the camera cuts to ", - detailed, - flags=re.IGNORECASE, - ) - detailed = re.sub( - r"(\[Shot (?:[2-9]|[1-9]\d+)\]\s+At\s+\d{2,}:\d{2}\.\d{3},\s*)(?:a\s+)?(?:quick|hard|clean)?\s*cut\s+to\s+", - r"\1the camera cuts to ", - detailed, - flags=re.IGNORECASE, - ) - detailed = re.sub( - r"(\[Shot (?:[2-9]|[1-9]\d+)\]\s+At\s+\d{2,}:\d{2}\.\d{3},)(?!\s*the\s+(?:camera|shot|scene)\s+(?:(?:hard\s+)?cuts|transitions|changes|switches|cross-dissolves|fades|wipes)\s+(?:to|into)\b)(\s*)", - r"\1\2the shot cuts to: ", - detailed, - flags=re.IGNORECASE, - ) - return detailed + return _repair_minimax_h3_timed_cut_openers(detailed) def _repair_minimax_h3_ref_subject_timeline_labels(sections: dict[str, str]) -> None: @@ -1887,14 +1992,30 @@ def _repair_minimax_h3_ref_subject_timeline_labels(sections: dict[str, str]) -> retention = sections["retention_analysis"] timeline = detailed[shot1_match.start() :] + missing_subjects = [ + (tag, description) + for tag, description in _minimax_h3_ref_definition_items( + sections["subject_definitions"] + ) + if tag.startswith("\S.*)$", + retention, + ) + if retention_match is None: + return + subject_semantics = f"{description} {retention_match.group('body')}" if not re.search( r"\b(?:environment|landscape|setting|world|location|background|scenery|production design|visual style|palette|" - r"lighting|town|city|street|square|field|forest|room|interior|exterior)\b", - description, + r"lighting|town|city|street|square|field|forest|room|interior|exterior)\b|" + r"^\s*(?:the\s+|an?\s+)?(?:void\s+of\s+space|(?:outer|deep)\s+space|" + r"(?:curved\s+)?horizon\s+of\s+(?:the\s+)?earth|earth(?:['’]s)?\s+(?:curved\s+)?(?:horizon|curve)|" + r"(?:orbital|planetary)\s+(?:space|vista|scene)|starfield|starscape|night\s+sky)\b", + subject_semantics, flags=re.IGNORECASE, ) or re.search( r"\b(?:person|character|courier|protagonist|antagonist|creature|performer|actor|man|woman|boy|girl|animal|monster)\b", @@ -1903,14 +2024,40 @@ def _repair_minimax_h3_ref_subject_timeline_labels(sections: dict[str, str]) -> ): return source_tags = _minimax_h3_reference_tags(description) - if tag not in retention or len(source_tags) != 1 or source_tags[0] not in timeline: - return - repairs.append((tag, source_tags[0])) + if len(source_tags) == 1 and source_tags[0] in timeline: + repairs.append((tag, source_tags[0])) + continue + if ( + len(missing_subjects) == 1 + and not source_tags + and re.search( + r"\b(?:environment|landscape|setting|world|location|background|scenery|town|city|street|square|field|" + r"forest|room|interior|exterior)\b|" + r"^\s*(?:the\s+|an?\s+)?(?:void\s+of\s+space|(?:outer|deep)\s+space|" + r"(?:curved\s+)?horizon\s+of\s+(?:the\s+)?earth|earth(?:['’]s)?\s+(?:curved\s+)?(?:horizon|curve)|" + r"(?:orbital|planetary)\s+(?:space|vista|scene)|starfield|starscape|night\s+sky)\b", + subject_semantics, + flags=re.IGNORECASE, + ) + ): + implicit_environment_tag = tag + continue + return - if not repairs or len({source_tag for _tag, source_tag in repairs}) != len(repairs): + if repairs and len({source_tag for _tag, source_tag in repairs}) != len(repairs): return for tag, source_tag in repairs: timeline = timeline.replace(source_tag, f"{tag} (derived from {source_tag})", 1) + if implicit_environment_tag: + relative_end = shot1_match.end() - shot1_match.start() + suffix = timeline[relative_end:] + separator = "" if suffix.startswith((" ", "\n")) else " " + timeline = ( + f"{timeline[:relative_end]} The setting is {implicit_environment_tag} as defined above." + f"{separator}{suffix}" + ) + if not repairs and not implicit_environment_tag: + return sections["detailed_description"] = f"{detailed[: shot1_match.start()]}{timeline}" @@ -2776,6 +2923,73 @@ def _deterministically_repaired_prompt_packet_json( return None, [], False +def _repair_single_h3_dialogue_closer_json_escape( + raw_output: str, + minimax_h3_mode: str, +) -> tuple[dict[str, Any] | None, list[str]]: + """Remove only an invalid JSON backslash before a native ```` tag. + + The recovery is limited to the model-facing one-key H3 envelope. The + resulting text must parse as strict JSON with no duplicate or extra packet + keys, and its value must already contain a complete canonical H3 prompt. + No prompt prose or dialogue delimiter is synthesized. + """ + + text = str(raw_output or "").strip() + if not text: + return None, [] + payload, had_boundary = _extract_delimited_final_payload(text) + if had_boundary: + text = payload.strip() + if not re.match( + r'\A\s*\{\s*"minimax_h3_prompt"\s*:', + text, + flags=re.DOTALL, + ): + return None, [] + packet_key_pattern = re.compile( + r'"(?:ltx_prompt|ideogram_prompt|minimax_h3_prompt|negative_prompt|scene_segments|metadata|grounding_ledger)"\s*:', + flags=re.IGNORECASE, + ) + packet_keys = packet_key_pattern.findall(text) + if ( + len(packet_keys) != 1 + or not re.fullmatch( + r'"minimax_h3_prompt"\s*:', + packet_keys[0], + flags=re.IGNORECASE, + ) + ): + return None, [] + repaired_text, repair_count = re.subn( + r"\\(?=)", + "", + text, + flags=re.IGNORECASE, + ) + if repair_count <= 0: + return None, [] + try: + parsed = _strict_prompt_packet_json(repaired_text) + except _DuplicatePromptPacketKey: + return None, [] + if ( + parsed is None + or set(parsed) != {"minimax_h3_prompt"} + or not isinstance(parsed.get("minimax_h3_prompt"), str) + or not _extract_complete_minimax_h3_plain_prompt( + parsed["minimax_h3_prompt"], + minimax_h3_mode, + ) + ): + return None, [] + repairs = [] + if had_boundary: + repairs.append("removed_final_answer_wrapper") + repairs.append(f"removed_invalid_h3_dialogue_closer_json_escape:{repair_count}") + return parsed, repairs + + def _single_paragraph(text: str) -> str: return _strip_generation_artifacts(re.sub(r"\s+", " ", (text or "").replace("\n", " ")).strip()) @@ -3368,6 +3582,30 @@ def _has_negated_prompt_phrase(text: str, pattern: str) -> bool: return False +def _minimax_h3_battle_intent_requested(text: str) -> bool: + """Return true for combat intent, not compound character/style labels.""" + + source = str(text or "") + scrubbed = list(source) + for match in re.finditer(r"\b(?:battle|fight|duel|combat|clash)\b", source, flags=re.IGNORECASE): + before = source[match.start() - 1] if match.start() else "" + after = source[match.end()] if match.end() < len(source) else "" + descriptor_role = bool( + re.match( + r"\s+(?:cyborg|robot|android|automaton|armor|armour|suit|gear|form|mode|" + r"costume|outfit|design|machine|vehicle|bear)\b", + source[match.end() :], + flags=re.IGNORECASE, + ) + ) + if before in "-_/" or after in "-_/" or descriptor_role: + scrubbed[match.start() : match.end()] = " " * (match.end() - match.start()) + return _has_unnegated_prompt_phrase( + "".join(scrubbed), + r"\b(?:battle|fight|duel|combat|clash)\b", + ) + + _MINIMAX_H3_LIVE_ACTION_PATTERN = r"\b(?:live[- ]action|practical\s+(?:film|photography)|photoreal(?:istic|ism)?|realistic\s+film\s+photography)\b" _MINIMAX_H3_STOP_MOTION_PATTERN = r"\b(?:stop[- ]motion|claymation|clay[- ]animation|puppet[- ]animation)\b" _MINIMAX_H3_3D_ANIMATION_PATTERN = r"\b(?:3[ -]?d(?:\s+(?:cg|cgi|animation|animated|render(?:ed|ing)?))?|cgi?(?:[- ](?:animated|animation|render(?:ed|ing)?))?|computer[- ](?:animated|generated)|cg[- ]animation)\b" @@ -3888,6 +4126,16 @@ _MINIMAX_H3_DIALOGUE_TAGLIKE_PATTERN = re.compile( flags=re.IGNORECASE, ) +_MINIMAX_H3_DIALOGUE_LANG_ATTRIBUTE_OPEN_PATTERN = re.compile( + r"<\s*d\s+lang\s*=\s*(?P['\"])(?P[A-Za-z][A-Za-z -]{0,31})(?P=quote)\s*>", + flags=re.IGNORECASE, +) + +_MINIMAX_H3_DIALOGUE_DUPLICATE_PREFIX_CLOSE_PATTERN = re.compile( + r"<\s*/\s*<\s*/\s*d\s*>", + flags=re.IGNORECASE, +) + def _minimax_h3_timeline_bounds( prompt_text: Any, @@ -3908,6 +4156,197 @@ def _minimax_h3_timeline_bounds( return opening.end(), opening.end() + closing.start() +def _repair_minimax_h3_t2va_contract_transport( + prompt_text: Any, + minimax_h3_mode: Any, +) -> tuple[str, list[str]]: + """Repair deterministic T2VA syntax without rewriting creative content. + + Repairs are deliberately limited to the integrated shot timeline: native + cut delimiters, the common ``English>`` bracket loss, and unambiguous + speaker placeholders such as ``(Sx: Girl)``. Ambiguous speaker mappings + are left untouched for the validator (or a permissive generation gate). + """ + + prompt = str(prompt_text or "") + if _normalize_minimax_h3_mode(minimax_h3_mode) != "t2va": + return prompt, [] + bounds = _minimax_h3_timeline_bounds(prompt, "t2va") + if bounds is None: + return prompt, [] + timeline_start, timeline_end = bounds + original_timeline = prompt[timeline_start:timeline_end] + timeline = re.sub( + r"(\[Shot (?:[2-9]|[1-9]\d+)\]\s+At\s+)([0-5]?\d):(\d{3})(?=\s*,)", + lambda match: f"{match.group(1)}00:{int(match.group(2)):02d}.{match.group(3)}", + original_timeline, + flags=re.IGNORECASE, + ) + repairs: list[str] = [] + if timeline != original_timeline: + repairs.append("canonicalized_timed_cut_timestamps") + timeline_before_cut_openers = timeline + timeline = _repair_minimax_h3_timed_cut_openers(timeline) + if timeline != timeline_before_cut_openers: + repairs.append("canonicalized_timed_cut_openers") + + # DiffusionGemma occasionally drops only the square brackets around the + # language label. Repair all such blocks transactionally; nested, + # unbalanced, empty, or cross-shot markup is not guessed at. + literal_tags = list(re.finditer(r"", timeline, flags=re.IGNORECASE)) + language_replacements: list[tuple[int, int, str]] = [] + language_structure_valid = True + open_tag: re.Match[str] | None = None + for tag in literal_tags: + closing = bool(re.fullmatch(r"", tag.group(0), flags=re.IGNORECASE)) + if not closing: + if open_tag is not None: + language_structure_valid = False + break + open_tag = tag + continue + if open_tag is None: + language_structure_valid = False + break + between = timeline[open_tag.end() : tag.start()] + if "[Shot " in between or not between.strip(): + language_structure_valid = False + break + if not re.match(r"[ \t]*\[[^\]\n]+\]\s*\S", between): + malformed = re.match( + r"(?P[ \t]*)(?P[A-Za-z][A-Za-z -]{0,31})>(?=\s*\S)", + between, + ) + if malformed is None: + language_structure_valid = False + break + language_replacements.append( + ( + open_tag.end(), + open_tag.end() + malformed.end(), + f"{malformed.group('leading')}[{malformed.group('language')}]", + ) + ) + open_tag = None + if open_tag is not None: + language_structure_valid = False + if language_structure_valid and language_replacements: + for start, end, replacement in sorted(language_replacements, reverse=True): + timeline = timeline[:start] + replacement + timeline[end:] + repairs.append("restored_dialogue_language_brackets") + + # Resolve only cues that are actually associated with a canonical dialogue + # block. Stable labels receive stable numeric IDs; existing IDs are + # reserved. A transaction with more than one valid interpretation is left + # wholly unchanged. + dialogue_blocks = list( + re.finditer( + r"\[[^\]\n]+\]\s*\S(?:(?!|\[Shot \d+\]).)*?", + timeline, + flags=re.IGNORECASE | re.DOTALL, + ) + ) + speaker_pattern = re.compile( + r"\(\s*S(?P[1-9]\d*|x)" + r"(?:\s*[-:]\s*(?P