diff --git a/crates/artcraftx/src/api_adapters/models/audio/mod.rs b/crates/artcraftx/src/api_adapters/models/audio/mod.rs new file mode 100644 index 00000000..ce52b580 --- /dev/null +++ b/crates/artcraftx/src/api_adapters/models/audio/mod.rs @@ -0,0 +1,2 @@ +pub mod tauri_audio_model_to_generation_model; +pub mod tauri_audio_model_to_router_model; diff --git a/crates/artcraftx/src/api_adapters/models/audio/tauri_audio_model_to_generation_model.rs b/crates/artcraftx/src/api_adapters/models/audio/tauri_audio_model_to_generation_model.rs new file mode 100644 index 00000000..d37918a2 --- /dev/null +++ b/crates/artcraftx/src/api_adapters/models/audio/tauri_audio_model_to_generation_model.rs @@ -0,0 +1,13 @@ +use crate::events::generation_events::common::GenerationModel; +use crate::commands::generate::generate_audio::request::TauriAudioModel; + +/// Map TauriAudioModel to the frontend-event GenerationModel. +pub fn tauri_audio_model_to_generation_model(model: TauriAudioModel) -> GenerationModel { + match model { + TauriAudioModel::SunoMusic => GenerationModel::SunoMusic, + TauriAudioModel::SunoRemix => GenerationModel::SunoRemix, + TauriAudioModel::SunoSounds => GenerationModel::SunoSounds, + TauriAudioModel::SunoSample => GenerationModel::SunoSample, + TauriAudioModel::SeedAudio1p0 => GenerationModel::SeedAudio1p0, + } +} diff --git a/crates/artcraftx/src/api_adapters/models/audio/tauri_audio_model_to_router_model.rs b/crates/artcraftx/src/api_adapters/models/audio/tauri_audio_model_to_router_model.rs new file mode 100644 index 00000000..eb68f59b --- /dev/null +++ b/crates/artcraftx/src/api_adapters/models/audio/tauri_audio_model_to_router_model.rs @@ -0,0 +1,14 @@ +use router::api::router_audio_model::RouterAudioModel; + +use crate::commands::generate::generate_audio::request::TauriAudioModel; + +/// Map TauriAudioModel to the router's RouterAudioModel. +pub fn tauri_audio_model_to_router_model(model: TauriAudioModel) -> RouterAudioModel { + match model { + TauriAudioModel::SunoMusic => RouterAudioModel::SunoMusic, + TauriAudioModel::SunoRemix => RouterAudioModel::SunoRemix, + TauriAudioModel::SunoSounds => RouterAudioModel::SunoSounds, + TauriAudioModel::SunoSample => RouterAudioModel::SunoSample, + TauriAudioModel::SeedAudio1p0 => RouterAudioModel::SeedAudio1p0, + } +} diff --git a/crates/artcraftx/src/api_adapters/models/mesh/mod.rs b/crates/artcraftx/src/api_adapters/models/mesh/mod.rs new file mode 100644 index 00000000..c188ecba --- /dev/null +++ b/crates/artcraftx/src/api_adapters/models/mesh/mod.rs @@ -0,0 +1,2 @@ +pub mod tauri_mesh_model_to_generation_model; +pub mod tauri_mesh_model_to_router_model; diff --git a/crates/artcraftx/src/api_adapters/models/mesh/tauri_mesh_model_to_generation_model.rs b/crates/artcraftx/src/api_adapters/models/mesh/tauri_mesh_model_to_generation_model.rs new file mode 100644 index 00000000..5ce6200b --- /dev/null +++ b/crates/artcraftx/src/api_adapters/models/mesh/tauri_mesh_model_to_generation_model.rs @@ -0,0 +1,19 @@ +use crate::events::generation_events::common::GenerationModel; +use crate::commands::generate::generate_mesh::request::TauriMeshModel; + +/// Map TauriMeshModel to the frontend-event GenerationModel. +pub fn tauri_mesh_model_to_generation_model(model: TauriMeshModel) -> GenerationModel { + match model { + TauriMeshModel::Hunyuan3d2p0 => GenerationModel::Hunyuan3d2_0, + TauriMeshModel::Hunyuan3d2p1 => GenerationModel::Hunyuan3d2_1, + TauriMeshModel::Hunyuan3d3 => GenerationModel::Hunyuan3d3, + TauriMeshModel::Hunyuan3d3Sketch => GenerationModel::Hunyuan3d3Sketch, + TauriMeshModel::Hunyuan3d3p1Pro => GenerationModel::Hunyuan3d3p1Pro, + TauriMeshModel::Hunyuan3d3p1Rapid => GenerationModel::Hunyuan3d3p1Rapid, + TauriMeshModel::Hunyuan3d3p1Part => GenerationModel::Hunyuan3d3p1Part, + TauriMeshModel::Hunyuan3d3p1SmartTopology => GenerationModel::Hunyuan3d3p1SmartTopology, + TauriMeshModel::Tripo3dH3p1 => GenerationModel::Tripo3dH3p1, + TauriMeshModel::MeshyV6 => GenerationModel::MeshyV6, + TauriMeshModel::Rodin2p5Fast => GenerationModel::Rodin2p5Fast, + } +} diff --git a/crates/artcraftx/src/api_adapters/models/mesh/tauri_mesh_model_to_router_model.rs b/crates/artcraftx/src/api_adapters/models/mesh/tauri_mesh_model_to_router_model.rs new file mode 100644 index 00000000..25d639f1 --- /dev/null +++ b/crates/artcraftx/src/api_adapters/models/mesh/tauri_mesh_model_to_router_model.rs @@ -0,0 +1,20 @@ +use router::api::router_mesh_model::RouterMeshModel; + +use crate::commands::generate::generate_mesh::request::TauriMeshModel; + +/// Map TauriMeshModel to the router's RouterMeshModel. +pub fn tauri_mesh_model_to_router_model(model: TauriMeshModel) -> RouterMeshModel { + match model { + TauriMeshModel::Hunyuan3d2p0 => RouterMeshModel::Hunyuan3d2p0, + TauriMeshModel::Hunyuan3d2p1 => RouterMeshModel::Hunyuan3d2p1, + TauriMeshModel::Hunyuan3d3 => RouterMeshModel::Hunyuan3d3, + TauriMeshModel::Hunyuan3d3Sketch => RouterMeshModel::Hunyuan3d3Sketch, + TauriMeshModel::Hunyuan3d3p1Pro => RouterMeshModel::Hunyuan3d3p1Pro, + TauriMeshModel::Hunyuan3d3p1Rapid => RouterMeshModel::Hunyuan3d3p1Rapid, + TauriMeshModel::Hunyuan3d3p1Part => RouterMeshModel::Hunyuan3d3p1Part, + TauriMeshModel::Hunyuan3d3p1SmartTopology => RouterMeshModel::Hunyuan3d3p1SmartTopology, + TauriMeshModel::Tripo3dH3p1 => RouterMeshModel::Tripo3dH3p1, + TauriMeshModel::MeshyV6 => RouterMeshModel::MeshyV6, + TauriMeshModel::Rodin2p5Fast => RouterMeshModel::Rodin2p5Fast, + } +} diff --git a/crates/artcraftx/src/api_adapters/models/mod.rs b/crates/artcraftx/src/api_adapters/models/mod.rs index c10a6ca1..53eac2cd 100644 --- a/crates/artcraftx/src/api_adapters/models/mod.rs +++ b/crates/artcraftx/src/api_adapters/models/mod.rs @@ -1,2 +1,5 @@ +pub mod audio; pub mod image; pub mod video; +pub mod mesh; +pub mod splat; diff --git a/crates/artcraftx/src/api_adapters/models/splat/mod.rs b/crates/artcraftx/src/api_adapters/models/splat/mod.rs new file mode 100644 index 00000000..b259373a --- /dev/null +++ b/crates/artcraftx/src/api_adapters/models/splat/mod.rs @@ -0,0 +1,2 @@ +pub mod tauri_splat_model_to_generation_model; +pub mod tauri_splat_model_to_router_model; diff --git a/crates/artcraftx/src/api_adapters/models/splat/tauri_splat_model_to_generation_model.rs b/crates/artcraftx/src/api_adapters/models/splat/tauri_splat_model_to_generation_model.rs new file mode 100644 index 00000000..c1f4fc32 --- /dev/null +++ b/crates/artcraftx/src/api_adapters/models/splat/tauri_splat_model_to_generation_model.rs @@ -0,0 +1,15 @@ +use crate::events::generation_events::common::GenerationModel; +use crate::commands::generate::generate_splat::request::TauriSplatModel; + +/// Map TauriSplatModel to the frontend-event GenerationModel. +pub fn tauri_splat_model_to_generation_model(model: TauriSplatModel) -> GenerationModel { + match model { + TauriSplatModel::Marble0p1Mini => GenerationModel::WorldlabsMarble0p1Mini, + TauriSplatModel::Marble0p1Plus => GenerationModel::WorldlabsMarble0p1Plus, + TauriSplatModel::Marble1p0 => GenerationModel::Marble1p0, + TauriSplatModel::Marble1p0Draft => GenerationModel::Marble1p0Draft, + TauriSplatModel::Marble1p1 => GenerationModel::Marble1p1, + TauriSplatModel::Marble1p1Plus => GenerationModel::Marble1p1Plus, + TauriSplatModel::TripoSplat => GenerationModel::TripoSplat, + } +} diff --git a/crates/artcraftx/src/api_adapters/models/splat/tauri_splat_model_to_router_model.rs b/crates/artcraftx/src/api_adapters/models/splat/tauri_splat_model_to_router_model.rs new file mode 100644 index 00000000..9906b8c5 --- /dev/null +++ b/crates/artcraftx/src/api_adapters/models/splat/tauri_splat_model_to_router_model.rs @@ -0,0 +1,16 @@ +use router::api::router_splat_model::RouterSplatModel; + +use crate::commands::generate::generate_splat::request::TauriSplatModel; + +/// Map TauriSplatModel to the router's RouterSplatModel. +pub fn tauri_splat_model_to_router_model(model: TauriSplatModel) -> RouterSplatModel { + match model { + TauriSplatModel::Marble0p1Mini => RouterSplatModel::Marble0p1Mini, + TauriSplatModel::Marble0p1Plus => RouterSplatModel::Marble0p1Plus, + TauriSplatModel::Marble1p0 => RouterSplatModel::Marble1p0, + TauriSplatModel::Marble1p0Draft => RouterSplatModel::Marble1p0Draft, + TauriSplatModel::Marble1p1 => RouterSplatModel::Marble1p1, + TauriSplatModel::Marble1p1Plus => RouterSplatModel::Marble1p1Plus, + TauriSplatModel::TripoSplat => RouterSplatModel::TripoSplat, + } +} diff --git a/crates/artcraftx/src/commands/enqueue/task_enqueue_success.rs b/crates/artcraftx/src/commands/enqueue/task_enqueue_success.rs index cebec4e9..6cfbd7d4 100644 --- a/crates/artcraftx/src/commands/enqueue/task_enqueue_success.rs +++ b/crates/artcraftx/src/commands/enqueue/task_enqueue_success.rs @@ -26,6 +26,7 @@ impl TaskEnqueueSuccess{ TaskType::ImageGeneration => GenerationAction::GenerateImage, TaskType::GaussianGeneration => GenerationAction::GenerateGaussian, TaskType::VideoGeneration => GenerationAction::GenerateVideo, + TaskType::AudioGeneration => GenerationAction::GenerateAudio, TaskType::BackgroundRemoval => GenerationAction::RemoveBackground, TaskType::ObjectGeneration => GenerationAction::ImageTo3d, TaskType::ImageInpaintEdit => GenerationAction::ImageInpaintEdit, @@ -109,6 +110,24 @@ impl TaskEnqueueSuccess{ Some(GenerationModel::Hunyuan3d2_0) => Some(TaskModelType::Hunyuan3d2_0), Some(GenerationModel::Hunyuan3d2_1) => Some(TaskModelType::Hunyuan3d2_1), Some(GenerationModel::Hunyuan3d3) => Some(TaskModelType::Hunyuan3d3), + Some(GenerationModel::Hunyuan3d3Sketch) => Some(TaskModelType::Hunyuan3d3Sketch), + Some(GenerationModel::Hunyuan3d3p1Pro) => Some(TaskModelType::Hunyuan3d3p1Pro), + Some(GenerationModel::Hunyuan3d3p1Rapid) => Some(TaskModelType::Hunyuan3d3p1Rapid), + Some(GenerationModel::Hunyuan3d3p1Part) => Some(TaskModelType::Hunyuan3d3p1Part), + Some(GenerationModel::Hunyuan3d3p1SmartTopology) => Some(TaskModelType::Hunyuan3d3p1SmartTopology), + Some(GenerationModel::Tripo3dH3p1) => Some(TaskModelType::Tripo3dH3p1), + Some(GenerationModel::MeshyV6) => Some(TaskModelType::MeshyV6), + Some(GenerationModel::Rodin2p5Fast) => Some(TaskModelType::Rodin2p5Fast), + Some(GenerationModel::Marble1p0) => Some(TaskModelType::Marble1p0), + Some(GenerationModel::Marble1p0Draft) => Some(TaskModelType::Marble1p0Draft), + Some(GenerationModel::Marble1p1) => Some(TaskModelType::Marble1p1), + Some(GenerationModel::Marble1p1Plus) => Some(TaskModelType::Marble1p1Plus), + Some(GenerationModel::TripoSplat) => Some(TaskModelType::TripoSplat), + Some(GenerationModel::SunoMusic) => Some(TaskModelType::SunoMusic), + Some(GenerationModel::SunoRemix) => Some(TaskModelType::SunoRemix), + Some(GenerationModel::SunoSounds) => Some(TaskModelType::SunoSounds), + Some(GenerationModel::SunoSample) => Some(TaskModelType::SunoSample), + Some(GenerationModel::SeedAudio1p0) => Some(TaskModelType::SeedAudio1p0), Some(GenerationModel::WorldlabsMarble) => Some(TaskModelType::WorldlabsMarble), Some(GenerationModel::WorldlabsMarble0p1Mini) => Some(TaskModelType::WorldlabsMarble0p1Mini), Some(GenerationModel::WorldlabsMarble0p1Plus) => Some(TaskModelType::WorldlabsMarble0p1Plus), diff --git a/crates/artcraftx/src/commands/generate/generate_audio/generate_audio_command.rs b/crates/artcraftx/src/commands/generate/generate_audio/generate_audio_command.rs new file mode 100644 index 00000000..a5e363d0 --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_audio/generate_audio_command.rs @@ -0,0 +1,117 @@ +use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; +use crate::commands::enqueue::generate_error::GenerateError; +use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; +use crate::commands::generate::generate_audio::handle_credential_router::handle_credential_router; +use crate::commands::generate::generate_audio::request::{ + TauriGenerateAudioErrorType, TauriGenerateAudioRequest, TauriGenerateAudioResponse, +}; +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::data_dir::app_data_root::AppDataRoot; +use crate::state::task_database::TaskDatabase; +use log::{error, info}; +use tauri::{AppHandle, State}; + +#[tauri::command] +pub async fn generate_audio_command( + request: TauriGenerateAudioRequest, + app: AppHandle, + app_data_root: State<'_, AppDataRoot>, + + task_database: State<'_, TaskDatabase>, +) -> Response { + + info!("generate_audio_command called, request: {:?}", request); + + let result = handle_request( + request, + &app_data_root, + + &task_database, + ).await; + + match result { + Err(err) => { + error!("generate_audio_command error: {:?}", err); + + notify_frontend_of_errors(&app, &err).await; + + let mut status = CommandErrorStatus::ServerError; + let mut error_type = TauriGenerateAudioErrorType::ServerError; + 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(_) => { + status = CommandErrorStatus::BadRequest; + error_type = TauriGenerateAudioErrorType::ModelNotSpecified; + error_message = "No model specified for audio generation".to_string(); + } + GenerateError::CredentialProblem(_) => { + status = CommandErrorStatus::Unauthorized; + error_type = TauriGenerateAudioErrorType::CredentialProblem; + error_message = "There's a problem with the selected account.".to_string(); + } + GenerateError::NotYetImplemented(message) => { + error_message = message; + } + _ => {}, // Fall-through + } + + Err(CommandErrorResponseWrapper { + status, + error_message: Some(error_message), + error_type: Some(error_type), + error_details: None, + }) + } + Ok(event) => { + let event = GenerationEnqueueSuccessEvent { + action: event.to_frontend_event_action(), + service: event.to_frontend_event_service(), + model: event.model, + }; + + if let Err(err) = event.send(&app) { + error!("Failed to emit event: {:?}", err); + } + + CreditsBalanceChangedEvent{}.send_infallible(&app); + + Ok(TauriGenerateAudioResponse {}.into()) + } + } +} + +async fn handle_request( + request: TauriGenerateAudioRequest, + app_data_root: &AppDataRoot, + + task_database: &TaskDatabase, +) -> Result { + + // Generation is credential-driven: the request names a stored credential, + // and the router dispatches to that credential's service. + let success_event = handle_credential_router(&request, app_data_root).await?; + + let result = success_event + .insert_into_task_database_with_frontend_payload( + task_database, + request.frontend_caller, + request.frontend_subscriber_id.as_deref(), + request.frontend_subscriber_payload.as_deref(), + ) + .await; + + if let Err(err) = result { + error!("Failed to create task in database: {:?}", err); + } + + // NB: The usage tracker has no audio method yet. + + Ok(success_event) +} diff --git a/crates/artcraftx/src/commands/generate/generate_audio/handle_credential_router.rs b/crates/artcraftx/src/commands/generate/generate_audio/handle_credential_router.rs new file mode 100644 index 00000000..c605b9a4 --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_audio/handle_credential_router.rs @@ -0,0 +1,138 @@ +use enums::common::generation_provider::GenerationProvider; +use enums::tauri::tasks::task_type::TaskType; +use log::{info, warn}; +use router::api::router_provider::RouterProvider; +use router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use router::client::router_artcraft_client::RouterArtcraftClient; +use router::client::router_client::RouterClient; +use router::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder; +use router::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest; +use router::api::audio_list_ref::AudioListRef; +use router::api::image_list_ref::ImageListRef; + +use crate::api_adapters::models::audio::tauri_audio_model_to_generation_model::tauri_audio_model_to_generation_model; +use crate::api_adapters::models::audio::tauri_audio_model_to_router_model::tauri_audio_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_audio::request::TauriGenerateAudioRequest; +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 audio 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: &TauriGenerateAudioRequest, + app_data_root: &AppDataRoot, +) -> Result { + let credential = resolve_generation_credential( + request.credential_id.as_deref(), + app_data_root, + )?; + + info!( + "handle_credential_router (audio): 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 audio yet", other), + )), + } +} + +/// Generate via the router's Artcraft provider, authenticating with the +/// stored session cookies. Requests carry media tokens only, which Artcraft +/// accepts directly — no pre-upload step is needed. +async fn handle_artcraft_credential( + request: &TauriGenerateAudioRequest, + credential: &Credential, +) -> Result { + let tauri_model = request.model.ok_or(GenerateError::no_model_specified())?; + + let router_model = tauri_audio_model_to_router_model(tauri_model); + let generation_model = tauri_audio_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)?; + + let client = RouterClient::Artcraft(RouterArtcraftClient::new( + api_host, + creds, + )); + + let router_request = GenerateAudioRequestBuilder { + model: router_model, + provider: RouterProvider::Artcraft, + prompt: request.prompt.clone(), + style_prompt: request.style_prompt.clone(), + audio_references: request.audio_media_tokens.clone().map(AudioListRef::MediaFileTokens), + image_references: request.image_media_tokens.clone().map(ImageListRef::MediaFileTokens), + keep_lyrics: request.keep_lyrics, + is_instrumental: request.is_instrumental, + is_loopable: request.is_loopable, + bpm: request.bpm, + musical_key: request.musical_key, + sample_rate_hz: request.sample_rate_hz, + speed: request.speed, + volume: request.volume, + pitch: request.pitch, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + idempotency_token: None, + }; + + info!("Building Artcraft audio generation plan: model={:?}", router_model); + + let generation_request = match router_request.build2() { + Ok(AudioGenerationDraftOrRequest::Request(generation_request)) => generation_request, + Ok(AudioGenerationDraftOrRequest::Draft(draft)) => { + warn!("Artcraft build unexpectedly produced a draft: {:?}", draft); + return Err(GenerateError::NotYetImplemented( + "Artcraft requests should not require a draft phase".to_string(), + )); + } + Err(err) => { + warn!("Could not build Artcraft router request: {:?}", err); + return Err(err.into()); + } + }; + + let response = generation_request.send_request(&client).await + .map_err(|err| { + warn!("Artcraft router audio generation failed: {:?}", err); + GenerateError::from(err) + })?; + + let payload = response.get_artcraft_payload() + .ok_or(GenerateError::ResponseHadNoJobTokens)?; + + info!( + "Router audio generation enqueued: inference_job_token={}, all_tokens={:?}, response={:?}", + payload.inference_job_token.as_str(), + payload.all_inference_job_tokens, + response, + ); + + Ok(TaskEnqueueSuccess { + task_type: TaskType::AudioGeneration, + model: Some(generation_model), + provider: GenerationProvider::Artcraft, + provider_job_id: Some(payload.inference_job_token.to_string()), + maybe_queue_status_url: None, + maybe_queue_response_url: None, + maybe_prompt_token: None, + }) +} diff --git a/crates/artcraftx/src/commands/generate/generate_audio/mod.rs b/crates/artcraftx/src/commands/generate/generate_audio/mod.rs new file mode 100644 index 00000000..b9ace028 --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_audio/mod.rs @@ -0,0 +1,3 @@ +pub mod generate_audio_command; +mod handle_credential_router; +pub mod request; diff --git a/crates/artcraftx/src/commands/generate/generate_audio/request.rs b/crates/artcraftx/src/commands/generate/generate_audio/request.rs new file mode 100644 index 00000000..a59eac8b --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_audio/request.rs @@ -0,0 +1,107 @@ +use enums::tauri::ux::tauri_command_caller::TauriCommandCaller; +use serde_derive::{Deserialize, Serialize}; +use enums::common::generation::common_musical_key::CommonMusicalKey; +use tokens::tokens::media_files::MediaFileToken; + +use crate::commands::response::success_response_wrapper::SerializeMarker; + +// ── Request ── + +#[derive(Deserialize, Debug)] +pub struct TauriGenerateAudioRequest { + /// 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, + + /// The model to use. + pub model: Option, + + /// Text prompt. + pub prompt: Option, + + /// Style/genre prompt (Suno's "tags"). + pub style_prompt: Option, + + /// Reference audio (already uploaded). + pub audio_media_tokens: Option>, + + /// Reference images (already uploaded). + pub image_media_tokens: Option>, + + /// Keep the original lyrics (Suno Remix). + pub keep_lyrics: Option, + + /// Instrumental-only toggle (Suno Music / Sample). + pub is_instrumental: Option, + + /// Loopable vs single-hit toggle (Suno Sounds). + pub is_loopable: Option, + + /// Target BPM (Suno Sounds). + pub bpm: Option, + + /// Musical key (Suno Sounds). + pub musical_key: Option, + + /// Sample rate in Hz (Seed Audio). + pub sample_rate_hz: Option, + + /// Playback speed (Seed Audio, 0.5–2.0). + pub speed: Option, + + /// Volume (Seed Audio, 0.5–2.0). + pub volume: Option, + + /// Pitch shift in semitones (Seed Audio, -12..=12). + pub pitch: Option, + + // ── Frontend metadata ── + + /// Name of the frontend caller. + pub frontend_caller: Option, + + /// A frontend-defined identifier sent back as a Tauri event on task completion. + pub frontend_subscriber_id: Option, + + /// A frontend-defined payload sent back as a Tauri event on task completion. + pub frontend_subscriber_payload: Option, +} + +/// The audio models the frontend can request, identified by their omni +/// model ids (`CommonAudioModel` serde strings). +#[derive(Clone, Copy, Debug, Deserialize)] +pub enum TauriAudioModel { + #[serde(rename = "suno_music")] + SunoMusic, + #[serde(rename = "suno_remix")] + SunoRemix, + #[serde(rename = "suno_sounds")] + SunoSounds, + #[serde(rename = "suno_sample")] + SunoSample, + #[serde(rename = "seed_audio_1p0")] + SeedAudio1p0, +} + +// ── Response ── + +#[derive(Serialize)] +pub struct TauriGenerateAudioResponse { +} + +impl SerializeMarker for TauriGenerateAudioResponse {} + +// ── Error ── + +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TauriGenerateAudioErrorType { + /// Caller didn't specify a model + ModelNotSpecified, + /// Generic server error + ServerError, + /// Problem with the selected account credential (absent, unknown, or + /// unusable). The backend also flashes a dismissable modal. + CredentialProblem, +} diff --git a/crates/artcraftx/src/commands/generate/generate_mesh/generate_mesh_command.rs b/crates/artcraftx/src/commands/generate/generate_mesh/generate_mesh_command.rs new file mode 100644 index 00000000..d77f70d4 --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_mesh/generate_mesh_command.rs @@ -0,0 +1,119 @@ +use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; +use crate::commands::enqueue::generate_error::GenerateError; +use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; +use crate::commands::generate::generate_mesh::handle_credential_router::handle_credential_router; +use crate::commands::generate::generate_mesh::request::{ + TauriGenerateMeshErrorType, TauriGenerateMeshRequest, TauriGenerateMeshResponse, +}; +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::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::task_database::TaskDatabase; +use log::{error, info, warn}; +use tauri::{AppHandle, State}; + +#[tauri::command] +pub async fn generate_mesh_command( + request: TauriGenerateMeshRequest, + app: AppHandle, + app_data_root: State<'_, AppDataRoot>, + artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>, + task_database: State<'_, TaskDatabase>, +) -> Response { + + info!("generate_mesh_command called, request: {:?}", request); + + let result = handle_request( + request, + &app_data_root, + &artcraft_usage_tracker, + &task_database, + ).await; + + match result { + Err(err) => { + error!("generate_mesh_command error: {:?}", err); + + notify_frontend_of_errors(&app, &err).await; + + let mut status = CommandErrorStatus::ServerError; + let mut error_type = TauriGenerateMeshErrorType::ServerError; + 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(_) => { + status = CommandErrorStatus::BadRequest; + error_type = TauriGenerateMeshErrorType::ModelNotSpecified; + error_message = "No model specified for mesh generation".to_string(); + } + GenerateError::CredentialProblem(_) => { + status = CommandErrorStatus::Unauthorized; + error_type = TauriGenerateMeshErrorType::CredentialProblem; + error_message = "There's a problem with the selected account.".to_string(); + } + GenerateError::NotYetImplemented(message) => { + error_message = message; + } + _ => {}, // Fall-through + } + + Err(CommandErrorResponseWrapper { + status, + error_message: Some(error_message), + error_type: Some(error_type), + error_details: None, + }) + } + Ok(event) => { + let event = GenerationEnqueueSuccessEvent { + action: event.to_frontend_event_action(), + service: event.to_frontend_event_service(), + model: event.model, + }; + + if let Err(err) = event.send(&app) { + error!("Failed to emit event: {:?}", err); + } + + CreditsBalanceChangedEvent{}.send_infallible(&app); + + Ok(TauriGenerateMeshResponse {}.into()) + } + } +} + +async fn handle_request( + request: TauriGenerateMeshRequest, + app_data_root: &AppDataRoot, + artcraft_usage_tracker: &ArtcraftUsageTracker, + task_database: &TaskDatabase, +) -> Result { + + // Generation is credential-driven: the request names a stored credential, + // and the router dispatches to that credential's service. + let success_event = handle_credential_router(&request, app_data_root).await?; + + let result = success_event + .insert_into_task_database_with_frontend_payload( + task_database, + request.frontend_caller, + request.frontend_subscriber_id.as_deref(), + request.frontend_subscriber_payload.as_deref(), + ) + .await; + + if let Err(err) = result { + error!("Failed to create task in database: {:?}", err); + } + + if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::ObjectPage) { + warn!("Failed to report usage: {:?}", err); + } + + Ok(success_event) +} diff --git a/crates/artcraftx/src/commands/generate/generate_mesh/handle_credential_router.rs b/crates/artcraftx/src/commands/generate/generate_mesh/handle_credential_router.rs new file mode 100644 index 00000000..f9a6ae4e --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_mesh/handle_credential_router.rs @@ -0,0 +1,140 @@ +use enums::common::generation_provider::GenerationProvider; +use enums::tauri::tasks::task_type::TaskType; +use log::{info, warn}; +use router::api::router_provider::RouterProvider; +use router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use router::client::router_artcraft_client::RouterArtcraftClient; +use router::client::router_client::RouterClient; +use router::generate::generate_mesh::generate_mesh_request_builder::GenerateMeshRequestBuilder; +use router::generate::generate_mesh::mesh_generation_draft_or_request::MeshGenerationDraftOrRequest; +use router::api::image_list_ref::ImageListRef; +use router::api::image_ref::ImageRef; +use router::api::mesh_ref::MeshRef; + +use crate::api_adapters::models::mesh::tauri_mesh_model_to_generation_model::tauri_mesh_model_to_generation_model; +use crate::api_adapters::models::mesh::tauri_mesh_model_to_router_model::tauri_mesh_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_mesh::request::TauriGenerateMeshRequest; +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 mesh 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: &TauriGenerateMeshRequest, + app_data_root: &AppDataRoot, +) -> Result { + let credential = resolve_generation_credential( + request.credential_id.as_deref(), + app_data_root, + )?; + + info!( + "handle_credential_router (mesh): 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 mesh yet", other), + )), + } +} + +/// Generate via the router's Artcraft provider, authenticating with the +/// stored session cookies. Requests carry media tokens only, which Artcraft +/// accepts directly — no pre-upload step is needed. +async fn handle_artcraft_credential( + request: &TauriGenerateMeshRequest, + credential: &Credential, +) -> Result { + let tauri_model = request.model.ok_or(GenerateError::no_model_specified())?; + + let router_model = tauri_mesh_model_to_router_model(tauri_model); + let generation_model = tauri_mesh_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)?; + + let client = RouterClient::Artcraft(RouterArtcraftClient::new( + api_host, + creds, + )); + + let router_request = GenerateMeshRequestBuilder { + model: router_model, + provider: RouterProvider::Artcraft, + prompt: request.prompt.clone(), + reference_images: request.reference_image_media_tokens.clone().map(ImageListRef::MediaFileTokens), + front_image: request.front_image_media_token.clone().map(ImageRef::MediaFileToken), + back_image: request.back_image_media_token.clone().map(ImageRef::MediaFileToken), + left_image: request.left_image_media_token.clone().map(ImageRef::MediaFileToken), + right_image: request.right_image_media_token.clone().map(ImageRef::MediaFileToken), + input_mesh: request.input_mesh_media_token.clone().map(MeshRef::MediaFileToken), + mesh_output_type: request.mesh_output_type, + polygon_type: request.polygon_type, + face_count: request.face_count, + enable_pbr: request.enable_pbr, + enable_texture: request.enable_texture, + texture_quality: request.texture_quality, + geometry_quality: request.geometry_quality, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + idempotency_token: None, + }; + + info!("Building Artcraft mesh generation plan: model={:?}", router_model); + + let generation_request = match router_request.build2() { + Ok(MeshGenerationDraftOrRequest::Request(generation_request)) => generation_request, + Ok(MeshGenerationDraftOrRequest::Draft(draft)) => { + warn!("Artcraft build unexpectedly produced a draft: {:?}", draft); + return Err(GenerateError::NotYetImplemented( + "Artcraft requests should not require a draft phase".to_string(), + )); + } + Err(err) => { + warn!("Could not build Artcraft router request: {:?}", err); + return Err(err.into()); + } + }; + + let response = generation_request.send_request(&client).await + .map_err(|err| { + warn!("Artcraft router mesh generation failed: {:?}", err); + GenerateError::from(err) + })?; + + let payload = response.get_artcraft_payload() + .ok_or(GenerateError::ResponseHadNoJobTokens)?; + + info!( + "Router mesh generation enqueued: inference_job_token={}, all_tokens={:?}, response={:?}", + payload.inference_job_token.as_str(), + payload.all_inference_job_tokens, + response, + ); + + Ok(TaskEnqueueSuccess { + task_type: TaskType::ObjectGeneration, + model: Some(generation_model), + provider: GenerationProvider::Artcraft, + provider_job_id: Some(payload.inference_job_token.to_string()), + maybe_queue_status_url: None, + maybe_queue_response_url: None, + maybe_prompt_token: None, + }) +} diff --git a/crates/artcraftx/src/commands/generate/generate_mesh/mod.rs b/crates/artcraftx/src/commands/generate/generate_mesh/mod.rs new file mode 100644 index 00000000..92d68a4f --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_mesh/mod.rs @@ -0,0 +1,3 @@ +pub mod generate_mesh_command; +mod handle_credential_router; +pub mod request; diff --git a/crates/artcraftx/src/commands/generate/generate_mesh/request.rs b/crates/artcraftx/src/commands/generate/generate_mesh/request.rs new file mode 100644 index 00000000..3f746858 --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_mesh/request.rs @@ -0,0 +1,118 @@ +use enums::tauri::ux::tauri_command_caller::TauriCommandCaller; +use serde_derive::{Deserialize, Serialize}; +use enums::common::generation::common_mesh_output_type::CommonMeshOutputType; +use enums::common::generation::common_mesh_quality::CommonMeshQuality; +use enums::common::generation::common_polygon_type::CommonPolygonType; +use tokens::tokens::media_files::MediaFileToken; + +use crate::commands::response::success_response_wrapper::SerializeMarker; + +// ── Request ── + +#[derive(Deserialize, Debug)] +pub struct TauriGenerateMeshRequest { + /// 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, + + /// The model to use. + pub model: Option, + + /// Text prompt. + pub prompt: Option, + + /// Reference images (already uploaded). + pub reference_image_media_tokens: Option>, + + /// Multi-view images (already uploaded). + pub front_image_media_token: Option, + pub back_image_media_token: Option, + pub left_image_media_token: Option, + pub right_image_media_token: Option, + + /// Input mesh for mesh-to-mesh models (part splitting, retopology). + pub input_mesh_media_token: Option, + + /// Requested mesh output type. + pub mesh_output_type: Option, + + /// Requested polygon type. + pub polygon_type: Option, + + /// Target face count. + pub face_count: Option, + + /// Generate PBR materials. + pub enable_pbr: Option, + + /// Generate textures (off = untextured output). + pub enable_texture: Option, + + /// Texture quality level. + pub texture_quality: Option, + + /// Geometry quality level. + pub geometry_quality: Option, + + // ── Frontend metadata ── + + /// Name of the frontend caller. + pub frontend_caller: Option, + + /// A frontend-defined identifier sent back as a Tauri event on task completion. + pub frontend_subscriber_id: Option, + + /// A frontend-defined payload sent back as a Tauri event on task completion. + pub frontend_subscriber_payload: Option, +} + +/// The mesh models the frontend can request, identified by their omni +/// model ids (`CommonMeshModel` serde strings). +#[derive(Clone, Copy, Debug, Deserialize)] +pub enum TauriMeshModel { + #[serde(rename = "hunyuan_3d_2p0")] + Hunyuan3d2p0, + #[serde(rename = "hunyuan_3d_2p1")] + Hunyuan3d2p1, + #[serde(rename = "hunyuan_3d_3")] + Hunyuan3d3, + #[serde(rename = "hunyuan_3d_3_sketch")] + Hunyuan3d3Sketch, + #[serde(rename = "hunyuan_3d_3p1_pro")] + Hunyuan3d3p1Pro, + #[serde(rename = "hunyuan_3d_3p1_rapid")] + Hunyuan3d3p1Rapid, + #[serde(rename = "hunyuan_3d_3p1_part")] + Hunyuan3d3p1Part, + #[serde(rename = "hunyuan_3d_3p1_topology")] + Hunyuan3d3p1SmartTopology, + #[serde(rename = "tripo3d_h3p1")] + Tripo3dH3p1, + #[serde(rename = "meshy_v6")] + MeshyV6, + #[serde(rename = "rodin_2p5_fast")] + Rodin2p5Fast, +} + +// ── Response ── + +#[derive(Serialize)] +pub struct TauriGenerateMeshResponse { +} + +impl SerializeMarker for TauriGenerateMeshResponse {} + +// ── Error ── + +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TauriGenerateMeshErrorType { + /// Caller didn't specify a model + ModelNotSpecified, + /// Generic server error + ServerError, + /// Problem with the selected account credential (absent, unknown, or + /// unusable). The backend also flashes a dismissable modal. + CredentialProblem, +} diff --git a/crates/artcraftx/src/commands/generate/generate_splat/generate_splat_command.rs b/crates/artcraftx/src/commands/generate/generate_splat/generate_splat_command.rs new file mode 100644 index 00000000..8229df9d --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_splat/generate_splat_command.rs @@ -0,0 +1,119 @@ +use crate::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; +use crate::commands::enqueue::generate_error::GenerateError; +use crate::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; +use crate::commands::generate::generate_splat::handle_credential_router::handle_credential_router; +use crate::commands::generate::generate_splat::request::{ + TauriGenerateSplatErrorType, TauriGenerateSplatRequest, TauriGenerateSplatResponse, +}; +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::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::task_database::TaskDatabase; +use log::{error, info, warn}; +use tauri::{AppHandle, State}; + +#[tauri::command] +pub async fn generate_splat_command( + request: TauriGenerateSplatRequest, + app: AppHandle, + app_data_root: State<'_, AppDataRoot>, + artcraft_usage_tracker: State<'_, ArtcraftUsageTracker>, + task_database: State<'_, TaskDatabase>, +) -> Response { + + info!("generate_splat_command called, request: {:?}", request); + + let result = handle_request( + request, + &app_data_root, + &artcraft_usage_tracker, + &task_database, + ).await; + + match result { + Err(err) => { + error!("generate_splat_command error: {:?}", err); + + notify_frontend_of_errors(&app, &err).await; + + let mut status = CommandErrorStatus::ServerError; + let mut error_type = TauriGenerateSplatErrorType::ServerError; + 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(_) => { + status = CommandErrorStatus::BadRequest; + error_type = TauriGenerateSplatErrorType::ModelNotSpecified; + error_message = "No model specified for splat generation".to_string(); + } + GenerateError::CredentialProblem(_) => { + status = CommandErrorStatus::Unauthorized; + error_type = TauriGenerateSplatErrorType::CredentialProblem; + error_message = "There's a problem with the selected account.".to_string(); + } + GenerateError::NotYetImplemented(message) => { + error_message = message; + } + _ => {}, // Fall-through + } + + Err(CommandErrorResponseWrapper { + status, + error_message: Some(error_message), + error_type: Some(error_type), + error_details: None, + }) + } + Ok(event) => { + let event = GenerationEnqueueSuccessEvent { + action: event.to_frontend_event_action(), + service: event.to_frontend_event_service(), + model: event.model, + }; + + if let Err(err) = event.send(&app) { + error!("Failed to emit event: {:?}", err); + } + + CreditsBalanceChangedEvent{}.send_infallible(&app); + + Ok(TauriGenerateSplatResponse {}.into()) + } + } +} + +async fn handle_request( + request: TauriGenerateSplatRequest, + app_data_root: &AppDataRoot, + artcraft_usage_tracker: &ArtcraftUsageTracker, + task_database: &TaskDatabase, +) -> Result { + + // Generation is credential-driven: the request names a stored credential, + // and the router dispatches to that credential's service. + let success_event = handle_credential_router(&request, app_data_root).await?; + + let result = success_event + .insert_into_task_database_with_frontend_payload( + task_database, + request.frontend_caller, + request.frontend_subscriber_id.as_deref(), + request.frontend_subscriber_payload.as_deref(), + ) + .await; + + if let Err(err) = result { + error!("Failed to create task in database: {:?}", err); + } + + if let Err(err) = artcraft_usage_tracker.record_object_generation(1, ArtcraftUsageType::ImageToResult, ArtcraftUsagePage::OtherPage) { + warn!("Failed to report usage: {:?}", err); + } + + Ok(success_event) +} diff --git a/crates/artcraftx/src/commands/generate/generate_splat/handle_credential_router.rs b/crates/artcraftx/src/commands/generate/generate_splat/handle_credential_router.rs new file mode 100644 index 00000000..1953d177 --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_splat/handle_credential_router.rs @@ -0,0 +1,130 @@ +use enums::common::generation_provider::GenerationProvider; +use enums::tauri::tasks::task_type::TaskType; +use log::{info, warn}; +use router::api::router_provider::RouterProvider; +use router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use router::client::router_artcraft_client::RouterArtcraftClient; +use router::client::router_client::RouterClient; +use router::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; +use router::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; +use router::api::image_list_ref::ImageListRef; +use router::api::video_ref::VideoRef; + +use crate::api_adapters::models::splat::tauri_splat_model_to_generation_model::tauri_splat_model_to_generation_model; +use crate::api_adapters::models::splat::tauri_splat_model_to_router_model::tauri_splat_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_splat::request::TauriGenerateSplatRequest; +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 splat 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: &TauriGenerateSplatRequest, + app_data_root: &AppDataRoot, +) -> Result { + let credential = resolve_generation_credential( + request.credential_id.as_deref(), + app_data_root, + )?; + + info!( + "handle_credential_router (splat): 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 splat yet", other), + )), + } +} + +/// Generate via the router's Artcraft provider, authenticating with the +/// stored session cookies. Requests carry media tokens only, which Artcraft +/// accepts directly — no pre-upload step is needed. +async fn handle_artcraft_credential( + request: &TauriGenerateSplatRequest, + credential: &Credential, +) -> Result { + let tauri_model = request.model.ok_or(GenerateError::no_model_specified())?; + + let router_model = tauri_splat_model_to_router_model(tauri_model); + let generation_model = tauri_splat_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)?; + + let client = RouterClient::Artcraft(RouterArtcraftClient::new( + api_host, + creds, + )); + + let router_request = GenerateSplatRequestBuilder { + model: router_model, + provider: RouterProvider::Artcraft, + prompt: request.prompt.clone(), + reference_images: request.reference_image_media_tokens.clone().map(ImageListRef::MediaFileTokens), + reference_video: request.reference_video_media_token.clone().map(VideoRef::MediaFileToken), + is_panoramic: request.is_panoramic, + disable_recaption: request.disable_recaption, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + idempotency_token: None, + }; + + info!("Building Artcraft splat generation plan: model={:?}", router_model); + + let generation_request = match router_request.build2() { + Ok(SplatGenerationDraftOrRequest::Request(generation_request)) => generation_request, + Ok(SplatGenerationDraftOrRequest::Draft(draft)) => { + warn!("Artcraft build unexpectedly produced a draft: {:?}", draft); + return Err(GenerateError::NotYetImplemented( + "Artcraft requests should not require a draft phase".to_string(), + )); + } + Err(err) => { + warn!("Could not build Artcraft router request: {:?}", err); + return Err(err.into()); + } + }; + + let response = generation_request.send_request(&client).await + .map_err(|err| { + warn!("Artcraft router splat generation failed: {:?}", err); + GenerateError::from(err) + })?; + + let payload = response.get_artcraft_payload() + .ok_or(GenerateError::ResponseHadNoJobTokens)?; + + info!( + "Router splat generation enqueued: inference_job_token={}, all_tokens={:?}, response={:?}", + payload.inference_job_token.as_str(), + payload.all_inference_job_tokens, + response, + ); + + Ok(TaskEnqueueSuccess { + task_type: TaskType::GaussianGeneration, + model: Some(generation_model), + provider: GenerationProvider::Artcraft, + provider_job_id: Some(payload.inference_job_token.to_string()), + maybe_queue_status_url: None, + maybe_queue_response_url: None, + maybe_prompt_token: None, + }) +} diff --git a/crates/artcraftx/src/commands/generate/generate_splat/mod.rs b/crates/artcraftx/src/commands/generate/generate_splat/mod.rs new file mode 100644 index 00000000..c3349b2c --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_splat/mod.rs @@ -0,0 +1,3 @@ +pub mod generate_splat_command; +mod handle_credential_router; +pub mod request; diff --git a/crates/artcraftx/src/commands/generate/generate_splat/request.rs b/crates/artcraftx/src/commands/generate/generate_splat/request.rs new file mode 100644 index 00000000..588962b8 --- /dev/null +++ b/crates/artcraftx/src/commands/generate/generate_splat/request.rs @@ -0,0 +1,86 @@ +use enums::tauri::ux::tauri_command_caller::TauriCommandCaller; +use serde_derive::{Deserialize, Serialize}; +use tokens::tokens::media_files::MediaFileToken; + +use crate::commands::response::success_response_wrapper::SerializeMarker; + +// ── Request ── + +#[derive(Deserialize, Debug)] +pub struct TauriGenerateSplatRequest { + /// 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, + + /// The model to use. + pub model: Option, + + /// Text prompt. + pub prompt: Option, + + /// Reference images (already uploaded). + pub reference_image_media_tokens: Option>, + + /// Reference video (already uploaded). + pub reference_video_media_token: Option, + + /// The reference image is a 360-degree panorama. + pub is_panoramic: Option, + + /// Disable server-side recaptioning of the prompt. + pub disable_recaption: Option, + + // ── Frontend metadata ── + + /// Name of the frontend caller. + pub frontend_caller: Option, + + /// A frontend-defined identifier sent back as a Tauri event on task completion. + pub frontend_subscriber_id: Option, + + /// A frontend-defined payload sent back as a Tauri event on task completion. + pub frontend_subscriber_payload: Option, +} + +/// The splat models the frontend can request, identified by their omni +/// model ids (`CommonSplatModel` serde strings). +#[derive(Clone, Copy, Debug, Deserialize)] +pub enum TauriSplatModel { + #[serde(rename = "marble_0p1_mini")] + Marble0p1Mini, + #[serde(rename = "marble_0p1_plus")] + Marble0p1Plus, + #[serde(rename = "marble_1p0")] + Marble1p0, + #[serde(rename = "marble_1p0_draft")] + Marble1p0Draft, + #[serde(rename = "marble_1p1")] + Marble1p1, + #[serde(rename = "marble_1p1_plus")] + Marble1p1Plus, + #[serde(rename = "triposplat")] + TripoSplat, +} + +// ── Response ── + +#[derive(Serialize)] +pub struct TauriGenerateSplatResponse { +} + +impl SerializeMarker for TauriGenerateSplatResponse {} + +// ── Error ── + +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TauriGenerateSplatErrorType { + /// Caller didn't specify a model + ModelNotSpecified, + /// Generic server error + ServerError, + /// Problem with the selected account credential (absent, unknown, or + /// unusable). The backend also flashes a dismissable modal. + CredentialProblem, +} diff --git a/crates/artcraftx/src/commands/generate/mod.rs b/crates/artcraftx/src/commands/generate/mod.rs index 580c2626..013bb883 100644 --- a/crates/artcraftx/src/commands/generate/mod.rs +++ b/crates/artcraftx/src/commands/generate/mod.rs @@ -1,4 +1,7 @@ +pub mod generate_audio; pub mod generate_image; +pub mod generate_mesh; +pub mod generate_splat; pub mod generate_video; pub mod common; pub mod models; diff --git a/crates/artcraftx/src/events/generation_events/common.rs b/crates/artcraftx/src/events/generation_events/common.rs index 0b66cb5f..43b4fce6 100644 --- a/crates/artcraftx/src/events/generation_events/common.rs +++ b/crates/artcraftx/src/events/generation_events/common.rs @@ -67,6 +67,22 @@ pub enum GenerationModel { Hunyuan3d2_1, #[serde(rename = "hunyuan_3d_3")] Hunyuan3d3, + #[serde(rename = "hunyuan_3d_3_sketch")] + Hunyuan3d3Sketch, + #[serde(rename = "hunyuan_3d_3p1_pro")] + Hunyuan3d3p1Pro, + #[serde(rename = "hunyuan_3d_3p1_rapid")] + Hunyuan3d3p1Rapid, + #[serde(rename = "hunyuan_3d_3p1_part")] + Hunyuan3d3p1Part, + #[serde(rename = "hunyuan_3d_3p1_topology")] + Hunyuan3d3p1SmartTopology, + #[serde(rename = "tripo3d_h3p1")] + Tripo3dH3p1, + #[serde(rename = "meshy_v6")] + MeshyV6, + #[serde(rename = "rodin_2p5_fast")] + Rodin2p5Fast, #[serde(rename = "worldlabs_marble")] WorldlabsMarble, @@ -74,6 +90,28 @@ pub enum GenerationModel { WorldlabsMarble0p1Mini, #[serde(rename = "worldlabs_marble_0p1_plus")] WorldlabsMarble0p1Plus, + #[serde(rename = "marble_1p0")] + Marble1p0, + #[serde(rename = "marble_1p0_draft")] + Marble1p0Draft, + #[serde(rename = "marble_1p1")] + Marble1p1, + #[serde(rename = "marble_1p1_plus")] + Marble1p1Plus, + #[serde(rename = "triposplat")] + TripoSplat, + + // Audio generation models + #[serde(rename = "suno_music")] + SunoMusic, + #[serde(rename = "suno_remix")] + SunoRemix, + #[serde(rename = "suno_sounds")] + SunoSounds, + #[serde(rename = "suno_sample")] + SunoSample, + #[serde(rename = "seed_audio_1p0")] + SeedAudio1p0, // Generic Midjourney model, version unknown. #[serde(rename = "midjourney")] @@ -142,6 +180,7 @@ pub enum GenerationModel { pub enum GenerationAction { GenerateImage, GenerateVideo, + GenerateAudio, RemoveBackground, #[serde(rename = "image_to_3d")] ImageTo3d, diff --git a/crates/artcraftx/src/lib.rs b/crates/artcraftx/src/lib.rs index c1f76491..320a992e 100644 --- a/crates/artcraftx/src/lib.rs +++ b/crates/artcraftx/src/lib.rs @@ -36,7 +36,10 @@ use crate::commands::download::download_url_command::download_url_command; use crate::commands::enqueue::image_bg_removal::enqueue_image_bg_removal_command::enqueue_image_bg_removal_command; use crate::commands::enqueue::image_to_gaussian::enqueue_image_to_gaussian_command::enqueue_image_to_gaussian_command; use crate::commands::enqueue::image_to_object::enqueue_image_to_3d_object_command::enqueue_image_to_3d_object_command; +use crate::commands::generate::generate_audio::generate_audio_command::generate_audio_command; use crate::commands::generate::generate_image::generate_image_command::generate_image_command; +use crate::commands::generate::generate_mesh::generate_mesh_command::generate_mesh_command; +use crate::commands::generate::generate_splat::generate_splat_command::generate_splat_command; use crate::commands::generate::generate_video::generate_video_command::generate_video_command; use crate::commands::flip_image::flip_image; use crate::commands::get_app_info_command::get_app_info_command; @@ -227,7 +230,10 @@ pub fn run() { list_image_models_command, list_video_models_command, flip_image, + generate_audio_command, generate_image_command, + generate_mesh_command, + generate_splat_command, generate_video_command, get_app_info_command, get_app_preferences_command, diff --git a/crates/artcraftx/src/services/storyteller/threads/storyteller_task_polling_thread/events/maybe_handle_frontend_caller_notification.rs b/crates/artcraftx/src/services/storyteller/threads/storyteller_task_polling_thread/events/maybe_handle_frontend_caller_notification.rs index 71374431..2caa07e6 100644 --- a/crates/artcraftx/src/services/storyteller/threads/storyteller_task_polling_thread/events/maybe_handle_frontend_caller_notification.rs +++ b/crates/artcraftx/src/services/storyteller/threads/storyteller_task_polling_thread/events/maybe_handle_frontend_caller_notification.rs @@ -83,6 +83,9 @@ pub async fn maybe_handle_frontend_caller_notification( job_result, ).await?; } + TaskType::AudioGeneration => { + // No typed audio notification yet; generic completion events still fire. + } } Ok(()) diff --git a/crates/artcraftx/src/threads/third_party_task_polling_thread/events/notify_frontend_of_completion.rs b/crates/artcraftx/src/threads/third_party_task_polling_thread/events/notify_frontend_of_completion.rs index 7c95e9a3..2083b8fa 100644 --- a/crates/artcraftx/src/threads/third_party_task_polling_thread/events/notify_frontend_of_completion.rs +++ b/crates/artcraftx/src/threads/third_party_task_polling_thread/events/notify_frontend_of_completion.rs @@ -76,6 +76,11 @@ pub async fn notify_frontend_of_completion( TaskType::VideoGeneration => { notify_video_generation(app, task, completion) } + TaskType::AudioGeneration => { + // No typed audio notification yet; the generic GenerationCompleteEvent + // above already fired. + Ok(()) + } TaskType::ObjectGeneration => { notify_object_generation(app, task, completion) } @@ -259,6 +264,7 @@ fn task_type_to_generation_action(task_type: TaskType) -> GenerationAction { match task_type { TaskType::ImageGeneration => GenerationAction::GenerateImage, TaskType::VideoGeneration => GenerationAction::GenerateVideo, + TaskType::AudioGeneration => GenerationAction::GenerateAudio, TaskType::BackgroundRemoval => GenerationAction::RemoveBackground, TaskType::ObjectGeneration => GenerationAction::ImageTo3d, TaskType::GaussianGeneration => GenerationAction::GenerateGaussian, @@ -328,6 +334,24 @@ fn task_model_type_to_generation_model(model: TaskModelType) -> Option Some(GenerationModel::Hunyuan3d2_0), TaskModelType::Hunyuan3d2_1 => Some(GenerationModel::Hunyuan3d2_1), TaskModelType::Hunyuan3d3 => Some(GenerationModel::Hunyuan3d3), + TaskModelType::Hunyuan3d3Sketch => Some(GenerationModel::Hunyuan3d3Sketch), + TaskModelType::Hunyuan3d3p1Pro => Some(GenerationModel::Hunyuan3d3p1Pro), + TaskModelType::Hunyuan3d3p1Rapid => Some(GenerationModel::Hunyuan3d3p1Rapid), + TaskModelType::Hunyuan3d3p1Part => Some(GenerationModel::Hunyuan3d3p1Part), + TaskModelType::Hunyuan3d3p1SmartTopology => Some(GenerationModel::Hunyuan3d3p1SmartTopology), + TaskModelType::Tripo3dH3p1 => Some(GenerationModel::Tripo3dH3p1), + TaskModelType::MeshyV6 => Some(GenerationModel::MeshyV6), + TaskModelType::Rodin2p5Fast => Some(GenerationModel::Rodin2p5Fast), + TaskModelType::Marble1p0 => Some(GenerationModel::Marble1p0), + TaskModelType::Marble1p0Draft => Some(GenerationModel::Marble1p0Draft), + TaskModelType::Marble1p1 => Some(GenerationModel::Marble1p1), + TaskModelType::Marble1p1Plus => Some(GenerationModel::Marble1p1Plus), + TaskModelType::TripoSplat => Some(GenerationModel::TripoSplat), + TaskModelType::SunoMusic => Some(GenerationModel::SunoMusic), + TaskModelType::SunoRemix => Some(GenerationModel::SunoRemix), + TaskModelType::SunoSounds => Some(GenerationModel::SunoSounds), + TaskModelType::SunoSample => Some(GenerationModel::SunoSample), + TaskModelType::SeedAudio1p0 => Some(GenerationModel::SeedAudio1p0), TaskModelType::Midjourney => Some(GenerationModel::Midjourney), TaskModelType::Gemini25Flash => Some(GenerationModel::Gemini25Flash), TaskModelType::WorldlabsMarble => Some(GenerationModel::WorldlabsMarble), diff --git a/crates/artcraftx/src/threads/third_party_task_polling_thread/handlers/fal/handle_fal_failure.rs b/crates/artcraftx/src/threads/third_party_task_polling_thread/handlers/fal/handle_fal_failure.rs index 559fe0f6..22963478 100644 --- a/crates/artcraftx/src/threads/third_party_task_polling_thread/handlers/fal/handle_fal_failure.rs +++ b/crates/artcraftx/src/threads/third_party_task_polling_thread/handlers/fal/handle_fal_failure.rs @@ -43,6 +43,7 @@ fn task_type_to_generation_action(task_type: TaskType) -> GenerationAction { match task_type { TaskType::ImageGeneration => GenerationAction::GenerateImage, TaskType::VideoGeneration => GenerationAction::GenerateVideo, + TaskType::AudioGeneration => GenerationAction::GenerateAudio, TaskType::BackgroundRemoval => GenerationAction::RemoveBackground, TaskType::ObjectGeneration => GenerationAction::ImageTo3d, TaskType::GaussianGeneration => GenerationAction::GenerateGaussian, diff --git a/crates/artcraftx/src/utils/enum_conversion/task_type.rs b/crates/artcraftx/src/utils/enum_conversion/task_type.rs index 45992bcf..367d048e 100644 --- a/crates/artcraftx/src/utils/enum_conversion/task_type.rs +++ b/crates/artcraftx/src/utils/enum_conversion/task_type.rs @@ -5,6 +5,7 @@ pub fn to_generation_action(task_type: TaskType) -> GenerationAction { match task_type { TaskType::ImageGeneration => GenerationAction::GenerateImage, TaskType::VideoGeneration => GenerationAction::GenerateVideo, + TaskType::AudioGeneration => GenerationAction::GenerateAudio, TaskType::BackgroundRemoval => GenerationAction::RemoveBackground, TaskType::ObjectGeneration => GenerationAction::ImageTo3d, TaskType::GaussianGeneration => GenerationAction::GenerateGaussian, diff --git a/crates/schema/public/enums/src/tauri/tasks/task_model_type.rs b/crates/schema/public/enums/src/tauri/tasks/task_model_type.rs index 1a468b2a..2fab05e8 100644 --- a/crates/schema/public/enums/src/tauri/tasks/task_model_type.rs +++ b/crates/schema/public/enums/src/tauri/tasks/task_model_type.rs @@ -126,6 +126,46 @@ pub enum TaskModelType { WorldlabsMarble0p1Mini, #[serde(rename = "marble_0p1_plus")] WorldlabsMarble0p1Plus, + #[serde(rename = "hunyuan_3d_3_sketch")] + Hunyuan3d3Sketch, + #[serde(rename = "hunyuan_3d_3p1_pro")] + Hunyuan3d3p1Pro, + #[serde(rename = "hunyuan_3d_3p1_rapid")] + Hunyuan3d3p1Rapid, + #[serde(rename = "hunyuan_3d_3p1_part")] + Hunyuan3d3p1Part, + #[serde(rename = "hunyuan_3d_3p1_topology")] + Hunyuan3d3p1SmartTopology, + #[serde(rename = "tripo3d_h3p1")] + Tripo3dH3p1, + #[serde(rename = "meshy_v6")] + MeshyV6, + #[serde(rename = "rodin_2p5_fast")] + Rodin2p5Fast, + + // Splat generation models + #[serde(rename = "marble_1p0")] + Marble1p0, + #[serde(rename = "marble_1p0_draft")] + Marble1p0Draft, + #[serde(rename = "marble_1p1")] + Marble1p1, + #[serde(rename = "marble_1p1_plus")] + Marble1p1Plus, + #[serde(rename = "triposplat")] + TripoSplat, + + // Audio generation models + #[serde(rename = "suno_music")] + SunoMusic, + #[serde(rename = "suno_remix")] + SunoRemix, + #[serde(rename = "suno_sounds")] + SunoSounds, + #[serde(rename = "suno_sample")] + SunoSample, + #[serde(rename = "seed_audio_1p0")] + SeedAudio1p0, } impl_enum_display_and_debug_using_to_str!(TaskModelType); @@ -194,6 +234,24 @@ impl TaskModelType { Self::WorldlabsMarble => "worldlabs_marble", Self::WorldlabsMarble0p1Mini => "marble_0p1_mini", Self::WorldlabsMarble0p1Plus => "marble_0p1_plus", + Self::Hunyuan3d3Sketch => "hunyuan_3d_3_sketch", + Self::Hunyuan3d3p1Pro => "hunyuan_3d_3p1_pro", + Self::Hunyuan3d3p1Rapid => "hunyuan_3d_3p1_rapid", + Self::Hunyuan3d3p1Part => "hunyuan_3d_3p1_part", + Self::Hunyuan3d3p1SmartTopology => "hunyuan_3d_3p1_topology", + Self::Tripo3dH3p1 => "tripo3d_h3p1", + Self::MeshyV6 => "meshy_v6", + Self::Rodin2p5Fast => "rodin_2p5_fast", + Self::Marble1p0 => "marble_1p0", + Self::Marble1p0Draft => "marble_1p0_draft", + Self::Marble1p1 => "marble_1p1", + Self::Marble1p1Plus => "marble_1p1_plus", + Self::TripoSplat => "triposplat", + Self::SunoMusic => "suno_music", + Self::SunoRemix => "suno_remix", + Self::SunoSounds => "suno_sounds", + Self::SunoSample => "suno_sample", + Self::SeedAudio1p0 => "seed_audio_1p0", } } @@ -256,6 +314,24 @@ impl TaskModelType { "worldlabs_marble" => Ok(Self::WorldlabsMarble), "marble_0p1_mini" => Ok(Self::WorldlabsMarble0p1Mini), "marble_0p1_plus" => Ok(Self::WorldlabsMarble0p1Plus), + "hunyuan_3d_3_sketch" => Ok(Self::Hunyuan3d3Sketch), + "hunyuan_3d_3p1_pro" => Ok(Self::Hunyuan3d3p1Pro), + "hunyuan_3d_3p1_rapid" => Ok(Self::Hunyuan3d3p1Rapid), + "hunyuan_3d_3p1_part" => Ok(Self::Hunyuan3d3p1Part), + "hunyuan_3d_3p1_topology" => Ok(Self::Hunyuan3d3p1SmartTopology), + "tripo3d_h3p1" => Ok(Self::Tripo3dH3p1), + "meshy_v6" => Ok(Self::MeshyV6), + "rodin_2p5_fast" => Ok(Self::Rodin2p5Fast), + "marble_1p0" => Ok(Self::Marble1p0), + "marble_1p0_draft" => Ok(Self::Marble1p0Draft), + "marble_1p1" => Ok(Self::Marble1p1), + "marble_1p1_plus" => Ok(Self::Marble1p1Plus), + "triposplat" => Ok(Self::TripoSplat), + "suno_music" => Ok(Self::SunoMusic), + "suno_remix" => Ok(Self::SunoRemix), + "suno_sounds" => Ok(Self::SunoSounds), + "suno_sample" => Ok(Self::SunoSample), + "seed_audio_1p0" => Ok(Self::SeedAudio1p0), _ => Err(EnumError::CouldNotConvertFromString(value.to_string())), } } @@ -321,6 +397,24 @@ impl TaskModelType { Self::WorldlabsMarble, Self::WorldlabsMarble0p1Mini, Self::WorldlabsMarble0p1Plus, + Self::Hunyuan3d3Sketch, + Self::Hunyuan3d3p1Pro, + Self::Hunyuan3d3p1Rapid, + Self::Hunyuan3d3p1Part, + Self::Hunyuan3d3p1SmartTopology, + Self::Tripo3dH3p1, + Self::MeshyV6, + Self::Rodin2p5Fast, + Self::Marble1p0, + Self::Marble1p0Draft, + Self::Marble1p1, + Self::Marble1p1Plus, + Self::TripoSplat, + Self::SunoMusic, + Self::SunoRemix, + Self::SunoSounds, + Self::SunoSample, + Self::SeedAudio1p0, ]) } } @@ -393,6 +487,24 @@ mod tests { assert_serialization(TaskModelType::WorldlabsMarble, "worldlabs_marble"); assert_serialization(TaskModelType::WorldlabsMarble0p1Mini, "marble_0p1_mini"); assert_serialization(TaskModelType::WorldlabsMarble0p1Plus, "marble_0p1_plus"); + assert_serialization(TaskModelType::Hunyuan3d3Sketch, "hunyuan_3d_3_sketch"); + assert_serialization(TaskModelType::Hunyuan3d3p1Pro, "hunyuan_3d_3p1_pro"); + assert_serialization(TaskModelType::Hunyuan3d3p1Rapid, "hunyuan_3d_3p1_rapid"); + assert_serialization(TaskModelType::Hunyuan3d3p1Part, "hunyuan_3d_3p1_part"); + assert_serialization(TaskModelType::Hunyuan3d3p1SmartTopology, "hunyuan_3d_3p1_topology"); + assert_serialization(TaskModelType::Tripo3dH3p1, "tripo3d_h3p1"); + assert_serialization(TaskModelType::MeshyV6, "meshy_v6"); + assert_serialization(TaskModelType::Rodin2p5Fast, "rodin_2p5_fast"); + assert_serialization(TaskModelType::Marble1p0, "marble_1p0"); + assert_serialization(TaskModelType::Marble1p0Draft, "marble_1p0_draft"); + assert_serialization(TaskModelType::Marble1p1, "marble_1p1"); + assert_serialization(TaskModelType::Marble1p1Plus, "marble_1p1_plus"); + assert_serialization(TaskModelType::TripoSplat, "triposplat"); + assert_serialization(TaskModelType::SunoMusic, "suno_music"); + assert_serialization(TaskModelType::SunoRemix, "suno_remix"); + assert_serialization(TaskModelType::SunoSounds, "suno_sounds"); + assert_serialization(TaskModelType::SunoSample, "suno_sample"); + assert_serialization(TaskModelType::SeedAudio1p0, "seed_audio_1p0"); } #[test] @@ -451,6 +563,24 @@ mod tests { assert_eq!(TaskModelType::WorldlabsMarble.to_str(), "worldlabs_marble"); assert_eq!(TaskModelType::WorldlabsMarble0p1Mini.to_str(), "marble_0p1_mini"); assert_eq!(TaskModelType::WorldlabsMarble0p1Plus.to_str(), "marble_0p1_plus"); + assert_eq!(TaskModelType::Hunyuan3d3Sketch.to_str(), "hunyuan_3d_3_sketch"); + assert_eq!(TaskModelType::Hunyuan3d3p1Pro.to_str(), "hunyuan_3d_3p1_pro"); + assert_eq!(TaskModelType::Hunyuan3d3p1Rapid.to_str(), "hunyuan_3d_3p1_rapid"); + assert_eq!(TaskModelType::Hunyuan3d3p1Part.to_str(), "hunyuan_3d_3p1_part"); + assert_eq!(TaskModelType::Hunyuan3d3p1SmartTopology.to_str(), "hunyuan_3d_3p1_topology"); + assert_eq!(TaskModelType::Tripo3dH3p1.to_str(), "tripo3d_h3p1"); + assert_eq!(TaskModelType::MeshyV6.to_str(), "meshy_v6"); + assert_eq!(TaskModelType::Rodin2p5Fast.to_str(), "rodin_2p5_fast"); + assert_eq!(TaskModelType::Marble1p0.to_str(), "marble_1p0"); + assert_eq!(TaskModelType::Marble1p0Draft.to_str(), "marble_1p0_draft"); + assert_eq!(TaskModelType::Marble1p1.to_str(), "marble_1p1"); + assert_eq!(TaskModelType::Marble1p1Plus.to_str(), "marble_1p1_plus"); + assert_eq!(TaskModelType::TripoSplat.to_str(), "triposplat"); + assert_eq!(TaskModelType::SunoMusic.to_str(), "suno_music"); + assert_eq!(TaskModelType::SunoRemix.to_str(), "suno_remix"); + assert_eq!(TaskModelType::SunoSounds.to_str(), "suno_sounds"); + assert_eq!(TaskModelType::SunoSample.to_str(), "suno_sample"); + assert_eq!(TaskModelType::SeedAudio1p0.to_str(), "seed_audio_1p0"); } #[test] @@ -509,6 +639,24 @@ mod tests { assert_eq!(TaskModelType::from_str("worldlabs_marble").unwrap(), TaskModelType::WorldlabsMarble); assert_eq!(TaskModelType::from_str("marble_0p1_mini").unwrap(), TaskModelType::WorldlabsMarble0p1Mini); assert_eq!(TaskModelType::from_str("marble_0p1_plus").unwrap(), TaskModelType::WorldlabsMarble0p1Plus); + assert_eq!(TaskModelType::from_str("hunyuan_3d_3_sketch").unwrap(), TaskModelType::Hunyuan3d3Sketch); + assert_eq!(TaskModelType::from_str("hunyuan_3d_3p1_pro").unwrap(), TaskModelType::Hunyuan3d3p1Pro); + assert_eq!(TaskModelType::from_str("hunyuan_3d_3p1_rapid").unwrap(), TaskModelType::Hunyuan3d3p1Rapid); + assert_eq!(TaskModelType::from_str("hunyuan_3d_3p1_part").unwrap(), TaskModelType::Hunyuan3d3p1Part); + assert_eq!(TaskModelType::from_str("hunyuan_3d_3p1_topology").unwrap(), TaskModelType::Hunyuan3d3p1SmartTopology); + assert_eq!(TaskModelType::from_str("tripo3d_h3p1").unwrap(), TaskModelType::Tripo3dH3p1); + assert_eq!(TaskModelType::from_str("meshy_v6").unwrap(), TaskModelType::MeshyV6); + assert_eq!(TaskModelType::from_str("rodin_2p5_fast").unwrap(), TaskModelType::Rodin2p5Fast); + assert_eq!(TaskModelType::from_str("marble_1p0").unwrap(), TaskModelType::Marble1p0); + assert_eq!(TaskModelType::from_str("marble_1p0_draft").unwrap(), TaskModelType::Marble1p0Draft); + assert_eq!(TaskModelType::from_str("marble_1p1").unwrap(), TaskModelType::Marble1p1); + assert_eq!(TaskModelType::from_str("marble_1p1_plus").unwrap(), TaskModelType::Marble1p1Plus); + assert_eq!(TaskModelType::from_str("triposplat").unwrap(), TaskModelType::TripoSplat); + assert_eq!(TaskModelType::from_str("suno_music").unwrap(), TaskModelType::SunoMusic); + assert_eq!(TaskModelType::from_str("suno_remix").unwrap(), TaskModelType::SunoRemix); + assert_eq!(TaskModelType::from_str("suno_sounds").unwrap(), TaskModelType::SunoSounds); + assert_eq!(TaskModelType::from_str("suno_sample").unwrap(), TaskModelType::SunoSample); + assert_eq!(TaskModelType::from_str("seed_audio_1p0").unwrap(), TaskModelType::SeedAudio1p0); } #[test] @@ -525,7 +673,7 @@ mod tests { #[test] fn all_variants() { let mut variants = TaskModelType::all_variants(); - assert_eq!(variants.len(), 54); + assert_eq!(variants.len(), 72); // Image models assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Dev)); assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Schnell)); @@ -583,6 +731,24 @@ mod tests { assert_eq!(variants.pop_first(), Some(TaskModelType::WorldlabsMarble)); assert_eq!(variants.pop_first(), Some(TaskModelType::WorldlabsMarble0p1Mini)); assert_eq!(variants.pop_first(), Some(TaskModelType::WorldlabsMarble0p1Plus)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d3Sketch)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d3p1Pro)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d3p1Rapid)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d3p1Part)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Hunyuan3d3p1SmartTopology)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Tripo3dH3p1)); + assert_eq!(variants.pop_first(), Some(TaskModelType::MeshyV6)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Rodin2p5Fast)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Marble1p0)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Marble1p0Draft)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Marble1p1)); + assert_eq!(variants.pop_first(), Some(TaskModelType::Marble1p1Plus)); + assert_eq!(variants.pop_first(), Some(TaskModelType::TripoSplat)); + assert_eq!(variants.pop_first(), Some(TaskModelType::SunoMusic)); + assert_eq!(variants.pop_first(), Some(TaskModelType::SunoRemix)); + assert_eq!(variants.pop_first(), Some(TaskModelType::SunoSounds)); + assert_eq!(variants.pop_first(), Some(TaskModelType::SunoSample)); + assert_eq!(variants.pop_first(), Some(TaskModelType::SeedAudio1p0)); assert_eq!(variants.pop_first(), None); } } diff --git a/crates/schema/public/enums/src/tauri/tasks/task_type.rs b/crates/schema/public/enums/src/tauri/tasks/task_type.rs index 8322e294..14ab9334 100644 --- a/crates/schema/public/enums/src/tauri/tasks/task_type.rs +++ b/crates/schema/public/enums/src/tauri/tasks/task_type.rs @@ -13,6 +13,7 @@ pub enum TaskType { ImageGeneration, ImageInpaintEdit, VideoGeneration, + AudioGeneration, ObjectGeneration, GaussianGeneration, BackgroundRemoval, @@ -30,6 +31,7 @@ impl TaskType { Self::ImageGeneration => "image_generation", Self::ImageInpaintEdit => "image_inpaint_edit", Self::VideoGeneration => "video_generation", + Self::AudioGeneration => "audio_generation", Self::ObjectGeneration => "object_generation", Self::GaussianGeneration => "gaussian_generation", Self::BackgroundRemoval => "background_removal", @@ -41,6 +43,7 @@ impl TaskType { "image_generation" => Ok(Self::ImageGeneration), "image_inpaint_edit" => Ok(Self::ImageInpaintEdit), "video_generation" => Ok(Self::VideoGeneration), + "audio_generation" => Ok(Self::AudioGeneration), "object_generation" => Ok(Self::ObjectGeneration), "gaussian_generation" => Ok(Self::GaussianGeneration), "background_removal" => Ok(Self::BackgroundRemoval), @@ -55,6 +58,7 @@ impl TaskType { Self::ImageGeneration, Self::ImageInpaintEdit, Self::VideoGeneration, + Self::AudioGeneration, Self::ObjectGeneration, Self::GaussianGeneration, Self::BackgroundRemoval, @@ -76,6 +80,7 @@ mod tests { assert_serialization(TaskType::ImageGeneration, "image_generation"); assert_serialization(TaskType::ImageInpaintEdit, "image_inpaint_edit"); assert_serialization(TaskType::VideoGeneration, "video_generation"); + assert_serialization(TaskType::AudioGeneration, "audio_generation"); assert_serialization(TaskType::ObjectGeneration, "object_generation"); assert_serialization(TaskType::GaussianGeneration, "gaussian_generation"); assert_serialization(TaskType::BackgroundRemoval, "background_removal"); @@ -86,6 +91,7 @@ mod tests { assert_eq!(TaskType::ImageGeneration.to_str(), "image_generation"); assert_eq!(TaskType::ImageInpaintEdit.to_str(), "image_inpaint_edit"); assert_eq!(TaskType::VideoGeneration.to_str(), "video_generation"); + assert_eq!(TaskType::AudioGeneration.to_str(), "audio_generation"); assert_eq!(TaskType::ObjectGeneration.to_str(), "object_generation"); assert_eq!(TaskType::GaussianGeneration.to_str(), "gaussian_generation"); assert_eq!(TaskType::BackgroundRemoval.to_str(), "background_removal"); @@ -96,6 +102,7 @@ mod tests { assert_eq!(TaskType::from_str("image_generation").unwrap(), TaskType::ImageGeneration); assert_eq!(TaskType::from_str("image_inpaint_edit").unwrap(), TaskType::ImageInpaintEdit); assert_eq!(TaskType::from_str("video_generation").unwrap(), TaskType::VideoGeneration); + assert_eq!(TaskType::from_str("audio_generation").unwrap(), TaskType::AudioGeneration); assert_eq!(TaskType::from_str("object_generation").unwrap(), TaskType::ObjectGeneration); assert_eq!(TaskType::from_str("gaussian_generation").unwrap(), TaskType::GaussianGeneration); assert_eq!(TaskType::from_str("background_removal").unwrap(), TaskType::BackgroundRemoval); @@ -115,10 +122,11 @@ mod tests { #[test] fn all_variants() { let mut variants = TaskType::all_variants(); - assert_eq!(variants.len(), 6); + assert_eq!(variants.len(), 7); assert_eq!(variants.pop_first(), Some(TaskType::ImageGeneration)); assert_eq!(variants.pop_first(), Some(TaskType::ImageInpaintEdit)); assert_eq!(variants.pop_first(), Some(TaskType::VideoGeneration)); + assert_eq!(variants.pop_first(), Some(TaskType::AudioGeneration)); assert_eq!(variants.pop_first(), Some(TaskType::ObjectGeneration)); assert_eq!(variants.pop_first(), Some(TaskType::GaussianGeneration)); assert_eq!(variants.pop_first(), Some(TaskType::BackgroundRemoval)); diff --git a/frontend/libs/tauri-api/src/index.ts b/frontend/libs/tauri-api/src/index.ts index 0e6245b9..1575dd08 100644 --- a/frontend/libs/tauri-api/src/index.ts +++ b/frontend/libs/tauri-api/src/index.ts @@ -14,7 +14,10 @@ export * from "./lib/enqueue/EnqueueImageTo3dWorld.js"; export * from "./lib/enqueue/EnqueueImageToGaussian.js"; export * from "./lib/enqueue/EnqueueImageToVideo.js"; export * from "./lib/enqueue/EnqueueTextToImage.js"; +export * from "./lib/generate/GenerateAudio.js"; export * from "./lib/generate/GenerateImage.js"; +export * from "./lib/generate/GenerateMesh.js"; +export * from "./lib/generate/GenerateSplat.js"; export * from "./lib/generate/GenerateVideo.js"; export * from "./lib/generate/models/image/ListImageModels.js"; export * from "./lib/generate/models/video/ListVideoModels.js";