mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 05:48:06 -05:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71af9422d6 | ||
|
|
094a3e9a12 | ||
|
|
afb0bc32a1 | ||
|
|
2f8f233453 | ||
|
|
b957f9c223 | ||
|
|
c7ed21d595 | ||
|
|
3163748052 | ||
|
|
f5dbbac55d | ||
|
|
fed755588d | ||
|
|
e58e133539 | ||
|
|
54bac69997 | ||
|
|
691e5109a5 |
@@ -1,8 +1,6 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use wreq::StatusCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Seedance2ProSpecificApiError {
|
||||
/// The session cookies are expired or invalid.
|
||||
@@ -10,12 +8,6 @@ pub enum Seedance2ProSpecificApiError {
|
||||
|
||||
/// 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,
|
||||
body: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Error for Seedance2ProSpecificApiError {}
|
||||
@@ -25,7 +17,6 @@ impl Display for Seedance2ProSpecificApiError {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -5,7 +5,6 @@ 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;
|
||||
use crate::utils::common_headers::FIREFOX_USER_AGENT;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
@@ -309,7 +308,10 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result<GenerateVideo
|
||||
info!("Response status: {}, body: {}", status, response_body);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(categorize_seedance2pro_error(status, response_body));
|
||||
return Err(Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
let batch_response: Vec<BatchResponseItem> = serde_json::from_str(&response_body)
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
use wreq::StatusCode;
|
||||
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
|
||||
|
||||
/// Known error message substrings that indicate a billing/credits error.
|
||||
const BILLING_ERROR_MARKERS: &[&str] = &[
|
||||
"credits not enough",
|
||||
];
|
||||
|
||||
/// Categorize a non-success HTTP response into a specific or generic error.
|
||||
///
|
||||
/// 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,
|
||||
) -> Seedance2ProError {
|
||||
if is_billing_error(&body) {
|
||||
return Seedance2ProSpecificApiError::BillingError {
|
||||
status_code,
|
||||
body,
|
||||
}.into();
|
||||
}
|
||||
|
||||
Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body,
|
||||
}.into()
|
||||
}
|
||||
|
||||
/// Check if the response body contains a known billing error marker.
|
||||
fn is_billing_error(body: &str) -> bool {
|
||||
let body_lower = body.to_lowercase();
|
||||
BILLING_ERROR_MARKERS.iter().any(|marker| body_lower.contains(marker))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
|
||||
#[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");
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
#[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(),
|
||||
);
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub mod categorize_seedance2pro_error;
|
||||
pub mod common_headers;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
[
|
||||
{
|
||||
"error": {
|
||||
"json": {
|
||||
"message": "credits not enough",
|
||||
"code": -32600,
|
||||
"data": {
|
||||
"code": "BAD_REQUEST",
|
||||
"httpStatus": 400,
|
||||
"path": "workflow.runTask",
|
||||
"zodError": null
|
||||
},
|
||||
"stack": null
|
||||
},
|
||||
"meta": {
|
||||
"values": {
|
||||
"stack": [
|
||||
"undefined"
|
||||
]
|
||||
},
|
||||
"v": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
faBomb,
|
||||
faCircleExclamation,
|
||||
faTriangleExclamation,
|
||||
faCopy,
|
||||
faCheck,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import {
|
||||
@@ -224,7 +222,7 @@ const InProgressCard = ({
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="ml-auto flex h-6 w-6 items-center justify-center rounded-full text-base-fg/60 hover:bg-ui-controls"
|
||||
className="ml-auto h-6 w-6 rounded-full p-1 text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -304,7 +302,7 @@ const CompletedCard = ({
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="ml-auto flex h-6 w-6 items-center justify-center rounded-full text-base-fg/60 hover:bg-ui-controls"
|
||||
className="ml-auto h-6 w-6 rounded-full p-1 text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -346,14 +344,6 @@ const FailedCard = ({
|
||||
task: FailedTask;
|
||||
onDismiss?: () => void;
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [marqueePlaying, setMarqueePlaying] = useState(false);
|
||||
const [promptOverflows, setPromptOverflows] = useState(false);
|
||||
const promptRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const el = promptRef.current;
|
||||
if (el) setPromptOverflows(el.scrollWidth > el.clientWidth);
|
||||
}, [task.prompt]);
|
||||
const statusLabel = FAILED_STATUS_LABEL[task.status] || "Failed";
|
||||
const hasRefImages =
|
||||
task.refImageUrls && task.refImageUrls.length > 0;
|
||||
@@ -464,73 +454,19 @@ const FailedCard = ({
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{task.prompt && (
|
||||
<Tooltip
|
||||
content={task.prompt.length > 300 ? task.prompt.slice(0, 300) + "…" : task.prompt}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="max-w-[280px] text-wrap text-xs"
|
||||
zIndex={50}
|
||||
delay={400}
|
||||
>
|
||||
<div
|
||||
className="mt-1 overflow-hidden"
|
||||
onMouseEnter={promptOverflows ? () => setMarqueePlaying(true) : undefined}
|
||||
onMouseLeave={promptOverflows ? () => setMarqueePlaying(false) : undefined}
|
||||
>
|
||||
<div
|
||||
ref={promptRef}
|
||||
key={marqueePlaying ? "playing" : "idle"}
|
||||
className="whitespace-nowrap text-[11px] italic text-base-fg/40"
|
||||
style={marqueePlaying ? {
|
||||
animation: "marquee 8s linear infinite",
|
||||
animationDelay: "0.5s",
|
||||
animationFillMode: "both",
|
||||
} : undefined}
|
||||
>
|
||||
{task.prompt}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
{task.prompt && (
|
||||
<Tooltip
|
||||
content={copied ? "Copied!" : "Copy prompt"}
|
||||
position="bottom"
|
||||
strategy="fixed"
|
||||
className="text-xs"
|
||||
zIndex={50}
|
||||
delay={300}
|
||||
>
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Copy prompt"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(task.prompt!);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 3000);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={copied ? faCheck : faCopy} className={copied ? "text-green-400" : ""} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="ml-auto h-6 w-6 rounded-full p-1 text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss();
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,15 +22,10 @@ export default {
|
||||
"0%": { backgroundPosition: "200% 0" },
|
||||
"100%": { backgroundPosition: "-200% 0" },
|
||||
},
|
||||
marquee: {
|
||||
"0%": { transform: "translateX(0%)" },
|
||||
"100%": { transform: "translateX(-100%)" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
fadeIn: "fadeIn 0.2s ease-in-out",
|
||||
shimmer: "shimmer 2s ease-in-out infinite",
|
||||
marquee: "marquee 8s linear infinite paused",
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
|
||||
Reference in New Issue
Block a user