@oh-story/dsh 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/lib/client.js +35 -14
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +165 -69
- package/lib/drama/skills/short-drama/SKILL.md +5 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +126 -9
- package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +18 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +10 -1
- package/lib/drama/skills/short-drama-edit/SKILL.md +148 -0
- package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +81 -0
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +58 -0
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +96 -0
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +113 -0
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +55 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1041 -0
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +456 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
- package/lib/drama/skills/short-drama-produce/SKILL.md +24 -3
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +28 -0
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +24 -5
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
- package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +57 -2
- package/lib/drama/skills/short-drama-review/SKILL.md +3 -0
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +20 -0
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +36 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +101 -6
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +7 -1
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +35 -0
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +13 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +35 -4
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +13 -2
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
- package/lib/index.js +4 -2
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/manifest.json +28 -28
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +6 -27
- package/lib/novel-to-game/skills/game-build/SKILL.md +12 -29
- package/lib/novel-to-game/skills/game-concept/SKILL.md +10 -26
- package/lib/novel-to-game/skills/game-qa/SKILL.md +5 -15
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +1 -1
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -28
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +1 -2
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +6 -9
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +6 -11
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -4
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +1 -3
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +3 -5
- package/lib/oh-story/manifest.json +292 -172
- package/lib/oh-story/roles/character-designer.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +56 -242
- package/lib/oh-story/roles/story-architect.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +1 -1
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/SKILL.md +20 -182
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-import/SKILL.md +2 -2
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +20 -193
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +62 -7
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/project-files.md +108 -0
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +87 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +51 -52
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +8 -7
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +36 -15
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +410 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +176 -12
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +283 -0
- package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-review/SKILL.md +13 -3
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-review/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -2
- package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +2 -29
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +17 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +19 -47
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +69 -2
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/SKILL.md +18 -186
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +10 -8
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +6 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +53 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +77 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +17 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +31 -1
- package/lib/video-recap/manifest.json +162 -162
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
- package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
- package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
- package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
- package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
- package/package.json +41 -41
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
"""Loudness, source handoffs, ducking envelopes, and audio mix graphs."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
import math
|
|
5
4
|
import re
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
from artifacts import _load_work_json, _value_fingerprint
|
|
9
8
|
from audio_automation import (
|
|
10
9
|
coalesce_duck_windows,
|
|
11
|
-
default_bridge,
|
|
12
10
|
ducking_expression,
|
|
13
11
|
release_ducking_expression,
|
|
14
12
|
)
|
|
15
13
|
from lib import CONFIG, log, run_cmd
|
|
16
14
|
|
|
17
15
|
def _limiter_filter():
|
|
18
|
-
|
|
19
|
-
return f"alimiter=limit={peak:.2f}:level=false"
|
|
16
|
+
return f"alimiter=limit={CONFIG['final_limiter_peak']:.2f}:level=false"
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
def _loudness_mode(measured=None):
|
|
23
|
-
if not CONFIG
|
|
20
|
+
if not CONFIG["final_loudnorm"]:
|
|
24
21
|
return "limiter_only"
|
|
25
22
|
return "two_pass_linear" if measured else "equivalent"
|
|
26
23
|
|
|
@@ -34,12 +31,12 @@ def final_loudnorm_filter(measured=None):
|
|
|
34
31
|
pass; without it we still force the same target and peak limiter as a
|
|
35
32
|
documented equivalent/fallback path.
|
|
36
33
|
"""
|
|
37
|
-
if not CONFIG
|
|
34
|
+
if not CONFIG["final_loudnorm"]:
|
|
38
35
|
return _limiter_filter()
|
|
39
36
|
filt = (
|
|
40
|
-
f"loudnorm=I={CONFIG
|
|
41
|
-
f":TP={CONFIG
|
|
42
|
-
f":LRA={CONFIG
|
|
37
|
+
f"loudnorm=I={CONFIG['target_lufs']}"
|
|
38
|
+
f":TP={CONFIG['target_true_peak']}"
|
|
39
|
+
f":LRA={CONFIG['target_lra']}"
|
|
43
40
|
f":linear=true"
|
|
44
41
|
)
|
|
45
42
|
if measured:
|
|
@@ -58,21 +55,21 @@ def final_loudnorm_filter(measured=None):
|
|
|
58
55
|
|
|
59
56
|
def _parse_loudnorm_json(text):
|
|
60
57
|
"""Extract ffmpeg loudnorm JSON from stderr/stdout."""
|
|
61
|
-
for match in reversed(list(re.finditer(r"\{[\s\S]*?\}",
|
|
58
|
+
for match in reversed(list(re.finditer(r"\{[\s\S]*?\}", text))):
|
|
62
59
|
try:
|
|
63
60
|
data = json.loads(match.group(0))
|
|
64
61
|
except ValueError:
|
|
65
62
|
continue
|
|
66
|
-
if
|
|
63
|
+
if {"input_i", "input_tp", "input_lra", "input_thresh", "target_offset"} <= set(data):
|
|
67
64
|
return data
|
|
68
65
|
return None
|
|
69
66
|
|
|
70
67
|
|
|
71
68
|
def _loudnorm_first_pass_filter():
|
|
72
69
|
return (
|
|
73
|
-
f"loudnorm=I={CONFIG
|
|
74
|
-
f":TP={CONFIG
|
|
75
|
-
f":LRA={CONFIG
|
|
70
|
+
f"loudnorm=I={CONFIG['target_lufs']}"
|
|
71
|
+
f":TP={CONFIG['target_true_peak']}"
|
|
72
|
+
f":LRA={CONFIG['target_lra']}"
|
|
76
73
|
f":print_format=json"
|
|
77
74
|
)
|
|
78
75
|
|
|
@@ -84,7 +81,7 @@ def _run_loudnorm_first_pass(input_video, narration_wav, original_audio_input,
|
|
|
84
81
|
Returns ffmpeg loudnorm JSON, or None when probing fails. The caller then
|
|
85
82
|
falls back to the documented equivalent single-pass target+limiter filter.
|
|
86
83
|
"""
|
|
87
|
-
if not CONFIG
|
|
84
|
+
if not CONFIG["final_loudnorm"]:
|
|
88
85
|
return None
|
|
89
86
|
probe_fc = f"{filter_complex};[aout]{_loudnorm_first_pass_filter()}[lnprobe]"
|
|
90
87
|
probe_script = Path(work_dir) / ".filter_complex_loudnorm_probe.txt"
|
|
@@ -106,7 +103,7 @@ def _run_loudnorm_first_pass(input_video, narration_wav, original_audio_input,
|
|
|
106
103
|
if result.returncode != 0:
|
|
107
104
|
log(f" ⚠️ loudnorm 首遍测量失败,降级到目标滤镜+limiter: {result.stderr}")
|
|
108
105
|
return None
|
|
109
|
-
measured = _parse_loudnorm_json(
|
|
106
|
+
measured = _parse_loudnorm_json(result.stdout + "\n" + result.stderr)
|
|
110
107
|
if not measured:
|
|
111
108
|
log(" ⚠️ loudnorm 首遍未返回 JSON,降级到目标滤镜+limiter")
|
|
112
109
|
return None
|
|
@@ -114,10 +111,8 @@ def _run_loudnorm_first_pass(input_video, narration_wav, original_audio_input,
|
|
|
114
111
|
|
|
115
112
|
|
|
116
113
|
def _seg_place_window(seg):
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
e = seg.get("actual_place_end", seg.get("end", 0))
|
|
120
|
-
return s, e
|
|
114
|
+
"""A segment's actual placed (start, end) on the output timeline; zero-width when unplaced."""
|
|
115
|
+
return seg["actual_place_start"], seg["actual_place_end"]
|
|
121
116
|
|
|
122
117
|
|
|
123
118
|
def _load_sentence_handoff_anchors(work_dir):
|
|
@@ -126,94 +121,58 @@ def _load_sentence_handoff_anchors(work_dir):
|
|
|
126
121
|
cut_mode = (work_dir / "edited_source.mp4").exists() or (
|
|
127
122
|
work_dir / "clip_plan_validated.json"
|
|
128
123
|
).exists()
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
124
|
+
artifact = "speech_boundary_anchors_output.json" if cut_mode else "speech_boundary_anchors.json"
|
|
125
|
+
payload = _load_work_json(work_dir, artifact)
|
|
126
|
+
if payload is None:
|
|
127
|
+
return [], None, {"require_measured": cut_mode}
|
|
128
|
+
if cut_mode:
|
|
129
|
+
# Output-clock anchors are only trusted when they were derived from the current cut plan.
|
|
130
|
+
plan = _load_work_json(work_dir, "clip_plan_validated.json")
|
|
131
|
+
fresh = (
|
|
132
|
+
payload.get("schema_version") == 2
|
|
133
|
+
and payload.get("timeline") == "cut_output"
|
|
134
|
+
and plan is not None
|
|
135
|
+
and payload.get("clip_plan_fingerprint") == _value_fingerprint(plan)
|
|
135
136
|
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
except (OSError, json.JSONDecodeError):
|
|
143
|
-
continue
|
|
144
|
-
if cut_mode:
|
|
145
|
-
plan = _load_work_json(work_dir, "clip_plan_validated.json")
|
|
146
|
-
if not (
|
|
147
|
-
isinstance(payload, dict)
|
|
148
|
-
and payload.get("schema_version") == 2
|
|
149
|
-
and payload.get("timeline") == "cut_output"
|
|
150
|
-
and isinstance(plan, dict)
|
|
151
|
-
and payload.get("clip_plan_fingerprint") == _value_fingerprint(plan)
|
|
152
|
-
):
|
|
153
|
-
return [], None, {"require_measured": True}
|
|
154
|
-
payload = {**payload, "require_measured": True}
|
|
155
|
-
raw = payload.get("sentence_anchors", []) if isinstance(payload, dict) else []
|
|
156
|
-
anchors = []
|
|
157
|
-
for item in raw:
|
|
158
|
-
if not isinstance(item, dict) or item.get("confidence") not in {"high", "medium"}:
|
|
159
|
-
continue
|
|
160
|
-
try:
|
|
161
|
-
when = float(item.get("time"))
|
|
162
|
-
except (TypeError, ValueError):
|
|
163
|
-
continue
|
|
164
|
-
if math.isfinite(when) and when >= 0:
|
|
165
|
-
try:
|
|
166
|
-
pause_start = float(item.get("pause_start", when - 0.12))
|
|
167
|
-
except (TypeError, ValueError):
|
|
168
|
-
pause_start = when - 0.12
|
|
169
|
-
anchors.append({
|
|
170
|
-
"time": round(when, 4),
|
|
171
|
-
"pause_start": round(max(0.0, min(pause_start, when)), 4),
|
|
172
|
-
})
|
|
173
|
-
unique = {(row["time"], row["pause_start"]): row for row in anchors}
|
|
174
|
-
return sorted(unique.values(), key=lambda row: row["time"]), path.name, payload
|
|
175
|
-
return [], None, {"require_measured": cut_mode}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _handoff_timed_rows(payload, key):
|
|
179
|
-
rows = payload.get(key, []) if isinstance(payload, dict) else []
|
|
180
|
-
out = []
|
|
181
|
-
for row in rows if isinstance(rows, list) else []:
|
|
182
|
-
if not isinstance(row, dict):
|
|
137
|
+
if not fresh:
|
|
138
|
+
return [], None, {"require_measured": True}
|
|
139
|
+
payload = {**payload, "require_measured": True}
|
|
140
|
+
anchors = {}
|
|
141
|
+
for item in payload["sentence_anchors"]:
|
|
142
|
+
if item["confidence"] not in {"high", "medium"}:
|
|
183
143
|
continue
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
144
|
+
when = float(item["time"])
|
|
145
|
+
pause_start = float(item.get("pause_start", when - 0.12))
|
|
146
|
+
row = {
|
|
147
|
+
"time": round(when, 4),
|
|
148
|
+
"pause_start": round(max(0.0, min(pause_start, when)), 4),
|
|
149
|
+
}
|
|
150
|
+
anchors[(row["time"], row["pause_start"])] = row
|
|
151
|
+
return sorted(anchors.values(), key=lambda row: row["time"]), artifact, payload
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _timed_rows(rows):
|
|
155
|
+
return [{"start": float(row["start"]), "end": float(row["end"])} for row in rows]
|
|
191
156
|
|
|
192
157
|
|
|
193
|
-
def
|
|
194
|
-
|
|
195
|
-
|
|
158
|
+
def _asr_segments(work_dir):
|
|
159
|
+
"""Cleaned ASR segments (asr_clean.json) when present, else raw asr_result.json; [] when absent."""
|
|
160
|
+
clean = _load_work_json(work_dir, "asr_clean.json")
|
|
161
|
+
if clean is not None:
|
|
162
|
+
return clean["segments"]
|
|
163
|
+
return _load_work_json(work_dir, "asr_result.json") or []
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _handoff_speech_evidence(work_dir, payload):
|
|
167
|
+
speech = _timed_rows(payload.get("speech_spans", []))
|
|
168
|
+
quiet = _timed_rows(payload.get("quiet_windows", []))
|
|
196
169
|
if payload.get("require_measured"):
|
|
197
170
|
return speech, quiet
|
|
198
|
-
if artifact == "speech_boundary_anchors_output.json":
|
|
199
|
-
return speech, quiet
|
|
200
171
|
if not speech:
|
|
201
|
-
|
|
202
|
-
raw = _load_work_json(work_dir, name)
|
|
203
|
-
if isinstance(raw, list):
|
|
204
|
-
raw = {"speech_spans": raw}
|
|
205
|
-
elif isinstance(raw, dict):
|
|
206
|
-
raw = {"speech_spans": raw.get("segments", [])}
|
|
207
|
-
speech = _handoff_timed_rows(raw, "speech_spans")
|
|
208
|
-
if speech:
|
|
209
|
-
break
|
|
172
|
+
speech = _timed_rows(_asr_segments(work_dir))
|
|
210
173
|
if not quiet:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
row for row in raw
|
|
214
|
-
if isinstance(row, dict) and not bool(row.get("has_speech", False))
|
|
215
|
-
]} if isinstance(raw, list) else {}
|
|
216
|
-
quiet = _handoff_timed_rows(raw, "quiet_windows")
|
|
174
|
+
silence = _load_work_json(work_dir, "silence_periods.json") or []
|
|
175
|
+
quiet = _timed_rows(row for row in silence if not row.get("has_speech", False))
|
|
217
176
|
return speech, quiet
|
|
218
177
|
|
|
219
178
|
|
|
@@ -250,10 +209,7 @@ def _measured_speech_owned(
|
|
|
250
209
|
start, end, speech, quiet, anchors, authored, require_measured=False
|
|
251
210
|
):
|
|
252
211
|
duration = max(0.0, end - start)
|
|
253
|
-
quiet_min = max(
|
|
254
|
-
0.3,
|
|
255
|
-
duration * float(CONFIG.get("quiet_overlap_min_ratio", 0.8) or 0.8),
|
|
256
|
-
)
|
|
212
|
+
quiet_min = max(0.3, duration * CONFIG["quiet_overlap_min_ratio"])
|
|
257
213
|
if speech:
|
|
258
214
|
return _speech_overlap_excluding_quiet(start, end, speech, quiet) > 0.05
|
|
259
215
|
quiet_overlap = sum(
|
|
@@ -276,19 +232,10 @@ def _entry_speech_owned(
|
|
|
276
232
|
return True if anchors or require_measured else bool(authored)
|
|
277
233
|
|
|
278
234
|
|
|
279
|
-
def _work_has_source_speech(work_dir, speech_spans
|
|
235
|
+
def _work_has_source_speech(work_dir, speech_spans, require_measured):
|
|
280
236
|
if speech_spans or require_measured:
|
|
281
237
|
return True
|
|
282
|
-
for
|
|
283
|
-
payload = _load_work_json(work_dir, name)
|
|
284
|
-
if isinstance(payload, dict):
|
|
285
|
-
payload = payload.get("segments", [])
|
|
286
|
-
if isinstance(payload, list) and any(
|
|
287
|
-
isinstance(item, dict) and str(item.get("text") or "").strip()
|
|
288
|
-
for item in payload
|
|
289
|
-
):
|
|
290
|
-
return True
|
|
291
|
-
return False
|
|
238
|
+
return any(item["text"].strip() for item in _asr_segments(work_dir))
|
|
292
239
|
|
|
293
240
|
|
|
294
241
|
def _apply_source_sentence_handoffs(tts_segments, work_dir, video_duration):
|
|
@@ -297,21 +244,14 @@ def _apply_source_sentence_handoffs(tts_segments, work_dir, video_duration):
|
|
|
297
244
|
This does not move or trim narration. It only extends the ORIGINAL-audio duck
|
|
298
245
|
envelope so returning the source track cannot reveal the middle of a sentence.
|
|
299
246
|
"""
|
|
300
|
-
fade =
|
|
301
|
-
bridge =
|
|
247
|
+
fade = CONFIG["duck_fade_seconds"]
|
|
248
|
+
bridge = CONFIG["duck_bridge_seconds"]
|
|
302
249
|
anchors, artifact, evidence_payload = _load_sentence_handoff_anchors(work_dir)
|
|
303
|
-
speech_spans, quiet_windows = _handoff_speech_evidence(
|
|
304
|
-
|
|
305
|
-
)
|
|
306
|
-
require_measured = bool(evidence_payload.get("require_measured"))
|
|
250
|
+
speech_spans, quiet_windows = _handoff_speech_evidence(work_dir, evidence_payload)
|
|
251
|
+
require_measured = evidence_payload.get("require_measured", False)
|
|
307
252
|
placed = []
|
|
308
|
-
for seg in tts_segments
|
|
309
|
-
|
|
310
|
-
continue
|
|
311
|
-
try:
|
|
312
|
-
start, end = map(float, _seg_place_window(seg))
|
|
313
|
-
except (TypeError, ValueError):
|
|
314
|
-
continue
|
|
253
|
+
for seg in tts_segments:
|
|
254
|
+
start, end = _seg_place_window(seg)
|
|
315
255
|
if end > start:
|
|
316
256
|
placed.append((start, end, seg))
|
|
317
257
|
placed.sort(key=lambda item: (item[0], item[1]))
|
|
@@ -326,14 +266,12 @@ def _apply_source_sentence_handoffs(tts_segments, work_dir, video_duration):
|
|
|
326
266
|
else:
|
|
327
267
|
runs.append({"start": start, "end": end, "segments": [seg]})
|
|
328
268
|
|
|
329
|
-
source_has_speech = _work_has_source_speech(
|
|
330
|
-
work_dir, speech_spans, require_measured=require_measured
|
|
331
|
-
)
|
|
269
|
+
source_has_speech = _work_has_source_speech(work_dir, speech_spans, require_measured)
|
|
332
270
|
report = []
|
|
333
271
|
for run in runs:
|
|
334
272
|
ownership = []
|
|
335
273
|
for seg in run["segments"]:
|
|
336
|
-
start, end =
|
|
274
|
+
start, end = _seg_place_window(seg)
|
|
337
275
|
measured = _measured_speech_owned(
|
|
338
276
|
start,
|
|
339
277
|
end,
|
|
@@ -351,7 +289,7 @@ def _apply_source_sentence_handoffs(tts_segments, work_dir, video_duration):
|
|
|
351
289
|
speech_spans,
|
|
352
290
|
quiet_windows,
|
|
353
291
|
anchors,
|
|
354
|
-
first
|
|
292
|
+
first["overlaps_speech"],
|
|
355
293
|
require_measured=require_measured,
|
|
356
294
|
)
|
|
357
295
|
speech_owned = entry_owned or any(ownership)
|
|
@@ -420,60 +358,35 @@ def _amix_tail(narr_vol, bgm_chain=""):
|
|
|
420
358
|
return narr + "[orig][narr]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]"
|
|
421
359
|
|
|
422
360
|
|
|
423
|
-
def
|
|
424
|
-
"""Collect [(start, end, level)] placement windows for the placed beats, using
|
|
425
|
-
`level_for(seg)` to pick each beat's duck level. Skips non-dicts and empty spans."""
|
|
426
|
-
windows = []
|
|
427
|
-
for seg in tts_segments:
|
|
428
|
-
if not isinstance(seg, dict):
|
|
429
|
-
continue
|
|
430
|
-
s, e = _seg_place_window(seg)
|
|
431
|
-
if source_safe_end:
|
|
432
|
-
try:
|
|
433
|
-
e = max(float(e), float(seg.get("source_duck_end", e)))
|
|
434
|
-
except (TypeError, ValueError):
|
|
435
|
-
pass
|
|
436
|
-
if e - s <= 0:
|
|
437
|
-
continue
|
|
438
|
-
windows.append((s, e, level_for(seg)))
|
|
439
|
-
return windows
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def _duck_envelope(tts_segments, idle, speech_vol, quiet_vol, fade, bridge=None):
|
|
361
|
+
def _duck_envelope(tts_segments, idle, speech_vol, quiet_vol, fade, bridge):
|
|
443
362
|
"""Per-beat ducking automation for the ORIGINAL track.
|
|
444
363
|
|
|
445
364
|
Uses the shared ducking contract: [start-fade,start] pre-roll ramp down,
|
|
446
365
|
[start,end] held at the selected duck level, and [end,end+fade] release.
|
|
447
366
|
Bridged spans use the most-ducked (lowest) level, matching timeline.json /
|
|
448
|
-
JianYing keyframes. Returns a volume= expression, or None when no beat
|
|
449
|
-
|
|
367
|
+
JianYing keyframes. Returns a volume= expression, or None when no beat was
|
|
368
|
+
placed (caller falls back to a constant).
|
|
450
369
|
"""
|
|
451
|
-
if bridge is None:
|
|
452
|
-
bridge = default_bridge(fade)
|
|
453
370
|
windows = []
|
|
454
|
-
for seg in tts_segments
|
|
455
|
-
|
|
456
|
-
continue
|
|
457
|
-
try:
|
|
458
|
-
start, narration_end = map(float, _seg_place_window(seg))
|
|
459
|
-
hold_end = max(narration_end, float(seg.get("source_duck_end", narration_end)))
|
|
460
|
-
restore_at = max(hold_end, float(seg.get("source_restore_at", hold_end + fade)))
|
|
461
|
-
except (TypeError, ValueError):
|
|
462
|
-
continue
|
|
371
|
+
for seg in tts_segments:
|
|
372
|
+
start, narration_end = _seg_place_window(seg)
|
|
463
373
|
if narration_end <= start:
|
|
464
374
|
continue
|
|
375
|
+
hold_end = max(narration_end, seg.get("source_duck_end", narration_end))
|
|
376
|
+
restore_at = max(hold_end, seg.get("source_restore_at", hold_end + fade))
|
|
465
377
|
level = speech_vol if seg.get("overlaps_speech", True) else quiet_vol
|
|
466
378
|
windows.append((start, hold_end, level, restore_at))
|
|
467
379
|
return release_ducking_expression(windows, idle, fade, bridge=bridge)
|
|
468
380
|
|
|
469
381
|
|
|
470
|
-
def _bgm_envelope(tts_segments, base, duck, fade, bridge
|
|
382
|
+
def _bgm_envelope(tts_segments, base, duck, fade, bridge):
|
|
471
383
|
"""Per-beat ducking automation for the BGM track using the shared contract."""
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
384
|
+
windows = [
|
|
385
|
+
(start, end, duck)
|
|
386
|
+
for start, end in map(_seg_place_window, tts_segments)
|
|
387
|
+
if end > start
|
|
388
|
+
]
|
|
389
|
+
return ducking_expression(coalesce_duck_windows(windows, bridge), base, fade)
|
|
477
390
|
|
|
478
391
|
|
|
479
392
|
def _build_audio_filter_complex(
|
|
@@ -481,7 +394,7 @@ def _build_audio_filter_complex(
|
|
|
481
394
|
has_bgm=False,
|
|
482
395
|
*,
|
|
483
396
|
original_audio_label="0:a",
|
|
484
|
-
bgm_audio_label=
|
|
397
|
+
bgm_audio_label="2:a",
|
|
485
398
|
):
|
|
486
399
|
"""Compose the audio tracks into [aout], like a cut-software timeline.
|
|
487
400
|
|
|
@@ -495,26 +408,24 @@ def _build_audio_filter_complex(
|
|
|
495
408
|
fixed = the gap-fill envelope above; sidechaincompress = auto-duck keyed off the
|
|
496
409
|
narration; none = no ducking. Placement comes from actual_place_start/end.
|
|
497
410
|
"""
|
|
498
|
-
ducking_mode = CONFIG
|
|
411
|
+
ducking_mode = CONFIG["ducking_mode"]
|
|
499
412
|
if ducking_mode == "sidechaincompress" and any(
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
> float(seg.get("actual_place_end", 0) or 0) + 1e-6
|
|
503
|
-
for seg in tts_segments or []
|
|
413
|
+
"source_duck_end" in seg and seg["source_duck_end"] > seg["actual_place_end"] + 1e-6
|
|
414
|
+
for seg in tts_segments
|
|
504
415
|
):
|
|
505
416
|
log("sidechaincompress 无法保持句末交接窗口,已回退 fixed ducking")
|
|
506
417
|
ducking_mode = "fixed"
|
|
507
|
-
narr_vol = CONFIG
|
|
508
|
-
fade = CONFIG
|
|
418
|
+
narr_vol = CONFIG["ducking_narr_weight"]
|
|
419
|
+
fade = CONFIG["duck_fade_seconds"]
|
|
420
|
+
bridge = CONFIG["duck_bridge_seconds"]
|
|
509
421
|
original_in = f"[{original_audio_label}]"
|
|
510
|
-
bgm_in = f"[{bgm_audio_label
|
|
422
|
+
bgm_in = f"[{bgm_audio_label}]"
|
|
511
423
|
|
|
512
424
|
# BGM bed (input [2:a]): ducked under each narration window when present.
|
|
513
425
|
bgm_chain = ""
|
|
514
426
|
if has_bgm:
|
|
515
|
-
base = CONFIG
|
|
516
|
-
bgm_expr = _bgm_envelope(tts_segments, base, CONFIG
|
|
517
|
-
bridge=CONFIG.get("duck_bridge_seconds", 1.5))
|
|
427
|
+
base = CONFIG["bgm_volume"]
|
|
428
|
+
bgm_expr = _bgm_envelope(tts_segments, base, CONFIG["bgm_ducking_volume"], fade, bridge)
|
|
518
429
|
if bgm_expr:
|
|
519
430
|
bgm_chain = f"{bgm_in}volume='{bgm_expr}':eval=frame,aresample=48000[bgm];"
|
|
520
431
|
else:
|
|
@@ -539,17 +450,16 @@ def _build_audio_filter_complex(
|
|
|
539
450
|
return f"{original_in}aresample=48000[orig];" + _amix_tail(narr_vol, bgm_chain)
|
|
540
451
|
|
|
541
452
|
# fixed (default): gap-fill ducking envelope on the original track.
|
|
542
|
-
idle = CONFIG
|
|
543
|
-
speech_vol = CONFIG
|
|
544
|
-
quiet_vol = CONFIG
|
|
545
|
-
|
|
546
|
-
expr = _duck_envelope(tts_segments, idle, speech_vol, quiet_vol, fade, bridge=bridge)
|
|
453
|
+
idle = CONFIG["idle_orig_volume"]
|
|
454
|
+
speech_vol = CONFIG["speech_ducking_volume"]
|
|
455
|
+
quiet_vol = CONFIG["zone_ducking_volume"]
|
|
456
|
+
expr = _duck_envelope(tts_segments, idle, speech_vol, quiet_vol, fade, bridge)
|
|
547
457
|
if expr:
|
|
548
|
-
n_overlap = sum(1 for s in tts_segments if
|
|
549
|
-
n_quiet =
|
|
458
|
+
n_overlap = sum(1 for s in tts_segments if s.get("overlaps_speech", True))
|
|
459
|
+
n_quiet = len(tts_segments) - n_overlap
|
|
550
460
|
log(f"gap-fill ducking: 间隙原声={idle}, 对白段={speech_vol}({n_overlap}), 安静段={quiet_vol}({n_quiet}), 桥接间隙<{bridge}s")
|
|
551
461
|
orig = f"{original_in}volume='{expr}':eval=frame,aresample=48000[orig];"
|
|
552
462
|
else:
|
|
553
463
|
# No placement info at all: hold the original at a constant level.
|
|
554
|
-
orig = f"{original_in}volume={CONFIG
|
|
464
|
+
orig = f"{original_in}volume={CONFIG['ducking_orig_volume']},aresample=48000[orig];"
|
|
555
465
|
return orig + _amix_tail(narr_vol, bgm_chain)
|
|
@@ -82,8 +82,7 @@ def windowed_volume_keyframes(keyframes, seg_start_s, seg_end_s, default_gain, n
|
|
|
82
82
|
return volume_keyframes(selected, start, new_id)
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def clip_from_segment(segment
|
|
86
|
-
segment = segment or {}
|
|
85
|
+
def clip_from_segment(segment):
|
|
87
86
|
scale = segment.get("scale") if isinstance(segment.get("scale"), dict) else {}
|
|
88
87
|
position = segment.get("position") if isinstance(segment.get("position"), dict) else {}
|
|
89
88
|
flip = segment.get("flip") if isinstance(segment.get("flip"), dict) else {}
|
|
@@ -229,48 +228,32 @@ def unsupported_track_note(kind):
|
|
|
229
228
|
|
|
230
229
|
|
|
231
230
|
RESOURCE_TRACKS = {
|
|
232
|
-
"sound":
|
|
233
|
-
"sticker":
|
|
234
|
-
"text_template":
|
|
235
|
-
"video_effect":
|
|
236
|
-
"face_effect":
|
|
231
|
+
"sound": "audios",
|
|
232
|
+
"sticker": "stickers",
|
|
233
|
+
"text_template": "text_templates",
|
|
234
|
+
"video_effect": "video_effects",
|
|
235
|
+
"face_effect": "video_effects",
|
|
237
236
|
}
|
|
238
237
|
|
|
239
238
|
|
|
240
|
-
def _resource_value(config, snake_case, camel_case, default=None):
|
|
241
|
-
if snake_case in config:
|
|
242
|
-
return config[snake_case]
|
|
243
|
-
return config.get(camel_case, default)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def _load_resource_config(config, kind):
|
|
247
|
-
if isinstance(config, str):
|
|
248
|
-
with open(config, encoding="utf-8") as source:
|
|
249
|
-
config = json.load(source)
|
|
250
|
-
if not isinstance(config, dict):
|
|
251
|
-
raise ValueError(f"{kind} resource_config must be an object")
|
|
252
|
-
return config
|
|
253
|
-
|
|
254
|
-
|
|
255
239
|
def _resource_material(segment, kind, new_id):
|
|
256
240
|
raw = segment.get("material")
|
|
257
241
|
config = segment.get("resource_config")
|
|
258
242
|
if raw is not None and config is not None:
|
|
259
243
|
raise ValueError(f"{kind} segment must use either material or resource_config, not both")
|
|
260
244
|
if config is not None:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
raw = json.loads(raw)
|
|
245
|
+
if not isinstance(config, dict):
|
|
246
|
+
raise ValueError(f"{kind} resource_config must be an object")
|
|
247
|
+
raw = config.get("main_config")
|
|
265
248
|
if not isinstance(raw, dict):
|
|
266
249
|
raise ValueError(f"{kind} resource_config.main_config must be an object")
|
|
267
250
|
raw = deepcopy(raw)
|
|
268
|
-
resource_id =
|
|
251
|
+
resource_id = config.get("resource_id")
|
|
269
252
|
if resource_id is not None:
|
|
270
253
|
raw.setdefault("resource_id", resource_id)
|
|
271
254
|
if config.get("resources"):
|
|
272
255
|
raw["_bundle_resources"] = deepcopy(config["resources"])
|
|
273
|
-
cover_img =
|
|
256
|
+
cover_img = config.get("cover_img")
|
|
274
257
|
if kind == "sticker" and cover_img:
|
|
275
258
|
raw["icon_url"] = cover_img
|
|
276
259
|
raw["preview_cover_url"] = cover_img
|
|
@@ -284,7 +267,7 @@ def _resource_material(segment, kind, new_id):
|
|
|
284
267
|
|
|
285
268
|
def build_resource_track(ctx, timeline_track):
|
|
286
269
|
kind = timeline_track["kind"]
|
|
287
|
-
materials_key
|
|
270
|
+
materials_key = RESOURCE_TRACKS[kind]
|
|
288
271
|
track_name = timeline_track.get("name", kind)
|
|
289
272
|
for item in timeline_track.get("segments", []):
|
|
290
273
|
ts, te = float(item["timeline_start"]), float(item["timeline_end"])
|
|
@@ -300,8 +283,6 @@ def build_resource_track(ctx, timeline_track):
|
|
|
300
283
|
material = _resource_material(authored_item, kind, ctx.new_id)
|
|
301
284
|
ctx.materials[materials_key].append(material)
|
|
302
285
|
config = authored_item.get("resource_config") or {}
|
|
303
|
-
if config:
|
|
304
|
-
config = _load_resource_config(config, kind)
|
|
305
286
|
if kind == "text_template":
|
|
306
287
|
subordinate_resources = deepcopy(material.get("_bundle_resources", []))
|
|
307
288
|
subordinate_texts = deepcopy(config.get("texts", []))
|
|
@@ -332,12 +313,10 @@ def _attachment_material(spec, kind, new_id):
|
|
|
332
313
|
if not isinstance(spec, dict):
|
|
333
314
|
raise ValueError(f"video {kind} must be an object")
|
|
334
315
|
material = deepcopy(spec)
|
|
335
|
-
config = material.pop("main_config",
|
|
316
|
+
config = material.pop("main_config", None)
|
|
336
317
|
resources = material.pop("resources", None)
|
|
337
|
-
resource_id = material.pop("
|
|
318
|
+
resource_id = material.pop("resource_id", None)
|
|
338
319
|
if config is not None:
|
|
339
|
-
if isinstance(config, str):
|
|
340
|
-
config = json.loads(config)
|
|
341
320
|
if not isinstance(config, dict):
|
|
342
321
|
raise ValueError(f"video {kind}.main_config must be an object")
|
|
343
322
|
merged = deepcopy(config)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Callable
|
|
6
6
|
|
|
7
7
|
from jianying_schema import us
|
|
8
8
|
from jianying_tracks import TrackAllocator
|
|
@@ -50,20 +50,22 @@ class DraftBuildContext:
|
|
|
50
50
|
|
|
51
51
|
@classmethod
|
|
52
52
|
def from_timeline(cls, timeline, new_id, probe):
|
|
53
|
-
|
|
53
|
+
"""Build from a timeline already validated by jianying_timeline_contract."""
|
|
54
|
+
canvas = timeline["canvas"]
|
|
54
55
|
return cls(
|
|
55
|
-
width=
|
|
56
|
-
height=
|
|
57
|
-
fps=float(canvas
|
|
58
|
-
total_us=us(timeline
|
|
56
|
+
width=canvas["width"],
|
|
57
|
+
height=canvas["height"],
|
|
58
|
+
fps=float(canvas["fps"]),
|
|
59
|
+
total_us=us(timeline["duration"]),
|
|
59
60
|
new_id=new_id,
|
|
60
61
|
probe=probe,
|
|
61
|
-
resource_packages=
|
|
62
|
-
style_presets=
|
|
62
|
+
resource_packages=timeline.get("resource_packages", {}),
|
|
63
|
+
style_presets=timeline.get("style_presets", {}),
|
|
63
64
|
)
|
|
64
65
|
|
|
65
66
|
def media_duration(self, path, fallback_us):
|
|
66
|
-
|
|
67
|
+
"""(duration_us, width, height) probed from the file; images and missing files fall back."""
|
|
68
|
+
if os.path.exists(path):
|
|
67
69
|
duration_us, width, height = self.probe(path)
|
|
68
70
|
return duration_us or fallback_us, width, height
|
|
69
71
|
return fallback_us, 0, 0
|
|
@@ -97,14 +99,14 @@ class DraftBuildContext:
|
|
|
97
99
|
def finalize_tracks(self):
|
|
98
100
|
# Python's stable sort preserves the timeline's authored order inside
|
|
99
101
|
# one semantic band (for example narration before BGM).
|
|
100
|
-
self.tracks.sort(key=lambda item: item
|
|
102
|
+
self.tracks.sort(key=lambda item: item["_layout_order"])
|
|
101
103
|
type_counts = {}
|
|
102
104
|
for track in self.tracks:
|
|
103
105
|
count = type_counts.get(track["type"], 0)
|
|
104
106
|
track["flag"] = 0 if count == 0 else 2
|
|
105
107
|
type_counts[track["type"]] = count + 1
|
|
106
|
-
track
|
|
107
|
-
track
|
|
108
|
+
del track["_semantic_kind"]
|
|
109
|
+
del track["_layout_order"]
|
|
108
110
|
return self.tracks
|
|
109
111
|
|
|
110
112
|
def note(self, message):
|