mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 01:18:34 +00:00
router wip, need to change artcraft_router
This commit is contained in:
1 parent
5f1ade7600
commit
86bdd4bd4f
11 files changed
+352
-23
No files matched your search
@@ -9,4 +9,11 @@ impl RouterFalClient {
|
||||
pub fn new(api_key: FalApiKey, webhook_url: String) -> Self {
|
||||
Self { api_key, webhook_url }
|
||||
}
|
||||
|
||||
pub fn new_from_raw_key(api_key: &str, webhook_url: String) -> Self {
|
||||
Self {
|
||||
api_key: FalApiKey::from_str(api_key),
|
||||
webhook_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-22
@@ -1,7 +1,8 @@
|
||||
use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::providers::artcraft::handle_artcraft;
|
||||
use crate::core::commands::generate::generate_image::providers::router::handle_router::handle_router;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::{
|
||||
TauriGenerateImageErrorType, TauriGenerateImageRequest, TauriGenerateImageResponse,
|
||||
};
|
||||
@@ -10,6 +11,7 @@ use crate::core::commands::response::shorthand::Response;
|
||||
use crate::core::events::basic_sendable_event_trait::BasicSendableEvent;
|
||||
use crate::core::events::functional_events::credits_balance_changed_event::CreditsBalanceChangedEvent;
|
||||
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
|
||||
use crate::core::providers::credentials::provider_credential_loading_cache::ProviderCredentialLoadingCache;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
@@ -24,32 +26,37 @@ pub async fn generate_image_command(
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
task_database: State<'_, TaskDatabase>,
|
||||
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
|
||||
credential_cache: State<'_, ProviderCredentialLoadingCache>,
|
||||
) -> Response<TauriGenerateImageResponse, TauriGenerateImageErrorType, ()> {
|
||||
|
||||
info!("generate_image_command called, request: {:?}", request);
|
||||
|
||||
// Only Artcraft provider is supported via this command.
|
||||
// Other providers (Grok, Sora, Midjourney) should use the legacy commands.
|
||||
let provider = request.provider.unwrap_or(GenerationProvider::Artcraft);
|
||||
|
||||
let is_artcraft_provider = matches!(
|
||||
request.provider,
|
||||
None | Some(GenerationProvider::Artcraft)
|
||||
);
|
||||
|
||||
if !is_artcraft_provider {
|
||||
return Err(CommandErrorResponseWrapper {
|
||||
status: CommandErrorStatus::BadRequest,
|
||||
error_message: Some("Only Artcraft is supported.".to_string()),
|
||||
error_type: Some(TauriGenerateImageErrorType::NoProviderAvailable),
|
||||
error_details: None,
|
||||
});
|
||||
}
|
||||
|
||||
let result = handle_artcraft(
|
||||
&request,
|
||||
&app_env_configs,
|
||||
&storyteller_creds_manager,
|
||||
).await;
|
||||
let result = match provider {
|
||||
GenerationProvider::Artcraft => {
|
||||
handle_artcraft(
|
||||
&request,
|
||||
&app_env_configs,
|
||||
&storyteller_creds_manager,
|
||||
).await
|
||||
}
|
||||
// Midjourney uses its own legacy command path, not this one.
|
||||
GenerationProvider::Midjourney => {
|
||||
Err(GenerateError::NotYetImplemented(
|
||||
"Midjourney should use its dedicated command".to_string(),
|
||||
))
|
||||
}
|
||||
// All other providers go through the router.
|
||||
other => {
|
||||
handle_router(
|
||||
&request,
|
||||
other,
|
||||
&app_env_configs,
|
||||
&credential_cache,
|
||||
).await
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(success) => handle_success_behavior(&app, &task_database, &request, success).await,
|
||||
@@ -119,6 +126,8 @@ async fn handle_error_behavior(
|
||||
|
||||
let error_type = match &err {
|
||||
GenerateError::BadInput(_) => TauriGenerateImageErrorType::BadInput,
|
||||
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey) => TauriGenerateImageErrorType::NeedsFalApiKey,
|
||||
GenerateError::MissingCredentials(MissingCredentialsReason::NeedsGrokCredentials) => TauriGenerateImageErrorType::NeedsGrokCredentials,
|
||||
GenerateError::MissingCredentials(_) => TauriGenerateImageErrorType::NeedsStorytellerCredentials,
|
||||
GenerateError::NoProviderAvailable => TauriGenerateImageErrorType::NoProviderAvailable,
|
||||
GenerateError::BillingIssue(_) => TauriGenerateImageErrorType::BillingIssue,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod artcraft;
|
||||
pub mod artcraft;
|
||||
pub mod router;
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::client::router_fal_client::RouterFalClient;
|
||||
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use artcraft_router::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use enums::tauri::tasks::task_type::TaskType;
|
||||
use log::info;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::core::api_adapters::models::image::tauri_image_model_to_generation_model::tauri_image_model_to_generation_model;
|
||||
use crate::core::api_adapters::models::image::tauri_image_model_to_router_model::tauri_image_model_to_router_model;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::providers::router::utils::convert_enums_to_router::{convert_aspect_ratio, convert_quality, convert_resolution};
|
||||
use crate::core::commands::generate::generate_image::providers::router::utils::map_media_files_to_urls::map_media_file_tokens_to_cdn_urls;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
|
||||
|
||||
/// Handle image generation via FAL using the user's own API key.
|
||||
pub async fn handle_fal(
|
||||
request: &TauriGenerateImageRequest,
|
||||
api_key: &str,
|
||||
api_host: &ApiHost,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let tauri_model = request.model.ok_or(GenerateError::no_model_specified())?;
|
||||
|
||||
let router_model = tauri_image_model_to_router_model(tauri_model)
|
||||
.ok_or(GenerateError::NotYetImplemented(
|
||||
format!("Model {:?} is not supported via the FAL router path", tauri_model),
|
||||
))?;
|
||||
|
||||
// Collect all media file tokens that need resolving.
|
||||
let image_inputs = resolve_image_inputs(request, api_host).await?;
|
||||
|
||||
let webhook_url = build_fal_webhook_url(api_host);
|
||||
|
||||
let router_request = GenerateImageRequest {
|
||||
model: router_model,
|
||||
provider: Provider::Fal,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
resolution: request.resolution.map(convert_resolution),
|
||||
aspect_ratio: request.aspect_ratio.map(convert_aspect_ratio),
|
||||
quality: request.quality.map(convert_quality),
|
||||
image_batch_count: request.batch_size.map(|n| n as u16),
|
||||
horizontal_angle: request.adjust_horizontal_angle,
|
||||
vertical_angle: request.adjust_vertical_angle,
|
||||
zoom: request.adjust_zoom,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
|
||||
info!("Building FAL image generation plan: model={:?}", router_model);
|
||||
let plan = router_request.build()?;
|
||||
|
||||
let fal_client = RouterFalClient::new_from_raw_key(api_key, webhook_url);
|
||||
let client = RouterClient::Fal(fal_client);
|
||||
|
||||
info!("Executing FAL image generation...");
|
||||
let response = plan.generate_image(&client).await?;
|
||||
|
||||
build_task_enqueue_success(tauri_model, response)
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
/// Collect media file tokens from the request and resolve them to CDN URLs.
|
||||
///
|
||||
/// FAL accepts image URLs directly, so we map our media file tokens to their
|
||||
/// CDN URLs rather than downloading the bytes locally.
|
||||
async fn resolve_image_inputs(
|
||||
request: &TauriGenerateImageRequest,
|
||||
api_host: &ApiHost,
|
||||
) -> Result<Option<ImageListRef>, GenerateError> {
|
||||
let mut tokens: Vec<MediaFileToken> = Vec::new();
|
||||
|
||||
if let Some(canvas_token) = &request.canvas_image_media_token {
|
||||
tokens.push(canvas_token.clone());
|
||||
}
|
||||
|
||||
if let Some(scene_token) = &request.scene_image_media_token {
|
||||
tokens.push(scene_token.clone());
|
||||
}
|
||||
|
||||
if let Some(media_tokens) = &request.image_media_tokens {
|
||||
tokens.extend(media_tokens.clone());
|
||||
}
|
||||
|
||||
if tokens.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let urls = map_media_file_tokens_to_cdn_urls(&tokens, api_host).await?;
|
||||
Ok(Some(ImageListRef::Urls(urls)))
|
||||
}
|
||||
|
||||
fn build_fal_webhook_url(api_host: &ApiHost) -> String {
|
||||
let base = api_host.to_api_hostname_and_scheme();
|
||||
format!("{}/v1/webhooks/fal", base)
|
||||
}
|
||||
|
||||
fn build_task_enqueue_success(
|
||||
tauri_model: TauriImageModel,
|
||||
response: GenerateImageResponse,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let fal_payload = response.get_fal_payload()
|
||||
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
|
||||
|
||||
let provider_job_id = fal_payload.request_id
|
||||
.or(fal_payload.gateway_request_id)
|
||||
.ok_or(GenerateError::ResponseHadNoJobTokens)?;
|
||||
|
||||
let generation_model = tauri_image_model_to_generation_model(tauri_model);
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
task_type: TaskType::ImageGeneration,
|
||||
model: Some(generation_model),
|
||||
provider: GenerationProvider::Fal,
|
||||
provider_job_id: Some(provider_job_id),
|
||||
})
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use log::info;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::{GenerateError, MissingCredentialsReason};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::providers::router::handle_api_providers::handle_fal;
|
||||
use crate::core::commands::generate::generate_image::providers::router::handle_web_login_providers::handle_web_login_provider;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::providers::credentials::payload::provider_credential_payload::ProviderCredentialPayload;
|
||||
use crate::core::providers::credentials::provider_credential_key::ProviderCredentialKey;
|
||||
use crate::core::providers::credentials::provider_credential_loading_cache::ProviderCredentialLoadingCache;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
/// Dispatch an image generation request to a third-party provider via the router.
|
||||
///
|
||||
/// This handles providers that authenticate with the user's own credentials
|
||||
/// (API key or web login) rather than going through the Artcraft backend.
|
||||
pub async fn handle_router(
|
||||
request: &TauriGenerateImageRequest,
|
||||
provider: GenerationProvider,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
credential_cache: &ProviderCredentialLoadingCache,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
let credential_key = map_provider_to_credential_key(provider)?;
|
||||
|
||||
info!("handle_router: provider={:?}, credential_key={:?}", provider, credential_key);
|
||||
|
||||
let payload = credential_cache.get_credentials(credential_key)
|
||||
.map_err(|err| {
|
||||
GenerateError::AnyhowError(anyhow::anyhow!("Failed to load credentials: {:?}", err))
|
||||
})?
|
||||
.ok_or_else(|| map_provider_to_missing_credentials_error(provider))?;
|
||||
|
||||
match payload {
|
||||
ProviderCredentialPayload::ApiKey(api_key_data) => {
|
||||
handle_api_key_provider(request, provider, api_key_data.as_str(), app_env_configs).await
|
||||
}
|
||||
ProviderCredentialPayload::WebLogin(web_login_data) => {
|
||||
handle_web_login_provider(request, provider, &web_login_data).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
async fn handle_api_key_provider(
|
||||
request: &TauriGenerateImageRequest,
|
||||
provider: GenerationProvider,
|
||||
api_key: &str,
|
||||
app_env_configs: &AppEnvConfigs,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
match provider {
|
||||
GenerationProvider::Fal => {
|
||||
handle_fal(request, api_key, &app_env_configs.storyteller_host).await
|
||||
}
|
||||
_ => {
|
||||
Err(GenerateError::NotYetImplemented(
|
||||
format!("API key provider {:?} is not yet supported via the router path", provider),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_provider_to_credential_key(
|
||||
provider: GenerationProvider,
|
||||
) -> Result<ProviderCredentialKey, GenerateError> {
|
||||
match provider {
|
||||
GenerationProvider::Fal => Ok(ProviderCredentialKey::FalApiKey),
|
||||
_ => Err(GenerateError::NotYetImplemented(
|
||||
format!("Provider {:?} does not have a mapped credential key yet", provider),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_provider_to_missing_credentials_error(provider: GenerationProvider) -> GenerateError {
|
||||
match provider {
|
||||
GenerationProvider::Fal => GenerateError::MissingCredentials(MissingCredentialsReason::NeedsFalApiKey),
|
||||
GenerationProvider::Grok => GenerateError::MissingCredentials(MissingCredentialsReason::NeedsGrokCredentials),
|
||||
GenerationProvider::Midjourney => GenerateError::MissingCredentials(MissingCredentialsReason::NeedsMidjourneyCredentials),
|
||||
GenerationProvider::Sora => GenerateError::MissingCredentials(MissingCredentialsReason::NeedsSoraCredentials),
|
||||
GenerationProvider::WorldLabs => GenerateError::MissingCredentials(MissingCredentialsReason::NeedsWorldLabsCredentials),
|
||||
_ => GenerateError::NoProviderAvailable,
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_image::tauri_generate_image_request::TauriGenerateImageRequest;
|
||||
use crate::core::providers::credentials::payload::web_login::WebLoginData;
|
||||
|
||||
/// Handle image generation for providers that authenticate via web login.
|
||||
///
|
||||
/// Not yet implemented — placeholder for Grok, Runway, Higgsfield, etc.
|
||||
pub async fn handle_web_login_provider(
|
||||
_request: &TauriGenerateImageRequest,
|
||||
_provider: GenerationProvider,
|
||||
_web_login: &WebLoginData,
|
||||
) -> Result<TaskEnqueueSuccess, GenerateError> {
|
||||
unimplemented!("Web login provider image generation is not yet supported via the router path")
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
pub mod handle_router;
|
||||
mod handle_api_providers;
|
||||
mod handle_web_login_providers;
|
||||
mod utils;
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterAspectRatio;
|
||||
use artcraft_router::api::common_quality::CommonQuality as RouterQuality;
|
||||
use artcraft_router::api::common_resolution::CommonResolution as RouterResolution;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as EnumsAspectRatio;
|
||||
use enums::common::generation::common_quality::CommonQuality as EnumsQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution as EnumsResolution;
|
||||
|
||||
pub fn convert_aspect_ratio(value: EnumsAspectRatio) -> RouterAspectRatio {
|
||||
match value {
|
||||
EnumsAspectRatio::Auto => RouterAspectRatio::Auto,
|
||||
EnumsAspectRatio::Square => RouterAspectRatio::Square,
|
||||
EnumsAspectRatio::WideThreeByTwo => RouterAspectRatio::WideThreeByTwo,
|
||||
EnumsAspectRatio::WideFourByThree => RouterAspectRatio::WideFourByThree,
|
||||
EnumsAspectRatio::WideFiveByFour => RouterAspectRatio::WideFiveByFour,
|
||||
EnumsAspectRatio::WideSixteenByNine => RouterAspectRatio::WideSixteenByNine,
|
||||
EnumsAspectRatio::WideTwentyOneByNine => RouterAspectRatio::WideTwentyOneByNine,
|
||||
EnumsAspectRatio::TallTwoByThree => RouterAspectRatio::TallTwoByThree,
|
||||
EnumsAspectRatio::TallThreeByFour => RouterAspectRatio::TallThreeByFour,
|
||||
EnumsAspectRatio::TallFourByFive => RouterAspectRatio::TallFourByFive,
|
||||
EnumsAspectRatio::TallNineBySixteen => RouterAspectRatio::TallNineBySixteen,
|
||||
EnumsAspectRatio::TallNineByTwentyOne => RouterAspectRatio::TallNineByTwentyOne,
|
||||
EnumsAspectRatio::Wide => RouterAspectRatio::Wide,
|
||||
EnumsAspectRatio::Tall => RouterAspectRatio::Tall,
|
||||
EnumsAspectRatio::Auto2k => RouterAspectRatio::Auto2k,
|
||||
EnumsAspectRatio::Auto3k => RouterAspectRatio::Auto3k,
|
||||
EnumsAspectRatio::Auto4k => RouterAspectRatio::Auto4k,
|
||||
EnumsAspectRatio::SquareHd => RouterAspectRatio::SquareHd,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_quality(value: EnumsQuality) -> RouterQuality {
|
||||
match value {
|
||||
EnumsQuality::High => RouterQuality::High,
|
||||
EnumsQuality::Medium => RouterQuality::Medium,
|
||||
EnumsQuality::Low => RouterQuality::Low,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_resolution(value: EnumsResolution) -> RouterResolution {
|
||||
match value {
|
||||
EnumsResolution::OneK => RouterResolution::OneK,
|
||||
EnumsResolution::TwoK => RouterResolution::TwoK,
|
||||
EnumsResolution::ThreeK => RouterResolution::ThreeK,
|
||||
EnumsResolution::FourK => RouterResolution::FourK,
|
||||
EnumsResolution::HalfK => RouterResolution::HalfK,
|
||||
EnumsResolution::FourEightyP => RouterResolution::FourEightyP,
|
||||
EnumsResolution::SevenTwentyP => RouterResolution::SevenTwentyP,
|
||||
EnumsResolution::TenEightyP => RouterResolution::TenEightyP,
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
use artcraft_client::endpoints::media_files::get_media_file::get_media_file;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use log::info;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
|
||||
/// Resolve a list of media file tokens to their CDN URLs.
|
||||
///
|
||||
/// FAL (and other third-party providers) can fetch images by URL directly,
|
||||
/// so we just need the CDN link — no need to download the bytes locally.
|
||||
pub async fn map_media_file_tokens_to_cdn_urls(
|
||||
tokens: &[MediaFileToken],
|
||||
api_host: &ApiHost,
|
||||
) -> Result<Vec<String>, GenerateError> {
|
||||
let mut urls = Vec::with_capacity(tokens.len());
|
||||
|
||||
for token in tokens {
|
||||
info!("Resolving media file token {} to CDN URL...", token.as_str());
|
||||
let response = get_media_file(api_host, token).await?;
|
||||
let cdn_url = response.media_file.media_links.cdn_url.to_string();
|
||||
info!("Resolved {} -> {}", token.as_str(), &cdn_url);
|
||||
urls.push(cdn_url);
|
||||
}
|
||||
|
||||
Ok(urls)
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
pub mod convert_enums_to_router;
|
||||
pub mod map_media_files_to_urls;
|
||||
+2
@@ -116,6 +116,8 @@ pub enum TauriGenerateImageErrorType {
|
||||
ServerError,
|
||||
/// Needs to be logged into Artcraft
|
||||
NeedsStorytellerCredentials,
|
||||
/// Needs a FAL API key
|
||||
NeedsFalApiKey,
|
||||
/// Needs Grok credentials
|
||||
NeedsGrokCredentials,
|
||||
/// Billing issue
|
||||
|
||||
Reference in new issue
Block a user