From ea1905cfcf27e4bcb022e070df29ca05aae59e14 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 14 Jun 2025 01:25:26 -0400 Subject: [PATCH] tauri bg removal fix (#672) --- Cargo.lock | 10 ++++ Cargo.toml | 2 + crates/desktop/artcraft/Cargo.toml | 1 + .../artcraft/src/core/artcraft_error.rs | 48 +++++++++++++++++++ .../enqueue/object/internal_object_error.rs | 16 +++++++ crates/desktop/artcraft/src/core/mod.rs | 1 + .../utils/download_media_file_to_temp_dir.rs | 4 +- .../utils/save_base64_image_to_temp_dir.rs | 11 ++--- .../fal_background_removal_command.rs | 26 ++++++++-- .../fal_hunyuan_image_to_3d_command.rs | 24 ++++++++-- .../fal_kling_image_to_video_command.rs | 23 +++++++-- .../sora/commands/sora_image_remix_command.rs | 9 ++-- crates/lib/api_clients/fal_client/Cargo.toml | 1 + .../requests/queue/remove_background_rembg.rs | 9 ++-- .../src/media_files/get_media_file.rs | 9 ++-- .../upload_image_media_file_from_file.rs | 19 +++++--- .../recipes/upload_media_file_from_file.rs | 2 +- crates/lib/data/web_base64/Cargo.toml | 25 ++++++++++ crates/lib/data/web_base64/src/lib.rs | 5 ++ .../data/web_base64/src/web_base64_decode.rs | 14 ++++++ crates/vendor/fal-rs/src/queue.rs | 7 ++- 21 files changed, 225 insertions(+), 41 deletions(-) create mode 100644 crates/desktop/artcraft/src/core/artcraft_error.rs create mode 100644 crates/lib/data/web_base64/Cargo.toml create mode 100644 crates/lib/data/web_base64/src/lib.rs create mode 100644 crates/lib/data/web_base64/src/web_base64_decode.rs diff --git a/Cargo.lock b/Cargo.lock index c5ffc174b0..62703108f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -688,6 +688,7 @@ dependencies = [ "tokio", "tokio-util", "url", + "web_base64", "workspace-build-acceleration", ] @@ -3597,6 +3598,7 @@ dependencies = [ "fal", "file_to_base64_url", "futures 0.3.31", + "log", "reqwest 0.12.15", "serde", "testing", @@ -12210,6 +12212,14 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_base64" +version = "0.0.1" +dependencies = [ + "base64 0.22.1", + "workspace-build-acceleration", +] + [[package]] name = "web_scrapers" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 49125c675a..02d95bec34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "crates/lib/data/crockford", "crates/lib/data/datetimes", "crates/lib/data/primitives", + "crates/lib/data/web_base64", "crates/lib/deprecated/config", # TODO(bt, 2023-02-13): Deprecate or repurpose "crates/lib/deprecated/http_server_common", # TODO(bt, 2023-03-20): Move useful pieces to actix_helpers "crates/lib/dimensions", @@ -228,3 +229,4 @@ shared_service_components = { path = "crates/lib/shared_service_components" } storyteller_client = { path = "crates/lib/api_clients/storyteller_client" } subprocess_common = { path = "crates/lib/subprocess_common" } tokens = { path = "crates/schema/public/tokens" } +web_base64= { path = "crates/lib/data/web_base64" } diff --git a/crates/desktop/artcraft/Cargo.toml b/crates/desktop/artcraft/Cargo.toml index 494d9ec0fe..7953bc8e35 100644 --- a/crates/desktop/artcraft/Cargo.toml +++ b/crates/desktop/artcraft/Cargo.toml @@ -35,6 +35,7 @@ openai_sora_client = { workspace = true } primitives = { workspace = true } storyteller_client = { workspace = true } tokens = { workspace = true } +web_base64 = { workspace = true } # External anyhow = { workspace = true } diff --git a/crates/desktop/artcraft/src/core/artcraft_error.rs b/crates/desktop/artcraft/src/core/artcraft_error.rs new file mode 100644 index 0000000000..e6efa82d41 --- /dev/null +++ b/crates/desktop/artcraft/src/core/artcraft_error.rs @@ -0,0 +1,48 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; +use storyteller_client::error::storyteller_error::StorytellerError; + +#[derive(Debug)] +pub enum ArtcraftError { + AnyhowError(anyhow::Error), + DecodeError(base64::DecodeError), + IoError(std::io::Error), + StorytellerError(StorytellerError), +} + +impl Error for ArtcraftError {} + +impl Display for ArtcraftError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::AnyhowError(e) => write!(f, "AnyhowError: {:?}", e), + Self::DecodeError(e) => write!(f, "DecodeError: {:?}", e), + Self::IoError(e) => write!(f, "IoError: {:?}", e), + Self::StorytellerError(e) => write!(f, "StorytellerError: {:?}", e), + } + } +} + +impl From for ArtcraftError { + fn from(value: anyhow::Error) -> Self { + Self::AnyhowError(value) + } +} + +impl From for ArtcraftError { + fn from(value: base64::DecodeError) -> Self { + Self::DecodeError(value) + } +} + +impl From for ArtcraftError { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} + +impl From for ArtcraftError { + fn from(value: StorytellerError) -> Self { + Self::StorytellerError(value) + } +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/object/internal_object_error.rs b/crates/desktop/artcraft/src/core/commands/enqueue/object/internal_object_error.rs index bab6d0d7cd..a85886af73 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/object/internal_object_error.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/object/internal_object_error.rs @@ -1,3 +1,5 @@ +use crate::core::artcraft_error::ArtcraftError; +use anyhow::anyhow; use base64::DecodeError; use errors::AnyhowError; use fal_client::error::fal_error_plus::FalErrorPlus; @@ -47,3 +49,17 @@ impl From for InternalObjectError { Self::IoError(value) } } + +impl From for InternalObjectError { + fn from(value: ArtcraftError) -> Self { + match value { + ArtcraftError::AnyhowError(e) => Self::AnyhowError(e), + ArtcraftError::DecodeError(e) => Self::DecodeError(e), + ArtcraftError::IoError(e) => Self::IoError(e), + ArtcraftError::StorytellerError(e) => Self::StorytellerError(e), + _ => { + Self::AnyhowError(anyhow!("Other error: {:?}", value)) + } + } + } +} diff --git a/crates/desktop/artcraft/src/core/mod.rs b/crates/desktop/artcraft/src/core/mod.rs index da908b11f7..208ed1126b 100644 --- a/crates/desktop/artcraft/src/core/mod.rs +++ b/crates/desktop/artcraft/src/core/mod.rs @@ -1,3 +1,4 @@ +pub mod artcraft_error; pub mod commands; pub mod events; pub mod state; diff --git a/crates/desktop/artcraft/src/core/utils/download_media_file_to_temp_dir.rs b/crates/desktop/artcraft/src/core/utils/download_media_file_to_temp_dir.rs index 611f88affa..c8910e5368 100644 --- a/crates/desktop/artcraft/src/core/utils/download_media_file_to_temp_dir.rs +++ b/crates/desktop/artcraft/src/core/utils/download_media_file_to_temp_dir.rs @@ -1,7 +1,7 @@ +use crate::core::artcraft_error::ArtcraftError; use crate::core::state::data_dir::app_data_root::AppDataRoot; use crate::core::utils::get_url_file_extension::get_url_file_extension; use crate::core::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile; -use errors::AnyhowResult; use storyteller_client::media_files::get_media_file::get_media_file; use storyteller_client::utils::api_host::ApiHost; use tempfile::NamedTempFile; @@ -9,7 +9,7 @@ use tokens::tokens::media_files::MediaFileToken; // TODO: Better more concrete error handling -pub async fn download_media_file_to_temp_dir(app_data_root: &AppDataRoot, token: &MediaFileToken) -> AnyhowResult { +pub async fn download_media_file_to_temp_dir(app_data_root: &AppDataRoot, token: &MediaFileToken) -> Result { let response = get_media_file(&ApiHost::Storyteller, token).await?; let media_file_url = &response.media_file.media_links.cdn_url; diff --git a/crates/desktop/artcraft/src/core/utils/save_base64_image_to_temp_dir.rs b/crates/desktop/artcraft/src/core/utils/save_base64_image_to_temp_dir.rs index 4dcb759f2f..8e5138a1d8 100644 --- a/crates/desktop/artcraft/src/core/utils/save_base64_image_to_temp_dir.rs +++ b/crates/desktop/artcraft/src/core/utils/save_base64_image_to_temp_dir.rs @@ -1,15 +1,12 @@ +use crate::core::artcraft_error::ArtcraftError; use crate::core::state::data_dir::app_data_root::AppDataRoot; -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use errors::AnyhowResult; use mimetypes::mimetype_info::mimetype_info::MimetypeInfo; use std::io::Write; use tempfile::NamedTempFile; +use web_base64::web_base64_decode::web_base64_decode; -// TODO: Retain error types (io/decode) - -pub async fn save_base64_image_to_temp_dir(app_data_root: &AppDataRoot, base64_image: String) -> AnyhowResult { - let bytes = BASE64_STANDARD.decode(base64_image)?; +pub async fn save_base64_image_to_temp_dir(app_data_root: &AppDataRoot, base64_image: String) -> Result { + let bytes = web_base64_decode(&base64_image)?; let extension = MimetypeInfo::get_for_bytes(&bytes) .map(|info| info.file_extension()) diff --git a/crates/desktop/artcraft/src/services/fal/commands/fal_background_removal_command.rs b/crates/desktop/artcraft/src/services/fal/commands/fal_background_removal_command.rs index aef5224350..73f8bbf33c 100644 --- a/crates/desktop/artcraft/src/services/fal/commands/fal_background_removal_command.rs +++ b/crates/desktop/artcraft/src/services/fal/commands/fal_background_removal_command.rs @@ -1,3 +1,4 @@ +use crate::core::artcraft_error::ArtcraftError; use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; use crate::core::commands::response::shorthand::Response; use crate::core::commands::response::success_response_wrapper::SerializeMarker; @@ -49,6 +50,7 @@ use std::path::PathBuf; use std::time::Duration; use storyteller_client::error::api_error::ApiError; use storyteller_client::error::api_error::ApiError::InternalServerError; +use storyteller_client::error::storyteller_error::StorytellerError; use storyteller_client::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse}; use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file; use storyteller_client::utils::api_host::ApiHost; @@ -176,9 +178,10 @@ pub async fn fal_background_removal_command( enum InnerError { FalError(FalErrorPlus), AnyhowError(AnyhowError), - StorytellerApiError(ApiError), + StorytellerError(StorytellerError), DecodeError(DecodeError), IoError(std::io::Error), + ArtcraftError(ArtcraftError), } impl From for InnerError { @@ -193,9 +196,9 @@ impl From for InnerError { } } -impl From for InnerError { - fn from(value: ApiError) -> Self { - Self::StorytellerApiError(value) +impl From for InnerError { + fn from(value: StorytellerError) -> Self { + Self::StorytellerError(value) } } @@ -211,6 +214,17 @@ impl From for InnerError { } } +impl From for InnerError { + fn from(value: ArtcraftError) -> Self { + match value { + ArtcraftError::AnyhowError(e) => Self::AnyhowError(e), + ArtcraftError::DecodeError(e) => Self::DecodeError(e), + ArtcraftError::IoError(e) => Self::IoError(e), + _ => Self::ArtcraftError(value), + } + } +} + pub async fn remove_background( request: FalBackgroundRemovalRequest, app_data_root: &AppDataRoot, @@ -224,9 +238,11 @@ pub async fn remove_background( let mut temp_download; if let Some(media_token) = request.image_media_token { + info!("From media token: {:?}", &media_token); temp_download = download_media_file_to_temp_dir(&app_data_root, &media_token).await?; } else if let Some(base64_bytes) = request.base64_image { + info!("From base64 bytes..."); temp_download = save_base64_image_to_temp_dir(&app_data_root, base64_bytes).await?; } else { return Err(InnerError::AnyhowError(anyhow!("No image media token or base64 image provided"))); @@ -237,6 +253,8 @@ pub async fn remove_background( let filename = temp_download.path().to_path_buf(); let result = remove_background_rembg(filename, &api_key).await?; + + info!("Unpacking FAL result..."); let extension_with_dot = get_url_file_extension(&result.image_url) .map(|ext| format!(".{}", ext)) diff --git a/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs b/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs index 12cfb18759..af50182aae 100644 --- a/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs +++ b/crates/desktop/artcraft/src/services/fal/commands/fal_hunyuan_image_to_3d_command.rs @@ -1,3 +1,4 @@ +use crate::core::artcraft_error::ArtcraftError; use crate::core::commands::response::failure_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus}; use crate::core::commands::response::shorthand::Response; use crate::core::events::basic_sendable_event_trait::BasicSendableEvent; @@ -49,6 +50,7 @@ use std::path::PathBuf; use std::time::Duration; use storyteller_client::error::api_error::ApiError; use storyteller_client::error::api_error::ApiError::InternalServerError; +use storyteller_client::error::storyteller_error::StorytellerError; use storyteller_client::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse}; use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file; use storyteller_client::utils::api_host::ApiHost; @@ -152,9 +154,10 @@ pub async fn fal_hunyuan_image_to_3d_command( enum InnerError { FalError(FalErrorPlus), AnyhowError(AnyhowError), - StorytellerApiError(ApiError), + StorytellerError(StorytellerError), DecodeError(DecodeError), IoError(std::io::Error), + ArtcraftError(ArtcraftError), } impl From for InnerError { @@ -169,9 +172,9 @@ impl From for InnerError { } } -impl From for InnerError { - fn from(value: ApiError) -> Self { - Self::StorytellerApiError(value) +impl From for InnerError { + fn from(value: StorytellerError) -> Self { + Self::StorytellerError(value) } } @@ -187,6 +190,19 @@ impl From for InnerError { } } +impl From for InnerError { + fn from(value: ArtcraftError) -> Self { + match value { + ArtcraftError::AnyhowError(e) => Self::AnyhowError(e), + ArtcraftError::DecodeError(e) => Self::DecodeError(e), + ArtcraftError::IoError(e) => Self::IoError(e), + ArtcraftError::StorytellerError(e) => Self::StorytellerError(e), + _ => Self::ArtcraftError(value), + } + } +} + + pub async fn image_to_3d( request: FalHunyuanImageTo3dRequest, app_data_root: &AppDataRoot, diff --git a/crates/desktop/artcraft/src/services/fal/commands/fal_kling_image_to_video_command.rs b/crates/desktop/artcraft/src/services/fal/commands/fal_kling_image_to_video_command.rs index d80e978a37..828dd0fc24 100644 --- a/crates/desktop/artcraft/src/services/fal/commands/fal_kling_image_to_video_command.rs +++ b/crates/desktop/artcraft/src/services/fal/commands/fal_kling_image_to_video_command.rs @@ -54,7 +54,9 @@ use storyteller_client::media_files::upload_image_media_file_from_file::upload_i use storyteller_client::utils::api_host::ApiHost; use tauri::{AppHandle, Emitter, Manager, State}; use tempfile::NamedTempFile; +use storyteller_client::error::storyteller_error::StorytellerError; use tokens::tokens::media_files::MediaFileToken; +use crate::core::artcraft_error::ArtcraftError; #[derive(Deserialize)] pub struct FalKlingImageToVideoRequest { @@ -157,9 +159,10 @@ pub async fn fal_kling_image_to_video_command( enum InnerError { FalError(FalErrorPlus), AnyhowError(AnyhowError), - StorytellerApiError(ApiError), + StorytellerError(StorytellerError), DecodeError(DecodeError), IoError(std::io::Error), + ArtcraftError(ArtcraftError), } impl From for InnerError { @@ -174,9 +177,9 @@ impl From for InnerError { } } -impl From for InnerError { - fn from(value: ApiError) -> Self { - Self::StorytellerApiError(value) +impl From for InnerError { + fn from(value: StorytellerError) -> Self { + Self::StorytellerError(value) } } @@ -192,6 +195,18 @@ impl From for InnerError { } } +impl From for InnerError { + fn from(value: ArtcraftError) -> Self { + match value { + ArtcraftError::AnyhowError(e) => Self::AnyhowError(e), + ArtcraftError::DecodeError(e) => Self::DecodeError(e), + ArtcraftError::IoError(e) => Self::IoError(e), + ArtcraftError::StorytellerError(e) => Self::StorytellerError(e), + _ => Self::ArtcraftError(value), + } + } +} + pub async fn image_to_video( request: FalKlingImageToVideoRequest, app_data_root: &AppDataRoot, diff --git a/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs b/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs index 4fa0cf0864..dd6ff313ea 100644 --- a/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs +++ b/crates/desktop/artcraft/src/services/sora/commands/sora_image_remix_command.rs @@ -39,6 +39,7 @@ use storyteller_client::error::api_error::ApiError::InternalServerError; use storyteller_client::media_files::get_media_file::get_media_file; use storyteller_client::utils::api_host::ApiHost; use tauri::{AppHandle, Emitter, Manager, State}; +use storyteller_client::error::storyteller_error::StorytellerError; use tokens::tokens::media_files::MediaFileToken; const SORA_IMAGE_UPLOAD_TIMEOUT: Duration = Duration::from_millis(1000 * 30); // 30 seconds @@ -199,7 +200,7 @@ pub async fn sora_image_remix_command( enum InnerError { SoraError(SoraError), AnyhowError(AnyhowError), - StorytellerApiError(ApiError), + StorytellerError(StorytellerError), } impl From for InnerError { @@ -214,9 +215,9 @@ impl From for InnerError { } } -impl From for InnerError { - fn from(value: ApiError) -> Self { - Self::StorytellerApiError(value) +impl From for InnerError { + fn from(value: StorytellerError) -> Self { + Self::StorytellerError(value) } } diff --git a/crates/lib/api_clients/fal_client/Cargo.toml b/crates/lib/api_clients/fal_client/Cargo.toml index 16912f0fa9..3fbe4cdba1 100644 --- a/crates/lib/api_clients/fal_client/Cargo.toml +++ b/crates/lib/api_clients/fal_client/Cargo.toml @@ -23,6 +23,7 @@ errors = { workspace = true } # External anyhow = { workspace = true } futures = { workspace = true } +log = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } tokio = { workspace = true } diff --git a/crates/lib/api_clients/fal_client/src/requests/queue/remove_background_rembg.rs b/crates/lib/api_clients/fal_client/src/requests/queue/remove_background_rembg.rs index 9a6632ee2b..cd464fbb9b 100644 --- a/crates/lib/api_clients/fal_client/src/requests/queue/remove_background_rembg.rs +++ b/crates/lib/api_clients/fal_client/src/requests/queue/remove_background_rembg.rs @@ -22,7 +22,7 @@ pub async fn remove_background_rembg>(image_path: P, api_key: &Fa crop_to_bbox: None, sync_mode: None }; - + let result = rembg(request) .with_api_key(&api_key.0) .queue() @@ -56,6 +56,7 @@ mod tests { use crate::creds::fal_api_key::FalApiKey; use crate::requests::queue::remove_background_rembg::remove_background_rembg; use errors::AnyhowResult; + use std::fs::read_to_string; use testing::test_file_path::test_file_path; #[tokio::test] @@ -63,9 +64,9 @@ mod tests { async fn test_remove_background_rembg() -> AnyhowResult<()> { let image = test_file_path("test_data/image/juno.jpg")?; - // TODO: DO NOT COMMIT SECRET ! - const KEY : &str = "DO NOT COMMIT"; - let api_key = FalApiKey::from_str(KEY); + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); let result = remove_background_rembg(image, &api_key).await?; diff --git a/crates/lib/api_clients/storyteller_client/src/media_files/get_media_file.rs b/crates/lib/api_clients/storyteller_client/src/media_files/get_media_file.rs index 78d897cf86..afa09edaa3 100644 --- a/crates/lib/api_clients/storyteller_client/src/media_files/get_media_file.rs +++ b/crates/lib/api_clients/storyteller_client/src/media_files/get_media_file.rs @@ -1,4 +1,5 @@ use crate::error::api_error::ApiError; +use crate::error::storyteller_error::StorytellerError; use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails; use crate::shared_response_types::media_links::MediaLinks; use crate::shared_response_types::simple_entity_stats::SimpleEntityStats; @@ -23,16 +24,18 @@ use tokens::tokens::model_weights::ModelWeightToken; use tokens::tokens::prompts::PromptToken; /// Get details about a media file from our backend -pub async fn get_media_file(api_host: &ApiHost, media_file_token: &MediaFileToken) -> Result { +pub async fn get_media_file(api_host: &ApiHost, media_file_token: &MediaFileToken) -> Result { let url = get_media_file_token_route(api_host, media_file_token); debug!("Requesting {:?}", &url); let response = http_get_anonymous(url).await?; let response = filter_bad_response(response).await?; - let response_body = &response.text().await?; + let response_body = &response.text().await + .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - let media_file = serde_json::from_str(&response_body)?; + let media_file = serde_json::from_str(&response_body) + .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; Ok(media_file) } diff --git a/crates/lib/api_clients/storyteller_client/src/media_files/upload_image_media_file_from_file.rs b/crates/lib/api_clients/storyteller_client/src/media_files/upload_image_media_file_from_file.rs index 6b9f1083f7..211ed98cfd 100644 --- a/crates/lib/api_clients/storyteller_client/src/media_files/upload_image_media_file_from_file.rs +++ b/crates/lib/api_clients/storyteller_client/src/media_files/upload_image_media_file_from_file.rs @@ -1,5 +1,7 @@ use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; use crate::error::api_error::ApiError; +use crate::error::client_error::ClientError; +use crate::error::storyteller_error::StorytellerError; use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails; use crate::shared_response_types::media_links::MediaLinks; use crate::shared_response_types::simple_entity_stats::SimpleEntityStats; @@ -36,7 +38,7 @@ pub async fn upload_image_media_file_from_file>( api_host: &ApiHost, maybe_creds: Option<&StorytellerCredentialSet>, path: P, -) -> Result { +) -> Result { let url = get_route(api_host); @@ -46,12 +48,14 @@ pub async fn upload_image_media_file_from_file>( let client = Client::builder() .gzip(true) - .build()?; + .build() + .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; let form = Form::new() .text("uuid_idempotency_token", generate_random_uuid()) .file("file", path) - .await?; + .await + .map_err(|err| StorytellerError::Client(ClientError::from(err)))?; let mut request_builder = client.post(url) .header("User-Agent", USER_AGENT) @@ -66,12 +70,15 @@ pub async fn upload_image_media_file_from_file>( let response = request_builder .multipart(form) .send() - .await?; + .await + .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; let response = filter_bad_response(response).await?; - let response_body = &response.text().await?; + let response_body = &response.text().await + .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; - let media_file = serde_json::from_str(&response_body)?; + let media_file = serde_json::from_str(&response_body) + .map_err(|err| StorytellerError::Api(ApiError::from(err)))?; Ok(media_file) } diff --git a/crates/lib/api_clients/storyteller_client/src/recipes/upload_media_file_from_file.rs b/crates/lib/api_clients/storyteller_client/src/recipes/upload_media_file_from_file.rs index 714da66e23..75ef1f0827 100644 --- a/crates/lib/api_clients/storyteller_client/src/recipes/upload_media_file_from_file.rs +++ b/crates/lib/api_clients/storyteller_client/src/recipes/upload_media_file_from_file.rs @@ -54,7 +54,7 @@ pub async fn upload_media_file_from_file>( success: result.success, media_file_token: result.media_file_token, }), - Err(err) => Err(StorytellerError::Api(err)), + Err(err) => Err(err), } } Some(_) => Err(StorytellerError::Client(ClientError::FileTypeNotHandled(path.as_ref().to_path_buf()))), diff --git a/crates/lib/data/web_base64/Cargo.toml b/crates/lib/data/web_base64/Cargo.toml new file mode 100644 index 0000000000..ff3818572c --- /dev/null +++ b/crates/lib/data/web_base64/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "web_base64" +edition = "2024" +version = "0.0.1" +publish = false + +[package.metadata.cargo-udeps.ignore] +normal = ["workspace-build-acceleration"] + +[lib] +name = "web_base64" +path = "src/lib.rs" + +[dependencies] +# Workspace hack (faster compile times) +workspace-build-acceleration = { workspace = true } + +# Internal +# (None) + +# External +base64 = { workspace = true } + +[dev-dependencies] +# (None) diff --git a/crates/lib/data/web_base64/src/lib.rs b/crates/lib/data/web_base64/src/lib.rs new file mode 100644 index 0000000000..c5c08aeb78 --- /dev/null +++ b/crates/lib/data/web_base64/src/lib.rs @@ -0,0 +1,5 @@ +//! Base64 encoding and decoding library that handles data from the web. +//! Most of the base64 we receive from the web is of a particular encoding +//! type, and we also have to juggle URL prefixes. + +pub mod web_base64_decode; diff --git a/crates/lib/data/web_base64/src/web_base64_decode.rs b/crates/lib/data/web_base64/src/web_base64_decode.rs new file mode 100644 index 0000000000..f80161118c --- /dev/null +++ b/crates/lib/data/web_base64/src/web_base64_decode.rs @@ -0,0 +1,14 @@ +use base64::prelude::BASE64_STANDARD; +use base64::Engine; + +pub fn web_base64_decode(base64_string: &str) -> Result, base64::DecodeError> { + // Remove the data URL prefix if it exists + // eg: "data:image/png;base64,..." + let base64_data = if base64_string.starts_with("data:") { + base64_string.split(',').nth(1).unwrap_or(base64_string) + } else { + base64_string + }; + + BASE64_STANDARD.decode(base64_data) +} diff --git a/crates/vendor/fal-rs/src/queue.rs b/crates/vendor/fal-rs/src/queue.rs index 2f5d26d563..598c571ab8 100644 --- a/crates/vendor/fal-rs/src/queue.rs +++ b/crates/vendor/fal-rs/src/queue.rs @@ -22,11 +22,14 @@ pub enum Status { Completed, } +// NB(bt, 2025-06-13): Some of these fields have started to become optional. +// eg, errors: +// "missing field `source`", line: 1, column: 1019 #[derive(Debug, Serialize, Deserialize)] pub struct RequestLog { pub timestamp: String, - pub level: String, - pub source: String, + pub level: Option, // NB(bt): Now optional + pub source: Option, // NB(bt): Now optional pub message: String, }