Compare commits

...

No commits in common. "bt-sadtalker" and "bt-new-models" have entirely different histories.

7804 changed files with 517253 additions and 663685 deletions

View File

@ -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

View File

@ -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

73
.claude/rules/testing.md Normal file
View File

@ -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

14
.config/hakari.toml Normal file
View File

@ -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

View File

@ -1,16 +0,0 @@
*secret*sh
*~
.dockerignore
.env
.env-secrets
.git
.idea/
Dockerfile
__pycache__/
_ignored_schema.rs
models/Wav2Lip/python/
models/tacotron2/python/
production_database_secret_connect.sh
secrets.toml
secrets.yaml
target/

77
.env
View File

@ -1,77 +0,0 @@
SERVER_ENVIRONMENT=development
DATABASE_URL=mysql://storyteller:password@localhost/storyteller
#DATABASE_URL=mysql://root:root@localhost/storyteller
#DATABASE_URL=mysql://root@localhost/storyteller
# Diesel-cli reads this for the migration directory to use.
# We've renamed it so the migrations are sorted near the top of the project directory tree
# (since they kind of serve as documentation)
MIGRATION_DIRECTORY=_sql/migrations/
# SQLX normally requires a live database to reflect upon at build time.
# This setting turns that on and requires a `sql-data.json` cache file instead.
# See https://github.com/launchbadge/sqlx/tree/master/sqlx-cli#readme
#SQLX_OFFLINE=true
# NB(bt, 2023-03-20):
# These are used in storyteller-web, image-generation-service, and websocket-gateway.
# I'm moving them to env configs that live under storyteller-web/.env.
# This will break the other services for now.
# # We can't serve HTTPS locally without self-signed certificate nonsense.
# COOKIE_SECURE=false
# COOKIE_DOMAIN=.jungle.horse
# WEBSITE_HOMEPAGE_REDIRECT=http://jungle.horse
# Bucket config (non-secret)
REGION_NAME=us-east1
TTS_DOWNLOAD_BUCKET_NAME=dev-vocodes-private-uploads
TTS_DOWNLOAD_BUCKET_ROOT=/user_uploaded_tts_models
W2L_PRIVATE_DOWNLOAD_BUCKET_NAME=dev-vocodes-private-uploads
W2L_PUBLIC_DOWNLOAD_BUCKET_NAME=dev-vocodes-public
PRIVATE_BUCKET_NAME=dev-vocodes-private-uploads
PUBLIC_BUCKET_NAME=dev-vocodes-public
# Bucket roots
AUDIO_UPLOADS_BUCKET_ROOT=/user_uploaded_w2l_audio
W2L_DOWNLOAD_BUCKET_ROOT=/user_uploaded_w2l_templates
W2L_INFERENCE_OUTPUT_BUCKET_ROOT=/w2l_output
W2L_CODE_DIRECTORY=/home/bt/dev/storyteller/storyteller-web/models/Wav2Lip
W2L_INFERENCE_SCRIPT_NAME=vocodes_process_inference.py
W2L_MODEL_FILENAME=wav2lip_gan.pth
#W2L_MODEL_CHECKPOINT=/home/bt/models/wav2lip/s3fd-619a316812.pth
W2L_MODEL_CHECKPOINT=/home/bt/models/wav2lip/wav2lip_gan.pth
W2L_END_BUMP_FILENAME=vocodes-short-end-bump.mp4
W2L_DOWNLOAD_SCRIPT_NAME=vocodes_process_upload.py
TTS_CODE_DIRECTORY=/home/bt/dev/storyteller/storyteller-ml/tts
TTS_INFERENCE_SCRIPT_NAME=vocodes_inference.py
TACOTRON_ROOT_CODE_DIRECTORY=/home/bt/dev/storyteller/storyteller-ml/tts
TACOTRON_VIRTUAL_ENV_ACTIVATION_COMMAND="source python/bin/activate"
TACOTRON_MODEL_CHECK_SCRIPT_NAME="vocodes_model_check_tacotron.py"
TALKNET_ROOT_CODE_DIRECTORY=/home/bt/dev/storyteller/storyteller-ml/tts
TALKNET_MODEL_CHECK_SCRIPT_NAME="vocodes_model_check_talknet.py"
HIFIGAN_ROOT_CODE_DIRECTORY=/home/bt/dev/storyteller/storyteller-ml/tts
HIFIGAN_VIRTUAL_ENV_ACTIVATION_COMMAND="source python/bin/activate"
HIFIGAN_MODEL_CHECK_SCRIPT_NAME="vocodes_model_check_hifigan.py"
TTS_INFERENCE_SIDECAR_HOSTNAME=localhost:8000
#TTS_WAVEGLOW_VOCODER_MODEL_FILENAME=waveglow_256channels_universal_v5.pt # THIS SOUNDS HORRID
#TTS_WAVEGLOW_VOCODER_MODEL_FILENAME=waveglow_256channels_v4_uberduck.pt # THIS IS TOO SMALL! 300MB vs 1.2 GB!
TTS_WAVEGLOW_VOCODER_MODEL_FILENAME=waveglow_256channels_v4.pt
TTS_HIFIGAN_VOCODER_MODEL_FILENAME=hifigan_universal_g_02500000.pt
TTS_HIFIGAN_SUPERRES_VOCODER_MODEL_FILENAME=hifigan_superres_twilight_33000.pt
# Development key. Permissible to check in.
NEWRELIC_API_KEY=c402be1c4c4216ec83d0cfc0851520cfFFFFNRAL

5
.gitattributes vendored
View File

