mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 13:54:53 -05:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67dd65c636 | ||
|
|
cc4203ff3e | ||
|
|
6c053aba7c | ||
|
|
671fe5892b |
+1
-2
@@ -87,8 +87,7 @@ mod tests {
|
||||
use crate::requests::image_websocket::messages::websocket_server_message::WebsocketServerMessage;
|
||||
|
||||
fn json_path(file_name: &str) -> String {
|
||||
// NB: Cargo runs tests with the crate root as the working directory.
|
||||
format!("test_data/websocket_messages/{}", file_name)
|
||||
format!("/Users/bt/dev/storyteller/storyteller-rust/crates/api_clients/grok_consumer_client/test_data/websocket_messages/{}", file_name)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -89,15 +89,7 @@ mod tests {
|
||||
let cookies = format!("cookie={}", "a".repeat(10000));
|
||||
assert_eq!(has_session_cookie(&cookies).unwrap(), SessionCookiePresence::MaybePresent);
|
||||
|
||||
// Synthetic fixture (the original real session token was redacted); the session
|
||||
// cookie name is what matters for detection.
|
||||
let cookies = "__Host-authjs.csrf-token=abc123; \
|
||||
__Secure-authjs.callback-url=https%3A%2F%2Fsora.com%2F; \
|
||||
__Secure-authjs.session-token=fake-session-token-value";
|
||||
|
||||
assert_eq!(has_session_cookie(cookies).unwrap(), SessionCookiePresence::Present);
|
||||
|
||||
let cookies = "__Secure-next-auth.session-token=fake-session-token-value";
|
||||
let cookies = "";
|
||||
|
||||
assert_eq!(has_session_cookie(cookies).unwrap(), SessionCookiePresence::Present);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod seedance2pro_bad_request_api_error;
|
||||
pub mod seedance2pro_client_error;
|
||||
pub mod seedance2pro_error;
|
||||
pub mod seedance2pro_generic_api_error;
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// These represent known "400s" that are the fault of the client sending a bad request.
|
||||
#[derive(Debug)]
|
||||
pub enum Seedance2ProBadRequestApiError {
|
||||
/// The video generation request was flagged as a content violation.
|
||||
VideoGenerationViolation { raw_body: String },
|
||||
|
||||
/// The prompt exceeded Kinovi's maximum length (10,000 characters at the
|
||||
/// time of writing).
|
||||
PromptIsTooLong { raw_body: String },
|
||||
|
||||
/// Too many media URLs were attached to the request (`uploadedUrls` is
|
||||
/// capped at 9 elements at the time of writing).
|
||||
TooManyUrls { raw_body: String },
|
||||
}
|
||||
|
||||
impl Error for Seedance2ProBadRequestApiError {}
|
||||
|
||||
impl Display for Seedance2ProBadRequestApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::VideoGenerationViolation { raw_body } => write!(f, "Video generation violation: {}", raw_body),
|
||||
Self::PromptIsTooLong { raw_body } => write!(f, "Prompt is too long: {}", raw_body),
|
||||
Self::TooManyUrls { raw_body } => write!(f, "Too many urls: {}", raw_body),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
@@ -10,7 +9,6 @@ pub enum Seedance2ProError {
|
||||
Client(Seedance2ProClientError),
|
||||
ApiSpecific(Seedance2ProSpecificApiError),
|
||||
ApiGeneric(Seedance2ProGenericApiError),
|
||||
ApiBadRequest(Seedance2ProBadRequestApiError),
|
||||
}
|
||||
|
||||
impl Seedance2ProError {
|
||||
@@ -40,7 +38,6 @@ impl std::fmt::Display for Seedance2ProError {
|
||||
Self::Client(e) => write!(f, "Seedance2ProClientError: {:?}", e),
|
||||
Self::ApiSpecific(e) => write!(f, "Seedance2ProSpecificApiError: {:?}", e),
|
||||
Self::ApiGeneric(e) => write!(f, "Seedance2ProGenericApiError: {:?}", e),
|
||||
Self::ApiBadRequest(e) => write!(f, "Seedance2ProBadRequestApiError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,9 +59,3 @@ impl From<Seedance2ProGenericApiError> for Seedance2ProError {
|
||||
Self::ApiGeneric(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Seedance2ProBadRequestApiError> for Seedance2ProError {
|
||||
fn from(error: Seedance2ProBadRequestApiError) -> Self {
|
||||
Self::ApiBadRequest(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,8 @@ pub enum Seedance2ProGenericApiError {
|
||||
/// Includes the original body.
|
||||
SerdeParseErrorWithBodyOnNon200(serde_json::Error, String),
|
||||
|
||||
/// A response that parsed, but didn't have the shape we expected
|
||||
/// (eg. an empty batch response array).
|
||||
UnexpectedResponseShape {
|
||||
explanation: String,
|
||||
raw_body: String,
|
||||
},
|
||||
/// An uncategorized bad HTTP response.
|
||||
UncategorizedBadResponse(String),
|
||||
|
||||
/// An uncategorized bad HTTP response with status code and body.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
@@ -41,7 +37,7 @@ impl Display for Seedance2ProGenericApiError {
|
||||
Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err),
|
||||
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
|
||||
Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body),
|
||||
Self::UnexpectedResponseShape { explanation, raw_body } => write!(f, "Unexpected response shape: {}. Body: {}", explanation, raw_body),
|
||||
Self::UncategorizedBadResponse(msg) => write!(f, "Uncategorized bad response: {}", msg),
|
||||
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
|
||||
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ pub enum Seedance2ProSpecificApiError {
|
||||
/// The session cookies are expired or invalid.
|
||||
UnauthorizedSessionExpired,
|
||||
|
||||
/// The video generation request was flagged as a content violation.
|
||||
VideoGenerationViolation(String),
|
||||
|
||||
/// The account does not have enough credits to perform the operation.
|
||||
BillingError {
|
||||
status_code: StatusCode,
|
||||
@@ -21,6 +24,7 @@ impl Display for Seedance2ProSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::UnauthorizedSessionExpired => write!(f, "Unauthorized: session cookies expired or invalid."),
|
||||
Self::VideoGenerationViolation(body) => write!(f, "Video generation violation: {}", body),
|
||||
Self::BillingError { status_code, body } => write!(f, "Billing error (status {}): {}", status_code, body),
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -119,10 +119,9 @@ pub async fn generate_character(args: GenerateCharacterArgs<'_>) -> Result<Gener
|
||||
let data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
+5
-6
@@ -1,8 +1,8 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use crate::requests::generate_image::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
|
||||
@@ -285,16 +285,15 @@ pub async fn generate_image(args: GenerateImageArgs<'_>) -> Result<GenerateImage
|
||||
let task_data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
if task_data.violation_warning {
|
||||
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
|
||||
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
|
||||
}
|
||||
|
||||
Ok(GenerateImageResponse {
|
||||
|
||||
+5
-6
@@ -1,8 +1,8 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use crate::requests::generate_video::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
|
||||
@@ -403,16 +403,15 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
let task_data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
if task_data.violation_warning {
|
||||
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
|
||||
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
|
||||
}
|
||||
|
||||
Ok(GenerateVideoResponse {
|
||||
|
||||
+3
-4
@@ -94,10 +94,9 @@ pub async fn get_user_auth_details(
|
||||
let json = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
+3
-4
@@ -181,10 +181,9 @@ pub async fn poll_characters(args: PollCharactersArgs<'_>) -> Result<PollCharact
|
||||
let data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
@@ -201,10 +201,9 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
|
||||
let json = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
+3
-4
@@ -106,10 +106,9 @@ pub async fn prepare_file_upload(args: PrepareFileUploadArgs<'_>) -> Result<Prep
|
||||
let upload_url = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
+3
-4
@@ -102,10 +102,9 @@ pub async fn update_character(args: UpdateCharacterArgs<'_>) -> Result<UpdateCha
|
||||
let data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
+5
-6
@@ -1,8 +1,8 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
use crate::requests::workflow_run_task::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::categorize_seedance2pro_error::categorize_seedance2pro_error;
|
||||
@@ -415,16 +415,15 @@ pub async fn workflow_run_task(args: WorkflowRunTaskArgs<'_>) -> Result<Workflow
|
||||
let task_data = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UnexpectedResponseShape {
|
||||
explanation: "Empty batch response array".to_string(),
|
||||
raw_body: response_body.clone(),
|
||||
})?
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
if task_data.violation_warning {
|
||||
return Err(Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: response_body }.into());
|
||||
return Err(Seedance2ProSpecificApiError::VideoGenerationViolation(response_body).into());
|
||||
}
|
||||
|
||||
Ok(WorkflowRunTaskResponse {
|
||||
|
||||
+51
-164
@@ -1,6 +1,5 @@
|
||||
use wreq::StatusCode;
|
||||
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
@@ -10,25 +9,11 @@ const BILLING_ERROR_MARKERS: &[&str] = &[
|
||||
"credits not enough",
|
||||
];
|
||||
|
||||
/// Zod validation message for a string field exceeding its maximum length,
|
||||
/// eg. "参数验证失败 - String must contain at most 10000 character(s)".
|
||||
const STRING_TOO_LONG_MARKER: &str = "String must contain at most";
|
||||
|
||||
/// Zod validation message for an array field exceeding its maximum size,
|
||||
/// eg. "参数验证失败 - Array must contain at most 9 element(s)".
|
||||
const ARRAY_TOO_LARGE_MARKER: &str = "Array must contain at most";
|
||||
|
||||
/// Zod `fieldErrors` path entry for the prompt field.
|
||||
const PROMPT_FIELD_MARKER: &str = "\"prompt\"";
|
||||
|
||||
/// Zod `fieldErrors` path entry for the uploaded media urls field.
|
||||
const UPLOADED_URLS_FIELD_MARKER: &str = "\"uploadedUrls\"";
|
||||
|
||||
/// Categorize a non-success HTTP response into a specific or generic error.
|
||||
///
|
||||
/// Checks the response body for known patterns (e.g. billing errors, request
|
||||
/// validation failures) and returns a specific error variant when possible,
|
||||
/// falling back to `UncategorizedBadResponseWithStatusAndBody` otherwise.
|
||||
/// Checks the response body for known patterns (e.g. billing errors) and returns
|
||||
/// a specific error variant when possible, falling back to
|
||||
/// `UncategorizedBadResponseWithStatusAndBody` otherwise.
|
||||
pub fn categorize_seedance2pro_error(
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
@@ -40,15 +25,6 @@ pub fn categorize_seedance2pro_error(
|
||||
}.into();
|
||||
}
|
||||
|
||||
if status_code == StatusCode::BAD_REQUEST {
|
||||
if is_prompt_too_long_error(&body) {
|
||||
return Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body: body }.into();
|
||||
}
|
||||
if is_too_many_urls_error(&body) {
|
||||
return Seedance2ProBadRequestApiError::TooManyUrls { raw_body: body }.into();
|
||||
}
|
||||
}
|
||||
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body,
|
||||
@@ -61,164 +37,75 @@ fn is_billing_error(body: &str) -> bool {
|
||||
BILLING_ERROR_MARKERS.iter().any(|marker| body_lower.contains(marker))
|
||||
}
|
||||
|
||||
/// Check if the response body is a zod validation failure for an over-long prompt.
|
||||
fn is_prompt_too_long_error(body: &str) -> bool {
|
||||
body.contains(STRING_TOO_LONG_MARKER) && body.contains(PROMPT_FIELD_MARKER)
|
||||
}
|
||||
|
||||
/// Check if the response body is a zod validation failure for too many uploaded urls.
|
||||
fn is_too_many_urls_error(body: &str) -> bool {
|
||||
body.contains(ARRAY_TOO_LARGE_MARKER) && body.contains(UPLOADED_URLS_FIELD_MARKER)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
|
||||
mod billing_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn real_not_enough_credits_response() {
|
||||
let body = std::fs::read_to_string("test_data/responses/not_enough_credits.json")
|
||||
.expect("Failed to read test data file");
|
||||
|
||||
#[test]
|
||||
fn real_not_enough_credits_response() {
|
||||
let body = read_response_body("not_enough_credits.json");
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { status_code, body }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::BAD_REQUEST);
|
||||
assert!(body.contains("credits not enough"));
|
||||
}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn billing_error_case_insensitive() {
|
||||
let body = r#"{"error":"Credits Not Enough"}"#.to_string();
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { .. }
|
||||
) => {}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { status_code, body }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::BAD_REQUEST);
|
||||
assert!(body.contains("credits not enough"));
|
||||
}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
mod bad_request_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn billing_error_case_insensitive() {
|
||||
let body = r#"{"error":"Credits Not Enough"}"#.to_string();
|
||||
|
||||
#[test]
|
||||
fn real_prompt_too_long_response() {
|
||||
let body = read_response_body("prompt_too_long.json");
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiBadRequest(
|
||||
Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body }
|
||||
) => {
|
||||
assert!(raw_body.contains("String must contain at most 10000 character(s)"));
|
||||
assert!(raw_body.contains("\"prompt\""));
|
||||
}
|
||||
other => panic!("Expected PromptIsTooLong, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_too_many_urls_response() {
|
||||
let body = read_response_body("too_many_urls.json");
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiBadRequest(
|
||||
Seedance2ProBadRequestApiError::TooManyUrls { raw_body }
|
||||
) => {
|
||||
assert!(raw_body.contains("Array must contain at most 9 element(s)"));
|
||||
assert!(raw_body.contains("\"uploadedUrls\""));
|
||||
}
|
||||
other => panic!("Expected TooManyUrls, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_request_markers_require_400_status() {
|
||||
let body = read_response_body("prompt_too_long.json");
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::INTERNAL_SERVER_ERROR, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_failure_for_other_field_is_not_categorized() {
|
||||
// Same zod shape, but for a field we don't have a variant for.
|
||||
let body = r#"[{"error":{"json":{"message":"validation failed - String must contain at most 100 character(s)","code":-32600,"data":{"code":"BAD_REQUEST","httpStatus":400,"zodError":{"formErrors":[],"fieldErrors":{"apiParams":[{"code":"custom","message":"String must contain at most 100 character(s)","path":["apiParams","negativePrompt"],"fatal":true}]}}}}}]"#.to_string();
|
||||
|
||||
let error = categorize_seedance2pro_error(StatusCode::BAD_REQUEST, body);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
match error {
|
||||
Seedance2ProError::ApiSpecific(
|
||||
Seedance2ProSpecificApiError::BillingError { .. }
|
||||
) => {}
|
||||
other => panic!("Expected BillingError, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
mod fallback_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn unknown_error_falls_back_to_uncategorized() {
|
||||
let body = r#"{"error":"something else went wrong"}"#.to_string();
|
||||
|
||||
#[test]
|
||||
fn unknown_error_falls_back_to_uncategorized() {
|
||||
let body = r#"{"error":"something else went wrong"}"#.to_string();
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
body.clone(),
|
||||
);
|
||||
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
body.clone(),
|
||||
);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: b }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(b, body);
|
||||
}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_body_falls_back_to_uncategorized() {
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
String::new(),
|
||||
);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body: b }
|
||||
) => {
|
||||
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(b, body);
|
||||
}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_response_body(filename: &str) -> String {
|
||||
std::fs::read_to_string(format!("test_data/responses/{}", filename))
|
||||
.expect("Failed to read test data file")
|
||||
#[test]
|
||||
fn empty_body_falls_back_to_uncategorized() {
|
||||
let error = categorize_seedance2pro_error(
|
||||
StatusCode::BAD_REQUEST,
|
||||
String::new(),
|
||||
);
|
||||
|
||||
match error {
|
||||
Seedance2ProError::ApiGeneric(
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. }
|
||||
) => {}
|
||||
other => panic!("Expected UncategorizedBadResponseWithStatusAndBody, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
[
|
||||
{
|
||||
"error": {
|
||||
"json": {
|
||||
"message": "[{\"code\": \"custom\",\"message\": \"参数验证失败 - String must contain at most 10000 character(s)\",\"path\": [\"apiParams\",\"prompt\"],\"fatal\": true}]",
|
||||
"code": -32600,
|
||||
"data": {
|
||||
"code": "BAD_REQUEST",
|
||||
"httpStatus": 400,
|
||||
"path": "workflow.runTask",
|
||||
"zodError": {
|
||||
"formErrors": [],
|
||||
"fieldErrors": {
|
||||
"apiParams": [
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "参数验证失败 - String must contain at most 10000 character(s)",
|
||||
"path": [
|
||||
"apiParams",
|
||||
"prompt"
|
||||
],
|
||||
"fatal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"stack": null
|
||||
},
|
||||
"meta": {
|
||||
"values": {
|
||||
"stack": [
|
||||
"undefined"
|
||||
]
|
||||
},
|
||||
"v": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,40 +0,0 @@
|
||||
[
|
||||
{
|
||||
"error": {
|
||||
"json": {
|
||||
"message": "[{\"code\": \"custom\",\"message\": \"参数验证失败 - Array must contain at most 9 element(s)\",\"path\": [\"apiParams\",\"uploadedUrls\"],\"fatal\": true}]",
|
||||
"code": -32600,
|
||||
"data": {
|
||||
"code": "BAD_REQUEST",
|
||||
"httpStatus": 400,
|
||||
"path": "workflow.runTask",
|
||||
"zodError": {
|
||||
"formErrors": [],
|
||||
"fieldErrors": {
|
||||
"apiParams": [
|
||||
{
|
||||
"code": "custom",
|
||||
"message": "参数验证失败 - Array must contain at most 9 element(s)",
|
||||
"path": [
|
||||
"apiParams",
|
||||
"uploadedUrls"
|
||||
],
|
||||
"fatal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"stack": null
|
||||
},
|
||||
"meta": {
|
||||
"values": {
|
||||
"stack": [
|
||||
"undefined"
|
||||
]
|
||||
},
|
||||
"v": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
+2
-2
@@ -136,12 +136,12 @@ struct RawResponse {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::api_types::upload_object_id::UploadObjectId;
|
||||
use crate::api::requests::objects::begin_object_image_upload::{get_url, BASE_URL};
|
||||
use crate::api::requests::objects::begin_object_image_upload::get_url;
|
||||
|
||||
#[test]
|
||||
fn test_get_url() {
|
||||
let upload_id = UploadObjectId("foo-bar-baz-bin".to_string());
|
||||
let expected = format!("{}/foo-bar-baz-bin:upload", BASE_URL);
|
||||
let expected = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects/foo-bar-baz-bin:upload";
|
||||
assert_eq!(get_url(&upload_id), expected);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -147,9 +147,6 @@ mod tests {
|
||||
#[test]
|
||||
fn test_request() {
|
||||
let request = RawRequest::for_mime_type(UploadMimeType::ImageJpeg);
|
||||
// The display name embeds a random uuid, eg. "uploaded_image_{uuid}.jpeg"
|
||||
assert!(request.display_name.starts_with("uploaded_image_"));
|
||||
assert!(request.display_name.ends_with(".jpeg"));
|
||||
assert_eq!(request.mime_type, "image/jpeg");
|
||||
assert_eq!(request.display_name, "uploaded_image_jpeg");
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -135,7 +135,7 @@ struct RawResponse {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::api_types::upload_object_id::UploadObjectId;
|
||||
use crate::api::requests::objects::finalize_object_image_upload::{finalize_object_image_upload, get_url, FinalizeObjectImageUploadArgs, BASE_URL};
|
||||
use crate::api::requests::objects::finalize_object_image_upload::{finalize_object_image_upload, get_url, FinalizeObjectImageUploadArgs};
|
||||
use crate::test_utils::get_test_bearer_token::get_test_bearer_token;
|
||||
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
@@ -144,7 +144,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_get_url() {
|
||||
let upload_id = UploadObjectId("foo-bar-baz-bin".to_string());
|
||||
let expected = format!("{}/foo-bar-baz-bin:complete", BASE_URL);
|
||||
let expected = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects/foo-bar-baz-bin:complete";
|
||||
assert_eq!(get_url(&upload_id), expected);
|
||||
}
|
||||
|
||||
|
||||
+3
-86
@@ -405,10 +405,6 @@ mod tests {
|
||||
use crate::api::api_types::pano_object_id::PanoObjectId;
|
||||
use crate::api::api_types::run_object_id::RunObjectId;
|
||||
use crate::api::requests::objects::update_run_object_with_upload::{RawRequest, UpdateRunObjectWithUploadPayloadArgs};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// Any timestamp from `Utc::now()` should exceed this (2023-11-14 in epoch millis).
|
||||
const MIN_PLAUSIBLE_EPOCH_MILLIS: u64 = 1_700_000_000_000;
|
||||
|
||||
#[test]
|
||||
fn request_default() {
|
||||
@@ -436,88 +432,9 @@ mod tests {
|
||||
|
||||
let request = RawRequest::from_args(&args);
|
||||
|
||||
// The `createdAt` / `updatedAt` timestamps come from `Utc::now()` at build time,
|
||||
// so they're nondeterministic. Sanity-check them, then zero them out before
|
||||
// comparing the rest of the document.
|
||||
assert!(request.object.metadata.created_at > MIN_PLAUSIBLE_EPOCH_MILLIS);
|
||||
assert!(request.object.metadata.updated_at >= request.object.metadata.created_at);
|
||||
let json = serde_json::to_string_pretty(&request).unwrap();
|
||||
println!("{}", json);
|
||||
|
||||
let mut actual = serde_json::to_value(&request).unwrap();
|
||||
zero_out_timestamps(&mut actual);
|
||||
|
||||
let expected = json!({
|
||||
"object": {
|
||||
"metadata": {
|
||||
"version": "0.0.1",
|
||||
"createdAt": 0,
|
||||
"updatedAt": 0,
|
||||
"usesAdvancedEditing": false,
|
||||
"draftMode": false,
|
||||
"nodes": {
|
||||
"bc17252b-811e-49d7-be3e-b7c538df9d30": {
|
||||
"id": "bc17252b-811e-49d7-be3e-b7c538df9d30",
|
||||
"type": "input",
|
||||
"parentId": null,
|
||||
"source": {
|
||||
"type": "image",
|
||||
"image_url": "https://todo.com"
|
||||
},
|
||||
"createdAt": 0
|
||||
},
|
||||
"82f6b488-8afe-4311-9022-80ad5789ad92": {
|
||||
"id": "82f6b488-8afe-4311-9022-80ad5789ad92",
|
||||
"type": "pano",
|
||||
"parentId": "bc17252b-811e-49d7-be3e-b7c538df9d30",
|
||||
"source": {
|
||||
"type": "generate_world",
|
||||
"world_id": "pending",
|
||||
"status": "pending"
|
||||
},
|
||||
"createdAt": 0
|
||||
},
|
||||
"f08ec501-601c-47c6-a104-402d1820b8f0": {
|
||||
"id": "f08ec501-601c-47c6-a104-402d1820b8f0",
|
||||
"type": "world",
|
||||
"parentId": "82f6b488-8afe-4311-9022-80ad5789ad92",
|
||||
"source": {
|
||||
"type": "generate_world",
|
||||
"world_id": "pending",
|
||||
"posed_cubemaps_url": "",
|
||||
"minimap_url": "",
|
||||
"minimap_metadata": "",
|
||||
"status": "pending"
|
||||
},
|
||||
"createdAt": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"update_mask": [
|
||||
"metadata"
|
||||
]
|
||||
});
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
/// Recursively replaces nondeterministic `createdAt` / `updatedAt` values with 0.
|
||||
fn zero_out_timestamps(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
for (key, child) in map.iter_mut() {
|
||||
if key == "createdAt" || key == "updatedAt" {
|
||||
*child = json!(0);
|
||||
} else {
|
||||
zero_out_timestamps(child);
|
||||
}
|
||||
}
|
||||
},
|
||||
Value::Array(items) => {
|
||||
for item in items.iter_mut() {
|
||||
zero_out_timestamps(item);
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
assert_eq!(json, r#"{"type":"abcxyz_image_input_id","value":"image_input_id"}"#);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-89
@@ -411,10 +411,6 @@ mod tests {
|
||||
use crate::api::api_types::meta_world_object_id::MetaWorldObjectId;
|
||||
use crate::api::api_types::world_id::WorldObjectId;
|
||||
use crate::api::requests::objects::update_run_object_with_world::{RawRequest, UpdateRunObjectWithWorldPayloadArgs};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// Any timestamp from `Utc::now()` should exceed this (2023-11-14 in epoch millis).
|
||||
const MIN_PLAUSIBLE_EPOCH_MILLIS: u64 = 1_700_000_000_000;
|
||||
|
||||
#[test]
|
||||
fn request_default() {
|
||||
@@ -450,91 +446,9 @@ mod tests {
|
||||
|
||||
let request = RawRequest::from_args(&args);
|
||||
|
||||
// The `createdAt` / `updatedAt` timestamps come from `Utc::now()` at build time,
|
||||
// so they're nondeterministic. Sanity-check them, then zero them out before
|
||||
// comparing the rest of the document.
|
||||
assert!(request.object.metadata.created_at > MIN_PLAUSIBLE_EPOCH_MILLIS);
|
||||
assert!(request.object.metadata.updated_at >= request.object.metadata.created_at);
|
||||
let json = serde_json::to_string_pretty(&request).unwrap();
|
||||
println!("{}", json);
|
||||
|
||||
let mut actual = serde_json::to_value(&request).unwrap();
|
||||
zero_out_timestamps(&mut actual);
|
||||
|
||||
let expected = json!({
|
||||
"object": {
|
||||
"metadata": {
|
||||
"version": "0.0.1",
|
||||
"name": title,
|
||||
"createdAt": 0,
|
||||
"updatedAt": 0,
|
||||
"usesAdvancedEditing": false,
|
||||
"draftMode": false,
|
||||
"nodes": {
|
||||
"bc17252b-811e-49d7-be3e-b7c538df9d30": {
|
||||
"id": "bc17252b-811e-49d7-be3e-b7c538df9d30",
|
||||
"type": "input",
|
||||
"parentId": null,
|
||||
"source": {
|
||||
"type": "image",
|
||||
"image_url": "https://cdn.marble.worldlabs.ai/object/13ca760c-8ef9-4bf5-acc3-5a507fad3abf/asset.jpg"
|
||||
},
|
||||
"createdAt": 0,
|
||||
"title": title,
|
||||
"caption": caption
|
||||
},
|
||||
"82f6b488-8afe-4311-9022-80ad5789ad92": {
|
||||
"id": "82f6b488-8afe-4311-9022-80ad5789ad92",
|
||||
"type": "pano",
|
||||
"parentId": "bc17252b-811e-49d7-be3e-b7c538df9d30",
|
||||
"source": {
|
||||
"type": "generate_world",
|
||||
"world_id": "d60cfa21-5506-43de-9a9c-0707fc17a5ec",
|
||||
"status": "pending"
|
||||
},
|
||||
"createdAt": 0
|
||||
},
|
||||
"f08ec501-601c-47c6-a104-402d1820b8f0": {
|
||||
"id": "f08ec501-601c-47c6-a104-402d1820b8f0",
|
||||
"type": "world",
|
||||
"parentId": "82f6b488-8afe-4311-9022-80ad5789ad92",
|
||||
"source": {
|
||||
"type": "generate_world",
|
||||
"world_id": "d60cfa21-5506-43de-9a9c-0707fc17a5ec",
|
||||
"posed_cubemaps_url": "",
|
||||
"minimap_url": "",
|
||||
"minimap_metadata": "",
|
||||
"status": "pending"
|
||||
},
|
||||
"createdAt": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"update_mask": [
|
||||
"metadata"
|
||||
]
|
||||
});
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
/// Recursively replaces nondeterministic `createdAt` / `updatedAt` values with 0.
|
||||
fn zero_out_timestamps(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
for (key, child) in map.iter_mut() {
|
||||
if key == "createdAt" || key == "updatedAt" {
|
||||
*child = json!(0);
|
||||
} else {
|
||||
zero_out_timestamps(child);
|
||||
}
|
||||
}
|
||||
},
|
||||
Value::Array(items) => {
|
||||
for item in items.iter_mut() {
|
||||
zero_out_timestamps(item);
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
assert_eq!(json, r#"{"type":"abcxyz_image_input_id","value":"image_input_id"}"#);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -151,7 +151,7 @@ struct SpzUrls {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::api_types::world_id::WorldObjectId;
|
||||
use crate::api::requests::worlds::poll_world_status::{get_url, poll_world_status, PollWorldStatusArgs, BASE_URL};
|
||||
use crate::api::requests::worlds::poll_world_status::{get_url, poll_world_status, PollWorldStatusArgs};
|
||||
use crate::test_utils::get_test_bearer_token::get_test_bearer_token;
|
||||
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
@@ -160,7 +160,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_get_url() {
|
||||
let world_id = WorldObjectId("d60cfa21-5506-43de-9a9c-0707fc17a5ec".to_string());
|
||||
let expected = format!("{}/d60cfa21-5506-43de-9a9c-0707fc17a5ec", BASE_URL);
|
||||
let expected = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/worlds/d60cfa21-5506-43de-9a9c-0707fc17a5ec";
|
||||
assert_eq!(get_url(&world_id), expected);
|
||||
}
|
||||
|
||||
|
||||
@@ -407,7 +407,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Transliteration for the time-bombed characters (α, đ, ß) was never implemented.
|
||||
pub fn most_frequent_failures_2p5k_usages() {
|
||||
assert_converted("~", "~"); // b'\\uff5e' 2507
|
||||
time_bomb_ignore(|| assert_converted("â", "a")); // b'\xe2' 2607
|
||||
@@ -430,7 +429,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Transliteration for the time-bombed characters (ę, ě, ᴾ, ł) was never implemented.
|
||||
pub fn most_frequent_failures_2k_usages() {
|
||||
assert_converted("、", ","); // b'\\u3001' 2001
|
||||
assert_converted("ć", "c"); // b'\\u0107' 2017
|
||||
@@ -444,7 +442,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Transliteration for the time-bombed characters (ū, ι, ε) was never implemented.
|
||||
pub fn most_frequent_failures_1p5k_usages() {
|
||||
assert_converted("·", " "); // b'\xb7' 1514
|
||||
assert_converted("š", "s"); // b'\\u0161' 1514
|
||||
|
||||
@@ -222,15 +222,14 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::LivePortrait));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::RvcV2));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SadTalker));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SeedVc));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SoVitsSvc));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::Tacotron2));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::MocapNet));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SeedVc));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::StyleTTS2));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::StableDiffusion15));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::GptSovits));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::StorytellerStudio));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::StorytellerStudioImageGen));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::VideoStyleTransfer));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::ComfyUi));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::VallEX));
|
||||
|
||||
+1
-3
@@ -117,9 +117,7 @@ mod tests {
|
||||
fn test_serialization() {
|
||||
assert_serialization(PromptContextSemanticType::VidStartFrame, "vid_start_frame");
|
||||
assert_serialization(PromptContextSemanticType::VidEndFrame, "vid_end_frame");
|
||||
// NB: serde serialization ("vid_ref") intentionally differs from to_str()/the DB value
|
||||
// ("vidref"); see the "Current DB values" comment at the top of this file.
|
||||
assert_serialization(PromptContextSemanticType::VidRef, "vid_ref");
|
||||
assert_serialization(PromptContextSemanticType::VidRef, "vidref");
|
||||
assert_serialization(PromptContextSemanticType::Imgsrc, "imgsrc");
|
||||
assert_serialization(PromptContextSemanticType::Imgmask, "imgmask");
|
||||
assert_serialization(PromptContextSemanticType::Imgref, "imgref");
|
||||
|
||||
+3
-3
@@ -127,7 +127,7 @@ mod tests {
|
||||
{"id":"sub_1LhA3MEU5se17MekeWvmTNyk","automatic_tax":{"enabled":false},
|
||||
"billing_cycle_anchor":1662979188,"cancel_at_period_end":false,
|
||||
"collection_method":"charge_automatically","created":1662979188,"current_period_end":1665571188,
|
||||
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox","currency":"usd","default_tax_rates":[],
|
||||
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox","default_tax_rates":[],
|
||||
"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,"deleted":false,"metadata":{},
|
||||
"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,"billing_scheme":"per_unit",
|
||||
"created":1662278586,"currency":"usd","deleted":false,"livemode":false,"metadata":{},
|
||||
@@ -172,7 +172,7 @@ mod tests {
|
||||
"cancel_at_period_end":false,"collection_method":"charge_automatically","created":1662948037,
|
||||
"current_period_end":1665540037,"current_period_start":1662948037,
|
||||
"customer":"cus_MPrgIen5Wh6QKG",
|
||||
"default_payment_method":"pm_1Lh1wtEU5se17MekpirKtMJm","currency":"usd","default_tax_rates":[],
|
||||
"default_payment_method":"pm_1Lh1wtEU5se17MekpirKtMJm","default_tax_rates":[],
|
||||
"items":{"data":[{"id":"si_MPrgTAV333Nq7c","created":1662948037,"deleted":false,"metadata":{},
|
||||
"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,"billing_scheme":"per_unit",
|
||||
"created":1662278586,"currency":"usd","deleted":false,"livemode":false,"metadata":{},
|
||||
@@ -216,7 +216,7 @@ mod tests {
|
||||
"billing_cycle_anchor":1662979188,"cancel_at_period_end":false,"canceled_at":1662979740,
|
||||
"collection_method":"charge_automatically","created":1662979188,"current_period_end":1665571188,
|
||||
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox",
|
||||
"default_payment_method":"pm_1LhA3LEU5se17MekViNvoYAx","currency":"usd","default_tax_rates":[],
|
||||
"default_payment_method":"pm_1LhA3LEU5se17MekViNvoYAx","default_tax_rates":[],
|
||||
"ended_at":1662979740,"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,
|
||||
"deleted":false,"metadata":{},"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,
|
||||
"billing_scheme":"per_unit","created":1662278586,"currency":"usd","deleted":false,
|
||||
|
||||
+6
-25
@@ -127,11 +127,8 @@ mod tests {
|
||||
{"id":"sub_1LhA3MEU5se17MekeWvmTNyk","automatic_tax":{"enabled":false},
|
||||
"billing_cycle_anchor":1662979188,"cancel_at_period_end":false,
|
||||
"collection_method":"charge_automatically","created":1662979188,"current_period_end":1665571188,
|
||||
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox","currency":"usd","default_tax_rates":[],"discounts":[],
|
||||
"invoice_settings":{"issuer":{"type":"self"}},
|
||||
"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,"current_period_end":1665571188,
|
||||
"current_period_start":1662979188,"deleted":false,"discounts":[],
|
||||
"plan":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","object":"plan","active":true,"amount":700,"amount_decimal":"700","billing_scheme":"per_unit","created":1662278586,"currency":"usd","interval":"month","interval_count":1,"livemode":false,"metadata":{},"product":"prod_MMxi2J5y69VPbO","usage_type":"licensed"},"metadata":{},
|
||||
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox","default_tax_rates":[],
|
||||
"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,"deleted":false,"metadata":{},
|
||||
"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,"billing_scheme":"per_unit",
|
||||
"created":1662278586,"currency":"usd","deleted":false,"livemode":false,"metadata":{},
|
||||
"product":"prod_MMxi2J5y69VPbO","recurring":{"interval":"month","interval_count":1,
|
||||
@@ -175,11 +172,8 @@ mod tests {
|
||||
"cancel_at_period_end":false,"collection_method":"charge_automatically","created":1662948037,
|
||||
"current_period_end":1665540037,"current_period_start":1662948037,
|
||||
"customer":"cus_MPrgIen5Wh6QKG",
|
||||
"default_payment_method":"pm_1Lh1wtEU5se17MekpirKtMJm","currency":"usd","default_tax_rates":[],"discounts":[],
|
||||
"invoice_settings":{"issuer":{"type":"self"}},
|
||||
"items":{"data":[{"id":"si_MPrgTAV333Nq7c","created":1662948037,"current_period_end":1665540037,
|
||||
"current_period_start":1662948037,"deleted":false,"discounts":[],
|
||||
"plan":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","object":"plan","active":true,"amount":700,"amount_decimal":"700","billing_scheme":"per_unit","created":1662278586,"currency":"usd","interval":"month","interval_count":1,"livemode":false,"metadata":{},"product":"prod_MMxi2J5y69VPbO","usage_type":"licensed"},"metadata":{},
|
||||
"default_payment_method":"pm_1Lh1wtEU5se17MekpirKtMJm","default_tax_rates":[],
|
||||
"items":{"data":[{"id":"si_MPrgTAV333Nq7c","created":1662948037,"deleted":false,"metadata":{},
|
||||
"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,"billing_scheme":"per_unit",
|
||||
"created":1662278586,"currency":"usd","deleted":false,"livemode":false,"metadata":{},
|
||||
"product":"prod_MMxi2J5y69VPbO","recurring":{"interval":"month","interval_count":1,
|
||||
@@ -222,12 +216,9 @@ mod tests {
|
||||
"billing_cycle_anchor":1662979188,"cancel_at_period_end":false,"canceled_at":1662979740,
|
||||
"collection_method":"charge_automatically","created":1662979188,"current_period_end":1665571188,
|
||||
"current_period_start":1662979188,"customer":"cus_MQ03py0gWUh0Ox",
|
||||
"default_payment_method":"pm_1LhA3LEU5se17MekViNvoYAx","currency":"usd","default_tax_rates":[],"discounts":[],
|
||||
"invoice_settings":{"issuer":{"type":"self"}},
|
||||
"default_payment_method":"pm_1LhA3LEU5se17MekViNvoYAx","default_tax_rates":[],
|
||||
"ended_at":1662979740,"items":{"data":[{"id":"si_MQ03hadVNfRnaS","created":1662979188,
|
||||
"current_period_end":1665571188,"current_period_start":1662979188,
|
||||
"deleted":false,"discounts":[],
|
||||
"plan":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","object":"plan","active":true,"amount":700,"amount_decimal":"700","billing_scheme":"per_unit","created":1662278586,"currency":"usd","interval":"month","interval_count":1,"livemode":false,"metadata":{},"product":"prod_MMxi2J5y69VPbO","usage_type":"licensed"},"metadata":{},"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,
|
||||
"deleted":false,"metadata":{},"price":{"id":"price_1LeDnKEU5se17MekVr1iYYNf","active":true,
|
||||
"billing_scheme":"per_unit","created":1662278586,"currency":"usd","deleted":false,
|
||||
"livemode":false,"metadata":{},"product":"prod_MMxi2J5y69VPbO",
|
||||
"recurring":{"interval":"month","interval_count":1,"usage_type":"licensed"},
|
||||
@@ -292,14 +283,7 @@ mod tests {
|
||||
],
|
||||
"description": null,
|
||||
"discount": null,
|
||||
"discounts": [
|
||||
],
|
||||
"ended_at": null,
|
||||
"invoice_settings": {
|
||||
"issuer": {
|
||||
"type": "self"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
@@ -308,9 +292,6 @@ mod tests {
|
||||
"object": "subscription_item",
|
||||
"billing_thresholds": null,
|
||||
"created": 1663430298,
|
||||
"current_period_end": 1666022298,
|
||||
"current_period_start": 1663430298,
|
||||
"discounts": [],
|
||||
"metadata": {
|
||||
},
|
||||
"plan": {
|
||||
|
||||
+3
-3
@@ -123,14 +123,14 @@ mod tests {
|
||||
|
||||
assert_eq!(cover_image.maybe_cover_image_public_bucket_path, Some("/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png".to_string()));
|
||||
assert_eq!(cover_image.maybe_cover_image_public_bucket_url,
|
||||
Some(Url::parse("https://cdn-2.fakeyou.com/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png").unwrap()));
|
||||
Some(Url::parse("https://storage.googleapis.com/vocodes-public/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png").unwrap()));
|
||||
assert_eq!(cover_image.default_cover.image_index, 18);
|
||||
|
||||
let links = cover_image.maybe_links.unwrap();
|
||||
|
||||
assert_eq!(links.cdn_url, Url::parse(
|
||||
"https://cdn-2.fakeyou.com/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png").unwrap());
|
||||
"https://cdn.storyteller.ai/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png").unwrap());
|
||||
assert_eq!(links.thumbnail_template,
|
||||
"https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png");
|
||||
"https://cdn.storyteller.ai/cdn-cgi/image/width={WIDTH},quality=95/media/b/u/c/k/e/bucket_hash/image_bucket_hash.png");
|
||||
}
|
||||
}
|
||||
|
||||
+2
-7
@@ -41,11 +41,6 @@ pub fn is_job_failure_test(prompt: &str) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const EXPECTED_FAILURE_MESSAGE: &str =
|
||||
"This was a simulated failure. This message comes from the backend and \
|
||||
database to simulate a dynamic failure message. This message could in \
|
||||
theory be anything.";
|
||||
|
||||
#[test]
|
||||
fn no_trigger_returns_none() {
|
||||
assert!(test_synthetic_failure_reason("a normal prompt").is_none());
|
||||
@@ -55,14 +50,14 @@ mod tests {
|
||||
fn trigger_simulate_with_no_category_defaults_to_generation_failed() {
|
||||
let result = test_synthetic_failure_reason("simulate_artcraft_failure").unwrap();
|
||||
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
|
||||
assert_eq!(result.frontend_failure_message.as_deref(), Some(EXPECTED_FAILURE_MESSAGE));
|
||||
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_test_with_no_category_defaults_to_generation_failed() {
|
||||
let result = test_synthetic_failure_reason("test_artcraft_failure").unwrap();
|
||||
assert_eq!(result.frontend_failure_category, FrontendFailureCategory::GenerationFailed);
|
||||
assert_eq!(result.frontend_failure_message.as_deref(), Some(EXPECTED_FAILURE_MESSAGE));
|
||||
assert_eq!(result.frontend_failure_message.as_deref(), Some("This was a simulated failure"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
//! Maps Kinovi (Seedance2Pro) client errors to web errors.
|
||||
//!
|
||||
//! Known bad requests (content violations, over-long prompts, too many urls)
|
||||
//! become 400s with a user-facing message. Everything else is unanticipated
|
||||
//! and stays a 500.
|
||||
|
||||
use seedance2pro_client::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
|
||||
/// Map a raw Kinovi client error to a web error.
|
||||
pub fn map_seedance2pro_error_to_web_error(error: Seedance2ProError) -> CommonWebError {
|
||||
match error {
|
||||
Seedance2ProError::ApiBadRequest(bad_request) => {
|
||||
CommonWebError::BadInputWithSimpleMessage(user_facing_bad_request_message(&bad_request))
|
||||
}
|
||||
other => CommonWebError::from_error(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn user_facing_bad_request_message(error: &Seedance2ProBadRequestApiError) -> String {
|
||||
match error {
|
||||
Seedance2ProBadRequestApiError::VideoGenerationViolation { .. } => {
|
||||
"The generation request was flagged as a content violation. \
|
||||
Please adjust your prompt or input media and try again.".to_string()
|
||||
}
|
||||
Seedance2ProBadRequestApiError::PromptIsTooLong { .. } => {
|
||||
"The prompt is too long. Please shorten it below 10,000 characters and try again.".to_string()
|
||||
}
|
||||
Seedance2ProBadRequestApiError::TooManyUrls { .. } => {
|
||||
"Too many input files were attached. \
|
||||
Please remove some and try again.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::ResponseError;
|
||||
use seedance2pro_client::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bad_requests_become_400s_with_user_facing_messages() {
|
||||
let cases = [
|
||||
(
|
||||
Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: "{}".to_string() },
|
||||
"content violation",
|
||||
),
|
||||
(
|
||||
Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body: "{}".to_string() },
|
||||
"prompt is too long",
|
||||
),
|
||||
(
|
||||
Seedance2ProBadRequestApiError::TooManyUrls { raw_body: "{}".to_string() },
|
||||
"Too many input files",
|
||||
),
|
||||
];
|
||||
|
||||
for (bad_request, expected_message_fragment) in cases {
|
||||
let error = map_seedance2pro_error_to_web_error(
|
||||
Seedance2ProError::ApiBadRequest(bad_request));
|
||||
|
||||
assert_eq!(error.status_code().as_u16(), 400);
|
||||
match error {
|
||||
CommonWebError::BadInputWithSimpleMessage(message) => {
|
||||
assert!(
|
||||
message.contains(expected_message_fragment),
|
||||
"message {:?} should contain {:?}", message, expected_message_fragment);
|
||||
}
|
||||
other => panic!("Expected BadInputWithSimpleMessage, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unanticipated_errors_stay_500s() {
|
||||
let error = map_seedance2pro_error_to_web_error(
|
||||
Seedance2ProError::ApiSpecific(Seedance2ProSpecificApiError::UnauthorizedSessionExpired));
|
||||
|
||||
assert_eq!(error.status_code().as_u16(), 500);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod job_failure_test;
|
||||
pub mod map_seedance2pro_web_errors;
|
||||
pub mod payments_error_test;
|
||||
mod test_synthetic_failure_reason;
|
||||
+15
-28
@@ -1,10 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use actix_http::StatusCode;
|
||||
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure;
|
||||
use crate::http_server::endpoints::generate::common::map_seedance2pro_web_errors::map_seedance2pro_error_to_web_error;
|
||||
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
|
||||
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
@@ -13,7 +12,7 @@ use crate::state::server_state::ServerState;
|
||||
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
|
||||
use crate::util::lookup::lookup_media_file_urls_as_map::lookup_media_file_urls_as_map;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest, ResponseError};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount, Seedance2p0MultiFunctionVideoGenRequest,
|
||||
Seedance2p0MultiFunctionVideoGenResponse, Seedance2p0OutputResolution,
|
||||
@@ -235,19 +234,13 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Regular session fallback also failed for user {:?}: {:?}", user_token, err);
|
||||
|
||||
// Client-fault errors (eg. content violations, over-long prompts)
|
||||
// surface to the user as 400s and shouldn't page.
|
||||
if err.status_code() != StatusCode::BAD_REQUEST {
|
||||
let notification = NotificationDetailsBuilder::from_boxed_error(err.clone().into())
|
||||
.set_title("Seedance 2.0 generation failed (whitelist + fallback)".to_string())
|
||||
.set_urgency(Some(NotificationUrgency::High))
|
||||
.build();
|
||||
if let Err(page_err) = server_state.pager.enqueue_page(notification) {
|
||||
warn!("Failed to enqueue pager alert: {:?}", page_err);
|
||||
}
|
||||
let notification = NotificationDetailsBuilder::from_boxed_error(err.clone().into())
|
||||
.set_title("Seedance 2.0 generation failed (whitelist + fallback)".to_string())
|
||||
.set_urgency(Some(NotificationUrgency::High))
|
||||
.build();
|
||||
if let Err(page_err) = server_state.pager.enqueue_page(notification) {
|
||||
warn!("Failed to enqueue pager alert: {:?}", page_err);
|
||||
}
|
||||
|
||||
refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?;
|
||||
return Err(err);
|
||||
}
|
||||
@@ -276,19 +269,13 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
warn!("Error calling seedance2pro generate_video: {:?}", err);
|
||||
|
||||
// Client-fault errors (eg. content violations, over-long prompts)
|
||||
// surface to the user as 400s and shouldn't page.
|
||||
if err.status_code() != StatusCode::BAD_REQUEST {
|
||||
let notification = NotificationDetailsBuilder::from_boxed_error(err.clone().into())
|
||||
.set_title("Seedance 2.0 generation failed".to_string())
|
||||
.set_urgency(Some(NotificationUrgency::High))
|
||||
.build();
|
||||
if let Err(page_err) = server_state.pager.enqueue_page(notification) {
|
||||
warn!("Failed to enqueue pager alert: {:?}", page_err);
|
||||
}
|
||||
let notification = NotificationDetailsBuilder::from_boxed_error(err.clone().into())
|
||||
.set_title("Seedance 2.0 generation failed".to_string())
|
||||
.set_urgency(Some(NotificationUrgency::High))
|
||||
.build();
|
||||
if let Err(page_err) = server_state.pager.enqueue_page(notification) {
|
||||
warn!("Failed to enqueue pager alert: {:?}", page_err);
|
||||
}
|
||||
|
||||
refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?;
|
||||
return Err(err);
|
||||
}
|
||||
@@ -626,7 +613,7 @@ async fn upload_and_generate(
|
||||
let gen_response = generate_video(video_gen_args).await
|
||||
.map_err(|err| {
|
||||
warn!("Error calling seedance2pro generate_video: {:?}", err);
|
||||
map_seedance2pro_error_to_web_error(err)
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(SeedanceGenerationResult {
|
||||
|
||||
+2
-2
@@ -394,7 +394,7 @@ mod tests {
|
||||
let result = payload.maybe_result.expect("should have result");
|
||||
|
||||
assert_eq!(
|
||||
Url::parse("https://cdn-2.fakeyou.com/media/t/p/k/8/4/tpk848b5s5zwhnwrph75jhyfyja3j42v/fakeyou_tpk848b5s5zwhnwrph75jhyfyja3j42v.wav").unwrap(),
|
||||
Url::parse("https://cdn.storyteller.ai/media/t/p/k/8/4/tpk848b5s5zwhnwrph75jhyfyja3j42v/fakeyou_tpk848b5s5zwhnwrph75jhyfyja3j42v.wav").unwrap(),
|
||||
result.media_links.cdn_url
|
||||
);
|
||||
|
||||
@@ -433,7 +433,7 @@ mod tests {
|
||||
let result = payload.maybe_result.expect("should have result");
|
||||
|
||||
assert_eq!(
|
||||
Url::parse("https://cdn-2.fakeyou.com/tts_inference_output/9/9/7/vocodes_997e710c-d733-406c-9b6e-5e1d0c471816.wav").unwrap(),
|
||||
Url::parse("https://cdn.fakeyou.com/tts_inference_output/9/9/7/vocodes_997e710c-d733-406c-9b6e-5e1d0c471816.wav").unwrap(),
|
||||
result.media_links.cdn_url
|
||||
);
|
||||
|
||||
|
||||
+2
-3
@@ -18,7 +18,6 @@ use tokens::tokens::users::UserToken;
|
||||
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::omni_gen::generate::image::pipeline_result::ImagePipelineResult;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::map_seedance2pro_router_error::map_router_error_to_web_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::MediaFilesAsCdnUrlListAndMap;
|
||||
|
||||
@@ -156,7 +155,7 @@ async fn finalize_and_generate(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to finalize image v2 draft: {:?}", err);
|
||||
map_router_error_to_web_error(err)
|
||||
CommonWebError::from_error(err)
|
||||
})?
|
||||
}
|
||||
};
|
||||
@@ -165,7 +164,7 @@ async fn finalize_and_generate(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("v2 image generation failed: {:?}", err);
|
||||
map_router_error_to_web_error(err)
|
||||
CommonWebError::from_error(err)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -19,7 +19,6 @@ use crate::http_server::endpoints::omni_gen::generate::video::helpers::build_rou
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::helpers::pipeline_result::PipelineResult;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::helpers::resolve_media_tokens_to_urls::resolve_media_tokens_to_urls;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::kinovi_account::KinoviAccount;
|
||||
use crate::http_server::endpoints::omni_gen::shared_utils::map_seedance2pro_router_error::map_router_error_to_web_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
pub struct RunPipelineV2Args<'a> {
|
||||
@@ -171,7 +170,7 @@ async fn upload_and_generate(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to finalize v2 draft: {:?}", err);
|
||||
map_router_error_to_web_error(err)
|
||||
CommonWebError::from_error(err)
|
||||
})?
|
||||
}
|
||||
};
|
||||
@@ -180,6 +179,6 @@ async fn upload_and_generate(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("v2 video generation failed: {:?}", err);
|
||||
map_router_error_to_web_error(err)
|
||||
CommonWebError::from_error(err)
|
||||
})
|
||||
}
|
||||
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
//! Maps artcraft router errors to web errors for the omni_gen pipelines.
|
||||
//!
|
||||
//! Known Kinovi (Seedance2Pro) bad requests (content violations, over-long
|
||||
//! prompts, too many urls) become 400s with a user-facing message. Everything
|
||||
//! else is unanticipated and stays a 500.
|
||||
|
||||
use artcraft_router::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use artcraft_router::errors::provider_error::ProviderError;
|
||||
use seedance2pro_client::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
|
||||
/// Map an artcraft router error to a web error, unwrapping Kinovi bad requests.
|
||||
pub fn map_router_error_to_web_error(error: ArtcraftRouterError) -> CommonWebError {
|
||||
match error {
|
||||
ArtcraftRouterError::Provider(
|
||||
ProviderError::Seedance2Pro(Seedance2ProError::ApiBadRequest(bad_request))
|
||||
) => {
|
||||
CommonWebError::BadInputWithSimpleMessage(user_facing_bad_request_message(&bad_request))
|
||||
}
|
||||
other => CommonWebError::from_error(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn user_facing_bad_request_message(error: &Seedance2ProBadRequestApiError) -> String {
|
||||
match error {
|
||||
Seedance2ProBadRequestApiError::VideoGenerationViolation { .. } => {
|
||||
"The generation request was flagged as a content violation. \
|
||||
Please adjust your prompt or input media and try again.".to_string()
|
||||
}
|
||||
Seedance2ProBadRequestApiError::PromptIsTooLong { .. } => {
|
||||
"The prompt is too long. Please shorten it below 10,000 characters and try again.".to_string()
|
||||
}
|
||||
Seedance2ProBadRequestApiError::TooManyUrls { .. } => {
|
||||
"Too many input files were attached. \
|
||||
Please remove some and try again.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::ResponseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn kinovi_bad_requests_become_400s_with_user_facing_messages() {
|
||||
let cases = [
|
||||
(
|
||||
Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: "{}".to_string() },
|
||||
"content violation",
|
||||
),
|
||||
(
|
||||
Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body: "{}".to_string() },
|
||||
"prompt is too long",
|
||||
),
|
||||
(
|
||||
Seedance2ProBadRequestApiError::TooManyUrls { raw_body: "{}".to_string() },
|
||||
"Too many input files",
|
||||
),
|
||||
];
|
||||
|
||||
for (bad_request, expected_message_fragment) in cases {
|
||||
let error = map_router_error_to_web_error(
|
||||
ArtcraftRouterError::Provider(
|
||||
ProviderError::Seedance2Pro(
|
||||
Seedance2ProError::ApiBadRequest(bad_request))));
|
||||
|
||||
assert_eq!(error.status_code().as_u16(), 400);
|
||||
match error {
|
||||
CommonWebError::BadInputWithSimpleMessage(message) => {
|
||||
assert!(
|
||||
message.contains(expected_message_fragment),
|
||||
"message {:?} should contain {:?}", message, expected_message_fragment);
|
||||
}
|
||||
other => panic!("Expected BadInputWithSimpleMessage, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_router_errors_stay_500s() {
|
||||
let error = map_router_error_to_web_error(
|
||||
ArtcraftRouterError::UnsupportedModel("some_model".to_string()));
|
||||
|
||||
assert_eq!(error.status_code().as_u16(), 500);
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
pub mod map_seedance2pro_router_error;
|
||||
pub mod video;
|
||||
pub mod video;
|
||||
Reference in New Issue
Block a user