mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 17:28:24 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9ee643bf6 | ||
|
|
07e9a35044 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
}
|
||||
|
||||
@@ -74,9 +74,6 @@ pub enum RouterVideoModel {
|
||||
#[serde(rename = "seedance_2p0_bpu_mini")]
|
||||
Seedance2p0BytePlusUltraMini,
|
||||
|
||||
#[serde(rename = "seedance_2p5_preview")]
|
||||
Seedance2p5Preview,
|
||||
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
|
||||
@@ -136,11 +133,6 @@ mod tests {
|
||||
assert_serde_round_trip(RouterVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seedance_2p5_preview_serializes() {
|
||||
assert_serde_round_trip(RouterVideoModel::Seedance2p5Preview, "seedance_2p5_preview");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn veo_3p1_lite_and_vidu_variants_serialize() {
|
||||
// NB: These strings must match `CommonVideoModel` — the two enums convert
|
||||
|
||||
@@ -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> {
|
||||
|
||||
+6
-6
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
+9
-9
@@ -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(_))));
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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)]
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+3
-3
@@ -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()
|
||||
};
|
||||
|
||||
+17
-17
@@ -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),
|
||||
|
||||
+1
-1
@@ -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()])),
|
||||
|
||||
+3
-3
@@ -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()
|
||||
|
||||
+2
-2
@@ -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(|| {
|
||||
|
||||
+19
-19
@@ -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");
|
||||
|
||||
+1
-1
@@ -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()])),
|
||||
|
||||
+3
-3
@@ -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()
|
||||
|
||||
+2
-2
@@ -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(|| {
|
||||
|
||||
+19
-19
@@ -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");
|
||||
|
||||
+2
-2
@@ -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()
|
||||
}
|
||||
|
||||
+3
-3
@@ -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),
|
||||
|
||||
+17
-17
@@ -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),
|
||||
|
||||
+6
-6
@@ -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(),
|
||||
}
|
||||
|
||||
+6
-6
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -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
|
||||
}
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
|
||||
|
||||
/// Cost state for Artcraft Flux 2 LoRA Angles. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_flux_2_lora_angles`): 4¢ per output image.
|
||||
/// (`estimate_image_cost_artcraft_flux_2_lora_angles`): 4¢ per output image
|
||||
/// (Artcraft markup over Fal's 2¢/image base rate).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux2LoraAnglesCostState {
|
||||
pub num_images: u16,
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+6
-6
@@ -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() };
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+5
-5
@@ -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.
|
||||
|
||||
|
||||
+15
-15
@@ -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());
|
||||
}
|
||||
|
||||
+5
-5
@@ -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
-1
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+15
-15
@@ -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());
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
use kinovi_web_client::generate::image::generate_midjourney_v8::{
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v8::{
|
||||
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+15
-15
@@ -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());
|
||||
}
|
||||
|
||||
+5
-5
@@ -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))
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
-1
@@ -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,
|
||||
};
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+15
-15
@@ -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());
|
||||
}
|
||||
|
||||
+8
-8
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+6
-10
@@ -72,8 +72,6 @@ use crate::generate::generate_video::providers::fal::vidu_q3::build::build_fal_v
|
||||
use crate::generate::generate_video::providers::fal::vidu_q3_turbo::build::build_fal_vidu_q3_turbo;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::build::build_kinovi_seedance_2p0_mini;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::build::build_artcraft_seedance_2p5_preview;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::build::build_kinovi_seedance_2p5_preview;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
/// RouterProvider-agnostic video generation request. Distilled by `build2()` into a
|
||||
@@ -191,7 +189,6 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Mini) => build_artcraft_seedance_2p0_mini(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusMini) => build_artcraft_seedance_2p0_bp_mini(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_artcraft_seedance_2p0_bpu_mini(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p5Preview) => build_artcraft_seedance_2p5_preview(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::PreviewModel) => build_artcraft_preview_model(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Seedance10Lite) => build_artcraft_seedance_1p0_lite(self),
|
||||
@@ -233,13 +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::KinoviWeb, RouterVideoModel::Seedance2p5Preview) => build_kinovi_seedance_2p5_preview(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(),
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -14,7 +14,8 @@ use crate::generate::generate_video::video_generation_request::VideoGenerationRe
|
||||
/// Same constraints as the v1 variant — xAI v1.5 still supports only 480p
|
||||
/// and 720p and no 21:9 ultra-wide. The on-wire model identifier is
|
||||
/// `grok-imagine-video-1.5`; the cost calculator in
|
||||
/// [`super::cost`] keys off that to apply the v1.5 pricing tier.
|
||||
/// [`super::cost`] keys off that to apply the v1.5 pricing tier (with a 5%
|
||||
/// ArtCraft markup on top of the upstream rates).
|
||||
pub fn build_artcraft_grok_imagine_video_1p5(
|
||||
builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
|
||||
+6
-2
@@ -14,7 +14,10 @@ use crate::generate::generate_video::providers::artcraft::grok_imagine_video_1p5
|
||||
|
||||
// ── Markup ──
|
||||
//
|
||||
// The markup is applied at cent granularity with ceiling rounding.
|
||||
// ArtCraft adds a modest 5% markup over the underlying xAI v1.5 pricing. The
|
||||
// markup is applied at cent granularity with ceiling rounding so the final
|
||||
// charge always covers the upstream cost (we never bill the user less than
|
||||
// the cost of the API call we forwarded).
|
||||
const MARKUP_NUMERATOR: u64 = 105;
|
||||
const MARKUP_DENOMINATOR: u64 = 100;
|
||||
|
||||
@@ -66,7 +69,8 @@ impl ArtcraftGrokImagineVideo1p5CostState {
|
||||
// composing. The image requirement is enforced at send time (request.rs)
|
||||
// and by the generate endpoints.
|
||||
let base = self.base_cost_in_cents_for_batch();
|
||||
// Ceil at cent granularity.
|
||||
// 5% markup, ceil so the user is always charged enough to cover the
|
||||
// upstream grok cost regardless of rounding.
|
||||
let usd_cents = base.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
|
||||
|
||||
Ok(VideoGenerationCostEstimate {
|
||||
|
||||
-1
@@ -29,7 +29,6 @@ pub mod seedance_2p0_bpu_fast;
|
||||
pub mod seedance_2p0_mini;
|
||||
pub mod seedance_2p0_bp_mini;
|
||||
pub mod seedance_2p0_bpu_mini;
|
||||
pub mod seedance_2p5_preview;
|
||||
pub mod sora_2;
|
||||
pub mod sora_2_pro;
|
||||
pub mod veo_2;
|
||||
|
||||
+3
-2
@@ -25,7 +25,7 @@ impl ArtcraftSeedance2p0BytePlusMiniCostState {
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// BytePlus / BytePlus Ultra Mini share a rate set; see
|
||||
// BytePlus / BytePlus Ultra Mini share a higher-margin rate set; see
|
||||
// seedance_common::seedance_2p0_byteplus_mini_usd_cents.
|
||||
let usd_cents = seedance_2p0_byteplus_mini_usd_cents(
|
||||
self.resolution,
|
||||
@@ -58,7 +58,8 @@ mod tests {
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// Every combination, driven through the full build pipeline.
|
||||
// Every combination, driven through the full build pipeline. These prices are
|
||||
// the supplier's per-combo cost plus a 5% margin, rounded up to whole cents.
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
+3
-2
@@ -25,7 +25,7 @@ impl ArtcraftSeedance2p0BytePlusUltraMiniCostState {
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// BytePlus / BytePlus Ultra Mini share a rate set; see
|
||||
// BytePlus / BytePlus Ultra Mini share a higher-margin rate set; see
|
||||
// seedance_common::seedance_2p0_byteplus_mini_usd_cents.
|
||||
let usd_cents = seedance_2p0_byteplus_mini_usd_cents(
|
||||
self.resolution,
|
||||
@@ -58,7 +58,8 @@ mod tests {
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// Every combination, driven through the full build pipeline.
|
||||
// Every combination, driven through the full build pipeline. These prices are
|
||||
// the supplier's per-combo cost plus a 5% margin, rounded up to whole cents.
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
+2
-1
@@ -58,7 +58,8 @@ mod tests {
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// Every combination, driven through the full build pipeline.
|
||||
// Every combination, driven through the full build pipeline. These prices are
|
||||
// the supplier's per-combo cost plus a 5% margin, rounded up to whole cents.
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
-250
@@ -1,250 +0,0 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::request::ArtcraftSeedance2p5PreviewRequestState;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p5_preview(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Parity with the Kinovi build: 2.5 Preview only operates in reference mode
|
||||
// (no keyframe start/end frames) and has no character support. Erroring
|
||||
// here keeps the billing estimate consistent with the execution build.
|
||||
if builder.start_frame.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame",
|
||||
value: "Seedance 2.5 Preview has no keyframe mode; use reference_images".to_string(),
|
||||
}));
|
||||
}
|
||||
if builder.end_frame.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "end_frame",
|
||||
value: "Seedance 2.5 Preview has no keyframe mode; use reference_images".to_string(),
|
||||
}));
|
||||
}
|
||||
if builder.reference_character_tokens.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_character_tokens",
|
||||
value: "Seedance 2.5 Preview does not support character references".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
// 2.5 Preview allows 4-30 second durations (longer than the shared 4-15
|
||||
// range in build_common) and generates a single video per request. Plan
|
||||
// both here, leaving None for the shared builder, then set them after.
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
|
||||
let mut request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p5Preview,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
request.duration_seconds = duration_seconds;
|
||||
request.video_batch_count = Some(1);
|
||||
|
||||
let state = ArtcraftSeedance2p5PreviewRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Preview(state)))
|
||||
}
|
||||
|
||||
// Seedance 2.5 Preview supports durations of 4-30 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u16>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 30;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(d)),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(d.clamp(MIN, MAX))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.5 Preview generates a single video per request (no batching).
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<(), ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(()),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_correct() {
|
||||
let req = unwrap_request(builder_with(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p5Preview)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_30_is_passed_through() {
|
||||
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(30); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max_30() {
|
||||
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_min_4() {
|
||||
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(2); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_is_always_one() {
|
||||
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_over_one_error_out() {
|
||||
let result = build_artcraft_seedance_2p5_preview(builder_with(|b| {
|
||||
b.video_batch_count = Some(4);
|
||||
b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
|
||||
}));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod unsupported_option_errors {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_errors() {
|
||||
let result = build_artcraft_seedance_2p5_preview(builder_with(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string())));
|
||||
}));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_frame_errors() {
|
||||
let result = build_artcraft_seedance_2p5_preview(builder_with(|b| {
|
||||
b.end_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_end".to_string())));
|
||||
}));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_references_error() {
|
||||
let result = build_artcraft_seedance_2p5_preview(builder_with(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(vec![
|
||||
CharacterToken::new("char_abc".to_string()),
|
||||
]));
|
||||
}));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
}
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
MediaFileToken::new("mf_a".to_string()),
|
||||
MediaFileToken::new("mf_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(builder_with(|b| { b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone())); }));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
fn builder_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p5Preview,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p5PreviewRequestState {
|
||||
let result = build_artcraft_seedance_2p5_preview(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Preview(state)) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p5Preview request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p5_preview_usd_cents;
|
||||
use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::request::ArtcraftSeedance2p5PreviewRequestState;
|
||||
|
||||
/// Only the resolution and duration matter — 2.5 Preview has no
|
||||
/// video-reference surcharge and no batching.
|
||||
pub struct ArtcraftSeedance2p5PreviewCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p5PreviewCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p5PreviewRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
|
||||
Self { resolution, duration_seconds }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let usd_cents = seedance_2p5_preview_usd_cents(
|
||||
self.resolution,
|
||||
self.duration_seconds,
|
||||
);
|
||||
|
||||
// ArtCraft credits: 100 credits = $1.00, so credits = cents.
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn full_duration_table() {
|
||||
assert_eq!(cents(Some(RouterResolution::FourEightyP), 4), 82);
|
||||
assert_eq!(cents(Some(RouterResolution::FourEightyP), 10), 205);
|
||||
assert_eq!(cents(Some(RouterResolution::FourEightyP), 15), 307);
|
||||
assert_eq!(cents(Some(RouterResolution::FourEightyP), 20), 409);
|
||||
assert_eq!(cents(Some(RouterResolution::FourEightyP), 25), 511);
|
||||
assert_eq!(cents(Some(RouterResolution::FourEightyP), 30), 613);
|
||||
}
|
||||
}
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn full_duration_table() {
|
||||
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 4), 164);
|
||||
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 10), 409);
|
||||
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 15), 613);
|
||||
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 20), 817);
|
||||
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 25), 1021);
|
||||
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 30), 1226);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(cents(None, 10), cents(Some(RouterResolution::SevenTwentyP), 10));
|
||||
}
|
||||
}
|
||||
|
||||
mod reference_invariance_tests {
|
||||
use super::*;
|
||||
|
||||
/// References never affect 2.5 Preview pricing.
|
||||
#[test]
|
||||
fn video_reference_does_not_change_price() {
|
||||
let without = cents(Some(RouterResolution::SevenTwentyP), 10);
|
||||
let estimate = estimate_with(|b| {
|
||||
b.resolution = Some(RouterResolution::SevenTwentyP);
|
||||
b.duration_seconds = Some(10);
|
||||
b.reference_videos = Some(VideoListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_ref".to_string()),
|
||||
]));
|
||||
});
|
||||
assert_eq!(estimate.cost_in_usd_cents, Some(without));
|
||||
}
|
||||
}
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents() {
|
||||
for dur in [4u16, 10, 15, 20, 25, 30] {
|
||||
let estimate = estimate_with(|b| {
|
||||
b.resolution = Some(RouterResolution::FourEightyP);
|
||||
b.duration_seconds = Some(dur);
|
||||
});
|
||||
assert_eq!(estimate.cost_in_credits, estimate.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn estimate_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> VideoGenerationCostEstimate {
|
||||
let mut builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p5Preview,
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut builder);
|
||||
builder.build2()
|
||||
.expect("build should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate should succeed")
|
||||
}
|
||||
|
||||
fn cents(resolution: Option<RouterResolution>, duration_seconds: u16) -> u64 {
|
||||
estimate_with(|b| {
|
||||
b.resolution = resolution;
|
||||
b.duration_seconds = Some(duration_seconds);
|
||||
}).cost_in_usd_cents.unwrap()
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p5PreviewRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p5PreviewRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_video_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+15
-42
@@ -15,7 +15,7 @@ const FOUR_K_CENTI_CENTS_PER_SECOND: u64 = 8660;
|
||||
/// at 4K (17.20 ¢/s surcharge).
|
||||
const FOUR_K_VIDEO_REFERENCE_SURCHARGE_CENTI_CENTS_PER_SECOND: u64 = 1720;
|
||||
|
||||
/// ArtCraft's price (USD cents) for Seedance 2.0 at 4K.
|
||||
/// ArtCraft's user-facing price (USD cents) for Seedance 2.0 at 4K.
|
||||
///
|
||||
/// A reference video adds a per-second surcharge (4K with and without a video
|
||||
/// reference are priced differently). The cost scales with duration and batch
|
||||
@@ -39,13 +39,13 @@ pub fn seedance_2p0_four_k_usd_cents(
|
||||
|
||||
// ── Seedance 2.0 Mini pricing ──
|
||||
//
|
||||
// Mini offers only 480p and 720p. The rates below are the price in USD cents
|
||||
// per second; a reference video adds a per-second surcharge. Rates are
|
||||
// fractional, so the total is rounded UP to a whole cent once at the end
|
||||
// (after multiplying by duration × batch).
|
||||
// Mini offers only 480p and 720p. The rates below are the per-second USER price
|
||||
// in USD cents (margin already included); a reference video adds a per-second
|
||||
// surcharge. Rates are fractional, so the total is rounded UP to a whole cent
|
||||
// once at the end (after multiplying by duration × batch).
|
||||
//
|
||||
// The regular Mini and the BytePlus / BytePlus Ultra Minis have separate
|
||||
// rate sets and helpers below.
|
||||
// The regular Mini and the BytePlus / BytePlus Ultra Minis carry DIFFERENT
|
||||
// margins, so they have separate rate sets and helpers below.
|
||||
|
||||
/// Regular Mini — 480p price, USD cents per second.
|
||||
const MINI_CENTS_PER_SECOND_480P: f64 = 3.24074074;
|
||||
@@ -65,7 +65,7 @@ const BYTEPLUS_MINI_CENTS_PER_SECOND_720P: f64 = 8.72427984;
|
||||
/// BytePlus / BytePlus Ultra Mini — 720p reference-video surcharge, USD cents per second.
|
||||
const BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P: f64 = 1.74485597;
|
||||
|
||||
/// ArtCraft's price (USD cents) for the regular Seedance 2.0 Mini.
|
||||
/// ArtCraft's user-facing price (USD cents) for the regular Seedance 2.0 Mini.
|
||||
///
|
||||
/// Only 480p and 720p are offered; any other resolution prices at 720p. A
|
||||
/// reference video adds the per-second surcharge. The fractional total is
|
||||
@@ -88,8 +88,9 @@ pub fn seedance_2p0_mini_usd_cents(
|
||||
)
|
||||
}
|
||||
|
||||
/// ArtCraft's price (USD cents) for the Seedance 2.0 BytePlus Mini and
|
||||
/// BytePlus Ultra Mini (which share the same rates).
|
||||
/// ArtCraft's user-facing price (USD cents) for the Seedance 2.0 BytePlus Mini
|
||||
/// and BytePlus Ultra Mini (which share the same rates — a higher margin than
|
||||
/// the regular Mini).
|
||||
///
|
||||
/// Only 480p and 720p are offered; any other resolution prices at 720p. A
|
||||
/// reference video adds the per-second surcharge. The fractional total is
|
||||
@@ -112,35 +113,6 @@ pub fn seedance_2p0_byteplus_mini_usd_cents(
|
||||
)
|
||||
}
|
||||
|
||||
// ── Seedance 2.5 Preview pricing ──
|
||||
//
|
||||
// 2.5 Preview offers only 480p and 720p, generates one video per request (no
|
||||
// batching), and — unlike the 2.0 family — references of any kind do NOT
|
||||
// change the price.
|
||||
|
||||
/// Seedance 2.5 Preview — 480p price, USD cents per second.
|
||||
const SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_480P: f64 = 20.41909465;
|
||||
/// Seedance 2.5 Preview — 720p price, USD cents per second.
|
||||
const SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_720P: f64 = 40.8381893;
|
||||
|
||||
/// ArtCraft's price (USD cents) for Seedance 2.5 Preview.
|
||||
///
|
||||
/// Only 480p and 720p are offered; any other resolution prices at 720p.
|
||||
/// References never affect the price and there is no batching. The fractional
|
||||
/// total is rounded UP to a whole cent.
|
||||
pub fn seedance_2p5_preview_usd_cents(
|
||||
resolution: CommonResolution,
|
||||
duration_seconds: u16,
|
||||
) -> u64 {
|
||||
let cents_per_second = match resolution {
|
||||
CommonResolution::FourEightyP => SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_480P,
|
||||
// Everything else (including 720p and unsupported resolutions) prices at 720p.
|
||||
_ => SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_720P,
|
||||
};
|
||||
|
||||
(cents_per_second * duration_seconds as f64).ceil() as u64
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn mini_usd_cents(
|
||||
resolution: CommonResolution,
|
||||
@@ -207,7 +179,8 @@ mod tests {
|
||||
// ── Seedance 2.0 Mini ──
|
||||
//
|
||||
// Every combination (480p/720p × with/without video reference × 4/5/10/15s)
|
||||
// at batch 1, rounded up to whole cents.
|
||||
// at batch 1. These are the supplier's per-combo cost plus a 5% margin,
|
||||
// rounded up to whole cents.
|
||||
|
||||
mod mini {
|
||||
use super::*;
|
||||
@@ -280,8 +253,8 @@ mod tests {
|
||||
|
||||
// ── Seedance 2.0 BytePlus / BytePlus Ultra Mini ──
|
||||
//
|
||||
// Same combinations as the regular Mini; the prices are equal-or-higher
|
||||
// than the regular Mini.
|
||||
// Same combinations as the regular Mini, but at a higher margin (6% vs 5%),
|
||||
// so the prices are equal-or-higher than the regular Mini.
|
||||
|
||||
mod byteplus_mini {
|
||||
use super::*;
|
||||
|
||||
+2
-2
@@ -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),
|
||||
|
||||
+2
-2
@@ -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),
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+25
-25
@@ -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
@@ -2,6 +2,5 @@ pub mod happy_horse_1p0;
|
||||
pub mod seedance_2p0;
|
||||
pub mod seedance_2p0_fast;
|
||||
pub mod seedance_2p0_mini;
|
||||
pub mod seedance_2p5_preview;
|
||||
pub(crate) mod resolve;
|
||||
pub(crate) mod upload;
|
||||
|
||||
+8
-8
@@ -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))
|
||||
}
|
||||
|
||||
+36
-36
@@ -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),
|
||||
|
||||
+7
-7
@@ -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),
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+6
-6
@@ -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,
|
||||
|
||||
+37
-37
@@ -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),
|
||||
|
||||
+3
-3
@@ -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),
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+6
-6
@@ -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,
|
||||
|
||||
+3
-3
@@ -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),
|
||||
|
||||
+3
-3
@@ -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),
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+6
-6
@@ -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,
|
||||
|
||||
-498
@@ -1,498 +0,0 @@
|
||||
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::{
|
||||
KinoviSeedance2p5PreviewAspectRatio as KinoviAspectRatio,
|
||||
KinoviSeedance2p5PreviewOutputResolution as KinoviOutputResolution,
|
||||
};
|
||||
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::draft::{KinoviSeedance2p5PreviewDraftState, KinoviSeedance2p5PreviewRemainingItems};
|
||||
use crate::generate::generate_video::video_generation_draft::VideoGenerationDraftRequest;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
pub fn build_kinovi_seedance_2p5_preview(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let draft = do_build_kinovi_seedance_2p5_preview(builder)?;
|
||||
Ok(VideoGenerationDraftOrRequest::Draft(VideoGenerationDraftRequest::KinoviSeedance2p5Preview(draft)))
|
||||
}
|
||||
|
||||
fn do_build_kinovi_seedance_2p5_preview(mut builder: GenerateVideoRequestBuilder) -> Result<KinoviSeedance2p5PreviewDraftState, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Seedance 2.5 Preview only operates in reference mode — there is no
|
||||
// keyframe (start/end frame) mode. Dropping a caller's frames would change
|
||||
// the meaning of the request, so this errors regardless of strategy.
|
||||
if builder.start_frame.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "start_frame",
|
||||
value: "Seedance 2.5 Preview has no keyframe mode; use reference_images".to_string(),
|
||||
}));
|
||||
}
|
||||
if builder.end_frame.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "end_frame",
|
||||
value: "Seedance 2.5 Preview has no keyframe mode; use reference_images".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Kinovi character references are not supported by the 2.5 Preview API.
|
||||
if builder.reference_character_tokens.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "reference_character_tokens",
|
||||
value: "Seedance 2.5 Preview does not support character references".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
|
||||
plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let prompt = builder.prompt.take().unwrap_or_default();
|
||||
|
||||
// NB: `builder.bitrate` is intentionally ignored — 2.5 Preview has no
|
||||
// bitrate option (and bitrate never affects cost).
|
||||
|
||||
let unhandled_request_state = KinoviSeedance2p5PreviewRemainingItems {
|
||||
reference_images: builder.reference_images.take(),
|
||||
reference_videos: builder.reference_videos.take(),
|
||||
reference_audio: builder.reference_audio.take(),
|
||||
};
|
||||
|
||||
Ok(KinoviSeedance2p5PreviewDraftState {
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration_seconds,
|
||||
prompt,
|
||||
unhandled_request_state: Some(unhandled_request_state),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Plan helpers ──
|
||||
|
||||
// Seedance 2.5 Preview supports all six aspect ratios:
|
||||
// 16:9, 21:9, 9:16, 1:1, 4:3, 3:4. All supported ratios cost the same, so
|
||||
// both upgrade and downgrade pick the nearest match.
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<RouterAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<KinoviAspectRatio, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
// No preference or auto — default to landscape
|
||||
None
|
||||
| Some(RouterAspectRatio::Auto)
|
||||
| Some(RouterAspectRatio::Auto2k)
|
||||
| Some(RouterAspectRatio::Auto4k) => Ok(KinoviAspectRatio::Landscape16x9),
|
||||
|
||||
// Direct mappings
|
||||
Some(RouterAspectRatio::WideSixteenByNine) | Some(RouterAspectRatio::Wide) => {
|
||||
Ok(KinoviAspectRatio::Landscape16x9)
|
||||
}
|
||||
Some(RouterAspectRatio::TallNineBySixteen) | Some(RouterAspectRatio::Tall) => {
|
||||
Ok(KinoviAspectRatio::Portrait9x16)
|
||||
}
|
||||
Some(RouterAspectRatio::Square) | Some(RouterAspectRatio::SquareHd) => {
|
||||
Ok(KinoviAspectRatio::Square1x1)
|
||||
}
|
||||
Some(RouterAspectRatio::WideFourByThree) => Ok(KinoviAspectRatio::Standard4x3),
|
||||
Some(RouterAspectRatio::TallThreeByFour) => Ok(KinoviAspectRatio::Portrait3x4),
|
||||
Some(RouterAspectRatio::WideTwentyOneByNine) => Ok(KinoviAspectRatio::UltraWide21x9),
|
||||
|
||||
// Mismatches — apply strategy
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(nearest_aspect_ratio(unsupported))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the nearest supported aspect ratio by AR value (width / height).
|
||||
fn nearest_aspect_ratio(aspect_ratio: RouterAspectRatio) -> KinoviAspectRatio {
|
||||
match aspect_ratio {
|
||||
RouterAspectRatio::WideFiveByFour => KinoviAspectRatio::Standard4x3, // 1.25, nearest 1.33
|
||||
RouterAspectRatio::WideThreeByTwo => KinoviAspectRatio::Standard4x3, // 1.50, nearest 1.33
|
||||
RouterAspectRatio::TallFourByFive => KinoviAspectRatio::Portrait3x4, // 0.80, nearest 0.75
|
||||
RouterAspectRatio::TallTwoByThree => KinoviAspectRatio::Portrait3x4, // 0.67, nearest 0.75
|
||||
RouterAspectRatio::TallNineByTwentyOne => KinoviAspectRatio::Portrait9x16, // 0.43, nearest 0.56
|
||||
_ => KinoviAspectRatio::Square1x1,
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.5 Preview supports output resolutions: 480p and 720p only.
|
||||
// 1080p (and higher) is NOT supported — downgrade to 720p or error based on strategy.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<RouterResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<KinoviOutputResolution>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
// Direct mappings
|
||||
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
|
||||
Some(RouterResolution::SevenTwentyP) => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
|
||||
|
||||
// 1080p is not supported — handle via strategy
|
||||
Some(RouterResolution::TenEightyP) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", RouterResolution::TenEightyP),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
// 1080p not available — downgrade to 720p (highest supported)
|
||||
Ok(Some(KinoviOutputResolution::SevenTwentyP))
|
||||
}
|
||||
},
|
||||
|
||||
// Other unsupported resolutions
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(match unsupported {
|
||||
RouterResolution::HalfK => KinoviOutputResolution::FourEightyP,
|
||||
_ => KinoviOutputResolution::SevenTwentyP,
|
||||
}))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.5 Preview generates a single video per request (no batching).
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<(), ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(()),
|
||||
// Over the maximum of 1.
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.5 Preview supports durations of 4–30 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u8, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 30;
|
||||
const DEFAULT: u8 = 5;
|
||||
match duration_seconds {
|
||||
None => Ok(DEFAULT),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(d as u8),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(d.clamp(MIN, MAX) as u8),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::{
|
||||
KinoviSeedance2p5PreviewAspectRatio as KinoviAspectRatio,
|
||||
KinoviSeedance2p5PreviewOutputResolution as KinoviOutputResolution,
|
||||
};
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::draft::KinoviSeedance2p5PreviewDraftState;
|
||||
use crate::generate::generate_video::video_generation_draft::VideoGenerationDraftRequest;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod materialized_field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(preview_builder()));
|
||||
assert_eq!(draft.prompt, "a cat dancing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_defaults_to_empty() {
|
||||
let builder = GenerateVideoRequestBuilder { prompt: None, ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert_eq!(draft.prompt, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_seconds_converted() {
|
||||
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(30), ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert_eq!(draft.duration_seconds, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_defaults_to_5() {
|
||||
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert_eq!(draft.duration_seconds, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max_30() {
|
||||
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(99), ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert_eq!(draft.duration_seconds, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_min_4() {
|
||||
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(2), ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert_eq!(draft.duration_seconds, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_out_of_range_error_out() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
duration_seconds: Some(99),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..preview_builder()
|
||||
};
|
||||
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod unsupported_option_errors {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_errors() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
|
||||
..preview_builder()
|
||||
};
|
||||
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_frame_errors() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
|
||||
..preview_builder()
|
||||
};
|
||||
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_references_error() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
|
||||
CharacterToken::new("char_abc".to_string()),
|
||||
])),
|
||||
..preview_builder()
|
||||
};
|
||||
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod batch_count_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_one_is_accepted() {
|
||||
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..preview_builder() };
|
||||
assert!(build_kinovi_seedance_2p5_preview(builder).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_over_one_downgrades_to_single() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
video_batch_count: Some(4),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..preview_builder()
|
||||
};
|
||||
assert!(build_kinovi_seedance_2p5_preview(builder).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_over_one_error_out() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
video_batch_count: Some(4),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..preview_builder()
|
||||
};
|
||||
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_zero_errors() {
|
||||
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(0), ..preview_builder() };
|
||||
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
mod aspect_ratio_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn direct_mappings() {
|
||||
let expected = [
|
||||
(RouterAspectRatio::WideSixteenByNine, KinoviAspectRatio::Landscape16x9),
|
||||
(RouterAspectRatio::WideTwentyOneByNine, KinoviAspectRatio::UltraWide21x9),
|
||||
(RouterAspectRatio::TallNineBySixteen, KinoviAspectRatio::Portrait9x16),
|
||||
(RouterAspectRatio::Square, KinoviAspectRatio::Square1x1),
|
||||
(RouterAspectRatio::WideFourByThree, KinoviAspectRatio::Standard4x3),
|
||||
(RouterAspectRatio::TallThreeByFour, KinoviAspectRatio::Portrait3x4),
|
||||
];
|
||||
for (input, output) in expected {
|
||||
let builder = GenerateVideoRequestBuilder { aspect_ratio: Some(input), ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert!(
|
||||
std::mem::discriminant(&draft.aspect_ratio) == std::mem::discriminant(&output),
|
||||
"aspect ratio {input:?} mapped wrong",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_defaults_to_landscape() {
|
||||
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolution_480p() {
|
||||
let builder = GenerateVideoRequestBuilder { resolution: Some(RouterResolution::FourEightyP), ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_720p() {
|
||||
let builder = GenerateVideoRequestBuilder { resolution: Some(RouterResolution::SevenTwentyP), ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_1080p_downgrades_to_720p() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..preview_builder()
|
||||
};
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_1080p_error_out() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
resolution: Some(RouterResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..preview_builder()
|
||||
};
|
||||
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_none() {
|
||||
let builder = GenerateVideoRequestBuilder { resolution: None, ..preview_builder() };
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
assert!(draft.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod unhandled_request_state {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn media_refs_placed_in_unhandled() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/ref.jpg".to_string()])),
|
||||
reference_videos: Some(VideoListRef::Urls(vec!["https://example.com/vid.mp4".to_string()])),
|
||||
reference_audio: Some(AudioListRef::Urls(vec!["https://example.com/audio.mp3".to_string()])),
|
||||
..preview_builder()
|
||||
};
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
|
||||
let remaining = draft.unhandled_request_state.unwrap();
|
||||
assert!(remaining.reference_images.is_some());
|
||||
assert!(remaining.reference_videos.is_some());
|
||||
assert!(remaining.reference_audio.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_refs_are_none_in_unhandled() {
|
||||
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(preview_builder()));
|
||||
let remaining = draft.unhandled_request_state.unwrap();
|
||||
assert!(remaining.reference_images.is_none());
|
||||
assert!(remaining.reference_videos.is_none());
|
||||
assert!(remaining.reference_audio.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn preview_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::Seedance2p5Preview,
|
||||
provider: RouterProvider::KinoviWeb,
|
||||
prompt: Some("a cat dancing".to_string()),
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_draft(result: Result<VideoGenerationDraftOrRequest, ArtcraftRouterError>) -> KinoviSeedance2p5PreviewDraftState {
|
||||
match result.expect("build should succeed") {
|
||||
VideoGenerationDraftOrRequest::Draft(
|
||||
VideoGenerationDraftRequest::KinoviSeedance2p5Preview(draft)
|
||||
) => draft,
|
||||
_ => panic!("expected KinoviSeedance2p5Preview draft"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user