Compare commits
No commits in common. "bt-new-models" and "bt-sadtalker" have entirely different histories.
bt-new-mod
...
bt-sadtalk
|
|
@ -1,56 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
# 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
|
||||
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
hakari-package = "workspace-build-acceleration"
|
||||
|
||||
resolver = "1"
|
||||
|
||||
dep-format-version = "4"
|
||||
workspace-hack-line-style = "workspace-dotted"
|
||||
|
||||
platforms = [
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
]
|
||||
|
||||
exact-versions = true
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
*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/
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
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
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
# Ignore certain code for Github language statistics
|
||||
# https://github.com/github/linguist/blob/master/docs/overrides.md
|
||||
old_code/trumpet-trumped.com/dictionary/cmudict-0.7b linguist-vendored
|
||||
old_code/voder-vocodes-1.0/frontends/admin/node_modules/** linguist-vendored
|
||||
test_data/html_scraping/** linguist-vendored
|
||||
windows/sqlite/** linguist-vendored
|
||||
old_code/voder-vocodes-1.0/frontends/admin/node_modules linguist-vendored
|
||||
windows/sqlite linguist-vendored
|
||||
|
|
|
|||
|
|
@ -1,237 +0,0 @@
|
|||
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 }}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
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 }}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
# 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
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
# 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
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
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
|
||||
|
|
@ -2,70 +2,33 @@
|
|||
*secret*sh
|
||||
.env
|
||||
.env-aichatbot-secrets
|
||||
.env-analytics-job
|
||||
.env-elasticsearch-secrets
|
||||
.env-migration-tool-secrets
|
||||
.env-secrets
|
||||
.env-support-tool-secrets
|
||||
_develop/tools/.secrets
|
||||
_develop/tools/db_secrets.toml
|
||||
crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env
|
||||
db.secrets
|
||||
db_secrets.toml
|
||||
env-secrets
|
||||
pubkeys.txt
|
||||
secrets.toml
|
||||
secrets.yaml
|
||||
secrets/
|
||||
twitch_secrets.toml
|
||||
|
||||
# Old env secrets
|
||||
.*-secrets
|
||||
|
||||
# New env secrets (per-app)
|
||||
*-secrets.env
|
||||
|
||||
# Utilities with secrets
|
||||
production_database_secret_connect.sh
|
||||
production_database_secret_query.sh
|
||||
production_database_voice_clone_report_download.sh
|
||||
script/artcraft/mac_signing_build.sh
|
||||
script/production_database_secret_backup.sh
|
||||
script/production_database_secret_connect.sh
|
||||
script/socat_dev_database_connect.sh
|
||||
secrets.toml
|
||||
secrets.yaml
|
||||
_develop/tools/.secrets
|
||||
_develop/tools/db_secrets.toml
|
||||
twitch_secrets.toml
|
||||
crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env
|
||||
|
||||
# Python virtual envs (named by convention)
|
||||
venv/
|
||||
|
||||
# LLM Testing
|
||||
/external/
|
||||
/prompts/
|
||||
/reports/
|
||||
/requests/
|
||||
|
||||
# Scratch data
|
||||
scratch/
|
||||
test_data/temp/
|
||||
untracked/
|
||||
tmp/
|
||||
# New env secrets (per-app)
|
||||
*-secrets.env
|
||||
|
||||
# Rust build directory
|
||||
target/
|
||||
crates/lib/database_queries/target/
|
||||
crates/lib/sqlite_queries/target/
|
||||
|
||||
# Javascript build directories
|
||||
node_modules/
|
||||
|
||||
# Frontend build outputs
|
||||
frontend/apps/artcraft/bundle-report.html
|
||||
|
||||
# Vim noise
|
||||
*.swo
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# IntelliJ/CLion/RustRover noise
|
||||
# IntelliJ/CLion noise
|
||||
.idea/
|
||||
|
||||
# Python noise
|
||||
|
|
@ -74,9 +37,6 @@ __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
|
||||
|
||||
|
|
@ -93,15 +53,7 @@ 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/
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<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>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE tasks\n SET task_status = ?\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dc0367e9e43c3d7dda3623e77337ab4529eac2192475be7d08589886703ba1cb"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"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
75
AGENTS.md
|
|
@ -1,75 +0,0 @@
|
|||
# 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.
|
||||
File diff suppressed because it is too large
Load Diff
183
Cargo.toml
183
Cargo.toml
|
|
@ -1,126 +1,71 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
# Desktop application and its local build/test dependencies.
|
||||
members = [
|
||||
"crates/api_clients/artcraft/artcraft_api_defs",
|
||||
"crates/api_clients/artcraft/artcraft_client",
|
||||
"crates/api_clients/artcraft/artcraft_router",
|
||||
"crates/api_clients/fal_client",
|
||||
"crates/api_clients/gmicloud_client",
|
||||
"crates/api_clients/grok_api_client",
|
||||
"crates/api_clients/grok_consumer_client",
|
||||
"crates/api_clients/kinovi_web_client",
|
||||
"crates/api_clients/midjourney_client",
|
||||
"crates/api_clients/openai_sora_client",
|
||||
"crates/api_clients/worldlabs_api_client",
|
||||
"crates/api_clients/worldlabs_consumer_client",
|
||||
"crates/desktop/artcraft",
|
||||
"crates/lib/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",
|
||||
# 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/lib/errors",
|
||||
"crates/lib/files/file_to_base64_url",
|
||||
"crates/lib/files/archives",
|
||||
"crates/lib/files/filesys",
|
||||
"crates/lib/files/images",
|
||||
"crates/lib/files/hashing",
|
||||
"crates/lib/files/media",
|
||||
"crates/lib/files/mimetypes",
|
||||
"crates/lib/http_headers",
|
||||
"crates/lib/jwt_light",
|
||||
"crates/lib/url_utils",
|
||||
"crates/lib/uuid_utils",
|
||||
"crates/schema/database/sqlite_tasks",
|
||||
"crates/schema/public/enums",
|
||||
"crates/schema/public/tokens",
|
||||
"crates/testing/test_data",
|
||||
"crates/testing/test_utils",
|
||||
"crates/vendor/tauri-plugin-http",
|
||||
"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",
|
||||
]
|
||||
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
70
Cranky.toml
|
|
@ -1,70 +0,0 @@
|
|||
# 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
86
LICENSE.md
|
|
@ -1,86 +0,0 @@
|
|||
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
181
README.md
|
|
@ -1,139 +1,46 @@
|
|||
<h1 align="center">ArtCraft</h1>
|
||||
<p align="center"><strong>The IDE for artists.</strong></p>
|
||||
storyteller-rust
|
||||
================
|
||||
|
||||
<!-- Main video: https://www.youtube.com/watch?v=kzvQMdg66Go
|
||||
GitHub media issue: https://github.com/storytold/artcraft/issues/1951 -->
|
||||
Our Rust monorepo, which serves both our API surface area and our downloadable client applications.
|
||||
|
||||
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&label=Discord&color=5865F2&logo=discord&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&logo=youtube&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&logo=x&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)
|
||||
```
|
||||
."-,.__
|
||||
`. `. ,
|
||||
.--' .._,'"-' `.
|
||||
. .' `'
|
||||
`. / ,'
|
||||
` '--. ,-"'
|
||||
`"` | \
|
||||
-. \, |
|
||||
`--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
|
||||
/_|.-'\ ,". '.'`__'-( \
|
||||
/ ,"'"\,' `/ `-.|"
|
||||
```
|
||||
47
ROADMAP.md
47
ROADMAP.md
|
|
@ -1,47 +0,0 @@
|
|||
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™
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
-- noinspection SqlDialectInspectionForFile
|
||||
--
|
||||
-- NB: This version needs to be bumped even if just adding or changing comments!
|
||||
-- Failure to do so will cause Windows to segfault at start. (This is horrible!)
|
||||
--
|
||||
-- NB: `app_state_dir.rs` contains the file version number.
|
||||
--
|
||||
-- Migration history:
|
||||
-- tasks_v1.sqlite - initial version
|
||||
-- tasks_v2.sqlite - added model_type (nullable)
|
||||
-- tasks_v3.sqlite - added frontend_caller, comments
|
||||
-- tasks_v4.sqlite - added is_dismissed_by_user
|
||||
-- tasks_v5.sqlite - added five fields:
|
||||
-- on_complete_batch_token,
|
||||
-- on_complete_primary_media_file_token,
|
||||
-- on_complete_primary_media_file_class,
|
||||
-- on_complete_primary_media_file_cdn_url,
|
||||
-- on_complete_primary_media_file_thumbnail_url_template,
|
||||
-- tasks_v6.sqlite - added two fields:
|
||||
-- on_failure_type
|
||||
-- on_failure_message
|
||||
-- tasks_v7.sqlite - added prompt token + two fields (for fal):
|
||||
-- prompt_token
|
||||
-- queue_status_url
|
||||
-- queue_response_url
|
||||
|
||||
CREATE TABLE tasks (
|
||||
-- Task auto-incrementing primary key.
|
||||
-- We don't use this.
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
|
||||
-- TaskStatus enum
|
||||
-- This is how the job system manages job states.
|
||||
-- e.g. pending, started, complete_success, dead, etc.
|
||||
task_status TEXT NOT NULL,
|
||||
|
||||
-- TaskType enum
|
||||
-- e.g. image_generation, video_generation, etc.
|
||||
task_type TEXT NOT NULL,
|
||||
|
||||
-- TaskModelType enum
|
||||
-- e.g. flux_1_dev, veo_2, etc.
|
||||
model_type TEXT,
|
||||
|
||||
-- GenerationProvider enum
|
||||
-- Together with `provider_job_id`, this creates a foreign key lookup to the job.
|
||||
-- e.g. 'artcraft', 'fal', 'sora', etc.
|
||||
provider TEXT,
|
||||
|
||||
-- The primary key for the job in the provider's system.
|
||||
provider_job_id TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- For third party jobs, we need to record the prompt details upfront.
|
||||
-- We create a prompt record and store the token to associate once the task completes.
|
||||
prompt_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- Tell the job system which caller initiated the task.
|
||||
-- e.g. 'canvas'
|
||||
frontend_caller TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An arbitrary opaque identifier that frontend can set.
|
||||
frontend_subscriber_id TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An opaque JSON payload set by the frontend subscriber that
|
||||
-- will be re-emitted back to the frontend.
|
||||
frontend_subscriber_payload TEXT,
|
||||
|
||||
-- Whether the user has dismissed the task from view.
|
||||
is_dismissed_by_user INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- For some third party systems (eg. FAL), the URL to check the status of the
|
||||
-- job in the queue.
|
||||
queue_status_url TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- For some third party systems (eg. FAL), the URL to check the
|
||||
-- results (when the job completes)
|
||||
queue_response_url TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the batch token (if any).
|
||||
-- This will point to a collection of multiple files.
|
||||
on_complete_batch_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the media token of the "primary" media file.
|
||||
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||
on_complete_primary_media_file_token TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- When the generation completes, the file type of the primary media file.
|
||||
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
|
||||
on_complete_primary_media_file_class TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- The URL to the media file.
|
||||
on_complete_primary_media_file_cdn_url TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- The URL template for the thumbnail of the primary media file.
|
||||
on_complete_primary_media_file_thumbnail_url_template TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- An enum-like string describing the failure reason (even for non-terminal failures).
|
||||
-- The enum is `TaskFailureType`, which is a close analogue of the backend API's `FrontendFailureCategory`.
|
||||
on_failure_type TEXT,
|
||||
|
||||
-- OPTIONAL.
|
||||
-- A human-readable message describing the failure reason (even for non-terminal failures).
|
||||
on_failure_message TEXT,
|
||||
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
|
||||
completed_at INTEGER DEFAULT NULL
|
||||
);
|
||||
|
||||
-- Unique Indices
|
||||
CREATE UNIQUE INDEX idx_tasks_on_provider_job_id ON tasks(provider_job_id);
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX idx_tasks_on_status ON tasks(task_status);
|
||||
CREATE INDEX idx_tasks_on_provider ON tasks(provider);
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
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.
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
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.
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
development notes
|
||||
=================
|
||||
|
||||
Actix notes
|
||||
-----------
|
||||
|
||||
json request
|
||||
```
|
||||
async fn handler(request: web::Json<Mytype>) -> impl Responder { "whatever" }
|
||||
```
|
||||
|
||||
form-multipart request
|
||||
```
|
||||
async fn handler(request: web::Form<Mytype>) -> impl Responder { "whatever" }
|
||||
```
|
||||
|
||||
route parameters
|
||||
```
|
||||
#[get("/{name}")]
|
||||
async fn hello(name: web::Path<String>) -> impl Responder { format!("Hi {}", name) }
|
||||
```
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# 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`
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
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
|
||||
```
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
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.
|
||||
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
production builds and deployment
|
||||
================================
|
||||
|
||||
### Github Docker builds
|
||||
|
||||
The repository needs to be given read access to the base docker image:
|
||||
|
||||
https://github.com/orgs/storytold/packages/container/docker-base-images-rust-ssl/settings
|
||||
|
||||
These instructions assume running on GCP.
|
||||
|
||||
### Database migrations
|
||||
|
||||
1. Set `DATABASE_URL` in `.env` to the production secrets (DO NOT COMMIT!)
|
||||
2. Run `diesel migration run`
|
||||
|
||||
### Setting up public buckets without list permission
|
||||
|
||||
Public buckets that deny the `list` action should use the following Role:
|
||||
|
||||
`roles/storage.legacyObjectReader`
|
||||
|
||||
See:
|
||||
|
||||
* https://stackoverflow.com/a/56354633
|
||||
* https://cloud.google.com/storage/docs/access-control/making-data-public#buckets
|
||||
|
||||
### Generating Bucket Access Key and Secret Key
|
||||
|
||||
https://cloud.telestream.net/tutorials/how-to-setting-up-google-cloud-storage/
|
||||
|
||||
1. Go to the GCS page
|
||||
2. Click "settings"
|
||||
3. Click "interoperability" tab
|
||||
4. (enable interoperable access if not already set)
|
||||
5. Click "create new key"
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
redis schema
|
||||
============
|
||||
|
||||
Database 0: TTS / RateLimiter
|
||||
-----------------------------
|
||||
|
||||
TODO: Document rate limiter
|
||||
|
||||
Public statistics on usage displayed on the website:
|
||||
|
||||
* `ttsUseCount:{model_token}`: INCR, GET
|
||||
* `w2lUseCount:{template_token}`: INCR, GET
|
||||
|
||||
Temporary job progress reports to the user:
|
||||
|
||||
These are written as a single string, such as "downloading model", "uploading model",
|
||||
"generating preview", "executing", etc.
|
||||
|
||||
* `ttsDownloadExtraStatus:{inference_job_token}`: SETEX, GET
|
||||
* `ttsInferenceExtraStatus:{inference_job_token}`: SETEX, GET
|
||||
* `w2lDownloadExtraStatus:{inference_job_token}`: SETEX, GET
|
||||
* `w2lInferenceExtraStatus:{inference_job_token}`: SETEX, GET
|
||||
|
||||
Database 1: Obs / Twitch
|
||||
------------------------
|
||||
|
||||
* WIP
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
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"]
|
||||
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
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
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
# 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;
|
||||
# }
|
||||
# }
|
||||
#
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
#!/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
|
||||
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
#!/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';"
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/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
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# LOCALHOST ONLY
|
||||
|
||||
curl -vvv \
|
||||
-H "content-type: application/json" \
|
||||
--data '{"username_or_email": "echelon", "password": "BAD_PASSWORD"}' \
|
||||
http://localhost:12345/login
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# LOCALHOST ONLY
|
||||
|
||||
curl -vvv \
|
||||
-H "content-type: application/json" \
|
||||
--data '{"username_or_email": "nobody", "password": "testing"}' \
|
||||
http://localhost:12345/login
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# LOCALHOST ONLY
|
||||
|
||||
curl -vvv \
|
||||
-H "content-type: application/json" \
|
||||
--data '{"username_or_email": "echelon@gmail.com", "password": "testing"}' \
|
||||
http://localhost:12345/login
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# LOCALHOST ONLY
|
||||
|
||||
curl -vvv \
|
||||
-H "content-type: application/json" \
|
||||
--data '{"username_or_email": "echelon", "password": "testing"}' \
|
||||
http://localhost:12345/login
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#!/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))
|
||||
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#!/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')
|
||||
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u storyteller \
|
||||
-ppassword \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from badges\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u storyteller \
|
||||
-ppassword \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from ip_address_bans\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u storyteller \
|
||||
-ppassword \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from firehose_entries\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u root \
|
||||
-proot \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from user_sessions\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u storyteller \
|
||||
-ppassword \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from tts_inference_jobs\G"
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#!/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}"
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u storyteller \
|
||||
-ppassword \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from tts_models\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u root \
|
||||
-proot \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from tts_model_upload_jobs\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u root \
|
||||
-proot \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from users\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u root \
|
||||
-proot \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from w2l_inference_jobs\G"
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#!/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}"
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u storyteller \
|
||||
-ppassword \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from w2l_templates\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u root \
|
||||
-proot \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from w2l_template_upload_jobs\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u storyteller \
|
||||
-ppassword \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from user_roles\G"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/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"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/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"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# ONLY FOR LOCAL DEV
|
||||
mysql -u storyteller \
|
||||
-ppassword \
|
||||
-h localhost \
|
||||
-D storyteller \
|
||||
-e "select * from tts_result_synthetic_ids\G"
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Secrets
|
||||
db_secrets.toml
|
||||
|
||||
# Python Virtual Env
|
||||
python/
|
||||
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
#!/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)
|
||||
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
#!/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")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
mysql-connector-python==8.0.32
|
||||
protobuf==3.20.3
|
||||
toml==0.10.2
|
||||
|
|
@ -1,625 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
Dev Setup
|
||||
=========
|
||||
|
||||
# ArtCraft
|
||||
|
||||
ArtCraft is a Rust / Tauri app.
|
||||
|
||||
To set up the ArtCraft development environment, install the following:
|
||||
|
||||
1. [Install Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html).
|
||||
2. [Install npm](https://nodejs.org/en/download) or [nvm](https://github.com/nvm-sh/nvm). (Node version `v24.13.0` works at time of writing.)
|
||||
3. [Install nx](https://nx.dev/docs/getting-started/installation). (Nx version `v22.4.5` works at time of writing.)
|
||||
4. [Install Tauri CLI](https://v2.tauri.app/reference/cli/). (Version `tauri-cli 2.10.0` works at time of writing.)
|
||||
|
||||
An easy way to get started with running the app in development is to run the two commands (in separate terminals):
|
||||
|
||||
**Mac and Linux Development**
|
||||
|
||||
```bash
|
||||
# Run the frontend dev server
|
||||
./script/artcraft/unix_frontend_dev.sh
|
||||
|
||||
# Run the Tauri Rust application
|
||||
./script/artcraft/unix_rust_dev.sh
|
||||
```
|
||||
|
||||
**Windows Development**
|
||||
|
||||
```powershell
|
||||
# Run the frontend dev server
|
||||
.\script\artcraft\windows_frontend_dev.ps1
|
||||
|
||||
# Run the Tauri Rust application
|
||||
.\script\artcraft\windows_rust_dev.ps1
|
||||
```
|
||||
|
||||
Backend services and website builds live in the separate `artcraft-services` repository.
|
||||
This repository retains the desktop task database in
|
||||
`_database/sql/artcraft_migrations/` and its SQLite query cache in `.sqlx/`.
|
||||
Use `SQLX_OFFLINE=true cargo check -p artcraft` to check Rust without a database server.
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
-- 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;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
-- 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';
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
-- 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');
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
-- 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;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
-- 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
|
||||
);
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
-- 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'
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
-- 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');
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
-- 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
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
-- 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.
|
||||
|
||||
-- Status values: past_due, incomplete, active, canceled, incomplete_expired (perhaps not exhaustive)
|
||||
select distinct status
|
||||
from subscriptions
|
||||
|
||||
select count(*)
|
||||
from subscriptions
|
||||
where status IN ('active');
|
||||
|
||||
-- Metadata is in the 'subscriptions_metadata' table, and we store 'username', 'email', and 'user_token'
|
||||
select
|
||||
m.value,
|
||||
m.key,
|
||||
s.id,
|
||||
s.created,
|
||||
s.cancel_at,
|
||||
s.canceled_at
|
||||
from subscriptions as s
|
||||
join subscriptions_metadata as m
|
||||
on m.subscription_id = s.id
|
||||
where m.key = 'username'
|
||||
|
||||
|
||||
-- Full mapping of Stripe subscribers to FakeYou users
|
||||
select
|
||||
s.id as subscription_id,
|
||||
user_tokens.user_token,
|
||||
usernames.username,
|
||||
emails.email,
|
||||
s.status as subscription_status,
|
||||
s.created,
|
||||
s.cancel_at,
|
||||
s.canceled_at
|
||||
from subscriptions as s
|
||||
join (
|
||||
select
|
||||
m.subscription_id,
|
||||
m.value as username
|
||||
from subscriptions_metadata as m
|
||||
where m.key = 'username'
|
||||
) as usernames
|
||||
on s.id = usernames.subscription_id
|
||||
join (
|
||||
select m.subscription_id,
|
||||
m.value as email
|
||||
from subscriptions_metadata as m
|
||||
where m.key = 'email'
|
||||
) as emails
|
||||
on s.id = emails.subscription_id
|
||||
join (
|
||||
select m.subscription_id,
|
||||
m.value as user_token
|
||||
from subscriptions_metadata as m
|
||||
where m.key = 'user_token'
|
||||
) as user_tokens
|
||||
on s.id = user_tokens.subscription_id
|
||||
order by s.id asc
|
||||
|
||||
-- Without emails
|
||||
select
|
||||
s.id as subscription_id,
|
||||
user_tokens.user_token,
|
||||
usernames.username,
|
||||
s.status as subscription_status,
|
||||
s.created,
|
||||
s.cancel_at,
|
||||
s.canceled_at
|
||||
from subscriptions as s
|
||||
join (
|
||||
select
|
||||
m.subscription_id,
|
||||
m.value as username
|
||||
from subscriptions_metadata as m
|
||||
where m.key = 'username'
|
||||
) as usernames
|
||||
on s.id = usernames.subscription_id
|
||||
join (
|
||||
select m.subscription_id,
|
||||
m.value as user_token
|
||||
from subscriptions_metadata as m
|
||||
where m.key = 'user_token'
|
||||
) as user_tokens
|
||||
on s.id = user_tokens.subscription_id
|
||||
order by s.id asc
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue