mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-24 18:22:29 +00:00
hunyuan 3d 2.1
This commit is contained in:
+6
-1
@@ -46,6 +46,10 @@ pub struct ExtractedContents {
|
||||
/// Parsed from `payload.model_glb`.
|
||||
pub model_glb: Option<ModelGlbData>,
|
||||
|
||||
/// Parsed from `payload.model_glb_pbr` (e.g. Hunyuan 3D 2.1's PBR-textured
|
||||
/// GLB variant, sent alongside `model_glb`).
|
||||
pub model_glb_pbr: 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!
|
||||
@@ -96,7 +100,8 @@ pub struct VideoData {
|
||||
pub file_size: Option<u64>,
|
||||
}
|
||||
|
||||
/// Data under `payload.model_glb` (there may be other sibling keys too)
|
||||
/// Data under `payload.model_glb` (there may be other sibling keys too).
|
||||
/// Also used for `payload.model_glb_pbr`, which has the same shape.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ModelGlbData {
|
||||
pub content_type: Option<String>,
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::webhook_payload::hydrated::hydrated_webhook_contents::ModelGlbData;
|
||||
|
||||
/// Extract and deserialize the `model_glb_pbr` key from a webhook success
|
||||
/// payload. Sent alongside `model_glb` by e.g. Hunyuan 3D 2.1; same shape.
|
||||
pub(crate) fn extract_model_glb_pbr(obj: &Map<String, Value>) -> Option<ModelGlbData> {
|
||||
let value = obj.get("model_glb_pbr")?;
|
||||
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 model_glbs_from_hunyuan_3d_2p1_test_file() {
|
||||
let webhook = load_test_webhook("success/hunyuan_3d_2p1_model_glbs_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 glb = contents.model_glb.expect("model_glb should be Some");
|
||||
assert_eq!(glb.url.as_deref(), Some("https://v3b.fal.media/files/b/0aa1b916/QKdBTd8WJW_rtPB3IwlEN_demo_textured.glb"));
|
||||
assert_eq!(glb.content_type.as_deref(), Some("application/octet-stream"));
|
||||
assert_eq!(glb.file_name.as_deref(), Some("demo_textured.glb"));
|
||||
assert_eq!(glb.file_size, Some(1574216));
|
||||
|
||||
let glb_pbr = contents.model_glb_pbr.expect("model_glb_pbr should be Some");
|
||||
assert_eq!(glb_pbr.url.as_deref(), Some("https://v3b.fal.media/files/b/0aa1b921/SJw7AARzkc4l9QG5Lrvmu_demo_textured_pbr.glb"));
|
||||
assert_eq!(glb_pbr.content_type.as_deref(), Some("application/octet-stream"));
|
||||
assert_eq!(glb_pbr.file_name.as_deref(), Some("demo_textured_pbr.glb"));
|
||||
assert_eq!(glb_pbr.file_size, Some(7512760));
|
||||
|
||||
// The payload also carries a `model_mesh` zip archive of the whole
|
||||
// generation. It parses out, but the webhook handler prefers the GLBs.
|
||||
let mesh = contents.model_mesh.expect("model_mesh should be Some");
|
||||
assert_eq!(mesh.url.as_deref(), Some("https://v3b.fal.media/files/b/0aa1b915/B25DrTdiZJNV-60RaUh4u_3d_model.zip"));
|
||||
assert_eq!(mesh.file_name.as_deref(), Some("3d_model.zip"));
|
||||
|
||||
assert!(contents.thumbnail.is_none());
|
||||
assert!(contents.preprocessed_image.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthetic_model_glb_pbr_payload() {
|
||||
let obj: serde_json::Map<String, serde_json::Value> = serde_json::from_str(r#"{
|
||||
"model_glb_pbr": {
|
||||
"url": "https://cdn.example.com/model_pbr.glb",
|
||||
"content_type": "model/gltf-binary",
|
||||
"file_name": "model_pbr.glb",
|
||||
"file_size": 7654321
|
||||
}
|
||||
}"#).unwrap();
|
||||
|
||||
let glb_pbr = extract_model_glb_pbr(&obj).expect("should extract model_glb_pbr");
|
||||
assert_eq!(glb_pbr.url.as_deref(), Some("https://cdn.example.com/model_pbr.glb"));
|
||||
assert_eq!(glb_pbr.content_type.as_deref(), Some("model/gltf-binary"));
|
||||
assert_eq!(glb_pbr.file_name.as_deref(), Some("model_pbr.glb"));
|
||||
assert_eq!(glb_pbr.file_size, Some(7654321));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_model_glb_pbr_key_returns_none() {
|
||||
let obj: serde_json::Map<String, serde_json::Value> = serde_json::from_str(r#"{
|
||||
"model_glb": {"url": "https://example.com/model.glb"}
|
||||
}"#).unwrap();
|
||||
|
||||
assert!(extract_model_glb_pbr(&obj).is_none());
|
||||
}
|
||||
}
|
||||
+27
@@ -49,6 +49,33 @@ mod tests {
|
||||
assert!(contents.thumbnail.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn triposplat_ply_splat_from_test_file() {
|
||||
// TripoSplat sends its PLY gaussian splat under `model_mesh`.
|
||||
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 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));
|
||||
|
||||
// The segmented input image rides along as `preprocessed_image`.
|
||||
assert!(contents.preprocessed_image.is_some());
|
||||
|
||||
assert!(contents.model_glb.is_none());
|
||||
assert!(contents.model_glb_pbr.is_none());
|
||||
assert!(contents.thumbnail.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthetic_model_mesh_payload() {
|
||||
let obj: serde_json::Map<String, serde_json::Value> = serde_json::from_str(r#"{
|
||||
|
||||
+8
-3
@@ -2,6 +2,7 @@ pub mod extract_image_payload;
|
||||
pub mod extract_images_payload;
|
||||
pub mod extract_video_payload;
|
||||
pub mod extract_model_glb_payload;
|
||||
pub mod extract_model_glb_pbr_payload;
|
||||
pub mod extract_model_mesh_payload;
|
||||
pub mod extract_model_obj_payload;
|
||||
pub mod extract_preprocessed_image_payload;
|
||||
@@ -15,8 +16,9 @@ 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`, `model_obj`, `result_files`,
|
||||
/// `thumbnail`, `preprocessed_image`. If at least one is found, returns
|
||||
/// `video`, `model_glb`, `model_glb_pbr`, `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.
|
||||
@@ -27,6 +29,7 @@ pub fn extract_contents_from_payload(payload: &Value) -> Option<ExtractedContent
|
||||
let images = extract_images_payload::extract_images(obj);
|
||||
let video = extract_video_payload::extract_video(obj);
|
||||
let model_glb = extract_model_glb_payload::extract_model_glb(obj);
|
||||
let model_glb_pbr = extract_model_glb_pbr_payload::extract_model_glb_pbr(obj);
|
||||
let model_mesh = extract_model_mesh_payload::extract_model_mesh(obj);
|
||||
let model_obj = extract_model_obj_payload::extract_model_obj(obj);
|
||||
let result_files = extract_result_files_payload::extract_result_files(obj);
|
||||
@@ -34,7 +37,8 @@ pub fn extract_contents_from_payload(payload: &Value) -> Option<ExtractedContent
|
||||
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()
|
||||
&& model_glb.is_none() && model_glb_pbr.is_none()
|
||||
&& model_mesh.is_none() && model_obj.is_none()
|
||||
&& result_files.is_none() && thumbnail.is_none() && preprocessed_image.is_none()
|
||||
{
|
||||
return None;
|
||||
@@ -45,6 +49,7 @@ pub fn extract_contents_from_payload(payload: &Value) -> Option<ExtractedContent
|
||||
images,
|
||||
video,
|
||||
model_glb,
|
||||
model_glb_pbr,
|
||||
model_mesh,
|
||||
model_obj,
|
||||
result_files,
|
||||
|
||||
@@ -99,6 +99,7 @@ mod tests {
|
||||
fn from_str_known() {
|
||||
assert_eq!(WebhookErrorType::from_str("content_policy_violation"), WebhookErrorType::ContentPolicyViolation);
|
||||
assert_eq!(WebhookErrorType::from_str("face_detection_error"), WebhookErrorType::FaceDetectionError);
|
||||
assert_eq!(WebhookErrorType::from_str("input_value_error"), WebhookErrorType::InputValueError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"error": null,
|
||||
"gateway_request_id": "019f4d49-6859-77b3-aaac-3b91b248481a",
|
||||
"payload": {
|
||||
"model_glb": {
|
||||
"content_type": "application/octet-stream",
|
||||
"file_name": "demo_textured.glb",
|
||||
"file_size": 1574216,
|
||||
"url": "https://v3b.fal.media/files/b/0aa1b916/QKdBTd8WJW_rtPB3IwlEN_demo_textured.glb"
|
||||
},
|
||||
"model_glb_pbr": {
|
||||
"content_type": "application/octet-stream",
|
||||
"file_name": "demo_textured_pbr.glb",
|
||||
"file_size": 7512760,
|
||||
"url": "https://v3b.fal.media/files/b/0aa1b921/SJw7AARzkc4l9QG5Lrvmu_demo_textured_pbr.glb"
|
||||
},
|
||||
"model_mesh": {
|
||||
"content_type": "application/octet-stream",
|
||||
"file_name": "3d_model.zip",
|
||||
"file_size": 16332900,
|
||||
"url": "https://v3b.fal.media/files/b/0aa1b915/B25DrTdiZJNV-60RaUh4u_3d_model.zip"
|
||||
},
|
||||
"seed": 283904
|
||||
},
|
||||
"request_id": "019f4d49-6859-77b3-aaac-3b91b248481a",
|
||||
"status": "OK"
|
||||
}
|
||||
+14
-1
@@ -96,12 +96,25 @@ pub async fn handle_successful_fal_webhook(
|
||||
}
|
||||
}
|
||||
|
||||
// NB: `model_glb` takes priority over `model_mesh`. Hunyuan 3D 2.1 sends
|
||||
// both, where `model_mesh` is a zip archive of the whole generation that
|
||||
// we skip in favor of the GLBs (standard + optional PBR variant).
|
||||
if let Some(ref model_glb_data) = extracted.model_glb {
|
||||
info!("Handling model_glb payload for request_id {} / job {:?}", request_id, job.job_token);
|
||||
let token = process_model_glb_payload(model_glb_data, extracted.thumbnail.as_ref(), &job, server_state).await?;
|
||||
let (token, batch_token) = process_model_glb_payload(
|
||||
model_glb_data,
|
||||
extracted.model_glb_pbr.as_ref(),
|
||||
extracted.thumbnail.as_ref(),
|
||||
&job,
|
||||
server_state,
|
||||
pager,
|
||||
).await?;
|
||||
if maybe_media_token.is_none() {
|
||||
maybe_media_token = Some(token);
|
||||
}
|
||||
if maybe_batch_token.is_none() {
|
||||
maybe_batch_token = batch_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!
|
||||
|
||||
+96
-102
@@ -1,22 +1,16 @@
|
||||
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::http_server::endpoints::webhooks::fal::process_success::process_model_mesh_payload::{upload_mesh_file, UploadMeshFileArgs};
|
||||
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_engine_category::MediaFileEngineCategory;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
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 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 pager::client::pager::Pager;
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
const PREFIX : Option<&str> = Some("artcraft_");
|
||||
|
||||
/*
|
||||
Example payload, from Hunyuan 3d 3.0,
|
||||
|
||||
@@ -52,14 +46,31 @@ const PREFIX : Option<&str> = Some("artcraft_");
|
||||
"seed": null
|
||||
}
|
||||
|
||||
Hunyuan 3D 2.1 additionally sends a PBR-textured GLB variant (plus a
|
||||
`model_mesh` zip archive of the whole generation, which we skip in favor
|
||||
of the GLBs):
|
||||
|
||||
{
|
||||
"model_glb": { "url": "...demo_textured.glb", ... },
|
||||
"model_glb_pbr": { "url": "...demo_textured_pbr.glb", ... },
|
||||
"model_mesh": { "url": "...3d_model.zip", ... },
|
||||
"seed": 283904
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/// Process a `model_glb` payload, plus the optional `model_glb_pbr` variant
|
||||
/// (e.g. Hunyuan 3D 2.1). Both GLBs are uploaded as mesh media files; when
|
||||
/// both are present they share a batch generation token, with the standard
|
||||
/// GLB as the primary media file.
|
||||
pub async fn process_model_glb_payload(
|
||||
model_glb_data: &ModelGlbData,
|
||||
maybe_model_glb_pbr_data: Option<&ModelGlbData>,
|
||||
maybe_thumbnail_data: Option<&ThumbnailData>,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> Result<MediaFileToken, CommonWebError> {
|
||||
pager: &Pager,
|
||||
) -> Result<(MediaFileToken, Option<BatchGenerationToken>), CommonWebError> {
|
||||
let mesh_url = model_glb_data.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| {
|
||||
@@ -67,100 +78,38 @@ pub async fn process_model_glb_payload(
|
||||
CommonWebError::server_error_with_message("no `url` in model glb payload")
|
||||
})?;
|
||||
|
||||
// Download with a retry if the first attempt returns suspiciously few bytes.
|
||||
let mut file_bytes = http_download_url_to_bytes(mesh_url)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download mesh from {}: {:?}", mesh_url, err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
// NB: We don't create `batch_generations` table records. The foreign key
|
||||
// in `media_files` is enough to look up the rest of the batch.
|
||||
let maybe_batch_token = maybe_model_glb_pbr_data
|
||||
.map(|_| BatchGenerationToken::generate());
|
||||
|
||||
if file_bytes.len() <= 10 {
|
||||
warn!(
|
||||
"Downloaded only {} bytes from {} — retrying once",
|
||||
file_bytes.len(),
|
||||
mesh_url,
|
||||
);
|
||||
file_bytes = http_download_url_to_bytes(mesh_url)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to download mesh on retry from {}: {:?}", mesh_url, e);
|
||||
CommonWebError::server_error_with_message(
|
||||
&format!("Failed to download mesh on retry: {:?}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Resolve mime type: magic bytes first, fal content_type as fallback.
|
||||
let metadata = resolve_file_metadata(&file_bytes, model_glb_data.content_type.as_deref())
|
||||
.ok_or_else(|| {
|
||||
warn!(
|
||||
"Could not determine file type for mesh (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(),
|
||||
model_glb_data.content_type,
|
||||
);
|
||||
CommonWebError::server_error_with_message(
|
||||
&format!("Could not determine file type for mesh (bytes: {}, fal content_type: {:?})",
|
||||
file_bytes.len(), model_glb_data.content_type))
|
||||
})?;
|
||||
|
||||
let mime_type = metadata.mime_type.as_str();
|
||||
|
||||
info!("Mime type of mesh: {}, source: {:?}", mime_type, metadata.source);
|
||||
|
||||
let media_file_type = MediaFileType::try_from_mime_type(mime_type)
|
||||
.ok_or_else(|| {
|
||||
warn!("Unsupported 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 mesh bytes: {:?}", e);
|
||||
CommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let public_upload_path = MediaFileBucketPath::generate_new(PREFIX, Some(&extension_with_period));
|
||||
|
||||
info!("Uploading 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 mesh to bucket: {:?}", e);
|
||||
CommonWebError::from_anyhow_error(e)
|
||||
})?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.creator_ip_address(&job.creator_ip_address)
|
||||
.public_bucket_directory_hash(&public_upload_path)
|
||||
.media_file_class(MediaFileClass::Mesh)
|
||||
.media_file_type(media_file_type)
|
||||
.media_file_origin_category(MediaFileOriginCategory::Inference)
|
||||
.maybe_engine_category(Some(MediaFileEngineCategory::Object))
|
||||
//.media_file_origin_product_category(MediaFileOriginProductCategory::Unknown)
|
||||
.mime_type(mime_type)
|
||||
.file_size_bytes(file_size_bytes as u64)
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.maybe_platform_type(job.maybe_platform_type)
|
||||
.checksum_sha2(&file_hash)
|
||||
.insert_pool(&server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to insert glb media file record: {:?}", e);
|
||||
CommonWebError::from_error(e)
|
||||
})?;
|
||||
let media_token = upload_mesh_file(UploadMeshFileArgs {
|
||||
mesh_url,
|
||||
maybe_content_type: model_glb_data.content_type.as_deref(),
|
||||
maybe_file_name: model_glb_data.file_name.as_deref(),
|
||||
maybe_batch_token: maybe_batch_token.as_ref(),
|
||||
job,
|
||||
server_state,
|
||||
}).await?;
|
||||
|
||||
info!("Glb media file uploaded with token: {}", media_token);
|
||||
|
||||
// Upload the PBR variant into the same batch. NB: Fail open — the primary
|
||||
// GLB is already uploaded, so page ourselves instead of failing the job.
|
||||
if let Some(pbr_data) = maybe_model_glb_pbr_data {
|
||||
let result = upload_model_glb_pbr(
|
||||
pbr_data,
|
||||
maybe_batch_token.as_ref(),
|
||||
job,
|
||||
server_state,
|
||||
).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
warn!("Failed to upload PBR GLB variant: {:?}", err);
|
||||
page_about_pbr_failure(err, job, pager);
|
||||
}
|
||||
}
|
||||
|
||||
let result = try_to_attach_thumbnail(
|
||||
maybe_thumbnail_data,
|
||||
job,
|
||||
@@ -173,6 +122,33 @@ pub async fn process_model_glb_payload(
|
||||
warn!("Could not attach thumbnail as cover image to media file: {:?}", err);
|
||||
}
|
||||
|
||||
Ok((media_token, maybe_batch_token))
|
||||
}
|
||||
|
||||
async fn upload_model_glb_pbr(
|
||||
pbr_data: &ModelGlbData,
|
||||
maybe_batch_token: Option<&BatchGenerationToken>,
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
) -> Result<MediaFileToken, CommonWebError> {
|
||||
let pbr_url = pbr_data.url
|
||||
.as_deref()
|
||||
.ok_or_else(|| {
|
||||
warn!("No `url` in model glb pbr payload");
|
||||
CommonWebError::server_error_with_message("no `url` in model glb pbr payload")
|
||||
})?;
|
||||
|
||||
let media_token = upload_mesh_file(UploadMeshFileArgs {
|
||||
mesh_url: pbr_url,
|
||||
maybe_content_type: pbr_data.content_type.as_deref(),
|
||||
maybe_file_name: pbr_data.file_name.as_deref(),
|
||||
maybe_batch_token,
|
||||
job,
|
||||
server_state,
|
||||
}).await?;
|
||||
|
||||
info!("PBR glb media file uploaded with token: {}", media_token);
|
||||
|
||||
Ok(media_token)
|
||||
}
|
||||
|
||||
@@ -207,3 +183,21 @@ async fn try_to_attach_thumbnail(
|
||||
server_state,
|
||||
}).await
|
||||
}
|
||||
|
||||
fn page_about_pbr_failure(err: CommonWebError, job: &FalJobDetails, pager: &Pager) {
|
||||
let notification = NotificationDetailsBuilder::from_boxed_error(err.into())
|
||||
.set_title("Failure to download PBR GLB variant from FAL webhook".to_string())
|
||||
.set_description(Some(format!(
|
||||
"We uploaded the primary GLB and marked the job as a success, \
|
||||
but the PBR variant failed and the user may need assistance.\n\
|
||||
**Internal Job Token**: {}\n\
|
||||
**Fal ID**: {}\n",
|
||||
job.job_token.as_str(),
|
||||
job.external_third_party_id)))
|
||||
.set_urgency(Some(NotificationUrgency::Medium))
|
||||
.build();
|
||||
|
||||
if let Err(pager_err) = pager.enqueue_page(notification) {
|
||||
warn!("Failed to enqueue PBR GLB failure alert: {:?}", pager_err);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user