@oh-story/dsh 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/lib/client.js +35 -14
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +165 -69
- package/lib/drama/skills/short-drama/SKILL.md +5 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
- package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
- package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
- package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
- package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
- package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
- package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
- package/lib/index.js +4 -2
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/manifest.json +28 -28
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
- package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
- package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
- package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
- package/lib/oh-story/manifest.json +292 -172
- package/lib/oh-story/roles/character-designer.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +56 -242
- package/lib/oh-story/roles/story-architect.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +1 -1
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-import/SKILL.md +2 -2
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
- package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-review/SKILL.md +13 -3
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
- package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +17 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +31 -1
- package/lib/video-recap/manifest.json +162 -162
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
- package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
- package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
- package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
- package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
- package/package.json +41 -41
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
|
-
import math
|
|
6
5
|
import os
|
|
7
6
|
import shlex
|
|
8
7
|
import sys
|
|
9
8
|
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from lib import load_json
|
|
10
11
|
from recap_runtime import (
|
|
11
12
|
_coerce_videos,
|
|
12
13
|
_entry,
|
|
13
|
-
_load_json,
|
|
14
14
|
_load_run_manifest,
|
|
15
15
|
_multi_run_manifest_payload,
|
|
16
16
|
_run_manifest_payload,
|
|
@@ -44,87 +44,43 @@ _ALLOWED_VISUAL_OVERLAY_TYPES = {"top_title", "inline_label_or_callout"}
|
|
|
44
44
|
_VISUAL_OVERLAYS = "visual_overlays.json"
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
number = float(value)
|
|
50
|
-
except (TypeError, ValueError):
|
|
51
|
-
return None
|
|
52
|
-
return number if math.isfinite(number) else None
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _iter_narration_segments(narration_data):
|
|
56
|
-
if isinstance(narration_data, list):
|
|
57
|
-
yield from (item for item in narration_data if isinstance(item, dict))
|
|
58
|
-
return
|
|
59
|
-
if not isinstance(narration_data, dict):
|
|
60
|
-
return
|
|
61
|
-
for key in ("segments", "narration", "items"):
|
|
62
|
-
value = narration_data.get(key)
|
|
63
|
-
if isinstance(value, list):
|
|
64
|
-
yield from (item for item in value if isinstance(item, dict))
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _canonical_visual_overlay(overlay, segment=None):
|
|
69
|
-
"""Return the first-release assemble overlay contract, or None.
|
|
47
|
+
def _canonical_visual_overlay(overlay, segment):
|
|
48
|
+
"""Return the first-release assemble overlay contract, or None for unsupported types.
|
|
70
49
|
|
|
71
|
-
Recap owns the handoff artifact but does not invent richer overlay semantics
|
|
72
|
-
two overlay types implemented by assemble
|
|
73
|
-
|
|
50
|
+
Recap owns the handoff artifact but does not invent richer overlay semantics: only the
|
|
51
|
+
two overlay types implemented by assemble pass through, an overlay without its own
|
|
52
|
+
start/end inherits the narration segment's, and renderer placement hints are preserved.
|
|
74
53
|
"""
|
|
75
|
-
if not
|
|
76
|
-
return None
|
|
77
|
-
typ = overlay.get("type")
|
|
78
|
-
if typ not in _ALLOWED_VISUAL_OVERLAY_TYPES:
|
|
79
|
-
return None
|
|
80
|
-
text = overlay.get("text")
|
|
81
|
-
if text is None or str(text) == "":
|
|
82
|
-
return None
|
|
83
|
-
seg = segment if isinstance(segment, dict) else {}
|
|
84
|
-
start = _finite_number(overlay.get("start"))
|
|
85
|
-
end = _finite_number(overlay.get("end"))
|
|
86
|
-
if start is None:
|
|
87
|
-
start = _finite_number(seg.get("start"))
|
|
88
|
-
if end is None:
|
|
89
|
-
end = _finite_number(seg.get("end"))
|
|
90
|
-
if start is None or end is None or end <= start:
|
|
54
|
+
if overlay["type"] not in _ALLOWED_VISUAL_OVERLAY_TYPES:
|
|
91
55
|
return None
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
56
|
+
item = {
|
|
57
|
+
"type": overlay["type"],
|
|
58
|
+
"text": overlay["text"],
|
|
59
|
+
"start": overlay.get("start", segment["start"]),
|
|
60
|
+
"end": overlay.get("end", segment["end"]),
|
|
61
|
+
}
|
|
96
62
|
for key in ("anchor", "x", "y", "max_width", "style"):
|
|
97
63
|
if key in overlay:
|
|
98
64
|
item[key] = overlay[key]
|
|
99
65
|
return item
|
|
100
66
|
|
|
101
67
|
|
|
102
|
-
def _extract_visual_overlays_from_narration(narration_path):
|
|
103
|
-
data = _load_json(narration_path)
|
|
104
|
-
overlays = []
|
|
105
|
-
for segment in _iter_narration_segments(data):
|
|
106
|
-
raw = segment.get("visual_overlays")
|
|
107
|
-
if not isinstance(raw, list):
|
|
108
|
-
continue
|
|
109
|
-
for overlay in raw:
|
|
110
|
-
item = _canonical_visual_overlay(overlay, segment)
|
|
111
|
-
if item is not None:
|
|
112
|
-
overlays.append(item)
|
|
113
|
-
return overlays
|
|
114
|
-
|
|
115
|
-
|
|
116
68
|
def _write_canonical_visual_overlays(work_dir, narration_path):
|
|
117
69
|
"""Write assemble's canonical work_dir/visual_overlays.json recap handoff.
|
|
118
70
|
|
|
119
71
|
Direct assemble still supports user-authored/manual visual_overlays.json files. Once
|
|
120
|
-
recap owns the handoff for a narration,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
72
|
+
recap owns the handoff for a narration, the canonical artifact is a deterministic
|
|
73
|
+
reflection of the current narration so reused work_dirs cannot render stale overlays
|
|
74
|
+
from a previous run; unsupported types are filtered out and represented as an explicit
|
|
75
|
+
empty overlay list.
|
|
124
76
|
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
77
|
+
path = Path(work_dir) / _VISUAL_OVERLAYS
|
|
78
|
+
overlays = [
|
|
79
|
+
item
|
|
80
|
+
for segment in load_json(narration_path)
|
|
81
|
+
for overlay in segment.get("visual_overlays", [])
|
|
82
|
+
if (item := _canonical_visual_overlay(overlay, segment)) is not None
|
|
83
|
+
]
|
|
128
84
|
payload = {"schema_version": 1, "overlays": overlays}
|
|
129
85
|
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
130
86
|
print(f"[video-recap] 🧩 visual overlays: {len(overlays)} → {path}", flush=True)
|
|
@@ -135,18 +91,14 @@ def _print_grounding_qc_pointer(work_dir):
|
|
|
135
91
|
qc_path = Path(work_dir) / "grounding_qc.json"
|
|
136
92
|
if not qc_path.exists():
|
|
137
93
|
return
|
|
138
|
-
data =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
flush=True,
|
|
147
|
-
)
|
|
148
|
-
else:
|
|
149
|
-
print(f"[video-recap] 🧭 Grounding QC → {qc_path}", flush=True)
|
|
94
|
+
data = load_json(qc_path)
|
|
95
|
+
ranges = data["review_coverage"]["time_ranges"]
|
|
96
|
+
warnings = data["warnings"]
|
|
97
|
+
suffix = f" · warnings {len(warnings)}" if warnings else ""
|
|
98
|
+
print(
|
|
99
|
+
f"[video-recap] 🧭 Grounding QC: {data['verdict']} · ranges {len(ranges)}{suffix} → {qc_path}",
|
|
100
|
+
flush=True,
|
|
101
|
+
)
|
|
150
102
|
|
|
151
103
|
|
|
152
104
|
def _print_narration_review_pointer(work_dir, *, review_ran=True):
|
|
@@ -161,17 +113,20 @@ def _print_narration_review_pointer(work_dir, *, review_ran=True):
|
|
|
161
113
|
review_md = Path(work_dir) / "narration_review.md"
|
|
162
114
|
if not review_md.exists():
|
|
163
115
|
return
|
|
164
|
-
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
116
|
+
review_json = Path(work_dir) / "narration_review.json"
|
|
117
|
+
if not review_json.exists():
|
|
118
|
+
print(f"[video-recap] 📋 解说评审(建议性,不拦截)→ {review_md}")
|
|
119
|
+
return
|
|
120
|
+
try:
|
|
121
|
+
data = load_json(review_json)
|
|
122
|
+
except (OSError, ValueError):
|
|
123
|
+
print(f"[video-recap] 📋 解说评审(建议性,不拦截)→ {review_md}")
|
|
124
|
+
return
|
|
125
|
+
n_err = sum(1 for f in data["findings"] if f["severity"] == "error")
|
|
126
|
+
print(
|
|
127
|
+
f"[video-recap] 📋 解说评审(建议性,不拦截): {data['verdict']} · "
|
|
128
|
+
f"{len(data['findings'])} 条意见(error {n_err})→ {review_md}"
|
|
129
|
+
)
|
|
175
130
|
|
|
176
131
|
|
|
177
132
|
def _settings_for_compare(settings):
|
|
@@ -182,7 +137,7 @@ def _settings_for_compare(settings):
|
|
|
182
137
|
old default (or missing the key entirely, pre-dating it) must still resume — otherwise
|
|
183
138
|
flipping `--consolidate`'s default ON would hard-fail every in-flight work_dir.
|
|
184
139
|
"""
|
|
185
|
-
s = dict(settings
|
|
140
|
+
s = dict(settings)
|
|
186
141
|
s.pop("consolidate", None)
|
|
187
142
|
s.pop("consolidate_asr", None)
|
|
188
143
|
return s
|
|
@@ -191,19 +146,16 @@ def _settings_for_compare(settings):
|
|
|
191
146
|
def _manifest_mismatches(work_dir, video, args):
|
|
192
147
|
expected = _run_manifest_payload(video, args)
|
|
193
148
|
actual = _load_run_manifest(work_dir)
|
|
194
|
-
if
|
|
195
|
-
return [
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
mismatches = [
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if _settings_for_compare(actual.get("settings")) != _settings_for_compare(
|
|
205
|
-
expected.get("settings")
|
|
206
|
-
):
|
|
149
|
+
if actual is None:
|
|
150
|
+
return ["缺少 recap_run_manifest.json;不能证明 work_dir 属于当前视频/参数"]
|
|
151
|
+
# A multi-source manifest carries `sources` instead of these keys, so .get() reports it
|
|
152
|
+
# as a mismatch rather than a crash.
|
|
153
|
+
mismatches = [
|
|
154
|
+
f"{key}: expected {expected[key]!r}, got {actual.get(key)!r}"
|
|
155
|
+
for key in ("source_video", "source_video_fingerprint")
|
|
156
|
+
if actual.get(key) != expected[key]
|
|
157
|
+
]
|
|
158
|
+
if _settings_for_compare(actual["settings"]) != _settings_for_compare(expected["settings"]):
|
|
207
159
|
mismatches.append("settings: 当前 CLI/env 参数与 Phase A manifest 不匹配")
|
|
208
160
|
return mismatches
|
|
209
161
|
|
|
@@ -211,51 +163,29 @@ def _manifest_mismatches(work_dir, video, args):
|
|
|
211
163
|
def _multi_manifest_mismatches(work_dir, videos, args, source_records):
|
|
212
164
|
expected = _multi_run_manifest_payload(videos, args, source_records)
|
|
213
165
|
actual = _load_run_manifest(work_dir)
|
|
214
|
-
if
|
|
215
|
-
return [
|
|
216
|
-
"缺少或无法读取 recap_run_manifest.json;不能证明 work_dir 属于当前多视频/参数"
|
|
217
|
-
]
|
|
218
|
-
mismatches = []
|
|
166
|
+
if actual is None:
|
|
167
|
+
return ["缺少 recap_run_manifest.json;不能证明 work_dir 属于当前多视频/参数"]
|
|
219
168
|
if actual.get("mode") != "multi_source":
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
for s in expected.get("sources", [])
|
|
228
|
-
]
|
|
229
|
-
actual_sources = [
|
|
230
|
-
{
|
|
231
|
-
"source_id": s.get("source_id"),
|
|
232
|
-
"source_path": s.get("source_path"),
|
|
233
|
-
"source_video_fingerprint": s.get("source_video_fingerprint"),
|
|
234
|
-
}
|
|
235
|
-
for s in actual.get("sources", [])
|
|
236
|
-
if isinstance(s, dict)
|
|
237
|
-
]
|
|
238
|
-
if actual_sources != expected_sources:
|
|
169
|
+
return [f"mode: expected 'multi_source', got {actual.get('mode')!r}"]
|
|
170
|
+
mismatches = []
|
|
171
|
+
identity = ("source_id", "source_path", "source_video_fingerprint")
|
|
172
|
+
if [{k: s[k] for k in identity} for s in actual["sources"]] != [
|
|
173
|
+
{k: s[k] for k in identity} for s in expected["sources"]
|
|
174
|
+
]:
|
|
239
175
|
mismatches.append(
|
|
240
176
|
"sources: 当前输入视频列表/顺序/source_id/fingerprint 与 Phase A manifest 不匹配"
|
|
241
177
|
)
|
|
242
|
-
if _settings_for_compare(actual
|
|
243
|
-
expected.get("settings")
|
|
244
|
-
):
|
|
178
|
+
if _settings_for_compare(actual["settings"]) != _settings_for_compare(expected["settings"]):
|
|
245
179
|
mismatches.append("settings: 当前 CLI/env 参数与 Phase A manifest 不匹配")
|
|
246
180
|
return mismatches
|
|
247
181
|
|
|
248
182
|
|
|
249
183
|
def _read_assembly_output(work_dir):
|
|
250
|
-
|
|
251
|
-
if isinstance(manifest, dict) and manifest.get("final_output"):
|
|
252
|
-
return Path(manifest["final_output"])
|
|
253
|
-
return None
|
|
184
|
+
return Path(load_json(Path(work_dir) / ASSEMBLY_MANIFEST)["final_output"])
|
|
254
185
|
|
|
255
186
|
|
|
256
187
|
def _file_md5(path):
|
|
257
|
-
|
|
258
|
-
return hashlib.md5(path.read_bytes()).hexdigest() if path.exists() else None
|
|
188
|
+
return hashlib.md5(Path(path).read_bytes()).hexdigest()
|
|
259
189
|
|
|
260
190
|
|
|
261
191
|
def _read_phase_ledger(work_dir):
|
|
@@ -264,9 +194,10 @@ def _read_phase_ledger(work_dir):
|
|
|
264
194
|
Lets resume be driven by recorded phase state rather than bare file existence — the
|
|
265
195
|
prerequisite for the cut-first/narrate-second two-pause flow, and the guard that keeps a
|
|
266
196
|
narration written for one clip_plan from silently driving a different cut into TTS.
|
|
197
|
+
None before the first cut pass has recorded anything.
|
|
267
198
|
"""
|
|
268
|
-
|
|
269
|
-
return
|
|
199
|
+
path = Path(work_dir) / PHASE_LEDGER
|
|
200
|
+
return load_json(path) if path.exists() else None
|
|
270
201
|
|
|
271
202
|
|
|
272
203
|
def _write_phase_ledger(work_dir, **fields):
|
|
@@ -282,18 +213,14 @@ def _cut_narration_is_stale(ledger, current_clip_plan_fp):
|
|
|
282
213
|
"""Two-pass cut: the narration is authored against the rendered cut shown at the A2 pause,
|
|
283
214
|
i.e. against the clip_plan recorded in the ledger. If clip_plan changed since (a re-cut)
|
|
284
215
|
while that narration is still present, it describes the OLD cut — stale."""
|
|
285
|
-
|
|
286
|
-
return False
|
|
287
|
-
recorded_cp = ledger.get("clip_plan_fingerprint")
|
|
288
|
-
return bool(recorded_cp is not None and recorded_cp != current_clip_plan_fp)
|
|
216
|
+
return ledger is not None and ledger["clip_plan_fingerprint"] != current_clip_plan_fp
|
|
289
217
|
|
|
290
218
|
|
|
291
219
|
def _continuation_command(video, work_dir, args):
|
|
292
|
-
videos = _coerce_videos(video)
|
|
293
220
|
parts = [
|
|
294
221
|
sys.executable,
|
|
295
222
|
str(_entry("video-recap", "recap.py")),
|
|
296
|
-
*[str(v) for v in
|
|
223
|
+
*[str(v) for v in _coerce_videos(video)],
|
|
297
224
|
"--work-dir",
|
|
298
225
|
str(work_dir),
|
|
299
226
|
]
|
|
@@ -307,58 +234,53 @@ def _continuation_command(video, work_dir, args):
|
|
|
307
234
|
parts += ["--edit-mode", args.edit_mode]
|
|
308
235
|
if args.target_duration:
|
|
309
236
|
parts += ["--target-duration", args.target_duration]
|
|
310
|
-
if
|
|
237
|
+
if args.allow_duration_drift:
|
|
311
238
|
parts.append("--allow-duration-drift")
|
|
312
|
-
if
|
|
239
|
+
if args.allow_sparse_cut:
|
|
313
240
|
parts.append("--allow-sparse-cut")
|
|
314
241
|
if args.skip_asr:
|
|
315
242
|
parts.append("--skip-asr")
|
|
316
243
|
if args.mimo_video_overview:
|
|
317
244
|
parts.append("--mimo-video-overview")
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if getattr(args, "mimo_qc_refresh", False):
|
|
245
|
+
if args.mimo_qc != "off":
|
|
246
|
+
parts += ["--mimo-qc", args.mimo_qc]
|
|
247
|
+
if args.mimo_qc_refresh:
|
|
322
248
|
parts.append("--mimo-qc-refresh")
|
|
323
|
-
if not args.consolidate: # default is ON
|
|
249
|
+
if not args.consolidate: # default is ON; only the opt-out needs to round-trip
|
|
324
250
|
parts.append("--no-consolidate")
|
|
325
251
|
if args.consolidate_asr:
|
|
326
252
|
parts.append("--consolidate-asr")
|
|
327
|
-
if
|
|
253
|
+
if args.mimo_tts_voice:
|
|
328
254
|
parts += ["--mimo-tts-voice", args.mimo_tts_voice]
|
|
329
|
-
if
|
|
255
|
+
if args.tts_provider != "auto":
|
|
330
256
|
parts += ["--tts-provider", args.tts_provider]
|
|
331
|
-
if
|
|
257
|
+
if args.voice_ref:
|
|
332
258
|
parts += ["--voice-ref", args.voice_ref]
|
|
333
|
-
if
|
|
259
|
+
if args.allow_partial_tts:
|
|
334
260
|
parts.append("--allow-partial-tts")
|
|
335
|
-
if
|
|
336
|
-
parts.append(
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
if getattr(args, "subtitle_y_top", None) is not None:
|
|
261
|
+
if args.burn_subtitles is not None:
|
|
262
|
+
parts.append("--burn-subtitles" if args.burn_subtitles else "--no-burn-subtitles")
|
|
263
|
+
if args.subtitle_y_top is not None:
|
|
340
264
|
parts += ["--subtitle-y-top", str(args.subtitle_y_top)]
|
|
341
|
-
if
|
|
265
|
+
if args.subtitle_y_bot is not None:
|
|
342
266
|
parts += ["--subtitle-y-bot", str(args.subtitle_y_bot)]
|
|
343
|
-
if
|
|
267
|
+
if args.output_dir:
|
|
344
268
|
parts += ["--output-dir", args.output_dir]
|
|
345
|
-
if
|
|
269
|
+
if args.export_jianying:
|
|
346
270
|
parts.append("--export-jianying")
|
|
347
|
-
if
|
|
271
|
+
if args.jianying_bundle_media:
|
|
348
272
|
parts.append("--jianying-bundle-media")
|
|
349
|
-
if
|
|
273
|
+
if args.jianying_no_bundle_media:
|
|
350
274
|
parts.append("--jianying-no-bundle-media")
|
|
351
|
-
if
|
|
352
|
-
parts.append(
|
|
353
|
-
|
|
354
|
-
)
|
|
355
|
-
if getattr(args, "require_narration_review", False):
|
|
275
|
+
if args.review_narration is not None:
|
|
276
|
+
parts.append("--review-narration" if args.review_narration else "--no-review-narration")
|
|
277
|
+
if args.require_narration_review:
|
|
356
278
|
parts.append("--require-narration-review")
|
|
357
|
-
if
|
|
279
|
+
if args.material_library_dir:
|
|
358
280
|
parts += ["--material-library-dir", args.material_library_dir]
|
|
359
|
-
if
|
|
281
|
+
if args.use_materials:
|
|
360
282
|
parts.append("--use-materials")
|
|
361
|
-
if
|
|
283
|
+
if args.save_materials:
|
|
362
284
|
parts.append("--save-materials")
|
|
363
285
|
return " ".join(shlex.quote(part) for part in parts)
|
|
364
286
|
|
|
@@ -374,13 +296,13 @@ def _understand_args_for_source(source_record, source_work_dir, args):
|
|
|
374
296
|
str(source_work_dir),
|
|
375
297
|
"--style",
|
|
376
298
|
args.style,
|
|
299
|
+
"--edit-mode",
|
|
300
|
+
args.edit_mode,
|
|
377
301
|
]
|
|
378
302
|
if args.context:
|
|
379
303
|
uargs += ["--context", args.context]
|
|
380
304
|
if args.scene_threshold is not None:
|
|
381
305
|
uargs += ["--scene-threshold", str(args.scene_threshold)]
|
|
382
|
-
if args.edit_mode:
|
|
383
|
-
uargs += ["--edit-mode", args.edit_mode]
|
|
384
306
|
if args.target_duration:
|
|
385
307
|
uargs += ["--target-duration", args.target_duration]
|
|
386
308
|
if args.skip_asr:
|
|
@@ -394,16 +316,10 @@ def _understand_args_for_source(source_record, source_work_dir, args):
|
|
|
394
316
|
|
|
395
317
|
|
|
396
318
|
def _brief_excerpt(path, limit=1200):
|
|
397
|
-
|
|
398
|
-
text = Path(path).read_text(encoding="utf-8")
|
|
399
|
-
except OSError:
|
|
400
|
-
return ""
|
|
401
|
-
if limit <= 0:
|
|
402
|
-
return ""
|
|
319
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
403
320
|
if len(text) <= limit:
|
|
404
321
|
return text
|
|
405
|
-
|
|
406
|
-
return text[:limit]
|
|
322
|
+
marker = "\n…\n"
|
|
407
323
|
|
|
408
324
|
def section(heading):
|
|
409
325
|
lines = text.splitlines()
|
|
@@ -412,11 +328,7 @@ def _brief_excerpt(path, limit=1200):
|
|
|
412
328
|
except ValueError:
|
|
413
329
|
return ""
|
|
414
330
|
end = next(
|
|
415
|
-
(
|
|
416
|
-
index
|
|
417
|
-
for index in range(start + 1, len(lines))
|
|
418
|
-
if lines[index].startswith("## ")
|
|
419
|
-
),
|
|
331
|
+
(i for i in range(start + 1, len(lines)) if lines[i].startswith("## ")),
|
|
420
332
|
len(lines),
|
|
421
333
|
)
|
|
422
334
|
return "\n".join(lines[start:end]).strip()
|
|
@@ -424,37 +336,29 @@ def _brief_excerpt(path, limit=1200):
|
|
|
424
336
|
def clipped(value, budget):
|
|
425
337
|
if len(value) <= budget:
|
|
426
338
|
return value
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
usable = max(0, budget - len(marker))
|
|
431
|
-
head = max(1, round(usable * 0.7))
|
|
432
|
-
tail = max(0, usable - head)
|
|
433
|
-
suffix = value[-tail:].lstrip() if tail else ""
|
|
434
|
-
return value[:head].rstrip() + marker + suffix
|
|
339
|
+
usable = budget - len(marker)
|
|
340
|
+
head = round(usable * 0.7)
|
|
341
|
+
return value[:head].rstrip() + marker + value[len(value) - (usable - head):].lstrip()
|
|
435
342
|
|
|
436
343
|
# The generic writing contract occupies the front of every generated brief. A raw
|
|
437
344
|
# prefix therefore drops the source facts that multi-source planning actually needs.
|
|
438
345
|
# Give evidence-bearing sections equal space and retain both their opening and tail.
|
|
439
346
|
preferred = [
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
347
|
+
value
|
|
348
|
+
for value in (
|
|
349
|
+
section("## Story context (from background_research.json)"),
|
|
350
|
+
section("## Understanding index (from consolidate.py)"),
|
|
351
|
+
section("## ASR writing chunks (semantic windows)"),
|
|
352
|
+
section("## Scene timing guide"),
|
|
353
|
+
)
|
|
354
|
+
if value
|
|
444
355
|
]
|
|
445
|
-
preferred = [value for value in preferred if value]
|
|
446
356
|
if preferred:
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
marker = "\n…\n"
|
|
453
|
-
usable = max(0, limit - len(marker))
|
|
454
|
-
head = max(1, usable // 4)
|
|
455
|
-
tail = max(0, usable - head)
|
|
456
|
-
suffix = text[-tail:].lstrip() if tail else ""
|
|
457
|
-
return text[:head].rstrip() + marker + suffix
|
|
357
|
+
per_section = (limit - 2 * (len(preferred) - 1)) // len(preferred)
|
|
358
|
+
return "\n\n".join(clipped(value, per_section) for value in preferred)[:limit]
|
|
359
|
+
usable = limit - len(marker)
|
|
360
|
+
head = usable // 4
|
|
361
|
+
return text[:head].rstrip() + marker + text[len(text) - (usable - head):].lstrip()
|
|
458
362
|
|
|
459
363
|
|
|
460
364
|
def _write_multi_source_clip_brief(work_dir, source_records, args):
|
|
@@ -495,7 +399,7 @@ def _write_multi_source_clip_brief(work_dir, source_records, args):
|
|
|
495
399
|
f"- path: `{s['source_path']}`",
|
|
496
400
|
f"- work_dir: `{swd}`",
|
|
497
401
|
f"- fingerprint: `{s['source_video_fingerprint']}`",
|
|
498
|
-
f"- material_id: `{s
|
|
402
|
+
f"- material_id: `{s['material_id']}`",
|
|
499
403
|
]
|
|
500
404
|
excerpt = _brief_excerpt(swd / "agent_narration_brief.md")
|
|
501
405
|
if excerpt:
|
|
@@ -505,89 +409,71 @@ def _write_multi_source_clip_brief(work_dir, source_records, args):
|
|
|
505
409
|
)
|
|
506
410
|
|
|
507
411
|
|
|
412
|
+
def _source_speech_rows(source_dir):
|
|
413
|
+
"""The cleaned transcript wins when it carries text; same precedence as
|
|
414
|
+
audio_mix._handoff_speech_evidence and sentence_boundaries._load_source_speech_spans."""
|
|
415
|
+
clean = source_dir / "asr_clean.json"
|
|
416
|
+
if clean.exists():
|
|
417
|
+
rows = load_json(clean)["segments"]
|
|
418
|
+
if any(row["text"].strip() for row in rows):
|
|
419
|
+
return rows
|
|
420
|
+
raw = source_dir / "asr_result.json"
|
|
421
|
+
return load_json(raw) if raw.exists() else []
|
|
422
|
+
|
|
423
|
+
|
|
508
424
|
def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
|
|
509
425
|
"""Map each source's speech and quiet evidence onto the combined output clock."""
|
|
510
|
-
|
|
511
|
-
source_by_id = {str(row["source_id"]): row for row in source_records}
|
|
426
|
+
source_by_id = {row["source_id"]: row for row in source_records}
|
|
512
427
|
cache = {}
|
|
513
428
|
|
|
514
429
|
def load_source(source_id):
|
|
515
|
-
if source_id in cache:
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
# derive its speech spans from a different artifact than every other consumer.
|
|
525
|
-
for name in ("asr_clean.json", "asr_result.json"):
|
|
526
|
-
speech = _load_json(source_dir / name) if source_dir else None
|
|
527
|
-
rows = speech.get("segments", []) if isinstance(speech, dict) else speech
|
|
528
|
-
if isinstance(rows, list) and any(
|
|
529
|
-
isinstance(row, dict) and str(row.get("text") or "").strip()
|
|
530
|
-
for row in rows
|
|
531
|
-
):
|
|
532
|
-
break
|
|
533
|
-
if isinstance(speech, dict):
|
|
534
|
-
speech = speech.get("segments", [])
|
|
535
|
-
quiet = _load_json(source_dir / "silence_periods.json") if source_dir else None
|
|
536
|
-
cache[source_id] = (
|
|
537
|
-
anchors.get("sentence_anchors", []) if isinstance(anchors, dict) else [],
|
|
538
|
-
speech if isinstance(speech, list) else [],
|
|
539
|
-
quiet if isinstance(quiet, list) else [],
|
|
540
|
-
)
|
|
430
|
+
if source_id not in cache:
|
|
431
|
+
source_dir = _source_work_dir(work_dir, source_by_id[source_id])
|
|
432
|
+
anchors_path = source_dir / "speech_boundary_anchors.json"
|
|
433
|
+
quiet_path = source_dir / "silence_periods.json"
|
|
434
|
+
cache[source_id] = (
|
|
435
|
+
load_json(anchors_path)["sentence_anchors"] if anchors_path.exists() else [],
|
|
436
|
+
_source_speech_rows(source_dir),
|
|
437
|
+
load_json(quiet_path) if quiet_path.exists() else [],
|
|
438
|
+
)
|
|
541
439
|
return cache[source_id]
|
|
542
440
|
|
|
543
441
|
mapped_anchors, mapped_speech, mapped_quiet = [], [], []
|
|
544
|
-
for clip in clips:
|
|
545
|
-
source_id =
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
output_start = float(clip["output_start"])
|
|
550
|
-
except (KeyError, TypeError, ValueError):
|
|
551
|
-
continue
|
|
442
|
+
for clip in plan["clips"]:
|
|
443
|
+
source_id = clip["source_id"]
|
|
444
|
+
source_start = float(clip["source_start"])
|
|
445
|
+
source_end = float(clip["source_end"])
|
|
446
|
+
output_start = float(clip["output_start"])
|
|
552
447
|
anchors, speech_rows, quiet_rows = load_source(source_id)
|
|
553
448
|
for anchor in anchors:
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
except (AttributeError, TypeError, ValueError):
|
|
449
|
+
when = float(anchor["time"])
|
|
450
|
+
if not (source_start - 0.05 <= when <= source_end + 0.05):
|
|
557
451
|
continue
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
pause_start=round(output_start + pause - source_start, 3),
|
|
573
|
-
)
|
|
574
|
-
mapped_anchors.append(item)
|
|
452
|
+
try:
|
|
453
|
+
pause = float(anchor["pause_start"])
|
|
454
|
+
except (TypeError, ValueError):
|
|
455
|
+
pause = when
|
|
456
|
+
pause = max(source_start, min(pause, when))
|
|
457
|
+
item = dict(anchor)
|
|
458
|
+
item.update(
|
|
459
|
+
source_id=source_id,
|
|
460
|
+
source_time=round(when, 3),
|
|
461
|
+
time=round(output_start + when - source_start, 3),
|
|
462
|
+
source_pause_start=round(pause, 3),
|
|
463
|
+
pause_start=round(output_start + pause - source_start, 3),
|
|
464
|
+
)
|
|
465
|
+
mapped_anchors.append(item)
|
|
575
466
|
for rows, destination, require_text in (
|
|
576
467
|
(speech_rows, mapped_speech, True),
|
|
577
468
|
(quiet_rows, mapped_quiet, False),
|
|
578
469
|
):
|
|
579
470
|
for row in rows:
|
|
580
|
-
if not
|
|
581
|
-
continue
|
|
582
|
-
if require_text and not str(row.get("text") or "").strip():
|
|
471
|
+
if require_text and not row["text"].strip():
|
|
583
472
|
continue
|
|
584
|
-
if not require_text and
|
|
585
|
-
continue
|
|
586
|
-
try:
|
|
587
|
-
start = max(source_start, float(row["start"]))
|
|
588
|
-
end = min(source_end, float(row["end"]))
|
|
589
|
-
except (KeyError, TypeError, ValueError):
|
|
473
|
+
if not require_text and row["has_speech"]:
|
|
590
474
|
continue
|
|
475
|
+
start = max(source_start, float(row["start"]))
|
|
476
|
+
end = min(source_end, float(row["end"]))
|
|
591
477
|
if end <= start:
|
|
592
478
|
continue
|
|
593
479
|
item = dict(row)
|
|
@@ -610,7 +496,7 @@ def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
|
|
|
610
496
|
plan, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str
|
|
611
497
|
).encode("utf-8")
|
|
612
498
|
).hexdigest(),
|
|
613
|
-
"sentence_anchors": sorted(mapped_anchors, key=lambda row:
|
|
499
|
+
"sentence_anchors": sorted(mapped_anchors, key=lambda row: row["time"]),
|
|
614
500
|
"speech_spans": sorted(mapped_speech, key=lambda row: (row["start"], row["end"])),
|
|
615
501
|
"quiet_windows": sorted(mapped_quiet, key=lambda row: (row["start"], row["end"])),
|
|
616
502
|
}
|
|
@@ -621,8 +507,7 @@ def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
|
|
|
621
507
|
|
|
622
508
|
|
|
623
509
|
def _write_multi_source_output_brief(work_dir, source_records, validated_plan_path):
|
|
624
|
-
plan =
|
|
625
|
-
clips = plan.get("clips", []) if isinstance(plan, dict) else []
|
|
510
|
+
plan = load_json(validated_plan_path)
|
|
626
511
|
source_by_id = {s["source_id"]: s for s in source_records}
|
|
627
512
|
speech_evidence = _write_multi_source_output_speech_evidence(
|
|
628
513
|
work_dir, source_records, plan
|
|
@@ -652,22 +537,21 @@ def _write_multi_source_output_brief(work_dir, source_records, validated_plan_pa
|
|
|
652
537
|
"",
|
|
653
538
|
"## Kept clips (output → source)",
|
|
654
539
|
]
|
|
655
|
-
for c in clips:
|
|
656
|
-
|
|
657
|
-
|
|
540
|
+
for c in plan["clips"]:
|
|
541
|
+
src = source_by_id[c["source_id"]]
|
|
542
|
+
reason = f" — {c['reason']}" if c["reason"] else ""
|
|
658
543
|
lines.append(
|
|
659
|
-
f"- output {_fmt_range(c
|
|
660
|
-
f"{
|
|
661
|
-
f"source {_fmt_range(c
|
|
662
|
-
f"{('— ' + str(c.get('reason'))) if c.get('reason') else ''}"
|
|
544
|
+
f"- output {_fmt_range(c['output_start'], c['output_end'])} → "
|
|
545
|
+
f"{c['source_id']} `{src['source_path']}` "
|
|
546
|
+
f"source {_fmt_range(c['source_start'], c['source_end'])}{reason}"
|
|
663
547
|
)
|
|
664
548
|
anchors = speech_evidence["sentence_anchors"]
|
|
665
549
|
if anchors:
|
|
666
550
|
lines += ["", "## 原声句末安全切入点"]
|
|
667
551
|
lines.extend(
|
|
668
|
-
f"- {
|
|
552
|
+
f"- {row['time']:.3f}s ({row['source_id']})"
|
|
669
553
|
for row in anchors
|
|
670
|
-
if row
|
|
554
|
+
if row["confidence"] in {"high", "medium"}
|
|
671
555
|
)
|
|
672
556
|
lines += ["", "## Source work dirs"]
|
|
673
557
|
for s in source_records:
|
|
@@ -677,97 +561,34 @@ def _write_multi_source_output_brief(work_dir, source_records, validated_plan_pa
|
|
|
677
561
|
)
|
|
678
562
|
|
|
679
563
|
|
|
680
|
-
def
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if not isinstance(qc, dict):
|
|
694
|
-
return []
|
|
695
|
-
reasons = []
|
|
696
|
-
for key in (
|
|
697
|
-
"status",
|
|
698
|
-
"target_duration_status",
|
|
699
|
-
"duration_status",
|
|
700
|
-
"boundary_status",
|
|
701
|
-
):
|
|
702
|
-
if _qc_status_is_blocking(qc.get(key)):
|
|
703
|
-
reasons.append(f"{key}={qc.get(key)}")
|
|
704
|
-
for key in ("blocking", "blocked", "failed", "fail", "errors"):
|
|
705
|
-
value = qc.get(key)
|
|
706
|
-
if value:
|
|
707
|
-
reasons.append(f"{key}={value}")
|
|
708
|
-
checks = qc.get("checks") if isinstance(qc.get("checks"), list) else []
|
|
709
|
-
for item in checks:
|
|
710
|
-
if isinstance(item, dict) and _qc_status_is_blocking(item.get("status")):
|
|
711
|
-
reasons.append(f"{item.get('name', 'check')}={item.get('status')}")
|
|
712
|
-
return reasons
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
def _cut_qc_summary_lines(qc):
|
|
716
|
-
if not isinstance(qc, dict) or not qc:
|
|
717
|
-
return []
|
|
718
|
-
parts = []
|
|
719
|
-
for key in (
|
|
720
|
-
"target_duration_status",
|
|
721
|
-
"total_duration",
|
|
722
|
-
"clip_count",
|
|
723
|
-
"join_fade_ms",
|
|
724
|
-
):
|
|
725
|
-
if key in qc:
|
|
726
|
-
parts.append(f"{key}={qc.get(key)}")
|
|
727
|
-
geometry = qc.get("output_geometry")
|
|
728
|
-
if isinstance(geometry, dict):
|
|
729
|
-
dims = "x".join(
|
|
730
|
-
str(geometry.get(k))
|
|
731
|
-
for k in ("width", "height")
|
|
732
|
-
if geometry.get(k) is not None
|
|
733
|
-
)
|
|
734
|
-
fps = geometry.get("fps")
|
|
735
|
-
reason = geometry.get("reason") or qc.get("output_geometry_reason")
|
|
736
|
-
fps_suffix = f"@{fps}fps" if fps else ""
|
|
737
|
-
reason_suffix = f" reason={reason}" if reason else ""
|
|
738
|
-
parts.append(f"output_geometry={dims or geometry}{fps_suffix}{reason_suffix}")
|
|
739
|
-
warnings = qc.get("warnings")
|
|
740
|
-
if warnings:
|
|
741
|
-
parts.append(
|
|
742
|
-
f"warnings={len(warnings) if isinstance(warnings, list) else warnings}"
|
|
743
|
-
)
|
|
744
|
-
return ["[video-recap] cut QC: " + "; ".join(parts)] if parts else []
|
|
564
|
+
def _cut_qc_summary_line(qc):
|
|
565
|
+
geometry = qc["output_geometry"]
|
|
566
|
+
parts = [
|
|
567
|
+
f"target_duration_status={qc['target_duration_status']}",
|
|
568
|
+
f"total_duration={qc['total_duration']}",
|
|
569
|
+
f"clip_count={qc['clip_count']}",
|
|
570
|
+
f"join_fade_ms={qc['join_fade_ms']}",
|
|
571
|
+
f"output_geometry={geometry['width']}x{geometry['height']}@{geometry['fps']}fps"
|
|
572
|
+
f" reason={qc['output_geometry_reason']}",
|
|
573
|
+
]
|
|
574
|
+
if qc.get("warnings"):
|
|
575
|
+
parts.append(f"warnings={len(qc['warnings'])}")
|
|
576
|
+
return "[video-recap] cut QC: " + "; ".join(parts)
|
|
745
577
|
|
|
746
578
|
|
|
747
579
|
def _surface_cut_qc(work_dir):
|
|
748
|
-
|
|
749
|
-
qc =
|
|
750
|
-
|
|
751
|
-
print(line, flush=True)
|
|
752
|
-
blocking = _cut_qc_blocking_reasons(qc)
|
|
753
|
-
if blocking:
|
|
754
|
-
raise SystemExit("video-cut QC blocking/fail status: " + "; ".join(blocking))
|
|
580
|
+
"""Print the cut QC video-cut recorded; cut.py itself already fails on blocking QC."""
|
|
581
|
+
qc = load_json(Path(work_dir) / "clip_plan_validated.json")["qc"]
|
|
582
|
+
print(_cut_qc_summary_line(qc), flush=True)
|
|
755
583
|
return qc
|
|
756
584
|
|
|
757
585
|
|
|
758
586
|
def _fmt_range(start, end):
|
|
759
|
-
|
|
760
|
-
return f"{float(start):.3f}-{float(end):.3f}s"
|
|
761
|
-
except (TypeError, ValueError):
|
|
762
|
-
return f"{start}-{end}s"
|
|
587
|
+
return f"{start:.3f}-{end:.3f}s"
|
|
763
588
|
|
|
764
589
|
|
|
765
590
|
def _material_library_dir(args):
|
|
766
|
-
return (
|
|
767
|
-
args.material_library_dir
|
|
768
|
-
or os.environ.get("VIDEO_RECAP_MATERIAL_LIBRARY_DIR")
|
|
769
|
-
or None
|
|
770
|
-
)
|
|
591
|
+
return args.material_library_dir or os.environ.get("VIDEO_RECAP_MATERIAL_LIBRARY_DIR") or None
|
|
771
592
|
|
|
772
593
|
|
|
773
594
|
def _materials_enabled(args):
|
|
@@ -781,9 +602,7 @@ def _save_materials_enabled(args):
|
|
|
781
602
|
def _pause_for_agent(work_dir, need_text, cont, inspect_hint=None):
|
|
782
603
|
brief = Path(work_dir) / "agent_narration_brief.md"
|
|
783
604
|
print("=" * 50)
|
|
784
|
-
if
|
|
785
|
-
encoding="utf-8"
|
|
786
|
-
):
|
|
605
|
+
if "Research the story FIRST" in brief.read_text(encoding="utf-8"):
|
|
787
606
|
print(
|
|
788
607
|
"[video-recap] ⚑ 理解素材偏薄:先按 brief 顶部「Research the story FIRST」调研并写 "
|
|
789
608
|
"background_research.json,再写解说,避免看图说话。"
|