diff --git a/crates/artcraftx/src/commands/generate/common/generation_credential.rs b/crates/artcraftx/src/commands/generate/common/generation_credential.rs new file mode 100644 index 00000000..9b3e55a1 --- /dev/null +++ b/crates/artcraftx/src/commands/generate/common/generation_credential.rs @@ -0,0 +1,58 @@ +use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet; + +use crate::commands::enqueue::generate_error::{CredentialProblemReason, GenerateError}; +use crate::credentials::credential::Credential; +use crate::state::data_dir::app_data_root::AppDataRoot; + +/// Resolve the stored credential a generation request names via +/// `credential_id`. Every failure is a [`GenerateError::CredentialProblem`], +/// which flashes the frontend's dismissable credential-error modal. +pub fn resolve_generation_credential( + maybe_credential_id: Option<&str>, + app_data_root: &AppDataRoot, +) -> Result { + let credential_id = maybe_credential_id + .filter(|id| !id.trim().is_empty()) + .ok_or(GenerateError::CredentialProblem( + CredentialProblemReason::NoCredentialSupplied, + ))?; + + let maybe_credential = app_data_root + .credentials_dir() + .find_credential_by_id(credential_id) + .map_err(GenerateError::from)?; + + maybe_credential.ok_or_else(|| { + GenerateError::CredentialProblem(CredentialProblemReason::CredentialNotFound { + credential_id: credential_id.to_string(), + }) + }) +} + +/// Rebuild the web-session credential set from a stored cookie credential. +pub fn storyteller_creds_from_credential( + credential: &Credential, +) -> Result { + let cookie = credential.cookies().ok_or_else(|| { + credential_not_usable(credential, "the account has no session cookies") + })?; + + StorytellerCredentialSet::parse_multi_cookie_header(&cookie.cookie_header) + .map_err(|err| { + credential_not_usable( + credential, + &format!("the stored session cookies could not be parsed ({})", err), + ) + })? + .filter(|creds| !creds.is_empty()) + .ok_or_else(|| { + credential_not_usable(credential, "the stored session cookies are empty") + }) +} + +pub fn credential_not_usable(credential: &Credential, reason: &str) -> GenerateError { + GenerateError::CredentialProblem(CredentialProblemReason::CredentialNotUsable { + credential_id: credential.id.to_string(), + reason: reason.to_string(), + }) +} diff --git a/crates/artcraftx/src/commands/generate/common/mod.rs b/crates/artcraftx/src/commands/generate/common/mod.rs index 7f7bebef..f4c42703 100644 --- a/crates/artcraftx/src/commands/generate/common/mod.rs +++ b/crates/artcraftx/src/commands/generate/common/mod.rs @@ -1,2 +1,3 @@ +pub mod generation_credential; pub mod router_image_request_to_artcraft_prompt; pub mod router_video_request_to_artcraft_prompt; diff --git a/crates/artcraftx/src/commands/generate/generate_image/providers/router/handle_credential_router.rs b/crates/artcraftx/src/commands/generate/generate_image/providers/router/handle_credential_router.rs index d0a7502e..015d28fc 100644 --- a/crates/artcraftx/src/commands/generate/generate_image/providers/router/handle_credential_router.rs +++ b/crates/artcraftx/src/commands/generate/generate_image/providers/router/handle_credential_router.rs @@ -1,5 +1,3 @@ -use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet; -use artcraft_client::utils::api_host::ApiHost; use enums::common::generation_provider::GenerationProvider; use enums::tauri::tasks::task_type::TaskType; use log::{info, warn}; @@ -15,8 +13,9 @@ use tokens::tokens::media_files::MediaFileToken; use crate::api_adapters::models::image::tauri_image_model_to_generation_model::tauri_image_model_to_generation_model; use crate::api_adapters::models::image::tauri_image_model_to_router_model::tauri_image_model_to_router_model; -use crate::commands::enqueue::generate_error::{CredentialProblemReason, GenerateError}; +use crate::commands::enqueue::generate_error::GenerateError; use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; +use crate::commands::generate::common::generation_credential::{credential_not_usable, resolve_generation_credential, storyteller_creds_from_credential}; use crate::commands::generate::generate_image::providers::router::handle_api_providers::handle_api_key_provider; use crate::commands::generate::generate_image::providers::router::utils::convert_enums_to_router::{convert_aspect_ratio, convert_quality, convert_resolution}; use crate::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest; @@ -37,7 +36,10 @@ pub async fn handle_credential_router( app_env_configs: &AppEnvConfigs, storyteller_creds_manager: &StorytellerCredentialManager, ) -> Result { - let credential = resolve_credential(request, app_data_root)?; + let credential = resolve_generation_credential( + request.credential_id.as_deref(), + app_data_root, + )?; info!( "handle_credential_router: credential={} service={}", @@ -70,29 +72,6 @@ pub async fn handle_credential_router( } } -/// Require and load the credential named by the request. -fn resolve_credential( - request: &TauriGenerateImageRequest, - app_data_root: &AppDataRoot, -) -> Result { - let credential_id = request.credential_id.as_deref() - .filter(|id| !id.trim().is_empty()) - .ok_or(GenerateError::CredentialProblem( - CredentialProblemReason::NoCredentialSupplied, - ))?; - - let maybe_credential = app_data_root - .credentials_dir() - .find_credential_by_id(credential_id) - .map_err(GenerateError::from)?; - - maybe_credential.ok_or_else(|| { - GenerateError::CredentialProblem(CredentialProblemReason::CredentialNotFound { - credential_id: credential_id.to_string(), - }) - }) -} - /// Generate via the router's Artcraft provider, authenticating with the /// stored session cookies. Raw canvas/scene/mask bytes are uploaded to /// ArtCraft first (with the same credential) so the router request only @@ -186,27 +165,6 @@ async fn handle_artcraft_credential( }) } -/// Rebuild the web-session credential set from the stored cookie header. -fn storyteller_creds_from_credential( - credential: &Credential, -) -> Result { - let cookie = credential.cookies().ok_or_else(|| { - credential_not_usable(credential, "the account has no session cookies") - })?; - - StorytellerCredentialSet::parse_multi_cookie_header(&cookie.cookie_header) - .map_err(|err| { - credential_not_usable( - credential, - &format!("the stored session cookies could not be parsed ({})", err), - ) - })? - .filter(|creds| !creds.is_empty()) - .ok_or_else(|| { - credential_not_usable(credential, "the stored session cookies are empty") - }) -} - /// Reference images for the router request: canvas first, then scene, then /// the un-semantic reference images. (Artcraft accepts media tokens /// directly; no URL resolution needed.) @@ -235,9 +193,3 @@ fn collect_image_inputs( } } -fn credential_not_usable(credential: &Credential, reason: &str) -> GenerateError { - GenerateError::CredentialProblem(CredentialProblemReason::CredentialNotUsable { - credential_id: credential.id.to_string(), - reason: reason.to_string(), - }) -} diff --git a/crates/artcraftx/src/commands/generate/generate_video/artcraft/handle_artcraft_video.rs b/crates/artcraftx/src/commands/generate/generate_video/artcraft/handle_artcraft_video.rs index ad6c6be4..86283a37 100644 --- a/crates/artcraftx/src/commands/generate/generate_video/artcraft/handle_artcraft_video.rs +++ b/crates/artcraftx/src/commands/generate/generate_video/artcraft/handle_artcraft_video.rs @@ -26,5 +26,5 @@ pub async fn handle_video_artcraft( let router_model = tauri_video_model_to_router_model(model); let generation_model = tauri_video_model_to_generation_model(model); - handle_artcraft_video_via_router(request, app_env_configs, &creds, router_model, generation_model).await + handle_artcraft_video_via_router(request, &app_env_configs.storyteller_host, &creds, router_model, generation_model).await } diff --git a/crates/artcraftx/src/commands/generate/generate_video/artcraft/handle_artcraft_video_via_router.rs b/crates/artcraftx/src/commands/generate/generate_video/artcraft/handle_artcraft_video_via_router.rs index e9950c26..5fe2419f 100644 --- a/crates/artcraftx/src/commands/generate/generate_video/artcraft/handle_artcraft_video_via_router.rs +++ b/crates/artcraftx/src/commands/generate/generate_video/artcraft/handle_artcraft_video_via_router.rs @@ -2,8 +2,8 @@ use crate::commands::enqueue::generate_error::GenerateError; use crate::commands::generate::generate_video::request::TauriGenerateVideoRequest; use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; use crate::events::generation_events::common::GenerationModel; -use crate::state::app_env_configs::app_env_configs::AppEnvConfigs; use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet; +use artcraft_client::utils::api_host::ApiHost; use router::api::audio_list_ref::AudioListRef; use router::api::character_list_ref::CharacterListRef; use router::api::router_video_model::RouterVideoModel; @@ -21,15 +21,15 @@ use enums::common::generation_provider::GenerationProvider; use enums::tauri::tasks::task_type::TaskType; use log::{error, info}; -pub(super) async fn handle_artcraft_video_via_router( +pub async fn handle_artcraft_video_via_router( request: &TauriGenerateVideoRequest, - app_env_configs: &AppEnvConfigs, + api_host: &ApiHost, creds: &StorytellerCredentialSet, model: RouterVideoModel, generation_model: GenerationModel, ) -> Result { let client = RouterClient::Artcraft(RouterArtcraftClient::new( - app_env_configs.storyteller_host.clone(), + api_host.clone(), creds.clone(), )); @@ -70,6 +70,12 @@ pub(super) async fn handle_artcraft_video_via_router( .map(|p| p.inference_job_token.to_string()) .ok_or(GenerateError::ResponseHadNoJobTokens)?; + info!( + "Router video generation enqueued: inference_job_token={}, response={:?}", + job_id, + response, + ); + Ok(TaskEnqueueSuccess { task_type: TaskType::VideoGeneration, model: Some(generation_model), diff --git a/crates/artcraftx/src/commands/generate/generate_video/artcraft/mod.rs b/crates/artcraftx/src/commands/generate/generate_video/artcraft/mod.rs index 5dd8e997..17cfe93b 100644 --- a/crates/artcraftx/src/commands/generate/generate_video/artcraft/mod.rs +++ b/crates/artcraftx/src/commands/generate/generate_video/artcraft/mod.rs @@ -1,3 +1,3 @@ -mod get_storyteller_creds_or_error; -mod handle_artcraft_video_via_router; -pub (super) mod handle_artcraft_video; +pub mod get_storyteller_creds_or_error; +pub mod handle_artcraft_video_via_router; +pub mod handle_artcraft_video; diff --git a/crates/artcraftx/src/commands/generate/generate_video/generate_video_command.rs b/crates/artcraftx/src/commands/generate/generate_video/generate_video_command.rs index 90151634..a920382f 100644 --- a/crates/artcraftx/src/commands/generate/generate_video/generate_video_command.rs +++ b/crates/artcraftx/src/commands/generate/generate_video/generate_video_command.rs @@ -1,29 +1,19 @@ use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; use crate::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason}; use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; -use crate::commands::generate::generate_video::artcraft::handle_artcraft_video::handle_video_artcraft; -use crate::commands::generate::generate_video::grok::handle_grok_video::handle_grok_video; +use crate::commands::generate::generate_video::handle_credential_router::handle_credential_router; use crate::commands::generate::generate_video::request::{ TauriGenerateVideoErrorType, TauriGenerateVideoRequest, TauriGenerateVideoResponse, - TauriVideoModel, }; -use crate::commands::generate::generate_video::sora2::handle_sora_sora2::handle_sora_sora2; use crate::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; use crate::commands::response::shorthand::Response; use crate::events::basic_sendable_event_trait::BasicSendableEvent; use crate::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent; use crate::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent; -use crate::state::app_env_configs::app_env_configs::AppEnvConfigs; use crate::state::artcraft_usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker; use crate::state::artcraft_usage_tracker::artcraft_usage_type::{ArtcraftUsagePage, ArtcraftUsageType}; use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::provider_priority::{Provider, ProviderPriorityStore}; use crate::state::task_database::TaskDatabase; -use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; -use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; -use crate::services::sora::state::sora_task_queue::SoraTaskQueue; -use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; -use enums::common::generation_provider::GenerationProvider; use log::{error, info, warn}; use tauri::{AppHandle, State}; @@ -31,15 +21,9 @@ use tauri::{AppHandle, State}; pub async fn generate_video_command( mut request: TauriGenerateVideoRequest, app: AppHandle, - app_env_configs: State<'_, AppEnvConfigs>, app_data_root: State<'_, AppDataRoot>, artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>, - provider_priority_store: State<'_, ProviderPriorityStore>, task_database: State<'_, TaskDatabase>, - grok_creds_manager: State<'_, GrokCredentialManager>, - storyteller_creds_manager: State<'_, StorytellerCredentialManager>, - sora_task_queue: State<'_, SoraTaskQueue>, - sora_creds_manager: State<'_, SoraCredentialManager>, ) -> Response { info!("generate_video_command called, request: {:?}", request); @@ -58,15 +42,9 @@ pub async fn generate_video_command( let result = handle_request( request, - &app, - &app_env_configs, &app_data_root, &artcraft_usage_tracker, - &provider_priority_store, &task_database, - &grok_creds_manager, - &sora_creds_manager, - &storyteller_creds_manager, ).await; match result { @@ -77,35 +55,43 @@ pub async fn generate_video_command( let mut status = CommandErrorStatus::ServerError; let mut error_type = TauriGenerateVideoErrorType::ServerError; - let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem."; + let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.".to_string(); match err { GenerateError::BadInput(BadInputReason::NoModelSpecified) => { status = CommandErrorStatus::BadRequest; error_type = TauriGenerateVideoErrorType::ModelNotSpecified; - error_message = "No model specified for video generation"; + error_message = "No model specified for video generation".to_string(); } GenerateError::NoProviderAvailable => { status = CommandErrorStatus::ServerError; error_type = TauriGenerateVideoErrorType::NoProviderAvailable; - error_message = "No configured provider available for video generation"; + error_message = "No configured provider available for video generation".to_string(); + } + GenerateError::CredentialProblem(_) => { + status = CommandErrorStatus::Unauthorized; + error_type = TauriGenerateVideoErrorType::CredentialProblem; + error_message = "There's a problem with the selected account.".to_string(); + } + GenerateError::NotYetImplemented(message) => { + error_message = message; } GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => { status = CommandErrorStatus::Unauthorized; error_type = TauriGenerateVideoErrorType::NeedsFalApiKey; - error_message = "You need to set a FAL api key"; + error_message = "You need to set a FAL api key".to_string(); }, GenerateError::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials) => { status = CommandErrorStatus::Unauthorized; error_type = TauriGenerateVideoErrorType::NeedsStorytellerCredentials; - error_message = "You need to be logged into Artcraft."; + error_message = "You need to be logged into Artcraft.".to_string(); } _ => {}, // Fall-through } Err(CommandErrorResponseWrapper { status, - error_message: Some(error_message.to_string()), + error_message: Some(error_message), error_type: Some(error_type), error_details: None, }) @@ -130,63 +116,14 @@ pub async fn generate_video_command( async fn handle_request( request: TauriGenerateVideoRequest, - app: &AppHandle, - app_env_configs: &AppEnvConfigs, app_data_root: &AppDataRoot, artcraft_usage_tracker: &ArtcraftUsageTracker, - provider_priority_store: &ProviderPriorityStore, task_database: &TaskDatabase, - grok_creds_manager: &GrokCredentialManager, - sora_creds_manager: &SoraCredentialManager, - storyteller_creds_manager: &StorytellerCredentialManager, ) -> Result { - let model = match request.model { - Some(model) => model, - None => { - return Err(GenerateError::no_model_specified()) - } - }; - - let provider = match (model, request.provider) { - (TauriVideoModel::GrokVideo, _) => GenerationProvider::Grok, - // NB: Grok Imagine 1.5 is served via Artcraft (storyteller-web / artcraft_router) - // for now — force it regardless of the requested provider. In the future we - // may serve it natively via the Grok provider. - (TauriVideoModel::GrokImagineVideo1p5, _) => GenerationProvider::Artcraft, - _ => request.provider.unwrap_or(GenerationProvider::Artcraft), - }; - - info!("generate video with {:?} via provider {:?}", &model, &provider); - - let result = match provider { - GenerationProvider::Grok => { - handle_grok_video( - &request, - app, - app_data_root, - app_env_configs, - grok_creds_manager, - ).await - } - GenerationProvider::Sora => { - handle_sora_sora2( - &request, - app, - app_data_root, - app_env_configs, - sora_creds_manager, - ).await - } - _ => { - handle_video_artcraft( - &request, - app, - app_env_configs, - storyteller_creds_manager, - ).await - } - }; + // Generation is credential-driven: the request names a stored credential, + // and the router dispatches to that credential's service. + let result = handle_credential_router(&request, app_data_root).await; let success_event = match result { Err(err) => return Err(err), diff --git a/crates/artcraftx/src/commands/generate/generate_video/handle_credential_router.rs b/crates/artcraftx/src/commands/generate/generate_video/handle_credential_router.rs new file mode 100644 index 00000000..992b8a53 --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_video/handle_credential_router.rs @@ -0,0 +1,70 @@ +use log::info; + +use crate::api_adapters::models::video::tauri_video_model_to_generation_model::tauri_video_model_to_generation_model; +use crate::api_adapters::models::video::tauri_video_model_to_router_model::tauri_video_model_to_router_model; +use crate::commands::enqueue::generate_error::GenerateError; +use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; +use crate::commands::generate::common::generation_credential::{credential_not_usable, resolve_generation_credential, storyteller_creds_from_credential}; +use crate::commands::generate::generate_video::artcraft::handle_artcraft_video_via_router::handle_artcraft_video_via_router; +use crate::commands::generate::generate_video::request::TauriGenerateVideoRequest; +use crate::credentials::artcraft_api_host::maybe_artcraft_api_host_for_service; +use crate::credentials::credential::Credential; +use crate::credentials::credential_service_type::CredentialServiceType; +use crate::state::data_dir::app_data_root::AppDataRoot; + +/// Credential-driven video generation: resolve the stored credential named +/// by the request's `credential_id`, then route to that credential's service +/// via the router. The router owns upload + dispatch mechanics per provider. +pub async fn handle_credential_router( + request: &TauriGenerateVideoRequest, + app_data_root: &AppDataRoot, +) -> Result { + let credential = resolve_generation_credential( + request.credential_id.as_deref(), + app_data_root, + )?; + + info!( + "handle_credential_router (video): credential={} service={}", + credential.id.as_str(), + credential.service, + ); + + match credential.service { + CredentialServiceType::Artcraft + | CredentialServiceType::ArtcraftLocal + | CredentialServiceType::ArtcraftCookies => { + handle_artcraft_credential(request, &credential).await + } + other => Err(credential_not_usable( + &credential, + &format!("accounts for service {} can't generate videos yet", other), + )), + } +} + +/// Generate via the router's Artcraft provider, authenticating with the +/// stored session cookies. Video requests carry media tokens only, which +/// Artcraft accepts directly — no pre-upload step is needed. +async fn handle_artcraft_credential( + request: &TauriGenerateVideoRequest, + credential: &Credential, +) -> Result { + let tauri_model = request.model.ok_or(GenerateError::no_model_specified())?; + + let router_model = tauri_video_model_to_router_model(tauri_model); + let generation_model = tauri_video_model_to_generation_model(tauri_model); + + let api_host = maybe_artcraft_api_host_for_service(credential.service) + .expect("caller only routes ArtCraft services here"); + + let creds = storyteller_creds_from_credential(credential)?; + + handle_artcraft_video_via_router( + request, + &api_host, + &creds, + router_model, + generation_model, + ).await +} diff --git a/crates/artcraftx/src/commands/generate/generate_video/mod.rs b/crates/artcraftx/src/commands/generate/generate_video/mod.rs index f7a6a5a7..02ee6f67 100644 --- a/crates/artcraftx/src/commands/generate/generate_video/mod.rs +++ b/crates/artcraftx/src/commands/generate/generate_video/mod.rs @@ -1,5 +1,6 @@ pub mod generate_video_command; +mod handle_credential_router; pub mod request; -mod artcraft; -mod grok; +pub mod artcraft; +pub mod grok; pub mod sora2; diff --git a/crates/artcraftx/src/commands/generate/generate_video/request.rs b/crates/artcraftx/src/commands/generate/generate_video/request.rs index 5791ae63..23069643 100644 --- a/crates/artcraftx/src/commands/generate/generate_video/request.rs +++ b/crates/artcraftx/src/commands/generate/generate_video/request.rs @@ -84,6 +84,11 @@ pub enum TauriVideoModel { #[derive(Deserialize, Debug, Clone)] pub struct TauriGenerateVideoRequest { + /// Stable id (`credential_{entropy}`) of the stored credential (account) + /// to generate with. Loaded from disk; generation routes to the + /// credential's service. + pub credential_id: Option, + pub provider: Option, pub model: Option, @@ -149,4 +154,7 @@ pub enum TauriGenerateVideoErrorType { NeedsFalApiKey, FalError, NeedsStorytellerCredentials, + /// Problem with the selected account credential (absent, unknown, or + /// unusable). The backend also flashes a dismissable modal. + CredentialProblem, } diff --git a/frontend/apps/artcraft/app/src/pages/PageVideo/ImageToVideo.tsx b/frontend/apps/artcraft/app/src/pages/PageVideo/ImageToVideo.tsx index c5bf5572..704cb630 100644 --- a/frontend/apps/artcraft/app/src/pages/PageVideo/ImageToVideo.tsx +++ b/frontend/apps/artcraft/app/src/pages/PageVideo/ImageToVideo.tsx @@ -26,6 +26,7 @@ import { GenerationProvider } from "@storyteller/api-enums"; import { Clapperboard } from "lucide-react"; import { PromptShell, useComposerTasks } from "~/components/PromptShell"; import { AccountSelector } from "~/components/account-selector/AccountSelector"; +import { useAccountSelectorStore } from "~/components/account-selector/accountSelectorStore"; const PAGE_ID: ModelPage = ModelPage.ImageToVideo; @@ -39,6 +40,7 @@ interface ImageToVideoProps { const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => { const imageToVideoModelList = useImageToVideoPageModelList(); const startBatch = useImageToVideoStore((s) => s.startBatch); + const selectedAccountId = useAccountSelectorStore((s) => s.selectedAccountId); const completeBatch = useImageToVideoStore((s) => s.completeBatch); const selectedVideoModel: VideoModel | undefined = @@ -93,6 +95,7 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => { uploadAudio={UploadAudioMedia} credits={videoCredits} accountSelector={} + credentialId={selectedAccountId} modelSelector={ { @@ -1080,6 +1085,10 @@ export const PromptBoxVideo = ({ frontend_subscriber_id: subscriberId, }; + if (!!credentialId) { + request.credential_id = credentialId; + } + if (!!selectedProvider) { request.provider = selectedProvider; } @@ -1208,7 +1217,11 @@ export const PromptBoxVideo = ({ await Promise.all(enqueuePromises); } catch (err) { console.error("PromptBoxVideo - enqueue failed", err); - toast.error("Failed to start video generation. Please try again."); + const backendMessage = (err as { error_message?: string } | null) + ?.error_message; + toast.error( + backendMessage || "Failed to start video generation. Please try again.", + ); } onEnqueuePressed?.(prompt, subscriberIds); diff --git a/frontend/libs/tauri-api/src/lib/generate/GenerateVideo.ts b/frontend/libs/tauri-api/src/lib/generate/GenerateVideo.ts index 02692083..d140d78f 100644 --- a/frontend/libs/tauri-api/src/lib/generate/GenerateVideo.ts +++ b/frontend/libs/tauri-api/src/lib/generate/GenerateVideo.ts @@ -8,6 +8,11 @@ import { import { GenerationProvider } from "@storyteller/api-enums"; export interface GenerateVideoRequest { + // Stable id (`credential_{entropy}`) of the stored credential (account) + // to generate with. The backend loads it from disk and routes to that + // credential's service. + credential_id?: string; + // The provider to use (defaults to Artcraft/Storyteller). provider?: GenerationProvider; @@ -50,6 +55,7 @@ export interface GenerateVideoRequest { } interface RawGenerateVideoRequest { + credential_id?: string; provider?: GenerationProvider; model?: string; prompt?: string; @@ -117,6 +123,7 @@ export const GenerateVideo = async ( model: modelName, }; + if (!!request.credential_id) mutableRequest.credential_id = request.credential_id; if (!!request.provider) mutableRequest.provider = request.provider; if (!!request.prompt) mutableRequest.prompt = request.prompt; if (!!request.negative_prompt) mutableRequest.negative_prompt = request.negative_prompt;