@lemoncat7/dsh-partner 2.5.0 → 2.7.0
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 +4 -0
- package/docs/THIRD_PARTY_LICENSES.md +13 -0
- package/docs/attachment-storage.md +18 -0
- package/docs/director-skills.md +24 -0
- package/docs/long-task-continuation.md +1 -1
- package/docs/releases/2.6.0.md +11 -0
- package/docs/releases/2.7.0.md +21 -0
- package/lib/agent-runtime.d.ts +3 -1
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +35 -11
- package/lib/agent-runtime.js.map +1 -1
- package/lib/api/features/attachments-api.d.ts.map +1 -1
- package/lib/api/features/attachments-api.js +31 -1
- package/lib/api/features/attachments-api.js.map +1 -1
- package/lib/api.d.ts +1 -1
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +1 -1
- package/lib/api.js.map +1 -1
- package/lib/attachments/grouped-storage.d.ts +5 -0
- package/lib/attachments/grouped-storage.d.ts.map +1 -0
- package/lib/attachments/grouped-storage.js +47 -0
- package/lib/attachments/grouped-storage.js.map +1 -0
- package/lib/attachments/service.d.ts +17 -0
- package/lib/attachments/service.d.ts.map +1 -1
- package/lib/attachments/service.js +100 -8
- package/lib/attachments/service.js.map +1 -1
- package/lib/attachments/shared-storage.d.ts +7 -0
- package/lib/attachments/shared-storage.d.ts.map +1 -0
- package/lib/attachments/shared-storage.js +44 -0
- package/lib/attachments/shared-storage.js.map +1 -0
- package/lib/attachments/storage-admin.d.ts +6 -0
- package/lib/attachments/storage-admin.d.ts.map +1 -0
- package/lib/attachments/storage-admin.js +43 -0
- package/lib/attachments/storage-admin.js.map +1 -0
- package/lib/attachments/storage-types.d.ts +33 -0
- package/lib/attachments/storage-types.d.ts.map +1 -0
- package/lib/attachments/storage-types.js +2 -0
- package/lib/attachments/storage-types.js.map +1 -0
- package/lib/attachments/tool.d.ts.map +1 -1
- package/lib/attachments/tool.js +16 -5
- package/lib/attachments/tool.js.map +1 -1
- package/lib/channels/delivery-policy.d.ts.map +1 -1
- package/lib/channels/delivery-policy.js +6 -4
- package/lib/channels/delivery-policy.js.map +1 -1
- package/lib/channels/direct/message-format.js +418 -281
- package/lib/channels/direct/message-format.js.map +4 -4
- package/lib/channels/manager.d.ts +6 -3
- package/lib/channels/manager.d.ts.map +1 -1
- package/lib/channels/manager.js +64 -44
- package/lib/channels/manager.js.map +1 -1
- package/lib/channels/weixin/api.d.ts.map +1 -1
- package/lib/channels/weixin/api.js +7 -2
- package/lib/channels/weixin/api.js.map +1 -1
- package/lib/channels/weixin/errors.d.ts +14 -0
- package/lib/channels/weixin/errors.d.ts.map +1 -0
- package/lib/channels/weixin/errors.js +29 -0
- package/lib/channels/weixin/errors.js.map +1 -0
- package/lib/channels/weixin/poll.d.ts +14 -0
- package/lib/channels/weixin/poll.d.ts.map +1 -0
- package/lib/channels/weixin/poll.js +50 -0
- package/lib/channels/weixin/poll.js.map +1 -0
- package/lib/client-controller.d.ts.map +1 -1
- package/lib/client-controller.js +3 -3
- package/lib/client-controller.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +996 -645
- package/lib/client.js.map +4 -4
- package/lib/collaboration/composition.d.ts.map +1 -1
- package/lib/collaboration/composition.js +16 -6
- package/lib/collaboration/composition.js.map +1 -1
- package/lib/collaboration/domain.d.ts +6 -0
- package/lib/collaboration/domain.d.ts.map +1 -1
- package/lib/collaboration/domain.js.map +1 -1
- package/lib/collaboration/service.d.ts.map +1 -1
- package/lib/collaboration/service.js +33 -3
- package/lib/collaboration/service.js.map +1 -1
- package/lib/concern-notification.d.ts.map +1 -1
- package/lib/concern-notification.js +2 -1
- package/lib/concern-notification.js.map +1 -1
- package/lib/concern-tool.js +1 -1
- package/lib/concern-tool.js.map +1 -1
- package/lib/execution/reply-progress.d.ts.map +1 -1
- package/lib/execution/reply-progress.js +4 -2
- package/lib/execution/reply-progress.js.map +1 -1
- package/lib/execution/service.d.ts +1 -1
- package/lib/execution/service.d.ts.map +1 -1
- package/lib/execution/service.js +4 -7
- package/lib/execution/service.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/memory-reflection.js +2 -2
- package/lib/memory-reflection.js.map +1 -1
- package/lib/message-source.d.ts +25 -0
- package/lib/message-source.d.ts.map +1 -0
- package/lib/message-source.js +8 -0
- package/lib/message-source.js.map +1 -0
- package/lib/observation-loop.js +4 -4
- package/lib/observation-loop.js.map +1 -1
- package/lib/pendant/reader.js +1 -1
- package/lib/pendant/reader.js.map +1 -1
- package/lib/pendant/settings-panel.js +1 -1
- package/lib/pendant/settings-panel.js.map +1 -1
- package/lib/persona/service.js +1 -1
- package/lib/persona/service.js.map +1 -1
- package/lib/requirements/attachments.d.ts +7 -0
- package/lib/requirements/attachments.d.ts.map +1 -0
- package/lib/requirements/attachments.js +26 -0
- package/lib/requirements/attachments.js.map +1 -0
- package/lib/requirements/domain.d.ts +1 -1
- package/lib/requirements/domain.d.ts.map +1 -1
- package/lib/requirements/service.d.ts.map +1 -1
- package/lib/requirements/service.js +4 -1
- package/lib/requirements/service.js.map +1 -1
- package/lib/scheduler/channel-origin.d.ts +7 -0
- package/lib/scheduler/channel-origin.d.ts.map +1 -0
- package/lib/scheduler/channel-origin.js +27 -0
- package/lib/scheduler/channel-origin.js.map +1 -0
- package/lib/scheduler/continuations.d.ts +2 -2
- package/lib/scheduler/continuations.d.ts.map +1 -1
- package/lib/scheduler/continuations.js +28 -8
- package/lib/scheduler/continuations.js.map +1 -1
- package/lib/scheduler/domain.d.ts +13 -0
- package/lib/scheduler/domain.d.ts.map +1 -1
- package/lib/scheduler/final-reply.d.ts +13 -0
- package/lib/scheduler/final-reply.d.ts.map +1 -0
- package/lib/scheduler/final-reply.js +21 -0
- package/lib/scheduler/final-reply.js.map +1 -0
- package/lib/skills/builtin-resources.d.ts +6 -0
- package/lib/skills/builtin-resources.d.ts.map +1 -0
- package/lib/skills/builtin-resources.js +47 -0
- package/lib/skills/builtin-resources.js.map +1 -0
- package/lib/skills/builtin.d.ts +2 -5
- package/lib/skills/builtin.d.ts.map +1 -1
- package/lib/skills/builtin.js +6 -0
- package/lib/skills/builtin.js.map +1 -1
- package/lib/skills/director-builtins.d.ts +8 -0
- package/lib/skills/director-builtins.d.ts.map +1 -0
- package/lib/skills/director-builtins.js +65 -0
- package/lib/skills/director-builtins.js.map +1 -0
- package/lib/skills/drama-builtin.d.ts +3 -0
- package/lib/skills/drama-builtin.d.ts.map +1 -0
- package/lib/skills/drama-builtin.js +49 -0
- package/lib/skills/drama-builtin.js.map +1 -0
- package/lib/skills/long-task-continuation.d.ts +2 -2
- package/lib/skills/long-task-continuation.d.ts.map +1 -1
- package/lib/skills/long-task-continuation.js +2 -2
- package/lib/skills/service.d.ts.map +1 -1
- package/lib/skills/service.js +8 -3
- package/lib/skills/service.js.map +1 -1
- package/lib/skills/viora-canvas-builtin.d.ts +3 -0
- package/lib/skills/viora-canvas-builtin.d.ts.map +1 -0
- package/lib/skills/viora-canvas-builtin.js +19 -0
- package/lib/skills/viora-canvas-builtin.js.map +1 -0
- package/lib/storage/migrate-layout.d.ts.map +1 -1
- package/lib/storage/migrate-layout.js +13 -4
- package/lib/storage/migrate-layout.js.map +1 -1
- package/lib/tasks/context.js +1 -0
- package/lib/tasks/context.js.map +1 -1
- package/lib/tasks/continuation.d.ts.map +1 -1
- package/lib/tasks/continuation.js +3 -0
- package/lib/tasks/continuation.js.map +1 -1
- package/lib/tasks/domain.d.ts +2 -0
- package/lib/tasks/domain.d.ts.map +1 -1
- package/lib/tasks/service.d.ts +2 -1
- package/lib/tasks/service.d.ts.map +1 -1
- package/lib/tasks/service.js +15 -1
- package/lib/tasks/service.js.map +1 -1
- package/lib/ui/attachment-storage.d.ts +2 -0
- package/lib/ui/attachment-storage.d.ts.map +1 -0
- package/lib/ui/attachment-storage.js +59 -0
- package/lib/ui/attachment-storage.js.map +1 -0
- package/lib/ui/capability-editor.js +1 -1
- package/lib/ui/capability-editor.js.map +1 -1
- package/lib/ui/companion-access-panel.js +1 -1
- package/lib/ui/companion-access-panel.js.map +1 -1
- package/lib/ui/companion-create.js +1 -1
- package/lib/ui/companion-create.js.map +1 -1
- package/lib/ui/companion-picker.js +1 -1
- package/lib/ui/companion-picker.js.map +1 -1
- package/lib/ui/identity-editor.js +1 -1
- package/lib/ui/identity-editor.js.map +1 -1
- package/lib/ui/mcp-capabilities.js +1 -1
- package/lib/ui/mcp-capabilities.js.map +1 -1
- package/lib/ui/mcp-panel.js +1 -1
- package/lib/ui/mcp-panel.js.map +1 -1
- package/lib/ui/memory/concern-board.js +2 -2
- package/lib/ui/memory/concern-board.js.map +1 -1
- package/lib/ui/memory/concern-deletion.d.ts +1 -1
- package/lib/ui/memory/concern-deletion.d.ts.map +1 -1
- package/lib/ui/memory/concern-deletion.js +9 -4
- package/lib/ui/memory/concern-deletion.js.map +1 -1
- package/lib/ui/memory/concern-panel.js +1 -1
- package/lib/ui/memory/concern-panel.js.map +1 -1
- package/lib/ui/memory/memory-library.js +1 -1
- package/lib/ui/memory/memory-library.js.map +1 -1
- package/lib/ui/memory/memory-panel.js +1 -1
- package/lib/ui/memory/memory-panel.js.map +1 -1
- package/lib/ui/partner-components.js +1 -1
- package/lib/ui/partner-components.js.map +1 -1
- package/lib/ui/requirement-board.js +1 -1
- package/lib/ui/requirement-board.js.map +1 -1
- package/lib/ui/requirement-dialog.js +1 -1
- package/lib/ui/requirement-dialog.js.map +1 -1
- package/lib/ui/requirement-tasks-panel.js +1 -1
- package/lib/ui/requirement-tasks-panel.js.map +1 -1
- package/lib/ui/schedule-panel.js +1 -1
- package/lib/ui/schedule-panel.js.map +1 -1
- package/lib/ui/skills-panel.js +1 -1
- package/lib/ui/skills-panel.js.map +1 -1
- package/lib/ui/storage-settings.d.ts.map +1 -1
- package/lib/ui/storage-settings.js +2 -1
- package/lib/ui/storage-settings.js.map +1 -1
- package/lib/ui/task-board-stage.js +1 -1
- package/lib/ui/task-board-stage.js.map +1 -1
- package/lib/ui/workspace-components.js +1 -1
- package/lib/ui/workspace-components.js.map +1 -1
- package/lib/ui/workspace-navigation.d.ts +16 -16
- package/lib/ui/workspace-navigation.js +1 -1
- package/lib/ui/workspace-navigation.js.map +1 -1
- package/package.json +53 -41
- package/resources/skills/cinematic-director/manifest.json +205 -0
- package/resources/skills/cinematic-director/upstream/CHANGELOG.md +213 -0
- package/resources/skills/cinematic-director/upstream/CONTRIBUTING.md +316 -0
- package/resources/skills/cinematic-director/upstream/LICENSE +21 -0
- package/resources/skills/cinematic-director/upstream/README.md +507 -0
- package/resources/skills/cinematic-director/upstream/SKILL.md +420 -0
- package/resources/skills/cinematic-director/upstream/assets/beat-sheet-template.md +97 -0
- package/resources/skills/cinematic-director/upstream/assets/director-book-template.md +254 -0
- package/resources/skills/cinematic-director/upstream/assets/edit-timeline-template.md +147 -0
- package/resources/skills/cinematic-director/upstream/assets/keyframe-prompt-template.md +334 -0
- package/resources/skills/cinematic-director/upstream/assets/qc-checklist.md +153 -0
- package/resources/skills/cinematic-director/upstream/assets/shot-plan-template.md +95 -0
- package/resources/skills/cinematic-director/upstream/assets/sound-plan-template.md +181 -0
- package/resources/skills/cinematic-director/upstream/assets/video-prompt-template.md +305 -0
- package/resources/skills/cinematic-director/upstream/evals/run.md +69 -0
- package/resources/skills/cinematic-director/upstream/references/ai-video-tool-adapters.md +527 -0
- package/resources/skills/cinematic-director/upstream/references/blocking-and-staging.md +424 -0
- package/resources/skills/cinematic-director/upstream/references/cinematic-language.md +595 -0
- package/resources/skills/cinematic-director/upstream/references/continuity-bible.md +410 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/01_spielberg.md +192 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/02_hitchcock.md +193 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/03_kubrick.md +196 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/04_kurosawa.md +196 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/05_scorsese.md +195 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/06_fellini.md +197 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/07_bergman.md +197 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/08_tarkovsky.md +184 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/09_wong_kar_wai.md +189 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/10_nolan.md +185 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/11_villeneuve.md +188 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/12_fincher.md +185 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/13_refn.md +186 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/14_bi_gan.md +188 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/15_zhang_yimou.md +192 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/16_hou_hsiao_hsien.md +192 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/17_park_chan_wook.md +192 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/18_malick.md +191 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/19_michael_mann.md +192 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/20_coen_brothers.md +194 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/README.md +89 -0
- package/resources/skills/cinematic-director/upstream/references/director_styles/example_comparisons.md +140 -0
- package/resources/skills/cinematic-director/upstream/references/editing-and-assembly.md +263 -0
- package/resources/skills/cinematic-director/upstream/references/failure-modes.md +594 -0
- package/resources/skills/cinematic-director/upstream/references/genre-playbooks.md +407 -0
- package/resources/skills/cinematic-director/upstream/references/image-model-adapters.md +290 -0
- package/resources/skills/cinematic-director/upstream/references/lighting-and-color.md +349 -0
- package/resources/skills/cinematic-director/upstream/references/product-and-macro.md +207 -0
- package/resources/skills/cinematic-director/upstream/references/production-workflow.md +238 -0
- package/resources/skills/cinematic-director/upstream/references/prompt-lexicon.md +602 -0
- package/resources/skills/cinematic-director/upstream/references/sound-and-dialogue.md +364 -0
- package/resources/skills/director-skills/manifest.json +541 -0
- package/resources/skills/director-skills/upstream/CHANGELOG.md +32 -0
- package/resources/skills/director-skills/upstream/LICENSE +21 -0
- package/resources/skills/director-skills/upstream/MANIFEST.md +366 -0
- package/resources/skills/director-skills/upstream/README.md +251 -0
- package/resources/skills/director-skills/upstream/RESEARCH_PROVENANCE.md +37 -0
- package/resources/skills/director-skills/upstream/SKILL.md +114 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/SKILL.md +81 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/checklists/continuity_checklist.md +8 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/examples/character_continuity_update.md +9 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/examples/location_continuity_update.md +9 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/continuity_system.md +21 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/golden_image_workflow.md +29 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/identity_anchors.md +14 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/references/visual_bible_system.md +15 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/character_profile.md +15 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/continuity_checklist.md +11 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/costume_profile.md +12 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/location_profile.md +14 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/prop_profile.md +13 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/templates/visual_style_bible.md +15 -0
- package/resources/skills/director-skills/upstream/skills/character-location-prop-bible/tests/continuity_tests.md +6 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/SKILL.md +80 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/checklists/image_prompt_quality_checklist.md +10 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/examples/shot_list_to_image_prompts.md +4 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/image_prompt_components.md +22 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/lighting_and_photorealism.md +19 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/negative_prompt_patterns.md +18 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/references/prompt_anatomy.md +18 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/character_reference_sheet_prompt.md +5 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/cinematic_still_prompt.md +3 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/keyframe_prompt.md +6 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/location_reference_prompt.md +3 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/templates/prop_prompt.md +3 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-image-prompting/tests/image_prompt_completeness_tests.md +5 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/SKILL.md +84 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/checklists/video_prompt_quality_checklist.md +9 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/examples/image_prompt_to_video_prompt.md +3 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/ai_film_production_pipeline.md +21 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/audio_motion_timing.md +16 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/camera_lens_movement.md +26 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/negative_prompt_patterns.md +18 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/prompt_anatomy.md +18 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/references/video_prompt_components.md +25 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/templates/image_to_video_prompt.md +5 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/templates/text_to_video_prompt.md +5 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/templates/video_beat_prompt.md +9 -0
- package/resources/skills/director-skills/upstream/skills/cinematic-video-prompting/tests/video_prompt_completeness_tests.md +5 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/SKILL.md +82 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/checklists/model_export_checklist.md +9 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/failed_model_transfer_diagnosis.md +5 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/image_prompt_translation.md +10 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/same_scene_across_models.md +9 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/universal_prompt_to_model_specific_exports.md +14 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/examples/video_prompt_translation.md +9 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/references/failure_modes_by_model.md +13 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/references/image_model_profiles.md +226 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/references/model_capability_matrix.md +24 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/references/model_update_protocol.md +10 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/references/parameter_compatibility.md +20 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/references/prompt_translation_rules.md +18 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/references/video_model_profiles.md +251 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/model_comparison_table.md +4 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/model_specific_prompt_export.md +21 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/multi_model_export_package.md +18 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/prompt_translation_report.md +13 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/templates/universal_prompt_brief.md +25 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/model_routing_tests.md +8 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/prompt_translation_tests.md +7 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/research_provenance_tests.md +6 -0
- package/resources/skills/director-skills/upstream/skills/model-adaptation/tests/unknown_capability_tests.md +7 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/SKILL.md +82 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/checklists/diagnostic_checklist.md +8 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/checklists/release_quality_checklist.md +9 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/examples/failed_output_to_revised_prompt.md +10 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/examples/final_prompt_package_export.md +10 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/failure_modes.md +13 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/iteration_diagnostics.md +17 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/revision_loops.md +17 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/references/story_voice_release_qc.md +30 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/templates/failed_output_intake.md +13 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/templates/final_production_package.md +11 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/templates/prompt_revision_report.md +17 -0
- package/resources/skills/director-skills/upstream/skills/prompt-iteration-and-diagnostics/tests/prompt_quality_checklist_tests.md +6 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/SKILL.md +81 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/checklists/scene_quality_checklist.md +8 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/examples/scene_to_screenplay_excerpt.md +18 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/dialogue_and_subtext.md +15 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/fountain_workflow.md +29 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/scene_construction.md +20 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/references/screenplay_format.md +18 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/beat_sheet.md +6 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/dialogue_pass.md +9 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/fountain_scene.md +25 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/templates/screenplay_scene.md +19 -0
- package/resources/skills/director-skills/upstream/skills/screenplay-and-scene-writing/tests/screenplay_scene_tests.md +6 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/SKILL.md +83 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/checklists/concept_quality_checklist.md +8 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/examples/idea_to_logline.md +13 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/examples/logline_to_outline.md +10 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/references/ai_storytelling_workflow.md +46 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/references/continuity_handoff.md +8 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/references/screenplay_structure.md +19 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/references/story_development_workflow.md +23 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/templates/beat_sheet.md +7 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/templates/idea_intake.md +16 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/templates/logline.md +10 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/templates/short_film_treatment.md +23 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/templates/short_story_plan.md +22 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/templates/story_premise.md +19 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/templates/story_revision_audit.md +22 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/tests/output_format_tests.md +3 -0
- package/resources/skills/director-skills/upstream/skills/short-film-development/tests/routing_tests.md +7 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/SKILL.md +77 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/checklists/shotlist_quality_checklist.md +8 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/examples/screenplay_scene_to_shot_list.md +8 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/references/camera_plan.md +11 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/references/production_breakdown.md +11 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/references/shotlist_schema.md +21 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/asset_list.md +7 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/camera_plan.md +10 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/scene_breakdown.md +16 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/templates/shot_list.md +5 -0
- package/resources/skills/director-skills/upstream/skills/shotlist-and-visual-breakdown/tests/screenplay_to_shotlist_tests.md +5 -0
- package/resources/skills/director-skills/upstream/skills/style-cinematography-director/SKILL.md +77 -0
- package/resources/skills/director-skills/upstream/skills/style-cinematography-director/checklists/style_quality_checklist.md +8 -0
- package/resources/skills/director-skills/upstream/skills/style-cinematography-director/examples/genre_to_visual_direction.md +15 -0
- package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/camera_lens_movement.md +26 -0
- package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/cinematic_language.md +32 -0
- package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/genre_style_tone.md +17 -0
- package/resources/skills/director-skills/upstream/skills/style-cinematography-director/references/lighting_and_photorealism.md +19 -0
- package/resources/skills/director-skills/upstream/skills/style-cinematography-director/templates/visual_style_bible.md +33 -0
- package/resources/skills/director-skills/upstream/skills/style-cinematography-director/tests/style_translation_tests.md +5 -0
- package/resources/skills/director-skills/upstream/tests/master_router_tests.md +80 -0
- package/resources/skills/drama-production/manifest.json +933 -0
- package/resources/skills/drama-production/upstream/LICENSE +21 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/SKILL.md +153 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/assets/creator-decision.example.jsonl +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/assets/dashboard/app.js +1263 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/assets/dashboard/index.html +114 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/assets/dashboard/styles.css +297 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/assets/production-observation.example.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/assets/project-template/short-drama.json +36 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/assets/reference-observation.example.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/audience-reveal.md +21 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/contract-and-ownership.md +45 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/creator-documents.md +230 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/creator-workflow.md +98 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards/Q/347/211/210/350/241/250/350/276/276.md +73 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//344/272/214/347/273/264/345/212/250/346/200/201/346/274/253.md +74 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//345/233/275/346/274/253/344/272/214/346/254/241/345/205/203.md +79 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//345/256/236/346/213/215.md +75 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//346/260/264/345/242/250/347/254/224/350/247/246.md +85 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/form-cards//351/243/216/346/240/274/345/214/226/344/270/211/347/273/264.md +72 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/knowhow-index.md +85 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/look-development.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/pickup-and-alternate.md +24 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/production-form-profiles.md +126 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/reference-roles.md +53 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/references/runtime-preflight.md +24 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/scripts/creator_markdown_check.py +1413 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/scripts/dashboard_server.py +1798 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/scripts/project_tool.py +2454 -0
- package/resources/skills/drama-production/upstream/skills/short-drama/scripts/selftest.py +287 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/SKILL.md +76 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/character-look.example.jsonl +3 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/continuity.example.jsonl +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/decisions.example.jsonl +5 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/location-view.example.jsonl +3 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/occurrences.example.jsonl +5 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/assets/prop-state.example.jsonl +3 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/examples/minimal/characters.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/examples/minimal/looks.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/asset-review-checklist.md +82 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/character-and-look.md +190 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/continuity-delta.md +120 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/continuity-lock.md +122 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/identity-vs-variant.md +140 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/location-and-view.md +84 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/occurrence-extraction.md +108 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/prop-and-state.md +126 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/stage-contract.md +43 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/references/voice-direction.md +179 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/scripts/asset_check.py +267 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-assets/scripts/selftest.py +89 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/SKILL.md +157 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/creative-brief.md +120 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/episode-map.jsonl +1 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/assets/story-engine.md +181 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/adaptation-craft.md +219 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/creative-reference-intake.md +57 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/director-brief-craft.md +113 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/episode-design.md +509 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-and-hook-playbook.md +240 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//344/272/262/345/255/220/351/232/220/347/247/230.md +112 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//344/273/231/344/276/240/344/277/256/347/234/237.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//345/212/250/344/275/234/344/273/273/345/212/241.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//345/217/244/350/243/205/346/235/203/350/260/213.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//345/256/266/345/272/255/345/205/263/347/263/273.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//346/202/254/347/226/221/350/247/204/345/210/231.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//347/224/237/346/264/273/346/265/201.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//347/240/264/351/225/234/351/207/215/345/234/206.md +109 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//350/201/214/345/234/272/345/226/234/345/211/247.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//350/261/252/351/227/250/345/251/232/346/201/213.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards//350/272/253/344/273/275/351/224/231/344/275/215.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/genre-cards.md +92 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/mechanism-loop.md +282 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/multi-episode-intake.md +128 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/premise-devices.md +211 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/reveal-reversal-payoff.md +99 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/serial-character-and-memory.md +293 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/stage-contract.md +88 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/references/story-craft.md +359 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/scripts/episode_intake.py +699 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-develop/scripts/selftest.py +47 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/SKILL.md +146 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/README.md +60 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/package.json +15 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/cut-craft.md +38 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/delivery-verify.md +29 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/generated-footage.md +54 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/sound-and-subtitles.md +60 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/references/stage-contract.md +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/scripts/edit_tool.py +1156 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-edit/scripts/selftest.py +520 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/SKILL.md +73 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/image-prompt-spec.jsonl.md +187 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/image-prompts.md +42 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/lookdev-frame-spec.jsonl.md +128 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/assets/lookdev-prompts.md +34 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/examples/minimal-image-prompt-specs.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/character-and-look.md +153 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/common-recipe.md +241 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/edit-and-revision.md +110 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/location-plate.md +151 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/look-and-state-variant.md +97 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/lookdev-frame.md +34 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/production-sheet-recipes.md +177 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/prop-plate.md +137 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/review-and-fixtures.md +92 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/references/stage-contract.md +32 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/scripts/image_prompt_check.py +443 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-image-prompts/scripts/selftest.py +95 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/SKILL.md +245 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +3 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/adaptation-triage.md +116 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/adaptation-value.md +124 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +155 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/chapter-extraction.md +144 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/references/stage-contract.md +67 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/scripts/novel_index.py +651 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-novel-analyze/scripts/selftest.py +51 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/SKILL.md +183 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/adapter-contract.md +219 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/gpt-image-2.md +24 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/minimax-h3-video.md +86 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/minimax-music.md +25 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/references/providers/seedance.md +64 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/fixture_adapter.py +77 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/production_tool.py +2205 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/provider_adapters.py +1747 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-produce/scripts/selftest.py +135 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/SKILL.md +118 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/assets/finding-template.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/assets/supersession-decision.example.json +42 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/assets/verdict-template.json +32 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/examples/minimal-findings.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/examples/minimal-verdict.json +34 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/references/anti-template-repair.md +109 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/references/production-quality-gates.md +276 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/references/project-calibration.md +83 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/references/review-method.md +81 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-assets-prompts.md +111 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-source-analysis.md +108 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-story-script.md +264 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/references/rubric-visual-motion.md +184 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/references/stage-contract.md +28 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/scripts/review_check.py +325 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-review/scripts/selftest.py +87 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/SKILL.md +175 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/coverage-audition.example.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/coverage-template.json +49 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/keyframe-prompts.md +36 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/keyframe-template.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/revision-lineage.fragment.json +12 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/scene-visual-plan.example.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/assets/shot-template.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/blocking-playbooks.md +216 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/comic-keyframe-lexicon.md +156 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/coverage-audition.md +34 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/keyframe-craft.md +174 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/production-shot-grammar.md +279 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/review-and-fixtures.md +36 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/scene-visual-plan.md +30 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +155 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/shot-craft.md +266 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/shot-revision-identity.md +25 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/references/stage-contract.md +57 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/scripts/selftest.py +191 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-storyboard/scripts/storyboard_check.py +824 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/SKILL.md +154 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/coverage-scope.fragment.json +21 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/delivery-container.jsonl.md +101 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +217 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/motion-terminal.example.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/music-spec.jsonl.md +31 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/performance.fragment.json +38 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/assets/video-prompts.md +67 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/examples/minimal-music-specs.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/camera-audio-continuity.md +259 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/delivery-profile.md +273 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/generability.md +93 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/minimax-h3.md +253 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/motion-recipe.md +315 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/performance-action-timing.md +209 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/production-prompt-grammar.md +304 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/review-and-fixtures.md +139 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/seedance-2.0.md +50 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/seedance-2.5.md +122 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/stage-contract.md +60 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/references/target-model-profile.md +116 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/container_check.py +329 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/motion_timing_check.py +450 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/music_spec_check.py +327 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-video-prompts/scripts/selftest.py +82 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/SKILL.md +99 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/agents/openai.yaml +4 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/beats.jsonl +2 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/episode-card-standalone.json +120 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/episode-card.json +55 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/screenplay.md +36 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/assets/voice-record-sheet.jsonl.md +89 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/references/dialogue-craft.md +526 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/references/production-format-dialect.md +273 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/references/scene-handoff-capsule.md +25 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/references/scene-sound-dramaturgy.md +43 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/references/screenplay-format.md +266 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/references/script-craft.md +745 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/references/stage-contract.md +36 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/references/substitutable-realization.md +107 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/duration_estimate.py +309 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/screenplay_index.py +1091 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/selftest.py +229 -0
- package/resources/skills/drama-production/upstream/skills/short-drama-write/scripts/voice_sheet_check.py +416 -0
- package/resources/skills/viora-canvas-production/manifest.json +23 -0
- package/resources/skills/viora-canvas-production/references/canvas-contract.md +50 -0
- package/resources/skills/viora-canvas-production/references/creative-production.md +50 -0
- package/resources/skills/viora-canvas-production/references/generation-delivery.md +36 -0
- package/resources/skills/viora-canvas-production/references/production-ledger.md +25 -0
package/resources/skills/drama-production/upstream/skills/short-drama-edit/scripts/edit_tool.py
ADDED
|
@@ -0,0 +1,1156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Assemble a short-drama episode from its cut list.
|
|
3
|
+
|
|
4
|
+
Three subcommands, deliberately separated so a report can never borrow one's
|
|
5
|
+
evidence for another's claim:
|
|
6
|
+
|
|
7
|
+
``check`` parse and cross-check ``剪辑单.md`` against the project. No rendering.
|
|
8
|
+
``render`` cut, join, burn subtitles and normalize loudness into 制作成果/成片/.
|
|
9
|
+
``verify`` measure an already-rendered film and print the numbers.
|
|
10
|
+
|
|
11
|
+
``verify`` prints measurements, never verdicts. Whether the film is any good is
|
|
12
|
+
a question for review or for the creator, and no number here answers it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, NamedTuple, Optional, Sequence
|
|
26
|
+
|
|
27
|
+
MINIMUM_PYTHON = (3, 9)
|
|
28
|
+
if sys.version_info < MINIMUM_PYTHON:
|
|
29
|
+
raise SystemExit(
|
|
30
|
+
"short-drama-edit needs Python {}.{} or newer; this interpreter is {}.{}".format(
|
|
31
|
+
*MINIMUM_PYTHON, sys.version_info.major, sys.version_info.minor
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
CUT_LIST_NAME = "剪辑单.md"
|
|
36
|
+
MOTION_DOCUMENT = "视频提示词.md"
|
|
37
|
+
SCREENPLAY_DOCUMENT = "剧本.md"
|
|
38
|
+
OUTPUT_DIRECTORY = Path("制作成果") / "成片"
|
|
39
|
+
SEGMENT_DIRECTORY = "分段"
|
|
40
|
+
# A CJK subtitle needs a font that actually has the glyphs. libass falls back
|
|
41
|
+
# through fontconfig when this one is absent, which is the right behaviour: a
|
|
42
|
+
# missing font shows as a different typeface, never as empty boxes.
|
|
43
|
+
SUBTITLE_FONT = "Noto Sans CJK SC"
|
|
44
|
+
# The optional Remotion route ships as source only. Its `node_modules` is
|
|
45
|
+
# hundreds of megabytes of third-party code that is neither creative content nor
|
|
46
|
+
# part of the skill, so the runnable copy lives outside the project entirely --
|
|
47
|
+
# the same rule the suite applies to adapter configuration and credentials.
|
|
48
|
+
REMOTION_SOURCE = Path(__file__).resolve().parent.parent / "assets" / "remotion"
|
|
49
|
+
REMOTION_SOURCE_FILES = (
|
|
50
|
+
"package.json",
|
|
51
|
+
"tsconfig.json",
|
|
52
|
+
"remotion.config.ts",
|
|
53
|
+
"src/index.ts",
|
|
54
|
+
"src/schema.ts",
|
|
55
|
+
"src/Root.tsx",
|
|
56
|
+
"src/Subtitles.tsx",
|
|
57
|
+
"src/font.ts",
|
|
58
|
+
)
|
|
59
|
+
DEFAULT_REMOTION_WORKSPACE = Path.home() / ".cache" / "short-drama-edit" / "remotion"
|
|
60
|
+
REMOTION_COMPOSITION = "Subtitles"
|
|
61
|
+
# Remotion defaults its worker count to the machine's core count, and each worker
|
|
62
|
+
# is a browser holding a full frame. On a 10-core / 8 GB laptop that took the
|
|
63
|
+
# machine down mid-render. Raise this only after measuring the host.
|
|
64
|
+
DEFAULT_REMOTION_CONCURRENCY = 2
|
|
65
|
+
SUBTITLE_RENDERERS = ("ffmpeg", "remotion")
|
|
66
|
+
|
|
67
|
+
CUT_HEADING = re.compile(r"^##\s+(CUT-[^\s·]+)\s*(?:·\s*(.*))?$")
|
|
68
|
+
MOTION_HEADING = re.compile(r"^##\s+(MOTION-[^\s·]+)")
|
|
69
|
+
FIELD = re.compile(r"^-\s*([^:]+):\s*(.*)$")
|
|
70
|
+
UNUSED_LINE = re.compile(r"^-\s*未采用镜头:\s*(.*)$")
|
|
71
|
+
# A source line is "MOTION-... · relative/path". The separator is the same
|
|
72
|
+
# middle dot the storyboard uses for reference slots, so the two documents read
|
|
73
|
+
# alike; a plain slash would collide with the path itself.
|
|
74
|
+
SOURCE = re.compile(r"^(MOTION-\S+)\s*·\s*(.+?)\s*$")
|
|
75
|
+
SUBTITLE_WINDOW = re.compile(r"^\s*([0-9.]+)\s*[-–~]\s*([0-9.]+)\s*$")
|
|
76
|
+
# One shot can carry several lines: an exchange of three is one shot, not three.
|
|
77
|
+
# Numbered fields keep them ordered and let each one state its own window.
|
|
78
|
+
SUBTITLE_FIELD = re.compile(r"^字幕(?:\s*(\d+))?$")
|
|
79
|
+
SUBTITLE_CUE = re.compile(r"^\s*([0-9.]+)\s*[-–~]\s*([0-9.]+)\s+(.+?)\s*$")
|
|
80
|
+
# A shot-match is three numbers and nothing else. Anything richer belongs in a
|
|
81
|
+
# grading tool, and anything implicit belongs nowhere: a correction the cut list
|
|
82
|
+
# does not state is a correction no reviewer can see.
|
|
83
|
+
PICTURE_KEYS = {"亮度": "brightness", "饱和": "saturation", "色温": "warmth"}
|
|
84
|
+
PICTURE_TERM = re.compile(r"(亮度|饱和|色温)\s*([+-]?[0-9]*\.?[0-9]+)")
|
|
85
|
+
# Each term's real range, not a symmetric magnitude: ffmpeg's `eq` takes
|
|
86
|
+
# saturation in 0..3, so a negative value that passes an abs() bound is accepted
|
|
87
|
+
# by the document and then rejected by the renderer, halfway through a render.
|
|
88
|
+
PICTURE_LIMITS = {
|
|
89
|
+
"brightness": (-0.25, 0.25),
|
|
90
|
+
"saturation": (0.0, 2.0),
|
|
91
|
+
"warmth": (-30.0, 30.0),
|
|
92
|
+
}
|
|
93
|
+
TOLERANCE = 0.005
|
|
94
|
+
# ffmpeg's `noise` strength runs to 100, which is snow, not grain. The useful
|
|
95
|
+
# band for a finished film is single digits; the cap keeps a typo from shipping
|
|
96
|
+
# a broken-signal look that measures perfectly fine.
|
|
97
|
+
GRAIN_LIMIT = 20.0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class EditError(Exception):
|
|
101
|
+
"""A defect in the cut list or its inputs, reported rather than raised through."""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Cut(NamedTuple):
|
|
105
|
+
cut_id: str
|
|
106
|
+
title: str
|
|
107
|
+
motion: str
|
|
108
|
+
media: str
|
|
109
|
+
start: float
|
|
110
|
+
end: float
|
|
111
|
+
declared: float
|
|
112
|
+
subtitles: tuple[tuple[Optional[float], Optional[float], str], ...]
|
|
113
|
+
picture: dict[str, float]
|
|
114
|
+
line_number: int
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Delivery(NamedTuple):
|
|
118
|
+
target_seconds: Optional[float]
|
|
119
|
+
loudness_lufs: Optional[float]
|
|
120
|
+
burn_subtitles: bool
|
|
121
|
+
frame_size: Optional[tuple[int, int]]
|
|
122
|
+
fps: Optional[float]
|
|
123
|
+
grain: Optional[float]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _seconds(raw: str, *, field: str, line: int) -> float:
|
|
127
|
+
try:
|
|
128
|
+
return float(raw.strip())
|
|
129
|
+
except ValueError as error:
|
|
130
|
+
raise EditError(f"{CUT_LIST_NAME}:{line}: {field} 不是秒数: {raw!r}") from error
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_delivery(lines: Sequence[str]) -> Delivery:
|
|
134
|
+
target = None
|
|
135
|
+
loudness = None
|
|
136
|
+
burn = True
|
|
137
|
+
frame_size: Optional[tuple[int, int]] = None
|
|
138
|
+
fps: Optional[float] = None
|
|
139
|
+
grain: Optional[float] = None
|
|
140
|
+
for raw in lines:
|
|
141
|
+
match = FIELD.match(raw)
|
|
142
|
+
if not match:
|
|
143
|
+
continue
|
|
144
|
+
name, value = match.group(1).strip(), match.group(2).strip()
|
|
145
|
+
if name == "成片目标时长":
|
|
146
|
+
found = re.search(r"[0-9]+(?:\.[0-9]+)?", value)
|
|
147
|
+
if found:
|
|
148
|
+
target = float(found.group(0))
|
|
149
|
+
elif name == "交付响度":
|
|
150
|
+
found = re.search(r"-?[0-9]+(?:\.[0-9]+)?", value)
|
|
151
|
+
if found:
|
|
152
|
+
loudness = float(found.group(0))
|
|
153
|
+
elif name == "字幕":
|
|
154
|
+
burn = "无" != value.strip() and "不烧" not in value
|
|
155
|
+
elif name == "颗粒":
|
|
156
|
+
# Grain belongs to the delivery spec rather than to a cut, for the
|
|
157
|
+
# same reason loudness does: applied per cut it would become one
|
|
158
|
+
# more thing that differs between segments, which is the defect it
|
|
159
|
+
# is here to cover.
|
|
160
|
+
if value.strip() in {"无", "不加"}:
|
|
161
|
+
grain = None
|
|
162
|
+
else:
|
|
163
|
+
found = re.search(r"[0-9]+(?:\.[0-9]+)?", value)
|
|
164
|
+
if found:
|
|
165
|
+
amount = float(found.group(0))
|
|
166
|
+
if not 0.0 <= amount <= GRAIN_LIMIT:
|
|
167
|
+
raise EditError(
|
|
168
|
+
f"{CUT_LIST_NAME}: 颗粒 {amount} 超出 0–{GRAIN_LIMIT:g};"
|
|
169
|
+
"这一档以上不再像胶片,像信号故障"
|
|
170
|
+
)
|
|
171
|
+
grain = amount or None
|
|
172
|
+
elif name == "画幅与帧率":
|
|
173
|
+
size = re.search(r"([0-9]{2,5})\s*[×x*]\s*([0-9]{2,5})", value)
|
|
174
|
+
if size:
|
|
175
|
+
frame_size = (int(size.group(1)), int(size.group(2)))
|
|
176
|
+
rate = re.search(r"([0-9]+(?:\.[0-9]+)?)\s*fps", value, re.I)
|
|
177
|
+
if rate:
|
|
178
|
+
fps = float(rate.group(1))
|
|
179
|
+
return Delivery(target, loudness, burn, frame_size, fps, grain)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def parse_cut_list(path: Path) -> tuple[Delivery, list[Cut], list[str]]:
|
|
183
|
+
"""Read 剪辑单.md into a delivery spec, ordered cuts, and unused-material notes."""
|
|
184
|
+
|
|
185
|
+
if not path.is_file():
|
|
186
|
+
raise EditError(f"没有 {path}")
|
|
187
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
188
|
+
preamble: list[str] = []
|
|
189
|
+
unused: list[str] = []
|
|
190
|
+
cuts: list[Cut] = []
|
|
191
|
+
current: Optional[dict[str, Any]] = None
|
|
192
|
+
|
|
193
|
+
def close(pending: Optional[dict[str, Any]]) -> None:
|
|
194
|
+
if pending is None:
|
|
195
|
+
return
|
|
196
|
+
cuts.append(_finish_cut(pending))
|
|
197
|
+
|
|
198
|
+
for number, raw in enumerate(lines, start=1):
|
|
199
|
+
heading = CUT_HEADING.match(raw)
|
|
200
|
+
if heading:
|
|
201
|
+
close(current)
|
|
202
|
+
current = {
|
|
203
|
+
"cut_id": heading.group(1),
|
|
204
|
+
"title": (heading.group(2) or "").strip(),
|
|
205
|
+
"line": number,
|
|
206
|
+
"fields": {},
|
|
207
|
+
}
|
|
208
|
+
continue
|
|
209
|
+
if raw.startswith("## "):
|
|
210
|
+
close(current)
|
|
211
|
+
current = None
|
|
212
|
+
continue
|
|
213
|
+
if current is None:
|
|
214
|
+
unused_match = UNUSED_LINE.match(raw)
|
|
215
|
+
if unused_match:
|
|
216
|
+
unused.extend(
|
|
217
|
+
item.strip() for item in unused_match.group(1).split(";") if item.strip()
|
|
218
|
+
)
|
|
219
|
+
preamble.append(raw)
|
|
220
|
+
continue
|
|
221
|
+
field = FIELD.match(raw)
|
|
222
|
+
if field:
|
|
223
|
+
current["fields"][field.group(1).strip()] = (field.group(2).strip(), number)
|
|
224
|
+
close(current)
|
|
225
|
+
return _parse_delivery(preamble), cuts, unused
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _finish_cut(pending: dict[str, Any]) -> Cut:
|
|
229
|
+
fields: dict[str, tuple[str, int]] = pending["fields"]
|
|
230
|
+
cut_id: str = pending["cut_id"]
|
|
231
|
+
line: int = pending["line"]
|
|
232
|
+
|
|
233
|
+
def required(name: str) -> tuple[str, int]:
|
|
234
|
+
if name not in fields:
|
|
235
|
+
raise EditError(f"{CUT_LIST_NAME}:{line}: {cut_id} 缺少「{name}」")
|
|
236
|
+
return fields[name]
|
|
237
|
+
|
|
238
|
+
source_raw, source_line = required("来源")
|
|
239
|
+
source = SOURCE.match(source_raw)
|
|
240
|
+
if not source:
|
|
241
|
+
raise EditError(
|
|
242
|
+
f"{CUT_LIST_NAME}:{source_line}: {cut_id} 的来源要写成 "
|
|
243
|
+
f"「MOTION-... · 项目相对路径」,当前是 {source_raw!r}"
|
|
244
|
+
)
|
|
245
|
+
start_raw, start_line = required("入点")
|
|
246
|
+
end_raw, end_line = required("出点")
|
|
247
|
+
declared_raw, declared_line = required("时长")
|
|
248
|
+
subtitles = _parse_subtitles(fields, cut_id=cut_id, line=line)
|
|
249
|
+
picture: dict[str, float] = {}
|
|
250
|
+
picture_raw = fields.get("画面")
|
|
251
|
+
if picture_raw is not None:
|
|
252
|
+
text, where = picture_raw
|
|
253
|
+
if text.strip() not in {"", "无", "不校"}:
|
|
254
|
+
for label, value in PICTURE_TERM.findall(text):
|
|
255
|
+
key = PICTURE_KEYS[label]
|
|
256
|
+
number = float(value)
|
|
257
|
+
low, high = PICTURE_LIMITS[key]
|
|
258
|
+
if not low <= number <= high:
|
|
259
|
+
raise EditError(
|
|
260
|
+
f"{CUT_LIST_NAME}:{where}: {cut_id} 的画面「{label}」超出允许范围"
|
|
261
|
+
f"({low:g} 到 {high:g}):{number}"
|
|
262
|
+
)
|
|
263
|
+
picture[key] = number
|
|
264
|
+
if not picture:
|
|
265
|
+
raise EditError(
|
|
266
|
+
f"{CUT_LIST_NAME}:{where}: {cut_id} 的画面写了内容但没有可执行的项;"
|
|
267
|
+
"只认「亮度 <数>」「饱和 <数>」「色温 <数>」,不需要校正时写「无」"
|
|
268
|
+
)
|
|
269
|
+
return Cut(
|
|
270
|
+
cut_id=cut_id,
|
|
271
|
+
title=pending["title"],
|
|
272
|
+
motion=source.group(1),
|
|
273
|
+
media=source.group(2),
|
|
274
|
+
start=_seconds(start_raw, field="入点", line=start_line),
|
|
275
|
+
end=_seconds(end_raw, field="出点", line=end_line),
|
|
276
|
+
declared=_seconds(declared_raw, field="时长", line=declared_line),
|
|
277
|
+
subtitles=subtitles,
|
|
278
|
+
picture=picture,
|
|
279
|
+
line_number=line,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _parse_subtitles(
|
|
284
|
+
fields: dict[str, tuple[str, int]], *, cut_id: str, line: int
|
|
285
|
+
) -> tuple[tuple[Optional[float], Optional[float], str], ...]:
|
|
286
|
+
"""Read every subtitle a cut declares, in written order.
|
|
287
|
+
|
|
288
|
+
A shot is not one line. The exchange "就是什么 / 就是少了点东西 / 少了什么" is a
|
|
289
|
+
single over-shoulder shot carrying three, and a cut list that can hold only
|
|
290
|
+
one of them silently drops the other two — the film then plays lines that
|
|
291
|
+
never reach the screen, and nothing reports it.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
numbered: list[tuple[int, str, int]] = []
|
|
295
|
+
plain: Optional[tuple[str, int]] = None
|
|
296
|
+
for key, (value, where) in fields.items():
|
|
297
|
+
match = SUBTITLE_FIELD.match(key.strip())
|
|
298
|
+
if not match:
|
|
299
|
+
continue
|
|
300
|
+
if match.group(1) is None:
|
|
301
|
+
plain = (value, where)
|
|
302
|
+
else:
|
|
303
|
+
numbered.append((int(match.group(1)), value, where))
|
|
304
|
+
|
|
305
|
+
if numbered and plain is not None:
|
|
306
|
+
raise EditError(
|
|
307
|
+
f"{CUT_LIST_NAME}:{line}: {cut_id} 同时写了「字幕」和「字幕 N」;"
|
|
308
|
+
"一段用一种写法——一句用「字幕」,多句全部编号"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if numbered:
|
|
312
|
+
numbered.sort()
|
|
313
|
+
expected = list(range(1, len(numbered) + 1))
|
|
314
|
+
if [item[0] for item in numbered] != expected:
|
|
315
|
+
raise EditError(
|
|
316
|
+
f"{CUT_LIST_NAME}:{line}: {cut_id} 的字幕编号要从 1 连续排到 "
|
|
317
|
+
f"{len(numbered)},当前是 {[item[0] for item in numbered]}"
|
|
318
|
+
)
|
|
319
|
+
cues: list[tuple[Optional[float], Optional[float], str]] = []
|
|
320
|
+
for index, value, where in numbered:
|
|
321
|
+
found = SUBTITLE_CUE.match(value)
|
|
322
|
+
if not found:
|
|
323
|
+
raise EditError(
|
|
324
|
+
f"{CUT_LIST_NAME}:{where}: {cut_id} 的「字幕 {index}」要写成"
|
|
325
|
+
"「<起>-<止> <台词>」,多句必须各自带时间"
|
|
326
|
+
)
|
|
327
|
+
cues.append((float(found.group(1)), float(found.group(2)), found.group(3)))
|
|
328
|
+
for earlier, later in zip(cues, cues[1:]):
|
|
329
|
+
if later[0] is not None and earlier[1] is not None and later[0] < earlier[1]:
|
|
330
|
+
raise EditError(
|
|
331
|
+
f"{CUT_LIST_NAME}:{line}: {cut_id} 的字幕时间重叠:两句不能同时在屏上"
|
|
332
|
+
)
|
|
333
|
+
return tuple(cues)
|
|
334
|
+
|
|
335
|
+
if plain is None or plain[0].strip() == "无":
|
|
336
|
+
return ()
|
|
337
|
+
window_raw = fields.get("字幕时间")
|
|
338
|
+
if window_raw is None:
|
|
339
|
+
return ((None, None, plain[0].strip()),)
|
|
340
|
+
found = SUBTITLE_WINDOW.match(window_raw[0])
|
|
341
|
+
if not found:
|
|
342
|
+
raise EditError(
|
|
343
|
+
f"{CUT_LIST_NAME}:{window_raw[1]}: {cut_id} 的字幕时间要写成「起-止」秒数"
|
|
344
|
+
)
|
|
345
|
+
return ((float(found.group(1)), float(found.group(2)), plain[0].strip()),)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _which(name: str) -> Optional[str]:
|
|
349
|
+
return shutil.which(name)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _require(name: str) -> str:
|
|
353
|
+
found = _which(name)
|
|
354
|
+
if found is None:
|
|
355
|
+
raise EditError(
|
|
356
|
+
f"PATH 上没有 {name}。本阶段要渲染和测量真实媒体,没有它就没有可报告的事实;"
|
|
357
|
+
"先安装 ffmpeg,不用别的手段近似。"
|
|
358
|
+
)
|
|
359
|
+
return found
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def probe_duration(media: Path) -> float:
|
|
363
|
+
probe = _require("ffprobe")
|
|
364
|
+
result = subprocess.run(
|
|
365
|
+
[probe, "-v", "error", "-show_entries", "format=duration",
|
|
366
|
+
"-of", "default=nw=1:nk=1", str(media)],
|
|
367
|
+
capture_output=True, text=True, check=False,
|
|
368
|
+
)
|
|
369
|
+
if result.returncode != 0:
|
|
370
|
+
raise EditError(f"ffprobe 读不出时长: {media} ({result.stderr.strip()})")
|
|
371
|
+
return float(result.stdout.strip())
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def probe_stream(media: Path) -> dict[str, Any]:
|
|
375
|
+
probe = _require("ffprobe")
|
|
376
|
+
result = subprocess.run(
|
|
377
|
+
[probe, "-v", "error", "-select_streams", "v:0", "-show_entries",
|
|
378
|
+
"stream=width,height,r_frame_rate", "-show_entries", "format=duration",
|
|
379
|
+
"-of", "json", str(media)],
|
|
380
|
+
capture_output=True, text=True, check=False,
|
|
381
|
+
)
|
|
382
|
+
if result.returncode != 0:
|
|
383
|
+
raise EditError(f"ffprobe 读不出流信息: {media} ({result.stderr.strip()})")
|
|
384
|
+
document = json.loads(result.stdout)
|
|
385
|
+
stream = (document.get("streams") or [{}])[0]
|
|
386
|
+
rate = stream.get("r_frame_rate", "0/1")
|
|
387
|
+
numerator, _, denominator = rate.partition("/")
|
|
388
|
+
fps = float(numerator) / float(denominator) if float(denominator or 0) else 0.0
|
|
389
|
+
return {
|
|
390
|
+
"width": stream.get("width"),
|
|
391
|
+
"height": stream.get("height"),
|
|
392
|
+
"fps": round(fps, 3),
|
|
393
|
+
"duration": float(document.get("format", {}).get("duration", 0.0)),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _unaccounted_shots(known: set[str], cuts: Sequence[Cut], unused: Sequence[str]) -> list[str]:
|
|
398
|
+
"""Require each source to be used or explicitly omitted with a reason."""
|
|
399
|
+
|
|
400
|
+
used = {cut.motion for cut in cuts}
|
|
401
|
+
excused = set()
|
|
402
|
+
for note in unused:
|
|
403
|
+
match = re.fullmatch(r"(MOTION-[\w-]+)\s*[((]理由[::]\s*(.+?)[))]", note.strip())
|
|
404
|
+
if match and match.group(2).strip():
|
|
405
|
+
excused.add(match.group(1))
|
|
406
|
+
missing = sorted(known - used - excused)
|
|
407
|
+
if not missing:
|
|
408
|
+
return []
|
|
409
|
+
return [
|
|
410
|
+
f"{CUT_LIST_NAME}: 以下镜头未采用,且缺少「未采用镜头」及理由:"
|
|
411
|
+
+ "、".join(missing)
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def check_cuts(
|
|
416
|
+
episode: Path,
|
|
417
|
+
cuts: Sequence[Cut],
|
|
418
|
+
project_root: Path,
|
|
419
|
+
*,
|
|
420
|
+
probe: bool,
|
|
421
|
+
unused: Sequence[str] = (),
|
|
422
|
+
) -> list[str]:
|
|
423
|
+
"""Every mechanical cross-check the cut list can be held to. Returns findings."""
|
|
424
|
+
|
|
425
|
+
findings: list[str] = []
|
|
426
|
+
if not cuts:
|
|
427
|
+
findings.append(f"{CUT_LIST_NAME}: 没有 CUT 条目")
|
|
428
|
+
return findings
|
|
429
|
+
|
|
430
|
+
seen: set[str] = set()
|
|
431
|
+
for cut in cuts:
|
|
432
|
+
if cut.cut_id in seen:
|
|
433
|
+
findings.append(f"{CUT_LIST_NAME}:{cut.line_number}: CUT ID 重复: {cut.cut_id}")
|
|
434
|
+
seen.add(cut.cut_id)
|
|
435
|
+
|
|
436
|
+
motion_path = episode / MOTION_DOCUMENT
|
|
437
|
+
if motion_path.is_file():
|
|
438
|
+
known = {
|
|
439
|
+
match.group(1)
|
|
440
|
+
for match in (
|
|
441
|
+
MOTION_HEADING.match(line)
|
|
442
|
+
for line in motion_path.read_text(encoding="utf-8").splitlines()
|
|
443
|
+
)
|
|
444
|
+
if match
|
|
445
|
+
}
|
|
446
|
+
for cut in cuts:
|
|
447
|
+
if cut.motion not in known:
|
|
448
|
+
findings.append(
|
|
449
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的来源 "
|
|
450
|
+
f"{cut.motion} 不在《{MOTION_DOCUMENT}》中"
|
|
451
|
+
)
|
|
452
|
+
findings.extend(_unaccounted_shots(known, cuts, unused))
|
|
453
|
+
else:
|
|
454
|
+
findings.append(f"没有 {motion_path},来源 MOTION 无法核对")
|
|
455
|
+
|
|
456
|
+
screenplay = ""
|
|
457
|
+
screenplay_path = episode / SCREENPLAY_DOCUMENT
|
|
458
|
+
if screenplay_path.is_file():
|
|
459
|
+
screenplay = screenplay_path.read_text(encoding="utf-8")
|
|
460
|
+
|
|
461
|
+
media_format: Optional[tuple[Any, Any, float]] = None
|
|
462
|
+
for cut in cuts:
|
|
463
|
+
span = cut.end - cut.start
|
|
464
|
+
if cut.start < 0:
|
|
465
|
+
findings.append(f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 入点为负")
|
|
466
|
+
if span <= 0:
|
|
467
|
+
findings.append(
|
|
468
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 出点不晚于入点"
|
|
469
|
+
)
|
|
470
|
+
elif abs(span - cut.declared) > TOLERANCE:
|
|
471
|
+
findings.append(
|
|
472
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 出点 - 入点 = "
|
|
473
|
+
f"{span:.2f},与「时长:{cut.declared:.2f}」不符"
|
|
474
|
+
)
|
|
475
|
+
media = _resolve_media(episode, project_root, cut.media)
|
|
476
|
+
if media is None:
|
|
477
|
+
findings.append(
|
|
478
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的素材不存在: {cut.media}"
|
|
479
|
+
)
|
|
480
|
+
elif probe:
|
|
481
|
+
stream = probe_stream(media)
|
|
482
|
+
available = stream["duration"]
|
|
483
|
+
current_format = (stream["width"], stream["height"], stream["fps"])
|
|
484
|
+
if media_format is None:
|
|
485
|
+
media_format = current_format
|
|
486
|
+
elif current_format != media_format:
|
|
487
|
+
findings.append(
|
|
488
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的画幅或帧率 "
|
|
489
|
+
f"{current_format} 与首段 {media_format} 不一致;"
|
|
490
|
+
"先在外部统一素材规格,再更新来源路径与入出点"
|
|
491
|
+
)
|
|
492
|
+
if cut.end > available + TOLERANCE:
|
|
493
|
+
findings.append(
|
|
494
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 出点 {cut.end:.2f} "
|
|
495
|
+
f"超过素材实际时长 {available:.2f}"
|
|
496
|
+
)
|
|
497
|
+
for window_start, window_end, text in cut.subtitles:
|
|
498
|
+
if screenplay and _normalize(text) not in _normalize(screenplay):
|
|
499
|
+
findings.append(
|
|
500
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的字幕在《"
|
|
501
|
+
f"{SCREENPLAY_DOCUMENT}》里找不到原文: {text}"
|
|
502
|
+
)
|
|
503
|
+
if window_start is None or window_end is None:
|
|
504
|
+
continue
|
|
505
|
+
if not 0 <= window_start < window_end <= span + TOLERANCE:
|
|
506
|
+
findings.append(
|
|
507
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的字幕时间超出本段区间"
|
|
508
|
+
f": {window_start}-{window_end}"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
findings.extend(_overlap_findings(cuts))
|
|
512
|
+
findings.extend(_stale_window_findings(episode, project_root, cuts))
|
|
513
|
+
return findings
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _stale_window_findings(
|
|
517
|
+
episode: Path, project_root: Path, cuts: Sequence[Cut]
|
|
518
|
+
) -> list[str]:
|
|
519
|
+
"""Material newer than the cut list means the timings were measured on something else.
|
|
520
|
+
|
|
521
|
+
Regenerating one shot is routine — the take was hazy, the action was wrong.
|
|
522
|
+
What is easy to forget is that every subtitle window bound to it was reverse-
|
|
523
|
+
engineered from the *old* take's audio. Nothing else notices: the numbers are
|
|
524
|
+
still in range, the render still succeeds, and the subtitle simply appears at
|
|
525
|
+
a moment when nobody is speaking. This is the one form of staleness the file
|
|
526
|
+
system can see, so it is reported rather than trusted to memory.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
listing = episode / CUT_LIST_NAME
|
|
530
|
+
try:
|
|
531
|
+
authored = listing.stat().st_mtime
|
|
532
|
+
except OSError:
|
|
533
|
+
return []
|
|
534
|
+
findings: list[str] = []
|
|
535
|
+
for cut in cuts:
|
|
536
|
+
if not any(start is not None for start, _, _ in cut.subtitles):
|
|
537
|
+
continue
|
|
538
|
+
media = _resolve_media(episode, project_root, cut.media)
|
|
539
|
+
if media is None:
|
|
540
|
+
continue
|
|
541
|
+
try:
|
|
542
|
+
changed = media.stat().st_mtime
|
|
543
|
+
except OSError:
|
|
544
|
+
continue
|
|
545
|
+
if changed > authored + 1.0:
|
|
546
|
+
findings.append(
|
|
547
|
+
f"{CUT_LIST_NAME}:{cut.line_number}: {cut.cut_id} 的素材比剪辑单新"
|
|
548
|
+
f"({cut.media});这一段的字幕时间是按旧素材的发声区间反推的,"
|
|
549
|
+
"重出之后必须重测再改,不能沿用"
|
|
550
|
+
)
|
|
551
|
+
return findings
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _overlap_findings(cuts: Sequence[Cut]) -> list[str]:
|
|
555
|
+
"""Two cuts drawn from one file must not reuse the same frames (EDT-06)."""
|
|
556
|
+
|
|
557
|
+
findings: list[str] = []
|
|
558
|
+
by_media: dict[str, list[Cut]] = {}
|
|
559
|
+
for cut in cuts:
|
|
560
|
+
by_media.setdefault(cut.media, []).append(cut)
|
|
561
|
+
for media, group in by_media.items():
|
|
562
|
+
ordered = sorted(group, key=lambda item: item.start)
|
|
563
|
+
for earlier, later in zip(ordered, ordered[1:]):
|
|
564
|
+
if later.start < earlier.end - TOLERANCE:
|
|
565
|
+
findings.append(
|
|
566
|
+
f"{CUT_LIST_NAME}: {earlier.cut_id} 与 {later.cut_id} 在同一素材 "
|
|
567
|
+
f"{media} 上区间重叠({later.start:.2f} < {earlier.end:.2f})"
|
|
568
|
+
)
|
|
569
|
+
return findings
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _normalize(text: str) -> str:
|
|
573
|
+
"""Compare dialogue ignoring punctuation and whitespace, never ignoring characters."""
|
|
574
|
+
|
|
575
|
+
return re.sub(r"[\s,。!?、;:…—·\-“”‘’\"'()()]", "", text)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _resolve_media(episode: Path, project_root: Path, relative: str) -> Optional[Path]:
|
|
579
|
+
for base in (episode, project_root):
|
|
580
|
+
candidate = (base / relative).resolve()
|
|
581
|
+
if candidate.is_file():
|
|
582
|
+
return candidate
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def render(
|
|
587
|
+
episode: Path,
|
|
588
|
+
project_root: Path,
|
|
589
|
+
cuts: Sequence[Cut],
|
|
590
|
+
delivery: Delivery,
|
|
591
|
+
*,
|
|
592
|
+
burn_subtitles: bool,
|
|
593
|
+
renderer: str = "ffmpeg",
|
|
594
|
+
remotion_workspace: Path = DEFAULT_REMOTION_WORKSPACE,
|
|
595
|
+
remotion_concurrency: int = DEFAULT_REMOTION_CONCURRENCY,
|
|
596
|
+
) -> dict[str, Any]:
|
|
597
|
+
ffmpeg = _require("ffmpeg")
|
|
598
|
+
_require("ffprobe")
|
|
599
|
+
output_root = episode / OUTPUT_DIRECTORY
|
|
600
|
+
segments_root = output_root / SEGMENT_DIRECTORY
|
|
601
|
+
segments_root.mkdir(parents=True, exist_ok=True)
|
|
602
|
+
|
|
603
|
+
segments: list[Path] = []
|
|
604
|
+
spans: list[float] = []
|
|
605
|
+
for cut in cuts:
|
|
606
|
+
media = _resolve_media(episode, project_root, cut.media)
|
|
607
|
+
if media is None:
|
|
608
|
+
raise EditError(f"{cut.cut_id} 的素材不存在: {cut.media}")
|
|
609
|
+
segment = segments_root / f"{cut.cut_id}.mp4"
|
|
610
|
+
command = [
|
|
611
|
+
ffmpeg, "-hide_banner", "-loglevel", "error", "-y",
|
|
612
|
+
"-ss", f"{cut.start:.3f}", "-t", f"{cut.end - cut.start:.3f}", "-i", str(media),
|
|
613
|
+
]
|
|
614
|
+
match = _shot_match_filter(cut)
|
|
615
|
+
if match:
|
|
616
|
+
command += ["-vf", match]
|
|
617
|
+
command += [
|
|
618
|
+
"-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p",
|
|
619
|
+
"-c:a", "aac", "-b:a", "192k", str(segment),
|
|
620
|
+
]
|
|
621
|
+
_run(command)
|
|
622
|
+
segments.append(segment)
|
|
623
|
+
spans.append(probe_duration(segment))
|
|
624
|
+
|
|
625
|
+
with tempfile.TemporaryDirectory() as scratch:
|
|
626
|
+
listing = Path(scratch) / "segments.txt"
|
|
627
|
+
listing.write_text(
|
|
628
|
+
"".join(f"file '{segment.as_posix()}'\n" for segment in segments), encoding="utf-8"
|
|
629
|
+
)
|
|
630
|
+
joined = output_root / "成片-未混音.mp4"
|
|
631
|
+
_run([ffmpeg, "-hide_banner", "-loglevel", "error", "-y",
|
|
632
|
+
"-f", "concat", "-safe", "0", "-i", str(listing), "-c", "copy", str(joined)])
|
|
633
|
+
|
|
634
|
+
filters: list[str] = []
|
|
635
|
+
subtitle_path = None
|
|
636
|
+
styled_path = None
|
|
637
|
+
overlay_path = None
|
|
638
|
+
if burn_subtitles and any(cut.subtitles for cut in cuts):
|
|
639
|
+
canvas = probe_stream(segments[0])
|
|
640
|
+
cues = _subtitle_cues(cuts, spans)
|
|
641
|
+
subtitle_path = output_root / "字幕.srt"
|
|
642
|
+
subtitle_path.write_text(_build_srt(cues), encoding="utf-8")
|
|
643
|
+
if renderer == "remotion":
|
|
644
|
+
overlay_path = _render_remotion_overlay(
|
|
645
|
+
output_root, cues, canvas, sum(spans), remotion_workspace,
|
|
646
|
+
concurrency=remotion_concurrency,
|
|
647
|
+
)
|
|
648
|
+
else:
|
|
649
|
+
styled = styled_path = output_root / "字幕.ass"
|
|
650
|
+
styled.write_text(
|
|
651
|
+
_build_ass(cues, canvas["width"] or 1080, canvas["height"] or 1920),
|
|
652
|
+
encoding="utf-8",
|
|
653
|
+
)
|
|
654
|
+
escaped = (
|
|
655
|
+
str(styled).replace("\\", "/").replace(":", r"\:").replace("'", r"\'")
|
|
656
|
+
)
|
|
657
|
+
filters.append(f"ass='{escaped}'")
|
|
658
|
+
|
|
659
|
+
# Grain goes on last, over the whole assembled film, so one texture sits
|
|
660
|
+
# across every cut. `t` makes it move frame to frame — static noise reads
|
|
661
|
+
# as dirt on the lens, not as film.
|
|
662
|
+
grain = (
|
|
663
|
+
f"noise=alls={delivery.grain:g}:allf=t+u"
|
|
664
|
+
if delivery.grain is not None
|
|
665
|
+
else None
|
|
666
|
+
)
|
|
667
|
+
if grain is not None:
|
|
668
|
+
filters.append(grain)
|
|
669
|
+
|
|
670
|
+
final = output_root / "成片.mp4"
|
|
671
|
+
command = [ffmpeg, "-hide_banner", "-loglevel", "error", "-y", "-i", str(joined)]
|
|
672
|
+
if overlay_path is not None:
|
|
673
|
+
# VP8 carries its alpha as WebM block additions, so the decoder has
|
|
674
|
+
# to be named: the default one drops it and the overlay arrives as
|
|
675
|
+
# an opaque black rectangle.
|
|
676
|
+
command += ["-c:v", "libvpx", "-i", str(overlay_path)]
|
|
677
|
+
# The overlay branch owns its own chain, so grain is spliced in
|
|
678
|
+
# after the composite rather than left in `filters`, which this
|
|
679
|
+
# branch never reads.
|
|
680
|
+
chain = "[0:v][1:v]overlay=0:0:format=auto"
|
|
681
|
+
if grain is not None:
|
|
682
|
+
chain += f",{grain}"
|
|
683
|
+
command += [
|
|
684
|
+
"-filter_complex",
|
|
685
|
+
f"{chain},format=yuv420p[v]",
|
|
686
|
+
"-map", "[v]", "-map", "0:a",
|
|
687
|
+
"-c:v", "libx264", "-preset", "medium", "-crf", "18",
|
|
688
|
+
]
|
|
689
|
+
elif filters:
|
|
690
|
+
command += ["-vf", ",".join(filters), "-c:v", "libx264",
|
|
691
|
+
"-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p"]
|
|
692
|
+
else:
|
|
693
|
+
command += ["-c:v", "copy"]
|
|
694
|
+
if delivery.loudness_lufs is not None:
|
|
695
|
+
command += ["-af", _loudnorm_filter(ffmpeg, joined, delivery.loudness_lufs)]
|
|
696
|
+
command += ["-c:a", "aac", "-b:a", "192k", "-ar", "48000"]
|
|
697
|
+
else:
|
|
698
|
+
command += ["-c:a", "copy"]
|
|
699
|
+
command.append(str(final))
|
|
700
|
+
_run(command)
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
"成片": str(final),
|
|
704
|
+
"分段": [str(segment) for segment in segments],
|
|
705
|
+
"字幕": str(subtitle_path) if subtitle_path else None,
|
|
706
|
+
"压制字幕": str(styled_path) if styled_path else None,
|
|
707
|
+
"字幕叠层": str(overlay_path) if overlay_path else None,
|
|
708
|
+
"字幕渲染": renderer if subtitle_path else None,
|
|
709
|
+
"段数": len(segments),
|
|
710
|
+
"各段时长之和": round(sum(cut.end - cut.start for cut in cuts), 2),
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _shot_match_filter(cut: Cut) -> str:
|
|
715
|
+
"""Match one cut to its neighbours, using only what the cut list declared.
|
|
716
|
+
|
|
717
|
+
Generated shots drift: two takes of the same person at the same table come
|
|
718
|
+
back a stop apart and half a step of white balance away from each other, and
|
|
719
|
+
the join reads as a mistake rather than a cut. The correction stays a stated
|
|
720
|
+
creator decision — the tool never measures a clip and adjusts it on its own,
|
|
721
|
+
because a correction nobody wrote down is one nobody can review.
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
stages: list[str] = []
|
|
725
|
+
eq = [
|
|
726
|
+
f"{name}={cut.picture[key]}"
|
|
727
|
+
for key, name in (("brightness", "brightness"), ("saturation", "saturation"))
|
|
728
|
+
if key in cut.picture
|
|
729
|
+
]
|
|
730
|
+
if eq:
|
|
731
|
+
stages.append("eq=" + ":".join(eq))
|
|
732
|
+
warmth = cut.picture.get("warmth")
|
|
733
|
+
if warmth:
|
|
734
|
+
# Positive is warmer: lift red, drop blue, by the same small amount.
|
|
735
|
+
amount = warmth / 100.0
|
|
736
|
+
stages.append(f"colorbalance=rm={amount:.4f}:bm={-amount:.4f}")
|
|
737
|
+
return ",".join(stages)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _loudnorm_filter(ffmpeg: str, media: Path, target: float) -> str:
|
|
741
|
+
"""Measure EBU R128 before normalization; verify the encoded output afterward.
|
|
742
|
+
|
|
743
|
+
FFmpeg can fall back to dynamic processing when linear gain would exceed
|
|
744
|
+
the peak or loudness-range target. Two passes do not guarantee target LUFS.
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
common = f"I={target}:TP=-1.5:LRA=11"
|
|
748
|
+
result = subprocess.run(
|
|
749
|
+
[ffmpeg, "-hide_banner", "-nostats", "-i", str(media),
|
|
750
|
+
"-af", f"loudnorm={common}:print_format=json", "-f", "null", "-"],
|
|
751
|
+
capture_output=True, text=True, check=False,
|
|
752
|
+
)
|
|
753
|
+
measured = _last_json_object(result.stderr)
|
|
754
|
+
required = ("input_i", "input_tp", "input_lra", "input_thresh", "target_offset")
|
|
755
|
+
if not measured or not all(key in measured for key in required):
|
|
756
|
+
# Say so rather than silently delivering a single-pass approximation.
|
|
757
|
+
raise EditError(
|
|
758
|
+
"loudnorm 第一遍没有返回可用的测量结果,无法做两遍响度标准化;"
|
|
759
|
+
"检查成片音轨是否为空"
|
|
760
|
+
)
|
|
761
|
+
return (
|
|
762
|
+
f"loudnorm={common}:measured_I={measured['input_i']}"
|
|
763
|
+
f":measured_TP={measured['input_tp']}:measured_LRA={measured['input_lra']}"
|
|
764
|
+
f":measured_thresh={measured['input_thresh']}"
|
|
765
|
+
f":offset={measured['target_offset']}:linear=true:print_format=summary"
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _sync_remotion_workspace(workspace: Path) -> Path:
|
|
770
|
+
"""Copy the shipped composition into a runnable workspace outside the project.
|
|
771
|
+
|
|
772
|
+
Editing the composition means editing the skill's own source; the workspace
|
|
773
|
+
is a build directory that is rewritten on every run, so a change here can
|
|
774
|
+
never be quietly lost, and `node_modules` never lands inside the repository.
|
|
775
|
+
"""
|
|
776
|
+
|
|
777
|
+
workspace = workspace.expanduser().resolve()
|
|
778
|
+
(workspace / "src").mkdir(parents=True, exist_ok=True)
|
|
779
|
+
for name in REMOTION_SOURCE_FILES:
|
|
780
|
+
source = REMOTION_SOURCE / name
|
|
781
|
+
if not source.is_file():
|
|
782
|
+
raise EditError(f"技能里缺少 Remotion 源文件: {name}")
|
|
783
|
+
shutil.copyfile(source, workspace / name)
|
|
784
|
+
return workspace
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _render_remotion_overlay(
|
|
788
|
+
output_root: Path,
|
|
789
|
+
cues: Sequence[tuple[float, float, str]],
|
|
790
|
+
canvas: dict[str, Any],
|
|
791
|
+
duration: float,
|
|
792
|
+
workspace_root: Path,
|
|
793
|
+
concurrency: int = DEFAULT_REMOTION_CONCURRENCY,
|
|
794
|
+
) -> Path:
|
|
795
|
+
"""Render the subtitle layer as a transparent video with Remotion.
|
|
796
|
+
|
|
797
|
+
The picture is never re-drawn by the browser: only the type is, onto an
|
|
798
|
+
empty frame, and ffmpeg composites that over untouched footage. So the
|
|
799
|
+
route costs one overlay pass and buys real typography — weight, rim, safe
|
|
800
|
+
area, wrapping and an entry animation — expressed once in CSS instead of in
|
|
801
|
+
a subtitle format whose own scaling has to be reasoned about.
|
|
802
|
+
|
|
803
|
+
Remotion is a separate project with its own licence: free for individuals
|
|
804
|
+
and small companies, paid above that. It is opt-in for exactly that reason,
|
|
805
|
+
and nothing installs it behind the creator's back.
|
|
806
|
+
"""
|
|
807
|
+
|
|
808
|
+
workspace = _sync_remotion_workspace(workspace_root)
|
|
809
|
+
if not (workspace / "node_modules").is_dir():
|
|
810
|
+
raise EditError(
|
|
811
|
+
f"Remotion 还没安装。先运行一次:\n"
|
|
812
|
+
f" cd {workspace} && npm install\n"
|
|
813
|
+
"或改用默认的 ffmpeg 字幕(去掉 --subtitles remotion)。\n"
|
|
814
|
+
"注意 Remotion 有自己的许可证:个人与小团队免费,超出规模需要商业授权;"
|
|
815
|
+
"本工具不会替你安装它。"
|
|
816
|
+
)
|
|
817
|
+
npx = _which("npx")
|
|
818
|
+
if npx is None:
|
|
819
|
+
raise EditError(
|
|
820
|
+
"PATH 上没有 npx,无法运行 Remotion;装好 Node.js,或改用默认的 ffmpeg 字幕。"
|
|
821
|
+
)
|
|
822
|
+
props = output_root / "字幕.props.json"
|
|
823
|
+
props.write_text(
|
|
824
|
+
json.dumps(
|
|
825
|
+
{
|
|
826
|
+
"cues": [
|
|
827
|
+
{"start": round(start, 3), "end": round(end, 3), "text": text}
|
|
828
|
+
for start, end, text in cues
|
|
829
|
+
],
|
|
830
|
+
"width": canvas["width"] or 1080,
|
|
831
|
+
"height": canvas["height"] or 1920,
|
|
832
|
+
"fps": canvas["fps"] or 24,
|
|
833
|
+
"durationInSeconds": round(duration, 3),
|
|
834
|
+
"fontScale": 0.034,
|
|
835
|
+
"bottomScale": 0.055,
|
|
836
|
+
"fontFamily": (
|
|
837
|
+
'"PingFang SC", "Noto Sans CJK SC", "Source Han Sans SC", '
|
|
838
|
+
'"Microsoft YaHei", sans-serif'
|
|
839
|
+
),
|
|
840
|
+
},
|
|
841
|
+
ensure_ascii=False,
|
|
842
|
+
indent=2,
|
|
843
|
+
),
|
|
844
|
+
encoding="utf-8",
|
|
845
|
+
)
|
|
846
|
+
overlay = output_root / "字幕叠层.webm"
|
|
847
|
+
result = subprocess.run(
|
|
848
|
+
[npx, "remotion", "render", REMOTION_COMPOSITION, str(overlay),
|
|
849
|
+
f"--props={props}", "--log=error",
|
|
850
|
+
f"--concurrency={max(1, concurrency)}"],
|
|
851
|
+
cwd=str(workspace), capture_output=True, text=True, check=False,
|
|
852
|
+
)
|
|
853
|
+
if result.returncode != 0 or not overlay.is_file():
|
|
854
|
+
raise EditError(
|
|
855
|
+
"Remotion 渲染失败:\n" + (result.stderr or result.stdout or "").strip()[-2000:]
|
|
856
|
+
)
|
|
857
|
+
return overlay
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _frame_geometry(media: Path) -> str:
|
|
861
|
+
stream = probe_stream(media)
|
|
862
|
+
return f"{stream['width']}x{stream['height']}"
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _subtitle_cues(
|
|
866
|
+
cuts: Sequence[Cut], spans: Sequence[float]
|
|
867
|
+
) -> list[tuple[float, float, str]]:
|
|
868
|
+
"""Place each line in output time, measuring the segments that were written.
|
|
869
|
+
|
|
870
|
+
A segment lands on a frame boundary, so it is a few milliseconds longer than
|
|
871
|
+
the cut list declares. Accumulating the declared numbers instead drifts —
|
|
872
|
+
a third of a second by the end of eight cuts here — and the subtitle leaves
|
|
873
|
+
before the actor stops speaking. The rendered files are the timeline.
|
|
874
|
+
"""
|
|
875
|
+
|
|
876
|
+
cues: list[tuple[float, float, str]] = []
|
|
877
|
+
cursor = 0.0
|
|
878
|
+
for cut, span in zip(cuts, spans):
|
|
879
|
+
declared = cut.end - cut.start
|
|
880
|
+
# The windows were authored against the declared span; hold them in
|
|
881
|
+
# place proportionally rather than letting the tail slip out.
|
|
882
|
+
scale = span / declared if declared > 0 else 1.0
|
|
883
|
+
for window_start, window_end, text in cut.subtitles:
|
|
884
|
+
start = 0.0 if window_start is None else window_start
|
|
885
|
+
end = declared if window_end is None else window_end
|
|
886
|
+
cues.append((cursor + start * scale, cursor + end * scale, text))
|
|
887
|
+
cursor += span
|
|
888
|
+
return cues
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _build_srt(cues: Sequence[tuple[float, float, str]]) -> str:
|
|
892
|
+
"""The text is the screenplay's, verbatim; only the timing is ours."""
|
|
893
|
+
|
|
894
|
+
return "\n".join(
|
|
895
|
+
f"{index}\n{_timecode(start)} --> {_timecode(end)}\n{text}\n"
|
|
896
|
+
for index, (start, end, text) in enumerate(cues, start=1)
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _build_ass(
|
|
901
|
+
cues: Sequence[tuple[float, float, str]], width: int, height: int
|
|
902
|
+
) -> str:
|
|
903
|
+
"""Author the ASS directly so the type size is stated in frame pixels.
|
|
904
|
+
|
|
905
|
+
Letting ffmpeg convert the SRT hands libass a 384-high canvas, and every
|
|
906
|
+
size in the style is then scaled by height/384 on the way to the frame.
|
|
907
|
+
Computing a FontSize from the real height on top of that scales it twice:
|
|
908
|
+
the first attempt here produced type a third of the frame wide, sitting in
|
|
909
|
+
the middle of the picture with both ends of the line cut off. Declaring
|
|
910
|
+
PlayRes as the frame removes the conversion, so one unit is one pixel.
|
|
911
|
+
"""
|
|
912
|
+
|
|
913
|
+
font_size = max(18, round(height * 0.034))
|
|
914
|
+
outline = max(2, round(height * 0.0022))
|
|
915
|
+
margin_v = max(24, round(height * 0.055))
|
|
916
|
+
margin_h = max(24, round(width * 0.06))
|
|
917
|
+
header = (
|
|
918
|
+
"[Script Info]\n"
|
|
919
|
+
"ScriptType: v4.00+\n"
|
|
920
|
+
"WrapStyle: 2\n"
|
|
921
|
+
"ScaledBorderAndShadow: yes\n"
|
|
922
|
+
f"PlayResX: {width}\n"
|
|
923
|
+
f"PlayResY: {height}\n"
|
|
924
|
+
"\n[V4+ Styles]\n"
|
|
925
|
+
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, "
|
|
926
|
+
"OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, "
|
|
927
|
+
"ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, "
|
|
928
|
+
"MarginL, MarginR, MarginV, Encoding\n"
|
|
929
|
+
f"Style: 正片,{SUBTITLE_FONT},{font_size},&H00FFFFFF,&H00FFFFFF,"
|
|
930
|
+
f"&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,{outline},1,2,"
|
|
931
|
+
f"{margin_h},{margin_h},{margin_v},1\n"
|
|
932
|
+
"\n[Events]\n"
|
|
933
|
+
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, "
|
|
934
|
+
"Effect, Text\n"
|
|
935
|
+
)
|
|
936
|
+
lines = [
|
|
937
|
+
f"Dialogue: 0,{_ass_time(start)},{_ass_time(end)},正片,,0,0,0,,{_ass_text(text)}"
|
|
938
|
+
for start, end, text in cues
|
|
939
|
+
]
|
|
940
|
+
return header + "\n".join(lines) + "\n"
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _ass_text(text: str) -> str:
|
|
944
|
+
"""Escape a line so ASS renders its characters instead of reading them.
|
|
945
|
+
|
|
946
|
+
`{`…`}` is an override block in ASS and `\\` starts an escape, so a quoted
|
|
947
|
+
line that happens to contain either would lose characters on screen with no
|
|
948
|
+
error anywhere — and EDT-05 exists precisely to keep the burned text equal
|
|
949
|
+
to the screenplay's, character for character.
|
|
950
|
+
"""
|
|
951
|
+
|
|
952
|
+
return text.replace("\\", "\\\\").replace("{", "\\{").replace("}", "\\}")
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _ass_time(seconds: float) -> str:
|
|
956
|
+
hundredths = int(round(seconds * 100))
|
|
957
|
+
hours, hundredths = divmod(hundredths, 360000)
|
|
958
|
+
minutes, hundredths = divmod(hundredths, 6000)
|
|
959
|
+
whole, hundredths = divmod(hundredths, 100)
|
|
960
|
+
return f"{hours:d}:{minutes:02d}:{whole:02d}.{hundredths:02d}"
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _timecode(seconds: float) -> str:
|
|
964
|
+
milliseconds = int(round(seconds * 1000))
|
|
965
|
+
hours, milliseconds = divmod(milliseconds, 3_600_000)
|
|
966
|
+
minutes, milliseconds = divmod(milliseconds, 60_000)
|
|
967
|
+
whole, milliseconds = divmod(milliseconds, 1000)
|
|
968
|
+
return f"{hours:02d}:{minutes:02d}:{whole:02d},{milliseconds:03d}"
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _segment_colour(ffmpeg: str, path: Path) -> Optional[tuple[float, float]]:
|
|
972
|
+
"""Mean luma and blue-red difference of one segment, on a 0-255 scale."""
|
|
973
|
+
|
|
974
|
+
result = subprocess.run(
|
|
975
|
+
[ffmpeg, "-v", "error", "-i", str(path), "-vf", "fps=2,scale=96:-1",
|
|
976
|
+
"-pix_fmt", "rgb24", "-f", "rawvideo", "-"],
|
|
977
|
+
capture_output=True,
|
|
978
|
+
)
|
|
979
|
+
raw = result.stdout
|
|
980
|
+
if result.returncode != 0 or len(raw) < 3:
|
|
981
|
+
return None
|
|
982
|
+
count = len(raw) // 3
|
|
983
|
+
red = sum(raw[i * 3] for i in range(count)) / count
|
|
984
|
+
green = sum(raw[i * 3 + 1] for i in range(count)) / count
|
|
985
|
+
blue = sum(raw[i * 3 + 2] for i in range(count)) / count
|
|
986
|
+
return 0.299 * red + 0.587 * green + 0.114 * blue, blue - red
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _segment_colours(ffmpeg: str, output_root: Path, cuts: Sequence[Cut]) -> list[dict[str, Any]]:
|
|
990
|
+
"""Report current cut segments individually, without imposing a shared grade."""
|
|
991
|
+
|
|
992
|
+
rows: list[dict[str, Any]] = []
|
|
993
|
+
for cut in cuts:
|
|
994
|
+
path = output_root / SEGMENT_DIRECTORY / f"{cut.cut_id}.mp4"
|
|
995
|
+
colour = _segment_colour(ffmpeg, path) if path.is_file() else None
|
|
996
|
+
row: dict[str, Any] = {"分段": path.name}
|
|
997
|
+
if colour is None:
|
|
998
|
+
row["测量"] = "未测(分段缺失或不可读)"
|
|
999
|
+
else:
|
|
1000
|
+
row.update({"平均亮度": round(colour[0], 1), "蓝减红": round(colour[1], 1)})
|
|
1001
|
+
rows.append(row)
|
|
1002
|
+
return rows
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def verify(episode: Path, cuts: Sequence[Cut], delivery: Delivery) -> dict[str, Any]:
|
|
1006
|
+
"""Measure the rendered film. Every entry is a number or an honest 未测."""
|
|
1007
|
+
|
|
1008
|
+
ffmpeg = _require("ffmpeg")
|
|
1009
|
+
final = episode / OUTPUT_DIRECTORY / "成片.mp4"
|
|
1010
|
+
if not final.is_file():
|
|
1011
|
+
raise EditError(f"没有 {final};先运行 render")
|
|
1012
|
+
stream = probe_stream(final)
|
|
1013
|
+
expected = sum(cut.end - cut.start for cut in cuts)
|
|
1014
|
+
measurements: dict[str, Any] = {
|
|
1015
|
+
"成片": str(final),
|
|
1016
|
+
"实测时长": round(stream["duration"], 2),
|
|
1017
|
+
"各段时长之和": round(expected, 2),
|
|
1018
|
+
"时长差": round(stream["duration"] - expected, 2),
|
|
1019
|
+
"画幅": f"{stream['width']}×{stream['height']}",
|
|
1020
|
+
"画幅是否等于交付规格": _matches_frame_size(delivery, stream),
|
|
1021
|
+
"帧率": stream["fps"],
|
|
1022
|
+
"帧率是否等于交付规格": _matches_fps(delivery, stream),
|
|
1023
|
+
"目标时长": delivery.target_seconds,
|
|
1024
|
+
"与目标时长的差": (
|
|
1025
|
+
round(stream["duration"] - delivery.target_seconds, 2)
|
|
1026
|
+
if delivery.target_seconds is not None
|
|
1027
|
+
else "未测(剪辑单没有声明目标时长)"
|
|
1028
|
+
),
|
|
1029
|
+
}
|
|
1030
|
+
result = subprocess.run(
|
|
1031
|
+
[ffmpeg, "-hide_banner", "-nostats", "-i", str(final),
|
|
1032
|
+
"-af", "loudnorm=print_format=json", "-f", "null", "-"],
|
|
1033
|
+
capture_output=True, text=True, check=False,
|
|
1034
|
+
)
|
|
1035
|
+
measured = _last_json_object(result.stderr)
|
|
1036
|
+
if measured and "input_i" in measured:
|
|
1037
|
+
measurements["实测响度 LUFS"] = float(measured["input_i"])
|
|
1038
|
+
measurements["实测真峰 dBTP"] = float(measured["input_tp"])
|
|
1039
|
+
else:
|
|
1040
|
+
measurements["实测响度 LUFS"] = "未测(loudnorm 没有返回可解析的测量结果)"
|
|
1041
|
+
measurements["交付响度目标"] = delivery.loudness_lufs
|
|
1042
|
+
measurements["分段色彩观测"] = _segment_colours(ffmpeg, episode / OUTPUT_DIRECTORY, cuts)
|
|
1043
|
+
measurements["画内可读文字"] = (
|
|
1044
|
+
"未测(抽有画内文字的帧,逐字对《剧本.md》的「画面文字」与提示词声明的内容)"
|
|
1045
|
+
)
|
|
1046
|
+
measurements["台词完整性"] = "未测(本工具不做转写;在成片上转写后逐句对《剧本.md》原文)"
|
|
1047
|
+
measurements["边界帧"] = "未测(抽剪辑点前后各一帧目视核对黑场/白场/半渲染帧)"
|
|
1048
|
+
return measurements
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def _matches_frame_size(delivery: Delivery, stream: dict[str, Any]) -> Any:
|
|
1052
|
+
if delivery.frame_size is None:
|
|
1053
|
+
return "未测(剪辑单没有声明画幅)"
|
|
1054
|
+
return (stream["width"], stream["height"]) == delivery.frame_size
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _matches_fps(delivery: Delivery, stream: dict[str, Any]) -> Any:
|
|
1058
|
+
if delivery.fps is None:
|
|
1059
|
+
return "未测(剪辑单没有声明帧率)"
|
|
1060
|
+
return abs(stream["fps"] - delivery.fps) < 0.05
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _last_json_object(text: str) -> Optional[dict[str, Any]]:
|
|
1064
|
+
"""ffmpeg prints its own tail after the loudnorm block, so decode a prefix."""
|
|
1065
|
+
|
|
1066
|
+
decoder = json.JSONDecoder()
|
|
1067
|
+
start = text.rfind("{")
|
|
1068
|
+
while start != -1:
|
|
1069
|
+
try:
|
|
1070
|
+
document, _ = decoder.raw_decode(text[start:].lstrip())
|
|
1071
|
+
except json.JSONDecodeError:
|
|
1072
|
+
start = text.rfind("{", 0, start)
|
|
1073
|
+
continue
|
|
1074
|
+
if isinstance(document, dict):
|
|
1075
|
+
return document
|
|
1076
|
+
start = text.rfind("{", 0, start)
|
|
1077
|
+
return None
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _run(command: Sequence[str]) -> None:
|
|
1081
|
+
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
|
1082
|
+
if result.returncode != 0:
|
|
1083
|
+
raise EditError(f"命令失败: {' '.join(command[:6])}…\n{result.stderr.strip()}")
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _emit(payload: dict[str, Any]) -> None:
|
|
1087
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=False))
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
1091
|
+
parser = argparse.ArgumentParser(description="短剧剪辑:核对剪辑单、渲染成片、测量成片")
|
|
1092
|
+
parser.add_argument("command", choices=("check", "render", "verify"))
|
|
1093
|
+
parser.add_argument("episode", help="剧集/<EP> 目录")
|
|
1094
|
+
parser.add_argument("--project-root", default=".", help="项目根目录(解析素材相对路径)")
|
|
1095
|
+
parser.add_argument("--no-subtitles", action="store_true", help="render 时不烧字幕")
|
|
1096
|
+
parser.add_argument(
|
|
1097
|
+
"--subtitles", choices=SUBTITLE_RENDERERS, default="ffmpeg",
|
|
1098
|
+
help="字幕渲染方式:ffmpeg(默认,无外部依赖)或 remotion(需先安装,排版更好)",
|
|
1099
|
+
)
|
|
1100
|
+
parser.add_argument(
|
|
1101
|
+
"--remotion-workspace", type=Path, default=DEFAULT_REMOTION_WORKSPACE,
|
|
1102
|
+
help=f"Remotion 运行工作区(默认 {DEFAULT_REMOTION_WORKSPACE}),必须在项目之外",
|
|
1103
|
+
)
|
|
1104
|
+
parser.add_argument(
|
|
1105
|
+
"--remotion-concurrency", type=int, default=DEFAULT_REMOTION_CONCURRENCY,
|
|
1106
|
+
help=(
|
|
1107
|
+
f"Remotion 的并发无头浏览器数(默认 {DEFAULT_REMOTION_CONCURRENCY})。"
|
|
1108
|
+
"每个都持有一整帧,调高很容易把内存吃满"
|
|
1109
|
+
),
|
|
1110
|
+
)
|
|
1111
|
+
arguments = parser.parse_args(argv)
|
|
1112
|
+
|
|
1113
|
+
episode = Path(arguments.episode).resolve()
|
|
1114
|
+
project_root = Path(arguments.project_root).resolve()
|
|
1115
|
+
try:
|
|
1116
|
+
delivery, cuts, unused = parse_cut_list(episode / CUT_LIST_NAME)
|
|
1117
|
+
if arguments.command == "check":
|
|
1118
|
+
findings = check_cuts(
|
|
1119
|
+
episode, cuts, project_root,
|
|
1120
|
+
probe=_which("ffprobe") is not None, unused=unused,
|
|
1121
|
+
)
|
|
1122
|
+
payload: dict[str, Any] = {
|
|
1123
|
+
"段数": len(cuts),
|
|
1124
|
+
"各段时长之和": round(sum(cut.end - cut.start for cut in cuts), 2),
|
|
1125
|
+
"目标时长": delivery.target_seconds,
|
|
1126
|
+
"未采用镜头": unused,
|
|
1127
|
+
"findings": findings,
|
|
1128
|
+
}
|
|
1129
|
+
if _which("ffprobe") is None:
|
|
1130
|
+
payload["未测"] = ["区间是否超过素材实际时长(PATH 上没有 ffprobe)"]
|
|
1131
|
+
_emit(payload)
|
|
1132
|
+
return 1 if findings else 0
|
|
1133
|
+
if arguments.command == "render":
|
|
1134
|
+
findings = check_cuts(
|
|
1135
|
+
episode, cuts, project_root, probe=True, unused=unused
|
|
1136
|
+
)
|
|
1137
|
+
if findings:
|
|
1138
|
+
_emit({"findings": findings, "已渲染": False})
|
|
1139
|
+
return 1
|
|
1140
|
+
_emit(render(
|
|
1141
|
+
episode, project_root, cuts, delivery,
|
|
1142
|
+
burn_subtitles=delivery.burn_subtitles and not arguments.no_subtitles,
|
|
1143
|
+
renderer=arguments.subtitles,
|
|
1144
|
+
remotion_workspace=arguments.remotion_workspace,
|
|
1145
|
+
remotion_concurrency=arguments.remotion_concurrency,
|
|
1146
|
+
))
|
|
1147
|
+
return 0
|
|
1148
|
+
_emit(verify(episode, cuts, delivery))
|
|
1149
|
+
return 0
|
|
1150
|
+
except EditError as error:
|
|
1151
|
+
print(str(error), file=sys.stderr)
|
|
1152
|
+
return 2
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
if __name__ == "__main__":
|
|
1156
|
+
raise SystemExit(main())
|