diff --git a/_database/sql/migrations_squashed/characters/character_creation_assets.sql b/_database/sql/migrations_squashed/characters/character_creation_assets.sql new file mode 100644 index 000000000..78233d4b6 --- /dev/null +++ b/_database/sql/migrations_squashed/characters/character_creation_assets.sql @@ -0,0 +1,29 @@ +-- NB: This is a manually squashed view of all the CREATE and ALTER statements, +-- with comments attached to the fields for centralized documentation. + +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Stores the media files (images) used as input when creating a character. +CREATE TABLE character_creation_assets ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Foreign key to the characters table. + character_token VARCHAR(32) NOT NULL, + + -- The media file used as a creation input (e.g. reference image). + asset_media_file_token VARCHAR(32) NOT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- ========== INDICES ========== + + PRIMARY KEY (id), + KEY index_character_token (character_token), + KEY index_asset_media_file_token (asset_media_file_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_database/sql/migrations_squashed/characters/character_creation_prompts.sql b/_database/sql/migrations_squashed/characters/character_creation_prompts.sql new file mode 100644 index 000000000..d5a97ea28 --- /dev/null +++ b/_database/sql/migrations_squashed/characters/character_creation_prompts.sql @@ -0,0 +1,28 @@ +-- NB: This is a manually squashed view of all the CREATE and ALTER statements, +-- with comments attached to the fields for centralized documentation. + +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Stores prompts/descriptions used when creating a character. +CREATE TABLE character_creation_prompts ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Foreign key to the characters table. + character_token VARCHAR(32) NOT NULL, + + -- The prompt or description used during character creation. + prompt TEXT DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- ========== INDICES ========== + + PRIMARY KEY (id), + KEY index_character_token (character_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_database/sql/migrations_squashed/characters/character_third_party_entities.sql b/_database/sql/migrations_squashed/characters/character_third_party_entities.sql new file mode 100644 index 000000000..d08c1f24d --- /dev/null +++ b/_database/sql/migrations_squashed/characters/character_third_party_entities.sql @@ -0,0 +1,71 @@ +-- NB: This is a manually squashed view of all the CREATE and ALTER statements, +-- with comments attached to the fields for centralized documentation. + +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Maps our characters to their representations in third-party systems. +-- A single character may have entries in multiple third-party systems +-- (e.g. Kinovi for Seedance, Runway for Gen-3, etc.). +-- If a third-party character needs to be recreated, a new row is inserted. +CREATE TABLE character_third_party_entities ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + -- We may need to recreate the character in a third party, so this gets its own token. + token VARCHAR(32) NOT NULL, + + -- Foreign key to the characters table. + character_token VARCHAR(32) NOT NULL, + + -- Whether this third-party entity is active and usable. + is_active BOOLEAN NOT NULL DEFAULT false, + + -- ========== THIRD PARTY DETAILS ========== + + -- Which third-party system the character lives in (enum crate type). + -- e.g. "kinovi", "runway", etc. + third_party VARCHAR(32) NOT NULL, + + -- Which types of models can use this character (enum crate type). + -- e.g. "seedance_2_pro", "seedance_2_fast", etc. + third_party_scopes VARCHAR(32) NOT NULL, + + -- The identifier in the third-party system. + -- e.g. Kinovi's "char_1774752056469_2wlxoq" + third_party_id VARCHAR(64) NOT NULL, + + -- The name used by the third party. + -- Important for Kinovi since characters are referenced by name in prompts. + third_party_name VARCHAR(255) NOT NULL, + + -- An upstream third party of the third party (if any). + -- e.g. Kinovi's assetId "asset-20260329104101-kk5kl" + maybe_fourth_party_id VARCHAR(64) DEFAULT NULL, + + -- ========== JOB TRACKING ========== + + -- Our job system token (if the character was created via a job). + maybe_origin_job_token VARCHAR(32) DEFAULT NULL, + + -- Third party's job system token (if any). + maybe_origin_third_party_job_id VARCHAR(64) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP DEFAULT NULL, + + -- ========== INDICES ========== + + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY index_character_token (character_token), + KEY index_third_party (third_party), + KEY index_third_party_id (third_party_id), + KEY index_is_active (is_active) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_database/sql/migrations_squashed/characters/characters.sql b/_database/sql/migrations_squashed/characters/characters.sql new file mode 100644 index 000000000..b78132d2f --- /dev/null +++ b/_database/sql/migrations_squashed/characters/characters.sql @@ -0,0 +1,60 @@ +-- NB: This is a manually squashed view of all the CREATE and ALTER statements, +-- with comments attached to the fields for centralized documentation. + +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Characters are reusable entities that can be referenced in video/image generations. +-- A character is created from one or more reference images and registered with third-party +-- systems (e.g. Kinovi/Seedance) that support character-based generation. +CREATE TABLE characters ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + token VARCHAR(32) NOT NULL, + + -- Whether the character has been fully created and is available for use. + -- A character may exist in the database before its creation job completes. + is_active BOOLEAN NOT NULL DEFAULT false, + + -- ========== CHARACTER DETAILS ========== + + -- Display name for the character. + name VARCHAR(255) NOT NULL, + + -- Optional description of the character. + maybe_description VARCHAR(512) DEFAULT NULL, + + -- ========== MEDIA ========== + + -- Avatar image (small/thumbnail). + maybe_avatar_media_token VARCHAR(32) DEFAULT NULL, + + -- Full-size reference image. + maybe_full_image_media_token VARCHAR(32) DEFAULT NULL, + + -- ========== CREATOR ========== + + -- The user who created this character. + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + creator_ip_address VARCHAR(40) NOT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP DEFAULT NULL, + + -- ========== INDICES ========== + + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY index_maybe_creator_user_token (maybe_creator_user_token), + KEY index_is_active (is_active), + KEY index_created_at (created_at) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_database/sql/migrations_squashed/generic_inference_jobs.sql b/_database/sql/migrations_squashed/generic_inference_jobs.sql index abec304ef..de83a4624 100644 --- a/_database/sql/migrations_squashed/generic_inference_jobs.sql +++ b/_database/sql/migrations_squashed/generic_inference_jobs.sql @@ -46,6 +46,7 @@ CREATE TABLE generic_inference_jobs ( -- * background_removal inference_category VARCHAR(32) NOT NULL, + -- Enum: InferenceJobExternalThirdParty -- If the job is externally enqueued, this is the third-party service -- responsible for the job. Typically, we won't run an inference job for -- jobs being run on a third party. diff --git a/_database/sql/migrations_todo/2026-03-29-150019-0000_characters/down.sql b/_database/sql/migrations_todo/2026-03-29-150019-0000_characters/down.sql new file mode 100644 index 000000000..91f03efa2 --- /dev/null +++ b/_database/sql/migrations_todo/2026-03-29-150019-0000_characters/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE IF EXISTS characters; diff --git a/_database/sql/migrations_todo/2026-03-29-150019-0000_characters/up.sql b/_database/sql/migrations_todo/2026-03-29-150019-0000_characters/up.sql new file mode 100644 index 000000000..fde055800 --- /dev/null +++ b/_database/sql/migrations_todo/2026-03-29-150019-0000_characters/up.sql @@ -0,0 +1,55 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE characters ( + -- Not used for anything except replication (private, not even used in queries) + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Public-facing "primary key" (PUBLIC) + token VARCHAR(32) NOT NULL, + + -- Whether the character has been fully created and is available for use. + -- A character may exist in the database before its creation job completes. + is_active BOOLEAN NOT NULL DEFAULT false, + + -- ========== CHARACTER DETAILS ========== + + -- User's display name for the character. + name VARCHAR(255) NOT NULL, + + -- Optional user description of the character. + maybe_description VARCHAR(512) DEFAULT NULL, + + -- ========== MEDIA ========== + + -- Avatar image (cropped to face, ideally). + maybe_avatar_media_token VARCHAR(32) DEFAULT NULL, + + -- Full-size reference image. + maybe_full_image_media_token VARCHAR(32) DEFAULT NULL, + + -- ========== CREATOR ========== + + -- The user who created this character. + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + creator_ip_address VARCHAR(40) NOT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP DEFAULT NULL, + + -- ========== INDICES ========== + + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY index_maybe_creator_user_token (maybe_creator_user_token), + KEY index_is_active (is_active), + KEY index_created_at (created_at), + KEY index_deleted_at (deleted_at) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_database/sql/migrations_todo/2026-03-29-150026-0000_character_third_party_entities/down.sql b/_database/sql/migrations_todo/2026-03-29-150026-0000_character_third_party_entities/down.sql new file mode 100644 index 000000000..664bbb5cd --- /dev/null +++ b/_database/sql/migrations_todo/2026-03-29-150026-0000_character_third_party_entities/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE IF EXISTS character_third_party_entities; diff --git a/_database/sql/migrations_todo/2026-03-29-150026-0000_character_third_party_entities/up.sql b/_database/sql/migrations_todo/2026-03-29-150026-0000_character_third_party_entities/up.sql new file mode 100644 index 000000000..1cb3e4c6a --- /dev/null +++ b/_database/sql/migrations_todo/2026-03-29-150026-0000_character_third_party_entities/up.sql @@ -0,0 +1,60 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE character_third_party_entities ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Foreign key to the characters table. + character_token VARCHAR(32) NOT NULL, + + -- Whether this third-party entity is active and usable. + is_active BOOLEAN NOT NULL DEFAULT false, + + -- ========== THIRD PARTY DETAILS ========== + + -- Which third-party system the character lives in (enum crate type). + -- e.g. "kinovi", "runway", etc. + third_party VARCHAR(32) NOT NULL, + + -- The identifier in the third-party system. + -- e.g. Kinovi's "char_1774752056469_2wlxoq" + third_party_id VARCHAR(64) NOT NULL, + + -- Which types of models can use this character (enum crate type). + -- e.g. "kinovi_seedance_2_pro", "kinovi_seedance_2_fast", etc. + third_party_scopes VARCHAR(32) NOT NULL, + + -- The name used by the third party. + -- Important for Kinovi since characters are referenced by name in prompts. + third_party_name VARCHAR(255) NOT NULL, + + -- An upstream third party of the third party (if any). + -- e.g. Kinovi's assetId "asset-20260329104101-kk5kl" + maybe_fourth_party_id VARCHAR(64) DEFAULT NULL, + + -- ========== JOB TRACKING ========== + + -- Our job system token (if the character was created via a job). + maybe_origin_job_token VARCHAR(32) DEFAULT NULL, + + -- Third party's job system token (if any). + maybe_origin_third_party_job_id VARCHAR(64) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP DEFAULT NULL, + + -- ========== INDICES ========== + + PRIMARY KEY (id), + UNIQUE KEY unique_third_party_id (third_party, third_party_id), + KEY index_character_token (character_token), + KEY index_third_party (third_party), + KEY index_third_party_id (third_party_id), + KEY index_is_active (is_active) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_database/sql/migrations_todo/2026-03-29-150034-0000_character_creation_assets/down.sql b/_database/sql/migrations_todo/2026-03-29-150034-0000_character_creation_assets/down.sql new file mode 100644 index 000000000..57dd167c0 --- /dev/null +++ b/_database/sql/migrations_todo/2026-03-29-150034-0000_character_creation_assets/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE IF EXISTS character_creation_assets; diff --git a/_database/sql/migrations_todo/2026-03-29-150034-0000_character_creation_assets/up.sql b/_database/sql/migrations_todo/2026-03-29-150034-0000_character_creation_assets/up.sql new file mode 100644 index 000000000..c12446f40 --- /dev/null +++ b/_database/sql/migrations_todo/2026-03-29-150034-0000_character_creation_assets/up.sql @@ -0,0 +1,26 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Stores the media files (images) used as input when creating a character. +CREATE TABLE character_creation_assets ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Foreign key to the characters table. + character_token VARCHAR(32) NOT NULL, + + -- The media file used as a creation input (e.g. reference image). + asset_media_file_token VARCHAR(32) NOT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- ========== INDICES ========== + + PRIMARY KEY (id), + KEY index_character_token (character_token), + KEY index_asset_media_file_token (asset_media_file_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_database/sql/migrations_todo/2026-03-29-150042-0000_character_creation_prompts/down.sql b/_database/sql/migrations_todo/2026-03-29-150042-0000_character_creation_prompts/down.sql new file mode 100644 index 000000000..a2defc9a8 --- /dev/null +++ b/_database/sql/migrations_todo/2026-03-29-150042-0000_character_creation_prompts/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE IF EXISTS character_creation_prompts; diff --git a/_database/sql/migrations_todo/2026-03-29-150042-0000_character_creation_prompts/up.sql b/_database/sql/migrations_todo/2026-03-29-150042-0000_character_creation_prompts/up.sql new file mode 100644 index 000000000..397513ee3 --- /dev/null +++ b/_database/sql/migrations_todo/2026-03-29-150042-0000_character_creation_prompts/up.sql @@ -0,0 +1,25 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Stores prompts/descriptions used when creating a character. +CREATE TABLE character_creation_prompts ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Foreign key to the characters table. + character_token VARCHAR(32) NOT NULL, + + -- The prompt or description used during character creation. + prompt TEXT DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + -- ========== INDICES ========== + + PRIMARY KEY (id), + KEY index_character_token (character_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/characters/create_character.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/create_character.rs new file mode 100644 index 000000000..92b9a768d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/create_character.rs @@ -0,0 +1,26 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use enums::common::generation::common_model_type::CommonModelType; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; + +/// Request body for creating a character. +#[derive(Deserialize, ToSchema)] +pub struct CreateCharacterRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// The model to create the character for. + pub model: CommonModelType, + + /// The reference image media file token. + pub image_media_token: MediaFileToken, +} + +/// Response body for creating a character. +#[derive(Serialize, ToSchema)] +pub struct CreateCharacterResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/characters/delete_character.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/delete_character.rs new file mode 100644 index 000000000..a752672db --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/delete_character.rs @@ -0,0 +1,16 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::characters::CharacterToken; + +/// Path parameters for deleting a character. +#[derive(Deserialize, ToSchema)] +pub struct DeleteCharacterPathInfo { + pub character_token: CharacterToken, +} + +/// Response body for deleting a character. +#[derive(Serialize, ToSchema)] +pub struct DeleteCharacterResponse { + pub success: bool, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/characters/edit_character.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/edit_character.rs new file mode 100644 index 000000000..fa2c4a483 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/edit_character.rs @@ -0,0 +1,23 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use tokens::tokens::characters::CharacterToken; + +/// Request body for editing a character. +#[derive(Deserialize, ToSchema)] +pub struct EditCharacterRequest { + /// The character to edit. + pub token: CharacterToken, + + /// Updated name (if changing). + pub updated_name: Option, + + /// Updated description (if changing). + pub updated_description: Option, +} + +/// Response body for editing a character. +#[derive(Serialize, ToSchema)] +pub struct EditCharacterResponse { + pub success: bool, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/characters/get_character.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/get_character.rs new file mode 100644 index 000000000..3a8a3cf7e --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/get_character.rs @@ -0,0 +1,36 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::common::responses::media_links::MediaLinks; +use enums::common::generation::common_model_type::CommonModelType; +use tokens::tokens::characters::CharacterToken; + +/// Path parameters for getting a character. +#[derive(Deserialize, ToSchema)] +pub struct GetCharacterPathInfo { + pub character_token: CharacterToken, +} + +/// Response body for getting a character. +#[derive(Serialize, ToSchema)] +pub struct GetCharacterResponse { + pub success: bool, + pub character: GetCharacterDetails, +} + +/// Full character details returned by the get endpoint. +#[derive(Serialize, ToSchema)] +pub struct GetCharacterDetails { + pub token: CharacterToken, + + /// Which models this character can be used with. + pub models: Vec, + + pub name: String, + + pub maybe_description: Option, + + pub maybe_avatar: Option, + + pub maybe_full_image: Option, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/characters/list_characters.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/list_characters.rs new file mode 100644 index 000000000..348ad2e64 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/list_characters.rs @@ -0,0 +1,38 @@ +use serde_derive::{Deserialize, Serialize}; +use utoipa::{IntoParams, ToSchema}; + +use crate::common::responses::media_links::MediaLinks; +use enums::common::generation::common_model_type::CommonModelType; +use tokens::tokens::characters::CharacterToken; + +/// Query string parameters for listing characters. +#[derive(Deserialize, IntoParams)] +pub struct ListCharactersQuery { + /// Optional cursor for pagination. + pub cursor: Option, +} + +/// Response body for listing characters in the current session. +#[derive(Serialize, ToSchema)] +pub struct ListCharactersResponse { + pub success: bool, + pub characters: Vec, + pub next_cursor: Option, +} + +/// A character entry in the list response. +#[derive(Serialize, ToSchema)] +pub struct ListCharactersEntry { + pub token: CharacterToken, + + /// Which models this character can be used with. + pub models: Vec, + + pub name: String, + + pub maybe_description: Option, + + pub maybe_avatar: Option, + + pub maybe_full_image: Option, +} diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/characters/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/mod.rs new file mode 100644 index 000000000..bb9070c8b --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/characters/mod.rs @@ -0,0 +1,5 @@ +pub mod create_character; +pub mod delete_character; +pub mod edit_character; +pub mod get_character; +pub mod list_characters; diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/lib.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/lib.rs index 5870f2f57..a161c4065 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/lib.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/lib.rs @@ -1,4 +1,5 @@ pub mod analytics; +pub mod characters; pub mod common; pub mod credits; pub mod generate; diff --git a/crates/schema/public/tokens/src/prefixes.rs b/crates/schema/public/tokens/src/prefixes.rs index 173b333b7..b1cf8630e 100644 --- a/crates/schema/public/tokens/src/prefixes.rs +++ b/crates/schema/public/tokens/src/prefixes.rs @@ -28,6 +28,7 @@ pub(crate) enum TokenPrefix { BatchGeneration, BetaKey, BrowserSessionLog, + Character, Comment, DownloadJob, EmailSenderJob, @@ -111,6 +112,7 @@ impl PrefixGenerator for TokenPrefix { Self::BatchGeneration => "batch_g_", Self::BetaKey => "beta_key_", Self::BrowserSessionLog => "bsl_", + Self::Character => "character_", Self::Comment => "comment_", Self::DownloadJob => "jdown_", // NB: Previously "JGUP:" Self::EmailSenderJob => "email_job_", @@ -339,6 +341,7 @@ mod tests { || prefix == "email_job_" || prefix == "pw_reset_" || prefix == "tts_task_" + || prefix == "app_session_" { // TODO/FIXME: I'm too tired at 5AM to replacen from the left. Make this test valid. // These tokens are from the AIChatBot sidecar, so asserting their validity is less important. diff --git a/crates/schema/public/tokens/src/tokens/characters.rs b/crates/schema/public/tokens/src/tokens/characters.rs new file mode 100644 index 000000000..de5208a2e --- /dev/null +++ b/crates/schema/public/tokens/src/tokens/characters.rs @@ -0,0 +1,17 @@ +use std::fmt::Debug; + +use serde::Deserialize; +use serde::Serialize; +use utoipa::ToSchema; + +use crate::prefixes::TokenPrefix; + +/// The primary key for Characters +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, ToSchema)] +#[cfg_attr(feature = "database", derive(sqlx::Type))] +#[cfg_attr(feature = "database", sqlx(transparent))] +pub struct CharacterToken(pub String); + +impl_string_token!(CharacterToken); +impl_mysql_token_from_row!(CharacterToken); +impl_crockford_generator!(CharacterToken, 32usize, TokenPrefix::Character, CrockfordLower); diff --git a/crates/schema/public/tokens/src/tokens/mod.rs b/crates/schema/public/tokens/src/tokens/mod.rs index 580677f00..95709879e 100644 --- a/crates/schema/public/tokens/src/tokens/mod.rs +++ b/crates/schema/public/tokens/src/tokens/mod.rs @@ -6,6 +6,7 @@ pub mod audit_logs; pub mod batch_generations; pub mod beta_keys; pub mod browser_session_logs; +pub mod characters; pub mod comments; pub mod email_sender_job_token; pub mod firehose_entries; diff --git a/crates/service/web/storyteller_web/src/docs/api_doc.rs b/crates/service/web/storyteller_web/src/docs/api_doc.rs index cd7ef2831..f92f63737 100644 --- a/crates/service/web/storyteller_web/src/docs/api_doc.rs +++ b/crates/service/web/storyteller_web/src/docs/api_doc.rs @@ -279,6 +279,11 @@ use artcraft_api_defs::subscriptions::get_session_subscription::*; use artcraft_api_defs::media_file::list_batch_generated_media_files::*; // Handler modules with locally-defined types use crate::http_server::endpoints::moderation::info::moderator_token_info_handler::*; +use artcraft_api_defs::characters::create_character::*; +use artcraft_api_defs::characters::delete_character::*; +use artcraft_api_defs::characters::edit_character::*; +use artcraft_api_defs::characters::get_character::*; +use artcraft_api_defs::characters::list_characters::*; use artcraft_api_defs::moderation::alerts::moderation_send_alert::*; use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::*; use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::*; @@ -476,6 +481,12 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux crate::http_server::endpoints::voice_conversion::enqueue_seed_vc_inference_handler::enqueue_infer_seed_vc_handler, // TTS crate::http_server::endpoints::tts::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler::enqueue_infer_f5_tts_handler, + // Characters + crate::http_server::endpoints::characters::list_characters_handler::list_characters_handler, + crate::http_server::endpoints::characters::create_character_handler::create_character_handler, + crate::http_server::endpoints::characters::get_character_handler::get_character_handler, + crate::http_server::endpoints::characters::edit_character_handler::edit_character_handler, + crate::http_server::endpoints::characters::delete_character_handler::delete_character_handler, // Omni Gen crate::http_server::endpoints::omni_gen::cost::image::omni_gen_image_cost_handler::omni_gen_image_cost_handler, crate::http_server::endpoints::omni_gen::cost::video::omni_gen_video_cost_handler::omni_gen_video_cost_handler, @@ -693,6 +704,17 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux ModeratorUserLookupRequest, ModeratorUserLookupSuccessResponse, ModeratorUserLookupUserDetails, + CreateCharacterRequest, + CreateCharacterResponse, + DeleteCharacterPathInfo, + DeleteCharacterResponse, + EditCharacterRequest, + EditCharacterResponse, + GetCharacterDetails, + GetCharacterPathInfo, + GetCharacterResponse, + ListCharactersEntry, + ListCharactersResponse, OmniGenImageCostAndGenerateRequest, OmniGenImageCostResponse, OmniGenImageGenerateResponse, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/create_character_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/create_character_handler.rs new file mode 100644 index 000000000..1f0615fdd --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/create_character_handler.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::characters::create_character::{CreateCharacterRequest, CreateCharacterResponse}; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::state::server_state::ServerState; + +/// Create a new character. +#[utoipa::path( + post, + tag = "Characters", + path = "/v1/character/create", + request_body = CreateCharacterRequest, + responses( + (status = 200, description = "Success", body = CreateCharacterResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Unauthorized"), + (status = 402, description = "Payment required"), + (status = 500, description = "Server error"), + ), +)] +pub async fn create_character_handler( + _http_request: HttpRequest, + _request: Json, + _server_state: web::Data>, +) -> Result, CommonWebError> { + todo!() +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/delete_character_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/delete_character_handler.rs new file mode 100644 index 000000000..6ef15cccc --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/delete_character_handler.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use actix_web::web::{Json, Path}; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::characters::delete_character::{DeleteCharacterPathInfo, DeleteCharacterResponse}; +use tokens::tokens::characters::CharacterToken; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::state::server_state::ServerState; + +/// Delete a character. +#[utoipa::path( + delete, + tag = "Characters", + path = "/v1/character/{character_token}", + responses( + (status = 200, description = "Success", body = DeleteCharacterResponse), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Not found"), + (status = 500, description = "Server error"), + ), + params( + ("character_token" = CharacterToken, Path, description = "Character token"), + ) +)] +pub async fn delete_character_handler( + _http_request: HttpRequest, + _path: Path, + _server_state: web::Data>, +) -> Result, CommonWebError> { + todo!() +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/edit_character_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/edit_character_handler.rs new file mode 100644 index 000000000..709279938 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/edit_character_handler.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; + +use artcraft_api_defs::characters::edit_character::{EditCharacterRequest, EditCharacterResponse}; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::state::server_state::ServerState; + +/// Edit a character's name or description. +#[utoipa::path( + post, + tag = "Characters", + path = "/v1/character/edit", + request_body = EditCharacterRequest, + responses( + (status = 200, description = "Success", body = EditCharacterResponse), + (status = 400, description = "Bad input"), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Not found"), + (status = 500, description = "Server error"), + ), +)] +pub async fn edit_character_handler( + _http_request: HttpRequest, + _request: Json, + _server_state: web::Data>, +) -> Result, CommonWebError> { + todo!() +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/get_character_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/get_character_handler.rs new file mode 100644 index 000000000..49483b113 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/get_character_handler.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; + +use actix_web::web::{Json, Path}; +use actix_web::{web, HttpRequest}; +use log::warn; + +use artcraft_api_defs::characters::get_character::{GetCharacterDetails, GetCharacterPathInfo, GetCharacterResponse}; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::common::generation::common_model_type::CommonModelType; +use mysql_queries::queries::media_files::get::get_media_file::get_media_file; +use tokens::tokens::characters::CharacterToken; +use tokens::tokens::media_files::MediaFileToken; + +use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::state::server_state::ServerState; + +/// Get a character by token. +#[utoipa::path( + get, + tag = "Characters", + path = "/v1/character/{character_token}", + responses( + (status = 200, description = "Success", body = GetCharacterResponse), + (status = 404, description = "Not found"), + (status = 500, description = "Server error"), + ), + params( + ("character_token" = CharacterToken, Path, description = "Character token"), + ) +)] +pub async fn get_character_handler( + http_request: HttpRequest, + path: Path, + server_state: web::Data>, +) -> Result, CommonWebError> { + // TODO: Replace with real character query. This is test data using a real media file (Ernest). + + let media_domain = get_media_domain(&http_request); + let ernest_media_token = MediaFileToken::new_from_str("m_1997908e4tcbaj34wa0s5bvr4y4szt"); + + let maybe_links = match get_media_file(&ernest_media_token, false, &server_state.mysql_pool).await { + Ok(Some(record)) => { + let path = MediaFileBucketPath::from_object_hash( + &record.public_bucket_directory_hash, + record.maybe_public_bucket_prefix.as_deref(), + record.maybe_public_bucket_extension.as_deref(), + ); + Some(MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &path)) + } + Ok(None) => { + warn!("Ernest media file not found in database"); + None + } + Err(err) => { + warn!("Failed to look up Ernest media file: {:?}", err); + None + } + }; + + let character = GetCharacterDetails { + token: path.character_token.clone(), + models: vec![CommonModelType::Seedance2p0], + name: "Ernest".to_string(), + maybe_description: Some("Ernest P. Worrell".to_string()), + maybe_avatar: maybe_links.clone(), + maybe_full_image: maybe_links, + }; + + Ok(Json(GetCharacterResponse { + success: true, + character, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/list_characters_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/list_characters_handler.rs new file mode 100644 index 000000000..4d66226bb --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/list_characters_handler.rs @@ -0,0 +1,91 @@ +use std::sync::Arc; + +use actix_web::web::{Json, Query}; +use actix_web::{web, HttpRequest}; +use log::warn; + +use artcraft_api_defs::characters::list_characters::{ListCharactersEntry, ListCharactersQuery, ListCharactersResponse}; +use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath; +use enums::common::generation::common_model_type::CommonModelType; +use mysql_queries::queries::media_files::get::batch_get_media_files_by_tokens::batch_get_media_files_by_tokens; +use tokens::tokens::characters::CharacterToken; +use tokens::tokens::media_files::MediaFileToken; + +use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder; +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain; +use crate::state::server_state::ServerState; + +/// List characters for the current session. +#[utoipa::path( + get, + tag = "Characters", + path = "/v1/characters/session", + params(ListCharactersQuery), + responses( + (status = 200, description = "Success", body = ListCharactersResponse), + (status = 401, description = "Unauthorized"), + (status = 500, description = "Server error"), + ), +)] +pub async fn list_characters_handler( + http_request: HttpRequest, + _query: Query, + server_state: web::Data>, +) -> Result, CommonWebError> { + // TODO: Replace with real character queries. This is test data using real media files. + + let media_domain = get_media_domain(&http_request); + + struct TestCharacter { + token: &'static str, + name: &'static str, + description: &'static str, + media_token: &'static str, + } + + let test_characters = vec![ + TestCharacter { token: "character_mock_ernest_001", name: "Ernest", description: "Ernest P. Worrell", media_token: "m_1997908e4tcbaj34wa0s5bvr4y4szt" }, + TestCharacter { token: "character_mock_juno_002", name: "Juno", description: "Juno the corgi", media_token: "m_m1bz02z1kkzanxy6rb4vk1kvq9de9g" }, + TestCharacter { token: "character_mock_mochi_003", name: "Mochi", description: "Mochi with glasses", media_token: "m_5y81rcnpnrbey55da6cc2s9z4d2f45" }, + ]; + + let media_tokens: Vec = test_characters.iter() + .map(|c| MediaFileToken::new_from_str(c.media_token)) + .collect(); + + let media_records = batch_get_media_files_by_tokens(&server_state.mysql_pool, &media_tokens, false) + .await + .unwrap_or_else(|err| { + warn!("Failed to look up media files for test characters: {:?}", err); + vec![] + }); + + let characters = test_characters.iter().map(|tc| { + let maybe_links = media_records.iter() + .find(|r| r.token.as_str() == tc.media_token) + .map(|r| { + let path = MediaFileBucketPath::from_object_hash( + &r.public_bucket_directory_hash, + r.maybe_public_bucket_prefix.as_deref(), + r.maybe_public_bucket_extension.as_deref(), + ); + MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &path) + }); + + ListCharactersEntry { + token: CharacterToken::new_from_str(tc.token), + models: vec![CommonModelType::Seedance2p0], + name: tc.name.to_string(), + maybe_description: Some(tc.description.to_string()), + maybe_avatar: maybe_links.clone(), + maybe_full_image: maybe_links, + } + }).collect(); + + Ok(Json(ListCharactersResponse { + success: true, + characters, + next_cursor: None, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/characters/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/mod.rs new file mode 100644 index 000000000..3bad866c7 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/characters/mod.rs @@ -0,0 +1,5 @@ +pub mod create_character_handler; +pub mod delete_character_handler; +pub mod edit_character_handler; +pub mod get_character_handler; +pub mod list_characters_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs index f5ec36bfc..016be34e7 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs @@ -1,6 +1,7 @@ pub mod analytics; pub mod app_state; pub mod beta_keys; +pub mod characters; pub mod comments; pub mod credits; pub mod download_job; diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/add_application_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/add_application_routes.rs index cd4ac848e..3a606d47d 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/add_application_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/add_application_routes.rs @@ -1,4 +1,5 @@ use crate::http_server::routes::application_routes::analytics_routes::add_analytics_routes; +use crate::http_server::routes::application_routes::character_routes::add_character_routes; use crate::http_server::routes::application_routes::comments_routes::add_comments_routes; use crate::http_server::routes::application_routes::credits_routes::add_credits_routes; use crate::http_server::routes::application_routes::featured_item_routes::add_featured_item_routes; @@ -42,6 +43,7 @@ where // Artcraft surface area app = add_generate_routes(app); // /v1/generate/... app = add_omni_gen_routes(app); // /v1/omni_gen/... + app = add_character_routes(app); // /v1/character/... and /v1/characters/... app = add_webhook_routes(app); // /v1/webhooks/... (fal) // Legacy FakeYou surface area that might be useful again one day diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/character_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/character_routes.rs new file mode 100644 index 000000000..3360c7cb3 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/character_routes.rs @@ -0,0 +1,45 @@ +use actix_http::body::MessageBody; +use actix_service::ServiceFactory; +use actix_web::dev::{ServiceRequest, ServiceResponse}; +use actix_web::{web, App, Error, HttpResponse}; + +use crate::http_server::endpoints::characters::create_character_handler::create_character_handler; +use crate::http_server::endpoints::characters::delete_character_handler::delete_character_handler; +use crate::http_server::endpoints::characters::edit_character_handler::edit_character_handler; +use crate::http_server::endpoints::characters::get_character_handler::get_character_handler; +use crate::http_server::endpoints::characters::list_characters_handler::list_characters_handler; + +pub fn add_character_routes(app: App) -> App +where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app + // List characters for the current session + .service(web::resource("/v1/characters/session") + .route(web::get().to(list_characters_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + // Create a new character + .service(web::resource("/v1/character/create") + .route(web::post().to(create_character_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + // Edit a character + .service(web::resource("/v1/character/edit") + .route(web::post().to(edit_character_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + // Get or delete a character by token + .service(web::resource("/v1/character/{character_token}") + .route(web::get().to(get_character_handler)) + .route(web::delete().to(delete_character_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/mod.rs b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/mod.rs index 5df0870d6..286be45a5 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/application_routes/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/application_routes/mod.rs @@ -1,4 +1,5 @@ mod analytics_routes; +mod character_routes; mod comments_routes; mod credits_routes; mod featured_item_routes; diff --git a/includes/binary_includes/usernames/reserved_usernames.txt b/includes/binary_includes/usernames/reserved_usernames.txt index 730c02767..f9c90e8d7 100644 --- a/includes/binary_includes/usernames/reserved_usernames.txt +++ b/includes/binary_includes/usernames/reserved_usernames.txt @@ -10,6 +10,27 @@ art_craft artcraft hanashi +# Models +chatgpt +flux +gemini +gpt +grok +grokimagine +ltx +midjourney +nanobanana +nanobanana2 +nanobananapro +seedance +seedance2 +seedream +sora +veo +veo2 +veo3 +wan + # Possible uses # See https://www.wired.com/story/by-seizing-music-elon-musk-shows-he-doesnt-know-what-made-twitter-good/ 3d @@ -282,6 +303,7 @@ category channel channels character +characters checkout_cancel checkout_success clone @@ -319,6 +341,7 @@ search signup stream studio +studios terms testing text_to_image @@ -621,6 +644,7 @@ upvote amazon anthropic apple +bytedance cash cashapp epic