Compare commits

..
Author SHA1 Message Date
Tanish Manku b9ee643bf6 Stop the bootstrap scripts claiming the dev webapp targets the local backend
It never has: setDevelopment() is commented out in main.tsx, so the dev server
falls through to its /v1 proxy at api.storyteller.ai. Both bootstrap epilogues
and the seed script now say so and print the VITE_DEV_API_HOST needed to
actually reach the local backend.
2026-08-03 17:24:39 -07:00
Tanish Manku 07e9a35044 Add portable local MySQL/Redis bootstrap scripts for backend dev
Brings up a fully local storyteller-web: portable MySQL 8.4 and Redis under a
gitignored .devstack/ (plain user processes -- no admin, no services, no WSL,
no Docker), all 281 migrations, role/badge seeds, and a demo login with
credits. Windows and Linux variants, plus a doctor that reports what is and
isn't healthy.

Scripts and docs only. Nothing here is compiled into any binary, and no
application behaviour changes: the backend it starts is the one that ships.

Integrations that cannot be provided locally -- object storage, generation
providers, email -- get placeholder credentials, so those endpoints fail when
exercised. Swap in a real dev credential for the one you are working on. The
doc records exactly which areas that leaves working.

An earlier revision closed those gaps with development-only behaviour inside
storyteller-web (DEV_FAKE_GENERATION, a filesystem bucket mode, a CDN_BASE_URL
override). That is deliberately not here: a switch that exists in the shipped
binary can be flipped. Frontend work that wants a populated, working stack
should use frontend/apps/fake-storyteller-web instead.
2026-08-03 14:41:14 -07:00
263 changed files with 3593 additions and 1407 deletions
+5
View File
@@ -1,3 +1,8 @@
# Shell scripts and dotenv config files are executed/parsed on Linux (WSL2, CI)
# even when the repo is cloned on Windows — they must stay LF in the worktree.
*.sh text eol=lf
*.env text eol=lf
# Ignore certain code for Github language statistics
# https://github.com/github/linguist/blob/master/docs/overrides.md
old_code/trumpet-trumped.com/dictionary/cmudict-0.7b linguist-vendored
+3
View File
@@ -104,3 +104,6 @@ windows/sqlite/sqlite3.lib
crates/desktop/src-tauri/*.dll
windows_dlls/
# Portable local dev stack (script/bootstrap) - MySQL/Redis binaries, data, logs
.devstack/
Generated
+27 -27
View File
@@ -757,9 +757,9 @@ dependencies = [
"fal_client",
"gmicloud_client",
"grok_api_client",
"kinovi_web_client",
"log",
"reqwest 0.12.23",
"seedance2pro_client",
"serde",
"serde_derive",
"serde_json",
@@ -6359,29 +6359,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "kinovi_web_client"
version = "0.0.1"
dependencies = [
"chrono",
"cloudflare_errors",
"env",
"env_logger 0.11.9",
"errors",
"log",
"rand 0.9.2",
"serde",
"serde_derive",
"serde_json",
"test_data",
"test_utils",
"tokio",
"url",
"workspace-build-acceleration",
"wreq",
"wreq-util",
]
[[package]]
name = "kubectl-pod-cleanup"
version = "0.0.1"
@@ -10542,7 +10519,6 @@ dependencies = [
"errors",
"hashing",
"jobs_common",
"kinovi_web_client",
"log",
"mysql_queries",
"pager",
@@ -10550,6 +10526,7 @@ dependencies = [
"reqwest 0.12.23",
"rootly_client",
"rootly_config",
"seedance2pro_client",
"serde",
"serde_derive",
"serde_json",
@@ -10561,6 +10538,29 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "seedance2pro_client"
version = "0.0.1"
dependencies = [
"chrono",
"cloudflare_errors",
"env",
"env_logger 0.11.9",
"errors",
"log",
"rand 0.9.2",
"serde",
"serde_derive",
"serde_json",
"test_data",
"test_utils",
"tokio",
"url",
"workspace-build-acceleration",
"wreq",
"wreq-util",
]
[[package]]
name = "selectors"
version = "0.24.0"
@@ -11632,7 +11632,6 @@ dependencies = [
"images",
"infer 0.16.0",
"jwt_signer",
"kinovi_web_client",
"language-tags",
"lexical-sort",
"limitation",
@@ -11664,6 +11663,7 @@ dependencies = [
"rootly_config",
"rust-s3",
"rustls 0.23.36",
"seedance2pro_client",
"serde",
"serde_derive",
"serde_json",
@@ -11867,8 +11867,8 @@ dependencies = [
"easyenv",
"enums",
"errors",
"kinovi_web_client",
"log",
"seedance2pro_client",
"serde_json",
"tokens",
"tokio",
+2 -2
View File
@@ -27,7 +27,7 @@ members = [
"crates/api_clients/openart_client",
"crates/api_clients/rootly_client",
"crates/api_clients/runwayml_client",
"crates/api_clients/kinovi_web_client",
"crates/api_clients/seedance2pro_client",
"crates/api_clients/thumbnail_generator",
"crates/api_clients/worldlabs_api_client",
"crates/api_clients/worldlabs_consumer_client",
@@ -285,7 +285,7 @@ mysql_queries = { path = "crates/schema/database/mysql_queries" }
pager = { path = "crates/lib/pager" }
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
kinovi_web_client = { path = "crates/api_clients/kinovi_web_client" }
seedance2pro_client = { path = "crates/api_clients/seedance2pro_client" }
server_environment = { path = "crates/lib/server_environment" }
sha1_hash = { path = "crates/lib/sha1_hash" }
shared_service_components = { path = "crates/lib/shared_service_components" }
+293
View File
@@ -0,0 +1,293 @@
# Local Dev Stack — Requirements & Bootstrap
How to run `storyteller-web` on your own machine against a local MySQL and
Redis — no shared dev database, no Docker, no admin rights. This is for **work
on the backend itself**.
> **Working on the frontend?** You probably want
> `frontend/apps/fake-storyteller-web` instead — an in-memory stand-in for this
> API that needs no database, no build, and no setup. Everything below is
> unnecessary for webapp work. Come back here when you need the real server.
These scripts install and supervise infrastructure. They change no application
behaviour, and nothing in them is compiled into any binary: the backend they
start is the same one that ships. Integrations they cannot provide locally
(object storage, generation providers, email) get placeholder credentials, so
the endpoints that use them fail when exercised. Swap in a real dev credential
for whichever one you're actually working on.
The automated path on **Windows** (primary platform, native — no WSL, no
Docker, no admin rights for the stack itself):
```powershell
.\script\bootstrap\windows\bootstrap_dev_stack.ps1 # one-time setup, idempotent, re-run anytime
.\script\bootstrap\windows\run_backend_dev.ps1 # terminal 1: API on http://localhost:12345
.\script\bootstrap\windows\seed_demo_user.ps1 # terminal 2: create demo login (localdev1 / localdev1pass)
$env:VITE_DEV_API_HOST = "http://localhost:12345" # terminal 2: aim the webapp at your backend
cd frontend; npx nx dev artcraft-webapp # webapp on http://localhost:4201
.\script\bootstrap\windows\dev_stack_doctor.ps1 # health check, any time
.\script\bootstrap\windows\stop_dev_services.ps1 # stop MySQL/Redis when done
```
The equivalent on **Linux** (Ubuntu 22.04+ — native, WSL2, or CI):
```bash
./script/bootstrap/bootstrap_dev_stack.sh # one-time setup, idempotent, re-run anytime
./script/bootstrap/run_backend_dev.sh # terminal 1: API on http://localhost:12345
./script/bootstrap/seed_demo_user.sh # terminal 2: create demo login (localdev1 / localdev1pass)
cd frontend && VITE_DEV_API_HOST=http://localhost:12345 npx nx dev artcraft-webapp # terminal 2: webapp on :4201
./script/bootstrap/dev_stack_doctor.sh # health check, any time
```
This document records the underlying requirements so the scripts aren't a
black box, and corrects some stale claims in older docs.
## Supported platforms
| Platform | Status |
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Windows 10/11 | Primary. `script/bootstrap/windows/*.ps1` run a **portable stack**: MySQL 8.4 (official zip) and Redis live under the gitignored `.devstack/` and run as plain user processes — no admin, no services, no WSL, no Docker. Build tools (cmake, perl, nasm, llvm) are offered via winget |
| Ubuntu 22.04+ | Supported by `script/bootstrap/*.sh` (native, WSL2, or CI runner/container); services via apt |
| macOS | Not covered by the scripts; follow `_docs/dev_setup_server.md` manually |
### Windows specifics
- Everything stateful lives in `.devstack/` (gitignored): `mysql/` (extracted
zip), `mysql-data/`, `redis/`, `logs/`, `pids/`, `downloads/` (cached zips),
`my.ini`. Delete the directory to fully reset; re-run bootstrap to rebuild.
- Pinned services: MySQL **8.4.11** from `cdn.mysql.com` (the 8.x series is
required; only some patch versions exist on the CDN) and Redis **5.0.14.1**
(tporadowski build — old but sufficient for the backend's keepalive/TTL/
rate-limit usage; override `REDIS_ZIP_URL` to use Memurai/Garnet instead).
- The portable MySQL root user has **no password** (`--initialize-insecure`) —
local dev only, bound to 127.0.0.1.
- diesel-cli is compiled against the portable MySQL's own client library
(`MYSQLCLIENT_LIB_DIR`/`MYSQLCLIENT_VERSION`); `libmysql.dll` must be on
`PATH` to *run* it — the scripts handle both.
- Compiling the backend needs cmake, Strawberry Perl, NASM, and LLVM/libclang
(the `wreq` HTTP client builds BoringSSL, and `aws-lc-sys`/`ring` need
NASM/clang on MSVC). The bootstrap checks and offers winget installs.
- The generated secrets file additionally sets `TEMP_DIR` (the upload temp-dir
helper defaults to `/tmp`, which doesn't exist on Windows).
## Requirements matrix
| Requirement | Version / detail | Needed for |
|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------|
| MySQL server | 8.x series (9.x unsupported by tooling) | Backend boot (eager connect) + migrations |
| Redis server | any recent | Backend boot (r2d2 pool connects eagerly) |
| Rust (stable) | no pinned MSRV; Docker builds use 1.93.0 | Building `storyteller-web`, diesel_cli |
| diesel_cli | `cargo install diesel_cli --no-default-features --features mysql,sqlite` | Running the 281 migrations |
| Node.js | 20+ (enforced by `script/common/frontend_preflight.sh`) | Frontend (Nx 21 / Vite 6) |
| npm | ships with Node; **pnpm must not be used** | Frontend install (`frontend/package-lock.json`) |
| System libs (apt) | build-essential, cmake, libssl-dev, libclang-dev, libfontconfig1-dev, fontconfig, perl, pkg-config, ffmpeg, libmysqlclient-dev, libsqlite3-dev, curl, jq | Compiling the workspace + diesel_cli; seeding scripts |
| Elasticsearch | optional (7.x) | Only 4 search endpoints; server boots without it |
| Docker | **not required** — no docker-compose exists | — |
Database identity (used consistently by docs, `.env`, and defaults):
database `storyteller`, user `storyteller`, password `password`, host
`localhost`.
## Backend: what `storyteller-web` needs to boot
Binary: `storyteller-web` in `crates/service/web/storyteller_web`
(binaries: `storyteller-web`, `docs` — Swagger UI on :8989, `docs-cli` — emits
`api.json`). Binds `0.0.0.0:12345` (`BIND_ADDRESS`), plain HTTP.
**Run it from the repo root** — the config search path and `includes/` paths
are cwd-relative.
### Config loading (corrected facts)
- Mode is selected by the `SERVER_ENVIRONMENT` **process env var only**
(default: Development). Setting it inside an env file does nothing — it is
read before any file loads.
- In Development mode, files load from
`crates/service/web/storyteller_web/config/` in this order:
`storyteller-web.common.env` → `storyteller-web.development.env` →
`storyteller-web.development-secrets.env` (gitignored; the bootstrap
generates it).
- **Precedence is first-value-wins** (dotenv never overrides an existing key):
real process env > common.env > development.env > development-secrets.env.
The secrets file therefore cannot override anything already set in
`development.env`.
- The repo-root `.env` is **NOT loaded by the server** (it passes
`ignore_legacy_dot_env_file: true`). Root `.env`'s `DATABASE_URL` feeds
diesel-cli/sqlx-cli only; the server reads **`MYSQL_URL`** (default
`mysql://storyteller:password@localhost/storyteller`).
### Hard requirements at boot
| Requirement | Behavior if missing |
|-------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| MySQL reachable via `MYSQL_URL` | Boot fails (sqlx pool connects eagerly; the DB must exist and be migrated — health-check and ban-poller threads query immediately) |
| Redis reachable via `REDIS_0_URL` (default `redis://localhost/0`) | Boot fails (r2d2 pool connects eagerly) |
| ~30 `get_env_string_required` vars present | Boot fails on the first absent var |
Of the required-present vars, 19 are supplied by **no** checked-in file and
live in the generated secrets file: `ACCESS_KEY`, `SECRET_KEY`, `REGION_NAME`,
`W2L_PRIVATE_DOWNLOAD_BUCKET_NAME`, `W2L_PUBLIC_DOWNLOAD_BUCKET_NAME`,
`RESEND_API_KEY`, `FAL_API_KEY`, `GMICLOUD_API_KEY`, `GROK_API_KEY`,
`BEEBLE_API_KEY`, `OPENAI_API_KEY`, `WORLDLABS_API_KEY`,
`SEEDANCE2PRO_COOKIES`, `SEEDANCE2PRO_WHITELIST_COOKIES`,
`SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES`, `STRIPE_FAKEYOU_ACCOUNT_ID`,
`STRIPE_ARTCRAFT_ACCOUNT_ID`, `STRIPE_ARTCRAFT_SECRET_KEY`,
`STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY`.
**Placeholder values are safe at boot**: none of these services is contacted
at boot (clients are constructed offline; Elasticsearch, S3/R2, Stripe,
Resend, and Google-cert fetches are all lazy). But two earlier claims here
were wrong and are corrected:
- **The omni_gen endpoints call providers synchronously in the HTTP handler**
(`/v1/omni_gen/generate/{image,video}` — the path the webapp uses). With
placeholder keys the provider call fails and the submit returns HTTP 500
*after* billing the wallet (the image pipeline has no refund; video refunds
only Kinovi). Only the legacy polling providers (GmiCloud, Grok, Kinovi
orders, WorldLabs) are worker-driven.
- **Uploads fail hard with placeholder S3 creds** — every media upload endpoint
uploads to the bucket *before* inserting the DB row, and `rust-s3` is built
with `fail-on-err`.
### What does and doesn't work locally
| Area | Works with placeholder credentials? |
|----------------------------------------------|-------------------------------------|
| Login, sessions, account, profile | Yes |
| Wallets, credits, billing reads | Yes |
| Folders, tags, prompts, characters, projects | Yes |
| Media listing (rows) | Yes — but the images 404, see below |
| Media uploads | No — needs real R2/S3 credentials |
| Image/video/mesh/splat generation | No — needs a real provider key |
| Transactional email | No — needs a real Resend key |
| Elasticsearch-backed search | No — needs a local ES + reindex |
The rows-but-no-images case is the one that surprises people: `media_files`
rows carry a bucket path, and the URL the API builds from it points at the
shared R2 bucket, which does not have your local objects. Put real dev R2
credentials in the secrets file if you need media to render, or use
`fake-storyteller-web`, which serves its own bytes.
### Auth on localhost
- `POST /v1/login` takes `{"username_or_email": "...", "password": "..."}`;
success returns `{"success": true, "signed_session": "<jwt>"}` **and** a
`session` cookie.
- With the checked-in dev config (`COOKIE_DOMAIN=.jungle.horse`), the session
cookie is issued non-Secure and host-only (the Domain attribute is never
set), so plain-HTTP localhost login works as-is. The frontend additionally
stores `signed_session` in localStorage and sends it as a `session` header.
- Dev-mode CORS allows any `localhost`/`127.0.0.1` origin on any port, with
credentials.
- Health endpoint for scripts/CI: `GET /_status`.
### Build
- Compile with `SQLX_OFFLINE=true` (uses the checked-in `.sqlx/` cache; 483
queries). Without it, the sqlx macros open a **live DB connection at compile
time** using `DATABASE_URL` from the root `.env`. `SQLX_OFFLINE` has no
runtime effect — ignore older notes saying not to set it when running.
- First build takes several minutes; the crate has ~370 pre-existing warnings.
## Database: migrations and seed data
- 281 Diesel migrations in `_database/sql/migrations/` (`diesel.toml` at repo
root points there). Run from the repo root: `diesel migration run`.
`"Encountered unknown type for Mysql: enum"` warnings are harmless.
`migrations_squashed/` is documentation only — not executable.
- Seed data: `_database/sql/seed/sql/system_roles.sql` (roles `user`, `mod`,
`admin`) and `user_badges.sql`. **The `user` role is mandatory** — account
creation hardcodes `user_role_slug='user'`. The legacy wrapper
`bootstrap_inserts_roles_etc.sh` must run with cwd `_database/sql/` and is
not idempotent; the bootstrap script applies the same SQL files with
idempotency guards instead.
- Demo user: created through **`POST /v1/create_account`** (no captcha, no
invite code, no email-verification gate; password just needs 6+ chars). This
exercises the real signup path instead of hand-rolled SQL. Gotcha: usernames
like `demo`, `admin`, `test`, `dev`, `artcraft` are reserved
(`includes/binary_includes/usernames/reserved_usernames.txt`) — the default
demo user is `localdev1`. The Rust seeder
(`cargo run --bin dev-database-seed`) exists but hard-requires a
`.env-secrets` file and also seeds model weights, so the HTTP path is
preferred.
- Demo credits: signup creates **no wallet**, and the webapp gates the
generate button on wallet balance — so `seed_demo_user` also seeds an
`artcraft`-namespace wallet with `DEMO_CREDITS` (default 100,000) banked
credits, plus matching ledger rows. Idempotent: re-runs top the balance
back up to `DEMO_CREDITS` but never lower it. (Raw SQL, because the only
real credit-granting paths are Stripe webhooks and moderator tooling.)
- Demo gallery: **not seeded**. Media rows without reachable bytes render as
broken tiles, so seeding them locally would be worse than an empty library.
Use `fake-storyteller-web` for a populated one.
## Frontend: `artcraft-webapp`
- Nx workspace at repo-root `frontend/` (npm workspaces; `npm install` from
`frontend/`). The FontAwesome Pro registry token is committed in
`frontend/.npmrc` — no manual auth step.
- Serve: `npx nx dev artcraft-webapp` → `http://localhost:4201`. No lib
pre-build is needed in dev (libs resolve to TS source via the
`"development"` export condition); production `nx build` does need lib
builds.
- **By default the dev webapp targets production.** `src/main.tsx` points the
API host at the Vite dev origin, and Vite proxies `/v1` to
`https://api.storyteller.ai`. The adjacent `setDevelopment()` call — which
would point it at `localhost:12345` — is commented out. To aim the webapp at
your local backend, set `VITE_DEV_API_HOST=http://localhost:12345` before
`nx dev` (that override ships with the `fake-storyteller-web` change; until
it lands, uncomment `setDevelopment()` locally instead).
- Optional: `VITE_GOOGLE_CLIENT_ID` in `frontend/apps/artcraft-webapp/.env`
enables the Google SSO button; email/password login needs nothing.
- Other dev ports if you run more apps: website 4200, dashboard 4202 (separate
pnpm app, excluded from Nx), video-info-website 4250, genhub 4300, Tauri
frontend 5173.
## What still touches production (known, deliberate seams)
The bootstrap changes no runtime behaviour, so these remain open. Each is
closed by supplying a real credential, not by a code path:
| Seam | Today |
|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Media/CDN URLs in API responses | The compiled-in `cdn_link.rs` constants point at the public `pub-….r2.dev` bucket, so a locally-created row's media URL resolves to an object that isn't there |
| Media uploads | 500 with placeholder credentials; supply real dev R2/S3 keys to exercise them |
| Generation | 500 with placeholder keys, *after* the wallet is billed; supply a real key for the provider under test |
| Vite `/v1` proxy in webapp | Targets `https://api.storyteller.ai`, so a dev webapp with no host override reads and writes production |
| Elasticsearch-backed search | `search_featured` / `search_session` media files, `weights/search`, legacy `tts/search` fail without a local ES + `elasticsearch-cli` reindex (optional) |
## CI usage
The same bootstrap works on a fresh Ubuntu runner/container:
```bash
export MYSQL_ROOT_PASSWORD=root # GitHub Actions' preinstalled MySQL uses password auth
./script/bootstrap/bootstrap_dev_stack.sh --yes
./script/bootstrap/run_backend_dev.sh &
./script/bootstrap/seed_demo_user.sh # waits for /_status
./script/bootstrap/dev_stack_doctor.sh
```
`--yes` answers all prompts; `--skip-frontend` / `--skip-rust-build` /
`--skip-packages` slice the work for focused jobs.
## Verification checklist
- `./script/bootstrap/dev_stack_doctor.sh` exits 0 with no FAILs.
- `POST /v1/login` with the demo user returns `success: true` plus a
`signed_session` (done automatically by `seed_demo_user.sh`).
- `GET /_status` returns 200, and MySQL has the full migration set applied.
- With `VITE_DEV_API_HOST` pointed at `localhost:12345`, the webapp logs in as
`localdev1` and shows its credits in the top bar. The library is empty and
generation 500s — both expected, see the table above.
## Corrections to older docs
- `crates/service/web/storyteller_web/CLAUDE.md`: the repo-root `.env` is
*not* auto-loaded by the server, and `SQLX_OFFLINE=true` is fine (and
recommended) when running — it only affects compile-time query checking.
- `_docs/dev_setup_server.md`: `./script/dev_mysql_connect.sh` doesn't exist —
it's `./script/dev_database_connect.sh`.
- `diesel.toml`'s comment about needing an unreleased diesel CLI is stale —
any current `diesel_cli` works.
- `TESTING_INFRA_HANDOFF.md` (untracked): the server env var is `MYSQL_URL`,
not `DATABASE_URL`; migration count is 281; crate path is
`crates/service/web/storyteller_web`.
+2 -2
View File
@@ -51,9 +51,9 @@ helpers go in the parent `mod tests` so sub-modules can `use super::*`.
- `storyteller_web` — main HTTP API monolith (actix-web)
- `artcraft` (desktop) — Tauri desktop app
- `artcraft_router` — provider routing for image/video generation (Artcraft, Fal, KinoviWeb, Muapi)
- `artcraft_router` — provider routing for image/video generation (Artcraft, Fal, Seedance2Pro, Muapi)
- `artcraft_api_defs` — shared HTTP API type definitions
- `kinovi_web_client` — HTTP client for the Kinovi/KinoviWeb video generation service
- `seedance2pro_client` — HTTP client for the Kinovi/Seedance2Pro video generation service
- `enums` — database-backed enums stored as VARCHAR fields
- `tokens` — primary key identifiers with Stripe-like prefixes (e.g. `user_`, `mf_`)
- `mysql_queries` — SQLx MySQL queries for the backend
@@ -11,7 +11,7 @@ pub struct OmniGenVideoGenerateResponse {
/// All job tokens created by this request (including the primary).
/// For single-job requests this will contain just one element matching
/// `inference_job_token`. For batch requests (eg. KinoviWeb with
/// `inference_job_token`. For batch requests (eg. Seedance2Pro with
/// multiple order IDs) this will contain all of them.
pub all_job_tokens: Vec<InferenceJobToken>,
}
@@ -22,7 +22,7 @@ artcraft_client.workspace = true
fal_client.workspace = true
gmicloud_client.workspace = true
grok_api_client.workspace = true
kinovi_web_client.workspace = true
seedance2pro_client.workspace = true
tokens.workspace = true
url_utils.workspace = true
worldlabs_api_client.workspace = true
@@ -8,6 +8,6 @@ pub enum RouterProvider {
Fal,
GmiCloud,
GrokApi,
KinoviWeb,
Seedance2Pro,
WorldLabs,
}
@@ -7,5 +7,5 @@ pub mod router_client;
pub mod router_fal_client;
pub mod router_gmicloud_client;
pub mod router_grok_api_client;
pub mod router_kinovi_web_client;
pub mod router_seedance2pro_client;
pub mod router_worldlabs_client;
@@ -2,7 +2,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::client::router_grok_api_client::RouterGrokApiClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
use crate::errors::client_error::{ClientError, ClientType};
@@ -11,7 +11,7 @@ pub struct MultiRouterClient {
pub(crate) fal_client: Option<RouterFalClient>,
pub(crate) gmicloud_client: Option<RouterGmiCloudClient>,
pub(crate) grok_api_client: Option<RouterGrokApiClient>,
pub(crate) kinovi_web_client: Option<RouterKinoviWebClient>,
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
pub(crate) worldlabs_client: Option<RouterWorldLabsClient>,
}
@@ -36,9 +36,9 @@ impl MultiRouterClient {
.ok_or(ClientError::ClientNotConfigured(ClientType::GrokApi))
}
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ClientError> {
self.kinovi_web_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::KinoviWeb))
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
self.seedance2pro_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::Seedance2Pro))
}
pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ClientError> {
@@ -3,7 +3,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::client::router_grok_api_client::RouterGrokApiClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
pub struct MultiRouterClientBuilder {
@@ -11,7 +11,7 @@ pub struct MultiRouterClientBuilder {
fal_client: Option<RouterFalClient>,
gmicloud_client: Option<RouterGmiCloudClient>,
grok_api_client: Option<RouterGrokApiClient>,
kinovi_web_client: Option<RouterKinoviWebClient>,
seedance2pro_client: Option<RouterSeedance2ProClient>,
worldlabs_client: Option<RouterWorldLabsClient>,
}
@@ -22,7 +22,7 @@ impl MultiRouterClientBuilder {
fal_client: None,
gmicloud_client: None,
grok_api_client: None,
kinovi_web_client: None,
seedance2pro_client: None,
worldlabs_client: None,
}
}
@@ -47,8 +47,8 @@ impl MultiRouterClientBuilder {
self
}
pub fn set_kinovi_web_client(mut self, client: RouterKinoviWebClient) -> Self {
self.kinovi_web_client = Some(client);
pub fn set_seedance2pro_client(mut self, client: RouterSeedance2ProClient) -> Self {
self.seedance2pro_client = Some(client);
self
}
@@ -63,7 +63,7 @@ impl MultiRouterClientBuilder {
fal_client: self.fal_client,
gmicloud_client: self.gmicloud_client,
grok_api_client: self.grok_api_client,
kinovi_web_client: self.kinovi_web_client,
seedance2pro_client: self.seedance2pro_client,
worldlabs_client: self.worldlabs_client,
}
}
@@ -3,7 +3,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::client::router_grok_api_client::RouterGrokApiClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
use crate::errors::client_error::{ClientError, ClientType};
@@ -13,7 +13,7 @@ pub enum RouterClient {
Fal(RouterFalClient),
GmiCloud(RouterGmiCloudClient),
GrokApi(RouterGrokApiClient),
KinoviWeb(RouterKinoviWebClient),
Seedance2Pro(RouterSeedance2ProClient),
WorldLabs(RouterWorldLabsClient),
}
@@ -50,11 +50,11 @@ impl RouterClient {
}
}
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ClientError> {
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
match self {
RouterClient::KinoviWeb(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_kinovi_web_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::KinoviWeb)),
RouterClient::Seedance2Pro(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_seedance2pro_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::Seedance2Pro)),
}
}
@@ -1,11 +0,0 @@
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
pub struct RouterKinoviWebClient {
pub(crate) session: KinoviWebSession,
}
impl RouterKinoviWebClient {
pub fn new(session: KinoviWebSession) -> Self {
RouterKinoviWebClient { session }
}
}
@@ -0,0 +1,11 @@
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
pub struct RouterSeedance2ProClient {
pub(crate) session: Seedance2ProSession,
}
impl RouterSeedance2ProClient {
pub fn new(session: Seedance2ProSession) -> Self {
RouterSeedance2ProClient { session }
}
}
@@ -2,8 +2,8 @@ use crate::errors::client_error::ClientError;
use crate::errors::download_error::DownloadError;
use crate::errors::provider_error::ProviderError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use kinovi_web_client::error::kinovi_web_error::KinoviWebError;
use kinovi_web_client::error::kinovi_web_specific_api_error::KinoviWebSpecificApiError;
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
use seedance2pro_client::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
use std::error::Error;
use std::fmt::{Display, Formatter};
use artcraft_client::error::api_error::ApiError;
@@ -65,7 +65,7 @@ impl From<ProviderError> for ArtcraftRouterError {
fn from(error: ProviderError) -> Self {
let is_billing_error = match &error {
ProviderError::Fal(FalErrorPlus::FalBillingError(_)) => true,
ProviderError::KinoviWeb(KinoviWebError::ApiSpecific(KinoviWebSpecificApiError::BillingError { .. })) => true,
ProviderError::Seedance2Pro(Seedance2ProError::ApiSpecific(Seedance2ProSpecificApiError::BillingError { .. })) => true,
ProviderError::Storyteller(StorytellerError::Api(ApiError::PaymentRequired(_))) => true,
_ => false,
};
@@ -9,7 +9,7 @@ pub enum ClientType {
Fal,
GmiCloud,
GrokApi,
KinoviWeb,
Seedance2Pro,
WorldLabs,
}
@@ -20,7 +20,7 @@ impl Display for ClientType {
Self::Fal => write!(f, "Fal"),
Self::GmiCloud => write!(f, "GmiCloud"),
Self::GrokApi => write!(f, "GrokApi"),
Self::KinoviWeb => write!(f, "KinoviWeb"),
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
Self::WorldLabs => write!(f, "WorldLabs"),
}
}
@@ -47,8 +47,8 @@ pub enum ClientError {
/// Fal only accepts image URLs for image inputs, not media tokens.
FalOnlySupportsUrls,
/// KinoviWeb only accepts URLs for media inputs, not media tokens.
KinoviWebOnlySupportsUrls,
/// Seedance2Pro only accepts URLs for media inputs, not media tokens.
Seedance2ProOnlySupportsUrls,
/// The Fal endpoint requires a webhook URL but the caller built the client
/// in polling/queue mode (no webhook URL). Returned by webhook-only
@@ -91,8 +91,8 @@ impl Display for ClientError {
Self::FalOnlySupportsUrls => {
write!(f, "Fal only supports image URLs for image inputs, not media tokens")
}
Self::KinoviWebOnlySupportsUrls => {
write!(f, "KinoviWeb only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
Self::Seedance2ProOnlySupportsUrls => {
write!(f, "Seedance2Pro only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
}
Self::WebhookUrlRequired => {
write!(f, "This Fal endpoint only supports webhook dispatch; the caller built RouterFalClient in polling-only mode (no webhook URL)")
@@ -4,7 +4,7 @@ use artcraft_client::error::storyteller_error::StorytellerError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use gmicloud_client::error::gmicloud_error::GmiCloudError;
use grok_api_client::error::grok_error::GrokError;
use kinovi_web_client::error::kinovi_web_error::KinoviWebError;
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
use worldlabs_api_client::error::world_labs_error::WorldLabsError;
#[derive(Debug)]
@@ -13,7 +13,7 @@ pub enum ProviderError {
Fal(FalErrorPlus),
GmiCloud(GmiCloudError),
Grok(GrokError),
KinoviWeb(KinoviWebError),
Seedance2Pro(Seedance2ProError),
WorldLabs(WorldLabsError),
}
@@ -26,7 +26,7 @@ impl Display for ProviderError {
Self::Fal(e) => write!(f, "Fal provider error: {}", e),
Self::GmiCloud(e) => write!(f, "GmiCloud provider error: {}", e),
Self::Grok(e) => write!(f, "Grok provider error: {}", e),
Self::KinoviWeb(e) => write!(f, "KinoviWeb provider error: {}", e),
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
Self::WorldLabs(e) => write!(f, "WorldLabs provider error: {}", e),
}
}
@@ -56,9 +56,9 @@ impl From<GrokError> for ProviderError {
}
}
impl From<KinoviWebError> for ProviderError {
fn from(error: KinoviWebError) -> Self {
Self::KinoviWeb(error)
impl From<Seedance2ProError> for ProviderError {
fn from(error: Seedance2ProError) -> Self {
Self::Seedance2Pro(error)
}
}
@@ -26,8 +26,8 @@ impl AudioGenerationDraftRequest {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::KinoviSunoRemix(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoSample(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoRemix(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoSample(_) => RouterProvider::Seedance2Pro,
}
}
@@ -4,7 +4,7 @@ use std::fmt::Debug;
use tokens::tokens::media_files::MediaFileToken;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
@@ -19,9 +19,9 @@ pub struct AudioGenerationDraftContext<'a> {
}
impl <'a> AudioGenerationDraftContext<'a> {
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ArtcraftRouterError> {
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
client.get_kinovi_web_client_ref()
client.get_seedance2pro_client_ref()
.map_err(|err| ArtcraftRouterError::Client(err))
}
}
@@ -48,10 +48,10 @@ impl AudioGenerationRequest {
Self::ArtcraftSunoSample(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedAudio1p0(_) => RouterProvider::Artcraft,
Self::FalSeedAudio1p0(_) => RouterProvider::Fal,
Self::KinoviSunoMusic(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoRemix(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoSounds(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoSample(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoMusic(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoRemix(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoSounds(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoSample(_) => RouterProvider::Seedance2Pro,
}
}
@@ -100,19 +100,19 @@ impl AudioGenerationRequest {
request.send(client_ref).await
},
AudioGenerationRequest::KinoviSunoMusic(request) => {
let client_ref = client.get_kinovi_web_client_ref()?;
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
AudioGenerationRequest::KinoviSunoRemix(request) => {
let client_ref = client.get_kinovi_web_client_ref()?;
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
AudioGenerationRequest::KinoviSunoSounds(request) => {
let client_ref = client.get_kinovi_web_client_ref()?;
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
AudioGenerationRequest::KinoviSunoSample(request) => {
let client_ref = client.get_kinovi_web_client_ref()?;
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
}
@@ -116,10 +116,10 @@ impl GenerateAudioRequestBuilder {
// Fal
(RouterProvider::Fal, RouterAudioModel::SeedAudio1p0) => build_fal_seed_audio_1p0(self),
// Kinovi
(RouterProvider::KinoviWeb, RouterAudioModel::SunoMusic) => build_kinovi_suno_music(self),
(RouterProvider::KinoviWeb, RouterAudioModel::SunoRemix) => build_kinovi_suno_remix(self),
(RouterProvider::KinoviWeb, RouterAudioModel::SunoSounds) => build_kinovi_suno_sounds(self),
(RouterProvider::KinoviWeb, RouterAudioModel::SunoSample) => build_kinovi_suno_sample(self),
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoMusic) => build_kinovi_suno_music(self),
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoRemix) => build_kinovi_suno_remix(self),
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSounds) => build_kinovi_suno_sounds(self),
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSample) => build_kinovi_suno_sample(self),
_ => self.unsupported_provider_and_model(),
}
}
@@ -202,7 +202,7 @@ mod tests {
#[test]
fn kinovi_suno_music_dispatches_to_request() {
let result = builder(RouterProvider::KinoviWeb, RouterAudioModel::SunoMusic).build2().expect("build");
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SunoMusic).build2().expect("build");
assert!(matches!(
result,
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::KinoviSunoMusic(_))
@@ -211,7 +211,7 @@ mod tests {
#[test]
fn kinovi_suno_sounds_dispatches_to_request() {
let result = builder(RouterProvider::KinoviWeb, RouterAudioModel::SunoSounds).build2().expect("build");
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSounds).build2().expect("build");
assert!(matches!(
result,
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::KinoviSunoSounds(_))
@@ -220,7 +220,7 @@ mod tests {
#[test]
fn kinovi_suno_remix_dispatches_to_draft() {
let result = builder_with_audio_ref(RouterProvider::KinoviWeb, RouterAudioModel::SunoRemix).build2().expect("build");
let result = builder_with_audio_ref(RouterProvider::Seedance2Pro, RouterAudioModel::SunoRemix).build2().expect("build");
assert!(matches!(
result,
AudioGenerationDraftOrRequest::Draft(AudioGenerationDraftRequest::KinoviSunoRemix(_))
@@ -229,7 +229,7 @@ mod tests {
#[test]
fn kinovi_suno_sample_dispatches_to_draft() {
let result = builder_with_audio_ref(RouterProvider::KinoviWeb, RouterAudioModel::SunoSample).build2().expect("build");
let result = builder_with_audio_ref(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSample).build2().expect("build");
assert!(matches!(
result,
AudioGenerationDraftOrRequest::Draft(AudioGenerationDraftRequest::KinoviSunoSample(_))
@@ -242,7 +242,7 @@ mod tests {
#[test]
fn kinovi_seed_audio_1p0_is_unsupported() {
let result = builder(RouterProvider::KinoviWeb, RouterAudioModel::SeedAudio1p0).build2();
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SeedAudio1p0).build2();
assert!(matches!(result, Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_))));
}
@@ -10,7 +10,7 @@ pub struct ArtcraftAudioResponsePayload {
}
#[derive(Clone, Debug)]
pub struct KinoviWebAudioResponsePayload {
pub struct Seedance2proAudioResponsePayload {
pub order_id: String,
pub task_id: String,
}
@@ -37,7 +37,7 @@ pub struct FalAudioResponsePayload {
#[derive(Clone, Debug)]
pub enum GenerateAudioResponse {
Artcraft(ArtcraftAudioResponsePayload),
KinoviWeb(KinoviWebAudioResponsePayload),
Seedance2Pro(Seedance2proAudioResponsePayload),
Fal(FalAudioResponsePayload),
}
@@ -49,9 +49,9 @@ impl GenerateAudioResponse {
}
}
pub fn get_kinovi_web_payload(&self) -> Option<KinoviWebAudioResponsePayload> {
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proAudioResponsePayload> {
match self {
Self::KinoviWeb(p) => Some(p.clone()),
Self::Seedance2Pro(p) => Some(p.clone()),
_ => None,
}
}
@@ -5,14 +5,14 @@
//! draft `finalize()` phase. The upload plumbing is shared with the Kinovi
//! video provider (`generate_video::providers::kinovi::upload`).
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::audio_list_ref::AudioListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
use crate::generate::generate_video::providers::kinovi::upload::upload_to_kinovi_web;
use crate::generate::generate_video::providers::kinovi::upload::upload_to_seedance2pro;
/// A single unresolved audio reference: either an ArtCraft/public URL or a
/// media file token that resolves to one via the draft context's map.
@@ -55,7 +55,7 @@ pub(crate) fn require_single_audio_ref(
/// Resolve a single audio reference to a source URL and re-upload it to the
/// Kinovi CDN, returning the Kinovi CDN URL.
pub(crate) async fn resolve_and_upload_audio_ref(
session: &KinoviWebSession,
session: &Seedance2ProSession,
audio_ref: &SingleAudioRef,
draft_context: &AudioGenerationDraftContext<'_>,
) -> Result<String, ArtcraftRouterError> {
@@ -71,7 +71,7 @@ pub(crate) async fn resolve_and_upload_audio_ref(
})?
}
};
upload_to_kinovi_web(session, &source_url).await
upload_to_seedance2pro(session, &source_url).await
}
#[cfg(test)]
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
use seedance2pro_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
@@ -119,7 +119,7 @@ mod tests {
fn base_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoMusic,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a song about corgis".to_string()),
style_prompt: Some("sea shanty".to_string()),
..Default::default()
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
use seedance2pro_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
use crate::generate::generate_audio::providers::kinovi::suno_music::request::KinoviSunoMusicRequestState;
@@ -13,7 +13,7 @@ impl KinoviSunoMusicCostState {
}
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
// Cost math is owned by kinovi_web_client's binding — the router just
// Cost math is owned by seedance2pro_client's binding — the router just
// forwards the result so router cost ≡ binding cost by construction.
let pricing_request = GenerateSunoMusicRequest {
prompt: String::new(),
@@ -54,7 +54,7 @@ mod tests {
fn cost_from_built_request_is_seven_cents() {
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoMusic,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a song".to_string()),
..Default::default()
};
@@ -1,12 +1,12 @@
use kinovi_web_client::generate::audio::generate_suno_music::{
use seedance2pro_client::generate::audio::generate_suno_music::{
generate_suno_music, GenerateSunoMusicArgs, GenerateSunoMusicRequest,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_audio::generate_audio_response::{
GenerateAudioResponse, KinoviWebAudioResponsePayload,
GenerateAudioResponse, Seedance2proAudioResponsePayload,
};
#[derive(Debug, Clone)]
@@ -16,7 +16,7 @@ pub struct KinoviSunoMusicRequestState {
}
impl KinoviSunoMusicRequestState {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSunoMusicArgs {
@@ -27,9 +27,9 @@ impl KinoviSunoMusicRequestState {
let response = generate_suno_music(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
@@ -38,12 +38,12 @@ impl KinoviSunoMusicRequestState {
#[cfg(test)]
mod tests {
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use crate::api::router_audio_model::RouterAudioModel;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
@@ -56,7 +56,7 @@ mod tests {
style_prompt: Some("Sea shanty, folk".to_string()),
..suno_music_builder()
}).await;
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -69,7 +69,7 @@ mod tests {
is_instrumental: Some(true),
..suno_music_builder()
}).await;
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -78,20 +78,20 @@ mod tests {
fn suno_music_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoMusic,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
..Default::default()
}
}
fn get_kinovi_web_client() -> RouterClient {
fn get_seedance2pro_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
}
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
let client = get_kinovi_web_client();
let client = get_seedance2pro_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let request = match draft_or_request {
@@ -102,7 +102,7 @@ mod tests {
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateAudioResponse::KinoviWeb(p) => {
GenerateAudioResponse::Seedance2Pro(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -168,7 +168,7 @@ mod tests {
fn base_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoRemix,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("make this electronic".to_string()),
style_prompt: Some("EDM style".to_string()),
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()])),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::audio::generate_suno_remix::{
use seedance2pro_client::generate::audio::generate_suno_remix::{
GenerateSunoRemixRequest, KinoviSunoRemixSource,
};
@@ -20,7 +20,7 @@ impl KinoviSunoRemixCostState {
}
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
// Cost math is owned by kinovi_web_client's binding — the router just
// Cost math is owned by seedance2pro_client's binding — the router just
// forwards the result so router cost ≡ binding cost by construction.
let pricing_request = GenerateSunoRemixRequest {
prompt: String::new(),
@@ -63,7 +63,7 @@ mod tests {
fn cost_from_draft_is_seven_cents() {
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoRemix,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("make this electronic".to_string()),
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()])),
..Default::default()
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::audio::generate_suno_remix::{
use seedance2pro_client::generate::audio::generate_suno_remix::{
GenerateSunoRemixRequest, KinoviSunoRemixSource,
};
@@ -26,7 +26,7 @@ impl KinoviSunoRemixDraftState {
&mut self,
draft_context: &AudioGenerationDraftContext<'_>,
) -> Result<KinoviSunoRemixRequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let audio_source = self.audio_source.take().ok_or_else(|| {
@@ -1,12 +1,12 @@
use kinovi_web_client::generate::audio::generate_suno_remix::{
use seedance2pro_client::generate::audio::generate_suno_remix::{
generate_suno_remix, GenerateSunoRemixArgs, GenerateSunoRemixRequest,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_audio::generate_audio_response::{
GenerateAudioResponse, KinoviWebAudioResponsePayload,
GenerateAudioResponse, Seedance2proAudioResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviSunoRemixRequestState {
}
impl KinoviSunoRemixRequestState {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSunoRemixArgs {
@@ -28,9 +28,9 @@ impl KinoviSunoRemixRequestState {
let response = generate_suno_remix(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
@@ -39,16 +39,16 @@ impl KinoviSunoRemixRequestState {
#[cfg(test)]
mod tests {
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use test_utils::test_file_path::test_file_path;
use crate::api::audio_list_ref::AudioListRef;
use crate::api::router_audio_model::RouterAudioModel;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
@@ -59,13 +59,13 @@ mod tests {
#[tokio::test]
#[ignore] // Sends a real generation to Kinovi; costs credits. Requires a local audio file.
async fn remix_uploaded_audio() {
let client = get_kinovi_web_client();
let client = get_seedance2pro_client();
let audio_url = upload_test_audio(&client).await;
println!("Uploaded audio: {}", audio_url);
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoRemix,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("Make this electronic".to_string()),
style_prompt: Some("EDM style".to_string()),
audio_references: Some(AudioListRef::Urls(vec![audio_url])),
@@ -73,17 +73,17 @@ mod tests {
};
let response = run_pipeline(&client, builder).await;
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
// ── Helpers ──
fn get_kinovi_web_client() -> RouterClient {
fn get_seedance2pro_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
}
async fn run_pipeline(client: &RouterClient, builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
@@ -102,7 +102,7 @@ mod tests {
let response = request.send_request(client).await.expect("send_request should succeed");
match &response {
GenerateAudioResponse::KinoviWeb(p) => {
GenerateAudioResponse::Seedance2Pro(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -112,7 +112,7 @@ mod tests {
}
async fn upload_test_audio(client: &RouterClient) -> String {
let session = &client.get_kinovi_web_client_ref().expect("kinovi_web client").session;
let session = &client.get_seedance2pro_client_ref().expect("seedance2pro client").session;
let audio_path = test_file_path(TEST_AUDIO_PATH).expect("test audio should exist");
let audio_bytes = std::fs::read(&audio_path).expect("read test audio");
@@ -162,7 +162,7 @@ mod tests {
fn base_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSample,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("mystical RPG adventure".to_string()),
style_prompt: Some("fantasy video game".to_string()),
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.aac".to_string()])),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
use seedance2pro_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
use crate::generate::generate_audio::providers::kinovi::suno_sample::draft::KinoviSunoSampleDraftState;
@@ -18,7 +18,7 @@ impl KinoviSunoSampleCostState {
}
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
// Cost math is owned by kinovi_web_client's binding — the router just
// Cost math is owned by seedance2pro_client's binding — the router just
// forwards the result so router cost ≡ binding cost by construction.
let pricing_request = GenerateSunoSampleRequest {
prompt: String::new(),
@@ -63,7 +63,7 @@ mod tests {
fn cost_from_draft_is_seven_cents() {
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSample,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("mystical RPG adventure".to_string()),
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.aac".to_string()])),
..Default::default()
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
use seedance2pro_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
@@ -34,7 +34,7 @@ impl KinoviSunoSampleDraftState {
&mut self,
draft_context: &AudioGenerationDraftContext<'_>,
) -> Result<KinoviSunoSampleRequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let audio_source = self.audio_source.take().ok_or_else(|| {
@@ -1,12 +1,12 @@
use kinovi_web_client::generate::audio::generate_suno_sample::{
use seedance2pro_client::generate::audio::generate_suno_sample::{
generate_suno_sample, GenerateSunoSampleArgs, GenerateSunoSampleRequest,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_audio::generate_audio_response::{
GenerateAudioResponse, KinoviWebAudioResponsePayload,
GenerateAudioResponse, Seedance2proAudioResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviSunoSampleRequestState {
}
impl KinoviSunoSampleRequestState {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSunoSampleArgs {
@@ -28,9 +28,9 @@ impl KinoviSunoSampleRequestState {
let response = generate_suno_sample(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
@@ -39,16 +39,16 @@ impl KinoviSunoSampleRequestState {
#[cfg(test)]
mod tests {
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use test_utils::test_file_path::test_file_path;
use crate::api::audio_list_ref::AudioListRef;
use crate::api::router_audio_model::RouterAudioModel;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
@@ -59,13 +59,13 @@ mod tests {
#[tokio::test]
#[ignore] // Sends a real generation to Kinovi; costs credits. Requires a local audio file.
async fn sample_instrumental() {
let client = get_kinovi_web_client();
let client = get_seedance2pro_client();
let audio_url = upload_test_audio(&client).await;
println!("Uploaded audio: {}", audio_url);
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSample,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("Mystical RPG adventure, make it have a grand climax".to_string()),
style_prompt: Some("Fantasy video game score".to_string()),
is_instrumental: Some(true),
@@ -74,17 +74,17 @@ mod tests {
};
let response = run_pipeline(&client, builder).await;
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
// ── Helpers ──
fn get_kinovi_web_client() -> RouterClient {
fn get_seedance2pro_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
}
async fn run_pipeline(client: &RouterClient, builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
@@ -103,7 +103,7 @@ mod tests {
let response = request.send_request(client).await.expect("send_request should succeed");
match &response {
GenerateAudioResponse::KinoviWeb(p) => {
GenerateAudioResponse::Seedance2Pro(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -113,7 +113,7 @@ mod tests {
}
async fn upload_test_audio(client: &RouterClient) -> String {
let session = &client.get_kinovi_web_client_ref().expect("kinovi_web client").session;
let session = &client.get_seedance2pro_client_ref().expect("seedance2pro client").session;
let audio_path = test_file_path(TEST_AUDIO_PATH).expect("test audio should exist");
let audio_bytes = std::fs::read(&audio_path).expect("read test audio");
@@ -1,5 +1,5 @@
use enums::common::generation::common_musical_key::CommonMusicalKey;
use kinovi_web_client::generate::audio::generate_suno_sound::{
use seedance2pro_client::generate::audio::generate_suno_sound::{
GenerateSunoSoundRequest, KinoviSunoSoundKey, KinoviSunoSoundType,
};
@@ -201,7 +201,7 @@ mod tests {
fn base_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSounds,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("rain sound effects".to_string()),
..Default::default()
}
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::audio::generate_suno_sound::{
use seedance2pro_client::generate::audio::generate_suno_sound::{
GenerateSunoSoundRequest, KinoviSunoSoundKey, KinoviSunoSoundType,
};
@@ -15,7 +15,7 @@ impl KinoviSunoSoundsCostState {
}
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
// Cost math is owned by kinovi_web_client's binding — the router just
// Cost math is owned by seedance2pro_client's binding — the router just
// forwards the result so router cost ≡ binding cost by construction.
let pricing_request = GenerateSunoSoundRequest {
prompt: String::new(),
@@ -57,7 +57,7 @@ mod tests {
fn cost_from_built_request_is_two_cents() {
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSounds,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("thunder".to_string()),
is_loopable: Some(true),
bpm: Some(60),
@@ -1,12 +1,12 @@
use kinovi_web_client::generate::audio::generate_suno_sound::{
use seedance2pro_client::generate::audio::generate_suno_sound::{
generate_suno_sound, GenerateSunoSoundArgs, GenerateSunoSoundRequest,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_audio::generate_audio_response::{
GenerateAudioResponse, KinoviWebAudioResponsePayload,
GenerateAudioResponse, Seedance2proAudioResponsePayload,
};
#[derive(Debug, Clone)]
@@ -16,7 +16,7 @@ pub struct KinoviSunoSoundsRequestState {
}
impl KinoviSunoSoundsRequestState {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSunoSoundArgs {
@@ -27,9 +27,9 @@ impl KinoviSunoSoundsRequestState {
let response = generate_suno_sound(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
@@ -39,12 +39,12 @@ impl KinoviSunoSoundsRequestState {
#[cfg(test)]
mod tests {
use enums::common::generation::common_musical_key::CommonMusicalKey;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use crate::api::router_audio_model::RouterAudioModel;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
@@ -56,7 +56,7 @@ mod tests {
prompt: Some("A heavy castle door creaking open".to_string()),
..suno_sounds_builder()
}).await;
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -70,7 +70,7 @@ mod tests {
musical_key: Some(CommonMusicalKey::AMinor),
..suno_sounds_builder()
}).await;
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -79,20 +79,20 @@ mod tests {
fn suno_sounds_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSounds,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
..Default::default()
}
}
fn get_kinovi_web_client() -> RouterClient {
fn get_seedance2pro_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
}
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
let client = get_kinovi_web_client();
let client = get_seedance2pro_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let request = match draft_or_request {
@@ -103,7 +103,7 @@ mod tests {
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateAudioResponse::KinoviWeb(p) => {
GenerateAudioResponse::Seedance2Pro(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -146,12 +146,12 @@ impl GenerateImageRequestBuilder {
(RouterProvider::Fal, RouterImageModel::QwenEdit2511Angles) => build_fal_qwen_edit_2511_angles(self),
(RouterProvider::Fal, RouterImageModel::Flux2LoraAngles) => build_fal_flux_2_lora_angles(self),
// Kinovi / KinoviWeb (Midjourney + Seedream)
(RouterProvider::KinoviWeb, RouterImageModel::Midjourney7) => build_kinovi_midjourney_7(self),
(RouterProvider::KinoviWeb, RouterImageModel::Midjourney7Niji) => build_kinovi_midjourney_7_niji(self),
(RouterProvider::KinoviWeb, RouterImageModel::Midjourney8) => build_kinovi_midjourney_8(self),
(RouterProvider::KinoviWeb, RouterImageModel::Seedream5p0Pro) => build_kinovi_seedream_5p0_pro(self),
(RouterProvider::KinoviWeb, RouterImageModel::Seedream5p0ProUltra) => build_kinovi_seedream_5p0_pro(self),
// Kinovi / Seedance2Pro (Midjourney + Seedream)
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7) => build_kinovi_midjourney_7(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7Niji) => build_kinovi_midjourney_7_niji(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney8) => build_kinovi_midjourney_8(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Seedream5p0Pro) => build_kinovi_seedream_5p0_pro(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Seedream5p0ProUltra) => build_kinovi_seedream_5p0_pro(self),
_ => self.unsupported_provider_and_model(),
}
@@ -25,11 +25,11 @@ pub struct FalImageResponsePayload {
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
}
/// Response from the KinoviWeb/Kinovi provider (used for Midjourney
/// image generation). Mirrors `KinoviWebVideoResponsePayload` on the
/// Response from the Seedance2Pro/Kinovi provider (used for Midjourney
/// image generation). Mirrors `Seedance2proVideoResponsePayload` on the
/// video side.
#[derive(Clone, Debug)]
pub struct KinoviWebImageResponsePayload {
pub struct Seedance2proImageResponsePayload {
pub order_id: String,
pub task_id: String,
pub maybe_order_ids: Option<Vec<String>>,
@@ -40,7 +40,7 @@ pub struct KinoviWebImageResponsePayload {
pub enum GenerateImageResponse {
Artcraft(ArtcraftImageResponsePayload),
Fal(FalImageResponsePayload),
KinoviWeb(KinoviWebImageResponsePayload),
Seedance2Pro(Seedance2proImageResponsePayload),
}
impl GenerateImageResponse {
@@ -58,9 +58,9 @@ impl GenerateImageResponse {
}
}
pub fn get_kinovi_web_payload(&self) -> Option<KinoviWebImageResponsePayload> {
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proImageResponsePayload> {
match self {
Self::KinoviWeb(p) => Some(p.clone()),
Self::Seedance2Pro(p) => Some(p.clone()),
_ => None,
}
}
@@ -17,7 +17,7 @@ use crate::generate::generate_image::providers::kinovi::seedream_5p0_pro::draft:
/// Drafts hold validated/planned parameters but may still need media
/// resolution (e.g. uploading local files to a provider) before they can
/// be sent. Today only the Kinovi models need this — when the caller
/// supplies reference images, we must upload them to the KinoviWeb
/// supplies reference images, we must upload them to the Seedance2Pro
/// CDN before sending the request.
#[derive(Clone, Debug)]
pub enum ImageGenerationDraftRequest {
@@ -30,10 +30,10 @@ pub enum ImageGenerationDraftRequest {
impl ImageGenerationDraftRequest {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::KinoviMidjourney7(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney7Niji(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney8(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedream5p0Pro(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedream5p0Pro(_) => RouterProvider::Seedance2Pro,
}
}
@@ -4,21 +4,21 @@ use std::fmt::Debug;
use tokens::tokens::media_files::MediaFileToken;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
/// Context passed when finalizing an image draft into a concrete request.
///
/// Drafts only exist for providers that need to upload assets before
/// sending the request. Today that's the KinoviWeb/Kinovi pipeline
/// sending the request. Today that's the Seedance2Pro/Kinovi pipeline
/// (used for Midjourney image generation with reference images); other
/// providers — Artcraft, Fal — accept image URLs directly and skip the
/// draft phase entirely.
#[derive(Clone, Default)]
pub struct ImageGenerationDraftContext<'a> {
/// Optional: the router client, needed for providers that require
/// authenticated calls (e.g. KinoviWeb/Kinovi uploads).
/// authenticated calls (e.g. Seedance2Pro/Kinovi uploads).
pub client: Option<&'a RouterClient>,
/// Optional: a map of Media File Tokens to their ArtCraft URLs.
@@ -29,9 +29,9 @@ pub struct ImageGenerationDraftContext<'a> {
}
impl<'a> ImageGenerationDraftContext<'a> {
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ArtcraftRouterError> {
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
client.get_kinovi_web_client_ref()
client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)
}
}
@@ -123,7 +123,7 @@ pub enum ImageGenerationRequest {
FalQwenEdit2511Angles(FalQwenEdit2511AnglesRequestState),
FalFlux2LoraAngles(FalFlux2LoraAnglesRequestState),
// ── Kinovi / KinoviWeb provider (Midjourney + Seedream image generation) ──
// ── Kinovi / Seedance2Pro provider (Midjourney + Seedream image generation) ──
KinoviMidjourney7(KinoviMidjourney7RequestState),
KinoviMidjourney7Niji(KinoviMidjourney7NijiRequestState),
KinoviMidjourney8(KinoviMidjourney8RequestState),
@@ -170,10 +170,10 @@ impl ImageGenerationRequest {
Self::FalQwenEdit2511Angles(_) => RouterProvider::Fal,
Self::FalFlux2LoraAngles(_) => RouterProvider::Fal,
Self::KinoviMidjourney7(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney7Niji(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney8(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedream5p0Pro(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedream5p0Pro(_) => RouterProvider::Seedance2Pro,
}
}
@@ -416,24 +416,24 @@ impl ImageGenerationRequest {
request.send(fal_client).await
}
// ── Kinovi / KinoviWeb (Midjourney + Seedream) ──
// ── Kinovi / Seedance2Pro (Midjourney + Seedream) ──
Self::KinoviMidjourney7(request) => {
let seedance_client = client.get_kinovi_web_client_ref()
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviMidjourney7Niji(request) => {
let seedance_client = client.get_kinovi_web_client_ref()
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviMidjourney8(request) => {
let seedance_client = client.get_kinovi_web_client_ref()
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviSeedream5p0Pro(request) => {
let seedance_client = client.get_kinovi_web_client_ref()
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
@@ -148,7 +148,7 @@ mod tests {
#[test]
fn price_covers_kinovi_cost() {
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
KinoviSeedream5p0ProBatchCount, KinoviSeedream5p0ProResolution,
};
use crate::generate::generate_image::providers::kinovi::seedream_5p0_pro::cost::KinoviSeedream5p0ProCostState;
@@ -169,7 +169,7 @@ mod tests {
#[test]
fn price_covers_kinovi_cost() {
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
KinoviSeedream5p0ProBatchCount, KinoviSeedream5p0ProResolution,
};
use crate::generate::generate_image::providers::kinovi::seedream_5p0_pro::cost::KinoviSeedream5p0ProCostState;
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_midjourney_v7::{
use seedance2pro_client::generate::image::generate_midjourney_v7::{
GenerateMidjourneyV7AspectRatio, GenerateMidjourneyV7Quality,
GenerateMidjourneyV7Request, KinoviMidjourneyBatchCount,
};
@@ -50,7 +50,7 @@ pub fn build_kinovi_midjourney_7(
}
// Image inputs present → must go through the draft phase so we can
// upload each image to the KinoviWeb CDN before sending.
// upload each image to the Seedance2Pro CDN before sending.
let draft = KinoviMidjourney7DraftState {
prompt,
aspect_ratio,
@@ -159,7 +159,7 @@ pub(crate) fn plan_batch_count(
#[cfg(test)]
mod tests {
use kinovi_web_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount;
use seedance2pro_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -178,7 +178,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a corgi astronaut".to_string()),
image_inputs: None,
resolution: None,
@@ -291,7 +291,7 @@ mod tests {
mod aspect_ratio_tests {
use super::*;
use kinovi_web_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio as Ar;
use seedance2pro_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio as Ar;
fn aspect_ratio_for(input: Option<RouterAspectRatio>) -> Ar {
let builder = GenerateImageRequestBuilder { aspect_ratio: input, ..base_builder() };
@@ -334,7 +334,7 @@ mod tests {
mod quality_tests {
use super::*;
use kinovi_web_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7Quality as Q;
use seedance2pro_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7Quality as Q;
fn quality_for(input: Option<RouterQuality>) -> Option<Q> {
let builder = GenerateImageRequestBuilder { quality: input, ..base_builder() };
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_midjourney_v7::{
use seedance2pro_client::generate::image::generate_midjourney_v7::{
GenerateMidjourneyV7AspectRatio, GenerateMidjourneyV7Request, KinoviMidjourneyBatchCount,
};
@@ -171,7 +171,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("test".to_string()),
image_inputs: None,
resolution: None,
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_midjourney_v7::{
use seedance2pro_client::generate::image::generate_midjourney_v7::{
GenerateMidjourneyV7AspectRatio, GenerateMidjourneyV7Quality,
GenerateMidjourneyV7Request, KinoviMidjourneyBatchCount,
};
@@ -17,7 +17,7 @@ pub struct KinoviMidjourney7DraftState {
pub quality: Option<GenerateMidjourneyV7Quality>,
pub batch_count: KinoviMidjourneyBatchCount,
/// Image references that still need to be uploaded to KinoviWeb CDN
/// Image references that still need to be uploaded to Seedance2Pro CDN
/// before the request is sendable. `None` once `to_request()` has run.
pub unhandled_request_state: Option<KinoviMidjourney7RemainingItems>,
}
@@ -32,7 +32,7 @@ impl KinoviMidjourney7DraftState {
&mut self,
draft_context: &ImageGenerationDraftContext<'_>,
) -> Result<KinoviMidjourney7RequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let mut reference_image_urls = None;
@@ -63,10 +63,10 @@ impl KinoviMidjourney7DraftState {
#[cfg(test)]
mod tests {
use super::*;
use kinovi_web_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio;
use seedance2pro_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio;
// Note: most behavioral tests live in `build.rs` and `cost.rs`. The
// `to_request()` method requires a live KinoviWeb client to upload
// `to_request()` method requires a live Seedance2Pro client to upload
// images, so its end-to-end behavior is exercised by the `request.rs`
// #[ignore]'d integration tests rather than unit-tested here.
@@ -1,12 +1,12 @@
use kinovi_web_client::generate::image::generate_midjourney_v7::{
use seedance2pro_client::generate::image::generate_midjourney_v7::{
generate_midjourney_v7, GenerateMidjourneyV7Args, GenerateMidjourneyV7Request,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
GenerateImageResponse, KinoviWebImageResponsePayload,
GenerateImageResponse, Seedance2proImageResponsePayload,
};
#[derive(Debug, Clone)]
@@ -18,7 +18,7 @@ pub struct KinoviMidjourney7RequestState {
impl KinoviMidjourney7RequestState {
pub async fn send(
&self,
client: &RouterKinoviWebClient,
client: &RouterSeedance2ProClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = GenerateMidjourneyV7Args {
session: &client.session,
@@ -28,9 +28,9 @@ impl KinoviMidjourney7RequestState {
let response = generate_midjourney_v7(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -43,7 +43,7 @@ impl KinoviMidjourney7RequestState {
mod tests {
use std::collections::HashMap;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -52,7 +52,7 @@ mod tests {
use crate::api::router_provider::RouterProvider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
@@ -64,7 +64,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a serene mountain lake at sunrise, cinematic".to_string()),
image_inputs: None,
resolution: None,
@@ -82,9 +82,9 @@ mod tests {
fn get_kinovi_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
}
#[tokio::test]
@@ -98,9 +98,9 @@ mod tests {
_ => panic!("expected direct Request for text-to-image"),
};
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
.await.expect("send should succeed");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
println!("v7 t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -135,7 +135,7 @@ mod tests {
.finalize(ctx).await.expect("finalize");
let response = request.send_request(&client).await.expect("send_request");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
println!("v7 with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
GenerateMidjourneyV7NijiAspectRatio, GenerateMidjourneyV7NijiQuality,
GenerateMidjourneyV7NijiRequest, KinoviMidjourneyBatchCount,
};
@@ -147,7 +147,7 @@ pub(crate) fn plan_batch_count(
#[cfg(test)]
mod tests {
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount;
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -166,7 +166,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7Niji,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a magical anime forest scene".to_string()),
image_inputs: None,
resolution: None,
@@ -235,7 +235,7 @@ mod tests {
#[test]
fn aspect_ratio_passes_through_to_niji_enum() {
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiAspectRatio as Ar;
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiAspectRatio as Ar;
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::WideTwentyOneByNine),
..base_builder()
@@ -249,7 +249,7 @@ mod tests {
#[test]
fn quality_high_maps_to_full() {
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiQuality as Q;
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiQuality as Q;
let builder = GenerateImageRequestBuilder {
quality: Some(RouterQuality::High),
..base_builder()
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
GenerateMidjourneyV7NijiAspectRatio, GenerateMidjourneyV7NijiRequest, KinoviMidjourneyBatchCount,
};
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
GenerateMidjourneyV7NijiAspectRatio, GenerateMidjourneyV7NijiQuality,
GenerateMidjourneyV7NijiRequest, KinoviMidjourneyBatchCount,
};
@@ -29,7 +29,7 @@ impl KinoviMidjourney7NijiDraftState {
&mut self,
draft_context: &ImageGenerationDraftContext<'_>,
) -> Result<KinoviMidjourney7NijiRequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let mut reference_image_urls = None;
@@ -1,12 +1,12 @@
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
generate_midjourney_v7_niji, GenerateMidjourneyV7NijiArgs, GenerateMidjourneyV7NijiRequest,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
GenerateImageResponse, KinoviWebImageResponsePayload,
GenerateImageResponse, Seedance2proImageResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviMidjourney7NijiRequestState {
impl KinoviMidjourney7NijiRequestState {
pub async fn send(
&self,
client: &RouterKinoviWebClient,
client: &RouterSeedance2ProClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = GenerateMidjourneyV7NijiArgs {
session: &client.session,
@@ -27,9 +27,9 @@ impl KinoviMidjourney7NijiRequestState {
let response = generate_midjourney_v7_niji(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -42,7 +42,7 @@ impl KinoviMidjourney7NijiRequestState {
mod tests {
use std::collections::HashMap;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -51,7 +51,7 @@ mod tests {
use crate::api::router_provider::RouterProvider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
@@ -62,7 +62,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7Niji,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a magical anime fox spirit in a moonlit bamboo forest".to_string()),
image_inputs: None,
resolution: None,
@@ -80,9 +80,9 @@ mod tests {
fn get_kinovi_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
}
#[tokio::test]
@@ -96,9 +96,9 @@ mod tests {
_ => panic!("expected direct Request for text-to-image"),
};
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
.await.expect("send should succeed");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
println!("v7-niji t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -133,7 +133,7 @@ mod tests {
.finalize(ctx).await.expect("finalize");
let response = request.send_request(&client).await.expect("send_request");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
println!("v7-niji with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_midjourney_v8::{
use seedance2pro_client::generate::image::generate_midjourney_v8::{
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Quality,
GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
};
@@ -147,7 +147,7 @@ pub(crate) fn plan_batch_count(
#[cfg(test)]
mod tests {
use kinovi_web_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount;
use seedance2pro_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -164,7 +164,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney8,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a spooky horror scene".to_string()),
image_inputs: None,
resolution: None,
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_midjourney_v8::{
use seedance2pro_client::generate::image::generate_midjourney_v8::{
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
};
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_midjourney_v8::{
use seedance2pro_client::generate::image::generate_midjourney_v8::{
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Quality,
GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
};
@@ -29,7 +29,7 @@ impl KinoviMidjourney8DraftState {
&mut self,
draft_context: &ImageGenerationDraftContext<'_>,
) -> Result<KinoviMidjourney8RequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let mut reference_image_urls = None;
@@ -1,12 +1,12 @@
use kinovi_web_client::generate::image::generate_midjourney_v8::{
use seedance2pro_client::generate::image::generate_midjourney_v8::{
generate_midjourney_v8, GenerateMidjourneyV8Args, GenerateMidjourneyV8Request,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
GenerateImageResponse, KinoviWebImageResponsePayload,
GenerateImageResponse, Seedance2proImageResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviMidjourney8RequestState {
impl KinoviMidjourney8RequestState {
pub async fn send(
&self,
client: &RouterKinoviWebClient,
client: &RouterSeedance2ProClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = GenerateMidjourneyV8Args {
session: &client.session,
@@ -27,9 +27,9 @@ impl KinoviMidjourney8RequestState {
let response = generate_midjourney_v8(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -42,7 +42,7 @@ impl KinoviMidjourney8RequestState {
mod tests {
use std::collections::HashMap;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -51,7 +51,7 @@ mod tests {
use crate::api::router_provider::RouterProvider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
@@ -62,7 +62,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney8,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a spooky horror scene with ghosts in an abandoned mansion".to_string()),
image_inputs: None,
resolution: None,
@@ -80,9 +80,9 @@ mod tests {
fn get_kinovi_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
}
#[tokio::test]
@@ -96,9 +96,9 @@ mod tests {
_ => panic!("expected direct Request for text-to-image"),
};
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
.await.expect("send should succeed");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
println!("v8 t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -133,7 +133,7 @@ mod tests {
.finalize(ctx).await.expect("finalize");
let response = request.send_request(&client).await.expect("send_request");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
println!("v8 with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -1,21 +1,21 @@
use std::collections::HashMap;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::providers::kinovi::upload::upload_to_kinovi_web;
use crate::generate::generate_image::providers::kinovi::upload::upload_to_seedance2pro;
/// Resolve an [`ImageListRef`] to a list of public KinoviWeb CDN URLs,
/// Resolve an [`ImageListRef`] to a list of public Seedance2Pro CDN URLs,
/// uploading each image one at a time. Preserves order.
///
/// Returns `Ok(None)` when the input is `None` or an empty list — the
/// caller can then omit the field from the wire request entirely. (The
/// downstream Midjourney API rejects empty arrays.)
pub(crate) async fn resolve_and_upload_image_list(
session: &KinoviWebSession,
session: &Seedance2ProSession,
list: Option<ImageListRef>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
@@ -29,7 +29,7 @@ pub(crate) async fn resolve_and_upload_image_list(
let mut uploaded = Vec::with_capacity(source_urls.len());
for url in &source_urls {
uploaded.push(upload_to_kinovi_web(session, url).await?);
uploaded.push(upload_to_seedance2pro(session, url).await?);
}
Ok(Some(uploaded))
}
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
GenerateSeedream5p0ProRequest, KinoviSeedream5p0ProAspectRatio, KinoviSeedream5p0ProBatchCount,
KinoviSeedream5p0ProResolution, MAX_REFERENCE_IMAGES,
};
@@ -67,7 +67,7 @@ fn has_image_inputs(image_inputs: Option<&ImageListRef>) -> bool {
}
/// Reject over-limit reference lists before any uploads happen — the
/// KinoviWeb client would reject them anyway, but only after the draft
/// Seedance2Pro client would reject them anyway, but only after the draft
/// stage has already uploaded every image to the Kinovi CDN.
fn validate_reference_image_count(
image_inputs: Option<&ImageListRef>,
@@ -191,7 +191,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Seedream5p0Pro,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("an anime girl riding a dinosaur".to_string()),
image_inputs: None,
resolution: None,
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
GenerateSeedream5p0ProRequest, KinoviSeedream5p0ProAspectRatio, KinoviSeedream5p0ProBatchCount,
KinoviSeedream5p0ProResolution,
};
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
GenerateSeedream5p0ProRequest, KinoviSeedream5p0ProAspectRatio, KinoviSeedream5p0ProBatchCount,
KinoviSeedream5p0ProResolution,
};
@@ -29,7 +29,7 @@ impl KinoviSeedream5p0ProDraftState {
&mut self,
draft_context: &ImageGenerationDraftContext<'_>,
) -> Result<KinoviSeedream5p0ProRequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let mut reference_image_urls = None;
@@ -1,12 +1,12 @@
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
generate_seedream_5p0_pro, GenerateSeedream5p0ProArgs, GenerateSeedream5p0ProRequest,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
GenerateImageResponse, KinoviWebImageResponsePayload,
GenerateImageResponse, Seedance2proImageResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviSeedream5p0ProRequestState {
impl KinoviSeedream5p0ProRequestState {
pub async fn send(
&self,
client: &RouterKinoviWebClient,
client: &RouterSeedance2ProClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = GenerateSeedream5p0ProArgs {
session: &client.session,
@@ -27,9 +27,9 @@ impl KinoviSeedream5p0ProRequestState {
let response = generate_seedream_5p0_pro(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -42,7 +42,7 @@ impl KinoviSeedream5p0ProRequestState {
mod tests {
use std::collections::HashMap;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -52,7 +52,7 @@ mod tests {
use crate::api::router_resolution::RouterResolution;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
@@ -63,7 +63,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Seedream5p0Pro,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("an anime girl riding a dinosaur through a neon city".to_string()),
image_inputs: None,
resolution: Some(RouterResolution::OneK),
@@ -81,9 +81,9 @@ mod tests {
fn get_kinovi_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
}
#[tokio::test]
@@ -97,9 +97,9 @@ mod tests {
_ => panic!("expected direct Request for text-to-image"),
};
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
.await.expect("send should succeed");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
println!("seedream 5.0 pro t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -134,7 +134,7 @@ mod tests {
.finalize(ctx).await.expect("finalize");
let response = request.send_request(&client).await.expect("send_request");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
println!("seedream 5.0 pro with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -1,6 +1,6 @@
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use url_utils::extension::extract_extension_from_url::{extract_extension_from_url_str, ExtractExtensions};
use crate::errors::artcraft_router_error::ArtcraftRouterError;
@@ -8,14 +8,14 @@ use crate::errors::provider_error::ProviderError;
use crate::utils::download_file::download_file;
/// Downloads an image from a source URL and re-uploads it to the
/// KinoviWeb/Kinovi CDN. Returns the public URL.
/// Seedance2Pro/Kinovi CDN. Returns the public URL.
///
/// This is the image-side twin of `generate_video::providers::kinovi::upload`.
/// The actual upload path doesn't care whether the file is a video or an
/// image, but keeping the two helpers separate lets each side evolve
/// independently and makes intent clear at call sites.
pub(crate) async fn upload_to_kinovi_web(
session: &KinoviWebSession,
pub(crate) async fn upload_to_seedance2pro(
session: &Seedance2ProSession,
source_url: &str,
) -> Result<String, ArtcraftRouterError> {
let extension = extract_extension_from_url_str(source_url, &ExtractExtensions::All)
@@ -30,7 +30,7 @@ pub(crate) async fn upload_to_kinovi_web(
host_override: None,
})
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
let upload_response = upload_file(UploadFileArgs {
upload_url: prepare_response.upload_url,
@@ -38,7 +38,7 @@ pub(crate) async fn upload_to_kinovi_web(
host_override: None,
})
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(upload_response.public_url)
}
@@ -268,7 +268,7 @@ mod tests {
for provider in [
RouterProvider::GmiCloud,
RouterProvider::GrokApi,
RouterProvider::KinoviWeb,
RouterProvider::Seedance2Pro,
RouterProvider::WorldLabs,
] {
let result = builder_with_url_image(provider, RouterMeshModel::Hunyuan3d3).build2();
@@ -252,7 +252,7 @@ mod tests {
RouterProvider::Fal,
RouterProvider::GmiCloud,
RouterProvider::GrokApi,
RouterProvider::KinoviWeb,
RouterProvider::Seedance2Pro,
] {
let result = GenerateSplatRequestBuilder {
provider,
@@ -74,17 +74,17 @@ generate_video/
│ ├── seedance_2p0/ ← Seedance 2.0 Pro model (skips draft, returns Request directly)
│ │ ├── mod.rs
│ │ ├── build.rs ← builder → Request conversion + plan helpers
│ │ ├── cost.rs ← standalone cost estimation (independent of kinovi_web_client)
│ │ ├── cost.rs ← standalone cost estimation (independent of seedance2pro_client)
│ │ └── request.rs ← request state + send() via Artcraft multi-function API
│ └── seedance_2p0_fast/ ← Seedance 2.0 Fast model (skips draft, 480p/720p only)
│ ├── mod.rs
│ ├── build.rs ← builder → Request conversion + plan helpers (no 1080p)
│ ├── cost.rs ← standalone cost estimation with Fast-specific pricing
│ └── request.rs ← request state + send() via Artcraft omni-gen API
└── kinovi/ ← Kinovi/KinoviWeb provider
└── kinovi/ ← Kinovi/Seedance2Pro provider
├── mod.rs
├── resolve.rs ← shared: media token resolution, upload helpers
├── upload.rs ← shared: download + re-upload to KinoviWeb CDN
├── upload.rs ← shared: download + re-upload to Seedance2Pro CDN
├── seedance_2p0/ ← Seedance 2.0 Pro model
│ ├── mod.rs
│ ├── build.rs ← builder → draft conversion + plan helpers
@@ -230,12 +230,12 @@ impl GenerateVideoRequestBuilder {
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo) => build_grok_api_grok_imagine_video(self),
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo1p5) => build_grok_api_grok_imagine_video_1p5(self),
// Kinovi
(RouterProvider::KinoviWeb, RouterVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0Mini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0BytePlusMini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_kinovi_seedance_2p0_mini(self),
(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(),
}
}
@@ -10,7 +10,7 @@ pub struct ArtcraftVideoResponsePayload {
}
#[derive(Clone, Debug)]
pub struct KinoviWebVideoResponsePayload {
pub struct Seedance2proVideoResponsePayload {
pub order_id: String,
pub task_id: String,
pub maybe_order_ids: Option<Vec<String>>,
@@ -58,7 +58,7 @@ pub enum GenerateVideoResponse {
Artcraft(ArtcraftVideoResponsePayload),
GmiCloud(GmiCloudVideoResponsePayload),
Grok(GrokVideoResponsePayload),
KinoviWeb(KinoviWebVideoResponsePayload),
Seedance2Pro(Seedance2proVideoResponsePayload),
Fal(FalVideoResponsePayload),
}
@@ -70,9 +70,9 @@ impl GenerateVideoResponse {
}
}
pub fn get_kinovi_web_payload(&self) -> Option<KinoviWebVideoResponsePayload> {
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proVideoResponsePayload> {
match self {
Self::KinoviWeb(p) => Some(p.clone()),
Self::Seedance2Pro(p) => Some(p.clone()),
_ => None,
}
}
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_happy_horse_1p0::{
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
KinoviHappyHorse1p0AspectRatio, KinoviHappyHorse1p0BatchCount,
KinoviHappyHorse1p0OutputResolution,
};
@@ -441,7 +441,7 @@ mod tests {
fn happy_horse_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a cat dancing".to_string()),
duration_seconds: Some(5),
video_batch_count: Some(1),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_happy_horse_1p0::{
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
GenerateHappyHorse1p0Request, KinoviHappyHorse1p0BatchCount,
KinoviHappyHorse1p0OutputResolution,
};
@@ -310,7 +310,7 @@ mod tests {
use crate::api::router_video_model::RouterVideoModel;
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::HappyHorse1p0,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
resolution,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_happy_horse_1p0::{
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
GenerateHappyHorse1p0Request, KinoviHappyHorse1p0AspectRatio,
KinoviHappyHorse1p0BatchCount, KinoviHappyHorse1p0OutputResolution,
};
@@ -30,7 +30,7 @@ impl KinoviHappyHorse1p0DraftState {
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviHappyHorse1p0RequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let mut start_frame_url = None;
@@ -1,11 +1,11 @@
use kinovi_web_client::generate::video::generate_happy_horse_1p0::{
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
generate_happy_horse_1p0, GenerateHappyHorse1p0Args, GenerateHappyHorse1p0Request,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
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, KinoviWebVideoResponsePayload};
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
#[derive(Debug, Clone)]
pub struct KinoviHappyHorse1p0RequestState {
@@ -13,7 +13,7 @@ pub struct KinoviHappyHorse1p0RequestState {
}
impl KinoviHappyHorse1p0RequestState {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateHappyHorse1p0Args {
@@ -24,9 +24,9 @@ impl KinoviHappyHorse1p0RequestState {
let response = generate_happy_horse_1p0(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateVideoResponse::KinoviWeb(KinoviWebVideoResponsePayload {
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -47,12 +47,12 @@ mod tests {
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
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_context::VideoGenerationDraftContext;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
mod aspect_ratio_tests {
@@ -66,7 +66,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -78,7 +78,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::TallNineBySixteen),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -90,7 +90,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::Square),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
@@ -106,7 +106,7 @@ mod tests {
resolution: Some(RouterResolution::SevenTwentyP),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -118,7 +118,7 @@ mod tests {
resolution: Some(RouterResolution::TenEightyP),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
@@ -134,7 +134,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -152,7 +152,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..happy_horse_builder()
}, media_map).await;
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -172,7 +172,7 @@ mod tests {
duration_seconds: Some(15),
..happy_horse_builder()
}, media_map).await;
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
@@ -182,22 +182,22 @@ mod tests {
fn happy_horse_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::HappyHorse1p0,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
duration_seconds: Some(4),
video_batch_count: Some(1),
..Default::default()
}
}
fn get_kinovi_web_client() -> RouterClient {
fn get_seedance2pro_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
}
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
let client = get_kinovi_web_client();
let client = get_seedance2pro_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let draft = match draft_or_request {
@@ -214,7 +214,7 @@ mod tests {
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::KinoviWeb(p) => {
GenerateVideoResponse::Seedance2Pro(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -227,7 +227,7 @@ mod tests {
builder: GenerateVideoRequestBuilder,
media_map: HashMap<MediaFileToken, String>,
) -> GenerateVideoResponse {
let client = get_kinovi_web_client();
let client = get_seedance2pro_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let draft = match draft_or_request {
@@ -245,7 +245,7 @@ mod tests {
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::KinoviWeb(p) => {
GenerateVideoResponse::Seedance2Pro(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::audio_list_ref::AudioListRef;
@@ -10,7 +10,7 @@ use crate::api::image_ref::ImageRef;
use crate::api::video_list_ref::VideoListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::providers::kinovi::upload::upload_to_kinovi_web;
use crate::generate::generate_video::providers::kinovi::upload::upload_to_seedance2pro;
use crate::generate::generate_video::video_generation_draft_context::VideoGenerationDraftContext;
/// Either resolved URLs or unresolved media file tokens.
@@ -40,9 +40,9 @@ pub(crate) fn audio_list_ref_into_urls_or_tokens(list: AudioListRef) -> UrlsOrTo
}
}
/// Resolve a single ImageRef and upload to KinoviWeb CDN.
/// Resolve a single ImageRef and upload to Seedance2Pro CDN.
pub(crate) async fn resolve_and_upload_single(
session: &KinoviWebSession,
session: &Seedance2ProSession,
image_ref: Option<ImageRef>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<String>, ArtcraftRouterError> {
@@ -51,13 +51,13 @@ pub(crate) async fn resolve_and_upload_single(
Some(ImageRef::Url(url)) => url,
Some(ImageRef::MediaFileToken(token)) => resolve_token(maybe_map, &token)?,
};
Ok(Some(upload_to_kinovi_web(session, &source_url).await?))
Ok(Some(upload_to_seedance2pro(session, &source_url).await?))
}
/// Resolve a list of refs to URLs and upload each to KinoviWeb CDN.
/// Resolve a list of refs to URLs and upload each to Seedance2Pro CDN.
/// Order is preserved.
pub(crate) async fn resolve_and_upload_list(
session: &KinoviWebSession,
session: &Seedance2ProSession,
urls_or_tokens: Option<UrlsOrTokens>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
@@ -71,7 +71,7 @@ pub(crate) async fn resolve_and_upload_list(
let mut uploaded = Vec::with_capacity(source_urls.len());
for url in &source_urls {
uploaded.push(upload_to_kinovi_web(session, url).await?);
uploaded.push(upload_to_seedance2pro(session, url).await?);
}
Ok(Some(uploaded))
}
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_seedance_2p0::{
use seedance2pro_client::generate::video::generate_seedance_2p0::{
KinoviSeedance2p0AspectRatio as KinoviAspectRatio,
KinoviSeedance2p0Bitrate as KinoviBitrate,
KinoviSeedance2p0BatchCount as KinoviBatchCount,
@@ -212,7 +212,7 @@ fn plan_bitrate(bitrate: Option<RouterBitrate>) -> Option<KinoviBitrate> {
#[cfg(test)]
mod tests {
use kinovi_web_client::generate::video::generate_seedance_2p0::{
use seedance2pro_client::generate::video::generate_seedance_2p0::{
KinoviSeedance2p0AspectRatio as KinoviAspectRatio,
KinoviSeedance2p0BatchCount as KinoviBatchCount,
KinoviSeedance2p0OutputResolution as KinoviOutputResolution,
@@ -242,27 +242,27 @@ mod tests {
#[test]
fn prompt_is_passed_through() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(kinovi_web_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
assert_eq!(draft.prompt, "a cat dancing");
}
#[test]
fn prompt_defaults_to_empty() {
let builder = GenerateVideoRequestBuilder { prompt: None, ..kinovi_web_builder() };
let builder = GenerateVideoRequestBuilder { prompt: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.prompt, "");
}
#[test]
fn duration_seconds_converted() {
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..kinovi_web_builder() };
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 10);
}
#[test]
fn duration_defaults_to_5() {
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..kinovi_web_builder() };
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 5);
}
@@ -271,7 +271,7 @@ mod tests {
fn duration_clamped_to_max() {
let builder = GenerateVideoRequestBuilder {
duration_seconds: Some(99),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 15);
@@ -279,21 +279,21 @@ mod tests {
#[test]
fn batch_count_one() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..kinovi_web_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::One));
}
#[test]
fn batch_count_two() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..kinovi_web_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Two));
}
#[test]
fn batch_count_four() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..kinovi_web_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Four));
}
@@ -308,7 +308,7 @@ mod tests {
fn aspect_ratio_wide() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
@@ -318,7 +318,7 @@ mod tests {
fn aspect_ratio_tall() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::TallNineBySixteen),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait9x16));
@@ -328,7 +328,7 @@ mod tests {
fn aspect_ratio_square() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::Square),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Square1x1));
@@ -336,7 +336,7 @@ mod tests {
#[test]
fn aspect_ratio_defaults_to_landscape() {
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..kinovi_web_builder() };
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
@@ -351,7 +351,7 @@ mod tests {
fn resolution_480p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourEightyP),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
@@ -361,7 +361,7 @@ mod tests {
fn resolution_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::SevenTwentyP),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -371,7 +371,7 @@ mod tests {
fn resolution_1080p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
@@ -381,7 +381,7 @@ mod tests {
fn resolution_4k() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourK),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourK)));
@@ -389,7 +389,7 @@ mod tests {
#[test]
fn resolution_none() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..kinovi_web_builder() };
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(draft.resolution.is_none());
}
@@ -399,7 +399,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::ThreeK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..kinovi_web_builder()
..seedance2pro_builder()
};
assert!(build_kinovi_seedance_2p0(builder).is_err());
}
@@ -409,7 +409,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::ThreeK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
@@ -425,7 +425,7 @@ mod tests {
fn bitrate_high() {
let builder = GenerateVideoRequestBuilder {
bitrate: Some(RouterBitrate::High),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.bitrate, Some(KinoviBitrate::High)));
@@ -435,7 +435,7 @@ mod tests {
fn bitrate_normal_maps_to_standard() {
let builder = GenerateVideoRequestBuilder {
bitrate: Some(RouterBitrate::Normal),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(draft.bitrate.is_none());
@@ -443,7 +443,7 @@ mod tests {
#[test]
fn bitrate_none_maps_to_standard() {
let builder = GenerateVideoRequestBuilder { bitrate: None, ..kinovi_web_builder() };
let builder = GenerateVideoRequestBuilder { bitrate: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(draft.bitrate.is_none());
}
@@ -456,7 +456,7 @@ mod tests {
#[test]
fn unhandled_state_is_present() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(kinovi_web_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
assert!(draft.unhandled_request_state.is_some());
}
@@ -464,7 +464,7 @@ mod tests {
fn start_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -475,7 +475,7 @@ mod tests {
fn end_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -486,7 +486,7 @@ mod tests {
fn start_frame_media_token_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test123".to_string()))),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -500,7 +500,7 @@ mod tests {
"https://example.com/ref1.jpg".to_string(),
"https://example.com/ref2.jpg".to_string(),
])),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -520,7 +520,7 @@ mod tests {
reference_videos: Some(VideoListRef::Urls(vec![
"https://example.com/vid.mp4".to_string(),
])),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -533,7 +533,7 @@ mod tests {
reference_audio: Some(AudioListRef::Urls(vec![
"https://example.com/audio.mp3".to_string(),
])),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -547,7 +547,7 @@ mod tests {
CharacterToken::new("char_abc".to_string()),
CharacterToken::new("char_def".to_string()),
])),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -563,7 +563,7 @@ mod tests {
#[test]
fn empty_refs_are_none_in_unhandled() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(kinovi_web_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.start_frame.is_none());
assert!(remaining.end_frame.is_none());
@@ -592,7 +592,7 @@ mod tests {
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_xyz".to_string()),
])),
..kinovi_web_builder()
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
@@ -613,9 +613,9 @@ mod tests {
// ── Helpers ──
fn kinovi_web_builder() -> GenerateVideoRequestBuilder {
fn seedance2pro_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a cat dancing".to_string()),
duration_seconds: Some(5),
video_batch_count: Some(1),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_seedance_2p0::{
use seedance2pro_client::generate::video::generate_seedance_2p0::{
GenerateSeedance2p0Request, KinoviSeedance2p0BatchCount,
KinoviSeedance2p0OutputResolution,
};
@@ -85,7 +85,7 @@ impl KinoviSeedance2p0CostState {
#[cfg(test)]
mod tests {
use super::*;
use kinovi_web_client::generate::video::generate_seedance_2p0::{
use seedance2pro_client::generate::video::generate_seedance_2p0::{
KinoviSeedance2p0OutputResolution as KinoviOutputResolution,
KinoviSeedance2p0BatchCount as KinoviBatchCount,
};
@@ -177,7 +177,7 @@ mod tests {
// ── 4K pricing ──
//
// Routes to the kinovi_web_client: 4K base is 200 credits/s (vs 90 at 1080p).
// 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 {
@@ -202,7 +202,7 @@ mod tests {
}
}
// ── 4K: router cost must match the kinovi_web_client binding ──
// ── 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
@@ -213,7 +213,7 @@ mod tests {
mod four_k_matches_binding {
use super::*;
use kinovi_web_client::generate::video::generate_seedance_2p0::GenerateSeedance2p0Request;
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 {
@@ -226,7 +226,7 @@ mod tests {
.estimate_cost()
}
/// The kinovi_web_client binding's own 4K cost for the same inputs.
/// 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 {
@@ -579,7 +579,7 @@ mod tests {
};
let builder = GenerateVideoRequestBuilder {
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
resolution,
reference_videos,
duration_seconds: Some(duration_seconds),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_seedance_2p0::{
use seedance2pro_client::generate::video::generate_seedance_2p0::{
GenerateSeedance2p0Request, KinoviSeedance2p0AspectRatio, KinoviSeedance2p0Bitrate,
KinoviSeedance2p0BatchCount, KinoviSeedance2p0OutputResolution,
};
@@ -47,7 +47,7 @@ impl KinoviSeedance2p0DraftState {
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviSeedance2p0RequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let mut start_frame_url = None;
@@ -1,11 +1,11 @@
use kinovi_web_client::generate::video::generate_seedance_2p0::{
use seedance2pro_client::generate::video::generate_seedance_2p0::{
generate_seedance_2p0, GenerateSeedance2p0Args, GenerateSeedance2p0Request,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
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, KinoviWebVideoResponsePayload};
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p0RequestState {
@@ -14,7 +14,7 @@ pub struct KinoviSeedance2p0RequestState {
}
impl KinoviSeedance2p0RequestState {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSeedance2p0Args {
@@ -25,9 +25,9 @@ impl KinoviSeedance2p0RequestState {
let response = generate_seedance_2p0(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateVideoResponse::KinoviWeb(KinoviWebVideoResponsePayload {
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
KinoviSeedance2p0FastAspectRatio as KinoviAspectRatio,
KinoviSeedance2p0FastBitrate as KinoviBitrate,
KinoviSeedance2p0FastBatchCount as KinoviBatchCount,
@@ -226,7 +226,7 @@ fn plan_bitrate(bitrate: Option<RouterBitrate>) -> Option<KinoviBitrate> {
#[cfg(test)]
mod tests {
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
KinoviSeedance2p0FastAspectRatio as KinoviAspectRatio,
KinoviSeedance2p0FastBatchCount as KinoviBatchCount,
KinoviSeedance2p0FastOutputResolution as KinoviOutputResolution,
@@ -258,27 +258,27 @@ mod tests {
#[test]
fn prompt_is_passed_through() {
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(kinovi_web_fast_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(seedance2pro_fast_builder()));
assert_eq!(draft.prompt, "a cat dancing");
}
#[test]
fn prompt_defaults_to_empty() {
let builder = GenerateVideoRequestBuilder { prompt: None, ..kinovi_web_fast_builder() };
let builder = GenerateVideoRequestBuilder { prompt: None, ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.prompt, "");
}
#[test]
fn duration_seconds_converted() {
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..kinovi_web_fast_builder() };
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.duration_seconds, 10);
}
#[test]
fn duration_defaults_to_5() {
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..kinovi_web_fast_builder() };
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.duration_seconds, 5);
}
@@ -287,7 +287,7 @@ mod tests {
fn duration_clamped_to_max() {
let builder = GenerateVideoRequestBuilder {
duration_seconds: Some(99),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.duration_seconds, 15);
@@ -295,21 +295,21 @@ mod tests {
#[test]
fn batch_count_one() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..kinovi_web_fast_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::One));
}
#[test]
fn batch_count_two() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..kinovi_web_fast_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Two));
}
#[test]
fn batch_count_four() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..kinovi_web_fast_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Four));
}
@@ -324,7 +324,7 @@ mod tests {
fn aspect_ratio_wide() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
@@ -334,7 +334,7 @@ mod tests {
fn aspect_ratio_tall() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::TallNineBySixteen),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait9x16));
@@ -344,7 +344,7 @@ mod tests {
fn aspect_ratio_square() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::Square),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Square1x1));
@@ -352,7 +352,7 @@ mod tests {
#[test]
fn aspect_ratio_defaults_to_landscape() {
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..kinovi_web_fast_builder() };
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
@@ -367,7 +367,7 @@ mod tests {
fn resolution_480p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourEightyP),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
@@ -377,7 +377,7 @@ mod tests {
fn resolution_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::SevenTwentyP),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -388,7 +388,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -399,7 +399,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -410,14 +410,14 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
assert!(build_kinovi_seedance_2p0_fast(builder).is_err());
}
#[test]
fn resolution_none() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..kinovi_web_fast_builder() };
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(draft.resolution.is_none());
}
@@ -427,7 +427,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
assert!(build_kinovi_seedance_2p0_fast(builder).is_err());
}
@@ -437,7 +437,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -453,7 +453,7 @@ mod tests {
fn bitrate_high() {
let builder = GenerateVideoRequestBuilder {
bitrate: Some(RouterBitrate::High),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.bitrate, Some(KinoviBitrate::High)));
@@ -463,7 +463,7 @@ mod tests {
fn bitrate_normal_maps_to_standard() {
let builder = GenerateVideoRequestBuilder {
bitrate: Some(RouterBitrate::Normal),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(draft.bitrate.is_none());
@@ -471,7 +471,7 @@ mod tests {
#[test]
fn bitrate_none_maps_to_standard() {
let builder = GenerateVideoRequestBuilder { bitrate: None, ..kinovi_web_fast_builder() };
let builder = GenerateVideoRequestBuilder { bitrate: None, ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(draft.bitrate.is_none());
}
@@ -484,7 +484,7 @@ mod tests {
#[test]
fn unhandled_state_is_present() {
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(kinovi_web_fast_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(seedance2pro_fast_builder()));
assert!(draft.unhandled_request_state.is_some());
}
@@ -492,7 +492,7 @@ mod tests {
fn start_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -503,7 +503,7 @@ mod tests {
fn end_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -514,7 +514,7 @@ mod tests {
fn start_frame_media_token_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test123".to_string()))),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -528,7 +528,7 @@ mod tests {
"https://example.com/ref1.jpg".to_string(),
"https://example.com/ref2.jpg".to_string(),
])),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -548,7 +548,7 @@ mod tests {
reference_videos: Some(VideoListRef::Urls(vec![
"https://example.com/vid.mp4".to_string(),
])),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -561,7 +561,7 @@ mod tests {
reference_audio: Some(AudioListRef::Urls(vec![
"https://example.com/audio.mp3".to_string(),
])),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -575,7 +575,7 @@ mod tests {
CharacterToken::new("char_abc".to_string()),
CharacterToken::new("char_def".to_string()),
])),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -591,7 +591,7 @@ mod tests {
#[test]
fn empty_refs_are_none_in_unhandled() {
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(kinovi_web_fast_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(seedance2pro_fast_builder()));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.start_frame.is_none());
assert!(remaining.end_frame.is_none());
@@ -620,7 +620,7 @@ mod tests {
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_xyz".to_string()),
])),
..kinovi_web_fast_builder()
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
@@ -641,10 +641,10 @@ mod tests {
// ── Helpers ──
fn kinovi_web_fast_builder() -> GenerateVideoRequestBuilder {
fn seedance2pro_fast_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Fast,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a cat dancing".to_string()),
duration_seconds: Some(5),
video_batch_count: Some(1),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
GenerateSeedance2p0FastRequest, KinoviSeedance2p0FastBatchCount,
KinoviSeedance2p0FastOutputResolution,
};
@@ -86,7 +86,7 @@ impl KinoviSeedance2p0FastCostState {
mod tests {
use super::*;
use crate::api::router_resolution::RouterResolution;
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
KinoviSeedance2p0FastOutputResolution as KinoviOutputResolution,
KinoviSeedance2p0FastBatchCount as KinoviBatchCount,
};
@@ -386,7 +386,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Fast,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
resolution,
reference_videos,
duration_seconds: Some(duration_seconds),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
GenerateSeedance2p0FastRequest, KinoviSeedance2p0FastAspectRatio, KinoviSeedance2p0FastBitrate,
KinoviSeedance2p0FastBatchCount, KinoviSeedance2p0FastOutputResolution,
};
@@ -44,7 +44,7 @@ impl KinoviSeedance2p0FastDraftState {
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviSeedance2p0FastRequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let mut start_frame_url = None;
@@ -1,11 +1,11 @@
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
generate_seedance_2p0_fast, GenerateSeedance2p0FastArgs, GenerateSeedance2p0FastRequest,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
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, KinoviWebVideoResponsePayload};
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p0FastRequestState {
@@ -14,7 +14,7 @@ pub struct KinoviSeedance2p0FastRequestState {
}
impl KinoviSeedance2p0FastRequestState {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSeedance2p0FastArgs {
@@ -25,9 +25,9 @@ impl KinoviSeedance2p0FastRequestState {
let response = generate_seedance_2p0_fast(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateVideoResponse::KinoviWeb(KinoviWebVideoResponsePayload {
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
KinoviSeedance2p0MiniAspectRatio as KinoviAspectRatio,
KinoviSeedance2p0MiniBitrate as KinoviBitrate,
KinoviSeedance2p0MiniBatchCount as KinoviBatchCount,
@@ -221,7 +221,7 @@ fn plan_bitrate(bitrate: Option<RouterBitrate>) -> Option<KinoviBitrate> {
#[cfg(test)]
mod tests {
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
KinoviSeedance2p0MiniAspectRatio as KinoviAspectRatio,
KinoviSeedance2p0MiniBatchCount as KinoviBatchCount,
KinoviSeedance2p0MiniOutputResolution as KinoviOutputResolution,
@@ -491,7 +491,7 @@ mod tests {
fn mini_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Mini,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
prompt: Some("a cat dancing".to_string()),
duration_seconds: Some(5),
video_batch_count: Some(1),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
GenerateSeedance2p0MiniRequest, KinoviSeedance2p0MiniBatchCount,
KinoviSeedance2p0MiniOutputResolution,
};
@@ -89,7 +89,7 @@ impl KinoviSeedance2p0MiniCostState {
mod tests {
use super::*;
use crate::api::router_resolution::RouterResolution;
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
KinoviSeedance2p0MiniOutputResolution as KinoviOutputResolution,
KinoviSeedance2p0MiniBatchCount as KinoviBatchCount,
};
@@ -328,7 +328,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Mini,
provider: RouterProvider::KinoviWeb,
provider: RouterProvider::Seedance2Pro,
resolution,
reference_videos,
duration_seconds: Some(duration_seconds),
@@ -1,4 +1,4 @@
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
GenerateSeedance2p0MiniRequest, KinoviSeedance2p0MiniAspectRatio, KinoviSeedance2p0MiniBitrate,
KinoviSeedance2p0MiniBatchCount, KinoviSeedance2p0MiniOutputResolution,
};
@@ -44,7 +44,7 @@ impl KinoviSeedance2p0MiniDraftState {
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviSeedance2p0MiniRequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let mut start_frame_url = None;
@@ -1,11 +1,11 @@
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
generate_seedance_2p0_mini, GenerateSeedance2p0MiniArgs, GenerateSeedance2p0MiniRequest,
};
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
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, KinoviWebVideoResponsePayload};
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p0MiniRequestState {
@@ -14,7 +14,7 @@ pub struct KinoviSeedance2p0MiniRequestState {
}
impl KinoviSeedance2p0MiniRequestState {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSeedance2p0MiniArgs {
@@ -25,9 +25,9 @@ impl KinoviSeedance2p0MiniRequestState {
let response = generate_seedance_2p0_mini(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateVideoResponse::KinoviWeb(KinoviWebVideoResponsePayload {
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -1,14 +1,14 @@
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::utils::download_file::download_file;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use url_utils::extension::extract_extension_from_url::{extract_extension_from_url_str, ExtractExtensions};
/// Downloads a file from a source URL and re-uploads it to kinovi_web CDN.
pub(crate) async fn upload_to_kinovi_web(
session: &KinoviWebSession,
/// Downloads a file from a source URL and re-uploads it to seedance2pro CDN.
pub(crate) async fn upload_to_seedance2pro(
session: &Seedance2ProSession,
source_url: &str,
) -> Result<String, ArtcraftRouterError> {
let extension = extract_extension_from_url_str(source_url, &ExtractExtensions::All)
@@ -23,7 +23,7 @@ pub(crate) async fn upload_to_kinovi_web(
host_override: None,
})
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
let upload_response = upload_file(UploadFileArgs {
upload_url: prepare_response.upload_url,
@@ -31,7 +31,7 @@ pub(crate) async fn upload_to_kinovi_web(
host_override: None,
})
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(upload_response.public_url)
}
@@ -27,10 +27,10 @@ impl VideoGenerationDraftRequest {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::KinoviHappyHorse1p0(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedance2p0(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedance2p0Fast(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedance2p0Mini(_) => RouterProvider::KinoviWeb,
Self::KinoviHappyHorse1p0(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedance2p0(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedance2p0Fast(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedance2p0Mini(_) => RouterProvider::Seedance2Pro,
}
}
@@ -1,5 +1,5 @@
use crate::client::router_client::RouterClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use std::collections::HashMap;
@@ -22,9 +22,9 @@ pub struct VideoGenerationDraftContext<'a> {
}
impl <'a> VideoGenerationDraftContext<'a> {
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ArtcraftRouterError> {
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
client.get_kinovi_web_client_ref()
client.get_seedance2pro_client_ref()
.map_err(|err| ArtcraftRouterError::Client(err))
}
@@ -256,10 +256,10 @@ impl VideoGenerationRequest {
Self::GmiCloudSeedance2p0UltraFast(_) => RouterProvider::GmiCloud,
Self::GrokApiGrokImagineVideo(_) => RouterProvider::GrokApi,
Self::GrokApiGrokImagineVideo1p5(_) => RouterProvider::GrokApi,
Self::KinoviHappyHorse1p0(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedance2p0(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedance2p0Fast(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedance2p0Mini(_) => RouterProvider::KinoviWeb,
Self::KinoviHappyHorse1p0(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedance2p0(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedance2p0Fast(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedance2p0Mini(_) => RouterProvider::Seedance2Pro,
}
}
@@ -568,19 +568,19 @@ impl VideoGenerationRequest {
request.send(client_ref).await
},
VideoGenerationRequest::KinoviHappyHorse1p0(request) => {
let client_ref = client.get_kinovi_web_client_ref()?;
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::KinoviSeedance2p0(request) => {
let client_ref = client.get_kinovi_web_client_ref()?;
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::KinoviSeedance2p0Fast(request) => {
let client_ref = client.get_kinovi_web_client_ref()?;
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::KinoviSeedance2p0Mini(request) => {
let client_ref = client.get_kinovi_web_client_ref()?;
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
}
@@ -1,19 +0,0 @@
use crate::creds::kinovi_web_cookies::KinoviWebCookies;
/// Holds the full session data needed to make authenticated requests to Seedance2 Pro.
#[derive(Clone)]
pub struct KinoviWebSession {
pub cookies: KinoviWebCookies,
}
impl KinoviWebSession {
pub fn new(cookies: KinoviWebCookies) -> Self {
KinoviWebSession { cookies }
}
pub fn from_cookies_string(cookies: String) -> Self {
KinoviWebSession {
cookies: KinoviWebCookies::new(cookies),
}
}
}
@@ -1,2 +0,0 @@
pub mod kinovi_web_cookies;
pub mod kinovi_web_session;
@@ -1,70 +0,0 @@
use crate::error::kinovi_web_bad_request_api_error::KinoviWebBadRequestApiError;
use crate::error::kinovi_web_client_error::KinoviWebClientError;
use crate::error::kinovi_web_generic_api_error::KinoviWebGenericApiError;
use crate::error::kinovi_web_specific_api_error::KinoviWebSpecificApiError;
use cloudflare_errors::cloudflare_error::CloudflareError;
use std::error::Error;
#[derive(Debug)]
pub enum KinoviWebError {
Client(KinoviWebClientError),
ApiSpecific(KinoviWebSpecificApiError),
ApiGeneric(KinoviWebGenericApiError),
ApiBadRequest(KinoviWebBadRequestApiError),
}
impl KinoviWebError {
pub fn is_having_downtime_issues(&self) -> bool {
match self {
Self::ApiGeneric(KinoviWebGenericApiError::CloudflareError(CloudflareError::BadGateway502)) => true,
Self::ApiGeneric(KinoviWebGenericApiError::CloudflareError(CloudflareError::GatewayTimeout504)) => true,
Self::ApiGeneric(KinoviWebGenericApiError::CloudflareError(CloudflareError::TimeoutOccurred524)) => true,
Self::ApiGeneric(KinoviWebGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: _ }) => {
match status_code.as_u16() {
502 => true,
504 => true,
524 => true,
_ => false,
}
},
_ => false,
}
}
}
impl Error for KinoviWebError {}
impl std::fmt::Display for KinoviWebError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Client(e) => write!(f, "KinoviWebClientError: {:?}", e),
Self::ApiSpecific(e) => write!(f, "KinoviWebSpecificApiError: {:?}", e),
Self::ApiGeneric(e) => write!(f, "KinoviWebGenericApiError: {:?}", e),
Self::ApiBadRequest(e) => write!(f, "KinoviWebBadRequestApiError: {:?}", e),
}
}
}
impl From<KinoviWebClientError> for KinoviWebError {
fn from(error: KinoviWebClientError) -> Self {
Self::Client(error)
}
}
impl From<KinoviWebSpecificApiError> for KinoviWebError {
fn from(error: KinoviWebSpecificApiError) -> Self {
Self::ApiSpecific(error)
}
}
impl From<KinoviWebGenericApiError> for KinoviWebError {
fn from(error: KinoviWebGenericApiError) -> Self {
Self::ApiGeneric(error)
}
}
impl From<KinoviWebBadRequestApiError> for KinoviWebError {
fn from(error: KinoviWebBadRequestApiError) -> Self {
Self::ApiBadRequest(error)
}
}
@@ -1,5 +0,0 @@
pub mod kinovi_web_bad_request_api_error;
pub mod kinovi_web_client_error;
pub mod kinovi_web_error;
pub mod kinovi_web_generic_api_error;
pub mod kinovi_web_specific_api_error;
@@ -1,5 +1,5 @@
[package]
name = "kinovi_web_client"
name = "seedance2pro_client"
edition = "2024"
version = "0.0.1"
publish = false
@@ -8,7 +8,7 @@ publish = false
normal = ["workspace-build-acceleration"]
[lib]
name = "kinovi_web_client"
name = "seedance2pro_client"
path = "src/lib.rs"
[dependencies]
@@ -4,10 +4,10 @@ use serde::Deserialize;
use wreq::Client;
use wreq_util::Emulation;
use crate::creds::kinovi_web_session::KinoviWebSession;
use crate::error::kinovi_web_client_error::KinoviWebClientError;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::error::kinovi_web_generic_api_error::KinoviWebGenericApiError;
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
use crate::utils::common_headers::FIREFOX_USER_AGENT;
use crate::utils::number_coercion::de_u64_int_or_float;
@@ -18,7 +18,7 @@ pub const DEFAULT_BILLING_PAGE_LIMIT: u32 = 20;
// --- Args & response ---
pub struct GetBillingPaymentsHistoryArgs<'a> {
pub session: &'a KinoviWebSession,
pub session: &'a Seedance2ProSession,
/// Page size. The web dashboard uses [`DEFAULT_BILLING_PAGE_LIMIT`].
pub limit: u32,
@@ -114,7 +114,7 @@ impl BillingPaymentType {
pub async fn get_billing_payments_history(
args: GetBillingPaymentsHistoryArgs<'_>,
) -> Result<BillingPaymentsHistoryPage, KinoviWebError> {
) -> Result<BillingPaymentsHistoryPage, Seedance2ProError> {
let host = resolve_host(args.host_override.as_ref());
let base_url = host.api_base_url();
let url = format!("{}/api/trpc/billing.getPayments", base_url);
@@ -126,7 +126,7 @@ pub async fn get_billing_payments_history(
let client = Client::builder()
.emulation(Emulation::Firefox143)
.build()
.map_err(|err| KinoviWebClientError::WreqClientError(err))?;
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
let cookie = args.session.cookies.as_str();
let referer = format!("{}/app/dashboard/billing", base_url);
@@ -148,21 +148,21 @@ pub async fn get_billing_payments_history(
.header("Priority", "u=4")
.header("TE", "trailers")
.build()
.map_err(|err| KinoviWebClientError::WreqClientError(err))?;
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
let response = client.execute(request)
.await
.map_err(|err| KinoviWebGenericApiError::WreqError(err))?;
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
let status = response.status();
let response_body = response.text()
.await
.map_err(|err| KinoviWebGenericApiError::WreqError(err))?;
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
info!("Billing payments response status: {}", status);
if !status.is_success() {
return Err(KinoviWebGenericApiError::UncategorizedBadResponseWithStatusAndBody {
return Err(Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
status_code: status,
body: response_body,
}.into());
@@ -171,14 +171,14 @@ pub async fn get_billing_payments_history(
parse_response_body(&response_body)
}
fn parse_response_body(response_body: &str) -> Result<BillingPaymentsHistoryPage, KinoviWebError> {
fn parse_response_body(response_body: &str) -> Result<BillingPaymentsHistoryPage, Seedance2ProError> {
let batch_response: Vec<BatchResponseItem> = serde_json::from_str(response_body)
.map_err(|err| KinoviWebGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
.map_err(|err| Seedance2ProGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
let json = batch_response
.into_iter()
.next()
.ok_or_else(|| KinoviWebGenericApiError::UnexpectedResponseShape {
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.to_string(),
})?
@@ -274,7 +274,7 @@ struct RawPayment {
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::kinovi_web_session::KinoviWebSession;
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;
@@ -358,7 +358,7 @@ mod tests {
/// Pages through the ENTIRE payments history with throttling and prints
/// lifetime totals. ~1,000 rows / 50 pages as of 2026-07.
/// cargo test -p kinovi_web_client live_paginate_entire_payments_history -- --ignored --nocapture
/// cargo test -p seedance2pro_client live_paginate_entire_payments_history -- --ignored --nocapture
#[tokio::test]
#[ignore] // manual test — requires real cookies; fetches every page
async fn live_paginate_entire_payments_history() -> AnyhowResult<()> {
@@ -413,9 +413,9 @@ mod tests {
Ok(())
}
fn test_session() -> AnyhowResult<KinoviWebSession> {
fn test_session() -> AnyhowResult<Seedance2ProSession> {
let cookies = get_test_cookies()?;
Ok(KinoviWebSession::from_cookies_string(cookies))
Ok(Seedance2ProSession::from_cookies_string(cookies))
}
}
}
@@ -4,10 +4,10 @@ use serde::Deserialize;
use wreq::Client;
use wreq_util::Emulation;
use crate::creds::kinovi_web_session::KinoviWebSession;
use crate::error::kinovi_web_client_error::KinoviWebClientError;
use crate::error::kinovi_web_error::KinoviWebError;
use crate::error::kinovi_web_generic_api_error::KinoviWebGenericApiError;
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
use crate::utils::common_headers::FIREFOX_USER_AGENT;
use crate::utils::number_coercion::{de_i64_int_or_float, de_u64_int_or_float};
@@ -18,7 +18,7 @@ pub const DEFAULT_CREDITS_PAGE_LIMIT: u32 = 20;
// --- Args & response ---
pub struct GetCreditsHistoryArgs<'a> {
pub session: &'a KinoviWebSession,
pub session: &'a Seedance2ProSession,
/// Page size. The web dashboard uses [`DEFAULT_CREDITS_PAGE_LIMIT`].
pub limit: u32,
@@ -106,7 +106,7 @@ impl CreditHistoryEntryType {
pub async fn get_credits_history(
args: GetCreditsHistoryArgs<'_>,
) -> Result<CreditsHistoryPage, KinoviWebError> {
) -> Result<CreditsHistoryPage, Seedance2ProError> {
let host = resolve_host(args.host_override.as_ref());
let base_url = host.api_base_url();
let url = format!("{}/api/trpc/credits.getCreditHistory", base_url);
@@ -118,7 +118,7 @@ pub async fn get_credits_history(
let client = Client::builder()
.emulation(Emulation::Firefox143)
.build()
.map_err(|err| KinoviWebClientError::WreqClientError(err))?;
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
let cookie = args.session.cookies.as_str();
let referer = format!("{}/app/dashboard/billing", base_url);
@@ -140,21 +140,21 @@ pub async fn get_credits_history(
.header("Priority", "u=4")
.header("TE", "trailers")
.build()
.map_err(|err| KinoviWebClientError::WreqClientError(err))?;
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
let response = client.execute(request)
.await
.map_err(|err| KinoviWebGenericApiError::WreqError(err))?;
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
let status = response.status();
let response_body = response.text()
.await
.map_err(|err| KinoviWebGenericApiError::WreqError(err))?;
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
info!("Credits history response status: {}", status);
if !status.is_success() {
return Err(KinoviWebGenericApiError::UncategorizedBadResponseWithStatusAndBody {
return Err(Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
status_code: status,
body: response_body,
}.into());
@@ -163,14 +163,14 @@ pub async fn get_credits_history(
parse_response_body(&response_body)
}
fn parse_response_body(response_body: &str) -> Result<CreditsHistoryPage, KinoviWebError> {
fn parse_response_body(response_body: &str) -> Result<CreditsHistoryPage, Seedance2ProError> {
let batch_response: Vec<BatchResponseItem> = serde_json::from_str(response_body)
.map_err(|err| KinoviWebGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
.map_err(|err| Seedance2ProGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
let json = batch_response
.into_iter()
.next()
.ok_or_else(|| KinoviWebGenericApiError::UnexpectedResponseShape {
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
explanation: "Empty batch response array".to_string(),
raw_body: response_body.to_string(),
})?
@@ -251,7 +251,7 @@ struct RawCreditEntry {
#[cfg(test)]
mod tests {
use super::*;
use crate::creds::kinovi_web_session::KinoviWebSession;
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;
@@ -344,7 +344,7 @@ mod tests {
/// per-type tallies (this is where any refund-like entry types will
/// surface). ~300k rows as of 2026-07 — expect a long run; the ledger
/// also grows while paging, so entries are deduped by id.
/// cargo test -p kinovi_web_client live_paginate_entire_credits_history -- --ignored --nocapture
/// cargo test -p seedance2pro_client live_paginate_entire_credits_history -- --ignored --nocapture
#[tokio::test]
#[ignore] // manual test — requires real cookies; fetches every page
async fn live_paginate_entire_credits_history() -> AnyhowResult<()> {
@@ -407,9 +407,9 @@ mod tests {
Ok(())
}
fn test_session() -> AnyhowResult<KinoviWebSession> {
fn test_session() -> AnyhowResult<Seedance2ProSession> {
let cookies = get_test_cookies()?;
Ok(KinoviWebSession::from_cookies_string(cookies))
Ok(Seedance2ProSession::from_cookies_string(cookies))
}
}
}

Some files were not shown because too many files have changed in this diff Show More