@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,4 +1,5 @@
|
|
|
1
1
|
"""Self-contained utilities for the video-cut skill (no cross-skill imports)."""
|
|
2
|
+
import math
|
|
2
3
|
import os
|
|
3
4
|
import subprocess
|
|
4
5
|
|
|
@@ -12,20 +13,27 @@ def env_bool(name, default):
|
|
|
12
13
|
val = os.environ.get(name)
|
|
13
14
|
if val is None:
|
|
14
15
|
return default
|
|
15
|
-
|
|
16
|
+
text = val.strip().lower()
|
|
17
|
+
if text in ("1", "true", "yes"):
|
|
18
|
+
return True
|
|
19
|
+
if text in ("0", "false", "no"):
|
|
20
|
+
return False
|
|
21
|
+
raise ValueError(f"{name} must be 1/true/yes or 0/false/no, got {val!r}")
|
|
16
22
|
|
|
17
23
|
|
|
18
24
|
def env_float(name, default, min_val=None):
|
|
19
|
-
"""Read an env var as a float,
|
|
25
|
+
"""Read an env var as a float, rejecting malformed or below-minimum values."""
|
|
20
26
|
val = os.environ.get(name)
|
|
21
27
|
if val is None:
|
|
22
28
|
return default
|
|
23
29
|
try:
|
|
24
|
-
result = float(val
|
|
25
|
-
except
|
|
26
|
-
|
|
27
|
-
if
|
|
28
|
-
|
|
30
|
+
result = float(val)
|
|
31
|
+
except ValueError as exc:
|
|
32
|
+
raise ValueError(f"{name} must be a number, got {val!r}") from exc
|
|
33
|
+
if not math.isfinite(result):
|
|
34
|
+
raise ValueError(f"{name} must be finite, got {val!r}")
|
|
35
|
+
if min_val is not None and result < min_val:
|
|
36
|
+
raise ValueError(f"{name} must be >= {min_val}, got {val!r}")
|
|
29
37
|
return result
|
|
30
38
|
|
|
31
39
|
|
|
@@ -49,29 +57,19 @@ CONFIG = {
|
|
|
49
57
|
|
|
50
58
|
|
|
51
59
|
def run_cmd(cmd, **kwargs):
|
|
52
|
-
"""Run a command and return the CompletedProcess (stdout/stderr captured)."""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
text = str(part)
|
|
57
|
-
display_parts.append(text if len(text) <= 240 else text[:237] + "...")
|
|
58
|
-
display = " ".join(display_parts)
|
|
59
|
-
else:
|
|
60
|
-
display = str(cmd)
|
|
61
|
-
if len(display) > 2000:
|
|
62
|
-
display = display[:1997] + "..."
|
|
60
|
+
"""Run a command list and return the CompletedProcess (stdout/stderr captured)."""
|
|
61
|
+
display = " ".join(
|
|
62
|
+
str(part) if len(str(part)) <= 240 else str(part)[:237] + "..." for part in cmd
|
|
63
|
+
)
|
|
63
64
|
log(f"运行: {display}")
|
|
64
65
|
return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
def get_video_duration(video_path):
|
|
68
|
-
"""Return media duration in seconds via ffprobe
|
|
69
|
-
cmd = ["ffprobe", "-v", "
|
|
69
|
+
"""Return media duration in seconds via ffprobe."""
|
|
70
|
+
cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
|
70
71
|
"-of", "csv=p=0", str(video_path)]
|
|
71
72
|
result = run_cmd(cmd)
|
|
72
73
|
if result.returncode != 0:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return float(result.stdout.strip())
|
|
76
|
-
except (TypeError, ValueError):
|
|
77
|
-
return 0.0
|
|
74
|
+
raise RuntimeError(f"ffprobe 无法读取时长: {video_path}: {result.stderr.strip()}")
|
|
75
|
+
return float(result.stdout.strip())
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
from lib import run_cmd
|
|
7
6
|
|
|
8
7
|
|
|
@@ -24,71 +23,42 @@ def _has_audio_stream(video_path):
|
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
class VideoGeometry(tuple):
|
|
27
|
-
"""Tuple-compatible
|
|
26
|
+
"""Tuple-compatible (width, height, fps) with probe facts attached for QC callers."""
|
|
28
27
|
|
|
29
|
-
def __new__(cls, width, height, fps, facts
|
|
28
|
+
def __new__(cls, width, height, fps, facts):
|
|
30
29
|
obj = super().__new__(cls, (width, height, fps))
|
|
31
|
-
obj.facts = facts
|
|
30
|
+
obj.facts = facts
|
|
32
31
|
return obj
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
def _parse_ratio(value):
|
|
35
|
+
"""ffprobe aspect ratios are 'N:D' (or 'N/D'); '0:1' and 'N/A' mean unknown."""
|
|
36
36
|
if value in (None, "", "0:1", "0/1", "N/A"):
|
|
37
37
|
return None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if not sep:
|
|
41
|
-
try:
|
|
42
|
-
ratio = float(text)
|
|
43
|
-
return ratio if ratio > 0 else None
|
|
44
|
-
except ValueError:
|
|
45
|
-
return None
|
|
46
|
-
left, _, right = text.partition(sep)
|
|
47
|
-
try:
|
|
48
|
-
num, den = float(left), float(right)
|
|
49
|
-
except ValueError:
|
|
50
|
-
return None
|
|
38
|
+
left, _, right = str(value).replace("/", ":").partition(":")
|
|
39
|
+
num, den = float(left), float(right)
|
|
51
40
|
return num / den if den > 0 and num > 0 else None
|
|
52
41
|
|
|
53
42
|
|
|
54
43
|
def _stream_rotation(stream):
|
|
55
|
-
|
|
56
|
-
tags = stream.get("tags"
|
|
44
|
+
"""Rotation from the legacy `rotate` tag or the display-matrix side data, else 0."""
|
|
45
|
+
tags = stream.get("tags", {})
|
|
57
46
|
if "rotate" in tags:
|
|
58
|
-
|
|
59
|
-
for side_data in stream.get("side_data_list"
|
|
60
|
-
if
|
|
61
|
-
|
|
62
|
-
for value in candidates:
|
|
63
|
-
try:
|
|
64
|
-
return int(round(float(value))) % 360
|
|
65
|
-
except (TypeError, ValueError):
|
|
66
|
-
continue
|
|
47
|
+
return int(round(float(tags["rotate"]))) % 360
|
|
48
|
+
for side_data in stream.get("side_data_list", []):
|
|
49
|
+
if "rotation" in side_data:
|
|
50
|
+
return int(round(float(side_data["rotation"]))) % 360
|
|
67
51
|
return 0
|
|
68
52
|
|
|
69
53
|
|
|
70
54
|
def _fps_from_rate(rate):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
num
|
|
74
|
-
try:
|
|
75
|
-
num_f, den_f = float(num), float(den)
|
|
76
|
-
return num_f / den_f if den_f > 0 else 0.0
|
|
77
|
-
except ValueError:
|
|
78
|
-
return 0.0
|
|
79
|
-
|
|
55
|
+
"""ffprobe frame rates are 'N/D' fractions; '0/0' means unknown."""
|
|
56
|
+
num, _, den = rate.partition("/")
|
|
57
|
+
return float(num) / float(den) if float(den) > 0 else 0.0
|
|
80
58
|
|
|
81
|
-
def _geometry_from_stream(stream, *, fallback=False):
|
|
82
|
-
coded_width = coded_height = 0
|
|
83
|
-
try:
|
|
84
|
-
coded_width = int(float(stream.get("width") or 0))
|
|
85
|
-
coded_height = int(float(stream.get("height") or 0))
|
|
86
|
-
except (TypeError, ValueError):
|
|
87
|
-
coded_width = coded_height = 0
|
|
88
|
-
if coded_width <= 0 or coded_height <= 0:
|
|
89
|
-
coded_width, coded_height = 1280, 720
|
|
90
|
-
fallback = True
|
|
91
59
|
|
|
60
|
+
def _geometry_from_stream(stream):
|
|
61
|
+
coded_width, coded_height = stream["width"], stream["height"]
|
|
92
62
|
parsed_sar = _parse_ratio(stream.get("sample_aspect_ratio"))
|
|
93
63
|
dar = _parse_ratio(stream.get("display_aspect_ratio"))
|
|
94
64
|
rotation = _stream_rotation(stream)
|
|
@@ -110,8 +80,8 @@ def _geometry_from_stream(stream, *, fallback=False):
|
|
|
110
80
|
display_width, display_height = display_height, display_width
|
|
111
81
|
|
|
112
82
|
width, height = _clamp_even_geometry(round(display_width), round(display_height))
|
|
113
|
-
fps = _fps_from_rate(stream
|
|
114
|
-
stream.get("avg_frame_rate")
|
|
83
|
+
fps = _fps_from_rate(stream["r_frame_rate"]) or _fps_from_rate(
|
|
84
|
+
stream.get("avg_frame_rate", "0/0")
|
|
115
85
|
)
|
|
116
86
|
if not 0 < fps <= 120:
|
|
117
87
|
fps = 30.0
|
|
@@ -121,27 +91,25 @@ def _geometry_from_stream(stream, *, fallback=False):
|
|
|
121
91
|
"width": width,
|
|
122
92
|
"height": height,
|
|
123
93
|
"fps": round(fps, 3),
|
|
124
|
-
"sample_aspect_ratio": stream.get("sample_aspect_ratio"
|
|
125
|
-
"sample_aspect_ratio_float": round(
|
|
94
|
+
"sample_aspect_ratio": stream.get("sample_aspect_ratio", "1:1"),
|
|
95
|
+
"sample_aspect_ratio_float": round(sar, 6),
|
|
126
96
|
"display_aspect_ratio": stream.get("display_aspect_ratio"),
|
|
127
|
-
"display_aspect_ratio_float": round(
|
|
97
|
+
"display_aspect_ratio_float": round(dar or 0.0, 6),
|
|
128
98
|
"display_aspect_source": aspect_source,
|
|
129
99
|
"display_width": width,
|
|
130
100
|
"display_height": height,
|
|
131
101
|
"rotation": rotation,
|
|
132
102
|
"rotation_swaps_axes": rotation_swaps_axes,
|
|
133
|
-
"fallback": bool(fallback),
|
|
134
103
|
}
|
|
135
104
|
return VideoGeometry(width, height, round(fps, 3), facts)
|
|
136
105
|
|
|
137
106
|
|
|
138
107
|
def _probe_video_geometry(video_path):
|
|
139
|
-
"""
|
|
108
|
+
"""Display geometry (width, height, fps) of the first video stream, rotation/SAR/DAR-aware.
|
|
140
109
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
mismatched width/height/SAR/pixel-format/fps).
|
|
110
|
+
Unpacks like a 3-tuple while exposing `.facts` for QC. Used to normalize heterogeneous
|
|
111
|
+
multi-source segments to one square-pixel geometry before concat (ffmpeg's concat filter
|
|
112
|
+
rejects mismatched width/height/SAR/pixel-format/fps).
|
|
145
113
|
"""
|
|
146
114
|
cmd = [
|
|
147
115
|
"ffprobe",
|
|
@@ -155,38 +123,13 @@ def _probe_video_geometry(video_path):
|
|
|
155
123
|
"json",
|
|
156
124
|
str(video_path),
|
|
157
125
|
]
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
and (result.stdout or "").strip()
|
|
166
|
-
):
|
|
167
|
-
try:
|
|
168
|
-
payload = json.loads(result.stdout)
|
|
169
|
-
streams = payload.get("streams") or []
|
|
170
|
-
if streams:
|
|
171
|
-
return _geometry_from_stream(streams[0])
|
|
172
|
-
except (AttributeError, TypeError, ValueError):
|
|
173
|
-
pass
|
|
174
|
-
|
|
175
|
-
# Backward-compatible fallback for tests/mocks that still return CSV output.
|
|
176
|
-
stream = {}
|
|
177
|
-
if result is not None and (result.stdout or "").strip():
|
|
178
|
-
parts = result.stdout.strip().splitlines()[0].split(",")
|
|
179
|
-
if len(parts) >= 2:
|
|
180
|
-
stream["width"], stream["height"] = parts[0], parts[1]
|
|
181
|
-
if len(parts) >= 3:
|
|
182
|
-
stream["r_frame_rate"] = parts[2]
|
|
183
|
-
if len(parts) >= 4 and parts[3] not in ("", "N/A"):
|
|
184
|
-
stream["tags"] = {"rotate": parts[3]}
|
|
185
|
-
if len(parts) >= 5 and parts[4] not in ("", "N/A"):
|
|
186
|
-
stream["sample_aspect_ratio"] = parts[4]
|
|
187
|
-
if len(parts) >= 6 and parts[5] not in ("", "N/A"):
|
|
188
|
-
stream["display_aspect_ratio"] = parts[5]
|
|
189
|
-
return _geometry_from_stream(stream, fallback=True)
|
|
126
|
+
result = run_cmd(cmd)
|
|
127
|
+
if result.returncode != 0:
|
|
128
|
+
raise RuntimeError(f"ffprobe 无法读取视频几何信息: {video_path}: {result.stderr.strip()}")
|
|
129
|
+
streams = json.loads(result.stdout).get("streams", [])
|
|
130
|
+
if not streams:
|
|
131
|
+
raise RuntimeError(f"没有视频流: {video_path}")
|
|
132
|
+
return _geometry_from_stream(streams[0])
|
|
190
133
|
|
|
191
134
|
|
|
192
135
|
def _orientation(width, height):
|
|
@@ -198,91 +141,53 @@ def _orientation(width, height):
|
|
|
198
141
|
|
|
199
142
|
|
|
200
143
|
def _fps_bucket(fps):
|
|
201
|
-
if not fps or fps <= 0:
|
|
202
|
-
return 30.0
|
|
203
144
|
common = [23.976, 24.0, 25.0, 29.97, 30.0, 50.0, 59.94, 60.0]
|
|
204
|
-
nearest = min(common, key=lambda x: abs(
|
|
205
|
-
|
|
145
|
+
nearest = min(common, key=lambda x: abs(fps - x))
|
|
146
|
+
bucket = nearest if abs(fps - nearest) <= 0.15 else round(fps)
|
|
147
|
+
return max(1.0, min(60.0, bucket))
|
|
206
148
|
|
|
207
149
|
|
|
208
|
-
def _clamp_even_geometry(width, height
|
|
209
|
-
|
|
210
|
-
height = max(2, int(height) - int(height) % 2)
|
|
211
|
-
max_height = int(max_height or 0)
|
|
212
|
-
if max_height > 0 and height > max_height:
|
|
213
|
-
scale = max_height / height
|
|
214
|
-
height = max_height - max_height % 2
|
|
215
|
-
width = max(2, int(width * scale))
|
|
216
|
-
width -= width % 2
|
|
217
|
-
return width, height
|
|
150
|
+
def _clamp_even_geometry(width, height):
|
|
151
|
+
return max(2, width - width % 2), max(2, height - height % 2)
|
|
218
152
|
|
|
219
153
|
|
|
220
|
-
def _select_output_geometry(source_paths, clips
|
|
154
|
+
def _select_output_geometry(source_paths, clips):
|
|
221
155
|
"""Deterministically select canvas/fps from all used sources, not just the first."""
|
|
222
156
|
used = {}
|
|
223
|
-
for clip in clips
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
used
|
|
228
|
-
if not used:
|
|
229
|
-
for path in source_paths or []:
|
|
230
|
-
used[str(path)] = 0.0
|
|
157
|
+
for clip in clips:
|
|
158
|
+
if "source_path" in clip:
|
|
159
|
+
used[clip["source_path"]] = used.get(clip["source_path"], 0.0) + clip["duration"]
|
|
160
|
+
if not used: # single-source plans carry no per-clip source_path
|
|
161
|
+
used = {str(path): 0.0 for path in source_paths}
|
|
231
162
|
rows = []
|
|
232
163
|
for path in sorted(used):
|
|
233
164
|
probed = _probe_video_geometry(path)
|
|
234
165
|
width, height, fps = probed
|
|
235
|
-
facts =
|
|
236
|
-
facts.setdefault("width", width)
|
|
237
|
-
facts.setdefault("height", height)
|
|
238
|
-
facts.setdefault("fps", fps)
|
|
239
|
-
facts.setdefault("rotation", 0)
|
|
240
|
-
facts.setdefault("sample_aspect_ratio", "1:1")
|
|
166
|
+
facts = probed.facts
|
|
241
167
|
rows.append(
|
|
242
168
|
{
|
|
243
169
|
"path": path,
|
|
244
170
|
"source_id": next(
|
|
245
|
-
(
|
|
246
|
-
str(c.get("source_id"))
|
|
247
|
-
for c in clips or []
|
|
248
|
-
if str(c.get("source_path") or "") == path
|
|
249
|
-
and c.get("source_id") is not None
|
|
250
|
-
),
|
|
251
|
-
None,
|
|
171
|
+
(c["source_id"] for c in clips if c.get("source_path") == path), None
|
|
252
172
|
),
|
|
253
173
|
"used_duration": round(used[path], 3),
|
|
254
174
|
"width": width,
|
|
255
175
|
"height": height,
|
|
256
|
-
"coded_width": facts
|
|
257
|
-
"coded_height": facts
|
|
258
|
-
"display_width": facts
|
|
259
|
-
"display_height": facts
|
|
176
|
+
"coded_width": facts["coded_width"],
|
|
177
|
+
"coded_height": facts["coded_height"],
|
|
178
|
+
"display_width": facts["display_width"],
|
|
179
|
+
"display_height": facts["display_height"],
|
|
260
180
|
"area": width * height,
|
|
261
181
|
"fps": fps,
|
|
262
|
-
"fps_bucket":
|
|
182
|
+
"fps_bucket": _fps_bucket(fps),
|
|
263
183
|
"orientation": _orientation(width, height),
|
|
264
|
-
"rotation": facts
|
|
265
|
-
"sample_aspect_ratio": facts
|
|
266
|
-
"sample_aspect_ratio_float": facts
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"display_aspect_ratio": facts.get("display_aspect_ratio"),
|
|
270
|
-
"rotation_swaps_axes": bool(facts.get("rotation_swaps_axes", False)),
|
|
184
|
+
"rotation": facts["rotation"],
|
|
185
|
+
"sample_aspect_ratio": facts["sample_aspect_ratio"],
|
|
186
|
+
"sample_aspect_ratio_float": facts["sample_aspect_ratio_float"],
|
|
187
|
+
"display_aspect_ratio": facts["display_aspect_ratio"],
|
|
188
|
+
"rotation_swaps_axes": facts["rotation_swaps_axes"],
|
|
271
189
|
}
|
|
272
190
|
)
|
|
273
|
-
if not rows:
|
|
274
|
-
return (
|
|
275
|
-
1280,
|
|
276
|
-
720,
|
|
277
|
-
30.0,
|
|
278
|
-
{
|
|
279
|
-
"width": 1280,
|
|
280
|
-
"height": 720,
|
|
281
|
-
"fps": 30.0,
|
|
282
|
-
"reason": "fallback_no_sources",
|
|
283
|
-
"source_id": None,
|
|
284
|
-
},
|
|
285
|
-
)
|
|
286
191
|
|
|
287
192
|
orientation_duration = {}
|
|
288
193
|
for row in rows:
|
|
@@ -298,10 +203,9 @@ def _select_output_geometry(source_paths, clips, max_height=None):
|
|
|
298
203
|
),
|
|
299
204
|
reverse=True,
|
|
300
205
|
)[0][0]
|
|
301
|
-
eligible = [r for r in rows if r["orientation"] == chosen_orientation]
|
|
206
|
+
eligible = [r for r in rows if r["orientation"] == chosen_orientation]
|
|
302
207
|
selected = sorted(
|
|
303
|
-
eligible,
|
|
304
|
-
key=lambda r: (-r["area"], str(r.get("source_id") or ""), r["path"]),
|
|
208
|
+
eligible, key=lambda r: (-r["area"], r["source_id"] or "", r["path"])
|
|
305
209
|
)[0]
|
|
306
210
|
|
|
307
211
|
fps_duration = {}
|
|
@@ -309,33 +213,26 @@ def _select_output_geometry(source_paths, clips, max_height=None):
|
|
|
309
213
|
fps_duration[row["fps_bucket"]] = (
|
|
310
214
|
fps_duration.get(row["fps_bucket"], 0.0) + row["used_duration"]
|
|
311
215
|
)
|
|
312
|
-
fps = sorted(fps_duration.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)[0][
|
|
313
|
-
0
|
|
314
|
-
]
|
|
315
|
-
fps = max(1.0, min(60.0, float(fps or 30.0)))
|
|
216
|
+
fps = sorted(fps_duration.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)[0][0]
|
|
316
217
|
|
|
317
|
-
width, height =
|
|
318
|
-
selected["width"], selected["height"], max_height=max_height
|
|
319
|
-
)
|
|
218
|
+
width, height = selected["width"], selected["height"]
|
|
320
219
|
reason = {
|
|
321
220
|
"width": width,
|
|
322
221
|
"height": height,
|
|
323
222
|
"fps": round(fps, 3),
|
|
324
223
|
"reason": "weighted_orientation_area_fps",
|
|
325
|
-
"source_id": selected
|
|
224
|
+
"source_id": selected["source_id"],
|
|
326
225
|
"source_path": selected["path"],
|
|
327
226
|
"orientation": chosen_orientation,
|
|
328
|
-
"orientation_used_duration": round(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
"
|
|
335
|
-
"
|
|
336
|
-
"
|
|
337
|
-
"display_width": selected.get("display_width"),
|
|
338
|
-
"display_height": selected.get("display_height"),
|
|
227
|
+
"orientation_used_duration": round(orientation_duration[chosen_orientation], 3),
|
|
228
|
+
"fps_bucket_used_duration": round(fps_duration[fps], 3),
|
|
229
|
+
"rotation": selected["rotation"],
|
|
230
|
+
"sample_aspect_ratio": selected["sample_aspect_ratio"],
|
|
231
|
+
"display_aspect_ratio": selected["display_aspect_ratio"],
|
|
232
|
+
"coded_width": selected["coded_width"],
|
|
233
|
+
"coded_height": selected["coded_height"],
|
|
234
|
+
"display_width": selected["display_width"],
|
|
235
|
+
"display_height": selected["display_height"],
|
|
339
236
|
"sources": rows,
|
|
340
237
|
}
|
|
341
238
|
return width, height, round(fps, 3), reason
|
|
@@ -4,51 +4,33 @@ from lib import log
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def source_time_to_output_time(source_time, clips):
|
|
7
|
-
"""Map a source timestamp into the post-concat output timeline."""
|
|
8
|
-
ts = float(source_time)
|
|
7
|
+
"""Map a source timestamp into the post-concat output timeline (None when cut away)."""
|
|
9
8
|
for clip in clips:
|
|
10
9
|
start = clip["source_start"]
|
|
11
10
|
end = clip["source_end"]
|
|
12
|
-
if start <=
|
|
13
|
-
mapped = clip["output_start"] + (
|
|
11
|
+
if start <= source_time <= end:
|
|
12
|
+
mapped = clip["output_start"] + (source_time - start)
|
|
14
13
|
return round(max(clip["output_start"], min(mapped, clip["output_end"])), 3)
|
|
15
14
|
return None
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
def _clips_for_midpoint(start, end, clips):
|
|
19
|
-
mid = (
|
|
18
|
+
mid = (start + end) / 2
|
|
20
19
|
return [clip for clip in clips if clip["source_start"] <= mid <= clip["source_end"]]
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
def map_narration_to_clips(narration, validated_plan, min_duration=0.3):
|
|
24
23
|
"""Convert source-time narration segments to edited-output timeline segments."""
|
|
25
|
-
clips =
|
|
26
|
-
validated_plan["clips"] if isinstance(validated_plan, dict) else validated_plan
|
|
27
|
-
)
|
|
24
|
+
clips = validated_plan["clips"]
|
|
28
25
|
mapped = []
|
|
29
|
-
for raw in narration
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
source_start = float(raw.get("start"))
|
|
34
|
-
source_end = float(raw.get("end"))
|
|
35
|
-
except (TypeError, ValueError):
|
|
36
|
-
continue
|
|
37
|
-
text = str(raw.get("narration", "")).strip()
|
|
38
|
-
if source_end <= source_start or not text:
|
|
39
|
-
continue
|
|
26
|
+
for raw in narration:
|
|
27
|
+
source_start = raw["start"]
|
|
28
|
+
source_end = raw["end"]
|
|
40
29
|
if raw.get("source_clip_id") is not None:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
except (TypeError, ValueError):
|
|
44
|
-
requested_clip_id = None
|
|
45
|
-
clip = next(
|
|
46
|
-
(c for c in clips if c.get("clip_id") == requested_clip_id), None
|
|
47
|
-
)
|
|
30
|
+
requested_clip_id = int(raw["source_clip_id"])
|
|
31
|
+
clip = next((c for c in clips if c["clip_id"] == requested_clip_id), None)
|
|
48
32
|
if clip and not (
|
|
49
|
-
clip["source_start"]
|
|
50
|
-
<= ((source_start + source_end) / 2)
|
|
51
|
-
<= clip["source_end"]
|
|
33
|
+
clip["source_start"] <= (source_start + source_end) / 2 <= clip["source_end"]
|
|
52
34
|
):
|
|
53
35
|
clip = None
|
|
54
36
|
else:
|
|
@@ -67,16 +49,12 @@ def map_narration_to_clips(narration, validated_plan, min_duration=0.3):
|
|
|
67
49
|
if clipped_source_end - clipped_source_start < min_duration:
|
|
68
50
|
log(f" 丢弃过短映射解说: {source_start:.1f}-{source_end:.1f}s")
|
|
69
51
|
continue
|
|
70
|
-
output_start = source_time_to_output_time(clipped_source_start, [clip])
|
|
71
|
-
output_end = source_time_to_output_time(clipped_source_end, [clip])
|
|
72
|
-
if output_start is None or output_end is None or output_end <= output_start:
|
|
73
|
-
continue
|
|
74
52
|
item = dict(raw)
|
|
75
53
|
item["source_start"] = round(clipped_source_start, 3)
|
|
76
54
|
item["source_end"] = round(clipped_source_end, 3)
|
|
77
55
|
item["source_clip_id"] = clip["clip_id"]
|
|
78
|
-
item["start"] =
|
|
79
|
-
item["end"] =
|
|
56
|
+
item["start"] = source_time_to_output_time(clipped_source_start, [clip])
|
|
57
|
+
item["end"] = source_time_to_output_time(clipped_source_end, [clip])
|
|
80
58
|
# Tag beats trimmed to a clip edge: their TEXT was written for a longer span and may
|
|
81
59
|
# now describe footage that was cut away (a stale-text desync the lint surfaces).
|
|
82
60
|
item["clamped"] = bool(
|
|
@@ -108,17 +86,15 @@ def lint_mapped_narration(
|
|
|
108
86
|
enforces unless --allow-sparse-cut. Clamped beats are always blocking because their TTS
|
|
109
87
|
sentence would be cut at a clip edge; sparse-cut intent never authorizes speech truncation.
|
|
110
88
|
"""
|
|
111
|
-
mapped = sorted(mapped
|
|
89
|
+
mapped = sorted(mapped, key=lambda s: s["start"])
|
|
112
90
|
mapped_count = len(mapped)
|
|
113
|
-
original_count = int(original_count or 0)
|
|
114
91
|
dropped = max(0, original_count - mapped_count)
|
|
115
92
|
drop_ratio = dropped / original_count if original_count else 0.0
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
gaps = [float(b["start"]) - float(a["end"]) for a, b in zip(mapped, mapped[1:])]
|
|
93
|
+
spm = mapped_count / (output_duration / 60) if output_duration > 0 else 0.0
|
|
94
|
+
gaps = [b["start"] - a["end"] for a, b in zip(mapped, mapped[1:])]
|
|
119
95
|
max_gap = max(gaps) if gaps else 0.0
|
|
120
|
-
covered = sum(max(0.0,
|
|
121
|
-
coverage = covered /
|
|
96
|
+
covered = sum(max(0.0, b["end"] - b["start"]) for b in mapped)
|
|
97
|
+
coverage = covered / output_duration if output_duration > 0 else 0.0
|
|
122
98
|
|
|
123
99
|
warnings = []
|
|
124
100
|
if drop_ratio >= drop_ratio_limit:
|
|
@@ -149,7 +125,7 @@ def lint_mapped_narration(
|
|
|
149
125
|
"max_gap_limit_seconds": max_gap_seconds,
|
|
150
126
|
}
|
|
151
127
|
)
|
|
152
|
-
clamped = [b for b in mapped if
|
|
128
|
+
clamped = [b for b in mapped if b["clamped"]]
|
|
153
129
|
if clamped:
|
|
154
130
|
warnings.append(
|
|
155
131
|
{
|
|
@@ -168,7 +144,7 @@ def lint_mapped_narration(
|
|
|
168
144
|
"mapped_count": mapped_count,
|
|
169
145
|
"dropped": dropped,
|
|
170
146
|
"drop_ratio": round(drop_ratio, 2),
|
|
171
|
-
"output_duration": round(
|
|
147
|
+
"output_duration": round(output_duration, 2),
|
|
172
148
|
"segments_per_minute": round(spm, 2),
|
|
173
149
|
"max_gap_seconds": round(max_gap, 2),
|
|
174
150
|
"coverage": round(coverage, 2),
|
|
@@ -180,12 +156,12 @@ def lint_mapped_narration(
|
|
|
180
156
|
|
|
181
157
|
def update_cut_qc(plan, *, allow_duration_drift=False, duration_drift_allowed_by=None):
|
|
182
158
|
"""Populate clip_plan_validated.json['qc'] as the single cut QC source."""
|
|
183
|
-
qc = dict(plan.get("qc"
|
|
184
|
-
warnings = list(qc.get("warnings"
|
|
185
|
-
blocking = list(qc.get("blocking"
|
|
186
|
-
total =
|
|
187
|
-
target = plan
|
|
188
|
-
if target
|
|
159
|
+
qc = dict(plan.get("qc", {}))
|
|
160
|
+
warnings = list(qc.get("warnings", []))
|
|
161
|
+
blocking = list(qc.get("blocking", []))
|
|
162
|
+
total = plan["total_duration"]
|
|
163
|
+
target = plan["target_duration"]
|
|
164
|
+
if target is None:
|
|
189
165
|
target_status = "missing"
|
|
190
166
|
target_qc = {
|
|
191
167
|
"status": target_status,
|
|
@@ -193,7 +169,6 @@ def update_cut_qc(plan, *, allow_duration_drift=False, duration_drift_allowed_by
|
|
|
193
169
|
"total_duration": round(total, 3),
|
|
194
170
|
}
|
|
195
171
|
else:
|
|
196
|
-
target = float(target)
|
|
197
172
|
ratio = total / target if target > 0 else 0.0
|
|
198
173
|
if ratio < 0.85:
|
|
199
174
|
target_status = "under"
|
|
@@ -237,13 +212,13 @@ def update_cut_qc(plan, *, allow_duration_drift=False, duration_drift_allowed_by
|
|
|
237
212
|
qc["target_duration_status"] = target_status
|
|
238
213
|
qc["target_duration"] = target_qc
|
|
239
214
|
qc.setdefault("boundary_status", {})
|
|
240
|
-
qc["clip_count"] = len(plan
|
|
215
|
+
qc["clip_count"] = len(plan["clips"])
|
|
241
216
|
qc["total_duration"] = round(total, 3)
|
|
242
217
|
if warnings:
|
|
243
218
|
qc["warnings"] = warnings
|
|
244
219
|
if blocking:
|
|
245
220
|
qc["blocking"] = blocking
|
|
246
|
-
|
|
221
|
+
else:
|
|
247
222
|
qc.pop("blocking", None)
|
|
248
223
|
plan["qc"] = qc
|
|
249
224
|
return plan
|