diff --git a/crates/api_clients/seedance2pro_client/src/requests/poll_orders/poll_orders.rs b/crates/api_clients/seedance2pro_client/src/requests/poll_orders/poll_orders.rs index 46d38b2d17..5a7198801f 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/poll_orders/poll_orders.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/poll_orders/poll_orders.rs @@ -107,10 +107,13 @@ impl TaskStatus { #[derive(Debug, Clone)] pub struct MediaResult { pub url: String, - pub width: u32, - pub height: u32, + /// Frame width in pixels. `None` when the API returns `null`/omits it + /// (it does so intermittently); callers should persist NULL, not a sentinel. + pub maybe_width: Option, + /// Frame height in pixels. `None` when the API returns `null`/omits it. + pub maybe_height: Option, // NB: We don't need these. - // /// Width / height ratio (e.g. 1.777… for 16:9). `None` when the server returns null (e.g. width/height are 0). + // /// Width / height ratio (e.g. 1.777… for 16:9). `None` when the server returns null. // pub ratio: Option, } @@ -232,8 +235,8 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result, + #[serde(rename = "height", default, deserialize_with = "de_opt_u32_int_or_float")] + pub maybe_height: Option, // pub ratio: Option, } diff --git a/crates/api_clients/seedance2pro_client/src/utils/number_coercion.rs b/crates/api_clients/seedance2pro_client/src/utils/number_coercion.rs index 9618b2c351..250021bc45 100644 --- a/crates/api_clients/seedance2pro_client/src/utils/number_coercion.rs +++ b/crates/api_clients/seedance2pro_client/src/utils/number_coercion.rs @@ -29,15 +29,13 @@ fn coerce_to_u32(number: &Number) -> u32 { } /// `#[serde(deserialize_with = "...")]` for a required `u64` field that may -/// arrive as an integer or a float — or, increasingly, as `null`. A `null` -/// (or JSON `null`) coerces to `0` rather than failing the whole parse; the -/// Kinovi API intermittently returns `null` for fields it used to populate. +/// arrive as an integer or a float. pub fn de_u64_int_or_float<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let maybe_number = Option::::deserialize(deserializer)?; - Ok(maybe_number.as_ref().map(coerce_to_u64).unwrap_or(0)) + let number = Number::deserialize(deserializer)?; + Ok(coerce_to_u64(&number)) } /// `#[serde(deserialize_with = "...")]` for an optional `u64` field (handles @@ -61,15 +59,13 @@ where } /// `#[serde(deserialize_with = "...")]` for a required `u32` field that may -/// arrive as an integer or a float — or, increasingly, as `null`. A `null` -/// coerces to `0` rather than failing the whole parse; the Kinovi API -/// intermittently returns `null` for result `width`/`height`. +/// arrive as an integer or a float. pub fn de_u32_int_or_float<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let maybe_number = Option::::deserialize(deserializer)?; - Ok(maybe_number.as_ref().map(coerce_to_u32).unwrap_or(0)) + let number = Number::deserialize(deserializer)?; + Ok(coerce_to_u32(&number)) } #[cfg(test)] @@ -130,15 +126,6 @@ mod tests { assert_eq!(parse_u64(r#"{"value": -5.5}"#), 0); } - #[test] - fn required_fields_coerce_null_to_zero() { - // The Kinovi API intermittently returns `null` for numbers it used to - // populate (e.g. result width/height). Required coercers must treat that - // as 0 rather than failing the whole response parse. - assert_eq!(parse_u32(r#"{"value": null}"#), 0); - assert_eq!(parse_u64(r#"{"value": null}"#), 0); - } - #[test] fn optional_handles_null_missing_and_values() { assert_eq!(serde_json::from_str::(r#"{"value": null}"#).unwrap().value, None); diff --git a/crates/service/job/seedance2_pro_job/src/jobs/order_processing_job/process_successful_image_job.rs b/crates/service/job/seedance2_pro_job/src/jobs/order_processing_job/process_successful_image_job.rs index 687cae78e2..0c7cb82919 100644 --- a/crates/service/job/seedance2_pro_job/src/jobs/order_processing_job/process_successful_image_job.rs +++ b/crates/service/job/seedance2_pro_job/src/jobs/order_processing_job/process_successful_image_job.rs @@ -224,8 +224,8 @@ async fn download_and_store_one_image( .maybe_batch_generation_token(maybe_batch_token) .maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref()) .maybe_creator_user(job.maybe_creator_user_token.as_ref()) - .maybe_frame_height(Some(result.height)) - .maybe_frame_width(Some(result.width)) + .maybe_frame_height(result.maybe_height) + .maybe_frame_width(result.maybe_width) .maybe_generation_provider(Some(GenerationProvider::Artcraft)) .maybe_prompt_token(job.maybe_prompt_token.as_ref()) .maybe_platform_type(job.maybe_platform_type) diff --git a/crates/service/job/seedance2_pro_job/src/jobs/order_processing_job/process_successful_video_job.rs b/crates/service/job/seedance2_pro_job/src/jobs/order_processing_job/process_successful_video_job.rs index 23e1cfdfec..ed810634d9 100644 --- a/crates/service/job/seedance2_pro_job/src/jobs/order_processing_job/process_successful_video_job.rs +++ b/crates/service/job/seedance2_pro_job/src/jobs/order_processing_job/process_successful_video_job.rs @@ -118,8 +118,9 @@ pub async fn process_successful_video_job( ); // Optionally extract frame dimensions from the order results. - let maybe_frame_width = order.results.first().map(|r| r.width); - let maybe_frame_height = order.results.first().map(|r| r.height); + // Persist the integer dimensions when the API provides them, NULL otherwise. + let maybe_frame_width = order.results.first().and_then(|r| r.maybe_width); + let maybe_frame_height = order.results.first().and_then(|r| r.maybe_height); // Insert media file record. let media_file_result = MediaFileInsertBuilder::new()