Compare commits

..
28 Commits
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 40e04793b5 Skip CodeRabbit reviews on Dependabot PRs (#2510)
Adds `.coderabbit.yaml` with `reviews.auto_review.ignore_usernames:
["dependabot[bot]"]` so CodeRabbit stops reviewing and commenting on
Dependabot PRs. Config-only change; same file is landing across the
craft repos.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:40:52 -04:00
Dylan M. Taylor 7a2935cf9e ci: configure Dependabot for Cargo and GitHub Actions (#2460)
Configures Dependabot to keep Rust / Cargo dependencies and GitHub
Actions workflows up to date.

- Schedules daily dependency checks at 06:00 America/New_York
- Groups Cargo updates into a single consolidated PR with `chore(deps)`
prefix
- Groups GitHub Actions updates into a single consolidated PR with
`ci(deps)` prefix
2026-10-18 15:10:55 -04:00
BFlat d4e4ac69bb artcraft-native: generate with Midjourney or fal (provider choice) (#2115)
Images can run on Midjourney (own account, via a sign-in window) or fal (own API key), chosen from the model selector's Select Provider submenu. Jobs finish into the library. The Midjourney credential manager and the router prompt helper move into shared crates used by both desktop apps.
2026-10-11 20:47:59 +07:00
BFlat 28a39a0de4 artcraft-native: Create Audio, in-app playback, animated previews and @Characters (#2079)
Create Audio (PromptBoxAudio) with waveform playback, in-app video playback, animated feed previews, and @Character mentions with the Characters dialog.
2026-10-11 02:03:53 +07:00
BFlatandClaude Opus 5.5 bb27304b8b artcraft-native: custom title bar and Claude-style layout (#2068)
## What

The native app now draws its own window chrome, laid out like the Claude
desktop app.

- **Custom title bar** (40 px) replaces the native frame:
  - Left: the ArtCraft mark (goes Home) and the sidebar toggle.
- Right: credits, the account menu or Sign in, then minimize,
maximize/restore and close.
  - Dragging the empty space moves the window; a double-click maximizes.
  - On macOS the traffic lights stay, with room left for them.
- **Sidebar** is flush with the window's left edge, on the darker
chrome, with no floating card.
- **Content panel** is inset 8 px from the window edges, with rounded
corners and a hairline border. Its header holds the breadcrumbs and the
select and grid/list toggles.
- **Borderless window**: the edges and corners resize it, and a hairline
outlines it (hidden when maximized).

## Notes

- The window chrome helpers (`ui/window.rs`) are ported from the
ArtCraft Launcher, which uses the same egui 0.36.
- The mark is the in-repo `services/artcraft.svg`, tinted brand blue.
The old wordmark loader is removed.
- Toasts are moved down to sit below the new header.

## Testing

- `cargo test -p artcraft_native`: 29 tests pass.
- `cargo clippy -p artcraft_native --all-targets --no-deps` is clean.
- Screenshot-verified against a live account.
- Not tested by hand: dragging, resizing and the caption buttons.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:54:50 +07:00
BFlatandClaude Opus 5.5 b5614ad333 Add artcraft-native: pure Rust (egui) desktop app, starting with Create Image and Create Video (#2067)
## What

A new workspace member, `artcraft-native/`. It is the ArtCraft desktop
app in pure Rust: an egui UI with no Tauri, no webview and no
JavaScript. It covers the two main product pages, **Create Image** and
**Create Video**, and reuses the Tauri-free API crates
(`artcraft_client`, `artcraft_api_defs`, `tokens`, `enums`).

It also adds:
- a Home page and a Library page
- a lightbox
- sign-in and settings dialogs
- a sidebar and top bar in the style of the brutalist
**artcraft-webapp**

Tools that aren't ported yet show a "coming soon" page that links to the
web app.

```bash
cargo run -p artcraft_native --release
```

## Why

To get a native, webview-free desktop app. Generation goes through
**OmniGen** (`/v1/omni_gen/{models,cost,generate}`), the same API as the
webapp. Model capabilities come from the server, so the pickers stay
current without app updates.

## Prompt box

Recreated feature for feature from `PromptBoxImage` / `PromptBoxVideo`:
- **Editor:** auto-grow, focus mode, resize grip, length counter with
the model's limit, Enter-to-generate setting, and `@Image1` / `@Video1`
/ `@Audio1` mentions with highlighting and autocomplete.
- **References:** the fanned reference deck (hover-expand, reorder, add
menus with limits) and the first/last keyframe slots with swap. Files
can be uploaded, picked from the library, dragged and dropped, or pasted
as images.
- **Model selector:** families with a flyout, maker logos and capability
badges.
- **Pickers:** aspect ratio, resolution, quality, count, bitrate, output
format, duration slider, sound and input mode (Keyframe / Omni
Reference).
- **Generate:** a live credit cost estimate, validation (starting frame,
text-only models, limits, uploads in progress), and clear-all with
confirmation.

The feed shows running jobs (with progress and time left), failures
(dismissable), finished results and the paginated library, in a masonry
grid or a list. It supports Recreate, Make Video, Share and Download,
plus select mode for batch downloads.

## Reviewer notes

- **Sign-in.** On first launch the app reuses an existing desktop login
by reading the session cookie from the Tauri app's jar, and keeps its
own copy in `~/Artcraft/credentials/native_session.json`. Browser
(device-code) and password sign-in are also available.
- **Decoding.**
- Generate and cost requests are sent as JSON maps, like the Tauri app's
omni path.
- Jobs, library, batch and prompt responses use lenient structs
(`backend/wire.rs`), so new server enum values can't break decoding.
- **Renderer.** It uses eframe's **glow** renderer. `wgpu-hal` fails to
build against the `windows` crate versions this workspace pins for
Tauri.
- **Model names and logos.** Known families use curated names: "Flux 1
Dev" rather than the server's "FLUX.1 [dev]". When the server reports
`artcraft` as a model's maker, the logo comes from the id prefix instead
(Nano Banana → Google), matching the webapp.
- **Fonts and assets.** Inter, Archivo and Geist Mono are bundled under
the OFL and listed in `NOTICE`. The wordmark, icon and maker logos are
the existing in-repo assets.
- **Diagnostics.** `ARTCRAFT_SCREENSHOT=<png>` (optionally with
`ARTCRAFT_PAGE` and `ARTCRAFT_SCREENSHOT_EXIT=1`) saves a screenshot
once the window has settled.

## Testing

- `cargo test -p artcraft_native`: 29 tests pass. Unit tests cover the
catalog, feed store, wire decoding and backend helpers. Headless
`egui_kittest` tests drive the prompt box: generate gating,
Enter-to-generate, pickers, `@` mentions and clear-all.
- `cargo clippy -p artcraft_native --all-targets --no-deps` is clean.
- Run against a real account: the session import, model lists, cost
estimates, job polling, library paging and thumbnails all worked.
- **Not exercised: a real paid generation and real uploads.** I held
back to avoid spending credits, so they need a manual check.

## Not done yet

- `@Character` mentions for Seedance 2.0
- Provider choice (Midjourney direct, fal); everything runs through
ArtCraft
- In-app video and audio playback; these open in the system player
- Animated previews in the feed
- The remaining tools (Audio, 3D, editors)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:44:59 +07:00
Brandon ThomasandClaude Opus 5.5 8abde75133 Relicense ArtCraft under MIT OR Apache-2.0 (#2043)
## Summary
- Removes the WIP fair-source `LICENSE.md` and adds `LICENSE-MIT` and
`LICENSE-APACHE`, matching PhotoCraft, LightCraft, and the other craft
apps.
- Adds `NOTICE`, which lists bundled third-party material (ammo.js, the
vendored tauri-plugin-http, normalize.css, three-dots, Font Awesome
icons, the Carter One font) and says that provider logos are trademarks
of their owners.
- Updates the README's License section, and sets `license` in
`crates/desktop/artcraft/Cargo.toml` and `frontend/package.json` to `MIT
OR Apache-2.0`.

## Follow-up needed
`NOTICE` excludes assets that ArtCraft doesn't own: Nintendo sound
effects in `resources/sound/`, meme images, the AllSky-style gray/night
skyboxes, the Lumine VMD, and the Mixamo animation GIFs. The exclusion
only says these files aren't MIT/Apache. They still need to be replaced
or removed in a separate PR. Nobody has confirmed where the avatars,
default covers, `characters/img*.png`, and the day skybox came from.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:14:38 -04:00
Brandon Thomas 910bfe4e84 artcraft 0.42.0 (#2026) 2026-10-09 13:03:51 -04:00
Brandon Thomas 7aebfc1c59 fix error messages so they're user friendly (#2025) 2026-10-09 13:01:10 -04:00
3e5793b693 PageScene: dispose GPU assets on desktop (#1972)
Targets PageScene and other 3D viewers, handles uniforms and textures,
amongst other general editor mount improvements

---------

Co-authored-by: heojunfo <kleague3@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 17:56:14 -07:00
BFlat 3210977fb1 Use the new ArtCraft icon for the app icon and favicons (#1965)
- Regenerate the desktop app icons (Windows, macOS, Store, Android, iOS)
from the new angular ArtCraft mark in the website logo blue
- Replace favicon.ico, the 16/32 favicons, and the apple-touch icon
- Add the SVG icon as the primary favicon, same as the webapp
2026-09-28 13:16:34 +07:00
BFlat 90c7a8a118 Match desktop app styling to the webapp (#1963) 2026-09-28 12:29:54 +07:00
Brandon Thomas 1b27c31531 thumbnail speedup (and fix for non-rendering thumbnails) (#1964) 2026-09-28 01:28:45 -04:00
Brandon Thomas 93b664d871 client identity (origin, user agent) (#1962) 2026-09-27 23:41:15 -04:00
Brandon Thomas 92e6f438c3 better dev script (#1961) 2026-09-27 14:48:34 -04:00
Brandon Thomas df707bf8e1 performance changes (#1959) 2026-09-26 17:50:36 -07:00
Brandon Thomas 780e4494e9 fix login video on desktop (#1958) 2026-09-26 17:08:31 -07:00
Brandon Thomas c3844acdad ArtCraft 0.41 (#1957)
- website-based login
2026-09-26 03:43:28 -07:00
Brandon Thomas 8456e2fd06 website based login challenges (#1956) 2026-09-26 03:42:09 -07:00
Brandon Thomas 95f8f659a5 ArtCraft 0.40.0 (#1955) 2026-09-18 16:26:35 -04:00
Brandon Thomas 3c5038c159 Added new models and modes: GPT Image 2.5, Wan, Output Formats (#1954) 2026-09-18 16:23:44 -04:00
Brandon Thomas 584fdb7044 backport midjourney fixes from ArtCraft-X (#1953) 2026-09-14 12:45:24 -04:00
Brandon Thomas 5c7807add6 readme update (#1952) 2026-09-11 18:47:17 -04:00
Brandon Thomas 906447fab2 artcraft 0.39: fix for CI builds (no-OP build) (#1950) 2026-09-11 15:47:13 -04:00
Brandon Thomas 6cc5ce7a67 Fix CI builds. We were relying on build caches. (#1949) 2026-09-11 15:46:14 -04:00
Brandon Thomas 9a9b406fdf bump artcraft to 0.38 to test repo reset (#1948)
The entire repo was purged of ancillary code (backend, frontend, models,
tools, etc.) and subsequently reset.

This is a no-op version that guarantees builds can continue on the new
repo state.
2026-09-11 14:48:01 -04:00
Brandon Thomas 28e5d71102 remove stale docs and files (#1947) 2026-09-11 14:43:33 -04:00
Brandon Thomas 0004726a36 ArtCraft Desktop (fresh start)
This is a fresh start for the `artcraft` repo.

The old monorepo, which included our web API gateway, job services, SRE
tools, various frontends, and more has moved to `artcraft-services`

You can find the full git history here:

https://github.com/storytold/artcraft-services

This change was motivated by the fact that the entire git history was
hundreds of gigabytes in size, which is unwieldy for anyone not in the
core team that doesn't care about support services outside of the core
artcraft app.

We wanted to keep this repo in its well-known place since we already
have users and activity here.

Please update your remotes to either `artcraft-services` or make a fresh
clone of this repo.

History can be found at the artcraft-services repo.
2026-09-11 14:19:51 -04:00
7703 changed files with 542890 additions and 639535 deletions
+56
View File
@@ -0,0 +1,56 @@
# API Conventions
## HTTP Handlers (Actix-Web)
Handlers in `storyteller_web` follow this structure:
1. **Request/Response types** with `#[derive(Deserialize/Serialize, ToSchema)]`
2. **Error enum** implementing `ResponseError`, `Display`, `Serialize`, `ToSchema`
3. **Handler function** annotated with `#[utoipa::path(...)]` for OpenAPI docs
4. Handler returns `Result<Json<Response>, ErrorType>`. Only use `Result<HttpResponse, ErrorType>` when you need to set headers/cookies.
### Error Types
- Use `CommonWebError` for simple handlers that only need standard HTTP error codes
- Use a custom error enum when you need endpoint-specific error variants (e.g. `LoginErrorType::InvalidCredentials`)
- Custom error enums MUST implement `ResponseError` (actix), `Display`, and `Serialize`
- When adding `From<RequireUserSessionError>` or similar conversions, implement them on `CommonWebError` in its own module (orphan rule)
- Error response bodies include: `success: false`, `error_code`, `error_code_str`
### Session/Auth
Helpers live in `storyteller_web` under `http_server/web_utils/user_session/`. Each takes a
generic `mysql_executor: E` (any `sqlx::Executor`), so whether it reuses a connection or acquires
its own depends on the **argument you pass**:
- `require_user_session(http_request, session_checker, mysql_executor)` — require a logged-in user.
- `require_user_session_extended(...)` — same, with extra session detail.
- `require_moderator(http_request, session_checker, mysql_executor)` — admin-only endpoints.
PREFER passing an already-open connection (`&mut *conn`) so the helper reuses it. Passing
`&server_state.mysql_pool` makes it acquire a fresh connection — avoid that when the handler
already holds one (the double-acquire adds pool pressure; this crate has had pool-timeout
incidents). There is no `*_using_connection` variant — the reuse/acquire choice is the argument.
### API Type Definitions
- Shared request/response types go in `artcraft_api_defs` crate (under `crates/api_clients/artcraft/artcraft_api_defs/`)
- Types that need `ResponseError` impl must stay in `storyteller_web` (orphan rule)
- New types must be added to `api_doc.rs` schemas (in storyteller-web) for Swagger generation
## Enums (Database-backed)
- Located in `crates/schema/public/enums/src/by_table/{table_name}/`
- Require manual `to_str()` and `from_str()` implementations
- Use `impl_enum_display_and_debug_using_to_str!` and `impl_mysql_enum_coders!` macros
- Add comprehensive tests: serialization, round-trip, variant count
- YOU CAN ADD NEW VALUES but DO NOT CHANGE EXISTING VALUES without a migration strategy
- Note that adding new values can break older versions of the Tauri desktop app that read
responses over Rust - make sure it consumes variants with an "Unknown(String)" variant
catch-all to future proof the APIs.
## Tokens (Primary Keys)
- Located in `crates/schema/public/tokens/src/tokens/`
- Opaque `String` wrappers with Stripe-like prefixes (e.g. `user_`, `mf_`)
- Use `impl_string_token!`, `impl_mysql_token_from_row!`, `impl_crockford_generator!` macros
+92
View File
@@ -0,0 +1,92 @@
# Code Style
## Rust
- Two spaces for indentation (not four)
- No minimum supported Rust version; use latest stable features freely
- `#[macro_use] extern crate serde_derive;` is used in binary crates; library crates use `use serde_derive::{Serialize, Deserialize};`
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros (`info!`, `warn!`, `error!`, `debug!`, `trace!`) instead
- Use `anyhow::Result` / `AnyhowResult` for fallible functions in application code; new database queries should use `Result<T, sqlx::Error>` instead
- SQLx: MySQL queries belong in the `mysql_queries` crate. Do NOT write `sqlx::query!` /
`sqlx::query()` in application/service crates (e.g. `storyteller-web`, which has zero direct
SQLx queries) — add or reuse a function in `mysql_queries` and call it. The compile-time
`query!` / `query_as!` macro conventions for that crate are documented in
`crates/schema/database/mysql_queries/CLAUDE.md`.
- Enum Display/Debug: use `impl_enum_display_and_debug_using_to_str!` macro, not `derive_more::Display` (CLion doesn't understand it)
- Fields that are optional use the `maybe_` prefix: `maybe_creator_user_token`, `maybe_prompt_token`
- When implementing `fmt::Display` for error types, use the pattern: `write!(f, "{:?}", self)`
## File Layout (Top-to-Bottom Reading Order)
Organize code so a human reading top-to-bottom encounters the most important things first and
can drill down into details only when curious. Think of it like a newspaper: headline first,
then the story, then the fine print.
### Ordering within a file
1. **Constants** at the top, right after imports.
2. **The primary public type** (the main struct/enum the file is about) comes next.
A reader opening this file should immediately see what it defines.
3. **Supporting/subordinate types** (sub-structs, helper enums, serialization DTOs) go
below the primary type, not above it. Ask: "Would a reader need to know this type
exists before seeing the main type?" — if no, push it down.
4. **Type ordering for API types**: Request, then Response (success), then Error.
5. **`impl` blocks** after their type definitions.
### Ordering within an `impl` block
1. **Constructors** (`new`, `from_*`, `builder`) at the top.
2. **Public methods** next — these are the main entry points a reader cares about.
3. **Private helper methods** below public methods.
- Among helpers: place "meaty" helpers (substantial logic, direct continuations of the
public entry point) above "leaf" helpers (small formatters, simple lookups).
- A method that calls another method in the same file should appear *above* the method
it calls. This lets readers follow the call chain downward.
### Ordering within test modules
Put test cases first, helpers last. A reader opening a test file should immediately see
*what* is being tested before *how* the test machinery works.
1. **Imports**
2. **Constants** (test IDs, URLs, fixture data) — small context that tests reference
3. **Test functions** or **sub-modules** grouped by category
4. **Helper functions** (builders, pipeline runners, assertions)
Group related tests into sub-modules (`mod pricing_tests { ... }`) when a category has 2+
tests. Don't create a sub-module for a single test. Keep nesting to two levels max
(`mod tests { mod category { ... } }`). Shared helpers go in the parent module so
sub-modules can reach them via `use super::*`.
## Naming
- Handler functions: `{verb}_{noun}_handler` (e.g. `get_health_check_handler`, `login_handler`)
- Error enums: `{Feature}Error` (e.g. `FalWebhookError`, `HealthCheckError`)
- Request/response structs: `{Action}Request`, `{Action}Response`, `{Action}SuccessResponse`
- Path params: `{Action}PathInfo`
- Builder structs: `{Thing}Builder` with `set_` prefixed setters and a `build()` method
## Imports
- Group imports: std, external crates, internal workspace crates, `crate::` imports
- Prefer `use` imports over inline fully-qualified paths. Inline qualification is acceptable
only for genuine one-off references where adding an import would be noise, or for types
that collide with std prelude names (`Result`, `Option`, `Error`). If you reference a symbol
more than once, always import it.
- Prefer specific imports over wildcards, except in `api_doc.rs` and test modules
- When two crates export a type with the same name, alias with a suffix describing the source crate:
`use enums::...::CommonResolution as CommonResolutionEnum;`
`use artcraft_router::...::CommonResolution as CommonResolutionRouter;`
This avoids long fully-qualified paths in function signatures and bodies.
## MySQL
- MySQL tables: primary key `id` columns must be `BIGINT(20) UNSIGNED`. Do not use signed IDs.
- Always create tables with `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin`.
## TypeScript / Frontend
- TypeScript with React, Vite, Zustand, Three.js
- Nx monorepo under `crates/frontend/`
- Two spaces for indentation
+73
View File
@@ -0,0 +1,73 @@
# Testing
## Database Fixture Tests
- Live in-crate (e.g. `storyteller-web`'s omni_gen video pricing tests) and
use the `mysql_testing` crate (`crates/schema/database/mysql_testing`) for
guarded pools, schema setup, and account/session/wallet fixtures
- They RUN BY DEFAULT with `cargo test -p storyteller-web` and from the IDE
(requires a local MySQL with an `artcraft_test` database). Machines/CI
without MySQL skip them via the off switch:
`SQLX_OFFLINE=true cargo test -p storyteller-web --features skip_database_tests`
- They connect ONLY via `ARTCRAFT_TEST_DATABASE_URL` (default
`mysql://root:@localhost:3306/artcraft_test`); the guard panics on any
database whose name lacks "test", on `storyteller`/`artcraft`, and on
cloud hosts — see `mysql_testing/README.md`
- Database tests run in PARALLEL: each test creates its own users, wallets,
and rows; schema setup single-flights via a MySQL named lock. Only tests
mutating shared/global state take
`mysql_testing::serial::acquire_serial_test_lock().await`
- External providers must never be called: the harness stubs Kinovi with an
in-process HTTP server via `KINOVI_CUSTOM_API_HOST`/`KINOVI_CUSTOM_CDN_HOST`
## Running Tests
- Most crates: `cargo test -p {crate_name}`
- Crates using SQLx offline mode: `SQLX_OFFLINE=true cargo test -p {crate_name}`
- The `storyteller-web` and `mysql_queries` crates require `SQLX_OFFLINE=true` for compilation
- Use `cargo check -p {crate_name}` for fast compilation checks without running tests
## Test Patterns
- Tests live in `#[cfg(test)] mod tests { ... }` at the bottom of each file
- Group related tests into sub-modules by category (e.g. `mod pricing_tests`, `mod resolution_tests`)
- Only create a sub-module when it has 2+ tests — don't wrap a single test in its own module
- Keep nesting shallow: `mod tests { mod category { ... } }` is fine; a third level is rarely needed
- Shared helpers used by multiple sub-modules belong in the parent `mod tests`, not duplicated in each sub-module
- Enum tests should cover: serialization, deserialization, `to_str`/`from_str` round-trip, variant count
- Password/crypto tests: be mindful of computation cost, don't add too many bcrypt test cases
## Test File Layout
Put test cases first, helpers last. Engineers should see the actual tests before scrolling
to understand helper plumbing.
Within a `mod tests { ... }` block:
1. **Imports** at the top
2. **Constants** (test data IDs, URLs, etc.) — small context that tests reference
3. **Test functions** (`#[test]` / `#[tokio::test]`) grouped by category
4. **Helper functions** (builders, pipeline runners, etc.) at the very bottom
If tests are grouped into sub-modules, the same rule applies within each sub-module:
constants first, then tests, then helpers at the end of that sub-module.
## What to Test
- New error types and their mappings
- Enum variant serialization stability (these are stored in the database)
- Hash/deduplication functions with known inputs
- Edge cases in parsers (empty input, missing fields, malformed data)
## Imports in Tests
- Import test fixtures and constants with `use`, not inline fully-qualified paths.
Write `use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;` at the top,
then reference `JUNO_AT_LAKE_IMAGE_URL` in test bodies.
- Same rule applies to enum variants used in match arms — import them, don't write
`crate::some::deep::module::MyEnum::Variant` inline.
## What Not to Test
- Don't add integration tests that require a running database unless explicitly asked
- Don't add tests for simple struct construction or trivial getters
+6
View File
@@ -0,0 +1,6 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
reviews:
auto_review:
# Dependabot bumps are checked by CI; skip CodeRabbit reviews on them.
ignore_usernames:
- "dependabot[bot]"
+14
View File
@@ -0,0 +1,14 @@
hakari-package = "workspace-build-acceleration"
resolver = "1"
dep-format-version = "4"
workspace-hack-line-style = "workspace-dotted"
platforms = [
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
]
exact-versions = true
-16
View File
@@ -1,16 +0,0 @@
*secret*sh
*~
.dockerignore
.env
.env-secrets
.git
.idea/
Dockerfile
__pycache__/
_ignored_schema.rs
models/Wav2Lip/python/
models/tacotron2/python/
production_database_secret_connect.sh
secrets.toml
secrets.yaml
target/
-84
View File
@@ -1,84 +0,0 @@
SERVER_ENVIRONMENT=development
DATABASE_URL=mysql://storyteller:password@localhost/storyteller
#DATABASE_URL=mysql://root:root@localhost/storyteller
#DATABASE_URL=mysql://root@localhost/storyteller
# Diesel-cli reads this for the migration directory to use.
# We've renamed it so the migrations are sorted near the top of the project directory tree
# (since they kind of serve as documentation)
MIGRATION_DIRECTORY=_sql/migrations/
# SQLX normally requires a live database to reflect upon at build time.
# This setting turns that on and requires a `sql-data.json` cache file instead.
# See https://github.com/launchbadge/sqlx/tree/master/sqlx-cli#readme
#SQLX_OFFLINE=true
# 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
# Stripe configs
# All secrets are development and okay to check in
STRIPE_CHECKOUT_SUCCESS_URL_PATH=/checkout_success
STRIPE_CHECKOUT_CANCEL_URL_PATH=/checkout_cancel
STRIPE_PORTAL_RETURN_URL_PATH=/portal_success
STRIPE_PORTAL_DEFAULT_CONFIG_ID=bpc_1LyPPREU5se17MekYiViZF12
# STRIPE_PORTAL_DEFAULT_CONFIG_ID=bpc_1M4jc3EU5se17MekmgdS4dDJ # Production (not a secret)
STRIPE_PUBLISHABLE_KEY=pk_test_51KZvWXEU5se17Mekx6e0gw32ovDF78k2eeaGctLIs8oQVxN6DerEp1BDqh6gLMYKli4VsTZoVGHoxq5RmwKMlxtP00r59UEEad
STRIPE_SECRET_KEY=sk_test_51KZvWXEU5se17Mek11ZdnWr4frnyw7tWOPuvl3pRzL290zvkT6KqcbGUl3fcvc3hA8oH6lWXz2kGWXXUlNcCuDxB003uVT9JX0
STRIPE_SECRET_WEBHOOK_SIGNING_KEY=whsec_40b60bd75768052532b63dbeefa7a1b163e182c3e657fb27fa906ef49a2cd59f
-25
View File
@@ -1,25 +0,0 @@
# Environmental configurations specific to `download-job`.
# These are defaulted for dev and are easy to overwrite in production with K8s environment variables.
# !! DOES *NOT* CONTAIN SECRETS !!
WEB_DOWNLOADER_COMMAND=./download_internet_file.py
WEB_DOWNLOADER_MAYBE_VENV_SCRIPT=./python/bin/activate
WEB_DOWNLOADER_MAYBE_DOCKER_IMAGE=d73f28ce3ff6
SOFTVC_MODEL_CHECK_ROOT_DIRECTORY=/
SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate"
SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=a79106991ce3
TACOTRON_MODEL_CHECK_ROOT_DIRECTORY=/models/tts
TACOTRON_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate"
TACOTRON_MODEL_CHECK_COMMAND=./vocodes_model_check_tacotron.py
TACOTRON_MODEL_CHECK_MAYBE_DOCKER_IMAGE=5642d0fd7fc1
HIFIGAN_SOFTVC_MODEL_CHECK_ROOT_DIRECTORY=/
HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate"
HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=a79106991ce3
HIFIGAN_MODEL_CHECK_ROOT_DIRECTORY=/models/tts
HIFIGAN_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate"
HIFIGAN_MODEL_CHECK_COMMAND=./vocodes_model_check_hifigan.py
HIFIGAN_MODEL_CHECK_MAYBE_DOCKER_IMAGE=5642d0fd7fc1
+3 -2
View File
@@ -1,5 +1,6 @@
# Ignore certain code for Github language statistics
# https://github.com/github/linguist/blob/master/docs/overrides.md
old_code/trumpet-trumped.com/dictionary/cmudict-0.7b linguist-vendored
old_code/voder-vocodes-1.0/frontends/admin/node_modules linguist-vendored
windows/sqlite linguist-vendored
old_code/voder-vocodes-1.0/frontends/admin/node_modules/** linguist-vendored
test_data/html_scraping/** linguist-vendored
windows/sqlite/** linguist-vendored
+30
View File
@@ -0,0 +1,30 @@
version: 2
updates:
# Maintain Rust / Cargo dependencies
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
time: "06:00"
timezone: "America/New_York"
# Consolidate updates into a single PR rather than one PR per crate
groups:
cargo-dependencies:
patterns:
- "*"
commit-message:
prefix: "chore(deps)"
# Maintain GitHub Actions workflows
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
time: "06:00"
timezone: "America/New_York"
groups:
actions:
patterns:
- "*"
commit-message:
prefix: "ci(deps)"
@@ -0,0 +1,237 @@
name: "Artcraft MacOS Publish"
on:
push:
branches:
- release
- artcraft-release
- artcraft-macos # Additional branch for MacOS testing
# This workflow creates Universal macOS builds with signing certificate support.
# On each push to the release branches it will create or update a GitHub release, build your app, and upload the artifacts to the release.
jobs:
publish-artcraft:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
# Universal macOS builds (arm64 and x86_64)
- platform: 'macos-latest-xlarge'
args: '--target universal-apple-darwin'
# Uncomment below for additional platforms
# - platform: 'ubuntu-22.04'
# args: ''
# - platform: 'windows-latest'
# args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest-xlarge' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: replace tauri config with mac-specific version
run: |
pushd ${{ github.workspace }}/crates/desktop/artcraft
# Use the mac-specific tauri conf for the build
# TODO(bt): Replace this with the platform-specific naming conventions
cp tauri-mac.conf.json tauri.conf.json
popd
- name: install frontend dependencies
run: |
# Navigate to the frontend directory to install dependencies
cd ./frontend
npm cache clean --force
echo "Installing all workspace dependencies..."
npm ci
echo "=== Version Information ==="
echo "Node version:"
node --version
echo "NPM version:"
npm --version
echo "=== Nx versions from lockfile ==="
npm list --depth=0 | grep nx
- name: import Apple Developer Certificate
if: matrix.platform == 'macos-latest-xlarge'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo "Starting certificate import process..."
# Create certificate file
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
# Debug: Check if certificate file exists and has content
echo "Certificate file info:"
ls -la certificate.p12
file certificate.p12
# Debug: Check certificate contents without password (just structure)
echo "Certificate structure (may show error if password protected):"
openssl pkcs12 -info -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -noout || echo "Could not read certificate structure"
# Debug: Try to list what's in the p12 file
echo "Attempting to list certificate contents:"
security cms -D -i certificate.p12 || echo "Not a CMS file, trying PKCS12..."
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -info -nodes 2>&1 | head -20 || echo "Failed to read PKCS12 contents"
# Debug: Extract certificate details to verify type and expiration
echo "Extracting certificate details..."
# Extract just the certificate portion and examine it
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -nokeys 2>/dev/null | openssl x509 -text -noout | grep -E "(Subject:|Issuer:|Not Before|Not After)" || echo "Failed to parse certificate details"
# Also try to see what type of certificate this is
echo "Certificate type check:"
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -nokeys 2>/dev/null | openssl x509 -text -noout | grep -E "(CN=|OU=)" || echo "Could not determine certificate type"
# Debug: Check if private key exists
echo "Checking for private key..."
openssl pkcs12 -in certificate.p12 -passin pass:$APPLE_CERTIFICATE_PASSWORD -nocerts -nodes 2>&1 | grep -E "(PRIVATE KEY|RSA KEY)" | head -5 || echo "No private key found"
# Create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
# Import certificate - ensure both certificate and private key are imported
echo "Importing certificate into keychain..."
# Import the p12 directly - this is the most reliable method
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
# Alternative: Try with additional flags if the first import didn't create an identity
echo "Verifying import..."
if ! security find-identity -v build.keychain | grep -q "1 valid identities found"; then
echo "First import didn't create identity, trying with -A flag..."
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -A
fi
# Set trust settings for the certificate
echo "Setting trust settings..."
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
# Unlock the keychain again to ensure it's accessible
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
# Add the keychain to the search list
security list-keychains -d user -s build.keychain $(security list-keychains -d user | sed 's/^[[:space:]]*"//;s/"$//')
# List all certificates to debug
echo "Listing all keychains:"
security list-keychains
echo "Listing all certificates in build keychain:"
security find-certificate -a -p build.keychain || echo "No certificates found"
echo "Detailed certificate info:"
security find-certificate -c "Learning Machines" build.keychain || echo "Certificate 'Learning Machines' not found"
# Check certificate trust settings
echo "Checking certificate trust settings:"
security dump-trust-settings || echo "No custom trust settings"
# Try to find codesigning identities
echo "Finding all identities in keychain:"
security find-identity -v build.keychain || echo "No identities found"
echo "Finding codesigning identities:"
security find-identity -v -p codesigning build.keychain || echo "No codesigning identities found"
# Additional debug: Check if this is a valid Developer ID cert
echo "Checking for Developer ID certificates:"
security find-certificate -a build.keychain | grep -A 5 -B 5 "Developer ID" || echo "No Developer ID certificates found"
- name: verify certificate
if: matrix.platform == 'macos-latest-xlarge'
run: |
echo "=== Certificate Verification ==="
echo "1. Checking codesigning identities:"
security find-identity -v -p codesigning build.keychain || echo "No codesigning identities found"
echo -e "\n2. Checking all identities:"
security find-identity -v build.keychain || echo "No identities found"
echo -e "\n3. Checking certificates in keychain:"
security find-certificate -c "Learning Machines" build.keychain -p | openssl x509 -text -noout | grep -E "(Subject:|CN=)" || echo "Could not parse certificate"
# Set the correct signing identity
echo "APPLE_SIGNING_IDENTITY=Developer ID Application: Learning Machines LLC (DJ6XS33FX8)" >> $GITHUB_ENV
echo "Proceeding with identity: Developer ID Application: Learning Machines LLC (DJ6XS33FX8)"
# Also set the team ID
echo "APPLE_DEVELOPMENT_TEAM=DJ6XS33FX8" >> $GITHUB_ENV
- name: Pre-build frontend to avoid Tauri build issues
run: |
# Build the frontend app from the correct directory
cd ./frontend
# Increase Node.js memory limit for the build
export NODE_OPTIONS="--max-old-space-size=8192"
# Use npx to ensure we use the local nx version
echo "Building with npx nx..."
npx nx run artcraft:build
echo "Checking build output..."
ls -la apps/artcraft/dist/ | head -10
# Update Tauri config to skip the build (since we already built)
cd ${{ github.workspace }}/crates/desktop/artcraft
cp tauri.conf.json tauri.conf.json.bak
# Set beforeBuildCommand to empty or a no-op command
if command -v jq &> /dev/null; then
jq '.build.beforeBuildCommand = ""' tauri.conf.json > tauri.conf.json.tmp
mv tauri.conf.json.tmp tauri.conf.json
else
node -e "const fs = require('fs'); const config = JSON.parse(fs.readFileSync('tauri.conf.json', 'utf8')); config.build.beforeBuildCommand = ''; fs.writeFileSync('tauri.conf.json', JSON.stringify(config, null, 2));"
fi
- name: build and publish
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
APPLE_DEVELOPMENT_TEAM: ${{ env.APPLE_DEVELOPMENT_TEAM }}
# Use the SQLx query cache for SQLite
SQLX_OFFLINE: true
with:
projectPath: ./crates/desktop/artcraft
tagName: artcraft-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
releaseName: "ArtCraft v__VERSION__"
releaseBody: "See the assets below to download this version and install."
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
@@ -0,0 +1,107 @@
name: "Artcraft Windows Publish"
on:
push:
branches:
- release
- artcraft-release
- artcraft-windows # Additional branch for Windows testing
# This workflow creates Windows builds.
# On each push to the release branches it will create or update a GitHub release, build your app, and upload the artifacts to the release.
jobs:
publish-artcraft-windows:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
# Windows builds
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: install wreq dependencies
run: |
choco install cmake -y
choco install strawberryperl -y
choco install pkgconfiglite -y
choco install llvm -y
choco install nasm -y
shell: cmd
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: install frontend dependencies
run: |
# Navigate to the frontend directory to install dependencies
cd ./frontend
npm cache clean --force
echo "Installing all workspace dependencies..."
npm ci
echo "=== Version Information ==="
echo "Node version:"
node --version
echo "NPM version:"
npm --version
echo "=== Nx versions from lockfile ==="
npm list --depth=0 | findstr nx
- name: Pre-build frontend to avoid Tauri build issues
run: |
# Build the frontend app from the correct directory
cd ./frontend
# Increase Node.js memory limit for the build
$env:NODE_OPTIONS="--max-old-space-size=8192"
# Use npx to ensure we use the local nx version
echo "Building with npx nx..."
npx nx run artcraft:build
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
echo "Checking build output..."
dir apps/artcraft/dist/ | Select-Object -First 10
# Update Tauri config to skip the build (since we already built)
cd ${{ github.workspace }}/crates/desktop/artcraft
Copy-Item tauri.conf.json tauri.conf.json.bak
# Set beforeBuildCommand to empty using PowerShell
$config = Get-Content tauri.conf.json | ConvertFrom-Json
$config.build.beforeBuildCommand = ""
$config | ConvertTo-Json -Depth 10 | Set-Content tauri.conf.json
- name: build and publish
uses: tauri-apps/tauri-action@v0
env:
# NB(bt): This is needed by wreq. Hopefully it doesn't kill Tauri
RUSTFLAGS: "-C target-feature=+crt-static"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Use the SQLx query cache for SQLite
SQLX_OFFLINE: true
with:
projectPath: ./crates/desktop/artcraft
tagName: artcraft-v__VERSION__ # the action automatically replaces __VERSION__ with the app version.
releaseName: "ArtCraft v__VERSION__"
releaseBody: "Production build of ArtCraft. See the assets below to download and install."
releaseDraft: true
prerelease: false
args: ${{ matrix.args }}
-52
View File
@@ -1,52 +0,0 @@
name: CI
on: [push, pull_request]
env:
MYSQL_URL: mysql://root:root@localhost/storyteller
DATABASE_URL: mysql://root:root@localhost/storyteller
REDIS_0_URL: redis://redisd@localhost/0
REDIS_1_URL: redis://redisd@localhost/1
DEBIAN_FRONTEND: noninteractive
jobs:
setup_build_test:
runs-on: ubuntu-20.04
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
- name: Setup mysql
run: |
sudo systemctl start mysql
sudo apt-get update
sudo apt-get -y install mysql-client libmysqlclient-dev
mysql -e "create user 'storyteller'@'localhost' IDENTIFIED BY 'password';" -uroot -proot
mysql -e "create database storyteller; grant all on storyteller.* to 'storyteller'@'localhost';" -uroot -proot
- name: Setup Redis
run: |
sudo apt-get -y install redis-server
sudo service redis-server start
- name: Setup Rust
run: |
rustup set profile minimal
rustup show
rustup install 1.66
rustup default 1.66
rustup show
cargo install sqlx-cli --features openssl-vendored
cargo install diesel_cli
- name: Run database migrations
run: |
# nothing to see here ;)
set +e +o pipefail
diesel migration run
set -e -o pipefail
- name: Build
run: cargo build --release
- name: Test
run: cargo test --release
-112
View File
@@ -1,112 +0,0 @@
# Adapted from
# https://github.com/actions/starter-workflows/ci/docker-publish.yml
# If using base images, don't forget to add READ access, eg. from a package settings page like:
# https://github.com/orgs/storytold/packages/container/docker-base-images-rust-ssl/settings/actions_access
name: Publish Docker Container to GHCR
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
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
# 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
+62 -6
View File
@@ -2,29 +2,70 @@
*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
secrets.toml
secrets.yaml
_develop/tools/.secrets
_develop/tools/db_secrets.toml
twitch_secrets.toml
script/artcraft/mac_signing_build.sh
script/production_database_secret_backup.sh
script/production_database_secret_connect.sh
script/socat_dev_database_connect.sh
# Python virtual envs (named by convention)
venv/
# LLM Testing
/external/
/prompts/
/reports/
/requests/
# Scratch data
scratch/
test_data/temp/
untracked/
tmp/
# Rust build directory
target/
crates/lib/database_queries/target/
crates/lib/sqlite_queries/target/
# Javascript build directories
node_modules/
# Frontend build outputs
frontend/apps/artcraft/bundle-report.html
# Vim noise
*.swo
*.swp
*~
# IntelliJ/CLion noise
# IntelliJ/CLion/RustRover noise
.idea/
# Python noise
@@ -33,6 +74,9 @@ __pycache__/
# We use diesel for DB migrations only. We don't care about its codgen.
_ignored_schema.rs
# Diesel lock file
.diesel_lock
# AI Chat Sidecar has its own schema managed by diesel, but we don't care for diesel codegen.
crates/programs/aichatbot-sidecar/_ignored_schema.rs
@@ -49,3 +93,15 @@ models/tacotron2/python/
# Stuff I want to keep around, but not commit
scraps/
# Mac nonsense
.DS_Store
# Windows build files
# These should be built by the platform
windows/sqlite/sqlite3.exp
windows/sqlite/sqlite3.lib
# ArtCraft Windows DLLs
crates/desktop/src-tauri/*.dll
windows_dlls/
-35
View File
@@ -1,35 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="bt">
<words>
<w>arpabet</w>
<w>backfill</w>
<w>cashapp</w>
<w>cheermote</w>
<w>crockford</w>
<w>culted</w>
<w>dalle</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>replayable</w>
<w>retryably</w>
<w>scrollback</w>
<w>softvc</w>
<w>symphonia</w>
<w>tacotron</w>
<w>techcrunch</w>
<w>transcoded</w>
<w>transcodings</w>
<w>upserting</w>
<w>vocoded</w>
<w>vocoder</w>
<w>vocoders</w>
<w>vocodes</w>
</words>
</dictionary>
</component>
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET is_dismissed_by_user = 1\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "13d435d8f681b73e4080748dcef0579e246e41049af67e1bd79ffc06aa6ba974"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "1e3758851fc8451670f86a9432009b73bdb81de4a08dcaa96a86afb5f9be7a68"
}
@@ -0,0 +1,128 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload,\n on_complete_primary_media_file_token,\n on_complete_primary_media_file_class,\n on_complete_batch_token,\n on_complete_primary_media_file_cdn_url,\n on_complete_primary_media_file_thumbnail_url_template,\n on_failure_type,\n on_failure_message,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n completed_at as \"completed_at: DateTime<Utc>\"\n FROM tasks\n WHERE is_dismissed_by_user == 0\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "task_status",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "task_type",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "model_type",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "provider",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "provider_job_id",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "frontend_caller",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "frontend_subscriber_id",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "frontend_subscriber_payload",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_token",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_class",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "on_complete_batch_token",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_cdn_url",
"ordinal": 12,
"type_info": "Text"
},
{
"name": "on_complete_primary_media_file_thumbnail_url_template",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "on_failure_type",
"ordinal": 14,
"type_info": "Text"
},
{
"name": "on_failure_message",
"ordinal": 15,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 16,
"type_info": "Int64"
},
{
"name": "updated_at: DateTime<Utc>",
"ordinal": 17,
"type_info": "Int64"
},
{
"name": "completed_at: DateTime<Utc>",
"ordinal": 18,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true
]
},
"hash": "2436eb7bf4b0e23a899311a67a77f71d89eb35461e2113e43cd9d221bfa91c9a"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET task_status = \"cancelled_by_user\"\n WHERE task_status NOT IN (\n \"complete_success\",\n \"complete_failure\",\n \"dead\",\n \"cancelled_by_user\",\n \"cancelled_by_provider\",\n \"cancelled_by_us\"\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "30272128d83543d9fbfca930b7cdd4af30adfb4d90f2de044463a111c6340495"
}
@@ -0,0 +1,86 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n frontend_caller,\n queue_status_url,\n queue_response_url,\n prompt_token,\n frontend_subscriber_id,\n frontend_subscriber_payload\n FROM tasks\n WHERE\n provider = ?\n AND provider_job_id = ?\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "task_status",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "task_type",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "model_type",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "provider",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "provider_job_id",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "frontend_caller",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "queue_status_url",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "queue_response_url",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "prompt_token",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "frontend_subscriber_id",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "frontend_subscriber_payload",
"ordinal": 11,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true
]
},
"hash": "35677894f7b02f32a1bf6396fa6d10caba69bb0f3f03ee7ffbfa1437689d42df"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO tasks (\n id,\n task_status,\n task_type,\n model_type,\n provider,\n provider_job_id,\n queue_status_url,\n queue_response_url,\n prompt_token,\n frontend_caller,\n frontend_subscriber_id,\n frontend_subscriber_payload\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "4fc9674ae39abaadc36054c200290ec95d0e2c7765336f0fa97805a2265f5c4c"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET\n task_status = ?,\n on_complete_batch_token = ?,\n on_complete_primary_media_file_token = ?,\n on_complete_primary_media_file_class = ?,\n on_complete_primary_media_file_cdn_url = ?,\n on_complete_primary_media_file_thumbnail_url_template = ?\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "50a2449b87abed7df3302a74ecd0e8ab87940077a247636680cb09b2fe488321"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET task_status = ?,\n on_failure_type = ?,\n on_failure_message = ?\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "9b99ccd0baf9a4b74d8903f63e3a3dcf4c09b75cad978f0532388cbd743b7516"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET task_status = ?\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "dc0367e9e43c3d7dda3623e77337ab4529eac2192475be7d08589886703ba1cb"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET is_dismissed_by_user = 1\n WHERE is_dismissed_by_user != 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "f83018ec5493982659a56576296914f937bb4e75db3a522eb4bda486d5caa402"
}
+76
View File
@@ -0,0 +1,76 @@
# 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
- Each Tauri command belongs in its own file module, named after the command. Keep shared state, types, and helpers in separate support modules; see the [Storyteller command conventions](crates/desktop/artcraft/src/services/storyteller/commands/AGENTS.md).
### 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.
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
Generated
+8537 -5098
View File
File diff suppressed because it is too large Load Diff
+124 -62
View File
@@ -1,67 +1,129 @@
[workspace]
resolver = "2"
# Desktop application and its local build/test dependencies.
members = [
# Types: common strongly-typed tokens, ids, payloads, etc.
# Also common cloud buckets and other "strongly typed" configs.
"crates/_types/buckets",
"crates/_types/enums",
"crates/_types/reusable_types", # TODO(bt, 2022-12-21): Deprecate
"crates/_types/tokens",
# CLI tools for operations, etc.
"crates/cli/database_backup",
"crates/cli/database_cleanup",
"crates/cli/scrape_test",
# Shared libraries
"crates/lib/actix_helpers",
"crates/lib/caching/memory_caching",
"crates/lib/caching/redis_caching",
"crates/lib/clients/cloud_storage",
"crates/lib/clients/fakeyou_client",
"crates/lib/clients/google_drive_common",
"crates/lib/clients/twitch_common",
"crates/lib/concurrency",
"crates/lib/config", # TODO(bt, 2023-02-13): Deprecate or repurpose
"crates/lib/container_common", # TODO(bt, 2023-02-13): Pull apart and rehome pieces
"crates/lib/crockford",
"crates/lib/database_queries", # TODO(bt2, 2023-02-13): Rename "mysql_queries", though this will not live beyond a single DB
"crates/lib/datetimes",
"artcraft-native", # Pure Rust (egui) desktop app; no Tauri or webview
"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/artcraft_client_identity",
"crates/lib/browser_emulation",
"crates/lib/build_metadata",
"crates/lib/caching/memory_store",
"crates/lib/cloudflare_errors",
"crates/lib/cloudflare_mitigation",
"crates/lib/cookie_store",
"crates/lib/cookie_store_wrapper",
"crates/lib/data/primitives",
"crates/lib/data/web_base64",
"crates/lib/errors",
"crates/lib/files",
"crates/lib/hashing",
"crates/lib/http_server_common",
"crates/lib/idempotency",
"crates/lib/jobs_common",
"crates/lib/media",
"crates/lib/redis_common",
"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/image_generation_service",
"crates/service/web/storyteller_web",
"crates/service/web/websocket_gateway",
# Service Plugins: complete subsystems that can be plugged into services (users, billing, etc.)
"crates/service/plugins/billing",
"crates/service/plugins/inference_workloads",
"crates/service/plugins/users",
"crates/lib/files/file_to_base64_url",
"crates/lib/files/filesys",
"crates/lib/files/images",
"crates/lib/files/mimetypes",
"crates/lib/http_headers",
"crates/lib/jwt_light",
"crates/lib/url_utils",
"crates/lib/uuid_utils",
"crates/schema/database/sqlite_tasks",
"crates/schema/public/enums",
"crates/schema/public/tokens",
"crates/testing/test_data",
"crates/testing/test_utils",
"crates/vendor/tauri-plugin-http",
]
default-members = ["crates/desktop/artcraft"]
[workspace.package]
publish = false
[workspace.dependencies]
artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
artcraft_client = { path = "crates/api_clients/artcraft/artcraft_client" }
artcraft_client_identity = { path = "crates/lib/artcraft_client_identity" }
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
View File
@@ -0,0 +1,70 @@
# Persist clippy check configurations:
# https://github.com/ericseppanen/cargo-cranky
# Clippy doesn't allow this (yet):
# https://github.com/rust-lang/rust-clippy/issues/6625
deny = [
# Rust
"unsafe_code", # We may remove this if needed
# Clippy - "serious" lints
"clippy::borrow_interior_mutable_const", # Seems dangerous to allow this. Eg. `const F = Lazy::new()`;
# Clippy - bad math
#"clippy::cast_possible_truncation", # Seems dangerous, eg f64 to u64.
#"clippy::cast_possible_wrap",
# Clippy - pointer safety / readability / correctness
"clippy::as_ptr_cast_mut",
"clippy::borrow_as_ptr",
"clippy::borrow_deref_ref",
# Clippy async
"clippy::await_holding_lock",
# Clippy - performance
"clippy::cmp_owned",
"clippy::useless_vec",
"clippy::format_in_format_args",
#"clippy::redundant_clone",
#"clippy::unnecessary_to_owned",
#"clippy::vec_init_then_push",
# Clippy - "ergonomic" / "usage" lints
"clippy::clone_on_copy", # Don't use clone() on Copy types
"clippy::option_option", # Lint: Don't ever Option<Option<T>>! Enums instead.
"clippy::zero_sized_map_values", # Lint: use a HashSet instead of HashMap<T, ()>.
# Clippy - "aesthetic" / "style" lints
"clippy::clone_double_ref", # Lint: feels safer/hygienic to disallow this, but I'm not strongly opposed.
"clippy::double_comparisons", # Lint: readability
"clippy::empty_structs_with_brackets", # Lint: {} feels less aesthetic than ;
"clippy::implicit_clone", # Lint: Recommendation here seems more idiomatic.
"clippy::needless_late_init", # Don't declare variables without initializing them
"clippy::needless_return", # Lint: not the worst offender, but let's try to keep unnecessary returns out.
"clippy::redundant_field_names", # Lint: Duplicate names is a burden when reading: you wonder why they're dupes.
"clippy::redundant_static_lifetimes", # Lint: feels more modern to elide static lifetimes
# Rust
"non_fmt_panics", # panic!(format!(...)) will be unsupported soon
"unused_imports", # These are easy to automatically cull
"unused_mut", # Be strict about mutability
"unused_parens", # Unused parens are ugly
"unused_variables", # Don't leave cruft
# Possible:
# "clippy::ptr_arg",
# "clippy::too_many_lines" (note: this is configurable)
]
warn = [
]
allow = [
# Rust
"deprecated", # Other systems can handle deprecated code instead of the linter
"missing_docs", # Not everything needs documentation. Too much documentation is bad. Literate code.
# Clippy
"clippy::if_same_then_else", # There are cases this is clearer! Branches may have notes / considerations.
"clippy::single_char_pattern", # This is like punishing trailing commas. Chars are dumb.
]
-228
View File
@@ -1,228 +0,0 @@
# ================================================================
# =============== (1) set up core rust build image ===============
# ================================================================
# Custom base image
# Make sure to add this repository so it has read acces to the base image:
# https://github.com/orgs/storytold/packages/container/docker-base-images-rust-ssl/settings/actions_access
# FROM ghcr.io/storytold/docker-base-images-rust-ssl:d94ce4350c3b as rust-build
FROM ubuntu:jammy as rust-base
# NB: This can be "stable" or another version.
ARG RUST_TOOLCHAIN="1.66.0"
WORKDIR /tmp
# NB: cmake is required for freetype-sys-0.13.1, which in turn has only been added for egui.
# NB: fontconfig is required by servo-fontconfig-sys, which is in the dependency chain for egui.
# NB: libfontconfig-dev is required by servo-fontconfig-sys, which is in the dependency chain for egui.
# NB: pkg-config and libssl are for container TLS; we may switch to rustls in the future.
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get install -y \
build-essential \
cmake \
curl \
fontconfig \
libfontconfig1-dev \
libssl-dev \
pkg-config
# NB: Fix for fontconfig (servo-fontconfig-sys): https://github.com/alacritty/alacritty/issues/4423#issuecomment-727277235
# TODO(bt, 2023-02-23): This has not been verified to work yet.
RUN export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig
# Install Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- --default-toolchain $RUST_TOOLCHAIN -y
# Install correct Rust version
#RUN $HOME/.cargo/bin/rustup install $RUST_VERSION
#RUN $HOME/.cargo/bin/rustup default $RUST_VERSION
# Report Rust version for build debugging
RUN $HOME/.cargo/bin/rustup show
RUN $HOME/.cargo/bin/rustc --version
RUN $HOME/.cargo/bin/cargo --version
# Cargo Chef does Rust build caching: https://github.com/LukeMathWalker/cargo-chef
# TODO(bt,2023-03-08): builds are failing with "no space left on device"; disabling build caching
#RUN $HOME/.cargo/bin/cargo install cargo-chef --locked
# ======================================================================
# =============== (2) use cargo-chef to "plan" the build ===============
# ======================================================================
FROM rust-base AS planner
# NB: Copying in everything does not appear to impact cached builds if irrelevant files are changed (at least at this step)
# TODO(bt,2023-03-08): builds are failing with "no space left on device"; disabling build caching
#COPY . .
# TODO(bt,2023-03-08): builds are failing with "no space left on device"; disabling build caching
#RUN $HOME/.cargo/bin/cargo chef prepare --recipe-path recipe.json
# ======================================================================================================
# =============== (3) "cook" the libraries (cacheable), then run the app build and tests ===============
# ======================================================================================================
FROM rust-base AS builder
# TODO(bt,2023-03-08): builds are failing with "no space left on device"; disabling build caching
#COPY --from=planner /tmp/recipe.json recipe.json
# NB: This step builds and caches the dependencies as its own layer.
# TODO(bt,2023-03-08): builds are failing with "no space left on device"; disabling build caching
#RUN $HOME/.cargo/bin/cargo chef cook --release --recipe-path recipe.json
# NB: Now we build and test our code.
COPY Cargo.lock .
COPY Cargo.toml .
COPY crates/ ./crates
COPY db/ ./db
COPY test_data/ ./test_data
COPY container_includes/ ./container_includes
# Print a report on disk space
#RUN echo "Disk usage at root (before tests):"
#RUN du -hsc / | sort -hr
RUN echo "Disk usage at current directory (before tests):"
RUN pwd
RUN du -hsc * | sort -hr
# Run all of the tests
#RUN SQLX_OFFLINE=true \
# LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
# $HOME/.cargo/bin/cargo test
# Print a report on disk space
#RUN echo "Disk usage at root (after tests):"
#RUN du -hsc / | sort -hr
RUN echo "Disk usage at current directory (after tests):"
RUN pwd
RUN du -hsc * | sort -hr
# Build all the binaries.
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin storyteller-web
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin dummy-service
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin analytics-job
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin download-job
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin inference-job
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin tts-download-job
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin w2l-download-job
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin tts-inference-job
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin w2l-inference-job
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin websocket-gateway
RUN SQLX_OFFLINE=true \
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
$HOME/.cargo/bin/cargo build \
--release \
--bin twitch-pubsub-subscriber
# Print a report on disk space
RUN echo "Disk usage at current directory (after all builds):"
RUN pwd
RUN du -hsc * | sort -hr
# =============================================================
# =============== (4) construct the final image ===============
# =============================================================
# Final image
FROM ubuntu:jammy as final
# See: https://github.com/opencontainers/image-spec/blob/master/annotations.md
LABEL org.opencontainers.image.title='Storyteller Rust'
LABEL org.opencontainers.image.authors='bt@brand.io, echelon@gmail.com'
LABEL org.opencontainers.image.description='All of the binaries from the Rust monorepo'
LABEL org.opencontainers.image.documentation='https://github.com/storytold/storyteller-web'
LABEL org.opencontainers.image.source='https://github.com/storytold/storyteller-web'
LABEL org.opencontainers.image.url='https://github.com/storytold/storyteller-web'
WORKDIR /
# Give the container its version so it can report over HTTP.
ARG GIT_SHA
RUN echo -n ${GIT_SHA} > GIT_SHA
# Copy all the binaries.
COPY --from=builder /tmp/target/release/storyteller-web /
COPY --from=builder /tmp/target/release/dummy-service /
COPY --from=builder /tmp/target/release/analytics-job /
COPY --from=builder /tmp/target/release/download-job /
COPY --from=builder /tmp/target/release/inference-job /
COPY --from=builder /tmp/target/release/tts-download-job /
COPY --from=builder /tmp/target/release/tts-inference-job /
COPY --from=builder /tmp/target/release/w2l-download-job /
COPY --from=builder /tmp/target/release/w2l-inference-job /
COPY --from=builder /tmp/target/release/websocket-gateway /
COPY --from=builder /tmp/target/release/twitch-pubsub-subscriber /
# SSL certs are required for crypto
COPY --from=builder /etc/ssl /etc/ssl
# Required dynamically linked libraries
COPY --from=builder /usr/lib/x86_64-linux-gnu/libssl.* /lib/x86_64-linux-gnu/
COPY --from=builder /usr/lib/x86_64-linux-gnu/libcrypto.* /lib/x86_64-linux-gnu/
# Container includes
COPY container_includes/ /container_includes
# Make sure all the links resolve
RUN ldd storyteller-web
# Without a .env file, Rust crashes "mysteriously" (ugh)
RUN touch .env
RUN touch .env-download-job
RUN touch .env-secrets
EXPOSE 8080
CMD LD_LIBRARY_PATH=/usr/lib /storyteller-web
+177
View File
@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ArtCraft Team and the ArtCraft contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+51
View File
@@ -0,0 +1,51 @@
ArtCraft
Copyright (c) 2026 ArtCraft Team and the ArtCraft contributors.
Licensed under the MIT License or the Apache License, Version 2.0, at your option
(LICENSE-MIT, LICENSE-APACHE).
The ArtCraft name, wordmark and logos are trademarks of the ArtCraft Team. They are not covered
by the license above.
Third-party material
This product bundles third-party material under its own licenses, listed below.
ammo.js (frontend/apps/artcraft/app/public/jsm/libs/ammo.wasm.*) — zlib License. A port of the
Bullet Physics engine. https://github.com/kripken/ammo.js
tauri-plugin-http (crates/vendor/tauri-plugin-http/) — MIT or Apache-2.0. Copyright (c) 2017 -
Present Tauri Apps Contributors; Copyright 2019-2023 Tauri Programme within The Commons
Conservancy. Modified fork; see that directory's LICENSE_MIT and LICENSE_APACHE-2.0.
normalize.css v8.0.1 (frontend/apps/artcraft/app/src/styles/normalize.css) — MIT License.
Copyright (c) Nicolas Gallagher and Jonathan Neal. https://github.com/necolas/normalize.css
three-dots v0.3.2 (portion of frontend/apps/artcraft/app/src/styles/base.css) — MIT License.
Copyright (c) 2018 nzbin. https://nzbin.github.io/three-dots/
Font Awesome Free icons (frontend/libs/icons/src/lib/brand-icons.tsx,
frontend/apps/artcraft/app/public/resources/images/services/generic.svg) — CC BY 4.0.
Copyright Fonticons, Inc. https://fontawesome.com/license/free
Carter One font (frontend/apps/artcraft/app/public/resources/fonts/CarterOneRegular.json) —
SIL Open Font License 1.1. https://fonts.google.com/specimen/Carter+One
Inter, Archivo and Geist Mono fonts (artcraft-native/assets/fonts/) — SIL Open Font License 1.1;
each font's license is next to it. https://rsms.me/inter/ ·
https://fonts.google.com/specimen/Archivo · https://vercel.com/font
Third-party trademarks
The AI provider and service logos in frontend/apps/artcraft/app/public/resources/images/services/
are trademarks of their respective owners. They are used only to identify those services and are
not covered by the license above.
Material not covered by the license above
The following files are owned by third parties. They are not licensed under MIT or Apache-2.0,
and no rights to them are granted by this repository. They remain the property of their
respective owners. These assets will be phased out of ArtCraft over time.
frontend/apps/artcraft/app/public/resources/sound/ — sound effects from Nintendo games (Golden
Sun, Metroid Prime, The Legend of Zelda: Ocarina of Time, Super Mario RPG).
frontend/apps/artcraft/app/public/resources/memes/ — meme images and likenesses owned by their
respective rights holders.
frontend/apps/artcraft/app/public/resources/skybox/gray/ and skybox/night/ — skybox textures
from a third-party asset pack.
frontend/apps/artcraft/app/public/resources/pose/Lumine Idle cycle.vmd — third-party MMD motion
data.
frontend/apps/artcraft/app/public/resources/characters_gif/ — renders of Mixamo animations.
+143 -44
View File
@@ -1,46 +1,145 @@
storyteller-rust
================
<h1 align="center">ArtCraft</h1>
<p align="center"><strong>The IDE for artists.</strong></p>
Our Rust monorepo, which serves both our API surface area and our downloadable client applications.
<!-- Main video: https://www.youtube.com/watch?v=kzvQMdg66Go
GitHub media issue: https://github.com/storytold/artcraft/issues/1951 -->
```
."-,.__
`. `. ,
.--' .._,'"-' `.
. .' `'
`. / ,'
` '--. ,-"'
`"` | \
-. \, |
`--Y.' ___.
\ L._, \
_., `. < <\ _
,' ' `, `. | \ ( `
../, `. ` | .\`. \ \_
,' ,.. . _.,' ||\l ) '".
, ,' \ ,'.-.`-._,' | . _._`.
,' / \ \ `' ' `--/ | \ / / ..\
.' / \ . |\__ - _ ,'` ` / / `.`.
| ' .. `-...-" | `-' / / . `.
| / |L__ | | / / `. `.
, / . . | | / / ` `
/ / ,. ,`._ `-_ | | _ ,-' / ` \
/ . \"`_/. `-_ \_,. ,' +-' `-' _, ..,-. \`.
. ' .-f ,' ` '. \__.---' _ .' ' \ \
' / `.' l .' / \.. ,_|/ `. ,'` L`
|' _.-""` `. \ _,' ` \ `.___`.'"`-. , | | | \
|| ,' `. `. ' _,...._ ` | `/ ' | ' .|
|| ,' `. ;.,.---' ,' `. `.. `-' .-' /_ .' ;_ ||
|| ' V / / ` | ` ,' ,' '. ! `. ||
||/ _,-------7 ' . | `-' l / `||
. | ,' .- ,' || | .-. `. .' ||
`' ,' `".' | | `. '. -.' `'
/ ,' | |,' \-.._,.'/'
. / . . \ .''
.`. | `. / :_,'.'
\ `...\ _ ,'-. .' /_.-' S T O R Y T E L L E R
`-.__ `, `' . _.>----''. _ __ / Turning memes into dreams
.' /"' | "' '_ One Rust Monorepo at a time
/_|.-'\ ,". '.'`__'-( \
/ ,"'"\,' `/ `-.|"
```
https://github.com/user-attachments/assets/7339bd09-3e0d-44a7-a024-af9de64e4c11
<p align="center">
<a href="https://www.youtube.com/watch?v=kzvQMdg66Go">Watch the two-minute ArtCraft demo on YouTube</a>
</p>
<p align="center">
<a href="https://discord.gg/artcraft"><img alt="Discord members online" src="https://img.shields.io/discord/1359579021108842617?style=for-the-badge&amp;label=Discord&amp;color=5865F2&amp;logo=discord&amp;logoColor=white"></a>
<a href="https://www.youtube.com/@OfficialArtCraftStudios"><img alt="YouTube" src="https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&amp;logo=youtube&amp;logoColor=white"></a>
<a href="https://x.com/get_artcraft"><img alt="X" src="https://img.shields.io/badge/X-181717?style=for-the-badge&amp;logo=x&amp;logoColor=white"></a>
<a href="https://www.linkedin.com/company/artcraft-ai"><img alt="LinkedIn" src="https://img.shields.io/badge/LinkedIn-0A66C2?style=for-the-badge"></a>
</p>
<p align="center">
<a href="https://getartcraft.com/">Download ArtCraft</a> ·
<a href="https://github.com/storytold/artcraft/releases">GitHub releases</a> ·
<a href="./_docs/dev_setup.md">Build from source</a>
</p>
ArtCraft is the IDE for interactive AI image and video creation. Compose in 2D, stage scenes in 3D, and choose the models that fit your work. Turn prompting into *crafting* with visual tools for precise, repeatable results.
## Show, Don't Tell: Advanced Crafting Features
Artists need control over composition, character identity, and camera placement. Build the scene before you generate it. Click any preview to view it at full size.
| Feature | Preview |
| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Image to Location**<br>Place virtual actors in a consistent environment and plan multiple shots in the same room. | <a href="https://github.com/user-attachments/assets/21f103e3-cc19-4882-a630-9caa1b76ae31"><img src="https://github.com/user-attachments/assets/21f103e3-cc19-4882-a630-9caa1b76ae31" width="180" alt="Image to Location demo"></a> |
| **3D Image Compositing**<br>Layer backdrops, foregrounds, and props in 3D to build compositions with depth. | <a href="https://github.com/user-attachments/assets/f93a616f-571d-474e-bcc0-53736de7303d"><img src="https://github.com/user-attachments/assets/f93a616f-571d-474e-bcc0-53736de7303d" width="180" alt="3D Image Compositing demo"></a> |
| **2D Image Compositing**<br>Compose a scene with image layers, background removal, and drawing tools. | <a href="https://github.com/user-attachments/assets/d6f99391-e496-4c62-9e37-29734ba5f899"><img src="https://github.com/user-attachments/assets/d6f99391-e496-4c62-9e37-29734ba5f899" width="180" alt="2D Image Compositing demo"></a> |
| **Image to 3D Mesh**<br>Turn an image into a 3D object you can position, rotate, and frame precisely. | <a href="https://github.com/user-attachments/assets/600a405c-e360-48c1-9b42-6e657ae6243b"><img src="https://github.com/user-attachments/assets/600a405c-e360-48c1-9b42-6e657ae6243b" width="180" alt="Image to 3D Mesh demo"></a> |
| **Character Posing**<br>Pose characters and set your camera before generating the final shot. | <a href="https://raw.githubusercontent.com/storytold/github-media/22a27373707b13f48fc56405f1f89ca143c43d5d/character-posing.webp"><img src="https://raw.githubusercontent.com/storytold/github-media/22a27373707b13f48fc56405f1f89ca143c43d5d/character-posing.webp" width="180" alt="Character Posing demo"></a> |
| **Scene Blocking with Kitbashing**<br>Combine 3D asset kits to control camera angles, object placement, and depth. | <a href="https://github.com/user-attachments/assets/eef025ac-0346-4a46-a023-d48e23629eb5"><img src="https://github.com/user-attachments/assets/eef025ac-0346-4a46-a023-d48e23629eb5" width="180" alt="Scene Blocking with Kitbashing demo"></a> |
| **Character Identity Transfer**<br>Use a posed mannequin to guide the placement and pose of your character. | <a href="https://github.com/user-attachments/assets/629119ee-8c76-4a83-9827-8c6c995a3ec1"><img src="https://github.com/user-attachments/assets/629119ee-8c76-4a83-9827-8c6c995a3ec1" width="180" alt="Character Identity Transfer demo"></a> |
| **Background Removal**<br>Isolate subjects to use as props, layers, or backdrops in 2D and 3D scenes. | <a href="https://github.com/user-attachments/assets/90c65057-5531-404f-83af-b34e66e24ec1"><img src="https://github.com/user-attachments/assets/90c65057-5531-404f-83af-b34e66e24ec1" width="180" alt="Background Removal demo"></a> |
| **Mixed Asset Crafting**<br>Combine image cutouts, worlds, and 3D meshes in one scene. | <a href="https://raw.githubusercontent.com/storytold/github-media/main/ship-editing.gif"><img src="https://raw.githubusercontent.com/storytold/github-media/main/ship-editing.gif" width="180" alt="Mixed Asset Crafting demo"></a> |
More previews are coming for scene blocking, canvas editing, and scene relighting.
## Quick and Easy Prompting
Start with a prompt when you want to explore an idea quickly, then use the canvas and scene tools to refine it.
| Feature | Preview |
| ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Text to Image**<br>Explore ideas across a wide range of image models with a text prompt. | <a href="https://github.com/user-attachments/assets/9cc289cd-faf4-4eaf-aed2-21134cce127c"><img src="https://github.com/user-attachments/assets/9cc289cd-faf4-4eaf-aed2-21134cce127c" width="180" alt="Text to Image demo"></a> |
| **Prompted Image Editing**<br>Describe changes to an image with models such as Nano Banana Pro and GPT Image. | <a href="https://github.com/user-attachments/assets/a06fa6ad-936c-42d0-8767-48fdbb8ff141"><img src="https://github.com/user-attachments/assets/a06fa6ad-936c-42d0-8767-48fdbb8ff141" width="180" alt="Prompted Image Editing demo"></a> |
| **Canvas Image Editing**<br>Use drawing, masks, and inpainting to guide exactly where an image changes. | <a href="https://github.com/user-attachments/assets/f036e08a-f3a6-417a-98ee-ec7f04b2b5ff"><img src="https://github.com/user-attachments/assets/f036e08a-f3a6-417a-98ee-ec7f04b2b5ff" width="180" alt="Canvas Image Editing demo"></a> |
| **Image to Video**<br>Animate an image with your choice of video models and generation controls. | <a href="https://github.com/user-attachments/assets/2bc6c592-511e-4fba-b40f-03c96699b7f7"><img src="https://github.com/user-attachments/assets/2bc6c592-511e-4fba-b40f-03c96699b7f7" width="180" alt="Image to Video demo"></a> |
More previews are coming for image inpainting and image ingredients.
## Models and Providers Supported within ArtCraft
| Provider | Models and workflows |
| ------------ | ----------------------------------------------------------------------------------------------- |
| **ArtCraft** | Images, video, music and sound, 3D meshes, and worlds. Browse the complete model catalog below. |
| Grok | Grok Imagine and Grok Video. |
| Midjourney | Midjourney image generation. |
| Sora | Sora 1, Sora 2, and GPT Image 1. |
| World Labs | Marble world generation with Gaussian splats. |
<details>
<summary><strong>ArtCraft: the full 62-model catalog</strong></summary>
<!-- Catalog source: artcraft-services/crates/service/web/storyteller_web/src/configs/omni_gen/
image_models.rs, video_models/video_models.rs + by_type/, audio_models.rs,
mesh_models.rs, and splat_models.rs. Include the GenerationProvider::Artcraft
mappings, not every model defined for other providers. -->
† Currently disabled. ‡ Restricted model; not currently available in the desktop app.
#### Images (16)
| Family | Models |
| ----------- | ----------------------------------------------------------------------------------- |
| Nano Banana | Nano Banana; Nano Banana 2; Nano Banana Pro |
| GPT Image | GPT Image 1.5; GPT Image 2; GPT Image 2.5 Flare; GPT Image 2.5 Sunburst |
| FLUX | FLUX.1 [dev]; FLUX.1 [schnell]; FLUX 1.1 [pro]; FLUX 1.1 [pro] ultra |
| Seedream | Seedream 4; Seedream 4.5; Seedream 5 Lite; Seedream 5.0 Pro; Seedream 5.0 Pro Ultra |
#### Video (25)
| Family | Models |
| -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Seedance | Seedance 1.0 Lite †; Seedance 1.5 Pro; Seedance 2.0 |
| Kling | Kling 1.6 Pro; Kling 2.1 Pro †; Kling 2.1 Master †; Kling 2.5 Turbo Pro; Kling 2.6 Pro; Kling 3.0 Pro †; Kling 3.0 Standard † |
| Veo | Veo 2 †; Veo 3 †; Veo 3 Fast; Veo 3.1; Veo 3.1 Fast; Veo 3.1 Lite |
| Sora | Sora 2 †; Sora 2 Pro † |
| Vidu | Vidu Q3; Vidu Q3 Turbo |
| MiniMax | MiniMax H3; MiniMax H3 Turbo ‡; MiniMax H3 Ultra ‡ |
| Flux | Flux 3; Flux 3 Draft |
#### Music and sound (5)
| Family | Models |
| ---------- | ------------------------------------------------ |
| Suno | Suno Music; Suno Remix; Suno Sounds; Suno Sample |
| Seed Audio | Seed Audio 1.0 |
#### 3D meshes (11)
| Family | Models |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Hunyuan 3D | Hunyuan 3D 2.0; Hunyuan 3D 2.1; Hunyuan 3D 3; Hunyuan 3D 3 Sketch; Hunyuan 3D 3.1 Pro; Hunyuan 3D 3.1 Rapid; Hunyuan 3D 3.1 Part; Hunyuan 3D 3.1 Smart Topology |
| Tripo3D | Tripo3D H3.1 |
| Meshy | Meshy 6 |
| Rodin | Rodin 2.5 Fast |
#### Worlds and Gaussian splats (5)
| Family | Models |
| ---------- | --------------------------------------------------------- |
| Marble | Marble 1.0; Marble 1.0 Draft; Marble 1.1; Marble 1.1 Plus |
| TripoSplat | TripoSplat |
</details>
Additional provider integrations are planned for Kling, Google, Runway, and Luma. We're also interested in supporting existing subscriptions with aggregators such as OpenArt and Freepik.
## Downloads
- [Stable releases for Windows and macOS](https://getartcraft.com/)
- [Latest builds on GitHub](https://github.com/storytold/artcraft/releases)
- [Build from source](./_docs/dev_setup.md), including Linux
## Documentation
- [Development setup](./_docs/dev_setup.md)
- [Developer documentation](./_docs)
- [Desktop build and development scripts](./script/artcraft)
- [Roadmap](./ROADMAP.md)
- [License](#license)
## License
ArtCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
Copyright (c) 2026 ArtCraft Team and the ArtCraft contributors. Required notices, including
third-party material not covered by this license, are in [NOTICE](NOTICE).
+47
View File
@@ -0,0 +1,47 @@
ArtCraft Roadmap
================
## 2026 Goals (non-exhaustive)
Product
- Make ArtCraft better and more useful than model aggregation websites (our #1 goal).
We don't dislike those websites; we just set a high standard for ourselves.
- Build the best "tangible" computing surfaces where you can mold scenes and videos like
clay and direct them with precision and clarity.
- Add every model and provider into ArtCraft so you can build your own crafting IDE.
Nothing will be left out.
- Build purpose-specific image models, eg. for relighting scenes and reposing characters
and props.
Studio
- Build our content studio. Make lots of narrative shorts and at least one
feature-length AI film by the year's end. Theatrical distribution, but also lots
of social media content. (We are creators, and we want to employ more creators!)
- Host festivals and competitions, including in-person meetups and screenings.
Architecture
- Remove dependence on ArtCraft hosted services.
- More Rust, more speed.
- Code cleanup, more/better tests, CI, ...
Community
- Build a safe community free of negativity and anti-AI stigma. A community of filmmakers
and artists.
## Loftier Goals
- Build an *Open* OpenAI / Google / Anthropic. Open models, open platforms, open hardware.
Why? Because the future could be bleak without it. How, you might ask? Get in touch if
you're curious.
- Don't Be Evil™
@@ -0,0 +1,127 @@
-- noinspection SqlDialectInspectionForFile
--
-- NB: This version needs to be bumped even if just adding or changing comments!
-- Failure to do so will cause Windows to segfault at start. (This is horrible!)
--
-- NB: `app_state_dir.rs` contains the file version number.
--
-- Migration history:
-- tasks_v1.sqlite - initial version
-- tasks_v2.sqlite - added model_type (nullable)
-- tasks_v3.sqlite - added frontend_caller, comments
-- tasks_v4.sqlite - added is_dismissed_by_user
-- tasks_v5.sqlite - added five fields:
-- on_complete_batch_token,
-- on_complete_primary_media_file_token,
-- on_complete_primary_media_file_class,
-- on_complete_primary_media_file_cdn_url,
-- on_complete_primary_media_file_thumbnail_url_template,
-- tasks_v6.sqlite - added two fields:
-- on_failure_type
-- on_failure_message
-- tasks_v7.sqlite - added prompt token + two fields (for fal):
-- prompt_token
-- queue_status_url
-- queue_response_url
CREATE TABLE tasks (
-- Task auto-incrementing primary key.
-- We don't use this.
id TEXT NOT NULL PRIMARY KEY,
-- TaskStatus enum
-- This is how the job system manages job states.
-- e.g. pending, started, complete_success, dead, etc.
task_status TEXT NOT NULL,
-- TaskType enum
-- e.g. image_generation, video_generation, etc.
task_type TEXT NOT NULL,
-- TaskModelType enum
-- e.g. flux_1_dev, veo_2, etc.
model_type TEXT,
-- GenerationProvider enum
-- Together with `provider_job_id`, this creates a foreign key lookup to the job.
-- e.g. 'artcraft', 'fal', 'sora', etc.
provider TEXT,
-- The primary key for the job in the provider's system.
provider_job_id TEXT,
-- OPTIONAL.
-- For third party jobs, we need to record the prompt details upfront.
-- We create a prompt record and store the token to associate once the task completes.
prompt_token TEXT,
-- OPTIONAL.
-- Tell the job system which caller initiated the task.
-- e.g. 'canvas'
frontend_caller TEXT,
-- OPTIONAL.
-- An arbitrary opaque identifier that frontend can set.
frontend_subscriber_id TEXT,
-- OPTIONAL.
-- An opaque JSON payload set by the frontend subscriber that
-- will be re-emitted back to the frontend.
frontend_subscriber_payload TEXT,
-- Whether the user has dismissed the task from view.
is_dismissed_by_user INTEGER NOT NULL DEFAULT 0,
-- OPTIONAL.
-- For some third party systems (eg. FAL), the URL to check the status of the
-- job in the queue.
queue_status_url TEXT,
-- OPTIONAL.
-- For some third party systems (eg. FAL), the URL to check the
-- results (when the job completes)
queue_response_url TEXT,
-- OPTIONAL.
-- When the generation completes, the batch token (if any).
-- This will point to a collection of multiple files.
on_complete_batch_token TEXT,
-- OPTIONAL.
-- When the generation completes, the media token of the "primary" media file.
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
on_complete_primary_media_file_token TEXT,
-- OPTIONAL.
-- When the generation completes, the file type of the primary media file.
-- We'll probably always generate homogenous types, but just in case this is the first or primary item in a batch.
on_complete_primary_media_file_class TEXT,
-- OPTIONAL.
-- The URL to the media file.
on_complete_primary_media_file_cdn_url TEXT,
-- OPTIONAL.
-- The URL template for the thumbnail of the primary media file.
on_complete_primary_media_file_thumbnail_url_template TEXT,
-- OPTIONAL.
-- An enum-like string describing the failure reason (even for non-terminal failures).
-- The enum is `TaskFailureType`, which is a close analogue of the backend API's `FrontendFailureCategory`.
on_failure_type TEXT,
-- OPTIONAL.
-- A human-readable message describing the failure reason (even for non-terminal failures).
on_failure_message TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
updated_at INTEGER NOT NULL DEFAULT (unixepoch('now')),
completed_at INTEGER DEFAULT NULL
);
-- Unique Indices
CREATE UNIQUE INDEX idx_tasks_on_provider_job_id ON tasks(provider_job_id);
-- Indices
CREATE INDEX idx_tasks_on_status ON tasks(task_status);
CREATE INDEX idx_tasks_on_provider ON tasks(provider);
-38
View File
@@ -1,38 +0,0 @@
AI Chatbot Sidecar
==================
This is the first app that doesn't run as a part of our SoA API architecture. (Though it is written such that we
could port it.) This app is responsible for generating all the content that goes into our streaming AI-powered TV
programs.
### Windows Installation
The application uses sqlite, which is tricky to install on Windows.
1. Put together an sqlite build directory containing the sources and dll file. One is checked in under this monorepo
in `./windows/sqlite` at the time of this writing, though that may change in the future.
2. Add environment variables for the sqlite build directory (see https://github.com/diesel-rs/diesel/issues/487)
* PATH (append the sqlite build directory to the end)
* SQLITE3_LIB_DIR (a new environment variable; set it to the sqllite build path)
3. Build the `sqlite3.lib` file with the following:
```
lib /def:sqlite3.def /out:sqlite3.lib /MACHINE:amd64
```
This must be done in "Developer Command Prompt for VS 2022" (or similar), not `cmd` or `PowerShell`.
4. Run the diesel_cli tool build
```
cargo install diesel_cli --no-default-features --features sqlite
```
If it still complains about the `sqlite3.lib` file not being found, be sure to double check that `echo %PATH%` shows
the updated PATH from the steps above.
-19
View File
@@ -1,19 +0,0 @@
database schema
================
Twitch Model
------------
- `twitch_user_id`
Though numeric, this should always be a string per
[https://discuss.dev.twitch.tv/t/bug-v5-api-returns--id-as-string-for-featured-channels/10310]
and
[https://discuss.dev.twitch.tv/t/type-of-user-id-in-api-responses/10205]
- `twitch_channel_id`
This is the same as `twitch_user_id`. They're exactly 1:1. Prefer to
use 'user_id' terminology.
- `twitch_username`
Between 4 and 25 characters.
-21
View File
@@ -1,21 +0,0 @@
development notes
=================
Actix notes
-----------
json request
```
async fn handler(request: web::Json<Mytype>) -> impl Responder { "whatever" }
```
form-multipart request
```
async fn handler(request: web::Form<Mytype>) -> impl Responder { "whatever" }
```
route parameters
```
#[get("/{name}")]
async fn hello(name: web::Path<String>) -> impl Responder { format!("Hi {}", name) }
```
-20
View File
@@ -1,20 +0,0 @@
# Local Kubernetes environment for standing up the APIs and their dependencies
- Install minikube, kubectl, docker, and musl from your OS package manager
- `rustup target add x86_64-unknown-linux-musl`
- `cargo build --release --bin storyteller-web --target=x86_64-unknown-linux-musl`
- `minikube start --extra-config=apiserver.service-node-port-range=1-65535`
- `eval $(minikube docker-env)`
- `minikube mount /path/to/storyteller-rust/:/storyteller-rust &> /dev/null &`
- `docker build -t storyteller-web -f localdev/Dockerfile .`
- `kubectl apply -f localdev/kubernetes.yml`
- Get the ip address of minikube with `minikube ip`, this is where all the services will be running on their
regular ports (12345 for the api, 6379 for redis, 3306 for mysql)
- That's it, we just spun up mysql, applied database migrations, spun up redis, and spun up our api
# Development tips
- Rebuild and "deploy" (locally) with `cargo build --release --bin storyteller-web --target=x86_64-unknown-linux-musl && kubectl rollout restart storyteller-web`
- View logs from the api with `kubectl logs storyteller-web<TAB>`
- Delete everything minikube (if you start running out of disk space or what-have-you) with `minikube delete`
- See pod status with `kubectl get pods`
- If there are errors you can get more info with `kubectl describe pods`
-248
View File
@@ -1,248 +0,0 @@
dev setup
=========
[Back to main README](../../README.md)
## Debugging and Fixing CUDA/PyTorch
See this spreadsheet for previous battles with version incompatibility:
https://docs.google.com/spreadsheets/d/1BEdLmwOzo3r83-iJn9sj6co1VT92t3lIeJ-lNH25bdQ/edit#gid=0
Ubuntu might upgrade the driver by accident, and it might need reinstallation. Who knows.
This stuff is a nightmare.
## Database Setup
### Mac Install
```
brew install mysql
# If mysql -uroot fails, reboot the machine:
sudo reboot now
```
### Linux Install
Install the following libraries, and see the notes further below about MySQL on Ubuntu 20.04.
```
jq # for combining mysql codegen outputs
libmysqlclient-dev
mysql-server
libsqlite3-dev
pkgconf # if using openssl instead of rustls
```
### Migrations
To manage the database and perform migrations, install the Rust tools diesel and sqlx.
We'll be using diesel to manage the migrations, but sqlx within the app to actually perform queries.
Diesel is an ORM, which is dumb, so we use sqlx as at-compile-time typesafe SQL.
The CLI is necessary to perform codegen:
```
sudo apt-get install libmysqlclient-dev
cargo install sqlx-cli --no-default-features --features rustls,mysql,sqlite
```
Diesel now supports configuring the migration directory via an environment variable,
so we can install the currently published version:
```
cargo install diesel_cli \
--no-default-features \
--features mysql,sqlite
```
If the modern version fails, try installing a cherry-picked version with a
TOML-based migration directory configuration:
```
cargo install diesel_cli \
--git https://github.com/diesel-rs/diesel.git \
--rev a213fe232a122f35a812b0ce0269708a1845a4c9 \
--no-default-features \
--features mysql
```
#### Sqlx Error on Linux when Performing Schema Migrations
You might get this error message during migration,
```
Encountered unknown type for Mysql: enum
thread 'main' panicked at 'internal error: entered unreachable code: Mysql only supports a closed set of types.
If you ever see this error message please open an issue at https://github.com/diesel-rs/diesel containing a dump of your schema definition.', diesel_cli/src/print_schema.rs:310:17
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
I haven't found the cause (it doesn't happen on Mac), but the migrations appear to work regardless of the error message.
#### Linux database notes
If MySql in local dev can't be connected to, reset the accounts:
https://linuxconfig.org/how-to-reset-root-mysql-mariadb-password-on-ubuntu-20-04-focal-fossa-linux
#### MySql on Ubuntu 22.04
Should work largely out of the box. We'll need a dev account:
```
# Connect to mysql:
# sudo mysql -u root -p (password is "root")
use mysql;
CREATE USER 'storyteller'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';
```
Then verify with `./dev_mysql_connect.sh`
#### Fixing dev MySql on Ubuntu 20.04
For some reason, the MySql default install on 20.04 gave me a bunch of trouble.
In retrospect, I _think_ this is because 'root@localhost' requires sudo to access, but if this
gives any trouble in the future, here's how I got around it (two full hours of distraction!)
```
sudo apt-get install mysql-server
# But for some reason the default password doesn't work and diesel/sqlx can't connect?
# Kill everything
sudo apt purge mysql-server mysql-client mysql-common
sudo apt autoremove
sudo mv -iv /var/lib/mysql /var/tmp/mysql-backup
sudo rm -rf /var/lib/mysql*
sudo /usr/bin/mysql_secure_installation
# still no work...
sudo systemctl stop mysql.service
# ugh, it wasn't chowned or created (both states observed in different installs)…
sudo mkdir -p /var/run/mysqld
sudo chown mysql:mysql /var/run/mysqld
sudo mysqld_safe --skip-grant-tables --skip-networking &
mysql -u root
flush privileges;
USE mysql;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
sudo killall -u mysql
sudo systemctl restart mysql.service
# now it works
sudo mysql -u root -p
# but now I have to use "sudo" !?!?
use mysql;
CREATE USER 'storyteller'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';
```
### Install Diesel (migrations only)
We use Diesel to manage migrations, but we don't use it in server code.
Actual server code uses SQLx. To install the CLI tool, run the following:
```
cargo install diesel_cli
```
Mac [has issues](https://github.com/diesel-rs/diesel/issues/2605) and requires a few dependencies:
```
brew install libpq
```
### Database migrations
To reset the entire database (drop, migrate), run:
```
diesel database reset
```
To migrate at the current step and beyond:
```
diesel migration run
```
To undo migrations at the current step:
```
diesel migration redo
```
### Server Query Codegen
We use SQLx instead of Diesel in the production server. It's typesafe
SQL instead of an ORM like Diesel.
SQLx connects to a database to derive type information, but obviously
cannot do this for builds in CI. In order to cache the types, we build
and check in a cache file (necessary for builds):
```
SQLX_OFFLINE=true cargo sqlx prepare
```
Now that we have multiple binaries, it's required to include all the queries in the main
binary so we can generate the cached queries as a single target. That's then executed
with:
```
SQLX_OFFLINE=true cargo sqlx prepare -- --bin storyteller-web
```
### Setting Up a Local Nginx Proxy
Set up a local nginx to proxy to the frontend and backend so cookie issues aren't annoying
Configure Nginx per the checked in Nginx configs (and instructions) in `localdev/nginx-http-config`.
```
And in /etc/hosts,
```
127.0.0.1 jungle.horse
127.0.0.1 api.jungle.horse
```
### Python 3.6 on Apple M1 Mac
Python3.6 isn't supported on Apple silicon, and it's not in homebrew. It can be installed with
[nix using Rosetta](https://stackoverflow.com/a/65980989):
Download: https://nixos.org/download.html#nix-quick-install
```
nix run nixpkgs.python36 -c python
```
Install venv:
```
nix run nixpkgs.python36 -c python -m venv python
```
Install other packages on Mac that aren't used in venv:
```
python3 -m pip install --user requests gdown youtube_dl
```
-77
View File
@@ -1,77 +0,0 @@
old readme
==========
Contents of the main README.md, which will always be woefully out of date. Docs will migrate into this folder on a
per-subject basis. App-specific docs will live under each app.
Application overview
--------------------
* [FakeYou.com](https://fakeyou.com) is powered by:
* HTTP APIs:
* `storyteller-web` - HTTP + database monolith for FakeYou
* Asynchronous Jobs:
* `tts-download-job` - Async download of models
* `tts-inference-job` - Async TTS inference
* `w2l-download-job` - Async download of videos and images
* `w2l-inference-job` - Async W2L inference
* [StorytellerStream](https://storyteller.stream) is powered by:
* HTTP APIs:
* `storyteller-web` - HTTP + database monolith for FakeYou
* Websocket events:
* `websocket-gateway` - Websocket to pull down Twitch events.
* Twitch API integration:
* `twitch-pubsub-subscriber` - Dynamically subscribes to Twitch PubSub for "active" users
Schema and API notes
--------------------
* [Redis Schema Notes README](_docs/redis_schema.md)
Local development
-----------------
[Local development setup README](_docs/local_development_setup.md)
Production and Deployment
-------------------------
[Production builds and deployment README](_docs/production_builds_and_deployment.md)
Development notes
-----------------
[Development notes README](_docs/development_notes.md)
Dump a MySQL report
-------------------
```
mysql -u storyteller \
-pPasswordHere \
-h IP_HERE \
-P 3306 \
-D storyteller \
-B \
-e "select * from voice_clone_requests;" | sed "s/'/\'/;s/\t/\",\"/g;s/^/\"/;s/$/\"/;s/\n//g" > voice_clone_report.csv
```
TODO
----
* Revise this README to be more useful.
Notes / TODOs:
* Examples for good Actix+Sqlx Tests:
https://stackoverflow.com/questions/65370752/how-do-i-create-an-actix-web-server-that-accepts-both-sqlx-database-pools-and-tr
* Actix/sqlx runtime compat:
https://github.com/launchbadge/sqlx/issues/1117#issuecomment-801237734
* Redis caching of more endpoints
* Jobs for analytics queries (#uses per model, etc)
* Jobs for ingesting Discord, Twitter, Reddit RPAN chat, Twitch chat (not PubSub), etc.
@@ -1,36 +0,0 @@
production builds and deployment
================================
### Github Docker builds
The repository needs to be given read access to the base docker image:
https://github.com/orgs/storytold/packages/container/docker-base-images-rust-ssl/settings
These instructions assume running on GCP.
### Database migrations
1. Set `DATABASE_URL` in `.env` to the production secrets (DO NOT COMMIT!)
2. Run `diesel migration run`
### Setting up public buckets without list permission
Public buckets that deny the `list` action should use the following Role:
`roles/storage.legacyObjectReader`
See:
* https://stackoverflow.com/a/56354633
* https://cloud.google.com/storage/docs/access-control/making-data-public#buckets
### Generating Bucket Access Key and Secret Key
https://cloud.telestream.net/tutorials/how-to-setting-up-google-cloud-storage/
1. Go to the GCS page
2. Click "settings"
3. Click "interoperability" tab
4. (enable interoperable access if not already set)
5. Click "create new key"
-27
View File
@@ -1,27 +0,0 @@
redis schema
============
Database 0: TTS / RateLimiter
-----------------------------
TODO: Document rate limiter
Public statistics on usage displayed on the website:
* `ttsUseCount:{model_token}`: INCR, GET
* `w2lUseCount:{template_token}`: INCR, GET
Temporary job progress reports to the user:
These are written as a single string, such as "downloading model", "uploading model",
"generating preview", "executing", etc.
* `ttsDownloadExtraStatus:{inference_job_token}`: SETEX, GET
* `ttsInferenceExtraStatus:{inference_job_token}`: SETEX, GET
* `w2lDownloadExtraStatus:{inference_job_token}`: SETEX, GET
* `w2lInferenceExtraStatus:{inference_job_token}`: SETEX, GET
Database 1: Obs / Twitch
------------------------
* WIP
-23
View File
@@ -1,23 +0,0 @@
FROM rust:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y \
mariadb-client \
imagemagick \
libmariadb-dev \
&& rm -rf /var/lib/apt/lists/*
RUN cargo install diesel_cli --features mysql && \
cargo install sqlx-cli --features mysql
WORKDIR /storyteller-rust
ARG GIT_SHA
RUN echo -n ${GIT_SHA} > /GIT_SHA
CMD ["/storyteller-rust/localdev/startup.sh"]
-153
View File
@@ -1,153 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0.30
env:
- name: MYSQL_USER
value: storyteller
- name: MYSQL_PASSWORD
value: password
- name: MYSQL_DATABASE
value: storyteller
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7.0.4
ports:
- containerPort: 6379
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: storyteller-web
spec:
replicas: 1
selector:
matchLabels:
app: storyteller-web
template:
metadata:
labels:
app: storyteller-web
spec:
containers:
- name: storyteller-web
image: storyteller-web:latest
imagePullPolicy: Never
env:
- name: MYSQL_URL
value: mysql://storyteller:password@mysql.default/storyteller
- name: REDIS_0_URL
value: redis://redisd@redis.default/0
- name: REDIS_1_URL
value: redis://redisd@redis.default/1
- name: ACCESS_KEY
value: garbage
- name: SECRET_KEY
value: garbage
- name: REGION_NAME
value: garbage
- name: W2L_PRIVATE_DOWNLOAD_BUCKET_NAME
value: garbage
- name: W2L_PUBLIC_DOWNLOAD_BUCKET_NAME
value: garbage
- name: AUDIO_UPLOADS_BUCKET_ROOT
value: garbage
- name: TWITCH_USERNAME
value: garbage
- name: TWITCH_STREAM_KEY
value: garbage
- name: TWITCH_APP_CLIENT_ID
value: garbage
- name: TWITCH_APP_CLIENT_SECRET
value: garbage
ports:
- containerPort: 12345
volumeMounts:
- mountPath: /storyteller-rust
name: storyteller-rust-folder
volumes:
- name: storyteller-rust-folder
hostPath:
path: /storyteller-rust
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
type: NodePort
ports:
- name: mysql
port: 3306
nodePort: 3306
selector:
app: mysql
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
type: NodePort
ports:
- name: redis
port: 6379
nodePort: 6379
selector:
app: redis
---
apiVersion: v1
kind: Service
metadata:
name: storyteller-web
spec:
type: NodePort
ports:
- name: storyteller-web
port: 12345
nodePort: 12345
selector:
app: storyteller-web
@@ -1,57 +0,0 @@
# Nginx configs for "jungle.horse" and "api.jungle.horse" (http)
#
# On Ubuntu Linux, this config is located at the following location:
#
# - /etc/nginx/sites-enabled/{name-of-site}
#
# On Mac, this is located at the following locations:
#
# - /opt/homebrew/etc/nginx/servers/{name-of-site} (for nginx installed with homebrew)
# - /usr/local/etc/nginx/sites-enabled/{name-of-site} (for default nginx install)
#
# API backend over plain HTTP
server {
listen 80;
server_name api.jungle.horse;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# TODO(2022-02-13): This is new and untested
# WebSocket API backend over plain HTTP
server {
listen 80;
server_name ws.jungle.horse;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:54321;
proxy_redirect off;
}
}
# Javascript frontend over plain HTTP
server {
listen 80;
server_name jungle.horse;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# TODO(2022-02-13): This is new and untested
# Javascript frontend #2 (OBS Layer) over plain HTTP
server {
listen 80;
server_name obs.jungle.horse;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7001;
proxy_redirect off;
}
}
@@ -1,258 +0,0 @@
# Nginx configs for "jungle.horse" and "api.jungle.horse" (http)
#
# On Ubuntu Linux, this config is located at the following location:
#
# - /etc/nginx/sites-enabled/{name-of-site}
#
# Move this file there, and then restart with,
#
# sudo service nginx restart
#
# On Mac, this is located at the following locations:
#
# - /opt/homebrew/etc/nginx/servers/{name-of-site} (for nginx installed with homebrew)
# - /usr/local/etc/nginx/sites-enabled/{name-of-site} (for default nginx install)
#
#
# On Mac, verify configs and restart with,
#
# sudo nginx -t
# sudo nginx -s reload
#
# ================
# SSL Instructions
# ================
#
# You can launch Chrome in a way it disregards self-signed certs and CORS:
#
# chromium --disable-web-security --ignore-certificate-errors --user-data-dir=~/chromeTemp
#
# To create self-signed certs on Ubuntu Linux,
#
# sudo apt-get install openssl
#
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/localhost.key -out /etc/ssl/certs/localhost.crt
#
# ^ most fields can be blank, but the "Common Name" should be `*.jungle.horse`
#
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/devfakeyou.key -out /etc/ssl/certs/devfakeyou.crt
# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/devstoryteller.key -out /etc/ssl/certs/devstoryteller.crt
#
# ^ most fields can be blank, but the "Common Name" should be `*.dev.fakeyou.com` and `*.dev.storyteller.io` respectively.
#
# For more: https://www.cloudsavvyit.com/1306/how-to-create-and-use-self-signed-ssl-on-nginx/
#
# To create self-signed certs on Mac,
#
# openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout localhost.key -out localhost.crt
#
# ^ most fields can be blank, but the "Common Name" should be `*.jungle.horse`
#
# On Mac, load the self signed cert into the keychain with the following command:
#
# sudo security add-trusted-cert \
# -d -r trustRoot \
# -k /Library/Keychains/System.keychain /etc/ssl/certs/localhost.crt
#
# Self signed certs might be considered insecure by the browser. In Firefox,
# set about:config `security.tls.insecure_fallback_hosts` to include a list of
# permitted domains. You may also need to configure other settings.
#
#
# ========================================================
# FAKEYOU
# ========================================================
# FakeYou javascript frontend over plain HTTP (launched by Yarn)
server {
listen 80;
server_name dev.fakeyou.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# FakeYou API backend over plain HTTP (Rust app)
server {
listen 80;
server_name api.dev.fakeyou.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# FakeYou javascript frontend over SSL (launched by Yarn)
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/devfakeyou.crt;
ssl_certificate_key /etc/ssl/private/devfakeyou.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
server_name dev.fakeyou.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# FakeYou API backend over SSL (Rust app)
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/devfakeyou.crt;
ssl_certificate_key /etc/ssl/private/devfakeyou.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
server_name api.dev.fakeyou.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# ========================================================
# STORYTELLER
# ========================================================
# Storyteller javascript frontend over plain HTTP (launched by Yarn)
server {
listen 80;
server_name dev.storyteller.io;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# Storyteller API backend over plain HTTP
server {
listen 80;
server_name api.dev.storyteller.io;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# Storyteller javascript frontend over SSL (launched by Yarn)
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/devstoryteller.crt;
ssl_certificate_key /etc/ssl/private/devstoryteller.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
server_name dev.storyteller.io;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:7000;
proxy_redirect off;
}
}
# Storyteller API backend over SSL
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/devstoryteller.crt;
ssl_certificate_key /etc/ssl/private/devstoryteller.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
server_name api.dev.storyteller.io;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:12345;
proxy_redirect off;
}
}
# ========================================================
# OLD PRODUCTS THAT NEED UPDATING
# ========================================================
# ## TODO(2022-02-13): This is new and untested
# # WebSocket API backend over plain HTTP
# server {
# listen 80;
# server_name ws.jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:54321;
# proxy_redirect off;
# }
# }
#
# # Websocket API backend over SSL
# server {
# listen 443 ssl;
# listen [::]:443 ssl;
# ssl_certificate /etc/ssl/certs/localhost.crt;
# ssl_certificate_key /etc/ssl/private/localhost.key;
# ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
# server_name ws.jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:54321;
# proxy_redirect off;
# }
# }
#
# # Javascript frontend over plain HTTP
# server {
# listen 80;
# server_name jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:7000;
# proxy_redirect off;
# }
# }
#
# # TODO(2022-02-13): This is new and untested
# # Javascript frontend (OBS Layer) over plain HTTP
# server {
# listen 80;
# server_name obs.jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:7007;
# proxy_redirect off;
# }
# }
#
# # Javascript frontend over SSL
# server {
# listen 443 ssl;
# listen [::]:443 ssl;
# ssl_certificate /etc/ssl/certs/localhost.crt;
# ssl_certificate_key /etc/ssl/private/localhost.key;
# ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
# server_name jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:7000;
# proxy_redirect off;
# }
# }
#
# # Javascript frontend over SSL (OBS Layer)
# server {
# listen 443 ssl;
# listen [::]:443 ssl;
# ssl_certificate /etc/ssl/certs/localhost.crt;
# ssl_certificate_key /etc/ssl/private/localhost.key;
# ssl_protocols TLSv1.2 TLSv1.1 TLSv1;
# server_name obs.jungle.horse;
# location / {
# proxy_set_header Host $host;
# proxy_pass http://127.0.0.1:7007;
# proxy_redirect off;
# }
# }
#
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
set +e +o pipefail
diesel migration run --database-url="${MYSQL_URL}"
set -e -o pipefail
./target/x86_64-unknown-linux-musl/release/storyteller-web
-36
View File
@@ -1,36 +0,0 @@
#!/bin/bash
echo 'Installing APT packages...'
# OpenSSL requirements: libssl-dev, pkg-config
# Database requirements: mysql-server, libmysqlclient-dev
# Other tools: jq (db codegen)
sudo apt-get install -y \
jq \
libmysqlclient-dev \
libssl-dev \
mysql-server \
pkg-config \
--no-install-recommends
echo 'Installing Cargo packages...'
# Install Diesel CLI for migrations
cargo install diesel_cli \
--no-default-features \
--features mysql
# Install SQLx for codegen
cargo install sqlx-cli \
--no-default-features \
--features rustls,mysql
echo 'Installing Database user...'
# Create DB account (using default system mysql password)
sudo mysql \
-u "root" \
-p"password" \
-e "use mysql; CREATE DATABASE IF NOT EXISTS storyteller; CREATE USER IF NOT EXISTS 'storyteller'@'localhost' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON storyteller.* TO 'storyteller'@'localhost';"
-61
View File
@@ -1,61 +0,0 @@
#!/usr/bin/env python3
import json
import re
import requests
import uuid
login_url = 'http://localhost:12345/login'
create_api_token_url = 'http://localhost:12345/api_tokens/create'
edit_api_token_url = 'http://localhost:12345/api_tokens/edit'
delete_api_token_url = 'http://localhost:12345/api_tokens/{}/delete'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
}
r = requests.post(create_api_token_url, cookies=cookies, json=payload)
print("===== Created API Token =====")
print('Status: {}'.format(r.status_code))
print(r.content)
api_token = json.loads(r.content)
payload = {
'api_token': api_token['api_token'],
'maybe_short_description': 'this describes my token',
}
r = requests.post(edit_api_token_url, cookies=cookies, json=payload)
print("===== Edited API Token =====")
print('Status: {}'.format(r.status_code))
print(r.content)
url = delete_api_token_url.format(api_token['api_token'])
r = requests.post(url, cookies=cookies)
print("===== Deleted API Token =====")
print('Status: {}'.format(r.status_code))
print(r.content)
-150
View File
@@ -1,150 +0,0 @@
#!/usr/bin/env python3
import json
import re
import requests
import uuid
login_url = 'http://localhost:12345/login'
category_create_url = 'http://localhost:12345/category/create'
category_delete_url_format = 'http://localhost:12345/category/{}/delete'
category_edit_url_format = 'http://localhost:12345/category/{}/edit'
tts_model_list_url = 'http://localhost:12345/tts/list'
tts_category_assignment_url = 'http://localhost:12345/category/assign/tts'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== [1] Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== [2] Create Category =====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'name': 'First Category',
'model_type': 'tts', # tts or w2l
'can_directly_have_models': False,
'can_have_subcategories': True,
'is_mod_approved': True,
}
r = requests.post(category_create_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
d = json.loads(r.content)
category_token = d['token']
print(f'created category token: #{category_token}')
print("===== [3] (Soft) Delete Category =====")
category_delete_url = category_delete_url_format.format(category_token)
cookies = { 'session': session_cookie }
payload = {
'set_delete': True,
}
r = requests.post(category_delete_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
print("===== [4] Undelete Category =====")
cookies = { 'session': session_cookie }
payload = {
'set_delete': False,
}
r = requests.post(category_delete_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
print("===== [5] Edit Category =====")
category_edit_url = category_edit_url_format.format(category_token)
cookies = { 'session': session_cookie }
payload = {
'name': 'Edited Name',
'dropdown_name': 'Edited Dropdown Name',
'can_directly_have_models': True,
'can_have_subcategories': True,
'can_only_mods_apply': False,
'is_mod_approved': True,
'maybe_mod_comments': 'mod comment',
}
r = requests.post(category_edit_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
print("===== [6] List TTS Models =====")
r = requests.get(tts_model_list_url, cookies=cookies)
print('Status: {}'.format(r.status_code))
d = json.loads(r.content)
tts_model_token = d['models'][0]['model_token']
print('TTS model token: {}'.format(tts_model_token))
print("===== [7] Create Category Assignment to TTS Model =====")
payload = {
'category_token': category_token,
'tts_model_token': tts_model_token,
'assign': True,
}
r = requests.post(tts_category_assignment_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
print("===== [8] Delete Category Assignment to TTS Model =====")
payload = {
'category_token': category_token,
'tts_model_token': tts_model_token,
'assign': False,
}
r = requests.post(tts_category_assignment_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
@@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username": "echelon", "password": "testing", "password_confirmation": "testing", "email_address": "echelon@gmail.com"}' \
http://localhost:12345/create_account
@@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username": "ernest", "password": "knowwhatimean", "password_confirmation": "knowwhatimean", "email_address": "vern@knowwhatimean.com"}' \
http://localhost:12345/create_account
@@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username": "vocodes", "password": "testing", "password_confirmation": "testing", "email_address": "vocodes@gmail.com"}' \
http://localhost:12345/create_account
-43
View File
@@ -1,43 +0,0 @@
#!/usr/bin/env python3
import requests
import re
login_url = 'http://localhost:12345/login'
inference_url = 'http://localhost:12345/tts/inference'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Infer TTS =====")
cookies = { 'session': session_cookie }
payload = {
'tts_model_token': 'TTS_TOKEN',
'inference_text': 'this is the text to generate',
}
r = requests.post(inference_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
-43
View File
@@ -1,43 +0,0 @@
#!/usr/bin/env python3
import requests
import re
login_url = 'http://localhost:12345/login'
inference_url = 'http://localhost:12345/w2l/inference'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Infer W2L =====")
cookies = { 'session': session_cookie }
payload = {
'w2l_template_token': 'W2L_TOKEN',
'tts_inference_result_token': 'TTS_TOKEN',
}
r = requests.post(inference_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
@@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username_or_email": "echelon", "password": "BAD_PASSWORD"}' \
http://localhost:12345/login
@@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username_or_email": "nobody", "password": "testing"}' \
http://localhost:12345/login
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username_or_email": "echelon@gmail.com", "password": "testing"}' \
http://localhost:12345/login
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# LOCALHOST ONLY
curl -vvv \
-H "content-type: application/json" \
--data '{"username_or_email": "echelon", "password": "testing"}' \
http://localhost:12345/login
-38
View File
@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import requests
import re
login_url = 'http://localhost:12345/login'
logout_url = 'http://localhost:12345/logout'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
cookies = { 'session': session_cookie }
r = requests.post(logout_url, cookies=cookies)
print("===== Logout =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
-123
View File
@@ -1,123 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
import socket
# There are some things here that will cause my laptop to freeze!
hostname = socket.gethostname()
def print_response(response):
print('\tStatus: {}'.format(response.status_code))
for k, v in response.headers.items():
print('\t header - {}: {}'.format(k, v))
print('\tbody: {}', response.json())
# ==================== CREATE ACCOUNTS ====================
def create_user_account(username, password, email):
print('Create account: {}'.format(username))
signup_url = 'http://localhost:12345/create_account'
payload = {
'username': username,
'password': password,
'password_confirmation': password,
'email_address': email,
}
r = requests.post(signup_url, json=payload)
print_response(r)
create_user_account('echelon', 'testing', 'echelon@gmail.com')
create_user_account('brandon', 'testing', 'brandon@storyteller.company')
create_user_account('nanashi', 'nanashi', 'somebody@somewhere.com')
create_user_account('other', 'testing', 'ryu@storyteller.company')
create_user_account('raptor9', 'testing', 'raptor9@storyteller.company')
create_user_account('ryu', 'testing', 'ryu@storyteller.company')
# ==================== LOGIN ====================
def login(username, password):
print('Login: {}'.format(username))
login_url = 'http://localhost:12345/login'
payload = {
'username_or_email': username,
'password': password,
}
r = requests.post(login_url, json=payload)
print_response(r)
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
return session_cookie
session_echelon = login('echelon', 'testing')
session_nanashi = login('nanashi', 'nanashi')
session_brandon = login('brandon', 'testing')
session_other = login('other', 'testing')
# ==================== UPLOAD W2L TEMPLATES ====================
def upload_w2l_template(content_url, title, session_cookie):
print('Upload W2L template')
upload_url = 'http://localhost:12345/w2l/upload'
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': title,
'download_url': content_url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print_response(r)
upload_w2l_template('https://drive.google.com/file/d/1yoBSb6nwFUZVI4CD-nM2BSofGdBjC8Nn/view?usp=sharing', 'Mr. Musk', session_nanashi)
upload_w2l_template('https://drive.google.com/file/d/1ysBAdRjlSmfWQPFjGlMJpv_zcGmVw7RC/view?usp=sharing', 'short bill clinton clip', session_brandon)
upload_w2l_template('https://i.imgur.com/NneE7Eq.jpg', 'itsa me mario!', session_echelon)
upload_w2l_template('https://i.imgur.com/W1EYiPU.jpg', 'Creepy Sonic does a gasp', session_echelon)
upload_w2l_template('https://i.imgur.com/8U3IdUa.png', 'Creepy Sonic actually talks', session_echelon)
upload_w2l_template('https://i.imgur.com/lKaQ4Er.jpg', 'hasta la vista', session_brandon)
upload_w2l_template('https://i.imgur.com/uoGuTJo.jpeg', 'what do you mean vern?', session_brandon)
upload_w2l_template('https://i.imgur.com/v8k9yau.jpeg', 'Miyamoto-san', session_brandon)
if hostname == 'halide':
upload_w2l_template('https://www.youtube.com/watch?v=a7mS9ZdU6k4', 'Steve Jobs talks', session_echelon)
upload_w2l_template('https://www.youtube.com/watch?v=6fg8SxgEUYI', 'Paul Graham lectures', session_echelon)
# ==================== UPLOAD TTS MODELS ====================
def upload_tts_model(content_url, title, session_cookie):
print('Upload TTS model')
upload_url = 'http://localhost:12345/tts/upload'
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': title,
'download_url': content_url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print_response(r)
# From others' Google Drives
#upload_tts_model('https://drive.google.com/file/d/1grwDGbAsPwCMVfye071TZ6PLUub1KGyR/view?usp=sharing', 'Jorgen', session_nanashi)
#upload_tts_model('https://drive.google.com/file/d/1w3oBk5vzyHurqYwP95UEQv-_NeMNu6pt/view?usp=sharing', 'Noire', session_echelon)
# Copied into Storyteller Google Drive
upload_tts_model('https://drive.google.com/file/d/1d8KPwYqbRsGiYx1c4TCW4xCtcRLdIU1s/view?usp=sharing', 'Zephyr', session_echelon)
upload_tts_model('https://drive.google.com/file/d/1yhoamPz1NOMy403gu2v8sIx5hPj3LQWs/view?usp=sharing', 'Wakko Warner', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1I6KGQZFMMB2ZvCzWEqy9xTgGl0L5cmfP/view?usp=sharing', 'Turret-192', session_echelon)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1T_hvgGcNOfofhNmDWUqpVtYDdSXQrWdE/view?usp=sharing', 'technoblade-324', session_brandon)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1NKyRiex3K1J0QyIv2np17D_H0rE98HyK/view?usp=sharing', 'Noire', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1VDHDOpsUR8C-MD07vEsnUf1Lpcf8hXg4/view?usp=sharing', 'MatPat80', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1atFn4vm7mjkufXNK9TZS6J8WzISrAn3-/view?usp=sharing', 'King Julien-70', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1-rlJpi2jAsagn-UCStcCc_ITNycXgn2F/view?usp=sharing', 'Jorgen', session_other)
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1togTNOEKnaarqWWrbZLSwIbZIZ8IT_qw/view?usp=sharing', 'edsheeransinging', session_nanashi)
# TEMP COMMENT DUE TO RATE LIMIT
# TEMP COMMENT DUE TO RATE LIMIT upload_tts_model('https://drive.google.com/file/d/1OAiu0cZDyUJMemyLc0Btqw7zEQgiN81D/view?usp=sharing', 'Wakko Warner v2 ST21', session_nanashi)
upload_tts_model('https://drive.google.com/file/d/1vRVI2gQ5tAs8SLQPrR_M1J77L05gGBOp/view?usp=sharing', 'Testing voice', session_nanashi)
#upload_tts_model('', '', session_other)
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/env python3
import requests
import re
login_url = 'http://localhost:12345/login'
session_info_url = 'http://localhost:12345/session'
r = requests.get(session_info_url)
print("===== Session Info (logged out) =====")
print(r.content)
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
cookies = { 'session': session_cookie }
r = requests.get(session_info_url, cookies=cookies)
print("===== Session Info (logged in) =====")
print(r.content)
@@ -1,306 +0,0 @@
#!/usr/bin/env python3
import json
import re
import requests
import uuid
import pprint
login_url = 'http://localhost:12345/login'
create_url = 'http://localhost:12345/twitch/event_rule/create'
list_url = 'http://localhost:12345/twitch/event_rule/list'
update_url = 'http://localhost:12345/twitch/event_rule/{}/update'
delete_url = 'http://localhost:12345/twitch/event_rule/{}/delete'
def delete_rule(token, cookies):
url = delete_url.format(token)
r = requests.delete(url, cookies=cookies)
print("===== Deleted Event Rule Output =====")
print('Status: {}'.format(r.status_code))
print(r.content)
def create_rule(payload, cookies):
r = requests.post(create_url, cookies=cookies, json=payload)
print("===== Created Event Rule Output =====")
print('Status: {}'.format(r.status_code))
print(r.content)
# ========== Setup / Login ==========
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
cookies = { 'session': session_cookie }
# ========== Delete All Existing ==========
r = requests.get(list_url, cookies=cookies)
print("===== List Event Rule Output =====")
print('Status: {}'.format(r.status_code))
response = json.loads(r.content)
tokens = [rule['token'] for rule in response['twitch_event_rules']]
for token in tokens:
delete_rule(token, cookies)
# ========== Create (1) ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_spend_threshold': {
'minimum_bits_spent': 1,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
r = requests.post(create_url, cookies=cookies, json=payload)
print("===== Created Event Rule Output =====")
print('Status: {}'.format(r.status_code))
print(r.content)
response = json.loads(r.content)
token = response['twitch_event_rule_token']
# ========== Update (1) ==========
payload = {
'event_match_predicate': {
'bits_spend_threshold': {
'minimum_bits_spent': 12345,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 123,
'rule_is_disabled': True,
}
url = update_url.format(token)
r = requests.post(url, cookies=cookies, json=payload)
print("===== Edited Event Rule Output =====")
print('Status: {}'.format(r.status_code))
print(r.content)
# ========== Create: Bits Spend Threshold ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_spend_threshold': {
'minimum_bits_spent': 1000,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': True,
}
create_rule(payload, cookies)
# ========== Create: Cheermote Exact Match ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_name_exact_match': {
'cheermote_name': 'Cheer1',
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_name_exact_match': {
'cheermote_name': 'Corgo5000',
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_name_exact_match': {
'cheermote_name': 'ZomboCom5000',
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
# ========== Create: Cheermote Prefix And Spend Threshold ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_prefix_spend_threshold': {
'cheermote_prefix': 'DoodleCheer',
'minimum_bits_spent': 1000,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_prefix_spend_threshold': {
'cheermote_prefix': 'Corgo',
'minimum_bits_spent': 12345,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'bits',
'event_match_predicate': {
'bits_cheermote_prefix_spend_threshold': {
'cheermote_prefix': 'ZomboCom',
'minimum_bits_spent': 1000,
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
# ========== Create: Channel Points ==========
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'channel_points',
'event_match_predicate': {
'channel_points_reward_name_exact_match': {
'reward_name': 'My Reward',
},
},
'event_response': {
'tts_single_voice': {
'tts_model_token': 'TM:40m3aqtt41y0', # "Wakko" voice (dev)
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
payload = {
'idempotency_token': str(uuid.uuid4()),
'event_category': 'channel_points',
'event_match_predicate': {
'channel_points_reward_name_exact_match': {
'reward_name': 'My Reward',
},
},
'event_response': {
'tts_random_voice': {
'tts_model_tokens': [
'TM:40m3aqtt41y0', # "Wakko" voice (dev)
'TM:4c1hycjj3a3t', # "Zephyr" voice (dev)
],
}
},
'user_specified_rule_order': 0,
'rule_is_disabled': False,
}
create_rule(payload, cookies)
# ========== List ==========
r = requests.get(list_url, cookies=cookies)
print("===== List Event Rule Output =====")
print('Status: {}'.format(r.status_code))
response = json.loads(r.content)
pprint.pprint(response)
-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/tts/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload TTS=====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new text to speech model',
'download_url': 'https://website.com',
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
@@ -1,45 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/tts/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload TTS=====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new text to speech model',
'download_url': 'https://drive.google.com/file/d/15-tkgblTZpa0ifmvvyr_kgWy2cguTpAe/view?usp=sharing',
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env python3
import re
import requests
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/w2l/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload W2L =====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new text to W2L template',
'download_url': 'https://video.com',
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
@@ -1,52 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/w2l/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload W2L =====")
cookies = { 'session': session_cookie }
# mario.zip
#download_url = 'https://drive.google.com/file/d/15-tkgblTZpa0ifmvvyr_kgWy2cguTpAe/view?usp=sharing'
# elon_integration_test.jpg
download_url = 'https://drive.google.com/file/d/1yoBSb6nwFUZVI4CD-nM2BSofGdBjC8Nn/view?usp=sharing'
#download_url = 'https://i.imgur.com/...'
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new image template',
'download_url': download_url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
@@ -1,61 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/w2l/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
def upload(url):
print("===== Upload W2L =====")
cookies = { 'session': session_cookie }
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new image template',
'download_url': url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
upload('https://i.imgur.com/W1EYiPU.jpg')
upload('https://i.imgur.com/NneE7Eq.jpg')
upload('https://i.imgur.com/v8k9yau.jpeg')
upload('https://drive.google.com/file/d/1yoBSb6nwFUZVI4CD-nM2BSofGdBjC8Nn/view?usp=sharing')
upload('https://drive.google.com/file/d/1ysBAdRjlSmfWQPFjGlMJpv_zcGmVw7RC/view?usp=sharing')
upload('https://i.imgur.com/lKaQ4Er.jpg')
upload('https://i.imgur.com/uoGuTJo.jpeg')
@@ -1,55 +0,0 @@
#!/usr/bin/env python3
import requests
import re
import uuid
login_url = 'http://localhost:12345/login'
upload_url = 'http://localhost:12345/w2l/upload'
payload = {
'username_or_email': 'echelon',
'password': 'testing',
}
r = requests.post(login_url, json=payload)
print("===== Login =====")
print('Status: {}'.format(r.status_code))
print(r.content)
for k, v in r.headers.items():
print(' {}: {}'.format(k, v))
# NB: May not be set due to cookie domain:
#r.cookies['session']
raw_set_cookie_header = r.headers['set-cookie']
m = re.match(r'session=([^;\s]+);', raw_set_cookie_header)
session_cookie = m.group(1)
print("===== Upload W2L =====")
cookies = { 'session': session_cookie }
# mario.zip
#download_url = 'https://drive.google.com/file/d/15-tkgblTZpa0ifmvvyr_kgWy2cguTpAe/view?usp=sharing'
# elon_integration_test.jpg
#download_url = 'https://drive.google.com/file/d/1yoBSb6nwFUZVI4CD-nM2BSofGdBjC8Nn/view?usp=sharing'
# bill-integration-test.mp4
# NB: This freezes up my poor laptop:
###download_url = 'https://drive.google.com/file/d/14yO_60lc1FJCYhDGaeiQabgGvYLa86WI/THIS_IS_TOO_BIGview?usp=sharing'
# bill-integration-small.mp4 (small enough to run on my laptop)
download_url = 'https://drive.google.com/file/d/1ysBAdRjlSmfWQPFjGlMJpv_zcGmVw7RC/view?usp=sharing'
payload = {
'idempotency_token': str(uuid.uuid4()),
'title': 'this is a new image template',
'download_url': download_url,
}
r = requests.post(upload_url, cookies=cookies, json=payload)
print('Status: {}'.format(r.status_code))
print(r.content)
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from badges\G"
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from ip_address_bans\G"
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from firehose_entries\G"
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from user_sessions\G"
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from tts_inference_jobs\G"
@@ -1,45 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from tts_results\G"
#echo '==============================================='
#
#read -r -d '' query << EOF
#
# SELECT
# w2l_results.token as w2l_result_token,
# w2l_results.maybe_tts_inference_result_token,
# w2l_templates.token as maybe_w2l_template_token,
# w2l_templates.template_type,
# users.token as maybe_creator_user_token,
# users.username as maybe_creator_username,
# users.display_name as maybe_creator_display_name,
# w2l_results.file_size_bytes,
# w2l_results.frame_width,
# w2l_results.frame_height,
# w2l_results.duration_millis,
# w2l_results.created_at,
# w2l_results.updated_at
# FROM
# w2l_results
# LEFT OUTER JOIN w2l_templates ON w2l_results.maybe_w2l_template_token = w2l_templates.token
# LEFT OUTER JOIN users ON w2l_results.maybe_creator_user_token = users.token
# WHERE
# w2l_results.deleted_at IS NULL
# AND users.username = 'echelon'\G
#EOF
#
#echo $query
#
#mysql -u root \
# -proot \
# -h localhost \
# -D storyteller \
# -e "${query}"
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from tts_models\G"
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from tts_model_upload_jobs\G"
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from users\G"
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from w2l_inference_jobs\G"
@@ -1,45 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from w2l_results\G"
echo '==============================================='
read -r -d '' query << EOF
SELECT
w2l_results.token as w2l_result_token,
w2l_results.maybe_tts_inference_result_token,
w2l_templates.token as maybe_w2l_template_token,
w2l_templates.template_type,
users.token as maybe_creator_user_token,
users.username as maybe_creator_username,
users.display_name as maybe_creator_display_name,
w2l_results.file_size_bytes,
w2l_results.frame_width,
w2l_results.frame_height,
w2l_results.duration_millis,
w2l_results.created_at,
w2l_results.updated_at
FROM
w2l_results
LEFT OUTER JOIN w2l_templates ON w2l_results.maybe_w2l_template_token = w2l_templates.token
LEFT OUTER JOIN users ON w2l_results.maybe_creator_user_token = users.token
WHERE
w2l_results.deleted_at IS NULL
AND users.username = 'echelon'\G
EOF
echo $query
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "${query}"
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from w2l_templates\G"
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u root \
-proot \
-h localhost \
-D storyteller \
-e "select * from w2l_template_upload_jobs\G"
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from user_roles\G"
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "update users set user_role_slug='admin' where username='echelon' limit 1\G"
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "update users set user_role_slug='user' where username='echelon' limit 1\G"
@@ -1,7 +0,0 @@
#!/bin/bash
# ONLY FOR LOCAL DEV
mysql -u storyteller \
-ppassword \
-h localhost \
-D storyteller \
-e "select * from tts_result_synthetic_ids\G"
@@ -1,88 +0,0 @@
#!/usr/bin/env python
# FIXME: Needs packages 'toml' and 'mysql-connector-python'. Put in requirements.txt.
import time
import toml
import mysql.connector
DB_SECRETS_FILE = 'db_secrets.toml'
# TODO: Share db-related setup when more scripts are added.
def read_database_secrets(secrets_file = DB_SECRETS_FILE):
secrets = None
with open(secrets_file, "r") as f:
secrets = toml.load(f)
required_keys = ['database', 'hostname', 'port', 'password', 'username']
for key in required_keys:
if key not in secrets:
raise Exception(f"Required key '#{key}' not present")
return secrets
def connect_to_mysql():
secrets = read_database_secrets()
connection = mysql.connector.connect(
host = secrets['hostname'],
port = secrets['port'],
user = secrets['username'],
password = secrets['password'],
database = secrets['database'],
)
return connection
connection = connect_to_mysql()
cursor = connection.cursor()
SAFE_ID = 76000000 # Reasonably completed on 2022-06-24
LIMIT = 100000
#def get_record_count():
# query = f"SELECT COUNT(*) FROM tts_inference_jobs"
# cursor.execute(query)
# result = cursor.fetchall()
# print(result)
# return result[0]
#
##total_count = get_record_count()
#total_count = 75082057
total_count = 21925602
def get_first_record(cursor):
query = f"select * from tts_inference_jobs order by id limit 1"
cursor.execute(query)
result = cursor.fetchall()
print(result)
return result
total_deleted = 0
keep_deleting = True
while keep_deleting:
query = f"DELETE FROM tts_inference_jobs WHERE id < {SAFE_ID} ORDER BY ID ASC LIMIT {LIMIT}"
query = f"DELETE FROM tts_inference_jobs WHERE status = 'complete_success' ORDER BY ID ASC LIMIT {LIMIT}"
#print(query)
cursor.execute(query)
connection.commit() # NB: Python does not autocommit changes!
#result = cursor.fetchall()
count = cursor.rowcount
total_deleted += count
percent_deleted = (total_deleted / total_count) * 100.0
print(f'Deleted {count}; total deleted = {total_deleted} ({percent_deleted:.4f})')
if count < LIMIT - 1:
keep_deleting = False
time.sleep(0.5)
#record = get_first_record(cursor)
#time.sleep(2)
+625
View File
@@ -0,0 +1,625 @@
# ArtCraft Omni API
> # 📚 Full API reference: **<https://storyteller-docs.netlify.app/>**
>
> **Every ArtCraft API endpoint is documented there.** This guide covers only the Omni API video
> endpoints — for everything else, start at the link above.
The **Omni API** is the API-key–authenticated surface for ArtCraft generation. It mirrors the
in-app "Omni Gen" endpoints but is designed for programmatic use: you authenticate with an API key
in the `Authorization` header instead of a browser session cookie.
This guide covers **video generation from URL inputs** — passing image, video, and audio reference
URLs directly. The server downloads each URL, stores it as a media file on your account, and then
runs generation against it. You never have to upload files yourself.
> All examples send **no cookies**. The Omni API is authenticated purely by your API key. If you
> send session cookies they are ignored.
---
## Base URLs
| Environment | Base URL |
| ------------- | ------------------------------ |
| Development | `http://localhost:12345` |
| Production | `https://api.storyteller.ai` |
All paths below are relative to the base URL (e.g. `POST /v1/omni_api/generate/video`).
The examples in this guide use the **development** base URL. To run against production, swap
`http://localhost:12345` for `https://api.storyteller.ai`.
Note: Our APIs are hosted on "`api.storyteller.ai`", not "`api.getartcraft.com`". This is not a
typo. Our Cloudflare CDN URLs are `cdn-2.fakeyou.com`.
---
## Getting an API key
Create and manage API keys from the ArtCraft web app:
1. Go to **<https://app.getartcraft.com/>**
2. Click **Account → Settings → API Keys** (your account must have API keys enabled from our staff)
3. Create a key and copy it. The secret is shown **once** — store it somewhere safe.
> **Don't see the API Keys section?** API access is gated per account. Contact the Artcraft team
> to enable it.
### What an API key looks like
- Prefix: `artcraft_api_`
- Followed by 40 random lowercase Crockford-base32 characters (`0-9 a-z`, excluding `i l o u`)
- Total length: **53 characters**, e.g. `artcraft_api_3k7q9w0xv2hs5n8m4d6r1t8y0p2s4f6h8j0k2m4n6`
Treat the key like a password: never commit it to source control or paste it into logs.
---
## Authentication
Send the secret key in the `Authorization` header. Three forms are accepted:
```
Authorization: Bearer artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: Key artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
`Bearer` is recommended. **Do not send cookies.**
---
## Video generation — `POST /v1/omni_api/generate/video`
Generates a video and returns an inference job token you can poll for results.
### Request body
| Field | Type | Notes |
| ---------------------------- | --------------- | ----- |
| `model` | string | Generation model, e.g. `seedance_2p0`. **Required.** |
| `prompt` | string | Text prompt. |
| `duration_seconds` | integer | Length of the output video, e.g. `6`. |
| `aspect_ratio` | string | e.g. `wide_sixteen_by_nine`, `tall_nine_by_sixteen`, `square`. |
| `idempotency_token` | string (UUID) | **Required.** A fresh UUID per request; prevents accidental duplicates. |
| `negative_prompt` | string | Optional. |
| `resolution` | string | Optional, e.g. `seven_twenty_p`, `ten_eighty_p`. |
| `generate_audio` | boolean | Optional. |
| **URL inputs** (this guide) | | |
| `start_frame_image_url` | string (URL) | Starting keyframe image. |
| `end_frame_image_url` | string (URL) | Ending keyframe image. |
| `reference_image_urls` | array of URL | Reference images. |
| `reference_video_urls` | array of URL | Reference videos (`mp4`). |
| `reference_audio_urls` | array of URL | Reference audio (`wav`, `mp3`, `ogg`, …). |
| **Media-token inputs** | | |
| `start_frame_image_media_token`, `end_frame_image_media_token`, `reference_image_media_tokens`, `reference_video_media_tokens`, `reference_audio_media_tokens` | token(s) | Use these instead if you've already uploaded media. |
#### Rules for URL inputs
- Each URL field and its media-token counterpart are **mutually exclusive**. Sending both
`reference_image_urls` and `reference_image_media_tokens`, for example, returns:
`Either reference_image_media_tokens or reference_image_urls must be set, not both`.
- Every URL must start with `http://` or `https://`, otherwise:
`URL must start with http:// or https://, bad URL: <url>`.
- Redirects are followed (up to 10), so CDN/redirecting URLs work.
- The file type is detected from the downloaded bytes. Images must be a real image
(`jpeg`/`png`/`gif`/`webp`), reference videos must be `mp4`, reference audio must be a
supported audio type (`wav`, `mp3`, `aac`, `ogg`, `flac`, …).
### Response
```json
{
"success": true,
"inference_job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
"all_job_tokens": ["jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"]
}
```
Use `inference_job_token` to poll for the finished video. `all_job_tokens` lists every job created
(batch requests can create more than one).
---
## Examples
The examples reuse these public sample URLs:
- Images (Wikimedia Commons): an Atlantic puffin, the Matterhorn, a toco toucan
- Video: a short sample `mp4`
- Audio: a short sample `wav`
Replace `YOUR_API_KEY` with your `artcraft_api_…` secret, and generate a fresh `idempotency_token`
(UUID) for every request.
---
### Example 1 — Starting-frame Seedance (`start_frame_image_url`)
Animate outward from a single starting image.
#### bash
```bash
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900"
}'
```
#### python
```python
import uuid
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
resp = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
},
timeout=240,
)
resp.raise_for_status()
print(resp.json())
```
#### javascript
```javascript
import { randomUUID } from "node:crypto";
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance_2p0",
prompt:
"An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
start_frame_image_url:
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
}),
});
console.log(resp.status, await resp.json());
```
---
### Example 2 — Image-reference Seedance (`reference_image_urls`)
Guide the generation with one or more reference images.
#### bash
```bash
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
]
}'
```
#### python
```python
import uuid
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
resp = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance_2p0",
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
},
timeout=240,
)
resp.raise_for_status()
print(resp.json())
```
#### javascript
```javascript
import { randomUUID } from "node:crypto";
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance_2p0",
prompt:
"A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
reference_image_urls: [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
}),
});
console.log(resp.status, await resp.json());
```
---
### Example 3 — Image + video reference Seedance (`reference_image_urls` + `reference_video_urls`)
Combine reference images with a reference video (and, optionally, reference audio). This is the
fullest URL-driven request.
#### bash
```bash
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
],
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"]
}'
```
#### python
```python
import uuid
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
resp = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance_2p0",
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"reference_image_urls": [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"],
},
timeout=240,
)
resp.raise_for_status()
print(resp.json())
```
#### javascript
```javascript
import { randomUUID } from "node:crypto";
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "seedance_2p0",
prompt:
"An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
reference_image_urls: [
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
],
reference_video_urls: ["https://download.samplelib.com/mp4/sample-5s.mp4"],
reference_audio_urls: ["https://download.samplelib.com/wav/sample-6s.wav"],
}),
});
console.log(resp.status, await resp.json());
```
---
## Checking job status
Generation is asynchronous. The generate call returns an `inference_job_token`; poll the job-status
endpoint until the job reaches a terminal state, then read the result URL.
### Endpoints
- **Single job:** `GET /v1/omni_api/job_status/job/{token}`
- **Batch (many jobs):** `GET /v1/omni_api/job_status/batch?tokens=jinf_aaa&tokens=jinf_bbb`
Both require the same `Authorization: Bearer …` header (no cookies).
### Response shape
```json
{
"success": true,
"state": {
"job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
"request": {
"inference_category": "video_generation",
"maybe_prompt_token": "prompt_xxxxxxxxxxxxxxxxxxxxxxxxx",
"maybe_model_type": "seedance_2p0",
"maybe_model_token": null
},
"status": {
"status": "pending",
"maybe_first_started_at": null,
"maybe_failure_category": null,
"progress_percentage": 0
},
"maybe_result": null,
"created_at": "2026-06-24T04:06:14Z",
"updated_at": "2026-06-24T04:06:14Z"
}
}
```
- `state.status.status` — the job state. Keep polling while it's `pending`, `started`, or
`attempt_failed`. **Terminal** states are `complete_success`, `complete_failure`, `dead`,
`cancelled_by_user`, and `cancelled_by_system`.
- `state.status.progress_percentage` — an integer 0–100.
- `state.maybe_result` — `null` until the job succeeds. On `complete_success` it contains the
finished media; the playable/downloadable URL is **`state.maybe_result.media_links.cdn_url`**.
For video, `media_links.maybe_video_previews` also carries still/animated preview URLs.
The batch endpoint returns `{ "success": true, "job_states": [ … ] }` where each entry is the same
`state` payload as above.
### Where the result video / image lives
When `state.status.status` is `complete_success`, the finished media is under `state.maybe_result`:
- **`state.maybe_result.media_links.cdn_url`** — the primary, playable/downloadable URL of the
finished asset. **This is the field you want.** For a video job it's the `.mp4`; for an image job
it's the image file (`.jpg` / `.png` / etc.).
- `state.maybe_result.entity_token` — the `m_…` media-file token for the result.
- `state.maybe_result.media_links.maybe_video_previews` — **video jobs only**. Contains a `still`
(poster `.jpg`) and an `animated` (`.gif`) preview URL, plus `{WIDTH}` thumbnail templates. This
is `null` for image jobs.
- `state.maybe_result.media_links.maybe_thumbnail_template` — **image jobs only**. A URL template;
replace `{WIDTH}` with a pixel width to fetch a resized thumbnail. `null` for video jobs.
**Example — completed video job:**
```json
{
"success": true,
"state": {
"job_token": "jinf_9fcn2qv4dv3vvbrrxbnmfgen06v",
"request": {
"inference_category": "video_generation",
"maybe_prompt_token": "prompt_q53tp4rh97qh8q159p21tazzt",
"maybe_model_type": "seedance_2p0",
"maybe_model_token": null
},
"status": {
"status": "complete_success",
"maybe_first_started_at": "2026-06-24T04:07:11Z",
"maybe_failure_category": null,
"progress_percentage": 100
},
"maybe_result": {
"entity_type": "media_file",
"entity_token": "m_ss682134tx19yhqs5y0nbnd0bd59a6",
"media_links": {
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4",
"maybe_thumbnail_template": null,
"maybe_video_previews": {
"still": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.jpg",
"animated": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.gif",
"still_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.jpg",
"animated_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.gif"
}
},
"maybe_successfully_completed_at": "2026-06-24T04:17:30Z"
},
"created_at": "2026-06-24T04:06:14Z",
"updated_at": "2026-06-24T04:17:30Z"
}
}
```
→ The video is at
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4`
(example, not a live URL).
**Example — completed image job** (from `POST /v1/omni_api/generate/image`):
```json
{
"maybe_result": {
"entity_type": "media_file",
"entity_token": "m_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6",
"media_links": {
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg",
"maybe_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg-thumb-{WIDTH}.jpg",
"maybe_video_previews": null
},
"maybe_successfully_completed_at": "2026-06-24T04:09:02Z"
}
}
```
→ The image is at
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg`
(example, not a live URL).
### Poll until complete
#### bash
```bash
JOB_TOKEN="jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
while :; do
resp=$(curl -s "http://localhost:12345/v1/omni_api/job_status/job/$JOB_TOKEN" \
-H "Authorization: Bearer YOUR_API_KEY")
status=$(echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['status']['status'])")
echo "status=$status"
case "$status" in
complete_success)
echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['maybe_result']['media_links']['cdn_url'])"
break ;;
complete_failure|dead|cancelled_by_user|cancelled_by_system)
echo "job ended without a result: $status"; break ;;
esac
sleep 5
done
```
#### python
```python
import time
import requests
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
API_KEY = "YOUR_API_KEY"
JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
TERMINAL_FAILURES = {"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system"}
while True:
resp = requests.get(
f"{BASE_URL}/v1/omni_api/job_status/job/{JOB_TOKEN}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
resp.raise_for_status()
state = resp.json()["state"]
status = state["status"]["status"]
print("status:", status, state["status"]["progress_percentage"], "%")
if status == "complete_success":
print("result URL:", state["maybe_result"]["media_links"]["cdn_url"])
break
if status in TERMINAL_FAILURES:
print("job ended without a result:", status)
break
time.sleep(5)
```
#### javascript
```javascript
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
const API_KEY = "YOUR_API_KEY";
const JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx";
const TERMINAL_FAILURES = new Set([
"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system",
]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
while (true) {
const resp = await fetch(`${BASE_URL}/v1/omni_api/job_status/job/${JOB_TOKEN}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const { state } = await resp.json();
const status = state.status.status;
console.log("status:", status, state.status.progress_percentage, "%");
if (status === "complete_success") {
console.log("result URL:", state.maybe_result.media_links.cdn_url);
break;
}
if (TERMINAL_FAILURES.has(status)) {
console.log("job ended without a result:", status);
break;
}
await sleep(5000);
}
```
---
## Notes & troubleshooting
- **Always send a fresh `idempotency_token`** (UUID). Reusing one is rejected as a duplicate.
- **No cookies.** The endpoint authenticates only via the `Authorization` header.
- **URL must be `http(s)`** and reachable by the server; redirects are followed.
- **Type mismatches** (e.g. a `.webm` passed as a reference video, which requires `mp4`) are
rejected with a `400` describing the unpermitted type.
- **`401 Unauthorized`** — missing/invalid API key, or the key's owner is banned.
- **`402 Payment required`** — insufficient credits/balance.
- There is a companion image endpoint, `POST /v1/omni_api/generate/image`, which accepts
`image_urls` (mutually exclusive with `image_media_tokens`) following the same rules.
+86
View File
@@ -0,0 +1,86 @@
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. The frontend's Nx and Vite dependencies are installed locally with npm; a global Nx installation is not required for the combined Unix launcher.
4. [Install Tauri CLI](https://v2.tauri.app/reference/cli/). (Version `tauri-cli 2.10.0` works at time of writing.)
**Mac and Linux Development**
```bash
# Start the frontend and Rust/Tauri together in one terminal
./script/artcraft/unix_dev.sh
# Optionally start the free-port search at another port
ARTCRAFT_DEV_PORT=6200 ./script/artcraft/unix_dev.sh
```
Use the combined launcher instead of starting the two legacy dev scripts. It
works from any working directory when invoked by its path and installs frontend
dependencies if the local Vite installation is missing. Node.js 20+ is required.
The launcher binds Vite to `127.0.0.1`, starting at port **5193** and trying
successive ports until one is available. It leaves existing listeners alone.
Only after the real frontend socket is bound does it start Tauri with that exact
`devUrl`. HTTP and the HMR websocket share the selected port; Rust communicates
through native Tauri IPC and needs no separate HTTP port. Configuration overrides
are passed in memory, without changing the checked-in Tauri config.
The native HTTP bridge normalizes loopback development origins for first-party
API requests so an automatically selected port does not break login or media
loading. See the [desktop session regression checks](../docs/desktop-session-regression.md)
when changing ports, the launcher, or authentication.
JavaScript, TypeScript, React, and CSS changes use Vite hot reload; compatible
React component edits use Fast Refresh. Changes that cannot be hot-replaced
reload the page. Rust changes use `cargo tauri dev`'s automatic **rebuild and app
restart**, including dependent workspace crates. SQLx metadata and SQLite
migrations are also watched. Rust reload is a process restart, so it does not
preserve unsaved in-memory app state. See [Tauri's watch behavior](https://v2.tauri.app/develop/#reacting-to-source-code-changes).
After startup, a Vite configuration restart must reuse the chosen port or report
an error; it cannot silently switch to another port while Tauri uses an old URL.
Ctrl-C stops this session's frontend, Rust watcher/compiler, and desktop app.
If the Rust watcher exits, the launcher closes Vite and propagates the exit code.
It never finds or terminates another app by name or port.
The launcher retains the existing Unix defaults for `VITE_ENVIRONMENT_TYPE`,
`SQLX_OFFLINE`, `RUSTFLAGS`, and the Linux WebKit workarounds. Explicit environment
overrides are preserved. It uses the normal Artcraft app data and Cargo target
directory; port separation does not create an isolated account/data profile, and
concurrent Rust builds can wait on Cargo's build lock.
To verify the launcher without compiling Rust or opening a desktop window:
```bash
node --test frontend/scripts/unix-dev.test.mjs
```
The tests use real Vite listeners and HMR, with a test-owned fake Cargo process
tree. They cover occupied ports, simultaneous launches, React edits, shutdown,
Rust-process failure, and watcher arguments. To verify native Rust reload, run
the launcher, wait for the desktop window, make a Rust source edit, and check for
a rebuild/restart in the same terminal. Frontend edits should update the webview
without restarting the Rust process.
**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.
@@ -1,43 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Select TTS models within a category
-- (NOTE CATEGORIES ARE *SOFT DELETED* AND THIS MAY MIS-ATTRIBUTE)
select
tts.token,
tts.ietf_language_tag,
tts.title
from tts_category_assignments as cat
join tts_models as tts
on tts.token = cat.model_token
where
cat.category_token = 'CAT:n06k9gfgq20';
-- Select TTS models within a category
select
tts.token,
tts.ietf_language_tag,
tts.title
from tts_category_assignments as cat
join tts_models as tts
on tts.token = cat.model_token
where
cat.category_token = 'CAT:jtdwpy6ptrn'
AND cat.deleted_at IS NULL;
-- Select TTS models within a category
-- Exclude given languages.
select
tts.token,
tts.ietf_language_tag,
tts.title
from tts_category_assignments as cat
join tts_models as tts
on tts.token = cat.model_token
where
cat.category_token = 'CAT:jtdwpy6ptrn'
AND tts.ietf_language_tag NOT IN ('es', 'es-419', 'es-ES', 'es-MX')
AND cat.deleted_at IS NULL;
-82
View File
@@ -1,82 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Kill pending jobs
update tts_inference_jobs set status = 'dead' where status = 'pending';
-- Total pending jobs
select count(*)
from tts_inference_jobs
where status = 'pending';
-- Total pending API jobs
select count(*)
from tts_inference_jobs
where status = 'pending'
and is_from_api = true;
-- Top IP addresses making requests
select distinct creator_ip_address, count(*) as attempts
from tts_inference_jobs
where status = 'pending'
group by creator_ip_address
order by attempts desc
limit 50;
-- Top voices in requests
select distinct model_token, count(*) as attempts
from tts_inference_jobs
where status = 'pending'
group by model_token
order by attempts desc
limit 50;
-- Kill 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');
-- Sample the pending inference text
select creator_ip_address,
model_token,
maybe_creator_user_token,
raw_inference_text
from tts_inference_jobs
where status = 'pending';
-- Sample the pending inference text (truncated)
select creator_ip_address,
model_token,
maybe_creator_user_token,
TRIM(REPLACE(SUBSTRING(raw_inference_text, 1, 50), '\n', ''))
from tts_inference_jobs
where status = 'pending'
limit 100;
-- Sample long pending texts
select creator_ip_address,
model_token,
maybe_creator_user_token,
raw_inference_text
from tts_inference_jobs
where status = 'pending'
and length(raw_inference_text) > 100
limit 100;
-- Delete old TTS inference jobs (1)
DELETE FROM tts_inference_jobs
WHERE status IN ('dead', 'complete_success', 'complete_failure')
LIMIT 100000;
-- Delete old TTS inference jobs (2)
-- Roughly 20 seconds to delete 100k, 4 minutes for 1M.
DELETE FROM tts_inference_jobs
WHERE created_at < ( CURDATE() - INTERVAL 1 DAY )
LIMIT 100000;
-302
View File
@@ -1,302 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
--
-- Top 100 models all time by use count
--
select
m.token,
m.text_pipeline_type,
m.ietf_language_tag,
m.title,
u.username,
r.use_count,
m.user_deleted_at,
m.mod_deleted_at,
m.created_at
from (
select model_token, count(*) as use_count
from tts_results
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
order by r.use_count desc
limit 100;
--
-- Same, but simpler...
--
select m.token, m.title, r.use_count from (
select model_token, count(*) as use_count from tts_results
group by model_token
order by use_count desc limit 100
) as r
join tts_models as m
on m.token = r.model_token;
--
-- Top 100 models by use count, last 5 days
-- Limited due to tmux scrollback.
--
select
m.token,
m.text_pipeline_type,
m.ietf_language_tag,
m.title,
u.username,
r.use_count,
m.created_at,
m.user_deleted_at,
m.mod_deleted_at
from (
select model_token, count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 12 HOUR )
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
order by r.use_count desc
limit 25;
--
-- Top 20 ***NEW*** models to share in #announcements
-- Can be scoped by creator.
--
select
m.token,
m.text_pipeline_type,
m.ietf_language_tag,
m.title,
u.username,
r.use_count,
m.user_deleted_at,
m.mod_deleted_at,
m.created_at
from (
select model_token, count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 5 DAY )
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
where m.created_at > ( CURDATE() - INTERVAL 31 DAY )
AND u.username IN ( 'vegito1089', 'justinjohn0306')
order by r.use_count desc
limit 100;
--
-- Top 100 models by use count, last 5 days, not spanish, etc.
-- Limited due to tmux scrollback.
--
select
m.token,
m.text_pipeline_type,
m.ietf_language_tag,
m.title,
u.username,
r.use_count,
m.user_deleted_at,
m.mod_deleted_at,
m.created_at
from (
select model_token, count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 5 DAY )
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
where m.ietf_language_tag NOT IN ('es', 'es-419', 'es-ES', 'es-MX', 'pt-BR')
order by r.use_count desc
limit 100;
--
-- Top 100 models by use count, last 5 days, in specific language.
-- Limited due to tmux scrollback.
--
-- English: ('en', 'en-US', 'en-AU', 'en-CA', 'en-GB')
-- Spanish: ('es', 'es-419', 'es-AR', 'es-CL', 'es-CO', 'es-ES', 'es-MX', 'es-PE', 'es-US')
-- Italian: ('it', 'it-CH', 'it-IT')
-- Portuguese: ('pt', 'pt-BR')
--
select
m.token,
m.title,
m.text_pipeline_type,
m.ietf_language_tag,
u.username,
r.use_count,
m.user_deleted_at,
m.mod_deleted_at,
m.created_at
from (
select model_token, count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 30 DAY )
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
where m.ietf_language_tag IN ('en', 'en-US', 'en-AU', 'en-CA', 'en-GB')
order by r.use_count desc
limit 100;
--
-- Most popular voices by use count over 5-day window, single language, single user.
--
select
m.token,
m.title,
m.ietf_language_tag,
u.username,
r.use_count,
m.user_deleted_at,
m.mod_deleted_at,
m.created_at
from (
select model_token, count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 5 DAY )
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
where
m.ietf_language_tag NOT IN ('es', 'es-419', 'es-ES', 'es-MX', 'pt-BR')
AND u.username = 'vegito1089'
order by r.use_count desc
limit 100;
--
-- Most popular deleted models
--
select
m.token,
m.title,
m.ietf_language_tag,
u.username,
r.use_count,
m.user_deleted_at,
m.mod_deleted_at,
m.created_at
from (
select model_token, count(*) as use_count
from tts_results
where model_token IN (
select token
from tts_models
where
user_deleted_at IS NOT NULL
OR mod_deleted_at IS NOT NULL
)
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
order by r.use_count desc
limit 100;
--
-- Most popular deleted models (only by users, not mods)
-- Mod-deleted models were probably on purpose
--
select
m.token,
m.title,
m.ietf_language_tag,
u.username,
r.use_count,
m.user_deleted_at,
m.created_at
from (
select model_token, count(*) as use_count
from tts_results
where model_token IN (
select token
from tts_models
where
user_deleted_at IS NOT NULL
)
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
order by r.use_count desc
limit 100;
--
-- Models uploaded recently (to check if they use the right text_pipeline_type,
-- vocoder -todo-, etc.)
--
select
m.token,
m.title,
m.text_pipeline_type,
m.ietf_language_tag,
u.username,
r.use_count,
m.user_deleted_at,
m.mod_deleted_at,
m.created_at
from (
select model_token, count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 5 DAY )
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
order by m.created_at desc
limit 100;
--
-- Models deleted recently (voice actor take down)
--
select
m.token,
m.title,
m.text_pipeline_type,
m.ietf_language_tag,
u.username,
r.use_count,
m.created_at,
m.user_deleted_at,
m.mod_deleted_at
from (
select model_token, count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 30 DAY )
group by model_token
) as r
join tts_models as m
on m.token = r.model_token
join users as u
on u.token = m.creator_user_token
where m.mod_deleted_at > ( CURDATE() - INTERVAL 14 DAY )
or m.user_deleted_at > ( CURDATE() - INTERVAL 14 DAY )
order by r.use_count desc
limit 500;
-101
View File
@@ -1,101 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
--
-- Usage count in the past 24 hours
--
select count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 1 DAY )
--
-- Usage count in the past week
--
select count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 7 DAY )
-- On-prem worker mix
select count(*) as use_count
from tts_results
where created_at > ( CURDATE() - INTERVAL 1 MINUTE )
and is_generated_on_prem IS TRUE
-- Calculate a percentage of on-prem worker capacity
select count(*) as on_prem_count
from (
select is_generated_on_prem
from tts_results
order by id desc limit 1000
) as sample
where sample.is_generated_on_prem IS TRUE;
-- Find TTS results for a single model
select count(*) from tts_results
where model_token = 'TM:ztt5s1be5tq6';
-- Find TTS results for a single model that are not deleted
select count(*) from tts_results
where model_token = 'TM:ztt5s1be5tq6'
and mod_deleted_at is NULL;
-- Sample results for a single model
select
TRIM(REPLACE(SUBSTRING(raw_inference_text, 1, 50), '\n', ''))
from tts_results
where model_token='TM:yt4gfbkngsjj'
order by id desc
limit 150;
-- Search for results by eminem
select
token,
created_at,
raw_inference_text
from tts_results
where
model_token IN ('TM:27fj0gsh11pd', 'TM:8h9bfjgabeer', 'TM:pdf9c1anbdjq')
AND raw_inference_text LIKE "%rave%"
AND raw_inference_text LIKE "%sound%"
AND created_at > ( CURDATE() - INTERVAL 5 MONTH )
ORDER BY created_at DESC
-- 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;
--- Search for particular text by a particular model
SELECT
raw_inference_text,
maybe_creator_user_token,
created_at
FROM
tts_results
WHERE
model_token = 'TM:ztt5s1be5tq6'
AND raw_inference_text LIKE '%fakeyou%'
---
-- WTF WHY IS THIS SLOW???
SELECT
res.id,
model.title,
res.raw_inference_text,
res.maybe_creator_user_token
FROM
tts_results AS res
JOIN
tts_models as model
ON
res.model_token = model.token
ORDER BY res.id
LIMIT 10;
@@ -1,119 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- TODO: Do an analysis of voice binned by # of paying customers that use it.
-- The most popular models amongst paying subscribers.
-- This should give us insight as to which models subscribers pay us for.
SELECT *
FROM (
SELECT
t.token,
t.title,
count(*) as use_count,
t.text_pipeline_type,
t.ietf_language_tag,
t.created_at,
t.user_deleted_at,
t.mod_deleted_at
FROM
tts_inference_jobs as j
JOIN
tts_models as t
ON
t.token = j.model_token
WHERE j.maybe_creator_user_token IN
(
select distinct user_token
from user_subscriptions
)
AND
j.created_at > ( CURDATE() - INTERVAL 1 DAY )
GROUP BY t.token
ORDER BY use_count DESC
) as x
WHERE
x.use_count > 10;
-- The first voice a paying customer uses
-- TODO: List *all* of the voices a paying customer uses before they subscribe, not just the first.
-- select first row of partially aggregate query: https://stackoverflow.com/a/73157541
SELECT *
FROM (
SELECT
t.token,
t.title,
count(*) as use_count,
t.text_pipeline_type,
t.ietf_language_tag,
t.created_at,
t.user_deleted_at,
t.mod_deleted_at
FROM tts_models AS t
JOIN
(
SELECT
model_token
FROM (
SELECT
model_token,
maybe_creator_user_token,
row_number() over(partition by maybe_creator_user_token order by id asc) as rownum
FROM
tts_inference_jobs
WHERE maybe_creator_user_token IN
(
select distinct user_token
from user_subscriptions
where created_at > (CURDATE() - INTERVAL 2 DAY)
)
) AS x
WHERE rownum = 1
ORDER BY model_token desc
) AS y
ON t.token = y.model_token
GROUP BY t.token
) AS z
ORDER BY use_count desc
-- The last voice a paying customer uses
-- select first row of partially aggregate query: https://stackoverflow.com/a/73157541
SELECT *
FROM (
SELECT
t.token,
t.title,
count(*) as use_count,
t.text_pipeline_type,
t.ietf_language_tag,
t.created_at,
t.user_deleted_at,
t.mod_deleted_at
FROM tts_models AS t
JOIN
(
SELECT
model_token
FROM (
SELECT
model_token,
maybe_creator_user_token,
row_number() over(partition by maybe_creator_user_token order by id desc) as rownum
FROM
tts_inference_jobs
WHERE maybe_creator_user_token IN
(
select distinct user_token
from user_subscriptions
)
) AS x
WHERE rownum = 1
ORDER BY model_token desc
) AS y
ON t.token = y.model_token
GROUP BY t.token
) AS z
ORDER BY use_count desc
@@ -1,124 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Number of paid users (inaccurate)
select count(*) from users where maybe_stripe_customer_id IS NOT NULL;
-- Number of paid users (inaccurate)
select count(*) from user_subscriptions;
-- Number of paid users
-- NB: "active" status users are current, treating "past_due" as active too (for now)
select count(*) from user_subscriptions where maybe_stripe_subscription_status NOT IN ("canceled", "incomplete", "incomplete_expired");
-- ==================== TEXT TO SPEECH====================
-- All paid subscribers generating results in the last week
select u.username, count(*) as use_count
from users as u
join tts_inference_jobs as j
on u.token = j.maybe_creator_user_token
WHERE u.token IN (
select distinct user_token
from user_subscriptions
where maybe_stripe_subscription_status NOT IN ("canceled", "incomplete", "incomplete_expired")
)
AND j.created_at > NOW() - interval 7 day
GROUP BY u.username
ORDER BY use_count desc
-- Paid subscribers generating results in the last month (with # threshold)
select username, use_count
FROM (
select u.username, count(*) as use_count
from users as u
join tts_inference_jobs as j
on u.token = j.maybe_creator_user_token
WHERE u.token IN (
select distinct user_token
from user_subscriptions
where maybe_stripe_subscription_status NOT IN ("canceled", "incomplete", "incomplete_expired")
)
AND j.created_at > NOW() - interval 31 day
GROUP BY u.username
ORDER BY use_count desc
) as t
WHERE use_count > 10
-- Paid users generating results in the last month
select u.username, count(*) as use_count
from users as u
join tts_inference_jobs as j
on u.token = j.maybe_creator_user_token
WHERE u.token IN (
select distinct user_token
from user_subscriptions
where maybe_stripe_subscription_status NOT IN ("canceled", "incomplete", "incomplete_expired")
)
AND j.created_at > NOW() - interval 1 day
GROUP BY u.username
ORDER BY use_count desc
-- ==================== WAV2LIP ====================
select u.username, count(*) as use_count
from users as u
join w2l_inference_jobs as j
on u.token = j.maybe_creator_user_token
WHERE u.token IN (
select distinct user_token
from user_subscriptions
where maybe_stripe_subscription_status NOT IN ("canceled", "incomplete", "incomplete_expired")
)
AND j.created_at > NOW() - interval 31 day
GROUP BY u.username
ORDER BY use_count desc
select username, use_count
FROM (
select u.username, count(*) as use_count
from users as u
join w2l_inference_jobs as j
on u.token = j.maybe_creator_user_token
WHERE u.token IN (
select distinct user_token
from user_subscriptions
where maybe_stripe_subscription_status NOT IN ("canceled", "incomplete", "incomplete_expired")
)
AND j.created_at > NOW() - interval 31 day
GROUP BY u.username
ORDER BY use_count desc
) as t
WHERE use_count >=5;
-- ==================== TTS MODELS ====================
select u.username, count(*) as use_count
from users as u
join tts_model_upload_jobs as j
on u.token = j.creator_user_token
WHERE u.token IN (
select distinct user_token
from user_subscriptions
where maybe_stripe_subscription_status NOT IN ("canceled", "incomplete", "incomplete_expired")
)
AND j.created_at > NOW() - interval 1 day
GROUP BY u.username
ORDER BY use_count desc
-- ==================== W2L TEMPLATES ====================
select u.username, count(*) as use_count
from users as u
join w2l_template_upload_jobs as j
on u.token = j.creator_user_token
WHERE u.token IN (
select distinct user_token
from user_subscriptions
where maybe_stripe_subscription_status NOT IN ("canceled", "incomplete", "incomplete_expired")
)
AND j.created_at > NOW() - interval 1 day
GROUP BY u.username
ORDER BY use_count desc
@@ -1,30 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- NB: These are for use in Stripe Sigma, not MySQL
-- NB: You must use single quotes instead of double quotes when comparing string values.
-- Histogram of customers by country
select address_country, count(*)
from customers
group by address_country
select customer_id, subscriptions.status
from subscriptions
select distinct customer_id
from subscriptions
where subscriptions.status = 'active'
or subscriptions.status = 'past_due'
select address_country, count(*) as country_count
from customers
where id IN (
select distinct customer_id
from subscriptions
where subscriptions.status = 'active'
or subscriptions.status = 'past_due'
)
group by address_country

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