upload triposplat ply and attach cover image

This commit is contained in:
Brandon Thomas committed 2026-07-10 14:20:56 -04:00
1 parent cb6d0fe27c
commit 762498daa1
9 files changed
+372 -116

No files matched your search

@@ -47,6 +47,8 @@ pub struct ExtractedContents {
pub model_glb: Option<ModelGlbData>,
/// Parsed from `payload.model_mesh`.
/// NB: `triposplat` ply gaussian splat files also arrive via this payload handler.
/// These are decidedly *not* "mesh" files!
pub model_mesh: Option<ModelMeshData>,
/// Parsed from `payload.model_obj` (e.g. Hunyuan 3D v3.1 Rapid's OBJ output).
@@ -57,6 +59,10 @@ pub struct ExtractedContents {
/// Parsed from `payload.thumbnail`.
pub thumbnail: Option<ThumbnailData>,
/// Parsed from `payload.preprocessed_image` (e.g. TripoSplat's segmented
/// input image, usable as a cover image for the splat result).
pub preprocessed_image: Option<PreprocessedImageData>,
}
/// Data under `payload.image`:
@@ -100,6 +106,8 @@ pub struct ModelGlbData {
}
/// Data under `payload.model_mesh` (there may be other sibling keys too)
/// NB: `triposplat` ply gaussian splat files also arrive via this payload.
/// These are decidedly *not* "mesh" files!
#[derive(Debug, Deserialize)]
pub struct ModelMeshData {
pub content_type: Option<String>,
@@ -140,6 +148,19 @@ pub struct ThumbnailData {
pub url: Option<String>,
}
/// Data under `payload.preprocessed_image` (there may be other sibling keys too).
/// Seen together with `model_mesh` in TripoSplat results: the segmented input
/// image, which we use as a cover image for the splat.
#[derive(Debug, Deserialize)]
pub struct PreprocessedImageData {
pub content_type: Option<String>,
pub file_name: Option<String>,
pub file_size: Option<usize>,
pub height: Option<u64>,
pub url: Option<String>,
pub width: Option<u64>,
}
#[derive(Debug)]
pub struct ErrorData {
/// The first human-readable message from `payload.detail[].msg`, if any.
@@ -0,0 +1,84 @@
use serde_json::{Map, Value};
use crate::webhook_payload::hydrated::hydrated_webhook_contents::PreprocessedImageData;
/// Extract and deserialize the `preprocessed_image` key from a webhook success payload.
pub(crate) fn extract_preprocessed_image(obj: &Map<String, Value>) -> Option<PreprocessedImageData> {
let value = obj.get("preprocessed_image")?;
serde_json::from_value(value.clone()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::webhook_payload::hydrate_webhook_contents::hydrate_webhook_contents;
use crate::webhook_payload::hydrated::hydrated_webhook_contents::HydratedWebhookContents;
use crate::webhook_payload::raw::raw_webhook_payload::RawWebhookPayload;
fn load_test_webhook(filename: &str) -> RawWebhookPayload {
let path = format!("test_data/webhooks/{}", filename);
let json = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e));
serde_json::from_str(&json)
.unwrap_or_else(|e| panic!("Failed to parse {}: {}", path, e))
}
#[test]
fn preprocessed_image_from_triposplat_test_file() {
let webhook = load_test_webhook("success/triposplat_splat_payload_1.json");
let result = hydrate_webhook_contents(&webhook);
let HydratedWebhookContents::Success(data) = result else {
panic!("Expected Success, got {:?}", result);
};
let contents = data.extracted_contents
.expect("extracted_contents should be Some");
let image = contents.preprocessed_image.expect("preprocessed_image should be Some");
assert_eq!(image.url.as_deref(), Some("https://v3b.fal.media/files/b/0aa1b77c/YyJ19g7lJsRNXmvZvahTE_61fc73dbe84446b08b1fcce2167b71d5.png"));
assert_eq!(image.content_type.as_deref(), Some("image/png"));
assert_eq!(image.file_name.as_deref(), Some("61fc73dbe84446b08b1fcce2167b71d5.png"));
assert_eq!(image.file_size, Some(952754));
assert_eq!(image.width, Some(1024));
assert_eq!(image.height, Some(1024));
// The splat itself arrives under `model_mesh`.
let mesh = contents.model_mesh.expect("model_mesh should be Some");
assert_eq!(mesh.url.as_deref(), Some("https://v3b.fal.media/files/b/0aa1b77c/hEzg5xn9A-1qrLsnywjN4_output.ply"));
assert_eq!(mesh.content_type.as_deref(), Some("application/octet-stream"));
assert_eq!(mesh.file_name.as_deref(), Some("output.ply"));
assert_eq!(mesh.file_size, Some(17826208));
}
#[test]
fn synthetic_preprocessed_image_payload() {
let obj: serde_json::Map<String, serde_json::Value> = serde_json::from_str(r#"{
"preprocessed_image": {
"url": "https://cdn.example.com/segmented.png",
"content_type": "image/png",
"file_name": "segmented.png",
"file_size": 12345,
"width": 1024,
"height": 1024
}
}"#).unwrap();
let image = extract_preprocessed_image(&obj).expect("should extract preprocessed_image");
assert_eq!(image.url.as_deref(), Some("https://cdn.example.com/segmented.png"));
assert_eq!(image.content_type.as_deref(), Some("image/png"));
assert_eq!(image.file_name.as_deref(), Some("segmented.png"));
assert_eq!(image.file_size, Some(12345));
assert_eq!(image.width, Some(1024));
assert_eq!(image.height, Some(1024));
}
#[test]
fn missing_preprocessed_image_key_returns_none() {
let obj: serde_json::Map<String, serde_json::Value> = serde_json::from_str(r#"{
"model_mesh": {"url": "https://example.com/output.ply"}
}"#).unwrap();
assert!(extract_preprocessed_image(&obj).is_none());
}
}
@@ -4,6 +4,7 @@ pub mod extract_video_payload;
pub mod extract_model_glb_payload;
pub mod extract_model_mesh_payload;
pub mod extract_model_obj_payload;
pub mod extract_preprocessed_image_payload;
pub mod extract_result_files_payload;
pub mod extract_thumbnail_payload;
@@ -14,7 +15,8 @@ use crate::webhook_payload::hydrated::hydrated_webhook_contents::ExtractedConten
/// Try to extract known content keys from a success payload.
///
/// Checks the payload (as a JSON object) for any of: `image`, `images`,
/// `video`, `model_glb`, `model_mesh`. If at least one is found, returns
/// `video`, `model_glb`, `model_mesh`, `model_obj`, `result_files`,
/// `thumbnail`, `preprocessed_image`. If at least one is found, returns
/// `Some(ExtractedContents)` with copies of the relevant values. Multiple
/// keys can be populated simultaneously. Returns `None` if the payload is
/// not an object or none of the known keys are present.
@@ -29,10 +31,11 @@ pub fn extract_contents_from_payload(payload: &Value) -> Option<ExtractedContent
let model_obj = extract_model_obj_payload::extract_model_obj(obj);
let result_files = extract_result_files_payload::extract_result_files(obj);
let thumbnail = extract_thumbnail_payload::extract_thumbnail(obj);
let preprocessed_image = extract_preprocessed_image_payload::extract_preprocessed_image(obj);
if image.is_none() && images.is_none() && video.is_none()
&& model_glb.is_none() && model_mesh.is_none() && model_obj.is_none()
&& result_files.is_none() && thumbnail.is_none()
&& result_files.is_none() && thumbnail.is_none() && preprocessed_image.is_none()
{
return None;
}
@@ -46,5 +49,6 @@ pub fn extract_contents_from_payload(payload: &Value) -> Option<ExtractedContent
model_obj,
result_files,
thumbnail,
preprocessed_image,
})
}
@@ -0,0 +1,28 @@
{
"error": null,
"gateway_request_id": "019f4d0f-4cf2-76d3-b99b-c400641b3844",
"payload": {
"model_mesh": {
"content_type": "application/octet-stream",
"file_name": "output.ply",
"file_size": 17826208,
"url": "https://v3b.fal.media/files/b/0aa1b77c/hEzg5xn9A-1qrLsnywjN4_output.ply"
},
"num_gaussians": 262144,
"preprocessed_image": {
"content_type": "image/png",
"file_name": "61fc73dbe84446b08b1fcce2167b71d5.png",
"file_size": 952754,
"height": 1024,
"url": "https://v3b.fal.media/files/b/0aa1b77c/YyJ19g7lJsRNXmvZvahTE_61fc73dbe84446b08b1fcce2167b71d5.png",
"width": 1024
},
"seed": 1637204116,
"timings": {
"export": 0.794984320000367,
"inference": 4.911052849000043
}
},
"request_id": "019f4d0f-4cf2-76d3-b99b-c400641b3844",
"status": "OK"
}
@@ -0,0 +1,163 @@
//! Downloads an image from a fal result payload, uploads it to the public
//! bucket, inserts an intermediate media file record, and sets it as the
//! cover image of another media file (e.g. a mesh or gaussian splat).
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::webhooks::fal::process_success::resolve_file_metadata::resolve_file_metadata;
use crate::state::server_state::ServerState;
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::generation_provider::GenerationProvider;
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use log::{info, warn};
use mysql_queries::queries::generic_inference::api_providers::fal::get_inference_job_by_fal_id::FalJobDetails;
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
use mysql_queries::queries::media_files::edit::set_media_file_cover_image::{set_media_file_cover_image, UpdateArgs};
use tokens::tokens::media_files::MediaFileToken;
const PREFIX : Option<&str> = Some("artcraft_");
pub(crate) struct AttachCoverImageArgs<'a> {
pub image_url: &'a str,
pub maybe_content_type: Option<&'a str>,
/// Product category to tag the cover image record with, when the parent
/// media file carries one (e.g. `WorldGeneration` for gaussian splats).
pub maybe_origin_product_category: Option<MediaFileOriginProductCategory>,
/// The media file the cover image is attached to.
pub target_media_token: &'a MediaFileToken,
pub job: &'a FalJobDetails,
pub server_state: &'a ServerState,
}
/// Download a cover image from fal, upload it to the public bucket, insert an
/// intermediate media file record, and set it as the cover image of
/// `target_media_token`. Callers should fail open: a missing cover image is
/// not worth failing the whole webhook over.
pub(crate) async fn attach_cover_image(
args: AttachCoverImageArgs<'_>,
) -> Result<(), CommonWebError> {
let AttachCoverImageArgs {
image_url,
maybe_content_type,
maybe_origin_product_category,
target_media_token,
job,
server_state,
} = args;
// Download with a retry if the first attempt returns suspiciously few bytes.
let mut file_bytes = http_download_url_to_bytes(image_url)
.await
.map_err(|err| {
warn!("Failed to download cover image from {}: {:?}", image_url, err);
CommonWebError::server_error_with_message(
&format!("Failed to download cover image: {:?}", err))
})?;
if file_bytes.len() <= 10 {
warn!(
"Downloaded only {} bytes from {} — retrying once",
file_bytes.len(),
image_url,
);
file_bytes = http_download_url_to_bytes(image_url)
.await
.map_err(|err| {
warn!("Failed to download cover image on retry from {}: {:?}", image_url, err);
CommonWebError::server_error_with_message(
&format!("Failed to download cover image on retry: {:?}", err))
})?;
}
// Resolve mime type: magic bytes first, fal content_type as fallback.
let metadata = resolve_file_metadata(&file_bytes, maybe_content_type)
.ok_or_else(|| {
warn!(
"Could not determine file type for cover image (bytes: {}, fal content_type: {:?})",
file_bytes.len(),
maybe_content_type,
);
CommonWebError::server_error_with_message(
&format!("Could not determine file type for cover image (bytes: {}, fal content_type: {:?})",
file_bytes.len(), maybe_content_type))
})?;
let mime_type = metadata.mime_type.as_str();
info!("Mime type of cover image: {}, source: {:?}", mime_type, metadata.source);
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
.ok_or_else(|| {
warn!("Unsupported cover image media file type: {}", mime_type);
CommonWebError::server_error_with_message(
&format!("Unsupported media file type: {}", mime_type))
})?;
let extension_with_period = metadata.file_extension.extension_with_period();
let file_size_bytes = file_bytes.len();
let file_hash = sha256_hash_bytes(&file_bytes)
.map_err(|err| {
warn!("Failed to hash cover image bytes: {:?}", err);
CommonWebError::from_anyhow_error(err)
})?;
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
info!("Uploading cover image media to bucket path: {}", public_upload_path.get_full_object_path_str());
server_state.public_bucket_client.upload_file_with_content_type_process(
public_upload_path.get_full_object_path_str(),
file_bytes.as_ref(),
&mime_type)
.await
.map_err(|err| {
warn!("Failed to upload cover image to bucket: {:?}", err);
CommonWebError::from_anyhow_error(err)
})?;
let mut insert_builder = MediaFileInsertBuilder::new()
.checksum_sha2(&file_hash)
.creator_ip_address(&job.creator_ip_address)
.file_size_bytes(file_size_bytes as u64)
.is_intermediate_system_file(true)
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.media_file_class(MediaFileClass::Image)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.media_file_type(media_file_type)
.mime_type(mime_type)
.public_bucket_directory_hash(&public_upload_path);
if let Some(origin_product_category) = maybe_origin_product_category {
insert_builder = insert_builder
.media_file_origin_product_category(origin_product_category);
}
let cover_media_token = insert_builder
.insert_pool(&server_state.mysql_pool)
.await
.map_err(|err| {
warn!("Failed to insert cover image media file record: {:?}", err);
CommonWebError::from_error(err)
})?;
let query_result = set_media_file_cover_image(UpdateArgs {
media_file_token: target_media_token,
maybe_cover_image_media_file_token: Some(&cover_media_token),
mysql_pool: &server_state.mysql_pool,
}).await;
if let Err(err) = query_result {
warn!("Failed to set cover image on media file {}: {:?}", target_media_token, err);
}
Ok(())
}
@@ -103,8 +103,10 @@ pub async fn handle_successful_fal_webhook(
maybe_media_token = Some(token);
}
} else if let Some(ref model_mesh_data) = extracted.model_mesh {
// NB: triposplat ply gaussian splat files also arrive via this payload handler.
// These are decidedly *not* "mesh" files!
info!("Handling model_mesh payload for request_id {} / job {:?}", request_id, job.job_token);
let token = process_model_mesh_payload(model_mesh_data, &job, server_state).await?;
let token = process_model_mesh_payload(model_mesh_data, extracted.preprocessed_image.as_ref(), &job, server_state).await?;
if maybe_media_token.is_none() {
maybe_media_token = Some(token);
}
@@ -1,4 +1,5 @@
pub mod handle_successful_fal_webhook;
mod attach_cover_image;
mod process_image_payload;
mod process_images_payload;
mod process_model_glb_payload;
@@ -1,4 +1,5 @@
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::webhooks::fal::process_success::attach_cover_image::{attach_cover_image, AttachCoverImageArgs};
use crate::http_server::endpoints::webhooks::fal::process_success::resolve_file_metadata::resolve_file_metadata;
use crate::state::server_state::ServerState;
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
@@ -10,10 +11,8 @@ use enums::by_table::media_files::media_file_type::MediaFileType;
use fal_client::webhook_payload::hydrated::hydrated_webhook_contents::{ModelGlbData, ThumbnailData};
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use log::{info, warn};
use enums::common::generation_provider::GenerationProvider;
use mysql_queries::queries::generic_inference::api_providers::fal::get_inference_job_by_fal_id::FalJobDetails;
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
use mysql_queries::queries::media_files::edit::set_media_file_cover_image::{set_media_file_cover_image, UpdateArgs};
use tokens::tokens::media_files::MediaFileToken;
const PREFIX : Option<&str> = Some("artcraft_");
@@ -199,108 +198,12 @@ async fn try_to_attach_thumbnail(
CommonWebError::server_error_with_message("no `url` in thumbnail payload")
})?;
// Download with a retry if the first attempt returns suspiciously few bytes.
let mut file_bytes = http_download_url_to_bytes(thumbnail_url)
.await
.map_err(|e| {
warn!("Failed to download thumbnail image from {}: {:?}", thumbnail_url, e);
CommonWebError::server_error_with_message(
&format!("Failed to download thumbnail image: {:?}", e))
})?;
if file_bytes.len() <= 10 {
warn!(
"Downloaded only {} bytes from {} — retrying once",
file_bytes.len(),
thumbnail_url,
);
file_bytes = http_download_url_to_bytes(thumbnail_url)
.await
.map_err(|e| {
warn!("Failed to download thumbnail on retry from {}: {:?}", thumbnail_url, e);
CommonWebError::server_error_with_message(
&format!("Failed to download thumbnail on retry: {:?}", e))
})?;
}
// Resolve mime type: magic bytes first, fal content_type as fallback.
let metadata = resolve_file_metadata(&file_bytes, thumbnail_data.content_type.as_deref())
.ok_or_else(|| {
warn!(
"Could not determine file type for thumbnail (bytes: {}, fal content_type: {:?})",
file_bytes.len(),
thumbnail_data.content_type,
);
CommonWebError::server_error_with_message(
&format!("Could not determine file type for thumbnail (bytes: {}, fal content_type: {:?})",
file_bytes.len(), thumbnail_data.content_type))
})?;
let mime_type = metadata.mime_type.as_str();
info!("Mime type of thumbnail image: {}, source: {:?}", mime_type, metadata.source);
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
.ok_or_else(|| {
warn!("Unsupported thumbnail media file type: {}", mime_type);
CommonWebError::server_error_with_message(
&format!("Unsupported media file type: {}", mime_type))
})?;
let extension_with_period = metadata.file_extension.extension_with_period();
let file_size_bytes = file_bytes.len();
let file_hash = sha256_hash_bytes(&file_bytes)
.map_err(|e| {
warn!("Failed to hash thumbnail bytes: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
info!("Uploading cover image media to bucket path: {}", public_upload_path.get_full_object_path_str());
server_state.public_bucket_client.upload_file_with_content_type_process(
public_upload_path.get_full_object_path_str(),
file_bytes.as_ref(),
&mime_type)
.await
.map_err(|e| {
warn!("Failed to upload thumbnail to bucket: {:?}", e);
CommonWebError::from_anyhow_error(e)
})?;
let thumbnail_media_token = MediaFileInsertBuilder::new()
.checksum_sha2(&file_hash)
.creator_ip_address(&job.creator_ip_address)
.file_size_bytes(file_size_bytes as u64)
.is_intermediate_system_file(true)
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.media_file_class(MediaFileClass::Image)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.media_file_type(media_file_type)
.mime_type(mime_type)
.public_bucket_directory_hash(&public_upload_path)
.insert_pool(&server_state.mysql_pool)
.await
.map_err(|e| {
warn!("Failed to insert thumbnail media file record: {:?}", e);
CommonWebError::from_error(e)
})?;
let query_result = set_media_file_cover_image(UpdateArgs {
media_file_token: glb_media_token,
maybe_cover_image_media_file_token: Some(&thumbnail_media_token),
mysql_pool: &server_state.mysql_pool,
}).await;
if let Err(err) = query_result {
warn!("Failed to set cover image on media file {}: {:?}", glb_media_token, err);
}
Ok(())
attach_cover_image(AttachCoverImageArgs {
image_url: thumbnail_url,
maybe_content_type: thumbnail_data.content_type.as_deref(),
maybe_origin_product_category: None,
target_media_token: glb_media_token,
job,
server_state,
}).await
}
@@ -1,4 +1,5 @@
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::webhooks::fal::process_success::attach_cover_image::{attach_cover_image, AttachCoverImageArgs};
use crate::http_server::endpoints::webhooks::fal::process_success::resolve_file_metadata::resolve_file_metadata_with_file_name;
use crate::state::server_state::ServerState;
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
@@ -8,7 +9,7 @@ use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCat
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use fal_client::webhook_payload::hydrated::hydrated_webhook_contents::{ModelMeshData, ModelObjData};
use fal_client::webhook_payload::hydrated::hydrated_webhook_contents::{ModelMeshData, ModelObjData, PreprocessedImageData};
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
use log::{info, warn};
use enums::common::generation_provider::GenerationProvider;
@@ -19,8 +20,11 @@ use tokens::tokens::media_files::MediaFileToken;
const PREFIX : Option<&str> = Some("artcraft_");
// NB: triposplat ply gaussian splat files also arrive via this payload handler.
// These are decidedly *not* "mesh" files!
pub async fn process_model_mesh_payload(
model_mesh_data: &ModelMeshData,
maybe_preprocessed_image: Option<&PreprocessedImageData>,
job: &FalJobDetails,
server_state: &ServerState,
) -> Result<MediaFileToken, CommonWebError> {
@@ -31,14 +35,31 @@ pub async fn process_model_mesh_payload(
CommonWebError::server_error_with_message("no `url` in model mesh payload")
})?;
upload_mesh_file(UploadMeshFileArgs {
let media_token = upload_mesh_file(UploadMeshFileArgs {
mesh_url,
maybe_content_type: model_mesh_data.content_type.as_deref(),
maybe_file_name: model_mesh_data.file_name.as_deref(),
maybe_batch_token: None,
job,
server_state,
}).await
}).await?;
// TripoSplat sends the segmented input image alongside the splat; attach it
// as the cover image. NB: Fail open — the result is usable without a cover.
if let Some(preprocessed_image) = maybe_preprocessed_image {
let result = try_to_attach_preprocessed_image(
preprocessed_image,
job,
server_state,
&media_token,
).await;
if let Err(err) = result {
warn!("Could not attach preprocessed image as cover image to media file: {:?}", err);
}
}
Ok(media_token)
}
/// `model_obj` payloads (e.g. Hunyuan 3D v3.1 Rapid's OBJ output) have the
@@ -151,9 +172,9 @@ pub(crate) async fn upload_mesh_file(
})?;
// PLY results are Gaussian splats (e.g. TripoSplat), not meshes: store
// them the way the World Labs splat pipeline does — tagged as world
// generation, with no `Object` engine category. No fal mesh endpoint
// returns PLY, so the file type is a reliable discriminator.
// them the way the World Labs splat pipeline does — `Splat` media class,
// tagged as world generation, with no `Object` engine category. No fal
// mesh endpoint returns PLY, so the file type is a reliable discriminator.
let is_gaussian_splat = matches!(media_file_type, MediaFileType::Ply);
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
@@ -180,7 +201,6 @@ pub(crate) async fn upload_mesh_file(
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
.maybe_platform_type(job.maybe_platform_type)
.media_file_class(MediaFileClass::Mesh)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.media_file_type(media_file_type)
.mime_type(mime_type)
@@ -188,9 +208,11 @@ pub(crate) async fn upload_mesh_file(
if is_gaussian_splat {
insert_builder = insert_builder
.media_file_class(MediaFileClass::Splat)
.media_file_origin_product_category(MediaFileOriginProductCategory::WorldGeneration);
} else {
insert_builder = insert_builder
.media_file_class(MediaFileClass::Mesh)
.maybe_engine_category(Some(MediaFileEngineCategory::Object));
}
@@ -206,3 +228,31 @@ pub(crate) async fn upload_mesh_file(
Ok(media_token)
}
/// Attach a TripoSplat `preprocessed_image` (the segmented input image) as
/// the cover image of the uploaded splat, mirroring how `model_glb` results
/// attach their `thumbnail`.
async fn try_to_attach_preprocessed_image(
preprocessed_image: &PreprocessedImageData,
job: &FalJobDetails,
server_state: &ServerState,
splat_media_token: &MediaFileToken,
) -> Result<(), CommonWebError> {
info!("Fal Preprocessed Image Data: {:?}", preprocessed_image);
let image_url = preprocessed_image.url
.as_deref()
.ok_or_else(|| {
warn!("No `url` in preprocessed image payload");
CommonWebError::server_error_with_message("no `url` in preprocessed image payload")
})?;
attach_cover_image(AttachCoverImageArgs {
image_url,
maybe_content_type: preprocessed_image.content_type.as_deref(),
maybe_origin_product_category: Some(MediaFileOriginProductCategory::WorldGeneration),
target_media_token: splat_media_token,
job,
server_state,
}).await
}