remove background with fal

This commit is contained in:
Brandon Thomas committed 2025-05-13 04:31:35 -04:00
1 parent e0df1644a5
commit c9ccb7680f
13 files changed
+369 -1

No files matched your search

Generated
+115 -1
View File
@@ -1063,7 +1063,7 @@ dependencies = [
"serde_path_to_error",
"serde_qs 0.10.1",
"sha2 0.10.8",
"smart-default",
"smart-default 0.6.0",
"smol_str",
"thiserror 1.0.63",
"tokio",
@@ -4180,6 +4180,17 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "eventsource-stream"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
dependencies = [
"futures-core",
"nom",
"pin-project-lite",
]
[[package]]
name = "exr"
version = "1.73.0"
@@ -4201,6 +4212,67 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fal"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd7dd8565317310f05569296166e2cbdd2bc870173b2b24b77ed681166268d0"
dependencies = [
"base64 0.22.1",
"eventsource-stream",
"fal-derive",
"futures 0.3.29",
"image",
"reqwest 0.12.15",
"serde",
"serde_json",
"smart-default 0.7.1",
"thiserror 1.0.63",
]
[[package]]
name = "fal-derive"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f377f2f18a6bf107f907aa687df6bda2874bd38b25b1531f2a6fb38a9ca643"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "fal_client"
version = "0.0.1"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cookie 0.18.1",
"crockford",
"errors",
"fal",
"file_to_base64_url",
"filesys",
"futures 0.3.29",
"hex-literal",
"idempotency",
"log",
"once_cell",
"rand 0.9.0",
"reqwest 0.12.15",
"serde",
"serde_derive",
"serde_json",
"sha3",
"subprocess_common",
"testing",
"thiserror 2.0.12",
"tokio",
"toml 0.8.20",
"workspace-build-acceleration",
]
[[package]]
name = "fancy-regex"
version = "0.13.0"
@@ -4281,6 +4353,37 @@ dependencies = [
"rustc_version 0.4.0",
]
[[package]]
name = "file_to_base64_url"
version = "0.0.1"
dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cookie 0.18.1",
"crockford",
"errors",
"fal",
"filesys",
"hex-literal",
"idempotency",
"log",
"mimetypes",
"once_cell",
"rand 0.9.0",
"reqwest 0.12.15",
"serde",
"serde_derive",
"serde_json",
"sha3",
"subprocess_common",
"testing",
"thiserror 2.0.12",
"tokio",
"toml 0.8.20",
"workspace-build-acceleration",
]
[[package]]
name = "filesys"
version = "0.0.1"
@@ -11243,6 +11346,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "smart-default"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "smol_str"
version = "0.1.24"
+2
View File
@@ -30,6 +30,7 @@ members = [
"crates/lib/actix_cors_configs",
"crates/lib/actix_helpers",
"crates/lib/api_clients/cloud_storage",
"crates/lib/api_clients/fal_client",
"crates/lib/api_clients/google_drive_common",
"crates/lib/api_clients/google_sign_in",
"crates/lib/api_clients/openai_sora_client",
@@ -54,6 +55,7 @@ members = [
"crates/lib/email_sender",
"crates/lib/errors",
"crates/lib/files/archives",
"crates/lib/files/file_to_base64_url",
"crates/lib/files/filesys",
"crates/lib/files/hashing",
"crates/lib/files/images",
@@ -0,0 +1,48 @@
[package]
name = "fal_client"
edition = "2021"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "fal_client"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
crockford = { path = "../../data/crockford" }
file_to_base64_url = { path = "../../files/file_to_base64_url" }
errors = { path = "../../errors" }
idempotency = { path = "../../idempotency" }
subprocess_common = { path = "../../subprocess_common" }
# External
anyhow = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true}
cookie = { workspace = true }
fal = { version = "0.3.6", features = ["endpoints"] }
hex-literal = { workspace = true }
log = { workspace = true }
once_cell= { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["multipart", "gzip"] }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
sha3 = { workspace = true }
thiserror = { workspace = true }
tokio = { version = "1.38.0", features = ["macros"] }
toml = { workspace = true }
futures = "0.3.29"
[dev-dependencies]
testing = { path = "../../testing" }
filesys = { path = "../../files/filesys" }
@@ -0,0 +1,13 @@
#[derive(Clone)]
pub struct FalApiKey(pub String);
impl FalApiKey {
pub fn new(api_key: String) -> Self {
Self(api_key)
}
pub fn from_str(api_key: &str) -> Self {
Self(api_key.to_string())
}
}
@@ -0,0 +1 @@
pub mod fal_api_key;
@@ -0,0 +1,34 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
/// Additional errors that aren't included in `fal::FalError`.
#[derive(Debug)]
pub enum FalErrorPlus {
/// An error arising in the `fal` crate.
FalError(fal::FalError),
/// Another error we didn't handle.
AnyhowError(anyhow::Error),
}
impl Display for FalErrorPlus {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::FalError(err) => write!(f, "FalErrorPlus::FalError: {:?}", err),
Self::AnyhowError(err) => write!(f, "FalErrorPlus::AnyhowError: {:?}", err),
}
}
}
impl Error for FalErrorPlus {}
impl From<fal::FalError> for FalErrorPlus {
fn from(err: fal::FalError) -> Self {
FalErrorPlus::FalError(err)
}
}
impl From<anyhow::Error> for FalErrorPlus {
fn from(err: anyhow::Error) -> Self {
FalErrorPlus::AnyhowError(err)
}
}
@@ -0,0 +1,7 @@
//! A wrapper for the "fal" crate that bakes in a few extra recipes and utilities.
extern crate fal;
pub mod creds;
pub mod fal_error_plus;
pub mod requests;
@@ -0,0 +1 @@
pub mod remove_background_rembg;
@@ -0,0 +1,73 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::imageutils::rembg::RemoveBackgroundInput;
use fal::prelude::fal_ai::imageutils::rembg::rembg;
use fal::prelude::*;
use fal_client::file_to_base64_url::file_to_base64_url;
use futures::StreamExt;
use std::path::Path;
pub struct RemBgArgs<'a, P: AsRef<Path>> {
pub image_path: P,
pub api_key: &'a FalApiKey,
}
pub struct RemBgResponse {
pub image_url: String,
}
/// remove background using `fal_ai/imageutils/rembg`
pub async fn remove_background_rembg<P: AsRef<Path>>(args: RemBgArgs<'_, P>) -> Result<RemBgResponse, FalErrorPlus> {
let image_url = file_to_base64_url(&args.image_path)?;
let request = RemoveBackgroundInput {
image_url,
crop_to_bbox: None,
sync_mode: None
};
let result = rembg(request)
.with_api_key(&args.api_key.0)
.queue()
.await?;
let mut stream = result.stream(true).await?;
while let Some(status) = stream.next().await {
let status = status?;
if status.status == Status::Completed {
break;
}
}
let output = result.response().await?;
Ok(RemBgResponse {
image_url: output.image.url,
})
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::remove_background_rembg::{remove_background_rembg, RemBgArgs};
use errors::AnyhowResult;
use testing::test_file_path::test_file_path;
#[tokio::test]
#[ignore]
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);
let result = remove_background_rembg(RemBgArgs {
image_path: image,
api_key: &api_key,
}).await?;
Ok(())
}
}
Whitespace-only changes.
@@ -0,0 +1,48 @@
[package]
name = "file_to_base64_url"
edition = "2021"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "fal_client"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
crockford = { path = "../../data/crockford" }
errors = { path = "../../errors" }
idempotency = { path = "../../idempotency" }
subprocess_common = { path = "../../subprocess_common" }
# External
anyhow = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true}
cookie = { workspace = true }
fal = { version = "0.3.6", features = ["endpoints"] }
hex-literal = { workspace = true }
log = { workspace = true }
filesys = { workspace = true }
mimetypes = { workspace = true }
once_cell= { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["multipart", "gzip"] }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
sha3 = { workspace = true }
thiserror = { workspace = true }
tokio = { version = "1.38.0", features = ["macros"] }
toml = { workspace = true }
[dev-dependencies]
testing = { path = "../../testing" }
filesys = { path = "../../files/filesys" }
@@ -0,0 +1,26 @@
use anyhow::anyhow;
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE, BASE64_URL_SAFE_NO_PAD};
use base64::Engine;
use errors::AnyhowResult;
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use std::path::Path;
/// Read a file into a base64 URL-safe string.
pub fn file_to_base64_url<P: AsRef<Path>>(path: P) -> AnyhowResult<String> {
let bytes = std::fs::read(path.as_ref())?;
let mime = match MimetypeInfo::get_for_bytes(&bytes) {
Some(mime) => mime,
None => {
return Err(anyhow!("could not determine mimetype"));
},
};
let base64_bytes = BASE64_STANDARD.encode(&bytes);
match mime.mime_type() {
"image/png" => Ok(format!("data:image/png;base64,{}", base64_bytes)),
"image/jpeg" => Ok(format!("data:image/jpeg;base64,{}", base64_bytes)),
_ => Err(anyhow!("unknown or unsupported mime type: {}", mime.mime_type())),
}
}
@@ -0,0 +1 @@
pub mod file_to_base64_url;