Compare commits

...
Author SHA1 Message Date
Brandon Thomas ef36378e7a 400 on bad video input 2026-05-09 10:48:32 -04:00
Brandon Thomas 19e27f440c concrete error 2026-05-09 00:13:41 -04:00
Brandon Thomas 7b4835a44e set product category 2026-05-09 00:07:36 -04:00
5 changed files with 66 additions and 3 deletions
@@ -24,6 +24,13 @@ pub enum BeebleSpecificApiError {
message: String,
webhook_url: String,
},
/// 400 Bad Request — the video has too many frames.
VideoHasTooManyFrames {
max_frames: u64,
detected_frames: u64,
message: String,
},
}
impl Error for BeebleSpecificApiError {}
@@ -39,6 +46,9 @@ impl Display for BeebleSpecificApiError {
Self::BadWebhookUrl { message, webhook_url } => {
write!(f, "Beeble API: Bad webhook URL '{}': {}", webhook_url, message)
}
Self::VideoHasTooManyFrames { max_frames, detected_frames, message } => {
write!(f, "Beeble API: Video has too many frames ({} detected, max {}): {}", detected_frames, max_frames, message)
}
}
}
}
@@ -28,6 +28,16 @@ pub fn handle_error_response<T>(
webhook_url: context.maybe_callback_url.unwrap_or_default().to_string(),
}.into())
}
400 if response_body.contains("VIDEO_TOO_MANY_FRAMES") => {
let message = extract_error_message(response_body)
.unwrap_or_else(|| "Video has too many frames".to_string());
let (detected, max) = extract_frame_counts(&message);
Err(BeebleSpecificApiError::VideoHasTooManyFrames {
max_frames: max,
detected_frames: detected,
message,
}.into())
}
409 => Err(BeebleSpecificApiError::IdempotencyConflict.into()),
429 => Err(BeebleSpecificApiError::RateLimited.into()),
_ => {
@@ -45,3 +55,18 @@ fn extract_error_message(body: &str) -> Option<String> {
let parsed: serde_json::Value = serde_json::from_str(body).ok()?;
parsed.get("error")?.get("message")?.as_str().map(|s| s.to_string())
}
/// Parse frame counts from a message like "Video has 419 frames, which exceeds the maximum of 240."
fn extract_frame_counts(message: &str) -> (u64, u64) {
let numbers: Vec<u64> = message
.split(|c: char| !c.is_ascii_digit())
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse().ok())
.collect();
match numbers.as_slice() {
[detected, max, ..] => (*detected, *max),
[detected] => (*detected, 0),
_ => (0, 0),
}
}
@@ -69,6 +69,9 @@ pub enum MediaFileOriginProductCategory {
#[serde(rename = "video_gen")]
VideoGeneration,
#[serde(rename = "video_edit")]
VideoEdit,
#[serde(rename = "world_gen")]
WorldGeneration,
@@ -105,6 +108,7 @@ impl MediaFileOriginProductCategory {
Self::Mocap => "mocap",
Self::ImageGeneration => "image_gen",
Self::VideoGeneration => "video_gen",
Self::VideoEdit => "video_edit",
Self::WorldGeneration => "world_gen",
Self::VideoFilter => "video_filter",
Self::Workflow => "workflow",
@@ -126,6 +130,7 @@ impl MediaFileOriginProductCategory {
"mocap" => Ok(Self::Mocap),
"image_gen" => Ok(Self::ImageGeneration),
"video_gen" => Ok(Self::VideoGeneration),
"video_edit" => Ok(Self::VideoEdit),
"world_gen" => Ok(Self::WorldGeneration),
"video_filter" => Ok(Self::VideoFilter),
"workflow" => Ok(Self::Workflow),
@@ -150,6 +155,7 @@ impl MediaFileOriginProductCategory {
Self::Mocap,
Self::ImageGeneration,
Self::VideoGeneration,
Self::VideoEdit,
Self::WorldGeneration,
Self::VideoFilter,
Self::Workflow,
@@ -180,6 +186,7 @@ mod tests {
assert_serialization(MediaFileOriginProductCategory::Mocap, "mocap");
assert_serialization(MediaFileOriginProductCategory::ImageGeneration, "image_gen");
assert_serialization(MediaFileOriginProductCategory::VideoGeneration, "video_gen");
assert_serialization(MediaFileOriginProductCategory::VideoEdit, "video_edit");
assert_serialization(MediaFileOriginProductCategory::WorldGeneration, "world_gen");
assert_serialization(MediaFileOriginProductCategory::VideoFilter, "video_filter");
assert_serialization(MediaFileOriginProductCategory::Workflow, "workflow");
@@ -200,6 +207,7 @@ mod tests {
assert_eq!(MediaFileOriginProductCategory::Mocap.to_str(), "mocap");
assert_eq!(MediaFileOriginProductCategory::ImageGeneration.to_str(), "image_gen");
assert_eq!(MediaFileOriginProductCategory::VideoGeneration.to_str(), "video_gen");
assert_eq!(MediaFileOriginProductCategory::VideoEdit.to_str(), "video_edit");
assert_eq!(MediaFileOriginProductCategory::WorldGeneration.to_str(), "world_gen");
assert_eq!(MediaFileOriginProductCategory::VideoFilter.to_str(), "video_filter");
assert_eq!(MediaFileOriginProductCategory::Workflow.to_str(), "workflow");
@@ -220,6 +228,7 @@ mod tests {
assert_eq!(MediaFileOriginProductCategory::from_str("mocap").unwrap(), MediaFileOriginProductCategory::Mocap);
assert_eq!(MediaFileOriginProductCategory::from_str("image_gen").unwrap(),MediaFileOriginProductCategory::ImageGeneration);
assert_eq!(MediaFileOriginProductCategory::from_str("video_gen").unwrap(), MediaFileOriginProductCategory::VideoGeneration);
assert_eq!(MediaFileOriginProductCategory::from_str("video_edit").unwrap(), MediaFileOriginProductCategory::VideoEdit);
assert_eq!(MediaFileOriginProductCategory::from_str("world_gen").unwrap(), MediaFileOriginProductCategory::WorldGeneration);
assert_eq!(MediaFileOriginProductCategory::from_str("video_filter").unwrap(), MediaFileOriginProductCategory::VideoFilter);
assert_eq!(MediaFileOriginProductCategory::from_str("workflow").unwrap(), MediaFileOriginProductCategory::Workflow);
@@ -228,7 +237,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = MediaFileOriginProductCategory::all_variants();
assert_eq!(variants.len(), 16);
assert_eq!(variants.len(), 17);
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::Unknown));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceAnimator));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceFusion));
@@ -242,6 +251,7 @@ mod tests {
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::Mocap));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::ImageGeneration));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VideoGeneration));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VideoEdit));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::WorldGeneration));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VideoFilter));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::Workflow));
@@ -8,6 +8,8 @@ use sqlx::Acquire;
use artcraft_api_defs::generate::video::edit::beeble_switchx_edit_video::{
BeebleSwitchXEditVideoRequest, BeebleSwitchXEditVideoResponse,
};
use beeble_client::error::beeble_error::BeebleError;
use beeble_client::error::beeble_specific_api_error::BeebleSpecificApiError;
use beeble_client::requests::create_upload_url::upload_bytes::upload_bytes_to_beeble;
use beeble_client::requests::start_generation::start_generation::{
start_generation, BeebleAlphaMode, BeebleGenerationType, StartGenerationArgs,
@@ -218,8 +220,22 @@ pub async fn beeble_switchx_edit_video_gen_handler(
idempotency_key: Some(request.uuid_idempotency_token.clone()),
},
}).await.map_err(|err| {
error!("Beeble start_generation failed: {:?}", err);
AdvancedCommonWebError::from_error(err)
warn!("Beeble start_generation failed: {:?}", err);
match err {
BeebleError::ApiSpecific(BeebleSpecificApiError::VideoHasTooManyFrames {
max_frames,
detected_frames,
ref message,
}) => {
AdvancedCommonWebError::BadInputWithSimpleMessage(format!(
"Video has too many frames: {} (max: {}); {}",
detected_frames, max_frames, message
))
},
_ => {
AdvancedCommonWebError::from_error(err)
}
}
})?;
let external_job_id = &beeble_result.id;
@@ -17,6 +17,7 @@ use enums::by_table::debug_logs::debug_log_type::DebugLogType;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
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 ffmpeg_utils::ffprobe::ffprobe_get_info::ffprobe_get_info;
use filesys::path_to_string::path_to_string;
@@ -366,6 +367,7 @@ async fn download_and_upload_video(
.media_file_class(MediaFileClass::Video)
.media_file_type(media_file_type)
.media_file_origin_category(MediaFileOriginCategory::Inference)
.media_file_origin_product_category(MediaFileOriginProductCategory::VideoEdit)
.mime_type(mime_type)
.file_size_bytes(file_size_bytes as u64)
.maybe_frame_width(maybe_frame_width)