@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
|
@@ -10,7 +10,6 @@ from assemble_constants import (
|
|
|
10
10
|
VISUAL_OVERLAYS,
|
|
11
11
|
VISUAL_QC,
|
|
12
12
|
_SUPPORTED_VISUAL_OVERLAY_TYPES,
|
|
13
|
-
_VISUAL_DELIVERY_FORBIDDEN_KEYS,
|
|
14
13
|
)
|
|
15
14
|
from audio_automation import coalesce_duck_windows
|
|
16
15
|
from audio_mix import _seg_place_window
|
|
@@ -21,17 +20,24 @@ from source_subtitles import (
|
|
|
21
20
|
_source_subtitle_mask_policy,
|
|
22
21
|
)
|
|
23
22
|
from subtitle_core import (
|
|
23
|
+
_measured_subtitle_band,
|
|
24
24
|
_measured_subtitle_safe_area,
|
|
25
25
|
_normalize_subtitle_text,
|
|
26
26
|
_style_for_measured_subtitle_band,
|
|
27
27
|
_subtitle_style_config,
|
|
28
|
-
_validate_measured_subtitle_coordinate_domain,
|
|
29
28
|
)
|
|
30
29
|
|
|
30
|
+
_OVERFLOW_KINDS = {
|
|
31
|
+
"max_lines_exceeded": "line_count",
|
|
32
|
+
"safe_width_exceeded": "line_width",
|
|
33
|
+
"safe_height_exceeded": "safe_area",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
31
37
|
def _visual_text_units(text):
|
|
32
38
|
"""Approximate visual text width in em units for deterministic geometry QC."""
|
|
33
39
|
units = 0.0
|
|
34
|
-
for ch in
|
|
40
|
+
for ch in text:
|
|
35
41
|
if ch.isspace():
|
|
36
42
|
units += 0.35
|
|
37
43
|
elif ord(ch) < 128:
|
|
@@ -41,51 +47,37 @@ def _visual_text_units(text):
|
|
|
41
47
|
return units
|
|
42
48
|
|
|
43
49
|
|
|
44
|
-
def _subtitle_layout_qc(entries,
|
|
50
|
+
def _subtitle_layout_qc(entries, style, safe_area=None):
|
|
45
51
|
"""Machine-check subtitle safe-area/multiline/overflow facts for visual_qc.json."""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
max_lines = int(CONFIG.get("subtitle_max_lines", 2) or 2)
|
|
55
|
-
usable_w = max(1.0, play_x - margin_l - margin_r)
|
|
56
|
-
if safe_area:
|
|
52
|
+
play_x = int(style["play_res_x"])
|
|
53
|
+
play_y = int(style["play_res_y"])
|
|
54
|
+
margin_l = int(style["margin_l"])
|
|
55
|
+
margin_r = int(style["margin_r"])
|
|
56
|
+
margin_v = int(style["margin_v"])
|
|
57
|
+
font_size = float(style["font_size"])
|
|
58
|
+
max_lines = CONFIG["subtitle_max_lines"]
|
|
59
|
+
if safe_area is None:
|
|
57
60
|
safe_area = {
|
|
58
|
-
"x":
|
|
59
|
-
"y":
|
|
60
|
-
"width":
|
|
61
|
-
"height":
|
|
61
|
+
"x": margin_l,
|
|
62
|
+
"y": margin_v,
|
|
63
|
+
"width": max(1, play_x - margin_l - margin_r),
|
|
64
|
+
"height": max(1, play_y - 2 * margin_v),
|
|
62
65
|
"bottom_margin": margin_v,
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
else:
|
|
66
|
-
safe_area = {
|
|
67
|
-
"x": margin_l,
|
|
68
|
-
"y": margin_v,
|
|
69
|
-
"width": max(1, play_x - margin_l - margin_r),
|
|
70
|
-
"height": max(1, play_y - 2 * margin_v),
|
|
71
|
-
"bottom_margin": margin_v,
|
|
72
|
-
}
|
|
67
|
+
usable_w = float(safe_area["width"])
|
|
73
68
|
line_h = font_size * 1.25
|
|
74
69
|
overflow_entries = []
|
|
75
70
|
violations = []
|
|
76
71
|
multi_line_entries = []
|
|
77
72
|
max_observed_lines = 0
|
|
78
73
|
entry_facts = []
|
|
79
|
-
for i, entry in enumerate(entries
|
|
80
|
-
raw_text = _normalize_subtitle_text(entry
|
|
81
|
-
lines = [ln for ln in re.split(r"(?:\\N|\n)+", raw_text) if ln != ""]
|
|
82
|
-
if not lines:
|
|
83
|
-
lines = [""]
|
|
74
|
+
for i, entry in enumerate(entries):
|
|
75
|
+
raw_text = _normalize_subtitle_text(entry["text"])
|
|
76
|
+
lines = [ln for ln in re.split(r"(?:\\N|\n)+", raw_text) if ln != ""] or [""]
|
|
84
77
|
line_count = len(lines)
|
|
85
78
|
max_observed_lines = max(max_observed_lines, line_count)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
band_h = line_count * line_h + float(style.get("outline", 0)) * 2 + float(style.get("shadow", 0))
|
|
79
|
+
max_w = max(_visual_text_units(line) * font_size for line in lines)
|
|
80
|
+
band_h = line_count * line_h + float(style["outline"]) * 2 + float(style["shadow"])
|
|
89
81
|
overflow_reasons = []
|
|
90
82
|
if line_count > max_lines:
|
|
91
83
|
overflow_reasons.append("max_lines_exceeded")
|
|
@@ -95,8 +87,8 @@ def _subtitle_layout_qc(entries, canvas=None, style=None, safe_area=None):
|
|
|
95
87
|
overflow_reasons.append("safe_height_exceeded")
|
|
96
88
|
fact = {
|
|
97
89
|
"index": i,
|
|
98
|
-
"start": round(float(entry
|
|
99
|
-
"end": round(float(entry
|
|
90
|
+
"start": round(float(entry["start"]), 3),
|
|
91
|
+
"end": round(float(entry["end"]), 3),
|
|
100
92
|
"line_count": line_count,
|
|
101
93
|
"max_line_width": round(max_w, 2),
|
|
102
94
|
"safe_width": round(usable_w, 2),
|
|
@@ -109,23 +101,20 @@ def _subtitle_layout_qc(entries, canvas=None, style=None, safe_area=None):
|
|
|
109
101
|
multi_line_entries.append(i)
|
|
110
102
|
if overflow_reasons:
|
|
111
103
|
overflow_entries.append(fact)
|
|
112
|
-
|
|
113
|
-
kind
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"safe_height_exceeded": "safe_area",
|
|
117
|
-
}.get(reason, "safe_area")
|
|
118
|
-
violations.append({"index": i, "kind": kind, "reason": reason})
|
|
104
|
+
violations.extend(
|
|
105
|
+
{"index": i, "kind": _OVERFLOW_KINDS[reason], "reason": reason}
|
|
106
|
+
for reason in overflow_reasons
|
|
107
|
+
)
|
|
119
108
|
return {
|
|
120
|
-
"enabled":
|
|
121
|
-
"renderer": "ass" if CONFIG
|
|
109
|
+
"enabled": CONFIG["burn_subtitles"],
|
|
110
|
+
"renderer": "ass" if CONFIG["burn_subtitles"] else "sidecar_srt",
|
|
122
111
|
"style": {
|
|
123
112
|
"font_size": int(font_size),
|
|
124
|
-
"max_chars": int(style
|
|
113
|
+
"max_chars": int(style["max_chars"]),
|
|
125
114
|
"max_lines": max_lines,
|
|
126
115
|
"play_res_x": play_x,
|
|
127
116
|
"play_res_y": play_y,
|
|
128
|
-
"alignment": int(style
|
|
117
|
+
"alignment": int(style["alignment"]),
|
|
129
118
|
"margin_l": margin_l,
|
|
130
119
|
"margin_r": margin_r,
|
|
131
120
|
"margin_v": margin_v,
|
|
@@ -143,45 +132,35 @@ def _subtitle_layout_qc(entries, canvas=None, style=None, safe_area=None):
|
|
|
143
132
|
}
|
|
144
133
|
|
|
145
134
|
|
|
146
|
-
def _load_visual_overlays(work_dir
|
|
135
|
+
def _load_visual_overlays(work_dir):
|
|
136
|
+
"""Return (overlays, source) from the canonical visual_overlays.json handoff."""
|
|
147
137
|
path = Path(work_dir) / VISUAL_OVERLAYS
|
|
148
138
|
if not path.exists():
|
|
149
|
-
|
|
150
|
-
return result if with_source else result[0]
|
|
139
|
+
return [], {"present": False, "path": str(path), "fingerprint": None}
|
|
151
140
|
try:
|
|
152
141
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
153
|
-
except
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
142
|
+
except json.JSONDecodeError as exc:
|
|
143
|
+
raise ValueError(f"{path}: JSON 无效") from exc
|
|
144
|
+
if (
|
|
145
|
+
not isinstance(data, dict)
|
|
146
|
+
or type(data.get("schema_version")) is not int
|
|
147
|
+
or data["schema_version"] != 1
|
|
148
|
+
or not isinstance(data.get("overlays"), list)
|
|
149
|
+
or not all(isinstance(item, dict) for item in data["overlays"])
|
|
150
|
+
):
|
|
151
|
+
raise ValueError(f"{path}: visual_overlays.json schema 无效")
|
|
162
152
|
source = {
|
|
163
153
|
"present": True,
|
|
164
154
|
"path": str(path),
|
|
165
155
|
"fingerprint": _artifact_fingerprint(path),
|
|
166
|
-
"schema_version":
|
|
156
|
+
"schema_version": 1,
|
|
167
157
|
}
|
|
168
|
-
|
|
169
|
-
valid_schema_version = (
|
|
170
|
-
isinstance(schema_version, int)
|
|
171
|
-
and not isinstance(schema_version, bool)
|
|
172
|
-
and schema_version == 1
|
|
173
|
-
)
|
|
174
|
-
if isinstance(data, dict) and valid_schema_version and isinstance(data.get("overlays"), list):
|
|
175
|
-
overlays = data["overlays"]
|
|
176
|
-
else:
|
|
177
|
-
overlays = []
|
|
178
|
-
source["load_error"] = "invalid_schema"
|
|
179
|
-
return (overlays, source) if with_source else overlays
|
|
158
|
+
return data["overlays"], source
|
|
180
159
|
|
|
181
160
|
|
|
182
161
|
def _escape_drawtext_text(text):
|
|
183
162
|
return (
|
|
184
|
-
|
|
163
|
+
text
|
|
185
164
|
.replace("\\", "\\\\")
|
|
186
165
|
.replace(":", "\\:")
|
|
187
166
|
.replace("'", "\\'")
|
|
@@ -191,44 +170,30 @@ def _escape_drawtext_text(text):
|
|
|
191
170
|
|
|
192
171
|
|
|
193
172
|
def _overlay_time_window(overlay, video_duration):
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
start = 0.0
|
|
198
|
-
try:
|
|
199
|
-
end = float(overlay.get("end", video_duration) or video_duration)
|
|
200
|
-
except (TypeError, ValueError):
|
|
201
|
-
end = float(video_duration or 0.0)
|
|
202
|
-
end = max(start, end)
|
|
203
|
-
return start, end
|
|
173
|
+
start = float(overlay.get("start", 0.0))
|
|
174
|
+
end = float(overlay.get("end", video_duration))
|
|
175
|
+
return start, max(start, end)
|
|
204
176
|
|
|
205
177
|
|
|
206
178
|
def _overlay_bbox(overlay, canvas, *, default_y):
|
|
207
|
-
width =
|
|
208
|
-
height =
|
|
209
|
-
text =
|
|
210
|
-
font_size = int(overlay.get("font_size"
|
|
179
|
+
width = canvas["width"]
|
|
180
|
+
height = canvas["height"]
|
|
181
|
+
text = overlay["text"]
|
|
182
|
+
font_size = int(overlay.get("font_size", max(18, round(height * 0.045))))
|
|
211
183
|
lines = [ln for ln in text.splitlines() if ln.strip()] or [text]
|
|
212
|
-
max_w = max(
|
|
184
|
+
max_w = max(_visual_text_units(ln) * font_size for ln in lines)
|
|
213
185
|
text_h = len(lines) * font_size * 1.25
|
|
214
|
-
if overlay
|
|
186
|
+
if overlay["type"] == "top_title":
|
|
215
187
|
x = max(0.0, (width - max_w) / 2)
|
|
216
|
-
y = float(overlay.get("y", default_y)
|
|
188
|
+
y = float(overlay.get("y", default_y))
|
|
217
189
|
else:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
x = width * 0.08
|
|
226
|
-
try:
|
|
227
|
-
y = float(raw_y)
|
|
228
|
-
if 0.0 <= y <= 1.0:
|
|
229
|
-
y *= height
|
|
230
|
-
except (TypeError, ValueError):
|
|
231
|
-
y = height * 0.25
|
|
190
|
+
# Fractions of the canvas in [0, 1] are normalized coordinates; larger values are pixels.
|
|
191
|
+
x = float(overlay.get("x", 0.08))
|
|
192
|
+
y = float(overlay.get("y", 0.25))
|
|
193
|
+
if 0.0 <= x <= 1.0:
|
|
194
|
+
x *= width
|
|
195
|
+
if 0.0 <= y <= 1.0:
|
|
196
|
+
y *= height
|
|
232
197
|
return {
|
|
233
198
|
"x": round(x, 2),
|
|
234
199
|
"y": round(y, 2),
|
|
@@ -246,19 +211,15 @@ def _visual_overlay_filters(work_dir, canvas, video_duration):
|
|
|
246
211
|
Only two semantic renderers are supported: top_title and inline_label_or_callout.
|
|
247
212
|
Unsupported types are QC-blocking and deliberately do not silently render.
|
|
248
213
|
"""
|
|
249
|
-
overlays, source = _load_visual_overlays(work_dir
|
|
250
|
-
|
|
251
|
-
default_top_y = max(24, round(height * 0.05))
|
|
214
|
+
overlays, source = _load_visual_overlays(work_dir)
|
|
215
|
+
default_top_y = max(24, round(canvas["height"] * 0.05))
|
|
252
216
|
filters = []
|
|
253
217
|
facts = []
|
|
254
218
|
unsupported = []
|
|
255
219
|
overflow = []
|
|
256
220
|
for idx, overlay in enumerate(overlays):
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
continue
|
|
260
|
-
typ = str(overlay.get("type") or "").strip()
|
|
261
|
-
text = str(overlay.get("text") or "").strip()
|
|
221
|
+
typ = overlay.get("type")
|
|
222
|
+
text = overlay.get("text", "").strip()
|
|
262
223
|
if typ not in _SUPPORTED_VISUAL_OVERLAY_TYPES:
|
|
263
224
|
unsupported.append({"index": idx, "type": typ, "reason": "unsupported_overlay_type"})
|
|
264
225
|
continue
|
|
@@ -297,9 +258,8 @@ def _visual_overlay_filters(work_dir, canvas, video_duration):
|
|
|
297
258
|
})
|
|
298
259
|
qc = {
|
|
299
260
|
"source": source,
|
|
300
|
-
"load_error": source.get("load_error"),
|
|
301
261
|
"supported_types": sorted(_SUPPORTED_VISUAL_OVERLAY_TYPES),
|
|
302
|
-
"present":
|
|
262
|
+
"present": source["present"],
|
|
303
263
|
"count": len(overlays),
|
|
304
264
|
"rendered": len(facts),
|
|
305
265
|
"facts": facts,
|
|
@@ -309,49 +269,33 @@ def _visual_overlay_filters(work_dir, canvas, video_duration):
|
|
|
309
269
|
return filters, qc
|
|
310
270
|
|
|
311
271
|
|
|
312
|
-
def _visual_qc_has_forbidden_delivery_facts(value):
|
|
313
|
-
if isinstance(value, dict):
|
|
314
|
-
for key, child in value.items():
|
|
315
|
-
if key in _VISUAL_DELIVERY_FORBIDDEN_KEYS:
|
|
316
|
-
return True
|
|
317
|
-
if _visual_qc_has_forbidden_delivery_facts(child):
|
|
318
|
-
return True
|
|
319
|
-
elif isinstance(value, list):
|
|
320
|
-
return any(_visual_qc_has_forbidden_delivery_facts(item) for item in value)
|
|
321
|
-
return False
|
|
322
|
-
|
|
323
|
-
|
|
324
272
|
def _build_visual_qc(tts_segments, work_dir, video_duration, canvas, *, overlay_qc=None, mask_filter=None):
|
|
325
273
|
entries = _combined_subtitle_entries(tts_segments, work_dir, video_duration)
|
|
326
274
|
style = _style_for_measured_subtitle_band(_subtitle_style_config(canvas), canvas)
|
|
327
275
|
subtitle_layout = _subtitle_layout_qc(
|
|
328
|
-
entries,
|
|
276
|
+
entries, style, safe_area=_measured_subtitle_safe_area(style, canvas)
|
|
329
277
|
)
|
|
330
278
|
mask = _source_subtitle_mask_policy(work_dir)
|
|
331
|
-
ratio = None
|
|
332
|
-
if mask.get("active"):
|
|
333
|
-
ratio = max(0.0, min(0.5, float(CONFIG.get("source_subtitle_mask_ratio", 0.14) or 0.0)))
|
|
334
279
|
mask.update({
|
|
335
|
-
"ratio":
|
|
280
|
+
"ratio": min(0.5, CONFIG["source_subtitle_mask_ratio"]) if mask["active"] else None,
|
|
336
281
|
"filter": "drawbox" if mask_filter else None,
|
|
337
|
-
"opacity":
|
|
338
|
-
"timing":
|
|
339
|
-
"subtitle_y_top":
|
|
340
|
-
"subtitle_y_bot":
|
|
282
|
+
"opacity": CONFIG["subtitle_mask_opacity"],
|
|
283
|
+
"timing": CONFIG["source_subtitle_mask_timing"],
|
|
284
|
+
"subtitle_y_top": CONFIG["subtitle_y_top"],
|
|
285
|
+
"subtitle_y_bot": CONFIG["subtitle_y_bot"],
|
|
341
286
|
})
|
|
342
|
-
|
|
287
|
+
if overlay_qc is None:
|
|
288
|
+
overlay_qc = _visual_overlay_filters(work_dir, canvas, video_duration)[1]
|
|
343
289
|
blocking_codes = []
|
|
344
|
-
if mask
|
|
290
|
+
if mask["blocking"]:
|
|
345
291
|
blocking_codes.append("mask_policy_not_explicit")
|
|
346
|
-
if subtitle_layout
|
|
292
|
+
if subtitle_layout["overflow"]:
|
|
347
293
|
blocking_codes.append("subtitle_overflow")
|
|
348
|
-
if overlay_qc
|
|
349
|
-
blocking_codes.append("invalid_visual_overlays_json")
|
|
350
|
-
if overlay_qc.get("unsupported"):
|
|
294
|
+
if overlay_qc["unsupported"]:
|
|
351
295
|
blocking_codes.append("unsupported_visual_overlay")
|
|
352
|
-
if overlay_qc
|
|
296
|
+
if overlay_qc["overflow"]:
|
|
353
297
|
blocking_codes.append("visual_overlay_overflow")
|
|
354
|
-
|
|
298
|
+
return {
|
|
355
299
|
"schema_version": 1,
|
|
356
300
|
"artifact": VISUAL_QC,
|
|
357
301
|
"verdict": "FAIL" if blocking_codes else "PASS",
|
|
@@ -359,36 +303,31 @@ def _build_visual_qc(tts_segments, work_dir, video_duration, canvas, *, overlay_
|
|
|
359
303
|
"blocking_codes": blocking_codes,
|
|
360
304
|
"geometry": {
|
|
361
305
|
"canvas": {
|
|
362
|
-
"width":
|
|
363
|
-
"height":
|
|
364
|
-
"fps":
|
|
306
|
+
"width": canvas["width"],
|
|
307
|
+
"height": canvas["height"],
|
|
308
|
+
"fps": canvas["fps"],
|
|
365
309
|
},
|
|
366
310
|
"storage": {
|
|
367
|
-
"width":
|
|
368
|
-
"height":
|
|
311
|
+
"width": canvas["storage_width"],
|
|
312
|
+
"height": canvas["storage_height"],
|
|
369
313
|
},
|
|
370
|
-
"rotation":
|
|
371
|
-
"sample_aspect_ratio": canvas
|
|
372
|
-
"display_aspect_ratio": canvas
|
|
314
|
+
"rotation": canvas["rotation"],
|
|
315
|
+
"sample_aspect_ratio": canvas["sample_aspect_ratio"],
|
|
316
|
+
"display_aspect_ratio": canvas["display_aspect_ratio"],
|
|
373
317
|
},
|
|
374
318
|
"subtitles": subtitle_layout,
|
|
375
319
|
"mask": mask,
|
|
376
320
|
"overlays": overlay_qc,
|
|
377
321
|
"summary": {
|
|
378
|
-
"subtitle_entries": subtitle_layout
|
|
379
|
-
"subtitle_overflow":
|
|
380
|
-
"subtitle_multi_line":
|
|
381
|
-
"mask_policy": mask
|
|
382
|
-
"mask_active":
|
|
383
|
-
"overlay_rendered":
|
|
384
|
-
"overlay_unsupported": len(overlay_qc
|
|
322
|
+
"subtitle_entries": subtitle_layout["entries"],
|
|
323
|
+
"subtitle_overflow": subtitle_layout["overflow"],
|
|
324
|
+
"subtitle_multi_line": subtitle_layout["multi_line"],
|
|
325
|
+
"mask_policy": mask["policy"],
|
|
326
|
+
"mask_active": mask["active"],
|
|
327
|
+
"overlay_rendered": overlay_qc["rendered"],
|
|
328
|
+
"overlay_unsupported": len(overlay_qc["unsupported"]),
|
|
385
329
|
},
|
|
386
330
|
}
|
|
387
|
-
if _visual_qc_has_forbidden_delivery_facts(qc):
|
|
388
|
-
qc["verdict"] = "FAIL"
|
|
389
|
-
qc["blocking"] = True
|
|
390
|
-
qc["blocking_codes"].append("visual_qc_contains_delivery_fact")
|
|
391
|
-
return qc
|
|
392
331
|
|
|
393
332
|
|
|
394
333
|
def _write_visual_qc(work_dir, qc):
|
|
@@ -427,9 +366,7 @@ def _output_downscale_filter(max_h):
|
|
|
427
366
|
return f"scale=-2:'2*trunc(min(ih,{max_h})/2)':flags=lanczos"
|
|
428
367
|
|
|
429
368
|
|
|
430
|
-
def _source_subtitle_mask_filter(
|
|
431
|
-
canvas=None, work_dir=None, tts_segments=None, video_duration=None
|
|
432
|
-
):
|
|
369
|
+
def _source_subtitle_mask_filter(canvas, work_dir, tts_segments, video_duration):
|
|
433
370
|
"""Return source-subtitle drawbox filters, optionally scoped to narration windows.
|
|
434
371
|
|
|
435
372
|
Many source videos (e.g. 庆余年) ship hardcoded subtitles; without this the recap
|
|
@@ -438,71 +375,56 @@ def _source_subtitle_mask_filter(
|
|
|
438
375
|
remain configurable.
|
|
439
376
|
"""
|
|
440
377
|
policy = _source_subtitle_mask_policy(work_dir)
|
|
441
|
-
if not policy
|
|
378
|
+
if not policy["active"]:
|
|
442
379
|
return None
|
|
443
|
-
opacity =
|
|
444
|
-
|
|
445
|
-
|
|
380
|
+
opacity = CONFIG["subtitle_mask_opacity"]
|
|
381
|
+
timing = CONFIG["source_subtitle_mask_timing"]
|
|
382
|
+
if timing not in {"all", "narration"}:
|
|
383
|
+
raise ValueError(f"SOURCE_SUBTITLE_MASK_TIMING 必须是 all 或 narration,当前为 {timing!r}")
|
|
446
384
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if custom_band:
|
|
452
|
-
padding = int(CONFIG.get("subtitle_mask_padding", 4) or 0)
|
|
385
|
+
band = _measured_subtitle_band(canvas)
|
|
386
|
+
if band is not None:
|
|
387
|
+
y_top, y_bot = band
|
|
388
|
+
padding = CONFIG["subtitle_mask_padding"]
|
|
453
389
|
mask_top = max(0, y_top - padding)
|
|
454
|
-
mask_bot = min(
|
|
390
|
+
mask_bot = min(canvas["height"], y_bot + padding)
|
|
455
391
|
geometry = f"x=0:y={mask_top}:w=iw:h={mask_bot - mask_top}"
|
|
456
392
|
else:
|
|
457
|
-
ratio = max(0.0, min(0.5, float(CONFIG.get("source_subtitle_mask_ratio", 0.14) or 0.0)))
|
|
458
393
|
# Our subtitle cues are one line. Keep the mask large enough for that line and its
|
|
459
394
|
# margin, but never regress to the old two-line bar that hid ~23% of the image.
|
|
460
395
|
style = _subtitle_style_config(canvas)
|
|
461
|
-
play_res_y =
|
|
396
|
+
play_res_y = float(style["play_res_y"])
|
|
462
397
|
line_h = float(style["font_size"]) * 1.25
|
|
463
398
|
pad = 10.0 * play_res_y / SUBTITLE_STYLE_REF_H
|
|
464
399
|
sub_band = (float(style["margin_v"]) + line_h + pad) / play_res_y
|
|
465
|
-
ratio = min(0.5, max(
|
|
466
|
-
if ratio <= 0:
|
|
467
|
-
return None
|
|
400
|
+
ratio = min(0.5, max(CONFIG["source_subtitle_mask_ratio"], sub_band))
|
|
468
401
|
geometry = f"x=0:y=ih-ih*{ratio:.3f}:w=iw:h=ih*{ratio:.3f}"
|
|
469
402
|
|
|
470
403
|
base = f"drawbox={geometry}:color=black@{opacity:.2f}:t=fill"
|
|
471
|
-
timing = str(CONFIG.get("source_subtitle_mask_timing", "narration") or "narration").lower()
|
|
472
|
-
if timing not in {"all", "narration"}:
|
|
473
|
-
timing = "narration"
|
|
474
404
|
filters = []
|
|
475
|
-
if
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
# Avoid overlapping drawboxes: stacking two 60%-black masks would darken the overlap
|
|
491
|
-
# to 84%. Coalescing also keeps long filter chains smaller.
|
|
492
|
-
filters.extend(
|
|
493
|
-
f"{base}:enable='between(t,{start:.3f},{end:.3f})'"
|
|
494
|
-
for start, end, _ in coalesce_duck_windows(windows, bridge=0.001)
|
|
495
|
-
)
|
|
405
|
+
if opacity > 0:
|
|
406
|
+
if timing == "all":
|
|
407
|
+
filters.append(base)
|
|
408
|
+
else:
|
|
409
|
+
windows = [
|
|
410
|
+
(start, end, 0.0)
|
|
411
|
+
for start, end in map(_seg_place_window, tts_segments)
|
|
412
|
+
if end > start
|
|
413
|
+
]
|
|
414
|
+
# Avoid overlapping drawboxes: stacking two 60%-black masks would darken the
|
|
415
|
+
# overlap to 84%. Coalescing also keeps long filter chains smaller.
|
|
416
|
+
filters.extend(
|
|
417
|
+
f"{base}:enable='between(t,{start:.3f},{end:.3f})'"
|
|
418
|
+
for start, end, _ in coalesce_duck_windows(windows, bridge=0.001)
|
|
419
|
+
)
|
|
496
420
|
|
|
497
421
|
# A translucent mask deliberately leaves the source glyphs visible. Whenever we burn a
|
|
498
422
|
# replacement original-dialogue subtitle into a gap, cover that exact window opaquely first;
|
|
499
423
|
# otherwise the source hard-sub and replacement text are stacked on top of each other.
|
|
500
|
-
if
|
|
501
|
-
replacement_entries = _original_gap_subtitle_entries(
|
|
502
|
-
tts_segments or [], work_dir, video_duration
|
|
503
|
-
)
|
|
424
|
+
if not (timing == "all" and opacity >= 1.0 - 1e-9):
|
|
504
425
|
replacement_windows = [
|
|
505
|
-
(entry["start"], entry["end"], 0.0)
|
|
426
|
+
(entry["start"], entry["end"], 0.0)
|
|
427
|
+
for entry in _original_gap_subtitle_entries(tts_segments, work_dir, video_duration)
|
|
506
428
|
]
|
|
507
429
|
opaque = f"drawbox={geometry}:color=black@1.00:t=fill"
|
|
508
430
|
filters.extend(
|