install other modalities via router
This commit is contained in:
parent
a41d4ed204
commit
472c22ad9d
|
|
@ -0,0 +1,2 @@
|
|||
pub mod tauri_audio_model_to_generation_model;
|
||||
pub mod tauri_audio_model_to_router_model;
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
pub mod tauri_mesh_model_to_generation_model;
|
||||
pub mod tauri_mesh_model_to_router_model;
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,5 @@
|
|||
pub mod audio;
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
pub mod mesh;
|
||||
pub mod splat;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
pub mod tauri_splat_model_to_generation_model;
|
||||
pub mod tauri_splat_model_to_router_model;
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
pub mod generate_audio_command;
|
||||
mod handle_credential_router;
|
||||
pub mod request;
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
pub mod generate_mesh_command;
|
||||
mod handle_credential_router;
|
||||
pub mod request;
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
pub mod generate_splat_command;
|
||||
mod handle_credential_router;
|
||||
pub mod request;
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in New Issue