From 519d30e3b05ca8ea3b7f33dc1a13a2a45ab65091 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 2 Apr 2026 21:30:34 -0400 Subject: [PATCH] lookup characters in seedance --- .../seedance_2p0_multi_function_video_gen.rs | 5 ++ .../generate_video_artcraft_seedance2p0.rs | 1 + ...ookup_characters_by_token_for_prompting.rs | 82 +++++++++++++++++++ .../src/queries/characters/mod.rs | 1 + ...ce_2p0_multi_function_video_gen_handler.rs | 57 ++++++++++++- 5 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 crates/schema/database/mysql_queries/src/queries/characters/batch_lookup_characters_by_token_for_prompting.rs diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs index 533af8f3ad..d155b36239 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use tokens::tokens::characters::CharacterToken; use tokens::tokens::generic_inference_jobs::InferenceJobToken; use tokens::tokens::media_files::MediaFileToken; use utoipa::ToSchema; @@ -29,6 +30,10 @@ pub struct Seedance2p0MultiFunctionVideoGenRequest { /// Optional reference audio files. pub reference_audio_media_tokens: Option>, + /// Optional character tokens to reference in the prompt. + /// Characters are referenced in prompts as @CharacterName. + pub reference_character_tokens: Option>, + /// Video aspect ratio / resolution. pub aspect_ratio: Option, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_seedance2p0.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_seedance2p0.rs index 18b7c6054c..849d828251 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_seedance2p0.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/artcraft/generate_video_artcraft_seedance2p0.rs @@ -23,6 +23,7 @@ pub async fn execute_artcraft_seedance2p0( aspect_ratio: plan.aspect_ratio, duration_seconds: plan.duration_seconds, batch_count: Some(plan.batch_count), + reference_character_tokens: None, }; let response = seedance_2p0_multi_function_video_gen( diff --git a/crates/schema/database/mysql_queries/src/queries/characters/batch_lookup_characters_by_token_for_prompting.rs b/crates/schema/database/mysql_queries/src/queries/characters/batch_lookup_characters_by_token_for_prompting.rs new file mode 100644 index 0000000000..8c9a01ee57 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/characters/batch_lookup_characters_by_token_for_prompting.rs @@ -0,0 +1,82 @@ +use anyhow::anyhow; +use log::warn; +use sqlx::pool::PoolConnection; +use sqlx::MySql; + +use tokens::tokens::characters::CharacterToken; + +/// Character data needed for constructing prompts with character references. +#[derive(Debug)] +pub struct CharacterPromptData { + pub character_token: CharacterToken, + pub is_active: bool, + pub character_name: Option, + pub kinovi_character_id: Option, + pub kinovi_character_name: Option, +} + +/// Look up multiple characters by their tokens, returning the data needed for prompting. +/// +/// Characters that are not found are silently omitted from the results. +pub async fn batch_lookup_characters_by_token_for_prompting( + tokens: &[CharacterToken], + connection: &mut PoolConnection, +) -> anyhow::Result> { + if tokens.is_empty() { + return Ok(Vec::new()); + } + + // SQLx doesn't support IN-clause with dynamic lists in query! macro, + // so we build the query manually with placeholders. + let placeholders: Vec<&str> = tokens.iter().map(|_| "?").collect(); + let in_clause = placeholders.join(", "); + + let sql = format!( + r#" +SELECT + token, + is_active, + character_name, + kinovi_character_id, + kinovi_character_name +FROM characters +WHERE token IN ({}) + AND deleted_at IS NULL + "#, + in_clause, + ); + + let mut query = sqlx::query_as::<_, RawRow>(&sql); + + for token in tokens { + query = query.bind(token.as_str()); + } + + let rows = query + .fetch_all(&mut **connection) + .await + .map_err(|err| anyhow!("Error looking up characters by token: {:?}", err))?; + + let results = rows.into_iter().map(|row| { + CharacterPromptData { + character_token: row.token, + is_active: row.is_active, + character_name: row.character_name, + kinovi_character_id: row.kinovi_character_id, + kinovi_character_name: row.kinovi_character_name, + } + }).collect(); + + Ok(results) +} + +// =============== Private helpers =============== + +#[derive(sqlx::FromRow)] +struct RawRow { + token: CharacterToken, + is_active: bool, + character_name: Option, + kinovi_character_id: Option, + kinovi_character_name: Option, +} diff --git a/crates/schema/database/mysql_queries/src/queries/characters/mod.rs b/crates/schema/database/mysql_queries/src/queries/characters/mod.rs index ec82ecd953..fbac52e67f 100644 --- a/crates/schema/database/mysql_queries/src/queries/characters/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/characters/mod.rs @@ -1 +1,2 @@ +pub mod batch_lookup_characters_by_token_for_prompting; pub mod create_pending_character; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs index 9ec0161a03..c92101e233 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs @@ -25,6 +25,7 @@ use enums::common::generation_provider::GenerationProvider; use enums::common::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use log::{error, info, warn}; +use mysql_queries::queries::characters::batch_lookup_characters_by_token_for_prompting::batch_lookup_characters_by_token_for_prompting; use mysql_queries::queries::generic_inference::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::{ insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token, InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs, @@ -142,6 +143,56 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( ).await? }; + // --- Look up character tokens (if any) --- + + let kinovi_character_ids: Option> = if let Some(character_tokens) = request.reference_character_tokens.as_ref() { + if !character_tokens.is_empty() { + let characters = batch_lookup_characters_by_token_for_prompting( + character_tokens, + &mut mysql_connection, + ) + .await + .map_err(|err| { + error!("Error looking up characters: {:?}", err); + AdvancedCommonWebError::from_anyhow_error(err) + })?; + + // Verify all requested characters were found. + if characters.len() != character_tokens.len() { + warn!("Not all character tokens were found: requested {}, found {}", character_tokens.len(), characters.len()); + return Err(AdvancedCommonWebError::BadInputWithSimpleMessage( + "One or more character tokens were not found.".to_string() + )); + } + + // Verify all characters are active. + for character in &characters { + if !character.is_active { + warn!("Character {} is not active", character.character_token); + return Err(AdvancedCommonWebError::BadInputWithSimpleMessage( + format!("Character {} is not yet ready for use.", character.character_token) + )); + } + } + + // Extract the kinovi character IDs. + let ids: Vec = characters.iter() + .filter_map(|c| c.kinovi_character_id.clone()) + .collect(); + + if ids.is_empty() { + warn!("Characters found but none have kinovi_character_id"); + None + } else { + Some(ids) + } + } else { + None + } + } else { + None + }; + // --- Insert idempotency token --- insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection) @@ -191,6 +242,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( resolution, batch_count, duration_seconds, + kinovi_character_ids.clone(), ).await; match result { @@ -212,6 +264,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( resolution, batch_count, duration_seconds, + kinovi_character_ids.clone(), ).await; match result { @@ -248,6 +301,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( resolution, batch_count, duration_seconds, + kinovi_character_ids, ).await; match result { @@ -496,6 +550,7 @@ async fn upload_and_generate( resolution: Resolution, batch_count: BatchCount, duration_seconds: u8, + kinovi_character_ids: Option>, ) -> Result { // --- Upload files to seedance2pro CDN --- @@ -570,7 +625,7 @@ async fn upload_and_generate( reference_image_urls, reference_video_urls, reference_audio_urls, - character_ids: None, + character_ids: kinovi_character_ids, use_face_blur_hack: None, host_override: None, };