install other modalities via router

This commit is contained in:
Hanashi 2026-08-03 22:43:55 -04:00
parent a41d4ed204
commit 472c22ad9d
33 changed files with 1464 additions and 2 deletions

View File

@ -0,0 +1,2 @@
pub mod tauri_audio_model_to_generation_model;
pub mod tauri_audio_model_to_router_model;

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -0,0 +1,2 @@
pub mod tauri_mesh_model_to_generation_model;
pub mod tauri_mesh_model_to_router_model;

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -1,2 +1,5 @@
pub mod audio;
pub mod image;
pub mod video;
pub mod mesh;
pub mod splat;

View File

@ -0,0 +1,2 @@
pub mod tauri_splat_model_to_generation_model;
pub mod tauri_splat_model_to_router_model;

View File

@ -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,
}
}

View File

@ -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,
}
}

View File

@ -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),

View File

@ -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<TauriGenerateAudioResponse, TauriGenerateAudioErrorType, ()> {
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<TaskEnqueueSuccess, GenerateError> {
// 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)
}

View File

@ -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<TaskEnqueueSuccess, GenerateError> {
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<TaskEnqueueSuccess, GenerateError> {
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,
})
}

View File

@ -0,0 +1,3 @@
pub mod generate_audio_command;
mod handle_credential_router;
pub mod request;

View File

@ -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<String>,
/// The model to use.
pub model: Option<TauriAudioModel>,
/// Text prompt.
pub prompt: Option<String>,
/// Style/genre prompt (Suno's "tags").
pub style_prompt: Option<String>,
/// Reference audio (already uploaded).
pub audio_media_tokens: Option<Vec<MediaFileToken>>,
/// Reference images (already uploaded).
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Keep the original lyrics (Suno Remix).
pub keep_lyrics: Option<bool>,
/// Instrumental-only toggle (Suno Music / Sample).
pub is_instrumental: Option<bool>,
/// Loopable vs single-hit toggle (Suno Sounds).
pub is_loopable: Option<bool>,
/// Target BPM (Suno Sounds).
pub bpm: Option<u16>,
/// Musical key (Suno Sounds).
pub musical_key: Option<CommonMusicalKey>,
/// Sample rate in Hz (Seed Audio).
pub sample_rate_hz: Option<u32>,
/// Playback speed (Seed Audio, 0.5–2.0).
pub speed: Option<f64>,
/// Volume (Seed Audio, 0.5–2.0).
pub volume: Option<f64>,
/// Pitch shift in semitones (Seed Audio, -12..=12).
pub pitch: Option<f64>,
// ── Frontend metadata ──
/// Name of the frontend caller.
pub frontend_caller: Option<TauriCommandCaller>,
/// A frontend-defined identifier sent back as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// A frontend-defined payload sent back as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
/// 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,
}

View File

@ -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<TauriGenerateMeshResponse, TauriGenerateMeshErrorType, ()> {
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<TaskEnqueueSuccess, GenerateError> {
// 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)
}

View File

@ -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<TaskEnqueueSuccess, GenerateError> {
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<TaskEnqueueSuccess, GenerateError> {
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,
})
}

View File

@ -0,0 +1,3 @@
pub mod generate_mesh_command;
mod handle_credential_router;
pub mod request;

View File

@ -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<String>,
/// The model to use.
pub model: Option<TauriMeshModel>,
/// Text prompt.
pub prompt: Option<String>,
/// Reference images (already uploaded).
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
/// Multi-view images (already uploaded).
pub front_image_media_token: Option<MediaFileToken>,
pub back_image_media_token: Option<MediaFileToken>,
pub left_image_media_token: Option<MediaFileToken>,
pub right_image_media_token: Option<MediaFileToken>,
/// Input mesh for mesh-to-mesh models (part splitting, retopology).
pub input_mesh_media_token: Option<MediaFileToken>,
/// Requested mesh output type.
pub mesh_output_type: Option<CommonMeshOutputType>,
/// Requested polygon type.
pub polygon_type: Option<CommonPolygonType>,
/// Target face count.
pub face_count: Option<u64>,
/// Generate PBR materials.
pub enable_pbr: Option<bool>,
/// Generate textures (off = untextured output).
pub enable_texture: Option<bool>,
/// Texture quality level.
pub texture_quality: Option<CommonMeshQuality>,
/// Geometry quality level.
pub geometry_quality: Option<CommonMeshQuality>,
// ── Frontend metadata ──
/// Name of the frontend caller.
pub frontend_caller: Option<TauriCommandCaller>,
/// A frontend-defined identifier sent back as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// A frontend-defined payload sent back as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
/// 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,
}

View File

@ -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<TauriGenerateSplatResponse, TauriGenerateSplatErrorType, ()> {
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<TaskEnqueueSuccess, GenerateError> {
// 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)
}

View File

@ -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<TaskEnqueueSuccess, GenerateError> {
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<TaskEnqueueSuccess, GenerateError> {
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,
})
}

View File

@ -0,0 +1,3 @@
pub mod generate_splat_command;
mod handle_credential_router;
pub mod request;

View File

@ -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<String>,
/// The model to use.
pub model: Option<TauriSplatModel>,
/// Text prompt.
pub prompt: Option<String>,
/// Reference images (already uploaded).
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
/// Reference video (already uploaded).
pub reference_video_media_token: Option<MediaFileToken>,
/// The reference image is a 360-degree panorama.
pub is_panoramic: Option<bool>,
/// Disable server-side recaptioning of the prompt.
pub disable_recaption: Option<bool>,
// ── Frontend metadata ──
/// Name of the frontend caller.
pub frontend_caller: Option<TauriCommandCaller>,
/// A frontend-defined identifier sent back as a Tauri event on task completion.
pub frontend_subscriber_id: Option<String>,
/// A frontend-defined payload sent back as a Tauri event on task completion.
pub frontend_subscriber_payload: Option<String>,
}
/// 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,
}

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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(())

View File

@ -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<Generatio
TaskModelType::Hunyuan3d2_0 => 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),

View File

@ -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,

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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));

View File

@ -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";