mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 03:09:30 -04:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dda51ba50 | ||
|
|
cd47304db9 | ||
|
|
6543590fea | ||
|
|
786181917d | ||
|
|
31c51c5ab6 | ||
|
|
2aed062790 | ||
|
|
4d5f8e9c21 | ||
|
|
98f9504bdd | ||
|
|
7ff72d2ecc | ||
|
|
10eb062b6f | ||
|
|
3b225a7c6d |
@@ -1,5 +1,10 @@
|
||||
# ArtCraft Omni API
|
||||
|
||||
> # 📚 Full API reference: **<https://storyteller-docs.netlify.app/>**
|
||||
>
|
||||
> **Every ArtCraft API endpoint is documented there.** This guide covers only the Omni API video
|
||||
> endpoints — for everything else, start at the link above.
|
||||
|
||||
The **Omni API** is the API-key–authenticated surface for ArtCraft generation. It mirrors the
|
||||
in-app "Omni Gen" endpoints but is designed for programmatic use: you authenticate with an API key
|
||||
in the `Authorization` header instead of a browser session cookie.
|
||||
@@ -25,43 +30,21 @@ All paths below are relative to the base URL (e.g. `POST /v1/omni_api/generate/v
|
||||
The examples in this guide use the **development** base URL. To run against production, swap
|
||||
`http://localhost:12345` for `https://api.storyteller.ai`.
|
||||
|
||||
Note: Our APIs are hosted on "`api.storyteller.ai`", not "`api.getartcraft.com`". This is not a
|
||||
typo. Our Cloudflare CDN URLs are `cdn-2.fakeyou.com`.
|
||||
|
||||
---
|
||||
|
||||
## Getting an API key
|
||||
|
||||
API keys are created from a logged-in session. You only need to do this once; store the key
|
||||
somewhere safe.
|
||||
Create and manage API keys from the ArtCraft web app:
|
||||
|
||||
1. **Log in** to obtain a session (browser, or the `/v1/login` endpoint).
|
||||
2. **Create a key** by calling `POST /v1/api_keys/create` with your session:
|
||||
1. Go to **<https://app.getartcraft.com/>**
|
||||
2. Click **Account → Settings → API Keys** (your account must have API keys enabled from our staff)
|
||||
3. Create a key and copy it. The secret is shown **once** — store it somewhere safe.
|
||||
|
||||
```bash
|
||||
curl -s -c /tmp/cookies.txt -X POST http://localhost:12345/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username_or_email":"YOUR_USERNAME","password":"YOUR_PASSWORD"}'
|
||||
|
||||
curl -s -b /tmp/cookies.txt -X POST http://localhost:12345/v1/api_keys/create \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"my-integration","maybe_description":"server-to-server video gen"}'
|
||||
```
|
||||
|
||||
The response returns the secret **once** — it can never be retrieved again:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"api_key_token": "api_key_xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"api_key": "artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
- `api_key` — the **secret**. Store it now; treat it like a password. This is the value you put
|
||||
in the `Authorization` header.
|
||||
- `api_key_token` — a non-secret handle (prefix `api_key_`) used to manage the key later
|
||||
(get / update / delete). Safe to log and store.
|
||||
|
||||
> **403 Forbidden when creating a key?** API-key creation is gated per account. Contact the
|
||||
> Storyteller team to enable API access for your account.
|
||||
> **Don't see the API Keys section?** API access is gated per account. Contact the Artcraft team
|
||||
> to enable it.
|
||||
|
||||
### What an API key looks like
|
||||
|
||||
@@ -69,7 +52,7 @@ somewhere safe.
|
||||
- Followed by 40 random lowercase Crockford-base32 characters (`0-9 a-z`, excluding `i l o u`)
|
||||
- Total length: **53 characters**, e.g. `artcraft_api_3k7q9w0xv2hs5n8m4d6r1t8y0p2s4f6h8j0k2m4n6`
|
||||
|
||||
Never commit a key to source control or paste it into logs.
|
||||
Treat the key like a password: never commit it to source control or paste it into logs.
|
||||
|
||||
---
|
||||
|
||||
@@ -412,6 +395,223 @@ console.log(resp.status, await resp.json());
|
||||
|
||||
---
|
||||
|
||||
## Checking job status
|
||||
|
||||
Generation is asynchronous. The generate call returns an `inference_job_token`; poll the job-status
|
||||
endpoint until the job reaches a terminal state, then read the result URL.
|
||||
|
||||
### Endpoints
|
||||
|
||||
- **Single job:** `GET /v1/omni_api/job_status/job/{token}`
|
||||
- **Batch (many jobs):** `GET /v1/omni_api/job_status/batch?tokens=jinf_aaa&tokens=jinf_bbb`
|
||||
|
||||
Both require the same `Authorization: Bearer …` header (no cookies).
|
||||
|
||||
### Response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"state": {
|
||||
"job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"request": {
|
||||
"inference_category": "video_generation",
|
||||
"maybe_prompt_token": "prompt_xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"maybe_model_type": "seedance_2p0",
|
||||
"maybe_model_token": null
|
||||
},
|
||||
"status": {
|
||||
"status": "pending",
|
||||
"maybe_first_started_at": null,
|
||||
"maybe_failure_category": null,
|
||||
"progress_percentage": 0
|
||||
},
|
||||
"maybe_result": null,
|
||||
"created_at": "2026-06-24T04:06:14Z",
|
||||
"updated_at": "2026-06-24T04:06:14Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `state.status.status` — the job state. Keep polling while it's `pending`, `started`, or
|
||||
`attempt_failed`. **Terminal** states are `complete_success`, `complete_failure`, `dead`,
|
||||
`cancelled_by_user`, and `cancelled_by_system`.
|
||||
- `state.status.progress_percentage` — an integer 0–100.
|
||||
- `state.maybe_result` — `null` until the job succeeds. On `complete_success` it contains the
|
||||
finished media; the playable/downloadable URL is **`state.maybe_result.media_links.cdn_url`**.
|
||||
For video, `media_links.maybe_video_previews` also carries still/animated preview URLs.
|
||||
|
||||
The batch endpoint returns `{ "success": true, "job_states": [ … ] }` where each entry is the same
|
||||
`state` payload as above.
|
||||
|
||||
### Where the result video / image lives
|
||||
|
||||
When `state.status.status` is `complete_success`, the finished media is under `state.maybe_result`:
|
||||
|
||||
- **`state.maybe_result.media_links.cdn_url`** — the primary, playable/downloadable URL of the
|
||||
finished asset. **This is the field you want.** For a video job it's the `.mp4`; for an image job
|
||||
it's the image file (`.jpg` / `.png` / etc.).
|
||||
- `state.maybe_result.entity_token` — the `m_…` media-file token for the result.
|
||||
- `state.maybe_result.media_links.maybe_video_previews` — **video jobs only**. Contains a `still`
|
||||
(poster `.jpg`) and an `animated` (`.gif`) preview URL, plus `{WIDTH}` thumbnail templates. This
|
||||
is `null` for image jobs.
|
||||
- `state.maybe_result.media_links.maybe_thumbnail_template` — **image jobs only**. A URL template;
|
||||
replace `{WIDTH}` with a pixel width to fetch a resized thumbnail. `null` for video jobs.
|
||||
|
||||
**Example — completed video job:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"state": {
|
||||
"job_token": "jinf_9fcn2qv4dv3vvbrrxbnmfgen06v",
|
||||
"request": {
|
||||
"inference_category": "video_generation",
|
||||
"maybe_prompt_token": "prompt_q53tp4rh97qh8q159p21tazzt",
|
||||
"maybe_model_type": "seedance_2p0",
|
||||
"maybe_model_token": null
|
||||
},
|
||||
"status": {
|
||||
"status": "complete_success",
|
||||
"maybe_first_started_at": "2026-06-24T04:07:11Z",
|
||||
"maybe_failure_category": null,
|
||||
"progress_percentage": 100
|
||||
},
|
||||
"maybe_result": {
|
||||
"entity_type": "media_file",
|
||||
"entity_token": "m_ss682134tx19yhqs5y0nbnd0bd59a6",
|
||||
"media_links": {
|
||||
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4",
|
||||
"maybe_thumbnail_template": null,
|
||||
"maybe_video_previews": {
|
||||
"still": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.jpg",
|
||||
"animated": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.gif",
|
||||
"still_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.jpg",
|
||||
"animated_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.gif"
|
||||
}
|
||||
},
|
||||
"maybe_successfully_completed_at": "2026-06-24T04:17:30Z"
|
||||
},
|
||||
"created_at": "2026-06-24T04:06:14Z",
|
||||
"updated_at": "2026-06-24T04:17:30Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
→ The video is at
|
||||
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4`
|
||||
(example, not a live URL).
|
||||
|
||||
**Example — completed image job** (from `POST /v1/omni_api/generate/image`):
|
||||
|
||||
```json
|
||||
{
|
||||
"maybe_result": {
|
||||
"entity_type": "media_file",
|
||||
"entity_token": "m_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6",
|
||||
"media_links": {
|
||||
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg",
|
||||
"maybe_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg-thumb-{WIDTH}.jpg",
|
||||
"maybe_video_previews": null
|
||||
},
|
||||
"maybe_successfully_completed_at": "2026-06-24T04:09:02Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
→ The image is at
|
||||
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg`
|
||||
(example, not a live URL).
|
||||
|
||||
### Poll until complete
|
||||
|
||||
#### bash
|
||||
|
||||
```bash
|
||||
JOB_TOKEN="jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
while :; do
|
||||
resp=$(curl -s "http://localhost:12345/v1/omni_api/job_status/job/$JOB_TOKEN" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY")
|
||||
status=$(echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['status']['status'])")
|
||||
echo "status=$status"
|
||||
case "$status" in
|
||||
complete_success)
|
||||
echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['maybe_result']['media_links']['cdn_url'])"
|
||||
break ;;
|
||||
complete_failure|dead|cancelled_by_user|cancelled_by_system)
|
||||
echo "job ended without a result: $status"; break ;;
|
||||
esac
|
||||
sleep 5
|
||||
done
|
||||
```
|
||||
|
||||
#### python
|
||||
|
||||
```python
|
||||
import time
|
||||
import requests
|
||||
|
||||
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
|
||||
API_KEY = "YOUR_API_KEY"
|
||||
JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
TERMINAL_FAILURES = {"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system"}
|
||||
|
||||
while True:
|
||||
resp = requests.get(
|
||||
f"{BASE_URL}/v1/omni_api/job_status/job/{JOB_TOKEN}",
|
||||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||
timeout=30,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
state = resp.json()["state"]
|
||||
status = state["status"]["status"]
|
||||
print("status:", status, state["status"]["progress_percentage"], "%")
|
||||
|
||||
if status == "complete_success":
|
||||
print("result URL:", state["maybe_result"]["media_links"]["cdn_url"])
|
||||
break
|
||||
if status in TERMINAL_FAILURES:
|
||||
print("job ended without a result:", status)
|
||||
break
|
||||
time.sleep(5)
|
||||
```
|
||||
|
||||
#### javascript
|
||||
|
||||
```javascript
|
||||
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
|
||||
const API_KEY = "YOUR_API_KEY";
|
||||
const JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx";
|
||||
|
||||
const TERMINAL_FAILURES = new Set([
|
||||
"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system",
|
||||
]);
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
while (true) {
|
||||
const resp = await fetch(`${BASE_URL}/v1/omni_api/job_status/job/${JOB_TOKEN}`, {
|
||||
headers: { Authorization: `Bearer ${API_KEY}` },
|
||||
});
|
||||
const { state } = await resp.json();
|
||||
const status = state.status.status;
|
||||
console.log("status:", status, state.status.progress_percentage, "%");
|
||||
|
||||
if (status === "complete_success") {
|
||||
console.log("result URL:", state.maybe_result.media_links.cdn_url);
|
||||
break;
|
||||
}
|
||||
if (TERMINAL_FAILURES.has(status)) {
|
||||
console.log("job ended without a result:", status);
|
||||
break;
|
||||
}
|
||||
await sleep(5000);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes & troubleshooting
|
||||
|
||||
- **Always send a fresh `idempotency_token`** (UUID). Reusing one is rejected as a duplicate.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod omni_api_batch_get_job_status;
|
||||
pub mod omni_api_get_job_status;
|
||||
pub mod omni_api_job_status_payload;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::omni_api::job_status::omni_api_job_status_payload::OmniApiJobStatusPayload;
|
||||
|
||||
/// Query params for `GET /v1/omni_api/job_status/batch`.
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct OmniApiBatchGetJobStatusQueryParams {
|
||||
/// A grab bag of job tokens. Each token family generally has its own prefix,
|
||||
/// so a flat set is sufficient.
|
||||
///
|
||||
/// NB: handlers parse this with `actix_web_lab`'s `Query<T>` because the
|
||||
/// default actix-web `Query<T>` doesn't decode repeated/sequence params yet.
|
||||
/// See https://github.com/actix/actix-web/issues/1301
|
||||
pub tokens: HashSet<String>,
|
||||
}
|
||||
|
||||
/// Success body for `GET /v1/omni_api/job_status/batch`.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniApiBatchGetJobStatusSuccessResponse {
|
||||
pub success: bool,
|
||||
pub job_states: Vec<OmniApiJobStatusPayload>,
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
use crate::omni_api::job_status::omni_api_job_status_payload::OmniApiJobStatusPayload;
|
||||
|
||||
/// Path params for `GET /v1/omni_api/job_status/job/{token}`.
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct OmniApiGetJobStatusPathInfo {
|
||||
pub token: InferenceJobToken,
|
||||
}
|
||||
|
||||
/// Success body for `GET /v1/omni_api/job_status/job/{token}`.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniApiGetJobStatusSuccessResponse {
|
||||
pub success: bool,
|
||||
pub state: OmniApiJobStatusPayload,
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
|
||||
use crate::common::responses::media_links::MediaLinks;
|
||||
|
||||
/// Full status payload for a single inference job.
|
||||
///
|
||||
/// Shared by both the single (`/v1/omni_api/job_status/job/{token}`) and batch
|
||||
/// (`/v1/omni_api/job_status/batch`) Omni API status endpoints. Forked from the
|
||||
/// cookie-authenticated `/v1/jobs/*` response types so the Omni API surface can
|
||||
/// evolve independently.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniApiJobStatusPayload {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
pub request: OmniApiJobRequestDetails,
|
||||
pub status: OmniApiJobStatusDetails,
|
||||
pub maybe_result: Option<OmniApiJobResultDetails>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Details about what the user requested for generation.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniApiJobRequestDetails {
|
||||
pub inference_category: InferenceCategory,
|
||||
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
pub maybe_model_type: Option<String>,
|
||||
pub maybe_model_token: Option<String>,
|
||||
}
|
||||
|
||||
/// Details about the ongoing job status.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniApiJobStatusDetails {
|
||||
/// Primary status from the database (a state machine).
|
||||
pub status: JobStatusPlus,
|
||||
|
||||
pub maybe_first_started_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// An enum the frontend can use to display localized/I18N error
|
||||
/// messages. These pertain to both transient and permanent failures.
|
||||
pub maybe_failure_category: Option<FrontendFailureCategory>,
|
||||
|
||||
/// This is an integer number between 0 and 100 (both inclusive) that
|
||||
/// reports the completeness.
|
||||
pub progress_percentage: u8,
|
||||
}
|
||||
|
||||
/// Details about the completed result (if any).
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniApiJobResultDetails {
|
||||
pub entity_type: String,
|
||||
pub entity_token: String,
|
||||
|
||||
/// Rich CDN links to the media, including thumbnails, previews, and more.
|
||||
pub media_links: MediaLinks,
|
||||
|
||||
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod generate_requests;
|
||||
pub mod job_status;
|
||||
pub mod omni_upload_audio;
|
||||
pub mod omni_upload_image;
|
||||
pub mod omni_upload_video;
|
||||
|
||||
@@ -65,6 +65,15 @@ pub enum RouterVideoModel {
|
||||
#[serde(rename = "seedance_2p0_bpu_fast")]
|
||||
Seedance2p0BytePlusUltraFast,
|
||||
|
||||
#[serde(rename = "seedance_2p0_mini")]
|
||||
Seedance2p0Mini,
|
||||
|
||||
#[serde(rename = "seedance_2p0_bp_mini")]
|
||||
Seedance2p0BytePlusMini,
|
||||
|
||||
#[serde(rename = "seedance_2p0_bpu_mini")]
|
||||
Seedance2p0BytePlusUltraMini,
|
||||
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
|
||||
@@ -95,3 +104,23 @@ pub enum RouterVideoModel {
|
||||
#[serde(rename = "preview_model_fast")]
|
||||
PreviewModelFast,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_serde_round_trip(model: RouterVideoModel, expected: &str) {
|
||||
let json = serde_json::to_string(&model).unwrap();
|
||||
assert_eq!(json, format!("\"{expected}\""));
|
||||
let parsed: RouterVideoModel = serde_json::from_str(&json).unwrap();
|
||||
// RouterVideoModel isn't PartialEq, so round-trip back to the wire form.
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seedance_2p0_mini_variants_serialize() {
|
||||
assert_serde_round_trip(RouterVideoModel::Seedance2p0Mini, "seedance_2p0_mini");
|
||||
assert_serde_round_trip(RouterVideoModel::Seedance2p0BytePlusMini, "seedance_2p0_bp_mini");
|
||||
assert_serde_round_trip(RouterVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini");
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -32,6 +32,9 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp::build
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_fast::build::build_artcraft_seedance_2p0_bp_fast;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu::build::build_artcraft_seedance_2p0_bpu;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_fast::build::build_artcraft_seedance_2p0_bpu_fast;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::build::build_artcraft_seedance_2p0_mini;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_mini::build::build_artcraft_seedance_2p0_bp_mini;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_mini::build::build_artcraft_seedance_2p0_bpu_mini;
|
||||
use crate::generate::generate_video::providers::artcraft::sora_2::build::build_artcraft_sora_2;
|
||||
use crate::generate::generate_video::providers::artcraft::sora_2_pro::build::build_artcraft_sora_2_pro;
|
||||
use crate::generate::generate_video::providers::artcraft::veo_2::build::build_artcraft_veo_2;
|
||||
@@ -62,6 +65,7 @@ use crate::generate::generate_video::providers::fal::veo_3_fast::build::build_fa
|
||||
use crate::generate::generate_video::providers::fal::veo_3p1::build::build_fal_veo_3p1;
|
||||
use crate::generate::generate_video::providers::fal::veo_3p1_fast::build::build_fal_veo_3p1_fast;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::build::build_kinovi_seedance_2p0_mini;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
/// RouterProvider-agnostic video generation request. Distilled by `build2()` into a
|
||||
@@ -176,6 +180,9 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusFast) => build_artcraft_seedance_2p0_bp_fast(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltra) => build_artcraft_seedance_2p0_bpu(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltraFast) => build_artcraft_seedance_2p0_bpu_fast(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Mini) => build_artcraft_seedance_2p0_mini(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusMini) => build_artcraft_seedance_2p0_bp_mini(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_artcraft_seedance_2p0_bpu_mini(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::PreviewModel) => build_artcraft_preview_model(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance10Lite) => build_artcraft_seedance_1p0_lite(self),
|
||||
@@ -214,6 +221,9 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0Mini) => build_kinovi_seedance_2p0_mini(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0BytePlusMini) => build_kinovi_seedance_2p0_mini(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_kinovi_seedance_2p0_mini(self),
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
}
|
||||
}
|
||||
|
||||
+18
-2
@@ -21,6 +21,8 @@ use crate::generate::generate_video::providers::artcraft::resolve::{
|
||||
pub enum SupportedResolutions {
|
||||
/// 480p, 720p, 1080p
|
||||
Full,
|
||||
/// 480p, 720p, 1080p, 4K
|
||||
FullWith4k,
|
||||
/// 480p, 720p only (1080p downgrades to 720p)
|
||||
Fast,
|
||||
/// 720p, 1080p only (480p upgrades)
|
||||
@@ -157,7 +159,7 @@ fn plan_output_resolution(
|
||||
None => Ok(None),
|
||||
|
||||
Some(RouterResolution::FourEightyP) => match supported {
|
||||
SupportedResolutions::Full | SupportedResolutions::Fast => {
|
||||
SupportedResolutions::Full | SupportedResolutions::FullWith4k | SupportedResolutions::Fast => {
|
||||
Ok(Some(CommonResolutionEnum::FourEightyP))
|
||||
}
|
||||
SupportedResolutions::NoFourEightyP => match strategy {
|
||||
@@ -175,7 +177,7 @@ fn plan_output_resolution(
|
||||
Some(RouterResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
|
||||
Some(RouterResolution::TenEightyP) => match supported {
|
||||
SupportedResolutions::Full | SupportedResolutions::NoFourEightyP => {
|
||||
SupportedResolutions::Full | SupportedResolutions::FullWith4k | SupportedResolutions::NoFourEightyP => {
|
||||
Ok(Some(CommonResolutionEnum::TenEightyP))
|
||||
}
|
||||
SupportedResolutions::Fast => match strategy {
|
||||
@@ -189,6 +191,20 @@ fn plan_output_resolution(
|
||||
},
|
||||
},
|
||||
|
||||
// 4K is only available on models flagged `FullWith4k` (the non-Fast Seedance
|
||||
// 2.0 family). Other models error out or fall back to their best resolution.
|
||||
Some(RouterResolution::FourK) => match (supported, strategy) {
|
||||
(SupportedResolutions::FullWith4k, _) => Ok(Some(CommonResolutionEnum::FourK)),
|
||||
(_, RequestMismatchMitigationStrategy::ErrorOut) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", RouterResolution::FourK),
|
||||
}))
|
||||
}
|
||||
(SupportedResolutions::Fast, _) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
(_, _) => Ok(Some(CommonResolutionEnum::TenEightyP)),
|
||||
},
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
|
||||
+4
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod build_common;
|
||||
pub(crate) mod seedance_common;
|
||||
#[cfg(test)]
|
||||
mod parity_tests;
|
||||
pub mod grok_imagine_video;
|
||||
@@ -25,6 +26,9 @@ pub mod seedance_2p0_bp;
|
||||
pub mod seedance_2p0_bp_fast;
|
||||
pub mod seedance_2p0_bpu;
|
||||
pub mod seedance_2p0_bpu_fast;
|
||||
pub mod seedance_2p0_mini;
|
||||
pub mod seedance_2p0_bp_mini;
|
||||
pub mod seedance_2p0_bpu_mini;
|
||||
pub mod sora_2;
|
||||
pub mod sora_2_pro;
|
||||
pub mod veo_2;
|
||||
|
||||
+7
-1
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0(builder: GenerateVideoRequestBuilder) -> Resu
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0,
|
||||
SupportedResolutions::Full,
|
||||
SupportedResolutions::FullWith4k,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0RequestState { request };
|
||||
@@ -105,6 +105,12 @@ mod tests {
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_4k() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourK));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
|
||||
+62
-2
@@ -1,7 +1,8 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// ── Pricing constants ──
|
||||
//
|
||||
@@ -47,6 +48,23 @@ impl ArtcraftSeedance2p0CostState {
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
if self.resolution == CommonResolution::FourK {
|
||||
let usd_cents = seedance_2p0_four_k_usd_cents(
|
||||
self.duration_seconds,
|
||||
self.batch_count,
|
||||
self.has_video_reference,
|
||||
);
|
||||
return VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
};
|
||||
}
|
||||
|
||||
let cents_per_second = match self.resolution {
|
||||
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
|
||||
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
|
||||
@@ -71,8 +89,8 @@ impl ArtcraftSeedance2p0CostState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
// -- 720p pricing --
|
||||
@@ -157,6 +175,40 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- 4K pricing --
|
||||
|
||||
mod four_k_pricing {
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
fn artcraft_4k_cents(duration_seconds: u16, batch_count: u16, has_video_reference: bool) -> u64 {
|
||||
super::super::ArtcraftSeedance2p0CostState {
|
||||
resolution: CommonResolution::FourK,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
has_video_reference,
|
||||
}
|
||||
.estimate_cost()
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_4k_without_video_reference() {
|
||||
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
|
||||
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
|
||||
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
|
||||
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_4k_with_video_reference() {
|
||||
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
|
||||
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
|
||||
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
|
||||
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Relative pricing --
|
||||
|
||||
mod relative_pricing_tests {
|
||||
@@ -171,6 +223,13 @@ mod tests {
|
||||
assert!(c720 < c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_4k_more_expensive_than_1080p() {
|
||||
let c1080 = cost_cents(Some(RouterResolution::TenEightyP), 5, 1);
|
||||
let c4k = cost_cents(Some(RouterResolution::FourK), 5, 1);
|
||||
assert!(c4k > c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = cost_cents(Some(RouterResolution::SevenTwentyP), 4, 1);
|
||||
@@ -201,6 +260,7 @@ mod tests {
|
||||
Some(RouterResolution::FourEightyP),
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
Some(RouterResolution::TenEightyP),
|
||||
Some(RouterResolution::FourK),
|
||||
None,
|
||||
];
|
||||
for res in resolutions {
|
||||
|
||||
+7
-1
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0_bp(builder: GenerateVideoRequestBuilder) -> R
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0BytePlus,
|
||||
SupportedResolutions::Full,
|
||||
SupportedResolutions::FullWith4k,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0BytePlusRequestState { request };
|
||||
@@ -101,6 +101,12 @@ mod tests {
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_4k() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourK));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
|
||||
+57
-3
@@ -1,7 +1,8 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
/// USD cents per second by resolution:
|
||||
/// 480p: $0.10/s = 10.0 ¢/s
|
||||
@@ -15,6 +16,7 @@ pub struct ArtcraftSeedance2p0BytePlusCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusCostState {
|
||||
@@ -23,10 +25,30 @@ impl ArtcraftSeedance2p0BytePlusCostState {
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
Self { resolution, duration_seconds, batch_count }
|
||||
let has_video_reference = request.request.reference_video_media_tokens
|
||||
.as_ref()
|
||||
.is_some_and(|tokens| !tokens.is_empty());
|
||||
Self { resolution, duration_seconds, batch_count, has_video_reference }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
if self.resolution == CommonResolution::FourK {
|
||||
let usd_cents = seedance_2p0_four_k_usd_cents(
|
||||
self.duration_seconds,
|
||||
self.batch_count,
|
||||
self.has_video_reference,
|
||||
);
|
||||
return VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
};
|
||||
}
|
||||
|
||||
let cents_per_second = match self.resolution {
|
||||
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
|
||||
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
|
||||
@@ -49,9 +71,9 @@ impl ArtcraftSeedance2p0BytePlusCostState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
mod pricing_720p {
|
||||
@@ -125,6 +147,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod four_k_pricing {
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
fn artcraft_4k_cents(duration_seconds: u16, batch_count: u16, has_video_reference: bool) -> u64 {
|
||||
super::super::ArtcraftSeedance2p0BytePlusCostState {
|
||||
resolution: CommonResolution::FourK,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
has_video_reference,
|
||||
}
|
||||
.estimate_cost()
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_4k_without_video_reference() {
|
||||
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
|
||||
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
|
||||
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
|
||||
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_4k_with_video_reference() {
|
||||
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
|
||||
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
|
||||
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
|
||||
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
|
||||
}
|
||||
}
|
||||
|
||||
mod relative_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_mini::request::ArtcraftSeedance2p0BytePlusMiniRequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_bp_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
// Seedance 2.0 Mini supports 480p and 720p only (higher resolutions downgrade
|
||||
// to 720p), and all six aspect ratios including 21:9.
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0BytePlusMini,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0BytePlusMiniRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(state)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_bitrate::RouterBitrate;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_is_correct() {
|
||||
let req = unwrap_request(builder_with(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0BytePlusMini)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_clamped_to_platform_max() {
|
||||
// The ArtCraft platform caps batch at 4 and downgrades higher requests.
|
||||
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(8); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitrate_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.bitrate = Some(RouterBitrate::High); }));
|
||||
assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High));
|
||||
}
|
||||
}
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wide() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideSixteenByNine); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::WideSixteenByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ultra_wide() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideTwentyOneByNine); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::WideTwentyOneByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::TallNineBySixteen); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::TallNineBySixteen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::Square); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::Square));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_error_out() {
|
||||
let result = build_artcraft_seedance_2p0_bp_mini(GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution: Some(RouterResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(builder_with(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(builder_with(|b| { b.start_frame = Some(ImageRef::MediaFileToken(token.clone())); }));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_seedance_2p0_bp_mini(GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
MediaFileToken::new("mf_a".to_string()),
|
||||
MediaFileToken::new("mf_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(builder_with(|b| { b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone())); }));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
CharacterToken::new("char_a".to_string()),
|
||||
CharacterToken::new("char_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(builder_with(|b| { b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone())); }));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0BytePlusMiniRequestState {
|
||||
let result = build_artcraft_seedance_2p0_bp_mini(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(state)) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p0BytePlusMini request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_byteplus_mini_usd_cents;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_mini::request::ArtcraftSeedance2p0BytePlusMiniRequestState;
|
||||
|
||||
pub struct ArtcraftSeedance2p0BytePlusMiniCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusMiniCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p0BytePlusMiniRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
let has_video_reference = request.request.reference_video_media_tokens
|
||||
.as_ref()
|
||||
.is_some_and(|tokens| !tokens.is_empty());
|
||||
|
||||
Self { resolution, duration_seconds, batch_count, has_video_reference }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// BytePlus / BytePlus Ultra Mini share a higher-margin rate set; see
|
||||
// seedance_common::seedance_2p0_byteplus_mini_usd_cents.
|
||||
let usd_cents = seedance_2p0_byteplus_mini_usd_cents(
|
||||
self.resolution,
|
||||
self.duration_seconds,
|
||||
self.batch_count,
|
||||
self.has_video_reference,
|
||||
);
|
||||
|
||||
// ArtCraft credits: 100 credits = $1.00, so credits = cents.
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// Every combination, driven through the full build pipeline. These prices are
|
||||
// the supplier's per-combo cost plus a 5% margin, rounded up to whole cents.
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, false), 14);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 17);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, false), 33);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, false), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, true), 17);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, true), 21);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, true), 42);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, true), 63);
|
||||
}
|
||||
}
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, false), 35);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 44);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, false), 88);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, false), 131);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, true), 42);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, true), 53);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, true), 105);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, true), 158);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(cents_no_ref(None, 5, 1), cents(RouterResolution::SevenTwentyP, 5, 1, false));
|
||||
}
|
||||
}
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents_all_combos() {
|
||||
let resolutions = [
|
||||
Some(RouterResolution::FourEightyP),
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
None,
|
||||
];
|
||||
for res in resolutions {
|
||||
for dur in [4u16, 5, 10, 15] {
|
||||
for batch in [1u16, 2, 4] {
|
||||
for has_ref in [false, true] {
|
||||
let cost = build_cost(res, dur, batch, has_ref);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
with_video_reference: bool,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let reference_videos = with_video_reference.then(|| {
|
||||
VideoListRef::MediaFileTokens(vec![MediaFileToken::new("mf_ref".to_string())])
|
||||
});
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
reference_videos,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
}
|
||||
|
||||
fn cents(
|
||||
resolution: RouterResolution,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
with_video_reference: bool,
|
||||
) -> u64 {
|
||||
build_cost(Some(resolution), duration_seconds, video_batch_count, with_video_reference)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn cents_no_ref(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> u64 {
|
||||
build_cost(resolution, duration_seconds, video_batch_count, false)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p0BytePlusMiniRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusMiniRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_video_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn text_to_video() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()),
|
||||
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
|
||||
..mini_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn res_480p() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A shiba inu playing in autumn leaves.".to_string()),
|
||||
resolution: Some(RouterResolution::FourEightyP),
|
||||
..mini_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
fn mini_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(4),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
|
||||
let client = get_artcraft_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
VideoGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
request.send_request(&client).await.expect("send_request should succeed")
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0_bpu(builder: GenerateVideoRequestBuilder) ->
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0BytePlusUltra,
|
||||
SupportedResolutions::Full,
|
||||
SupportedResolutions::FullWith4k,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0BytePlusUltraRequestState { request };
|
||||
@@ -101,6 +101,12 @@ mod tests {
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_4k() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourK));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
|
||||
+57
-3
@@ -1,7 +1,8 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu::request::ArtcraftSeedance2p0BytePlusUltraRequestState;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
/// USD cents per second by resolution:
|
||||
/// 480p: $0.10/s = 10.0 ¢/s
|
||||
@@ -15,6 +16,7 @@ pub struct ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
@@ -23,10 +25,30 @@ impl ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
Self { resolution, duration_seconds, batch_count }
|
||||
let has_video_reference = request.request.reference_video_media_tokens
|
||||
.as_ref()
|
||||
.is_some_and(|tokens| !tokens.is_empty());
|
||||
Self { resolution, duration_seconds, batch_count, has_video_reference }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
if self.resolution == CommonResolution::FourK {
|
||||
let usd_cents = seedance_2p0_four_k_usd_cents(
|
||||
self.duration_seconds,
|
||||
self.batch_count,
|
||||
self.has_video_reference,
|
||||
);
|
||||
return VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
};
|
||||
}
|
||||
|
||||
let cents_per_second = match self.resolution {
|
||||
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
|
||||
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
|
||||
@@ -49,9 +71,9 @@ impl ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
mod pricing_720p {
|
||||
@@ -125,6 +147,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod four_k_pricing {
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
fn artcraft_4k_cents(duration_seconds: u16, batch_count: u16, has_video_reference: bool) -> u64 {
|
||||
super::super::ArtcraftSeedance2p0BytePlusUltraCostState {
|
||||
resolution: CommonResolution::FourK,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
has_video_reference,
|
||||
}
|
||||
.estimate_cost()
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_4k_without_video_reference() {
|
||||
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
|
||||
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
|
||||
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
|
||||
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_4k_with_video_reference() {
|
||||
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
|
||||
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
|
||||
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
|
||||
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
|
||||
}
|
||||
}
|
||||
|
||||
mod relative_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_mini::request::ArtcraftSeedance2p0BytePlusUltraMiniRequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_bpu_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
// Seedance 2.0 Mini supports 480p and 720p only (higher resolutions downgrade
|
||||
// to 720p), and all six aspect ratios including 21:9.
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0BytePlusUltraMini,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0BytePlusUltraMiniRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraMini(state)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_bitrate::RouterBitrate;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_is_correct() {
|
||||
let req = unwrap_request(builder_with(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0BytePlusUltraMini)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_clamped_to_platform_max() {
|
||||
// The ArtCraft platform caps batch at 4 and downgrades higher requests.
|
||||
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(8); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitrate_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.bitrate = Some(RouterBitrate::High); }));
|
||||
assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High));
|
||||
}
|
||||
}
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wide() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideSixteenByNine); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::WideSixteenByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ultra_wide() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideTwentyOneByNine); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::WideTwentyOneByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::TallNineBySixteen); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::TallNineBySixteen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::Square); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::Square));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_error_out() {
|
||||
let result = build_artcraft_seedance_2p0_bpu_mini(GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusUltraMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution: Some(RouterResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(builder_with(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(builder_with(|b| { b.start_frame = Some(ImageRef::MediaFileToken(token.clone())); }));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_seedance_2p0_bpu_mini(GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusUltraMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
MediaFileToken::new("mf_a".to_string()),
|
||||
MediaFileToken::new("mf_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(builder_with(|b| { b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone())); }));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
CharacterToken::new("char_a".to_string()),
|
||||
CharacterToken::new("char_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(builder_with(|b| { b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone())); }));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusUltraMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0BytePlusUltraMiniRequestState {
|
||||
let result = build_artcraft_seedance_2p0_bpu_mini(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraMini(state)) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p0BytePlusUltraMini request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_byteplus_mini_usd_cents;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_mini::request::ArtcraftSeedance2p0BytePlusUltraMiniRequestState;
|
||||
|
||||
pub struct ArtcraftSeedance2p0BytePlusUltraMiniCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusUltraMiniCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p0BytePlusUltraMiniRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
let has_video_reference = request.request.reference_video_media_tokens
|
||||
.as_ref()
|
||||
.is_some_and(|tokens| !tokens.is_empty());
|
||||
|
||||
Self { resolution, duration_seconds, batch_count, has_video_reference }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// BytePlus / BytePlus Ultra Mini share a higher-margin rate set; see
|
||||
// seedance_common::seedance_2p0_byteplus_mini_usd_cents.
|
||||
let usd_cents = seedance_2p0_byteplus_mini_usd_cents(
|
||||
self.resolution,
|
||||
self.duration_seconds,
|
||||
self.batch_count,
|
||||
self.has_video_reference,
|
||||
);
|
||||
|
||||
// ArtCraft credits: 100 credits = $1.00, so credits = cents.
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// Every combination, driven through the full build pipeline. These prices are
|
||||
// the supplier's per-combo cost plus a 5% margin, rounded up to whole cents.
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, false), 14);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 17);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, false), 33);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, false), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, true), 17);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, true), 21);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, true), 42);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, true), 63);
|
||||
}
|
||||
}
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, false), 35);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 44);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, false), 88);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, false), 131);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, true), 42);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, true), 53);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, true), 105);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, true), 158);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(cents_no_ref(None, 5, 1), cents(RouterResolution::SevenTwentyP, 5, 1, false));
|
||||
}
|
||||
}
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents_all_combos() {
|
||||
let resolutions = [
|
||||
Some(RouterResolution::FourEightyP),
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
None,
|
||||
];
|
||||
for res in resolutions {
|
||||
for dur in [4u16, 5, 10, 15] {
|
||||
for batch in [1u16, 2, 4] {
|
||||
for has_ref in [false, true] {
|
||||
let cost = build_cost(res, dur, batch, has_ref);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
with_video_reference: bool,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let reference_videos = with_video_reference.then(|| {
|
||||
VideoListRef::MediaFileTokens(vec![MediaFileToken::new("mf_ref".to_string())])
|
||||
});
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusUltraMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
reference_videos,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
}
|
||||
|
||||
fn cents(
|
||||
resolution: RouterResolution,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
with_video_reference: bool,
|
||||
) -> u64 {
|
||||
build_cost(Some(resolution), duration_seconds, video_batch_count, with_video_reference)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn cents_no_ref(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> u64 {
|
||||
build_cost(resolution, duration_seconds, video_batch_count, false)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p0BytePlusUltraMiniRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusUltraMiniRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_video_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn text_to_video() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()),
|
||||
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
|
||||
..mini_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn res_480p() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A shiba inu playing in autumn leaves.".to_string()),
|
||||
resolution: Some(RouterResolution::FourEightyP),
|
||||
..mini_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
fn mini_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0BytePlusUltraMini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(4),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
|
||||
let client = get_artcraft_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
VideoGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
request.send_request(&client).await.expect("send_request should succeed")
|
||||
}
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::request::ArtcraftSeedance2p0MiniRequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
// Seedance 2.0 Mini supports 480p and 720p only (higher resolutions downgrade
|
||||
// to 720p), and all six aspect ratios including 21:9.
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0Mini,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0MiniRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Mini(state)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_bitrate::RouterBitrate;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_is_correct() {
|
||||
let req = unwrap_request(builder_with(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0Mini)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_clamped_to_platform_max() {
|
||||
// The ArtCraft platform caps batch at 4 and downgrades higher requests.
|
||||
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(8); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitrate_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.bitrate = Some(RouterBitrate::High); }));
|
||||
assert_eq!(req.request.bitrate, Some(CommonBitrateEnum::High));
|
||||
}
|
||||
}
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wide() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideSixteenByNine); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::WideSixteenByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ultra_wide() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideTwentyOneByNine); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::WideTwentyOneByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::TallNineBySixteen); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::TallNineBySixteen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square() {
|
||||
let req = unwrap_request(builder_with(|b| { b.aspect_ratio = Some(RouterAspectRatio::Square); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::Square));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_error_out() {
|
||||
let result = build_artcraft_seedance_2p0_mini(GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0Mini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution: Some(RouterResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(builder_with(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(builder_with(|b| { b.start_frame = Some(ImageRef::MediaFileToken(token.clone())); }));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_seedance_2p0_mini(GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0Mini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
MediaFileToken::new("mf_a".to_string()),
|
||||
MediaFileToken::new("mf_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(builder_with(|b| { b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone())); }));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
CharacterToken::new("char_a".to_string()),
|
||||
CharacterToken::new("char_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(builder_with(|b| { b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone())); }));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0Mini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0MiniRequestState {
|
||||
let result = build_artcraft_seedance_2p0_mini(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Mini(state)) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p0Mini request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_mini_usd_cents;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::request::ArtcraftSeedance2p0MiniRequestState;
|
||||
|
||||
pub struct ArtcraftSeedance2p0MiniCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0MiniCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p0MiniRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
let has_video_reference = request.request.reference_video_media_tokens
|
||||
.as_ref()
|
||||
.is_some_and(|tokens| !tokens.is_empty());
|
||||
|
||||
Self { resolution, duration_seconds, batch_count, has_video_reference }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// Seedance 2.0 Mini is priced uniformly across its variants; see
|
||||
// seedance_common::seedance_2p0_mini_usd_cents.
|
||||
let usd_cents = seedance_2p0_mini_usd_cents(
|
||||
self.resolution,
|
||||
self.duration_seconds,
|
||||
self.batch_count,
|
||||
self.has_video_reference,
|
||||
);
|
||||
|
||||
// ArtCraft credits: 100 credits = $1.00, so credits = cents.
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// Every combination, driven through the full build pipeline. These prices are
|
||||
// the supplier's per-combo cost plus a 5% margin, rounded up to whole cents.
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, false), 13);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, false), 17);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, false), 33);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, false), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 4, 1, true), 17);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 5, 1, true), 21);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 10, 1, true), 42);
|
||||
assert_eq!(cents(RouterResolution::FourEightyP, 15, 1, true), 62);
|
||||
}
|
||||
}
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, false), 35);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, false), 44);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, false), 87);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, false), 130);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_video_reference() {
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 4, 1, true), 42);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 5, 1, true), 52);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 10, 1, true), 104);
|
||||
assert_eq!(cents(RouterResolution::SevenTwentyP, 15, 1, true), 156);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(cents_no_ref(None, 5, 1), cents(RouterResolution::SevenTwentyP, 5, 1, false));
|
||||
}
|
||||
}
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents_all_combos() {
|
||||
let resolutions = [
|
||||
Some(RouterResolution::FourEightyP),
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
None,
|
||||
];
|
||||
for res in resolutions {
|
||||
for dur in [4u16, 5, 10, 15] {
|
||||
for batch in [1u16, 2, 4] {
|
||||
for has_ref in [false, true] {
|
||||
let cost = build_cost(res, dur, batch, has_ref);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
with_video_reference: bool,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let reference_videos = with_video_reference.then(|| {
|
||||
VideoListRef::MediaFileTokens(vec![MediaFileToken::new("mf_ref".to_string())])
|
||||
});
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0Mini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
reference_videos,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
}
|
||||
|
||||
fn cents(
|
||||
resolution: RouterResolution,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
with_video_reference: bool,
|
||||
) -> u64 {
|
||||
build_cost(Some(resolution), duration_seconds, video_batch_count, with_video_reference)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn cents_no_ref(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> u64 {
|
||||
build_cost(resolution, duration_seconds, video_batch_count, false)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p0MiniRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0MiniRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_video_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn text_to_video() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()),
|
||||
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
|
||||
..mini_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn res_480p() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A shiba inu playing in autumn leaves.".to_string()),
|
||||
resolution: Some(RouterResolution::FourEightyP),
|
||||
..mini_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Artcraft(_)));
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
fn mini_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0Mini,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(4),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_artcraft_client() -> RouterClient {
|
||||
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
|
||||
.expect("Failed to read artcraft cookies");
|
||||
let cookies = cookies.trim().to_string();
|
||||
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
|
||||
.expect("Failed to parse cookies")
|
||||
.expect("No credentials found");
|
||||
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
|
||||
let client = get_artcraft_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
VideoGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Artcraft skips draft)"),
|
||||
};
|
||||
request.send_request(&client).await.expect("send_request should succeed")
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0_u(builder: GenerateVideoRequestBuilder) -> Re
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0Ultra,
|
||||
SupportedResolutions::Full,
|
||||
SupportedResolutions::FullWith4k,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0UltraRequestState { request };
|
||||
@@ -105,6 +105,12 @@ mod tests {
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_4k() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourK));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
|
||||
+57
-3
@@ -1,7 +1,8 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u::request::ArtcraftSeedance2p0UltraRequestState;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// ── Pricing constants ──
|
||||
|
||||
@@ -13,6 +14,7 @@ pub struct ArtcraftSeedance2p0UltraCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0UltraCostState {
|
||||
@@ -21,11 +23,31 @@ impl ArtcraftSeedance2p0UltraCostState {
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
let has_video_reference = request.request.reference_video_media_tokens
|
||||
.as_ref()
|
||||
.is_some_and(|tokens| !tokens.is_empty());
|
||||
|
||||
Self { resolution, duration_seconds, batch_count }
|
||||
Self { resolution, duration_seconds, batch_count, has_video_reference }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
if self.resolution == CommonResolution::FourK {
|
||||
let usd_cents = seedance_2p0_four_k_usd_cents(
|
||||
self.duration_seconds,
|
||||
self.batch_count,
|
||||
self.has_video_reference,
|
||||
);
|
||||
return VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
};
|
||||
}
|
||||
|
||||
let cents_per_second = match self.resolution {
|
||||
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
|
||||
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
|
||||
@@ -49,9 +71,9 @@ impl ArtcraftSeedance2p0UltraCostState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
mod pricing_720p {
|
||||
@@ -122,6 +144,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod four_k_pricing {
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
fn artcraft_4k_cents(duration_seconds: u16, batch_count: u16, has_video_reference: bool) -> u64 {
|
||||
super::super::ArtcraftSeedance2p0UltraCostState {
|
||||
resolution: CommonResolution::FourK,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
has_video_reference,
|
||||
}
|
||||
.estimate_cost()
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_4k_without_video_reference() {
|
||||
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
|
||||
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
|
||||
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
|
||||
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_4k_with_video_reference() {
|
||||
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
|
||||
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
|
||||
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
|
||||
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
|
||||
}
|
||||
}
|
||||
|
||||
mod relative_pricing {
|
||||
use super::*;
|
||||
|
||||
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
//! Shared pricing for the Seedance 2.0 family of ArtCraft providers.
|
||||
//!
|
||||
//! ArtCraft credits equal USD cents (100 credits = $1.00).
|
||||
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
// 4K is priced uniformly across the non-Fast Seedance 2.0 models. Rates are held
|
||||
// in hundredths of a USD cent per second so the math is exact integer arithmetic
|
||||
// (no floating point), then rounded up to whole cents.
|
||||
|
||||
/// 4K output price, in hundredths of a USD cent per second (86.60 ¢/s).
|
||||
const FOUR_K_CENTI_CENTS_PER_SECOND: u64 = 8660;
|
||||
|
||||
/// Extra hundredths of a USD cent per second when a reference video is attached
|
||||
/// at 4K (17.20 ¢/s surcharge).
|
||||
const FOUR_K_VIDEO_REFERENCE_SURCHARGE_CENTI_CENTS_PER_SECOND: u64 = 1720;
|
||||
|
||||
/// ArtCraft's user-facing price (USD cents) for Seedance 2.0 at 4K.
|
||||
///
|
||||
/// A reference video adds a per-second surcharge (4K with and without a video
|
||||
/// reference are priced differently). The cost scales with duration and batch
|
||||
/// count, rounded up to whole cents.
|
||||
pub fn seedance_2p0_four_k_usd_cents(
|
||||
duration_seconds: u16,
|
||||
batch_count: u16,
|
||||
has_video_reference: bool,
|
||||
) -> u64 {
|
||||
let mut centi_cents_per_second = FOUR_K_CENTI_CENTS_PER_SECOND;
|
||||
if has_video_reference {
|
||||
centi_cents_per_second += FOUR_K_VIDEO_REFERENCE_SURCHARGE_CENTI_CENTS_PER_SECOND;
|
||||
}
|
||||
|
||||
let total_centi_cents =
|
||||
centi_cents_per_second * duration_seconds as u64 * batch_count as u64;
|
||||
|
||||
// Round up to whole cents.
|
||||
total_centi_cents.div_ceil(100)
|
||||
}
|
||||
|
||||
// ── Seedance 2.0 Mini pricing ──
|
||||
//
|
||||
// Mini offers only 480p and 720p. The rates below are the per-second USER price
|
||||
// in USD cents (margin already included); a reference video adds a per-second
|
||||
// surcharge. Rates are fractional, so the total is rounded UP to a whole cent
|
||||
// once at the end (after multiplying by duration × batch).
|
||||
//
|
||||
// The regular Mini and the BytePlus / BytePlus Ultra Minis carry DIFFERENT
|
||||
// margins, so they have separate rate sets and helpers below.
|
||||
|
||||
/// Regular Mini — 480p price, USD cents per second.
|
||||
const MINI_CENTS_PER_SECOND_480P: f64 = 3.24074074;
|
||||
/// Regular Mini — 480p reference-video surcharge, USD cents per second.
|
||||
const MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_480P: f64 = 0.86419753;
|
||||
/// Regular Mini — 720p price, USD cents per second.
|
||||
const MINI_CENTS_PER_SECOND_720P: f64 = 8.64197531;
|
||||
/// Regular Mini — 720p reference-video surcharge, USD cents per second.
|
||||
const MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P: f64 = 1.72839506;
|
||||
|
||||
/// BytePlus / BytePlus Ultra Mini — 480p price, USD cents per second.
|
||||
const BYTEPLUS_MINI_CENTS_PER_SECOND_480P: f64 = 3.27160494;
|
||||
/// BytePlus / BytePlus Ultra Mini — 480p reference-video surcharge, USD cents per second.
|
||||
const BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_480P: f64 = 0.87242798;
|
||||
/// BytePlus / BytePlus Ultra Mini — 720p price, USD cents per second.
|
||||
const BYTEPLUS_MINI_CENTS_PER_SECOND_720P: f64 = 8.72427984;
|
||||
/// BytePlus / BytePlus Ultra Mini — 720p reference-video surcharge, USD cents per second.
|
||||
const BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P: f64 = 1.74485597;
|
||||
|
||||
/// ArtCraft's user-facing price (USD cents) for the regular Seedance 2.0 Mini.
|
||||
///
|
||||
/// Only 480p and 720p are offered; any other resolution prices at 720p. A
|
||||
/// reference video adds the per-second surcharge. The fractional total is
|
||||
/// rounded UP to a whole cent.
|
||||
pub fn seedance_2p0_mini_usd_cents(
|
||||
resolution: CommonResolution,
|
||||
duration_seconds: u16,
|
||||
batch_count: u16,
|
||||
has_video_reference: bool,
|
||||
) -> u64 {
|
||||
mini_usd_cents(
|
||||
resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
has_video_reference,
|
||||
MINI_CENTS_PER_SECOND_480P,
|
||||
MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_480P,
|
||||
MINI_CENTS_PER_SECOND_720P,
|
||||
MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P,
|
||||
)
|
||||
}
|
||||
|
||||
/// ArtCraft's user-facing price (USD cents) for the Seedance 2.0 BytePlus Mini
|
||||
/// and BytePlus Ultra Mini (which share the same rates — a higher margin than
|
||||
/// the regular Mini).
|
||||
///
|
||||
/// Only 480p and 720p are offered; any other resolution prices at 720p. A
|
||||
/// reference video adds the per-second surcharge. The fractional total is
|
||||
/// rounded UP to a whole cent.
|
||||
pub fn seedance_2p0_byteplus_mini_usd_cents(
|
||||
resolution: CommonResolution,
|
||||
duration_seconds: u16,
|
||||
batch_count: u16,
|
||||
has_video_reference: bool,
|
||||
) -> u64 {
|
||||
mini_usd_cents(
|
||||
resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
has_video_reference,
|
||||
BYTEPLUS_MINI_CENTS_PER_SECOND_480P,
|
||||
BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_480P,
|
||||
BYTEPLUS_MINI_CENTS_PER_SECOND_720P,
|
||||
BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn mini_usd_cents(
|
||||
resolution: CommonResolution,
|
||||
duration_seconds: u16,
|
||||
batch_count: u16,
|
||||
has_video_reference: bool,
|
||||
cents_per_second_480p: f64,
|
||||
video_reference_surcharge_per_second_480p: f64,
|
||||
cents_per_second_720p: f64,
|
||||
video_reference_surcharge_per_second_720p: f64,
|
||||
) -> u64 {
|
||||
let (base_per_second, video_reference_surcharge_per_second) = match resolution {
|
||||
CommonResolution::FourEightyP => (cents_per_second_480p, video_reference_surcharge_per_second_480p),
|
||||
// Everything else (including 720p and unsupported resolutions) prices at 720p.
|
||||
_ => (cents_per_second_720p, video_reference_surcharge_per_second_720p),
|
||||
};
|
||||
|
||||
let mut cents_per_second = base_per_second;
|
||||
if has_video_reference {
|
||||
cents_per_second += video_reference_surcharge_per_second;
|
||||
}
|
||||
|
||||
(cents_per_second * duration_seconds as f64 * batch_count as f64).ceil() as u64
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn four_k_without_video_reference() {
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(4, 1, false), 347);
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(5, 1, false), 433);
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(10, 1, false), 866);
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(15, 1, false), 1299);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_reference() {
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(4, 1, true), 416);
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(5, 1, true), 519);
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(10, 1, true), 1038);
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(15, 1, true), 1557);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_reference_always_costs_more() {
|
||||
for &duration in &[4u16, 5, 10, 15] {
|
||||
assert!(
|
||||
seedance_2p0_four_k_usd_cents(duration, 1, true)
|
||||
> seedance_2p0_four_k_usd_cents(duration, 1, false),
|
||||
"video reference should cost more at {duration}s",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_multiplies() {
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(5, 2, false), 866);
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(5, 4, false), 1732);
|
||||
assert_eq!(seedance_2p0_four_k_usd_cents(5, 2, true), 1038);
|
||||
}
|
||||
|
||||
// ── Seedance 2.0 Mini ──
|
||||
//
|
||||
// Every combination (480p/720p × with/without video reference × 4/5/10/15s)
|
||||
// at batch 1. These are the supplier's per-combo cost plus a 5% margin,
|
||||
// rounded up to whole cents.
|
||||
|
||||
mod mini {
|
||||
use super::*;
|
||||
|
||||
fn cents(res: CommonResolution, dur: u16, has_ref: bool) -> u64 {
|
||||
seedance_2p0_mini_usd_cents(res, dur, 1, has_ref)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_eighty_p_without_video_reference() {
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 4, false), 13);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 5, false), 17);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 10, false), 33);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 15, false), 49);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_eighty_p_with_video_reference() {
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 4, true), 17);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 5, true), 21);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 10, true), 42);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 15, true), 62);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_twenty_p_without_video_reference() {
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, false), 35);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, false), 44);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, false), 87);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, false), 130);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_twenty_p_with_video_reference() {
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, true), 42);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, true), 52);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, true), 104);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, true), 156);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_reference_always_costs_more() {
|
||||
for res in [CommonResolution::FourEightyP, CommonResolution::SevenTwentyP] {
|
||||
for dur in [4u16, 5, 10, 15] {
|
||||
assert!(
|
||||
cents(res, dur, true) > cents(res, dur, false),
|
||||
"video ref should cost more at {res:?} {dur}s",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_resolution_prices_at_720p() {
|
||||
assert_eq!(
|
||||
seedance_2p0_mini_usd_cents(CommonResolution::TenEightyP, 5, 1, false),
|
||||
seedance_2p0_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 1, false),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_scales_the_total() {
|
||||
// Batch is baked in before the single round-up, so batched totals can be
|
||||
// a cent under N× the single price.
|
||||
assert_eq!(seedance_2p0_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 2, false), 87);
|
||||
assert_eq!(seedance_2p0_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 4, false), 173);
|
||||
assert_eq!(seedance_2p0_mini_usd_cents(CommonResolution::FourEightyP, 5, 2, false), 33);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Seedance 2.0 BytePlus / BytePlus Ultra Mini ──
|
||||
//
|
||||
// Same combinations as the regular Mini, but at a higher margin (6% vs 5%),
|
||||
// so the prices are equal-or-higher than the regular Mini.
|
||||
|
||||
mod byteplus_mini {
|
||||
use super::*;
|
||||
|
||||
fn cents(res: CommonResolution, dur: u16, has_ref: bool) -> u64 {
|
||||
seedance_2p0_byteplus_mini_usd_cents(res, dur, 1, has_ref)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_eighty_p_without_video_reference() {
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 4, false), 14);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 5, false), 17);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 10, false), 33);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 15, false), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_eighty_p_with_video_reference() {
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 4, true), 17);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 5, true), 21);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 10, true), 42);
|
||||
assert_eq!(cents(CommonResolution::FourEightyP, 15, true), 63);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_twenty_p_without_video_reference() {
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, false), 35);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, false), 44);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, false), 88);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, false), 131);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_twenty_p_with_video_reference() {
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 4, true), 42);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 5, true), 53);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 10, true), 105);
|
||||
assert_eq!(cents(CommonResolution::SevenTwentyP, 15, true), 158);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn never_cheaper_than_regular_mini() {
|
||||
for res in [CommonResolution::FourEightyP, CommonResolution::SevenTwentyP] {
|
||||
for dur in [4u16, 5, 10, 15] {
|
||||
for has_ref in [false, true] {
|
||||
assert!(
|
||||
seedance_2p0_byteplus_mini_usd_cents(res, dur, 1, has_ref)
|
||||
>= seedance_2p0_mini_usd_cents(res, dur, 1, has_ref),
|
||||
"byteplus mini (6%) should be >= regular mini (5%) at {res:?} {dur}s ref={has_ref}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_resolution_prices_at_720p() {
|
||||
assert_eq!(
|
||||
seedance_2p0_byteplus_mini_usd_cents(CommonResolution::TenEightyP, 5, 1, false),
|
||||
seedance_2p0_byteplus_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 1, false),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_scales_the_total() {
|
||||
assert_eq!(seedance_2p0_byteplus_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 2, false), 88);
|
||||
assert_eq!(seedance_2p0_byteplus_mini_usd_cents(CommonResolution::SevenTwentyP, 5, 4, false), 175);
|
||||
assert_eq!(seedance_2p0_byteplus_mini_usd_cents(CommonResolution::FourEightyP, 5, 2, false), 33);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
pub mod happy_horse_1p0;
|
||||
pub mod seedance_2p0;
|
||||
pub mod seedance_2p0_fast;
|
||||
pub mod seedance_2p0_mini;
|
||||
pub(crate) mod resolve;
|
||||
pub(crate) mod upload;
|
||||
|
||||
+14
-3
@@ -111,7 +111,7 @@ fn nearest_aspect_ratio(aspect_ratio: RouterAspectRatio) -> KinoviAspectRatio {
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Pro supports output resolutions: 480p, 720p, 1080p.
|
||||
// Seedance 2.0 Pro supports output resolutions: 480p, 720p, 1080p, 4K.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<RouterResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
@@ -123,6 +123,7 @@ fn plan_output_resolution(
|
||||
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
|
||||
Some(RouterResolution::SevenTwentyP) => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
|
||||
Some(RouterResolution::TenEightyP) => Ok(Some(KinoviOutputResolution::TenEightyP)),
|
||||
Some(RouterResolution::FourK) => Ok(Some(KinoviOutputResolution::FourK)),
|
||||
|
||||
// Mismatches
|
||||
Some(unsupported) => match strategy {
|
||||
@@ -376,6 +377,16 @@ mod tests {
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_4k() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::FourK),
|
||||
..seedance2pro_builder()
|
||||
};
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourK)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_none() {
|
||||
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_builder() };
|
||||
@@ -386,7 +397,7 @@ mod tests {
|
||||
#[test]
|
||||
fn unsupported_resolution_error_out() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::FourK),
|
||||
resolution: Some(RouterResolution::ThreeK),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..seedance2pro_builder()
|
||||
};
|
||||
@@ -396,7 +407,7 @@ mod tests {
|
||||
#[test]
|
||||
fn unsupported_resolution_rounds_up() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::FourK),
|
||||
resolution: Some(RouterResolution::ThreeK),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..seedance2pro_builder()
|
||||
};
|
||||
|
||||
+145
@@ -175,6 +175,121 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4K pricing ──
|
||||
//
|
||||
// Routes to the seedance2pro_client: 4K base is 200 credits/s (vs 90 at 1080p).
|
||||
// Credits → cents at 243 credits/$1, rounded up.
|
||||
|
||||
mod pricing_4k {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_4k_batch_1() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 4, KinoviBatchCount::One), 330);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::One), 412);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 10, KinoviBatchCount::One), 824);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 15, KinoviBatchCount::One), 1235);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_4k_batch_2() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::Two), 824);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_4k_batch_4() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::Four), 1647);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4K: router cost must match the seedance2pro_client binding ──
|
||||
//
|
||||
// The router delegates to GenerateSeedance2p0Request::calculate_costs(), so these
|
||||
// assert the exact 4K numbers AND cross-check the router against the binding for
|
||||
// every duration (4/5/10/15s) with and without a video reference.
|
||||
//
|
||||
// 4K base = 200 credits/s; with a video reference = 240 credits/s (+40 surcharge).
|
||||
// USD cents = ceil(total_credits / 243 * 100).
|
||||
|
||||
mod four_k_matches_binding {
|
||||
use super::*;
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0::GenerateSeedance2p0Request;
|
||||
|
||||
/// Router-side 4K cost (batch 1) for a duration / video-reference combo.
|
||||
fn router(duration_seconds: u8, has_video_reference: bool) -> VideoGenerationCostEstimate {
|
||||
KinoviSeedance2p0CostState {
|
||||
resolution: Some(KinoviOutputResolution::FourK),
|
||||
duration_seconds,
|
||||
batch_count: Some(KinoviBatchCount::One),
|
||||
has_video_reference,
|
||||
}
|
||||
.estimate_cost()
|
||||
}
|
||||
|
||||
/// The seedance2pro_client binding's own 4K cost for the same inputs.
|
||||
/// Returns (kinovi_credits, usd_cents_rounded_up).
|
||||
fn binding(duration_seconds: u8, has_video_reference: bool) -> (u64, u64) {
|
||||
let reference_video_urls = if has_video_reference {
|
||||
Some(vec!["pricing-placeholder".to_string()])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let costs = GenerateSeedance2p0Request {
|
||||
output_resolution: Some(KinoviOutputResolution::FourK),
|
||||
duration_seconds,
|
||||
batch_count: Some(KinoviBatchCount::One),
|
||||
reference_video_urls,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: None,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
}
|
||||
.calculate_costs();
|
||||
(costs.total_cost.kinovi_credits, costs.total_cost.usd_cents_rounded_up)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_values_without_video_reference() {
|
||||
// 200 credits/s.
|
||||
assert_eq!((router(4, false).cost_in_credits, router(4, false).cost_in_usd_cents), (Some(800), Some(330)));
|
||||
assert_eq!((router(5, false).cost_in_credits, router(5, false).cost_in_usd_cents), (Some(1000), Some(412)));
|
||||
assert_eq!((router(10, false).cost_in_credits, router(10, false).cost_in_usd_cents), (Some(2000), Some(824)));
|
||||
assert_eq!((router(15, false).cost_in_credits, router(15, false).cost_in_usd_cents), (Some(3000), Some(1235)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_values_with_video_reference() {
|
||||
// 240 credits/s (200 base + 40 surcharge).
|
||||
assert_eq!((router(4, true).cost_in_credits, router(4, true).cost_in_usd_cents), (Some(960), Some(396)));
|
||||
assert_eq!((router(5, true).cost_in_credits, router(5, true).cost_in_usd_cents), (Some(1200), Some(494)));
|
||||
assert_eq!((router(10, true).cost_in_credits, router(10, true).cost_in_usd_cents), (Some(2400), Some(988)));
|
||||
assert_eq!((router(15, true).cost_in_credits, router(15, true).cost_in_usd_cents), (Some(3600), Some(1482)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_matches_binding_for_every_duration_and_video_ref() {
|
||||
for &duration in &[4u8, 5, 10, 15] {
|
||||
for &has_ref in &[false, true] {
|
||||
let r = router(duration, has_ref);
|
||||
let (b_credits, b_cents) = binding(duration, has_ref);
|
||||
assert_eq!(
|
||||
r.cost_in_credits, Some(b_credits),
|
||||
"credits differ at {duration}s has_video_reference={has_ref}",
|
||||
);
|
||||
assert_eq!(
|
||||
r.cost_in_usd_cents, Some(b_cents),
|
||||
"usd cents differ at {duration}s has_video_reference={has_ref}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Relative pricing ──
|
||||
|
||||
mod relative_pricing_tests {
|
||||
@@ -189,6 +304,13 @@ mod tests {
|
||||
assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_1080p_cheaper_than_4k() {
|
||||
let c1080 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One);
|
||||
let c4k = usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::One);
|
||||
assert!(c1080 < c4k, "1080p ({}) should be cheaper than 4K ({})", c1080, c4k);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One);
|
||||
@@ -306,6 +428,24 @@ mod tests {
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(31));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_draft_4k() {
|
||||
let draft = make_draft(5, 1, Some(RouterResolution::FourK), false);
|
||||
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
|
||||
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourK)));
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(1000));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(412));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_draft_4k_with_video_reference() {
|
||||
let draft = make_draft(5, 1, Some(RouterResolution::FourK), true);
|
||||
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
|
||||
// 4K base 200 + video-ref surcharge 40 = 240 credits/s -> 1200 credits for 5s.
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(1200));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(494));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_draft_1080p_batch_4() {
|
||||
let draft = make_draft(5, 4, Some(RouterResolution::TenEightyP), false);
|
||||
@@ -388,6 +528,11 @@ mod tests {
|
||||
fn credits_1080p() {
|
||||
assert_eq!(credits(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 450);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_4k() {
|
||||
assert_eq!(credits(KinoviOutputResolution::FourK, 5, KinoviBatchCount::One), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
|
||||
KinoviSeedance2p0MiniAspectRatio as KinoviAspectRatio,
|
||||
KinoviSeedance2p0MiniBitrate as KinoviBitrate,
|
||||
KinoviSeedance2p0MiniBatchCount as KinoviBatchCount,
|
||||
KinoviSeedance2p0MiniOutputResolution as KinoviOutputResolution,
|
||||
};
|
||||
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_bitrate::RouterBitrate;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::draft::{KinoviSeedance2p0MiniDraftState, KinoviSeedance2p0MiniRemainingItems};
|
||||
use crate::generate::generate_video::video_generation_draft::VideoGenerationDraftRequest;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
pub fn build_kinovi_seedance_2p0_mini(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let draft = do_build_kinovi_seedance_2p0_mini(builder)?;
|
||||
Ok(VideoGenerationDraftOrRequest::Draft(VideoGenerationDraftRequest::KinoviSeedance2p0Mini(draft)))
|
||||
}
|
||||
|
||||
fn do_build_kinovi_seedance_2p0_mini(mut builder: GenerateVideoRequestBuilder) -> Result<KinoviSeedance2p0MiniDraftState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
|
||||
let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let bitrate = plan_bitrate(builder.bitrate.take());
|
||||
let prompt = builder.prompt.take().unwrap_or_default();
|
||||
|
||||
let unhandled_request_state = KinoviSeedance2p0MiniRemainingItems {
|
||||
start_frame: builder.start_frame.take(),
|
||||
end_frame: builder.end_frame.take(),
|
||||
reference_images: builder.reference_images.take(),
|
||||
reference_videos: builder.reference_videos.take(),
|
||||
reference_audio: builder.reference_audio.take(),
|
||||
reference_character_tokens: builder.reference_character_tokens.take(),
|
||||
};
|
||||
|
||||
Ok(KinoviSeedance2p0MiniDraftState {
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
batch_count,
|
||||
duration_seconds,
|
||||
bitrate,
|
||||
prompt,
|
||||
unhandled_request_state: Some(unhandled_request_state),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Plan helpers ──
|
||||
|
||||
// Seedance 2.0 Mini supports all six aspect ratios:
|
||||
// 16:9, 21:9, 9:16, 1:1, 4:3, 3:4. All supported ratios cost the same, so
|
||||
// both upgrade and downgrade pick the nearest match.
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<RouterAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<KinoviAspectRatio, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
// No preference or auto — default to landscape
|
||||
None
|
||||
| Some(RouterAspectRatio::Auto)
|
||||
| Some(RouterAspectRatio::Auto2k)
|
||||
| Some(RouterAspectRatio::Auto4k) => Ok(KinoviAspectRatio::Landscape16x9),
|
||||
|
||||
// Direct mappings
|
||||
Some(RouterAspectRatio::WideSixteenByNine) | Some(RouterAspectRatio::Wide) => {
|
||||
Ok(KinoviAspectRatio::Landscape16x9)
|
||||
}
|
||||
Some(RouterAspectRatio::TallNineBySixteen) | Some(RouterAspectRatio::Tall) => {
|
||||
Ok(KinoviAspectRatio::Portrait9x16)
|
||||
}
|
||||
Some(RouterAspectRatio::Square) | Some(RouterAspectRatio::SquareHd) => {
|
||||
Ok(KinoviAspectRatio::Square1x1)
|
||||
}
|
||||
Some(RouterAspectRatio::WideFourByThree) => Ok(KinoviAspectRatio::Standard4x3),
|
||||
Some(RouterAspectRatio::TallThreeByFour) => Ok(KinoviAspectRatio::Portrait3x4),
|
||||
Some(RouterAspectRatio::WideTwentyOneByNine) => Ok(KinoviAspectRatio::UltraWide21x9),
|
||||
|
||||
// Mismatches — apply strategy
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(nearest_aspect_ratio(unsupported))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the nearest supported aspect ratio by AR value (width / height).
|
||||
fn nearest_aspect_ratio(aspect_ratio: RouterAspectRatio) -> KinoviAspectRatio {
|
||||
match aspect_ratio {
|
||||
RouterAspectRatio::WideFiveByFour => KinoviAspectRatio::Standard4x3, // 1.25, nearest 1.33
|
||||
RouterAspectRatio::WideThreeByTwo => KinoviAspectRatio::Standard4x3, // 1.50, nearest 1.33
|
||||
RouterAspectRatio::TallFourByFive => KinoviAspectRatio::Portrait3x4, // 0.80, nearest 0.75
|
||||
RouterAspectRatio::TallTwoByThree => KinoviAspectRatio::Portrait3x4, // 0.67, nearest 0.75
|
||||
RouterAspectRatio::TallNineByTwentyOne => KinoviAspectRatio::Portrait9x16, // 0.43, nearest 0.56
|
||||
_ => KinoviAspectRatio::Square1x1,
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Mini supports output resolutions: 480p and 720p only.
|
||||
// 1080p (and higher) is NOT supported — downgrade to 720p or error based on strategy.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<RouterResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<KinoviOutputResolution>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
// Direct mappings
|
||||
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
|
||||
Some(RouterResolution::SevenTwentyP) => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
|
||||
|
||||
// 1080p is not supported for Mini — handle via strategy
|
||||
Some(RouterResolution::TenEightyP) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", RouterResolution::TenEightyP),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
// 1080p not available — downgrade to 720p (highest supported)
|
||||
Ok(Some(KinoviOutputResolution::SevenTwentyP))
|
||||
}
|
||||
},
|
||||
|
||||
// Other unsupported resolutions
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(match unsupported {
|
||||
RouterResolution::HalfK => KinoviOutputResolution::FourEightyP,
|
||||
_ => KinoviOutputResolution::SevenTwentyP,
|
||||
}))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Mini supports batch counts of 1–8.
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<KinoviBatchCount, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(KinoviBatchCount::One),
|
||||
2 => Ok(KinoviBatchCount::Two),
|
||||
3 => Ok(KinoviBatchCount::Three),
|
||||
4 => Ok(KinoviBatchCount::Four),
|
||||
5 => Ok(KinoviBatchCount::Five),
|
||||
6 => Ok(KinoviBatchCount::Six),
|
||||
7 => Ok(KinoviBatchCount::Seven),
|
||||
8 => Ok(KinoviBatchCount::Eight),
|
||||
// Over the maximum of 8.
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(KinoviBatchCount::Eight),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Mini supports duration of 4–15 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u8, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
const DEFAULT: u8 = 5;
|
||||
match duration_seconds {
|
||||
None => Ok(DEFAULT),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(d as u8),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(d.clamp(MIN, MAX) as u8),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Mini bitrate: "standard" (default, field omitted) or "high".
|
||||
// Bitrate does not affect cost. `Normal` and unset both map to standard (None);
|
||||
// only `High` requests the higher bitrate.
|
||||
fn plan_bitrate(bitrate: Option<RouterBitrate>) -> Option<KinoviBitrate> {
|
||||
match bitrate {
|
||||
Some(RouterBitrate::High) => Some(KinoviBitrate::High),
|
||||
Some(RouterBitrate::Normal) | None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
|
||||
KinoviSeedance2p0MiniAspectRatio as KinoviAspectRatio,
|
||||
KinoviSeedance2p0MiniBatchCount as KinoviBatchCount,
|
||||
KinoviSeedance2p0MiniOutputResolution as KinoviOutputResolution,
|
||||
};
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::draft::KinoviSeedance2p0MiniDraftState;
|
||||
use crate::generate::generate_video::video_generation_draft::VideoGenerationDraftRequest;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod materialized_field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(mini_builder()));
|
||||
assert_eq!(draft.prompt, "a cat dancing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_defaults_to_empty() {
|
||||
let builder = GenerateVideoRequestBuilder { prompt: None, ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert_eq!(draft.prompt, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_seconds_converted() {
|
||||
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert_eq!(draft.duration_seconds, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_defaults_to_5() {
|
||||
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert_eq!(draft.duration_seconds, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(99), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert_eq!(draft.duration_seconds, 15);
|
||||
}
|
||||
}
|
||||
|
||||
mod batch_count_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_counts_one_through_eight() {
|
||||
let expected = [
|
||||
(1u16, KinoviBatchCount::One),
|
||||
(2, KinoviBatchCount::Two),
|
||||
(3, KinoviBatchCount::Three),
|
||||
(4, KinoviBatchCount::Four),
|
||||
(5, KinoviBatchCount::Five),
|
||||
(6, KinoviBatchCount::Six),
|
||||
(7, KinoviBatchCount::Seven),
|
||||
(8, KinoviBatchCount::Eight),
|
||||
];
|
||||
for (count, variant) in expected {
|
||||
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(count), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(
|
||||
std::mem::discriminant(&draft.batch_count) == std::mem::discriminant(&variant),
|
||||
"batch {count} mapped wrong",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_over_max_downgrades_to_eight() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
video_batch_count: Some(12),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..mini_builder()
|
||||
};
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.batch_count, KinoviBatchCount::Eight));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_over_max_error_out() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
video_batch_count: Some(12),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..mini_builder()
|
||||
};
|
||||
assert!(build_kinovi_seedance_2p0_mini(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_zero_errors() {
|
||||
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(0), ..mini_builder() };
|
||||
assert!(build_kinovi_seedance_2p0_mini(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod aspect_ratio_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_wide() {
|
||||
let builder = GenerateVideoRequestBuilder { aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_ultra_wide() {
|
||||
let builder = GenerateVideoRequestBuilder { aspect_ratio: Some(RouterAspectRatio::WideTwentyOneByNine), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::UltraWide21x9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_tall() {
|
||||
let builder = GenerateVideoRequestBuilder { aspect_ratio: Some(RouterAspectRatio::TallNineBySixteen), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait9x16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_square() {
|
||||
let builder = GenerateVideoRequestBuilder { aspect_ratio: Some(RouterAspectRatio::Square), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Square1x1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_four_by_three() {
|
||||
let builder = GenerateVideoRequestBuilder { aspect_ratio: Some(RouterAspectRatio::WideFourByThree), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Standard4x3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_three_by_four() {
|
||||
let builder = GenerateVideoRequestBuilder { aspect_ratio: Some(RouterAspectRatio::TallThreeByFour), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait3x4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_defaults_to_landscape() {
|
||||
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolution_480p() {
|
||||
let builder = GenerateVideoRequestBuilder { resolution: Some(RouterResolution::FourEightyP), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_720p() {
|
||||
let builder = GenerateVideoRequestBuilder { resolution: Some(RouterResolution::SevenTwentyP), ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_1080p_downgrades_to_720p() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..mini_builder()
|
||||
};
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_1080p_error_out() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..mini_builder()
|
||||
};
|
||||
assert!(build_kinovi_seedance_2p0_mini(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_none() {
|
||||
let builder = GenerateVideoRequestBuilder { resolution: None, ..mini_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
assert!(draft.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod unhandled_request_state {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn media_refs_placed_in_unhandled() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
|
||||
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/ref.jpg".to_string()])),
|
||||
reference_videos: Some(VideoListRef::Urls(vec!["https://example.com/vid.mp4".to_string()])),
|
||||
reference_audio: Some(AudioListRef::Urls(vec!["https://example.com/audio.mp3".to_string()])),
|
||||
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
|
||||
CharacterToken::new("char_abc".to_string()),
|
||||
])),
|
||||
..mini_builder()
|
||||
};
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
let remaining = draft.unhandled_request_state.unwrap();
|
||||
assert!(remaining.start_frame.is_some());
|
||||
assert!(remaining.end_frame.is_some());
|
||||
assert!(remaining.reference_images.is_some());
|
||||
assert!(remaining.reference_videos.is_some());
|
||||
assert!(remaining.reference_audio.is_some());
|
||||
assert!(remaining.reference_character_tokens.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_media_token_placed_in_unhandled() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test123".to_string()))),
|
||||
..mini_builder()
|
||||
};
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
|
||||
let remaining = draft.unhandled_request_state.unwrap();
|
||||
assert!(matches!(remaining.start_frame, Some(ImageRef::MediaFileToken(t)) if t.as_str() == "mf_test123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_refs_are_none_in_unhandled() {
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(mini_builder()));
|
||||
let remaining = draft.unhandled_request_state.unwrap();
|
||||
assert!(remaining.start_frame.is_none());
|
||||
assert!(remaining.end_frame.is_none());
|
||||
assert!(remaining.reference_images.is_none());
|
||||
assert!(remaining.reference_videos.is_none());
|
||||
assert!(remaining.reference_audio.is_none());
|
||||
assert!(remaining.reference_character_tokens.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn mini_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0Mini,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
prompt: Some("a cat dancing".to_string()),
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_draft(result: Result<VideoGenerationDraftOrRequest, ArtcraftRouterError>) -> KinoviSeedance2p0MiniDraftState {
|
||||
match result.expect("build should succeed") {
|
||||
VideoGenerationDraftOrRequest::Draft(
|
||||
VideoGenerationDraftRequest::KinoviSeedance2p0Mini(draft)
|
||||
) => draft,
|
||||
_ => panic!("expected KinoviSeedance2p0Mini draft"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
|
||||
GenerateSeedance2p0MiniRequest, KinoviSeedance2p0MiniBatchCount,
|
||||
KinoviSeedance2p0MiniOutputResolution,
|
||||
};
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::draft::KinoviSeedance2p0MiniDraftState;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::request::KinoviSeedance2p0MiniRequestState;
|
||||
|
||||
pub struct KinoviSeedance2p0MiniCostState {
|
||||
pub resolution: Option<KinoviSeedance2p0MiniOutputResolution>,
|
||||
pub duration_seconds: u8,
|
||||
pub batch_count: Option<KinoviSeedance2p0MiniBatchCount>,
|
||||
pub has_video_reference: bool,
|
||||
}
|
||||
|
||||
impl KinoviSeedance2p0MiniCostState {
|
||||
pub fn from_request(request: &KinoviSeedance2p0MiniRequestState) -> Self {
|
||||
Self {
|
||||
resolution: request.request.output_resolution,
|
||||
duration_seconds: request.request.duration_seconds,
|
||||
batch_count: request.request.batch_count,
|
||||
has_video_reference: request.request.reference_video_urls
|
||||
.as_ref()
|
||||
.is_some_and(|urls| !urls.is_empty()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_draft(draft: &KinoviSeedance2p0MiniDraftState) -> Self {
|
||||
let has_video_reference = draft.unhandled_request_state
|
||||
.as_ref()
|
||||
.and_then(|rem| rem.reference_videos.as_ref())
|
||||
.is_some();
|
||||
|
||||
Self {
|
||||
resolution: draft.resolution,
|
||||
duration_seconds: draft.duration_seconds,
|
||||
batch_count: Some(draft.batch_count),
|
||||
has_video_reference,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let pricing_request = GenerateSeedance2p0MiniRequest {
|
||||
output_resolution: self.resolution,
|
||||
duration_seconds: self.duration_seconds,
|
||||
batch_count: self.batch_count,
|
||||
|
||||
// PRESENCE of reference videos changes the price (per-second
|
||||
// surcharge); the URL contents don't.
|
||||
reference_video_urls: if self.has_video_reference {
|
||||
Some(vec!["pricing-placeholder".to_string()])
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
||||
// No impact on price
|
||||
prompt: String::new(),
|
||||
aspect_ratio: None,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
};
|
||||
|
||||
let costs = pricing_request.calculate_costs();
|
||||
// Mini credits can be fractional (e.g. 480p odd durations land on 37.5).
|
||||
// The router's credit field is an integer, so round to the nearest credit;
|
||||
// the USD cents (the authoritative charge) are already rounded up.
|
||||
let cost_in_credits = costs.total_cost.kinovi_credits.round() as u64;
|
||||
let cost_in_usd_cents = costs.total_cost.usd_cents_rounded_up;
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_credits),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
|
||||
KinoviSeedance2p0MiniOutputResolution as KinoviOutputResolution,
|
||||
KinoviSeedance2p0MiniBatchCount as KinoviBatchCount,
|
||||
};
|
||||
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_draft::VideoGenerationDraftRequest;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
// ── USD cents: every combination (480p/720p × with/without video ref) ──
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_video_reference() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One, false), 13);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One, false), 16);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One, false), 31);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One, false), 47);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_video_reference() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One, true), 16);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One, true), 20);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One, true), 40);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One, true), 59);
|
||||
}
|
||||
}
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_video_reference() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One, false), 33);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One, false), 42);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One, false), 83);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One, false), 124);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_video_reference() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One, true), 40);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One, true), 50);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One, true), 99);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One, true), 149);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Credits (mini bills fractional credits, rounded for the integer field) ──
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_480p() {
|
||||
// 7.5 credits/s; odd durations are fractional (37.5 → 38, 112.5 → 113).
|
||||
assert_eq!(credits(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One, false), 30);
|
||||
assert_eq!(credits(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One, false), 38);
|
||||
assert_eq!(credits(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One, false), 75);
|
||||
assert_eq!(credits(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One, false), 113);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_720p() {
|
||||
assert_eq!(credits(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One, false), 100);
|
||||
assert_eq!(credits(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One, false), 300);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Relative pricing ──
|
||||
|
||||
mod relative_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_480p_cheaper_than_720p() {
|
||||
let c480 = usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One, false);
|
||||
let c720 = usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One, false);
|
||||
assert!(c480 < c720, "480p ({c480}) should be cheaper than 720p ({c720})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_reference_costs_more() {
|
||||
for res in [KinoviOutputResolution::FourEightyP, KinoviOutputResolution::SevenTwentyP] {
|
||||
for dur in [4u8, 5, 10, 15] {
|
||||
let without = usd_cents(res, dur, KinoviBatchCount::One, false);
|
||||
let with = usd_cents(res, dur, KinoviBatchCount::One, true);
|
||||
assert!(with > without, "video ref should cost more at {res:?} {dur}s");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration_and_batch() {
|
||||
let c4 = usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One, false);
|
||||
let c15 = usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One, false);
|
||||
assert!(c4 < c15);
|
||||
let b1 = usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One, false);
|
||||
let b4 = usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four, false);
|
||||
assert!(b1 < b4);
|
||||
}
|
||||
}
|
||||
|
||||
// ── from_request() ──
|
||||
|
||||
mod from_request_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_request_720p() {
|
||||
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
|
||||
let cost = KinoviSeedance2p0MiniCostState::from_request(&req);
|
||||
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
|
||||
assert_eq!(cost.duration_seconds, 5);
|
||||
assert!(!cost.has_video_reference);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_request_480p_with_video_reference() {
|
||||
let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, true);
|
||||
let cost = KinoviSeedance2p0MiniCostState::from_request(&req);
|
||||
assert!(cost.has_video_reference);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_request_none_defaults_to_720p() {
|
||||
let req = make_request_state(None, 5, KinoviBatchCount::One, false);
|
||||
let cost = KinoviSeedance2p0MiniCostState::from_request(&req);
|
||||
assert!(cost.resolution.is_none());
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(42));
|
||||
}
|
||||
}
|
||||
|
||||
// ── from_draft() ──
|
||||
|
||||
mod from_draft_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_draft_480p() {
|
||||
let draft = make_draft(5, 1, Some(RouterResolution::FourEightyP), false);
|
||||
let cost = KinoviSeedance2p0MiniCostState::from_draft(&draft);
|
||||
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourEightyP)));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_draft_with_video_reference() {
|
||||
let draft = make_draft(5, 1, Some(RouterResolution::SevenTwentyP), true);
|
||||
let cost = KinoviSeedance2p0MiniCostState::from_draft(&draft);
|
||||
assert!(cost.has_video_reference);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_draft_batch_8() {
|
||||
// Mini supports up to batch 8: 720p 5s × 8 = 800 credits → 80000/243 → 330¢.
|
||||
let draft = make_draft(5, 8, Some(RouterResolution::SevenTwentyP), false);
|
||||
let cost = KinoviSeedance2p0MiniCostState::from_draft(&draft);
|
||||
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::Eight)));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(330));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-check: from_draft matches from_request ──
|
||||
|
||||
mod cross_check_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn draft_and_request_produce_same_cost() {
|
||||
let draft = make_draft(10, 2, Some(RouterResolution::FourEightyP), true);
|
||||
let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 10, KinoviBatchCount::Two, true);
|
||||
let draft_cost = KinoviSeedance2p0MiniCostState::from_draft(&draft).estimate_cost();
|
||||
let req_cost = KinoviSeedance2p0MiniCostState::from_request(&req).estimate_cost();
|
||||
assert_eq!(draft_cost.cost_in_usd_cents, req_cost.cost_in_usd_cents);
|
||||
assert_eq!(draft_cost.cost_in_credits, req_cost.cost_in_credits);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn usd_cents(
|
||||
resolution: KinoviOutputResolution,
|
||||
duration_seconds: u8,
|
||||
batch_count: KinoviBatchCount,
|
||||
has_video_reference: bool,
|
||||
) -> u64 {
|
||||
KinoviSeedance2p0MiniCostState {
|
||||
resolution: Some(resolution),
|
||||
duration_seconds,
|
||||
batch_count: Some(batch_count),
|
||||
has_video_reference,
|
||||
}
|
||||
.estimate_cost()
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn credits(
|
||||
resolution: KinoviOutputResolution,
|
||||
duration_seconds: u8,
|
||||
batch_count: KinoviBatchCount,
|
||||
has_video_reference: bool,
|
||||
) -> u64 {
|
||||
KinoviSeedance2p0MiniCostState {
|
||||
resolution: Some(resolution),
|
||||
duration_seconds,
|
||||
batch_count: Some(batch_count),
|
||||
has_video_reference,
|
||||
}
|
||||
.estimate_cost()
|
||||
.cost_in_credits
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Build a draft via the builder to test from_draft().
|
||||
fn make_draft(
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
resolution: Option<RouterResolution>,
|
||||
with_video_ref: bool,
|
||||
) -> KinoviSeedance2p0MiniDraftState {
|
||||
let reference_videos = if with_video_ref {
|
||||
Some(VideoListRef::Urls(vec!["https://example.com/video.mp4".to_string()]))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p0Mini,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
resolution,
|
||||
reference_videos,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match builder.build2().expect("build2 should succeed") {
|
||||
VideoGenerationDraftOrRequest::Draft(
|
||||
VideoGenerationDraftRequest::KinoviSeedance2p0Mini(draft)
|
||||
) => draft,
|
||||
_ => panic!("expected KinoviSeedance2p0Mini draft"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a request state for from_request() tests.
|
||||
fn make_request_state(
|
||||
resolution: Option<KinoviOutputResolution>,
|
||||
duration_seconds: u8,
|
||||
batch_count: KinoviBatchCount,
|
||||
with_video_ref: bool,
|
||||
) -> KinoviSeedance2p0MiniRequestState {
|
||||
let reference_video_urls = if with_video_ref {
|
||||
Some(vec!["https://cdn.seedance2-pro.com/video.mp4".to_string()])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
KinoviSeedance2p0MiniRequestState {
|
||||
request: GenerateSeedance2p0MiniRequest {
|
||||
prompt: "test".to_string(),
|
||||
aspect_ratio: None,
|
||||
output_resolution: resolution,
|
||||
duration_seconds,
|
||||
batch_count: Some(batch_count),
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
|
||||
GenerateSeedance2p0MiniRequest, KinoviSeedance2p0MiniAspectRatio, KinoviSeedance2p0MiniBitrate,
|
||||
KinoviSeedance2p0MiniBatchCount, KinoviSeedance2p0MiniOutputResolution,
|
||||
};
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::providers::kinovi::resolve::{
|
||||
audio_list_ref_into_urls_or_tokens, image_list_ref_into_urls_or_tokens,
|
||||
resolve_and_upload_list, resolve_and_upload_single, resolve_character_tokens,
|
||||
video_list_ref_into_urls_or_tokens,
|
||||
};
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::request::KinoviSeedance2p0MiniRequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_context::VideoGenerationDraftContext;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KinoviSeedance2p0MiniDraftState {
|
||||
pub prompt: String,
|
||||
pub aspect_ratio: KinoviSeedance2p0MiniAspectRatio,
|
||||
pub resolution: Option<KinoviSeedance2p0MiniOutputResolution>,
|
||||
pub duration_seconds: u8,
|
||||
pub batch_count: KinoviSeedance2p0MiniBatchCount,
|
||||
pub bitrate: Option<KinoviSeedance2p0MiniBitrate>,
|
||||
|
||||
pub unhandled_request_state: Option<KinoviSeedance2p0MiniRemainingItems>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KinoviSeedance2p0MiniRemainingItems {
|
||||
pub start_frame: Option<ImageRef>,
|
||||
pub end_frame: Option<ImageRef>,
|
||||
pub reference_images: Option<ImageListRef>,
|
||||
pub reference_videos: Option<VideoListRef>,
|
||||
pub reference_audio: Option<AudioListRef>,
|
||||
pub reference_character_tokens: Option<CharacterListRef>,
|
||||
}
|
||||
|
||||
impl KinoviSeedance2p0MiniDraftState {
|
||||
pub async fn to_request(
|
||||
&mut self,
|
||||
draft_context: &VideoGenerationDraftContext<'_>,
|
||||
) -> Result<KinoviSeedance2p0MiniRequestState, ArtcraftRouterError> {
|
||||
let client = draft_context.get_seedance2pro_client_ref()?;
|
||||
let session = &client.session;
|
||||
|
||||
let mut start_frame_url = None;
|
||||
let mut end_frame_url = None;
|
||||
let mut reference_image_urls = None;
|
||||
let mut reference_video_urls = None;
|
||||
let mut reference_audio_urls = None;
|
||||
let mut character_ids = None;
|
||||
|
||||
if let Some(remaining) = self.unhandled_request_state.take() {
|
||||
let map = draft_context.media_file_to_artcraft_url_map;
|
||||
|
||||
start_frame_url = resolve_and_upload_single(session, remaining.start_frame, map).await?;
|
||||
end_frame_url = resolve_and_upload_single(session, remaining.end_frame, map).await?;
|
||||
|
||||
reference_image_urls = resolve_and_upload_list(
|
||||
session, remaining.reference_images.map(image_list_ref_into_urls_or_tokens), map,
|
||||
).await?;
|
||||
|
||||
reference_video_urls = resolve_and_upload_list(
|
||||
session, remaining.reference_videos.map(video_list_ref_into_urls_or_tokens), map,
|
||||
).await?;
|
||||
|
||||
reference_audio_urls = resolve_and_upload_list(
|
||||
session, remaining.reference_audio.map(audio_list_ref_into_urls_or_tokens), map,
|
||||
).await?;
|
||||
|
||||
character_ids = resolve_character_tokens(
|
||||
remaining.reference_character_tokens.as_ref(),
|
||||
draft_context,
|
||||
)?;
|
||||
}
|
||||
|
||||
let request = GenerateSeedance2p0MiniRequest {
|
||||
prompt: self.prompt.clone(),
|
||||
aspect_ratio: Some(self.aspect_ratio),
|
||||
output_resolution: self.resolution,
|
||||
duration_seconds: self.duration_seconds,
|
||||
batch_count: Some(self.batch_count),
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: self.bitrate,
|
||||
};
|
||||
|
||||
Ok(KinoviSeedance2p0MiniRequestState { request })
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
pub mod build;
|
||||
pub mod draft;
|
||||
pub mod request;
|
||||
pub mod cost;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
|
||||
generate_seedance_2p0_mini, GenerateSeedance2p0MiniArgs, GenerateSeedance2p0MiniRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KinoviSeedance2p0MiniRequestState {
|
||||
/// Final materialized request; ready to fire.
|
||||
pub request: GenerateSeedance2p0MiniRequest,
|
||||
}
|
||||
|
||||
impl KinoviSeedance2p0MiniRequestState {
|
||||
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let session = &client.session;
|
||||
|
||||
let args = GenerateSeedance2p0MiniArgs {
|
||||
session,
|
||||
host_override: None,
|
||||
request: self.request.clone(),
|
||||
};
|
||||
|
||||
let response = generate_seedance_2p0_mini(args)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
maybe_order_ids: response.order_ids,
|
||||
maybe_task_ids: response.task_ids,
|
||||
}))
|
||||
}
|
||||
}
|
||||
+9
@@ -7,6 +7,8 @@ use crate::generate::generate_video::providers::kinovi::seedance_2p0::cost::Kino
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0::draft::KinoviSeedance2p0DraftState;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::cost::KinoviSeedance2p0FastCostState;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::draft::KinoviSeedance2p0FastDraftState;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::cost::KinoviSeedance2p0MiniCostState;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::draft::KinoviSeedance2p0MiniDraftState;
|
||||
use crate::generate::generate_video::video_generation_draft_context::VideoGenerationDraftContext;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
@@ -18,6 +20,7 @@ pub enum VideoGenerationDraftRequest {
|
||||
KinoviHappyHorse1p0(KinoviHappyHorse1p0DraftState),
|
||||
KinoviSeedance2p0(KinoviSeedance2p0DraftState),
|
||||
KinoviSeedance2p0Fast(KinoviSeedance2p0FastDraftState),
|
||||
KinoviSeedance2p0Mini(KinoviSeedance2p0MiniDraftState),
|
||||
}
|
||||
|
||||
impl VideoGenerationDraftRequest {
|
||||
@@ -27,6 +30,7 @@ impl VideoGenerationDraftRequest {
|
||||
Self::KinoviHappyHorse1p0(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0Fast(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0Mini(_) => RouterProvider::Seedance2Pro,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +40,7 @@ impl VideoGenerationDraftRequest {
|
||||
VideoGenerationDraftRequest::KinoviHappyHorse1p0(draft) => Ok(KinoviHappyHorse1p0CostState::from_draft(draft).estimate_cost()),
|
||||
VideoGenerationDraftRequest::KinoviSeedance2p0(draft) => Ok(KinoviSeedance2p0CostState::from_draft(draft).estimate_cost()),
|
||||
VideoGenerationDraftRequest::KinoviSeedance2p0Fast(draft) => Ok(KinoviSeedance2p0FastCostState::from_draft(draft).estimate_cost()),
|
||||
VideoGenerationDraftRequest::KinoviSeedance2p0Mini(draft) => Ok(KinoviSeedance2p0MiniCostState::from_draft(draft).estimate_cost()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +60,10 @@ impl VideoGenerationDraftRequest {
|
||||
let result = draft.to_request(&draft_context).await?;
|
||||
Ok(VideoGenerationRequest::KinoviSeedance2p0Fast(result))
|
||||
},
|
||||
VideoGenerationDraftRequest::KinoviSeedance2p0Mini(mut draft) => {
|
||||
let result = draft.to_request(&draft_context).await?;
|
||||
Ok(VideoGenerationRequest::KinoviSeedance2p0Mini(result))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -47,6 +47,12 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu::cost
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu::request::ArtcraftSeedance2p0BytePlusUltraRequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_fast::cost::ArtcraftSeedance2p0BytePlusUltraFastCostState;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_fast::request::ArtcraftSeedance2p0BytePlusUltraFastRequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::cost::ArtcraftSeedance2p0MiniCostState;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_mini::request::ArtcraftSeedance2p0MiniRequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_mini::cost::ArtcraftSeedance2p0BytePlusMiniCostState;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp_mini::request::ArtcraftSeedance2p0BytePlusMiniRequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_mini::cost::ArtcraftSeedance2p0BytePlusUltraMiniCostState;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu_mini::request::ArtcraftSeedance2p0BytePlusUltraMiniRequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::sora_2::cost::ArtcraftSora2CostState;
|
||||
use crate::generate::generate_video::providers::artcraft::sora_2::request::ArtcraftSora2RequestState;
|
||||
use crate::generate::generate_video::providers::artcraft::sora_2_pro::cost::ArtcraftSora2ProCostState;
|
||||
@@ -107,6 +113,8 @@ use crate::generate::generate_video::providers::kinovi::seedance_2p0::cost::Kino
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0::request::KinoviSeedance2p0RequestState;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::cost::KinoviSeedance2p0FastCostState;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::request::KinoviSeedance2p0FastRequestState;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::cost::KinoviSeedance2p0MiniCostState;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::request::KinoviSeedance2p0MiniRequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum VideoGenerationRequest {
|
||||
@@ -132,6 +140,9 @@ pub enum VideoGenerationRequest {
|
||||
ArtcraftSeedance2p0BytePlusFast(ArtcraftSeedance2p0BytePlusFastRequestState),
|
||||
ArtcraftSeedance2p0BytePlusUltra(ArtcraftSeedance2p0BytePlusUltraRequestState),
|
||||
ArtcraftSeedance2p0BytePlusUltraFast(ArtcraftSeedance2p0BytePlusUltraFastRequestState),
|
||||
ArtcraftSeedance2p0Mini(ArtcraftSeedance2p0MiniRequestState),
|
||||
ArtcraftSeedance2p0BytePlusMini(ArtcraftSeedance2p0BytePlusMiniRequestState),
|
||||
ArtcraftSeedance2p0BytePlusUltraMini(ArtcraftSeedance2p0BytePlusUltraMiniRequestState),
|
||||
ArtcraftSora2(ArtcraftSora2RequestState),
|
||||
ArtcraftSora2Pro(ArtcraftSora2ProRequestState),
|
||||
ArtcraftVeo2(ArtcraftVeo2RequestState),
|
||||
@@ -162,6 +173,7 @@ pub enum VideoGenerationRequest {
|
||||
KinoviHappyHorse1p0(KinoviHappyHorse1p0RequestState),
|
||||
KinoviSeedance2p0(KinoviSeedance2p0RequestState),
|
||||
KinoviSeedance2p0Fast(KinoviSeedance2p0FastRequestState),
|
||||
KinoviSeedance2p0Mini(KinoviSeedance2p0MiniRequestState),
|
||||
}
|
||||
|
||||
impl VideoGenerationRequest {
|
||||
@@ -190,6 +202,9 @@ impl VideoGenerationRequest {
|
||||
Self::ArtcraftSeedance2p0BytePlusFast(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0BytePlusUltra(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0BytePlusUltraFast(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0Mini(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0BytePlusMini(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0BytePlusUltraMini(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSora2(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftSora2Pro(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftVeo2(_) => RouterProvider::Artcraft,
|
||||
@@ -220,6 +235,7 @@ impl VideoGenerationRequest {
|
||||
Self::KinoviHappyHorse1p0(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0Fast(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0Mini(_) => RouterProvider::Seedance2Pro,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +264,9 @@ impl VideoGenerationRequest {
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(request) => Ok(ArtcraftSeedance2p0BytePlusFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltra(request) => Ok(ArtcraftSeedance2p0BytePlusUltraCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraFast(request) => Ok(ArtcraftSeedance2p0BytePlusUltraFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0Mini(request) => Ok(ArtcraftSeedance2p0MiniCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(request) => Ok(ArtcraftSeedance2p0BytePlusMiniCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraMini(request) => Ok(ArtcraftSeedance2p0BytePlusUltraMiniCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSora2(request) => Ok(ArtcraftSora2CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSora2Pro(request) => Ok(ArtcraftSora2ProCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftVeo2(request) => Ok(ArtcraftVeo2CostState::from_request(request).estimate_cost()),
|
||||
@@ -278,6 +297,7 @@ impl VideoGenerationRequest {
|
||||
VideoGenerationRequest::KinoviHappyHorse1p0(request) => Ok(KinoviHappyHorse1p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviSeedance2p0(request) => Ok(KinoviSeedance2p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviSeedance2p0Fast(request) => Ok(KinoviSeedance2p0FastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviSeedance2p0Mini(request) => Ok(KinoviSeedance2p0MiniCostState::from_request(request).estimate_cost()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,6 +393,18 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0Mini(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusMini(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusUltraMini(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftSora2(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
@@ -493,6 +525,10 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::KinoviSeedance2p0Mini(request) => {
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
use crate::cost::constants::CREDITS_PER_DOLLAR;
|
||||
|
||||
/// The cost of a Kinovi generation that bills FRACTIONAL credits, in both
|
||||
/// native credits and USD cents.
|
||||
///
|
||||
/// This is the fractional-credit counterpart of
|
||||
/// [`crate::cost::kinovi_generation_cost::KinoviGenerationCost`] (which uses
|
||||
/// `u64`). Some Kinovi models bill non-integer credits — e.g. Seedance 2.0
|
||||
/// Mini at 480p bills 7.5 credits/sec, landing on half-credits like 37.5 at
|
||||
/// odd durations. Any model with a fractional credit rate uses this type;
|
||||
/// whole-credit models use the cheaper `u64` `KinoviGenerationCost`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KinoviFractionalGenerationCost {
|
||||
/// The cost of the generation in Kinovi credits. May be fractional (e.g.
|
||||
/// 37.5 for Seedance 2.0 Mini 480p at 5 seconds).
|
||||
pub kinovi_credits: f64,
|
||||
|
||||
/// Estimated cost in cents, rounded up to the nearest whole cent.
|
||||
/// This does not account for discounts, prorations, etc.
|
||||
pub usd_cents_rounded_up: u64,
|
||||
|
||||
/// Estimated cost in cents, rounded down to the nearest whole cent.
|
||||
pub usd_cents_rounded_down: u64,
|
||||
|
||||
/// Estimated cost in cents without rounding (the exact fractional value).
|
||||
pub usd_cents_fractional: f64,
|
||||
}
|
||||
|
||||
impl KinoviFractionalGenerationCost {
|
||||
/// Build a cost from a (possibly fractional) Kinovi credit amount, deriving
|
||||
/// the USD cents via the credit-package rate.
|
||||
///
|
||||
/// The conversion is done on integer hundredths-of-a-credit, so the integer
|
||||
/// cent results are exact (no floating-point drift). Credit amounts are
|
||||
/// rounded to the nearest hundredth of a credit, which covers every Kinovi
|
||||
/// rate seen so far (e.g. Mini's half-credits).
|
||||
pub fn from_kinovi_credits(kinovi_credits: f64) -> Self {
|
||||
// usd_cents == credits × 100 / CREDITS_PER_DOLLAR == hundredths / CREDITS_PER_DOLLAR.
|
||||
let hundredths_of_credit = (kinovi_credits * 100.0).round() as u64;
|
||||
Self {
|
||||
kinovi_credits,
|
||||
usd_cents_rounded_up: hundredths_of_credit.div_ceil(CREDITS_PER_DOLLAR),
|
||||
usd_cents_rounded_down: hundredths_of_credit / CREDITS_PER_DOLLAR,
|
||||
usd_cents_fractional: hundredths_of_credit as f64 / CREDITS_PER_DOLLAR as f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const FLOAT_TOLERANCE: f64 = 1e-9;
|
||||
|
||||
#[test]
|
||||
fn fractional_credits_convert_to_usd() {
|
||||
// 37.5 credits; 3750/243 = 15.4320 cents.
|
||||
let cost = KinoviFractionalGenerationCost::from_kinovi_credits(37.5);
|
||||
assert_eq!(cost.kinovi_credits, 37.5);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 16);
|
||||
assert_eq!(cost.usd_cents_rounded_down, 15);
|
||||
assert!((cost.usd_cents_fractional - (3750.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whole_credits_convert_to_usd() {
|
||||
// 100 credits; 10000/243 = 41.1522 cents.
|
||||
let cost = KinoviFractionalGenerationCost::from_kinovi_credits(100.0);
|
||||
assert_eq!(cost.kinovi_credits, 100.0);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 42);
|
||||
assert_eq!(cost.usd_cents_rounded_down, 41);
|
||||
assert!((cost.usd_cents_fractional - (10000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded_bounds_bracket_the_fractional_value() {
|
||||
for half_credits in [0u64, 1, 15, 75, 95, 200, 225, 600, 720] {
|
||||
let credits = half_credits as f64 / 2.0;
|
||||
let cost = KinoviFractionalGenerationCost::from_kinovi_credits(credits);
|
||||
assert!(cost.usd_cents_rounded_down as f64 <= cost.usd_cents_fractional);
|
||||
assert!(cost.usd_cents_fractional <= cost.usd_cents_rounded_up as f64);
|
||||
assert!(cost.usd_cents_rounded_up - cost.usd_cents_rounded_down <= 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@ use crate::cost::kinovi_generation_cost::KinoviGenerationCost;
|
||||
/// The cost of a Kinovi Seedance generation, with the video-reference
|
||||
/// surcharge broken out from the base price.
|
||||
///
|
||||
/// Used by the WHOLE-credit Seedance video models (regular + Fast). Seedance
|
||||
/// 2.0 Mini bills fractional credits and has its own `f64` twin,
|
||||
/// [`crate::cost::kinovi_seedance_mini_generation_cost::KinoviSeedanceMiniGenerationCost`]
|
||||
/// — see that file for why they aren't merged.
|
||||
///
|
||||
/// `total_cost` covers base + surcharge. NB: the total's USD conversions are
|
||||
/// computed from the SUMMED credits (rounded once), so they may differ by a
|
||||
/// cent from adding the parts' rounded USD values together.
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
use crate::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WHY THIS TYPE EXISTS (don't merge it into KinoviSeedanceGenerationCost):
|
||||
//
|
||||
// This is the Mini twin of `KinoviSeedanceGenerationCost`. It is structurally
|
||||
// identical (base + optional video-reference surcharge = total) and prices the
|
||||
// SAME way conceptually — but its credits are fractional
|
||||
// (`KinoviFractionalGenerationCost`, `f64`) rather than whole
|
||||
// (`KinoviGenerationCost`, `u64`).
|
||||
//
|
||||
// Seedance 2.0 Mini at 480p bills 7.5 credits/sec, so odd durations land on
|
||||
// FRACTIONAL credits (5s = 37.5, 15s = 112.5). Every OTHER whole-credit
|
||||
// Seedance video model uses `KinoviSeedanceGenerationCost` (u64 credits).
|
||||
//
|
||||
// Making the shared `u64` credit type fractional would ripple `f64` through all
|
||||
// of those whole-credit models AND `artcraft_router` (which stores credits as
|
||||
// `u64`), forcing hundreds of integer-literal edits for one model's benefit.
|
||||
// Keeping Mini's fractional credits in their own small type is the cheaper,
|
||||
// better-isolated choice. The inner `KinoviFractionalGenerationCost` is generic
|
||||
// (not Mini-specific) — other fractional-credit models can reuse it.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The cost of a Kinovi Seedance 2.0 Mini generation, with the video-reference
|
||||
/// surcharge broken out from the base price.
|
||||
///
|
||||
/// Mirrors [`crate::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost`],
|
||||
/// but with FRACTIONAL credits (see the module note above).
|
||||
///
|
||||
/// `total_cost` covers base + surcharge. NB: the total's USD conversions are
|
||||
/// computed from the SUMMED credits (rounded once), so they may differ by a
|
||||
/// cent from adding the parts' rounded USD values together.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KinoviSeedanceMiniGenerationCost {
|
||||
/// The full cost (base + any surcharge), in credits and USD cents.
|
||||
pub total_cost: KinoviFractionalGenerationCost,
|
||||
|
||||
/// The base generation cost (resolution rate × output duration × batch
|
||||
/// count), excluding any surcharges.
|
||||
pub base_cost: KinoviFractionalGenerationCost,
|
||||
|
||||
/// The video-reference surcharge, when one or more reference videos are
|
||||
/// attached. `None` when no reference videos are attached.
|
||||
pub video_reference_surcharge_cost: Option<KinoviFractionalGenerationCost>,
|
||||
}
|
||||
|
||||
impl KinoviSeedanceMiniGenerationCost {
|
||||
/// Build a cost from base credits plus an optional video-reference
|
||||
/// surcharge (both in Kinovi credits, possibly fractional). The per-part
|
||||
/// and total USD conversions are derived.
|
||||
pub fn from_base_and_surcharge(
|
||||
base_credits: f64,
|
||||
maybe_video_reference_surcharge_credits: Option<f64>,
|
||||
) -> Self {
|
||||
let total_credits = base_credits + maybe_video_reference_surcharge_credits.unwrap_or(0.0);
|
||||
Self {
|
||||
total_cost: KinoviFractionalGenerationCost::from_kinovi_credits(total_credits),
|
||||
base_cost: KinoviFractionalGenerationCost::from_kinovi_credits(base_credits),
|
||||
video_reference_surcharge_cost: maybe_video_reference_surcharge_credits
|
||||
.map(KinoviFractionalGenerationCost::from_kinovi_credits),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn without_surcharge_total_equals_base() {
|
||||
let cost = KinoviSeedanceMiniGenerationCost::from_base_and_surcharge(37.5, None);
|
||||
assert!(cost.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(cost.base_cost.kinovi_credits, 37.5);
|
||||
assert_eq!(cost.total_cost, cost.base_cost);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_surcharge_sums_into_total() {
|
||||
// 480p 5s with video ref: 37.5 base + 10 surcharge = 47.5 total.
|
||||
let cost = KinoviSeedanceMiniGenerationCost::from_base_and_surcharge(37.5, Some(10.0));
|
||||
assert_eq!(cost.base_cost.kinovi_credits, 37.5);
|
||||
assert_eq!(
|
||||
cost.video_reference_surcharge_cost.map(|c| c.kinovi_credits),
|
||||
Some(10.0),
|
||||
);
|
||||
assert_eq!(cost.total_cost.kinovi_credits, 47.5);
|
||||
// 47.5 credits; 4750/243 = 19.5473 → 20¢.
|
||||
assert_eq!(cost.total_cost.usd_cents_rounded_up, 20);
|
||||
assert_eq!(cost.total_cost.usd_cents_rounded_down, 19);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod constants;
|
||||
pub mod kinovi_fractional_generation_cost;
|
||||
pub mod kinovi_generation_cost;
|
||||
pub mod kinovi_seedance_generation_cost;
|
||||
pub mod kinovi_seedance_mini_generation_cost;
|
||||
|
||||
@@ -54,6 +54,7 @@ pub enum KinoviSeedance2p0OutputResolution {
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
FourK,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -77,6 +78,7 @@ pub enum KinoviSeedance2p0Bitrate {
|
||||
// | 480p | 15 |
|
||||
// | 720p | 40 |
|
||||
// | 1080p | 90 |
|
||||
// | 4K | 200 |
|
||||
//
|
||||
// Default resolution (None) is 720p.
|
||||
// Batch count multiplies the total cost.
|
||||
@@ -93,6 +95,7 @@ impl GenerateSeedance2p0Request {
|
||||
Some(KinoviSeedance2p0OutputResolution::FourEightyP) => 15,
|
||||
Some(KinoviSeedance2p0OutputResolution::SevenTwentyP) | None => 40,
|
||||
Some(KinoviSeedance2p0OutputResolution::TenEightyP) => 90,
|
||||
Some(KinoviSeedance2p0OutputResolution::FourK) => 200,
|
||||
};
|
||||
|
||||
// Video-reference surcharge, billed per second of OUTPUT duration
|
||||
@@ -103,6 +106,7 @@ impl GenerateSeedance2p0Request {
|
||||
// | 480p | 4 |
|
||||
// | 720p | 8 |
|
||||
// | 1080p | 18 |
|
||||
// | 4K | 40 |
|
||||
//
|
||||
// NB: Assumed flat per generation regardless of how many reference
|
||||
// videos are attached (Kinovi's pricing page only shows one).
|
||||
@@ -111,6 +115,7 @@ impl GenerateSeedance2p0Request {
|
||||
Some(KinoviSeedance2p0OutputResolution::FourEightyP) => 4,
|
||||
Some(KinoviSeedance2p0OutputResolution::SevenTwentyP) | None => 8,
|
||||
Some(KinoviSeedance2p0OutputResolution::TenEightyP) => 18,
|
||||
Some(KinoviSeedance2p0OutputResolution::FourK) => 40,
|
||||
}
|
||||
} else {
|
||||
0
|
||||
@@ -222,6 +227,7 @@ fn map_output_resolution(res: KinoviSeedance2p0OutputResolution) -> KinoviOutput
|
||||
KinoviSeedance2p0OutputResolution::FourEightyP => KinoviOutputResolutionRaw::FourEightyP,
|
||||
KinoviSeedance2p0OutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP,
|
||||
KinoviSeedance2p0OutputResolution::TenEightyP => KinoviOutputResolutionRaw::TenEightyP,
|
||||
KinoviSeedance2p0OutputResolution::FourK => KinoviOutputResolutionRaw::FourK,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +294,141 @@ mod tests {
|
||||
build_request(dur, Some(KinoviSeedance2p0OutputResolution::TenEightyP), None)
|
||||
}
|
||||
|
||||
/// A 4K request with NO video reference.
|
||||
fn r4k(dur: u8) -> GenerateSeedance2p0Request {
|
||||
build_request(dur, Some(KinoviSeedance2p0OutputResolution::FourK), None)
|
||||
}
|
||||
|
||||
/// A 4K request WITH a video reference (adds the per-second surcharge).
|
||||
fn r4k_with_video_ref(dur: u8) -> GenerateSeedance2p0Request {
|
||||
let mut request = r4k(dur);
|
||||
request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]);
|
||||
request
|
||||
}
|
||||
|
||||
fn total_credits(request: &GenerateSeedance2p0Request) -> u64 {
|
||||
request.calculate_costs().total_cost.kinovi_credits
|
||||
}
|
||||
|
||||
fn total_usd_cents(request: &GenerateSeedance2p0Request) -> u64 {
|
||||
request.calculate_costs().total_cost.usd_cents_rounded_up
|
||||
}
|
||||
|
||||
// ── 4K pricing (Seedance 2.0 only) ──
|
||||
//
|
||||
// Base: 200 credits/sec. With a video reference: 240 credits/sec
|
||||
// (200 base + 40/sec surcharge). Every duration × video-ref combination
|
||||
// is asserted below against Kinovi's published 4K numbers.
|
||||
|
||||
mod four_k_pricing {
|
||||
use super::*;
|
||||
|
||||
// ── No video reference: 200 credits/sec ──
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_4_seconds_is_800_credits() {
|
||||
assert_eq!(total_credits(&r4k(4)), 800);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_5_seconds_is_1000_credits() {
|
||||
assert_eq!(total_credits(&r4k(5)), 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_10_seconds_is_2000_credits() {
|
||||
assert_eq!(total_credits(&r4k(10)), 2000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_15_seconds_is_3000_credits() {
|
||||
assert_eq!(total_credits(&r4k(15)), 3000);
|
||||
}
|
||||
|
||||
// ── With video reference: 240 credits/sec (200 base + 40 surcharge) ──
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_4_seconds_is_960_credits() {
|
||||
assert_eq!(total_credits(&r4k_with_video_ref(4)), 960);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_5_seconds_is_1200_credits() {
|
||||
assert_eq!(total_credits(&r4k_with_video_ref(5)), 1200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_10_seconds_is_2400_credits() {
|
||||
assert_eq!(total_credits(&r4k_with_video_ref(10)), 2400);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_15_seconds_is_3600_credits() {
|
||||
assert_eq!(total_credits(&r4k_with_video_ref(15)), 3600);
|
||||
}
|
||||
|
||||
// ── Total cost in USD cents — no video reference ──
|
||||
//
|
||||
// 200 credits/sec; usd_cents = ceil(total_credits / 243 * 100).
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_4_seconds_is_330_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k(4)), 330);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_5_seconds_is_412_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k(5)), 412);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_10_seconds_is_824_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k(10)), 824);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_no_video_ref_15_seconds_is_1235_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k(15)), 1235);
|
||||
}
|
||||
|
||||
// ── Total cost in USD cents — with video reference ──
|
||||
//
|
||||
// 240 credits/sec (200 base + 40 surcharge); usd_cents = ceil(total_credits / 243 * 100).
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_4_seconds_is_396_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k_with_video_ref(4)), 396);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_5_seconds_is_494_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k_with_video_ref(5)), 494);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_10_seconds_is_988_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k_with_video_ref(10)), 988);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_15_seconds_is_1482_cents() {
|
||||
assert_eq!(total_usd_cents(&r4k_with_video_ref(15)), 1482);
|
||||
}
|
||||
|
||||
// ── Base / surcharge breakdown (5 seconds) ──
|
||||
|
||||
#[test]
|
||||
fn four_k_with_video_ref_breaks_base_and_surcharge_apart() {
|
||||
let costs = r4k_with_video_ref(5).calculate_costs();
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 1000); // 5s × 200
|
||||
assert_eq!(
|
||||
costs.video_reference_surcharge_cost.as_ref().map(|c| c.kinovi_credits),
|
||||
Some(200), // 5s × 40
|
||||
);
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 1200);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Comprehensive per-resolution coverage ──
|
||||
//
|
||||
// Seedance 2.0: 480p 15/s (+4/s video ref), 720p 40/s (+8/s), 1080p 90/s (+18/s).
|
||||
@@ -1093,6 +1234,42 @@ mod tests {
|
||||
assert_eq!(1, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 4K with image references (Seedance 2.0 only), 5-second clip.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_4k_image_references() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let img1 = upload_test_image(&session, test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL).await?;
|
||||
let img2 = upload_test_image(&session, test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL).await?;
|
||||
let img3 = upload_test_image(&session, test_data::web::image_urls::FOREST_BACKDROP_IMAGE_URL).await?;
|
||||
|
||||
let result = generate_seedance_2p0(GenerateSeedance2p0Args {
|
||||
session: &session,
|
||||
host_override: None,
|
||||
request: GenerateSeedance2p0Request {
|
||||
prompt: "The dog in @1 explores the scenery in @3 and meets the friendly man in @2. Cinematic 4K detail.".to_string(),
|
||||
aspect_ratio: Some(KinoviSeedance2p0AspectRatio::Landscape16x9),
|
||||
output_resolution: Some(KinoviSeedance2p0OutputResolution::FourK),
|
||||
batch_count: None,
|
||||
duration_seconds: 5,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: Some(vec![img1, img2, img3]),
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("4K image ref — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert!(!result.order_id.is_empty());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod video_reference {
|
||||
|
||||
+755
@@ -0,0 +1,755 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::cost::kinovi_seedance_mini_generation_cost::KinoviSeedanceMiniGenerationCost;
|
||||
use crate::requests::kinovi_host::KinoviHost;
|
||||
use crate::requests::workflow_run_task::workflow_run_task::{
|
||||
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, KinoviBitrateRaw,
|
||||
KinoviModelTypeRaw, KinoviOutputResolutionRaw, WorkflowRunTaskArgs,
|
||||
WorkflowRunTaskRequest,
|
||||
};
|
||||
|
||||
// ── Args ──
|
||||
|
||||
pub struct GenerateSeedance2p0MiniArgs<'a> {
|
||||
pub request: GenerateSeedance2p0MiniRequest,
|
||||
pub session: &'a Seedance2ProSession,
|
||||
pub host_override: Option<KinoviHost>,
|
||||
}
|
||||
|
||||
// ── Request ──
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateSeedance2p0MiniRequest {
|
||||
pub prompt: String,
|
||||
pub aspect_ratio: Option<KinoviSeedance2p0MiniAspectRatio>,
|
||||
pub output_resolution: Option<KinoviSeedance2p0MiniOutputResolution>,
|
||||
pub duration_seconds: u8,
|
||||
pub batch_count: Option<KinoviSeedance2p0MiniBatchCount>,
|
||||
pub start_frame_url: Option<String>,
|
||||
pub end_frame_url: Option<String>,
|
||||
pub reference_image_urls: Option<Vec<String>>,
|
||||
pub reference_video_urls: Option<Vec<String>>,
|
||||
pub reference_audio_urls: Option<Vec<String>>,
|
||||
pub character_ids: Option<Vec<String>>,
|
||||
pub use_face_blur_hack: Option<bool>,
|
||||
/// Output video bitrate. None defaults to "normal"; `High` requests a
|
||||
/// higher bitrate. Does not affect cost.
|
||||
pub bitrate: Option<KinoviSeedance2p0MiniBitrate>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum KinoviSeedance2p0MiniAspectRatio {
|
||||
Landscape16x9,
|
||||
UltraWide21x9,
|
||||
Portrait9x16,
|
||||
Square1x1,
|
||||
Standard4x3,
|
||||
Portrait3x4,
|
||||
}
|
||||
|
||||
/// Output resolution. Mini supports only 480p and 720p (no 1080p / 4K).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum KinoviSeedance2p0MiniOutputResolution {
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
}
|
||||
|
||||
/// Number of videos to generate in one request. Mini allows 1–8.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum KinoviSeedance2p0MiniBatchCount {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
Five,
|
||||
Six,
|
||||
Seven,
|
||||
Eight,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum KinoviSeedance2p0MiniBitrate {
|
||||
High,
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
//
|
||||
// Seedance 2.0 Mini credit pricing (per second of OUTPUT duration):
|
||||
//
|
||||
// | Resolution | Credits/sec | Video-ref surcharge/sec |
|
||||
// |------------|-------------|-------------------------|
|
||||
// | 480p | 7.5 | 2 |
|
||||
// | 720p | 20 | 4 |
|
||||
//
|
||||
// 480p credits can be FRACTIONAL (7.5/sec lands on half-credits at odd
|
||||
// durations, e.g. 5s = 37.5). Default resolution (None) is 720p. Batch count
|
||||
// multiplies the total. Attaching reference VIDEOS adds the surcharge;
|
||||
// reference images and audio are free.
|
||||
//
|
||||
// Credit package: 525,000 credits for $2,159.0909 (~243 credits/$1).
|
||||
|
||||
impl GenerateSeedance2p0MiniRequest {
|
||||
/// Calculate the cost of this generation request, in Kinovi credits
|
||||
/// (possibly fractional) and USD cents.
|
||||
pub fn calculate_costs(&self) -> KinoviSeedanceMiniGenerationCost {
|
||||
let credits_per_second: f64 = match self.output_resolution {
|
||||
Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP) => 7.5,
|
||||
Some(KinoviSeedance2p0MiniOutputResolution::SevenTwentyP) | None => 20.0,
|
||||
};
|
||||
|
||||
let video_reference_surcharge_per_second: f64 = if self.has_video_reference() {
|
||||
match self.output_resolution {
|
||||
Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP) => 2.0,
|
||||
Some(KinoviSeedance2p0MiniOutputResolution::SevenTwentyP) | None => 4.0,
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let batch_multiplier = f64::from(self.batch_multiplier());
|
||||
let duration = f64::from(self.duration_seconds);
|
||||
|
||||
let base_credits = duration * credits_per_second * batch_multiplier;
|
||||
let maybe_video_reference_surcharge_credits = if self.has_video_reference() {
|
||||
Some(duration * video_reference_surcharge_per_second * batch_multiplier)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
KinoviSeedanceMiniGenerationCost::from_base_and_surcharge(
|
||||
base_credits,
|
||||
maybe_video_reference_surcharge_credits,
|
||||
)
|
||||
}
|
||||
|
||||
fn has_video_reference(&self) -> bool {
|
||||
self.reference_video_urls
|
||||
.as_ref()
|
||||
.is_some_and(|urls| !urls.is_empty())
|
||||
}
|
||||
|
||||
fn batch_multiplier(&self) -> u8 {
|
||||
match self.batch_count {
|
||||
None | Some(KinoviSeedance2p0MiniBatchCount::One) => 1,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Two) => 2,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Three) => 3,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Four) => 4,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Five) => 5,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Six) => 6,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Seven) => 7,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Eight) => 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateSeedance2p0MiniResponse {
|
||||
pub task_id: String,
|
||||
pub order_id: String,
|
||||
pub task_ids: Option<Vec<String>>,
|
||||
pub order_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
pub async fn generate_seedance_2p0_mini(
|
||||
args: GenerateSeedance2p0MiniArgs<'_>,
|
||||
) -> Result<GenerateSeedance2p0MiniResponse, Seedance2ProError> {
|
||||
let raw_response = workflow_run_task(WorkflowRunTaskArgs {
|
||||
request: to_raw_request(args.request),
|
||||
session: args.session,
|
||||
host_override: args.host_override,
|
||||
}).await?;
|
||||
|
||||
Ok(GenerateSeedance2p0MiniResponse {
|
||||
task_id: raw_response.task_id,
|
||||
order_id: raw_response.order_id,
|
||||
task_ids: raw_response.task_ids,
|
||||
order_ids: raw_response.order_ids,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Mapping helpers ──
|
||||
|
||||
fn to_raw_request(req: GenerateSeedance2p0MiniRequest) -> WorkflowRunTaskRequest {
|
||||
WorkflowRunTaskRequest {
|
||||
model_type: KinoviModelTypeRaw::Seedance2Mini,
|
||||
prompt: req.prompt,
|
||||
aspect_ratio: map_aspect_ratio(req.aspect_ratio),
|
||||
output_resolution: req.output_resolution.map(map_output_resolution),
|
||||
batch_count: map_batch_count(req.batch_count),
|
||||
duration_seconds: req.duration_seconds,
|
||||
start_frame_url: req.start_frame_url,
|
||||
end_frame_url: req.end_frame_url,
|
||||
reference_image_urls: req.reference_image_urls,
|
||||
reference_video_urls: req.reference_video_urls,
|
||||
reference_audio_urls: req.reference_audio_urls,
|
||||
character_ids: req.character_ids,
|
||||
use_face_blur_hack: req.use_face_blur_hack,
|
||||
bitrate: map_bitrate(req.bitrate),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_aspect_ratio(ar: Option<KinoviSeedance2p0MiniAspectRatio>) -> KinoviAspectRatioRaw {
|
||||
match ar {
|
||||
Some(KinoviSeedance2p0MiniAspectRatio::Landscape16x9) => KinoviAspectRatioRaw::Landscape16x9,
|
||||
Some(KinoviSeedance2p0MiniAspectRatio::UltraWide21x9) => KinoviAspectRatioRaw::UltraWide21x9,
|
||||
Some(KinoviSeedance2p0MiniAspectRatio::Portrait9x16) => KinoviAspectRatioRaw::Portrait9x16,
|
||||
Some(KinoviSeedance2p0MiniAspectRatio::Square1x1) => KinoviAspectRatioRaw::Square1x1,
|
||||
Some(KinoviSeedance2p0MiniAspectRatio::Standard4x3) => KinoviAspectRatioRaw::Landscape4x3,
|
||||
Some(KinoviSeedance2p0MiniAspectRatio::Portrait3x4) => KinoviAspectRatioRaw::Portrait3x4,
|
||||
None => KinoviAspectRatioRaw::Landscape16x9,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_output_resolution(res: KinoviSeedance2p0MiniOutputResolution) -> KinoviOutputResolutionRaw {
|
||||
match res {
|
||||
KinoviSeedance2p0MiniOutputResolution::FourEightyP => KinoviOutputResolutionRaw::FourEightyP,
|
||||
KinoviSeedance2p0MiniOutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_batch_count(bc: Option<KinoviSeedance2p0MiniBatchCount>) -> KinoviBatchCountRaw {
|
||||
match bc {
|
||||
Some(KinoviSeedance2p0MiniBatchCount::One) | None => KinoviBatchCountRaw::One,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Two) => KinoviBatchCountRaw::Two,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Three) => KinoviBatchCountRaw::Three,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Four) => KinoviBatchCountRaw::Four,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Five) => KinoviBatchCountRaw::Five,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Six) => KinoviBatchCountRaw::Six,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Seven) => KinoviBatchCountRaw::Seven,
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Eight) => KinoviBatchCountRaw::Eight,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_bitrate(bitrate: Option<KinoviSeedance2p0MiniBitrate>) -> Option<KinoviBitrateRaw> {
|
||||
match bitrate {
|
||||
Some(KinoviSeedance2p0MiniBitrate::High) => Some(KinoviBitrateRaw::High),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ──
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::test_utils::get_test_cookies::get_test_cookies;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
|
||||
mod pricing_tests {
|
||||
use super::*;
|
||||
|
||||
const FLOAT_TOLERANCE: f64 = 1e-9;
|
||||
|
||||
fn build_request(
|
||||
duration_seconds: u8,
|
||||
output_resolution: Option<KinoviSeedance2p0MiniOutputResolution>,
|
||||
batch_count: Option<KinoviSeedance2p0MiniBatchCount>,
|
||||
) -> GenerateSeedance2p0MiniRequest {
|
||||
GenerateSeedance2p0MiniRequest {
|
||||
prompt: String::new(),
|
||||
aspect_ratio: None,
|
||||
output_resolution,
|
||||
batch_count,
|
||||
duration_seconds,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn r480(dur: u8) -> GenerateSeedance2p0MiniRequest {
|
||||
build_request(dur, Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP), None)
|
||||
}
|
||||
|
||||
fn r720(dur: u8) -> GenerateSeedance2p0MiniRequest {
|
||||
build_request(dur, None, None)
|
||||
}
|
||||
|
||||
fn with_video_ref(mut request: GenerateSeedance2p0MiniRequest) -> GenerateSeedance2p0MiniRequest {
|
||||
request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]);
|
||||
request
|
||||
}
|
||||
|
||||
// ── Full pricing table (credits) ──
|
||||
//
|
||||
// The exact table provided by Kinovi for Seedance 2.0 Mini. 480p produces
|
||||
// fractional credits at odd durations (37.5, 112.5, …).
|
||||
|
||||
mod kinovi_pricing_table {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn table_480p_without_video_reference() {
|
||||
assert_eq!(r480(4).calculate_costs().total_cost.kinovi_credits, 30.0);
|
||||
assert_eq!(r480(5).calculate_costs().total_cost.kinovi_credits, 37.5);
|
||||
assert_eq!(r480(10).calculate_costs().total_cost.kinovi_credits, 75.0);
|
||||
assert_eq!(r480(15).calculate_costs().total_cost.kinovi_credits, 112.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_720p_without_video_reference() {
|
||||
assert_eq!(r720(4).calculate_costs().total_cost.kinovi_credits, 80.0);
|
||||
assert_eq!(r720(5).calculate_costs().total_cost.kinovi_credits, 100.0);
|
||||
assert_eq!(r720(10).calculate_costs().total_cost.kinovi_credits, 200.0);
|
||||
assert_eq!(r720(15).calculate_costs().total_cost.kinovi_credits, 300.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_480p_with_video_reference() {
|
||||
assert_eq!(with_video_ref(r480(4)).calculate_costs().total_cost.kinovi_credits, 38.0);
|
||||
assert_eq!(with_video_ref(r480(5)).calculate_costs().total_cost.kinovi_credits, 47.5);
|
||||
assert_eq!(with_video_ref(r480(10)).calculate_costs().total_cost.kinovi_credits, 95.0);
|
||||
assert_eq!(with_video_ref(r480(15)).calculate_costs().total_cost.kinovi_credits, 142.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_720p_with_video_reference() {
|
||||
assert_eq!(with_video_ref(r720(4)).calculate_costs().total_cost.kinovi_credits, 96.0);
|
||||
assert_eq!(with_video_ref(r720(5)).calculate_costs().total_cost.kinovi_credits, 120.0);
|
||||
assert_eq!(with_video_ref(r720(10)).calculate_costs().total_cost.kinovi_credits, 240.0);
|
||||
assert_eq!(with_video_ref(r720(15)).calculate_costs().total_cost.kinovi_credits, 360.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Base / surcharge breakdown ──
|
||||
|
||||
mod video_reference_surcharge {
|
||||
use super::*;
|
||||
|
||||
/// The surcharge is the difference between the with-ref total and the
|
||||
/// base: 480p +2/s, 720p +4/s.
|
||||
#[test]
|
||||
fn surcharge_table() {
|
||||
// (request builder, duration, base credits, surcharge credits)
|
||||
let cases: &[(fn(u8) -> GenerateSeedance2p0MiniRequest, u8, f64, f64)] = &[
|
||||
// 480p: 7.5/s base, +2/s surcharge
|
||||
(r480, 4, 30.0, 8.0),
|
||||
(r480, 5, 37.5, 10.0),
|
||||
(r480, 10, 75.0, 20.0),
|
||||
(r480, 15, 112.5, 30.0),
|
||||
// 720p: 20/s base, +4/s surcharge
|
||||
(r720, 4, 80.0, 16.0),
|
||||
(r720, 5, 100.0, 20.0),
|
||||
(r720, 10, 200.0, 40.0),
|
||||
(r720, 15, 300.0, 60.0),
|
||||
];
|
||||
|
||||
for (make, duration, base, surcharge) in cases {
|
||||
let costs = with_video_ref(make(*duration)).calculate_costs();
|
||||
assert_eq!(costs.base_cost.kinovi_credits, *base, "base at {duration}s");
|
||||
assert_eq!(
|
||||
costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits),
|
||||
Some(*surcharge),
|
||||
"surcharge at {duration}s",
|
||||
);
|
||||
assert_eq!(costs.total_cost.kinovi_credits, base + surcharge, "total at {duration}s");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_video_reference_has_no_surcharge() {
|
||||
let costs = r480(5).calculate_costs();
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.total_cost, costs.base_cost);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_video_reference_list_has_no_surcharge() {
|
||||
let mut request = r720(5);
|
||||
request.reference_video_urls = Some(vec![]);
|
||||
let costs = request.calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 100.0);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
}
|
||||
|
||||
/// The surcharge applies per generated video, so batches multiply it.
|
||||
#[test]
|
||||
fn batch_multiplies_surcharge() {
|
||||
let request = with_video_ref(build_request(
|
||||
5,
|
||||
Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP),
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Two),
|
||||
));
|
||||
// (37.5 base + 10 surcharge) × 2
|
||||
let costs = request.calculate_costs();
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 75.0);
|
||||
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(20.0));
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 95.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── USD cents conversion ──
|
||||
|
||||
mod usd_cents {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fractional_480p_5s() {
|
||||
// 37.5 credits; 3750/243 = 15.43 → 16¢ up, 15¢ down.
|
||||
let costs = r480(5).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 37.5);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 16);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 15);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (3750.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whole_720p_5s() {
|
||||
// 100 credits; 10000/243 = 41.15 → 42¢ up, 41¢ down.
|
||||
let costs = r720(5).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 100.0);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 42);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 41);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (10000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fractional_480p_15s_with_video_reference() {
|
||||
// 142.5 credits; 14250/243 = 58.64 → 59¢ up, 58¢ down.
|
||||
let costs = with_video_ref(r480(15)).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 142.5);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 59);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 58);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (14250.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Batch multiplier ──
|
||||
|
||||
mod batch_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_scales_total_linearly() {
|
||||
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let by_count = [
|
||||
(KinoviSeedance2p0MiniBatchCount::One, 1.0),
|
||||
(KinoviSeedance2p0MiniBatchCount::Two, 2.0),
|
||||
(KinoviSeedance2p0MiniBatchCount::Three, 3.0),
|
||||
(KinoviSeedance2p0MiniBatchCount::Four, 4.0),
|
||||
(KinoviSeedance2p0MiniBatchCount::Five, 5.0),
|
||||
(KinoviSeedance2p0MiniBatchCount::Six, 6.0),
|
||||
(KinoviSeedance2p0MiniBatchCount::Seven, 7.0),
|
||||
(KinoviSeedance2p0MiniBatchCount::Eight, 8.0),
|
||||
];
|
||||
for (count, multiplier) in by_count {
|
||||
let credits = build_request(5, None, Some(count)).calculate_costs().total_cost.kinovi_credits;
|
||||
assert_eq!(credits, base * multiplier, "batch {count:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_eight_480p_with_video_reference() {
|
||||
let costs = with_video_ref(build_request(
|
||||
5,
|
||||
Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP),
|
||||
Some(KinoviSeedance2p0MiniBatchCount::Eight),
|
||||
)).calculate_costs();
|
||||
// (37.5 + 10) × 8 = 380
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 380.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Defaults & invariants ──
|
||||
|
||||
#[test]
|
||||
fn default_resolution_is_720p() {
|
||||
let default = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let explicit = build_request(5, Some(KinoviSeedance2p0MiniOutputResolution::SevenTwentyP), None)
|
||||
.calculate_costs().total_cost.kinovi_credits;
|
||||
assert_eq!(default, explicit);
|
||||
assert_eq!(default, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = r720(4).calculate_costs().total_cost.kinovi_credits;
|
||||
let c10 = r720(10).calculate_costs().total_cost.kinovi_credits;
|
||||
let c15 = r720(15).calculate_costs().total_cost.kinovi_credits;
|
||||
assert!(c4 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_480p_cheaper_than_720p() {
|
||||
for dur in 4..=15u8 {
|
||||
let c480 = r480(dur).calculate_costs().total_cost.kinovi_credits;
|
||||
let c720 = r720(dur).calculate_costs().total_cost.kinovi_credits;
|
||||
assert!(c480 < c720, "480p should be cheaper than 720p at {dur}s");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_does_not_affect_credits() {
|
||||
let baseline = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let ratios = [
|
||||
KinoviSeedance2p0MiniAspectRatio::Landscape16x9,
|
||||
KinoviSeedance2p0MiniAspectRatio::UltraWide21x9,
|
||||
KinoviSeedance2p0MiniAspectRatio::Portrait9x16,
|
||||
KinoviSeedance2p0MiniAspectRatio::Square1x1,
|
||||
KinoviSeedance2p0MiniAspectRatio::Standard4x3,
|
||||
KinoviSeedance2p0MiniAspectRatio::Portrait3x4,
|
||||
];
|
||||
for ar in ratios {
|
||||
let mut req = r720(5);
|
||||
req.aspect_ratio = Some(ar);
|
||||
assert_eq!(req.calculate_costs().total_cost.kinovi_credits, baseline, "{ar:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_bitrate_does_not_affect_credits() {
|
||||
let baseline = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let mut high = r720(5);
|
||||
high.bitrate = Some(KinoviSeedance2p0MiniBitrate::High);
|
||||
assert_eq!(high.calculate_costs().total_cost.kinovi_credits, baseline);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Request shape (mapping to the raw Kinovi request) ──
|
||||
|
||||
mod request_shape_tests {
|
||||
use super::*;
|
||||
|
||||
fn sample() -> GenerateSeedance2p0MiniRequest {
|
||||
GenerateSeedance2p0MiniRequest {
|
||||
prompt: "a corgi".to_string(),
|
||||
aspect_ratio: Some(KinoviSeedance2p0MiniAspectRatio::Standard4x3),
|
||||
output_resolution: Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP),
|
||||
duration_seconds: 10,
|
||||
batch_count: Some(KinoviSeedance2p0MiniBatchCount::Eight),
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: Some(vec!["https://example.com/a.png".to_string()]),
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: Some(KinoviSeedance2p0MiniBitrate::High),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_to_mini_model_type() {
|
||||
let raw = to_raw_request(sample());
|
||||
assert!(matches!(raw.model_type, KinoviModelTypeRaw::Seedance2Mini));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_resolution_aspect_and_batch() {
|
||||
let raw = to_raw_request(sample());
|
||||
assert!(matches!(raw.aspect_ratio, KinoviAspectRatioRaw::Landscape4x3));
|
||||
assert!(matches!(raw.output_resolution, Some(KinoviOutputResolutionRaw::FourEightyP)));
|
||||
assert!(matches!(raw.batch_count, KinoviBatchCountRaw::Eight));
|
||||
assert!(matches!(raw.bitrate, Some(KinoviBitrateRaw::High)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passes_through_prompt_duration_and_references() {
|
||||
let raw = to_raw_request(sample());
|
||||
assert_eq!(raw.prompt, "a corgi");
|
||||
assert_eq!(raw.duration_seconds, 10);
|
||||
assert_eq!(raw.reference_image_urls, Some(vec!["https://example.com/a.png".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolution_maps_to_none() {
|
||||
let mut req = sample();
|
||||
req.output_resolution = None;
|
||||
let raw = to_raw_request(req);
|
||||
assert!(raw.output_resolution.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_batch_counts_map() {
|
||||
let expected: &[(KinoviSeedance2p0MiniBatchCount, u8)] = &[
|
||||
(KinoviSeedance2p0MiniBatchCount::One, 1),
|
||||
(KinoviSeedance2p0MiniBatchCount::Two, 2),
|
||||
(KinoviSeedance2p0MiniBatchCount::Three, 3),
|
||||
(KinoviSeedance2p0MiniBatchCount::Four, 4),
|
||||
(KinoviSeedance2p0MiniBatchCount::Five, 5),
|
||||
(KinoviSeedance2p0MiniBatchCount::Six, 6),
|
||||
(KinoviSeedance2p0MiniBatchCount::Seven, 7),
|
||||
(KinoviSeedance2p0MiniBatchCount::Eight, 8),
|
||||
];
|
||||
for (count, n) in expected {
|
||||
let mut req = sample();
|
||||
req.batch_count = Some(*count);
|
||||
let raw = to_raw_request(req);
|
||||
// The mapped raw batch matches the requested count's multiplier.
|
||||
let mut probe = build_probe();
|
||||
probe.batch_count = Some(*count);
|
||||
assert_eq!(probe.batch_multiplier(), *n, "{count:?}");
|
||||
// And the raw enum is the corresponding variant.
|
||||
assert!(!matches!(raw.model_type, KinoviModelTypeRaw::Seedance2Pro));
|
||||
}
|
||||
}
|
||||
|
||||
fn build_probe() -> GenerateSeedance2p0MiniRequest {
|
||||
GenerateSeedance2p0MiniRequest {
|
||||
prompt: String::new(),
|
||||
aspect_ratio: None,
|
||||
output_resolution: None,
|
||||
duration_seconds: 5,
|
||||
batch_count: None,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Live API tests (manual; require Kinovi cookies) ──
|
||||
|
||||
fn test_session() -> AnyhowResult<Seedance2ProSession> {
|
||||
let cookies = get_test_cookies()?;
|
||||
Ok(Seedance2ProSession::from_cookies_string(cookies))
|
||||
}
|
||||
|
||||
mod live {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_text_to_video_720p() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let result = generate_seedance_2p0_mini(GenerateSeedance2p0MiniArgs {
|
||||
session: &session,
|
||||
host_override: None,
|
||||
request: GenerateSeedance2p0MiniRequest {
|
||||
prompt: "A giant evil teddy bear steps on people in the city.".to_string(),
|
||||
aspect_ratio: Some(KinoviSeedance2p0MiniAspectRatio::Landscape16x9),
|
||||
output_resolution: None,
|
||||
batch_count: None,
|
||||
duration_seconds: 5,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("mini t2v 720p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert!(!result.order_id.is_empty());
|
||||
assert_eq!(1, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_text_to_video_480p() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let result = generate_seedance_2p0_mini(GenerateSeedance2p0MiniArgs {
|
||||
session: &session,
|
||||
host_override: None,
|
||||
request: GenerateSeedance2p0MiniRequest {
|
||||
prompt: "A snowman fights a bear".to_string(),
|
||||
aspect_ratio: Some(KinoviSeedance2p0MiniAspectRatio::Standard4x3),
|
||||
output_resolution: Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP),
|
||||
batch_count: None,
|
||||
duration_seconds: 10,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("mini t2v 480p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert_eq!(1, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_reference_to_video_720p() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let result = generate_seedance_2p0_mini(GenerateSeedance2p0MiniArgs {
|
||||
session: &session,
|
||||
host_override: None,
|
||||
request: GenerateSeedance2p0MiniRequest {
|
||||
prompt: "Girl walks around in the city, giving a historical tour".to_string(),
|
||||
aspect_ratio: Some(KinoviSeedance2p0MiniAspectRatio::UltraWide21x9),
|
||||
output_resolution: None,
|
||||
batch_count: None,
|
||||
duration_seconds: 15,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: Some(vec![
|
||||
"https://static.seedance2-pro.com/materials/20260624/1782342102687-53fe5496.png".to_string(),
|
||||
]),
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("mini reference-to-video 720p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert_eq!(1, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_video_reference_480p() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let result = generate_seedance_2p0_mini(GenerateSeedance2p0MiniArgs {
|
||||
session: &session,
|
||||
host_override: None,
|
||||
request: GenerateSeedance2p0MiniRequest {
|
||||
prompt: "Change @video1 to night time.".to_string(),
|
||||
aspect_ratio: Some(KinoviSeedance2p0MiniAspectRatio::Landscape16x9),
|
||||
output_resolution: Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP),
|
||||
batch_count: None,
|
||||
duration_seconds: 5,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: Some(vec![
|
||||
"https://static.seedance2-pro.com/materials/20260315/1773594284659-3a46d231.mp4".to_string(),
|
||||
]),
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("mini video ref 480p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert_eq!(1, 2);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod generate_happy_horse_1p0;
|
||||
pub mod generate_seedance_2p0;
|
||||
pub mod generate_seedance_2p0_fast;
|
||||
pub mod generate_seedance_2p0_mini;
|
||||
|
||||
+17
-2
@@ -22,12 +22,27 @@ pub (super) struct BatchRequestJson {
|
||||
#[derive(Serialize, Debug)]
|
||||
pub (super) struct ApiParams {
|
||||
pub prompt: String,
|
||||
pub resolution: String,
|
||||
/// Aspect ratio as pixel dimensions (e.g. "1280x720"). Most Kinovi
|
||||
/// models carry the aspect ratio here. None for models that use the
|
||||
/// `aspectRatio` field instead (Seedance 2.0 Mini).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution: Option<String>,
|
||||
/// Aspect ratio as a ratio string (e.g. "16:9"). Used by Seedance 2.0
|
||||
/// Mini instead of `resolution`. None for models that use `resolution`.
|
||||
#[serde(rename = "aspectRatio", skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio: Option<&'static str>,
|
||||
#[serde(rename = "contentMode")]
|
||||
pub content_mode: &'static str,
|
||||
pub model: &'static str,
|
||||
pub duration: String,
|
||||
pub mode: &'static str,
|
||||
/// Generation mode ("keyframe" / "reference"). Omitted for models that use
|
||||
/// `happyhorseMode` instead (Happy Horse).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<&'static str>,
|
||||
/// Happy Horse generation mode ("t2v" / "i2v"). Used instead of `mode` by
|
||||
/// Happy Horse; None for every other model.
|
||||
#[serde(rename = "happyhorseMode", skip_serializing_if = "Option::is_none")]
|
||||
pub happyhorse_mode: Option<&'static str>,
|
||||
#[serde(rename = "outputResolution", skip_serializing_if = "Option::is_none")]
|
||||
pub output_resolution: Option<&'static str>,
|
||||
#[serde(rename = "faceBlurMode", skip_serializing_if = "Option::is_none")]
|
||||
|
||||
+457
-449
@@ -105,80 +105,6 @@ impl std::fmt::Debug for WorkflowRunTaskArgs<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkflowRunTaskRequest {
|
||||
/// Estimates the credit cost for this generation request.
|
||||
///
|
||||
/// Pricing is per-second × batch count, with the per-second rate
|
||||
/// depending on model type and output resolution:
|
||||
///
|
||||
/// | Model | 480p | 720p | 1080p |
|
||||
/// |--------------|------|------|-------|
|
||||
/// | Pro | 15 | 40 | 90 |
|
||||
/// | Fast | 10 | 28 | n/a |
|
||||
/// | HappyHorse | 15 | 40 | 90 |
|
||||
/// TODO(bt,2026-04-23): Not sure pricing for Happy Horse is correct here.
|
||||
///
|
||||
/// Input mode (text, keyframe, reference) has no effect on cost.
|
||||
/// Aspect ratio (`resolution` field) has no effect on cost.
|
||||
pub fn estimate_credits(&self) -> u32 {
|
||||
let credits_per_second: u32 = match (self.model_type, self.output_resolution) {
|
||||
// Seedance 2.0 Pro
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP)) => 15,
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None)
|
||||
| (KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 40,
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP)) => 90,
|
||||
|
||||
// Seedance 2.0 Fast
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP)) => 10,
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None)
|
||||
| (KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 28,
|
||||
// NB: 1080p not officially supported for Fast, but price as 720p if requested
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::TenEightyP)) => 28,
|
||||
|
||||
// TODO(bt,2026-04-23): Not sure pricing for Happy Horse is correct here.
|
||||
// Happy Horse 1.0 — same credit rates as Seedance 2.0 Pro
|
||||
(KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::FourEightyP)) => 15,
|
||||
(KinoviModelTypeRaw::HappyHorse1p0, None)
|
||||
| (KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 40,
|
||||
(KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::TenEightyP)) => 90,
|
||||
};
|
||||
|
||||
let per_video = u32::from(self.duration_seconds) * credits_per_second;
|
||||
let batch_multiplier: u32 = match self.batch_count {
|
||||
KinoviBatchCountRaw::One => 1,
|
||||
KinoviBatchCountRaw::Two => 2,
|
||||
KinoviBatchCountRaw::Four => 4,
|
||||
};
|
||||
per_video * batch_multiplier
|
||||
}
|
||||
|
||||
/// Credits per dollar for billing conversion.
|
||||
///
|
||||
/// Legacy 720p pricing uses the original Kinovi credit package rates.
|
||||
/// All other model/resolution combos use the newer rate: 525,000 credits / $2,159.0909.
|
||||
fn credits_per_dollar(&self) -> f64 {
|
||||
match (self.model_type, self.output_resolution) {
|
||||
// Legacy: Seedance 2.0 Pro @ 720p — 25,000 credits for $99.99
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None)
|
||||
| (KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 250.0,
|
||||
|
||||
// Legacy: Seedance 2.0 Fast @ 720p — 22,000 credits for $99.99
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None)
|
||||
| (KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 220.0,
|
||||
|
||||
// New pricing: 525,000 credits for $2,159.0909 (~243.16 credits/$1)
|
||||
_ => 243.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
let credits = self.estimate_credits() as f64;
|
||||
let credits_per_dollar = self.credits_per_dollar();
|
||||
let cost = credits / credits_per_dollar * 100.0;
|
||||
cost.round() as u64
|
||||
}
|
||||
}
|
||||
|
||||
// --- Public enums ---
|
||||
|
||||
/// Video resolution / aspect ratio.
|
||||
@@ -199,14 +125,16 @@ pub enum KinoviAspectRatioRaw {
|
||||
}
|
||||
|
||||
impl KinoviAspectRatioRaw {
|
||||
fn as_str(&self) -> &'static str {
|
||||
/// The aspect ratio as a ratio string (e.g. "16:9"), used by models that
|
||||
/// send an `aspectRatio` field (Seedance 2.0 Mini) instead of `resolution`.
|
||||
fn as_aspect_ratio_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Landscape16x9 => "1280x720",
|
||||
Self::UltraWide21x9 => "1280x540",
|
||||
Self::Portrait9x16 => "720x1280",
|
||||
Self::Square1x1 => "720x720",
|
||||
Self::Landscape4x3 => "960x720",
|
||||
Self::Portrait3x4 => "720x960",
|
||||
Self::Landscape16x9 => "16:9",
|
||||
Self::UltraWide21x9 => "21:9",
|
||||
Self::Portrait9x16 => "9:16",
|
||||
Self::Square1x1 => "1:1",
|
||||
Self::Landscape4x3 => "4:3",
|
||||
Self::Portrait3x4 => "3:4",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,6 +148,9 @@ pub enum KinoviOutputResolutionRaw {
|
||||
SevenTwentyP,
|
||||
/// 1080p
|
||||
TenEightyP,
|
||||
/// 4K. Only supported by Seedance 2.0 (Pro). Seedance 2.0 Fast does NOT
|
||||
/// offer 4K — requesting it for Fast is not valid upstream.
|
||||
FourK,
|
||||
}
|
||||
|
||||
impl KinoviOutputResolutionRaw {
|
||||
@@ -230,6 +161,7 @@ impl KinoviOutputResolutionRaw {
|
||||
Self::FourEightyP => Some("480p"),
|
||||
Self::SevenTwentyP => None, // Default — omit from request
|
||||
Self::TenEightyP => Some("1080p"),
|
||||
Self::FourK => Some("4k"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,7 +171,12 @@ impl KinoviOutputResolutionRaw {
|
||||
pub enum KinoviBatchCountRaw {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
Five,
|
||||
Six,
|
||||
Seven,
|
||||
Eight,
|
||||
}
|
||||
|
||||
impl KinoviBatchCountRaw {
|
||||
@@ -247,7 +184,12 @@ impl KinoviBatchCountRaw {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
Self::Five => 5,
|
||||
Self::Six => 6,
|
||||
Self::Seven => 7,
|
||||
Self::Eight => 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,6 +201,8 @@ pub enum KinoviModelTypeRaw {
|
||||
Seedance2Pro,
|
||||
/// Seedance 2.0 Fast (lower quality, faster).
|
||||
Seedance2Fast,
|
||||
/// Seedance 2.0 Mini (cheapest; 480p/720p only).
|
||||
Seedance2Mini,
|
||||
/// Happy Horse 1.0.
|
||||
HappyHorse1p0,
|
||||
}
|
||||
@@ -268,9 +212,32 @@ impl KinoviModelTypeRaw {
|
||||
match self {
|
||||
Self::Seedance2Pro => "seedance-20",
|
||||
Self::Seedance2Fast => "seedance2-fast",
|
||||
Self::Seedance2Mini => "seedance2.0-mini",
|
||||
Self::HappyHorse1p0 => "happyhorse1.0",
|
||||
}
|
||||
}
|
||||
|
||||
/// The tRPC `businessType` for this model. Seedance 2.0 Mini uses its
|
||||
/// own business type; every other model uses the shared one.
|
||||
fn business_type(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Seedance2Mini => "seedance20-mini-video-generation",
|
||||
Self::HappyHorse1p0 => "happyhorse-video-generation",
|
||||
Self::Seedance2Pro | Self::Seedance2Fast => "wan22-video-generation",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the aspect ratio is sent in an `aspectRatio` field (true)
|
||||
/// rather than the `resolution` field. Mini and Happy Horse use `aspectRatio`.
|
||||
fn uses_aspect_ratio_field(&self) -> bool {
|
||||
matches!(self, Self::Seedance2Mini | Self::HappyHorse1p0)
|
||||
}
|
||||
|
||||
/// Whether this model uses Happy Horse's `happyhorseMode` (t2v/i2v)
|
||||
/// instead of the standard `mode` (keyframe/reference).
|
||||
fn uses_happyhorse_mode(&self) -> bool {
|
||||
matches!(self, Self::HappyHorse1p0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Output video bitrate. When omitted, defaults to "standard".
|
||||
@@ -315,79 +282,7 @@ pub async fn workflow_run_task(args: WorkflowRunTaskArgs<'_>) -> Result<Workflow
|
||||
|
||||
info!("Requesting video from Seedance2Pro (v2): {:?}", req);
|
||||
|
||||
let has_reference_images = req.reference_image_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
let has_reference_videos = req.reference_video_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
let has_reference_audio = req.reference_audio_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
let has_characters = req.character_ids.as_ref().is_some_and(|ids| !ids.is_empty());
|
||||
|
||||
let is_reference_mode = has_reference_images || has_reference_videos || has_reference_audio || has_characters;
|
||||
|
||||
let video_input_mode = if is_reference_mode { "reference" } else { "keyframe" };
|
||||
|
||||
let uploaded_urls: Option<Vec<String>> = if is_reference_mode {
|
||||
let mut urls = Vec::new();
|
||||
if let Some(video_urls) = req.reference_video_urls {
|
||||
urls.extend(video_urls);
|
||||
}
|
||||
if let Some(image_urls) = req.reference_image_urls {
|
||||
urls.extend(image_urls);
|
||||
}
|
||||
if urls.is_empty() { None } else { Some(urls) }
|
||||
} else {
|
||||
let mut urls = Vec::new();
|
||||
if let Some(url) = req.start_frame_url {
|
||||
urls.push(url);
|
||||
}
|
||||
if let Some(url) = req.end_frame_url {
|
||||
urls.push(url);
|
||||
}
|
||||
if urls.is_empty() { None } else { Some(urls) }
|
||||
};
|
||||
|
||||
let audio_urls: Option<Vec<String>> = if has_reference_audio {
|
||||
req.reference_audio_urls
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let face_blur_mode = match req.use_face_blur_hack {
|
||||
Some(true) => Some("on"),
|
||||
Some(false) => Some("off"),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let batch_count_value = req.batch_count.as_u8();
|
||||
let batch_count = if batch_count_value > 1 { Some(batch_count_value) } else { None };
|
||||
|
||||
let duration = format!("{}s", req.duration_seconds);
|
||||
|
||||
info!(
|
||||
"Generating video (v2): mode={}, resolution={}, duration={}, batch={}",
|
||||
video_input_mode, req.aspect_ratio.as_str(), duration, batch_count_value
|
||||
);
|
||||
|
||||
let request_body = BatchRequest {
|
||||
zero: BatchRequestInner {
|
||||
json: BatchRequestJson {
|
||||
business_type: "wan22-video-generation",
|
||||
api_params: ApiParams {
|
||||
prompt: req.prompt,
|
||||
resolution: req.aspect_ratio.as_str().to_string(),
|
||||
content_mode: "normal",
|
||||
model: req.model_type.as_api_str(),
|
||||
duration,
|
||||
mode: video_input_mode,
|
||||
output_resolution: req.output_resolution.and_then(|r| r.as_api_str()),
|
||||
face_blur_mode,
|
||||
character_ids: req.character_ids,
|
||||
uploaded_urls,
|
||||
audio_urls,
|
||||
batch_count,
|
||||
bitrate_mode: req.bitrate.and_then(|bitrate| bitrate.as_api_str()),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
let request_body = build_batch_request(req);
|
||||
|
||||
info!("Seedance2pro request (v2): {:?}", request_body);
|
||||
|
||||
@@ -458,6 +353,107 @@ pub async fn workflow_run_task(args: WorkflowRunTaskArgs<'_>) -> Result<Workflow
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the tRPC request body for a workflow run-task call.
|
||||
///
|
||||
/// Most models carry the aspect ratio as pixel dimensions in `resolution`.
|
||||
/// Seedance 2.0 Mini instead sends a ratio string in an `aspectRatio` field
|
||||
/// and a different `businessType`.
|
||||
fn build_batch_request(req: WorkflowRunTaskRequest) -> BatchRequest {
|
||||
let has_reference_images = req.reference_image_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
let has_reference_videos = req.reference_video_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
let has_reference_audio = req.reference_audio_urls.as_ref().is_some_and(|urls| !urls.is_empty());
|
||||
let has_characters = req.character_ids.as_ref().is_some_and(|ids| !ids.is_empty());
|
||||
|
||||
let is_reference_mode = has_reference_images || has_reference_videos || has_reference_audio || has_characters;
|
||||
|
||||
let video_input_mode = if is_reference_mode { "reference" } else { "keyframe" };
|
||||
|
||||
let uploaded_urls: Option<Vec<String>> = if is_reference_mode {
|
||||
let mut urls = Vec::new();
|
||||
if let Some(video_urls) = req.reference_video_urls {
|
||||
urls.extend(video_urls);
|
||||
}
|
||||
if let Some(image_urls) = req.reference_image_urls {
|
||||
urls.extend(image_urls);
|
||||
}
|
||||
if urls.is_empty() { None } else { Some(urls) }
|
||||
} else {
|
||||
let mut urls = Vec::new();
|
||||
if let Some(url) = req.start_frame_url {
|
||||
urls.push(url);
|
||||
}
|
||||
if let Some(url) = req.end_frame_url {
|
||||
urls.push(url);
|
||||
}
|
||||
if urls.is_empty() { None } else { Some(urls) }
|
||||
};
|
||||
|
||||
let audio_urls: Option<Vec<String>> = if has_reference_audio {
|
||||
req.reference_audio_urls
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let face_blur_mode = match req.use_face_blur_hack {
|
||||
Some(true) => Some("on"),
|
||||
Some(false) => Some("off"),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let batch_count_value = req.batch_count.as_u8();
|
||||
let batch_count = if batch_count_value > 1 { Some(batch_count_value) } else { None };
|
||||
|
||||
let duration = format!("{}s", req.duration_seconds);
|
||||
|
||||
// The aspect ratio is sent as a ratio string (e.g. "16:9"). Seedance Pro/Fast
|
||||
// carry it in the `resolution` field; Mini and Happy Horse use `aspectRatio`.
|
||||
let aspect_ratio_value = req.aspect_ratio.as_aspect_ratio_str();
|
||||
let (resolution, aspect_ratio) = if req.model_type.uses_aspect_ratio_field() {
|
||||
(None, Some(aspect_ratio_value))
|
||||
} else {
|
||||
(Some(aspect_ratio_value.to_string()), None)
|
||||
};
|
||||
|
||||
// Happy Horse uses `happyhorseMode` (t2v/i2v) instead of the standard `mode`
|
||||
// (keyframe/reference). i2v applies whenever an input image/video is attached.
|
||||
let (mode, happyhorse_mode) = if req.model_type.uses_happyhorse_mode() {
|
||||
let hh = if uploaded_urls.is_some() { "i2v" } else { "t2v" };
|
||||
(None, Some(hh))
|
||||
} else {
|
||||
(Some(video_input_mode), None)
|
||||
};
|
||||
|
||||
info!(
|
||||
"Generating video (v2): mode={}, model={}, duration={}, batch={}",
|
||||
video_input_mode, req.model_type.as_api_str(), duration, batch_count_value
|
||||
);
|
||||
|
||||
BatchRequest {
|
||||
zero: BatchRequestInner {
|
||||
json: BatchRequestJson {
|
||||
business_type: req.model_type.business_type(),
|
||||
api_params: ApiParams {
|
||||
prompt: req.prompt,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
content_mode: "normal",
|
||||
model: req.model_type.as_api_str(),
|
||||
duration,
|
||||
mode,
|
||||
happyhorse_mode,
|
||||
output_resolution: req.output_resolution.and_then(|r| r.as_api_str()),
|
||||
face_blur_mode,
|
||||
character_ids: req.character_ids,
|
||||
uploaded_urls,
|
||||
audio_urls,
|
||||
batch_count,
|
||||
bitrate_mode: req.bitrate.and_then(|bitrate| bitrate.as_api_str()),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
@@ -470,299 +466,6 @@ mod tests {
|
||||
use crate::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
|
||||
use crate::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
|
||||
|
||||
mod pricing_tests {
|
||||
use super::*;
|
||||
|
||||
fn make_args(
|
||||
model_type: KinoviModelTypeRaw,
|
||||
duration_seconds: u8,
|
||||
batch_count: KinoviBatchCountRaw,
|
||||
output_resolution: Option<KinoviOutputResolutionRaw>,
|
||||
) -> WorkflowRunTaskRequest {
|
||||
WorkflowRunTaskRequest {
|
||||
model_type,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: KinoviAspectRatioRaw::Square1x1,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
output_resolution,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pro(dur: u8, batch: KinoviBatchCountRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::Seedance2Pro, dur, batch, None)
|
||||
}
|
||||
|
||||
fn pro_res(dur: u8, batch: KinoviBatchCountRaw, res: KinoviOutputResolutionRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::Seedance2Pro, dur, batch, Some(res))
|
||||
}
|
||||
|
||||
fn fast(dur: u8, batch: KinoviBatchCountRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::Seedance2Fast, dur, batch, None)
|
||||
}
|
||||
|
||||
fn fast_res(dur: u8, batch: KinoviBatchCountRaw, res: KinoviOutputResolutionRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::Seedance2Fast, dur, batch, Some(res))
|
||||
}
|
||||
|
||||
// ── Spot checks: exact values from the pricing table ──
|
||||
|
||||
#[test]
|
||||
fn spot_check_pro_720p() {
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::One).estimate_credits(), 200);
|
||||
assert_eq!(pro(10, KinoviBatchCountRaw::One).estimate_credits(), 400);
|
||||
assert_eq!(pro(15, KinoviBatchCountRaw::One).estimate_credits(), 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_pro_480p() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 75);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 150);
|
||||
assert_eq!(pro_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 225);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_pro_1080p() {
|
||||
assert_eq!(pro_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 360);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 450);
|
||||
assert_eq!(pro_res(6, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 540);
|
||||
assert_eq!(pro_res(7, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 630);
|
||||
assert_eq!(pro_res(8, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 720);
|
||||
assert_eq!(pro_res(9, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 810);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 900);
|
||||
assert_eq!(pro_res(11, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 990);
|
||||
assert_eq!(pro_res(12, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1080);
|
||||
assert_eq!(pro_res(13, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1170);
|
||||
assert_eq!(pro_res(14, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1260);
|
||||
assert_eq!(pro_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1350);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_fast_720p() {
|
||||
assert_eq!(fast(5, KinoviBatchCountRaw::One).estimate_credits(), 140);
|
||||
assert_eq!(fast(10, KinoviBatchCountRaw::One).estimate_credits(), 280);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_fast_480p() {
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 50);
|
||||
assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_more_expensive_than_fast_720p() {
|
||||
let p = pro(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
let f = fast(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
assert!(p > f, "Pro 720p ({}) should be more than Fast 720p ({})", p, f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_more_expensive_than_fast_480p() {
|
||||
let p = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
|
||||
let f = fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
|
||||
assert!(p > f, "Pro 480p ({}) should be more than Fast 480p ({})", p, f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_1080p_more_than_720p_more_than_480p() {
|
||||
let c480 = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
|
||||
let c720 = pro(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
let c1080 = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits();
|
||||
assert!(c480 < c720, "480p ({}) should be less than 720p ({})", c480, c720);
|
||||
assert!(c720 < c1080, "720p ({}) should be less than 1080p ({})", c720, c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_720p_more_than_480p() {
|
||||
let c480 = fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
|
||||
let c720 = fast(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
assert!(c480 < c720, "Fast 480p ({}) should be less than Fast 720p ({})", c480, c720);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_none_same_as_720p() {
|
||||
let none = pro(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
let explicit = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).estimate_credits();
|
||||
assert_eq!(none, explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_none_same_as_720p() {
|
||||
let none = fast(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
let explicit = fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).estimate_credits();
|
||||
assert_eq!(none, explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_driven_credits() {
|
||||
let cases: Vec<(KinoviModelTypeRaw, Option<KinoviOutputResolutionRaw>, u8, KinoviBatchCountRaw, u32)> = vec![
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None, 4, KinoviBatchCountRaw::One, 160),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None, 5, KinoviBatchCountRaw::Two, 400),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, None, 10, KinoviBatchCountRaw::Four, 1600),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP), 5, KinoviBatchCountRaw::One, 75),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP), 10, KinoviBatchCountRaw::Two, 300),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP), 15, KinoviBatchCountRaw::Four, 900),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::One, 450),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP), 10, KinoviBatchCountRaw::Two, 1800),
|
||||
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP), 15, KinoviBatchCountRaw::Four, 5400),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None, 4, KinoviBatchCountRaw::One, 112),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None, 5, KinoviBatchCountRaw::Two, 280),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None, 15, KinoviBatchCountRaw::Four, 1680),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP), 5, KinoviBatchCountRaw::One, 50),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP), 10, KinoviBatchCountRaw::Two, 200),
|
||||
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP), 15, KinoviBatchCountRaw::Four, 600),
|
||||
];
|
||||
|
||||
for (i, (model, res, dur, batch, expected)) in cases.iter().enumerate() {
|
||||
let args = make_args(*model, *dur, *batch, *res);
|
||||
let actual = args.estimate_credits();
|
||||
assert_eq!(
|
||||
actual, *expected,
|
||||
"Case {}: {:?} {:?} {}s batch {:?} — expected {} credits, got {}",
|
||||
i, model, res, dur, batch, expected, actual,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_does_not_affect_credits() {
|
||||
let resolutions = [
|
||||
KinoviAspectRatioRaw::Landscape16x9,
|
||||
KinoviAspectRatioRaw::Portrait9x16,
|
||||
KinoviAspectRatioRaw::Square1x1,
|
||||
KinoviAspectRatioRaw::Landscape4x3,
|
||||
KinoviAspectRatioRaw::Portrait3x4,
|
||||
];
|
||||
|
||||
let baseline = pro(5, KinoviBatchCountRaw::One).estimate_credits();
|
||||
|
||||
for res in &resolutions {
|
||||
let req = WorkflowRunTaskRequest {
|
||||
model_type: KinoviModelTypeRaw::Seedance2Pro,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: *res,
|
||||
duration_seconds: 5,
|
||||
batch_count: KinoviBatchCountRaw::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
output_resolution: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
};
|
||||
assert_eq!(
|
||||
req.estimate_credits(), baseline,
|
||||
"Aspect ratio {:?} should not change credits from baseline {}",
|
||||
res, baseline,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_legacy_pro_720p() {
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 80);
|
||||
assert_eq!(pro(10, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 160);
|
||||
assert_eq!(pro(15, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 240);
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::Two).estimate_cost_in_usd_cents(), 160);
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::Four).estimate_cost_in_usd_cents(), 320);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_legacy_fast_720p() {
|
||||
assert_eq!(fast(5, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 64);
|
||||
assert_eq!(fast(10, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 127);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_pro_480p() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 31);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 62);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_pro_1080p() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 185);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 370);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_fast_480p() {
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 21);
|
||||
assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 41);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_per_dollar_legacy_rates() {
|
||||
assert_eq!(pro(5, KinoviBatchCountRaw::One).credits_per_dollar(), 250.0);
|
||||
assert_eq!(fast(5, KinoviBatchCountRaw::One).credits_per_dollar(), 220.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 250.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 220.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_per_dollar_new_rate() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0);
|
||||
}
|
||||
|
||||
// ── Happy Horse 1.0 ──
|
||||
|
||||
fn happy_horse(dur: u8, batch: KinoviBatchCountRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::HappyHorse1p0, dur, batch, None)
|
||||
}
|
||||
|
||||
fn happy_horse_res(dur: u8, batch: KinoviBatchCountRaw, res: KinoviOutputResolutionRaw) -> WorkflowRunTaskRequest {
|
||||
make_args(KinoviModelTypeRaw::HappyHorse1p0, dur, batch, Some(res))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_happy_horse_720p() {
|
||||
// Same as Pro: 40 credits/sec
|
||||
assert_eq!(happy_horse(5, KinoviBatchCountRaw::One).estimate_credits(), 200);
|
||||
assert_eq!(happy_horse(10, KinoviBatchCountRaw::One).estimate_credits(), 400);
|
||||
assert_eq!(happy_horse(15, KinoviBatchCountRaw::One).estimate_credits(), 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_happy_horse_480p() {
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_check_happy_horse_1080p() {
|
||||
assert_eq!(happy_horse_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 360);
|
||||
assert_eq!(happy_horse_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1350);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_horse_uses_new_pricing_rate() {
|
||||
// All Happy Horse resolutions use the new 243 credits/$1 rate (not legacy)
|
||||
assert_eq!(happy_horse(5, KinoviBatchCountRaw::One).credits_per_dollar(), 243.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_horse_usd_cents_1080p() {
|
||||
// 243 credits/$1: 360 credits (4s×90) = 148.15 → 148¢
|
||||
assert_eq!(happy_horse_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 148);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bitrate serialization ──
|
||||
//
|
||||
// The optional `bitrate_mode` field is sent only when `High` is requested;
|
||||
@@ -790,11 +493,13 @@ mod tests {
|
||||
fn base_api_params(bitrate_mode: Option<&'static str>) -> ApiParams {
|
||||
ApiParams {
|
||||
prompt: "a corgi".to_string(),
|
||||
resolution: "1280x720".to_string(),
|
||||
resolution: Some("16:9".to_string()),
|
||||
aspect_ratio: None,
|
||||
content_mode: "normal",
|
||||
model: "seedance-20",
|
||||
duration: "5s".to_string(),
|
||||
mode: "keyframe",
|
||||
mode: Some("keyframe"),
|
||||
happyhorse_mode: None,
|
||||
output_resolution: None,
|
||||
face_blur_mode: None,
|
||||
character_ids: None,
|
||||
@@ -806,6 +511,246 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Output-resolution serialization ──
|
||||
//
|
||||
// `outputResolution` is sent only when non-default; 720p (the default) omits
|
||||
// the field. 4K serializes to the literal "4k".
|
||||
|
||||
mod output_resolution_serialization_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn four_k_maps_to_4k() {
|
||||
assert_eq!(KinoviOutputResolutionRaw::FourK.as_api_str(), Some("4k"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_k_serializes_on_the_wire() {
|
||||
let api_params = base_api_params(KinoviOutputResolutionRaw::FourK.as_api_str());
|
||||
let json = serde_json::to_string(&api_params).unwrap();
|
||||
assert!(json.contains(r#""outputResolution":"4k""#), "expected outputResolution in {json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_720p_omits_output_resolution() {
|
||||
assert_eq!(KinoviOutputResolutionRaw::SevenTwentyP.as_api_str(), None);
|
||||
let api_params = base_api_params(KinoviOutputResolutionRaw::SevenTwentyP.as_api_str());
|
||||
let json = serde_json::to_string(&api_params).unwrap();
|
||||
assert!(!json.contains("outputResolution"), "expected no outputResolution in {json}");
|
||||
}
|
||||
|
||||
fn base_api_params(output_resolution: Option<&'static str>) -> ApiParams {
|
||||
ApiParams {
|
||||
prompt: "a corgi".to_string(),
|
||||
resolution: Some("16:9".to_string()),
|
||||
aspect_ratio: None,
|
||||
content_mode: "normal",
|
||||
model: "seedance-20",
|
||||
duration: "5s".to_string(),
|
||||
mode: Some("reference"),
|
||||
happyhorse_mode: None,
|
||||
output_resolution,
|
||||
face_blur_mode: None,
|
||||
character_ids: None,
|
||||
uploaded_urls: None,
|
||||
audio_urls: None,
|
||||
batch_count: None,
|
||||
bitrate_mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Seedance 2.0 Mini request shape ──
|
||||
//
|
||||
// Mini differs from the other models on the wire: a
|
||||
// `seedance20-mini-video-generation` businessType, a `seedance2.0-mini`
|
||||
// model, and an `aspectRatio` ratio string (e.g. "16:9") in place of the
|
||||
// pixel-dimension `resolution` field.
|
||||
|
||||
mod mini_request_shape_tests {
|
||||
use super::*;
|
||||
|
||||
fn mini_request(
|
||||
aspect_ratio: KinoviAspectRatioRaw,
|
||||
output_resolution: Option<KinoviOutputResolutionRaw>,
|
||||
batch_count: KinoviBatchCountRaw,
|
||||
) -> WorkflowRunTaskRequest {
|
||||
WorkflowRunTaskRequest {
|
||||
model_type: KinoviModelTypeRaw::Seedance2Mini,
|
||||
prompt: "a corgi".to_string(),
|
||||
aspect_ratio,
|
||||
output_resolution,
|
||||
duration_seconds: 5,
|
||||
batch_count,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mini_uses_aspect_ratio_field_and_business_type() {
|
||||
let body = build_batch_request(mini_request(
|
||||
KinoviAspectRatioRaw::Landscape4x3,
|
||||
Some(KinoviOutputResolutionRaw::FourEightyP),
|
||||
KinoviBatchCountRaw::One,
|
||||
));
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(json.contains(r#""businessType":"seedance20-mini-video-generation""#), "{json}");
|
||||
assert!(json.contains(r#""model":"seedance2.0-mini""#), "{json}");
|
||||
assert!(json.contains(r#""aspectRatio":"4:3""#), "{json}");
|
||||
assert!(json.contains(r#""outputResolution":"480p""#), "{json}");
|
||||
// Mini does NOT send the pixel-dimension `resolution` field.
|
||||
assert!(!json.contains(r#""resolution":"#), "{json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mini_720p_omits_output_resolution() {
|
||||
let body = build_batch_request(mini_request(
|
||||
KinoviAspectRatioRaw::Landscape16x9,
|
||||
None,
|
||||
KinoviBatchCountRaw::One,
|
||||
));
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(json.contains(r#""aspectRatio":"16:9""#), "{json}");
|
||||
assert!(!json.contains("outputResolution"), "{json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mini_batch_count_eight_serializes() {
|
||||
let body = build_batch_request(mini_request(
|
||||
KinoviAspectRatioRaw::Landscape16x9,
|
||||
None,
|
||||
KinoviBatchCountRaw::Eight,
|
||||
));
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(json.contains(r#""batchCount":8"#), "{json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_mini_still_uses_resolution_field() {
|
||||
let mut req = mini_request(KinoviAspectRatioRaw::Landscape16x9, None, KinoviBatchCountRaw::One);
|
||||
req.model_type = KinoviModelTypeRaw::Seedance2Pro;
|
||||
let body = build_batch_request(req);
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(json.contains(r#""businessType":"wan22-video-generation""#), "{json}");
|
||||
assert!(json.contains(r#""resolution":"16:9""#), "{json}");
|
||||
assert!(!json.contains("aspectRatio"), "{json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_strings_and_business_types() {
|
||||
assert_eq!(KinoviModelTypeRaw::Seedance2Mini.as_api_str(), "seedance2.0-mini");
|
||||
assert_eq!(KinoviModelTypeRaw::Seedance2Mini.business_type(), "seedance20-mini-video-generation");
|
||||
assert_eq!(KinoviModelTypeRaw::Seedance2Pro.business_type(), "wan22-video-generation");
|
||||
assert_eq!(KinoviModelTypeRaw::Seedance2Fast.business_type(), "wan22-video-generation");
|
||||
assert_eq!(KinoviModelTypeRaw::HappyHorse1p0.business_type(), "happyhorse-video-generation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_strings() {
|
||||
assert_eq!(KinoviAspectRatioRaw::Landscape16x9.as_aspect_ratio_str(), "16:9");
|
||||
assert_eq!(KinoviAspectRatioRaw::UltraWide21x9.as_aspect_ratio_str(), "21:9");
|
||||
assert_eq!(KinoviAspectRatioRaw::Portrait9x16.as_aspect_ratio_str(), "9:16");
|
||||
assert_eq!(KinoviAspectRatioRaw::Square1x1.as_aspect_ratio_str(), "1:1");
|
||||
assert_eq!(KinoviAspectRatioRaw::Landscape4x3.as_aspect_ratio_str(), "4:3");
|
||||
assert_eq!(KinoviAspectRatioRaw::Portrait3x4.as_aspect_ratio_str(), "3:4");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2026-06-24 request-shape change ──
|
||||
//
|
||||
// The aspect ratio is now a ratio string ("16:9"). Seedance Pro/Fast keep it
|
||||
// in `resolution`; Happy Horse moved to its own `businessType`, an
|
||||
// `aspectRatio` field, and a `happyhorseMode` (t2v/i2v) in place of `mode`.
|
||||
|
||||
mod wire_shape_change_tests {
|
||||
use super::*;
|
||||
|
||||
fn request(
|
||||
model_type: KinoviModelTypeRaw,
|
||||
aspect_ratio: KinoviAspectRatioRaw,
|
||||
start_frame_url: Option<String>,
|
||||
) -> WorkflowRunTaskRequest {
|
||||
WorkflowRunTaskRequest {
|
||||
model_type,
|
||||
prompt: "a corgi".to_string(),
|
||||
aspect_ratio,
|
||||
output_resolution: None,
|
||||
duration_seconds: 5,
|
||||
batch_count: KinoviBatchCountRaw::One,
|
||||
start_frame_url,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seedance_pro_resolution_is_a_ratio_string() {
|
||||
let body = build_batch_request(request(
|
||||
KinoviModelTypeRaw::Seedance2Pro, KinoviAspectRatioRaw::Landscape16x9, None));
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(json.contains(r#""businessType":"wan22-video-generation""#), "{json}");
|
||||
assert!(json.contains(r#""resolution":"16:9""#), "{json}");
|
||||
assert!(json.contains(r#""mode":"keyframe""#), "{json}");
|
||||
assert!(!json.contains("aspectRatio"), "{json}");
|
||||
assert!(!json.contains("1280x720"), "{json}");
|
||||
assert!(!json.contains("happyhorseMode"), "{json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seedance_fast_resolution_is_a_ratio_string() {
|
||||
let body = build_batch_request(request(
|
||||
KinoviModelTypeRaw::Seedance2Fast, KinoviAspectRatioRaw::Portrait3x4, None));
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(json.contains(r#""model":"seedance2-fast""#), "{json}");
|
||||
assert!(json.contains(r#""resolution":"3:4""#), "{json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_horse_text_to_video_shape() {
|
||||
let body = build_batch_request(request(
|
||||
KinoviModelTypeRaw::HappyHorse1p0, KinoviAspectRatioRaw::Portrait9x16, None));
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(json.contains(r#""businessType":"happyhorse-video-generation""#), "{json}");
|
||||
assert!(json.contains(r#""model":"happyhorse1.0""#), "{json}");
|
||||
assert!(json.contains(r#""happyhorseMode":"t2v""#), "{json}");
|
||||
assert!(json.contains(r#""aspectRatio":"9:16""#), "{json}");
|
||||
// Happy Horse omits the standard `mode` and the `resolution` field.
|
||||
assert!(!json.contains(r#""mode":"#), "{json}");
|
||||
assert!(!json.contains(r#""resolution":"#), "{json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_horse_image_to_video_uses_i2v() {
|
||||
let body = build_batch_request(request(
|
||||
KinoviModelTypeRaw::HappyHorse1p0, KinoviAspectRatioRaw::Landscape16x9,
|
||||
Some("https://example.com/start.png".to_string())));
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(json.contains(r#""happyhorseMode":"i2v""#), "{json}");
|
||||
assert!(json.contains(r#""uploadedUrls":["https://example.com/start.png"]"#), "{json}");
|
||||
assert!(!json.contains(r#""mode":"#), "{json}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn business_types() {
|
||||
assert_eq!(KinoviModelTypeRaw::Seedance2Pro.business_type(), "wan22-video-generation");
|
||||
assert_eq!(KinoviModelTypeRaw::Seedance2Fast.business_type(), "wan22-video-generation");
|
||||
assert_eq!(KinoviModelTypeRaw::Seedance2Mini.business_type(), "seedance20-mini-video-generation");
|
||||
assert_eq!(KinoviModelTypeRaw::HappyHorse1p0.business_type(), "happyhorse-video-generation");
|
||||
}
|
||||
}
|
||||
|
||||
mod real_requests {
|
||||
use super::*;
|
||||
|
||||
@@ -1641,6 +1586,69 @@ mod tests {
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 4K with image references (Seedance 2.0 Pro only), 5-second sample.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_4k_image_references() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
|
||||
// Upload a few reference images first, then drive a 5-second 4K job.
|
||||
let image_urls_to_upload = [
|
||||
test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL,
|
||||
test_data::web::image_urls::WHITE_HOUSE_SUNSET_IMAGE_URL,
|
||||
test_data::web::image_urls::FOREST_BACKDROP_IMAGE_URL,
|
||||
];
|
||||
|
||||
let mut uploaded_urls = Vec::new();
|
||||
for (i, source_url) in image_urls_to_upload.iter().enumerate() {
|
||||
let image_bytes = crate::test_utils::http_download::http_download_to_bytes(source_url).await?;
|
||||
let ext = if source_url.ends_with(".png") { "png" } else { "jpg" };
|
||||
|
||||
let prepare_result = prepare_file_upload(PrepareFileUploadArgs {
|
||||
session: &session,
|
||||
extension: ext.to_string(),
|
||||
host_override: None,
|
||||
}).await?;
|
||||
|
||||
let upload_result = upload_file(UploadFileArgs {
|
||||
upload_url: prepare_result.upload_url,
|
||||
file_bytes: image_bytes,
|
||||
host_override: None,
|
||||
}).await?;
|
||||
|
||||
println!("Uploaded ref image {}: {}", i + 1, upload_result.public_url);
|
||||
uploaded_urls.push(upload_result.public_url);
|
||||
}
|
||||
|
||||
let args = WorkflowRunTaskArgs {
|
||||
session: &session,
|
||||
host_override: None,
|
||||
request: WorkflowRunTaskRequest {
|
||||
model_type: KinoviModelTypeRaw::Seedance2Pro,
|
||||
prompt: "The dog in @1 explores the scenery in @3 near the building in @2. Cinematic 4K detail.".to_string(),
|
||||
aspect_ratio: KinoviAspectRatioRaw::Landscape16x9,
|
||||
duration_seconds: 5,
|
||||
batch_count: KinoviBatchCountRaw::One,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: Some(uploaded_urls),
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: Some(KinoviOutputResolutionRaw::FourK),
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
println!("Seedance 2.0 @ 4K (image refs) — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
assert!(!result.task_id.is_empty());
|
||||
assert!(!result.order_id.is_empty());
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod seedance_2_fast {
|
||||
|
||||
+3
@@ -74,6 +74,9 @@ fn video_model_to_common_model_type(model: RouterVideoModel) -> Option<CommonMod
|
||||
RouterVideoModel::Seedance2p0UltraFast => Some(CommonModelType::Seedance2p0UltraFast),
|
||||
RouterVideoModel::Seedance2p0BytePlusUltra => Some(CommonModelType::Seedance2p0BytePlusUltra),
|
||||
RouterVideoModel::Seedance2p0BytePlusUltraFast => Some(CommonModelType::Seedance2p0BytePlusUltraFast),
|
||||
RouterVideoModel::Seedance2p0Mini => Some(CommonModelType::Seedance2p0Mini),
|
||||
RouterVideoModel::Seedance2p0BytePlusMini => Some(CommonModelType::Seedance2p0BytePlusMini),
|
||||
RouterVideoModel::Seedance2p0BytePlusUltraMini => Some(CommonModelType::Seedance2p0BytePlusUltraMini),
|
||||
RouterVideoModel::PreviewModel => Some(CommonModelType::PreviewModel),
|
||||
RouterVideoModel::PreviewModelFast => Some(CommonModelType::PreviewModelFast),
|
||||
RouterVideoModel::GrokImagineVideo => Some(CommonModelType::GrokImagineVideo),
|
||||
|
||||
+27
@@ -198,6 +198,12 @@ pub enum InferenceModelType {
|
||||
Seedance2p0BytePlusUltra,
|
||||
#[serde(rename = "seedance_2p0_bpu_fast")]
|
||||
Seedance2p0BytePlusUltraFast,
|
||||
#[serde(rename = "seedance_2p0_mini")]
|
||||
Seedance2p0Mini,
|
||||
#[serde(rename = "seedance_2p0_bp_mini")]
|
||||
Seedance2p0BytePlusMini,
|
||||
#[serde(rename = "seedance_2p0_bpu_mini")]
|
||||
Seedance2p0BytePlusUltraMini,
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
#[serde(rename = "sora_2_pro")]
|
||||
@@ -320,6 +326,9 @@ impl InferenceModelType {
|
||||
Self::Seedance2p0UltraFast => "seedance_2p0_u_fast",
|
||||
Self::Seedance2p0BytePlusUltra => "seedance_2p0_bpu",
|
||||
Self::Seedance2p0BytePlusUltraFast => "seedance_2p0_bpu_fast",
|
||||
Self::Seedance2p0Mini => "seedance_2p0_mini",
|
||||
Self::Seedance2p0BytePlusMini => "seedance_2p0_bp_mini",
|
||||
Self::Seedance2p0BytePlusUltraMini => "seedance_2p0_bpu_mini",
|
||||
Self::Sora2 => "sora_2",
|
||||
Self::Sora2Pro => "sora_2_pro",
|
||||
Self::Veo2 => "veo_2",
|
||||
@@ -422,6 +431,9 @@ impl InferenceModelType {
|
||||
"seedance_2p0_u_fast" => Ok(Self::Seedance2p0UltraFast),
|
||||
"seedance_2p0_bpu" => Ok(Self::Seedance2p0BytePlusUltra),
|
||||
"seedance_2p0_bpu_fast" => Ok(Self::Seedance2p0BytePlusUltraFast),
|
||||
"seedance_2p0_mini" => Ok(Self::Seedance2p0Mini),
|
||||
"seedance_2p0_bp_mini" => Ok(Self::Seedance2p0BytePlusMini),
|
||||
"seedance_2p0_bpu_mini" => Ok(Self::Seedance2p0BytePlusUltraMini),
|
||||
"sora_2" => Ok(Self::Sora2),
|
||||
"sora_2_pro" => Ok(Self::Sora2Pro),
|
||||
"veo_2" => Ok(Self::Veo2),
|
||||
@@ -528,6 +540,9 @@ impl InferenceModelType {
|
||||
Self::Seedance2p0UltraFast,
|
||||
Self::Seedance2p0BytePlusUltra,
|
||||
Self::Seedance2p0BytePlusUltraFast,
|
||||
Self::Seedance2p0Mini,
|
||||
Self::Seedance2p0BytePlusMini,
|
||||
Self::Seedance2p0BytePlusUltraMini,
|
||||
Self::Sora2,
|
||||
Self::Sora2Pro,
|
||||
Self::Veo2,
|
||||
@@ -617,6 +632,9 @@ impl InferenceModelType {
|
||||
CommonModelType::Seedance2p0UltraFast => Self::Seedance2p0UltraFast,
|
||||
CommonModelType::Seedance2p0BytePlusUltra => Self::Seedance2p0BytePlusUltra,
|
||||
CommonModelType::Seedance2p0BytePlusUltraFast => Self::Seedance2p0BytePlusUltraFast,
|
||||
CommonModelType::Seedance2p0Mini => Self::Seedance2p0Mini,
|
||||
CommonModelType::Seedance2p0BytePlusMini => Self::Seedance2p0BytePlusMini,
|
||||
CommonModelType::Seedance2p0BytePlusUltraMini => Self::Seedance2p0BytePlusUltraMini,
|
||||
CommonModelType::Sora2 => Self::Sora2,
|
||||
CommonModelType::Sora2Pro => Self::Sora2Pro,
|
||||
CommonModelType::Veo2 => Self::Veo2,
|
||||
@@ -729,6 +747,9 @@ mod tests {
|
||||
assert_serialization(InferenceModelType::Seedance2p0UltraFast, "seedance_2p0_u_fast");
|
||||
assert_serialization(InferenceModelType::Seedance2p0BytePlusUltra, "seedance_2p0_bpu");
|
||||
assert_serialization(InferenceModelType::Seedance2p0BytePlusUltraFast, "seedance_2p0_bpu_fast");
|
||||
assert_serialization(InferenceModelType::Seedance2p0Mini, "seedance_2p0_mini");
|
||||
assert_serialization(InferenceModelType::Seedance2p0BytePlusMini, "seedance_2p0_bp_mini");
|
||||
assert_serialization(InferenceModelType::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini");
|
||||
assert_serialization(InferenceModelType::Sora2, "sora_2");
|
||||
assert_serialization(InferenceModelType::Sora2Pro, "sora_2_pro");
|
||||
assert_serialization(InferenceModelType::Veo2, "veo_2");
|
||||
@@ -830,6 +851,9 @@ mod tests {
|
||||
assert_eq!(InferenceModelType::Seedance2p0UltraFast.to_str(), "seedance_2p0_u_fast");
|
||||
assert_eq!(InferenceModelType::Seedance2p0BytePlusUltra.to_str(), "seedance_2p0_bpu");
|
||||
assert_eq!(InferenceModelType::Seedance2p0BytePlusUltraFast.to_str(), "seedance_2p0_bpu_fast");
|
||||
assert_eq!(InferenceModelType::Seedance2p0Mini.to_str(), "seedance_2p0_mini");
|
||||
assert_eq!(InferenceModelType::Seedance2p0BytePlusMini.to_str(), "seedance_2p0_bp_mini");
|
||||
assert_eq!(InferenceModelType::Seedance2p0BytePlusUltraMini.to_str(), "seedance_2p0_bpu_mini");
|
||||
assert_eq!(InferenceModelType::Sora2.to_str(), "sora_2");
|
||||
assert_eq!(InferenceModelType::Sora2Pro.to_str(), "sora_2_pro");
|
||||
assert_eq!(InferenceModelType::Veo2.to_str(), "veo_2");
|
||||
@@ -931,6 +955,9 @@ mod tests {
|
||||
assert_eq!(InferenceModelType::from_str("seedance_2p0_u_fast").unwrap(), InferenceModelType::Seedance2p0UltraFast);
|
||||
assert_eq!(InferenceModelType::from_str("seedance_2p0_bpu").unwrap(), InferenceModelType::Seedance2p0BytePlusUltra);
|
||||
assert_eq!(InferenceModelType::from_str("seedance_2p0_bpu_fast").unwrap(), InferenceModelType::Seedance2p0BytePlusUltraFast);
|
||||
assert_eq!(InferenceModelType::from_str("seedance_2p0_mini").unwrap(), InferenceModelType::Seedance2p0Mini);
|
||||
assert_eq!(InferenceModelType::from_str("seedance_2p0_bp_mini").unwrap(), InferenceModelType::Seedance2p0BytePlusMini);
|
||||
assert_eq!(InferenceModelType::from_str("seedance_2p0_bpu_mini").unwrap(), InferenceModelType::Seedance2p0BytePlusUltraMini);
|
||||
assert_eq!(InferenceModelType::from_str("sora_2").unwrap(), InferenceModelType::Sora2);
|
||||
assert_eq!(InferenceModelType::from_str("sora_2_pro").unwrap(), InferenceModelType::Sora2Pro);
|
||||
assert_eq!(InferenceModelType::from_str("veo_2").unwrap(), InferenceModelType::Veo2);
|
||||
|
||||
@@ -167,6 +167,12 @@ pub enum CommonModelType {
|
||||
Seedance2p0BytePlusUltra,
|
||||
#[serde(rename = "seedance_2p0_bpu_fast")]
|
||||
Seedance2p0BytePlusUltraFast,
|
||||
#[serde(rename = "seedance_2p0_mini")]
|
||||
Seedance2p0Mini,
|
||||
#[serde(rename = "seedance_2p0_bp_mini")]
|
||||
Seedance2p0BytePlusMini,
|
||||
#[serde(rename = "seedance_2p0_bpu_mini")]
|
||||
Seedance2p0BytePlusUltraMini,
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
#[serde(rename = "sora_2_pro")]
|
||||
@@ -273,6 +279,9 @@ impl CommonModelType {
|
||||
Self::Seedance2p0UltraFast => "seedance_2p0_u_fast",
|
||||
Self::Seedance2p0BytePlusUltra => "seedance_2p0_bpu",
|
||||
Self::Seedance2p0BytePlusUltraFast => "seedance_2p0_bpu_fast",
|
||||
Self::Seedance2p0Mini => "seedance_2p0_mini",
|
||||
Self::Seedance2p0BytePlusMini => "seedance_2p0_bp_mini",
|
||||
Self::Seedance2p0BytePlusUltraMini => "seedance_2p0_bpu_mini",
|
||||
Self::Sora2 => "sora_2",
|
||||
Self::Sora2Pro => "sora_2_pro",
|
||||
Self::Veo2 => "veo_2",
|
||||
@@ -358,6 +367,9 @@ impl CommonModelType {
|
||||
"seedance_2p0_u_fast" => Ok(Self::Seedance2p0UltraFast),
|
||||
"seedance_2p0_bpu" => Ok(Self::Seedance2p0BytePlusUltra),
|
||||
"seedance_2p0_bpu_fast" => Ok(Self::Seedance2p0BytePlusUltraFast),
|
||||
"seedance_2p0_mini" => Ok(Self::Seedance2p0Mini),
|
||||
"seedance_2p0_bp_mini" => Ok(Self::Seedance2p0BytePlusMini),
|
||||
"seedance_2p0_bpu_mini" => Ok(Self::Seedance2p0BytePlusUltraMini),
|
||||
"sora_2" => Ok(Self::Sora2),
|
||||
"sora_2_pro" => Ok(Self::Sora2Pro),
|
||||
"veo_2" => Ok(Self::Veo2),
|
||||
@@ -447,6 +459,9 @@ impl CommonModelType {
|
||||
Self::Seedance2p0UltraFast,
|
||||
Self::Seedance2p0BytePlusUltra,
|
||||
Self::Seedance2p0BytePlusUltraFast,
|
||||
Self::Seedance2p0Mini,
|
||||
Self::Seedance2p0BytePlusMini,
|
||||
Self::Seedance2p0BytePlusUltraMini,
|
||||
Self::Sora2,
|
||||
Self::Sora2Pro,
|
||||
Self::Veo2,
|
||||
@@ -533,6 +548,9 @@ impl CommonModelType {
|
||||
Self::Seedance2p0UltraFast => CommonModelClass::Video,
|
||||
Self::Seedance2p0BytePlusUltra => CommonModelClass::Video,
|
||||
Self::Seedance2p0BytePlusUltraFast => CommonModelClass::Video,
|
||||
Self::Seedance2p0Mini => CommonModelClass::Video,
|
||||
Self::Seedance2p0BytePlusMini => CommonModelClass::Video,
|
||||
Self::Seedance2p0BytePlusUltraMini => CommonModelClass::Video,
|
||||
Self::Sora2 => CommonModelClass::Video,
|
||||
Self::Sora2Pro => CommonModelClass::Video,
|
||||
Self::Veo2 => CommonModelClass::Video,
|
||||
@@ -628,6 +646,9 @@ mod tests {
|
||||
assert_serialization(CommonModelType::Seedance2p0UltraFast, "seedance_2p0_u_fast");
|
||||
assert_serialization(CommonModelType::Seedance2p0BytePlusUltra, "seedance_2p0_bpu");
|
||||
assert_serialization(CommonModelType::Seedance2p0BytePlusUltraFast, "seedance_2p0_bpu_fast");
|
||||
assert_serialization(CommonModelType::Seedance2p0Mini, "seedance_2p0_mini");
|
||||
assert_serialization(CommonModelType::Seedance2p0BytePlusMini, "seedance_2p0_bp_mini");
|
||||
assert_serialization(CommonModelType::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini");
|
||||
assert_serialization(CommonModelType::Sora2, "sora_2");
|
||||
assert_serialization(CommonModelType::Sora2Pro, "sora_2_pro");
|
||||
assert_serialization(CommonModelType::Veo2, "veo_2");
|
||||
@@ -710,6 +731,9 @@ mod tests {
|
||||
assert_eq!(CommonModelType::Seedance2p0UltraFast.to_str(), "seedance_2p0_u_fast");
|
||||
assert_eq!(CommonModelType::Seedance2p0BytePlusUltra.to_str(), "seedance_2p0_bpu");
|
||||
assert_eq!(CommonModelType::Seedance2p0BytePlusUltraFast.to_str(), "seedance_2p0_bpu_fast");
|
||||
assert_eq!(CommonModelType::Seedance2p0Mini.to_str(), "seedance_2p0_mini");
|
||||
assert_eq!(CommonModelType::Seedance2p0BytePlusMini.to_str(), "seedance_2p0_bp_mini");
|
||||
assert_eq!(CommonModelType::Seedance2p0BytePlusUltraMini.to_str(), "seedance_2p0_bpu_mini");
|
||||
assert_eq!(CommonModelType::Sora2.to_str(), "sora_2");
|
||||
assert_eq!(CommonModelType::Sora2Pro.to_str(), "sora_2_pro");
|
||||
assert_eq!(CommonModelType::Veo2.to_str(), "veo_2");
|
||||
@@ -792,6 +816,9 @@ mod tests {
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_u_fast").unwrap(), CommonModelType::Seedance2p0UltraFast);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_bpu").unwrap(), CommonModelType::Seedance2p0BytePlusUltra);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_bpu_fast").unwrap(), CommonModelType::Seedance2p0BytePlusUltraFast);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_mini").unwrap(), CommonModelType::Seedance2p0Mini);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_bp_mini").unwrap(), CommonModelType::Seedance2p0BytePlusMini);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_bpu_mini").unwrap(), CommonModelType::Seedance2p0BytePlusUltraMini);
|
||||
assert_eq!(CommonModelType::from_str("sora_2").unwrap(), CommonModelType::Sora2);
|
||||
assert_eq!(CommonModelType::from_str("sora_2_pro").unwrap(), CommonModelType::Sora2Pro);
|
||||
assert_eq!(CommonModelType::from_str("veo_2").unwrap(), CommonModelType::Veo2);
|
||||
@@ -815,7 +842,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonModelType::all_variants();
|
||||
assert_eq!(variants.len(), 73);
|
||||
assert_eq!(variants.len(), 76);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell));
|
||||
@@ -876,6 +903,9 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0UltraFast));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0BytePlusUltra));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0BytePlusUltraFast));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0Mini));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0BytePlusMini));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0BytePlusUltraMini));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Veo2));
|
||||
@@ -974,6 +1004,9 @@ mod tests {
|
||||
assert_eq!(CommonModelType::Veo3.get_model_class(), CommonModelClass::Video);
|
||||
assert_eq!(CommonModelType::Sora2.get_model_class(), CommonModelClass::Video);
|
||||
assert_eq!(CommonModelType::Kling3p0Pro.get_model_class(), CommonModelClass::Video);
|
||||
assert_eq!(CommonModelType::Seedance2p0Mini.get_model_class(), CommonModelClass::Video);
|
||||
assert_eq!(CommonModelType::Seedance2p0BytePlusMini.get_model_class(), CommonModelClass::Video);
|
||||
assert_eq!(CommonModelType::Seedance2p0BytePlusUltraMini.get_model_class(), CommonModelClass::Video);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -71,6 +71,15 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "seedance_2p0_bpu_fast")]
|
||||
Seedance2p0BytePlusUltraFast,
|
||||
|
||||
#[serde(rename = "seedance_2p0_mini")]
|
||||
Seedance2p0Mini,
|
||||
|
||||
#[serde(rename = "seedance_2p0_bp_mini")]
|
||||
Seedance2p0BytePlusMini,
|
||||
|
||||
#[serde(rename = "seedance_2p0_bpu_mini")]
|
||||
Seedance2p0BytePlusUltraMini,
|
||||
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
|
||||
@@ -128,6 +137,9 @@ impl CommonVideoModel {
|
||||
Self::Seedance2p0UltraFast => CommonModelType::Seedance2p0UltraFast,
|
||||
Self::Seedance2p0BytePlusUltra => CommonModelType::Seedance2p0BytePlusUltra,
|
||||
Self::Seedance2p0BytePlusUltraFast => CommonModelType::Seedance2p0BytePlusUltraFast,
|
||||
Self::Seedance2p0Mini => CommonModelType::Seedance2p0Mini,
|
||||
Self::Seedance2p0BytePlusMini => CommonModelType::Seedance2p0BytePlusMini,
|
||||
Self::Seedance2p0BytePlusUltraMini => CommonModelType::Seedance2p0BytePlusUltraMini,
|
||||
Self::Sora2 => CommonModelType::Sora2,
|
||||
Self::Sora2Pro => CommonModelType::Sora2Pro,
|
||||
Self::Veo2 => CommonModelType::Veo2,
|
||||
@@ -170,6 +182,9 @@ mod tests {
|
||||
assert_serialization(CommonVideoModel::Seedance2p0UltraFast, "seedance_2p0_u_fast");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0BytePlusUltra, "seedance_2p0_bpu");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0BytePlusUltraFast, "seedance_2p0_bpu_fast");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0Mini, "seedance_2p0_mini");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0BytePlusMini, "seedance_2p0_bp_mini");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini");
|
||||
assert_serialization(CommonVideoModel::Sora2, "sora_2");
|
||||
assert_serialization(CommonVideoModel::Sora2Pro, "sora_2_pro");
|
||||
assert_serialization(CommonVideoModel::Veo2, "veo_2");
|
||||
@@ -205,6 +220,9 @@ mod tests {
|
||||
("seedance_2p0_u_fast", CommonVideoModel::Seedance2p0UltraFast),
|
||||
("seedance_2p0_bpu", CommonVideoModel::Seedance2p0BytePlusUltra),
|
||||
("seedance_2p0_bpu_fast", CommonVideoModel::Seedance2p0BytePlusUltraFast),
|
||||
("seedance_2p0_mini", CommonVideoModel::Seedance2p0Mini),
|
||||
("seedance_2p0_bp_mini", CommonVideoModel::Seedance2p0BytePlusMini),
|
||||
("seedance_2p0_bpu_mini", CommonVideoModel::Seedance2p0BytePlusUltraMini),
|
||||
("sora_2", CommonVideoModel::Sora2),
|
||||
("sora_2_pro", CommonVideoModel::Sora2Pro),
|
||||
("veo_2", CommonVideoModel::Veo2),
|
||||
@@ -247,6 +265,9 @@ mod tests {
|
||||
CommonVideoModel::Seedance2p0UltraFast,
|
||||
CommonVideoModel::Seedance2p0BytePlusUltra,
|
||||
CommonVideoModel::Seedance2p0BytePlusUltraFast,
|
||||
CommonVideoModel::Seedance2p0Mini,
|
||||
CommonVideoModel::Seedance2p0BytePlusMini,
|
||||
CommonVideoModel::Seedance2p0BytePlusUltraMini,
|
||||
CommonVideoModel::Sora2,
|
||||
CommonVideoModel::Sora2Pro,
|
||||
CommonVideoModel::Veo2,
|
||||
@@ -288,6 +309,9 @@ mod tests {
|
||||
(CommonVideoModel::Seedance2p0UltraFast, CommonModelType::Seedance2p0UltraFast),
|
||||
(CommonVideoModel::Seedance2p0BytePlusUltra, CommonModelType::Seedance2p0BytePlusUltra),
|
||||
(CommonVideoModel::Seedance2p0BytePlusUltraFast, CommonModelType::Seedance2p0BytePlusUltraFast),
|
||||
(CommonVideoModel::Seedance2p0Mini, CommonModelType::Seedance2p0Mini),
|
||||
(CommonVideoModel::Seedance2p0BytePlusMini, CommonModelType::Seedance2p0BytePlusMini),
|
||||
(CommonVideoModel::Seedance2p0BytePlusUltraMini, CommonModelType::Seedance2p0BytePlusUltraMini),
|
||||
(CommonVideoModel::Sora2, CommonModelType::Sora2),
|
||||
(CommonVideoModel::Sora2Pro, CommonModelType::Sora2Pro),
|
||||
(CommonVideoModel::Veo2, CommonModelType::Veo2),
|
||||
|
||||
@@ -1,894 +0,0 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::{OmniGenVideoModelDetails, OmniGenVideoModelProviderDetails, OmniGenVideoModelsResponse, OmniGenVideoProviderModelDetails};
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use once_cell::sync::Lazy;
|
||||
use enums::common::generation::common_bitrate::CommonBitrate;
|
||||
|
||||
pub const OMNI_GEN_VIDEO_MODELS_AND_PROVIDERS: Lazy<OmniGenVideoModelsResponse> = Lazy::new(|| {
|
||||
let models = build_omni_gen_video_models();
|
||||
let providers= build_omni_gen_video_model_providers();
|
||||
OmniGenVideoModelsResponse {
|
||||
success: true,
|
||||
models,
|
||||
providers,
|
||||
}
|
||||
});
|
||||
|
||||
fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
model_creator: Some(ModelCreator::Grok),
|
||||
full_name: Some("Grok Imagine".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(false),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(7),
|
||||
//video_references_supported: Some(false),
|
||||
//video_references_max: Some(3),
|
||||
//video_references_max_total_duration_seconds: Some(15),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
//batch_size_options: Some(vec![1, 2, 4]),
|
||||
//batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(1),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_max_with_image_references: Some(10),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::GrokImagineVideo1p5,
|
||||
model_creator: Some(ModelCreator::Grok),
|
||||
full_name: Some("Grok Imagine 1.5 Preview".to_string()),
|
||||
extra_info: Some("Fast and high quality".to_string()),
|
||||
extra_info_short: Some("Fast and high quality".to_string()),
|
||||
text_to_video_supported: Some(false), // NB: This might be temporary
|
||||
text_prompt_supported: Some(true),
|
||||
text_prompt_max_length: Some(4096),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(false),
|
||||
image_references_supported: Some(false),
|
||||
//image_references_max: Some(7),
|
||||
//video_references_supported: Some(false),
|
||||
//video_references_max: Some(3),
|
||||
//video_references_max_total_duration_seconds: Some(15),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
//batch_size_options: Some(vec![1, 2, 4]),
|
||||
//batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(1),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_max_with_image_references: Some(10),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::HappyHorse1p0,
|
||||
model_creator: Some(ModelCreator::Alibaba),
|
||||
full_name: Some("Happy Horse 1.0".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(3),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 1.6 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true), // NB: 1.6 Elements!
|
||||
image_references_max: Some(4), // NB: 1.6 Elements!
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// TODO: Kling 2.5 doesn't let you control aspect ratio for image-to-video
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.5 Turbo Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// TODO: Kling 2.6 doesn't let you control aspect ratio for image-to-video
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Kling2p6Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.6 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 1.5 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::Auto,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(12),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0".to_string()),
|
||||
extra_info: Some("The Chinese Volcengine (ByteDance China API platform) version of Seedance 2.0. Checkpoint is from January 2026. This may be better at some characters than the other Seedance models.".to_string()),
|
||||
extra_info_short: Some("Original Seedance 2.0".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(true),
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Fast".to_string()),
|
||||
extra_info: Some("The Chinese Volcengine (ByteDance China API platform) version of Seedance 2.0 Fast. Checkpoint is from January 2026. This may be better at some characters than the other Seedance models.".to_string()),
|
||||
extra_info_short: Some("Original Seedance 2.0 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(true),
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlus,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus".to_string()),
|
||||
extra_info: Some("The Chinese BytePlus (ByteDance's Western API platform) version of Seedance 2.0. This has fewer restrictions on faces and IP.".to_string()),
|
||||
extra_info_short: Some("Seedance 2.0 (BytePlus version)".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusFast,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus Fast".to_string()),
|
||||
extra_info: Some("The Chinese BytePlus (ByteDance's Western API platform) version of Seedance 2.0 Fast. This has fewer restrictions on faces and IP.".to_string()),
|
||||
extra_info_short: Some("Seedance 2.0 Fast (BytePlus version)".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusUltra,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus Ultra".to_string()),
|
||||
extra_info: Some("This is the same BytePlus version of Seedance 2.0, but with even fewer restrictions around content. Horror movies, action movie violence, and more is possible.".to_string()),
|
||||
extra_info_short: Some("Seedance 2.0 (BytePlus version); less filtered".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusUltraFast,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus Ultra Fast".to_string()),
|
||||
extra_info: Some("This is the same BytePlus version of Seedance 2.0 Fast, but with even fewer restrictions around content. Horror movies, action movie violence, and more is possible.".to_string()),
|
||||
extra_info_short: Some("Seedance 2.0 Fast (BytePlus version); less filtered".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// TODO(bt,2026-04-10): Veo 2 image-to-video doesn't support aspect ratio
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo2,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 2".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
// TODO(bt,2026-04-10): Veo 2 image-to-video doesn't support aspect ratio
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_min: Some(5),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::Auto, // TODO: Only for image-to-video
|
||||
]),
|
||||
// TODO: image-to-video aspect ratio options
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Veo3Fast,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3p1,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3.1".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3p1Fast,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3.1 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 1.6 Pro".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling21Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.1 Pro".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling21Master,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.1 Master".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.5 Turbo Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling2p6Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.6 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 3.0 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_min: Some(3),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 3.0 Standard".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_min: Some(3),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Seedance10Lite,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 1.0 Lite".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Sora2,
|
||||
model_creator: Some(ModelCreator::OpenAi),
|
||||
full_name: Some("Sora 2".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![CommonResolution::SevenTwentyP]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds_options: Some(vec![4, 8, 12]),
|
||||
duration_seconds_default: Some(4),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Sora2Pro,
|
||||
model_creator: Some(ModelCreator::OpenAi),
|
||||
full_name: Some("Sora 2 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_options: Some(vec![4, 8, 12]),
|
||||
duration_seconds_default: Some(4),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
|
||||
fn build_omni_gen_video_model_providers() -> Vec<OmniGenVideoModelProviderDetails> {
|
||||
let mut providers = Vec::new();
|
||||
|
||||
providers.push(OmniGenVideoModelProviderDetails {
|
||||
provider: GenerationProvider::Artcraft,
|
||||
models: vec![
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Seedance10Lite,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Sora2,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Sora2Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo2,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo3,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo3Fast,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo3p1,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo3p1Fast,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling21Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling21Master,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling2p6Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
overrides: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
providers
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::OmniGenVideoModelDetails;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
|
||||
/// Grok Imagine video models.
|
||||
pub fn grok_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
model_creator: Some(ModelCreator::Grok),
|
||||
full_name: Some("Grok Imagine".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(false),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(7),
|
||||
//video_references_supported: Some(false),
|
||||
//video_references_max: Some(3),
|
||||
//video_references_max_total_duration_seconds: Some(15),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
//batch_size_options: Some(vec![1, 2, 4]),
|
||||
//batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(1),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_max_with_image_references: Some(10),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::GrokImagineVideo1p5,
|
||||
model_creator: Some(ModelCreator::Grok),
|
||||
full_name: Some("Grok Imagine 1.5 Preview".to_string()),
|
||||
extra_info: Some("Fast and high quality".to_string()),
|
||||
extra_info_short: Some("Fast and high quality".to_string()),
|
||||
text_to_video_supported: Some(false), // NB: This might be temporary
|
||||
text_prompt_supported: Some(true),
|
||||
text_prompt_max_length: Some(4096),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(false),
|
||||
image_references_supported: Some(false),
|
||||
//image_references_max: Some(7),
|
||||
//video_references_supported: Some(false),
|
||||
//video_references_max: Some(3),
|
||||
//video_references_max_total_duration_seconds: Some(15),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
//batch_size_options: Some(vec![1, 2, 4]),
|
||||
//batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(1),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_max_with_image_references: Some(10),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::OmniGenVideoModelDetails;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
|
||||
/// Happy Horse video models.
|
||||
pub fn happy_horse_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::HappyHorse1p0,
|
||||
model_creator: Some(ModelCreator::Alibaba),
|
||||
full_name: Some("Happy Horse 1.0".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(3),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::OmniGenVideoModelDetails;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
|
||||
/// Active Kling video models (shown in the list).
|
||||
pub fn kling_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 1.6 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true), // NB: 1.6 Elements!
|
||||
image_references_max: Some(4), // NB: 1.6 Elements!
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// TODO: Kling 2.5 doesn't let you control aspect ratio for image-to-video
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.5 Turbo Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// TODO: Kling 2.6 doesn't let you control aspect ratio for image-to-video
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Kling2p6Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.6 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
|
||||
/// Disabled (coming-soon / hidden) Kling video models.
|
||||
pub fn kling_disabled_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 1.6 Pro".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling21Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.1 Pro".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling21Master,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.1 Master".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.5 Turbo Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling2p6Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 2.6 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 3.0 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_min: Some(3),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
model_creator: Some(ModelCreator::Kling),
|
||||
full_name: Some("Kling 3.0 Standard".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_min: Some(3),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod grok_video_models;
|
||||
pub mod happy_horse_video_models;
|
||||
pub mod kling_video_models;
|
||||
pub mod seedance_1x_video_models;
|
||||
pub mod seedance_2p0_video_models;
|
||||
pub mod sora_video_models;
|
||||
pub mod veo_video_models;
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::OmniGenVideoModelDetails;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
|
||||
/// Seedance 1.5 video models.
|
||||
pub fn seedance_1p5_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 1.5 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::Auto,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(12),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
|
||||
/// Seedance 1.0 video models.
|
||||
pub fn seedance_1p0_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Seedance10Lite,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 1.0 Lite".to_string()),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds_options: Some(vec![5, 10]),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::OmniGenVideoModelDetails;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_bitrate::CommonBitrate;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
|
||||
/// The Seedance 2.0 family of video models, in display order:
|
||||
/// Seedance 2.0, Seedance 2.0 Fast, Seedance 2.0 BytePlus, Seedance 2.0 BytePlus
|
||||
/// Fast, Seedance 2.0 BytePlus Ultra, Seedance 2.0 BytePlus Ultra Fast,
|
||||
/// Seedance 2.0 Mini, Seedance 2.0 BytePlus Mini, Seedance 2.0 BytePlus Ultra Mini.
|
||||
pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0".to_string()),
|
||||
extra_info: Some("The Chinese Volcengine (ByteDance China API platform) version of Seedance 2.0. Checkpoint is from January 2026. This may be better at some characters than the other Seedance models.".to_string()),
|
||||
extra_info_short: Some("Original Seedance 2.0".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(true),
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
CommonResolution::FourK,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Fast".to_string()),
|
||||
extra_info: Some("The Chinese Volcengine (ByteDance China API platform) version of Seedance 2.0 Fast. Checkpoint is from January 2026. This may be better at some characters than the other Seedance models.".to_string()),
|
||||
extra_info_short: Some("Original Seedance 2.0 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(true),
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlus,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus".to_string()),
|
||||
extra_info: Some("The Chinese BytePlus (ByteDance's Western API platform) version of Seedance 2.0. This has fewer restrictions on faces and IP.".to_string()),
|
||||
extra_info_short: Some("Seedance 2.0 (BytePlus version)".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
CommonResolution::FourK,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusFast,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus Fast".to_string()),
|
||||
extra_info: Some("The Chinese BytePlus (ByteDance's Western API platform) version of Seedance 2.0 Fast. This has fewer restrictions on faces and IP.".to_string()),
|
||||
extra_info_short: Some("Seedance 2.0 Fast (BytePlus version)".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusUltra,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus Ultra".to_string()),
|
||||
extra_info: Some("This is the same BytePlus version of Seedance 2.0, but with even fewer restrictions around content. Horror movies, action movie violence, and more is possible.".to_string()),
|
||||
extra_info_short: Some("Seedance 2.0 (BytePlus version); less filtered".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
CommonResolution::FourK,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusUltraFast,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus Ultra Fast".to_string()),
|
||||
extra_info: Some("This is the same BytePlus version of Seedance 2.0 Fast, but with even fewer restrictions around content. Horror movies, action movie violence, and more is possible.".to_string()),
|
||||
extra_info_short: Some("Seedance 2.0 Fast (BytePlus version); less filtered".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0Mini,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Mini".to_string()),
|
||||
extra_info: Some("The Chinese Volcengine (ByteDance China API platform) version of Seedance 2.0 Mini. This may be better at some characters than the other Seedance models.".to_string()),
|
||||
extra_info_short: Some("Chinese Volcengine Version".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(true),
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusMini,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus Mini".to_string()),
|
||||
extra_info: Some("The Chinese BytePlus (ByteDance's Western API platform) version of Seedance 2.0 Mini. This has fewer restrictions on faces and IP.".to_string()),
|
||||
extra_info_short: Some("BytePlus Version".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusUltraMini,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Plus Ultra Mini".to_string()),
|
||||
extra_info: Some("This is the same BytePlus version of Seedance 2.0 Mini, but with even fewer restrictions around content. Horror movies, action movie violence, and more is possible.".to_string()),
|
||||
extra_info_short: Some("BytePlus Version".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(9),
|
||||
audio_references_supported: Some(true),
|
||||
audio_references_max: Some(3),
|
||||
audio_references_max_total_duration_seconds: Some(15),
|
||||
video_references_supported: Some(true),
|
||||
video_references_max: Some(3),
|
||||
video_references_max_total_duration_seconds: Some(15),
|
||||
character_references_supported: Some(false), // TODO
|
||||
character_references_max: Some(9),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
bitrate_options: Some(vec![
|
||||
CommonBitrate::Normal,
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
batch_size_options: Some(vec![1, 2, 4]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::OmniGenVideoModelDetails;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
|
||||
/// Sora (OpenAI) video models.
|
||||
pub fn sora_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Sora2,
|
||||
model_creator: Some(ModelCreator::OpenAi),
|
||||
full_name: Some("Sora 2".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![CommonResolution::SevenTwentyP]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds_options: Some(vec![4, 8, 12]),
|
||||
duration_seconds_default: Some(4),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Sora2Pro,
|
||||
model_creator: Some(ModelCreator::OpenAi),
|
||||
full_name: Some("Sora 2 Pro".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_options: Some(vec![4, 8, 12]),
|
||||
duration_seconds_default: Some(4),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::OmniGenVideoModelDetails;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
|
||||
/// Veo (Google) video models.
|
||||
pub fn veo_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
// TODO(bt,2026-04-10): Veo 2 image-to-video doesn't support aspect ratio
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo2,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 2".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
// TODO(bt,2026-04-10): Veo 2 image-to-video doesn't support aspect ratio
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
duration_seconds_min: Some(5),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(5),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::Auto, // TODO: Only for image-to-video
|
||||
]),
|
||||
// TODO: image-to-video aspect ratio options
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Veo3Fast,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3p1,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3.1".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
is_disabled: Some(true), // TODO: Temporarily disable
|
||||
model: CommonVideoModel::Veo3p1Fast,
|
||||
model_creator: Some(ModelCreator::Google),
|
||||
full_name: Some("Veo 3.1 Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
show_generate_with_sound_toggle: Some(true),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::Auto,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::TenEightyP),
|
||||
duration_seconds_min: Some(4),
|
||||
duration_seconds_max: Some(8),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod video_models;
|
||||
mod by_type;
|
||||
@@ -0,0 +1,118 @@
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::{OmniGenVideoModelDetails, OmniGenVideoModelProviderDetails, OmniGenVideoModelsResponse, OmniGenVideoProviderModelDetails};
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::by_type::grok_video_models::grok_video_models;
|
||||
use super::by_type::happy_horse_video_models::happy_horse_video_models;
|
||||
use super::by_type::kling_video_models::{kling_disabled_video_models, kling_video_models};
|
||||
use super::by_type::seedance_1x_video_models::{seedance_1p0_video_models, seedance_1p5_video_models};
|
||||
use super::by_type::seedance_2p0_video_models::seedance_2p0_video_models;
|
||||
use super::by_type::sora_video_models::sora_video_models;
|
||||
use super::by_type::veo_video_models::veo_video_models;
|
||||
|
||||
pub const OMNI_GEN_VIDEO_MODELS_AND_PROVIDERS: Lazy<OmniGenVideoModelsResponse> = Lazy::new(|| {
|
||||
let models = build_omni_gen_video_models();
|
||||
let providers= build_omni_gen_video_model_providers();
|
||||
OmniGenVideoModelsResponse {
|
||||
success: true,
|
||||
models,
|
||||
providers,
|
||||
}
|
||||
});
|
||||
|
||||
fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.extend(grok_video_models());
|
||||
models.extend(happy_horse_video_models());
|
||||
models.extend(kling_video_models());
|
||||
models.extend(kling_disabled_video_models());
|
||||
models.extend(seedance_1p0_video_models());
|
||||
models.extend(seedance_1p5_video_models());
|
||||
models.extend(seedance_2p0_video_models());
|
||||
models.extend(sora_video_models());
|
||||
models.extend(veo_video_models());
|
||||
|
||||
models
|
||||
}
|
||||
|
||||
fn build_omni_gen_video_model_providers() -> Vec<OmniGenVideoModelProviderDetails> {
|
||||
let mut providers = Vec::new();
|
||||
|
||||
providers.push(OmniGenVideoModelProviderDetails {
|
||||
provider: GenerationProvider::Artcraft,
|
||||
models: vec![
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Seedance10Lite,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Sora2,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Sora2Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo2,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo3,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo3Fast,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo3p1,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Veo3p1Fast,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling16Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling21Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling21Master,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling2p5TurboPro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling2p6Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
overrides: None,
|
||||
},
|
||||
OmniGenVideoProviderModelDetails {
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
overrides: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
providers
|
||||
}
|
||||
@@ -349,6 +349,9 @@ use artcraft_api_defs::omni_gen::generate_response::omni_gen_image_generate_resp
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::*;
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_image_models::*;
|
||||
use artcraft_api_defs::omni_gen::models::omni_gen_video_models::*;
|
||||
use artcraft_api_defs::omni_api::job_status::omni_api_batch_get_job_status::*;
|
||||
use artcraft_api_defs::omni_api::job_status::omni_api_get_job_status::*;
|
||||
use artcraft_api_defs::omni_api::job_status::omni_api_job_status_payload::*;
|
||||
use artcraft_api_defs::omni_api::omni_upload_audio::*;
|
||||
use artcraft_api_defs::omni_api::omni_upload_image::*;
|
||||
use artcraft_api_defs::omni_api::omni_upload_video::*;
|
||||
@@ -573,6 +576,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::omni_api::upload::omni_upload_audio_media_file_handler::omni_upload_audio_media_file_handler,
|
||||
crate::http_server::endpoints::omni_api::upload::omni_upload_image_media_file_handler::omni_upload_image_media_file_handler,
|
||||
crate::http_server::endpoints::omni_api::upload::omni_upload_video_media_file_handler::omni_upload_video_media_file_handler,
|
||||
crate::http_server::endpoints::omni_api::job_status::get_job_status_handler::omni_api_get_job_status_handler,
|
||||
crate::http_server::endpoints::omni_api::job_status::batch_get_job_status_handler::omni_api_batch_get_job_status_handler,
|
||||
// Moderation
|
||||
crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler,
|
||||
crate::http_server::endpoints::moderation::info::moderator_token_info_handler::moderator_get_token_info_handler,
|
||||
@@ -826,7 +831,13 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
GetCharacterResponse,
|
||||
ListCharactersEntry,
|
||||
ListCharactersResponse,
|
||||
OmniApiBatchGetJobStatusSuccessResponse,
|
||||
OmniApiGetJobStatusSuccessResponse,
|
||||
OmniApiImageGenerateRequest,
|
||||
OmniApiJobRequestDetails,
|
||||
OmniApiJobResultDetails,
|
||||
OmniApiJobStatusDetails,
|
||||
OmniApiJobStatusPayload,
|
||||
OmniApiVideoGenerateRequest,
|
||||
OmniGenImageCostAndGenerateRequest,
|
||||
OmniGenImageCostResponse,
|
||||
|
||||
+3
@@ -150,6 +150,9 @@ fn map_video_model(model: CommonVideoModel) -> Result<RouterVideoModel, HandlerE
|
||||
CommonVideoModel::Seedance2p0BytePlusFast=> RouterVideoModel::Seedance2p0BytePlusFast,
|
||||
CommonVideoModel::Seedance2p0BytePlusUltra => RouterVideoModel::Seedance2p0BytePlusUltra,
|
||||
CommonVideoModel::Seedance2p0BytePlusUltraFast => RouterVideoModel::Seedance2p0BytePlusUltraFast,
|
||||
CommonVideoModel::Seedance2p0Mini => RouterVideoModel::Seedance2p0Mini,
|
||||
CommonVideoModel::Seedance2p0BytePlusMini => RouterVideoModel::Seedance2p0BytePlusMini,
|
||||
CommonVideoModel::Seedance2p0BytePlusUltraMini => RouterVideoModel::Seedance2p0BytePlusUltraMini,
|
||||
CommonVideoModel::HappyHorse1p0 => RouterVideoModel::HappyHorse1p0,
|
||||
CommonVideoModel::Sora2 => RouterVideoModel::Sora2,
|
||||
CommonVideoModel::Sora2Pro => RouterVideoModel::Sora2Pro,
|
||||
|
||||
+2
@@ -188,9 +188,11 @@ pub async fn omni_api_video_generate_handler(
|
||||
// BytePlus Ultra
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusUltra) => KinoviAccount::BytePlusUltra,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusUltraFast) => KinoviAccount::BytePlusUltra,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusUltraMini) => KinoviAccount::BytePlusUltra,
|
||||
// BytePlus
|
||||
Some(CommonVideoModel::Seedance2p0BytePlus) => KinoviAccount::BytePlus,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusFast) => KinoviAccount::BytePlus,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusMini) => KinoviAccount::BytePlus,
|
||||
Some(CommonVideoModel::PreviewModel) => KinoviAccount::BytePlus,
|
||||
Some(CommonVideoModel::PreviewModelFast) => KinoviAccount::BytePlus,
|
||||
// Everything else goes through Volcengine
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use actix_web_lab::extract::Query;
|
||||
use log::{error, warn};
|
||||
use redis::Commands;
|
||||
|
||||
use artcraft_api_defs::omni_api::job_status::omni_api_batch_get_job_status::{OmniApiBatchGetJobStatusQueryParams, OmniApiBatchGetJobStatusSuccessResponse};
|
||||
use mysql_queries::queries::generic_inference::web::batch_get_inference_job_status::batch_get_inference_job_status;
|
||||
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
|
||||
use redis_common::redis_keys::RedisKeys;
|
||||
use server_environment::ServerEnvironment;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::endpoints::omni_api::job_status::record_to_payload::record_to_payload;
|
||||
use crate::http_server::user_lookup::api_keys::require_api_key_user::require_api_key_user;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For certain jobs or job classes (eg. non-premium), we kill the jobs if the user hasn't
|
||||
/// maintained a keepalive. This prevents wasted work when users who are unlikely to return
|
||||
/// navigate away. Premium users have accounts and can always return to the site, so they
|
||||
/// typically do not require keepalive.
|
||||
const JOB_KEEPALIVE_TTL_SECONDS: u64 = 60 * 3;
|
||||
|
||||
/// Get job statuses for a batch of multiple job tokens (Omni API, API-key authenticated).
|
||||
///
|
||||
/// Mirrors `/v1/jobs/batch` but reads the caller's identity from the
|
||||
/// `Authorization` header API key instead of a session cookie.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Omni API",
|
||||
path = "/v1/omni_api/job_status/batch",
|
||||
responses(
|
||||
(status = 200, body = OmniApiBatchGetJobStatusSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 500),
|
||||
),
|
||||
params(
|
||||
OmniApiBatchGetJobStatusQueryParams
|
||||
)
|
||||
)]
|
||||
pub async fn omni_api_batch_get_job_status_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<OmniApiBatchGetJobStatusQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniApiBatchGetJobStatusSuccessResponse>, CommonWebError> {
|
||||
// ==================== API KEY USER ==================== //
|
||||
|
||||
// API-key authentication (Authorization header) instead of a session cookie. Never cached, and a
|
||||
// banned owner is rejected inside `require_api_key_user`. Drop the connection before the job
|
||||
// lookups below so we don't hold a pool slot idle.
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
require_api_key_user(&http_request, &mut *mysql_connection).await?;
|
||||
drop(mysql_connection);
|
||||
|
||||
let tokens = query.tokens.iter()
|
||||
.map(|token| token.trim())
|
||||
// NB: A bunch of Python clients use our API and can fail in this manner.
|
||||
.filter(|token| !token.is_empty() && *token != "None")
|
||||
.map(|token| InferenceJobToken::new_from_str(token))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if tokens.is_empty() {
|
||||
// NB: MediaDomain doesn't matter since it's an empty list.
|
||||
return Ok(records_to_response(
|
||||
Vec::new(),
|
||||
server_state.server_environment,
|
||||
MediaDomain::Storyteller,
|
||||
));
|
||||
}
|
||||
|
||||
let records = batch_get_inference_job_status(
|
||||
&tokens,
|
||||
&server_state.mysql_pool,
|
||||
).await
|
||||
.map_err(|err| {
|
||||
warn!("Batch job query error: {:?}", err);
|
||||
CommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
warn!("Redis pool error: {:?}", e);
|
||||
CommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
// TODO(bt,2024-04-22): Look up the extra redis statuses per item.
|
||||
|
||||
let keepalive_keys = records.iter()
|
||||
.filter(|record| record.is_keepalive_required)
|
||||
.map(|record| RedisKeys::generic_inference_keepalive(record.job_token.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for key in keepalive_keys.iter() {
|
||||
// TODO(bt,2024-04-22): There is no msetex. We'll need to run a Redis pipeline here.
|
||||
let _: Option<String> = match redis.set_ex(key, "1", JOB_KEEPALIVE_TTL_SECONDS) {
|
||||
Ok(Some(status)) => Some(status),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
error!("redis error setting job keepalive: {:?}", e);
|
||||
None // Fail open (which in this case is bad! it will kill jobs if cluster has many jobs / is slow!)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
|
||||
Ok(records_to_response(records, server_state.server_environment, media_domain))
|
||||
}
|
||||
|
||||
fn records_to_response(
|
||||
records: Vec<GenericInferenceJobStatus>,
|
||||
server_environment: ServerEnvironment,
|
||||
media_domain: MediaDomain,
|
||||
) -> Json<OmniApiBatchGetJobStatusSuccessResponse> {
|
||||
Json(OmniApiBatchGetJobStatusSuccessResponse {
|
||||
success: true,
|
||||
job_states: records.into_iter()
|
||||
.map(|record| record_to_payload(record, server_environment, media_domain))
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::{error, warn};
|
||||
use redis::Commands;
|
||||
|
||||
use artcraft_api_defs::omni_api::job_status::omni_api_get_job_status::{OmniApiGetJobStatusPathInfo, OmniApiGetJobStatusSuccessResponse};
|
||||
use mysql_queries::queries::generic_inference::web::get_inference_job_status::get_inference_job_status;
|
||||
use redis_common::redis_keys::RedisKeys;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::endpoints::omni_api::job_status::record_to_payload::record_to_payload;
|
||||
use crate::http_server::user_lookup::api_keys::require_api_key_user::require_api_key_user;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For certain jobs or job classes (eg. non-premium), we kill the jobs if the user hasn't
|
||||
/// maintained a keepalive. This prevents wasted work when users who are unlikely to return
|
||||
/// navigate away. Premium users have accounts and can always return to the site, so they
|
||||
/// typically do not require keepalive.
|
||||
const JOB_KEEPALIVE_TTL_SECONDS: u64 = 60 * 3;
|
||||
|
||||
/// Get the status for a single job (Omni API, API-key authenticated).
|
||||
///
|
||||
/// Mirrors `/v1/jobs/job/{token}` but reads the caller's identity from the
|
||||
/// `Authorization` header API key instead of a session cookie.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Omni API",
|
||||
path = "/v1/omni_api/job_status/job/{token}",
|
||||
params(
|
||||
("path" = OmniApiGetJobStatusPathInfo, description = "Path params for Request")
|
||||
),
|
||||
responses(
|
||||
(status = 200, body = OmniApiGetJobStatusSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404),
|
||||
(status = 500),
|
||||
),
|
||||
)]
|
||||
pub async fn omni_api_get_job_status_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<OmniApiGetJobStatusPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniApiGetJobStatusSuccessResponse>, CommonWebError> {
|
||||
// ==================== API KEY USER ==================== //
|
||||
|
||||
// API-key authentication (Authorization header) instead of a session cookie. Never cached, and a
|
||||
// banned owner is rejected inside `require_api_key_user`. Drop the connection before the job
|
||||
// lookups below so we don't hold a pool slot idle.
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
require_api_key_user(&http_request, &mut *mysql_connection).await?;
|
||||
drop(mysql_connection);
|
||||
|
||||
if path.token.as_str().trim() == "None" {
|
||||
// NB: A bunch of Python clients use our API and can fail in this manner.
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
let maybe_status = get_inference_job_status(
|
||||
&path.token,
|
||||
&server_state.mysql_pool,
|
||||
).await;
|
||||
|
||||
let record = match maybe_status {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => return Err(CommonWebError::NotFound),
|
||||
Err(err) => {
|
||||
warn!("Job query error for token {}: {:?}", path.token.as_str(), err);
|
||||
return Err(CommonWebError::from_anyhow_error(err));
|
||||
}
|
||||
};
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
warn!("Redis pool error: {:?}", e);
|
||||
CommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
if record.is_keepalive_required {
|
||||
// TODO(bt,2023-05-21): Make async.
|
||||
let extra_status_key = RedisKeys::generic_inference_extra_status_info(path.token.as_str());
|
||||
let _: Option<String> = match redis.set_ex(&extra_status_key, "1", JOB_KEEPALIVE_TTL_SECONDS) {
|
||||
Ok(Some(status)) => Some(status),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
error!("redis error setting job keepalive: {:?}", e);
|
||||
None // Fail open (which in this case is bad! it will kill jobs if cluster has many jobs / is slow!)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
|
||||
let state = record_to_payload(
|
||||
record,
|
||||
server_state.server_environment,
|
||||
media_domain,
|
||||
);
|
||||
|
||||
Ok(Json(OmniApiGetJobStatusSuccessResponse {
|
||||
success: true,
|
||||
state,
|
||||
}))
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod batch_get_job_status_handler;
|
||||
pub mod get_job_status_handler;
|
||||
pub mod record_to_payload;
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
use artcraft_api_defs::omni_api::job_status::omni_api_job_status_payload::{
|
||||
OmniApiJobRequestDetails, OmniApiJobResultDetails, OmniApiJobStatusDetails, OmniApiJobStatusPayload,
|
||||
};
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use bucket_paths::legacy::typified_paths::public::voice_conversion_results::bucket_file_path::VoiceConversionResultOriginalFilePath;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
|
||||
use server_environment::ServerEnvironment;
|
||||
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
|
||||
use crate::http_server::endpoints::inference_job::utils::extractors::extract_polymorphic_inference_args::extract_polymorphic_inference_args;
|
||||
use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name;
|
||||
|
||||
/// Convert a DB job-status record into the Omni API response payload.
|
||||
///
|
||||
/// This is the same mapping the cookie-authenticated `/v1/jobs/*` handlers use;
|
||||
/// it's shared by both the single and batch Omni API status handlers.
|
||||
#[allow(deprecated)] // We match deprecated `InferenceCategory` variants in the result-path mapping.
|
||||
pub fn record_to_payload(
|
||||
record: GenericInferenceJobStatus,
|
||||
server_environment: ServerEnvironment,
|
||||
media_domain: MediaDomain,
|
||||
) -> OmniApiJobStatusPayload {
|
||||
let inference_category = record.request_details.inference_category;
|
||||
|
||||
// NB: Fail open. We don't want to fail the request if we can't extract the args.
|
||||
let maybe_polymorphic_args = extract_polymorphic_inference_args(&record)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let progress_percentage = estimate_job_progress(&record, maybe_polymorphic_args.as_ref());
|
||||
|
||||
OmniApiJobStatusPayload {
|
||||
job_token: record.job_token,
|
||||
request: OmniApiJobRequestDetails {
|
||||
inference_category: record.request_details.inference_category,
|
||||
maybe_prompt_token: record.request_details.maybe_prompt_token,
|
||||
maybe_model_type: maybe_filter_model_name(record.request_details.maybe_model_type.as_deref()),
|
||||
maybe_model_token: record.request_details.maybe_model_token,
|
||||
},
|
||||
status: OmniApiJobStatusDetails {
|
||||
status: record.status,
|
||||
maybe_first_started_at: record.maybe_first_started_at,
|
||||
maybe_failure_category: record.maybe_frontend_failure_category,
|
||||
progress_percentage,
|
||||
},
|
||||
maybe_result: record.maybe_result_details.map(|result_details| {
|
||||
// NB: Be careful here, because this varies based on the type of inference result.
|
||||
let public_bucket_media_path = match inference_category {
|
||||
// NB: TTS has to be special cased due to legacy behavior
|
||||
InferenceCategory::TextToSpeech
|
||||
| InferenceCategory::F5TTS => {
|
||||
match result_details.entity_type.as_str() {
|
||||
"media_file" => {
|
||||
// NB: We're migrating TTS to media_files.
|
||||
// Zero shot TTS uses media files.
|
||||
// Legacy TT2 uses old pathing.
|
||||
MediaFileBucketPath::from_object_hash(
|
||||
&result_details.public_bucket_location_or_hash,
|
||||
result_details.maybe_media_file_public_bucket_prefix.as_deref(),
|
||||
result_details.maybe_media_file_public_bucket_extension.as_deref())
|
||||
.get_full_object_path_str()
|
||||
.to_string()
|
||||
}
|
||||
_ => {
|
||||
// NB: TTS results receive the legacy treatment where their table only reports the full bucket path
|
||||
result_details.public_bucket_location_or_hash
|
||||
}
|
||||
}
|
||||
}
|
||||
// NB: Voice conversion has to be special cased due to legacy behavior
|
||||
InferenceCategory::VoiceConversion | InferenceCategory::SeedVc => {
|
||||
match result_details.entity_type.as_str() {
|
||||
"media_file" => {
|
||||
// NB: We're migrating voice conversion to media_files.
|
||||
MediaFileBucketPath::from_object_hash(
|
||||
&result_details.public_bucket_location_or_hash,
|
||||
result_details.maybe_media_file_public_bucket_prefix.as_deref(),
|
||||
result_details.maybe_media_file_public_bucket_extension.as_deref())
|
||||
.get_full_object_path_str()
|
||||
.to_string()
|
||||
}
|
||||
_ => {
|
||||
// NB: This is the old voice conversion result pathing.
|
||||
VoiceConversionResultOriginalFilePath::from_object_hash(&result_details.public_bucket_location_or_hash)
|
||||
.get_full_object_path_str()
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unsupported media files.
|
||||
InferenceCategory::FormatConversion |
|
||||
InferenceCategory::ConvertBvhToWorkflow => {
|
||||
"".to_string()
|
||||
}
|
||||
// Deprecated
|
||||
InferenceCategory::DeprecatedField => {
|
||||
"".to_string() // TODO(bt,2024-07-16): Read job type instead
|
||||
}
|
||||
// The blessed path that modern media files use.
|
||||
_ => {
|
||||
MediaFileBucketPath::from_object_hash(
|
||||
&result_details.public_bucket_location_or_hash,
|
||||
result_details.maybe_media_file_public_bucket_prefix.as_deref(),
|
||||
result_details.maybe_media_file_public_bucket_extension.as_deref())
|
||||
.get_full_object_path_str()
|
||||
.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
OmniApiJobResultDetails {
|
||||
entity_type: result_details.entity_type,
|
||||
entity_token: result_details.entity_token,
|
||||
media_links: MediaLinksBuilder::from_rooted_path_and_env(
|
||||
media_domain,
|
||||
server_environment,
|
||||
&public_bucket_media_path,
|
||||
),
|
||||
maybe_successfully_completed_at: result_details.maybe_successfully_completed_at,
|
||||
}
|
||||
}),
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod generate;
|
||||
pub mod job_status;
|
||||
pub mod shared_utils;
|
||||
pub mod upload;
|
||||
|
||||
+2
@@ -178,9 +178,11 @@ pub async fn omni_gen_video_generate_handler(
|
||||
// BytePlus Ultra
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusUltra) => KinoviAccount::BytePlusUltra,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusUltraFast) => KinoviAccount::BytePlusUltra,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusUltraMini) => KinoviAccount::BytePlusUltra,
|
||||
// BytePlus
|
||||
Some(CommonVideoModel::Seedance2p0BytePlus) => KinoviAccount::BytePlus,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusFast) => KinoviAccount::BytePlus,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusMini) => KinoviAccount::BytePlus,
|
||||
Some(CommonVideoModel::PreviewModel) => KinoviAccount::BytePlus,
|
||||
Some(CommonVideoModel::PreviewModelFast) => KinoviAccount::BytePlus,
|
||||
// Everything else goes through Volcengine
|
||||
|
||||
+3
@@ -67,6 +67,9 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
|
||||
RouterVideoModel::HappyHorse1p0 => RouterProvider::Seedance2Pro,
|
||||
RouterVideoModel::Seedance2p0 => RouterProvider::Seedance2Pro,
|
||||
RouterVideoModel::Seedance2p0Fast => RouterProvider::Seedance2Pro,
|
||||
RouterVideoModel::Seedance2p0Mini => RouterProvider::Seedance2Pro,
|
||||
RouterVideoModel::Seedance2p0BytePlusMini => RouterProvider::Seedance2Pro,
|
||||
RouterVideoModel::Seedance2p0BytePlusUltraMini => RouterProvider::Seedance2Pro,
|
||||
//RouterVideoModel::Seedance2p0Ultra => RouterProvider::GmiCloud,
|
||||
//RouterVideoModel::Seedance2p0UltraFast => RouterProvider::GmiCloud,
|
||||
RouterVideoModel::GrokImagineVideo => RouterProvider::GrokApi,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::configs::omni_gen::video_models::OMNI_GEN_VIDEO_MODELS_AND_PROVIDERS;
|
||||
use crate::configs::omni_gen::video_models::video_models::OMNI_GEN_VIDEO_MODELS_AND_PROVIDERS;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_feature_flags::UserSessionFeatureFlags;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
+12
@@ -5,6 +5,8 @@ use actix_web::{web, App, Error, HttpResponse};
|
||||
|
||||
use crate::http_server::endpoints::omni_api::generate::image::omni_api_image_generate_handler::omni_api_image_generate_handler;
|
||||
use crate::http_server::endpoints::omni_api::generate::video::omni_api_video_generate_handler::omni_api_video_generate_handler;
|
||||
use crate::http_server::endpoints::omni_api::job_status::batch_get_job_status_handler::omni_api_batch_get_job_status_handler;
|
||||
use crate::http_server::endpoints::omni_api::job_status::get_job_status_handler::omni_api_get_job_status_handler;
|
||||
use crate::http_server::endpoints::omni_api::upload::omni_upload_audio_media_file_handler::omni_upload_audio_media_file_handler;
|
||||
use crate::http_server::endpoints::omni_api::upload::omni_upload_image_media_file_handler::omni_upload_image_media_file_handler;
|
||||
use crate::http_server::endpoints::omni_api::upload::omni_upload_video_media_file_handler::omni_upload_video_media_file_handler;
|
||||
@@ -49,5 +51,15 @@ where
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
.service(web::scope("/job_status")
|
||||
.service(web::resource("/job/{token}")
|
||||
.route(web::get().to(omni_api_get_job_status_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
.service(web::resource("/batch")
|
||||
.route(web::get().to(omni_api_batch_get_job_status_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
faCircleQuestion,
|
||||
faDownload,
|
||||
faGift,
|
||||
faPencil,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
@@ -56,7 +57,7 @@ const CREATE_ITEMS_STATIC: NavItem[] = [
|
||||
{ label: "Image", href: "/create-image", icon: faImage },
|
||||
{ label: "Video", href: "/create-video", icon: faVideo },
|
||||
// Edit Image is hidden from the sidebar for now.
|
||||
// { label: "Edit Image", href: "/edit-image", icon: faPencil },
|
||||
{ label: "Edit Image", href: "/edit-image", icon: faPencil },
|
||||
{ label: "Edit 3D", href: "/edit-3d", icon: faCube },
|
||||
{ label: "Edit Video", href: "/video-editor", icon: faFilm, badge: "BETA" },
|
||||
{
|
||||
@@ -180,11 +181,7 @@ function NavMenuItem({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={active}
|
||||
tooltip={item.label}
|
||||
>
|
||||
<SidebarMenuButton asChild isActive={active} tooltip={item.label}>
|
||||
{item.external ? (
|
||||
<a
|
||||
href={item.href}
|
||||
|
||||
@@ -22,7 +22,7 @@ if (import.meta.env.DEV) {
|
||||
// (disabled — it overrides the origin above and points at localhost:12345,
|
||||
// which breaks dev unless a local backend is running. Re-enable only when
|
||||
// testing against a local storyteller-web.)
|
||||
// StorytellerApiHostStore.getInstance().setDevelopment();
|
||||
StorytellerApiHostStore.getInstance().setDevelopment();
|
||||
} catch (e) {
|
||||
console.warn("Failed to set dev API host override", e);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ const LABEL_TO_BITRATE: Record<string, string> = Object.fromEntries(
|
||||
// UI-only — they are not in `_modelLookup`, so the click handler is a no-op
|
||||
// (the `disabled` flag also prevents the row from firing).
|
||||
const COMING_SOON_MODELS: ReadonlyArray<{ id: string; label: string }> = [
|
||||
{ id: "seedance_2p1", label: "Seedance 2.1" },
|
||||
{ id: "seedance_2p1", label: "Seedance 2.5" },
|
||||
];
|
||||
|
||||
let _modelLookup = new Map<string, OmniGenVideoModelInfo>();
|
||||
@@ -405,8 +405,7 @@ export default function CreateVideo() {
|
||||
const hasSizeOptions = (selectedModel?.aspect_ratio_options?.length ?? 0) > 0;
|
||||
const hasResolutionOptions =
|
||||
(selectedModel?.resolution_options?.length ?? 0) > 0;
|
||||
const hasBitrateOptions =
|
||||
(selectedModel?.bitrate_options?.length ?? 0) > 0;
|
||||
const hasBitrateOptions = (selectedModel?.bitrate_options?.length ?? 0) > 0;
|
||||
const hasSound = !!selectedModel?.show_generate_with_sound_toggle;
|
||||
const supportsImagePrompts =
|
||||
!!selectedModel?.starting_keyframe_supported ||
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../popover"
|
||||
"path": "../modal"
|
||||
},
|
||||
{
|
||||
"path": "../button-icon-select"
|
||||
"path": "../button"
|
||||
},
|
||||
{
|
||||
"path": "../tooltip"
|
||||
},
|
||||
{
|
||||
"path": "../button"
|
||||
"path": "../popover"
|
||||
},
|
||||
{
|
||||
"path": "../button-icon-select"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
|
||||
@@ -26,16 +26,19 @@
|
||||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../popover/tsconfig.lib.json"
|
||||
"path": "../modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../button-icon-select/tsconfig.lib.json"
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../tooltip/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../button/tsconfig.lib.json"
|
||||
"path": "../popover/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../button-icon-select/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+102
-303
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user