@lemoncat7/dsh-partner 2.5.0 → 2.7.1
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.1.md +10 -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 +294 -157
- 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 +983 -575
- 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/renderer.d.ts.map +1 -1
- package/lib/pendant/renderer.js +18 -2
- package/lib/pendant/renderer.js.map +1 -1
- package/lib/pendant/settings-panel.js +1 -1
- package/lib/pendant/settings-panel.js.map +1 -1
- package/lib/pendant/startup.d.ts +4 -0
- package/lib/pendant/startup.d.ts.map +1 -0
- package/lib/pendant/startup.js +64 -0
- package/lib/pendant/startup.js.map +1 -0
- package/lib/pendant/widget.d.ts.map +1 -1
- package/lib/pendant/widget.js +2 -1
- package/lib/pendant/widget.js.map +1 -1
- package/lib/pendant-renderer.js +169 -169
- 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
|
@@ -0,0 +1,1747 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stdlib-only production adapters for supported media providers.
|
|
3
|
+
|
|
4
|
+
The public ``compile_*`` functions are deterministic and perform no I/O. The
|
|
5
|
+
CLI reads the confirmed production job from stdin and writes only the adapter
|
|
6
|
+
contract JSON to stdout after a provider result has been saved to a temporary
|
|
7
|
+
regular file.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import base64
|
|
14
|
+
import binascii
|
|
15
|
+
import json
|
|
16
|
+
import mimetypes
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import secrets
|
|
20
|
+
import stat
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.parse
|
|
25
|
+
import urllib.request
|
|
26
|
+
from collections.abc import Collection, Mapping, Sequence
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
OPENAI_MODEL = "gpt-image-2"
|
|
31
|
+
MINIMAX_MUSIC_MODEL = "music-3.0"
|
|
32
|
+
OPENAI_BASE_URL = "https://api.openai.com/v1"
|
|
33
|
+
SEEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
|
|
34
|
+
MINIMAX_BASE_URL = "https://api.minimax.io/v1"
|
|
35
|
+
MINIMAX_VIDEO_BASE_URL = "https://api.minimax.io/v2"
|
|
36
|
+
MAX_JSON_RESPONSE = 128 * 1024 * 1024
|
|
37
|
+
MAX_OUTPUT_BYTES = 512 * 1024 * 1024
|
|
38
|
+
MAX_REFERENCE_BYTES = 50 * 1024 * 1024
|
|
39
|
+
MAX_MULTIPART_BYTES = 200 * 1024 * 1024
|
|
40
|
+
MAX_ERROR_BODY_BYTES = 64 * 1024
|
|
41
|
+
TERMINAL_FAILURES = {"failed", "cancelled", "canceled", "timeout", "expired"}
|
|
42
|
+
SEEDANCE_RATIOS = {"adaptive", "1:1", "3:4", "4:3", "9:16", "16:9", "21:9"}
|
|
43
|
+
SEEDANCE_REFERENCE_ROLES = {
|
|
44
|
+
"first_frame": "image_url",
|
|
45
|
+
"last_frame": "image_url",
|
|
46
|
+
"reference_image": "image_url",
|
|
47
|
+
"reference_video": "video_url",
|
|
48
|
+
"reference_audio": "audio_url",
|
|
49
|
+
}
|
|
50
|
+
# One opening frame and one closing frame, because each names a single position
|
|
51
|
+
# in the take. Note what is deliberately absent: the sibling adapter forbids
|
|
52
|
+
# mixing frame conditioning with reference conditioning, and that rule belongs
|
|
53
|
+
# to that provider's reference, not to this one. Do not carry it across.
|
|
54
|
+
SEEDANCE_SINGULAR_ROLES = {"first_frame", "last_frame"}
|
|
55
|
+
MINIMAX_VIDEO_RATIOS = {"adaptive", "1:1", "3:4", "4:3", "9:16", "16:9", "21:9"}
|
|
56
|
+
MINIMAX_VIDEO_RESOLUTIONS = {"480P", "768P", "2K"}
|
|
57
|
+
MINIMAX_VIDEO_PROMPT_LIMIT = 7000
|
|
58
|
+
MINIMAX_SPEECH_TEXT_LIMIT = 5000
|
|
59
|
+
MINIMAX_VIDEO_ROLES = {
|
|
60
|
+
"first_frame": "image_url",
|
|
61
|
+
"last_frame": "image_url",
|
|
62
|
+
"reference_image": "image_url",
|
|
63
|
+
"reference_video": "video_url",
|
|
64
|
+
"reference_audio": "audio_url",
|
|
65
|
+
}
|
|
66
|
+
# Both providers document a `data:<mime>;base64,<...>` URI as an accepted media
|
|
67
|
+
# input alongside a public URL, so a local project reference needs no upload
|
|
68
|
+
# service to reach them. The caps are MiniMax's published per-modality limits;
|
|
69
|
+
# Seedance publishes no numbers, so the same conservative guard is applied there
|
|
70
|
+
# and can be raised per deployment. Base64 inflates bytes by about a third, so
|
|
71
|
+
# the request cap is checked against the encoded size.
|
|
72
|
+
INLINE_REFERENCE_LIMITS = {
|
|
73
|
+
"image_url": 30 * 1024 * 1024,
|
|
74
|
+
"video_url": 50 * 1024 * 1024,
|
|
75
|
+
"audio_url": 15 * 1024 * 1024,
|
|
76
|
+
}
|
|
77
|
+
INLINE_REFERENCE_BODY_LIMIT = 64 * 1024 * 1024
|
|
78
|
+
INLINE_REFERENCE_MIME = {
|
|
79
|
+
".png": "image/png",
|
|
80
|
+
".jpg": "image/jpeg",
|
|
81
|
+
".jpeg": "image/jpeg",
|
|
82
|
+
".webp": "image/webp",
|
|
83
|
+
".mp4": "video/mp4",
|
|
84
|
+
".mov": "video/quicktime",
|
|
85
|
+
".webm": "video/webm",
|
|
86
|
+
".wav": "audio/wav",
|
|
87
|
+
".mp3": "audio/mpeg",
|
|
88
|
+
".m4a": "audio/mp4",
|
|
89
|
+
".aac": "audio/aac",
|
|
90
|
+
".flac": "audio/flac",
|
|
91
|
+
}
|
|
92
|
+
GPT_IMAGE_MIN_PIXELS = 655_360
|
|
93
|
+
GPT_IMAGE_MAX_PIXELS = 8_294_400
|
|
94
|
+
MINIMUM_PYTHON = (3, 9)
|
|
95
|
+
if sys.version_info < MINIMUM_PYTHON:
|
|
96
|
+
raise SystemExit("provider_adapters.py requires Python 3.9 or newer")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AdapterFailure(RuntimeError):
|
|
100
|
+
"""A safe-to-report adapter failure without provider response contents."""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
message: str,
|
|
105
|
+
*,
|
|
106
|
+
category: str = "provider_response",
|
|
107
|
+
code: str = "adapter_failure",
|
|
108
|
+
http_status: int | None = None,
|
|
109
|
+
request_id: str | None = None,
|
|
110
|
+
retryable: bool = False,
|
|
111
|
+
) -> None:
|
|
112
|
+
super().__init__(message)
|
|
113
|
+
self.category = category
|
|
114
|
+
self.code = _safe_token(code) or "adapter_failure"
|
|
115
|
+
self.http_status = http_status
|
|
116
|
+
self.request_id = _safe_token(request_id)
|
|
117
|
+
self.retryable = retryable
|
|
118
|
+
|
|
119
|
+
def public(self, provider: str) -> dict[str, Any]:
|
|
120
|
+
result: dict[str, Any] = {
|
|
121
|
+
"provider": provider,
|
|
122
|
+
"category": self.category,
|
|
123
|
+
"code": self.code,
|
|
124
|
+
"retryable": self.retryable,
|
|
125
|
+
}
|
|
126
|
+
if self.http_status is not None:
|
|
127
|
+
result["http_status"] = self.http_status
|
|
128
|
+
if self.request_id is not None:
|
|
129
|
+
result["request_id"] = self.request_id
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _safe_token(value: object) -> str | None:
|
|
134
|
+
if isinstance(value, str) and re.fullmatch(
|
|
135
|
+
r"[A-Za-z0-9][A-Za-z0-9._:-]{0,199}", value
|
|
136
|
+
):
|
|
137
|
+
return value
|
|
138
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
139
|
+
return str(value)
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _request_id(headers: Mapping[str, Any]) -> str | None:
|
|
144
|
+
folded = {str(key).casefold(): value for key, value in headers.items()}
|
|
145
|
+
for name in ("x-request-id", "x-tt-logid", "x-trace-id", "trace-id"):
|
|
146
|
+
value = _safe_token(folded.get(name))
|
|
147
|
+
if value is not None:
|
|
148
|
+
return value
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _provider_code(document: object) -> str | None:
|
|
153
|
+
if not isinstance(document, Mapping):
|
|
154
|
+
return None
|
|
155
|
+
error = document.get("error")
|
|
156
|
+
if isinstance(error, Mapping):
|
|
157
|
+
value = _safe_token(error.get("code"))
|
|
158
|
+
if value is not None:
|
|
159
|
+
return value
|
|
160
|
+
base_resp = document.get("base_resp")
|
|
161
|
+
if isinstance(base_resp, Mapping):
|
|
162
|
+
value = _safe_token(base_resp.get("status_code"))
|
|
163
|
+
if value not in {None, "0"}:
|
|
164
|
+
return value
|
|
165
|
+
return _safe_token(document.get("code"))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _http_failure(provider: str, error: urllib.error.HTTPError) -> AdapterFailure:
|
|
169
|
+
status = error.code
|
|
170
|
+
try:
|
|
171
|
+
raw = error.read(MAX_ERROR_BODY_BYTES + 1)
|
|
172
|
+
document = json.loads(raw) if len(raw) <= MAX_ERROR_BODY_BYTES else None
|
|
173
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
174
|
+
document = None
|
|
175
|
+
if status == 401:
|
|
176
|
+
category = "authentication"
|
|
177
|
+
elif status == 403:
|
|
178
|
+
category = "permission"
|
|
179
|
+
elif status == 429:
|
|
180
|
+
category = "rate_limit"
|
|
181
|
+
elif 500 <= status <= 599:
|
|
182
|
+
category = "server"
|
|
183
|
+
elif 400 <= status <= 499:
|
|
184
|
+
category = "invalid_request"
|
|
185
|
+
else:
|
|
186
|
+
category = "provider_response"
|
|
187
|
+
return AdapterFailure(
|
|
188
|
+
f"{provider} HTTP request failed",
|
|
189
|
+
category=category,
|
|
190
|
+
code=_provider_code(document) or f"http_{status}",
|
|
191
|
+
http_status=status,
|
|
192
|
+
request_id=_request_id(
|
|
193
|
+
dict(error.headers.items()) if error.headers is not None else {}
|
|
194
|
+
),
|
|
195
|
+
retryable=status == 429 or 500 <= status <= 599,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _require_job(job: Mapping[str, Any], modality: str) -> tuple[str, dict[str, Any]]:
|
|
200
|
+
if not isinstance(job, Mapping) or job.get("modality") != modality:
|
|
201
|
+
raise ValueError(f"adapter requires a {modality} job")
|
|
202
|
+
prompt = job.get("prompt")
|
|
203
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
204
|
+
raise ValueError("job prompt must be non-empty")
|
|
205
|
+
parameters = job.get("parameters", {})
|
|
206
|
+
if not isinstance(parameters, Mapping):
|
|
207
|
+
raise ValueError("job parameters must be an object")
|
|
208
|
+
outputs = job.get("outputs")
|
|
209
|
+
if not isinstance(outputs, list) or len(outputs) != 1 or not isinstance(outputs[0], str):
|
|
210
|
+
raise ValueError("provider adapter requires exactly one output")
|
|
211
|
+
return prompt, dict(parameters)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _prompt_with_reference_contract(
|
|
215
|
+
prompt: str,
|
|
216
|
+
job: Mapping[str, Any],
|
|
217
|
+
*,
|
|
218
|
+
prompt_language: str | None = None,
|
|
219
|
+
reference_tokens: Sequence[str] = (),
|
|
220
|
+
zh_reference_prefix: str = "参考",
|
|
221
|
+
zh_heading: str = "参考约束:",
|
|
222
|
+
) -> str:
|
|
223
|
+
bindings = job.get("reference_bindings", [])
|
|
224
|
+
references = job.get("references", [])
|
|
225
|
+
if not bindings:
|
|
226
|
+
return prompt
|
|
227
|
+
if (
|
|
228
|
+
not isinstance(bindings, list)
|
|
229
|
+
or not isinstance(references, list)
|
|
230
|
+
or len(bindings) != len(references)
|
|
231
|
+
):
|
|
232
|
+
raise ValueError("reference bindings must match job references")
|
|
233
|
+
if reference_tokens and len(reference_tokens) != len(references):
|
|
234
|
+
raise ValueError("reference tokens must match job references")
|
|
235
|
+
language = (prompt_language or "en").casefold()
|
|
236
|
+
instructions: list[str] = []
|
|
237
|
+
for index, binding in enumerate(bindings, 1):
|
|
238
|
+
if not isinstance(binding, Mapping) or binding.get("order") != index:
|
|
239
|
+
raise ValueError("reference binding order is invalid")
|
|
240
|
+
if binding.get("path") != references[index - 1]:
|
|
241
|
+
raise ValueError("reference binding path does not match job references")
|
|
242
|
+
label = binding.get("label")
|
|
243
|
+
role = binding.get("role")
|
|
244
|
+
may_control = binding.get("may_control")
|
|
245
|
+
must_not_control = binding.get("must_not_control")
|
|
246
|
+
if (
|
|
247
|
+
not isinstance(label, str)
|
|
248
|
+
or not label.strip()
|
|
249
|
+
or not isinstance(role, str)
|
|
250
|
+
or not role.strip()
|
|
251
|
+
or not isinstance(may_control, list)
|
|
252
|
+
or not may_control
|
|
253
|
+
or not all(isinstance(item, str) and item.strip() for item in may_control)
|
|
254
|
+
or not isinstance(must_not_control, list)
|
|
255
|
+
or not must_not_control
|
|
256
|
+
or not all(isinstance(item, str) and item.strip() for item in must_not_control)
|
|
257
|
+
):
|
|
258
|
+
raise ValueError("reference binding semantics are invalid")
|
|
259
|
+
values = {
|
|
260
|
+
"order": index,
|
|
261
|
+
"reference": reference_tokens[index - 1] if reference_tokens else str(index),
|
|
262
|
+
"label": label.strip(),
|
|
263
|
+
"role": role.strip(),
|
|
264
|
+
"may": ", ".join(item.strip() for item in may_control),
|
|
265
|
+
"must": ", ".join(item.strip() for item in must_not_control),
|
|
266
|
+
}
|
|
267
|
+
if language.startswith("zh"):
|
|
268
|
+
instructions.append(
|
|
269
|
+
f"{zh_reference_prefix} "
|
|
270
|
+
+ (
|
|
271
|
+
"{reference}({label}),用途 {role}。允许控制:{may}。"
|
|
272
|
+
"不得控制:{must}。".format(**values)
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
elif language.startswith("en"):
|
|
276
|
+
instructions.append(
|
|
277
|
+
"Reference {reference} ({label}), role {role}. May control: {may}. "
|
|
278
|
+
"Must not control: {must}.".format(**values)
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
instructions.append(
|
|
282
|
+
"[REF {order} | {label} | {role}] [+] {may} [-] {must}".format(
|
|
283
|
+
**values
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
if language.startswith("zh"):
|
|
287
|
+
heading = zh_heading
|
|
288
|
+
elif language.startswith("en"):
|
|
289
|
+
heading = "Reference contract:"
|
|
290
|
+
else:
|
|
291
|
+
heading = "<REF_CONTRACT>"
|
|
292
|
+
return f"{prompt}\n\n{heading}\n" + "\n".join(instructions)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _pop_prompt_language(parameters: dict[str, Any]) -> str | None:
|
|
296
|
+
value = parameters.pop("prompt_language", None)
|
|
297
|
+
if value is None:
|
|
298
|
+
return None
|
|
299
|
+
if not isinstance(value, str) or not value.strip() or len(value) > 64:
|
|
300
|
+
raise ValueError("prompt_language must be a non-empty bounded language tag")
|
|
301
|
+
return value.strip()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _take(parameters: dict[str, Any], allowed: set[str]) -> dict[str, Any]:
|
|
305
|
+
unknown = set(parameters) - allowed
|
|
306
|
+
if unknown:
|
|
307
|
+
raise ValueError("unsupported provider parameters: " + ", ".join(sorted(unknown)))
|
|
308
|
+
return parameters
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def compile_seedance_payload(
|
|
312
|
+
job: Mapping[str, Any],
|
|
313
|
+
*,
|
|
314
|
+
model: str,
|
|
315
|
+
reference_urls: Sequence[str] = (),
|
|
316
|
+
reference_roles: Sequence[str] = (),
|
|
317
|
+
allowed_ratios: Collection[str] | None = None,
|
|
318
|
+
duration_range: tuple[int, int] | None = None,
|
|
319
|
+
) -> dict[str, Any]:
|
|
320
|
+
"""Compile a video production job into the official Seedance task body.
|
|
321
|
+
|
|
322
|
+
``model`` is deliberately mandatory: callers must obtain it from explicit
|
|
323
|
+
runtime configuration rather than assuming any Seedance release.
|
|
324
|
+
"""
|
|
325
|
+
prompt, parameters = _require_job(job, "video")
|
|
326
|
+
if not isinstance(model, str) or not model.strip():
|
|
327
|
+
raise ValueError("Seedance model must be explicitly configured")
|
|
328
|
+
references = job.get("references", [])
|
|
329
|
+
if not isinstance(references, list) or len(reference_urls) != len(references):
|
|
330
|
+
raise ValueError("Seedance reference URLs must match job references")
|
|
331
|
+
if len(reference_roles) != len(reference_urls):
|
|
332
|
+
raise ValueError("Seedance reference roles must match job references")
|
|
333
|
+
if Path(job["outputs"][0]).suffix.casefold() != ".mp4":
|
|
334
|
+
raise ValueError("Seedance adapter requires an MP4 target")
|
|
335
|
+
parameters = _take(
|
|
336
|
+
parameters,
|
|
337
|
+
{
|
|
338
|
+
"duration",
|
|
339
|
+
"ratio",
|
|
340
|
+
"generate_audio",
|
|
341
|
+
"omni_reference_task_type",
|
|
342
|
+
"prompt_language",
|
|
343
|
+
},
|
|
344
|
+
)
|
|
345
|
+
prompt_language = _pop_prompt_language(parameters)
|
|
346
|
+
duration = parameters.get("duration")
|
|
347
|
+
if duration is not None and (
|
|
348
|
+
not isinstance(duration, int)
|
|
349
|
+
or isinstance(duration, bool)
|
|
350
|
+
or (duration != -1 and not 1 <= duration <= 30)
|
|
351
|
+
):
|
|
352
|
+
raise ValueError("Seedance duration must be -1 or an integer from 1 to 30")
|
|
353
|
+
if duration is not None and duration != -1:
|
|
354
|
+
if duration_range is None:
|
|
355
|
+
raise ValueError("Seedance duration needs an explicit model profile")
|
|
356
|
+
minimum, maximum = duration_range
|
|
357
|
+
if not 1 <= minimum <= maximum <= 30 or not minimum <= duration <= maximum:
|
|
358
|
+
raise ValueError("Seedance duration is outside the configured model profile")
|
|
359
|
+
ratio = parameters.get("ratio")
|
|
360
|
+
if ratio is not None and (
|
|
361
|
+
not isinstance(ratio, str) or ratio.strip() not in SEEDANCE_RATIOS
|
|
362
|
+
):
|
|
363
|
+
raise ValueError("Seedance ratio is outside the supported profile")
|
|
364
|
+
if ratio is not None:
|
|
365
|
+
configured_ratios = set(allowed_ratios or ())
|
|
366
|
+
if not configured_ratios:
|
|
367
|
+
raise ValueError("Seedance ratio needs an explicit model profile")
|
|
368
|
+
if not configured_ratios <= SEEDANCE_RATIOS or ratio.strip() not in configured_ratios:
|
|
369
|
+
raise ValueError("Seedance ratio is outside the configured model profile")
|
|
370
|
+
ratio = ratio.strip()
|
|
371
|
+
generate_audio = parameters.get("generate_audio")
|
|
372
|
+
if generate_audio is not None and not isinstance(generate_audio, bool):
|
|
373
|
+
raise ValueError("Seedance generate_audio must be a boolean")
|
|
374
|
+
task_type = parameters.get("omni_reference_task_type")
|
|
375
|
+
if task_type is not None and task_type not in {"auto", "reference", "edit", "extend"}:
|
|
376
|
+
raise ValueError("Seedance omni reference task type is invalid")
|
|
377
|
+
counters = {"image_url": 0, "video_url": 0, "audio_url": 0}
|
|
378
|
+
labels = {"image_url": "图片", "video_url": "视频", "audio_url": "音频"}
|
|
379
|
+
reference_tokens: list[str] = []
|
|
380
|
+
singular_seen: set[str] = set()
|
|
381
|
+
for role in reference_roles:
|
|
382
|
+
field = SEEDANCE_REFERENCE_ROLES.get(role)
|
|
383
|
+
if field is None:
|
|
384
|
+
raise ValueError(f"unsupported Seedance reference role: {role}")
|
|
385
|
+
if role in SEEDANCE_SINGULAR_ROLES:
|
|
386
|
+
if role in singular_seen:
|
|
387
|
+
raise ValueError(f"Seedance accepts one {role} reference")
|
|
388
|
+
singular_seen.add(role)
|
|
389
|
+
# A frame role still occupies a picture slot, so it keeps counting: the
|
|
390
|
+
# tokens are positional, and skipping one would point every later
|
|
391
|
+
# `@图片N` in the prose at the wrong picture.
|
|
392
|
+
counters[field] += 1
|
|
393
|
+
reference_tokens.append(f"@{labels[field]}{counters[field]}")
|
|
394
|
+
if task_type in {"edit", "extend"} and "reference_video" not in reference_roles:
|
|
395
|
+
raise ValueError(f"Seedance {task_type} requires a reference video")
|
|
396
|
+
if task_type == "edit" and (ratio != "adaptive" or duration != -1):
|
|
397
|
+
raise ValueError("Seedance edit requires adaptive ratio and duration -1")
|
|
398
|
+
if task_type == "extend" and ratio != "adaptive":
|
|
399
|
+
raise ValueError("Seedance extend requires an adaptive ratio")
|
|
400
|
+
text = _prompt_with_reference_contract(
|
|
401
|
+
prompt,
|
|
402
|
+
job,
|
|
403
|
+
prompt_language=prompt_language,
|
|
404
|
+
reference_tokens=reference_tokens,
|
|
405
|
+
zh_reference_prefix=(
|
|
406
|
+
"输入素材" if task_type in {"edit", "extend"} else "参考"
|
|
407
|
+
),
|
|
408
|
+
zh_heading=(
|
|
409
|
+
"输入素材约束:" if task_type in {"edit", "extend"} else "参考约束:"
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
content: list[dict[str, Any]] = [{"type": "text", "text": text}]
|
|
413
|
+
for index, url in enumerate(reference_urls):
|
|
414
|
+
if not isinstance(url, str) or not url:
|
|
415
|
+
raise ValueError("Seedance reference URL must be non-empty")
|
|
416
|
+
role = reference_roles[index]
|
|
417
|
+
field = SEEDANCE_REFERENCE_ROLES[role]
|
|
418
|
+
suffix = Path(str(references[index])).suffix.casefold()
|
|
419
|
+
expected = {
|
|
420
|
+
"image_url": {".png", ".jpg", ".jpeg", ".webp"},
|
|
421
|
+
"video_url": {".mp4", ".mov", ".webm"},
|
|
422
|
+
"audio_url": {".wav", ".mp3", ".m4a", ".aac", ".flac"},
|
|
423
|
+
}[field]
|
|
424
|
+
if suffix not in expected:
|
|
425
|
+
raise ValueError(f"Seedance {role} does not match the reference file type")
|
|
426
|
+
parsed = urllib.parse.urlparse(url)
|
|
427
|
+
if not (
|
|
428
|
+
(parsed.scheme == "https" and parsed.netloc)
|
|
429
|
+
or (
|
|
430
|
+
parsed.scheme == "asset"
|
|
431
|
+
and parsed.netloc.startswith("asset-")
|
|
432
|
+
and not parsed.path
|
|
433
|
+
)
|
|
434
|
+
or _is_inline_reference(url)
|
|
435
|
+
):
|
|
436
|
+
raise ValueError(
|
|
437
|
+
"Seedance reference URL must be HTTPS, asset:// or a base64 data URI"
|
|
438
|
+
)
|
|
439
|
+
content.append({"type": field, field: {"url": url}, "role": role})
|
|
440
|
+
body: dict[str, Any] = {"model": model.strip(), "content": content}
|
|
441
|
+
if ratio is not None:
|
|
442
|
+
body["ratio"] = ratio
|
|
443
|
+
if duration is not None:
|
|
444
|
+
body["duration"] = duration
|
|
445
|
+
if generate_audio is not None:
|
|
446
|
+
body["generate_audio"] = generate_audio
|
|
447
|
+
if task_type is not None:
|
|
448
|
+
body["omni_reference_task_type"] = task_type
|
|
449
|
+
return body
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _seedance_runtime_profile(
|
|
453
|
+
) -> tuple[frozenset[str] | None, tuple[int, int] | None]:
|
|
454
|
+
raw_ratios = os.environ.get("SEEDANCE_ALLOWED_RATIOS")
|
|
455
|
+
allowed_ratios = (
|
|
456
|
+
frozenset(item.strip() for item in raw_ratios.split(",") if item.strip())
|
|
457
|
+
if raw_ratios is not None
|
|
458
|
+
else None
|
|
459
|
+
)
|
|
460
|
+
minimum_raw = os.environ.get("SEEDANCE_MIN_DURATION")
|
|
461
|
+
maximum_raw = os.environ.get("SEEDANCE_MAX_DURATION")
|
|
462
|
+
if (minimum_raw is None) != (maximum_raw is None):
|
|
463
|
+
raise AdapterFailure(
|
|
464
|
+
"Seedance duration profile is incomplete",
|
|
465
|
+
category="configuration",
|
|
466
|
+
code="invalid_model_profile",
|
|
467
|
+
)
|
|
468
|
+
try:
|
|
469
|
+
duration_range = (
|
|
470
|
+
(int(minimum_raw), int(maximum_raw))
|
|
471
|
+
if minimum_raw is not None and maximum_raw is not None
|
|
472
|
+
else None
|
|
473
|
+
)
|
|
474
|
+
except ValueError as exc:
|
|
475
|
+
raise AdapterFailure(
|
|
476
|
+
"Seedance duration profile is invalid",
|
|
477
|
+
category="configuration",
|
|
478
|
+
code="invalid_model_profile",
|
|
479
|
+
) from exc
|
|
480
|
+
if allowed_ratios is not None and (
|
|
481
|
+
not allowed_ratios or not allowed_ratios <= SEEDANCE_RATIOS
|
|
482
|
+
):
|
|
483
|
+
raise AdapterFailure(
|
|
484
|
+
"Seedance ratio profile is invalid",
|
|
485
|
+
category="configuration",
|
|
486
|
+
code="invalid_model_profile",
|
|
487
|
+
)
|
|
488
|
+
if duration_range is not None and not (
|
|
489
|
+
1 <= duration_range[0] <= duration_range[1] <= 30
|
|
490
|
+
):
|
|
491
|
+
raise AdapterFailure(
|
|
492
|
+
"Seedance duration profile is invalid",
|
|
493
|
+
category="configuration",
|
|
494
|
+
code="invalid_model_profile",
|
|
495
|
+
)
|
|
496
|
+
return allowed_ratios, duration_range
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def compile_minimax_h3_payload(
|
|
500
|
+
job: Mapping[str, Any],
|
|
501
|
+
*,
|
|
502
|
+
model: str,
|
|
503
|
+
reference_urls: Sequence[str] = (),
|
|
504
|
+
reference_roles: Sequence[str] = (),
|
|
505
|
+
allowed_ratios: Collection[str] | None = None,
|
|
506
|
+
allowed_resolutions: Collection[str] | None = None,
|
|
507
|
+
duration_range: tuple[int, int] | None = None,
|
|
508
|
+
) -> dict[str, Any]:
|
|
509
|
+
"""Compile a video job into the official MiniMax video-generation task body.
|
|
510
|
+
|
|
511
|
+
``model`` is mandatory for the same reason it is for Seedance: which release
|
|
512
|
+
is enabled, and what it accepts, is deployment configuration, never an
|
|
513
|
+
assumption made here.
|
|
514
|
+
"""
|
|
515
|
+
prompt, parameters = _require_job(job, "video")
|
|
516
|
+
if not isinstance(model, str) or not model.strip():
|
|
517
|
+
raise ValueError("MiniMax video model must be explicitly configured")
|
|
518
|
+
references = job.get("references", [])
|
|
519
|
+
if not isinstance(references, list) or len(reference_urls) != len(references):
|
|
520
|
+
raise ValueError("MiniMax reference URLs must match job references")
|
|
521
|
+
if len(reference_roles) != len(reference_urls):
|
|
522
|
+
raise ValueError("MiniMax reference roles must match job references")
|
|
523
|
+
if Path(job["outputs"][0]).suffix.casefold() != ".mp4":
|
|
524
|
+
raise ValueError("MiniMax video adapter requires an MP4 target")
|
|
525
|
+
parameters = _take(
|
|
526
|
+
parameters, {"duration", "ratio", "resolution", "prompt_language"}
|
|
527
|
+
)
|
|
528
|
+
prompt_language = _pop_prompt_language(parameters)
|
|
529
|
+
|
|
530
|
+
duration = parameters.get("duration")
|
|
531
|
+
if not isinstance(duration, int) or isinstance(duration, bool):
|
|
532
|
+
raise ValueError("MiniMax video duration must be an integer number of seconds")
|
|
533
|
+
if duration_range is None:
|
|
534
|
+
raise ValueError("MiniMax video duration needs an explicit model profile")
|
|
535
|
+
minimum, maximum = duration_range
|
|
536
|
+
if not 1 <= minimum <= maximum or not minimum <= duration <= maximum:
|
|
537
|
+
raise ValueError("MiniMax video duration is outside the configured model profile")
|
|
538
|
+
|
|
539
|
+
resolution = parameters.get("resolution")
|
|
540
|
+
if not isinstance(resolution, str) or resolution.strip() not in MINIMAX_VIDEO_RESOLUTIONS:
|
|
541
|
+
raise ValueError("MiniMax video resolution is outside the supported profile")
|
|
542
|
+
configured_resolutions = set(allowed_resolutions or ())
|
|
543
|
+
if not configured_resolutions:
|
|
544
|
+
raise ValueError("MiniMax video resolution needs an explicit model profile")
|
|
545
|
+
if (
|
|
546
|
+
not configured_resolutions <= MINIMAX_VIDEO_RESOLUTIONS
|
|
547
|
+
or resolution.strip() not in configured_resolutions
|
|
548
|
+
):
|
|
549
|
+
raise ValueError("MiniMax video resolution is outside the configured model profile")
|
|
550
|
+
|
|
551
|
+
ratio = parameters.get("ratio")
|
|
552
|
+
if ratio is not None:
|
|
553
|
+
if not isinstance(ratio, str) or ratio.strip() not in MINIMAX_VIDEO_RATIOS:
|
|
554
|
+
raise ValueError("MiniMax video ratio is outside the supported profile")
|
|
555
|
+
configured_ratios = set(allowed_ratios or ())
|
|
556
|
+
if not configured_ratios:
|
|
557
|
+
raise ValueError("MiniMax video ratio needs an explicit model profile")
|
|
558
|
+
if (
|
|
559
|
+
not configured_ratios <= MINIMAX_VIDEO_RATIOS
|
|
560
|
+
or ratio.strip() not in configured_ratios
|
|
561
|
+
):
|
|
562
|
+
raise ValueError("MiniMax video ratio is outside the configured model profile")
|
|
563
|
+
|
|
564
|
+
counters = {"image_url": 0, "video_url": 0, "audio_url": 0}
|
|
565
|
+
labels = {"image_url": "Picture", "video_url": "Video", "audio_url": "Audio"}
|
|
566
|
+
reference_tokens: list[str] = []
|
|
567
|
+
for role in reference_roles:
|
|
568
|
+
field = MINIMAX_VIDEO_ROLES.get(role)
|
|
569
|
+
if field is None:
|
|
570
|
+
raise ValueError(f"unsupported MiniMax reference role: {role}")
|
|
571
|
+
counters[field] += 1
|
|
572
|
+
reference_tokens.append(f"<{labels[field]} {counters[field]}>")
|
|
573
|
+
text = _prompt_with_reference_contract(
|
|
574
|
+
prompt,
|
|
575
|
+
job,
|
|
576
|
+
prompt_language=prompt_language,
|
|
577
|
+
reference_tokens=reference_tokens,
|
|
578
|
+
)
|
|
579
|
+
if len(text) > MINIMAX_VIDEO_PROMPT_LIMIT:
|
|
580
|
+
raise ValueError("MiniMax video prompt exceeds the provider limit")
|
|
581
|
+
content: list[dict[str, Any]] = [{"type": "text", "text": text}]
|
|
582
|
+
seen_roles: list[str] = []
|
|
583
|
+
for index, url in enumerate(reference_urls):
|
|
584
|
+
role = reference_roles[index]
|
|
585
|
+
if role not in MINIMAX_VIDEO_ROLES:
|
|
586
|
+
raise ValueError(f"unsupported MiniMax reference role: {role}")
|
|
587
|
+
if role in {"first_frame", "last_frame"} and role in seen_roles:
|
|
588
|
+
raise ValueError(f"MiniMax accepts one {role} reference")
|
|
589
|
+
seen_roles.append(role)
|
|
590
|
+
if not isinstance(url, str) or not url:
|
|
591
|
+
raise ValueError("MiniMax reference URL must be non-empty")
|
|
592
|
+
parsed = urllib.parse.urlparse(url)
|
|
593
|
+
if not (
|
|
594
|
+
(parsed.scheme == "https" and parsed.netloc)
|
|
595
|
+
or (parsed.scheme == "mm_file" and parsed.netloc and not parsed.path)
|
|
596
|
+
or _is_inline_reference(url)
|
|
597
|
+
):
|
|
598
|
+
raise ValueError(
|
|
599
|
+
"MiniMax reference URL must be HTTPS, mm_file:// or a base64 data URI"
|
|
600
|
+
)
|
|
601
|
+
field = MINIMAX_VIDEO_ROLES[role]
|
|
602
|
+
content.append({"type": field, field: {"url": url}, "role": role})
|
|
603
|
+
frame_roles = {"first_frame", "last_frame"}
|
|
604
|
+
reference_roles_present = {"reference_image", "reference_video", "reference_audio"}
|
|
605
|
+
if frame_roles.intersection(seen_roles) and reference_roles_present.intersection(
|
|
606
|
+
seen_roles
|
|
607
|
+
):
|
|
608
|
+
raise ValueError(
|
|
609
|
+
"MiniMax frame conditioning cannot be mixed with reference conditioning"
|
|
610
|
+
)
|
|
611
|
+
if ratio is None and len(content) == 1:
|
|
612
|
+
raise ValueError("MiniMax text-to-video requires an explicit ratio")
|
|
613
|
+
body: dict[str, Any] = {
|
|
614
|
+
"model": model.strip(),
|
|
615
|
+
"content": content,
|
|
616
|
+
"duration": duration,
|
|
617
|
+
"resolution": resolution.strip(),
|
|
618
|
+
}
|
|
619
|
+
if ratio is not None:
|
|
620
|
+
if ratio.strip() == "adaptive" and len(content) == 1:
|
|
621
|
+
raise ValueError("MiniMax text-to-video cannot use an adaptive ratio")
|
|
622
|
+
body["ratio"] = ratio.strip()
|
|
623
|
+
return body
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _minimax_video_runtime_profile() -> tuple[
|
|
627
|
+
frozenset[str] | None, frozenset[str] | None, tuple[int, int] | None
|
|
628
|
+
]:
|
|
629
|
+
def _set(name: str, supported: Collection[str]) -> frozenset[str] | None:
|
|
630
|
+
raw = os.environ.get(name)
|
|
631
|
+
if raw is None:
|
|
632
|
+
return None
|
|
633
|
+
values = frozenset(item.strip() for item in raw.split(",") if item.strip())
|
|
634
|
+
if not values or not values <= set(supported):
|
|
635
|
+
raise AdapterFailure(
|
|
636
|
+
f"MiniMax video profile is invalid: {name}",
|
|
637
|
+
category="configuration",
|
|
638
|
+
code="invalid_model_profile",
|
|
639
|
+
)
|
|
640
|
+
return values
|
|
641
|
+
|
|
642
|
+
allowed_ratios = _set("MINIMAX_VIDEO_RATIOS", MINIMAX_VIDEO_RATIOS)
|
|
643
|
+
allowed_resolutions = _set("MINIMAX_VIDEO_RESOLUTIONS", MINIMAX_VIDEO_RESOLUTIONS)
|
|
644
|
+
minimum_raw = os.environ.get("MINIMAX_VIDEO_MIN_DURATION")
|
|
645
|
+
maximum_raw = os.environ.get("MINIMAX_VIDEO_MAX_DURATION")
|
|
646
|
+
if (minimum_raw is None) != (maximum_raw is None):
|
|
647
|
+
raise AdapterFailure(
|
|
648
|
+
"MiniMax video duration profile is incomplete",
|
|
649
|
+
category="configuration",
|
|
650
|
+
code="invalid_model_profile",
|
|
651
|
+
)
|
|
652
|
+
try:
|
|
653
|
+
duration_range = (
|
|
654
|
+
(int(minimum_raw), int(maximum_raw))
|
|
655
|
+
if minimum_raw is not None and maximum_raw is not None
|
|
656
|
+
else None
|
|
657
|
+
)
|
|
658
|
+
except ValueError as exc:
|
|
659
|
+
raise AdapterFailure(
|
|
660
|
+
"MiniMax video duration profile is invalid",
|
|
661
|
+
category="configuration",
|
|
662
|
+
code="invalid_model_profile",
|
|
663
|
+
) from exc
|
|
664
|
+
if duration_range is not None and not 1 <= duration_range[0] <= duration_range[1]:
|
|
665
|
+
raise AdapterFailure(
|
|
666
|
+
"MiniMax video duration profile is invalid",
|
|
667
|
+
category="configuration",
|
|
668
|
+
code="invalid_model_profile",
|
|
669
|
+
)
|
|
670
|
+
return allowed_ratios, allowed_resolutions, duration_range
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def compile_gpt_image_2_payload(job: Mapping[str, Any]) -> dict[str, Any]:
|
|
674
|
+
"""Compile an image job into GPT Image 2 generation/edit fields."""
|
|
675
|
+
prompt, parameters = _require_job(job, "image")
|
|
676
|
+
parameters = _take(
|
|
677
|
+
parameters,
|
|
678
|
+
{
|
|
679
|
+
"width",
|
|
680
|
+
"height",
|
|
681
|
+
"size",
|
|
682
|
+
"quality",
|
|
683
|
+
"background",
|
|
684
|
+
"moderation",
|
|
685
|
+
"prompt_language",
|
|
686
|
+
},
|
|
687
|
+
)
|
|
688
|
+
prompt_language = _pop_prompt_language(parameters)
|
|
689
|
+
prompt = _prompt_with_reference_contract(
|
|
690
|
+
prompt, job, prompt_language=prompt_language
|
|
691
|
+
)
|
|
692
|
+
if len(prompt) > 32000:
|
|
693
|
+
raise ValueError("GPT Image prompt exceeds the provider limit")
|
|
694
|
+
width = parameters.pop("width", None)
|
|
695
|
+
height = parameters.pop("height", None)
|
|
696
|
+
if (width is None) != (height is None) or (width is not None and "size" in parameters):
|
|
697
|
+
raise ValueError("use either both width/height or size")
|
|
698
|
+
if width is not None:
|
|
699
|
+
if not isinstance(width, int) or not isinstance(height, int) or width <= 0 or height <= 0:
|
|
700
|
+
raise ValueError("image dimensions must be positive integers")
|
|
701
|
+
if width % 16 or height % 16 or not (1 / 3 <= width / height <= 3):
|
|
702
|
+
raise ValueError("GPT Image dimensions must be divisible by 16 with ratio between 1:3 and 3:1")
|
|
703
|
+
parameters["size"] = f"{width}x{height}"
|
|
704
|
+
size = parameters.get("size")
|
|
705
|
+
if size != "auto" and size is not None:
|
|
706
|
+
if not isinstance(size, str) or "x" not in size:
|
|
707
|
+
raise ValueError("GPT Image size is invalid")
|
|
708
|
+
try:
|
|
709
|
+
parsed_width, parsed_height = (int(part) for part in size.split("x", 1))
|
|
710
|
+
except ValueError as exc:
|
|
711
|
+
raise ValueError("GPT Image size is invalid") from exc
|
|
712
|
+
if (
|
|
713
|
+
parsed_width <= 0
|
|
714
|
+
or parsed_height <= 0
|
|
715
|
+
or parsed_width % 16
|
|
716
|
+
or parsed_height % 16
|
|
717
|
+
or not (1 / 3 <= parsed_width / parsed_height <= 3)
|
|
718
|
+
or parsed_width > 3840
|
|
719
|
+
or parsed_height > 3840
|
|
720
|
+
or not (
|
|
721
|
+
GPT_IMAGE_MIN_PIXELS
|
|
722
|
+
<= parsed_width * parsed_height
|
|
723
|
+
<= GPT_IMAGE_MAX_PIXELS
|
|
724
|
+
)
|
|
725
|
+
):
|
|
726
|
+
raise ValueError("GPT Image size violates official dimension constraints")
|
|
727
|
+
if parameters.get("background") == "transparent":
|
|
728
|
+
raise ValueError("GPT Image 2 transparent background is unsupported")
|
|
729
|
+
if "background" in parameters and parameters["background"] not in {"auto", "opaque"}:
|
|
730
|
+
raise ValueError("GPT Image background is invalid")
|
|
731
|
+
if "quality" in parameters and parameters["quality"] not in {"auto", "low", "medium", "high"}:
|
|
732
|
+
raise ValueError("GPT Image quality is invalid")
|
|
733
|
+
if "moderation" in parameters and parameters["moderation"] not in {"auto", "low"}:
|
|
734
|
+
raise ValueError("GPT Image moderation is invalid")
|
|
735
|
+
suffix = Path(job["outputs"][0]).suffix.casefold()
|
|
736
|
+
formats = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".webp": "webp"}
|
|
737
|
+
if suffix not in formats:
|
|
738
|
+
raise ValueError("unsupported GPT Image output extension")
|
|
739
|
+
references = job.get("references", [])
|
|
740
|
+
if not isinstance(references, list) or len(references) > 16:
|
|
741
|
+
raise ValueError("GPT Image accepts at most sixteen references")
|
|
742
|
+
if any(
|
|
743
|
+
not isinstance(reference, str)
|
|
744
|
+
or Path(reference).suffix.casefold() not in {".png", ".jpg", ".jpeg", ".webp"}
|
|
745
|
+
for reference in references
|
|
746
|
+
):
|
|
747
|
+
raise ValueError("GPT Image references must be supported image files")
|
|
748
|
+
return {
|
|
749
|
+
"model": OPENAI_MODEL,
|
|
750
|
+
"prompt": prompt,
|
|
751
|
+
"n": 1,
|
|
752
|
+
"output_format": formats[suffix],
|
|
753
|
+
**parameters,
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
MINIMAX_SPEECH_EMOTIONS = {
|
|
758
|
+
"happy", "sad", "angry", "fearful", "disgusted", "surprised", "neutral",
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def compile_minimax_speech_payload(job: Mapping[str, Any]) -> dict[str, Any]:
|
|
763
|
+
"""Compile a tts job into the official MiniMax speech JSON body.
|
|
764
|
+
|
|
765
|
+
The voice catalogue is deliberately not enumerated here. Which preset voices
|
|
766
|
+
an account can reach depends on the model and the account, no published list
|
|
767
|
+
is authoritative for both, and a list frozen into this file would either
|
|
768
|
+
refuse a voice that works or vouch for one that does not. The document owns
|
|
769
|
+
the value and a reviewer reads it; the adapter only checks its shape.
|
|
770
|
+
"""
|
|
771
|
+
|
|
772
|
+
text, parameters = _require_job(job, "tts")
|
|
773
|
+
parameters = _take(
|
|
774
|
+
parameters,
|
|
775
|
+
{"model", "voice_id", "speed", "vol", "pitch", "emotion",
|
|
776
|
+
"sample_rate", "bitrate", "format"},
|
|
777
|
+
)
|
|
778
|
+
model = parameters.pop("model", "")
|
|
779
|
+
if not isinstance(model, str) or not model.strip():
|
|
780
|
+
raise ValueError("MiniMax speech model must be explicitly configured")
|
|
781
|
+
voice_id = parameters.pop("voice_id", "")
|
|
782
|
+
if not isinstance(voice_id, str) or not voice_id.strip() or voice_id != voice_id.strip():
|
|
783
|
+
raise ValueError("MiniMax speech requires a confirmed voice_id")
|
|
784
|
+
if len(text) > MINIMAX_SPEECH_TEXT_LIMIT:
|
|
785
|
+
raise ValueError("MiniMax speech text exceeds the provider limit")
|
|
786
|
+
|
|
787
|
+
target_format = Path(job["outputs"][0]).suffix.casefold().lstrip(".")
|
|
788
|
+
requested_format = parameters.pop("format", target_format)
|
|
789
|
+
if requested_format != target_format or requested_format not in {"mp3", "wav"}:
|
|
790
|
+
raise ValueError("MiniMax output format must match a supported target extension")
|
|
791
|
+
sample_rate = parameters.pop("sample_rate", 32000)
|
|
792
|
+
bitrate = parameters.pop("bitrate", 128000)
|
|
793
|
+
if sample_rate not in {16000, 24000, 32000, 44100}:
|
|
794
|
+
raise ValueError("unsupported MiniMax sample rate")
|
|
795
|
+
if bitrate not in {32000, 64000, 128000, 256000}:
|
|
796
|
+
raise ValueError("unsupported MiniMax bitrate")
|
|
797
|
+
|
|
798
|
+
voice_setting: dict[str, Any] = {"voice_id": voice_id}
|
|
799
|
+
emotion = parameters.pop("emotion", None)
|
|
800
|
+
if emotion is not None:
|
|
801
|
+
if emotion not in MINIMAX_SPEECH_EMOTIONS:
|
|
802
|
+
raise ValueError("unsupported MiniMax speech emotion")
|
|
803
|
+
voice_setting["emotion"] = emotion
|
|
804
|
+
for name, low, high in (("speed", 0.5, 2.0), ("vol", 0.1, 10.0)):
|
|
805
|
+
value = parameters.pop(name, None)
|
|
806
|
+
if value is None:
|
|
807
|
+
continue
|
|
808
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
809
|
+
raise ValueError(f"MiniMax speech {name} must be a number")
|
|
810
|
+
if not low <= float(value) <= high:
|
|
811
|
+
raise ValueError(f"MiniMax speech {name} is outside the supported range")
|
|
812
|
+
voice_setting[name] = float(value)
|
|
813
|
+
pitch = parameters.pop("pitch", None)
|
|
814
|
+
if pitch is not None:
|
|
815
|
+
if isinstance(pitch, bool) or not isinstance(pitch, int) or not -12 <= pitch <= 12:
|
|
816
|
+
raise ValueError("MiniMax speech pitch is outside the supported range")
|
|
817
|
+
voice_setting["pitch"] = pitch
|
|
818
|
+
|
|
819
|
+
return {
|
|
820
|
+
"model": model.strip(),
|
|
821
|
+
"text": text,
|
|
822
|
+
"stream": False,
|
|
823
|
+
"output_format": "hex",
|
|
824
|
+
"voice_setting": voice_setting,
|
|
825
|
+
"audio_setting": {
|
|
826
|
+
"sample_rate": sample_rate,
|
|
827
|
+
"bitrate": bitrate,
|
|
828
|
+
"format": requested_format,
|
|
829
|
+
},
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def compile_minimax_music_payload(job: Mapping[str, Any]) -> dict[str, Any]:
|
|
834
|
+
"""Compile an audio job into the official MiniMax Music 3.0 JSON body."""
|
|
835
|
+
prompt, parameters = _require_job(job, "music")
|
|
836
|
+
parameters = _take(
|
|
837
|
+
parameters,
|
|
838
|
+
{"lyrics", "sample_rate", "bitrate", "format", "lyrics_optimizer", "is_instrumental"},
|
|
839
|
+
)
|
|
840
|
+
target_format = Path(job["outputs"][0]).suffix.casefold().lstrip(".")
|
|
841
|
+
requested_format = parameters.pop("format", target_format)
|
|
842
|
+
if requested_format != target_format or requested_format not in {"mp3", "wav"}:
|
|
843
|
+
raise ValueError("MiniMax output format must match a supported target extension")
|
|
844
|
+
lyrics = parameters.pop("lyrics", None)
|
|
845
|
+
instrumental = parameters.pop("is_instrumental", False)
|
|
846
|
+
optimizer = parameters.pop("lyrics_optimizer", False)
|
|
847
|
+
if not isinstance(instrumental, bool) or not isinstance(optimizer, bool):
|
|
848
|
+
raise ValueError("MiniMax boolean parameters must be booleans")
|
|
849
|
+
if optimizer:
|
|
850
|
+
raise ValueError("MiniMax lyrics_optimizer is forbidden for confirmed production")
|
|
851
|
+
if instrumental and lyrics not in {None, ""}:
|
|
852
|
+
raise ValueError("MiniMax instrumental music must not carry lyrics")
|
|
853
|
+
if not instrumental and (not isinstance(lyrics, str) or not lyrics.strip()):
|
|
854
|
+
raise ValueError("MiniMax vocal music requires confirmed lyrics")
|
|
855
|
+
if len(prompt) > 2000 or (isinstance(lyrics, str) and len(lyrics) > 3500):
|
|
856
|
+
raise ValueError("MiniMax prompt or lyrics exceeds the provider limit")
|
|
857
|
+
sample_rate = parameters.pop("sample_rate", 44100)
|
|
858
|
+
bitrate = parameters.pop("bitrate", 256000)
|
|
859
|
+
if sample_rate not in {16000, 24000, 32000, 44100}:
|
|
860
|
+
raise ValueError("unsupported MiniMax sample rate")
|
|
861
|
+
if bitrate not in {32000, 64000, 128000, 256000}:
|
|
862
|
+
raise ValueError("unsupported MiniMax bitrate")
|
|
863
|
+
body: dict[str, Any] = {
|
|
864
|
+
"model": MINIMAX_MUSIC_MODEL,
|
|
865
|
+
"prompt": prompt,
|
|
866
|
+
"stream": False,
|
|
867
|
+
"output_format": "hex",
|
|
868
|
+
"audio_setting": {
|
|
869
|
+
"sample_rate": sample_rate,
|
|
870
|
+
"bitrate": bitrate,
|
|
871
|
+
"format": requested_format,
|
|
872
|
+
},
|
|
873
|
+
"lyrics_optimizer": False,
|
|
874
|
+
"is_instrumental": instrumental,
|
|
875
|
+
}
|
|
876
|
+
if lyrics is not None:
|
|
877
|
+
if not isinstance(lyrics, str):
|
|
878
|
+
raise ValueError("MiniMax lyrics must be a string")
|
|
879
|
+
body["lyrics"] = lyrics
|
|
880
|
+
return body
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _base_url(env_name: str, default: str) -> str:
|
|
884
|
+
value = os.environ.get(env_name, default).rstrip("/")
|
|
885
|
+
parsed = urllib.parse.urlparse(value)
|
|
886
|
+
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password:
|
|
887
|
+
raise AdapterFailure(
|
|
888
|
+
"provider base URL is invalid",
|
|
889
|
+
category="configuration",
|
|
890
|
+
code="invalid_base_url",
|
|
891
|
+
)
|
|
892
|
+
return value
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _credential(name: str) -> str:
|
|
896
|
+
value = os.environ.get(name)
|
|
897
|
+
if not value:
|
|
898
|
+
raise AdapterFailure(
|
|
899
|
+
"provider credential is not configured",
|
|
900
|
+
category="configuration",
|
|
901
|
+
code="missing_credential",
|
|
902
|
+
)
|
|
903
|
+
return value
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def _request_json(
|
|
907
|
+
url: str,
|
|
908
|
+
*,
|
|
909
|
+
provider: str,
|
|
910
|
+
method: str = "POST",
|
|
911
|
+
body: Mapping[str, Any] | None = None,
|
|
912
|
+
token: str,
|
|
913
|
+
) -> tuple[dict[str, Any], Mapping[str, str]]:
|
|
914
|
+
data = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
915
|
+
request = urllib.request.Request(url, data=data, method=method)
|
|
916
|
+
request.add_header("Authorization", f"Bearer {token}")
|
|
917
|
+
if body is not None:
|
|
918
|
+
request.add_header("Content-Type", "application/json")
|
|
919
|
+
try:
|
|
920
|
+
with urllib.request.urlopen(request, timeout=180) as response:
|
|
921
|
+
raw = response.read(MAX_JSON_RESPONSE + 1)
|
|
922
|
+
headers = dict(response.headers.items())
|
|
923
|
+
except urllib.error.HTTPError as exc:
|
|
924
|
+
raise _http_failure(provider, exc) from exc
|
|
925
|
+
except TimeoutError as exc:
|
|
926
|
+
raise AdapterFailure(
|
|
927
|
+
f"{provider} HTTP request timed out",
|
|
928
|
+
category="timeout",
|
|
929
|
+
code="request_timeout",
|
|
930
|
+
retryable=True,
|
|
931
|
+
) from exc
|
|
932
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
933
|
+
raise AdapterFailure(
|
|
934
|
+
f"{provider} HTTP request failed",
|
|
935
|
+
category="network",
|
|
936
|
+
code="network_error",
|
|
937
|
+
retryable=True,
|
|
938
|
+
) from exc
|
|
939
|
+
if len(raw) > MAX_JSON_RESPONSE:
|
|
940
|
+
raise AdapterFailure(
|
|
941
|
+
f"{provider} response is too large", code="response_too_large"
|
|
942
|
+
)
|
|
943
|
+
try:
|
|
944
|
+
document = json.loads(raw)
|
|
945
|
+
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
946
|
+
raise AdapterFailure(
|
|
947
|
+
f"{provider} returned invalid JSON", code="invalid_json"
|
|
948
|
+
) from exc
|
|
949
|
+
if not isinstance(document, dict):
|
|
950
|
+
raise AdapterFailure(
|
|
951
|
+
f"{provider} returned an invalid response", code="invalid_response"
|
|
952
|
+
)
|
|
953
|
+
return document, headers
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _read_reference(path: Path) -> bytes:
|
|
957
|
+
before = path.lstat()
|
|
958
|
+
if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
|
|
959
|
+
raise ValueError("reference is not a regular file")
|
|
960
|
+
if before.st_size > MAX_REFERENCE_BYTES:
|
|
961
|
+
raise ValueError("reference exceeds the provider input size limit")
|
|
962
|
+
# Windows opens files in text mode unless told otherwise, which stops the
|
|
963
|
+
# read at the first 0x1A byte -- the seventh byte of every PNG signature.
|
|
964
|
+
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
965
|
+
descriptor = os.open(path, flags)
|
|
966
|
+
try:
|
|
967
|
+
opened = os.fstat(descriptor)
|
|
968
|
+
if (
|
|
969
|
+
not stat.S_ISREG(opened.st_mode)
|
|
970
|
+
or (opened.st_dev, opened.st_ino) != (before.st_dev, before.st_ino)
|
|
971
|
+
):
|
|
972
|
+
raise ValueError("reference changed while opening")
|
|
973
|
+
chunks: list[bytes] = []
|
|
974
|
+
size = 0
|
|
975
|
+
while chunk := os.read(descriptor, 1024 * 1024):
|
|
976
|
+
size += len(chunk)
|
|
977
|
+
if size > MAX_REFERENCE_BYTES:
|
|
978
|
+
raise ValueError("reference exceeds the provider input size limit")
|
|
979
|
+
chunks.append(chunk)
|
|
980
|
+
return b"".join(chunks)
|
|
981
|
+
finally:
|
|
982
|
+
os.close(descriptor)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _multipart(fields: Mapping[str, Any], paths: Sequence[Path]) -> tuple[bytes, str]:
|
|
986
|
+
boundary = "short-drama-" + secrets.token_hex(16)
|
|
987
|
+
chunks: list[bytes] = []
|
|
988
|
+
for name, value in fields.items():
|
|
989
|
+
chunks.extend(
|
|
990
|
+
[
|
|
991
|
+
f"--{boundary}\r\n".encode(),
|
|
992
|
+
f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode(),
|
|
993
|
+
str(value).lower().encode() if isinstance(value, bool) else str(value).encode("utf-8"),
|
|
994
|
+
b"\r\n",
|
|
995
|
+
]
|
|
996
|
+
)
|
|
997
|
+
for path in paths:
|
|
998
|
+
content = _read_reference(path)
|
|
999
|
+
media_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
|
1000
|
+
chunks.extend(
|
|
1001
|
+
[
|
|
1002
|
+
f"--{boundary}\r\n".encode(),
|
|
1003
|
+
f'Content-Disposition: form-data; name="image[]"; filename="{path.name}"\r\n'.encode(),
|
|
1004
|
+
f"Content-Type: {media_type}\r\n\r\n".encode(),
|
|
1005
|
+
content,
|
|
1006
|
+
b"\r\n",
|
|
1007
|
+
]
|
|
1008
|
+
)
|
|
1009
|
+
chunks.append(f"--{boundary}--\r\n".encode())
|
|
1010
|
+
encoded = b"".join(chunks)
|
|
1011
|
+
if len(encoded) > MAX_MULTIPART_BYTES:
|
|
1012
|
+
raise ValueError("multipart provider input exceeds the total size limit")
|
|
1013
|
+
return encoded, f"multipart/form-data; boundary={boundary}"
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _reference_paths(job: Mapping[str, Any]) -> list[Path]:
|
|
1017
|
+
raw_root = job.get("project_root")
|
|
1018
|
+
if not isinstance(raw_root, str) or not Path(raw_root).is_absolute():
|
|
1019
|
+
raise ValueError("project_root is invalid")
|
|
1020
|
+
root = Path(raw_root).resolve()
|
|
1021
|
+
if not root.is_dir():
|
|
1022
|
+
raise ValueError("project_root is invalid")
|
|
1023
|
+
result: list[Path] = []
|
|
1024
|
+
for reference in job.get("references", []):
|
|
1025
|
+
if not isinstance(reference, str):
|
|
1026
|
+
raise ValueError("reference path is invalid")
|
|
1027
|
+
path = (root / reference).resolve()
|
|
1028
|
+
try:
|
|
1029
|
+
path.relative_to(root)
|
|
1030
|
+
except ValueError as exc:
|
|
1031
|
+
raise ValueError("reference escapes project root") from exc
|
|
1032
|
+
if not path.is_file() or path.is_symlink():
|
|
1033
|
+
raise ValueError("reference is not a regular file")
|
|
1034
|
+
result.append(path)
|
|
1035
|
+
return result
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _binding_roles(
|
|
1039
|
+
job: Mapping[str, Any], *, allowed: Mapping[str, str], provider: str
|
|
1040
|
+
) -> list[str]:
|
|
1041
|
+
"""The provider role of each reference, taken from the confirmed job.
|
|
1042
|
+
|
|
1043
|
+
`role` is the production-side translation of the creator document's 用途, so
|
|
1044
|
+
it is the job -- not this adapter -- that decides what a picture is for.
|
|
1045
|
+
"""
|
|
1046
|
+
bindings = job.get("reference_bindings", [])
|
|
1047
|
+
references = job.get("references", [])
|
|
1048
|
+
if not isinstance(bindings, list) or len(bindings) != len(references):
|
|
1049
|
+
raise AdapterFailure(
|
|
1050
|
+
f"{provider} references need one reference_bindings entry each, "
|
|
1051
|
+
"carrying the provider role for that file",
|
|
1052
|
+
category="configuration",
|
|
1053
|
+
code="missing_reference_roles",
|
|
1054
|
+
)
|
|
1055
|
+
roles: list[str] = []
|
|
1056
|
+
for binding in bindings:
|
|
1057
|
+
role = binding.get("role") if isinstance(binding, Mapping) else None
|
|
1058
|
+
if not isinstance(role, str) or role not in allowed:
|
|
1059
|
+
raise AdapterFailure(
|
|
1060
|
+
f"{provider} reference role must be one of "
|
|
1061
|
+
+ ", ".join(sorted(allowed))
|
|
1062
|
+
+ f"; got {role!r}",
|
|
1063
|
+
category="configuration",
|
|
1064
|
+
code="invalid_reference_role",
|
|
1065
|
+
)
|
|
1066
|
+
roles.append(role)
|
|
1067
|
+
return roles
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def _inline_reference_urls(
|
|
1071
|
+
paths: Sequence[Path], roles: Sequence[str], *, allowed: Mapping[str, str], provider: str
|
|
1072
|
+
) -> list[str]:
|
|
1073
|
+
"""Encode local project references as `data:` URIs the provider accepts."""
|
|
1074
|
+
urls: list[str] = []
|
|
1075
|
+
total = 0
|
|
1076
|
+
for path, role in zip(paths, roles):
|
|
1077
|
+
field = allowed[role]
|
|
1078
|
+
suffix = path.suffix.casefold()
|
|
1079
|
+
mime = INLINE_REFERENCE_MIME.get(suffix)
|
|
1080
|
+
expected = field.split("_", 1)[0]
|
|
1081
|
+
if mime is None or not mime.startswith(expected):
|
|
1082
|
+
raise AdapterFailure(
|
|
1083
|
+
f"{provider} {role} does not accept a {suffix or 'suffixless'} file",
|
|
1084
|
+
category="configuration",
|
|
1085
|
+
code="invalid_reference_type",
|
|
1086
|
+
)
|
|
1087
|
+
try:
|
|
1088
|
+
content = path.read_bytes()
|
|
1089
|
+
except OSError as exc:
|
|
1090
|
+
raise AdapterFailure(
|
|
1091
|
+
f"{provider} could not read a project reference",
|
|
1092
|
+
category="configuration",
|
|
1093
|
+
code="unreadable_reference",
|
|
1094
|
+
) from exc
|
|
1095
|
+
if not content:
|
|
1096
|
+
raise AdapterFailure(
|
|
1097
|
+
f"{provider} project reference is empty",
|
|
1098
|
+
category="configuration",
|
|
1099
|
+
code="empty_reference",
|
|
1100
|
+
)
|
|
1101
|
+
# The bytes have to be the media type the extension claims, or the
|
|
1102
|
+
# provider rejects a request this adapter said was well formed.
|
|
1103
|
+
_validate_media_content(path.name, content)
|
|
1104
|
+
limit = INLINE_REFERENCE_LIMITS[field]
|
|
1105
|
+
if len(content) > limit:
|
|
1106
|
+
raise AdapterFailure(
|
|
1107
|
+
f"{provider} reference exceeds the {limit // (1024 * 1024)}MB inline "
|
|
1108
|
+
"limit; host it and bind an HTTPS URL instead",
|
|
1109
|
+
category="configuration",
|
|
1110
|
+
code="reference_too_large",
|
|
1111
|
+
)
|
|
1112
|
+
encoded = base64.b64encode(content).decode("ascii")
|
|
1113
|
+
total += len(encoded)
|
|
1114
|
+
if total > INLINE_REFERENCE_BODY_LIMIT:
|
|
1115
|
+
raise AdapterFailure(
|
|
1116
|
+
f"{provider} inline references exceed the request body limit; "
|
|
1117
|
+
"host the largest ones and bind HTTPS URLs instead",
|
|
1118
|
+
category="configuration",
|
|
1119
|
+
code="reference_body_too_large",
|
|
1120
|
+
)
|
|
1121
|
+
urls.append(f"data:{mime};base64,{encoded}")
|
|
1122
|
+
return urls
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def _is_inline_reference(url: str) -> bool:
|
|
1126
|
+
return bool(re.fullmatch(r"data:[\w.+-]+/[\w.+-]+;base64,[A-Za-z0-9+/]+=*", url))
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _validate_media_content(target: str, content: bytes) -> None:
|
|
1130
|
+
suffix = Path(target).suffix.casefold()
|
|
1131
|
+
signatures = {
|
|
1132
|
+
".png": content.startswith(b"\x89PNG\r\n\x1a\n"),
|
|
1133
|
+
".jpg": content.startswith(b"\xff\xd8\xff"),
|
|
1134
|
+
".jpeg": content.startswith(b"\xff\xd8\xff"),
|
|
1135
|
+
".webp": len(content) >= 12
|
|
1136
|
+
and content.startswith(b"RIFF")
|
|
1137
|
+
and content[8:12] == b"WEBP",
|
|
1138
|
+
".mp3": content.startswith(b"ID3")
|
|
1139
|
+
or (
|
|
1140
|
+
len(content) >= 2
|
|
1141
|
+
and content[0] == 0xFF
|
|
1142
|
+
and content[1] & 0xE0 == 0xE0
|
|
1143
|
+
),
|
|
1144
|
+
".wav": len(content) >= 12
|
|
1145
|
+
and content.startswith(b"RIFF")
|
|
1146
|
+
and content[8:12] == b"WAVE",
|
|
1147
|
+
".mp4": len(content) >= 12 and content[4:8] == b"ftyp",
|
|
1148
|
+
}
|
|
1149
|
+
if not signatures.get(suffix, False):
|
|
1150
|
+
raise AdapterFailure("provider output does not match the target media type")
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def _output_root(job: Mapping[str, Any]) -> Path:
|
|
1154
|
+
raw = job.get("output_root")
|
|
1155
|
+
if not isinstance(raw, str):
|
|
1156
|
+
raise ValueError("output_root is invalid")
|
|
1157
|
+
root = Path(raw)
|
|
1158
|
+
if not root.is_absolute():
|
|
1159
|
+
raise ValueError("output_root is invalid")
|
|
1160
|
+
try:
|
|
1161
|
+
details = root.lstat()
|
|
1162
|
+
except OSError as exc:
|
|
1163
|
+
raise ValueError("output_root is missing") from exc
|
|
1164
|
+
if stat.S_ISLNK(details.st_mode) or not stat.S_ISDIR(details.st_mode):
|
|
1165
|
+
raise ValueError("output_root is unsafe")
|
|
1166
|
+
return root
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def _temporary_output(
|
|
1170
|
+
job: Mapping[str, Any], target: str, content: bytes
|
|
1171
|
+
) -> Path:
|
|
1172
|
+
if not content or len(content) > MAX_OUTPUT_BYTES:
|
|
1173
|
+
raise AdapterFailure("provider output is too large")
|
|
1174
|
+
_validate_media_content(target, content)
|
|
1175
|
+
path = _output_root(job) / ("result" + Path(target).suffix.casefold())
|
|
1176
|
+
with path.open("xb") as handle:
|
|
1177
|
+
handle.write(content)
|
|
1178
|
+
return path
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def _download(
|
|
1182
|
+
job: Mapping[str, Any],
|
|
1183
|
+
url: str,
|
|
1184
|
+
target: str,
|
|
1185
|
+
*,
|
|
1186
|
+
provider: str = "seedance",
|
|
1187
|
+
) -> Path:
|
|
1188
|
+
parsed = urllib.parse.urlparse(url)
|
|
1189
|
+
if parsed.scheme != "https" or not parsed.netloc:
|
|
1190
|
+
raise AdapterFailure("provider output URL is invalid")
|
|
1191
|
+
path = _output_root(job) / ("result" + Path(target).suffix.casefold())
|
|
1192
|
+
size = 0
|
|
1193
|
+
try:
|
|
1194
|
+
with urllib.request.urlopen(url, timeout=180) as response, path.open("xb") as handle:
|
|
1195
|
+
while chunk := response.read(1024 * 1024):
|
|
1196
|
+
size += len(chunk)
|
|
1197
|
+
if size > MAX_OUTPUT_BYTES:
|
|
1198
|
+
raise AdapterFailure("provider output is too large")
|
|
1199
|
+
handle.write(chunk)
|
|
1200
|
+
except AdapterFailure:
|
|
1201
|
+
path.unlink(missing_ok=True)
|
|
1202
|
+
raise
|
|
1203
|
+
except urllib.error.HTTPError as exc:
|
|
1204
|
+
path.unlink(missing_ok=True)
|
|
1205
|
+
raise _http_failure(provider, exc) from exc
|
|
1206
|
+
except TimeoutError as exc:
|
|
1207
|
+
path.unlink(missing_ok=True)
|
|
1208
|
+
raise AdapterFailure(
|
|
1209
|
+
"provider output download timed out",
|
|
1210
|
+
category="timeout",
|
|
1211
|
+
code="download_timeout",
|
|
1212
|
+
retryable=True,
|
|
1213
|
+
) from exc
|
|
1214
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
1215
|
+
path.unlink(missing_ok=True)
|
|
1216
|
+
raise AdapterFailure(
|
|
1217
|
+
"provider output download failed",
|
|
1218
|
+
category="network",
|
|
1219
|
+
code="download_failed",
|
|
1220
|
+
retryable=True,
|
|
1221
|
+
) from exc
|
|
1222
|
+
if size == 0:
|
|
1223
|
+
raise AdapterFailure("provider output is empty")
|
|
1224
|
+
with path.open("rb") as handle:
|
|
1225
|
+
_validate_media_content(target, handle.read(16))
|
|
1226
|
+
return path
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def _record_handle(job: Mapping[str, Any], provider_job_id: str) -> None:
|
|
1230
|
+
"""Write the provider task id where the caller can find it after a crash.
|
|
1231
|
+
|
|
1232
|
+
A video task is billed at submission. Everything after that — polling,
|
|
1233
|
+
downloading — can be interrupted, and without this the caller is left with a
|
|
1234
|
+
live, already-paid task it has no id for. Written before the first poll, and
|
|
1235
|
+
deliberately best-effort: failing to record the handle must not fail a task
|
|
1236
|
+
that was submitted successfully.
|
|
1237
|
+
"""
|
|
1238
|
+
|
|
1239
|
+
destination = job.get("handle_path")
|
|
1240
|
+
if not isinstance(destination, str) or not destination:
|
|
1241
|
+
return
|
|
1242
|
+
try:
|
|
1243
|
+
path = Path(destination)
|
|
1244
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1245
|
+
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
1246
|
+
temporary.write_text(
|
|
1247
|
+
json.dumps({"provider_job_id": provider_job_id}, ensure_ascii=True),
|
|
1248
|
+
encoding="utf-8",
|
|
1249
|
+
)
|
|
1250
|
+
temporary.replace(path)
|
|
1251
|
+
except OSError:
|
|
1252
|
+
return
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _collect_target(job: Mapping[str, Any]) -> str | None:
|
|
1256
|
+
value = job.get("collect_provider_job_id")
|
|
1257
|
+
if isinstance(value, str) and value:
|
|
1258
|
+
return value
|
|
1259
|
+
return None
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def _run_seedance(job: Mapping[str, Any]) -> tuple[Path, str]:
|
|
1263
|
+
token = _credential("ARK_API_KEY")
|
|
1264
|
+
base = _base_url("SEEDANCE_BASE_URL", SEEDANCE_BASE_URL)
|
|
1265
|
+
collecting = _collect_target(job)
|
|
1266
|
+
if collecting is not None:
|
|
1267
|
+
return _poll_seedance(job, base=base, token=token, task_id=collecting)
|
|
1268
|
+
model = os.environ.get("SEEDANCE_MODEL", "")
|
|
1269
|
+
references = _reference_paths(job)
|
|
1270
|
+
reference_roles = (
|
|
1271
|
+
_binding_roles(job, allowed=SEEDANCE_REFERENCE_ROLES, provider="Seedance")
|
|
1272
|
+
if references
|
|
1273
|
+
else []
|
|
1274
|
+
)
|
|
1275
|
+
reference_urls = _inline_reference_urls(
|
|
1276
|
+
references, reference_roles, allowed=SEEDANCE_REFERENCE_ROLES, provider="Seedance"
|
|
1277
|
+
)
|
|
1278
|
+
allowed_ratios, duration_range = _seedance_runtime_profile()
|
|
1279
|
+
body = compile_seedance_payload(
|
|
1280
|
+
job,
|
|
1281
|
+
model=model,
|
|
1282
|
+
reference_urls=reference_urls,
|
|
1283
|
+
reference_roles=reference_roles,
|
|
1284
|
+
allowed_ratios=allowed_ratios,
|
|
1285
|
+
duration_range=duration_range,
|
|
1286
|
+
)
|
|
1287
|
+
created, _ = _request_json(
|
|
1288
|
+
f"{base}/contents/generations/tasks",
|
|
1289
|
+
provider="seedance",
|
|
1290
|
+
body=body,
|
|
1291
|
+
token=token,
|
|
1292
|
+
)
|
|
1293
|
+
task_id = created.get("id")
|
|
1294
|
+
if not isinstance(task_id, str) or not task_id:
|
|
1295
|
+
raise AdapterFailure(
|
|
1296
|
+
"Seedance did not return a task id", code="missing_task_id"
|
|
1297
|
+
)
|
|
1298
|
+
# Billed from here on. Record the id before the first poll.
|
|
1299
|
+
_record_handle(job, task_id)
|
|
1300
|
+
return _poll_seedance(job, base=base, token=token, task_id=task_id)
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
def _poll_seedance(
|
|
1304
|
+
job: Mapping[str, Any], *, base: str, token: str, task_id: str
|
|
1305
|
+
) -> tuple[Path, str]:
|
|
1306
|
+
try:
|
|
1307
|
+
interval = float(os.environ.get("SEEDANCE_POLL_INTERVAL", "5"))
|
|
1308
|
+
deadline = time.monotonic() + float(os.environ.get("SEEDANCE_TIMEOUT_SECONDS", "1800"))
|
|
1309
|
+
except ValueError as exc:
|
|
1310
|
+
raise AdapterFailure("Seedance polling configuration is invalid") from exc
|
|
1311
|
+
if interval <= 0 or deadline <= time.monotonic():
|
|
1312
|
+
raise AdapterFailure("Seedance polling configuration is invalid")
|
|
1313
|
+
while time.monotonic() < deadline:
|
|
1314
|
+
status_doc, _ = _request_json(
|
|
1315
|
+
f"{base}/contents/generations/tasks/{urllib.parse.quote(task_id, safe='')}",
|
|
1316
|
+
provider="seedance",
|
|
1317
|
+
method="GET",
|
|
1318
|
+
token=token,
|
|
1319
|
+
)
|
|
1320
|
+
status = status_doc.get("status")
|
|
1321
|
+
if status == "succeeded":
|
|
1322
|
+
content = status_doc.get("content")
|
|
1323
|
+
url = content.get("video_url") if isinstance(content, Mapping) else None
|
|
1324
|
+
if not isinstance(url, str):
|
|
1325
|
+
raise AdapterFailure(
|
|
1326
|
+
"Seedance succeeded without a video URL",
|
|
1327
|
+
code="missing_video_url",
|
|
1328
|
+
request_id=task_id,
|
|
1329
|
+
)
|
|
1330
|
+
return _download(
|
|
1331
|
+
job,
|
|
1332
|
+
url,
|
|
1333
|
+
job["outputs"][0],
|
|
1334
|
+
provider="seedance",
|
|
1335
|
+
), task_id
|
|
1336
|
+
if isinstance(status, str) and status.casefold() in TERMINAL_FAILURES:
|
|
1337
|
+
raise AdapterFailure(
|
|
1338
|
+
"Seedance task failed",
|
|
1339
|
+
code="task_" + status.casefold(),
|
|
1340
|
+
request_id=task_id,
|
|
1341
|
+
)
|
|
1342
|
+
if status not in {"queued", "in_progress", "running", "processing", "pending"}:
|
|
1343
|
+
raise AdapterFailure(
|
|
1344
|
+
"Seedance returned an unknown task status",
|
|
1345
|
+
code="unknown_task_status",
|
|
1346
|
+
request_id=task_id,
|
|
1347
|
+
)
|
|
1348
|
+
time.sleep(interval)
|
|
1349
|
+
raise AdapterFailure(
|
|
1350
|
+
"Seedance task polling timed out",
|
|
1351
|
+
category="timeout",
|
|
1352
|
+
code="task_poll_timeout",
|
|
1353
|
+
request_id=task_id,
|
|
1354
|
+
retryable=True,
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
def _run_openai(job: Mapping[str, Any]) -> tuple[Path, str | None]:
|
|
1359
|
+
token = _credential("OPENAI_API_KEY")
|
|
1360
|
+
body = compile_gpt_image_2_payload(job)
|
|
1361
|
+
base = _base_url("OPENAI_BASE_URL", OPENAI_BASE_URL)
|
|
1362
|
+
references = _reference_paths(job)
|
|
1363
|
+
if references:
|
|
1364
|
+
encoded, content_type = _multipart(body, references)
|
|
1365
|
+
request = urllib.request.Request(f"{base}/images/edits", data=encoded, method="POST")
|
|
1366
|
+
request.add_header("Authorization", f"Bearer {token}")
|
|
1367
|
+
request.add_header("Content-Type", content_type)
|
|
1368
|
+
try:
|
|
1369
|
+
with urllib.request.urlopen(request, timeout=300) as response:
|
|
1370
|
+
raw = response.read(MAX_JSON_RESPONSE + 1)
|
|
1371
|
+
request_id = response.headers.get("x-request-id")
|
|
1372
|
+
except urllib.error.HTTPError as exc:
|
|
1373
|
+
raise _http_failure("gpt-image-2", exc) from exc
|
|
1374
|
+
except TimeoutError as exc:
|
|
1375
|
+
raise AdapterFailure(
|
|
1376
|
+
"OpenAI HTTP request timed out",
|
|
1377
|
+
category="timeout",
|
|
1378
|
+
code="request_timeout",
|
|
1379
|
+
retryable=True,
|
|
1380
|
+
) from exc
|
|
1381
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
1382
|
+
raise AdapterFailure(
|
|
1383
|
+
"OpenAI HTTP request failed",
|
|
1384
|
+
category="network",
|
|
1385
|
+
code="network_error",
|
|
1386
|
+
retryable=True,
|
|
1387
|
+
) from exc
|
|
1388
|
+
if len(raw) > MAX_JSON_RESPONSE:
|
|
1389
|
+
raise AdapterFailure(
|
|
1390
|
+
"OpenAI response is too large",
|
|
1391
|
+
code="response_too_large",
|
|
1392
|
+
request_id=request_id,
|
|
1393
|
+
)
|
|
1394
|
+
try:
|
|
1395
|
+
result = json.loads(raw)
|
|
1396
|
+
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
1397
|
+
raise AdapterFailure(
|
|
1398
|
+
"OpenAI returned invalid JSON",
|
|
1399
|
+
code="invalid_json",
|
|
1400
|
+
request_id=request_id,
|
|
1401
|
+
) from exc
|
|
1402
|
+
else:
|
|
1403
|
+
result, headers = _request_json(
|
|
1404
|
+
f"{base}/images/generations",
|
|
1405
|
+
provider="gpt-image-2",
|
|
1406
|
+
body=body,
|
|
1407
|
+
token=token,
|
|
1408
|
+
)
|
|
1409
|
+
request_id = headers.get("x-request-id")
|
|
1410
|
+
data = result.get("data") if isinstance(result, Mapping) else None
|
|
1411
|
+
image = data[0].get("b64_json") if isinstance(data, list) and len(data) == 1 and isinstance(data[0], Mapping) else None
|
|
1412
|
+
if not isinstance(image, str):
|
|
1413
|
+
raise AdapterFailure(
|
|
1414
|
+
"OpenAI did not return exactly one image",
|
|
1415
|
+
code="missing_image",
|
|
1416
|
+
request_id=request_id,
|
|
1417
|
+
)
|
|
1418
|
+
try:
|
|
1419
|
+
content = base64.b64decode(image, validate=True)
|
|
1420
|
+
except (ValueError, binascii.Error) as exc:
|
|
1421
|
+
raise AdapterFailure(
|
|
1422
|
+
"OpenAI returned invalid image data",
|
|
1423
|
+
code="invalid_image_data",
|
|
1424
|
+
request_id=request_id,
|
|
1425
|
+
) from exc
|
|
1426
|
+
return _temporary_output(job, job["outputs"][0], content), request_id
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def _run_minimax(job: Mapping[str, Any]) -> tuple[Path, str | None]:
|
|
1430
|
+
token = _credential("MINIMAX_API_KEY")
|
|
1431
|
+
body = compile_minimax_music_payload(job)
|
|
1432
|
+
base = _base_url("MINIMAX_BASE_URL", MINIMAX_BASE_URL)
|
|
1433
|
+
result, _ = _request_json(
|
|
1434
|
+
f"{base}/music_generation",
|
|
1435
|
+
provider="minimax-music",
|
|
1436
|
+
body=body,
|
|
1437
|
+
token=token,
|
|
1438
|
+
)
|
|
1439
|
+
base_resp = result.get("base_resp")
|
|
1440
|
+
if not isinstance(base_resp, Mapping) or base_resp.get("status_code") != 0:
|
|
1441
|
+
raise AdapterFailure(
|
|
1442
|
+
"MiniMax music generation failed",
|
|
1443
|
+
code=_provider_code(result) or "generation_failed",
|
|
1444
|
+
request_id=_safe_token(result.get("trace_id")),
|
|
1445
|
+
)
|
|
1446
|
+
data = result.get("data")
|
|
1447
|
+
audio = data.get("audio") if isinstance(data, Mapping) else None
|
|
1448
|
+
if not isinstance(data, Mapping) or data.get("status") != 2 or not isinstance(audio, str):
|
|
1449
|
+
raise AdapterFailure(
|
|
1450
|
+
"MiniMax did not return audio data",
|
|
1451
|
+
code="missing_audio",
|
|
1452
|
+
request_id=_safe_token(result.get("trace_id")),
|
|
1453
|
+
)
|
|
1454
|
+
try:
|
|
1455
|
+
content = bytes.fromhex(audio)
|
|
1456
|
+
except ValueError as exc:
|
|
1457
|
+
raise AdapterFailure("MiniMax returned invalid audio data") from exc
|
|
1458
|
+
if not content:
|
|
1459
|
+
raise AdapterFailure("MiniMax returned empty audio data")
|
|
1460
|
+
trace_id = result.get("trace_id")
|
|
1461
|
+
return (
|
|
1462
|
+
_temporary_output(job, job["outputs"][0], content),
|
|
1463
|
+
trace_id if isinstance(trace_id, str) else None,
|
|
1464
|
+
)
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _run_minimax_speech(job: Mapping[str, Any]) -> tuple[Path, str | None]:
|
|
1468
|
+
token = _credential("MINIMAX_API_KEY")
|
|
1469
|
+
body = compile_minimax_speech_payload(job)
|
|
1470
|
+
base = _base_url("MINIMAX_BASE_URL", MINIMAX_BASE_URL)
|
|
1471
|
+
result, _ = _request_json(
|
|
1472
|
+
f"{base}/t2a_v2",
|
|
1473
|
+
provider="minimax-speech",
|
|
1474
|
+
body=body,
|
|
1475
|
+
token=token,
|
|
1476
|
+
)
|
|
1477
|
+
base_resp = result.get("base_resp")
|
|
1478
|
+
if not isinstance(base_resp, Mapping) or base_resp.get("status_code") != 0:
|
|
1479
|
+
raise AdapterFailure(
|
|
1480
|
+
"MiniMax speech synthesis failed",
|
|
1481
|
+
code=_provider_code(result) or "generation_failed",
|
|
1482
|
+
request_id=_safe_token(result.get("trace_id")),
|
|
1483
|
+
)
|
|
1484
|
+
data = result.get("data")
|
|
1485
|
+
audio = data.get("audio") if isinstance(data, Mapping) else None
|
|
1486
|
+
if not isinstance(audio, str) or not audio:
|
|
1487
|
+
raise AdapterFailure(
|
|
1488
|
+
"MiniMax did not return audio data",
|
|
1489
|
+
code="missing_audio",
|
|
1490
|
+
request_id=_safe_token(result.get("trace_id")),
|
|
1491
|
+
)
|
|
1492
|
+
try:
|
|
1493
|
+
content = bytes.fromhex(audio)
|
|
1494
|
+
except ValueError as exc:
|
|
1495
|
+
raise AdapterFailure("MiniMax returned invalid audio data") from exc
|
|
1496
|
+
if not content:
|
|
1497
|
+
raise AdapterFailure("MiniMax returned empty audio data")
|
|
1498
|
+
trace_id = result.get("trace_id")
|
|
1499
|
+
return (
|
|
1500
|
+
_temporary_output(job, job["outputs"][0], content),
|
|
1501
|
+
trace_id if isinstance(trace_id, str) else None,
|
|
1502
|
+
)
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
def _run_minimax_video(job: Mapping[str, Any]) -> tuple[Path, str]:
|
|
1506
|
+
token = _credential("MINIMAX_API_KEY")
|
|
1507
|
+
base = _base_url("MINIMAX_VIDEO_BASE_URL", MINIMAX_VIDEO_BASE_URL)
|
|
1508
|
+
collecting = _collect_target(job)
|
|
1509
|
+
if collecting is not None:
|
|
1510
|
+
return _poll_minimax_video(job, base=base, token=token, task_id=collecting)
|
|
1511
|
+
model = os.environ.get("MINIMAX_VIDEO_MODEL", "")
|
|
1512
|
+
references = _reference_paths(job)
|
|
1513
|
+
reference_roles = (
|
|
1514
|
+
_binding_roles(job, allowed=MINIMAX_VIDEO_ROLES, provider="MiniMax")
|
|
1515
|
+
if references
|
|
1516
|
+
else []
|
|
1517
|
+
)
|
|
1518
|
+
reference_urls = _inline_reference_urls(
|
|
1519
|
+
references, reference_roles, allowed=MINIMAX_VIDEO_ROLES, provider="MiniMax"
|
|
1520
|
+
)
|
|
1521
|
+
ratios, resolutions, duration_range = _minimax_video_runtime_profile()
|
|
1522
|
+
body = compile_minimax_h3_payload(
|
|
1523
|
+
job,
|
|
1524
|
+
model=model,
|
|
1525
|
+
reference_urls=reference_urls,
|
|
1526
|
+
reference_roles=reference_roles,
|
|
1527
|
+
allowed_ratios=ratios,
|
|
1528
|
+
allowed_resolutions=resolutions,
|
|
1529
|
+
duration_range=duration_range,
|
|
1530
|
+
)
|
|
1531
|
+
created, _ = _request_json(
|
|
1532
|
+
f"{base}/video_generation",
|
|
1533
|
+
provider="minimax-h3",
|
|
1534
|
+
body=body,
|
|
1535
|
+
token=token,
|
|
1536
|
+
)
|
|
1537
|
+
task_id = created.get("task_id")
|
|
1538
|
+
if not isinstance(task_id, str) or not task_id:
|
|
1539
|
+
raise AdapterFailure(
|
|
1540
|
+
"MiniMax did not return a task id",
|
|
1541
|
+
code=_provider_code(created) or "missing_task_id",
|
|
1542
|
+
)
|
|
1543
|
+
# Billed from here on. Record the id before the first poll.
|
|
1544
|
+
_record_handle(job, task_id)
|
|
1545
|
+
return _poll_minimax_video(job, base=base, token=token, task_id=task_id)
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
def _poll_minimax_video(
|
|
1549
|
+
job: Mapping[str, Any], *, base: str, token: str, task_id: str
|
|
1550
|
+
) -> tuple[Path, str]:
|
|
1551
|
+
try:
|
|
1552
|
+
interval = float(os.environ.get("MINIMAX_VIDEO_POLL_INTERVAL", "5"))
|
|
1553
|
+
deadline = time.monotonic() + float(
|
|
1554
|
+
os.environ.get("MINIMAX_VIDEO_TIMEOUT_SECONDS", "1800")
|
|
1555
|
+
)
|
|
1556
|
+
except ValueError as exc:
|
|
1557
|
+
raise AdapterFailure("MiniMax polling configuration is invalid") from exc
|
|
1558
|
+
if interval <= 0 or deadline <= time.monotonic():
|
|
1559
|
+
raise AdapterFailure("MiniMax polling configuration is invalid")
|
|
1560
|
+
while time.monotonic() < deadline:
|
|
1561
|
+
document, _ = _request_json(
|
|
1562
|
+
f"{base}/query/video_generation/"
|
|
1563
|
+
+ urllib.parse.quote(task_id, safe=""),
|
|
1564
|
+
provider="minimax-h3",
|
|
1565
|
+
method="GET",
|
|
1566
|
+
token=token,
|
|
1567
|
+
)
|
|
1568
|
+
task = document.get("task")
|
|
1569
|
+
status = task.get("status") if isinstance(task, Mapping) else None
|
|
1570
|
+
if status == "succeeded":
|
|
1571
|
+
content = task.get("content") if isinstance(task, Mapping) else None
|
|
1572
|
+
url = content.get("url") if isinstance(content, Mapping) else None
|
|
1573
|
+
if not isinstance(url, str):
|
|
1574
|
+
raise AdapterFailure(
|
|
1575
|
+
"MiniMax succeeded without a video URL",
|
|
1576
|
+
code="missing_video_url",
|
|
1577
|
+
request_id=task_id,
|
|
1578
|
+
)
|
|
1579
|
+
return _download(
|
|
1580
|
+
job, url, job["outputs"][0], provider="minimax-h3"
|
|
1581
|
+
), task_id
|
|
1582
|
+
if isinstance(status, str) and status.casefold() in TERMINAL_FAILURES:
|
|
1583
|
+
raise AdapterFailure(
|
|
1584
|
+
"MiniMax video task failed",
|
|
1585
|
+
code="task_" + status.casefold(),
|
|
1586
|
+
request_id=task_id,
|
|
1587
|
+
)
|
|
1588
|
+
if status not in {"queued", "running", "processing", "pending", "in_progress"}:
|
|
1589
|
+
raise AdapterFailure(
|
|
1590
|
+
"MiniMax returned an unknown task status",
|
|
1591
|
+
code="unknown_task_status",
|
|
1592
|
+
request_id=task_id,
|
|
1593
|
+
)
|
|
1594
|
+
time.sleep(interval)
|
|
1595
|
+
raise AdapterFailure(
|
|
1596
|
+
"MiniMax video task polling timed out",
|
|
1597
|
+
category="timeout",
|
|
1598
|
+
code="task_poll_timeout",
|
|
1599
|
+
request_id=task_id,
|
|
1600
|
+
retryable=True,
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
|
|
1604
|
+
def _selftest() -> None:
|
|
1605
|
+
image = {
|
|
1606
|
+
"modality": "image", "prompt": "portrait", "references": [],
|
|
1607
|
+
"outputs": ["制作成果/a.png"], "parameters": {"width": 1024, "height": 1536},
|
|
1608
|
+
}
|
|
1609
|
+
video = {
|
|
1610
|
+
"modality": "video", "prompt": "slow push in", "references": [],
|
|
1611
|
+
"outputs": ["制作成果/a.mp4"], "parameters": {"duration": 5, "ratio": "9:16"},
|
|
1612
|
+
}
|
|
1613
|
+
music = {
|
|
1614
|
+
"modality": "music", "prompt": "cinematic", "references": [],
|
|
1615
|
+
"outputs": ["制作成果/a.mp3"], "parameters": {"lyrics": "[Verse]\nHello"},
|
|
1616
|
+
}
|
|
1617
|
+
speech = {
|
|
1618
|
+
"modality": "tts", "prompt": "你们做了多久?", "references": [],
|
|
1619
|
+
"outputs": ["制作成果/a.mp3"],
|
|
1620
|
+
"parameters": {"model": "configured-speech-model", "voice_id": "a-preset"},
|
|
1621
|
+
}
|
|
1622
|
+
if compile_gpt_image_2_payload(image)["model"] != OPENAI_MODEL:
|
|
1623
|
+
raise RuntimeError("GPT Image 2 self-test failed")
|
|
1624
|
+
if compile_minimax_speech_payload(speech)["voice_setting"]["voice_id"] != "a-preset":
|
|
1625
|
+
raise RuntimeError("MiniMax speech self-test failed")
|
|
1626
|
+
seedance = compile_seedance_payload(
|
|
1627
|
+
video,
|
|
1628
|
+
model="configured-model",
|
|
1629
|
+
allowed_ratios={"9:16"},
|
|
1630
|
+
duration_range=(5, 10),
|
|
1631
|
+
)
|
|
1632
|
+
if seedance["model"] != "configured-model":
|
|
1633
|
+
raise RuntimeError("Seedance model self-test failed")
|
|
1634
|
+
if seedance.get("ratio") != "9:16" or seedance.get("duration") != 5:
|
|
1635
|
+
raise RuntimeError("Seedance parameter self-test failed")
|
|
1636
|
+
minimax_video = compile_minimax_h3_payload(
|
|
1637
|
+
{**video, "parameters": {"duration": 6, "ratio": "9:16", "resolution": "768P"}},
|
|
1638
|
+
model="configured-video-model",
|
|
1639
|
+
allowed_ratios={"9:16"},
|
|
1640
|
+
allowed_resolutions={"768P"},
|
|
1641
|
+
duration_range=(4, 15),
|
|
1642
|
+
)
|
|
1643
|
+
if minimax_video["model"] != "configured-video-model":
|
|
1644
|
+
raise RuntimeError("MiniMax video model self-test failed")
|
|
1645
|
+
if minimax_video["duration"] != 6 or minimax_video["resolution"] != "768P":
|
|
1646
|
+
raise RuntimeError("MiniMax video parameter self-test failed")
|
|
1647
|
+
try:
|
|
1648
|
+
compile_minimax_h3_payload(
|
|
1649
|
+
{**video, "parameters": {"duration": 6, "resolution": "768P"}},
|
|
1650
|
+
model="configured-video-model",
|
|
1651
|
+
allowed_resolutions={"768P"},
|
|
1652
|
+
duration_range=(4, 15),
|
|
1653
|
+
)
|
|
1654
|
+
except ValueError:
|
|
1655
|
+
pass
|
|
1656
|
+
else:
|
|
1657
|
+
raise AssertionError("MiniMax text-to-video without a ratio was accepted")
|
|
1658
|
+
compiled_music = compile_minimax_music_payload(music)
|
|
1659
|
+
if compiled_music["model"] != MINIMAX_MUSIC_MODEL:
|
|
1660
|
+
raise RuntimeError("MiniMax model self-test failed")
|
|
1661
|
+
if compiled_music["output_format"] != "hex":
|
|
1662
|
+
raise RuntimeError("MiniMax output self-test failed")
|
|
1663
|
+
for invalid_image in (
|
|
1664
|
+
{**image, "parameters": {"background": "transparent"}},
|
|
1665
|
+
{**image, "parameters": {"input_fidelity": "high"}},
|
|
1666
|
+
{**image, "parameters": {"size": "1023x1024"}},
|
|
1667
|
+
):
|
|
1668
|
+
try:
|
|
1669
|
+
compile_gpt_image_2_payload(invalid_image)
|
|
1670
|
+
except ValueError:
|
|
1671
|
+
pass
|
|
1672
|
+
else:
|
|
1673
|
+
raise AssertionError("invalid GPT Image payload was accepted")
|
|
1674
|
+
invalid_reference = {**video, "references": ["clip.mp4"]}
|
|
1675
|
+
try:
|
|
1676
|
+
compile_seedance_payload(
|
|
1677
|
+
invalid_reference,
|
|
1678
|
+
model="configured-model",
|
|
1679
|
+
reference_urls=["asset://asset-example-clip"],
|
|
1680
|
+
reference_roles=["reference_image"],
|
|
1681
|
+
)
|
|
1682
|
+
except ValueError:
|
|
1683
|
+
pass
|
|
1684
|
+
else:
|
|
1685
|
+
raise AssertionError("mismatched Seedance reference role was accepted")
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
def main() -> int:
|
|
1689
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
1690
|
+
parser.add_argument(
|
|
1691
|
+
"provider",
|
|
1692
|
+
nargs="?",
|
|
1693
|
+
choices=(
|
|
1694
|
+
"seedance", "gpt-image-2", "minimax-music", "minimax-h3",
|
|
1695
|
+
"minimax-speech",
|
|
1696
|
+
),
|
|
1697
|
+
)
|
|
1698
|
+
parser.add_argument("--selftest", action="store_true")
|
|
1699
|
+
args = parser.parse_args()
|
|
1700
|
+
if args.selftest:
|
|
1701
|
+
_selftest()
|
|
1702
|
+
return 0
|
|
1703
|
+
if args.provider is None:
|
|
1704
|
+
parser.error("provider is required unless --selftest is used")
|
|
1705
|
+
try:
|
|
1706
|
+
job = json.load(sys.stdin.buffer)
|
|
1707
|
+
if not isinstance(job, Mapping):
|
|
1708
|
+
raise ValueError("adapter input must be an object")
|
|
1709
|
+
runners = {
|
|
1710
|
+
"seedance": _run_seedance,
|
|
1711
|
+
"gpt-image-2": _run_openai,
|
|
1712
|
+
"minimax-music": _run_minimax,
|
|
1713
|
+
"minimax-h3": _run_minimax_video,
|
|
1714
|
+
"minimax-speech": _run_minimax_speech,
|
|
1715
|
+
}
|
|
1716
|
+
path, provider_job_id = runners[args.provider](job)
|
|
1717
|
+
response: dict[str, Any] = {
|
|
1718
|
+
"outputs": [{"target": job["outputs"][0], "source": str(path)}]
|
|
1719
|
+
}
|
|
1720
|
+
if provider_job_id:
|
|
1721
|
+
response["provider_job_id"] = provider_job_id
|
|
1722
|
+
json.dump(response, sys.stdout, ensure_ascii=True)
|
|
1723
|
+
return 0
|
|
1724
|
+
except AdapterFailure as exc:
|
|
1725
|
+
# Provider bodies and credentials are intentionally never reflected.
|
|
1726
|
+
json.dump({"error": exc.public(args.provider)}, sys.stdout, ensure_ascii=True)
|
|
1727
|
+
print("provider adapter failed safely", file=sys.stderr)
|
|
1728
|
+
return 1
|
|
1729
|
+
except (ValueError, KeyError, TypeError, json.JSONDecodeError):
|
|
1730
|
+
json.dump(
|
|
1731
|
+
{
|
|
1732
|
+
"error": {
|
|
1733
|
+
"provider": args.provider,
|
|
1734
|
+
"category": "invalid_request",
|
|
1735
|
+
"code": "invalid_job",
|
|
1736
|
+
"retryable": False,
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
sys.stdout,
|
|
1740
|
+
ensure_ascii=True,
|
|
1741
|
+
)
|
|
1742
|
+
print("provider adapter failed safely", file=sys.stderr)
|
|
1743
|
+
return 1
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
if __name__ == "__main__":
|
|
1747
|
+
raise SystemExit(main())
|