@oh-story/dsh 0.1.7 → 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 +30 -15
- package/lib/client.js +93 -16
- 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
|
@@ -44,26 +44,25 @@ def _subtitle_style_config(canvas=None):
|
|
|
44
44
|
16:9 source (or the 1280x720 default) reproduces the legacy values exactly.
|
|
45
45
|
"""
|
|
46
46
|
style = {
|
|
47
|
-
"font_name": CONFIG
|
|
48
|
-
"font_size": CONFIG
|
|
49
|
-
"primary_color": CONFIG
|
|
50
|
-
"outline_color": CONFIG
|
|
51
|
-
"outline": CONFIG
|
|
52
|
-
"shadow": CONFIG
|
|
53
|
-
"alignment": CONFIG
|
|
54
|
-
"margin_l": CONFIG
|
|
55
|
-
"margin_r": CONFIG
|
|
56
|
-
"margin_v": CONFIG
|
|
57
|
-
"max_chars": CONFIG
|
|
58
|
-
"play_res_x": CONFIG
|
|
59
|
-
"play_res_y": CONFIG
|
|
47
|
+
"font_name": CONFIG["subtitle_font_name"],
|
|
48
|
+
"font_size": CONFIG["subtitle_font_size"],
|
|
49
|
+
"primary_color": CONFIG["subtitle_primary_color"],
|
|
50
|
+
"outline_color": CONFIG["subtitle_outline_color"],
|
|
51
|
+
"outline": CONFIG["subtitle_outline"],
|
|
52
|
+
"shadow": CONFIG["subtitle_shadow"],
|
|
53
|
+
"alignment": CONFIG["subtitle_alignment"],
|
|
54
|
+
"margin_l": CONFIG["subtitle_margin_l"],
|
|
55
|
+
"margin_r": CONFIG["subtitle_margin_r"],
|
|
56
|
+
"margin_v": CONFIG["subtitle_margin_v"],
|
|
57
|
+
"max_chars": CONFIG["subtitle_max_chars"],
|
|
58
|
+
"play_res_x": CONFIG["subtitle_play_res_x"],
|
|
59
|
+
"play_res_y": CONFIG["subtitle_play_res_y"],
|
|
60
60
|
}
|
|
61
61
|
pinned = "SUBTITLE_PLAY_RES_X" in os.environ or "SUBTITLE_PLAY_RES_Y" in os.environ
|
|
62
|
-
|
|
63
|
-
ch = int((canvas or {}).get("height", 0) or 0)
|
|
64
|
-
if canvas is None or pinned or cw <= 0 or ch <= 0:
|
|
62
|
+
if canvas is None or pinned:
|
|
65
63
|
return style # legacy / manually-pinned: unchanged
|
|
66
64
|
|
|
65
|
+
cw, ch = canvas["width"], canvas["height"]
|
|
67
66
|
base_font = float(style["font_size"])
|
|
68
67
|
kx = cw / float(SUBTITLE_STYLE_REF_W) # horizontal metrics ∝ width
|
|
69
68
|
ky = ch / float(SUBTITLE_STYLE_REF_H) # vertical metrics ∝ height
|
|
@@ -73,9 +72,9 @@ def _subtitle_style_config(canvas=None):
|
|
|
73
72
|
# height-proportional size, then cap so a full line of CJK glyphs (≈1em wide) fits the
|
|
74
73
|
# usable width — this is what keeps portrait text on-screen instead of overflowing.
|
|
75
74
|
usable_w = max(1.0, cw - margin_l - margin_r)
|
|
76
|
-
width_cap = usable_w /
|
|
75
|
+
width_cap = usable_w / int(style["max_chars"])
|
|
77
76
|
font_size = max(1, int(min(base_font * ky, width_cap))) # floor so a full line never overflows
|
|
78
|
-
font_scale = font_size / base_font
|
|
77
|
+
font_scale = font_size / base_font
|
|
79
78
|
style.update({
|
|
80
79
|
"font_size": font_size,
|
|
81
80
|
"outline": max(0, round(float(style["outline"]) * font_scale)),
|
|
@@ -89,48 +88,45 @@ def _subtitle_style_config(canvas=None):
|
|
|
89
88
|
return style
|
|
90
89
|
|
|
91
90
|
|
|
92
|
-
def
|
|
93
|
-
y_top
|
|
94
|
-
|
|
91
|
+
def _measured_subtitle_band(canvas):
|
|
92
|
+
"""Explicit (y_top, y_bot) display-frame subtitle band validated against the canvas, or None."""
|
|
93
|
+
y_top = CONFIG["subtitle_y_top"]
|
|
94
|
+
y_bot = CONFIG["subtitle_y_bot"]
|
|
95
95
|
if y_top < 0 and y_bot < 0:
|
|
96
|
-
return
|
|
97
|
-
sar_text =
|
|
96
|
+
return None
|
|
97
|
+
sar_text = canvas["sample_aspect_ratio"]
|
|
98
98
|
if abs(_ratio_to_float(sar_text, 0.0) - 1.0) >= 1e-9:
|
|
99
99
|
raise ValueError(
|
|
100
100
|
f"字幕带坐标仅支持方形像素画布 (SAR 1:1);当前 SAR={sar_text}"
|
|
101
101
|
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _style_for_measured_subtitle_band(style, canvas=None):
|
|
105
|
-
"""Fit the ASS baseline and font into explicit auto-rotated display-frame Y coordinates."""
|
|
106
|
-
style = dict(style)
|
|
107
|
-
y_top = int(CONFIG.get("subtitle_y_top", -1))
|
|
108
|
-
y_bot = int(CONFIG.get("subtitle_y_bot", -1))
|
|
109
|
-
canvas_h = int((canvas or {}).get("height", 0) or 0)
|
|
110
|
-
if y_top < 0 and y_bot < 0:
|
|
111
|
-
return style
|
|
112
|
-
_validate_measured_subtitle_coordinate_domain(canvas)
|
|
113
|
-
if canvas_h <= 0:
|
|
114
|
-
return style
|
|
102
|
+
canvas_h = canvas["height"]
|
|
115
103
|
if not 0 <= y_top < y_bot <= canvas_h:
|
|
116
104
|
raise ValueError(
|
|
117
105
|
f"字幕带坐标无效: top={y_top}, bot={y_bot}, 画布高度={canvas_h};"
|
|
118
106
|
"必须满足 0 <= top < bot <= height"
|
|
119
107
|
)
|
|
120
|
-
|
|
108
|
+
return y_top, y_bot
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _style_for_measured_subtitle_band(style, canvas):
|
|
112
|
+
"""Fit the ASS baseline and font into explicit auto-rotated display-frame Y coordinates."""
|
|
113
|
+
style = dict(style)
|
|
114
|
+
safe_area = _measured_subtitle_safe_area(style, canvas)
|
|
115
|
+
if safe_area is None:
|
|
116
|
+
return style
|
|
117
|
+
alignment = style["alignment"]
|
|
121
118
|
if alignment not in {1, 2, 3}:
|
|
122
119
|
raise ValueError(
|
|
123
120
|
"measured subtitle coordinates require a bottom-aligned ASS style "
|
|
124
121
|
f"(SUBTITLE_ALIGNMENT 1/2/3); got {alignment}"
|
|
125
122
|
)
|
|
123
|
+
canvas_h = canvas["height"]
|
|
126
124
|
scale_y = float(style["play_res_y"]) / canvas_h
|
|
127
|
-
style["margin_v"] = max(0, round((canvas_h -
|
|
128
|
-
current_font =
|
|
129
|
-
current_outline = float(style
|
|
130
|
-
current_shadow = float(style
|
|
131
|
-
|
|
132
|
-
available_height = int((safe_area or {}).get("height", 0) or 0)
|
|
133
|
-
fitted_font = current_font
|
|
125
|
+
style["margin_v"] = max(0, round((canvas_h - CONFIG["subtitle_y_bot"]) * scale_y))
|
|
126
|
+
current_font = int(style["font_size"])
|
|
127
|
+
current_outline = float(style["outline"])
|
|
128
|
+
current_shadow = float(style["shadow"])
|
|
129
|
+
available_height = safe_area["height"]
|
|
134
130
|
for candidate in range(current_font, 7, -1):
|
|
135
131
|
scale = candidate / current_font
|
|
136
132
|
outline = max(1 if current_outline > 0 else 0, round(current_outline * scale))
|
|
@@ -151,39 +147,27 @@ def _style_for_measured_subtitle_band(style, canvas=None):
|
|
|
151
147
|
return style
|
|
152
148
|
|
|
153
149
|
|
|
154
|
-
def _measured_subtitle_safe_area(style, canvas
|
|
150
|
+
def _measured_subtitle_safe_area(style, canvas):
|
|
155
151
|
"""Return the padded measured band in ASS PlayRes coordinates, or None."""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
canvas_w = int((canvas or {}).get("width", 0) or 0)
|
|
159
|
-
canvas_h = int((canvas or {}).get("height", 0) or 0)
|
|
160
|
-
if y_top < 0 and y_bot < 0:
|
|
152
|
+
band = _measured_subtitle_band(canvas)
|
|
153
|
+
if band is None:
|
|
161
154
|
return None
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if not 0 <= y_top < y_bot <= canvas_h:
|
|
166
|
-
raise ValueError(
|
|
167
|
-
f"字幕带坐标无效: top={y_top}, bot={y_bot}, 画布高度={canvas_h};"
|
|
168
|
-
"必须满足 0 <= top < bot <= height"
|
|
169
|
-
)
|
|
170
|
-
padding = max(0, int(CONFIG.get("subtitle_mask_padding", 4) or 0))
|
|
171
|
-
safe_top = max(0, y_top - padding)
|
|
155
|
+
y_top, y_bot = band
|
|
156
|
+
canvas_h = canvas["height"]
|
|
157
|
+
safe_top = max(0, y_top - CONFIG["subtitle_mask_padding"])
|
|
172
158
|
# The ASS style remains bottom-anchored at the measured y_bot. Bottom mask padding hides
|
|
173
159
|
# source glyph edges but is not usable subtitle layout space; only top padding can extend
|
|
174
160
|
# the line box without moving its baseline below the measured band.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
margin_l = int(style.get("margin_l") or 0)
|
|
180
|
-
margin_r = int(style.get("margin_r") or 0)
|
|
161
|
+
play_x = int(style["play_res_x"])
|
|
162
|
+
scale_y = int(style["play_res_y"]) / canvas_h
|
|
163
|
+
margin_l = int(style["margin_l"])
|
|
164
|
+
margin_r = int(style["margin_r"])
|
|
181
165
|
return {
|
|
182
166
|
"x": margin_l,
|
|
183
167
|
"y": round(safe_top * scale_y),
|
|
184
168
|
"width": max(1, play_x - margin_l - margin_r),
|
|
185
|
-
"height": max(1, round((
|
|
186
|
-
"bottom_margin": max(0, round((canvas_h -
|
|
169
|
+
"height": max(1, round((y_bot - safe_top) * scale_y)),
|
|
170
|
+
"bottom_margin": max(0, round((canvas_h - y_bot) * scale_y)),
|
|
187
171
|
}
|
|
188
172
|
|
|
189
173
|
|
|
@@ -193,9 +177,7 @@ def _subtitle_display_text(text):
|
|
|
193
177
|
Narration/TTS source text stays untouched; this is applied only to SRT/ASS cue text.
|
|
194
178
|
Closing quotes/brackets are preserved, so 「原声台词。」 renders as 「原声台词」.
|
|
195
179
|
"""
|
|
196
|
-
text =
|
|
197
|
-
if not text:
|
|
198
|
-
return ""
|
|
180
|
+
text = text.strip()
|
|
199
181
|
suffix = ""
|
|
200
182
|
while text and text[-1] in _SUBTITLE_CLOSING_QUOTES:
|
|
201
183
|
suffix = text[-1] + suffix
|
|
@@ -206,8 +188,7 @@ def _subtitle_display_text(text):
|
|
|
206
188
|
|
|
207
189
|
def _subtitle_chunk_weight(text):
|
|
208
190
|
"""Weight raw subtitle chunks for timing, independent of display punctuation cleanup."""
|
|
209
|
-
|
|
210
|
-
return max(1, len(core))
|
|
191
|
+
return max(1, len(re.sub(r"\s+", "", text)))
|
|
211
192
|
|
|
212
193
|
|
|
213
194
|
def _subtitle_entry_chunks(raw_chunks):
|
|
@@ -217,9 +198,8 @@ def _subtitle_entry_chunks(raw_chunks):
|
|
|
217
198
|
on the emitted text, while quote-only suffix chunks are folded into the previous cue
|
|
218
199
|
so a closing bracket never renders alone.
|
|
219
200
|
"""
|
|
220
|
-
chunks = [str(c).strip() for c in (raw_chunks or []) if str(c).strip()]
|
|
221
201
|
out = []
|
|
222
|
-
for
|
|
202
|
+
for chunk in raw_chunks:
|
|
223
203
|
display = _subtitle_display_text(chunk)
|
|
224
204
|
if not display:
|
|
225
205
|
continue
|
|
@@ -231,16 +211,11 @@ def _subtitle_entry_chunks(raw_chunks):
|
|
|
231
211
|
return out
|
|
232
212
|
|
|
233
213
|
|
|
234
|
-
def _normalize_subtitle_text(
|
|
214
|
+
def _normalize_subtitle_text(text):
|
|
235
215
|
"""Normalize Chinese em-dashes in burned subtitle text: a run of one-or-more "—" (incl. "——")
|
|
236
216
|
collapses to a single ",". Then collapse any resulting double commas (",,"→",") so the dash
|
|
237
|
-
swap never leaves a doubled comma.
|
|
238
|
-
|
|
239
|
-
if not text:
|
|
240
|
-
return text
|
|
241
|
-
text = re.sub(r"—+", ",", text)
|
|
242
|
-
text = re.sub(r",{2,}", ",", text)
|
|
243
|
-
return text
|
|
217
|
+
swap never leaves a doubled comma."""
|
|
218
|
+
return re.sub(r",{2,}", ",", re.sub(r"—+", ",", text))
|
|
244
219
|
|
|
245
220
|
|
|
246
221
|
def _split_subtitle_chunks(text, max_chars):
|
|
@@ -252,7 +227,7 @@ def _split_subtitle_chunks(text, max_chars):
|
|
|
252
227
|
chunks of at most `max_chars` — each chunk renders as ONE readable line synced to its slice of
|
|
253
228
|
the block's audio. Punctuation stays attached here for lossless splitting; the display layer
|
|
254
229
|
strips terminal sentence marks per subtitle-cue style."""
|
|
255
|
-
text =
|
|
230
|
+
text = text.strip()
|
|
256
231
|
if not text:
|
|
257
232
|
return []
|
|
258
233
|
breakers = ",。!?、;:…—,.!?;:"
|
|
@@ -298,42 +273,14 @@ def _subtitle_entries(narration):
|
|
|
298
273
|
Each placed segment is split into short one-line chunks and its played window
|
|
299
274
|
[actual_place_start, actual_place_end] is distributed across them in proportion to character
|
|
300
275
|
count — karaoke-style timing that keeps each line on screen only while it is roughly being
|
|
301
|
-
spoken, instead of holding a whole paragraph for the segment's full duration.
|
|
302
|
-
|
|
276
|
+
spoken, instead of holding a whole paragraph for the segment's full duration. Segments that
|
|
277
|
+
were not placed have a zero-width window and therefore produce no cue."""
|
|
278
|
+
max_chars = CONFIG["subtitle_max_chars"]
|
|
303
279
|
entries = []
|
|
304
280
|
for seg in narration:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
text
|
|
308
|
-
if not text:
|
|
309
|
-
continue
|
|
310
|
-
try:
|
|
311
|
-
start = float(seg.get("actual_place_start", seg["start"]))
|
|
312
|
-
end = float(seg.get("actual_place_end", seg["end"]))
|
|
313
|
-
except (KeyError, TypeError, ValueError):
|
|
314
|
-
continue
|
|
315
|
-
if end - start < 0.1:
|
|
316
|
-
continue
|
|
317
|
-
chunks = _subtitle_entry_chunks(_split_subtitle_chunks(text, max_chars))
|
|
318
|
-
if not chunks:
|
|
319
|
-
continue
|
|
320
|
-
if len(chunks) == 1:
|
|
321
|
-
entries.append({"start": start, "end": end, "text": chunks[0]["text"]})
|
|
322
|
-
continue
|
|
323
|
-
total_chars = sum(_subtitle_chunk_weight(c["raw"]) for c in chunks) or 1
|
|
324
|
-
span = end - start
|
|
325
|
-
cursor = start
|
|
326
|
-
for i, chunk in enumerate(chunks):
|
|
327
|
-
weight = _subtitle_chunk_weight(chunk["raw"])
|
|
328
|
-
chunk_end = end if i == len(chunks) - 1 else cursor + span * (weight / total_chars)
|
|
329
|
-
if i > 0 and chunk_end - cursor < 0.05:
|
|
330
|
-
# slice too short to show on its own — fold the text into the previous line of THIS
|
|
331
|
-
# block (i>0) and extend its end, so no chunk is ever silently dropped.
|
|
332
|
-
entries[-1]["text"] += chunk["text"]
|
|
333
|
-
entries[-1]["end"] = chunk_end
|
|
334
|
-
else:
|
|
335
|
-
entries.append({"start": cursor, "end": chunk_end, "text": chunk["text"]})
|
|
336
|
-
cursor = chunk_end
|
|
281
|
+
text = seg.get("spoken_text", seg["narration"])
|
|
282
|
+
start, end = float(seg["actual_place_start"]), float(seg["actual_place_end"])
|
|
283
|
+
entries.extend(_distribute_chunks(_split_subtitle_chunks(text, max_chars), start, end))
|
|
337
284
|
return entries
|
|
338
285
|
|
|
339
286
|
|
|
@@ -341,14 +288,15 @@ def _distribute_chunks(chunks, start, end):
|
|
|
341
288
|
"""Distribute [start,end] across raw chunks while emitting display-clean text.
|
|
342
289
|
|
|
343
290
|
Terminal subtitle punctuation is visual-only: it is stripped from final cue text,
|
|
344
|
-
but the raw split chunks remain the timing topology.
|
|
291
|
+
but the raw split chunks remain the timing topology. A slice too short to show on its
|
|
292
|
+
own is folded into the previous line of the same block, so no chunk is ever dropped.
|
|
345
293
|
"""
|
|
346
294
|
chunks = _subtitle_entry_chunks(chunks)
|
|
347
295
|
if not chunks or end - start < 0.1:
|
|
348
296
|
return []
|
|
349
297
|
if len(chunks) == 1:
|
|
350
298
|
return [{"start": start, "end": end, "text": chunks[0]["text"]}]
|
|
351
|
-
total_chars = sum(_subtitle_chunk_weight(c["raw"]) for c in chunks)
|
|
299
|
+
total_chars = sum(_subtitle_chunk_weight(c["raw"]) for c in chunks)
|
|
352
300
|
span = end - start
|
|
353
301
|
out, cursor = [], start
|
|
354
302
|
for i, chunk in enumerate(chunks):
|
|
@@ -365,12 +313,11 @@ def _distribute_chunks(chunks, start, end):
|
|
|
365
313
|
|
|
366
314
|
def _bracketed_original_chunks(text, start, end, max_chars):
|
|
367
315
|
"""Split original dialogue into timed chunks wrapped in 「」 for visual distinction."""
|
|
368
|
-
raw =
|
|
316
|
+
raw = text.strip()
|
|
369
317
|
if raw.startswith("「") and raw.endswith("」"):
|
|
370
318
|
raw = raw[1:-1].strip()
|
|
371
319
|
chunks = _split_subtitle_chunks(raw, max_chars)
|
|
372
320
|
if chunks:
|
|
373
|
-
chunks = list(chunks)
|
|
374
321
|
chunks[0] = "「" + chunks[0]
|
|
375
322
|
chunks[-1] = chunks[-1] + "」"
|
|
376
323
|
return _distribute_chunks(chunks, start, end)
|
|
@@ -6,21 +6,16 @@ from subtitle_core import (
|
|
|
6
6
|
_seconds_to_ass_time,
|
|
7
7
|
_seconds_to_srt_time,
|
|
8
8
|
_style_for_measured_subtitle_band,
|
|
9
|
-
_subtitle_entries,
|
|
10
9
|
_subtitle_style_config,
|
|
11
10
|
)
|
|
12
11
|
|
|
13
|
-
def _generate_srt(narration, work_dir, video_duration
|
|
14
|
-
"""将解说脚本转为 SRT
|
|
12
|
+
def _generate_srt(narration, work_dir, video_duration):
|
|
13
|
+
"""将解说脚本转为 SRT 字幕文件,使用实际音频放置时间;原声留白处补烧原声字幕。"""
|
|
15
14
|
srt_lines = []
|
|
16
|
-
entries = (_subtitle_entries(narration) if video_duration is None
|
|
17
|
-
else _combined_subtitle_entries(narration, work_dir, video_duration))
|
|
18
15
|
# entries are already split into short one-line chunks, so no wrapping here.
|
|
19
|
-
for idx, entry in enumerate(
|
|
20
|
-
start_ts = _seconds_to_srt_time(entry["start"])
|
|
21
|
-
end_ts = _seconds_to_srt_time(entry["end"])
|
|
16
|
+
for idx, entry in enumerate(_combined_subtitle_entries(narration, work_dir, video_duration), start=1):
|
|
22
17
|
srt_lines.append(str(idx))
|
|
23
|
-
srt_lines.append(f"{
|
|
18
|
+
srt_lines.append(f"{_seconds_to_srt_time(entry['start'])} --> {_seconds_to_srt_time(entry['end'])}")
|
|
24
19
|
srt_lines.append(_normalize_subtitle_text(entry["text"]))
|
|
25
20
|
srt_lines.append("")
|
|
26
21
|
srt_path = work_dir / "subtitles.srt"
|
|
@@ -31,7 +26,7 @@ def _generate_srt(narration, work_dir, video_duration=None):
|
|
|
31
26
|
def _escape_ass_text(text):
|
|
32
27
|
"""Escape user text for an ASS dialogue Text field."""
|
|
33
28
|
return (
|
|
34
|
-
|
|
29
|
+
text
|
|
35
30
|
.replace("\\", "\\\\")
|
|
36
31
|
.replace("{", "\\{")
|
|
37
32
|
.replace("}", "\\}")
|
|
@@ -41,10 +36,10 @@ def _escape_ass_text(text):
|
|
|
41
36
|
)
|
|
42
37
|
|
|
43
38
|
|
|
44
|
-
def _generate_ass(narration, work_dir, video_duration
|
|
45
|
-
"""Generate an ASS subtitle file for readable hard-sub rendering
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
def _generate_ass(narration, work_dir, video_duration, canvas):
|
|
40
|
+
"""Generate an ASS subtitle file for readable hard-sub rendering, including the original
|
|
41
|
+
dialogue during the original-audio gaps. canvas ({"width","height"}) scales the style to the
|
|
42
|
+
real frame so portrait/竖屏 subtitles are not stretched."""
|
|
48
43
|
style = _style_for_measured_subtitle_band(_subtitle_style_config(canvas), canvas)
|
|
49
44
|
ass_lines = [
|
|
50
45
|
"[Script Info]",
|
|
@@ -72,10 +67,8 @@ def _generate_ass(narration, work_dir, video_duration=None, canvas=None):
|
|
|
72
67
|
"[Events]",
|
|
73
68
|
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
|
|
74
69
|
]
|
|
75
|
-
entries = (_subtitle_entries(narration) if video_duration is None
|
|
76
|
-
else _combined_subtitle_entries(narration, work_dir, video_duration))
|
|
77
70
|
# entries are already split into short one-line chunks, so no wrapping here.
|
|
78
|
-
for entry in
|
|
71
|
+
for entry in _combined_subtitle_entries(narration, work_dir, video_duration):
|
|
79
72
|
text = _escape_ass_text(_normalize_subtitle_text(entry["text"]))
|
|
80
73
|
ass_lines.append(
|
|
81
74
|
"Dialogue: 0,"
|
|
@@ -38,8 +38,8 @@ def _ceil_time(value, digits=4):
|
|
|
38
38
|
|
|
39
39
|
def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
40
40
|
bgm=None, ducking=None, subtitle_segments=None,
|
|
41
|
-
image_segments=
|
|
42
|
-
style_presets=None, extra_tracks=
|
|
41
|
+
image_segments=(), resource_packages=None,
|
|
42
|
+
style_presets=None, extra_tracks=()):
|
|
43
43
|
"""Assemble a Timeline dict from resolved placement data.
|
|
44
44
|
|
|
45
45
|
canvas: {"width", "height", "fps"}
|
|
@@ -48,7 +48,8 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
48
48
|
"timeline_start", "timeline_end"}] (cut mode: one per clip;
|
|
49
49
|
full mode: a single clip spanning the whole video).
|
|
50
50
|
narration_segments: placed beats [{"source_path", "timeline_start",
|
|
51
|
-
"timeline_end", "text", "overlaps_speech", "gain"?}]
|
|
51
|
+
"timeline_end", "text", "overlaps_speech", "gain"?}]; a zero-width
|
|
52
|
+
beat (an unplaced segment) is skipped.
|
|
52
53
|
subtitle_segments: optional display-ready text cues [{"text", "timeline_start",
|
|
53
54
|
"timeline_end"}]. When present, this is authoritative for the
|
|
54
55
|
subtitle/text track; narration segment text remains raw editor metadata.
|
|
@@ -57,27 +58,21 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
57
58
|
automation; None disables original ducking (flat original). `bridge` holds
|
|
58
59
|
the duck across inter-beat gaps shorter than it (defaults to 2*fade).
|
|
59
60
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
duck_windows = [
|
|
64
|
-
(
|
|
65
|
-
float(s["timeline_start"]),
|
|
66
|
-
max(float(s["timeline_end"]), float(s.get("source_duck_end", s["timeline_end"]))),
|
|
67
|
-
float((ducking or {}).get("speech" if s.get("overlaps_speech", True) else "quiet", 1.0)),
|
|
68
|
-
max(
|
|
69
|
-
float(s["timeline_end"]),
|
|
70
|
-
float(s.get("source_duck_end", s["timeline_end"])),
|
|
71
|
-
float(s.get(
|
|
72
|
-
"source_restore_at",
|
|
73
|
-
max(float(s["timeline_end"]), float(s.get("source_duck_end", s["timeline_end"])))
|
|
74
|
-
+ float((ducking or {}).get("fade", 0.0)),
|
|
75
|
-
)),
|
|
76
|
-
),
|
|
77
|
-
)
|
|
78
|
-
for s in narration_segments
|
|
79
|
-
if s.get("timeline_end", 0) > s.get("timeline_start", 0)
|
|
61
|
+
placed = [
|
|
62
|
+
s for s in narration_segments
|
|
63
|
+
if float(s["timeline_end"]) > float(s["timeline_start"])
|
|
80
64
|
]
|
|
65
|
+
windows = [(float(s["timeline_start"]), float(s["timeline_end"])) for s in placed]
|
|
66
|
+
duck_windows = []
|
|
67
|
+
if ducking is not None:
|
|
68
|
+
for s in placed:
|
|
69
|
+
end = float(s["timeline_end"])
|
|
70
|
+
hold_end = max(end, float(s.get("source_duck_end", end)))
|
|
71
|
+
restore_at = max(
|
|
72
|
+
hold_end, float(s.get("source_restore_at", hold_end + float(ducking["fade"])))
|
|
73
|
+
)
|
|
74
|
+
level = float(ducking["speech" if s.get("overlaps_speech", True) else "quiet"])
|
|
75
|
+
duck_windows.append((float(s["timeline_start"]), hold_end, level, restore_at))
|
|
81
76
|
|
|
82
77
|
# --- video track: each clip carries its original audio + ducking automation
|
|
83
78
|
video_clip_objs = []
|
|
@@ -112,14 +107,11 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
112
107
|
|
|
113
108
|
# --- narration track
|
|
114
109
|
narr_segs = []
|
|
115
|
-
for s in
|
|
116
|
-
ts, te = float(s["timeline_start"]), float(s["timeline_end"])
|
|
117
|
-
if te <= ts:
|
|
118
|
-
continue
|
|
110
|
+
for s in placed:
|
|
119
111
|
narration = {
|
|
120
112
|
"source_path": s["source_path"],
|
|
121
|
-
"timeline_start": round(
|
|
122
|
-
"timeline_end": _ceil_time(
|
|
113
|
+
"timeline_start": round(float(s["timeline_start"]), 4),
|
|
114
|
+
"timeline_end": _ceil_time(s["timeline_end"], 4),
|
|
123
115
|
"gain": round(float(s.get("gain", 1.0)), 4),
|
|
124
116
|
"text": s.get("text", ""),
|
|
125
117
|
"overlaps_speech": bool(s.get("overlaps_speech", True)),
|
|
@@ -138,7 +130,7 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
138
130
|
if bgm and bgm.get("source_path"):
|
|
139
131
|
base = float(bgm.get("volume", 0.18))
|
|
140
132
|
duck = float(bgm.get("ducking_volume", 0.10))
|
|
141
|
-
fade = float(bgm.get("fade"
|
|
133
|
+
fade = float(bgm.get("fade", (ducking or {}).get("fade", 0.25)))
|
|
142
134
|
kfs = ducking_keyframes(windows, base, duck, fade, 0.0, duration_s,
|
|
143
135
|
bridge=(ducking or {}).get("bridge"))
|
|
144
136
|
tracks.append({
|
|
@@ -152,21 +144,17 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
152
144
|
}],
|
|
153
145
|
})
|
|
154
146
|
|
|
155
|
-
# --- subtitle (text) track
|
|
147
|
+
# --- subtitle (text) track: empty cues carry nothing to display
|
|
156
148
|
text_source = subtitle_segments if subtitle_segments is not None else narration_segments
|
|
157
149
|
text_segs = []
|
|
158
|
-
for s in text_source
|
|
159
|
-
if not
|
|
160
|
-
continue
|
|
161
|
-
try:
|
|
162
|
-
ts = float(s["timeline_start"])
|
|
163
|
-
te = float(s["timeline_end"])
|
|
164
|
-
except (KeyError, TypeError, ValueError):
|
|
150
|
+
for s in text_source:
|
|
151
|
+
if not s.get("text"):
|
|
165
152
|
continue
|
|
153
|
+
ts, te = float(s["timeline_start"]), float(s["timeline_end"])
|
|
166
154
|
if te <= ts:
|
|
167
155
|
continue
|
|
168
156
|
text_segment = {
|
|
169
|
-
"text": s
|
|
157
|
+
"text": s["text"],
|
|
170
158
|
"timeline_start": round(ts, 4),
|
|
171
159
|
"timeline_end": round(te, 4),
|
|
172
160
|
}
|
|
@@ -179,15 +167,15 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
179
167
|
|
|
180
168
|
# --- local image overlays (optional, timeline schema v2)
|
|
181
169
|
images = []
|
|
182
|
-
for segment in image_segments
|
|
170
|
+
for segment in image_segments:
|
|
183
171
|
if not isinstance(segment, dict) or not segment.get("source_path"):
|
|
184
172
|
continue
|
|
185
173
|
try:
|
|
186
|
-
ts = float(segment["timeline_start"])
|
|
187
|
-
te = float(segment["timeline_end"])
|
|
174
|
+
ts = max(0.0, float(segment["timeline_start"]))
|
|
175
|
+
te = min(float(duration_s), float(segment["timeline_end"]))
|
|
188
176
|
except (KeyError, TypeError, ValueError):
|
|
189
177
|
continue
|
|
190
|
-
if te <= ts:
|
|
178
|
+
if not math.isfinite(ts) or not math.isfinite(te) or te <= ts:
|
|
191
179
|
continue
|
|
192
180
|
scale = segment.get("scale") if isinstance(segment.get("scale"), dict) else {}
|
|
193
181
|
position = segment.get("position") if isinstance(segment.get("position"), dict) else {}
|
|
@@ -218,15 +206,12 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
218
206
|
if images:
|
|
219
207
|
tracks.append({"kind": "image", "name": "image", "segments": images})
|
|
220
208
|
|
|
221
|
-
for track in extra_tracks
|
|
222
|
-
if not isinstance(track, dict):
|
|
223
|
-
raise TypeError("extra_tracks entries must be objects")
|
|
224
|
-
tracks.append(deepcopy(track))
|
|
209
|
+
tracks.extend(deepcopy(track) for track in extra_tracks)
|
|
225
210
|
|
|
226
211
|
timeline = {
|
|
227
212
|
"schema_version": SCHEMA_VERSION,
|
|
228
213
|
"canvas": {"width": int(canvas["width"]), "height": int(canvas["height"]),
|
|
229
|
-
"fps": float(canvas
|
|
214
|
+
"fps": float(canvas["fps"])},
|
|
230
215
|
"duration": round(float(duration_s), 4),
|
|
231
216
|
"tracks": tracks,
|
|
232
217
|
}
|
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from audio_mix import _seg_place_window
|
|
6
6
|
from lib import CONFIG, log
|
|
7
|
-
from media import _build_video_clips
|
|
7
|
+
from media import _build_video_clips
|
|
8
8
|
from source_subtitles import _combined_subtitle_entries
|
|
9
9
|
from timeline import build_timeline, save_timeline
|
|
10
10
|
|
|
@@ -25,14 +25,11 @@ def _timeline_subtitle_segments(tts_segments, work_dir, duration_s):
|
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def _emit_timeline(input_video, tts_segments, work_dir, duration_s, has_bgm):
|
|
28
|
+
def _emit_timeline(input_video, tts_segments, work_dir, duration_s, canvas, has_bgm):
|
|
29
29
|
"""Build and persist the backend-neutral multi-track timeline.json."""
|
|
30
|
-
canvas = _probe_canvas(input_video)
|
|
31
30
|
video_clips = _build_video_clips(input_video, work_dir, duration_s)
|
|
32
31
|
narration_segments = []
|
|
33
32
|
for seg in tts_segments:
|
|
34
|
-
if not isinstance(seg, dict):
|
|
35
|
-
continue
|
|
36
33
|
s, e = _seg_place_window(seg)
|
|
37
34
|
if e <= s:
|
|
38
35
|
continue
|
|
@@ -40,9 +37,9 @@ def _emit_timeline(input_video, tts_segments, work_dir, duration_s, has_bgm):
|
|
|
40
37
|
# JianYing must consume the exact WAV written into narration.wav. In
|
|
41
38
|
# particular, a tempo-adjusted beat cannot reference its longer pre-fit
|
|
42
39
|
# source or the editor will trim its final words at timeline_end.
|
|
43
|
-
"source_path": seg
|
|
40
|
+
"source_path": seg["placed_audio_path"],
|
|
44
41
|
"timeline_start": s, "timeline_end": e,
|
|
45
|
-
"text": seg
|
|
42
|
+
"text": seg["narration"],
|
|
46
43
|
"overlaps_speech": seg.get("overlaps_speech", True),
|
|
47
44
|
"gain": 1.0,
|
|
48
45
|
}
|
|
@@ -50,31 +47,28 @@ def _emit_timeline(input_video, tts_segments, work_dir, duration_s, has_bgm):
|
|
|
50
47
|
if key in seg:
|
|
51
48
|
narration_item[key] = seg[key]
|
|
52
49
|
narration_segments.append(narration_item)
|
|
53
|
-
fade = CONFIG
|
|
50
|
+
fade = CONFIG["duck_fade_seconds"]
|
|
54
51
|
bgm = None
|
|
55
52
|
if has_bgm:
|
|
56
|
-
bgm = {"source_path": CONFIG
|
|
57
|
-
"volume": CONFIG
|
|
58
|
-
"ducking_volume": CONFIG
|
|
53
|
+
bgm = {"source_path": CONFIG["bgm_path"],
|
|
54
|
+
"volume": CONFIG["bgm_volume"],
|
|
55
|
+
"ducking_volume": CONFIG["bgm_ducking_volume"],
|
|
59
56
|
"fade": fade}
|
|
60
57
|
# carry ducking automation whenever ducking is on at all; even under sidechain
|
|
61
58
|
# mode the draft gets editable volume keyframes (ffmpeg stays the canonical mix)
|
|
62
59
|
ducking = None
|
|
63
|
-
if CONFIG
|
|
64
|
-
ducking = {"idle": CONFIG
|
|
65
|
-
"speech": CONFIG
|
|
66
|
-
"quiet": CONFIG
|
|
60
|
+
if CONFIG["ducking_mode"] != "none":
|
|
61
|
+
ducking = {"idle": CONFIG["idle_orig_volume"],
|
|
62
|
+
"speech": CONFIG["speech_ducking_volume"],
|
|
63
|
+
"quiet": CONFIG["zone_ducking_volume"],
|
|
67
64
|
"fade": fade,
|
|
68
|
-
"bridge": CONFIG
|
|
65
|
+
"bridge": CONFIG["duck_bridge_seconds"]}
|
|
69
66
|
subtitle_segments = _timeline_subtitle_segments(tts_segments, work_dir, duration_s)
|
|
70
67
|
timeline = build_timeline(canvas, duration_s, video_clips,
|
|
71
68
|
narration_segments, bgm=bgm, ducking=ducking,
|
|
72
69
|
subtitle_segments=subtitle_segments)
|
|
73
70
|
degraded = [
|
|
74
|
-
{
|
|
75
|
-
"source_path": clip.get("source_path"),
|
|
76
|
-
"reason": clip.get("provenance_reason") or "unknown",
|
|
77
|
-
}
|
|
71
|
+
{"source_path": clip["source_path"], "reason": clip["provenance_reason"]}
|
|
78
72
|
for clip in video_clips
|
|
79
73
|
if clip.get("provenance_degraded")
|
|
80
74
|
]
|