@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
|
@@ -4,7 +4,6 @@ import os
|
|
|
4
4
|
import wave
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from assemble_constants import SEGMENT_AUDIO_SCHEMA_VERSION
|
|
8
7
|
from lib import CONFIG, get_video_duration, log, narration_tempo_budget, run_cmd
|
|
9
8
|
|
|
10
9
|
def _apply_narration_speed(
|
|
@@ -21,44 +20,30 @@ def _apply_narration_speed(
|
|
|
21
20
|
snappier without the chipmunk effect. Rewrites each segment's audio_path/duration
|
|
22
21
|
to the sped copy so the rest of assembly is unchanged. No-op at speed 1.0.
|
|
23
22
|
"""
|
|
24
|
-
speed =
|
|
23
|
+
speed = CONFIG["narration_speed"]
|
|
25
24
|
if abs(speed - 1.0) <= 1e-3:
|
|
26
25
|
return
|
|
27
|
-
factor = max(0.5, min(2.0, speed))
|
|
28
26
|
done = 0
|
|
29
27
|
for seg in tts_segments:
|
|
30
|
-
src = seg
|
|
31
|
-
if not
|
|
32
|
-
continue
|
|
33
|
-
seg.
|
|
34
|
-
|
|
35
|
-
seg.setdefault("spoken_text", str(seg.get("spoken_text") or seg.get("narration") or ""))
|
|
36
|
-
seg.setdefault("truncated", False)
|
|
37
|
-
seg.setdefault("truncate_reason", "none")
|
|
38
|
-
seg.setdefault("source_audio_duration", seg.get("audio_duration"))
|
|
39
|
-
seg["global_narration_speed"] = factor
|
|
40
|
-
out = str(Path(work_dir) / f"_spd_{seg.get('index', 0)}.wav")
|
|
41
|
-
res = command_runner(["ffmpeg", "-y", "-i", src, "-filter:a", f"atempo={factor:.3f}",
|
|
28
|
+
src = seg["audio_path"]
|
|
29
|
+
if not os.path.exists(src):
|
|
30
|
+
continue # reported as a skipped segment during placement
|
|
31
|
+
out = str(Path(work_dir) / f"_spd_{seg['index']}.wav")
|
|
32
|
+
res = command_runner(["ffmpeg", "-y", "-i", src, "-filter:a", f"atempo={speed:.3f}",
|
|
42
33
|
"-ar", "44100", "-ac", "1", "-acodec", "pcm_s16le", out])
|
|
43
|
-
if res.returncode
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* float(seg.get("segment_tempo_factor", 1.0) or 1.0)
|
|
50
|
-
)
|
|
51
|
-
done += 1
|
|
52
|
-
logger(f"解说整体提速: atempo={factor:.2f} ({done} 段)")
|
|
34
|
+
if res.returncode != 0:
|
|
35
|
+
raise RuntimeError(f"解说提速失败 {src}: {res.stderr}")
|
|
36
|
+
seg["audio_path"] = out
|
|
37
|
+
seg["audio_duration"] = duration_probe(out)
|
|
38
|
+
done += 1
|
|
39
|
+
logger(f"解说整体提速: atempo={speed:.2f} ({done} 段)")
|
|
53
40
|
|
|
54
41
|
|
|
55
42
|
def _adjust_tts_speed(
|
|
56
43
|
audio_path,
|
|
57
44
|
target_duration,
|
|
58
|
-
work_dir,
|
|
59
45
|
tts_rate_offset=0.0,
|
|
60
46
|
*,
|
|
61
|
-
return_meta=True,
|
|
62
47
|
command_runner=run_cmd,
|
|
63
48
|
duration_probe=get_video_duration,
|
|
64
49
|
logger=log,
|
|
@@ -69,7 +54,6 @@ def _adjust_tts_speed(
|
|
|
69
54
|
audio fit, it returns `fit_status=no_safe_fit` and leaves the original audio
|
|
70
55
|
untouched for QC to block instead of guessing a spoken_text truncation.
|
|
71
56
|
"""
|
|
72
|
-
del work_dir, return_meta # Retained for direct callers of the pre-v0.4 private helper.
|
|
73
57
|
audio_path = Path(audio_path)
|
|
74
58
|
current_dur = duration_probe(audio_path)
|
|
75
59
|
budget = narration_tempo_budget(tts_rate_offset)
|
|
@@ -80,7 +64,7 @@ def _adjust_tts_speed(
|
|
|
80
64
|
"segment_tempo_factor": 1.0,
|
|
81
65
|
"truncated": False,
|
|
82
66
|
"truncate_reason": "none",
|
|
83
|
-
"tts_rate_offset": float(tts_rate_offset
|
|
67
|
+
"tts_rate_offset": float(tts_rate_offset),
|
|
84
68
|
"audio_duration": current_dur,
|
|
85
69
|
"placed_audio_duration": current_dur,
|
|
86
70
|
"global_narration_speed": budget["global_narration_speed"],
|
|
@@ -88,13 +72,11 @@ def _adjust_tts_speed(
|
|
|
88
72
|
"cumulative_tempo_max": budget["cumulative_tempo_max"],
|
|
89
73
|
"cumulative_tempo_hard_max": budget["cumulative_tempo_hard_max"],
|
|
90
74
|
}
|
|
91
|
-
if current_dur <= target_duration
|
|
75
|
+
if current_dur <= target_duration:
|
|
92
76
|
return (str(audio_path), current_dur, meta)
|
|
93
77
|
|
|
94
78
|
ratio = current_dur / target_duration
|
|
95
|
-
|
|
96
79
|
effective_max = budget["segment_tempo_max"]
|
|
97
|
-
|
|
98
80
|
if ratio > effective_max:
|
|
99
81
|
meta.update({
|
|
100
82
|
"fit_status": "no_safe_fit",
|
|
@@ -102,8 +84,6 @@ def _adjust_tts_speed(
|
|
|
102
84
|
"truncate_reason": "no_safe_boundary",
|
|
103
85
|
"placed_audio_duration": 0.0,
|
|
104
86
|
"needed_tempo_factor": ratio,
|
|
105
|
-
"segment_tempo_factor": 1.0,
|
|
106
|
-
"tempo_factor": 1.0,
|
|
107
87
|
})
|
|
108
88
|
logger(
|
|
109
89
|
f" TTS 无安全放置: {current_dur:.1f}s 需 x{ratio:.2f},"
|
|
@@ -119,34 +99,32 @@ def _adjust_tts_speed(
|
|
|
119
99
|
"-filter:a", f"atempo={tempo:.6f}",
|
|
120
100
|
"-ar", "44100", "-ac", "1", str(adjusted_path)]
|
|
121
101
|
result = command_runner(cmd)
|
|
122
|
-
if result.returncode
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"fit_status": "no_safe_fit",
|
|
128
|
-
"blocking": True,
|
|
129
|
-
"truncate_reason": "no_safe_boundary",
|
|
130
|
-
"placed_audio_duration": 0.0,
|
|
131
|
-
"needed_tempo_factor": new_dur / target_duration,
|
|
132
|
-
})
|
|
133
|
-
logger(
|
|
134
|
-
f" TTS 加速后仍超出安全窗口 {new_dur - target_duration:.3f}s;"
|
|
135
|
-
"禁止裁尾,交由 Agent 缩短/移动文本"
|
|
136
|
-
)
|
|
137
|
-
return (str(audio_path), current_dur, meta)
|
|
102
|
+
if result.returncode != 0:
|
|
103
|
+
raise RuntimeError(f"TTS 加速失败 {audio_path}: {result.stderr}")
|
|
104
|
+
new_dur = duration_probe(adjusted_path)
|
|
105
|
+
if new_dur > target_duration + (1.0 / 44100.0):
|
|
106
|
+
adjusted_path.unlink(missing_ok=True)
|
|
138
107
|
meta.update({
|
|
139
|
-
"fit_status": "
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"placed_audio_duration":
|
|
143
|
-
"
|
|
108
|
+
"fit_status": "no_safe_fit",
|
|
109
|
+
"blocking": True,
|
|
110
|
+
"truncate_reason": "no_safe_boundary",
|
|
111
|
+
"placed_audio_duration": 0.0,
|
|
112
|
+
"needed_tempo_factor": new_dur / target_duration,
|
|
144
113
|
})
|
|
145
|
-
logger(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
114
|
+
logger(
|
|
115
|
+
f" TTS 加速后仍超出安全窗口 {new_dur - target_duration:.3f}s;"
|
|
116
|
+
"禁止裁尾,交由 Agent 缩短/移动文本"
|
|
117
|
+
)
|
|
118
|
+
return (str(audio_path), current_dur, meta)
|
|
119
|
+
meta.update({
|
|
120
|
+
"fit_status": "tempo_adjusted",
|
|
121
|
+
"tempo_factor": tempo,
|
|
122
|
+
"segment_tempo_factor": tempo,
|
|
123
|
+
"placed_audio_duration": new_dur,
|
|
124
|
+
"effective_tempo": budget["global_narration_speed"] * budget["tts_rate_factor"] * tempo,
|
|
125
|
+
})
|
|
126
|
+
logger(f" TTS 温和加速: {current_dur:.1f}s → {new_dur:.1f}s (x{tempo:.2f})")
|
|
127
|
+
return (str(adjusted_path), new_dur, meta)
|
|
150
128
|
|
|
151
129
|
|
|
152
130
|
def _edge_quiet_samples(pcm16_mono, sample_count, *, from_start, threshold=260):
|
|
@@ -176,6 +154,16 @@ def _speech_safe_fade_lengths(pcm16_mono, sample_count, sample_rate, configured_
|
|
|
176
154
|
return min(configured, max(anti_click, leading)), min(configured, max(anti_click, trailing))
|
|
177
155
|
|
|
178
156
|
|
|
157
|
+
def _unplaced(seg, at, fit_status, reason, *, blocking=False):
|
|
158
|
+
"""Record a segment that contributes no audio: a zero-width window at `at` seconds."""
|
|
159
|
+
seg["actual_place_start"] = at
|
|
160
|
+
seg["actual_place_end"] = at
|
|
161
|
+
seg["placed_audio_duration"] = 0.0
|
|
162
|
+
seg["fit_status"] = fit_status
|
|
163
|
+
seg["truncate_reason"] = reason
|
|
164
|
+
seg["blocking"] = blocking
|
|
165
|
+
|
|
166
|
+
|
|
179
167
|
def _build_timed_narration(
|
|
180
168
|
tts_segments,
|
|
181
169
|
output_wav,
|
|
@@ -192,88 +180,46 @@ def _build_timed_narration(
|
|
|
192
180
|
buffer = bytearray(total_samples * 2)
|
|
193
181
|
last_written_end = 0 # 追踪已写入位置,防止重叠
|
|
194
182
|
prev_pause_samples = 0 # 前一段的 pause_after_ms,控制段间间隔
|
|
195
|
-
skipped_count = 0 # 因 WAV
|
|
183
|
+
skipped_count = 0 # 因 WAV 缺失或无安全放置而被跳过的段数
|
|
196
184
|
placed_count = 0 # 真正写入音频的段数;防止"成功"生成全静音旁白
|
|
197
185
|
no_safe_fit_count = 0 # 超预算但不能安全截断;交由 QC/manifest 阻断
|
|
198
186
|
prev_authored_end = None # 上一段作者标注的结束时间,用于判断"段落"边界
|
|
199
|
-
run_gap =
|
|
200
|
-
tighten =
|
|
201
|
-
tight_pause_samples = int(
|
|
187
|
+
run_gap = CONFIG["narration_run_gap_seconds"] # 作者留白 > 此值 = 新段落
|
|
188
|
+
tighten = CONFIG["narration_tighten"]
|
|
189
|
+
tight_pause_samples = int(CONFIG["narration_tight_pause_seconds"] * sample_rate)
|
|
202
190
|
# 漂移上限:收紧时一句最多比作者标注的时间提前 max_pull 秒,避免整段解说被全部压到前面、与画面脱节
|
|
203
|
-
max_pull_samples = int(
|
|
191
|
+
max_pull_samples = int(CONFIG["narration_max_pull_seconds"] * sample_rate)
|
|
192
|
+
configured_delay = CONFIG["narration_delay_seconds"]
|
|
193
|
+
tail_pad = CONFIG["narration_tail_pad_seconds"]
|
|
204
194
|
|
|
205
195
|
for seg in tts_segments:
|
|
206
|
-
seg.setdefault("segment_audio_schema_version", SEGMENT_AUDIO_SCHEMA_VERSION)
|
|
207
|
-
seg.setdefault("narration", str(seg.get("narration") or seg.get("spoken_text") or ""))
|
|
208
|
-
seg.setdefault("spoken_text", str(seg.get("spoken_text") or seg.get("narration") or ""))
|
|
209
|
-
seg.setdefault("truncated", False)
|
|
210
|
-
seg.setdefault("truncate_reason", "none")
|
|
211
|
-
seg.setdefault("fit_status", "pending_assembly")
|
|
212
|
-
seg.setdefault("blocking", False)
|
|
213
|
-
seg.setdefault("segment_tempo_factor", 1.0)
|
|
214
|
-
seg.setdefault("global_narration_speed", float(CONFIG.get("narration_speed", 1.0) or 1.0))
|
|
215
|
-
rate_factor = 1.0 + float(seg.get("tts_rate_offset", 0.0) or 0.0)
|
|
216
|
-
seg.setdefault("effective_tempo", float(seg["global_narration_speed"]) * rate_factor * float(seg.get("segment_tempo_factor", 1.0) or 1.0))
|
|
217
|
-
seg.setdefault("rms_dbfs_before", None)
|
|
218
|
-
seg.setdefault("rms_dbfs_after", None)
|
|
219
|
-
seg.setdefault("peak_after", None)
|
|
220
196
|
wav_path = seg["audio_path"]
|
|
221
|
-
|
|
197
|
+
pause_samples = int(seg.get("pause_after_ms", CONFIG["breath_ms"]) * sample_rate / 1000)
|
|
222
198
|
# 段落收紧:同一段落内(与上一句作者留白 <= run_gap)把这一句紧贴上一句的实际收尾播放,
|
|
223
199
|
# 句间间隔固定为 tight_pause,不受 slot 内居中延迟 / TTS 时长波动影响。段落之间(作者特意留
|
|
224
200
|
# 的大留白,让精彩原声透出)才放回原声。这样句间间隔稳定、不会出现"一句解说一段空白"。
|
|
225
|
-
cur_authored_start = float(seg
|
|
201
|
+
cur_authored_start = float(seg["start"])
|
|
226
202
|
is_run_start = (placed_count == 0 or prev_authored_end is None
|
|
227
203
|
or cur_authored_start - prev_authored_end > run_gap)
|
|
228
|
-
prev_authored_end = float(seg
|
|
204
|
+
prev_authored_end = float(seg["end"])
|
|
229
205
|
|
|
230
206
|
if not os.path.exists(wav_path):
|
|
231
|
-
seg["
|
|
232
|
-
|
|
233
|
-
seg["placed_audio_duration"] = 0.0
|
|
234
|
-
seg["fit_status"] = "skipped"
|
|
235
|
-
seg["truncate_reason"] = "missing_wav"
|
|
236
|
-
prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
|
|
207
|
+
_unplaced(seg, seg["start"], "skipped", "missing_wav")
|
|
208
|
+
prev_pause_samples = pause_samples
|
|
237
209
|
skipped_count += 1
|
|
238
210
|
continue
|
|
239
211
|
|
|
240
|
-
# WAV 格式验证 + 采样率检查(合并为一次 wave.open)
|
|
241
212
|
original_wav_path = wav_path
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
wf_sampwidth = wf_check.getsampwidth()
|
|
247
|
-
if wf_sampwidth != 2 or wf_channels != 1:
|
|
248
|
-
logger(f" 跳过非标准 WAV: {wav_path} (channels={wf_channels}, sampwidth={wf_sampwidth}), 需要 mono 16-bit")
|
|
249
|
-
seg["actual_place_start"] = seg["start"]
|
|
250
|
-
seg["actual_place_end"] = seg["start"]
|
|
251
|
-
seg["placed_audio_duration"] = 0.0
|
|
252
|
-
seg["fit_status"] = "skipped"
|
|
253
|
-
seg["truncate_reason"] = "resample_failed"
|
|
254
|
-
prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
|
|
255
|
-
skipped_count += 1
|
|
256
|
-
continue
|
|
257
|
-
if wf_check.getframerate() != sample_rate:
|
|
258
|
-
_do_resample = True
|
|
259
|
-
except Exception as e:
|
|
260
|
-
logger(f" WAV 读取失败: {wav_path}: {e}")
|
|
261
|
-
seg["actual_place_start"] = seg["start"]
|
|
262
|
-
seg["actual_place_end"] = seg["start"]
|
|
263
|
-
seg["placed_audio_duration"] = 0.0
|
|
264
|
-
seg["fit_status"] = "skipped"
|
|
265
|
-
seg["truncate_reason"] = "missing_wav"
|
|
266
|
-
prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
|
|
267
|
-
skipped_count += 1
|
|
268
|
-
continue
|
|
213
|
+
with wave.open(wav_path, "rb") as wf_check:
|
|
214
|
+
needs_resample = (
|
|
215
|
+
wf_check.getnchannels(), wf_check.getsampwidth(), wf_check.getframerate()
|
|
216
|
+
) != (1, 2, sample_rate)
|
|
269
217
|
|
|
270
218
|
tts_rate_offset = seg.get("tts_rate_offset", 0.0)
|
|
271
|
-
tts_dur = seg
|
|
219
|
+
tts_dur = seg["audio_duration"]
|
|
272
220
|
|
|
273
|
-
configured_delay = max(0.0, float(CONFIG.get("narration_delay_seconds", 0.0) or 0.0))
|
|
274
|
-
tail_pad = max(0.0, float(CONFIG.get("narration_tail_pad_seconds", 0.1) or 0.0))
|
|
275
221
|
slot_duration = max(0.0, float(seg["end"]) - float(seg["start"]))
|
|
276
|
-
max_delay = max(0.0, slot_duration -
|
|
222
|
+
max_delay = max(0.0, slot_duration - tts_dur - tail_pad)
|
|
277
223
|
narration_delay = min(configured_delay, max_delay)
|
|
278
224
|
start_sample = int((seg["start"] + narration_delay) * sample_rate)
|
|
279
225
|
end_boundary = int(min(seg["end"], video_duration) * sample_rate)
|
|
@@ -294,59 +240,43 @@ def _build_timed_narration(
|
|
|
294
240
|
available_samples = end_boundary - actual_start
|
|
295
241
|
available_duration = max(available_samples / sample_rate, 0)
|
|
296
242
|
if tts_dur > available_duration > 0:
|
|
297
|
-
|
|
298
|
-
wav_path, available_duration, work_dir, tts_rate_offset)
|
|
299
|
-
if len(adjusted_result) == 2:
|
|
300
|
-
wav_path, _actual_dur = adjusted_result
|
|
301
|
-
budget = narration_tempo_budget(tts_rate_offset)
|
|
302
|
-
fit_meta = {
|
|
303
|
-
"fit_status": "tempo_adjusted" if wav_path != original_wav_path else "fits",
|
|
304
|
-
"segment_tempo_factor": 1.0,
|
|
305
|
-
"effective_tempo": budget["global_narration_speed"],
|
|
306
|
-
"global_narration_speed": budget["global_narration_speed"],
|
|
307
|
-
"truncate_reason": "none",
|
|
308
|
-
}
|
|
309
|
-
else:
|
|
310
|
-
wav_path, _actual_dur, fit_meta = adjusted_result
|
|
243
|
+
wav_path, _actual_dur, fit_meta = adjust_speed(wav_path, available_duration, tts_rate_offset)
|
|
311
244
|
seg.update({
|
|
312
245
|
"fit_status": fit_meta["fit_status"],
|
|
313
|
-
"segment_tempo_factor": fit_meta
|
|
314
|
-
"effective_tempo": fit_meta
|
|
315
|
-
"global_narration_speed": fit_meta
|
|
316
|
-
"blocking":
|
|
246
|
+
"segment_tempo_factor": fit_meta["segment_tempo_factor"],
|
|
247
|
+
"effective_tempo": fit_meta["effective_tempo"],
|
|
248
|
+
"global_narration_speed": fit_meta["global_narration_speed"],
|
|
249
|
+
"blocking": fit_meta["blocking"],
|
|
317
250
|
})
|
|
318
251
|
if fit_meta["fit_status"] == "no_safe_fit":
|
|
319
|
-
seg
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
seg["truncate_reason"] = fit_meta["truncate_reason"]
|
|
323
|
-
prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
|
|
252
|
+
_unplaced(seg, actual_start / sample_rate, "no_safe_fit",
|
|
253
|
+
fit_meta["truncate_reason"], blocking=True)
|
|
254
|
+
prev_pause_samples = pause_samples
|
|
324
255
|
skipped_count += 1
|
|
325
256
|
no_safe_fit_count += 1
|
|
326
257
|
continue
|
|
327
258
|
else:
|
|
328
259
|
budget = narration_tempo_budget(tts_rate_offset)
|
|
329
|
-
seg
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
260
|
+
seg.update({
|
|
261
|
+
"fit_status": "fits",
|
|
262
|
+
"segment_tempo_factor": 1.0,
|
|
263
|
+
"global_narration_speed": budget["global_narration_speed"],
|
|
264
|
+
"effective_tempo": budget["global_narration_speed"] * budget["tts_rate_factor"],
|
|
265
|
+
"blocking": False,
|
|
266
|
+
})
|
|
333
267
|
|
|
334
268
|
# _adjust_tts_speed 输出固定 44100Hz mono 16bit,若文件被替换则无需 resample
|
|
335
269
|
if wav_path != original_wav_path:
|
|
336
|
-
|
|
337
|
-
if
|
|
338
|
-
tmp_path = str(Path(work_dir) / f"_rs_{seg
|
|
270
|
+
needs_resample = False
|
|
271
|
+
if needs_resample:
|
|
272
|
+
tmp_path = str(Path(work_dir) / f"_rs_{seg['index']}.wav")
|
|
339
273
|
rs_result = command_runner(["ffmpeg", "-y", "-i", wav_path,
|
|
340
274
|
"-ar", str(sample_rate), "-ac", "1",
|
|
341
275
|
"-acodec", "pcm_s16le", tmp_path])
|
|
342
|
-
if rs_result.returncode != 0
|
|
343
|
-
logger(f"
|
|
344
|
-
seg["
|
|
345
|
-
|
|
346
|
-
seg["placed_audio_duration"] = 0.0
|
|
347
|
-
seg["fit_status"] = "skipped"
|
|
348
|
-
seg["truncate_reason"] = "resample_failed"
|
|
349
|
-
prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
|
|
276
|
+
if rs_result.returncode != 0:
|
|
277
|
+
logger(f" 跳过: 重采样失败 {wav_path}: {rs_result.stderr}")
|
|
278
|
+
_unplaced(seg, seg["start"], "skipped", "resample_failed")
|
|
279
|
+
prev_pause_samples = pause_samples
|
|
350
280
|
skipped_count += 1
|
|
351
281
|
continue
|
|
352
282
|
wav_path = tmp_path
|
|
@@ -361,13 +291,8 @@ def _build_timed_narration(
|
|
|
361
291
|
|
|
362
292
|
if write_samples <= 0 or available <= 0:
|
|
363
293
|
logger(f" 跳过: {seg['start']:.1f}s-{seg['end']:.1f}s (无空间)")
|
|
364
|
-
seg["
|
|
365
|
-
|
|
366
|
-
seg["placed_audio_duration"] = 0.0
|
|
367
|
-
seg["fit_status"] = "no_safe_fit"
|
|
368
|
-
seg["blocking"] = True
|
|
369
|
-
seg["truncate_reason"] = "no_room"
|
|
370
|
-
prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
|
|
294
|
+
_unplaced(seg, seg["start"], "no_safe_fit", "no_room", blocking=True)
|
|
295
|
+
prev_pause_samples = pause_samples
|
|
371
296
|
no_safe_fit_count += 1
|
|
372
297
|
continue
|
|
373
298
|
|
|
@@ -376,14 +301,9 @@ def _build_timed_narration(
|
|
|
376
301
|
# consonant/vowel release. _adjust_tts_speed must produce a complete file
|
|
377
302
|
# that fits; otherwise block and ask the Agent to shorten/move the block.
|
|
378
303
|
over = (audio_samples - available) / sample_rate
|
|
379
|
-
logger(f" TTS 无安全放置: 段 {seg
|
|
380
|
-
seg
|
|
381
|
-
|
|
382
|
-
seg["placed_audio_duration"] = 0.0
|
|
383
|
-
seg["fit_status"] = "no_safe_fit"
|
|
384
|
-
seg["blocking"] = True
|
|
385
|
-
seg["truncate_reason"] = "no_safe_boundary"
|
|
386
|
-
prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
|
|
304
|
+
logger(f" TTS 无安全放置: 段 {seg['index']} 超出可用窗口 {over:.3f}s;禁止裁尾,交由 QC 阻断")
|
|
305
|
+
_unplaced(seg, actual_start / sample_rate, "no_safe_fit", "no_safe_boundary", blocking=True)
|
|
306
|
+
prev_pause_samples = pause_samples
|
|
387
307
|
skipped_count += 1
|
|
388
308
|
no_safe_fit_count += 1
|
|
389
309
|
continue
|
|
@@ -394,33 +314,23 @@ def _build_timed_narration(
|
|
|
394
314
|
if last_written_end >= actual_start + write_samples:
|
|
395
315
|
logger(f" 跳过重叠段: {actual_start/sample_rate:.1f}s "
|
|
396
316
|
f"(与前段重叠 {overlap_ms:.0f}ms)")
|
|
397
|
-
seg["
|
|
398
|
-
|
|
399
|
-
seg["placed_audio_duration"] = 0.0
|
|
400
|
-
seg["fit_status"] = "no_safe_fit"
|
|
401
|
-
seg["blocking"] = True
|
|
402
|
-
seg["truncate_reason"] = "no_room"
|
|
403
|
-
prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
|
|
317
|
+
_unplaced(seg, seg["start"], "no_safe_fit", "no_room", blocking=True)
|
|
318
|
+
prev_pause_samples = pause_samples
|
|
404
319
|
no_safe_fit_count += 1
|
|
405
320
|
continue
|
|
406
321
|
actual_start = last_written_end
|
|
407
322
|
available = end_boundary - actual_start
|
|
408
323
|
if write_samples > available:
|
|
409
324
|
logger(f" 重叠 {overlap_ms:.0f}ms 后无安全完整窗口,跳过")
|
|
410
|
-
seg
|
|
411
|
-
|
|
412
|
-
seg["placed_audio_duration"] = 0.0
|
|
413
|
-
seg["fit_status"] = "no_safe_fit"
|
|
414
|
-
seg["blocking"] = True
|
|
415
|
-
seg["truncate_reason"] = "no_safe_boundary"
|
|
416
|
-
prev_pause_samples = int(seg_pause_ms * sample_rate / 1000)
|
|
325
|
+
_unplaced(seg, actual_start / sample_rate, "no_safe_fit", "no_safe_boundary", blocking=True)
|
|
326
|
+
prev_pause_samples = pause_samples
|
|
417
327
|
skipped_count += 1
|
|
418
328
|
no_safe_fit_count += 1
|
|
419
329
|
continue
|
|
420
330
|
|
|
421
331
|
# fade-in / fade-out(在 overlap 裁剪之后应用,确保正确的音频包络)
|
|
422
332
|
fade_in_len, fade_out_len = _speech_safe_fade_lengths(
|
|
423
|
-
wf_data, write_samples, sample_rate, CONFIG
|
|
333
|
+
wf_data, write_samples, sample_rate, CONFIG["fade_ms"]
|
|
424
334
|
)
|
|
425
335
|
for i in range(fade_in_len):
|
|
426
336
|
gain = i / fade_in_len
|
|
@@ -440,7 +350,7 @@ def _build_timed_narration(
|
|
|
440
350
|
# Persist the exact complete per-beat PCM used by the canonical mix. Editable
|
|
441
351
|
# exports must reference this file, not the longer pre-fit TTS input; otherwise
|
|
442
352
|
# their timeline_end silently chops the final word even when ffmpeg is correct.
|
|
443
|
-
placed_path = Path(work_dir) / f"_placed_{
|
|
353
|
+
placed_path = Path(work_dir) / f"_placed_{seg['index']:04d}.wav"
|
|
444
354
|
with wave.open(str(placed_path), "wb") as placed_wav:
|
|
445
355
|
placed_wav.setnchannels(1)
|
|
446
356
|
placed_wav.setsampwidth(2)
|
|
@@ -452,12 +362,8 @@ def _build_timed_narration(
|
|
|
452
362
|
seg["actual_place_start"] = actual_start / sample_rate
|
|
453
363
|
seg["actual_place_end"] = (actual_start + write_samples) / sample_rate
|
|
454
364
|
seg["placed_audio_duration"] = write_samples / sample_rate
|
|
455
|
-
if seg.get("fit_status") == "pending_assembly":
|
|
456
|
-
seg["fit_status"] = "fits"
|
|
457
|
-
if seg.get("truncate_reason") in (None, ""):
|
|
458
|
-
seg["truncate_reason"] = "none"
|
|
459
365
|
last_written_end = actual_start + write_samples
|
|
460
|
-
prev_pause_samples =
|
|
366
|
+
prev_pause_samples = pause_samples
|
|
461
367
|
placed_count += 1
|
|
462
368
|
|
|
463
369
|
with wave.open(str(output_wav), "wb") as wf:
|
|
@@ -470,7 +376,7 @@ def _build_timed_narration(
|
|
|
470
376
|
output_wav.unlink(missing_ok=True)
|
|
471
377
|
raise RuntimeError(
|
|
472
378
|
f"全部 {len(tts_segments)} 段解说均被跳过或未能写入"
|
|
473
|
-
f"(WAV
|
|
379
|
+
f"(WAV 缺失或无可用时间;跳过 {skipped_count} 段),"
|
|
474
380
|
"已中止以避免生成无解说视频"
|
|
475
381
|
)
|
|
476
382
|
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
"""Local ffmpeg capability preflight for subtitle burn-in."""
|
|
2
2
|
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
3
6
|
from lib import CONFIG
|
|
4
7
|
|
|
5
8
|
def _ffmpeg_filters():
|
|
6
|
-
"""Return ffmpeg's compiled-in filter names.
|
|
7
|
-
|
|
8
|
-
This skill keeps a local capability probe so it remains self-contained; keep the
|
|
9
|
-
parser behavior aligned through tests.
|
|
10
|
-
"""
|
|
11
|
-
import shutil
|
|
12
|
-
import subprocess
|
|
13
|
-
ffmpeg = shutil.which("ffmpeg")
|
|
14
|
-
if not ffmpeg:
|
|
15
|
-
return set()
|
|
16
|
-
try:
|
|
17
|
-
result = subprocess.run(["ffmpeg", "-hide_banner", "-filters"],
|
|
18
|
-
text=True, capture_output=True, timeout=20)
|
|
19
|
-
except (OSError, subprocess.SubprocessError):
|
|
9
|
+
"""Return ffmpeg's compiled-in filter names."""
|
|
10
|
+
if shutil.which("ffmpeg") is None:
|
|
20
11
|
return set()
|
|
12
|
+
result = subprocess.run(["ffmpeg", "-hide_banner", "-filters"],
|
|
13
|
+
text=True, capture_output=True, timeout=20)
|
|
21
14
|
if result.returncode != 0:
|
|
22
15
|
return set()
|
|
23
16
|
filters = set()
|
|
@@ -30,13 +23,9 @@ def _ffmpeg_filters():
|
|
|
30
23
|
|
|
31
24
|
def _preflight_burn_subtitles():
|
|
32
25
|
"""Fail before the (re-encoding) render when burn-in is on but ffmpeg lacks the libass
|
|
33
|
-
`subtitles` filter.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
can't burn — an absent ffmpeg fails the render regardless and would also break the mocked,
|
|
37
|
-
ffmpeg-less test environment."""
|
|
38
|
-
import shutil
|
|
39
|
-
if not CONFIG.get("burn_subtitles", False):
|
|
26
|
+
`subtitles` filter. Only fires when ffmpeg EXISTS but can't burn — an absent ffmpeg fails
|
|
27
|
+
the render regardless."""
|
|
28
|
+
if not CONFIG["burn_subtitles"]:
|
|
40
29
|
return
|
|
41
30
|
if shutil.which("ffmpeg") is None:
|
|
42
31
|
return
|