From 240ce3dec5d8aa3359da5cd72aeb6ae0df82a7bd Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 8 May 2026 12:40:15 -0400 Subject: [PATCH] API working --- Cargo.lock | 2 + crates/api_clients/beeble_client/Cargo.toml | 2 + .../start_generation/start_generation.rs | 110 ++++++++++++++++++ .../testing/test_data/src/web/video_urls.rs | 2 +- 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 88b0e544e6..b13f7a11ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1408,12 +1408,14 @@ dependencies = [ name = "beeble_client" version = "0.0.1" dependencies = [ + "anyhow", "errors", "log", "reqwest 0.12.23", "serde", "serde_derive", "serde_json", + "test_data", "tokio", "workspace-build-acceleration", ] diff --git a/crates/api_clients/beeble_client/Cargo.toml b/crates/api_clients/beeble_client/Cargo.toml index 52a8c3f4bf..5dfc201e09 100644 --- a/crates/api_clients/beeble_client/Cargo.toml +++ b/crates/api_clients/beeble_client/Cargo.toml @@ -26,4 +26,6 @@ serde_derive.workspace = true serde_json.workspace = true [dev-dependencies] +anyhow.workspace = true +test_data.workspace = true tokio = { version = "1", features = ["full"] } diff --git a/crates/api_clients/beeble_client/src/requests/start_generation/start_generation.rs b/crates/api_clients/beeble_client/src/requests/start_generation/start_generation.rs index 5ef5ba0f3b..2573965d13 100644 --- a/crates/api_clients/beeble_client/src/requests/start_generation/start_generation.rs +++ b/crates/api_clients/beeble_client/src/requests/start_generation/start_generation.rs @@ -289,4 +289,114 @@ mod tests { assert!(result.id.starts_with("swx_")); Ok(()) } + + // ── Helpers for end-to-end tests ── + + async fn download_bytes(url: &str) -> errors::AnyhowResult> { + let response = reqwest::get(url).await?; + let bytes = response.bytes().await?; + Ok(bytes.to_vec()) + } + + fn guess_content_type(filename: &str) -> &'static str { + if filename.ends_with(".mp4") { "video/mp4" } + else if filename.ends_with(".mov") { "video/quicktime" } + else if filename.ends_with(".png") { "image/png" } + else if filename.ends_with(".jpg") || filename.ends_with(".jpeg") { "image/jpeg" } + else if filename.ends_with(".webp") { "image/webp" } + else { "application/octet-stream" } + } + + async fn upload_asset_bytes( + api_key: &crate::creds::beeble_api_key::BeebleApiKey, + filename: &str, + bytes: Vec, + ) -> errors::AnyhowResult { + use crate::requests::create_upload_url::create_upload_url::{ + create_upload_url, CreateUploadUrlArgs, + }; + + // 1. Get presigned upload URL. + let upload = create_upload_url(CreateUploadUrlArgs { + api_key: api_key.clone(), + filename: filename.to_string(), + }).await.map_err(|e| anyhow::anyhow!("create_upload_url failed: {:?}", e))?; + + println!("Upload ID: {}", upload.id); + println!("Upload URL: {}", upload.upload_url); + println!("Beeble URI: {}", upload.beeble_uri); + + // 2. PUT file bytes to the presigned URL with correct Content-Type. + let content_type = guess_content_type(filename); + println!("Uploading {} bytes with Content-Type: {}", bytes.len(), content_type); + + let client = reqwest::Client::new(); + let put_response = client.put(&upload.upload_url) + .header("Content-Type", content_type) + .body(bytes) + .send() + .await?; + + let put_status = put_response.status(); + let put_body = put_response.text().await.unwrap_or_default(); + println!("PUT upload status: {} body: {}", put_status, put_body); + assert!(put_status.is_success(), "PUT upload failed: {} body: {}", put_status, put_body); + + Ok(upload) + } + + #[tokio::test] + #[ignore] // manually test — requires real API key, downloads files, incurs costs + async fn test_end_to_end_video_generation() -> errors::AnyhowResult<()> { + use crate::test_utils::get_test_api_key::get_test_api_key; + use test_data::web::image_urls::FOREST_BACKDROP_IMAGE_URL; + use test_data::web::video_urls::ANGRY_SHIBA_VIDEO_URL; + + let api_key = get_test_api_key()?; + + // 1. Download the source video. + println!("Downloading video..."); + let video_bytes = download_bytes(ANGRY_SHIBA_VIDEO_URL).await?; + println!("Downloaded video: {} bytes", video_bytes.len()); + + // 2. Download the reference image. + println!("Downloading image..."); + let image_bytes = download_bytes(FOREST_BACKDROP_IMAGE_URL).await?; + println!("Downloaded image: {} bytes", image_bytes.len()); + + // 3. Upload the video. + println!("Uploading video..."); + let video_upload = upload_asset_bytes(&api_key, "shiba.mp4", video_bytes).await?; + + // 4. Upload the reference image. + println!("Uploading image..."); + let image_upload = upload_asset_bytes(&api_key, "forest.jpg", image_bytes).await?; + + // 5. Start generation. + println!("Starting generation..."); + let result = start_generation(StartGenerationArgs { + api_key, + request: StartGenerationRequest { + generation_type: BeebleGenerationType::Video, + source_uri: video_upload.beeble_uri, + alpha_mode: BeebleAlphaMode::Auto, + prompt: Some("Place the dog in a forest setting".to_string()), + reference_image_uri: Some(image_upload.beeble_uri), + alpha_uri: None, + max_resolution: Some(720), + callback_url: None, + idempotency_key: None, + }, + }).await.map_err(|e| anyhow::anyhow!("start_generation failed: {:?}", e))?; + + println!("Job ID: {}", result.id); + println!("Status: {}", result.status); + println!("Progress: {:?}", result.progress); + assert!(result.id.starts_with("swx_")); + + // NB: We don't poll to completion here — that would take too long for a test. + // Use get_job_status to poll manually if needed. + + Ok(()) + } } diff --git a/crates/testing/test_data/src/web/video_urls.rs b/crates/testing/test_data/src/web/video_urls.rs index a3046b0172..93583b2b2e 100644 --- a/crates/testing/test_data/src/web/video_urls.rs +++ b/crates/testing/test_data/src/web/video_urls.rs @@ -1,5 +1,5 @@ // media token: m_fz9r14yhfe05ffg9wqda5291p7mp75 -const ANGRY_SHIBA_VIDEO_URL : &str = "https://cdn-2.fakeyou.com/media/q/0/1/e/9/q01e9xsye0scamh5mrskr2emsv401x14/artcraft_q01e9xsye0scamh5mrskr2emsv401x14.mp4"; +pub const ANGRY_SHIBA_VIDEO_URL : &str = "https://cdn-2.fakeyou.com/media/q/0/1/e/9/q01e9xsye0scamh5mrskr2emsv401x14/artcraft_q01e9xsye0scamh5mrskr2emsv401x14.mp4";