mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
ArtCraft Desktop (fresh start)
This is a fresh start for the `artcraft` repo. The old monorepo, which included our web API gateway, job services, SRE tools, various frontends, and more has moved to `artcraft-services` You can find the full git history here: https://github.com/storytold/artcraft-services This change was motivated by the fact that the entire git history was hundreds of gigabytes in size, which is unwieldy for anyone not in the core team that doesn't care about support services outside of the core artcraft app. We wanted to keep this repo in its well-known place since we already have users and activity here. Please update your remotes to either `artcraft-services` or make a fresh clone of this repo. History can be found at the artcraft-services repo.
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
# API Conventions
|
||||||
|
|
||||||
|
## HTTP Handlers (Actix-Web)
|
||||||
|
|
||||||
|
Handlers in `storyteller_web` follow this structure:
|
||||||
|
|
||||||
|
1. **Request/Response types** with `#[derive(Deserialize/Serialize, ToSchema)]`
|
||||||
|
2. **Error enum** implementing `ResponseError`, `Display`, `Serialize`, `ToSchema`
|
||||||
|
3. **Handler function** annotated with `#[utoipa::path(...)]` for OpenAPI docs
|
||||||
|
4. Handler returns `Result<Json<Response>, ErrorType>`. Only use `Result<HttpResponse, ErrorType>` when you need to set headers/cookies.
|
||||||
|
|
||||||
|
### Error Types
|
||||||
|
|
||||||
|
- Use `CommonWebError` for simple handlers that only need standard HTTP error codes
|
||||||
|
- Use a custom error enum when you need endpoint-specific error variants (e.g. `LoginErrorType::InvalidCredentials`)
|
||||||
|
- Custom error enums MUST implement `ResponseError` (actix), `Display`, and `Serialize`
|
||||||
|
- When adding `From<RequireUserSessionError>` or similar conversions, implement them on `CommonWebError` in its own module (orphan rule)
|
||||||
|
- Error response bodies include: `success: false`, `error_code`, `error_code_str`
|
||||||
|
|
||||||
|
### Session/Auth
|
||||||
|
|
||||||
|
Helpers live in `storyteller_web` under `http_server/web_utils/user_session/`. Each takes a
|
||||||
|
generic `mysql_executor: E` (any `sqlx::Executor`), so whether it reuses a connection or acquires
|
||||||
|
its own depends on the **argument you pass**:
|
||||||
|
|
||||||
|
- `require_user_session(http_request, session_checker, mysql_executor)` — require a logged-in user.
|
||||||
|
- `require_user_session_extended(...)` — same, with extra session detail.
|
||||||
|
- `require_moderator(http_request, session_checker, mysql_executor)` — admin-only endpoints.
|
||||||
|
|
||||||
|
PREFER passing an already-open connection (`&mut *conn`) so the helper reuses it. Passing
|
||||||
|
`&server_state.mysql_pool` makes it acquire a fresh connection — avoid that when the handler
|
||||||
|
already holds one (the double-acquire adds pool pressure; this crate has had pool-timeout
|
||||||
|
incidents). There is no `*_using_connection` variant — the reuse/acquire choice is the argument.
|
||||||
|
|
||||||
|
### API Type Definitions
|
||||||
|
|
||||||
|
- Shared request/response types go in `artcraft_api_defs` crate (under `crates/api_clients/artcraft/artcraft_api_defs/`)
|
||||||
|
- Types that need `ResponseError` impl must stay in `storyteller_web` (orphan rule)
|
||||||
|
- New types must be added to `api_doc.rs` schemas (in storyteller-web) for Swagger generation
|
||||||
|
|
||||||
|
## Enums (Database-backed)
|
||||||
|
|
||||||
|
- Located in `crates/schema/public/enums/src/by_table/{table_name}/`
|
||||||
|
- Require manual `to_str()` and `from_str()` implementations
|
||||||
|
- Use `impl_enum_display_and_debug_using_to_str!` and `impl_mysql_enum_coders!` macros
|
||||||
|
- Add comprehensive tests: serialization, round-trip, variant count
|
||||||
|
- YOU CAN ADD NEW VALUES but DO NOT CHANGE EXISTING VALUES without a migration strategy
|
||||||
|
- Note that adding new values can break older versions of the Tauri desktop app that read
|
||||||
|
responses over Rust - make sure it consumes variants with an "Unknown(String)" variant
|
||||||
|
catch-all to future proof the APIs.
|
||||||
|
|
||||||
|
## Tokens (Primary Keys)
|
||||||
|
|
||||||
|
- Located in `crates/schema/public/tokens/src/tokens/`
|
||||||
|
- Opaque `String` wrappers with Stripe-like prefixes (e.g. `user_`, `mf_`)
|
||||||
|
- Use `impl_string_token!`, `impl_mysql_token_from_row!`, `impl_crockford_generator!` macros
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Code Style
|
||||||
|
|
||||||
|
## Rust
|
||||||
|
|
||||||
|
- Two spaces for indentation (not four)
|
||||||
|
- No minimum supported Rust version; use latest stable features freely
|
||||||
|
- `#[macro_use] extern crate serde_derive;` is used in binary crates; library crates use `use serde_derive::{Serialize, Deserialize};`
|
||||||
|
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros (`info!`, `warn!`, `error!`, `debug!`, `trace!`) instead
|
||||||
|
- Use `anyhow::Result` / `AnyhowResult` for fallible functions in application code; new database queries should use `Result<T, sqlx::Error>` instead
|
||||||
|
- SQLx: MySQL queries belong in the `mysql_queries` crate. Do NOT write `sqlx::query!` /
|
||||||
|
`sqlx::query()` in application/service crates (e.g. `storyteller-web`, which has zero direct
|
||||||
|
SQLx queries) — add or reuse a function in `mysql_queries` and call it. The compile-time
|
||||||
|
`query!` / `query_as!` macro conventions for that crate are documented in
|
||||||
|
`crates/schema/database/mysql_queries/CLAUDE.md`.
|
||||||
|
- Enum Display/Debug: use `impl_enum_display_and_debug_using_to_str!` macro, not `derive_more::Display` (CLion doesn't understand it)
|
||||||
|
- Fields that are optional use the `maybe_` prefix: `maybe_creator_user_token`, `maybe_prompt_token`
|
||||||
|
- When implementing `fmt::Display` for error types, use the pattern: `write!(f, "{:?}", self)`
|
||||||
|
|
||||||
|
## File Layout (Top-to-Bottom Reading Order)
|
||||||
|
|
||||||
|
Organize code so a human reading top-to-bottom encounters the most important things first and
|
||||||
|
can drill down into details only when curious. Think of it like a newspaper: headline first,
|
||||||
|
then the story, then the fine print.
|
||||||
|
|
||||||
|
### Ordering within a file
|
||||||
|
|
||||||
|
1. **Constants** at the top, right after imports.
|
||||||
|
2. **The primary public type** (the main struct/enum the file is about) comes next.
|
||||||
|
A reader opening this file should immediately see what it defines.
|
||||||
|
3. **Supporting/subordinate types** (sub-structs, helper enums, serialization DTOs) go
|
||||||
|
below the primary type, not above it. Ask: "Would a reader need to know this type
|
||||||
|
exists before seeing the main type?" — if no, push it down.
|
||||||
|
4. **Type ordering for API types**: Request, then Response (success), then Error.
|
||||||
|
5. **`impl` blocks** after their type definitions.
|
||||||
|
|
||||||
|
### Ordering within an `impl` block
|
||||||
|
|
||||||
|
1. **Constructors** (`new`, `from_*`, `builder`) at the top.
|
||||||
|
2. **Public methods** next — these are the main entry points a reader cares about.
|
||||||
|
3. **Private helper methods** below public methods.
|
||||||
|
- Among helpers: place "meaty" helpers (substantial logic, direct continuations of the
|
||||||
|
public entry point) above "leaf" helpers (small formatters, simple lookups).
|
||||||
|
- A method that calls another method in the same file should appear *above* the method
|
||||||
|
it calls. This lets readers follow the call chain downward.
|
||||||
|
|
||||||
|
### Ordering within test modules
|
||||||
|
|
||||||
|
Put test cases first, helpers last. A reader opening a test file should immediately see
|
||||||
|
*what* is being tested before *how* the test machinery works.
|
||||||
|
|
||||||
|
1. **Imports**
|
||||||
|
2. **Constants** (test IDs, URLs, fixture data) — small context that tests reference
|
||||||
|
3. **Test functions** or **sub-modules** grouped by category
|
||||||
|
4. **Helper functions** (builders, pipeline runners, assertions)
|
||||||
|
|
||||||
|
Group related tests into sub-modules (`mod pricing_tests { ... }`) when a category has 2+
|
||||||
|
tests. Don't create a sub-module for a single test. Keep nesting to two levels max
|
||||||
|
(`mod tests { mod category { ... } }`). Shared helpers go in the parent module so
|
||||||
|
sub-modules can reach them via `use super::*`.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
- Handler functions: `{verb}_{noun}_handler` (e.g. `get_health_check_handler`, `login_handler`)
|
||||||
|
- Error enums: `{Feature}Error` (e.g. `FalWebhookError`, `HealthCheckError`)
|
||||||
|
- Request/response structs: `{Action}Request`, `{Action}Response`, `{Action}SuccessResponse`
|
||||||
|
- Path params: `{Action}PathInfo`
|
||||||
|
- Builder structs: `{Thing}Builder` with `set_` prefixed setters and a `build()` method
|
||||||
|
|
||||||
|
## Imports
|
||||||
|
|
||||||
|
- Group imports: std, external crates, internal workspace crates, `crate::` imports
|
||||||
|
- Prefer `use` imports over inline fully-qualified paths. Inline qualification is acceptable
|
||||||
|
only for genuine one-off references where adding an import would be noise, or for types
|
||||||
|
that collide with std prelude names (`Result`, `Option`, `Error`). If you reference a symbol
|
||||||
|
more than once, always import it.
|
||||||
|
- Prefer specific imports over wildcards, except in `api_doc.rs` and test modules
|
||||||
|
- When two crates export a type with the same name, alias with a suffix describing the source crate:
|
||||||
|
`use enums::...::CommonResolution as CommonResolutionEnum;`
|
||||||
|
`use artcraft_router::...::CommonResolution as CommonResolutionRouter;`
|
||||||
|
This avoids long fully-qualified paths in function signatures and bodies.
|
||||||
|
|
||||||
|
## MySQL
|
||||||
|
|
||||||
|
- MySQL tables: primary key `id` columns must be `BIGINT(20) UNSIGNED`. Do not use signed IDs.
|
||||||
|
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
|
||||||
|
|
||||||
|
## TypeScript / Frontend
|
||||||
|
|
||||||
|
- TypeScript with React, Vite, Zustand, Three.js
|
||||||
|
- Nx monorepo under `crates/frontend/`
|
||||||
|
- Two spaces for indentation
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Testing
|
||||||
|
|
||||||
|
## Database Fixture Tests
|
||||||
|
|
||||||
|
- Live in-crate (e.g. `storyteller-web`'s omni_gen video pricing tests) and
|
||||||
|
use the `mysql_testing` crate (`crates/schema/database/mysql_testing`) for
|
||||||
|
guarded pools, schema setup, and account/session/wallet fixtures
|
||||||
|
- They RUN BY DEFAULT with `cargo test -p storyteller-web` and from the IDE
|
||||||
|
(requires a local MySQL with an `artcraft_test` database). Machines/CI
|
||||||
|
without MySQL skip them via the off switch:
|
||||||
|
`SQLX_OFFLINE=true cargo test -p storyteller-web --features skip_database_tests`
|
||||||
|
- They connect ONLY via `ARTCRAFT_TEST_DATABASE_URL` (default
|
||||||
|
`mysql://root:@localhost:3306/artcraft_test`); the guard panics on any
|
||||||
|
database whose name lacks "test", on `storyteller`/`artcraft`, and on
|
||||||
|
cloud hosts — see `mysql_testing/README.md`
|
||||||
|
- Database tests run in PARALLEL: each test creates its own users, wallets,
|
||||||
|
and rows; schema setup single-flights via a MySQL named lock. Only tests
|
||||||
|
mutating shared/global state take
|
||||||
|
`mysql_testing::serial::acquire_serial_test_lock().await`
|
||||||
|
- External providers must never be called: the harness stubs Kinovi with an
|
||||||
|
in-process HTTP server via `KINOVI_CUSTOM_API_HOST`/`KINOVI_CUSTOM_CDN_HOST`
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
- Most crates: `cargo test -p {crate_name}`
|
||||||
|
- Crates using SQLx offline mode: `SQLX_OFFLINE=true cargo test -p {crate_name}`
|
||||||
|
- The `storyteller-web` and `mysql_queries` crates require `SQLX_OFFLINE=true` for compilation
|
||||||
|
- Use `cargo check -p {crate_name}` for fast compilation checks without running tests
|
||||||
|
|
||||||
|
## Test Patterns
|
||||||
|
|
||||||
|
- Tests live in `#[cfg(test)] mod tests { ... }` at the bottom of each file
|
||||||
|
- Group related tests into sub-modules by category (e.g. `mod pricing_tests`, `mod resolution_tests`)
|
||||||
|
- Only create a sub-module when it has 2+ tests — don't wrap a single test in its own module
|
||||||
|
- Keep nesting shallow: `mod tests { mod category { ... } }` is fine; a third level is rarely needed
|
||||||
|
- Shared helpers used by multiple sub-modules belong in the parent `mod tests`, not duplicated in each sub-module
|
||||||
|
- Enum tests should cover: serialization, deserialization, `to_str`/`from_str` round-trip, variant count
|
||||||
|
- Password/crypto tests: be mindful of computation cost, don't add too many bcrypt test cases
|
||||||
|
|
||||||
|
## Test File Layout
|
||||||
|
|
||||||
|
Put test cases first, helpers last. Engineers should see the actual tests before scrolling
|
||||||
|
to understand helper plumbing.
|
||||||
|
|
||||||
|
Within a `mod tests { ... }` block:
|
||||||
|
|
||||||
|
1. **Imports** at the top
|
||||||
|
2. **Constants** (test data IDs, URLs, etc.) — small context that tests reference
|
||||||
|
3. **Test functions** (`#[test]` / `#[tokio::test]`) grouped by category
|
||||||
|
4. **Helper functions** (builders, pipeline runners, etc.) at the very bottom
|
||||||
|
|
||||||
|
If tests are grouped into sub-modules, the same rule applies within each sub-module:
|
||||||
|
constants first, then tests, then helpers at the end of that sub-module.
|
||||||
|
|
||||||
|
## What to Test
|
||||||
|
|
||||||
|
- New error types and their mappings
|
||||||
|
- Enum variant serialization stability (these are stored in the database)
|
||||||
|
- Hash/deduplication functions with known inputs
|
||||||
|
- Edge cases in parsers (empty input, missing fields, malformed data)
|
||||||
|
|
||||||
|
## Imports in Tests
|
||||||
|
|
||||||
|
- Import test fixtures and constants with `use`, not inline fully-qualified paths.
|
||||||
|
Write `use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;` at the top,
|
||||||
|
then reference `JUNO_AT_LAKE_IMAGE_URL` in test bodies.
|
||||||
|
- Same rule applies to enum variants used in match arms — import them, don't write
|
||||||
|
`crate::some::deep::module::MyEnum::Variant` inline.
|
||||||
|
|
||||||
|
## What Not to Test
|
||||||
|
|
||||||
|
- Don't add integration tests that require a running database unless explicitly asked
|
||||||
|
- Don't add tests for simple struct construction or trivial getters
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
hakari-package = "workspace-build-acceleration"
|
||||||
|
|
||||||
|
resolver = "1"
|
||||||
|
|
||||||
|
dep-format-version = "4"
|
||||||
|
workspace-hack-line-style = "workspace-dotted"
|
||||||
|
|
||||||
|
platforms = [
|
||||||
|
"x86_64-unknown-linux-gnu",
|
||||||
|
"x86_64-apple-darwin",
|
||||||
|
"x86_64-pc-windows-msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
exact-versions = true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 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
|
||||||
|
old_code/voder-vocodes-1.0/frontends/admin/node_modules/** linguist-vendored
|
||||||
|
test_data/html_scraping/** linguist-vendored
|
||||||
|
windows/sqlite/** linguist-vendored
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "cargo" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
name: "Artcraft MacOS Publish"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- release
|
||||||
|
- artcraft-release
|
||||||
|
- artcraft-macos # Additional branch for MacOS testing
|
||||||
|
|
||||||
|
# This workflow creates Universal macOS builds with signing certificate support.
|
||||||
|
# On each push to the release branches it will create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-artcraft:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Universal macOS builds (arm64 and x86_64)
|
||||||
|
- platform: 'macos-latest-xlarge'
|
||||||
|
args: '--target universal-apple-darwin'
|
||||||
|
# Uncomment below for additional platforms
|
||||||
|
# - platform: 'ubuntu-22.04'
|
||||||
|
# args: ''
|
||||||
|
# - platform: 'windows-latest'
|
||||||
|
# args: ''
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: setup node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.platform == 'macos-latest-xlarge' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- name: install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: replace tauri config with mac-specific version
|
||||||
|
run: |
|
||||||
|
pushd ${{ github.workspace }}/crates/desktop/artcraft
|
||||||
|
# Use the mac-specific tauri conf for the build
|
||||||
|
# TODO(bt): Replace this with the platform-specific naming conventions
|
||||||
|
cp tauri-mac.conf.json tauri.conf.json
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: install frontend dependencies
|
||||||
|
run: |
|
||||||
|
# Navigate to the frontend directory to install dependencies
|
||||||
|
cd ./frontend
|
||||||
|
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
|
echo "Installing all workspace dependencies..."
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
echo "=== Version Information ==="
|
||||||
|
echo "Node version:"
|
||||||
|
node --version
|
||||||
|
echo "NPM version:"
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
echo "=== Nx versions from lockfile ==="
|
||||||
|
npm list --depth=0 | grep nx
|
||||||
|
|
||||||
|
- name: import Apple Developer Certificate
|
||||||
|
if: matrix.platform == 'macos-latest-xlarge'
|
||||||
|
env:
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "Starting certificate import process..."
|
||||||
|
|
||||||
|
# Create certificate file
|
||||||
|
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||||
|
|
||||||
|
# Debug: Check if certificate file exists and has content
|
||||||
|
echo "Certificate file info:"
|
||||||
|
ls -la certificate.p12
|
||||||
|
file certificate.p12
|
||||||
|
|
||||||
|
# Debug: Check certificate contents without password (just structure)
|
||||||
|
echo "Certificate structure (may show error if password protected):"
|
||||||
|
openssl pkcs12 -info -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -noout || echo "Could not read certificate structure"
|
||||||
|
|
||||||
|
# Debug: Try to list what's in the p12 file
|
||||||
|
echo "Attempting to list certificate contents:"
|
||||||
|
security cms -D -i certificate.p12 || echo "Not a CMS file, trying PKCS12..."
|
||||||
|
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -info -nodes 2>&1 | head -20 || echo "Failed to read PKCS12 contents"
|
||||||
|
|
||||||
|
# Debug: Extract certificate details to verify type and expiration
|
||||||
|
echo "Extracting certificate details..."
|
||||||
|
# Extract just the certificate portion and examine it
|
||||||
|
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -nokeys 2>/dev/null | openssl x509 -text -noout | grep -E "(Subject:|Issuer:|Not Before|Not After)" || echo "Failed to parse certificate details"
|
||||||
|
|
||||||
|
# Also try to see what type of certificate this is
|
||||||
|
echo "Certificate type check:"
|
||||||
|
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -nokeys 2>/dev/null | openssl x509 -text -noout | grep -E "(CN=|OU=)" || echo "Could not determine certificate type"
|
||||||
|
|
||||||
|
# Debug: Check if private key exists
|
||||||
|
echo "Checking for private key..."
|
||||||
|
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -nocerts -nodes 2>&1 | grep -E "(PRIVATE KEY|RSA KEY)" | head -5 || echo "No private key found"
|
||||||
|
|
||||||
|
# Create temporary keychain
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||||
|
security default-keychain -s build.keychain
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||||
|
security set-keychain-settings -t 3600 -u build.keychain
|
||||||
|
|
||||||
|
# Import certificate - ensure both certificate and private key are imported
|
||||||
|
echo "Importing certificate into keychain..."
|
||||||
|
# Import the p12 directly - this is the most reliable method
|
||||||
|
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
||||||
|
|
||||||
|
# Alternative: Try with additional flags if the first import didn't create an identity
|
||||||
|
echo "Verifying import..."
|
||||||
|
if ! security find-identity -v build.keychain | grep -q "1 valid identities found"; then
|
||||||
|
echo "First import didn't create identity, trying with -A flag..."
|
||||||
|
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -A
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set trust settings for the certificate
|
||||||
|
echo "Setting trust settings..."
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||||
|
|
||||||
|
# Unlock the keychain again to ensure it's accessible
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||||
|
|
||||||
|
# Add the keychain to the search list
|
||||||
|
security list-keychains -d user -s build.keychain $(security list-keychains -d user | sed 's/^[[:space:]]*"//;s/"$//')
|
||||||
|
|
||||||
|
# List all certificates to debug
|
||||||
|
echo "Listing all keychains:"
|
||||||
|
security list-keychains
|
||||||
|
|
||||||
|
echo "Listing all certificates in build keychain:"
|
||||||
|
security find-certificate -a -p build.keychain || echo "No certificates found"
|
||||||
|
|
||||||
|
echo "Detailed certificate info:"
|
||||||
|
security find-certificate -c "Learning Machines" build.keychain || echo "Certificate 'Learning Machines' not found"
|
||||||
|
|
||||||
|
# Check certificate trust settings
|
||||||
|
echo "Checking certificate trust settings:"
|
||||||
|
security dump-trust-settings || echo "No custom trust settings"
|
||||||
|
|
||||||
|
# Try to find codesigning identities
|
||||||
|
echo "Finding all identities in keychain:"
|
||||||
|
security find-identity -v build.keychain || echo "No identities found"
|
||||||
|
|
||||||
|
echo "Finding codesigning identities:"
|
||||||
|
security find-identity -v -p codesigning build.keychain || echo "No codesigning identities found"
|
||||||
|
|
||||||
|
# Additional debug: Check if this is a valid Developer ID cert
|
||||||
|
echo "Checking for Developer ID certificates:"
|
||||||
|
security find-certificate -a build.keychain | grep -A 5 -B 5 "Developer ID" || echo "No Developer ID certificates found"
|
||||||
|
|
||||||
|
- name: verify certificate
|
||||||
|
if: matrix.platform == 'macos-latest-xlarge'
|
||||||
|
run: |
|
||||||
|
echo "=== Certificate Verification ==="
|
||||||
|
|
||||||
|
echo "1. Checking codesigning identities:"
|
||||||
|
security find-identity -v -p codesigning build.keychain || echo "No codesigning identities found"
|
||||||
|
|
||||||
|
echo -e "\n2. Checking all identities:"
|
||||||
|
security find-identity -v build.keychain || echo "No identities found"
|
||||||
|
|
||||||
|
echo -e "\n3. Checking certificates in keychain:"
|
||||||
|
security find-certificate -c "Learning Machines" build.keychain -p | openssl x509 -text -noout | grep -E "(Subject:|CN=)" || echo "Could not parse certificate"
|
||||||
|
|
||||||
|
# Set the correct signing identity
|
||||||
|
echo "APPLE_SIGNING_IDENTITY=Developer ID Application: Learning Machines LLC (DJ6XS33FX8)" >> $GITHUB_ENV
|
||||||
|
echo "Proceeding with identity: Developer ID Application: Learning Machines LLC (DJ6XS33FX8)"
|
||||||
|
|
||||||
|
# Also set the team ID
|
||||||
|
echo "APPLE_DEVELOPMENT_TEAM=DJ6XS33FX8" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Pre-build frontend to avoid Tauri build issues
|
||||||
|
run: |
|
||||||
|
# Build the frontend app from the correct directory
|
||||||
|
cd ./frontend
|
||||||
|
|
||||||
|
# Increase Node.js memory limit for the build
|
||||||
|
export NODE_OPTIONS="--max-old-space-size=8192"
|
||||||
|
|
||||||
|
# Use npx to ensure we use the local nx version
|
||||||
|
echo "Building with npx nx..."
|
||||||
|
npx nx run artcraft:build
|
||||||
|
|
||||||
|
echo "Checking build output..."
|
||||||
|
ls -la apps/artcraft/dist/ | head -10
|
||||||
|
|
||||||
|
# Update Tauri config to skip the build (since we already built)
|
||||||
|
cd ${{ github.workspace }}/crates/desktop/artcraft
|
||||||
|
cp tauri.conf.json tauri.conf.json.bak
|
||||||
|
# Set beforeBuildCommand to empty or a no-op command
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
jq '.build.beforeBuildCommand = ""' tauri.conf.json > tauri.conf.json.tmp
|
||||||
|
mv tauri.conf.json.tmp tauri.conf.json
|
||||||
|
else
|
||||||
|
node -e "const fs = require('fs'); const config = JSON.parse(fs.readFileSync('tauri.conf.json', 'utf8')); config.build.beforeBuildCommand = ''; fs.writeFileSync('tauri.conf.json', JSON.stringify(config, null, 2));"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
- name: build and publish
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_DEVELOPMENT_TEAM: ${{ env.APPLE_DEVELOPMENT_TEAM }}
|
||||||
|
# Use the SQLx query cache for SQLite
|
||||||
|
SQLX_OFFLINE: true
|
||||||
|
with:
|
||||||
|
projectPath: ./crates/desktop/artcraft
|
||||||
|
tagName: artcraft-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
|
||||||
|
releaseName: "ArtCraft v__VERSION__"
|
||||||
|
releaseBody: "See the assets below to download this version and install."
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
|
args: ${{ matrix.args }}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
name: "Artcraft Windows Publish"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- release
|
||||||
|
- artcraft-release
|
||||||
|
- artcraft-windows # Additional branch for Windows testing
|
||||||
|
|
||||||
|
# This workflow creates Windows builds.
|
||||||
|
# On each push to the release branches it will create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-artcraft-windows:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Windows builds
|
||||||
|
- platform: 'windows-latest'
|
||||||
|
args: ''
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: install wreq dependencies
|
||||||
|
run: |
|
||||||
|
choco install cmake -y
|
||||||
|
choco install strawberryperl -y
|
||||||
|
choco install pkgconfiglite -y
|
||||||
|
choco install llvm -y
|
||||||
|
choco install nasm -y
|
||||||
|
shell: cmd
|
||||||
|
|
||||||
|
- name: setup node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- name: install frontend dependencies
|
||||||
|
run: |
|
||||||
|
# Navigate to the frontend directory to install dependencies
|
||||||
|
cd ./frontend
|
||||||
|
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
|
echo "Installing all workspace dependencies..."
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
echo "=== Version Information ==="
|
||||||
|
echo "Node version:"
|
||||||
|
node --version
|
||||||
|
echo "NPM version:"
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
echo "=== Nx versions from lockfile ==="
|
||||||
|
npm list --depth=0 | findstr nx
|
||||||
|
|
||||||
|
- name: Pre-build frontend to avoid Tauri build issues
|
||||||
|
run: |
|
||||||
|
# Build the frontend app from the correct directory
|
||||||
|
cd ./frontend
|
||||||
|
|
||||||
|
# Increase Node.js memory limit for the build
|
||||||
|
$env:NODE_OPTIONS="--max-old-space-size=8192"
|
||||||
|
|
||||||
|
# Use npx to ensure we use the local nx version
|
||||||
|
echo "Building with npx nx..."
|
||||||
|
npx nx run artcraft:build
|
||||||
|
|
||||||
|
echo "Checking build output..."
|
||||||
|
dir apps/artcraft/dist/ | Select-Object -First 10
|
||||||
|
|
||||||
|
# Update Tauri config to skip the build (since we already built)
|
||||||
|
cd ${{ github.workspace }}/crates/desktop/artcraft
|
||||||
|
Copy-Item tauri.conf.json tauri.conf.json.bak
|
||||||
|
|
||||||
|
# Set beforeBuildCommand to empty using PowerShell
|
||||||
|
$config = Get-Content tauri.conf.json | ConvertFrom-Json
|
||||||
|
$config.build.beforeBuildCommand = ""
|
||||||
|
$config | ConvertTo-Json -Depth 10 | Set-Content tauri.conf.json
|
||||||
|
|
||||||
|
- name: build and publish
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
# NB(bt): This is needed by wreq. Hopefully it doesn't kill Tauri
|
||||||
|
RUSTFLAGS: "-C target-feature=+crt-static"
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Use the SQLx query cache for SQLite
|
||||||
|
SQLX_OFFLINE: true
|
||||||
|
with:
|
||||||
|
projectPath: ./crates/desktop/artcraft
|
||||||
|
tagName: artcraft-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
|
||||||
|
releaseName: "ArtCraft v__VERSION__"
|
||||||
|
releaseBody: "Production build of ArtCraft. See the assets below to download and install."
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
|
args: ${{ matrix.args }}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
# Secrets are BAD!
|
||||||
|
*secret*sh
|
||||||
|
.env
|
||||||
|
.env-aichatbot-secrets
|
||||||
|
.env-analytics-job
|
||||||
|
.env-elasticsearch-secrets
|
||||||
|
.env-migration-tool-secrets
|
||||||
|
.env-secrets
|
||||||
|
.env-support-tool-secrets
|
||||||
|
_develop/tools/.secrets
|
||||||
|
_develop/tools/db_secrets.toml
|
||||||
|
crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env
|
||||||
|
db.secrets
|
||||||
|
db_secrets.toml
|
||||||
|
env-secrets
|
||||||
|
pubkeys.txt
|
||||||
|
secrets.toml
|
||||||
|
secrets.yaml
|
||||||
|
secrets/
|
||||||
|
twitch_secrets.toml
|
||||||
|
|
||||||
|
# Old env secrets
|
||||||
|
.*-secrets
|
||||||
|
|
||||||
|
# New env secrets (per-app)
|
||||||
|
*-secrets.env
|
||||||
|
|
||||||
|
# Utilities with secrets
|
||||||
|
production_database_secret_connect.sh
|
||||||
|
production_database_secret_query.sh
|
||||||
|
production_database_voice_clone_report_download.sh
|
||||||
|
script/artcraft/mac_signing_build.sh
|
||||||
|
script/production_database_secret_backup.sh
|
||||||
|
script/production_database_secret_connect.sh
|
||||||
|
script/socat_dev_database_connect.sh
|
||||||
|
|
||||||
|
# Python virtual envs (named by convention)
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# LLM Testing
|
||||||
|
/external/
|
||||||
|
/prompts/
|
||||||
|
/reports/
|
||||||
|
/requests/
|
||||||
|
|
||||||
|
# Scratch data
|
||||||
|
scratch/
|
||||||
|
test_data/temp/
|
||||||
|
untracked/
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
# Rust build directory
|
||||||
|
target/
|
||||||
|
crates/lib/database_queries/target/
|
||||||
|
crates/lib/sqlite_queries/target/
|
||||||
|
|
||||||
|
# Javascript build directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Frontend build outputs
|
||||||
|
frontend/apps/artcraft/bundle-report.html
|
||||||
|
|
||||||
|
# Vim noise
|
||||||
|
*.swo
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|
||||||
|
# IntelliJ/CLion/RustRover noise
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Python noise
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# We use diesel for DB migrations only. We don't care about its codgen.
|
||||||
|
_ignored_schema.rs
|
||||||
|
|
||||||
|
# Diesel lock file
|
||||||
|
.diesel_lock
|
||||||
|
|
||||||
|
# AI Chat Sidecar has its own schema managed by diesel, but we don't care for diesel codegen.
|
||||||
|
crates/programs/aichatbot-sidecar/_ignored_schema.rs
|
||||||
|
|
||||||
|
# AI Chat sidecar's SQlite database shouldn't be checked in
|
||||||
|
crates/programs/aichatbot-sidecar/database.db
|
||||||
|
|
||||||
|
# Runtime data for the AI chatbot
|
||||||
|
runtime_data/
|
||||||
|
|
||||||
|
# Old Python virtual environments
|
||||||
|
models/Wav2Lip/python/
|
||||||
|
models/tacotron2/python/
|
||||||
|
|
||||||
|
# Stuff I want to keep around, but not commit
|
||||||
|
scraps/
|
||||||
|
|
||||||
|
# Mac nonsense
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Windows build files
|
||||||
|
# These should be built by the platform
|
||||||
|
windows/sqlite/sqlite3.exp
|
||||||
|
windows/sqlite/sqlite3.lib
|
||||||
|
|
||||||
|
# ArtCraft Windows DLLs
|
||||||
|
crates/desktop/src-tauri/*.dll
|
||||||
|
windows_dlls/
|
||||||
|
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE tasks\n SET is_dismissed_by_user = 1\n WHERE id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "13d435d8f681b73e4080748dcef0579e246e41049af67e1bd79ffc06aa6ba974"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 8
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "1e3758851fc8451670f86a9432009b73bdb81de4a08dcaa96a86afb5f9be7a68"
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n on_failure_type,\n on_failure_message,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "task_status",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "task_type",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "model_type",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider_job_id",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend_caller",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend_subscriber_id",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend_subscriber_payload",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "on_complete_primary_media_file_token",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "on_complete_primary_media_file_class",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "on_complete_batch_token",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "on_complete_primary_media_file_cdn_url",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "on_complete_primary_media_file_thumbnail_url_template",
|
||||||
|
"ordinal": 13,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "on_failure_type",
|
||||||
|
"ordinal": 14,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "on_failure_message",
|
||||||
|
"ordinal": 15,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"ordinal": 16,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at: DateTime<Utc>",
|
||||||
|
"ordinal": 17,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "completed_at: DateTime<Utc>",
|
||||||
|
"ordinal": 18,
|
||||||
|
"type_info": "Int64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "2436eb7bf4b0e23a899311a67a77f71d89eb35461e2113e43cd9d221bfa91c9a"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE tasks\n SET task_status = \"cancelled_by_user\"\n WHERE task_status NOT IN (\n \"complete_success\",\n \"complete_failure\",\n \"dead\",\n \"cancelled_by_user\",\n \"cancelled_by_provider\",\n \"cancelled_by_us\"\n )\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "30272128d83543d9fbfca930b7cdd4af30adfb4d90f2de044463a111c6340495"
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n queue_status_url,\n queue_response_url,\n prompt_token,\n frontend_subscriber_id,\n frontend_subscriber_payload\n FROM tasks\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "task_status",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "task_type",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "model_type",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider_job_id",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend_caller",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "queue_status_url",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "queue_response_url",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prompt_token",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend_subscriber_id",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend_subscriber_payload",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "35677894f7b02f32a1bf6396fa6d10caba69bb0f3f03ee7ffbfa1437689d42df"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n queue_status_url,\n queue_response_url,\n prompt_token,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 12
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "4fc9674ae39abaadc36054c200290ec95d0e2c7765336f0fa97805a2265f5c4c"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 7
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "50a2449b87abed7df3302a74ecd0e8ab87940077a247636680cb09b2fe488321"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE tasks\n SET task_status = ?,\n on_failure_type = ?,\n on_failure_message = ?\n WHERE id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "9b99ccd0baf9a4b74d8903f63e3a3dcf4c09b75cad978f0532388cbd743b7516"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE tasks\n SET task_status = ?\n WHERE id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "dc0367e9e43c3d7dda3623e77337ab4529eac2192475be7d08589886703ba1cb"
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n UPDATE tasks\n SET is_dismissed_by_user = 1\n WHERE is_dismissed_by_user != 1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "f83018ec5493982659a56576296914f937bb4e75db3a522eb4bda486d5caa402"
|
||||||
|
}
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/*~": true,
|
||||||
|
".sqlx/**": true,
|
||||||
|
"_database/**": true,
|
||||||
|
"_database/elasticsearch/**": true,
|
||||||
|
"_database/sql/**": true,
|
||||||
|
"frontend_old/**": true,
|
||||||
|
"includes/**": true,
|
||||||
|
"target/**": true,
|
||||||
|
"test_data/**": true,
|
||||||
|
"tmp/**": true,
|
||||||
|
"virtualenv/**": true,
|
||||||
|
"windows_dlls/**": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance for coding agents when working with the Artcraft monorepo.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Artcraft is a public-facing Tauri desktop app for generating AI images and video.
|
||||||
|
It uses Rust and TypeScript. Backend services, company tooling, and website builds
|
||||||
|
live in the separate `artcraft-services` repository.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
artcraft/
|
||||||
|
├── _database/sql/artcraft_migrations/ # Embedded SQLite task database migrations
|
||||||
|
├── .sqlx/ # SQLite query metadata for offline builds
|
||||||
|
├── crates/
|
||||||
|
│ ├── desktop/artcraft/ # Tauri app, configuration, capabilities, icons
|
||||||
|
│ ├── api_clients/ # Desktop provider clients and shared API types
|
||||||
|
│ ├── lib/ # Desktop support libraries
|
||||||
|
│ ├── schema/database/sqlite_tasks/ # Desktop task persistence
|
||||||
|
│ ├── schema/public/ # Shared wire-format enums and identifiers
|
||||||
|
│ ├── testing/ # Shared test helpers
|
||||||
|
│ └── vendor/tauri-plugin-http/ # HTTP plugin fork with cookie access
|
||||||
|
├── frontend/
|
||||||
|
│ ├── apps/artcraft/ # Tauri app frontend and public resources
|
||||||
|
│ └── libs/ # Shared frontend libraries
|
||||||
|
├── script/artcraft/ # Desktop development and build scripts
|
||||||
|
├── test_data/ # Fixtures used by retained crate tests
|
||||||
|
└── Cargo.toml # Desktop dependency workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep `external/` and `secrets/` untouched. Repository pruning must preserve
|
||||||
|
untracked and ignored local files; delete only Git-tracked paths unless the user
|
||||||
|
explicitly authorizes a cache cleanup.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Rust with no minimum supported version
|
||||||
|
- SQLx for SQLite; prefer `sqlx::query!` / `sqlx::query_as!` compile-time checked macros over runtime `sqlx::query()` whenever possible
|
||||||
|
- A mix of wreq and reqwest for Rust HTTP clients
|
||||||
|
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros instead
|
||||||
|
- When two crates export the same type name, alias imports with a suffix: `use foo::Bar as BarFoo;`
|
||||||
|
- Prefer `use` imports over inline fully-qualified paths; only qualify inline for true one-offs or std collisions
|
||||||
|
- TypeScript with Nx, React, Vite, Zustand, and Three.js
|
||||||
|
- Use two spaces for indentation
|
||||||
|
|
||||||
|
### File Layout
|
||||||
|
|
||||||
|
Organize for top-to-bottom reading. Important things first, details later.
|
||||||
|
|
||||||
|
- **Constants** at the top (after imports)
|
||||||
|
- **Structs/enums** next; outer structs above inner sub-structs
|
||||||
|
- **API types** in order: Request, Response, Error
|
||||||
|
- **In impl blocks**: constructors first, then public methods, then private helpers
|
||||||
|
- Private helpers go *below* the methods that call them
|
||||||
|
- Among helpers: meatier logic above leaf-level formatters
|
||||||
|
- **In test modules**: constants first, then test cases (grouped into sub-modules when 2+), then helper functions last
|
||||||
|
|
||||||
|
## Markdown
|
||||||
|
|
||||||
|
- **Tables must be space-padded so columns align in plain text.** Markdown
|
||||||
|
tables are read raw (terminals, diffs, editors) at least as often as
|
||||||
|
rendered, and condensed tables are unreadable there. Pad every cell to its
|
||||||
|
column width:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| Model | Configuration | Credits | Speed | Score |
|
||||||
|
|--------------|---------------|--------------------|---------|
|
||||||
|
| Meshy 6 | text or image | 104 | 80.0 | +24 |
|
||||||
|
| Rodin 2.5 | text or image | 13 | 10.0 | +3 |
|
||||||
|
```
|
||||||
|
|
||||||
|
Not: `| Model | Configuration | Credits | Speed | Score |` packed tight
|
||||||
|
with varying widths per row.
|
||||||
Generated
+9449
File diff suppressed because it is too large
Load Diff
+122
@@ -0,0 +1,122 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
# Desktop application and its local build/test dependencies.
|
||||||
|
members = [
|
||||||
|
"crates/api_clients/artcraft/artcraft_api_defs",
|
||||||
|
"crates/api_clients/artcraft/artcraft_client",
|
||||||
|
"crates/api_clients/artcraft/artcraft_router",
|
||||||
|
"crates/api_clients/fal_client",
|
||||||
|
"crates/api_clients/gmicloud_client",
|
||||||
|
"crates/api_clients/grok_api_client",
|
||||||
|
"crates/api_clients/grok_consumer_client",
|
||||||
|
"crates/api_clients/kinovi_web_client",
|
||||||
|
"crates/api_clients/midjourney_client",
|
||||||
|
"crates/api_clients/openai_sora_client",
|
||||||
|
"crates/api_clients/worldlabs_api_client",
|
||||||
|
"crates/api_clients/worldlabs_consumer_client",
|
||||||
|
"crates/desktop/artcraft",
|
||||||
|
"crates/lib/build_metadata",
|
||||||
|
"crates/lib/caching/memory_store",
|
||||||
|
"crates/lib/cloudflare_errors",
|
||||||
|
"crates/lib/cloudflare_mitigation",
|
||||||
|
"crates/lib/cookie_store",
|
||||||
|
"crates/lib/data/primitives",
|
||||||
|
"crates/lib/data/web_base64",
|
||||||
|
"crates/lib/errors",
|
||||||
|
"crates/lib/files/file_to_base64_url",
|
||||||
|
"crates/lib/files/filesys",
|
||||||
|
"crates/lib/files/images",
|
||||||
|
"crates/lib/files/mimetypes",
|
||||||
|
"crates/lib/http_headers",
|
||||||
|
"crates/lib/jwt_light",
|
||||||
|
"crates/lib/url_utils",
|
||||||
|
"crates/lib/uuid_utils",
|
||||||
|
"crates/schema/database/sqlite_tasks",
|
||||||
|
"crates/schema/public/enums",
|
||||||
|
"crates/schema/public/tokens",
|
||||||
|
"crates/testing/test_data",
|
||||||
|
"crates/testing/test_utils",
|
||||||
|
"crates/vendor/tauri-plugin-http",
|
||||||
|
]
|
||||||
|
default-members = ["crates/desktop/artcraft"]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
|
||||||
|
artcraft_client = { path = "crates/api_clients/artcraft/artcraft_client" }
|
||||||
|
artcraft_router = { path = "crates/api_clients/artcraft/artcraft_router" }
|
||||||
|
build_metadata = { path = "crates/lib/build_metadata" }
|
||||||
|
cloudflare_errors = { path = "crates/lib/cloudflare_errors" }
|
||||||
|
cloudflare_mitigation = { path = "crates/lib/cloudflare_mitigation" }
|
||||||
|
cookie_store = { path = "crates/lib/cookie_store" }
|
||||||
|
enums = { path = "crates/schema/public/enums" }
|
||||||
|
errors = { path = "crates/lib/errors" }
|
||||||
|
fal_client = { path = "crates/api_clients/fal_client" }
|
||||||
|
gmicloud_client = { path = "crates/api_clients/gmicloud_client" }
|
||||||
|
file_to_base64_url = { path = "crates/lib/files/file_to_base64_url" }
|
||||||
|
filesys = { path = "crates/lib/files/filesys" }
|
||||||
|
grok_api_client = { path = "crates/api_clients/grok_api_client" }
|
||||||
|
grok_consumer_client = { path = "crates/api_clients/grok_consumer_client" }
|
||||||
|
http_headers = { path = "crates/lib/http_headers" }
|
||||||
|
images = { path = "crates/lib/files/images" }
|
||||||
|
jwt_light = { path = "crates/lib/jwt_light" }
|
||||||
|
memory_store = { path = "crates/lib/caching/memory_store" }
|
||||||
|
midjourney_client = { path = "crates/api_clients/midjourney_client" }
|
||||||
|
mimetypes = { path = "crates/lib/files/mimetypes" }
|
||||||
|
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" }
|
||||||
|
sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
|
||||||
|
test_data = { path = "crates/testing/test_data" }
|
||||||
|
test_utils = { path = "crates/testing/test_utils" }
|
||||||
|
tokens = { path = "crates/schema/public/tokens" }
|
||||||
|
url_utils = { path = "crates/lib/url_utils" }
|
||||||
|
uuid_utils = { path = "crates/lib/uuid_utils" }
|
||||||
|
web_base64 = { path = "crates/lib/data/web_base64" }
|
||||||
|
worldlabs_api_client = { path = "crates/api_clients/worldlabs_api_client" }
|
||||||
|
worldlabs_consumer_client = { path = "crates/api_clients/worldlabs_consumer_client" }
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
assertor = "0.0.3"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
bstr = "1.12.1"
|
||||||
|
byteorder = "1.5.0"
|
||||||
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
|
cookie = "0.18.1"
|
||||||
|
env = "1.0.1"
|
||||||
|
env_logger = "0.11.9"
|
||||||
|
futures = { version = "0.3.32" }
|
||||||
|
indexmap = "2.13.0"
|
||||||
|
log = "0.4.29"
|
||||||
|
once_cell = "1.21.4"
|
||||||
|
rand = "0.9.1"
|
||||||
|
regex = "1.12.3"
|
||||||
|
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls", "gzip" ] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_derive = "1.0.228"
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
sha2 = "0.11.0-rc.5"
|
||||||
|
sha3 = "0.10.8"
|
||||||
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
|
tauri = { version = "2.11.2", features = ["unstable", "devtools", "protocol-asset"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
|
||||||
|
tauri-plugin-dialog = "2.7.1"
|
||||||
|
tauri-plugin-fs = "2.5.1"
|
||||||
|
tauri-plugin-log = "2.8.0"
|
||||||
|
tauri-plugin-upload = "2.4.0"
|
||||||
|
tempdir = "0.3.7"
|
||||||
|
tempfile = "3.27.0"
|
||||||
|
thiserror = "2.0.18"
|
||||||
|
tokio = { version = "1.50.0", features = ["macros"] }
|
||||||
|
tokio-util = "0.7.12"
|
||||||
|
toml = "1.0.7+spec-1.1.0"
|
||||||
|
url = { version = "2.5.8", features = ["serde"] }
|
||||||
|
utoipa = { version = "5.4.0", features = ["chrono", "actix_extras", "url"] }
|
||||||
|
uuid = "1.22.0"
|
||||||
|
walkdir = "2.4.0"
|
||||||
|
# wreq native prerequisites:
|
||||||
|
# macOS: brew install cmake pkg-config llvm
|
||||||
|
# Windows: choco install cmake strawberryperl pkgconfiglite llvm nasm
|
||||||
|
wreq-util = "3.0.0-rc.10"
|
||||||
|
wreq = { version = "6.0.0-rc.28", features = ["json", "query", "form", "multipart", "cookies", "ws", "stream", "gzip", "brotli", "zstd", "deflate"] }
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
# Persist clippy check configurations:
|
||||||
|
# https://github.com/ericseppanen/cargo-cranky
|
||||||
|
# Clippy doesn't allow this (yet):
|
||||||
|
# https://github.com/rust-lang/rust-clippy/issues/6625
|
||||||
|
deny = [
|
||||||
|
# Rust
|
||||||
|
"unsafe_code", # We may remove this if needed
|
||||||
|
|
||||||
|
# Clippy - "serious" lints
|
||||||
|
"clippy::borrow_interior_mutable_const", # Seems dangerous to allow this. Eg. `const F = Lazy::new()`;
|
||||||
|
|
||||||
|
# Clippy - bad math
|
||||||
|
#"clippy::cast_possible_truncation", # Seems dangerous, eg f64 to u64.
|
||||||
|
#"clippy::cast_possible_wrap",
|
||||||
|
|
||||||
|
# Clippy - pointer safety / readability / correctness
|
||||||
|
"clippy::as_ptr_cast_mut",
|
||||||
|
"clippy::borrow_as_ptr",
|
||||||
|
"clippy::borrow_deref_ref",
|
||||||
|
|
||||||
|
# Clippy async
|
||||||
|
"clippy::await_holding_lock",
|
||||||
|
|
||||||
|
# Clippy - performance
|
||||||
|
"clippy::cmp_owned",
|
||||||
|
"clippy::useless_vec",
|
||||||
|
"clippy::format_in_format_args",
|
||||||
|
#"clippy::redundant_clone",
|
||||||
|
#"clippy::unnecessary_to_owned",
|
||||||
|
#"clippy::vec_init_then_push",
|
||||||
|
|
||||||
|
# Clippy - "ergonomic" / "usage" lints
|
||||||
|
"clippy::clone_on_copy", # Don't use clone() on Copy types
|
||||||
|
"clippy::option_option", # Lint: Don't ever Option<Option<T>>! Enums instead.
|
||||||
|
"clippy::zero_sized_map_values", # Lint: use a HashSet instead of HashMap<T, ()>.
|
||||||
|
|
||||||
|
# Clippy - "aesthetic" / "style" lints
|
||||||
|
"clippy::clone_double_ref", # Lint: feels safer/hygienic to disallow this, but I'm not strongly opposed.
|
||||||
|
"clippy::double_comparisons", # Lint: readability
|
||||||
|
"clippy::empty_structs_with_brackets", # Lint: {} feels less aesthetic than ;
|
||||||
|
"clippy::implicit_clone", # Lint: Recommendation here seems more idiomatic.
|
||||||
|
"clippy::needless_late_init", # Don't declare variables without initializing them
|
||||||
|
"clippy::needless_return", # Lint: not the worst offender, but let's try to keep unnecessary returns out.
|
||||||
|
"clippy::redundant_field_names", # Lint: Duplicate names is a burden when reading: you wonder why they're dupes.
|
||||||
|
"clippy::redundant_static_lifetimes", # Lint: feels more modern to elide static lifetimes
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
"non_fmt_panics", # panic!(format!(...)) will be unsupported soon
|
||||||
|
"unused_imports", # These are easy to automatically cull
|
||||||
|
"unused_mut", # Be strict about mutability
|
||||||
|
"unused_parens", # Unused parens are ugly
|
||||||
|
"unused_variables", # Don't leave cruft
|
||||||
|
|
||||||
|
# Possible:
|
||||||
|
# "clippy::ptr_arg",
|
||||||
|
# "clippy::too_many_lines" (note: this is configurable)
|
||||||
|
]
|
||||||
|
|
||||||
|
warn = [
|
||||||
|
]
|
||||||
|
|
||||||
|
allow = [
|
||||||
|
# Rust
|
||||||
|
"deprecated", # Other systems can handle deprecated code instead of the linter
|
||||||
|
"missing_docs", # Not everything needs documentation. Too much documentation is bad. Literate code.
|
||||||
|
|
||||||
|
# Clippy
|
||||||
|
"clippy::if_same_then_else", # There are cases this is clearer! Branches may have notes / considerations.
|
||||||
|
"clippy::single_char_pattern", # This is like punishing trailing commas. Chars are dumb.
|
||||||
|
]
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
ArtCraft License (WIP)
|
||||||
|
======================
|
||||||
|
|
||||||
|
(**Warning**: This isn't a real lawyery license full of legitimate legalese yet, but we'll take care of that ASAP.)
|
||||||
|
|
||||||
|
ArtCraft is a form of "fair source" software. It's like [free beer](https://en.wiktionary.org/wiki/free_as_in_beer)
|
||||||
|
in that we give you the code, but there are a few small limitations on what you can do with it.
|
||||||
|
|
||||||
|
First, what you *can* do with ArtCraft:
|
||||||
|
|
||||||
|
> 1. You do not have to pay us to use ArtCraft. It's free.
|
||||||
|
>
|
||||||
|
> 2. You can back up, copy, modify, compile, and use the code for your own private personal purposes.
|
||||||
|
>
|
||||||
|
> 3. You can do anything you want with the media you create with ArtCraft. Generated assets are yours.
|
||||||
|
|
||||||
|
What you *can't do* with ArtCraft:
|
||||||
|
|
||||||
|
> 1. You cannot sell the ArtCraft software commercially (eg. as your own software).
|
||||||
|
>
|
||||||
|
> 2. You cannot use the ArtCraft code to develop a competing business or product (eg. selling AI image and video tools).
|
||||||
|
>
|
||||||
|
> 3. You cannot fork ArtCraft and remove ArtCraft community, donation links, or paid model services. We intend to
|
||||||
|
> make money to improve ArtCraft for everyone.
|
||||||
|
>
|
||||||
|
> 4. You cannot use the ArtCraft name, logo, or mascot characters to promote your business without permission.
|
||||||
|
|
||||||
|
This license means you can see what we're doing and not be at risk of having the rug pulled out from under
|
||||||
|
you (eg. if our founders die, if our business collapses, if zombies or space aliens invade). It also gives
|
||||||
|
you a really great way to customize the software or lend a hand in reporting (or even fixing) bugs.
|
||||||
|
|
||||||
|
Our desire is to support artists, designers, and filmmakers. We want to help you build your own creative studios
|
||||||
|
and businesses. We want to give you creative software you can use forever.
|
||||||
|
|
||||||
|
ArtCraft is free. We do offer models available through us where you can pay us and support our development, but we also
|
||||||
|
offer the ability for you to use other providers - and we'll keep adding more options so you have maximum flexibility.
|
||||||
|
|
||||||
|
## Our Promise
|
||||||
|
|
||||||
|
- All development and business plans will be communicated openly with the community.
|
||||||
|
|
||||||
|
- Except for minor test scripts or a few small places where we might need to use licensed 3rd party code, all of
|
||||||
|
our engineering efforts will live in a single publicly available monorepo. You can see everything we do and even
|
||||||
|
benefit from using it for personal use. We plan on building a lot more than what you see today, and you'll get
|
||||||
|
all of it.
|
||||||
|
|
||||||
|
- We will not "rug pull" our users. If our business shuts down, gets acquired, or otherwise changes, the tools
|
||||||
|
we create will always continue to function in the same way.
|
||||||
|
|
||||||
|
- If the business ends and our founders quit working in this space, we'll make it so the software transitions
|
||||||
|
to an OSI-approved open source license. (We don't intend to quit!)
|
||||||
|
|
||||||
|
## Why not OSI Open Source?
|
||||||
|
|
||||||
|
Philosophical answer: The OSI is an industry body led by hyperscalers. They poach from open source all the time, yet
|
||||||
|
hyperscaler systems and code (AWS, GCP, YouTube, etc.) remain closed source. The tiniest nuggets are open, but the vast
|
||||||
|
machinery surrounding it - systems, user behaviors, funnels, at-scale moneymakers - is totally closed.
|
||||||
|
|
||||||
|
Practical answer: OSI licenses make it exceptionally challenging to grow big things commercially. And many of
|
||||||
|
the licenses leave room for hyperscalers to reach in and cherry-pick pieces for themselves that they can better
|
||||||
|
commercialize than the original authors.
|
||||||
|
|
||||||
|
Then there's completely open source software that doesn't even attempt to be a business.
|
||||||
|
|
||||||
|
We love software like Blender, but (1) developing ArtCraft is our full-time job, (2) we want to build something at scale
|
||||||
|
that is much bigger than what you presently see, and (3) this will eventually become much more capital intensive than
|
||||||
|
just writing code - we want to train novel models that work directly in conjunction with the user interface.
|
||||||
|
|
||||||
|
If and when we do get to scale, we intend to keep every bit of our work in the open, including the models. Available
|
||||||
|
for use, inspection, learning, fitting onto consumer GPU hardware, and more.
|
||||||
|
|
||||||
|
Not having a way to make money is unfortunately what killed Stability.ai. Stability.ai walked so that others could
|
||||||
|
eventually run. (Black Forest Labs, et al.) We're not egotistical enough to count ourselves in that party yet, but we
|
||||||
|
believe in the original Stability.ai ethos and that their model can work with a few tweaks.
|
||||||
|
|
||||||
|
We want to build big and incredible tools the world hasn't seen before. We want to keep them free and entirely source
|
||||||
|
available. You don't have to pay us, but if you do, all of your investment goes right back into the engineering
|
||||||
|
and the tools. We're writing code you can clone and keep.
|
||||||
|
|
||||||
|
## Inspiration
|
||||||
|
|
||||||
|
We took inspiration for this from [Fair Source](https://fair.io/), [Fair Code](https://faircode.io/),
|
||||||
|
the [Defold License](https://defold.com/license/), and Hacker News discussions. The folks at Obsidian
|
||||||
|
have some pretty good thoughts on this as well.
|
||||||
|
|
||||||
|
~ Brandon Thomas
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<p align="center">
|
||||||
|
<video src="https://github.com/user-attachments/assets/b4e24c27-d87d-4fd1-8599-dc0d0b8af48d" width="100%" autoplay="true" loop controls>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">The IDE for artists.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://discord.gg/artcraft"><img alt="Discord" src="https://img.shields.io/discord/1359579021108842617?style=for-the-badge&label=discord&color=ffffff&logo=discord&logoColor=ffffff" /></a>
|
||||||
|
<a href="https://www.youtube.com/@OfficialArtCraftStudios"><img alt="YouTube" src="https://img.shields.io/youtube/channel/subscribers/UCdjY4VG0ntoGwFsKZO4sVWA?style=for-the-badge&logo=YouTube" /></a>
|
||||||
|
<a href="https://x.com/intent/follow?screen_name=get_artcraft"><img alt="X" src="https://img.shields.io/twitter/follow/get_artcraft?style=for-the-badge&label=follow&logo=x&logoColor=ffffff&color=ffffff" /></a>
|
||||||
|
<a href="https://www.linkedin.com/company/artcraft-ai"><img alt="LinkedIn" src="https://img.shields.io/badge/linkedin--0A66C2?style=for-the-badge"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
ArtCraft
|
||||||
|
========
|
||||||
|
ArtCraft is the IDE for interactive AI image and video creation.
|
||||||
|
We turn prompting into *crafting*, so your ideas become a form of tangible expression and computing.
|
||||||
|
This is Adobe Photoshop for everyone, and we're giving away the source code!
|
||||||
|
|
||||||
|
## Show, Don't Tell: Advanced Crafting Features
|
||||||
|
|
||||||
|
Text-to-image is great, but artists *need control*. It's important to know what your image will look like before you generate it, and it's vitally important to achieve consistency and repeatability.
|
||||||
|
|
||||||
|
| Feature | Demo + Explanation
|
||||||
|
|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| **Image to Location** |  Placing virtual actors into physical environments establishes single-location consistency. You can film multiple shots within a room without having things disappear. |
|
||||||
|
| **3D Image Compositing** |  Use images (backdrops, foreground elements, props, etc.) in scenes with depth and blend them naturally together. Just a couple of images usually leads to great compositions. |
|
||||||
|
| **2D Image Compositing** |  Use images, background removal, layers, and simple drawing tools to precisely compose a scene. |
|
||||||
|
| **Image to 3D Mesh** |  It's almost impossible to lay out complicated objects or block complicated scenes; turning images into 3D helps position elements exactingly and intentionally. |
|
||||||
|
| **Character Posing** |  You can dynamically pose your characters to achieve the precise character, scene, and camera blocking before calling "action". |
|
||||||
|
| **Scene Blocking w/ Kit Bashing** |  Use 3D asset kits to precisely block out your scene: get the correct angles, object positions, and rich depth layering you can't with text prompting. |
|
||||||
|
| **Character Identity Transfer** |  Use mannequins as simple 3D ControlNets for posing any character. |
|
||||||
|
| **Background Removal** |  Remove backgrounds from images to make them useful in 2D or 3D compositing. They can be props, layers, or backdrops. |
|
||||||
|
| **Mixed Asset Crafting** |  You can use image cutouts, worlds, and simple 3D meshes all together to precisely and intentionally lay out your scenes. |
|
||||||
|
| **Scene Blocking** | (preview coming soon) |
|
||||||
|
| **Canvas Editing** | (preview coming soon) |
|
||||||
|
| **Scene Relighting** | (preview coming soon) |
|
||||||
|
|
||||||
|
Note: all of the above videos were generated for free with Grok Video; the cost to build this README was negligible.
|
||||||
|
|
||||||
|
## Quick and Easy Prompting
|
||||||
|
We haven't abandoned text-to-asset generation for quick prototyping and ideation. We support every popular workflow in a first class fashion.
|
||||||
|
|
||||||
|
| Feature | Demo + Explanation
|
||||||
|
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| **Text to Image** |  Text prompt over a dozen different image models. |
|
||||||
|
| **Image Editing** |  Edit with Nano Banana Pro and GPT Image 1.5. |
|
||||||
|
| **Image Editing** |  Use inpainting, drawing, masking, etc. to edit images. |
|
||||||
|
| **Image to Video** |  Image to video with lots of different options and controls. |
|
||||||
|
| **Image Inpainting** | (preview coming soon) |
|
||||||
|
| **Image Ingredients** | (preview coming soon) |
|
||||||
|
|
||||||
|
## Models and Providers Supported within Artcraft
|
||||||
|
|
||||||
|
| Provider | Features |
|
||||||
|
|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| Artcraft | Nano Banana, Nano Banana Pro, GPT-Image-1 / 1.5, Seedance 1.0 Lite / 1.5 Pro / 2.0, Seedream 4 / 4.5, Flux 1.1 / Kontext, Veo 2 / 3 / 3.1, Kling 1.6 / 2.1 / 2.5 / 2.6, Sora 2 / Pro, Hunyuan 3d 2 / 3 |
|
||||||
|
| Grok | Grok Imagine, Grok Video |
|
||||||
|
| Midjourney | Image Gen (all versions) |
|
||||||
|
| Sora | Sora 1, Sora 2, GPT-Image-1 |
|
||||||
|
| WorldLabs | Marble (Gaussian Splat World Generation) |
|
||||||
|
|
||||||
|
We're going to be adding the following providers soon: Kling (via Kling website accounts), Google (via API keys),
|
||||||
|
Runway (via website account), Luma (via website account).
|
||||||
|
|
||||||
|
We're potentially interested in adding other aggregators for those who already have subscriptions and credits at
|
||||||
|
those providers, for example: OpenArt, FreePik, etc.
|
||||||
|
|
||||||
|
## Downloads
|
||||||
|
|
||||||
|
- [Visit our website for the stable Windows and MacOS releases](https://getartcraft.com/)
|
||||||
|
- Or you can grab a [more recent Windows and MacOS build directly](https://github.com/storytold/artcraft/releases)
|
||||||
|
- Linux requires building from source for now
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [developer documentation](./_docs)
|
||||||
|
- [tools, scripts, misc](./script)
|
||||||
|
- [license](./LICENSE.md)
|
||||||
|
- [roadmap](./ROADMAP.md)
|
||||||
|
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
ArtCraft Roadmap
|
||||||
|
================
|
||||||
|
|
||||||
|
## 2026 Goals (non-exhaustive)
|
||||||
|
|
||||||
|
Product
|
||||||
|
|
||||||
|
- Make ArtCraft better and more useful than model aggregation websites (our #1 goal).
|
||||||
|
We don't dislike those websites; we just set a high standard for ourselves.
|
||||||
|
|
||||||
|
- Build the best "tangible" computing surfaces where you can mold scenes and videos like
|
||||||
|
clay and direct them with precision and clarity.
|
||||||
|
|
||||||
|
- Add every model and provider into ArtCraft so you can build your own crafting IDE.
|
||||||
|
Nothing will be left out.
|
||||||
|
|
||||||
|
- Build purpose-specific image models, eg. for relighting scenes and reposing characters
|
||||||
|
and props.
|
||||||
|
|
||||||
|
Studio
|
||||||
|
|
||||||
|
- Build our content studio. Make lots of narrative shorts and at least one
|
||||||
|
feature-length AI film by the year's end. Theatrical distribution, but also lots
|
||||||
|
of social media content. (We are creators, and we want to employ more creators!)
|
||||||
|
|
||||||
|
- Host festivals and competitions, including in-person meetups and screenings.
|
||||||
|
|
||||||
|
Architecture
|
||||||
|
|
||||||
|
- Remove dependence on ArtCraft hosted services.
|
||||||
|
|
||||||
|
- More Rust, more speed.
|
||||||
|
|
||||||
|
- Code cleanup, more/better tests, CI, ...
|
||||||
|
|
||||||
|
Community
|
||||||
|
|
||||||
|
- Build a safe community free of negativity and anti-AI stigma. A community of filmmakers
|
||||||
|
and artists.
|
||||||
|
|
||||||
|
## Loftier Goals
|
||||||
|
|
||||||
|
- Build an *Open* OpenAI / Google / Anthropic. Open models, open platforms, open hardware.
|
||||||
|
Why? Because the future could be bleak without it. How, you might ask? Get in touch if
|
||||||
|
you're curious.
|
||||||
|
|
||||||
|
- Don't Be Evil™
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
-- noinspection SqlDialectInspectionForFile
|
||||||
|
--
|
||||||
|
-- NB: This version needs to be bumped even if just adding or changing comments!
|
||||||
|
-- Failure to do so will cause Windows to segfault at start. (This is horrible!)
|
||||||
|
--
|
||||||
|
-- NB: `app_state_dir.rs` contains the file version number.
|
||||||
|
--
|
||||||
|
-- Migration history:
|
||||||
|
-- tasks_v1.sqlite - initial version
|
||||||
|
-- tasks_v2.sqlite - added model_type (nullable)
|
||||||
|
-- tasks_v3.sqlite - added frontend_caller, comments
|
||||||
|
-- tasks_v4.sqlite - added is_dismissed_by_user
|
||||||
|
-- tasks_v5.sqlite - added five fields:
|
||||||
|
-- on_complete_batch_token,
|
||||||
|
-- on_complete_primary_media_file_token,
|
||||||
|
-- on_complete_primary_media_file_class,
|
||||||
|
-- on_complete_primary_media_file_cdn_url,
|
||||||
|
-- on_complete_primary_media_file_thumbnail_url_template,
|
||||||
|
-- tasks_v6.sqlite - added two fields:
|
||||||
|
-- on_failure_type
|
||||||
|
-- on_failure_message
|
||||||
|
-- tasks_v7.sqlite - added prompt token + two fields (for fal):
|
||||||
|
-- prompt_token
|
||||||
|
-- queue_status_url
|
||||||
|
-- queue_response_url
|
||||||
|
|
||||||
|
CREATE TABLE tasks (
|
||||||
|
-- Task auto-incrementing primary key.
|
||||||
|
-- We don't use this.
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- TaskStatus enum
|
||||||
|
-- This is how the job system manages job states.
|
||||||
|
-- e.g. pending, started, complete_success, dead, etc.
|
||||||
|
task_status TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- TaskType enum
|
||||||
|
-- e.g. image_generation, video_generation, etc.
|
||||||
|
task_type TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- TaskModelType enum
|
||||||
|
-- e.g. flux_1_dev, veo_2, etc.
|
||||||
|
model_type TEXT,
|
||||||
|
|
||||||
|
-- GenerationProvider enum
|
||||||
|
-- Together with `provider_job_id`, this creates a foreign key lookup to the job.
|
||||||
|
-- e.g. 'artcraft', 'fal', 'sora', etc.
|
||||||
|
provider TEXT,
|
||||||
|
|
||||||
|
-- The primary key for the job in the provider's system.
|
||||||
|
provider_job_id TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- For third party jobs, we need to record the prompt details upfront.
|
||||||
|
-- We create a prompt record and store the token to associate once the task completes.
|
||||||
|
prompt_token TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- Tell the job system which caller initiated the task.
|
||||||
|
-- e.g. 'canvas'
|
||||||
|
frontend_caller TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- An arbitrary opaque identifier that frontend can set.
|
||||||
|
frontend_subscriber_id TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- An opaque JSON payload set by the frontend subscriber that
|
||||||
|
-- will be re-emitted back to the frontend.
|
||||||
|
frontend_subscriber_payload TEXT,
|
||||||
|
|
||||||
|
-- Whether the user has dismissed the task from view.
|
||||||
|
is_dismissed_by_user INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- For some third party systems (eg. FAL), the URL to check the status of the
|
||||||
|
-- job in the queue.
|
||||||
|
queue_status_url TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- For some third party systems (eg. FAL), the URL to check the
|
||||||
|
-- results (when the job completes)
|
||||||
|
queue_response_url TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- When the generation completes, the batch token (if any).
|
||||||
|
-- This will point to a collection of multiple files.
|
||||||
|
on_complete_batch_token TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- When the generation completes, the media token of the "primary" media file.
|
||||||
|
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||||
|
on_complete_primary_media_file_token TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- When the generation completes, the file type of the primary media file.
|
||||||
|
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||||
|
on_complete_primary_media_file_class TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- The URL to the media file.
|
||||||
|
on_complete_primary_media_file_cdn_url TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- The URL template for the thumbnail of the primary media file.
|
||||||
|
on_complete_primary_media_file_thumbnail_url_template TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- An enum-like string describing the failure reason (even for non-terminal failures).
|
||||||
|
-- The enum is `TaskFailureType`, which is a close analogue of the backend API's `FrontendFailureCategory`.
|
||||||
|
on_failure_type TEXT,
|
||||||
|
|
||||||
|
-- OPTIONAL.
|
||||||
|
-- A human-readable message describing the failure reason (even for non-terminal failures).
|
||||||
|
on_failure_message TEXT,
|
||||||
|
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||||
|
completed_at INTEGER DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Unique Indices
|
||||||
|
CREATE UNIQUE INDEX idx_tasks_on_provider_job_id ON tasks(provider_job_id);
|
||||||
|
|
||||||
|
-- Indices
|
||||||
|
CREATE INDEX idx_tasks_on_status ON tasks(task_status);
|
||||||
|
CREATE INDEX idx_tasks_on_provider ON tasks(provider);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
Code Conventions
|
||||||
|
================
|
||||||
|
|
||||||
|
We use three languages at Artcraft: Rust, TypeScript, and Python.
|
||||||
|
|
||||||
|
## Rust style guide
|
||||||
|
|
||||||
|
We deviate from the official Rust style guide in a few places:
|
||||||
|
|
||||||
|
- We use two spaces for indentation instead of four.
|
||||||
|
- We typically keep one function or type definition per file, though this isn't a steadfast rule.
|
||||||
|
- We use a Cargo workspace to share code within our monorepo.
|
||||||
|
|
||||||
|
### Rust Preferred Libraries
|
||||||
|
|
||||||
|
We use the following Rust libraries, though we are open to migrating to alternatives:
|
||||||
|
|
||||||
|
- `actix-web` for our HTTP web server
|
||||||
|
- `reqwest` for HTTP client requests
|
||||||
|
- `sqlx` for SQL database operations
|
||||||
|
- `tokio` for asynchronous programming
|
||||||
|
|
||||||
|
## TypeScript style guide
|
||||||
|
|
||||||
|
- We use two spaces for indentation instead of four.
|
||||||
|
- We use `nx` for monorepo management
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
Dev Setup
|
||||||
|
=========
|
||||||
|
|
||||||
|
# ArtCraft
|
||||||
|
|
||||||
|
ArtCraft is a Rust / Tauri app.
|
||||||
|
|
||||||
|
To set up the ArtCraft development environment, install the following:
|
||||||
|
|
||||||
|
1. [Install Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html).
|
||||||
|
2. [Install npm](https://nodejs.org/en/download) or [nvm](https://github.com/nvm-sh/nvm). (Node version `v24.13.0` works at time of writing.)
|
||||||
|
3. [Install nx](https://nx.dev/docs/getting-started/installation). (Nx version `v22.4.5` works at time of writing.)
|
||||||
|
4. [Install Tauri CLI](https://v2.tauri.app/reference/cli/). (Version `tauri-cli 2.10.0` works at time of writing.)
|
||||||
|
|
||||||
|
An easy way to get started with running the app in development is to run the two commands (in separate terminals):
|
||||||
|
|
||||||
|
**Mac and Linux Development**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the frontend dev server
|
||||||
|
./script/artcraft/unix_frontend_dev.sh
|
||||||
|
|
||||||
|
# Run the Tauri Rust application
|
||||||
|
./script/artcraft/unix_rust_dev.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows Development**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Run the frontend dev server
|
||||||
|
.\script\artcraft\windows_frontend_dev.ps1
|
||||||
|
|
||||||
|
# Run the Tauri Rust application
|
||||||
|
.\script\artcraft\windows_rust_dev.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend services and website builds live in the separate `artcraft-services` repository.
|
||||||
|
This repository retains the desktop task database in
|
||||||
|
`_database/sql/artcraft_migrations/` and its SQLite query cache in `.sqlx/`.
|
||||||
|
Use `SQLX_OFFLINE=true cargo check -p artcraft` to check Rust without a database server.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# ArtCraft Server
|
||||||
|
|
||||||
|
ArtCraft's server is a Rust / Actix app called `storyteller-web`.
|
||||||
|
|
||||||
|
You don't need to run this to develop the ArtCraft application, but it can be useful to spin up
|
||||||
|
a development instance for adding new server functions or as your own private local copy.
|
||||||
|
|
||||||
|
`storyteller-web` can function entirely within the development environment, and ArtCraft can be
|
||||||
|
pointed to a local instance of the server.
|
||||||
|
|
||||||
|
**TODO**: Concisely describe setting up server components. Note: we have
|
||||||
|
previous docs in `old/` that may still be relevant, though they're perhaps
|
||||||
|
slightly out of date.
|
||||||
|
|
||||||
|
## Server Setup (Mac)
|
||||||
|
|
||||||
|
### Install and Migrate MySQL Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For now, the tooling doesn't support MySQL 9.6 as it was just released in January 2026.
|
||||||
|
# This will probably change in the future to no longer need to specify the 8.* series.
|
||||||
|
brew install mysql@8.4
|
||||||
|
|
||||||
|
# Try to connect
|
||||||
|
mysql -uroot
|
||||||
|
|
||||||
|
# If connection fails, start service, then try again.
|
||||||
|
brew services start mysql
|
||||||
|
mysql -uroot
|
||||||
|
|
||||||
|
# If that fails, reboot and try connecting again.
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the `storyteller` database table. Connect to the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mysql -u root
|
||||||
|
|
||||||
|
# If it needs a password, use argument -p and when prompted use "root" as the typical default password
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run this:
|
||||||
|
|
||||||
|
```mysql
|
||||||
|
use mysql;
|
||||||
|
CREATE DATABASE storyteller;
|
||||||
|
CREATE USER 'storyteller'@'localhost' IDENTIFIED BY 'password';
|
||||||
|
GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';
|
||||||
|
```
|
||||||
|
|
||||||
|
Then verify access with `./script/dev_mysql_connect.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install diesel_cli \
|
||||||
|
--no-default-features \
|
||||||
|
--features mysql,sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
If this fails,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install zstd
|
||||||
|
brew install openssl
|
||||||
|
```
|
||||||
|
|
||||||
|
Possibly,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RUSTFLAGS="-L$(brew --prefix zstd)/lib -L$(brew --prefix openssl)/lib"
|
||||||
|
```
|
||||||
|
|
||||||
|
In older Macs, there [have been some issues with Diesel CLI](https://github.com/diesel-rs/diesel/issues/2605)
|
||||||
|
that require a few extra dependencies to be installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If you're on Mac and the above command didn't work, run the following and then retry:
|
||||||
|
brew install libpq
|
||||||
|
```
|
||||||
|
|
||||||
|
You're ready to run the migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
diesel migration run
|
||||||
|
```
|
||||||
|
|
||||||
|
You might get a scary message about `"Encountered unknown type for Mysql: enum"` -- you can safely ignore this
|
||||||
|
error if you see it in isolation. It doesn't impact the migrations whatsoever.
|
||||||
|
|
||||||
|
Finally, you'll need the sqlx CLI tool to run codegen.
|
||||||
|
You likely won't need this now, but if you change any queries, this will be necessary:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install sqlx-cli --features rustls,mysql,sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Redis (Mac)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Redis
|
||||||
|
brew install redis
|
||||||
|
|
||||||
|
# Start the service:
|
||||||
|
brew services start redis
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
Local Development Setup Guide (for bare metal)
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
This document describes how to run the FakeYou infrastructure on your development
|
||||||
|
machine's bare metal (no containers).
|
||||||
|
|
||||||
|
Setup instructions and development were tested and known to work on the following
|
||||||
|
platforms:
|
||||||
|
|
||||||
|
- Mac OS (Apple M2 silicon)
|
||||||
|
- Ubuntu 22.04
|
||||||
|
- Windows Subsystem for Linux (Ubuntu 22.04 distro) aka "WSL"
|
||||||
|
|
||||||
|
The applications (binary targets) you might be interested in running include, but
|
||||||
|
are not limited to:
|
||||||
|
|
||||||
|
- `storyteller-web`, the HTTP API server
|
||||||
|
- `download-job`, which downloads models from the web and stores them in GCS
|
||||||
|
- `inference-job`, which downloads models from GCS and runs inference with them
|
||||||
|
|
||||||
|
Setup Instructions
|
||||||
|
------------------
|
||||||
|
|
||||||
|
### (1) Install Rust
|
||||||
|
|
||||||
|
Install Rust [using this guide](https://www.rust-lang.org/learn/get-started). If it asks,
|
||||||
|
you'll want "stable" Rust, not "nightly" Rust. If it doesn't ask, it defaults to "stable".
|
||||||
|
|
||||||
|
For Windows Subsystem for Linux, you'll want the WSL version, not the `.exe` file!
|
||||||
|
|
||||||
|
### (2) Install the necessary libraries
|
||||||
|
|
||||||
|
#### Mac
|
||||||
|
|
||||||
|
On an up-to-date M1/M2 Mac, there should be no libraries you need to install! Any exceptions
|
||||||
|
to this assumption should be documented here.
|
||||||
|
|
||||||
|
#### Ubuntu 22.04 and WSL:
|
||||||
|
|
||||||
|
If you're on Ubuntu or WSL, you'll need to install some libraries to build the Rust code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If you haven't run apt before, you'll need to fetch the package list:
|
||||||
|
sudo apt update
|
||||||
|
|
||||||
|
# Install the dependencies
|
||||||
|
sudo apt install jq \
|
||||||
|
libmysqlclient-dev \
|
||||||
|
libsqlite3-dev \
|
||||||
|
pkgconf
|
||||||
|
```
|
||||||
|
|
||||||
|
(Note: jq is for combining mysql codegen outputs; pkgconf is if using openssl instead of rustls.)
|
||||||
|
|
||||||
|
### (3) Install MySQL server
|
||||||
|
|
||||||
|
#### Mac
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install MySQL
|
||||||
|
brew install mysql
|
||||||
|
|
||||||
|
# Try to connect
|
||||||
|
mysql -uroot
|
||||||
|
```
|
||||||
|
|
||||||
|
In the event of failure to connect, try the following steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If connection fails, start service
|
||||||
|
brew services start mysql
|
||||||
|
|
||||||
|
# Try to connect again
|
||||||
|
# If this fails, reboot the machine, eg. using `sudo reboot now`
|
||||||
|
mysql -uroot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Ubuntu 22.04 and WSL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install MySQL
|
||||||
|
sudo apt install mysql-server
|
||||||
|
|
||||||
|
# Try to connect
|
||||||
|
# The default password is typically "root".
|
||||||
|
sudo mysql -u root -p
|
||||||
|
````
|
||||||
|
|
||||||
|
In the event of failure to connect, try the following steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If it failed, you may need to start MySQL (typically only on WSL, not on native Ubuntu)
|
||||||
|
sudo service mysql start
|
||||||
|
|
||||||
|
# You may also have to change some socket file permissions (again, typically only on WSL)
|
||||||
|
sudo chmod g+rx /var/run/mysqld
|
||||||
|
sudo usermod -aG mysql $USER
|
||||||
|
newgrp mysql
|
||||||
|
|
||||||
|
# Try to connect again
|
||||||
|
# The default password is typically "root".
|
||||||
|
# If this fails, try rebooting. If it still fails, read some of the suggestions at the bottom
|
||||||
|
# of this README.
|
||||||
|
sudo mysql -u root -p
|
||||||
|
```
|
||||||
|
|
||||||
|
### (4) Install a `storyteller` user and table in MySQL
|
||||||
|
|
||||||
|
First, connect to MySQL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If the following command asks for a password, the password is typically "root"
|
||||||
|
sudo mysql -u root -p
|
||||||
|
```
|
||||||
|
|
||||||
|
Once in MySQL, run the following to set up the database and user accounts:
|
||||||
|
|
||||||
|
```mysql
|
||||||
|
use mysql;
|
||||||
|
CREATE DATABASE storyteller;
|
||||||
|
CREATE USER 'storyteller'@'localhost' IDENTIFIED BY 'password';
|
||||||
|
GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';
|
||||||
|
```
|
||||||
|
|
||||||
|
Then verify access with `./dev_mysql_connect.sh`
|
||||||
|
|
||||||
|
### (5) Install Diesel CLI (for MySQL migrations):
|
||||||
|
|
||||||
|
In order to run MySQL database migrations, you'll need to install the migration tool we use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install diesel_cli \
|
||||||
|
--no-default-features \
|
||||||
|
--features mysql,sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
In older Macs, there [have been some issues with Diesel CLI](https://github.com/diesel-rs/diesel/issues/2605)
|
||||||
|
that require a few extra dependencies to be installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If you're on Mac and the above command didn't work, run the following and then retry:
|
||||||
|
brew install libpq
|
||||||
|
```
|
||||||
|
|
||||||
|
### (6) Run the pending database migrations:
|
||||||
|
|
||||||
|
You're ready to run the migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
diesel migration run
|
||||||
|
```
|
||||||
|
|
||||||
|
You might get a scary message about `"Encountered unknown type for Mysql: enum"` -- you can safely ignore this
|
||||||
|
error if you see it in isolation. It doesn't impact the migrations whatsoever. (See notes at the bottom of
|
||||||
|
the README for details.)
|
||||||
|
|
||||||
|
### (7) (optional) Install the SQLx CLI (if doing database development)
|
||||||
|
|
||||||
|
(You can probably skip this step unless you're a backend engineer.)
|
||||||
|
|
||||||
|
If (and only if) you're doing Rust service development that changes the MySQL queries, then you should install
|
||||||
|
SQLx utilities. This tool is used to cache the column types and query plans for CI, as SQLx statically verifies
|
||||||
|
raw MySQL queries against the schema and Rust types.
|
||||||
|
|
||||||
|
We'll be using diesel to manage the migrations, but sqlx within the app to actually perform queries.
|
||||||
|
Diesel is an immature ORM, which isn't a good tech bet, so we use sqlx as at-compile-time
|
||||||
|
typesafe SQL.
|
||||||
|
|
||||||
|
(TODO: Reevaluate this assessment for 2023-2024 and beyond. Maybe we do want Diesel now.)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# NB(bt,2023-10-04): "--no-default-features" not working
|
||||||
|
# cargo install sqlx-cli --no-default-features --features rustls,mysql,sqlite
|
||||||
|
cargo install sqlx-cli --features rustls,mysql,sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
### (8) Install hosts file:
|
||||||
|
|
||||||
|
(This is the same for Linux, Mac, and WSL.)
|
||||||
|
|
||||||
|
If you're developing against the frontend, it'll target development domains (eg. `dev.fakeyou.com`) instead
|
||||||
|
of `127.0.0.1` or `localhost`. You can make your machine route domains to localhost by editing your hosts
|
||||||
|
file (located at `/etc/hosts`) to include the following configuration lines:
|
||||||
|
|
||||||
|
```
|
||||||
|
# 1) Edit hosts file with your editor of choice
|
||||||
|
#
|
||||||
|
# - sudo vi /etc/hosts
|
||||||
|
# - sudo nano /etc/hosts
|
||||||
|
#
|
||||||
|
# 2) Then paste:
|
||||||
|
#
|
||||||
|
|
||||||
|
127.0.0.1 dev.fakeyou.com
|
||||||
|
127.0.0.1 api.dev.fakeyou.com
|
||||||
|
127.0.0.1 devproxy.fakeyou.com
|
||||||
|
|
||||||
|
127.0.0.1 dev.storyteller.ai
|
||||||
|
127.0.0.1 api.dev.storyteller.ai
|
||||||
|
127.0.0.1 devproxy.storyteller.ai
|
||||||
|
```
|
||||||
|
|
||||||
|
### (9) Install Redis
|
||||||
|
|
||||||
|
#### Mac:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Redis
|
||||||
|
brew install redis
|
||||||
|
|
||||||
|
# Start the service:
|
||||||
|
brew services start redis
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Ubuntu 22.04 and WSL
|
||||||
|
|
||||||
|
```
|
||||||
|
# Install Redis
|
||||||
|
sudo apt install redis
|
||||||
|
|
||||||
|
# Start the service (typically only needed on WSL):
|
||||||
|
sudo service redis-server start
|
||||||
|
```
|
||||||
|
|
||||||
|
### (10) Install Elasticsearch (optional)
|
||||||
|
|
||||||
|
(You can most likely skip this step.)
|
||||||
|
|
||||||
|
Mac: (TODO - haven't installed yet)
|
||||||
|
|
||||||
|
WSL: (TODO - haven't installed yet)
|
||||||
|
|
||||||
|
Ubuntu 22.04:
|
||||||
|
|
||||||
|
Install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elastic.gpg
|
||||||
|
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/elastic.gpg] https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-7.x.list
|
||||||
|
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install elasticsearch
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure:
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Edit /etc/elasticsearch/elasticsearch.yml
|
||||||
|
# Set the following:
|
||||||
|
network.host: localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl start elasticsearch
|
||||||
|
sudo systemctl enable elasticsearch
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET 'http://localhost:9200'
|
||||||
|
```
|
||||||
|
|
||||||
|
Install Kibana (prototyping UI):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install
|
||||||
|
sudo apt-get install kibana
|
||||||
|
|
||||||
|
# Run
|
||||||
|
sudo /bin/systemctl daemon-reload
|
||||||
|
sudo /bin/systemctl enable kibana.service
|
||||||
|
sudo systemctl start kibana.service
|
||||||
|
|
||||||
|
# Access the Kibana Status Page:
|
||||||
|
# http://localhost:5601/status
|
||||||
|
|
||||||
|
# Access the query console UI:
|
||||||
|
# Click Sidebar > Management > Dev Tools > Console
|
||||||
|
# http://localhost:5601/
|
||||||
|
```
|
||||||
|
|
||||||
|
Extra reading on setting up Elasticsearch:
|
||||||
|
|
||||||
|
- [Guide 1](https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-elasticsearch-on-ubuntu-22-04)
|
||||||
|
- [Guide 2](https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html)
|
||||||
|
- [Kibana](https://www.elastic.co/guide/en/kibana/current/deb.html)
|
||||||
|
|
||||||
|
### (11) Run one or more applications:
|
||||||
|
|
||||||
|
To start the HTTP API server,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin storyteller-web
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this compiles and runs the "development" binary. It's faster and easier to debug than the
|
||||||
|
optimized "production" build. To build a fully optimized production release,
|
||||||
|
run `cargo build --release --bin storyteller-web` . Note that this will take much longer.
|
||||||
|
|
||||||
|
You may notice that this might not work right off the bat. You may need to specify some environment variables
|
||||||
|
and supply some development secrets.
|
||||||
|
|
||||||
|
Secrets:
|
||||||
|
|
||||||
|
- Global secrets (ask Brandon for these):
|
||||||
|
- Put `.env-secrets` in the root `storyteller-rust` directory.
|
||||||
|
|
||||||
|
- Per app secrets (ask Brandon for these):
|
||||||
|
- Put `storteller-web.development-secrets.env` in `crates/service/web/storyteller_web/config`
|
||||||
|
- Put `inference-job.development-secrets.env` in `crates/service/job/inference_job/config`
|
||||||
|
- Put `donwload-job.development-secrets.env` in `crates/service/job/download_job/config`
|
||||||
|
|
||||||
|
**DO NOT CHECK THESE SECRET FILES INTO THE REPO!**
|
||||||
|
|
||||||
|
Environment config:
|
||||||
|
|
||||||
|
You may need to set local development paths for your environment. You can set these in `.env-secrets` too,
|
||||||
|
so that they won't be updated for other engineers.
|
||||||
|
|
||||||
|
```
|
||||||
|
# this should be the directory containing all of the "storyteller" projects:
|
||||||
|
STORYTELLER_ROOT = "/home/tensor/code/storyteller" # change this
|
||||||
|
|
||||||
|
# The following are entirely optional, but if you want to fine tune where each project lives, you may do so:
|
||||||
|
STORYTELLER_FRONTEND = "/somewhere/else/storyteller-frontend"
|
||||||
|
STORYTELLER_ML = "storyteller-ml"
|
||||||
|
STORYTELLER_RUST = "/Users/bt/Development/storyteller-rust"
|
||||||
|
```
|
||||||
|
|
||||||
|
To download some ML models, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin download-job
|
||||||
|
cargo run --bin tts-download-job
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to run both the HTTP API and the jobs, you'll need to run both processes in different
|
||||||
|
terminals or tmux sessions.
|
||||||
|
|
||||||
|
Note also that the configurations pointing to the ML monorepo must be set up for each application.
|
||||||
|
Additionally, some development secrets may be needed (ask the team to share).
|
||||||
|
|
||||||
|
To execute ML inference, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin inference-job
|
||||||
|
cargo run --bin tts-inference-job
|
||||||
|
```
|
||||||
|
|
||||||
|
Again, note that the configurations pointing to the ML monorepo must be set up for each application.
|
||||||
|
Additionally, some development secrets may be needed (ask the team to share).
|
||||||
|
|
||||||
|
Seeding the Database with Some Values
|
||||||
|
-------------------------------------
|
||||||
|
For voice designer you might want some pre-seeded audio that will hit the google cloud bucket, it will require env-secrets ask Brandon.
|
||||||
|
cargo run --bin dev-database-seed -- --bucket
|
||||||
|
|
||||||
|
Solutions to Common Setup Problems
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
### [Fix] Sqlx Error when Performing Schema Migrations
|
||||||
|
|
||||||
|
You might get this error message during migration,
|
||||||
|
|
||||||
|
```
|
||||||
|
Encountered unknown type for Mysql: enum
|
||||||
|
thread 'main' panicked at 'internal error: entered unreachable code: Mysql only supports a closed set of types.
|
||||||
|
If you ever see this error message please open an issue at https://github.com/diesel-rs/diesel
|
||||||
|
containing a dump of your schema definition.', diesel_cli/src/print_schema.rs:310:17
|
||||||
|
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||||
|
```
|
||||||
|
|
||||||
|
I haven't found the cause (it doesn't happen on newer installs), but the migrations appear to work regardless
|
||||||
|
of this error message. You can essentially ignore it.
|
||||||
|
|
||||||
|
### [Fix] Can't connect to local MySQL after install on Ubuntu
|
||||||
|
|
||||||
|
- If MySql in local dev can't be connected to, [reset the accounts](https://linuxconfig.org/how-to-reset-root-mysql-mariadb-password-on-ubuntu-20-04-focal-fossa-linux).
|
||||||
|
|
||||||
|
### API Server
|
||||||
|
```
|
||||||
|
cargo run --bin docs
|
||||||
|
```
|
||||||
|
This setups an api server with swagger docs to show you responses and requests with each associated endpoint
|
||||||
|
It is run at http://127.0.0.1:8989/
|
||||||
|
To obtain the json for the postman collection go to this url http://127.0.0.1:8989/api-docs/openapi.json
|
||||||
|
We use this: `https://github.com/juhaku/utoipa`
|
||||||
|
The documentation example is in:
|
||||||
|
`crates/service/web/storyteller_web/src/http_server/endpoints/voice_designer/inference/enqueue_tts_request.rs`
|
||||||
|
Write into the api doc to:
|
||||||
|
`crates/service/web/storyteller_web/src/api_doc.rs`
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
README
|
||||||
|
======
|
||||||
|
|
||||||
|
This directory contains a collection of (hopefully organized and up to date) documentation on
|
||||||
|
running the various backend pieces, development setup, production maintenance, etc. Check out
|
||||||
|
all the markdown documents in this directory tree for information organized by topic.
|
||||||
|
|
||||||
|
Development / Setup
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- [Most of the general documents for cross-cutting setup of FakeYou.com live here.](./local_development)
|
||||||
|
- Some per-application documents (such as for `aichatbot-sidecar`) live as READMEs under that particular
|
||||||
|
application's code directory, eg. `/crates/{appCategory}/{appName}/README.md` (These are irrelevant
|
||||||
|
for most folks.)
|
||||||
|
|
||||||
|
Web API Overview
|
||||||
|
----------------
|
||||||
|
|
||||||
|
### FakeYou.com
|
||||||
|
|
||||||
|
* [FakeYou.com](https://fakeyou.com) is powered by the following applications:
|
||||||
|
* HTTP APIs:
|
||||||
|
* `storyteller-web` - HTTP API + database monolith for FakeYou
|
||||||
|
* Asynchronous Jobs:
|
||||||
|
* `download-job` - Handle async download of ML models (RVC, SVC, etc.)
|
||||||
|
* `inference-job` - Run async ML model inference (RVC, SVC, SadTalker, etc.)
|
||||||
|
This will handle most model inference going forward.
|
||||||
|
* `tts-download-job` - (*deprecated*) Async download of Tacotron2 models only
|
||||||
|
* `tts-inference-job` - (*deprecated*) Async TTS inference of Tacotron2 models only
|
||||||
|
* `w2l-download-job` - (*deprecated*) Async download of videos and images
|
||||||
|
* `w2l-inference-job` - (*deprecated*) Async W2L inference
|
||||||
|
* Documentation Server:
|
||||||
|
* `docs` - this will run a local swagger server with the endpoints that you have access to, http://127.0.0.1:8989/api-docs/openapi.json use this to port everything over to the post man.
|
||||||
|
|
||||||
|
### PowerStream
|
||||||
|
|
||||||
|
* [PowerStream](https://power.stream) (deprecated) is powered by the following applications:
|
||||||
|
* HTTP APIs:
|
||||||
|
* `storyteller-web` - HTTP + database monolith for FakeYou ***and*** PowerStream.
|
||||||
|
* Twitch API integration:
|
||||||
|
* `twitch-pubsub-subscriber` - Dynamically subscribes to Twitch PubSub for "active" users
|
||||||
|
* Websocket events:
|
||||||
|
* `websocket-gateway` - Websocket for users' browsers to pull down enriched Twitch events.
|
||||||
|
|
||||||
|
TODO
|
||||||
|
----
|
||||||
|
|
||||||
|
Notes / TODOs:
|
||||||
|
|
||||||
|
* Examples for good Actix+Sqlx Tests:
|
||||||
|
https://stackoverflow.com/questions/65370752/how-do-i-create-an-actix-web-server-that-accepts-both-sqlx-database-pools-and-tr
|
||||||
|
|
||||||
|
* Actix/sqlx runtime compat:
|
||||||
|
https://github.com/launchbadge/sqlx/issues/1117#issuecomment-801237734
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
development notes
|
||||||
|
=================
|
||||||
|
|
||||||
|
Actix notes
|
||||||
|
-----------
|
||||||
|
|
||||||
|
json request
|
||||||
|
```
|
||||||
|
async fn handler(request: web::Json<Mytype>) -> impl Responder { "whatever" }
|
||||||
|
```
|
||||||
|
|
||||||
|
form-multipart request
|
||||||
|
```
|
||||||
|
async fn handler(request: web::Form<Mytype>) -> impl Responder { "whatever" }
|
||||||
|
```
|
||||||
|
|
||||||
|
route parameters
|
||||||
|
```
|
||||||
|
#[get("/{name}")]
|
||||||
|
async fn hello(name: web::Path<String>) -> impl Responder { format!("Hi {}", name) }
|
||||||
|
```
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
Migrations guide
|
||||||
|
================
|
||||||
|
|
||||||
|
## Diesel Migrations
|
||||||
|
|
||||||
|
To run pending migrations, execute the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
diesel migration run
|
||||||
|
```
|
||||||
|
|
||||||
|
To revert the last 3 migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
diesel migration revert -n 3
|
||||||
|
```
|
||||||
|
|
||||||
|
To reset the entire database (drop, migrate), run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
diesel database reset
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Query Codegen
|
||||||
|
|
||||||
|
We use SQLx instead of Diesel in the production server. It's typesafe
|
||||||
|
SQL instead of an ORM like Diesel.
|
||||||
|
|
||||||
|
SQLx connects to a database to derive type information, but obviously
|
||||||
|
cannot do this for builds in CI. In order to cache the types, we build
|
||||||
|
and check in a cache file (necessary for builds):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SQLX_OFFLINE=true cargo sqlx prepare
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we have multiple binaries, it's required to include all the queries in the main
|
||||||
|
binary so we can generate the cached queries as a single target. That's then executed
|
||||||
|
with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SQLX_OFFLINE=true cargo sqlx prepare -- --bin storyteller-web
|
||||||
|
```
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Local Kubernetes environment for standing up the APIs and their dependencies
|
||||||
|
|
||||||
|
(These are Paul's notes.)
|
||||||
|
|
||||||
|
- Install minikube, kubectl, docker, and musl from your OS package manager
|
||||||
|
- `rustup target add x86_64-unknown-linux-musl`
|
||||||
|
- `cargo build --release --bin storyteller-web --target=x86_64-unknown-linux-musl`
|
||||||
|
- `minikube start --extra-config=apiserver.service-node-port-range=1-65535`
|
||||||
|
- `eval $(minikube docker-env)`
|
||||||
|
- `minikube mount /path/to/storyteller-rust/:/storyteller-rust &> /dev/null &`
|
||||||
|
- `docker build -t storyteller-web -f localdev/Dockerfile .`
|
||||||
|
- `kubectl apply -f localdev/kubernetes.yml`
|
||||||
|
- Get the ip address of minikube with `minikube ip`, this is where all the services will be running on their
|
||||||
|
regular ports (12345 for the api, 6379 for redis, 3306 for mysql)
|
||||||
|
- That's it, we just spun up mysql, applied database migrations, spun up redis, and spun up our api
|
||||||
|
|
||||||
|
# Development tips
|
||||||
|
- Rebuild and "deploy" (locally) with `cargo build --release --bin storyteller-web --target=x86_64-unknown-linux-musl && kubectl rollout restart storyteller-web`
|
||||||
|
- View logs from the api with `kubectl logs storyteller-web<TAB>`
|
||||||
|
- Delete everything minikube (if you start running out of disk space or what-have-you) with `minikube delete`
|
||||||
|
- See pod status with `kubectl get pods`
|
||||||
|
- If there are errors you can get more info with `kubectl describe pods`
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
Local Development nginx proxy
|
||||||
|
=============================
|
||||||
|
|
||||||
|
It isn't necessary to use nginx for development, but it can be nice. Nginx allows for a more
|
||||||
|
sophisticated testing setup that makes cookie and CORS issues less of a pain.
|
||||||
|
|
||||||
|
You can use nginx to serve (locally-signed) SSL, redirect traffic to different ports, or run
|
||||||
|
multiple copies of the app side by side.
|
||||||
|
|
||||||
|
### Hosts file
|
||||||
|
|
||||||
|
Set the following configs in your `/etc/hosts` file (as you would probably do even without nginx),
|
||||||
|
|
||||||
|
```
|
||||||
|
127.0.0.1 dev.fakeyou.com
|
||||||
|
127.0.0.1 api.dev.fakeyou.com
|
||||||
|
127.0.0.1 devproxy.fakeyou.com
|
||||||
|
|
||||||
|
127.0.0.1 dev.storyteller.ai
|
||||||
|
127.0.0.1 api.dev.storyteller.ai
|
||||||
|
127.0.0.1 devproxy.storyteller.ai
|
||||||
|
|
||||||
|
# Deprecated development hostnames :
|
||||||
|
127.0.0.1 jungle.horse
|
||||||
|
127.0.0.1 api.jungle.horse
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nginx installation and administration
|
||||||
|
|
||||||
|
Configure Nginx per the checked in Nginx configs (and instructions) in
|
||||||
|
the directory `/_develop/localdev/nginx-http-config`.
|
||||||
|
|
||||||
|
This should be straightforward, but if it isn't, contribute docs here.
|
||||||
|
|
||||||
|
### [Fix] Python 3.6 on Apple M1 Mac
|
||||||
|
|
||||||
|
Python3.6 isn't supported on Apple silicon, and it's not in homebrew. It can be installed with
|
||||||
|
[nix using Rosetta](https://stackoverflow.com/a/65980989):
|
||||||
|
|
||||||
|
Download: https://nixos.org/download.html#nix-quick-install
|
||||||
|
|
||||||
|
```
|
||||||
|
nix run nixpkgs.python36 -c python
|
||||||
|
```
|
||||||
|
|
||||||
|
Install venv:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix run nixpkgs.python36 -c python -m venv python
|
||||||
|
```
|
||||||
|
|
||||||
|
Install other packages on Mac that aren't used in venv:
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 -m pip install --user requests gdown youtube_dl
|
||||||
|
```
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
production builds and deployment
|
||||||
|
================================
|
||||||
|
|
||||||
|
### Github Docker builds
|
||||||
|
|
||||||
|
The repository needs to be given read access to the base docker image:
|
||||||
|
|
||||||
|
https://github.com/orgs/storytold/packages/container/docker-base-images-rust-ssl/settings
|
||||||
|
|
||||||
|
These instructions assume running on GCP.
|
||||||
|
|
||||||
|
### Database migrations
|
||||||
|
|
||||||
|
1. Set `DATABASE_URL` in `.env` to the production secrets (DO NOT COMMIT!)
|
||||||
|
2. Run `diesel migration run`
|
||||||
|
|
||||||
|
### Setting up public buckets without list permission
|
||||||
|
|
||||||
|
Public buckets that deny the `list` action should use the following Role:
|
||||||
|
|
||||||
|
`roles/storage.legacyObjectReader`
|
||||||
|
|
||||||
|
See:
|
||||||
|
|
||||||
|
* https://stackoverflow.com/a/56354633
|
||||||
|
* https://cloud.google.com/storage/docs/access-control/making-data-public#buckets
|
||||||
|
|
||||||
|
### Generating Bucket Access Key and Secret Key
|
||||||
|
|
||||||
|
https://cloud.telestream.net/tutorials/how-to-setting-up-google-cloud-storage/
|
||||||
|
|
||||||
|
1. Go to the GCS page
|
||||||
|
2. Click "settings"
|
||||||
|
3. Click "interoperability" tab
|
||||||
|
4. (enable interoperable access if not already set)
|
||||||
|
5. Click "create new key"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Production Database
|
||||||
|
===================
|
||||||
|
|
||||||
|
Secrets can be subbed in for `.env` and the standard diesel migration tooling will work.
|
||||||
|
|
||||||
|
Dump a MySQL report
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysql -u storyteller \
|
||||||
|
-pPasswordHere \
|
||||||
|
-h IP_HERE \
|
||||||
|
-P 3306 \
|
||||||
|
-D storyteller \
|
||||||
|
-B \
|
||||||
|
-e "select * from voice_clone_requests;" | sed "s/'/\'/;s/\t/\",\"/g;s/^/\"/;s/$/\"/;s/\n//g" > voice_clone_report.csv
|
||||||
|
```
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
database schema
|
||||||
|
================
|
||||||
|
|
||||||
|
(This is woefully incomplete and unmaintained.)
|
||||||
|
|
||||||
|
Twitch Model
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `twitch_user_id`
|
||||||
|
Though numeric, this should always be a string per
|
||||||
|
[https://discuss.dev.twitch.tv/t/bug-v5-api-returns--id-as-string-for-featured-channels/10310]
|
||||||
|
and
|
||||||
|
[https://discuss.dev.twitch.tv/t/type-of-user-id-in-api-responses/10205]
|
||||||
|
|
||||||
|
- `twitch_channel_id`
|
||||||
|
This is the same as `twitch_user_id`. They're exactly 1:1. Prefer to
|
||||||
|
use 'user_id' terminology.
|
||||||
|
|
||||||
|
- `twitch_username`
|
||||||
|
Between 4 and 25 characters.
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
redis schema
|
||||||
|
============
|
||||||
|
|
||||||
|
Database 0: TTS / RateLimiter
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
TODO: Document rate limiter
|
||||||
|
|
||||||
|
Public statistics on usage displayed on the website:
|
||||||
|
|
||||||
|
* `ttsUseCount:{model_token}`: INCR, GET
|
||||||
|
* `w2lUseCount:{template_token}`: INCR, GET
|
||||||
|
|
||||||
|
Temporary job progress reports to the user:
|
||||||
|
|
||||||
|
These are written as a single string, such as "downloading model", "uploading model",
|
||||||
|
"generating preview", "executing", etc.
|
||||||
|
|
||||||
|
* `ttsDownloadExtraStatus:{inference_job_token}`: SETEX, GET
|
||||||
|
* `ttsInferenceExtraStatus:{inference_job_token}`: SETEX, GET
|
||||||
|
* `w2lDownloadExtraStatus:{inference_job_token}`: SETEX, GET
|
||||||
|
* `w2lInferenceExtraStatus:{inference_job_token}`: SETEX, GET
|
||||||
|
|
||||||
|
Database 1: Obs / Twitch
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
* WIP
|
||||||
@@ -0,0 +1,625 @@
|
|||||||
|
# ArtCraft Omni API
|
||||||
|
|
||||||
|
> # 📚 Full API reference: **<https://storyteller-docs.netlify.app/>**
|
||||||
|
>
|
||||||
|
> **Every ArtCraft API endpoint is documented there.** This guide covers only the Omni API video
|
||||||
|
> endpoints — for everything else, start at the link above.
|
||||||
|
|
||||||
|
The **Omni API** is the API-key–authenticated surface for ArtCraft generation. It mirrors the
|
||||||
|
in-app "Omni Gen" endpoints but is designed for programmatic use: you authenticate with an API key
|
||||||
|
in the `Authorization` header instead of a browser session cookie.
|
||||||
|
|
||||||
|
This guide covers **video generation from URL inputs** — passing image, video, and audio reference
|
||||||
|
URLs directly. The server downloads each URL, stores it as a media file on your account, and then
|
||||||
|
runs generation against it. You never have to upload files yourself.
|
||||||
|
|
||||||
|
> All examples send **no cookies**. The Omni API is authenticated purely by your API key. If you
|
||||||
|
> send session cookies they are ignored.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base URLs
|
||||||
|
|
||||||
|
| Environment | Base URL |
|
||||||
|
| ------------- | ------------------------------ |
|
||||||
|
| Development | `http://localhost:12345` |
|
||||||
|
| Production | `https://api.storyteller.ai` |
|
||||||
|
|
||||||
|
All paths below are relative to the base URL (e.g. `POST /v1/omni_api/generate/video`).
|
||||||
|
|
||||||
|
The examples in this guide use the **development** base URL. To run against production, swap
|
||||||
|
`http://localhost:12345` for `https://api.storyteller.ai`.
|
||||||
|
|
||||||
|
Note: Our APIs are hosted on "`api.storyteller.ai`", not "`api.getartcraft.com`". This is not a
|
||||||
|
typo. Our Cloudflare CDN URLs are `cdn-2.fakeyou.com`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting an API key
|
||||||
|
|
||||||
|
Create and manage API keys from the ArtCraft web app:
|
||||||
|
|
||||||
|
1. Go to **<https://app.getartcraft.com/>**
|
||||||
|
2. Click **Account → Settings → API Keys** (your account must have API keys enabled from our staff)
|
||||||
|
3. Create a key and copy it. The secret is shown **once** — store it somewhere safe.
|
||||||
|
|
||||||
|
> **Don't see the API Keys section?** API access is gated per account. Contact the Artcraft team
|
||||||
|
> to enable it.
|
||||||
|
|
||||||
|
### What an API key looks like
|
||||||
|
|
||||||
|
- Prefix: `artcraft_api_`
|
||||||
|
- Followed by 40 random lowercase Crockford-base32 characters (`0-9 a-z`, excluding `i l o u`)
|
||||||
|
- Total length: **53 characters**, e.g. `artcraft_api_3k7q9w0xv2hs5n8m4d6r1t8y0p2s4f6h8j0k2m4n6`
|
||||||
|
|
||||||
|
Treat the key like a password: never commit it to source control or paste it into logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Send the secret key in the `Authorization` header. Three forms are accepted:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
Authorization: Key artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
Authorization: artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
`Bearer` is recommended. **Do not send cookies.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Video generation — `POST /v1/omni_api/generate/video`
|
||||||
|
|
||||||
|
Generates a video and returns an inference job token you can poll for results.
|
||||||
|
|
||||||
|
### Request body
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
| ---------------------------- | --------------- | ----- |
|
||||||
|
| `model` | string | Generation model, e.g. `seedance_2p0`. **Required.** |
|
||||||
|
| `prompt` | string | Text prompt. |
|
||||||
|
| `duration_seconds` | integer | Length of the output video, e.g. `6`. |
|
||||||
|
| `aspect_ratio` | string | e.g. `wide_sixteen_by_nine`, `tall_nine_by_sixteen`, `square`. |
|
||||||
|
| `idempotency_token` | string (UUID) | **Required.** A fresh UUID per request; prevents accidental duplicates. |
|
||||||
|
| `negative_prompt` | string | Optional. |
|
||||||
|
| `resolution` | string | Optional, e.g. `seven_twenty_p`, `ten_eighty_p`. |
|
||||||
|
| `generate_audio` | boolean | Optional. |
|
||||||
|
| **URL inputs** (this guide) | | |
|
||||||
|
| `start_frame_image_url` | string (URL) | Starting keyframe image. |
|
||||||
|
| `end_frame_image_url` | string (URL) | Ending keyframe image. |
|
||||||
|
| `reference_image_urls` | array of URL | Reference images. |
|
||||||
|
| `reference_video_urls` | array of URL | Reference videos (`mp4`). |
|
||||||
|
| `reference_audio_urls` | array of URL | Reference audio (`wav`, `mp3`, `ogg`, …). |
|
||||||
|
| **Media-token inputs** | | |
|
||||||
|
| `start_frame_image_media_token`, `end_frame_image_media_token`, `reference_image_media_tokens`, `reference_video_media_tokens`, `reference_audio_media_tokens` | token(s) | Use these instead if you've already uploaded media. |
|
||||||
|
|
||||||
|
#### Rules for URL inputs
|
||||||
|
|
||||||
|
- Each URL field and its media-token counterpart are **mutually exclusive**. Sending both
|
||||||
|
`reference_image_urls` and `reference_image_media_tokens`, for example, returns:
|
||||||
|
`Either reference_image_media_tokens or reference_image_urls must be set, not both`.
|
||||||
|
- Every URL must start with `http://` or `https://`, otherwise:
|
||||||
|
`URL must start with http:// or https://, bad URL: <url>`.
|
||||||
|
- Redirects are followed (up to 10), so CDN/redirecting URLs work.
|
||||||
|
- The file type is detected from the downloaded bytes. Images must be a real image
|
||||||
|
(`jpeg`/`png`/`gif`/`webp`), reference videos must be `mp4`, reference audio must be a
|
||||||
|
supported audio type (`wav`, `mp3`, `aac`, `ogg`, `flac`, …).
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"inference_job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"all_job_tokens": ["jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `inference_job_token` to poll for the finished video. `all_job_tokens` lists every job created
|
||||||
|
(batch requests can create more than one).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
The examples reuse these public sample URLs:
|
||||||
|
|
||||||
|
- Images (Wikimedia Commons): an Atlantic puffin, the Matterhorn, a toco toucan
|
||||||
|
- Video: a short sample `mp4`
|
||||||
|
- Audio: a short sample `wav`
|
||||||
|
|
||||||
|
Replace `YOUR_API_KEY` with your `artcraft_api_…` secret, and generate a fresh `idempotency_token`
|
||||||
|
(UUID) for every request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 1 — Starting-frame Seedance (`start_frame_image_url`)
|
||||||
|
|
||||||
|
Animate outward from a single starting image.
|
||||||
|
|
||||||
|
#### bash
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
|
||||||
|
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "seedance_2p0",
|
||||||
|
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
|
||||||
|
"duration_seconds": 6,
|
||||||
|
"aspect_ratio": "wide_sixteen_by_nine",
|
||||||
|
"idempotency_token": "'"$(uuidgen)"'",
|
||||||
|
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import uuid
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
|
||||||
|
API_KEY = "YOUR_API_KEY"
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
f"{BASE_URL}/v1/omni_api/generate/video",
|
||||||
|
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||||
|
json={
|
||||||
|
"model": "seedance_2p0",
|
||||||
|
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
|
||||||
|
"duration_seconds": 6,
|
||||||
|
"aspect_ratio": "wide_sixteen_by_nine",
|
||||||
|
"idempotency_token": str(uuid.uuid4()),
|
||||||
|
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||||||
|
},
|
||||||
|
timeout=240,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
print(resp.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
#### javascript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
|
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
|
||||||
|
const API_KEY = "YOUR_API_KEY";
|
||||||
|
|
||||||
|
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${API_KEY}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "seedance_2p0",
|
||||||
|
prompt:
|
||||||
|
"An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
|
||||||
|
duration_seconds: 6,
|
||||||
|
aspect_ratio: "wide_sixteen_by_nine",
|
||||||
|
idempotency_token: randomUUID(),
|
||||||
|
start_frame_image_url:
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(resp.status, await resp.json());
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 2 — Image-reference Seedance (`reference_image_urls`)
|
||||||
|
|
||||||
|
Guide the generation with one or more reference images.
|
||||||
|
|
||||||
|
#### bash
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
|
||||||
|
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "seedance_2p0",
|
||||||
|
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
|
||||||
|
"duration_seconds": 6,
|
||||||
|
"aspect_ratio": "wide_sixteen_by_nine",
|
||||||
|
"idempotency_token": "'"$(uuidgen)"'",
|
||||||
|
"reference_image_urls": [
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import uuid
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
|
||||||
|
API_KEY = "YOUR_API_KEY"
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
f"{BASE_URL}/v1/omni_api/generate/video",
|
||||||
|
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||||
|
json={
|
||||||
|
"model": "seedance_2p0",
|
||||||
|
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
|
||||||
|
"duration_seconds": 6,
|
||||||
|
"aspect_ratio": "wide_sixteen_by_nine",
|
||||||
|
"idempotency_token": str(uuid.uuid4()),
|
||||||
|
"reference_image_urls": [
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
timeout=240,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
print(resp.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
#### javascript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
|
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
|
||||||
|
const API_KEY = "YOUR_API_KEY";
|
||||||
|
|
||||||
|
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${API_KEY}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "seedance_2p0",
|
||||||
|
prompt:
|
||||||
|
"A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
|
||||||
|
duration_seconds: 6,
|
||||||
|
aspect_ratio: "wide_sixteen_by_nine",
|
||||||
|
idempotency_token: randomUUID(),
|
||||||
|
reference_image_urls: [
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(resp.status, await resp.json());
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example 3 — Image + video reference Seedance (`reference_image_urls` + `reference_video_urls`)
|
||||||
|
|
||||||
|
Combine reference images with a reference video (and, optionally, reference audio). This is the
|
||||||
|
fullest URL-driven request.
|
||||||
|
|
||||||
|
#### bash
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
|
||||||
|
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "seedance_2p0",
|
||||||
|
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
|
||||||
|
"duration_seconds": 6,
|
||||||
|
"aspect_ratio": "wide_sixteen_by_nine",
|
||||||
|
"idempotency_token": "'"$(uuidgen)"'",
|
||||||
|
"reference_image_urls": [
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
|
||||||
|
],
|
||||||
|
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
|
||||||
|
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import uuid
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
|
||||||
|
API_KEY = "YOUR_API_KEY"
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
f"{BASE_URL}/v1/omni_api/generate/video",
|
||||||
|
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||||
|
json={
|
||||||
|
"model": "seedance_2p0",
|
||||||
|
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
|
||||||
|
"duration_seconds": 6,
|
||||||
|
"aspect_ratio": "wide_sixteen_by_nine",
|
||||||
|
"idempotency_token": str(uuid.uuid4()),
|
||||||
|
"reference_image_urls": [
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
|
||||||
|
],
|
||||||
|
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
|
||||||
|
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"],
|
||||||
|
},
|
||||||
|
timeout=240,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
print(resp.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
#### javascript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
|
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
|
||||||
|
const API_KEY = "YOUR_API_KEY";
|
||||||
|
|
||||||
|
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${API_KEY}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "seedance_2p0",
|
||||||
|
prompt:
|
||||||
|
"An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
|
||||||
|
duration_seconds: 6,
|
||||||
|
aspect_ratio: "wide_sixteen_by_nine",
|
||||||
|
idempotency_token: randomUUID(),
|
||||||
|
reference_image_urls: [
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||||||
|
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
|
||||||
|
],
|
||||||
|
reference_video_urls: ["https://download.samplelib.com/mp4/sample-5s.mp4"],
|
||||||
|
reference_audio_urls: ["https://download.samplelib.com/wav/sample-6s.wav"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(resp.status, await resp.json());
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checking job status
|
||||||
|
|
||||||
|
Generation is asynchronous. The generate call returns an `inference_job_token`; poll the job-status
|
||||||
|
endpoint until the job reaches a terminal state, then read the result URL.
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
- **Single job:** `GET /v1/omni_api/job_status/job/{token}`
|
||||||
|
- **Batch (many jobs):** `GET /v1/omni_api/job_status/batch?tokens=jinf_aaa&tokens=jinf_bbb`
|
||||||
|
|
||||||
|
Both require the same `Authorization: Bearer …` header (no cookies).
|
||||||
|
|
||||||
|
### Response shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"state": {
|
||||||
|
"job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"request": {
|
||||||
|
"inference_category": "video_generation",
|
||||||
|
"maybe_prompt_token": "prompt_xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"maybe_model_type": "seedance_2p0",
|
||||||
|
"maybe_model_token": null
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"status": "pending",
|
||||||
|
"maybe_first_started_at": null,
|
||||||
|
"maybe_failure_category": null,
|
||||||
|
"progress_percentage": 0
|
||||||
|
},
|
||||||
|
"maybe_result": null,
|
||||||
|
"created_at": "2026-06-24T04:06:14Z",
|
||||||
|
"updated_at": "2026-06-24T04:06:14Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `state.status.status` — the job state. Keep polling while it's `pending`, `started`, or
|
||||||
|
`attempt_failed`. **Terminal** states are `complete_success`, `complete_failure`, `dead`,
|
||||||
|
`cancelled_by_user`, and `cancelled_by_system`.
|
||||||
|
- `state.status.progress_percentage` — an integer 0–100.
|
||||||
|
- `state.maybe_result` — `null` until the job succeeds. On `complete_success` it contains the
|
||||||
|
finished media; the playable/downloadable URL is **`state.maybe_result.media_links.cdn_url`**.
|
||||||
|
For video, `media_links.maybe_video_previews` also carries still/animated preview URLs.
|
||||||
|
|
||||||
|
The batch endpoint returns `{ "success": true, "job_states": [ … ] }` where each entry is the same
|
||||||
|
`state` payload as above.
|
||||||
|
|
||||||
|
### Where the result video / image lives
|
||||||
|
|
||||||
|
When `state.status.status` is `complete_success`, the finished media is under `state.maybe_result`:
|
||||||
|
|
||||||
|
- **`state.maybe_result.media_links.cdn_url`** — the primary, playable/downloadable URL of the
|
||||||
|
finished asset. **This is the field you want.** For a video job it's the `.mp4`; for an image job
|
||||||
|
it's the image file (`.jpg` / `.png` / etc.).
|
||||||
|
- `state.maybe_result.entity_token` — the `m_…` media-file token for the result.
|
||||||
|
- `state.maybe_result.media_links.maybe_video_previews` — **video jobs only**. Contains a `still`
|
||||||
|
(poster `.jpg`) and an `animated` (`.gif`) preview URL, plus `{WIDTH}` thumbnail templates. This
|
||||||
|
is `null` for image jobs.
|
||||||
|
- `state.maybe_result.media_links.maybe_thumbnail_template` — **image jobs only**. A URL template;
|
||||||
|
replace `{WIDTH}` with a pixel width to fetch a resized thumbnail. `null` for video jobs.
|
||||||
|
|
||||||
|
**Example — completed video job:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"state": {
|
||||||
|
"job_token": "jinf_9fcn2qv4dv3vvbrrxbnmfgen06v",
|
||||||
|
"request": {
|
||||||
|
"inference_category": "video_generation",
|
||||||
|
"maybe_prompt_token": "prompt_q53tp4rh97qh8q159p21tazzt",
|
||||||
|
"maybe_model_type": "seedance_2p0",
|
||||||
|
"maybe_model_token": null
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"status": "complete_success",
|
||||||
|
"maybe_first_started_at": "2026-06-24T04:07:11Z",
|
||||||
|
"maybe_failure_category": null,
|
||||||
|
"progress_percentage": 100
|
||||||
|
},
|
||||||
|
"maybe_result": {
|
||||||
|
"entity_type": "media_file",
|
||||||
|
"entity_token": "m_ss682134tx19yhqs5y0nbnd0bd59a6",
|
||||||
|
"media_links": {
|
||||||
|
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4",
|
||||||
|
"maybe_thumbnail_template": null,
|
||||||
|
"maybe_video_previews": {
|
||||||
|
"still": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.jpg",
|
||||||
|
"animated": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.gif",
|
||||||
|
"still_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.jpg",
|
||||||
|
"animated_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.gif"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maybe_successfully_completed_at": "2026-06-24T04:17:30Z"
|
||||||
|
},
|
||||||
|
"created_at": "2026-06-24T04:06:14Z",
|
||||||
|
"updated_at": "2026-06-24T04:17:30Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
→ The video is at
|
||||||
|
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4`
|
||||||
|
(example, not a live URL).
|
||||||
|
|
||||||
|
**Example — completed image job** (from `POST /v1/omni_api/generate/image`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"maybe_result": {
|
||||||
|
"entity_type": "media_file",
|
||||||
|
"entity_token": "m_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6",
|
||||||
|
"media_links": {
|
||||||
|
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg",
|
||||||
|
"maybe_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg-thumb-{WIDTH}.jpg",
|
||||||
|
"maybe_video_previews": null
|
||||||
|
},
|
||||||
|
"maybe_successfully_completed_at": "2026-06-24T04:09:02Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
→ The image is at
|
||||||
|
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg`
|
||||||
|
(example, not a live URL).
|
||||||
|
|
||||||
|
### Poll until complete
|
||||||
|
|
||||||
|
#### bash
|
||||||
|
|
||||||
|
```bash
|
||||||
|
JOB_TOKEN="jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
resp=$(curl -s "http://localhost:12345/v1/omni_api/job_status/job/$JOB_TOKEN" \
|
||||||
|
-H "Authorization: Bearer YOUR_API_KEY")
|
||||||
|
status=$(echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['status']['status'])")
|
||||||
|
echo "status=$status"
|
||||||
|
case "$status" in
|
||||||
|
complete_success)
|
||||||
|
echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['maybe_result']['media_links']['cdn_url'])"
|
||||||
|
break ;;
|
||||||
|
complete_failure|dead|cancelled_by_user|cancelled_by_system)
|
||||||
|
echo "job ended without a result: $status"; break ;;
|
||||||
|
esac
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
#### python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
|
||||||
|
API_KEY = "YOUR_API_KEY"
|
||||||
|
JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
||||||
|
TERMINAL_FAILURES = {"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system"}
|
||||||
|
|
||||||
|
while True:
|
||||||
|
resp = requests.get(
|
||||||
|
f"{BASE_URL}/v1/omni_api/job_status/job/{JOB_TOKEN}",
|
||||||
|
headers={"Authorization": f"Bearer {API_KEY}"},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
state = resp.json()["state"]
|
||||||
|
status = state["status"]["status"]
|
||||||
|
print("status:", status, state["status"]["progress_percentage"], "%")
|
||||||
|
|
||||||
|
if status == "complete_success":
|
||||||
|
print("result URL:", state["maybe_result"]["media_links"]["cdn_url"])
|
||||||
|
break
|
||||||
|
if status in TERMINAL_FAILURES:
|
||||||
|
print("job ended without a result:", status)
|
||||||
|
break
|
||||||
|
time.sleep(5)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### javascript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
|
||||||
|
const API_KEY = "YOUR_API_KEY";
|
||||||
|
const JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx";
|
||||||
|
|
||||||
|
const TERMINAL_FAILURES = new Set([
|
||||||
|
"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system",
|
||||||
|
]);
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const resp = await fetch(`${BASE_URL}/v1/omni_api/job_status/job/${JOB_TOKEN}`, {
|
||||||
|
headers: { Authorization: `Bearer ${API_KEY}` },
|
||||||
|
});
|
||||||
|
const { state } = await resp.json();
|
||||||
|
const status = state.status.status;
|
||||||
|
console.log("status:", status, state.status.progress_percentage, "%");
|
||||||
|
|
||||||
|
if (status === "complete_success") {
|
||||||
|
console.log("result URL:", state.maybe_result.media_links.cdn_url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (TERMINAL_FAILURES.has(status)) {
|
||||||
|
console.log("job ended without a result:", status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await sleep(5000);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes & troubleshooting
|
||||||
|
|
||||||
|
- **Always send a fresh `idempotency_token`** (UUID). Reusing one is rejected as a duplicate.
|
||||||
|
- **No cookies.** The endpoint authenticates only via the `Authorization` header.
|
||||||
|
- **URL must be `http(s)`** and reachable by the server; redirects are followed.
|
||||||
|
- **Type mismatches** (e.g. a `.webm` passed as a reference video, which requires `mp4`) are
|
||||||
|
rejected with a `400` describing the unpermitted type.
|
||||||
|
- **`401 Unauthorized`** — missing/invalid API key, or the key's owner is banned.
|
||||||
|
- **`402 Payment required`** — insufficient credits/balance.
|
||||||
|
- There is a companion image endpoint, `POST /v1/omni_api/generate/image`, which accepts
|
||||||
|
`image_urls` (mutually exclusive with `image_media_tokens`) following the same rules.
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# PageDraw — Visual Design & Architecture Review
|
||||||
|
|
||||||
|
**Surface:** `@storyteller/ui-pagedraw` image editor + its `artcraft-webapp` integration (`/edit-image`).
|
||||||
|
**Lens:** Awwwards / Linear-tier "premium app chrome" rubric, adapted for an in-product creative tool.
|
||||||
|
**Date:** 2026-06-15 · **Branch:** `worktree-pagedraw-webapp`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. How to read this
|
||||||
|
|
||||||
|
PageDraw is a **professional canvas tool**, not a marketing page. So the elite-design rubric is
|
||||||
|
applied in its *app-chrome* register — the north star is **Figma / Linear / Photoshop-web**, not an
|
||||||
|
Awwwards landing page. That means:
|
||||||
|
|
||||||
|
- **Applies:** token discipline, concentric radii, hairline + inset-highlight surfaces ("double-bezel"),
|
||||||
|
island/pill controls, custom-eased micro-motion, z-index hygiene, premium type ramp.
|
||||||
|
- **Does _not_ apply:** `py-40` macro-whitespace, hero typography, editorial-split layouts, scroll-reveal
|
||||||
|
entrances. A tool surface is dense and persistent by design; forcing landing-page whitespace would hurt it.
|
||||||
|
|
||||||
|
Citations are `file:line` against the current tree.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Verdict
|
||||||
|
|
||||||
|
| Axis | Score | One-liner |
|
||||||
|
|---|---|---|
|
||||||
|
| Color system | ⚠️ 5/10 | Good token foundation, but hardcoded greys (`#1b1b1b`, `#303030`, `#404040`) leak through a *themeable lib*. |
|
||||||
|
| Typography | ❌ 3/10 | No type ramp, no font-family ownership; rides whatever the host sets. Banned-by-rubric default stack risk. |
|
||||||
|
| Radii | ⚠️ 4/10 | Five radius scales in play (`sm`→`2xl`→`full`) with no concentric nesting. |
|
||||||
|
| Surfaces (border/shadow) | ⚠️ 5/10 | Flat `shadow-lg` everywhere; no inset highlights; no double-bezel. Reads "functional", not "machined". |
|
||||||
|
| Motion | ❌ 3/10 | Only `transition-colors`/`ease-in-out`, durations 75–200ms; **zero custom easing**, no spring, no entrance choreography. |
|
||||||
|
| Layout / z-index | ⚠️ 6/10 | Clean fixed-panel composition, but a `z-[9999999]` escape hatch and a 6-tier ad-hoc z-stack. |
|
||||||
|
| Icons | ✅ 7/10 | FontAwesome Pro (solid + regular) — acceptable, but solid weight is heavier than the rubric's "ultra-light line" ideal. |
|
||||||
|
| Architecture | ⚠️ 5/10 | Clean adapter boundary; but `PaintSurface.tsx` (2005 LOC), `SceneState.ts` (1539), `PageDraw.tsx` (1292) are oversized, and the lib hardcodes host-theme colors. |
|
||||||
|
|
||||||
|
**Bottom line:** the editor is *structurally competent and functional* but visually reads as
|
||||||
|
"capable internal tool," not "$150k product surface." The single highest-leverage fix is a **shared
|
||||||
|
design-token layer** (color + radius + motion + type), because today the lib both (a) reaches for
|
||||||
|
host tokens (`bg-ui-panel`, `text-base-fg`) *and* (b) hardcodes its own greys — so it can never be
|
||||||
|
fully reskinned by the webapp vs. the desktop app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. What's already good (keep)
|
||||||
|
|
||||||
|
- **Token-aware base.** Most surfaces use semantic tokens (`bg-ui-panel`, `bg-ui-controls`,
|
||||||
|
`text-base-fg`, `border-ui-panel-border`) — the right instinct (SideToolbar, PromptEditor, HistoryStack).
|
||||||
|
- **Clean platform boundary.** The lib has zero REST/Tauri imports; everything I/O flows through
|
||||||
|
`PageDrawAdapter` (`adapter.ts`). The webapp adapter (`web-adapter.tsx`) and desktop adapter stay
|
||||||
|
swappable. This is genuinely good architecture and should be protected.
|
||||||
|
- **Performance-minded canvas.** Composite pre-baking on idle (`PageDraw.tsx` ~661–710) and worker
|
||||||
|
encoding (`generatePipeline.worker.ts`) keep the generate click off the main thread. Don't regress this.
|
||||||
|
- **CORS already handled** for browser canvas export (`crossOrigin="anonymous"` in `Node.ts:134`,
|
||||||
|
`SceneState.ts:1473`, `imageHelpers.ts:16`) — the web port works because of this.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Findings & recommendations
|
||||||
|
|
||||||
|
### 3.1 Color — stop hardcoding greys in a themeable lib ⚠️ P0
|
||||||
|
**Current:** semantic tokens *and* raw hex coexist:
|
||||||
|
- `#1b1b1b` pegboard (`pagedraw.css:6`), `#1A1A1A`/`#333333` context menu (`ContextMenu.tsx:44`),
|
||||||
|
`#303030`/`#404040` dividers + popovers (`SideToolbar.tsx:381,550`), `bg-zinc-700/600` swatch
|
||||||
|
(`SideToolbar.tsx:173`), plus ad-hoc `bg-blue-600/500` accents (`PageDraw.tsx:157`, base selector).
|
||||||
|
- Accent color is inconsistent: the lib's primary action uses raw **blue-600**, while the webapp's
|
||||||
|
design system uses `--primary` / `brand-primary`. So the editor's blue doesn't match the product.
|
||||||
|
|
||||||
|
**Why it fails the bar:** a premium component library exposes *one* surface ramp and *one* accent, both
|
||||||
|
driven by tokens, so it reskins per host. Hardcoded greys mean the desktop and web themes can drift, and
|
||||||
|
the raw `blue-600` clashes with `brand-primary` used elsewhere in the webapp.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
- Replace every raw grey with the existing ramp: `#1b1b1b`→`bg-ui-background`, `#1A1A1A`→`bg-ui-panel`,
|
||||||
|
`#303030`/`#404040`→`border-ui-panel-border` / `bg-ui-controls`. For the pegboard, add a token
|
||||||
|
`--st-canvas` so hosts can theme the canvas backdrop.
|
||||||
|
- Replace `blue-600/500` accents with `bg-primary` / `text-primary` (the token the rest of the webapp uses),
|
||||||
|
so the Generate/Edit-3D buttons match `brand-primary`.
|
||||||
|
- Move the react-colorful overrides off pure `rgb(255,255,255)` borders (`pagedraw.css:15,19`) onto
|
||||||
|
`white/15` hairlines for consistency with the rest of the chrome.
|
||||||
|
|
||||||
|
### 3.2 Typography — own a type ramp ❌ P0
|
||||||
|
**Current:** no `font-family` anywhere in the lib (grep: 0 hits); only `text-xs/sm/base/lg/xl` +
|
||||||
|
`font-medium/semibold/bold`. The editor inherits whatever the host's body font is.
|
||||||
|
|
||||||
|
**Why it fails the bar:** the rubric explicitly bans the default web stack (Inter/Roboto/Arial/Helvetica).
|
||||||
|
Inheriting an unspecified stack means the tool can render in a banned font with no recourse, and there's
|
||||||
|
no deliberate hierarchy (numerics in sliders, labels, modal titles all share one undifferentiated ramp).
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
- Adopt the product's display/grotesk token (e.g. a `font-sans` mapped to Geist / Plus Jakarta Sans at
|
||||||
|
the app level) and reference it in the lib's root container, not per-component.
|
||||||
|
- Define a small semantic ramp: `text-[11px] tracking-[0.16em] uppercase` for toolbar/section labels,
|
||||||
|
`text-sm` for controls, `text-base font-semibold` for modal titles. Apply `tabular-nums` to slider
|
||||||
|
read-outs (`SliderWithIndicator.tsx`) and dimension fields (`BlankCanvasModal`) so numbers don't jitter.
|
||||||
|
|
||||||
|
### 3.3 Radii — collapse to a concentric scale ⚠️ P1
|
||||||
|
**Current:** `rounded-sm / md / lg / xl / 2xl / full` all appear, with no parent→child relationship
|
||||||
|
(e.g. `rounded-2xl` card at `PageDraw.tsx:1123`, `rounded-xl` panels in SideToolbar, `rounded-lg`
|
||||||
|
buttons in InpaintToolBar).
|
||||||
|
|
||||||
|
**Do:** pick a 3-step system — **`rounded-2xl` shells, `rounded-xl` inner cards, `rounded-lg` controls,
|
||||||
|
`rounded-full` pills** — and apply concentrically (the "double-bezel" curve math: inner radius =
|
||||||
|
outer − padding). Drop `rounded-sm`/`rounded-md` from the editor entirely.
|
||||||
|
|
||||||
|
### 3.4 Surfaces — add the "machined" double-bezel ⚠️ P1
|
||||||
|
**Current:** panels are a single `bg-ui-panel` + `border` + `shadow-lg`. No nested enclosure, no inset
|
||||||
|
top-highlight. Reads flat.
|
||||||
|
|
||||||
|
**Do (for the four floating panels — SideToolbar, HistoryStack, PromptEditor, InpaintToolBar):**
|
||||||
|
- Outer shell: `bg-ui-panel/80 ring-1 ring-white/10 backdrop-blur-xl p-1.5 rounded-2xl`.
|
||||||
|
- Inner core: own bg + `shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] rounded-xl`.
|
||||||
|
- This is the single change that most moves the surface from "tool" to "premium hardware." The panels are
|
||||||
|
already `fixed` (`PageDraw.tsx:1144,1175,1195`), so `backdrop-blur` here is GPU-safe per the perf rules.
|
||||||
|
|
||||||
|
### 3.5 Motion — replace linear/ease with spring-eased micro-motion ❌ P0 (cheap, high impact)
|
||||||
|
**Current:** `transition-colors`/`transition-all` with `ease-in-out` and `duration-75/150/200`
|
||||||
|
(`SideToolbar.tsx:381,550`, `SliderWithIndicator.tsx:65`, `ContextMenu.tsx:53`). No custom easing,
|
||||||
|
no entrance/exit on popovers, color pickers, or the context menu (they pop instantly).
|
||||||
|
|
||||||
|
**Why it fails the bar:** instant state changes and `ease-in-out` are explicitly banned. A pro tool still
|
||||||
|
deserves *interpolated* motion — it's what separates Figma from a jQuery plugin.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
- Standardize on one curve: `transition-[transform,opacity,background-color] duration-300
|
||||||
|
ease-[cubic-bezier(0.32,0.72,0,1)]`.
|
||||||
|
- Tool buttons: `active:scale-[0.97]` press physics; selected tool gets a spring-eased indicator slide
|
||||||
|
rather than a hard background swap.
|
||||||
|
- Popovers / color picker / context menu: animate in with `opacity-0 translate-y-1 scale-[0.98]` →
|
||||||
|
resolved, ~180ms. Transform+opacity only (perf rule).
|
||||||
|
- History tiles: stagger their `opacity` reveal when a generation lands (they already use
|
||||||
|
`transition-opacity`, just add per-index delay).
|
||||||
|
|
||||||
|
### 3.6 Layout & z-index — tame the stack ⚠️ P1
|
||||||
|
**Current:** clean fixed-panel composition, but z-index is ad-hoc across 6 tiers and includes a
|
||||||
|
`z-[9999999]` on the context menu (`ContextMenu.tsx:44`) and a one-off `z-[60]` (`PageDraw.tsx:237`).
|
||||||
|
|
||||||
|
**Do:** define a named scale in one place — `canvas:0, chrome:10, floating-controls:20, overlay-button:40,
|
||||||
|
menu:50` — and replace the magic numbers. The portal menu only needs to clear the chrome, not `9999999`.
|
||||||
|
|
||||||
|
### 3.7 Responsive — declare intent ⚠️ P2
|
||||||
|
**Current:** essentially desktop-only; fixed sidebars assume a wide viewport; only LightboxDetails has
|
||||||
|
`sm:` variants. The webapp wrapper scopes the lib correctly (`pagedraw.tsx` `translateZ(0)` containing
|
||||||
|
block) but there's no mobile story.
|
||||||
|
|
||||||
|
**Do:** mirror the Edit-3D page's explicit **mobile gate** (`pagescene.tsx` `MobileGate`) on `/edit-image`
|
||||||
|
rather than letting fixed toolbars overlap on touch. A precision canvas tool is legitimately desktop-first;
|
||||||
|
just say so deliberately instead of degrading silently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Architecture notes
|
||||||
|
|
||||||
|
- **Oversized modules.** `PaintSurface.tsx` (2005 LOC), `SceneState.ts` (1539), `PageDraw.tsx` (1292).
|
||||||
|
These are review/maintenance hotspots. Suggest extracting from `SceneState.ts`: history/undo slice,
|
||||||
|
base-image-load slice, and generation/pending slice into composable Zustand slices. `PageDraw.tsx`'s
|
||||||
|
generate-flow (cases 1–3, ~780–868) could move to a `useGenerate()` hook.
|
||||||
|
- **Theme ownership.** Per §3.1, the lib should *only* consume tokens. Today it mixes tokens + hex, which
|
||||||
|
defeats the adapter pattern's promise (one component, many themed hosts).
|
||||||
|
- **Adapter surface is healthy.** `PageDrawAdapter` is small and intentional; the web vs. Tauri split is
|
||||||
|
clean. The webapp's `enqueueInpaint` stub and 402→credits-modal handling are explicit and documented.
|
||||||
|
- **Icon weight.** FontAwesome **solid** is the heaviest option; if a line/light set is available
|
||||||
|
(Phosphor/Remix or FA light), the toolbar would read more refined. Low priority.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Prioritized roadmap
|
||||||
|
|
||||||
|
| P | Item | Effort | Payoff |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **P0** | Token sweep: kill hardcoded greys; unify accent on `--primary` (§3.1) | M | Highest — enables true theming + product-consistent accent |
|
||||||
|
| **P0** | Own a font-family + semantic type ramp at the lib root (§3.2) | S | High — removes banned-font risk, adds hierarchy |
|
||||||
|
| **P0** | One custom easing curve + press/popover micro-motion (§3.5) | S | High — cheapest "feels expensive" win |
|
||||||
|
| **P1** | Double-bezel + inset highlights on the 4 floating panels (§3.4) | M | High — "machined" surface upgrade |
|
||||||
|
| **P1** | Concentric 3-step radius system (§3.3) | S | Medium — visual coherence |
|
||||||
|
| **P1** | Named z-index scale; remove `z-[9999999]` (§3.6) | S | Medium — maintainability |
|
||||||
|
| **P2** | Explicit mobile gate on `/edit-image` (§3.7) | S | Medium — correctness on touch |
|
||||||
|
| **P2** | Split `SceneState.ts` / `PaintSurface.tsx` into slices (§4) | L | Medium — maintainability |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Appendix — proposed token additions
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* canvas + chrome surface ramp (lib should consume only these) */
|
||||||
|
--st-canvas: <theme>; /* replaces #1b1b1b pegboard */
|
||||||
|
--st-accent: var(--primary); /* replaces blue-600/500 */
|
||||||
|
--st-hairline: rgb(255 255 255 / 0.10);
|
||||||
|
--st-inset-hi: inset 0 1px 0 rgb(255 255 255 / 0.08);
|
||||||
|
|
||||||
|
/* motion */
|
||||||
|
--st-ease: cubic-bezier(0.32, 0.72, 0, 1);
|
||||||
|
--st-dur-fast: 180ms;
|
||||||
|
--st-dur: 300ms;
|
||||||
|
|
||||||
|
/* radius scale (concentric) */
|
||||||
|
--st-r-shell: 1.25rem; /* rounded-2xl */
|
||||||
|
--st-r-card: 0.875rem; /* rounded-xl */
|
||||||
|
--st-r-control: 0.5rem; /* rounded-lg */
|
||||||
|
```
|
||||||
|
|
||||||
|
> Scope note: every recommendation here is **styling/architecture only** — none change the generate,
|
||||||
|
> polling, upload, bg-removal, or adapter contracts validated during the functional debug pass.
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
# PageScene Animations — Branch Handoff
|
||||||
|
|
||||||
|
Branch: `frontend/pagescene-animations` (forked from `main` @ `6329045350`, 2026-08-04).
|
||||||
|
Everything below is TypeScript-typechecked per-lib (`tsc -p <lib>/tsconfig.lib.json`); runtime
|
||||||
|
verification status is tracked in the checklist at the bottom. Drag-and-drop has had one manual
|
||||||
|
pass (confirmed working); most other surfaces have not been launched.
|
||||||
|
|
||||||
|
## What this branch does
|
||||||
|
|
||||||
|
Makes skeletal animations a first-class asset in the 3D editor (pagescene): browse them in the
|
||||||
|
asset drawer, drag them onto characters or the timeline with live rig-compatibility feedback,
|
||||||
|
play clips baked into uploaded models, preview animations everywhere models are previewed, and
|
||||||
|
upload Mixamo FBX files (with or without mesh) end to end.
|
||||||
|
|
||||||
|
## Commit map
|
||||||
|
|
||||||
|
| Commit | What |
|
||||||
|
|--------------|-------------------------------------------------------------------------|
|
||||||
|
| `7af3fb9856` | Animations tab in AssetModal; AnimationsDrawer retired |
|
||||||
|
| `1fcbc55780` | Pointer drag-and-drop with live rig-compatibility badge |
|
||||||
|
| `79d7ade8bc` | Baked-in clips on the timeline (+ clip-row eligibility widened) |
|
||||||
|
| `142279414e` | Animation dropdown in Viewer3D + upload preview |
|
||||||
|
| `913905308e` | Clip fixes: vanishing character, 1 s strips, select-to-delete, undo |
|
||||||
|
| `60251104e3` | FBX accepted at the picker, converted to GLB in-browser |
|
||||||
|
| `0942ec4804` | Nx tsconfig references for the new ui-select dependency |
|
||||||
|
| `d0c7b65a6e` | FBX conversion moved into a Web Worker (main-thread fallback) |
|
||||||
|
| `8e51b6c524` | Explicit "Upload as Animation" toggle + required duration_millis |
|
||||||
|
| `00ebeae10c` | User's uploaded animations merged into the Animations tab |
|
||||||
|
| `e1c7a0c7b0` | Persistent skeleton-helper toggle in the outliner |
|
||||||
|
| `30f40c0ddb` | Previewer bone toggles + mesh-less framing/mixer-binding fixes |
|
||||||
|
| `5f7ba7bbc5` | AnimationsModal (All/Presets/Uploaded) + timeline quality-of-life |
|
||||||
|
| `d55735620a` | Timeline ruler tick marks + strip button spacing |
|
||||||
|
| `79875d8c76` | Upload animations from the AnimationsModal (initialCategory) |
|
||||||
|
| *(this)* | NodeHierarchyHelper: bone toggle works for converted mesh-less GLBs |
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. Animations tab in the asset drawer (`AssetModal`)
|
||||||
|
|
||||||
|
- New left-rail section fed by `GET /v1/media_files/list_featured?filter_engine_categories=animation`
|
||||||
|
(GLB/GLTF only — the clip loader is GLTF-based). Featured results REPLACE the 37 hardcoded
|
||||||
|
`demoAnimationItems`; demos are only the empty-API fallback.
|
||||||
|
- The session user's own uploaded animations (`list/user`, same filter) render AHEAD of the
|
||||||
|
featured set. Fetched on every modal open and after each upload; fails silently for anonymous
|
||||||
|
users (`suppressErrorToast` on the user-media fetch path).
|
||||||
|
- Card click appends the clip to the selected character's row at the earliest free slot (toast
|
||||||
|
when no character is selected or the row is full). The old right-docked `AnimationsDrawer` is
|
||||||
|
deleted; the old HTML5-DnD path (`ANIMATION_CLIP_MIME`) is gone with it.
|
||||||
|
|
||||||
|
### 2. Drag-and-drop with rig-compatibility badge (`DndAsset`)
|
||||||
|
|
||||||
|
- Animation cards ride the same pointer-drag pipeline as every other asset. On drag start the
|
||||||
|
clip's track node names are preloaded (`loadRawGlb`, cached per media id); hovering a
|
||||||
|
character (or any skinned object) checks those names against its nodes — the exact direct-bind
|
||||||
|
rule `CharacterAnimationManager` plays by. No retargeting (explicitly out of scope).
|
||||||
|
- The shared `DragGhost` shows the verdict: green check (binds) / red slash (no target or rig
|
||||||
|
mismatch); nothing while the clip is still loading. Incompatible or slot-less drops are
|
||||||
|
rejected with a toast.
|
||||||
|
- Drop targets: a character in the 3D scene (earliest free slot) or a timeline row
|
||||||
|
(`data-clip-drop-uuid`; pointer-x → time via the `data-timeline-ruler` rect).
|
||||||
|
|
||||||
|
### 3. Timeline clips: behavior + UX
|
||||||
|
|
||||||
|
- **Strips default to 1 s** and the strip width is the authoritative play window
|
||||||
|
(`LaneRuntime.stripDuration`): trimming genuinely cuts playback; loop repeats inside the
|
||||||
|
strip; a non-loop strip wider than its clip freezes on the last frame. On load a strip only
|
||||||
|
shrinks (clip shorter than default) — never grows.
|
||||||
|
- **Rest-pose in gaps**: `Skeleton.pose()` was replaced with a rest-pose snapshot captured at
|
||||||
|
mixer creation (all nodes BELOW the object root; root excluded so gizmo/keyframes never snap
|
||||||
|
back). `pose()` rebuilds bone locals from inverse bind matrices and mis-scales cm-rigs — that
|
||||||
|
was the "character vanishes when moving a strip" bug.
|
||||||
|
- **Selection UX**: strips are click-to-select; × renders only on the selected strip;
|
||||||
|
Del/Backspace deletes; removal is individually undoable (`RemoveClipLaneAction` — the one
|
||||||
|
exception to clip edits riding the timeline Save/Cancel session).
|
||||||
|
- **Full rows reject adds**: `resolveFreeStart` returns null instead of accepting an
|
||||||
|
overlapping clamp.
|
||||||
|
|
||||||
|
### 4. Baked-in clips
|
||||||
|
|
||||||
|
- `ClipStrip.bakedClipIndex` marks a strip sourced from the object's own `animations[]`
|
||||||
|
(resolved synchronously — no media fetch). Objects with baked clips get a timeline clip row
|
||||||
|
with a "+" picker listing them.
|
||||||
|
- Clip-row eligibility is wider than characters: any skinned object (creatures, rigged uploads)
|
||||||
|
accepts animation drags (bind check gates); baked-only objects get a row for the picker.
|
||||||
|
- INVARIANT: baked clips are never removed from the THREE model. Removing a baked strip only
|
||||||
|
unschedules it; the clip stays on `object.animations` and in the picker. Library strips stay
|
||||||
|
fully removable.
|
||||||
|
- Outliner metadata (`hasSkeleton`, `bakedClips`) is computed in `convert_object` and refreshed
|
||||||
|
after every GLB load (`SceneDeps.onGlbLoaded` → `refreshOutliner`).
|
||||||
|
|
||||||
|
### 5. Previewers (Viewer3D + upload preview)
|
||||||
|
|
||||||
|
- Both play baked clips: autoplay the first, dropdown with every clip + "T-pose (none)", hidden
|
||||||
|
entirely for animation-less models.
|
||||||
|
- Both have a bone icon toggling a skeleton overlay — default ON when the model has no mesh
|
||||||
|
(Mixamo "without skin" exports), so animation-only files are visible. The upload thumbnail
|
||||||
|
snapshot then captures the skeleton render.
|
||||||
|
- The overlay adapts to the rig kind: real `THREE.Bone` joints get `THREE.SkeletonHelper`;
|
||||||
|
mesh-less models whose joints re-imported as plain nodes get `NodeHierarchyHelper`
|
||||||
|
(exported from `viewer-3d`) — GLTF only round-trips bone-ness through a *skin*, and a
|
||||||
|
mesh-less export has no skin, so converted FBX animations lose `Bone`-ness entirely. The
|
||||||
|
toggle shows for `hasBones || !hasMesh`.
|
||||||
|
- Latent bugs fixed: upload preview discarded `gltf.animations`; its child re-parenting loop
|
||||||
|
skipped every other child (`scene.add` mutates the array mid-forEach); its mixer bound before
|
||||||
|
the model entered the scene (silently played nothing); camera framing NaN'd on empty
|
||||||
|
bounding boxes (now falls back to bone world positions); camera now fits once, not per-child.
|
||||||
|
|
||||||
|
### 6. FBX upload support
|
||||||
|
|
||||||
|
- The 3D uploader accepts `.fbx` and normalizes it at the door: `FBXLoader` → `GLTFExporter`
|
||||||
|
→ binary GLB with clips preserved. Runs in a dedicated Web Worker (transferred buffers, jobs
|
||||||
|
naturally serialized); a broken worker environment falls back to the main thread. The raw FBX
|
||||||
|
can never reach the backend — retry of a failed conversion re-converts.
|
||||||
|
- Caveats: embedded textures may not survive; units are exported as parsed (cm-scaled Mixamo
|
||||||
|
rigs are not rescaled).
|
||||||
|
|
||||||
|
### 7. Upload as Animation
|
||||||
|
|
||||||
|
- Mutually exclusive checkbox next to "Upload as Character" → `engine_category=animation`.
|
||||||
|
Mesh-less files preselect it (preview loader reports `hasMesh:false`); a user click always
|
||||||
|
wins. Rig-type dropdown defaults to Mixamo (characters keep Mixamo ArKit first).
|
||||||
|
- The backend-required `maybe_duration_millis` is read from the longest clip at submit time;
|
||||||
|
clip-less files are flagged and excluded client-side.
|
||||||
|
|
||||||
|
### 8. Outliner skeleton toggle (`SkeletonHelperController`)
|
||||||
|
|
||||||
|
- Bone icon on skinned rows toggles a persistent `SkeletonHelper` on LAYER 1 (editor-only —
|
||||||
|
the render camera renders layer 0, so skeletons never appear in captures/recordings).
|
||||||
|
- Persists as `userData.skeletonVisible` through scene save/load (explicit restore in the proxy
|
||||||
|
load path). Reconciliation rides `OutlinerRefreshedEvent` — add/delete/load/new-scene and the
|
||||||
|
post-GLB-load refresh all funnel there.
|
||||||
|
- FK/pose-mode compatible: `PoseModeChangedEvent("pose")` suppresses the persistent helper for
|
||||||
|
the posed character (FKHelper draws its own rig); exit restores it. The toggle state itself
|
||||||
|
is never touched.
|
||||||
|
|
||||||
|
## Manual test checklist (outstanding)
|
||||||
|
|
||||||
|
- [ ] Prod data: does `list_featured?filter_engine_categories=animation` return rows? If yes
|
||||||
|
they replace the demo clips in the tab; if empty, the 37 demos show.
|
||||||
|
- [ ] Clip→rig bind smoke test on shipped characters (runtime-unverified `mixamorig:*`
|
||||||
|
assumption; a "bound 0 tracks" console warning means retargeting becomes the real task).
|
||||||
|
- [ ] Creature experiment: drag a clip onto a creature — badge should reflect bind result.
|
||||||
|
- [ ] Baked clips: "+" picker on an animated object's row; add/remove/re-add; save/reload
|
||||||
|
persistence (the `object.animations` stash on load is gated on `auto_add` — statically
|
||||||
|
unconfirmed for every load path).
|
||||||
|
- [ ] Clip UX round: strips at 1 s; trim semantics; select → ×/Del; undo/redo of removal;
|
||||||
|
character stays visible when a strip moves off the playhead.
|
||||||
|
- [ ] FBX round trip: pick `.fbx` → converts (worker; check console for worker load failures —
|
||||||
|
falls back to main thread) → skeleton visible + clip dropdown → Upload as Animation
|
||||||
|
preselected → appears at front of Animations tab → drag onto character → plays.
|
||||||
|
- [ ] Converted-FBX scale: if placed objects come in ~100× too big, the converter needs a
|
||||||
|
normalize-scale step.
|
||||||
|
- [ ] Outliner bone toggle: on/off, FK-mode enter/exit restore, save/reload persistence, and
|
||||||
|
absence from Capture/Record output.
|
||||||
|
- [ ] Previewer dropdown + bone toggle across lightbox, media page, lightbox-modal,
|
||||||
|
ImageTo3DExperience, upload modal.
|
||||||
|
|
||||||
|
## Debug pass + hardening round (post-review)
|
||||||
|
|
||||||
|
A four-agent review of the combined branch produced 30 findings (5 high / 12 medium / 13 low);
|
||||||
|
ALL have been fixed or explicitly closed, one commit per finding (range `72cb218e..b21732d1`).
|
||||||
|
Highlights, roughly in commit order:
|
||||||
|
|
||||||
|
- Highs: baked clips cloned per lane (shared-AnimationAction playback/removal corruption);
|
||||||
|
runtime revalidation for objects recreated under the same uuid (delete→undo, reloads) — later
|
||||||
|
hardened into LOOP-FREE terminal lane states (bound / missing / unbindable, reassessed only on
|
||||||
|
root-instance change; the pre-fetch rig gate is sound because every load path stamps the target
|
||||||
|
uuid only post-load); skeleton-helper sync self-heals across in-session reloads; upload modal
|
||||||
|
can no longer report success for empty/partial submissions; ref-guarded double-submit.
|
||||||
|
- Mediums: strip removal mirrored into the Save/Cancel `saved` snapshot (Cancel can't resurrect,
|
||||||
|
no phantom undo); stale strip/keyframe selections pruned in the bridge + live-checked before
|
||||||
|
swallowing Del/Backspace; one winning lane per character at flush boundaries (blend-glitch
|
||||||
|
frame was being ENCODED into recordings); orphaned tracks/lanes filtered from scene JSON at
|
||||||
|
save; per-clip queued-drop tokens; animation raycasts gated to the canvas element + honest
|
||||||
|
front-most-surface occlusion; virtualized-grid thumbnail state keyed by item; one shared
|
||||||
|
lazily-fetched animation-library store (was double-fetch + double-toast + divergent lists);
|
||||||
|
previewer rest-pose snapshots replace `Skeleton.pose()` everywhere; all-node camera-framing
|
||||||
|
fallback for plain-node rigs; upload-preview races (superseded-load cancellation, snapshot
|
||||||
|
after helper creation, File-identity row statuses, retry re-entry).
|
||||||
|
- Lows: duration-shrink re-fits strips; baked-clip picker portals out of the scrolling row list;
|
||||||
|
timeline resets on scene switch/new scene instead of leaking across; click-to-add accepts every
|
||||||
|
drag-eligible target; queued drops bail on a torn-down editor (`Editor.isLive`); top-level root
|
||||||
|
lookups unified; flick-click race killed via `wasDragGesture`; converting-overlay control reset;
|
||||||
|
conversion completions reload only the previewed entry; worker `onmessageerror` recovery +
|
||||||
|
corrupt-vs-clip-less error messages + explicit `initialCategory` wins over mesh-less
|
||||||
|
auto-detect; Uploaded tab pages past the 100-item cap.
|
||||||
|
- Closed without code: the "compat cache stales on streaming characters" finding (invalidated —
|
||||||
|
uuid-resolvable roots are always fully loaded) and the Tauri `promptSignup` gap (desktop is
|
||||||
|
always signed in by adapter contract).
|
||||||
|
|
||||||
|
## Landed since the original handoff
|
||||||
|
|
||||||
|
- **AnimationsModal** (was deferred — now built, `5f7ba7bbc5`/`79875d8c76`): standalone modal
|
||||||
|
with All / Presets / Uploaded tabs, opened from an "Add Animation" button next to Enter Pose
|
||||||
|
Mode and from the "+" add-asset menu. Adding a clip expands the timeline and scrolls to the
|
||||||
|
character (`timelineRevealObjectUuid`); the modal closes after an add unless "Reopen after
|
||||||
|
adding" is on. Its "Upload animation" button opens `UploadModal3D` with
|
||||||
|
`initialCategory="animation"` (anonymous users get the signup prompt), landing on the
|
||||||
|
Uploaded tab afterwards. `AssetModal`'s animations tab was folded into this.
|
||||||
|
- **Timeline QoL**: ruler tick marks (major/second/sub-tick), selected-object-only track
|
||||||
|
filter, directional expand/collapse chevrons, thumbnail-less animation cards get an icon
|
||||||
|
placeholder.
|
||||||
|
|
||||||
|
## Deferred (explicitly out of scope for this branch)
|
||||||
|
|
||||||
|
- **Split-screen preview window** (based on the `?output=` `DemoOutputOverlay`) — parked until
|
||||||
|
explicit approval.
|
||||||
|
- **Retargeting** (`SkeletonUtils.retargetClip`) — too complex for now; mismatched rigs are
|
||||||
|
simply rejected by the bind check.
|
||||||
|
- **Clip blending/layering** — one non-overlapping row per object, by design.
|
||||||
|
|
||||||
|
## Key files
|
||||||
|
|
||||||
|
| Area | Files |
|
||||||
|
|---------------------|-------------------------------------------------------------------------------------------|
|
||||||
|
| Drawer tab | `pagescene/src/lib/comps/AssetMenu/AssetModal.tsx`, `shared/ItemElement.tsx` |
|
||||||
|
| Drag-and-drop | `pagescene/src/lib/DragAndDrop/DndAsset.ts`, `gallery-modal/src/lib/DragGhost.tsx` |
|
||||||
|
| Timeline data/engine| `pagescene/src/lib/engine/timeline/types.ts`, `engine/editor/TimelineController.ts`, |
|
||||||
|
| | `engine/animation/CharacterAnimationManager.ts`, `engine/editor/actions/RemoveClipLaneAction.ts` |
|
||||||
|
| Timeline UI | `pagescene/src/lib/comps/Timeline/TimelineEditor.tsx`, `TimelineClipRow.tsx` |
|
||||||
|
| Skeleton toggle | `pagescene/src/lib/engine/editor/SkeletonHelperController.ts`, `comps/Outliner/Outliner.tsx` |
|
||||||
|
| Previewers | `viewer-3d/src/lib/viewer-3d.tsx`, `upload-modal/.../utilities/loadPreviewOnCanvas.ts` |
|
||||||
|
| FBX + upload | `upload-modal/.../utilities/convertFbxToGlb.ts` (+ `.worker.ts`), `UploadModal3D.tsx`, |
|
||||||
|
| | `UploadFiles3D.tsx`, `utilities/readGlbAnimationDuration.ts` |
|
||||||
|
|
||||||
|
The pagescene `CLAUDE.md` "Mixamo animation clips" section has been kept current with these
|
||||||
|
changes and carries the same invariants in engine-doc form.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Rust Workspace
|
||||||
|
|
||||||
|
All Rust code lives under `crates/`. The workspace is defined in `Cargo.toml` at the repo root.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Two spaces for indentation (not four)
|
||||||
|
- No minimum supported Rust version; use latest stable features freely
|
||||||
|
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros instead
|
||||||
|
- Use `anyhow::Result` / `AnyhowResult` for fallible functions in application code
|
||||||
|
- Fields that are optional use the `maybe_` prefix: `maybe_creator_user_token`
|
||||||
|
- When two crates export the same type name, alias with a suffix: `use foo::Bar as BarFoo;`
|
||||||
|
- Prefer `use` imports over inline fully-qualified paths; only qualify inline for true one-offs or std prelude collisions (`Result`, `Option`, `Error`)
|
||||||
|
|
||||||
|
## File Layout
|
||||||
|
|
||||||
|
Organize for top-to-bottom reading. Important things first, details later.
|
||||||
|
|
||||||
|
1. **Constants** at the top (after imports)
|
||||||
|
2. **Primary public type** (the main struct/enum the file defines)
|
||||||
|
3. **Supporting types** below the primary type
|
||||||
|
4. **API types** in order: Request, Response, Error
|
||||||
|
5. **`impl` blocks** after their type definitions
|
||||||
|
- Constructors first, then public methods, then private helpers
|
||||||
|
- Callers above callees — follow the call chain downward
|
||||||
|
|
||||||
|
## Test Layout
|
||||||
|
|
||||||
|
Within `#[cfg(test)] mod tests { ... }`:
|
||||||
|
|
||||||
|
1. **Imports**
|
||||||
|
2. **Constants** (test IDs, URLs, fixture data) — small context that tests reference
|
||||||
|
3. **Test functions or sub-modules** grouped by category
|
||||||
|
4. **Helper functions** (builders, pipeline runners, assertions)
|
||||||
|
|
||||||
|
Constants provide small context up front. Test cases come next so engineers see *what* is
|
||||||
|
tested before *how* the helpers work.
|
||||||
|
|
||||||
|
Group related tests into sub-modules (`mod pricing_tests { ... }`) when a category has 2+
|
||||||
|
tests. Don't wrap a single test in its own module. Keep nesting to two levels max. Shared
|
||||||
|
helpers go in the parent `mod tests` so sub-modules can `use super::*`.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
- Most crates: `cargo check -p {crate_name}` or `cargo test -p {crate_name}`
|
||||||
|
- Crates using SQLx: `SQLX_OFFLINE=true cargo check -p {crate_name}`
|
||||||
|
- The desktop SQLite query cache and migrations must remain in `.sqlx/` and `_database/sql/artcraft_migrations/`
|
||||||
|
|
||||||
|
## Key Crates
|
||||||
|
|
||||||
|
- `artcraft` (desktop) — Tauri desktop app
|
||||||
|
- `artcraft_router` — provider routing for image/video generation (Artcraft, Fal, KinoviWeb, Grok, GMICloud, WorldLabs)
|
||||||
|
- `artcraft_api_defs` — shared HTTP API type definitions
|
||||||
|
- `kinovi_web_client` — HTTP client for the Kinovi/KinoviWeb video generation service
|
||||||
|
- `enums` — database-backed enums stored as VARCHAR fields
|
||||||
|
- `tokens` — primary key identifiers with Stripe-like prefixes (e.g. `user_`, `mf_`)
|
||||||
|
|
||||||
|
- `sqlite_tasks` — SQLite persistence for desktop generation tasks
|
||||||
|
|
||||||
|
The workspace contains only desktop dependencies and their shared test helpers.
|
||||||
|
There is no Hakari workspace helper; keep new dependencies local to their consumers.
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
AGENTS.md
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# artcraft_api_defs
|
||||||
|
|
||||||
|
Shared HTTP API type definitions used by both the server (`storyteller_web`) and clients.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
Organized by feature: `users/`, `media_file/`, `jobs/`, `moderation/`, etc.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Request types: `#[derive(Deserialize, ToSchema)]`
|
||||||
|
- Response types: `#[derive(Serialize, ToSchema)]`
|
||||||
|
- Path params: `#[derive(Deserialize, ToSchema)]` with `PathInfo` suffix
|
||||||
|
- Query params: `#[derive(Deserialize, IntoParams)]`
|
||||||
|
|
||||||
|
## Orphan Rule
|
||||||
|
|
||||||
|
Types that need `ResponseError` impl (actix-web) CANNOT live here because of Rust's orphan rule. Keep error response types in `storyteller_web` and only put data types here.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Only `serde`, `serde_derive`, `utoipa`, `chrono`, `url`, and internal schema crates (`enums`, `tokens`). No actix-web dependency.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
AGENTS.md
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "artcraft_api_defs"
|
||||||
|
edition = "2024"
|
||||||
|
version = "0.0.1"
|
||||||
|
authors = [
|
||||||
|
"Brandon Thomas <bt@brand.io>",
|
||||||
|
"Brandon Thomas <echelon@gmail.com>",
|
||||||
|
]
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "artcraft_api_defs"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
# Internal
|
||||||
|
enums.workspace = true
|
||||||
|
tokens.workspace = true
|
||||||
|
|
||||||
|
# External
|
||||||
|
chrono.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde.workspace = true
|
||||||
|
serde_derive.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
utoipa.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::app_session::AppSessionToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
/// This is used in Artcraft versions 0.0.1 (several) through 0.3.0
|
||||||
|
/// Clients to this version do not send generation count data (even if the endpoint supports it).
|
||||||
|
pub const LOG_ACTIVE_USER_V1_PATH: &str = "/v1/analytics/active_user";
|
||||||
|
|
||||||
|
/// This is used in Artcraft versions 0.4.0 and later.
|
||||||
|
pub const LOG_ACTIVE_USER_V2_PATH: &str = "/v1/analytics/active_user_v2";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug, Default)]
|
||||||
|
pub struct LogAppActiveUserRequest {
|
||||||
|
/// Clientside-generated session token.
|
||||||
|
/// If present, it'll be validated against an expected format.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub maybe_app_session_token: Option<AppSessionToken>,
|
||||||
|
|
||||||
|
/// An override for the application platform/OS (windows, mac, linux).
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub maybe_os_platform: Option<String>,
|
||||||
|
|
||||||
|
/// An override for the version of the OS (e.g. 10.15.7, 11, 22.04).
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub maybe_os_version: Option<String>,
|
||||||
|
|
||||||
|
/// An override for the application name.
|
||||||
|
/// If set together with `maybe_app_version`, the two will be
|
||||||
|
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub maybe_app_name: Option<String>,
|
||||||
|
|
||||||
|
/// An override for the application version.
|
||||||
|
/// If set together with `maybe_app_name`, the two will be
|
||||||
|
/// concatenated as `{maybe_app_name}/{maybe_app_version}`.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub maybe_app_version: Option<String>,
|
||||||
|
|
||||||
|
/// How long the user has been active in the app, in seconds.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub maybe_session_duration_seconds: Option<u64>,
|
||||||
|
|
||||||
|
// ========== GENERATION COUNTS ==========
|
||||||
|
|
||||||
|
/// Number of items generated across all types.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub total_generation_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of images generated.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub image_generation_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of videos generated.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub video_generation_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of objects generated (meshes, etc.).
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub object_generation_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of images generated with text to image.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub text_to_image_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of images generated with image to image (image edits).
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub image_to_image_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of videos generated with text to video.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub text_to_video_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of videos generated with image to video.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub image_to_video_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of objects generated with text to object.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub text_to_object_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of objects generated with image to object.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub image_to_object_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of prompts from the image page.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub image_page_prompt_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of prompts from the video page.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub video_page_prompt_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of prompts from the edit page.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub edit_page_prompt_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of prompts from the stage page.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub stage_page_prompt_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of prompts from the object page.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub object_page_prompt_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of prompts from other pages.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub other_page_prompt_count: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct LogAppActiveUserResponse {
|
||||||
|
pub success: bool,
|
||||||
|
|
||||||
|
/// How long to wait until the next analytics event, in milliseconds.
|
||||||
|
/// The client should honor this and is free to add jitter.
|
||||||
|
pub wait_for_retry_millis: u64,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod log_active_user;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use tokens::tokens::api_keys::ApiKeyToken;
|
||||||
|
use tokens::tokens::users::UserToken;
|
||||||
|
|
||||||
|
/// Canonical wire shape for an API key, used by the list rows and the
|
||||||
|
/// single-key GET.
|
||||||
|
///
|
||||||
|
/// NB: the full secret `api_key` is NEVER included here — only
|
||||||
|
/// `truncated_api_key` (its first 20 characters), enough to disambiguate keys
|
||||||
|
/// in a UI. The full value is returned exactly once, at creation time.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ApiKeyInfo {
|
||||||
|
pub token: ApiKeyToken,
|
||||||
|
|
||||||
|
/// First 20 characters of the API key. The full secret is never returned
|
||||||
|
/// after creation.
|
||||||
|
pub truncated_api_key: String,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
pub maybe_description: Option<String>,
|
||||||
|
|
||||||
|
pub owner_user_token: UserToken,
|
||||||
|
|
||||||
|
pub ip_address_creation: String,
|
||||||
|
pub ip_address_update: String,
|
||||||
|
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// Soft-delete timestamp. `None` for live keys.
|
||||||
|
pub maybe_deleted_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Path info for endpoints addressed by the API key's `token` (NOT the secret
|
||||||
|
/// `api_key` value): `GET` / `DELETE` / `PUT` `/v1/api_keys/{api_key_token}`.
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct ApiKeyPathInfo {
|
||||||
|
pub api_key_token: ApiKeyToken,
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use tokens::tokens::api_keys::ApiKeyToken;
|
||||||
|
|
||||||
|
// ── POST /v1/api_keys/create ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct CreateApiKeyRequest {
|
||||||
|
/// Display name (title) for the key.
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// Optional description.
|
||||||
|
pub maybe_description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct CreateApiKeySuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
|
||||||
|
/// The key's `token` — used for all subsequent management
|
||||||
|
/// (get / update / delete). Safe to store and display.
|
||||||
|
pub api_key_token: ApiKeyToken,
|
||||||
|
|
||||||
|
/// The full secret API key value. This is the ONLY time it is ever
|
||||||
|
/// returned — the caller must store it now, as it cannot be retrieved again.
|
||||||
|
pub api_key: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
use serde_derive::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
// ── DELETE /v1/api_keys/{api_key_token} ──
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct DeleteApiKeySuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use serde_derive::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::api_keys::common::ApiKeyInfo;
|
||||||
|
|
||||||
|
// ── GET /v1/api_keys/{api_key_token} ──
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct GetApiKeySuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub api_key: ApiKeyInfo,
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
|
use crate::api_keys::common::ApiKeyInfo;
|
||||||
|
|
||||||
|
// ── GET /v1/api_keys/list ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||||
|
pub struct ListApiKeysQueryParams {
|
||||||
|
/// Page size. Defaults server-side if omitted.
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
|
||||||
|
/// Number of rows to skip. Defaults to 0.
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct ListApiKeysSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
|
||||||
|
/// The user's API keys (including soft-deleted ones), newest first. Each
|
||||||
|
/// entry's key value is truncated — the full secret is never listed.
|
||||||
|
pub api_keys: Vec<ApiKeyInfo>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod common;
|
||||||
|
pub mod create_api_key;
|
||||||
|
pub mod delete_api_key;
|
||||||
|
pub mod get_api_key;
|
||||||
|
pub mod list_api_keys;
|
||||||
|
pub mod update_api_key;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
// ── PUT /v1/api_keys/{api_key_token} ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct UpdateApiKeyRequest {
|
||||||
|
/// New description for the key. `None` clears it.
|
||||||
|
pub maybe_description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct UpdateApiKeySuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use enums::common::generation::common_model_type::CommonModelType;
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
|
||||||
|
/// Request body for creating a character.
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct CreateCharacterRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// The model to create the character for.
|
||||||
|
pub model: CommonModelType,
|
||||||
|
|
||||||
|
/// The reference image media file token.
|
||||||
|
pub image_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Name of the character.
|
||||||
|
pub character_name: String,
|
||||||
|
|
||||||
|
/// Description of the character.
|
||||||
|
pub character_description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response body for creating a character.
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct CreateCharacterResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use tokens::tokens::characters::CharacterToken;
|
||||||
|
|
||||||
|
/// Path parameters for deleting a character.
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct DeleteCharacterPathInfo {
|
||||||
|
pub character_token: CharacterToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response body for deleting a character.
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct DeleteCharacterResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use tokens::tokens::characters::CharacterToken;
|
||||||
|
|
||||||
|
/// Request body for editing a character.
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct EditCharacterRequest {
|
||||||
|
/// The character to edit.
|
||||||
|
pub token: CharacterToken,
|
||||||
|
|
||||||
|
/// Updated name (if changing).
|
||||||
|
pub updated_name: Option<String>,
|
||||||
|
|
||||||
|
/// Updated description (if changing).
|
||||||
|
pub updated_description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response body for editing a character.
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct EditCharacterResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::common::responses::media_links::MediaLinks;
|
||||||
|
use enums::common::generation::common_model_type::CommonModelType;
|
||||||
|
use tokens::tokens::characters::CharacterToken;
|
||||||
|
|
||||||
|
/// Path parameters for getting a character.
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct GetCharacterPathInfo {
|
||||||
|
pub character_token: CharacterToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response body for getting a character.
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct GetCharacterResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub character: GetCharacterDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full character details returned by the get endpoint.
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct GetCharacterDetails {
|
||||||
|
pub token: CharacterToken,
|
||||||
|
|
||||||
|
/// Which models this character can be used with.
|
||||||
|
pub models: Vec<CommonModelType>,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub maybe_description: Option<String>,
|
||||||
|
|
||||||
|
pub maybe_avatar: Option<MediaLinks>,
|
||||||
|
|
||||||
|
pub maybe_full_image: Option<MediaLinks>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
|
use crate::common::responses::media_links::MediaLinks;
|
||||||
|
use enums::common::generation::common_model_type::CommonModelType;
|
||||||
|
use tokens::tokens::characters::CharacterToken;
|
||||||
|
|
||||||
|
/// Query string parameters for listing characters.
|
||||||
|
#[derive(Deserialize, IntoParams)]
|
||||||
|
pub struct ListCharactersQuery {
|
||||||
|
/// Optional cursor for pagination.
|
||||||
|
pub cursor: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response body for listing characters in the current session.
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct ListCharactersResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub characters: Vec<ListCharactersEntry>,
|
||||||
|
pub next_cursor: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A character entry in the list response.
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct ListCharactersEntry {
|
||||||
|
pub token: CharacterToken,
|
||||||
|
|
||||||
|
/// Which models this character can be used with.
|
||||||
|
pub models: Vec<CommonModelType>,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub maybe_description: Option<String>,
|
||||||
|
|
||||||
|
pub maybe_avatar: Option<MediaLinks>,
|
||||||
|
|
||||||
|
pub maybe_full_image: Option<MediaLinks>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod create_character;
|
||||||
|
pub mod delete_character;
|
||||||
|
pub mod edit_character;
|
||||||
|
pub mod get_character;
|
||||||
|
pub mod list_characters;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod responses;
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
// TODO(bt,2025-08-20): Replace the storyteller-web version of this.
|
||||||
|
// All it needs is the impl ported into a builder.
|
||||||
|
|
||||||
|
/// Cover image links can only be jpg, png, etc. No videos.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct CoverImageLinks {
|
||||||
|
/// Primary link to the cover image via the CDN.
|
||||||
|
pub cdn_url: Url,
|
||||||
|
|
||||||
|
/// Template to construct thumbnail URLs.
|
||||||
|
/// Replace the string `{WIDTH}` with the desired width.
|
||||||
|
/// Only relevant for image media files. (Video media files instead have
|
||||||
|
/// video previews, which, in turn, have their own thumbnail templates.)
|
||||||
|
pub thumbnail_template: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
/// Details about submitted live portrait jobs (request arguments only)
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct JobDetailsLivePortraitRequest {
|
||||||
|
/// Source media token
|
||||||
|
/// This can be an image or a video media file token
|
||||||
|
/// This is what constitutes the "portrait" or the overall final video.
|
||||||
|
/// This video or image must contain a face.
|
||||||
|
pub source_media_file_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Driving media token
|
||||||
|
/// This must be a video media file token.
|
||||||
|
/// This drives the animation of the face, but the actor will disappear
|
||||||
|
/// and their facial expressions will be transferred to the source.
|
||||||
|
/// This video must contain a face.
|
||||||
|
pub face_driver_media_file_token: MediaFileToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Details about submitted lipsync jobs (request arguments only)
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct JobDetailsLipsyncRequest {
|
||||||
|
/// Media file token for the source audio.
|
||||||
|
/// This is probably an audio file, but in the future we might pull audio from video.
|
||||||
|
pub audio_source_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Media file token for the source visuals.
|
||||||
|
/// This is either an image or video.
|
||||||
|
pub image_or_video_source_token: MediaFileToken,
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
use crate::common::responses::cover_image_links::CoverImageLinks;
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
// TODO(bt,2025-08-20): Replace the storyteller-web version of this.
|
||||||
|
// We can't drop the old version just yet due to the `impl` (easy)
|
||||||
|
// and a few differing fields.
|
||||||
|
|
||||||
|
/// Everything we need to create a cover image.
|
||||||
|
/// Cover images are small descriptive images that can be set for any media file.
|
||||||
|
/// If a cover image is set, this is the path to the asset.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct MediaFileCoverImageDetails {
|
||||||
|
// /// (DEPRECATED) URL path to the media file
|
||||||
|
// #[deprecated(note="This field doesn't point to the full URL. Use media_links instead to leverage the CDN.")]
|
||||||
|
// pub maybe_cover_image_public_bucket_path: Option<String>,
|
||||||
|
|
||||||
|
// /// (DEPRECATED) Full URL to the media file
|
||||||
|
// #[deprecated(note="This points to the bucket. Use media_links instead to leverage the CDN.")]
|
||||||
|
// pub maybe_cover_image_public_bucket_url: Option<Url>,
|
||||||
|
|
||||||
|
// NB(bt,2024-09-19): I accidentally rolled this field out to production.
|
||||||
|
// I don't think this field is in use, but maybe ...
|
||||||
|
// /// (DEPRECATED) Use maybe_links instead.
|
||||||
|
// #[deprecated(note="Use `maybe_links` instead.")]
|
||||||
|
// pub maybe_media_links: Option<MediaLinks>,
|
||||||
|
|
||||||
|
/// Links to the cover image (CDN direct link, thumbnail template)
|
||||||
|
/// If a cover image is set, this is the path to the asset.
|
||||||
|
/// If a cover image is not set, use the information in `default_cover` instead.
|
||||||
|
/// Rich CDN links to the media, including thumbnails, previews, and more.
|
||||||
|
pub maybe_links: Option<CoverImageLinks>,
|
||||||
|
|
||||||
|
/// For items without a cover image, we can use one of our own.
|
||||||
|
pub default_cover: MediaFileDefaultCover,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default cover is composed of an image and color pair that are
|
||||||
|
/// predefined by the frontend.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct MediaFileDefaultCover {
|
||||||
|
pub image_index: u8,
|
||||||
|
pub color_index: u8,
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use url::Url;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
/// Links to media file locations (bucket, CDN, etc.)
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct MediaLinks {
|
||||||
|
/// Primary link to the asset via the CDN.
|
||||||
|
pub cdn_url: Url,
|
||||||
|
|
||||||
|
/// Template to construct thumbnail URLs.
|
||||||
|
/// Replace the string `{WIDTH}` with the desired width.
|
||||||
|
/// Only relevant for image media files. (Video media files instead have
|
||||||
|
/// video previews, which, in turn, have their own thumbnail templates.)
|
||||||
|
pub maybe_thumbnail_template: Option<String>,
|
||||||
|
|
||||||
|
/// Video preview images (still and animated gif) for mp4 video files.
|
||||||
|
/// These are only set for video media files.
|
||||||
|
/// These are not set for "cover images" that are gif or webms, but rather video files.
|
||||||
|
pub maybe_video_previews: Option<VideoPreviews>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct VideoPreviews {
|
||||||
|
/// A static single frame preview image of the video.
|
||||||
|
pub still: Url,
|
||||||
|
|
||||||
|
/// An animated gif preview of the video.
|
||||||
|
pub animated: Url,
|
||||||
|
|
||||||
|
/// A template used to construct the still thumbnail URL.
|
||||||
|
/// Replace the string `{WIDTH}` with the desired width.
|
||||||
|
pub still_thumbnail_template: String,
|
||||||
|
|
||||||
|
/// A template used to construct the animated thumbnail URL.
|
||||||
|
/// Replace the string `{WIDTH}` with the desired width.
|
||||||
|
pub animated_thumbnail_template: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
pub mod cover_image_links;
|
||||||
|
pub mod job_details;
|
||||||
|
pub mod media_file_cover_image_details;
|
||||||
|
pub mod media_links;
|
||||||
|
pub mod pagination_cursors;
|
||||||
|
pub mod simple_entity_stats;
|
||||||
|
pub mod simple_generic_json_success;
|
||||||
|
pub mod user_details_light;
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
// TODO(bt,2026-07-09): Replace the storyteller-web version of this.
|
||||||
|
|
||||||
|
/// Pagination by cursors
|
||||||
|
/// These types of pagination are for "infinite scrolling", which do not reveal a number of pages.
|
||||||
|
/// This is good so that investors and competitors cannot reveal how many database records we have.
|
||||||
|
/// This is typically used for "discovery" type pages, not user profiles.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct PaginationCursors {
|
||||||
|
/// The "next" cursor.
|
||||||
|
/// This is an opaque (typically even encrypted) handle.
|
||||||
|
pub maybe_next: Option<String>,
|
||||||
|
|
||||||
|
/// The "previous" cursor.
|
||||||
|
/// This is an opaque (typically even encrypted) handle.
|
||||||
|
pub maybe_previous: Option<String>,
|
||||||
|
|
||||||
|
/// Details whether we're walking forward or backward.
|
||||||
|
pub cursor_is_reversed: bool,
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// TODO(bt,2025-08-20): Replace the storyteller-web version of this.
|
||||||
|
|
||||||
|
/// Simple stats that can be attached to any entity
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SimpleEntityStats {
|
||||||
|
/// Number of positive ratings (or "likes") for this item
|
||||||
|
pub positive_rating_count: u32,
|
||||||
|
/// Number of bookmarks for this item
|
||||||
|
pub bookmark_count: u32,
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SimpleGenericJsonSuccess {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::users::UserToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
/// Everything we need to refer to a user on the public web interface.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct UserDetailsLight {
|
||||||
|
/// The token for the user
|
||||||
|
pub user_token: UserToken,
|
||||||
|
|
||||||
|
/// The unique username someone logs in with
|
||||||
|
/// As of 2023-08-23, this is always lowercase
|
||||||
|
pub username: String,
|
||||||
|
|
||||||
|
/// As of 2023-08-23, this is the username with capitalization
|
||||||
|
/// (In the future, a display name can be customized by the user.)
|
||||||
|
pub display_name: String,
|
||||||
|
|
||||||
|
/// Email hash for Gravatar
|
||||||
|
/// Always set for now since login is email/username+password.
|
||||||
|
/// In the future this will need to become an optional *OR* be filled with a bogus hash.
|
||||||
|
pub gravatar_hash: String,
|
||||||
|
|
||||||
|
/// For users without a gravatar, we show one of our own avatars.
|
||||||
|
pub default_avatar: UserDefaultAvatarInfo,
|
||||||
|
|
||||||
|
// In the future, we'll also support user-uploaded avatars that we store on our servers.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct UserDefaultAvatarInfo {
|
||||||
|
pub image_index: u8,
|
||||||
|
pub color_index: u8,
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct GetSessionCreditsResponse {
|
||||||
|
pub success: bool,
|
||||||
|
|
||||||
|
/// Any free credits the user might have
|
||||||
|
/// We might support daily free credits.
|
||||||
|
pub free_credits: u64,
|
||||||
|
|
||||||
|
/// Any monthly refilling credits the user might have
|
||||||
|
pub monthly_credits: u64,
|
||||||
|
|
||||||
|
/// Any banked credits the user might have.
|
||||||
|
pub banked_credits: u64,
|
||||||
|
|
||||||
|
/// All the credit amounts added together.
|
||||||
|
pub sum_total_credits: u64,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod get_session_credits;
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||||
|
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||||
|
use tokens::tokens::folders::FolderToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use tokens::tokens::users::UserToken;
|
||||||
|
|
||||||
|
// NB: The "folder media file" list-item shape lives in storyteller_web
|
||||||
|
// (see `endpoints/folders/media_files/list_folder_media_files_handler.rs`)
|
||||||
|
// because it embeds `MediaFileCoverImageDetails` and other domain types
|
||||||
|
// that depend on the request's `MediaDomain` + `ServerEnvironment`.
|
||||||
|
|
||||||
|
/// Canonical wire shape for a folder. Used by single-folder GETs, create
|
||||||
|
/// responses, list rows, and subfolder list rows.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FolderInfo {
|
||||||
|
pub token: FolderToken,
|
||||||
|
pub name: String,
|
||||||
|
pub owner_user_token: UserToken,
|
||||||
|
pub maybe_parent_folder_token: Option<FolderToken>,
|
||||||
|
|
||||||
|
/// The four most-recent media files added to the folder, in slot order
|
||||||
|
/// (the schema stores them as `maybe_last_media_file_token_1..4`).
|
||||||
|
/// Slots whose media file has been deleted are skipped, so the vec
|
||||||
|
/// may contain 0-4 entries.
|
||||||
|
pub last_media_thumbnails: Vec<FolderThumbnail>,
|
||||||
|
|
||||||
|
/// User-chosen cover image. When present, the frontend should use this
|
||||||
|
/// as the folder's primary thumbnail instead of the
|
||||||
|
/// `last_media_thumbnails` grid.
|
||||||
|
pub maybe_custom_cover_thumbnail: Option<FolderThumbnail>,
|
||||||
|
|
||||||
|
pub maybe_color_code: Option<String>,
|
||||||
|
pub has_star: bool,
|
||||||
|
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// True when `maybe_parent_folder_token` is set but the referenced
|
||||||
|
/// parent row is missing or soft-deleted.
|
||||||
|
pub is_orphaned: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A compact thumbnail descriptor for a media file referenced by a
|
||||||
|
/// folder (either one of the `last_media_*` slots or the custom cover).
|
||||||
|
///
|
||||||
|
/// Intentionally narrower than `MediaLinks` / `CoverImageLinks` — folders
|
||||||
|
/// only need the bare minimum to render a thumbnail card. The CDN URL is
|
||||||
|
/// the direct asset link; `maybe_thumbnail_template` carries the resizing
|
||||||
|
/// template URL (with `{{width}}` / `{{height}}` placeholders) when the
|
||||||
|
/// media class supports it.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FolderThumbnail {
|
||||||
|
pub token: MediaFileToken,
|
||||||
|
pub media_class: MediaFileClass,
|
||||||
|
pub media_type: MediaFileType,
|
||||||
|
|
||||||
|
/// Direct CDN URL to the underlying media asset.
|
||||||
|
pub cdn_url: Url,
|
||||||
|
|
||||||
|
/// Resizing template URL (with `{{width}}` / `{{height}}` placeholders)
|
||||||
|
/// when the media class supports it. `None` for media classes without
|
||||||
|
/// a thumbnail variant. (Video media files instead have video previews,
|
||||||
|
/// which, in turn, have their own thumbnail templates.)
|
||||||
|
pub maybe_thumbnail_template: Option<String>,
|
||||||
|
|
||||||
|
/// Video preview images (still and animated gif) for mp4 video files.
|
||||||
|
/// These are only set for video media files.
|
||||||
|
pub maybe_video_previews: Option<FolderThumbnailVideoPreviews>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Video preview images for a video media file referenced by a folder.
|
||||||
|
/// Mirrors the shape of `MediaLinks`' video previews, but is its own type
|
||||||
|
/// so the folder wire shape can evolve independently.
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FolderThumbnailVideoPreviews {
|
||||||
|
/// A static single frame preview image of the video.
|
||||||
|
pub still: Url,
|
||||||
|
|
||||||
|
/// An animated gif preview of the video.
|
||||||
|
pub animated: Url,
|
||||||
|
|
||||||
|
/// A template used to construct the still thumbnail URL.
|
||||||
|
/// Replace the string `{WIDTH}` with the desired width.
|
||||||
|
pub still_thumbnail_template: String,
|
||||||
|
|
||||||
|
/// A template used to construct the animated thumbnail URL.
|
||||||
|
/// Replace the string `{WIDTH}` with the desired width.
|
||||||
|
pub animated_thumbnail_template: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
|
use tokens::tokens::folders::FolderToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
|
||||||
|
use crate::folders::common::FolderInfo;
|
||||||
|
|
||||||
|
// ── POST /v1/folders/create ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct CreateFolderRequest {
|
||||||
|
pub name: String,
|
||||||
|
pub maybe_parent_folder_token: Option<FolderToken>,
|
||||||
|
pub maybe_color_code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct CreateFolderSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub folder: FolderInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GET /v1/folders/list_all ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||||
|
pub struct ListFoldersQueryParams {
|
||||||
|
pub cursor: Option<String>,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct ListFoldersSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub folders: Vec<FolderInfo>,
|
||||||
|
pub maybe_cursor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GET /v1/folders/folder/{folder_token} ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct FolderPathInfo {
|
||||||
|
pub folder_token: FolderToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct GetFolderSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub folder: FolderInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT /v1/folders/folder/{folder_token}/rename ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct RenameFolderRequest {
|
||||||
|
pub new_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct RenameFolderSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT /v1/folders/folder/{folder_token}/star ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct SetFolderStarRequest {
|
||||||
|
pub has_star: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct SetFolderStarSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT /v1/folders/folder/{folder_token}/color_code ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct SetFolderColorCodeRequest {
|
||||||
|
/// `None` clears the color tag.
|
||||||
|
pub maybe_color_code: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct SetFolderColorCodeSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT /v1/folders/folder/{folder_token}/cover_image ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct SetFolderCoverImageRequest {
|
||||||
|
/// `None` clears the cover image. If set, the server resolves it to a
|
||||||
|
/// usable cover-image media file token: image/video media files are
|
||||||
|
/// used directly; other types fall back to the file's own
|
||||||
|
/// `maybe_cover_image_media_file_token` (and fail if neither path
|
||||||
|
/// applies).
|
||||||
|
pub maybe_media_file_token: Option<MediaFileToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct SetFolderCoverImageSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
|
||||||
|
/// The media file token that ended up being stored as the cover image
|
||||||
|
/// (may differ from the requested token if the server resolved through
|
||||||
|
/// the file's own cover image). `None` when the cover was cleared.
|
||||||
|
pub maybe_resolved_cover_media_file_token: Option<MediaFileToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DELETE /v1/folders/folder/{folder_token} ──
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct DeleteFolderSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
|
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||||
|
use tokens::tokens::folders::FolderToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
|
||||||
|
// ── GET /v1/folders/media_files/{folder_token} ──
|
||||||
|
//
|
||||||
|
// NB: `ListFolderMediaFilesSuccessResponse` and `FolderMediaFileListItem`
|
||||||
|
// live in `storyteller_web`'s handler because the wire shape embeds
|
||||||
|
// `MediaFileCoverImageDetails` and `MediaLinks` constructors that depend
|
||||||
|
// on the request's `MediaDomain` + `ServerEnvironment`.
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct FolderMediaFilesPathInfo {
|
||||||
|
pub folder_token: FolderToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||||
|
pub struct ListFolderMediaFilesQueryParams {
|
||||||
|
pub cursor: Option<String>,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GET /v1/folders/media_files_without_folder ──
|
||||||
|
//
|
||||||
|
// NB: `ListMediaFilesWithoutFolderSuccessResponse` and its list item live in
|
||||||
|
// `storyteller_web`'s handler because the wire shape embeds
|
||||||
|
// `MediaFileCoverImageDetails` and `MediaLinks` constructors that depend
|
||||||
|
// on the request's `MediaDomain` + `ServerEnvironment`.
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||||
|
pub struct ListMediaFilesWithoutFolderQueryParams {
|
||||||
|
pub cursor: Option<String>,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
|
||||||
|
/// Optional filter on the coarse media class.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// - `?filter_media_class=image`
|
||||||
|
/// - `?filter_media_class=video`
|
||||||
|
/// - `?filter_media_class=mesh`
|
||||||
|
/// - `?filter_media_class=splat`
|
||||||
|
pub filter_media_class: Option<MediaFileClass>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT /v1/folders/media_files/{folder_token}/bulk_add ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct BulkAddFolderMediaFilesRequest {
|
||||||
|
/// Media files to add to the folder. Tokens that don't exist or are
|
||||||
|
/// soft-deleted are silently skipped. Already-added rows are no-ops
|
||||||
|
/// (idempotent via INSERT IGNORE on the composite primary key).
|
||||||
|
pub media_file_tokens: Vec<MediaFileToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct BulkAddFolderMediaFilesSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
/// Tokens that pre-existed (existed + not deleted). Already-added rows
|
||||||
|
/// are included since the operation is idempotent.
|
||||||
|
pub accepted_media_file_tokens: Vec<MediaFileToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT /v1/folders/media_files/{folder_token}/bulk_remove ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct BulkRemoveFolderMediaFilesRequest {
|
||||||
|
pub media_file_tokens: Vec<MediaFileToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct BulkRemoveFolderMediaFilesSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
/// How many membership rows were actually hard-deleted.
|
||||||
|
pub removed_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POST /v1/folders/media_files/{folder_token}/bulk_move ──
|
||||||
|
|
||||||
|
/// Atomically move the listed media files from `source_folder` to the
|
||||||
|
/// URL `folder_token` (destination). Idempotent on both sides: tokens
|
||||||
|
/// that aren't currently in the source are silently skipped, and tokens
|
||||||
|
/// that are already in the destination stay there.
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct BulkMoveFolderMediaFilesRequest {
|
||||||
|
pub source_folder: FolderToken,
|
||||||
|
pub media_file_tokens: Vec<MediaFileToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct BulkMoveFolderMediaFilesSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
|
||||||
|
/// Number of membership rows actually deleted from the source folder.
|
||||||
|
pub removed_from_source_count: u64,
|
||||||
|
|
||||||
|
/// Number of membership rows actually inserted into the destination
|
||||||
|
/// folder. May be lower than the input length because (a) tokens that
|
||||||
|
/// were already present in the destination don't insert, and (b)
|
||||||
|
/// tokens whose media file doesn't exist / is soft-deleted are filtered
|
||||||
|
/// out before the insert.
|
||||||
|
pub added_to_destination_count: u64,
|
||||||
|
|
||||||
|
/// The media file tokens that ended up resident in the destination
|
||||||
|
/// folder after the move — i.e. the subset of the input that actually
|
||||||
|
/// referred to live media files. Includes both newly-inserted rows and
|
||||||
|
/// rows that were already there.
|
||||||
|
pub accepted_media_file_tokens: Vec<MediaFileToken>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod common;
|
||||||
|
pub mod folder;
|
||||||
|
pub mod media_files;
|
||||||
|
pub mod subfolder;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
|
use tokens::tokens::folders::FolderToken;
|
||||||
|
|
||||||
|
use crate::folders::common::FolderInfo;
|
||||||
|
|
||||||
|
// ── GET /v1/folders/subfolders/{folder_token} ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct SubfolderPathInfo {
|
||||||
|
pub folder_token: FolderToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||||
|
pub struct ListSubfoldersQueryParams {
|
||||||
|
pub cursor: Option<String>,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct ListSubfoldersSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub subfolders: Vec<FolderInfo>,
|
||||||
|
pub maybe_cursor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_add ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct BulkAddSubfoldersRequest {
|
||||||
|
/// The folders to reparent under the URL `folder_token`. Tokens that
|
||||||
|
/// don't exist, aren't owned by the caller, are soft-deleted, or are
|
||||||
|
/// the parent itself are silently skipped (idempotent).
|
||||||
|
pub subfolder_tokens: Vec<FolderToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct BulkAddSubfoldersSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
/// Tokens that were actually accepted (existed + owned + not deleted +
|
||||||
|
/// not the parent itself). Already-parented tokens are included here
|
||||||
|
/// since the operation is idempotent.
|
||||||
|
pub accepted_subfolder_tokens: Vec<FolderToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_remove ──
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct BulkRemoveSubfoldersRequest {
|
||||||
|
/// Folders to unparent. Only rows currently parented to the URL
|
||||||
|
/// `folder_token` are affected; others are silently skipped.
|
||||||
|
pub subfolder_tokens: Vec<FolderToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct BulkRemoveSubfoldersSuccessResponse {
|
||||||
|
pub success: bool,
|
||||||
|
/// How many rows were actually unparented.
|
||||||
|
pub removed_count: u64,
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||||
|
use enums::common::generation::common_image_model::CommonImageModel;
|
||||||
|
use enums::common::generation::common_quality::CommonQuality;
|
||||||
|
use enums::common::generation::common_resolution::CommonResolution;
|
||||||
|
use enums::common::generation_provider::GenerationProvider;
|
||||||
|
|
||||||
|
pub const ESTIMATE_IMAGE_COST_PATH: &str = "/v1/generate/cost_estimate/image";
|
||||||
|
|
||||||
|
/// Request body for the image cost estimate endpoint.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct EstimateImageCostRequest {
|
||||||
|
/// The image model to estimate costs for.
|
||||||
|
pub model: CommonImageModel,
|
||||||
|
|
||||||
|
/// The provider to route the generation through.
|
||||||
|
pub provider: GenerationProvider,
|
||||||
|
|
||||||
|
/// The type of generation (determines whether input images are involved).
|
||||||
|
/// This is a tagged enum, so it looks like:
|
||||||
|
/// "generation_mode": {"type": "text_to_image"}
|
||||||
|
/// "generation_mode": {"type": "image_edit", "count": 2}
|
||||||
|
pub generation_mode: GenerationMode,
|
||||||
|
|
||||||
|
/// Optional aspect ratio.
|
||||||
|
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||||
|
|
||||||
|
/// Optional resolution.
|
||||||
|
pub resolution: Option<CommonResolution>,
|
||||||
|
|
||||||
|
// Optional quality.
|
||||||
|
pub quality: Option<CommonQuality>,
|
||||||
|
|
||||||
|
/// Number of images to generate in parallel.
|
||||||
|
pub image_batch_count: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the type of image generation being requested.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum GenerationMode {
|
||||||
|
/// No input images — generate from prompt only.
|
||||||
|
TextToImage,
|
||||||
|
/// One or more input images are provided for editing/compositing.
|
||||||
|
ImageEdit { count: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response body for the image cost estimate endpoint.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EstimateImageCostResponse {
|
||||||
|
pub success: bool,
|
||||||
|
|
||||||
|
/// Estimated cost in credits.
|
||||||
|
pub cost_in_credits: Option<u64>,
|
||||||
|
|
||||||
|
/// Estimated cost in USD cents.
|
||||||
|
pub cost_in_usd_cents: Option<u64>,
|
||||||
|
|
||||||
|
/// Whether the generation is free for this user/plan.
|
||||||
|
pub is_free: bool,
|
||||||
|
|
||||||
|
/// Whether the user has unlimited generations.
|
||||||
|
pub is_unlimited: bool,
|
||||||
|
|
||||||
|
/// Whether the user is rate limited.
|
||||||
|
pub is_rate_limited: bool,
|
||||||
|
|
||||||
|
/// Whether the output will have a watermark.
|
||||||
|
pub has_watermark: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error response for the image cost estimate endpoint.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EstimateImageCostError {
|
||||||
|
pub success: bool,
|
||||||
|
pub error_type: EstimateImageCostErrorType,
|
||||||
|
pub error_message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum EstimateImageCostErrorType {
|
||||||
|
InvalidProviderForModel,
|
||||||
|
InvalidInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialization_text_to_image() {
|
||||||
|
let request = EstimateImageCostRequest {
|
||||||
|
model: CommonImageModel::NanoBananaPro,
|
||||||
|
provider: GenerationProvider::Artcraft,
|
||||||
|
generation_mode: GenerationMode::TextToImage,
|
||||||
|
aspect_ratio: None,
|
||||||
|
resolution: None,
|
||||||
|
quality: None,
|
||||||
|
image_batch_count: None,
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&request).unwrap();
|
||||||
|
assert!(serialized.contains("\"generation_mode\":{\"type\":\"text_to_image\"}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialization_image_edit() {
|
||||||
|
let request = EstimateImageCostRequest {
|
||||||
|
model: CommonImageModel::NanoBananaPro,
|
||||||
|
provider: GenerationProvider::Artcraft,
|
||||||
|
generation_mode: GenerationMode::ImageEdit { count: 2 },
|
||||||
|
aspect_ratio: None,
|
||||||
|
resolution: None,
|
||||||
|
quality: None,
|
||||||
|
image_batch_count: None,
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&request).unwrap();
|
||||||
|
assert!(serialized.contains("\"generation_mode\":{\"type\":\"image_edit\",\"count\":2}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use enums::common::generation::common_splat_model::CommonSplatModel;
|
||||||
|
use enums::common::generation_provider::GenerationProvider;
|
||||||
|
|
||||||
|
pub const ESTIMATE_SPLAT_COST_PATH: &str = "/v1/generate/cost_estimate/splat";
|
||||||
|
|
||||||
|
/// Request body for the splat cost estimate endpoint.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EstimateSplatCostRequest {
|
||||||
|
/// The splat model to estimate costs for.
|
||||||
|
pub model: CommonSplatModel,
|
||||||
|
|
||||||
|
/// The provider to route the generation through.
|
||||||
|
pub provider: GenerationProvider,
|
||||||
|
|
||||||
|
/// Whether the request includes a reference image.
|
||||||
|
pub has_reference_image: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response body for the splat cost estimate endpoint.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EstimateSplatCostResponse {
|
||||||
|
pub success: bool,
|
||||||
|
|
||||||
|
/// Estimated cost in credits.
|
||||||
|
pub cost_in_credits: Option<u64>,
|
||||||
|
|
||||||
|
/// Estimated cost in USD cents.
|
||||||
|
pub cost_in_usd_cents: Option<u64>,
|
||||||
|
|
||||||
|
/// Whether the generation is free for this user/plan.
|
||||||
|
pub is_free: bool,
|
||||||
|
|
||||||
|
/// Whether the user has unlimited generations.
|
||||||
|
pub is_unlimited: bool,
|
||||||
|
|
||||||
|
/// Whether the user is rate limited.
|
||||||
|
pub is_rate_limited: bool,
|
||||||
|
|
||||||
|
/// Whether the output will have a watermark.
|
||||||
|
pub has_watermark: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error response for the splat cost estimate endpoint.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EstimateSplatCostError {
|
||||||
|
pub success: bool,
|
||||||
|
pub error_type: EstimateSplatCostErrorType,
|
||||||
|
pub error_message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum EstimateSplatCostErrorType {
|
||||||
|
InvalidProviderForModel,
|
||||||
|
InvalidInput,
|
||||||
|
}
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||||
|
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||||
|
use enums::common::generation::common_resolution::CommonResolution;
|
||||||
|
use enums::common::generation_provider::GenerationProvider;
|
||||||
|
|
||||||
|
pub const ESTIMATE_VIDEO_COST_PATH: &str = "/v1/generate/cost_estimate/video";
|
||||||
|
|
||||||
|
/// Request body for the video cost estimate endpoint.
|
||||||
|
///
|
||||||
|
/// Every model cares about its own set of parameters and not all
|
||||||
|
/// parameters may be relevant for every model.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct EstimateVideoCostRequest {
|
||||||
|
/// The video model to estimate costs for.
|
||||||
|
pub model: CommonVideoModel,
|
||||||
|
|
||||||
|
/// The provider to route the generation through.
|
||||||
|
pub provider: GenerationProvider,
|
||||||
|
|
||||||
|
/// The type of generation (determines how many input images are involved).
|
||||||
|
/// This is a tagged enum, so it looks like:
|
||||||
|
/// "generation_mode": {"type": "text_to_video"}
|
||||||
|
/// "generation_mode": {"type": "reference_image_to_video", "count": 1}
|
||||||
|
pub generation_mode: GenerationMode,
|
||||||
|
|
||||||
|
/// Optional aspect ratio.
|
||||||
|
pub aspect_ratio: Option<CommonAspectRatio>,
|
||||||
|
|
||||||
|
/// Optional resolution.
|
||||||
|
pub resolution: Option<CommonResolution>,
|
||||||
|
|
||||||
|
/// Duration in seconds.
|
||||||
|
pub duration_seconds: Option<u16>,
|
||||||
|
|
||||||
|
/// Number of videos to generate in parallel.
|
||||||
|
pub video_batch_count: Option<u16>,
|
||||||
|
|
||||||
|
/// Whether to generate audio for the video.
|
||||||
|
pub generate_audio: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the type of video generation being requested.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum GenerationMode {
|
||||||
|
TextToVideo,
|
||||||
|
StartFrameToVideo,
|
||||||
|
StartAndEndFrameToVideo,
|
||||||
|
ReferenceImageToVideo { count: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response body for the video cost estimate endpoint.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EstimateVideoCostResponse {
|
||||||
|
pub success: bool,
|
||||||
|
|
||||||
|
/// Estimated cost in credits.
|
||||||
|
pub cost_in_credits: Option<u64>,
|
||||||
|
|
||||||
|
/// Estimated cost in USD cents.
|
||||||
|
pub cost_in_usd_cents: Option<u64>,
|
||||||
|
|
||||||
|
/// Whether the generation is free for this user/plan.
|
||||||
|
pub is_free: bool,
|
||||||
|
|
||||||
|
/// Whether the user has unlimited generations.
|
||||||
|
pub is_unlimited: bool,
|
||||||
|
|
||||||
|
/// Whether the user is rate limited.
|
||||||
|
pub is_rate_limited: bool,
|
||||||
|
|
||||||
|
/// Whether the output will have a watermark.
|
||||||
|
pub has_watermark: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error response for the video cost estimate endpoint.
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct EstimateVideoCostError {
|
||||||
|
pub success: bool,
|
||||||
|
pub error_type: EstimateVideoCostErrorType,
|
||||||
|
pub error_message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum EstimateVideoCostErrorType {
|
||||||
|
InvalidProviderForModel,
|
||||||
|
InvalidInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialization_tagging1() {
|
||||||
|
let request = EstimateVideoCostRequest {
|
||||||
|
model: CommonVideoModel::GrokVideo,
|
||||||
|
provider: GenerationProvider::Artcraft,
|
||||||
|
generation_mode: GenerationMode::TextToVideo,
|
||||||
|
aspect_ratio: None,
|
||||||
|
resolution: None,
|
||||||
|
duration_seconds: None,
|
||||||
|
video_batch_count: None,
|
||||||
|
generate_audio: None,
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&request).unwrap();
|
||||||
|
assert!(serialized.contains("\"generation_mode\":{\"type\":\"text_to_video\"}"));
|
||||||
|
//assert_eq!(serialized, "{}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialization_tagging2() {
|
||||||
|
let request = EstimateVideoCostRequest {
|
||||||
|
model: CommonVideoModel::GrokVideo,
|
||||||
|
provider: GenerationProvider::Artcraft,
|
||||||
|
generation_mode: GenerationMode::ReferenceImageToVideo { count: 1 },
|
||||||
|
aspect_ratio: None,
|
||||||
|
resolution: None,
|
||||||
|
duration_seconds: None,
|
||||||
|
video_batch_count: None,
|
||||||
|
generate_audio: None,
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&request).unwrap();
|
||||||
|
assert!(serialized.contains("\"generation_mode\":{\"type\":\"reference_image_to_video\",\"count\":1}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod estimate_image_cost;
|
||||||
|
pub mod estimate_splat_cost;
|
||||||
|
pub mod estimate_video_cost;
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH: &str = "/v1/generate/image/angle/flux_2_lora";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct Flux2LoraEditImageAngleRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// The image to edit with angle adjustment.
|
||||||
|
pub image_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Horizontal rotation angle in degrees.
|
||||||
|
pub horizontal_angle: Option<f64>,
|
||||||
|
|
||||||
|
/// Vertical rotation angle in degrees.
|
||||||
|
pub vertical_angle: Option<f64>,
|
||||||
|
|
||||||
|
/// Zoom level.
|
||||||
|
pub zoom: Option<f64>,
|
||||||
|
|
||||||
|
/// Number of images to generate. Default is one.
|
||||||
|
pub num_images: Option<Flux2LoraEditImageAngleNumImages>,
|
||||||
|
|
||||||
|
/// Output image size. Default is square_hd.
|
||||||
|
pub image_size: Option<Flux2LoraEditImageAngleImageSize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Flux2LoraEditImageAngleNumImages {
|
||||||
|
One,
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Flux2LoraEditImageAngleImageSize {
|
||||||
|
Square,
|
||||||
|
SquareHd,
|
||||||
|
PortraitFourThree,
|
||||||
|
PortraitSixteenNine,
|
||||||
|
LandscapeFourThree,
|
||||||
|
LandscapeSixteenNine,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct Flux2LoraEditImageAngleResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod flux_2_lora_edit_image_angle;
|
||||||
|
pub mod qwen_edit_2511_edit_image_angle;
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH: &str = "/v1/generate/image/angle/qwen_edit_2511";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||||
|
pub struct QwenEdit2511EditImageAngleRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// The image to edit with angle adjustment.
|
||||||
|
pub image_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Horizontal rotation angle in degrees.
|
||||||
|
pub horizontal_angle: Option<f64>,
|
||||||
|
|
||||||
|
/// Vertical rotation angle in degrees.
|
||||||
|
pub vertical_angle: Option<f64>,
|
||||||
|
|
||||||
|
/// Zoom level.
|
||||||
|
pub zoom: Option<f64>,
|
||||||
|
|
||||||
|
/// Additional text prompt to guide the generation.
|
||||||
|
pub additional_prompt: Option<String>,
|
||||||
|
|
||||||
|
/// Number of images to generate. Default is one.
|
||||||
|
pub num_images: Option<QwenEdit2511EditImageAngleNumImages>,
|
||||||
|
|
||||||
|
/// Output image size. Default is square_hd.
|
||||||
|
pub image_size: Option<QwenEdit2511EditImageAngleImageSize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum QwenEdit2511EditImageAngleNumImages {
|
||||||
|
One,
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum QwenEdit2511EditImageAngleImageSize {
|
||||||
|
Square,
|
||||||
|
SquareHd,
|
||||||
|
PortraitFourThree,
|
||||||
|
PortraitSixteenNine,
|
||||||
|
LandscapeFourThree,
|
||||||
|
LandscapeSixteenNine,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct QwenEdit2511EditImageAngleResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod remove_image_background;
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const REMOVE_IMAGE_BACKGROUND_PATH: &str = "/v1/generate/image/remove_background";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct RemoveImageBackgroundRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// Source of the file to remove the background from.
|
||||||
|
/// It must be an image.
|
||||||
|
pub media_file_token: Option<MediaFileToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct RemoveImageBackgroundResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const FLUX_PRO_KONTEXT_MAX_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/flux_pro_kontext_max";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FluxProKontextMaxEditImageRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// Text prompt to generate the image from.
|
||||||
|
pub prompt: Option<String>,
|
||||||
|
|
||||||
|
/// The image we're editing.
|
||||||
|
pub image_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Number of images to generate. Default is one.
|
||||||
|
pub num_images: Option<FluxProKontextMaxEditImageNumImages>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum FluxProKontextMaxEditImageNumImages {
|
||||||
|
One, // Default
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FluxProKontextMaxEditImageResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const GEMINI_25_FLASH_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/gemini_25_flash";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct Gemini25FlashEditImageRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// Text prompt to generate the image from.
|
||||||
|
pub prompt: Option<String>,
|
||||||
|
|
||||||
|
// TODO: OneOf for image_urls or image_media_tokens
|
||||||
|
/// Image media tokens to include in the editing context.
|
||||||
|
pub image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||||
|
|
||||||
|
/// Number of images to generate. Default is one.
|
||||||
|
pub num_images: Option<Gemini25FlashEditImageNumImages>,
|
||||||
|
|
||||||
|
/// Quality of the image to generate. Default is High.
|
||||||
|
pub image_quality: Option<Gemini25FlashEditImageImageQuality>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Gemini25FlashEditImageNumImages {
|
||||||
|
One, // Default
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Gemini25FlashEditImageImageQuality {
|
||||||
|
Auto,
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High, // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct Gemini25FlashEditImageResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const GPT_IMAGE_1_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/gpt_image_1";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct GptImage1EditImageRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// Text prompt to generate the image from.
|
||||||
|
pub prompt: Option<String>,
|
||||||
|
|
||||||
|
// TODO: OneOf for image_urls or image_media_tokens
|
||||||
|
/// Image media tokens to include in the editing context.
|
||||||
|
pub image_media_tokens: Option<Vec<MediaFileToken>>,
|
||||||
|
|
||||||
|
/// Size of the image to generate. Default is Square.
|
||||||
|
pub image_size: Option<GptImage1EditImageImageSize>,
|
||||||
|
|
||||||
|
/// Number of images to generate. Default is one.
|
||||||
|
pub num_images: Option<GptImage1EditImageNumImages>,
|
||||||
|
|
||||||
|
/// Quality of the image to generate. Default is High.
|
||||||
|
pub image_quality: Option<GptImage1EditImageImageQuality>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum GptImage1EditImageImageSize {
|
||||||
|
Square, // 1:1, Default
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum GptImage1EditImageNumImages {
|
||||||
|
One, // Default
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum GptImage1EditImageImageQuality {
|
||||||
|
Auto,
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High, // Default
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct GptImage1EditImageResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
pub mod flux_pro_kontext_max_edit_image;
|
||||||
|
pub mod gemini_25_flash_edit_image;
|
||||||
|
pub mod gpt_image_1_edit_image;
|
||||||
|
pub mod qwen_edit_image;
|
||||||
|
pub mod seededit_3_edit_image;
|
||||||
|
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const QWEN_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/qwen";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct QwenEditImageRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// The image we're editing.
|
||||||
|
pub image_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Text prompt to generate the image from.
|
||||||
|
pub prompt: Option<String>,
|
||||||
|
|
||||||
|
/// Text prompt to generate the image from.
|
||||||
|
pub negative_prompt: Option<String>,
|
||||||
|
|
||||||
|
/// Acceleration level for image generation.
|
||||||
|
/// Options: 'none', 'regular'. Higher acceleration increases speed.
|
||||||
|
/// 'regular' balances speed and quality. Default value: "none"
|
||||||
|
pub acceleration: Option<QwenEditImageAcceleration>,
|
||||||
|
|
||||||
|
/// Number of images to generate. Default is one.
|
||||||
|
pub num_images: Option<QwenEditImageNumImages>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub enum QwenEditImageAcceleration {
|
||||||
|
None,
|
||||||
|
Regular,
|
||||||
|
High,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum QwenEditImageNumImages {
|
||||||
|
One, // Default
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct QwenEditImageResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const SEEDEDIT_3_EDIT_IMAGE_PATH: &str = "/v1/generate/image/edit/seededit_3";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SeedEdit3EditImageRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// The image we're editing.
|
||||||
|
pub image_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Text prompt to generate the image from.
|
||||||
|
pub prompt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct SeedEdit3EditImageResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const FLUX_DEV_JUGGERNAUT_INPAINT_PATH: &str = "/v1/generate/image/inpaint/flux_dev_juggernaut";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FluxDevJuggernautInpaintImageRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// Text prompt to generate the image from.
|
||||||
|
pub prompt: Option<String>,
|
||||||
|
|
||||||
|
/// The image we're editing.
|
||||||
|
pub image_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// The mask to use against the image.
|
||||||
|
pub mask_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Number of images to generate. Default is one.
|
||||||
|
pub num_images: Option<FluxDevJuggernautInpaintImageNumImages>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum FluxDevJuggernautInpaintImageNumImages {
|
||||||
|
One, // Default
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FluxDevJuggernautInpaintImageResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||||
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub const FLUX_PRO_1_INPAINT_PATH: &str = "/v1/generate/image/inpaint/flux_pro_1";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FluxPro1InpaintImageRequest {
|
||||||
|
/// Idempotency token to prevent duplicate requests.
|
||||||
|
pub uuid_idempotency_token: String,
|
||||||
|
|
||||||
|
/// Text prompt to generate the image from.
|
||||||
|
pub prompt: Option<String>,
|
||||||
|
|
||||||
|
/// The image we're editing.
|
||||||
|
pub image_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// The mask to use against the image.
|
||||||
|
pub mask_media_token: MediaFileToken,
|
||||||
|
|
||||||
|
/// Number of images to generate. Default is one.
|
||||||
|
pub num_images: Option<FluxPro1InpaintImageNumImages>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum FluxPro1InpaintImageNumImages {
|
||||||
|
One, // Default
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct FluxPro1InpaintImageResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub inference_job_token: InferenceJobToken,
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod flux_dev_juggernaut_inpaint_image;
|
||||||
|
pub mod flux_pro_1_inpaint_image;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user