diff --git a/README.md b/README.md index 13f9c7e..225b34a 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,12 @@ These descriptions are based on the current public Kie.ai model pages and live p - What it does: creates videos from text or images, supports 3 to 15 second generations, std/pro/4K generation modes, multi-shot prompting, stronger scene consistency, and native audio generation - Who it is by: Kling AI on Kie.ai +### Kling 3.0 Turbo Image to Video + +- Good at: faster, lower-cost image-to-video generation when 720p or 1080p output is enough +- What it does: animates one or more source images using `kling/v3-turbo-image-to-video`, 3 to 15 second duration, and 720p/1080p resolution controls +- Who it is by: Kling AI on Kie.ai + ### Kling 3.0 Motion Control - Good at: transferring motion or performance from a reference video onto a subject or scene defined by an image @@ -165,7 +171,7 @@ Use one model’s output as another model’s input: ### 5. Multimodal reference video -Use `seedance-2.0` or `seedance-2.0-fast` when you need richer video guidance: +Use `seedance-2.0`, `seedance-2.0-fast`, or `seedance-2.0-mini` when you need richer video guidance: - plain text-to-video with no media - one first-frame image - first+last-frame image guidance @@ -173,6 +179,8 @@ Use `seedance-2.0` or `seedance-2.0-fast` when you need richer video guidance: `seedance-2.0-fast` uses the same request shape with the lower-cost `bytedance/seedance-2-fast` provider model and supports `480p` and `720p`. +`seedance-2.0-mini` uses the same request shape with the lower-cost +`bytedance/seedance-2-mini` provider model and supports `480p` and `720p`. Use `seedance-2.0` when `1080p` output is required. Seedance uses role-aware media references in the runtime request model: diff --git a/docs/LLM_START_HERE.md b/docs/LLM_START_HERE.md index 5fa00a9..eadba92 100644 --- a/docs/LLM_START_HERE.md +++ b/docs/LLM_START_HERE.md @@ -101,6 +101,15 @@ Use: This proves your API key is loaded and the system can talk to Kie.ai. +Seedance 2.0 variants currently share the same multimodal request shape: +- `seedance-2.0` -> `bytedance/seedance-2`, supports `480p`, `720p`, and `1080p` +- `seedance-2.0-fast` -> `bytedance/seedance-2-fast`, supports `480p` and `720p` +- `seedance-2.0-mini` -> `bytedance/seedance-2-mini`, supports `480p` and `720p` + +Kling 3.0 Turbo Image to Video is separate from base Kling 3.0: +- `kling-3.0-turbo-i2v` -> `kling/v3-turbo-image-to-video`, supports `720p` and `1080p` +- It uses `prompt`, `image_urls`, `duration`, and `resolution`; it does not use base Kling 3.0 `mode`, `sound`, or `multi_shots` options. + ### Test 2: Nano Banana Prompt-Only Use: diff --git a/specs/models/kling_2_6_motion.yaml b/specs/models/kling_2_6_motion.yaml new file mode 100644 index 0000000..75f1148 --- /dev/null +++ b/specs/models/kling_2_6_motion.yaml @@ -0,0 +1,117 @@ +key: kling-2.6-motion +label: Kling 2.6 Motion Control +family: market +provider_model: kling-2.6/motion-control +task_modes: + - motion_control +inputs: + image: + required_min: 1 + required_max: 1 + video: + required_min: 1 + required_max: 1 + audio: + required_min: 0 + required_max: 0 +input_constraints: + image_formats: [jpg, jpeg, png] + image_max_mb: 10 + image_min_dimension_px: 300 + image_aspect_ratio_min: 0.4 + image_aspect_ratio_max: 2.5 + video_formats: [mp4, mov, quicktime, mkv, matroska, x-matroska] + video_max_mb: 100 + video_duration_min_seconds: 3 + video_duration_max_seconds: 30 + video_min_dimension_px: 300 + video_aspect_ratio_min: 0.4 + video_aspect_ratio_max: 2.5 +options: + character_orientation: + type: enum + allowed: [image, video] + default: null + required: true + notes: "'image' keeps the image orientation and caps driving-video duration at 10s; 'video' follows the video orientation and allows up to 30s." + mode: + type: enum + allowed: ['720p', '1080p'] + default: null + required: true + value_aliases: + std: '720p' + pro: '1080p' + notes: "The live page expected fields list uses 720p and 1080p, while the help text still mentions std/pro naming." +prompt: + required: false + max_chars: 2500 + enhancement_supported: true + enhancement_default_policy: ask + default_profile_key: kling_2_6_motion_control_v1 +defaults: {} +transport: + endpoint_family: market + create_path: /api/v1/jobs/createTask + status_path: /api/v1/jobs/recordInfo + callback_supported: true +verification: + verified_from: + - live_page_api_surface + - docs_kie_primary + - site_pricing_page_api + verified_on: '2026-06-19' + verification_notes: + - Verified provider model string and request example from https://docs.kie.ai/market/kling/motion-control. + - The docs request shape uses prompt, input_urls, video_urls, mode, and character_orientation. + - The live https://kie.ai/kling-2.6-motion-control page expected fields list shows prompt, input_urls, video_urls, character_orientation, and mode. + - The live page marks input_urls, video_urls, character_orientation, and mode as required. + - The live page supports JPEG, PNG, and JPG reference images up to 10MB, greater than 300px, with aspect ratio 2:5 to 5:2. + - The live page supports MP4, QuickTime, and X-Matroska videos up to 100MB and one file; help text names .mp4 and .mov. + - The live page states character_orientation=image allows max 10s driving video and character_orientation=video allows max 30s. + - Site pricing rows observed 11 credits / $0.055 per second for 720p and 18 credits / $0.09 per second for 1080p. + field_provenance: + key: inferred + label: inferred + family: inferred + provider_model: verified_docs + task_modes: inferred + inputs.image.required_min: verified_live + inputs.image.required_max: verified_live + inputs.video.required_min: verified_live + inputs.video.required_max: verified_live + inputs.audio.required_min: inferred + inputs.audio.required_max: inferred + input_constraints.image_formats: verified_live + input_constraints.image_max_mb: verified_live + input_constraints.image_min_dimension_px: verified_live + input_constraints.image_aspect_ratio_min: verified_live + input_constraints.image_aspect_ratio_max: verified_live + input_constraints.video_formats: verified_live + input_constraints.video_max_mb: verified_live + input_constraints.video_duration_min_seconds: verified_live + input_constraints.video_duration_max_seconds: verified_live + input_constraints.video_min_dimension_px: verified_live + input_constraints.video_aspect_ratio_min: verified_live + input_constraints.video_aspect_ratio_max: verified_live + options.character_orientation.type: inferred + options.character_orientation.allowed: verified_live + options.character_orientation.default: unknown + options.character_orientation.required: verified_live + options.character_orientation.notes: verified_live + options.mode.type: inferred + options.mode.allowed: verified_live + options.mode.default: unknown + options.mode.required: verified_live + options.mode.value_aliases: inferred + options.mode.notes: verified_live + prompt.required: verified_live + prompt.max_chars: verified_live + prompt.enhancement_supported: inferred + prompt.enhancement_default_policy: inferred + prompt.default_profile_key: inferred + defaults: inferred + transport.endpoint_family: inferred + transport.create_path: verified_docs + transport.status_path: verified_docs + transport.callback_supported: verified_docs diff --git a/specs/models/kling_3_0_turbo_i2v.yaml b/specs/models/kling_3_0_turbo_i2v.yaml new file mode 100644 index 0000000..c1b18d9 --- /dev/null +++ b/specs/models/kling_3_0_turbo_i2v.yaml @@ -0,0 +1,96 @@ +key: kling-3.0-turbo-i2v +label: Kling 3.0 Turbo Image to Video +family: market +provider_model: kling/v3-turbo-image-to-video +task_modes: + - image_to_video +inputs: + image: + required_min: 1 + video: + required_min: 0 + required_max: 0 + audio: + required_min: 0 + required_max: 0 +input_constraints: + image_formats: [jpeg, jpg, png] + image_max_mb: 10 +options: + duration: + type: int_range + min: 3 + max: 15 + default: 5 + required: true + label: Duration + help_text: "Video length in seconds. Kling 3.0 Turbo Image to Video supports 3s through 15s generations." + ui_group: generation + ui_order: 10 + resolution: + type: enum + allowed: [720p, 1080p] + default: 720p + required: true +prompt: + required: true + max_chars: 2500 + enhancement_supported: true + enhancement_default_policy: ask + default_profile_key: kling_3_0_turbo_i2v_v1 + default_profile_keys_by_input_pattern: + single_image: kling_3_0_turbo_i2v_v1 + first_last_frames: kling_3_0_turbo_i2v_v1 +defaults: {} +transport: + endpoint_family: market + create_path: /api/v1/jobs/createTask + status_path: /api/v1/jobs/recordInfo + callback_supported: true +verification: + verified_from: + - user_supplied_docs + - live_page_api_surface + - site_pricing_api + verified_on: '2026-06-26' + verification_notes: + - Verified against the user-supplied V3 Turbo Image To Video API documentation and https://kie.ai/kling-3-0-turbo on 2026-06-26. + - The live page identifies two flavors, kling/v3-turbo-image-to-video and kling/v3-turbo-text-to-video; this spec intentionally onboards image-to-video only. + - The createTask payload uses provider model kling/v3-turbo-image-to-video with input.prompt, input.image_urls, input.duration, and input.resolution. + - The docs list image/jpeg and image/png inputs up to 10MB, duration 3 through 15 seconds, and resolutions 720p and 1080p. + - The docs say image_urls supports multiple files but do not publish a maximum image count, so the SDK enforces the required minimum only. + - KIE's public pricing API publishes separate Turbo image-to-video rows for 720P and 1080P per-second pricing. + field_provenance: + key: inferred + label: inferred + family: inferred + provider_model: verified_docs + task_modes: inferred + inputs.image.required_min: verified_docs + inputs.image.required_max: unknown + inputs.video.required_min: inferred + inputs.video.required_max: inferred + inputs.audio.required_min: inferred + inputs.audio.required_max: inferred + input_constraints.image_formats: verified_docs + input_constraints.image_max_mb: verified_docs + options.duration.type: inferred + options.duration.min: verified_docs + options.duration.max: verified_docs + options.duration.default: verified_docs + options.duration.required: verified_docs + options.resolution.type: inferred + options.resolution.allowed: verified_docs + options.resolution.default: verified_docs + options.resolution.required: verified_docs + prompt.required: verified_docs + prompt.max_chars: verified_docs + prompt.enhancement_supported: inferred + prompt.enhancement_default_policy: inferred + prompt.default_profile_key: inferred + prompt.default_profile_keys_by_input_pattern: inferred + defaults: inferred + transport.endpoint_family: inferred + transport.create_path: verified_docs + transport.status_path: verified_docs + transport.callback_supported: verified_docs diff --git a/specs/models/seedance_2_0_mini.yaml b/specs/models/seedance_2_0_mini.yaml new file mode 100644 index 0000000..164fdae --- /dev/null +++ b/specs/models/seedance_2_0_mini.yaml @@ -0,0 +1,118 @@ +key: seedance-2.0-mini +label: Seedance 2.0 Mini +family: market +provider_model: bytedance/seedance-2-mini +task_modes: + - text_to_video + - reference_to_video +inputs: + image: + required_min: 0 + required_max: 9 + video: + required_min: 0 + required_max: 3 + audio: + required_min: 0 + required_max: 3 +input_constraints: + image_formats: [jpg, jpeg, png, webp, bmp, gif] + image_max_mb: 30 + video_duration_max_seconds: 15 +options: + return_last_frame: + type: bool + default: false + generate_audio: + type: bool + default: false + resolution: + type: enum + allowed: [480p, 720p] + default: 720p + aspect_ratio: + type: enum + allowed: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9', adaptive] + default: '16:9' + duration: + type: int_range + min: 4 + max: 15 + required: true + label: Duration + help_text: "Video length in seconds. Seedance 2.0 Mini supports 4s through 15s generations." + ui_group: generation + ui_order: 10 + web_search: + type: bool + default: false +prompt: + required: true + max_chars: null + enhancement_supported: true + enhancement_default_policy: ask + default_profile_key: seedance_2_0_t2v_v1 + default_profile_keys_by_input_pattern: + prompt_only: seedance_2_0_t2v_v1 + single_image: seedance_2_0_first_frame_v1 + first_last_frames: seedance_2_0_first_last_frame_v1 + multimodal_reference: seedance_2_0_multimodal_reference_v1 +defaults: {} +transport: + endpoint_family: market + create_path: /api/v1/jobs/createTask + status_path: /api/v1/jobs/recordInfo + callback_supported: true +verification: + verified_from: + - live_page_api_surface + - docs_kie_primary + - site_pricing_api + verified_on: '2026-06-26' + verification_notes: + - Verified against https://kie.ai/seedance-2-0-mini and https://docs.kie.ai/market/bytedance/seedance-2-mini on 2026-06-26. + - The live page identifies the provider model as bytedance/seedance-2-mini and uses the same createTask endpoint as the existing Seedance 2.0 models. + - The live page and docs expose the same Seedance multimodal request shape used by Standard and Fast, including first_frame_url, last_frame_url, reference_image_urls, reference_video_urls, reference_audio_urls, generate_audio, resolution, aspect_ratio, duration, and web_search. + - The live page exposes nsfw_checker, but this SDK intentionally leaves it unavailable so the checker remains disabled by default. + - The live pricing API publishes Mini rows for 480p and 720p, with separate no-video-input and with-video-input per-second pricing. + field_provenance: + key: inferred + label: inferred + family: inferred + provider_model: verified_live + task_modes: inferred + inputs.image.required_min: inferred + inputs.image.required_max: verified_docs + inputs.video.required_min: inferred + inputs.video.required_max: verified_docs + inputs.audio.required_min: inferred + inputs.audio.required_max: verified_docs + input_constraints.image_formats: verified_docs + input_constraints.image_max_mb: verified_docs + input_constraints.video_duration_max_seconds: verified_docs + options.return_last_frame.type: verified_docs + options.return_last_frame.default: inferred + options.generate_audio.type: verified_live + options.generate_audio.default: inferred + options.resolution.type: verified_live + options.resolution.allowed: verified_live + options.resolution.default: inferred + options.aspect_ratio.type: verified_live + options.aspect_ratio.allowed: verified_live + options.aspect_ratio.default: inferred + options.duration.type: verified_live + options.duration.min: verified_docs + options.duration.max: verified_live + options.duration.required: inferred + options.web_search.type: verified_live + options.web_search.default: inferred + prompt.required: verified_live + prompt.max_chars: unknown + prompt.enhancement_supported: inferred + prompt.enhancement_default_policy: inferred + prompt.default_profile_key: inferred + prompt.default_profile_keys_by_input_pattern: inferred + transport.endpoint_family: inferred + transport.create_path: verified_docs + transport.status_path: verified_docs + transport.callback_supported: verified_docs diff --git a/specs/prompt_profiles/kling_2_6_motion_control_v1/metadata.yaml b/specs/prompt_profiles/kling_2_6_motion_control_v1/metadata.yaml new file mode 100644 index 0000000..d32ea40 --- /dev/null +++ b/specs/prompt_profiles/kling_2_6_motion_control_v1/metadata.yaml @@ -0,0 +1,21 @@ +key: kling_2_6_motion_control_v1 +label: Kling 2.6 Motion Control Preset v1 +version: v1 +description: Built-in preset for Kling 2.6 motion control requests. +applies_to_models: + - kling-2.6-motion +applies_to_task_modes: + - motion_control +applies_to_input_patterns: + - motion_control +variables: + - user_prompt + - image_count + - video_count +rules: + - preserve source image identity + - preserve motion transfer intent + - improve clarity around motion, timing, and pose stability +source: builtin +notes: + - Built-in request-shape-aware preset for wrapper-side enhancement. diff --git a/specs/prompt_profiles/kling_2_6_motion_control_v1/prompt.md b/specs/prompt_profiles/kling_2_6_motion_control_v1/prompt.md new file mode 100644 index 0000000..9624fc1 --- /dev/null +++ b/specs/prompt_profiles/kling_2_6_motion_control_v1/prompt.md @@ -0,0 +1,10 @@ +You are preparing a final prompt for Kling 2.6 motion control. + +Reference image count: {{image_count}} +Reference video count: {{video_count}} + +User request: +{{user_prompt}} + +Rewrite the request so the model preserves the source image identity while transferring the intended motion from the reference video cleanly. +Improve clarity around gesture, timing, pose stability, facial consistency, and motion transfer intent. diff --git a/src/kie_api/adapters/market.py b/src/kie_api/adapters/market.py index 74ebcf9..b0c18be 100644 --- a/src/kie_api/adapters/market.py +++ b/src/kie_api/adapters/market.py @@ -16,7 +16,7 @@ ) from ..registry.models import ModelSpec -SEEDANCE_MODEL_KEYS = {"seedance-2.0", "seedance-2.0-fast"} +SEEDANCE_MODEL_KEYS = {"seedance-2.0", "seedance-2.0-fast", "seedance-2.0-mini"} PROVIDER_STATUS_MAP = { diff --git a/src/kie_api/fixtures.py b/src/kie_api/fixtures.py index dc53536..d3eed15 100644 --- a/src/kie_api/fixtures.py +++ b/src/kie_api/fixtures.py @@ -99,6 +99,18 @@ class RequestFixture: expected_model_key="kling-3.0-motion", notes=("Expected to surface a missing motion video requirement.",), ), + "kling_26_motion_complete": RequestFixture( + key="kling_26_motion_complete", + description="Use Kling 2.6 motion control with this image and this motion clip.", + request=RawUserRequest( + model_key="kling-2.6-motion", + images=["https://example.com/source/subject.png"], + videos=["https://example.com/source/motion.mp4"], + options={"character_orientation": "image", "mode": "720p"}, + ), + expected_state=ValidationState.READY, + expected_model_key="kling-2.6-motion", + ), "kling_3_pro_audio_15s": RequestFixture( key="kling_3_pro_audio_15s", description="Make a 15 second Kling 3.0 Pro video with audio.", diff --git a/src/kie_api/resources/pricing/2026-03-25_local_policy.yaml b/src/kie_api/resources/pricing/2026-03-25_local_policy.yaml index ad5a638..f411746 100644 --- a/src/kie_api/resources/pricing/2026-03-25_local_policy.yaml +++ b/src/kie_api/resources/pricing/2026-03-25_local_policy.yaml @@ -46,6 +46,22 @@ rules: 'false': 1.0 notes: - Local policy estimate for Kling 2.6 image-to-video. + - model_key: kling-2.6-motion + pricing_status: local_policy + billing_unit: second + base_credits: 11 + base_cost_usd: 0.055 + multipliers: + duration: + '5': 5.0 + '10': 10.0 + mode: + '720p': 1.0 + '1080p': 1.6363636363636365 + std: 1.0 + pro: 1.6363636363636365 + notes: + - Local policy estimate for Kling 2.6 motion control based on observed site pricing. - model_key: kling-3.0-t2v pricing_status: local_policy billing_unit: request diff --git a/src/kie_api/resources/pricing/2026-04-29_site_pricing_page.yaml b/src/kie_api/resources/pricing/2026-04-29_site_pricing_page.yaml index 41e5135..9e326b6 100644 --- a/src/kie_api/resources/pricing/2026-04-29_site_pricing_page.yaml +++ b/src/kie_api/resources/pricing/2026-04-29_site_pricing_page.yaml @@ -222,6 +222,73 @@ rules: pricing. - Kling 3.0 video pricing is modeled with an internal pricing_variant derived from mode plus sound because 4K rows do not add a separate audio surcharge. +- model_key: kling-3.0-turbo-i2v + pricing_status: observed_site_pricing + billing_unit: second + provider: Kling + interface_type: video + anchor_url: https://kie.ai/kling-3-0-turbo?model=kling%2Fv3-turbo-image-to-video + raw_credit_text: '18' + raw_usd_text: '0.09' + base_credits: 18.0 + base_cost_usd: 0.09 + multipliers: + duration: + '3': 3.0 + '4': 4.0 + '5': 5.0 + '6': 6.0 + '7': 7.0 + '8': 8.0 + '9': 9.0 + '10': 10.0 + '11': 11.0 + '12': 12.0 + '13': 13.0 + '14': 14.0 + '15': 15.0 + resolution: + 720p: 1.0 + 1080p: 1.25 + adders_credits: {} + adders_cost_usd: {} + observed_at: '2026-06-26' + source_row_labels: + - kling 3.0 turbo, image-to-video, 1080P + - kling 3.0 turbo, image-to-video, 720P + source_anchor_urls: [] + notes: + - Observed from https://api.kie.ai/client/v1/model-pricing/page on 2026-06-26. + - Kling 3.0 Turbo image-to-video pricing is modeled by duration and output resolution. +- model_key: kling-2.6-motion + pricing_status: observed_site_pricing + billing_unit: second + provider: Kling + interface_type: video + anchor_url: https://kie.ai/kling-2.6-motion-control + raw_credit_text: '11' + raw_usd_text: '0.055' + base_credits: 11.0 + base_cost_usd: 0.055 + multipliers: + duration: + '5': 5.0 + '10': 10.0 + mode: + 720p: 1.0 + 1080p: 1.6363636363636365 + std: 1.0 + pro: 1.6363636363636365 + adders_credits: {} + adders_cost_usd: {} + observed_at: '2026-04-29' + source_row_labels: + - kling 2.6 motion control, video to video, 1080P + - kling 2.6 motion control, video-to-video, 720P + source_anchor_urls: + - https://kie.ai/kling-2.6-motion-control + notes: + - Observed from https://api.kie.ai/client/v1/model-pricing/page on 2026-04-29. - model_key: kling-3.0-motion pricing_status: observed_site_pricing billing_unit: second @@ -345,6 +412,50 @@ rules: resolution plus whether reference_video_urls are present. - The site pricing API publishes separate rows for 'with video input' and 'no video input'; this rule maps those exactly for dry-run estimation. +- model_key: seedance-2.0-mini + pricing_status: observed_site_pricing + billing_unit: second + provider: ByteDance + interface_type: video + anchor_url: https://kie.ai/seedance-2-0-mini + raw_credit_text: '9.5' + raw_usd_text: '0.0475' + base_credits: 9.5 + base_cost_usd: 0.0475 + multipliers: + duration: + '4': 4.0 + '5': 5.0 + '6': 6.0 + '7': 7.0 + '8': 8.0 + '9': 9.0 + '10': 10.0 + '11': 11.0 + '12': 12.0 + '13': 13.0 + '14': 14.0 + '15': 15.0 + pricing_variant: + 480p_no_video_input: 1.0 + 720p_no_video_input: 2.1578947368421053 + 480p_with_video_input: 0.631578947368421 + 720p_with_video_input: 1.3157894736842106 + adders_credits: {} + adders_cost_usd: {} + observed_at: '2026-06-26' + source_row_labels: + - bytedance/seedance-2-mini, 720P no video + - bytedance/seedance-2-mini, 720P with video + - bytedance/seedance-2-mini, 480P no video + - bytedance/seedance-2-mini, 480P with video + source_anchor_urls: [] + notes: + - Observed from https://api.kie.ai/client/v1/model-pricing/page on 2026-06-26. + - Seedance Mini pricing is modeled with an internal pricing_variant derived from + request resolution plus whether reference_video_urls are present. + - The site pricing API publishes separate Mini rows for 'with video' and 'no video'; + this rule maps those exactly for dry-run estimation. - model_key: gpt-image-2-image-to-image pricing_status: observed_site_pricing billing_unit: request @@ -405,13 +516,16 @@ priced_model_keys: - gpt-image-2-image-to-image - gpt-image-2-text-to-image - kling-2.6-i2v +- kling-2.6-motion - kling-2.6-t2v - kling-3.0-i2v - kling-3.0-motion - kling-3.0-t2v +- kling-3.0-turbo-i2v - nano-banana-2 - nano-banana-pro - seedance-2.0 - seedance-2.0-fast +- seedance-2.0-mini missing_model_keys: [] unmapped_source_rows: [] diff --git a/src/kie_api/resources/prompt_profiles/kling_2_6_motion_control_v1/metadata.yaml b/src/kie_api/resources/prompt_profiles/kling_2_6_motion_control_v1/metadata.yaml new file mode 100644 index 0000000..d32ea40 --- /dev/null +++ b/src/kie_api/resources/prompt_profiles/kling_2_6_motion_control_v1/metadata.yaml @@ -0,0 +1,21 @@ +key: kling_2_6_motion_control_v1 +label: Kling 2.6 Motion Control Preset v1 +version: v1 +description: Built-in preset for Kling 2.6 motion control requests. +applies_to_models: + - kling-2.6-motion +applies_to_task_modes: + - motion_control +applies_to_input_patterns: + - motion_control +variables: + - user_prompt + - image_count + - video_count +rules: + - preserve source image identity + - preserve motion transfer intent + - improve clarity around motion, timing, and pose stability +source: builtin +notes: + - Built-in request-shape-aware preset for wrapper-side enhancement. diff --git a/src/kie_api/resources/prompt_profiles/kling_2_6_motion_control_v1/prompt.md b/src/kie_api/resources/prompt_profiles/kling_2_6_motion_control_v1/prompt.md new file mode 100644 index 0000000..9624fc1 --- /dev/null +++ b/src/kie_api/resources/prompt_profiles/kling_2_6_motion_control_v1/prompt.md @@ -0,0 +1,10 @@ +You are preparing a final prompt for Kling 2.6 motion control. + +Reference image count: {{image_count}} +Reference video count: {{video_count}} + +User request: +{{user_prompt}} + +Rewrite the request so the model preserves the source image identity while transferring the intended motion from the reference video cleanly. +Improve clarity around gesture, timing, pose stability, facial consistency, and motion transfer intent. diff --git a/src/kie_api/resources/prompt_profiles/kling_3_0_turbo_i2v_v1/metadata.yaml b/src/kie_api/resources/prompt_profiles/kling_3_0_turbo_i2v_v1/metadata.yaml new file mode 100644 index 0000000..056a960 --- /dev/null +++ b/src/kie_api/resources/prompt_profiles/kling_3_0_turbo_i2v_v1/metadata.yaml @@ -0,0 +1,21 @@ +key: kling_3_0_turbo_i2v_v1 +label: Kling 3.0 Turbo Image to Video Preset v1 +version: v1 +description: Built-in preset for Kling 3.0 Turbo image-to-video requests. +applies_to_models: + - kling-3.0-turbo-i2v +applies_to_task_modes: + - image_to_video +applies_to_input_patterns: + - single_image + - first_last_frames +variables: + - user_prompt + - image_count +rules: + - preserve source image identity and scene continuity + - improve motion and camera wording + - keep the final prompt compatible with Kling 3.0 Turbo image-to-video +source: builtin +notes: + - Built-in request-shape-aware preset for wrapper-side enhancement. diff --git a/src/kie_api/resources/prompt_profiles/kling_3_0_turbo_i2v_v1/prompt.md b/src/kie_api/resources/prompt_profiles/kling_3_0_turbo_i2v_v1/prompt.md new file mode 100644 index 0000000..4233374 --- /dev/null +++ b/src/kie_api/resources/prompt_profiles/kling_3_0_turbo_i2v_v1/prompt.md @@ -0,0 +1,10 @@ +You are preparing a final prompt for Kling 3.0 Turbo image-to-video. + +Reference image count: {{image_count}} + +User request: +{{user_prompt}} + +Rewrite the request so the generated video develops naturally from the provided source image or images. +Preserve subject identity, environment continuity, composition, and the user's intended action. +Improve motion, camera, pacing, facial/body performance, and emotional readability without introducing unrelated scene changes. diff --git a/src/kie_api/resources/prompt_profiles/seedance_2_0_first_frame_v1/metadata.yaml b/src/kie_api/resources/prompt_profiles/seedance_2_0_first_frame_v1/metadata.yaml index 54ca1af..b299c14 100644 --- a/src/kie_api/resources/prompt_profiles/seedance_2_0_first_frame_v1/metadata.yaml +++ b/src/kie_api/resources/prompt_profiles/seedance_2_0_first_frame_v1/metadata.yaml @@ -4,6 +4,7 @@ version: v1 applies_to_models: - seedance-2.0 - seedance-2.0-fast + - seedance-2.0-mini applies_to_task_modes: - reference_to_video applies_to_input_patterns: @@ -16,4 +17,4 @@ rules: - Describe motion that grows naturally out of the supplied starting frame. - Do not refer to multimodal reference tokens unless they are actually present. notes: - - Use this preset when Seedance 2.0 Standard or Fast has exactly one first-frame image and no last frame or reference media. + - Use this preset when Seedance 2.0 Standard, Fast, or Mini has exactly one first-frame image and no last frame or reference media. diff --git a/src/kie_api/resources/prompt_profiles/seedance_2_0_first_last_frame_v1/metadata.yaml b/src/kie_api/resources/prompt_profiles/seedance_2_0_first_last_frame_v1/metadata.yaml index c15514b..fc2fab5 100644 --- a/src/kie_api/resources/prompt_profiles/seedance_2_0_first_last_frame_v1/metadata.yaml +++ b/src/kie_api/resources/prompt_profiles/seedance_2_0_first_last_frame_v1/metadata.yaml @@ -4,6 +4,7 @@ version: v1 applies_to_models: - seedance-2.0 - seedance-2.0-fast + - seedance-2.0-mini applies_to_task_modes: - reference_to_video applies_to_input_patterns: @@ -17,4 +18,4 @@ rules: - Describe motion that bridges the two endpoint frames cleanly. - Avoid introducing visual changes that would contradict the ending frame. notes: - - Use this preset when Seedance 2.0 Standard or Fast has one first frame image and one last frame image. + - Use this preset when Seedance 2.0 Standard, Fast, or Mini has one first frame image and one last frame image. diff --git a/src/kie_api/resources/prompt_profiles/seedance_2_0_multimodal_reference_v1/metadata.yaml b/src/kie_api/resources/prompt_profiles/seedance_2_0_multimodal_reference_v1/metadata.yaml index 26a614c..251afcb 100644 --- a/src/kie_api/resources/prompt_profiles/seedance_2_0_multimodal_reference_v1/metadata.yaml +++ b/src/kie_api/resources/prompt_profiles/seedance_2_0_multimodal_reference_v1/metadata.yaml @@ -4,6 +4,7 @@ version: v1 applies_to_models: - seedance-2.0 - seedance-2.0-fast + - seedance-2.0-mini applies_to_task_modes: - reference_to_video applies_to_input_patterns: @@ -19,4 +20,4 @@ rules: - Mention reference tokens only when they materially help the provider stay aligned to the supplied assets. - Use the deterministic token guide exactly as given. notes: - - Use this preset when Seedance 2.0 Standard or Fast includes reference images, videos, or audio assets. + - Use this preset when Seedance 2.0 Standard, Fast, or Mini includes reference images, videos, or audio assets. diff --git a/src/kie_api/resources/prompt_profiles/seedance_2_0_t2v_v1/metadata.yaml b/src/kie_api/resources/prompt_profiles/seedance_2_0_t2v_v1/metadata.yaml index 7a849cb..c17d575 100644 --- a/src/kie_api/resources/prompt_profiles/seedance_2_0_t2v_v1/metadata.yaml +++ b/src/kie_api/resources/prompt_profiles/seedance_2_0_t2v_v1/metadata.yaml @@ -4,6 +4,7 @@ version: v1 applies_to_models: - seedance-2.0 - seedance-2.0-fast + - seedance-2.0-mini applies_to_task_modes: - text_to_video applies_to_input_patterns: @@ -16,4 +17,4 @@ rules: - Expand the prompt into concise cinematic video direction without inventing unsupported media references. - Keep the final wording compatible with a single text-to-video request. notes: - - Use this preset for plain Seedance 2.0 Standard or Fast text-to-video requests with no first/last frames or reference assets. + - Use this preset for plain Seedance 2.0 Standard, Fast, or Mini text-to-video requests with no first/last frames or reference assets. diff --git a/src/kie_api/resources/specs/models/kling_2_6_motion.yaml b/src/kie_api/resources/specs/models/kling_2_6_motion.yaml new file mode 100644 index 0000000..75f1148 --- /dev/null +++ b/src/kie_api/resources/specs/models/kling_2_6_motion.yaml @@ -0,0 +1,117 @@ +key: kling-2.6-motion +label: Kling 2.6 Motion Control +family: market +provider_model: kling-2.6/motion-control +task_modes: + - motion_control +inputs: + image: + required_min: 1 + required_max: 1 + video: + required_min: 1 + required_max: 1 + audio: + required_min: 0 + required_max: 0 +input_constraints: + image_formats: [jpg, jpeg, png] + image_max_mb: 10 + image_min_dimension_px: 300 + image_aspect_ratio_min: 0.4 + image_aspect_ratio_max: 2.5 + video_formats: [mp4, mov, quicktime, mkv, matroska, x-matroska] + video_max_mb: 100 + video_duration_min_seconds: 3 + video_duration_max_seconds: 30 + video_min_dimension_px: 300 + video_aspect_ratio_min: 0.4 + video_aspect_ratio_max: 2.5 +options: + character_orientation: + type: enum + allowed: [image, video] + default: null + required: true + notes: "'image' keeps the image orientation and caps driving-video duration at 10s; 'video' follows the video orientation and allows up to 30s." + mode: + type: enum + allowed: ['720p', '1080p'] + default: null + required: true + value_aliases: + std: '720p' + pro: '1080p' + notes: "The live page expected fields list uses 720p and 1080p, while the help text still mentions std/pro naming." +prompt: + required: false + max_chars: 2500 + enhancement_supported: true + enhancement_default_policy: ask + default_profile_key: kling_2_6_motion_control_v1 +defaults: {} +transport: + endpoint_family: market + create_path: /api/v1/jobs/createTask + status_path: /api/v1/jobs/recordInfo + callback_supported: true +verification: + verified_from: + - live_page_api_surface + - docs_kie_primary + - site_pricing_page_api + verified_on: '2026-06-19' + verification_notes: + - Verified provider model string and request example from https://docs.kie.ai/market/kling/motion-control. + - The docs request shape uses prompt, input_urls, video_urls, mode, and character_orientation. + - The live https://kie.ai/kling-2.6-motion-control page expected fields list shows prompt, input_urls, video_urls, character_orientation, and mode. + - The live page marks input_urls, video_urls, character_orientation, and mode as required. + - The live page supports JPEG, PNG, and JPG reference images up to 10MB, greater than 300px, with aspect ratio 2:5 to 5:2. + - The live page supports MP4, QuickTime, and X-Matroska videos up to 100MB and one file; help text names .mp4 and .mov. + - The live page states character_orientation=image allows max 10s driving video and character_orientation=video allows max 30s. + - Site pricing rows observed 11 credits / $0.055 per second for 720p and 18 credits / $0.09 per second for 1080p. + field_provenance: + key: inferred + label: inferred + family: inferred + provider_model: verified_docs + task_modes: inferred + inputs.image.required_min: verified_live + inputs.image.required_max: verified_live + inputs.video.required_min: verified_live + inputs.video.required_max: verified_live + inputs.audio.required_min: inferred + inputs.audio.required_max: inferred + input_constraints.image_formats: verified_live + input_constraints.image_max_mb: verified_live + input_constraints.image_min_dimension_px: verified_live + input_constraints.image_aspect_ratio_min: verified_live + input_constraints.image_aspect_ratio_max: verified_live + input_constraints.video_formats: verified_live + input_constraints.video_max_mb: verified_live + input_constraints.video_duration_min_seconds: verified_live + input_constraints.video_duration_max_seconds: verified_live + input_constraints.video_min_dimension_px: verified_live + input_constraints.video_aspect_ratio_min: verified_live + input_constraints.video_aspect_ratio_max: verified_live + options.character_orientation.type: inferred + options.character_orientation.allowed: verified_live + options.character_orientation.default: unknown + options.character_orientation.required: verified_live + options.character_orientation.notes: verified_live + options.mode.type: inferred + options.mode.allowed: verified_live + options.mode.default: unknown + options.mode.required: verified_live + options.mode.value_aliases: inferred + options.mode.notes: verified_live + prompt.required: verified_live + prompt.max_chars: verified_live + prompt.enhancement_supported: inferred + prompt.enhancement_default_policy: inferred + prompt.default_profile_key: inferred + defaults: inferred + transport.endpoint_family: inferred + transport.create_path: verified_docs + transport.status_path: verified_docs + transport.callback_supported: verified_docs diff --git a/src/kie_api/resources/specs/models/kling_3_0_turbo_i2v.yaml b/src/kie_api/resources/specs/models/kling_3_0_turbo_i2v.yaml new file mode 100644 index 0000000..c1b18d9 --- /dev/null +++ b/src/kie_api/resources/specs/models/kling_3_0_turbo_i2v.yaml @@ -0,0 +1,96 @@ +key: kling-3.0-turbo-i2v +label: Kling 3.0 Turbo Image to Video +family: market +provider_model: kling/v3-turbo-image-to-video +task_modes: + - image_to_video +inputs: + image: + required_min: 1 + video: + required_min: 0 + required_max: 0 + audio: + required_min: 0 + required_max: 0 +input_constraints: + image_formats: [jpeg, jpg, png] + image_max_mb: 10 +options: + duration: + type: int_range + min: 3 + max: 15 + default: 5 + required: true + label: Duration + help_text: "Video length in seconds. Kling 3.0 Turbo Image to Video supports 3s through 15s generations." + ui_group: generation + ui_order: 10 + resolution: + type: enum + allowed: [720p, 1080p] + default: 720p + required: true +prompt: + required: true + max_chars: 2500 + enhancement_supported: true + enhancement_default_policy: ask + default_profile_key: kling_3_0_turbo_i2v_v1 + default_profile_keys_by_input_pattern: + single_image: kling_3_0_turbo_i2v_v1 + first_last_frames: kling_3_0_turbo_i2v_v1 +defaults: {} +transport: + endpoint_family: market + create_path: /api/v1/jobs/createTask + status_path: /api/v1/jobs/recordInfo + callback_supported: true +verification: + verified_from: + - user_supplied_docs + - live_page_api_surface + - site_pricing_api + verified_on: '2026-06-26' + verification_notes: + - Verified against the user-supplied V3 Turbo Image To Video API documentation and https://kie.ai/kling-3-0-turbo on 2026-06-26. + - The live page identifies two flavors, kling/v3-turbo-image-to-video and kling/v3-turbo-text-to-video; this spec intentionally onboards image-to-video only. + - The createTask payload uses provider model kling/v3-turbo-image-to-video with input.prompt, input.image_urls, input.duration, and input.resolution. + - The docs list image/jpeg and image/png inputs up to 10MB, duration 3 through 15 seconds, and resolutions 720p and 1080p. + - The docs say image_urls supports multiple files but do not publish a maximum image count, so the SDK enforces the required minimum only. + - KIE's public pricing API publishes separate Turbo image-to-video rows for 720P and 1080P per-second pricing. + field_provenance: + key: inferred + label: inferred + family: inferred + provider_model: verified_docs + task_modes: inferred + inputs.image.required_min: verified_docs + inputs.image.required_max: unknown + inputs.video.required_min: inferred + inputs.video.required_max: inferred + inputs.audio.required_min: inferred + inputs.audio.required_max: inferred + input_constraints.image_formats: verified_docs + input_constraints.image_max_mb: verified_docs + options.duration.type: inferred + options.duration.min: verified_docs + options.duration.max: verified_docs + options.duration.default: verified_docs + options.duration.required: verified_docs + options.resolution.type: inferred + options.resolution.allowed: verified_docs + options.resolution.default: verified_docs + options.resolution.required: verified_docs + prompt.required: verified_docs + prompt.max_chars: verified_docs + prompt.enhancement_supported: inferred + prompt.enhancement_default_policy: inferred + prompt.default_profile_key: inferred + prompt.default_profile_keys_by_input_pattern: inferred + defaults: inferred + transport.endpoint_family: inferred + transport.create_path: verified_docs + transport.status_path: verified_docs + transport.callback_supported: verified_docs diff --git a/src/kie_api/resources/specs/models/seedance_2_0_mini.yaml b/src/kie_api/resources/specs/models/seedance_2_0_mini.yaml new file mode 100644 index 0000000..164fdae --- /dev/null +++ b/src/kie_api/resources/specs/models/seedance_2_0_mini.yaml @@ -0,0 +1,118 @@ +key: seedance-2.0-mini +label: Seedance 2.0 Mini +family: market +provider_model: bytedance/seedance-2-mini +task_modes: + - text_to_video + - reference_to_video +inputs: + image: + required_min: 0 + required_max: 9 + video: + required_min: 0 + required_max: 3 + audio: + required_min: 0 + required_max: 3 +input_constraints: + image_formats: [jpg, jpeg, png, webp, bmp, gif] + image_max_mb: 30 + video_duration_max_seconds: 15 +options: + return_last_frame: + type: bool + default: false + generate_audio: + type: bool + default: false + resolution: + type: enum + allowed: [480p, 720p] + default: 720p + aspect_ratio: + type: enum + allowed: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9', adaptive] + default: '16:9' + duration: + type: int_range + min: 4 + max: 15 + required: true + label: Duration + help_text: "Video length in seconds. Seedance 2.0 Mini supports 4s through 15s generations." + ui_group: generation + ui_order: 10 + web_search: + type: bool + default: false +prompt: + required: true + max_chars: null + enhancement_supported: true + enhancement_default_policy: ask + default_profile_key: seedance_2_0_t2v_v1 + default_profile_keys_by_input_pattern: + prompt_only: seedance_2_0_t2v_v1 + single_image: seedance_2_0_first_frame_v1 + first_last_frames: seedance_2_0_first_last_frame_v1 + multimodal_reference: seedance_2_0_multimodal_reference_v1 +defaults: {} +transport: + endpoint_family: market + create_path: /api/v1/jobs/createTask + status_path: /api/v1/jobs/recordInfo + callback_supported: true +verification: + verified_from: + - live_page_api_surface + - docs_kie_primary + - site_pricing_api + verified_on: '2026-06-26' + verification_notes: + - Verified against https://kie.ai/seedance-2-0-mini and https://docs.kie.ai/market/bytedance/seedance-2-mini on 2026-06-26. + - The live page identifies the provider model as bytedance/seedance-2-mini and uses the same createTask endpoint as the existing Seedance 2.0 models. + - The live page and docs expose the same Seedance multimodal request shape used by Standard and Fast, including first_frame_url, last_frame_url, reference_image_urls, reference_video_urls, reference_audio_urls, generate_audio, resolution, aspect_ratio, duration, and web_search. + - The live page exposes nsfw_checker, but this SDK intentionally leaves it unavailable so the checker remains disabled by default. + - The live pricing API publishes Mini rows for 480p and 720p, with separate no-video-input and with-video-input per-second pricing. + field_provenance: + key: inferred + label: inferred + family: inferred + provider_model: verified_live + task_modes: inferred + inputs.image.required_min: inferred + inputs.image.required_max: verified_docs + inputs.video.required_min: inferred + inputs.video.required_max: verified_docs + inputs.audio.required_min: inferred + inputs.audio.required_max: verified_docs + input_constraints.image_formats: verified_docs + input_constraints.image_max_mb: verified_docs + input_constraints.video_duration_max_seconds: verified_docs + options.return_last_frame.type: verified_docs + options.return_last_frame.default: inferred + options.generate_audio.type: verified_live + options.generate_audio.default: inferred + options.resolution.type: verified_live + options.resolution.allowed: verified_live + options.resolution.default: inferred + options.aspect_ratio.type: verified_live + options.aspect_ratio.allowed: verified_live + options.aspect_ratio.default: inferred + options.duration.type: verified_live + options.duration.min: verified_docs + options.duration.max: verified_live + options.duration.required: inferred + options.web_search.type: verified_live + options.web_search.default: inferred + prompt.required: verified_live + prompt.max_chars: unknown + prompt.enhancement_supported: inferred + prompt.enhancement_default_policy: inferred + prompt.default_profile_key: inferred + prompt.default_profile_keys_by_input_pattern: inferred + transport.endpoint_family: inferred + transport.create_path: verified_docs + transport.status_path: verified_docs + transport.callback_supported: verified_docs diff --git a/src/kie_api/services/normalizer.py b/src/kie_api/services/normalizer.py index 824240c..1513c73 100644 --- a/src/kie_api/services/normalizer.py +++ b/src/kie_api/services/normalizer.py @@ -16,7 +16,7 @@ "kling-2.6": {"with_image": "kling-2.6-i2v", "without_image": "kling-2.6-t2v"}, "kling-3.0": {"with_image": "kling-3.0-i2v", "without_image": "kling-3.0-t2v"}, } -SEEDANCE_MODEL_KEYS = {"seedance-2.0", "seedance-2.0-fast"} +SEEDANCE_MODEL_KEYS = {"seedance-2.0", "seedance-2.0-fast", "seedance-2.0-mini"} class RequestNormalizer: diff --git a/src/kie_api/services/pricing.py b/src/kie_api/services/pricing.py index 3248433..3c3cc3b 100644 --- a/src/kie_api/services/pricing.py +++ b/src/kie_api/services/pricing.py @@ -2,13 +2,14 @@ from __future__ import annotations +import math from typing import Any, Dict, List, Optional from ..models import EstimatedCost, NormalizedRequest from ..registry.loader import load_latest_pricing_snapshot from ..registry.models import PricingRule, PricingSnapshot -SEEDANCE_MODEL_KEYS = {"seedance-2.0", "seedance-2.0-fast"} +SEEDANCE_MODEL_KEYS = {"seedance-2.0", "seedance-2.0-fast", "seedance-2.0-mini"} class PricingRegistry: @@ -86,6 +87,12 @@ def _estimate(self, model_key: str, options: Dict[str, Any]) -> EstimatedCost: for option_name, value_map in rule.multipliers.items(): option_value = _normalize_option_value(options.get(option_name)) multiplier = value_map.get(option_value) + if multiplier is None: + multiplier = _second_billing_duration_multiplier( + rule, + option_name, + options.get(option_name), + ) if multiplier is None: continue applied_multipliers[option_name] = multiplier @@ -142,6 +149,24 @@ def _normalize_option_value(value: Any) -> str: return str(value).lower() +def _second_billing_duration_multiplier( + rule: PricingRule, + option_name: str, + option_value: Any, +) -> Optional[float]: + if rule.billing_unit != "second" or option_name != "duration": + return None + if isinstance(option_value, bool) or option_value in (None, ""): + return None + try: + duration = float(option_value) + except (TypeError, ValueError): + return None + if not math.isfinite(duration) or duration <= 0: + return None + return float(math.ceil(duration)) + + def _is_authoritative_pricing(pricing_status: str, source_kind: str) -> bool: authoritative_statuses = { "verified_provider", @@ -176,4 +201,20 @@ def _derive_request_pricing_options(request: NormalizedRequest) -> Dict[str, Any f"{resolution}_{'with_video_input' if has_video_input else 'no_video_input'}" ) + if request.model_key in {"kling-2.6-motion", "kling-3.0-motion"}: + duration = _first_video_duration_seconds(request) + if duration is not None: + derived["duration"] = math.ceil(duration) + return derived + + +def _first_video_duration_seconds(request: NormalizedRequest) -> Optional[float]: + for video in request.videos: + try: + duration = float(video.duration_seconds) if video.duration_seconds is not None else None + except (TypeError, ValueError): + continue + if duration is not None and math.isfinite(duration) and duration > 0: + return duration + return None diff --git a/src/kie_api/services/pricing_refresh.py b/src/kie_api/services/pricing_refresh.py index c1ef3a0..dbfc4f6 100644 --- a/src/kie_api/services/pricing_refresh.py +++ b/src/kie_api/services/pricing_refresh.py @@ -242,6 +242,11 @@ def build_supported_model_snapshot( _mark_rows_used(used_rows, kling_26_i2v_rows) rules.append(_build_kling_26_video_rule("kling-2.6-i2v", kling_26_i2v_rows, observed_at=released)) + kling_26_motion_rows = _rows_with_anchor(capture.rows, "https://kie.ai/kling-2.6-motion-control") + if kling_26_motion_rows: + _mark_rows_used(used_rows, kling_26_motion_rows) + rules.append(_build_kling_26_motion_rule(kling_26_motion_rows, observed_at=released)) + kling_30_rows = _rows_with_anchor(capture.rows, "https://kie.ai/kling-3-0") if kling_30_rows: _mark_rows_used(used_rows, kling_30_rows) @@ -252,6 +257,11 @@ def build_supported_model_snapshot( ] ) + kling_30_turbo_i2v_rows = _rows_with_phrase(capture.rows, "kling 3.0 turbo, image-to-video") + if kling_30_turbo_i2v_rows: + _mark_rows_used(used_rows, kling_30_turbo_i2v_rows) + rules.append(_build_kling_30_turbo_i2v_rule(kling_30_turbo_i2v_rows, observed_at=released)) + kling_30_motion_rows = _rows_with_anchor(capture.rows, "https://kie.ai/kling-3-motion-control") if kling_30_motion_rows: _mark_rows_used(used_rows, kling_30_motion_rows) @@ -274,6 +284,18 @@ def build_supported_model_snapshot( ) ) + seedance_mini_rows = _rows_with_phrase(capture.rows, "bytedance/seedance-2-mini,") + if seedance_mini_rows: + _mark_rows_used(used_rows, seedance_mini_rows) + rules.append( + _build_seedance_2_rule( + seedance_mini_rows, + model_key="seedance-2.0-mini", + anchor_url="https://kie.ai/seedance-2-0-mini", + observed_at=released, + ) + ) + existing_model_keys = {rule.model_key for rule in rules} for spec in sorted(resolved_registry.iter_models(), key=lambda item: item.key): if spec.key in existing_model_keys: @@ -453,6 +475,43 @@ def _build_kling_26_video_rule( ) +def _build_kling_26_motion_rule( + rows: List[PricingCatalogRow], + *, + observed_at: str, +) -> PricingRule: + base = _select_row(rows, "720p") or rows[0] + mode_multiplier = _ratio(_credit_for(rows, "1080p"), base.credit_price) + return _with_row_provenance( + PricingRule( + model_key="kling-2.6-motion", + pricing_status="observed_site_pricing", + billing_unit="second", + provider="Kling", + interface_type="video", + anchor_url="https://kie.ai/kling-2.6-motion-control", + raw_credit_text=base.credit_price_text, + raw_usd_text=base.usd_price_text, + base_credits=base.credit_price, + base_cost_usd=base.usd_price, + multipliers={ + "duration": {"5": 5.0, "10": 10.0}, + "mode": { + "720p": 1.0, + "1080p": mode_multiplier, + "std": 1.0, + "pro": mode_multiplier, + }, + }, + notes=[ + f"Observed from https://api.kie.ai/client/v1/model-pricing/page on {observed_at}.", + ], + ), + rows=rows, + observed_at=observed_at, + ) + + def _build_kling_30_video_rule( model_key: str, rows: List[PricingCatalogRow], @@ -531,6 +590,41 @@ def _build_kling_30_motion_rule( ) +def _build_kling_30_turbo_i2v_rule( + rows: List[PricingCatalogRow], + *, + observed_at: str, +) -> PricingRule: + base = _select_row(rows, "720p") or rows[0] + return _with_row_provenance( + PricingRule( + model_key="kling-3.0-turbo-i2v", + pricing_status="observed_site_pricing", + billing_unit="second", + provider="Kling", + interface_type="video", + anchor_url="https://kie.ai/kling-3-0-turbo?model=kling%2Fv3-turbo-image-to-video", + raw_credit_text=base.credit_price_text, + raw_usd_text=base.usd_price_text, + base_credits=base.credit_price, + base_cost_usd=base.usd_price, + multipliers={ + "duration": {str(value): float(value) for value in range(3, 16)}, + "resolution": { + "720p": 1.0, + "1080p": _ratio(_credit_for(rows, "1080p"), base.credit_price), + }, + }, + notes=[ + f"Observed from https://api.kie.ai/client/v1/model-pricing/page on {observed_at}.", + "Kling 3.0 Turbo image-to-video pricing is modeled by duration and output resolution.", + ], + ), + rows=rows, + observed_at=observed_at, + ) + + def _build_seedance_2_rule( rows: List[PricingCatalogRow], *, @@ -538,21 +632,21 @@ def _build_seedance_2_rule( anchor_url: str = "https://kie.ai/seedance-2-0", observed_at: str, ) -> PricingRule: - base = _select_row(rows, "480p no video input") or rows[0] + base = _select_row(rows, "480p no video input") or _select_row(rows, "480p no video") or rows[0] pricing_variant = { "480p_no_video_input": 1.0, - "720p_no_video_input": _ratio(_credit_for(rows, "720p no video input"), base.credit_price), - "480p_with_video_input": _ratio(_credit_for(rows, "480p with video input"), base.credit_price), - "720p_with_video_input": _ratio(_credit_for(rows, "720p with video input"), base.credit_price), + "720p_no_video_input": _ratio(_seedance_credit_for(rows, "720p", with_video=False), base.credit_price), + "480p_with_video_input": _ratio(_seedance_credit_for(rows, "480p", with_video=True), base.credit_price), + "720p_with_video_input": _ratio(_seedance_credit_for(rows, "720p", with_video=True), base.credit_price), } - if _select_row(rows, "1080p no video input"): + if _select_row(rows, "1080p no video input") or _select_row(rows, "1080p no video"): pricing_variant["1080p_no_video_input"] = _ratio( - _credit_for(rows, "1080p no video input"), + _seedance_credit_for(rows, "1080p", with_video=False), base.credit_price, ) - if _select_row(rows, "1080p with video input"): + if _select_row(rows, "1080p with video input") or _select_row(rows, "1080p with video"): pricing_variant["1080p_with_video_input"] = _ratio( - _credit_for(rows, "1080p with video input"), + _seedance_credit_for(rows, "1080p", with_video=True), base.credit_price, ) @@ -583,6 +677,16 @@ def _build_seedance_2_rule( ) +def _seedance_credit_for( + rows: List[PricingCatalogRow], + resolution: str, + *, + with_video: bool, +) -> Optional[float]: + row = _select_row(rows, f"{resolution} {'with video' if with_video else 'no video'}") + return row.credit_price if row else None + + def _build_generic_image_resolution_rule( spec: ModelSpec, rows: List[PricingCatalogRow], diff --git a/src/kie_api/services/validator.py b/src/kie_api/services/validator.py index 1140c9c..8b8605f 100644 --- a/src/kie_api/services/validator.py +++ b/src/kie_api/services/validator.py @@ -16,7 +16,7 @@ ) from ..registry.loader import SpecRegistry -SEEDANCE_MODEL_KEYS = {"seedance-2.0", "seedance-2.0-fast"} +SEEDANCE_MODEL_KEYS = {"seedance-2.0", "seedance-2.0-fast", "seedance-2.0-mini"} class RequestValidator: diff --git a/tests/test_pricing_and_credit_guard.py b/tests/test_pricing_and_credit_guard.py index b05fb17..e095b96 100644 --- a/tests/test_pricing_and_credit_guard.py +++ b/tests/test_pricing_and_credit_guard.py @@ -107,6 +107,38 @@ def test_pricing_registry_applies_kling_30_per_second_duration_range() -> None: assert estimate.estimated_cost_usd == pytest.approx(2.345) +def test_pricing_registry_derives_kling_motion_duration_from_video_metadata() -> None: + registry = PricingRegistry() + request = NormalizedRequest( + model_key="kling-2.6-motion", + provider_model="kling-2.6/motion-control", + task_mode=TaskMode.MOTION_CONTROL, + prompt="transfer the motion cleanly", + prompt_policy=PromptPolicy.OFF, + images=[ + { + "media_type": "image", + "url": "https://example.com/subject.png", + } + ], + videos=[ + { + "media_type": "video", + "url": "https://example.com/motion.mp4", + "duration_seconds": 20.083333, + } + ], + options={"mode": "720p", "character_orientation": "image"}, + ) + + estimate = registry.estimate_request(request) + + assert estimate.applied_multipliers["duration"] == pytest.approx(21.0) + assert estimate.applied_multipliers["mode"] == pytest.approx(1.0) + assert estimate.estimated_credits == pytest.approx(231.0) + assert estimate.estimated_cost_usd == pytest.approx(1.155) + + def test_credit_guard_rejects_when_estimated_credits_exceed_balance() -> None: pricing = PricingRegistry.from_rules( [PricingRule(model_key="kling-3.0-t2v", pricing_status="manual", base_credits=20)], @@ -243,3 +275,52 @@ def test_pricing_registry_applies_seedance_fast_variant() -> None: assert estimate.applied_multipliers["duration"] == 10.0 assert estimate.applied_multipliers["pricing_variant"] == pytest.approx(20.0 / 15.5) assert estimate.estimated_credits == pytest.approx(200.0) + + +def test_pricing_registry_applies_seedance_mini_variant() -> None: + registry = PricingRegistry() + request = NormalizedRequest( + model_key="seedance-2.0-mini", + provider_model="bytedance/seedance-2-mini", + task_mode=TaskMode.REFERENCE_TO_VIDEO, + prompt="use the reference clip for fast lower-cost motion timing", + prompt_policy=PromptPolicy.OFF, + videos=[ + { + "media_type": "video", + "url": "https://example.com/ref.mp4", + "role": "reference", + } + ], + options={"duration": 10, "resolution": "720p"}, + ) + + estimate = registry.estimate_request(request) + + assert estimate.applied_multipliers["duration"] == 10.0 + assert estimate.applied_multipliers["pricing_variant"] == pytest.approx(12.5 / 9.5) + assert estimate.estimated_credits == pytest.approx(125.0) + + +def test_pricing_registry_applies_kling_3_turbo_i2v_variant() -> None: + registry = PricingRegistry() + request = NormalizedRequest( + model_key="kling-3.0-turbo-i2v", + provider_model="kling/v3-turbo-image-to-video", + task_mode=TaskMode.IMAGE_TO_VIDEO, + prompt="animate this product still", + prompt_policy=PromptPolicy.OFF, + images=[ + { + "media_type": "image", + "url": "https://example.com/start.png", + } + ], + options={"duration": 5, "resolution": "1080p"}, + ) + + estimate = registry.estimate_request(request) + + assert estimate.applied_multipliers["duration"] == 5.0 + assert estimate.applied_multipliers["resolution"] == pytest.approx(22.5 / 18.0) + assert estimate.estimated_credits == pytest.approx(112.5) diff --git a/tests/test_pricing_refresh.py b/tests/test_pricing_refresh.py index dd89714..ea87634 100644 --- a/tests/test_pricing_refresh.py +++ b/tests/test_pricing_refresh.py @@ -303,6 +303,28 @@ def test_build_supported_model_snapshot_maps_live_pricing_rows() -> None: "discountRate": 12.5, "anchor": "https://kie.ai/kling-3-0", }, + { + "modelDescription": "kling 3.0 turbo, image-to-video, 1080P", + "interfaceType": "video", + "provider": "Kling", + "creditPrice": "22.5", + "creditUnit": "per second", + "usdPrice": "0.1125", + "falPrice": None, + "discountRate": 0.0, + "anchor": None, + }, + { + "modelDescription": "kling 3.0 turbo, image-to-video, 720P", + "interfaceType": "video", + "provider": "Kling", + "creditPrice": "18", + "creditUnit": "per second", + "usdPrice": "0.09", + "falPrice": None, + "discountRate": 0.0, + "anchor": None, + }, { "modelDescription": "kling 3.0 motion control, video-to-video, 1080P", "interfaceType": "video", @@ -325,6 +347,28 @@ def test_build_supported_model_snapshot_maps_live_pricing_rows() -> None: "discountRate": 9.09, "anchor": "https://kie.ai/kling-3-motion-control", }, + { + "modelDescription": "kling 2.6 motion control, video to video, 1080P", + "interfaceType": "video", + "provider": "Kling", + "creditPrice": "18", + "creditUnit": "per second", + "usdPrice": "0.09", + "falPrice": None, + "discountRate": 0.0, + "anchor": "https://kie.ai/kling-2.6-motion-control", + }, + { + "modelDescription": "kling 2.6 motion control, video-to-video, 720P", + "interfaceType": "video", + "provider": "Kling", + "creditPrice": "11", + "creditUnit": "per second", + "usdPrice": "0.055", + "falPrice": None, + "discountRate": 0.0, + "anchor": "https://kie.ai/kling-2.6-motion-control", + }, { "modelDescription": "bytedance/seedance-2, 1080p no video input", "interfaceType": "video", @@ -435,6 +479,50 @@ def test_build_supported_model_snapshot_maps_live_pricing_rows() -> None: "discountRate": 33.33, "anchor": "https://kie.ai/seedance-2-0?model=bytedance%2Fseedance-2-fast", }, + { + "modelDescription": "bytedance/seedance-2-mini, 720P no video", + "interfaceType": "video", + "provider": "ByteDance", + "creditPrice": "20.5", + "creditUnit": "per second", + "usdPrice": "0.1025", + "falPrice": None, + "discountRate": 0.0, + "anchor": None, + }, + { + "modelDescription": "bytedance/seedance-2-mini, 720P with video", + "interfaceType": "video", + "provider": "ByteDance", + "creditPrice": "12.5", + "creditUnit": "per second", + "usdPrice": "0.0625", + "falPrice": None, + "discountRate": 0.0, + "anchor": None, + }, + { + "modelDescription": "bytedance/seedance-2-mini, 480P no video", + "interfaceType": "video", + "provider": "ByteDance", + "creditPrice": "9.5", + "creditUnit": "per second", + "usdPrice": "0.0475", + "falPrice": None, + "discountRate": 0.0, + "anchor": None, + }, + { + "modelDescription": "bytedance/seedance-2-mini, 480P with video", + "interfaceType": "video", + "provider": "ByteDance", + "creditPrice": "6", + "creditUnit": "per second", + "usdPrice": "0.03", + "falPrice": None, + "discountRate": 0.0, + "anchor": None, + }, ] rows = [ @@ -482,6 +570,12 @@ def test_build_supported_model_snapshot_maps_live_pricing_rows() -> None: assert rules["kling-3.0-t2v"].multipliers["duration"]["15"] == 15.0 assert rules["kling-3.0-t2v"].multipliers["pricing_variant"]["4k_true"] == 67.0 / 14.0 assert rules["kling-3.0-t2v"].multipliers["pricing_variant"]["1080p_true"] == 27.0 / 14.0 + assert rules["kling-3.0-turbo-i2v"].billing_unit == "second" + assert rules["kling-3.0-turbo-i2v"].base_credits == 18.0 + assert rules["kling-3.0-turbo-i2v"].multipliers["resolution"]["1080p"] == 22.5 / 18.0 + assert rules["kling-2.6-motion"].base_credits == 11.0 + assert rules["kling-2.6-motion"].billing_unit == "second" + assert rules["kling-2.6-motion"].multipliers["mode"]["1080p"] == 18.0 / 11.0 assert rules["kling-3.0-motion"].multipliers["mode"]["1080p"] == 1.35 assert rules["seedance-2.0"].billing_unit == "second" assert rules["seedance-2.0"].multipliers["pricing_variant"]["720p_with_video_input"] == 25.0 / 19.0 @@ -490,6 +584,11 @@ def test_build_supported_model_snapshot_maps_live_pricing_rows() -> None: assert rules["seedance-2.0-fast"].base_credits == 15.5 assert rules["seedance-2.0-fast"].multipliers["pricing_variant"]["720p_with_video_input"] == 20.0 / 15.5 assert "1080p_no_video_input" not in rules["seedance-2.0-fast"].multipliers["pricing_variant"] + assert rules["seedance-2.0-mini"].billing_unit == "second" + assert rules["seedance-2.0-mini"].base_credits == 9.5 + assert rules["seedance-2.0-mini"].multipliers["pricing_variant"]["720p_no_video_input"] == 20.5 / 9.5 + assert rules["seedance-2.0-mini"].multipliers["pricing_variant"]["720p_with_video_input"] == 12.5 / 9.5 + assert "1080p_no_video_input" not in rules["seedance-2.0-mini"].multipliers["pricing_variant"] assert rules["suno-generate-music"].pricing_status == "unknown" assert rules["suno-generate-music"].interface_type == "music" assert snapshot.missing_model_keys == [] diff --git a/tests/test_prompt_enhancer.py b/tests/test_prompt_enhancer.py index 23d11e9..e1dbca0 100644 --- a/tests/test_prompt_enhancer.py +++ b/tests/test_prompt_enhancer.py @@ -222,6 +222,29 @@ def test_prompt_enhancer_resolves_seedance_multimodal_reference_preset() -> None assert "@audio1 -> reference audio 1 (ref1.mp3)" in (context.rendered_system_prompt or "") +def test_prompt_enhancer_resolves_kling_3_turbo_i2v_preset() -> None: + registry = load_registry() + enhancer = PromptEnhancer(registry) + + context = enhancer.resolve_context( + NormalizedRequest( + model_key="kling-3.0-turbo-i2v", + provider_model="kling/v3-turbo-image-to-video", + task_mode="image_to_video", + prompt="animate the subject with a gentle handheld camera push", + raw_prompt="animate the subject with a gentle handheld camera push", + prompt_policy=PromptPolicy.PREVIEW, + images=[ + {"media_type": "image", "url": "https://example.com/start.png"}, + ], + ) + ) + + assert str(context.input_pattern) == "single_image" + assert context.resolved_preset_key == "kling_3_0_turbo_i2v_v1" + assert "Kling 3.0 Turbo image-to-video" in (context.rendered_system_prompt or "") + + def test_prompt_enhancer_resolves_suno_music_preset() -> None: registry = load_registry() enhancer = PromptEnhancer(registry) diff --git a/tests/test_registry_loader.py b/tests/test_registry_loader.py index 85c7116..9a7ecc0 100644 --- a/tests/test_registry_loader.py +++ b/tests/test_registry_loader.py @@ -12,7 +12,7 @@ def test_registry_loads_verified_model_specs() -> None: registry = load_registry() - assert len(registry.model_specs) == 12 + assert len(registry.model_specs) == 15 assert registry.get_model("nano-banana-pro").inputs["image"].required_max == 8 assert registry.get_model("nano-banana-2").inputs["image"].required_max == 14 assert registry.get_model("gpt-image-2-image-to-image").inputs["image"].required_max == 16 @@ -25,6 +25,16 @@ def test_registry_loads_verified_model_specs() -> None: assert registry.get_model("seedance-2.0-fast").provider_model == "bytedance/seedance-2-fast" assert registry.get_model("seedance-2.0-fast").options["resolution"].allowed == ["480p", "720p"] assert registry.get_model("seedance-2.0-fast").options["aspect_ratio"].allowed[-1] == "adaptive" + assert registry.get_model("seedance-2.0-mini").provider_model == "bytedance/seedance-2-mini" + assert registry.get_model("seedance-2.0-mini").options["resolution"].allowed == ["480p", "720p"] + assert registry.get_model("seedance-2.0-mini").options["aspect_ratio"].allowed[-1] == "adaptive" + assert "nsfw_checker" not in registry.get_model("seedance-2.0-mini").options + assert registry.get_model("kling-3.0-turbo-i2v").provider_model == "kling/v3-turbo-image-to-video" + assert registry.get_model("kling-3.0-turbo-i2v").options["resolution"].allowed == ["720p", "1080p"] + assert registry.get_model("kling-3.0-turbo-i2v").options["duration"].min == 3 + assert registry.get_model("kling-3.0-turbo-i2v").options["duration"].max == 15 + assert "sound" not in registry.get_model("kling-3.0-turbo-i2v").options + assert "mode" not in registry.get_model("kling-3.0-turbo-i2v").options assert registry.get_model("kling-3.0-t2v").options["mode"].allowed == ["std", "pro", "4K"] assert registry.get_model("kling-3.0-t2v").options["mode"].hidden_from_studio is False assert registry.get_model("kling-3.0-t2v").options["mode"].label is None @@ -73,6 +83,15 @@ def test_registry_exposes_split_kling_models() -> None: ) assert registry.get_model("kling-2.6-t2v").options["duration"].allowed == [5, 10] assert registry.get_model("kling-2.6-t2v").options["aspect_ratio"].allowed == ["1:1", "16:9", "9:16"] + assert registry.get_model("kling-2.6-motion").provider_model == "kling-2.6/motion-control" + assert registry.get_model("kling-2.6-motion").inputs["image"].required_max == 1 + assert registry.get_model("kling-2.6-motion").inputs["video"].required_max == 1 + assert registry.get_model("kling-2.6-motion").options["mode"].required is True + assert registry.get_model("kling-2.6-motion").options["character_orientation"].required is True + assert ( + registry.get_model("kling-2.6-motion").prompt.default_profile_key + == "kling_2_6_motion_control_v1" + ) def test_registry_exposes_provider_capability_updates() -> None: @@ -97,7 +116,9 @@ def test_video_generation_models_expose_duration_controls() -> None: "kling-2.6-i2v": {"allowed": [5, 10]}, "kling-3.0-t2v": {"min": 3, "max": 15}, "kling-3.0-i2v": {"min": 3, "max": 15}, + "kling-3.0-turbo-i2v": {"min": 3, "max": 15}, "seedance-2.0": {"min": 4, "max": 15}, + "seedance-2.0-mini": {"min": 4, "max": 15}, } for model_key, expected in expected_duration_specs.items(): @@ -140,15 +161,25 @@ def test_registry_loads_new_prompt_preset_metadata() -> None: assert [str(item) for item in suno_preset.applies_to_input_patterns] == ["music_prompt"] assert "{{user_prompt}}" in suno_preset.template + kling_26_motion_preset = registry.get_prompt_profile("kling_2_6_motion_control_v1") + + assert kling_26_motion_preset.applies_to_models == ["kling-2.6-motion"] + assert [str(item) for item in kling_26_motion_preset.applies_to_task_modes] == ["motion_control"] + assert [str(item) for item in kling_26_motion_preset.applies_to_input_patterns] == ["motion_control"] + assert "{{user_prompt}}" in kling_26_motion_preset.template + def test_registry_exposes_field_level_provenance() -> None: registry = load_registry() motion_spec = registry.get_model("kling-3.0-motion") + kling_26_motion = registry.get_model("kling-2.6-motion") assert motion_spec.verification.field_provenance["options.mode.allowed"] == "verified_live" assert motion_spec.verification.field_provenance["options.mode.value_aliases"] == "inferred" assert motion_spec.verification.field_provenance["transport.create_path"] == "verified_docs" + assert kling_26_motion.verification.field_provenance["provider_model"] == "verified_docs" + assert kling_26_motion.verification.field_provenance["options.mode.required"] == "verified_live" def test_registry_can_load_bundled_package_specs() -> None: @@ -160,6 +191,7 @@ def test_registry_can_load_bundled_package_specs() -> None: assert registry.get_model("gpt-image-2-image-to-image").provider_model == "gpt-image-2-image-to-image" assert registry.get_model("gpt-image-2-text-to-image").provider_model == "gpt-image-2-text-to-image" assert registry.get_model("seedance-2.0-fast").provider_model == "bytedance/seedance-2-fast" + assert registry.get_model("kling-2.6-motion").provider_model == "kling-2.6/motion-control" assert registry.get_model("kling-3.0-motion").options["background_source"].type == "string" @@ -180,6 +212,10 @@ def test_latest_pricing_snapshot_loads_from_package_resources() -> None: gpt_t2i_rule = next(rule for rule in snapshot.rules if rule.model_key == "gpt-image-2-text-to-image") assert gpt_t2i_rule.pricing_status == "observed_site_pricing" assert gpt_t2i_rule.base_credits == 6 + kling_26_motion_rule = next(rule for rule in snapshot.rules if rule.model_key == "kling-2.6-motion") + assert kling_26_motion_rule.billing_unit == "second" + assert kling_26_motion_rule.base_credits == 11 + assert kling_26_motion_rule.multipliers["mode"]["1080p"] == pytest.approx(18.0 / 11.0) kling_rule = next(rule for rule in snapshot.rules if rule.model_key == "kling-3.0-t2v") assert kling_rule.multipliers["pricing_variant"]["4k_true"] == pytest.approx(67.0 / 14.0) seedance_rule = next(rule for rule in snapshot.rules if rule.model_key == "seedance-2.0") @@ -187,6 +223,12 @@ def test_latest_pricing_snapshot_loads_from_package_resources() -> None: seedance_fast_rule = next(rule for rule in snapshot.rules if rule.model_key == "seedance-2.0-fast") assert seedance_fast_rule.base_credits == 15.5 assert seedance_fast_rule.multipliers["pricing_variant"]["720p_with_video_input"] == pytest.approx(20.0 / 15.5) + seedance_mini_rule = next(rule for rule in snapshot.rules if rule.model_key == "seedance-2.0-mini") + assert seedance_mini_rule.base_credits == 9.5 + assert seedance_mini_rule.multipliers["pricing_variant"]["720p_with_video_input"] == pytest.approx(12.5 / 9.5) + kling_turbo_rule = next(rule for rule in snapshot.rules if rule.model_key == "kling-3.0-turbo-i2v") + assert kling_turbo_rule.base_credits == 18 + assert kling_turbo_rule.multipliers["resolution"]["1080p"] == pytest.approx(22.5 / 18) assert snapshot.missing_model_keys == [] assert "gpt-image-2-text-to-image" in snapshot.priced_model_keys diff --git a/tests/test_submit_client.py b/tests/test_submit_client.py index 621d6ab..870fef4 100644 --- a/tests/test_submit_client.py +++ b/tests/test_submit_client.py @@ -289,6 +289,33 @@ def test_submit_client_builds_motion_payload_with_separate_image_and_video_field assert payload["input"]["mode"] == "720p" +def test_submit_client_builds_kling_26_motion_payload() -> None: + registry = load_registry() + normalizer = RequestNormalizer(registry) + client = SubmitClient(KieSettings(api_key="test-key"), registry) + + normalized = normalizer.normalize( + RawUserRequest( + model_key="kling-2.6-motion", + prompt="The performer turns and raises one hand.", + images=["https://tempfile.redpandaai.co/kieai/183531/images/user-uploads/subject.png"], + videos=["https://kieai.redpandaai.co/kieai/183531/videos/user-uploads/motion.mp4"], + options={"mode": "720p", "character_orientation": "image"}, + ) + ) + payload = client.build_payload(normalized) + + assert payload["model"] == "kling-2.6/motion-control" + assert payload["input"]["input_urls"] == [ + "https://tempfile.redpandaai.co/kieai/183531/images/user-uploads/subject.png" + ] + assert payload["input"]["video_urls"] == [ + "https://kieai.redpandaai.co/kieai/183531/videos/user-uploads/motion.mp4" + ] + assert payload["input"]["mode"] == "720p" + assert payload["input"]["character_orientation"] == "image" + + def test_build_submission_payload_validates_motion_aliases_before_payload_build() -> None: registry = load_registry() result = validate_request( @@ -414,6 +441,53 @@ def test_submit_client_builds_seedance_fast_payload() -> None: assert payload["input"]["resolution"] == "720p" +def test_submit_client_builds_seedance_mini_payload_without_nsfw_checker() -> None: + registry = load_registry() + normalizer = RequestNormalizer(registry) + client = SubmitClient(KieSettings(api_key="test-key"), registry) + + normalized = normalizer.normalize( + RawUserRequest( + model_key="seedance-2.0-mini", + prompt="quick lower-cost product reveal with clean camera motion", + options={"duration": 5, "resolution": "720p", "aspect_ratio": "adaptive"}, + ) + ) + payload = client.build_payload(normalized) + + assert normalized.task_mode == "text_to_video" + assert payload["model"] == "bytedance/seedance-2-mini" + assert payload["input"]["resolution"] == "720p" + assert payload["input"]["aspect_ratio"] == "adaptive" + assert "nsfw_checker" not in payload["input"] + + +def test_submit_client_builds_kling_3_turbo_i2v_payload() -> None: + registry = load_registry() + normalizer = RequestNormalizer(registry) + client = SubmitClient(KieSettings(api_key="test-key"), registry) + + normalized = normalizer.normalize( + RawUserRequest( + model_key="kling-3.0-turbo-i2v", + prompt="animate the subject with a gentle handheld camera push", + images=["https://tempfile.redpandaai.co/kieai/183531/images/user-uploads/start.png"], + options={"duration": 5, "resolution": "1080p"}, + ) + ) + payload = client.build_payload(normalized) + + assert normalized.task_mode == "image_to_video" + assert payload["model"] == "kling/v3-turbo-image-to-video" + assert payload["input"]["image_urls"] == [ + "https://tempfile.redpandaai.co/kieai/183531/images/user-uploads/start.png" + ] + assert payload["input"]["duration"] == 5 + assert payload["input"]["resolution"] == "1080p" + assert "mode" not in payload["input"] + assert "sound" not in payload["input"] + + def test_submit_client_builds_suno_prompt_mode_payload() -> None: registry = load_registry() normalizer = RequestNormalizer(registry) diff --git a/tests/test_validator.py b/tests/test_validator.py index e50afa2..f805b0c 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -23,6 +23,25 @@ def test_validator_returns_needs_input_for_kling_motion_without_video() -> None: assert result.missing_inputs[0].field == "video" +def test_validator_returns_needs_input_for_kling_26_motion_without_video() -> None: + registry = load_registry() + normalizer = RequestNormalizer(registry) + validator = RequestValidator(registry) + + normalized = normalizer.normalize( + RawUserRequest( + model_key="kling-2.6-motion", + prompt="make the avatar wave", + images=["https://example.com/subject.png"], + options={"mode": "720p", "character_orientation": "image"}, + ) + ) + result = validator.validate(normalized) + + assert result.state == ValidationState.NEEDS_INPUT + assert result.missing_inputs[0].field == "video" + + def test_validator_returns_needs_input_for_kling_motion_without_image() -> None: registry = load_registry() normalizer = RequestNormalizer(registry) @@ -40,6 +59,24 @@ def test_validator_returns_needs_input_for_kling_motion_without_image() -> None: assert result.missing_inputs[0].field == "image" +def test_validator_requires_kling_26_motion_mode_and_orientation() -> None: + registry = load_registry() + normalizer = RequestNormalizer(registry) + validator = RequestValidator(registry) + + normalized = normalizer.normalize( + RawUserRequest( + model_key="kling-2.6-motion", + images=["https://example.com/subject.png"], + videos=["https://example.com/motion.mov"], + ) + ) + result = validator.validate(normalized) + + assert result.state == ValidationState.NEEDS_INPUT + assert {item.field for item in result.missing_inputs} == {"character_orientation", "mode"} + + def test_validator_requires_gpt_image_2_i2i_input_image() -> None: registry = load_registry() normalizer = RequestNormalizer(registry) @@ -705,3 +742,24 @@ def test_validator_rejects_seedance_fast_1080p_resolution() -> None: item.field == "resolution" and item.code == "invalid_enum_value" for item in result.impossible_inputs ) + + +def test_validator_rejects_seedance_mini_1080p_resolution() -> None: + registry = load_registry() + normalizer = RequestNormalizer(registry) + validator = RequestValidator(registry) + + normalized = normalizer.normalize( + RawUserRequest( + model_key="seedance-2.0-mini", + prompt="mini draft at high resolution", + options={"duration": 4, "resolution": "1080p"}, + ) + ) + result = validator.validate(normalized) + + assert result.state == ValidationState.INVALID + assert any( + item.field == "resolution" and item.code == "invalid_enum_value" + for item in result.impossible_inputs + )