@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""video-recap inspect — advisory, read-only orientation over a recap work_dir.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Reads ONLY the JSON artifacts already in work_dir (no ffmpeg, no frame reads, no video
|
|
5
|
+
probing, no new deps, no cross-skill import). A missing artifact is the legitimate
|
|
6
|
+
"that stage has not run yet" state and is reported as such.
|
|
7
7
|
|
|
8
8
|
Two subcommands:
|
|
9
9
|
state summarize the work_dir: source video + fingerprint, full|cut mode, which stage
|
|
@@ -22,6 +22,10 @@ import json
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def load_json(path):
|
|
26
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
27
|
+
|
|
28
|
+
|
|
25
29
|
# --- artifact catalog --------------------------------------------------------
|
|
26
30
|
# Stage artifacts probed by `state`. Order = rough pipeline order.
|
|
27
31
|
_UNDERSTANDING_ARTIFACTS = [
|
|
@@ -57,7 +61,6 @@ _STORYBOARD_ARTIFACTS = [
|
|
|
57
61
|
"storyboard/source_storyboard.json",
|
|
58
62
|
"storyboard/edited_storyboard.json",
|
|
59
63
|
]
|
|
60
|
-
# Forward-compat: if a write-side state file ever lands, prefer it as the state source.
|
|
61
64
|
_FORWARD_STATE_FILES = ["manifest.json", "task_state.json"]
|
|
62
65
|
|
|
63
66
|
_COMPACT_TEXT_LIMIT = 80
|
|
@@ -70,88 +73,71 @@ def _truncate(text, compact):
|
|
|
70
73
|
return text[: _COMPACT_TEXT_LIMIT - 1] + "…"
|
|
71
74
|
|
|
72
75
|
|
|
73
|
-
def
|
|
74
|
-
"""
|
|
76
|
+
def _load_optional(path):
|
|
77
|
+
"""The artifact's JSON, or None when the stage that writes it has not run yet."""
|
|
75
78
|
path = Path(path)
|
|
76
79
|
if not path.exists():
|
|
77
|
-
return None
|
|
80
|
+
return None
|
|
78
81
|
try:
|
|
79
|
-
return
|
|
80
|
-
except ValueError:
|
|
81
|
-
return None
|
|
82
|
-
except OSError as exc:
|
|
83
|
-
return None, f"{path.name} 读取失败: {exc}"
|
|
82
|
+
return load_json(path)
|
|
83
|
+
except (OSError, ValueError):
|
|
84
|
+
return None
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
def _fmt_seconds(value):
|
|
87
|
-
"""mm:ss for a
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return str(value)
|
|
92
|
-
sign = "-" if total < 0 else ""
|
|
93
|
-
total = abs(total)
|
|
94
|
-
m = int(total // 60)
|
|
95
|
-
s = total % 60
|
|
96
|
-
return f"{sign}{m:02d}:{s:05.2f}"
|
|
88
|
+
"""mm:ss.ss for a seconds value."""
|
|
89
|
+
sign = "-" if value < 0 else ""
|
|
90
|
+
total = abs(value)
|
|
91
|
+
return f"{sign}{int(total // 60):02d}:{total % 60:05.2f}"
|
|
97
92
|
|
|
98
93
|
|
|
99
94
|
# --- source video discovery --------------------------------------------------
|
|
100
95
|
def _discover_source(work_dir):
|
|
101
96
|
"""Find the source video path + fingerprint by reading recap artifacts, most authoritative
|
|
102
|
-
first. Returns
|
|
103
|
-
nothing records it.
|
|
97
|
+
first. Returns {path, fingerprint, origin} with None values + origin="unknown" when
|
|
98
|
+
nothing records it."""
|
|
99
|
+
work_dir = Path(work_dir)
|
|
104
100
|
# 1. recap_run_manifest.json — canonical: written before the first pause with both fields.
|
|
105
|
-
|
|
101
|
+
# A multi-source manifest carries `sources` instead of `source_video`.
|
|
102
|
+
data = _load_optional(work_dir / "recap_run_manifest.json")
|
|
106
103
|
if isinstance(data, dict) and data.get("source_video"):
|
|
107
104
|
return {
|
|
108
|
-
"path": data
|
|
105
|
+
"path": data["source_video"],
|
|
109
106
|
"fingerprint": data.get("source_video_fingerprint"),
|
|
110
107
|
"origin": "recap_run_manifest.json",
|
|
111
108
|
}
|
|
112
109
|
# 2. assembly_manifest.json — late stage; carries input_video (+ source_video in cut mode).
|
|
113
|
-
data
|
|
110
|
+
data = _load_optional(work_dir / "assembly_manifest.json")
|
|
114
111
|
if isinstance(data, dict) and (data.get("source_video") or data.get("input_video")):
|
|
115
112
|
return {
|
|
116
|
-
"path": data.get("source_video") or data
|
|
113
|
+
"path": data.get("source_video") or data["input_video"],
|
|
117
114
|
"fingerprint": data.get("source_video_fingerprint"),
|
|
118
115
|
"origin": "assembly_manifest.json",
|
|
119
116
|
}
|
|
120
|
-
# 3. edited_source.mp4.meta.json — cut
|
|
121
|
-
data
|
|
122
|
-
if
|
|
117
|
+
# 3. edited_source.mp4.meta.json — single-source cut; fingerprint only, no path.
|
|
118
|
+
data = _load_optional(work_dir / "edited_source.mp4.meta.json")
|
|
119
|
+
if data is not None and data.get("source_video_fingerprint"):
|
|
123
120
|
return {
|
|
124
121
|
"path": None,
|
|
125
|
-
"fingerprint": data
|
|
122
|
+
"fingerprint": data["source_video_fingerprint"],
|
|
126
123
|
"origin": "edited_source.mp4.meta.json",
|
|
127
124
|
}
|
|
128
125
|
return {"path": None, "fingerprint": None, "origin": "unknown"}
|
|
129
126
|
|
|
130
127
|
|
|
131
128
|
def _discover_multi_source(work_dir):
|
|
132
|
-
data
|
|
129
|
+
data = _load_optional(Path(work_dir) / "multi_source_manifest.json")
|
|
133
130
|
if not isinstance(data, dict) or not isinstance(data.get("sources"), list):
|
|
134
131
|
return None
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
"source_path": raw.get("source_path"),
|
|
142
|
-
"source_name": raw.get("source_name"),
|
|
143
|
-
"source_video_fingerprint": raw.get("source_video_fingerprint"),
|
|
144
|
-
"source_work_dir": raw.get("source_work_dir"),
|
|
145
|
-
"material_id": raw.get("material_id"),
|
|
146
|
-
})
|
|
147
|
-
return {"schema_version": data.get("schema_version", 1), "sources": sources}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# --- clip plan loading + forward affine map ----------------------------------
|
|
132
|
+
return {
|
|
133
|
+
"schema_version": data.get("schema_version", 1),
|
|
134
|
+
"sources": [source for source in data["sources"] if isinstance(source, dict)],
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
151
138
|
def _clip_entries(plan):
|
|
152
|
-
"""Normalize a clip plan (dict-with-clips or bare list) to the list of clip dicts."""
|
|
153
139
|
if isinstance(plan, dict):
|
|
154
|
-
entries = plan.get("clips"
|
|
140
|
+
entries = plan.get("clips")
|
|
155
141
|
elif isinstance(plan, list):
|
|
156
142
|
entries = plan
|
|
157
143
|
else:
|
|
@@ -160,57 +146,53 @@ def _clip_entries(plan):
|
|
|
160
146
|
|
|
161
147
|
|
|
162
148
|
def _normalize_clips(entries):
|
|
163
|
-
"""
|
|
164
|
-
|
|
165
|
-
Mirrors assemble._output_clip_spans / _build_video_clips field access: source_start/source_end
|
|
166
|
-
fall back to start/end; when output_start/output_end are absent they are derived with the same
|
|
167
|
-
forward cursor cut.py uses (sum of prior kept-clip durations). Bad rows are skipped, not fatal.
|
|
168
|
-
"""
|
|
149
|
+
"""Normalize current and historical validated plans for the advisory mapper."""
|
|
169
150
|
clips, cursor = [], 0.0
|
|
170
|
-
for
|
|
171
|
-
if not isinstance(
|
|
151
|
+
for index, raw in enumerate(entries):
|
|
152
|
+
if not isinstance(raw, dict):
|
|
172
153
|
continue
|
|
173
154
|
try:
|
|
174
|
-
|
|
175
|
-
|
|
155
|
+
source_start = float(raw.get("source_start", raw.get("start")))
|
|
156
|
+
source_end = float(raw.get("source_end", raw.get("end")))
|
|
176
157
|
except (TypeError, ValueError):
|
|
177
158
|
continue
|
|
178
|
-
if
|
|
159
|
+
if source_end <= source_start:
|
|
179
160
|
continue
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
cursor
|
|
161
|
+
output_start = raw.get("output_start")
|
|
162
|
+
output_end = raw.get("output_end")
|
|
163
|
+
if output_start is None or output_end is None:
|
|
164
|
+
output_start, output_end = cursor, cursor + source_end - source_start
|
|
184
165
|
else:
|
|
185
166
|
try:
|
|
186
|
-
|
|
167
|
+
output_start, output_end = float(output_start), float(output_end)
|
|
187
168
|
except (TypeError, ValueError):
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
169
|
+
continue
|
|
170
|
+
cursor = output_end
|
|
171
|
+
clips.append(
|
|
172
|
+
{
|
|
173
|
+
"clip_id": raw.get("clip_id", index),
|
|
174
|
+
"source_id": raw.get("source_id"),
|
|
175
|
+
"source_path": raw.get("source_path"),
|
|
176
|
+
"source_start": source_start,
|
|
177
|
+
"source_end": source_end,
|
|
178
|
+
"output_start": output_start,
|
|
179
|
+
"output_end": output_end,
|
|
180
|
+
"reason": str(raw.get("reason", raw.get("note", ""))).strip(),
|
|
181
|
+
}
|
|
182
|
+
)
|
|
202
183
|
return clips
|
|
203
184
|
|
|
204
185
|
|
|
186
|
+
# --- forward affine map --------------------------------------------------------
|
|
205
187
|
def _source_to_output(src, clip):
|
|
206
|
-
"""The forward affine map cut.py
|
|
207
|
-
mapped = clip["output_start"] + (
|
|
188
|
+
"""The forward affine map cut.py uses, clamped to the clip's output span."""
|
|
189
|
+
mapped = clip["output_start"] + (src - clip["source_start"])
|
|
208
190
|
return round(max(clip["output_start"], min(mapped, clip["output_end"])), 3)
|
|
209
191
|
|
|
210
192
|
|
|
211
193
|
def _output_to_source(out, clip):
|
|
212
194
|
"""Inverse of the forward affine map (same slope, clamped to the clip's source span)."""
|
|
213
|
-
mapped = clip["source_start"] + (
|
|
195
|
+
mapped = clip["source_start"] + (out - clip["output_start"])
|
|
214
196
|
return round(max(clip["source_start"], min(mapped, clip["source_end"])), 3)
|
|
215
197
|
|
|
216
198
|
|
|
@@ -255,20 +237,14 @@ def _next_pause(work_dir, mode):
|
|
|
255
237
|
|
|
256
238
|
|
|
257
239
|
def _stale_manifest_note(work_dir, mode):
|
|
258
|
-
"""Advisory stale-manifest risks read purely from
|
|
259
|
-
need the source bytes). Surfaces the two desync traps recap.py guards: a cut
|
|
260
|
-
|
|
240
|
+
"""Advisory stale-manifest risks read purely from file presence (no fingerprint recompute —
|
|
241
|
+
that would need the source bytes). Surfaces the two desync traps recap.py guards: a cut
|
|
242
|
+
narration without the phase ledger that ties it to a clip_plan, and a missing run manifest."""
|
|
261
243
|
notes = []
|
|
262
244
|
if not _present(work_dir, "recap_run_manifest.json"):
|
|
263
245
|
notes.append("缺少 recap_run_manifest.json:无法证明 work_dir 属于当前视频/参数(resume 会被拒绝)。")
|
|
264
|
-
if mode == "cut" and _present(work_dir, "narration.json"):
|
|
265
|
-
|
|
266
|
-
if isinstance(ledger, dict):
|
|
267
|
-
recorded = ledger.get("clip_plan_fingerprint")
|
|
268
|
-
if recorded is None:
|
|
269
|
-
notes.append("recap_phase.json 未记录 clip_plan_fingerprint:无法判断 narration 是否对当前剪辑写的。")
|
|
270
|
-
else:
|
|
271
|
-
notes.append("有 narration.json 但缺少 recap_phase.json:无法判断解说是否对当前剪辑写的(可能 stale)。")
|
|
246
|
+
if mode == "cut" and _present(work_dir, "narration.json") and not _present(work_dir, "recap_phase.json"):
|
|
247
|
+
notes.append("有 narration.json 但缺少 recap_phase.json:无法判断解说是否对当前剪辑写的(可能 stale)。")
|
|
272
248
|
return notes
|
|
273
249
|
|
|
274
250
|
|
|
@@ -277,44 +253,44 @@ def _present_storyboards(work_dir):
|
|
|
277
253
|
|
|
278
254
|
|
|
279
255
|
def cmd_state(work_dir, compact=None):
|
|
280
|
-
del compact
|
|
256
|
+
del compact
|
|
281
257
|
work_dir = Path(work_dir)
|
|
282
258
|
if not work_dir.exists():
|
|
283
259
|
return {"error": f"work_dir 不存在: {work_dir}"}
|
|
284
260
|
|
|
285
|
-
forward = [
|
|
261
|
+
forward = [name for name in _FORWARD_STATE_FILES if _present(work_dir, name)]
|
|
286
262
|
mode = _detect_mode(work_dir)
|
|
287
|
-
source = _discover_source(work_dir)
|
|
288
|
-
|
|
289
263
|
groups = {
|
|
290
264
|
"understanding": _UNDERSTANDING_ARTIFACTS,
|
|
291
265
|
"cut": _CUT_ARTIFACTS,
|
|
292
266
|
"script": _SCRIPT_ARTIFACTS,
|
|
293
267
|
"render": _RENDER_ARTIFACTS,
|
|
294
268
|
}
|
|
295
|
-
artifacts = {
|
|
296
|
-
|
|
297
|
-
artifacts[group] = {
|
|
269
|
+
artifacts = {
|
|
270
|
+
group: {
|
|
298
271
|
"present": [n for n in names if _present(work_dir, n)],
|
|
299
272
|
"missing": [n for n in names if not _present(work_dir, n)],
|
|
300
273
|
}
|
|
301
|
-
|
|
274
|
+
for group, names in groups.items()
|
|
275
|
+
}
|
|
302
276
|
pause = _next_pause(work_dir, mode)
|
|
303
277
|
return {
|
|
304
278
|
"work_dir": str(work_dir),
|
|
305
279
|
"mode": mode,
|
|
306
280
|
"forward_state_files": forward,
|
|
307
|
-
"source_video":
|
|
281
|
+
"source_video": _discover_source(work_dir),
|
|
308
282
|
"multi_source": _discover_multi_source(work_dir),
|
|
309
|
-
"next_pause":
|
|
310
|
-
{"artifact": pause[0], "hint": pause[1]} if pause else None
|
|
311
|
-
),
|
|
283
|
+
"next_pause": {"artifact": pause[0], "hint": pause[1]} if pause else None,
|
|
312
284
|
"artifacts": artifacts,
|
|
313
285
|
"storyboards": _present_storyboards(work_dir),
|
|
314
286
|
"stale_manifest_notes": _stale_manifest_note(work_dir, mode),
|
|
315
287
|
}
|
|
316
288
|
|
|
317
289
|
|
|
290
|
+
def _short_fingerprint(fp):
|
|
291
|
+
return f"{fp[:12]}…" if fp else "unknown"
|
|
292
|
+
|
|
293
|
+
|
|
318
294
|
def _render_state_md(state, compact):
|
|
319
295
|
if "error" in state:
|
|
320
296
|
return state["error"]
|
|
@@ -323,18 +299,17 @@ def _render_state_md(state, compact):
|
|
|
323
299
|
lines.append(f"状态来源(write-side manifest): {', '.join(state['forward_state_files'])}")
|
|
324
300
|
lines.append(f"模式: **{state['mode']}**")
|
|
325
301
|
src = state["source_video"]
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if state
|
|
302
|
+
lines.append(
|
|
303
|
+
f"源视频: {_truncate(src['path'] or 'unknown', compact)} "
|
|
304
|
+
f"(fp {_short_fingerprint(src['fingerprint'])}, 来源 {src['origin']})"
|
|
305
|
+
)
|
|
306
|
+
if state["multi_source"]:
|
|
331
307
|
lines.append("多源素材:")
|
|
332
|
-
for s in state["multi_source"]
|
|
333
|
-
fp = s.get("source_video_fingerprint")
|
|
334
|
-
short = (fp[:12] + "…") if isinstance(fp, str) and len(fp) > 12 else (fp or "unknown")
|
|
308
|
+
for s in state["multi_source"]["sources"]:
|
|
335
309
|
lines.append(
|
|
336
|
-
f" - {s
|
|
337
|
-
f"(fp {
|
|
310
|
+
f" - {s['source_id']}: {_truncate(s['source_path'], compact)} "
|
|
311
|
+
f"(fp {_short_fingerprint(s['source_video_fingerprint'])}, "
|
|
312
|
+
f"work_dir {s['source_work_dir']}, material {s['material_id']})"
|
|
338
313
|
)
|
|
339
314
|
lines.append("")
|
|
340
315
|
|
|
@@ -376,15 +351,16 @@ def cmd_clip_map(work_dir, output_start, output_end, source_start, source_end, c
|
|
|
376
351
|
if not validated.exists():
|
|
377
352
|
return {"error": "clip_plan_validated.json 不存在:这不是 cut 运行,或剪辑计划尚未被 cut.py 校验。"
|
|
378
353
|
"(full 模式没有源↔输出映射;cut 模式请先跑 video-cut。)"}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
354
|
+
try:
|
|
355
|
+
plan = load_json(validated)
|
|
356
|
+
except (OSError, ValueError):
|
|
357
|
+
return {"error": "clip_plan_validated.json 不是合法 JSON(可能写坏了)"}
|
|
382
358
|
entries = _clip_entries(plan)
|
|
383
359
|
if entries is None:
|
|
384
|
-
return {"error": "clip_plan_validated.json
|
|
360
|
+
return {"error": "clip_plan_validated.json 结构异常:需要 clips 数组。"}
|
|
385
361
|
clips = _normalize_clips(entries)
|
|
386
362
|
if not clips:
|
|
387
|
-
return {"error": "clip_plan_validated.json 没有有效的 clip
|
|
363
|
+
return {"error": "clip_plan_validated.json 没有有效的 clip。"}
|
|
388
364
|
|
|
389
365
|
have_output = output_start is not None or output_end is not None
|
|
390
366
|
have_source = source_start is not None or source_end is not None
|
|
@@ -407,10 +383,22 @@ def cmd_clip_map(work_dir, output_start, output_end, source_start, source_end, c
|
|
|
407
383
|
return result
|
|
408
384
|
|
|
409
385
|
|
|
386
|
+
def _segment(clip, *, source, output, compact):
|
|
387
|
+
# source_id/source_path are only written for multi-source cuts.
|
|
388
|
+
return {
|
|
389
|
+
"clip_id": clip["clip_id"],
|
|
390
|
+
"source_id": clip.get("source_id"),
|
|
391
|
+
"source_path": clip.get("source_path"),
|
|
392
|
+
"output": output,
|
|
393
|
+
"source": source,
|
|
394
|
+
"reason": _truncate(clip.get("reason"), compact),
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
410
398
|
def _map_output_window(out_start, out_end, clips, compact):
|
|
411
399
|
"""Map a queried OUTPUT window to its SOURCE window(s), one per touched clip."""
|
|
412
|
-
out_lo = clips[0]["output_start"] if out_start is None else
|
|
413
|
-
out_hi = clips[-1]["output_end"] if out_end is None else
|
|
400
|
+
out_lo = clips[0]["output_start"] if out_start is None else out_start
|
|
401
|
+
out_hi = clips[-1]["output_end"] if out_end is None else out_end
|
|
414
402
|
if out_hi < out_lo:
|
|
415
403
|
out_lo, out_hi = out_hi, out_lo
|
|
416
404
|
segments = []
|
|
@@ -418,14 +406,12 @@ def _map_output_window(out_start, out_end, clips, compact):
|
|
|
418
406
|
ov = _overlap(out_lo, out_hi, clip["output_start"], clip["output_end"])
|
|
419
407
|
if not ov:
|
|
420
408
|
continue
|
|
421
|
-
segments.append(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
"reason": _truncate(clip.get("reason"), compact),
|
|
428
|
-
})
|
|
409
|
+
segments.append(_segment(
|
|
410
|
+
clip,
|
|
411
|
+
output=[round(ov[0], 3), round(ov[1], 3)],
|
|
412
|
+
source=[_output_to_source(ov[0], clip), _output_to_source(ov[1], clip)],
|
|
413
|
+
compact=compact,
|
|
414
|
+
))
|
|
429
415
|
return {
|
|
430
416
|
"direction": "output→source",
|
|
431
417
|
"query": [round(out_lo, 3), round(out_hi, 3)],
|
|
@@ -439,8 +425,8 @@ def _map_output_window(out_start, out_end, clips, compact):
|
|
|
439
425
|
|
|
440
426
|
def _map_source_window(src_start, src_end, clips, compact):
|
|
441
427
|
"""Map a queried SOURCE window to its OUTPUT window(s), flagging source ranges in no clip."""
|
|
442
|
-
src_lo = clips[0]["source_start"] if src_start is None else
|
|
443
|
-
src_hi = clips[-1]["source_end"] if src_end is None else
|
|
428
|
+
src_lo = clips[0]["source_start"] if src_start is None else src_start
|
|
429
|
+
src_hi = clips[-1]["source_end"] if src_end is None else src_end
|
|
444
430
|
if src_hi < src_lo:
|
|
445
431
|
src_lo, src_hi = src_hi, src_lo
|
|
446
432
|
segments = []
|
|
@@ -450,14 +436,12 @@ def _map_source_window(src_start, src_end, clips, compact):
|
|
|
450
436
|
if not ov:
|
|
451
437
|
continue
|
|
452
438
|
covered.append(ov)
|
|
453
|
-
segments.append(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
"reason": _truncate(clip.get("reason"), compact),
|
|
460
|
-
})
|
|
439
|
+
segments.append(_segment(
|
|
440
|
+
clip,
|
|
441
|
+
source=[round(ov[0], 3), round(ov[1], 3)],
|
|
442
|
+
output=[_source_to_output(ov[0], clip), _source_to_output(ov[1], clip)],
|
|
443
|
+
compact=compact,
|
|
444
|
+
))
|
|
461
445
|
# Cut-out gaps: parts of [src_lo, src_hi] covered by NO kept clip (footage that was cut away).
|
|
462
446
|
gaps = []
|
|
463
447
|
cursor = src_lo
|
|
@@ -496,9 +480,9 @@ def _render_clip_map_md(result, compact):
|
|
|
496
480
|
for s in q["segments"]:
|
|
497
481
|
src = f"{_fmt_seconds(s['source'][0])}–{_fmt_seconds(s['source'][1])}"
|
|
498
482
|
out = f"{_fmt_seconds(s['output'][0])}–{_fmt_seconds(s['output'][1])}"
|
|
499
|
-
reason = f" 〔{s['reason']}〕" if s
|
|
500
|
-
sid = f" {s
|
|
501
|
-
spath = f" `{_truncate(s
|
|
483
|
+
reason = f" 〔{s['reason']}〕" if s["reason"] else ""
|
|
484
|
+
sid = f" {s['source_id']}" if s["source_id"] else ""
|
|
485
|
+
spath = f" `{_truncate(s['source_path'], compact)}`" if s["source_path"] else ""
|
|
502
486
|
lines.append(f" clip {s['clip_id']}{sid}{spath}: 源 {src} ↔ 输出 {out}{reason}")
|
|
503
487
|
if q["cut_out_source_gaps"]:
|
|
504
488
|
lines.append(" ✂️ 被剪掉的源区间(不在成片里):")
|
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
"""Apply the optional or strict narration-review gate before TTS."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
5
|
-
from recap_runtime import _env_bool, _load_json
|
|
6
|
-
|
|
7
7
|
|
|
8
8
|
def review_narration_enabled(args):
|
|
9
|
-
if
|
|
10
|
-
return
|
|
9
|
+
if args.review_narration is not None:
|
|
10
|
+
return args.review_narration
|
|
11
11
|
return _env_bool("REVIEW_NARRATION", True)
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def require_narration_review(args):
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
return args.require_narration_review or _env_bool("REQUIRE_NARRATION_REVIEW", False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _env_bool(name, default):
|
|
19
|
+
raw = os.environ.get(name)
|
|
20
|
+
return default if not raw else raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
def review_result_status(work_dir):
|
|
21
|
-
|
|
24
|
+
path = Path(work_dir) / "narration_review.json"
|
|
25
|
+
if not path.exists():
|
|
26
|
+
return {"ok": False, "reason": "missing or invalid narration_review.json"}
|
|
27
|
+
try:
|
|
28
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
29
|
+
except (OSError, json.JSONDecodeError):
|
|
30
|
+
return {"ok": False, "reason": "missing or invalid narration_review.json"}
|
|
22
31
|
if not isinstance(data, dict):
|
|
23
32
|
return {"ok": False, "reason": "missing or invalid narration_review.json"}
|
|
24
|
-
findings =
|
|
25
|
-
|
|
33
|
+
findings = data.get("findings")
|
|
34
|
+
if not isinstance(findings, list) or any(
|
|
35
|
+
not isinstance(finding, dict)
|
|
36
|
+
or not isinstance(finding.get("severity"), str)
|
|
37
|
+
for finding in findings
|
|
38
|
+
):
|
|
39
|
+
return {"ok": False, "reason": "missing or invalid narration_review.json"}
|
|
40
|
+
error_count = sum(1 for finding in findings if finding["severity"] == "error")
|
|
26
41
|
if data.get("parse_error"):
|
|
27
|
-
return {
|
|
28
|
-
"ok": False,
|
|
29
|
-
"reason": "parse_error",
|
|
30
|
-
"review": data,
|
|
31
|
-
"errors": error_count,
|
|
32
|
-
}
|
|
42
|
+
return {"ok": False, "reason": "parse_error", "review": data, "errors": error_count}
|
|
33
43
|
if error_count:
|
|
34
44
|
return {
|
|
35
45
|
"ok": False,
|
|
@@ -45,10 +55,7 @@ def review_result_status(work_dir):
|
|
|
45
55
|
def clear_narration_review_artifacts(work_dir):
|
|
46
56
|
"""Remove prior review artifacts before a fresh pre-TTS review run."""
|
|
47
57
|
for name in ("narration_review.json", "narration_review.md"):
|
|
48
|
-
|
|
49
|
-
(Path(work_dir) / name).unlink()
|
|
50
|
-
except FileNotFoundError:
|
|
51
|
-
pass
|
|
58
|
+
(Path(work_dir) / name).unlink(missing_ok=True)
|
|
52
59
|
|
|
53
60
|
|
|
54
61
|
def run_narration_review(work_dir, args, *, run, timeline="source"):
|