@ -1,5 +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
windows/sqlite linguist-vendored
old_code/voder-vocodes-1.0/frontends/admin/node_modules/** linguist-vendored
test_data/html_scraping/** linguist-vendored
windows/sqlite/** linguist-vendored

View File

@ -1,11 +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://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# 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: "daily"
interval: "weekly"

View File

@ -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 }}

View File

@ -0,0 +1,107 @@
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
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
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 }}

View File

@ -1,115 +0,0 @@
# Adapted from
# https://github.com/actions/starter-workflows/ci/docker-publish.yml
# If using base images, don't forget to add READ access, eg. from a package settings page like:
# https://github.com/orgs/storytold/packages/container/docker-base-images-rust-ssl/settings/actions_access
name: "[CPU] Build GHCR Docker artifact"
on:
push:
branches:
# NB: Default-branch doesn't work, despite Github's documentation.
#- $default-branch
- master
env:
IMAGE_NAME: storyteller-rust
jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
# See also https://depot.dev/blog/docker-layer-caching-in-github-actions
# See also https://evilmartians.com/chronicles/build-images-on-github-actions-with-docker-layer-caching
build-docker-image:
name: "Build Docker Image"
#runs-on: ubuntu-22.04 # runs-on: ubuntu-latest
#runs-on: github-ubuntu-16core # NB: Premium GitHub runner with more cores
runs-on: github-ubuntu-64core # NB: Premium GitHub runner with more cores
if: github.event_name == 'push'
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v3
# - uses: docker/setup-buildx-action@v1
# name: Set up Docker Buildx
- uses: docker/login-action@v1
name: Login to GitHub Container Registry
with:
registry: ghcr.io
username: ${{ github.actor }} # username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Calculate short SHA
id: vars
# echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
run: |
echo "sha_short=$(echo ${{ github.sha }} | cut -c1-12)" >> $GITHUB_OUTPUT
- name: Emit short SHA
run: |
echo "Full SHA: ${{ github.sha }}"
echo "Short SHA: ${{ steps.vars.outputs.sha_short }}"
#- uses: actions/cache@v2
# name: Cache Docker layers
# with:
# path: /tmp/.buildx-cache
# key: ${{ runner.os }}-buildx-${{ github.sha }}
# restore-keys: |
# ${{ runner.os }}-buildx-
# Docs for this action are at https://github.com/marketplace/actions/build-docker-images-using-cache
- uses: whoan/docker-build-with-cache-action@v6
with:
registry: ghcr.io
username: ${{ github.actor }} # username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
image_name: storytold/storyteller-rust
dockerfile: service_cpu.Dockerfile
# NB: The `git_push_tag: true` option does not work as expected, so we put the sha in `image_tag`:
# https://github.com/whoan/docker-build-with-cache-action/issues/72
image_tag: latest,${{ github.sha }},${{ steps.vars.outputs.sha_short }}
# NB: Inject the Git SHA into the container so the servers can emit their own version:
build_extra_args: "--build-arg=GIT_SHA=${{ github.sha }}"
# - name: Build image
# run: |
# SHORT_SHA=$(echo ${GITHUB_SHA} | cut -c1-12)
# docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" --build-arg GIT_SHA=${SHORT_SHA}
# - name: Push image to GitHub Container Registry
# run: |
# IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# # Change all uppercase to lowercase
# IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# # Strip git ref prefix from version
# VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# # Strip "v" prefix from tag name
# [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# # Use Docker `latest` tag convention
# [ "$VERSION" == "$default-branch" ] && VERSION=latest
# echo IMAGE_ID=$IMAGE_ID
# echo VERSION=$VERSION
# docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
# docker push $IMAGE_ID:$VERSION
# SHORT_SHA=$(echo ${GITHUB_SHA} | cut -c1-12)
# docker tag $IMAGE_NAME $IMAGE_ID:$SHORT_SHA
# docker push $IMAGE_ID:$SHORT_SHA
- name: Cleanup / Debug
# This job will run even on failures to help with debugging.
if: always()
run: |
echo "Report at final github action step:"
echo "Disk usage:"
pwd
du -hsc * | sort -hr

View File

@ -1,115 +0,0 @@
# Adapted from
# https://github.com/actions/starter-workflows/ci/docker-publish.yml
# If using base images, don't forget to add READ access, eg. from a package settings page like:
# https://github.com/orgs/storytold/packages/container/docker-base-images-rust-ssl/settings/actions_access
name: "[GPU] Build GHCR Docker artifact"
on:
push:
branches:
# NB: Default-branch doesn't work, despite Github's documentation.
#- $default-branch
- master
env:
IMAGE_NAME: storyteller-rust-gpu
jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
# See also https://depot.dev/blog/docker-layer-caching-in-github-actions
# See also https://evilmartians.com/chronicles/build-images-on-github-actions-with-docker-layer-caching
build-docker-image:
name: "Build Docker Image"
#runs-on: ubuntu-22.04 # runs-on: ubuntu-latest
#runs-on: github-ubuntu-16core # NB: Premium GitHub runner with more cores
runs-on: github-ubuntu-64core # NB: Premium GitHub runner with more cores
if: github.event_name == 'push'
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v3
# - uses: docker/setup-buildx-action@v1
# name: Set up Docker Buildx
- uses: docker/login-action@v1
name: Login to GitHub Container Registry
with:
registry: ghcr.io
username: ${{ github.actor }} # username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Calculate short SHA
id: vars
# echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
run: |
echo "sha_short=$(echo ${{ github.sha }} | cut -c1-12)" >> $GITHUB_OUTPUT
- name: Emit short SHA
run: |
echo "Full SHA: ${{ github.sha }}"
echo "Short SHA: ${{ steps.vars.outputs.sha_short }}"
#- uses: actions/cache@v2
# name: Cache Docker layers
# with:
# path: /tmp/.buildx-cache
# key: ${{ runner.os }}-buildx-${{ github.sha }}
# restore-keys: |
# ${{ runner.os }}-buildx-
# Docs for this action are at https://github.com/marketplace/actions/build-docker-images-using-cache
- uses: whoan/docker-build-with-cache-action@v6
with:
registry: ghcr.io
username: ${{ github.actor }} # username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
image_name: storytold/storyteller-rust-gpu
dockerfile: service_gpu.Dockerfile
# NB: The `git_push_tag: true` option does not work as expected, so we put the sha in `image_tag`:
# https://github.com/whoan/docker-build-with-cache-action/issues/72
image_tag: latest,${{ github.sha }},${{ steps.vars.outputs.sha_short }}
# NB: Inject the Git SHA into the container so the servers can emit their own version:
build_extra_args: "--build-arg=GIT_SHA=${{ github.sha }}"
# - name: Build image
# run: |
# SHORT_SHA=$(echo ${GITHUB_SHA} | cut -c1-12)
# docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" --build-arg GIT_SHA=${SHORT_SHA}
# - name: Push image to GitHub Container Registry
# run: |
# IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# # Change all uppercase to lowercase
# IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# # Strip git ref prefix from version
# VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# # Strip "v" prefix from tag name
# [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# # Use Docker `latest` tag convention
# [ "$VERSION" == "$default-branch" ] && VERSION=latest
# echo IMAGE_ID=$IMAGE_ID
# echo VERSION=$VERSION
# docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
# docker push $IMAGE_ID:$VERSION
# SHORT_SHA=$(echo ${GITHUB_SHA} | cut -c1-12)
# docker tag $IMAGE_NAME $IMAGE_ID:$SHORT_SHA
# docker push $IMAGE_ID:$SHORT_SHA
- name: Cleanup / Debug
# This job will run even on failures to help with debugging.
if: always()
run: |
echo "Report at final github action step:"
echo "Disk usage:"
pwd
du -hsc * | sort -hr

View File

@ -1,52 +0,0 @@
name: CI
on: [push, pull_request]
env:
MYSQL_URL: mysql://root:root@localhost/storyteller
DATABASE_URL: mysql://root:root@localhost/storyteller
REDIS_0_URL: redis://redisd@localhost/0
REDIS_1_URL: redis://redisd@localhost/1
DEBIAN_FRONTEND: noninteractive
jobs:
setup_build_test:
runs-on: ubuntu-20.04
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
- name: Setup mysql
run: |
sudo systemctl start mysql
sudo apt-get update
sudo apt-get -y install mysql-client libmysqlclient-dev
mysql -e "create user 'storyteller'@'localhost' IDENTIFIED BY 'password';" -uroot -proot
mysql -e "create database storyteller; grant all on storyteller.* to 'storyteller'@'localhost';" -uroot -proot
- name: Setup Redis
run: |
sudo apt-get -y install redis-server
sudo service redis-server start
- name: Setup Rust
run: |
rustup set profile minimal
rustup show
rustup install 1.66
rustup default 1.66
rustup show
cargo install sqlx-cli --features openssl-vendored
cargo install diesel_cli
- name: Run database migrations
run: |
# nothing to see here ;)
set +e +o pipefail
diesel migration run
set -e -o pipefail
- name: Build
run: cargo build --release
- name: Test
run: cargo test --release

66
.gitignore vendored
View File

@ -2,33 +2,70 @@
*secret*sh
.env
.env-aichatbot-secrets
.env-analytics-job
.env-elasticsearch-secrets
.env-migration-tool-secrets
.env-secrets
db.secrets
db_secrets.toml
production_database_secret_connect.sh
production_database_secret_query.sh
production_database_voice_clone_report_download.sh
secrets.toml
secrets.yaml
.env-support-tool-secrets
_develop/tools/.secrets
_develop/tools/db_secrets.toml
twitch_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 noise
# IntelliJ/CLion/RustRover noise
.idea/
# Python noise
@ -37,6 +74,9 @@ __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
@ -53,7 +93,15 @@ 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/

View File

@ -1,44 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="bt">
<words>
<w>arpabet</w>
<w>backfill</w>
<w>cashapp</w>
<w>cheermote</w>
<w>cidrs</w>
<w>crockford</w>
<w>culted</w>
<w>dalle</w>
<w>demuxer</w>
<w>downvote</w>
<w>fakeyou</w>
<w>ffprobe</w>
<w>greenlight</w>
<w>greenlighting</w>
<w>greenlit</w>
<w>hanashi</w>
<w>hifigan</w>
<w>hparams</w>
<w>huggingface</w>
<w>lipsync</w>
<w>pathbuf</w>
<w>replayable</w>
<w>retryably</w>
<w>scrollback</w>
<w>softvc</w>
<w>sql's</w>
<w>symphonia</w>
<w>tacotron</w>
<w>techcrunch</w>
<w>transcoded</w>
<w>transcodings</w>
<w>unpermitted</w>
<w>upserting</w>
<w>vits</w>
<w>vocoded</w>
<w>vocoder</w>
<w>vocoders</w>
<w>vocodes</w>
</words>
</dictionary>
</component>

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

75
AGENTS.md Normal file
View File

@ -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.

1
CLAUDE.md Symbolic link
View File

@ -0,0 +1 @@
AGENTS.md

12149
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,71 +1,126 @@
[workspace]
resolver = "2"
# Desktop application and its local build/test dependencies.
members = [
# Types: common strongly-typed tokens, ids, payloads, etc.
# Also common cloud buckets and other "strongly typed" configs.
"crates/_types/buckets",
"crates/_types/enums",
"crates/_types/reusable_types", # TODO(bt, 2022-12-21): Deprecate
"crates/_types/tokens",
# CLI tools for operations, etc.
"crates/cli/database_backup",
"crates/cli/database_cleanup",
"crates/cli/scrape_test",
# Shared libraries
"crates/lib/actix_helpers",
"crates/lib/api_clients/cloud_storage",
"crates/lib/api_clients/fakeyou_client",
"crates/lib/api_clients/google_drive_common",
"crates/lib/api_clients/twitch_common",
"crates/lib/bootstrap",
"crates/lib/caching/memory_caching",
"crates/lib/caching/redis_caching",
"crates/lib/concurrency",
"crates/lib/crockford",
"crates/lib/datetimes",
"crates/lib/deprecated/config", # TODO(bt, 2023-02-13): Deprecate or repurpose
"crates/lib/deprecated/container_common", # TODO(bt, 2023-02-13): Pull apart and rehome pieces
"crates/lib/deprecated/http_server_common", # TODO(bt, 2023-03-20): Move useful pieces to actix_helpers
"crates/lib/envvar",
"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/browser_emulation",
"crates/lib/build_metadata",
"crates/lib/caching/memory_store",
"crates/lib/cloudflare_errors",
"crates/lib/cloudflare_mitigation",
"crates/lib/cookie_store",
"crates/lib/cookie_store_wrapper",
"crates/lib/data/primitives",
"crates/lib/data/web_base64",
"crates/lib/errors",
"crates/lib/files/archives",
"crates/lib/files/file_to_base64_url",
"crates/lib/files/filesys",
"crates/lib/files/hashing",
"crates/lib/files/media",
"crates/lib/files/images",
"crates/lib/files/mimetypes",
"crates/lib/idempotency",
"crates/lib/jobs_common",
"crates/lib/mysql_queries",
"crates/lib/redis_common",
"crates/lib/server_environment",
"crates/lib/sqlite_queries",
"crates/lib/sqlx_mysql_helpers",
"crates/lib/subprocess_common",
"crates/lib/tts_common",
"crates/lib/url_config",
"crates/lib/user_input_common",
"crates/lib/web_scrapers",
# Programs: Desktop apps
"crates/programs/aichatbot-sidecar",
# Services: API servers, jobs, workers, etc.
"crates/service/job/analytics_job",
"crates/service/job/download_job",
"crates/service/job/inference_job",
"crates/service/job/tts_download_job",
"crates/service/job/tts_inference_job",
"crates/service/job/w2l_download_job",
"crates/service/job/w2l_inference_job",
"crates/service/other/twitch_pubsub_subscriber",
"crates/service/web/dummy_service",
"crates/service/web/storyteller_web",
"crates/service/web/websocket_gateway",
# Service Plugins: complete subsystems that can be plugged into services (users, billing, etc.)
"crates/service/plugins/billing",
"crates/service/plugins/inference_workloads",
"crates/service/plugins/users",
"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" }
browser_emulation = { path = "crates/lib/browser_emulation" }
cookie_store_wrapper = { path = "crates/lib/cookie_store_wrapper" }
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
Cranky.toml Normal file
View File

@ -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
LICENSE.md Normal file
View File

@ -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

181
README.md
View File

@ -1,46 +1,139 @@
storyteller-rust
================
<h1 align="center">ArtCraft</h1>
<p align="center"><strong>The IDE for artists.</strong></p>
Our Rust monorepo, which serves both our API surface area and our downloadable client applications.
<!-- Main video: https://www.youtube.com/watch?v=kzvQMdg66Go
GitHub media issue: https://github.com/storytold/artcraft/issues/1951 -->
```
."-,.__
`. `. ,
.--' .._,'"-' `.
. .' `'
`. / ,'
` '--. ,-"'
`"` | \
-. \, |
`--Y.' ___.
\ L._, \
_., `. < <\ _
,' ' `, `. | \ ( `
../, `. ` | .\`. \ \_
,' ,.. . _.,' ||\l ) '".
, ,' \ ,'.-.`-._,' | . _._`.
,' / \ \ `' ' `--/ | \ / / ..\
.' / \ . |\__ - _ ,'` ` / / `.`.
| ' .. `-...-" | `-' / / . `.
| / |L__ | | / / `. `.
, / . . | | / / ` `
/ / ,. ,`._ `-_ | | _ ,-' / ` \
/ . \"`_/. `-_ \_,. ,' +-' `-' _, ..,-. \`.
. ' .-f ,' ` '. \__.---' _ .' ' \ \
' / `.' l .' / \.. ,_|/ `. ,'` L`
|' _.-""` `. \ _,' ` \ `.___`.'"`-. , | | | \
|| ,' `. `. ' _,...._ ` | `/ ' | ' .|
|| ,' `. ;.,.---' ,' `. `.. `-' .-' /_ .' ;_ ||
|| ' V / / ` | ` ,' ,' '. ! `. ||
||/ _,-------7 ' . | `-' l / `||
. | ,' .- ,' || | .-. `. .' ||
`' ,' `".' | | `. '. -.' `'
/ ,' | |,' \-.._,.'/'
. / . . \ .''
.`. | `. / :_,'.'
\ `...\ _ ,'-. .' /_.-' S T O R Y T E L L E R
`-.__ `, `' . _.>----''. _ __ / Turning memes into dreams
.' /"' | "' '_ One Rust Monorepo at a time
/_|.-'\ ,". '.'`__'-( \
/ ,"'"\,' `/ `-.|"
```
https://github.com/user-attachments/assets/7339bd09-3e0d-44a7-a024-af9de64e4c11
<p align="center">
<a href="https://www.youtube.com/watch?v=kzvQMdg66Go">Watch the two-minute ArtCraft demo on YouTube</a>
</p>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Discord members online" src="https://img.shields.io/discord/1359579021108842617?style=for-the-badge&amp;label=Discord&amp;color=5865F2&amp;logo=discord&amp;logoColor=white"></a>
<a href="https://www.youtube.com/@OfficialArtCraftStudios"><img alt="YouTube" src="https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&amp;logo=youtube&amp;logoColor=white"></a>
<a href="https://x.com/get_artcraft"><img alt="X" src="https://img.shields.io/badge/X-181717?style=for-the-badge&amp;logo=x&amp;logoColor=white"></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>
<p align="center">
<a href="https://getartcraft.com/">Download ArtCraft</a> ·
<a href="https://github.com/storytold/artcraft/releases">GitHub releases</a> ·
<a href="./_docs/dev_setup.md">Build from source</a>
</p>
ArtCraft is the IDE for interactive AI image and video creation. Compose in 2D, stage scenes in 3D, and choose the models that fit your work. Turn prompting into *crafting* with visual tools for precise, repeatable results.
## Show, Don't Tell: Advanced Crafting Features
Artists need control over composition, character identity, and camera placement. Build the scene before you generate it. Click any preview to view it at full size.
| Feature | Preview |
| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Image to Location**<br>Place virtual actors in a consistent environment and plan multiple shots in the same room. | <a href="https://github.com/user-attachments/assets/21f103e3-cc19-4882-a630-9caa1b76ae31"><img src="https://github.com/user-attachments/assets/21f103e3-cc19-4882-a630-9caa1b76ae31" width="180" alt="Image to Location demo"></a> |
| **3D Image Compositing**<br>Layer backdrops, foregrounds, and props in 3D to build compositions with depth. | <a href="https://github.com/user-attachments/assets/f93a616f-571d-474e-bcc0-53736de7303d"><img src="https://github.com/user-attachments/assets/f93a616f-571d-474e-bcc0-53736de7303d" width="180" alt="3D Image Compositing demo"></a> |
| **2D Image Compositing**<br>Compose a scene with image layers, background removal, and drawing tools. | <a href="https://github.com/user-attachments/assets/d6f99391-e496-4c62-9e37-29734ba5f899"><img src="https://github.com/user-attachments/assets/d6f99391-e496-4c62-9e37-29734ba5f899" width="180" alt="2D Image Compositing demo"></a> |
| **Image to 3D Mesh**<br>Turn an image into a 3D object you can position, rotate, and frame precisely. | <a href="https://github.com/user-attachments/assets/600a405c-e360-48c1-9b42-6e657ae6243b"><img src="https://github.com/user-attachments/assets/600a405c-e360-48c1-9b42-6e657ae6243b" width="180" alt="Image to 3D Mesh demo"></a> |
| **Character Posing**<br>Pose characters and set your camera before generating the final shot. | <a href="https://raw.githubusercontent.com/storytold/github-media/22a27373707b13f48fc56405f1f89ca143c43d5d/character-posing.webp"><img src="https://raw.githubusercontent.com/storytold/github-media/22a27373707b13f48fc56405f1f89ca143c43d5d/character-posing.webp" width="180" alt="Character Posing demo"></a> |
| **Scene Blocking with Kitbashing**<br>Combine 3D asset kits to control camera angles, object placement, and depth. | <a href="https://github.com/user-attachments/assets/eef025ac-0346-4a46-a023-d48e23629eb5"><img src="https://github.com/user-attachments/assets/eef025ac-0346-4a46-a023-d48e23629eb5" width="180" alt="Scene Blocking with Kitbashing demo"></a> |
| **Character Identity Transfer**<br>Use a posed mannequin to guide the placement and pose of your character. | <a href="https://github.com/user-attachments/assets/629119ee-8c76-4a83-9827-8c6c995a3ec1"><img src="https://github.com/user-attachments/assets/629119ee-8c76-4a83-9827-8c6c995a3ec1" width="180" alt="Character Identity Transfer demo"></a> |
| **Background Removal**<br>Isolate subjects to use as props, layers, or backdrops in 2D and 3D scenes. | <a href="https://github.com/user-attachments/assets/90c65057-5531-404f-83af-b34e66e24ec1"><img src="https://github.com/user-attachments/assets/90c65057-5531-404f-83af-b34e66e24ec1" width="180" alt="Background Removal demo"></a> |
| **Mixed Asset Crafting**<br>Combine image cutouts, worlds, and 3D meshes in one scene. | <a href="https://raw.githubusercontent.com/storytold/github-media/main/ship-editing.gif"><img src="https://raw.githubusercontent.com/storytold/github-media/main/ship-editing.gif" width="180" alt="Mixed Asset Crafting demo"></a> |
More previews are coming for scene blocking, canvas editing, and scene relighting.
## Quick and Easy Prompting
Start with a prompt when you want to explore an idea quickly, then use the canvas and scene tools to refine it.
| Feature | Preview |
| ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Text to Image**<br>Explore ideas across a wide range of image models with a text prompt. | <a href="https://github.com/user-attachments/assets/9cc289cd-faf4-4eaf-aed2-21134cce127c"><img src="https://github.com/user-attachments/assets/9cc289cd-faf4-4eaf-aed2-21134cce127c" width="180" alt="Text to Image demo"></a> |
| **Prompted Image Editing**<br>Describe changes to an image with models such as Nano Banana Pro and GPT Image. | <a href="https://github.com/user-attachments/assets/a06fa6ad-936c-42d0-8767-48fdbb8ff141"><img src="https://github.com/user-attachments/assets/a06fa6ad-936c-42d0-8767-48fdbb8ff141" width="180" alt="Prompted Image Editing demo"></a> |
| **Canvas Image Editing**<br>Use drawing, masks, and inpainting to guide exactly where an image changes. | <a href="https://github.com/user-attachments/assets/f036e08a-f3a6-417a-98ee-ec7f04b2b5ff"><img src="https://github.com/user-attachments/assets/f036e08a-f3a6-417a-98ee-ec7f04b2b5ff" width="180" alt="Canvas Image Editing demo"></a> |
| **Image to Video**<br>Animate an image with your choice of video models and generation controls. | <a href="https://github.com/user-attachments/assets/2bc6c592-511e-4fba-b40f-03c96699b7f7"><img src="https://github.com/user-attachments/assets/2bc6c592-511e-4fba-b40f-03c96699b7f7" width="180" alt="Image to Video demo"></a> |
More previews are coming for image inpainting and image ingredients.
## Models and Providers Supported within ArtCraft
| Provider | Models and workflows |
| ------------ | ----------------------------------------------------------------------------------------------- |
| **ArtCraft** | Images, video, music and sound, 3D meshes, and worlds. Browse the complete model catalog below. |
| Grok | Grok Imagine and Grok Video. |
| Midjourney | Midjourney image generation. |
| Sora | Sora 1, Sora 2, and GPT Image 1. |
| World Labs | Marble world generation with Gaussian splats. |
<details>
<summary><strong>ArtCraft: the full 62-model catalog</strong></summary>
<!-- Catalog source: artcraft-services/crates/service/web/storyteller_web/src/configs/omni_gen/
image_models.rs, video_models/video_models.rs + by_type/, audio_models.rs,
mesh_models.rs, and splat_models.rs. Include the GenerationProvider::Artcraft
mappings, not every model defined for other providers. -->
† Currently disabled. ‡ Restricted model; not currently available in the desktop app.
#### Images (16)
| Family | Models |
| ----------- | ----------------------------------------------------------------------------------- |
| Nano Banana | Nano Banana; Nano Banana 2; Nano Banana Pro |
| GPT Image | GPT Image 1.5; GPT Image 2; GPT Image 2.5 Flare; GPT Image 2.5 Sunburst |
| FLUX | FLUX.1 [dev]; FLUX.1 [schnell]; FLUX 1.1 [pro]; FLUX 1.1 [pro] ultra |
| Seedream | Seedream 4; Seedream 4.5; Seedream 5 Lite; Seedream 5.0 Pro; Seedream 5.0 Pro Ultra |
#### Video (25)
| Family | Models |
| -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Seedance | Seedance 1.0 Lite †; Seedance 1.5 Pro; Seedance 2.0 |
| Kling | Kling 1.6 Pro; Kling 2.1 Pro †; Kling 2.1 Master †; Kling 2.5 Turbo Pro; Kling 2.6 Pro; Kling 3.0 Pro †; Kling 3.0 Standard † |
| Veo | Veo 2 †; Veo 3 †; Veo 3 Fast; Veo 3.1; Veo 3.1 Fast; Veo 3.1 Lite |
| Sora | Sora 2 †; Sora 2 Pro † |
| Vidu | Vidu Q3; Vidu Q3 Turbo |
| MiniMax | MiniMax H3; MiniMax H3 Turbo ‡; MiniMax H3 Ultra ‡ |
| Flux | Flux 3; Flux 3 Draft |
#### Music and sound (5)
| Family | Models |
| ---------- | ------------------------------------------------ |
| Suno | Suno Music; Suno Remix; Suno Sounds; Suno Sample |
| Seed Audio | Seed Audio 1.0 |
#### 3D meshes (11)
| Family | Models |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Hunyuan 3D | Hunyuan 3D 2.0; Hunyuan 3D 2.1; Hunyuan 3D 3; Hunyuan 3D 3 Sketch; Hunyuan 3D 3.1 Pro; Hunyuan 3D 3.1 Rapid; Hunyuan 3D 3.1 Part; Hunyuan 3D 3.1 Smart Topology |
| Tripo3D | Tripo3D H3.1 |
| Meshy | Meshy 6 |
| Rodin | Rodin 2.5 Fast |
#### Worlds and Gaussian splats (5)
| Family | Models |
| ---------- | --------------------------------------------------------- |
| Marble | Marble 1.0; Marble 1.0 Draft; Marble 1.1; Marble 1.1 Plus |
| TripoSplat | TripoSplat |
</details>
Additional provider integrations are planned for Kling, Google, Runway, and Luma. We're also interested in supporting existing subscriptions with aggregators such as OpenArt and Freepik.
## Downloads
- [Stable releases for Windows and macOS](https://getartcraft.com/)
- [Latest builds on GitHub](https://github.com/storytold/artcraft/releases)
- [Build from source](./_docs/dev_setup.md), including Linux
## Documentation
- [Development setup](./_docs/dev_setup.md)
- [Developer documentation](./_docs)
- [Desktop build and development scripts](./script/artcraft)
- [Roadmap](./ROADMAP.md)
- [License](./LICENSE.md)

47
ROADMAP.md Normal file
View File

@ -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™

View File

@ -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);

View File

@ -1,38 +0,0 @@
AI Chatbot Sidecar
==================
This is the first app that doesn't run as a part of our SoA API architecture. (Though it is written such that we
could port it.) This app is responsible for generating all the content that goes into our streaming AI-powered TV
programs.
### Windows Installation
The application uses sqlite, which is tricky to install on Windows.
1. Put together an sqlite build directory containing the sources and dll file. One is checked in under this monorepo
in `./windows/sqlite` at the time of this writing, though that may change in the future.
2. Add environment variables for the sqlite build directory (see https://github.com/diesel-rs/diesel/issues/487)
* PATH (append the sqlite build directory to the end)
* SQLITE3_LIB_DIR (a new environment variable; set it to the sqllite build path)
3. Build the `sqlite3.lib` file with the following:
```
lib /def:sqlite3.def /out:sqlite3.lib /MACHINE:amd64
```
This must be done in "Developer Command Prompt for VS 2022" (or similar), not `cmd` or `PowerShell`.
4. Run the diesel_cli tool build
```
cargo install diesel_cli --no-default-features --features sqlite
```
If it still complains about the `sqlite3.lib` file not being found, be sure to double check that `echo %PATH%` shows
the updated PATH from the steps above.

View File

@ -1,19 +0,0 @@
database schema
================
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.

View File

@ -1,21 +0,0 @@
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) }
```

View File

@ -1,20 +0,0 @@
# Local Kubernetes environment for standing up the APIs and their dependencies
- 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`

View File

@ -1,248 +0,0 @@
dev setup
=========
[Back to main README](../../README.md)
## Debugging and Fixing CUDA/PyTorch
See this spreadsheet for previous battles with version incompatibility:
https://docs.google.com/spreadsheets/d/1BEdLmwOzo3r83-iJn9sj6co1VT92t3lIeJ-lNH25bdQ/edit#gid=0
Ubuntu might upgrade the driver by accident, and it might need reinstallation. Who knows.
This stuff is a nightmare.
## Database Setup
### Mac Install
```
brew install mysql
# If mysql -uroot fails, reboot the machine:
sudo reboot now
```
### Linux Install
Install the following libraries, and see the notes further below about MySQL on Ubuntu 20.04.
```
jq # for combining mysql codegen outputs
libmysqlclient-dev
mysql-server
libsqlite3-dev
pkgconf # if using openssl instead of rustls
```
### Migrations
To manage the database and perform migrations, install the Rust tools diesel and sqlx.
We'll be using diesel to manage the migrations, but sqlx within the app to actually perform queries.
Diesel is an ORM, which is dumb, so we use sqlx as at-compile-time typesafe SQL.
The CLI is necessary to perform codegen:
```
sudo apt-get install libmysqlclient-dev
cargo install sqlx-cli --no-default-features --features rustls,mysql,sqlite
```
Diesel now supports configuring the migration directory via an environment variable,
so we can install the currently published version:
```
cargo install diesel_cli \
--no-default-features \
--features mysql,sqlite
```
If the modern version fails, try installing a cherry-picked version with a
TOML-based migration directory configuration:
```
cargo install diesel_cli \
--git https://github.com/diesel-rs/diesel.git \
--rev a213fe232a122f35a812b0ce0269708a1845a4c9 \
--no-default-features \
--features mysql
```
#### Sqlx Error on Linux 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 Mac), but the migrations appear to work regardless of the error message.
#### Linux database notes
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
#### MySql on Ubuntu 22.04
Should work largely out of the box. We'll need a dev account:
```
# Connect to mysql:
# sudo mysql -u root -p (password is "root")
use mysql;
CREATE USER 'storyteller'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';
```
Then verify with `./dev_mysql_connect.sh`
#### Fixing dev MySql on Ubuntu 20.04
For some reason, the MySql default install on 20.04 gave me a bunch of trouble.
In retrospect, I _think_ this is because 'root@localhost' requires sudo to access, but if this
gives any trouble in the future, here's how I got around it (two full hours of distraction!)
```
sudo apt-get install mysql-server
# But for some reason the default password doesn't work and diesel/sqlx can't connect?
# Kill everything
sudo apt purge mysql-server mysql-client mysql-common
sudo apt autoremove
sudo mv -iv /var/lib/mysql /var/tmp/mysql-backup
sudo rm -rf /var/lib/mysql*
sudo /usr/bin/mysql_secure_installation
# still no work...
sudo systemctl stop mysql.service
# ugh, it wasn't chowned or created (both states observed in different installs)…
sudo mkdir -p /var/run/mysqld
sudo chown mysql:mysql /var/run/mysqld
sudo mysqld_safe --skip-grant-tables --skip-networking &
mysql -u root
flush privileges;
USE mysql;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
sudo killall -u mysql
sudo systemctl restart mysql.service
# now it works
sudo mysql -u root -p
# but now I have to use "sudo" !?!?
use mysql;
CREATE USER 'storyteller'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';
```
### Install Diesel (migrations only)
We use Diesel to manage migrations, but we don't use it in server code.
Actual server code uses SQLx. To install the CLI tool, run the following:
```
cargo install diesel_cli
```
Mac [has issues](https://github.com/diesel-rs/diesel/issues/2605) and requires a few dependencies:
```
brew install libpq
```
### Database migrations
To reset the entire database (drop, migrate), run:
```
diesel database reset
```
To migrate at the current step and beyond:
```
diesel migration run
```
To undo migrations at the current step:
```
diesel migration redo
```
### 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):
```
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:
```
SQLX_OFFLINE=true cargo sqlx prepare -- --bin storyteller-web
```
### Setting Up a Local Nginx Proxy
Set up a local nginx to proxy to the frontend and backend so cookie issues aren't annoying
Configure Nginx per the checked in Nginx configs (and instructions) in `localdev/nginx-http-config`.
```
And in /etc/hosts,
```
127.0.0.1 jungle.horse
127.0.0.1 api.jungle.horse
```
### 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
```

View File

@ -1,77 +0,0 @@
old readme
==========
Contents of the main README.md, which will always be woefully out of date. Docs will migrate into this folder on a
per-subject basis. App-specific docs will live under each app.
Application overview
--------------------
* [FakeYou.com](https://fakeyou.com) is powered by:
* HTTP APIs:
* `storyteller-web` - HTTP + database monolith for FakeYou
* Asynchronous Jobs:
* `tts-download-job` - Async download of models
* `tts-inference-job` - Async TTS inference
* `w2l-download-job` - Async download of videos and images
* `w2l-inference-job` - Async W2L inference
* [StorytellerStream](https://storyteller.stream) is powered by:
* HTTP APIs:
* `storyteller-web` - HTTP + database monolith for FakeYou
* Websocket events:
* `websocket-gateway` - Websocket to pull down Twitch events.
* Twitch API integration:
* `twitch-pubsub-subscriber` - Dynamically subscribes to Twitch PubSub for "active" users
Schema and API notes
--------------------
* [Redis Schema Notes README](_docs/redis_schema.md)
Local development
-----------------
[Local development setup README](_docs/local_development_setup.md)
Production and Deployment
-------------------------
[Production builds and deployment README](_docs/production_builds_and_deployment.md)
Development notes
-----------------
[Development notes README](_docs/development_notes.md)
Dump a MySQL report
-------------------
```
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
```
TODO
----
* Revise this README to be more useful.
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
* Redis caching of more endpoints
* Jobs for analytics queries (#uses per model, etc)
* Jobs for ingesting Discord, Twitter, Reddit RPAN chat, Twitch chat (not PubSub), etc.

View File

@ -1,36 +0,0 @@
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"

View File

@ -1,27 +0,0 @@
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

View File

@ -1,23 +0,0 @@
FROM rust:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y \
mariadb-client \
imagemagick \
libmariadb-dev \
&& rm -rf /var/lib/apt/lists/*
RUN cargo install diesel_cli --features mysql && \
cargo install sqlx-cli --features mysql
WORKDIR /storyteller-rust
ARG GIT_SHA
RUN echo -n ${GIT_SHA} > /GIT_SHA
CMD ["/storyteller-rust/localdev/startup.sh"]

View File

@ -1,153 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0.30
env:
- name: MYSQL_USER
value: storyteller
- name: MYSQL_PASSWORD
value: password
- name: MYSQL_DATABASE
value: storyteller
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7.0.4
ports:
- containerPort: 6379
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: storyteller-web
spec:
replicas: 1
selector:
matchLabels:
app: storyteller-web
template:
metadata:
labels:
app: storyteller-web
spec:
containers:
- name: storyteller-web
image: storyteller-web:latest
imagePullPolicy: Never
env:
- name: MYSQL_URL
value: mysql://storyteller:password@mysql.default/storyteller
- name: REDIS_0_URL
value: redis://redisd@redis.default/0
- name: REDIS_1_URL
value: redis://redisd@redis.default/1
- name: ACCESS_KEY
value: garbage
- name: SECRET_KEY
value: garbage
- name: REGION_NAME
value: garbage
- name: W2L_PRIVATE_DOWNLOAD_BUCKET_NAME
value: garbage
- name: W2L_PUBLIC_DOWNLOAD_BUCKET_NAME
value: garbage
- name: AUDIO_UPLOADS_BUCKET_ROOT
value: garbage
- name: TWITCH_USERNAME
value: garbage
- name: TWITCH_STREAM_KEY
value: garbage
- name: TWITCH_APP_CLIENT_ID
value: garbage
- name: TWITCH_APP_CLIENT_SECRET
value: garbage
ports:
- containerPort: 12345
volumeMounts:
- mountPath: /storyteller-rust
name: storyteller-rust-folder
volumes:
- name: storyteller-rust-folder
hostPath:
path: /storyteller-rust
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
type: NodePort
ports:
- name: mysql
port: 3306
nodePort: 3306
selector:
app: mysql
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
type: NodePort
ports:
- name: redis
port: 6379
nodePort: 6379
selector:
app: redis
---
apiVersion: v1
kind: Service
metadata:
name: storyteller-web
spec:
type: NodePort
ports:
- name: storyteller-web
port: 12345
nodePort: 12345
selector:
app: storyteller-web

View File

@ -1,57 +0,0 @@
# Nginx configs for "jungle.horse" and "api.jungle.horse" (http)
#
# On Ubuntu Linux, this config is located at the following location:
#
# - /etc/nginx/sites-enabled/{name-of-site}
#
# On Mac, this is located at the following locations:
#
# - /opt/homebrew/etc/nginx/servers/{name-of-site} (for nginx installed with homebrew)
# - /usr/local/etc/nginx/sites-enabled/{name-of-site} (for default nginx install)
#
# API backend over plain HTTP
server {
listen 80;
server_name api.jungle.horse;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# TODO(2022-02-13): This is new and untested
# WebSocket API backend over plain HTTP
server {
listen 80;
server_name ws.jungle.horse;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:54321;
proxy_redirect off;
}
}
# Javascript frontend over plain HTTP
server {
listen 80;
server_name jungle.horse;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# TODO(2022-02-13): This is new and untested
# Javascript frontend #2 (OBS Layer) over plain HTTP
server {
listen 80;
server_name obs.jungle.horse;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7001;
proxy_redirect off;
}
}

View File

@ -1,258 +0,0 @@
# Nginx configs for "jungle.horse" and "api.jungle.horse" (http)
#
# On Ubuntu Linux, this config is located at the following location:
#
# - /etc/nginx/sites-enabled/{name-of-site}
#
# Move this file there, and then restart with,
#
# sudo service nginx restart
#
# On Mac, this is located at the following locations:
#
# - /opt/homebrew/etc/nginx/servers/{name-of-site} (for nginx installed with homebrew)
# - /usr/local/etc/nginx/sites-enabled/{name-of-site} (for default nginx install)
#
#
# On Mac, verify configs and restart with,
#
# sudo nginx -t
# sudo nginx -s reload
#
# ================
# SSL Instructions
# ================
#
# You can launch Chrome in a way it disregards self-signed certs and CORS:
#
# chromium --disable-web-security --ignore-certificate-errors --user-data-dir=~/chromeTemp
#
# To create self-signed certs on Ubuntu Linux,
#
# sudo apt-get install openssl
#
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/localhost.key -out /etc/ssl/certs/localhost.crt
#
# ^ most fields can be blank, but the "Common Name" should be `*.jungle.horse`
#
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/devfakeyou.key -out /etc/ssl/certs/devfakeyou.crt
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/devstoryteller.key -out /etc/ssl/certs/devstoryteller.crt
#
# ^ most fields can be blank, but the "Common Name" should be `*.dev.fakeyou.com` and `*.dev.storyteller.io` respectively.
#
# For more: https://www.cloudsavvyit.com/1306/how-to-create-and-use-self-signed-ssl-on-nginx/
#
# To create self-signed certs on Mac,
#
# openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout localhost.key -out localhost.crt
#
# ^ most fields can be blank, but the "Common Name" should be `*.jungle.horse`
#
# On Mac, load the self signed cert into the keychain with the following command:
#
# sudo security add-trusted-cert \
# -d -r trustRoot \
# -k /Library/Keychains/System.keychain /etc/ssl/certs/localhost.crt
#
# Self signed certs might be considered insecure by the browser. In Firefox,
# set about:config `security.tls.insecure_fallback_hosts` to include a list of
# permitted domains. You may also need to configure other settings.
#
#
# ========================================================
# FAKEYOU
# ========================================================
# FakeYou javascript frontend over plain HTTP (launched by Yarn)
server {
listen 80;
server_name dev.fakeyou.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# FakeYou API backend over plain HTTP (Rust app)
server {
listen 80;
server_name api.dev.fakeyou.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# FakeYou javascript frontend over SSL (launched by Yarn)
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/devfakeyou.crt;
ssl_certificate_key /etc/ssl/private/devfakeyou.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
server_name dev.fakeyou.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# FakeYou API backend over SSL (Rust app)
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/devfakeyou.crt;
ssl_certificate_key /etc/ssl/private/devfakeyou.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
server_name api.dev.fakeyou.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# ========================================================
# STORYTELLER
# ========================================================
# Storyteller javascript frontend over plain HTTP (launched by Yarn)
server {
listen 80;
server_name dev.storyteller.io;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# Storyteller API backend over plain HTTP
server {
listen 80;
server_name api.dev.storyteller.io;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# Storyteller javascript frontend over SSL (launched by Yarn)
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/devstoryteller.crt;
ssl_certificate_key /etc/ssl/private/devstoryteller.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
server_name dev.storyteller.io;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# Storyteller API backend over SSL
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/devstoryteller.crt;
ssl_certificate_key /etc/ssl/private/devstoryteller.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
server_name api.dev.storyteller.io;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# ========================================================
# OLD PRODUCTS THAT NEED UPDATING
# ========================================================
# ## TODO(2022-02-13): This is new and untested
# # WebSocket API backend over plain HTTP
# server {
# listen 80;
# server_name ws.jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:54321;
# proxy_redirect off;
# }
# }
#
# # Websocket API backend over SSL
# server {
# listen 443 ssl;
# listen [::]:443 ssl;
# ssl_certificate /etc/ssl/certs/localhost.crt;
# ssl_certificate_key /etc/ssl/private/localhost.key;
# ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
# server_name ws.jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:54321;
# proxy_redirect off;
# }
# }
#
# # Javascript frontend over plain HTTP
# server {
# listen 80;
# server_name jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:7000;
# proxy_redirect off;
# }
# }
#
# # TODO(2022-02-13): This is new and untested
# # Javascript frontend (OBS Layer) over plain HTTP
# server {
# listen 80;
# server_name obs.jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:7007;
# proxy_redirect off;
# }
# }
#
# # Javascript frontend over SSL
# server {
# listen 443 ssl;
# listen [::]:443 ssl;
# ssl_certificate /etc/ssl/certs/localhost.crt;
# ssl_certificate_key /etc/ssl/private/localhost.key;
# ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
# server_name jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:7000;
# proxy_redirect off;
# }
# }
#
# # Javascript frontend over SSL (OBS Layer)
# server {
# listen 443 ssl;
# listen [::]:443 ssl;
# ssl_certificate /etc/ssl/certs/localhost.crt;
# ssl_certificate_key /etc/ssl/private/localhost.key;
# ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
# server_name obs.jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:7007;
# proxy_redirect off;
# }
# }
#

View File

@ -1,6 +0,0 @@
#!/bin/bash
set +e +o pipefail
diesel migration run --database-url="${MYSQL_URL}"
set -e -o pipefail
./target/x86_64-unknown-linux-musl/release/storyteller-web

View File

@ -1,36 +0,0 @@
#!/bin/bash
echo 'Installing APT packages...'
# OpenSSL requirements: libssl-dev, pkg-config
# Database requirements: mysql-server, libmysqlclient-dev
# Other tools: jq (db codegen)
sudo apt-get install -y \
jq \
libmysqlclient-dev \
libssl-dev \
mysql-server \
pkg-config \
--no-install-recommends
echo 'Installing Cargo packages...'
# Install Diesel CLI for migrations
cargo install diesel_cli \
--no-default-features \
--features mysql
# Install SQLx for codegen
cargo install sqlx-cli \
--no-default-features \
--features rustls,mysql
echo 'Installing Database user...'
# Create DB account (using default system mysql password)
sudo mysql \
-u "root" \
-p"password" \
-e "use mysql; CREATE DATABASE IF NOT EXISTS storyteller; CREATE USER IF NOT EXISTS 'storyteller'@'localhost' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';"

View File

@ -1,61 +0,0 @@
#!/usr/bin/env python3
import json
import re
import requests
import uuid
login_url = 'http://localhost:12345/login'
create_api_token_url = 'http://localhost:12345/api_tokens/create'
edit_api_token_url = 'http://localhost:12345/api_tokens/edit'
delete_api_token_url = 'http://localhost:12345/api_tokens/{}/delete'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
}
r = requests.post(create_api_token_url, cookies=cookies, json=payload)
print("===== Created API Token =====")
print('Status: {}'.format(r.status_code))
print(r.content)
api_token = json.loads(r.content)
payload = {
'api_token': api_token['api_token'],
'maybe_short_description': 'this describes my token',
}
r = requests.post(edit_api_token_url, cookies=cookies, json=payload)
print("===== Edited API Token =====")
print('Status: {}'.format(r.status_code))
print(r.content)
url = delete_api_token_url.format(api_token['api_token'])
r = requests.post(url, cookies=cookies)
print("===== Deleted API Token =====")
print('Status: {}'.format(r.status_code))
print(r.content)

View File

@ -1,150 +0,0 @@
#!/usr/bin/env python3
import json
import re
import requests
import uuid
login_url = 'http://localhost:12345/login'
category_create_url = 'http://localhost:12345/category/create'
category_delete_url_format = 'http://localhost:12345/category/{}/delete'
category_edit_url_format = 'http://localhost:12345/category/{}/edit'
tts_model_list_url = 'http://localhost:12345/tts/list'
tts_category_assignment_url = 'http://localhost:12345/category/assign/tts'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== [1] Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== [2] Create Category =====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'name': 'First Category',
'model_type': 'tts', # tts or w2l
'can_directly_have_models': False,
'can_have_subcategories': True,
'is_mod_approved': True,
}
r = requests.post(category_create_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
d = json.loads(r.content)
category_token = d['token']
print(f'created category token: #{category_token}')
print("===== [3] (Soft) Delete Category =====")
category_delete_url = category_delete_url_format.format(category_token)
cookies = { 'session': session_cookie }
payload = {
'set_delete': True,
}
r = requests.post(category_delete_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
print("===== [4] Undelete Category =====")
cookies = { 'session': session_cookie }
payload = {
'set_delete': False,
}
r = requests.post(category_delete_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
print("===== [5] Edit Category =====")
category_edit_url = category_edit_url_format.format(category_token)
cookies = { 'session': session_cookie }
payload = {
'name': 'Edited Name',
'dropdown_name': 'Edited Dropdown Name',
'can_directly_have_models': True,
'can_have_subcategories': True,
'can_only_mods_apply': False,
'is_mod_approved': True,
'maybe_mod_comments': 'mod comment',
}
r = requests.post(category_edit_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
print("===== [6] List TTS Models =====")
r = requests.get(tts_model_list_url, cookies=cookies)
print('Status: {}'.format(r.status_code))
d = json.loads(r.content)
tts_model_token = d['models'][0]['model_token']
print('TTS model token: {}'.format(tts_model_token))
print("===== [7] Create Category Assignment to TTS Model =====")
payload = {
'category_token': category_token,
'tts_model_token': tts_model_token,
'assign': True,
}
r = requests.post(tts_category_assignment_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
print("===== [8] Delete Category Assignment to TTS Model =====")
payload = {
'category_token': category_token,
'tts_model_token': tts_model_token,
'assign': False,
}
r = requests.post(tts_category_assignment_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)

View File

@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username": "echelon", "password": "testing", "password_confirmation": "testing", "email_address": "echelon@gmail.com"}' \
http://localhost:12345/create_account

View File

@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username": "ernest", "password": "knowwhatimean", "password_confirmation": "knowwhatimean", "email_address": "vern@knowwhatimean.com"}' \
http://localhost:12345/create_account

View File

@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username": "vocodes", "password": "testing", "password_confirmation": "testing", "email_address": "vocodes@gmail.com"}' \
http://localhost:12345/create_account

View File

@ -1,43 +0,0 @@
#!/usr/bin/env python3
import requests
import re
login_url = 'http://localhost:12345/login'
inference_url = 'http://localhost:12345/tts/inference'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Infer TTS =====")
cookies = { 'session': session_cookie }
payload = {
'tts_model_token': 'TTS_TOKEN',
'inference_text': 'this is the text to generate',
}
r = requests.post(inference_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)

View File

@ -1,43 +0,0 @@
#!/usr/bin/env python3
import requests
import re
login_url = 'http://localhost:12345/login'
inference_url = 'http://localhost:12345/w2l/inference'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Infer W2L =====")
cookies = { 'session': session_cookie }
payload = {
'w2l_template_token': 'W2L_TOKEN',
'tts_inference_result_token': 'TTS_TOKEN',
}
r = requests.post(inference_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)

View File

@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username_or_email": "echelon", "password": "BAD_PASSWORD"}' \
http://localhost:12345/login

View File

@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username_or_email": "nobody", "password": "testing"}' \
http://localhost:12345/login

View File

@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username_or_email": "echelon@gmail.com", "password": "testing"}' \
http://localhost:12345/login

View File

@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username_or_email": "echelon", "password": "testing"}' \
http://localhost:12345/login

View File

@ -1,38 +0,0 @@
#!/usr/bin/env python3
import requests
import re
login_url = 'http://localhost:12345/login'
logout_url = 'http://localhost:12345/logout'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
cookies = { 'session': session_cookie }
r = requests.post(logout_url, cookies=cookies)
print("===== Logout =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))

View File

@ -1,123 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
import socket
# There are some things here that will cause my laptop to freeze!
hostname = socket.gethostname()
def print_response(response):
print('\tStatus: {}'.format(response.status_code))
for k, v in response.headers.items():
print('\t header - {}: {}'.format(k, v))
print('\tbody: {}', response.json())
# ==================== CREATE ACCOUNTS ====================
def create_user_account(username, password, email):
print('Create account: {}'.format(username))
signup_url = 'http://localhost:12345/create_account'
payload = {
'username': username,
'password': password,
'password_confirmation': password,
'email_address': email,
}
r = requests.post(signup_url, json=payload)
print_response(r)
create_user_account('echelon', 'testing', 'echelon@gmail.com')
create_user_account('brandon', 'testing', 'brandon@storyteller.company')
create_user_account('nanashi', 'nanashi', 'somebody@somewhere.com')
create_user_account('other', 'testing', 'ryu@storyteller.company')
create_user_account('raptor9', 'testing', 'raptor9@storyteller.company')
create_user_account('ryu', 'testing', 'ryu@storyteller.company')
# ==================== LOGIN ====================
def login(username, password):
print('Login: {}'.format(username))
login_url = 'http://localhost:12345/login'
payload = {
'username_or_email': username,
'password': password,
}
r = requests.post(login_url, json=payload)
print_response(r)
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
return session_cookie
session_echelon = login('echelon', 'testing')
session_nanashi = login('nanashi', 'nanashi')
session_brandon = login('brandon', 'testing')
session_other = login('other', 'testing')
# ==================== UPLOAD W2L TEMPLATES ====================
def upload_w2l_template(content_url, title, session_cookie):
print('Upload W2L template')
upload_url = 'http://localhost:12345/w2l/upload'
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': title,
'download_url': content_url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print_response(r)
upload_w2l_template('https://drive.google.com/file/d/1yoBSb6nwFUZVI4CD-nM2BSofGdBjC8Nn/view?usp=sharing', 'Mr. Musk', session_nanashi)
upload_w2l_template('https://drive.google.com/file/d/1ysBAdRjlSmfWQPFjGlMJpv_zcGmVw7RC/view?usp=sharing', 'short bill clinton clip', session_brandon)
upload_w2l_template('https://i.imgur.com/NneE7Eq.jpg', 'itsa me mario!', session_echelon)
upload_w2l_template('https://i.imgur.com/W1EYiPU.jpg', 'Creepy Sonic does a gasp', session_echelon)
upload_w2l_template('https://i.imgur.com/8U3IdUa.png', 'Creepy Sonic actually talks', session_echelon)
upload_w2l_template('https://i.imgur.com/lKaQ4Er.jpg', 'hasta la vista', session_brandon)
upload_w2l_template('https://i.imgur.com/uoGuTJo.jpeg', 'what do you mean vern?', session_brandon)
upload_w2l_template('https://i.imgur.com/v8k9yau.jpeg', 'Miyamoto-san', session_brandon)
if hostname == 'halide':
upload_w2l_template('https://www.youtube.com/watch?v=a7mS9ZdU6k4', 'Steve Jobs talks', session_echelon)
upload_w2l_template('https://www.youtube.com/watch?v=6fg8SxgEUYI', 'Paul Graham lectures', session_echelon)
# ==================== UPLOAD TTS MODELS ====================
def upload_tts_model(content_url, title, session_cookie):
print('Upload TTS model')
upload_url = 'http://localhost:12345/tts/upload'
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': title,
'download_url': content_url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print_response(r)
# From others' Google Drives
#upload_tts_model('https://drive.google.com/file/d/1grwDGbAsPwCMVfye071TZ6PLUub1KGyR/view?usp=sharing', 'Jorgen', session_nanashi)
#upload_tts_model('https://drive.google.com/file/d/1w3oBk5vzyHurqYwP95UEQv-_NeMNu6pt/view?usp=sharing', 'Noire', session_echelon)
# Copied into Storyteller Google Drive
upload_tts_model('https://drive.google.com/file/d/1d8KPwYqbRsGiYx1c4TCW4xCtcRLdIU1s/view?usp=sharing', 'Zephyr', session_echelon)
upload_tts_model('https://drive.google.com/file/d/1yhoamPz1NOMy403gu2v8sIx5hPj3LQWs/view?usp=sharing', 'Wakko Warner', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1I6KGQZFMMB2ZvCzWEqy9xTgGl0L5cmfP/view?usp=sharing', 'Turret-192', session_echelon)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1T_hvgGcNOfofhNmDWUqpVtYDdSXQrWdE/view?usp=sharing', 'technoblade-324', session_brandon)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1NKyRiex3K1J0QyIv2np17D_H0rE98HyK/view?usp=sharing', 'Noire', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1VDHDOpsUR8C-MD07vEsnUf1Lpcf8hXg4/view?usp=sharing', 'MatPat80', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1atFn4vm7mjkufXNK9TZS6J8WzISrAn3-/view?usp=sharing', 'King Julien-70', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1-rlJpi2jAsagn-UCStcCc_ITNycXgn2F/view?usp=sharing', 'Jorgen', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1togTNOEKnaarqWWrbZLSwIbZIZ8IT_qw/view?usp=sharing', 'edsheeransinging', session_nanashi)
# TEMP COMMENT DUE TO RATE LIMIT
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1OAiu0cZDyUJMemyLc0Btqw7zEQgiN81D/view?usp=sharing', 'Wakko Warner v2 ST21', session_nanashi)
upload_tts_model('https://drive.google.com/file/d/1vRVI2gQ5tAs8SLQPrR_M1J77L05gGBOp/view?usp=sharing', 'Testing voice', session_nanashi)
#upload_tts_model('', '', session_other)

View File

@ -1,41 +0,0 @@
#!/usr/bin/env python3
import requests
import re
login_url = 'http://localhost:12345/login'
session_info_url = 'http://localhost:12345/session'
r = requests.get(session_info_url)
print("===== Session Info (logged out) =====")
print(r.content)
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
cookies = { 'session': session_cookie }
r = requests.get(session_info_url, cookies=cookies)
print("===== Session Info (logged in) =====")
print(r.content)

View File

@ -1,306 +0,0 @@
#!/usr/bin/env python3
import json
import re
import requests
import uuid
import pprint
login_url = 'http://localhost:12345/login'
create_url = 'http://localhost:12345/twitch/event_rule/create'
list_url = 'http://localhost:12345/twitch/event_rule/list'
update_url = 'http://localhost:12345/twitch/event_rule/{}/update'
delete_url = 'http://localhost:12345/twitch/event_rule/{}/delete'
def delete_rule(token, cookies):
url = delete_url.format(token)
r = requests.delete(url, cookies=cookies)
print("===== Deleted Event Rule Output =====")
print('Status: {}'.format(r.status_code))
print(r.content)
def create_rule(payload, cookies):
r = requests.post(create_url, cookies=cookies, json=payload)
print("===== Created Event Rule Output =====")
print('Status: {}'.format(r.status_code))
print(r.content)
# ========== Setup / Login ==========
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
cookies = { 'session': session_cookie }
# ========== Delete All Existing ==========
r = requests.get(list_url, cookies=cookies)
print("===== List Event Rule Output =====")
print('Status: {}'.format(r.status_code))
response = json.loads(r.content)
tokens = [rule['token'] for rule in response['twitch_event_rules']]
for token in tokens:
delete_rule(token, cookies)
# ========== Create (1) ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_spend_threshold': {
'minimum_bits_spent': 1,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
r = requests.post(create_url, cookies=cookies, json=payload)
print("===== Created Event Rule Output =====")
print('Status: {}'.format(r.status_code))
print(r.content)
response = json.loads(r.content)
token = response['twitch_event_rule_token']
# ========== Update (1) ==========
payload = {
'event_match_predicate': {
'bits_spend_threshold': {
'minimum_bits_spent': 12345,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 123,
'rule_is_disabled': True,
}
url = update_url.format(token)
r = requests.post(url, cookies=cookies, json=payload)
print("===== Edited Event Rule Output =====")
print('Status: {}'.format(r.status_code))
print(r.content)
# ========== Create: Bits Spend Threshold ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_spend_threshold': {
'minimum_bits_spent': 1000,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': True,
}
create_rule(payload, cookies)
# ========== Create: Cheermote Exact Match ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_name_exact_match': {
'cheermote_name': 'Cheer1',
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_name_exact_match': {
'cheermote_name': 'Corgo5000',
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_name_exact_match': {
'cheermote_name': 'ZomboCom5000',
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
# ========== Create: Cheermote Prefix And Spend Threshold ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_prefix_spend_threshold': {
'cheermote_prefix': 'DoodleCheer',
'minimum_bits_spent': 1000,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_prefix_spend_threshold': {
'cheermote_prefix': 'Corgo',
'minimum_bits_spent': 12345,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_prefix_spend_threshold': {
'cheermote_prefix': 'ZomboCom',
'minimum_bits_spent': 1000,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
# ========== Create: Channel Points ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'channel_points',
'event_match_predicate': {
'channel_points_reward_name_exact_match': {
'reward_name': 'My Reward',
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'channel_points',
'event_match_predicate': {
'channel_points_reward_name_exact_match': {
'reward_name': 'My Reward',
},
},
'event_response': {
'tts_random_voice': {
'tts_model_tokens': [
'TM:40m3aqtt41y0', # "Wakko" voice (dev)
'TM:4c1hycjj3a3t', # "Zephyr" voice (dev)
],
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
# ========== List ==========
r = requests.get(list_url, cookies=cookies)
print("===== List Event Rule Output =====")
print('Status: {}'.format(r.status_code))
response = json.loads(r.content)
pprint.pprint(response)

View File

@ -1,45 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/tts/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload TTS=====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new text to speech model',
'download_url': 'https://website.com',
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)

View File

@ -1,45 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/tts/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload TTS=====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new text to speech model',
'download_url': 'https://drive.google.com/file/d/15-tkgblTZpa0ifmvvyr_kgWy2cguTpAe/view?usp=sharing',
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)

View File

@ -1,45 +0,0 @@
#!/usr/bin/env python3
import re
import requests
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/w2l/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload W2L =====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new text to W2L template',
'download_url': 'https://video.com',
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)

View File

@ -1,52 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/w2l/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload W2L =====")
cookies = { 'session': session_cookie }
# mario.zip
#download_url = 'https://drive.google.com/file/d/15-tkgblTZpa0ifmvvyr_kgWy2cguTpAe/view?usp=sharing'
# elon_integration_test.jpg
download_url = 'https://drive.google.com/file/d/1yoBSb6nwFUZVI4CD-nM2BSofGdBjC8Nn/view?usp=sharing'
#download_url = 'https://i.imgur.com/...'
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new image template',
'download_url': download_url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)

View File

@ -1,61 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/w2l/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
def upload(url):
print("===== Upload W2L =====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new image template',
'download_url': url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
upload('https://i.imgur.com/W1EYiPU.jpg')
upload('https://i.imgur.com/NneE7Eq.jpg')
upload('https://i.imgur.com/v8k9yau.jpeg')
upload('https://drive.google.com/file/d/1yoBSb6nwFUZVI4CD-nM2BSofGdBjC8Nn/view?usp=sharing')
upload('https://drive.google.com/file/d/1ysBAdRjlSmfWQPFjGlMJpv_zcGmVw7RC/view?usp=sharing')
upload('https://i.imgur.com/lKaQ4Er.jpg')
upload('https://i.imgur.com/uoGuTJo.jpeg')

View File

@ -1,55 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/w2l/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload W2L =====")
cookies = { 'session': session_cookie }
# mario.zip
#download_url = 'https://drive.google.com/file/d/15-tkgblTZpa0ifmvvyr_kgWy2cguTpAe/view?usp=sharing'
# elon_integration_test.jpg
#download_url = 'https://drive.google.com/file/d/1yoBSb6nwFUZVI4CD-nM2BSofGdBjC8Nn/view?usp=sharing'
# bill-integration-test.mp4
# NB: This freezes up my poor laptop:
###download_url = 'https://drive.google.com/file/d/14yO_60lc1FJCYhDGaeiQabgGvYLa86WI/THIS_IS_TOO_BIGview?usp=sharing'
# bill-integration-small.mp4 (small enough to run on my laptop)
download_url = 'https://drive.google.com/file/d/1ysBAdRjlSmfWQPFjGlMJpv_zcGmVw7RC/view?usp=sharing'
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new image template',
'download_url': download_url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from badges\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from ip_address_bans\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from firehose_entries\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from user_sessions\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from tts_inference_jobs\G"

View File

@ -1,45 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from tts_results\G"
#echo '==============================================='
#
#read -r -d '' query << EOF
#
# SELECT
# w2l_results.token as w2l_result_token,
# w2l_results.maybe_tts_inference_result_token,
# w2l_templates.token as maybe_w2l_template_token,
# w2l_templates.template_type,
# users.token as maybe_creator_user_token,
# users.username as maybe_creator_username,
# users.display_name as maybe_creator_display_name,
# w2l_results.file_size_bytes,
# w2l_results.frame_width,
# w2l_results.frame_height,
# w2l_results.duration_millis,
# w2l_results.created_at,
# w2l_results.updated_at
# FROM
# w2l_results
# LEFT OUTER JOIN w2l_templates ON w2l_results.maybe_w2l_template_token = w2l_templates.token
# LEFT OUTER JOIN users ON w2l_results.maybe_creator_user_token = users.token
# WHERE
# w2l_results.deleted_at IS NULL
# AND users.username = 'echelon'\G
#EOF
#
#echo $query
#
#mysql -u root \
# -proot \
# -h localhost \
# -D storyteller \
# -e "${query}"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from tts_models\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from tts_model_upload_jobs\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from users\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from w2l_inference_jobs\G"

View File

@ -1,45 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from w2l_results\G"
echo '==============================================='
read -r -d '' query << EOF
SELECT
w2l_results.token as w2l_result_token,
w2l_results.maybe_tts_inference_result_token,
w2l_templates.token as maybe_w2l_template_token,
w2l_templates.template_type,
users.token as maybe_creator_user_token,
users.username as maybe_creator_username,
users.display_name as maybe_creator_display_name,
w2l_results.file_size_bytes,
w2l_results.frame_width,
w2l_results.frame_height,
w2l_results.duration_millis,
w2l_results.created_at,
w2l_results.updated_at
FROM
w2l_results
LEFT OUTER JOIN w2l_templates ON w2l_results.maybe_w2l_template_token = w2l_templates.token
LEFT OUTER JOIN users ON w2l_results.maybe_creator_user_token = users.token
WHERE
w2l_results.deleted_at IS NULL
AND users.username = 'echelon'\G
EOF
echo $query
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "${query}"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from w2l_templates\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from w2l_template_upload_jobs\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from user_roles\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "update users set user_role_slug='admin' where username='echelon' limit 1\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "update users set user_role_slug='user' where username='echelon' limit 1\G"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from tts_result_synthetic_ids\G"

View File

@ -1,6 +0,0 @@
# Secrets
db_secrets.toml
# Python Virtual Env
python/

View File

@ -1,88 +0,0 @@
#!/usr/bin/env python
# FIXME: Needs packages 'toml' and 'mysql-connector-python'. Put in requirements.txt.
import time
import toml
import mysql.connector
DB_SECRETS_FILE = 'db_secrets.toml'
# TODO: Share db-related setup when more scripts are added.
def read_database_secrets(secrets_file = DB_SECRETS_FILE):
secrets = None
with open(secrets_file, "r") as f:
secrets = toml.load(f)
required_keys = ['database', 'hostname', 'port', 'password', 'username']
for key in required_keys:
if key not in secrets:
raise Exception(f"Required key '#{key}' not present")
return secrets
def connect_to_mysql():
secrets = read_database_secrets()
connection = mysql.connector.connect(
host = secrets['hostname'],
port = secrets['port'],
user = secrets['username'],
password = secrets['password'],
database = secrets['database'],
)
return connection
connection = connect_to_mysql()
cursor = connection.cursor()
SAFE_ID = 76000000 # Reasonably completed on 2022-06-24
LIMIT = 100000
#def get_record_count():
# query = f"SELECT COUNT(*) FROM tts_inference_jobs"
# cursor.execute(query)
# result = cursor.fetchall()
# print(result)
# return result[0]
#
##total_count = get_record_count()
#total_count = 75082057
total_count = 21925602
def get_first_record(cursor):
query = f"select * from tts_inference_jobs order by id limit 1"
cursor.execute(query)
result = cursor.fetchall()
print(result)
return result
total_deleted = 0
keep_deleting = True
while keep_deleting:
query = f"DELETE FROM tts_inference_jobs WHERE id < {SAFE_ID} ORDER BY ID ASC LIMIT {LIMIT}"
query = f"DELETE FROM tts_inference_jobs WHERE status = 'complete_success' ORDER BY ID ASC LIMIT {LIMIT}"
#print(query)
cursor.execute(query)
connection.commit() # NB: Python does not autocommit changes!
#result = cursor.fetchall()
count = cursor.rowcount
total_deleted += count
percent_deleted = (total_deleted / total_count) * 100.0
print(f'Deleted {count}; total deleted = {total_deleted} ({percent_deleted:.4f})')
if count < LIMIT - 1:
keep_deleting = False
time.sleep(0.5)
#record = get_first_record(cursor)
#time.sleep(2)

View File

@ -1,100 +0,0 @@
#!/usr/bin/env python
# FIXME: Needs packages 'toml' and 'mysql-connector-python'. Put in requirements.txt.
import time
import toml
import mysql.connector
DB_SECRETS_FILE = 'db_secrets.toml'
# TODO: Share db-related setup when more scripts are added.
def read_database_secrets(secrets_file = DB_SECRETS_FILE):
secrets = None
with open(secrets_file, "r") as f:
secrets = toml.load(f)
required_keys = ['database', 'hostname', 'port', 'password', 'username']
for key in required_keys:
if key not in secrets:
raise Exception(f"Required key '#{key}' not present")
return secrets
def connect_to_mysql():
secrets = read_database_secrets()
connection = mysql.connector.connect(
host = secrets['hostname'],
port = secrets['port'],
user = secrets['username'],
password = secrets['password'],
database = secrets['database'],
)
return connection
connection = connect_to_mysql()
cursor = connection.cursor()
SAFE_ID = 76000000 # Reasonably completed on 2022-06-24
LIMIT = 100000
#def get_record_count():
# query = f"SELECT COUNT(*) FROM tts_inference_jobs"
# cursor.execute(query)
# result = cursor.fetchall()
# print(result)
# return result[0]
#
##total_count = get_record_count()
#total_count = 75082057
total_count = 21925602
def get_user_tokens(cursor):
query = f"""
SELECT
r.maybe_creator_user_token, r.creator_ip_address
FROM
tts_results as r
LEFT OUTER JOIN
users as u
ON
r.maybe_creator_user_token = u.token
WHERE
r.raw_inference_text LIKE '%spidermant%'
AND r.created_at > ( CURDATE() - INTERVAL 1 HOUR )
"""
cursor.execute(query)
results = cursor.fetchall()
user_tokens = [row[0] for row in results]
user_tokens = [token for token in user_tokens if token is not None]
user_tokens = set(user_tokens)
ip_addresses = [row[1] for row in results]
ip_addresses = [ip for ip in ip_addresses if ip is not None]
ip_addresses = set(ip_addresses)
return (user_tokens, ip_addresses)
(user_tokens, ip_addresses) = get_user_tokens(cursor)
print(user_tokens)
print(len(user_tokens))
print(len(ip_addresses))
with open('101_user_tokens.txt', 'w') as f:
for user_token in user_tokens:
f.write(user_token)
f.write("\n")
with open('101_ip_addresses.txt', 'w') as f:
for ip in ip_addresses:
f.write(ip)
f.write("\n")

View File

@ -1,3 +0,0 @@
mysql-connector-python==8.0.32
protobuf==3.20.3
toml==0.10.2

625
_docs/artcraft_omni_api.md Normal file
View File

@ -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.

40
_docs/dev_setup.md Normal file
View File

@ -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.

View File

@ -1,48 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- 1) Find uploads that failed (recently)
select
id,
token,
download_type,
title,
download_url,
status,
attempt_count
from generic_download_jobs
where status IN ('started', 'attempt_failed', 'complete_failure', 'dead')
and download_type NOT IN ('vits', 'hifigan')
and created_at > NOW() - INTERVAL 15 DAY;
-- 2) Then restart them
-- NB: Double inner query is to get away from query optimizer:
-- "You can't specify target table 'generic_download_jobs' for update in FROM clause"
update generic_download_jobs
set
status='pending',
attempt_count=0
where token IN (
select token from (
select token
from generic_download_jobs
where status IN ('started', 'attempt_failed', 'complete_failure', 'dead')
and download_type NOT IN ('vits', 'hifigan')
and created_at > NOW() - INTERVAL 15 DAY
) as x
);
-- Kill bad pending downloads
-- eg. awful voices, like Hitler
update generic_download_jobs
set
status='dead',
download_url = CONCAT('NOPE', download_url)
where token = 'jdown_ssape40dxbfgvhtsmedt34418x'
limit 1;

View File

@ -1,9 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Kill routed jobs
update generic_inference_jobs set status = 'dead' where maybe_routing_tag IS NOT NULL limit 100;
-- Get pending jobs
select count(*) from generic_inference_jobs where status = 'pending';

View File

@ -1,24 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Kill pending jobs
update tts_inference_jobs set status = 'dead' where status = 'pending';
-- Kill all low priority pending, waiting, and in-progress jobs
update tts_inference_jobs
set status = 'dead'
where status IN ('pending', 'started', 'attempt_failed')
and priority_level IN (0, 1);
-- Kill all pending jobs
update tts_inference_jobs
set status = 'dead'
where status = 'pending';
-- Kill all waiting and in-progress jobs
update tts_inference_jobs
set status = 'dead'
where status IN ('pending', 'started', 'attempt_failed');

View File

@ -1,11 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Delete TTS results for a single model
-- Do this in short batches so a lock isn't held for prohibitively long.
update tts_results
set mod_deleted_at = NOW()
where model_token = 'TM:ztt5s1be5tq6'
and mod_deleted_at IS NULL
limit 5000;

View File

@ -1,35 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Upgrade top TTS model uploaders to a loyalty plan
update users
set maybe_loyalty_program_key = 'fakeyou_contributor'
where token IN (
select creator_user_token from
(
select count(*) as upload_count,
creator_user_token
from tts_models
group by creator_user_token
having upload_count >= 10
order by upload_count desc
) as t
);
-- Upgrade top VC model uploaders to a loyalty plan
update users
set maybe_loyalty_program_key = 'fakeyou_contributor'
where token IN (
select creator_user_token from
(
select count(*) as upload_count,
creator_user_token
from voice_conversion_models
group by creator_user_token
having upload_count >= 10
order by upload_count desc
) as t
);

View File

@ -1,105 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Remove temporary test models
update voice_conversion_models
set
mod_deleted_at = NOW(),
maybe_mod_comments = 'temporary test model'
where token IN (
'vcm_jt9mdz9xjjcb',
'vcm_4xzkzdhs4kk5',
'vcm_502wkt2pca3y'
);
-- Remove broken models
update voice_conversion_models
set
mod_deleted_at = NOW(),
maybe_mod_comments = 'broken model'
where token IN (
'vcm_982y9g6v3jrg',
'vcm_16rpyehf7f0a',
'vcm_8vjtz3dbnw17',
'vcm_tc9mwr60cjth',
'vcm_bshsyp01w9vg',
'vcm_31whj8apmqj7',
'vcm_84qa0an44p8p',
'vcm_9xx4v5qz5fbg',
'vcm_7zp2b2hs2at2',
'vcm_zhvk0mnnb7zp',
'vcm_1vc4scq9qp6m',
'vcm_sr1b54mwy0ym',
'vcm_qx96y8aasydk'
);
-- Remove duplicate models
update voice_conversion_models
set
mod_deleted_at = NOW(),
maybe_mod_comments = 'duplicate model'
where token IN (
'vcm_6w21em1bs7q3',
'vcm_sbhvyj7926w4',
'vcm_anv469g89adz',
'vcm_xsx5yx585c7x',
'vcm_dpahzjjpkf18',
'vcm_y09dqe6zgwyk',
'vcm_xrp96dwgppv2',
'vcm_84awjbjxts6n',
'vcm_b2x2sn8kf28j',
'vcm_41nw6xgr5x3v',
'vcm_a6x83tk0ttf2',
'vcm_dgjgqxq5w97j',
'vcm_59k5j452x7xy',
'vcm_09p6vqnk7n05',
'vcm_attanwh383hg',
'vcm_t8d4f6w9pa8t',
'vcm_s8s4q4c65xsy',
'vcm_6e2av8pmkyz5',
'vcm_82x5f330z4wf',
'vcm_z6bax4kvjdx6',
'vcm_v8hf35cyq3ft',
'vcm_1h9b4v6nxmf5',
'vcm_781bzn81qf6k',
'vcm_w0xjjsexktby',
'vcm_awsq3q4c711w',
'vcm_kttxt7wgb6s6',
'vcm_ets26z5fpt8d',
'vcm_wjrw59wmwcrr',
'vcm_bebgka17n0sr',
'vcm_6mj0x3qd7yfd',
'vcm_b71ke4fm61nx',
'vcm_srz3gas5ra4d',
'vcm_k64jj2t2j05b',
'vcm_140rd035642m',
'vcm_5y93wgr90s91',
'vcm_jwp6p08byjn9',
'vcm_ets26z5fpt8d',
'vcm_7yfwzsyvrpq4',
'vcm_f1vq0hp0tes2',
'vcm_9f05kg36q4fg',
'vcm_1xs9c2sgykgy',
'vcm_hnd9z2fxq661',
'vcm_jxd74xhcszry',
'vcm_tvk0g6dwg3yv',
'vcm_vrsdv4ahterx',
'vcm_e2debszbm8vt'
);
-- Remove duplicate models
update voice_conversion_models
set
mod_deleted_at = NOW(),
maybe_mod_comments = 'takedown request'
where token IN (
'vcm_2af74np3jh6q',
'vcm_8kk7355dc184'
);

View File

@ -1,25 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Kill pending jobs
update w2l_inference_jobs set status='dead' where status IN ('pending', 'started', 'attempt_failed');
-- Unstick old jobs that might be stuck as "started"
update w2l_template_upload_jobs
set status='dead'
where status='started'
and created_at < ( CURDATE() - INTERVAL 1 DAY );
-- Kill pending jobs with tiktok URLs:
-- These need to be blocked in the server and job directly.
update w2l_template_upload_jobs
set status='dead'
where status='pending'
and download_url LIKE '%tiktok%';
-- Kill all pending jobs
update w2l_template_upload_jobs
set status='dead'
where status IN ('pending', 'started', 'attempt_failed');

View File

@ -1,36 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: These are for use in Stripe Sigma, not MySQL
-- NB: You must use single quotes instead of double quotes when comparing string values.
-- Histogram of customers by country
select address_country, count(*)
from customers
group by address_country
select customer_id, subscriptions.status
from subscriptions
select distinct customer_id
from subscriptions
where subscriptions.status = 'active'
or subscriptions.status = 'past_due'
select address_country, count(*) as country_count
from customers
where id IN (
select distinct customer_id
from subscriptions
where subscriptions.status = 'active'
or subscriptions.status = 'past_due'
)
group by address_country
-- Histogram
select address_country, count(*)
from customers
group by address_country

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