Compare commits

...
Author SHA1 Message Date
Brandon Thomas 046370c839 reuse lol 2026-02-20 15:45:21 +07:00
Brandon Thomas 6df8e03d97 cost estimate in credits 2026-02-20 14:48:03 +07:00
Brandon Thomas 1bc09a29e4 update endpoint 2026-02-20 14:31:59 +07:00
Brandon Thomas dfdfcaefeb need to create wallets for local testing 2026-02-20 14:25:31 +07:00
Brandon Thomas 5d1cfba7b4 cleanup 2026-02-20 14:13:20 +07:00
Brandon Thomas 03734a2961 all job tokens 2026-02-20 09:49:58 +07:00
Brandon Thomas 0d0595e557 test fix 2026-02-20 09:39:13 +07:00
Brandon Thomas 1b1c9b246c new handler 2026-02-20 09:34:21 +07:00
Brandon Thomas d5e0afc543 remove 2026-02-20 07:42:41 +07:00
Brandon Thomas 8be9e7ee30 cost estimates 2026-02-20 07:17:44 +07:00
27 changed files with 898 additions and 14 deletions
Generated
+1
View File
@@ -11452,6 +11452,7 @@ dependencies = [
"resend-rs",
"reusable_types",
"rust-s3",
"seedance2pro",
"serde",
"serde_derive",
"serde_json",
@@ -1,5 +1,6 @@
pub mod kling_2_5_turbo_multi_function_video_gen;
pub mod kling_2_6_multi_function_video_gen;
pub mod seedance_2p0_multi_function_video_gen;
pub mod sora_2_multi_function_video_gen;
pub mod sora_2_pro_multi_function_video_gen;
pub mod veo_3p1_fast_multi_function_video_gen;
@@ -0,0 +1,68 @@
use serde::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const SEEDANCE_2P0_MULTI_FUNCTION_VIDEO_GEN_PATH: &str = "/v1/generate/video/multi_function/seedance_2p0";
/// Seedance 2.0 multi-function video generation (text-to-video, image-to-video, and reference-based).
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Seedance2p0MultiFunctionVideoGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt.
pub prompt: Option<String>,
/// Optional start frame image.
pub start_frame_media_token: Option<MediaFileToken>,
/// Optional end frame image.
pub end_frame_media_token: Option<MediaFileToken>,
/// Optional reference images (reference mode). When present, takes priority over start/end frames.
pub reference_image_media_tokens: Option<Vec<MediaFileToken>>,
/// Video aspect ratio / resolution.
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
/// Duration in seconds (4–15).
pub duration_seconds: Option<u8>,
/// Number of videos to generate.
pub batch_count: Option<Seedance2p0BatchCount>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Seedance2p0AspectRatio {
/// 16:9 landscape (1280x720)
Landscape16x9,
/// 9:16 portrait (720x1280)
Portrait9x16,
/// 1:1 square (720x720)
Square1x1,
/// 4:3 standard (960x720)
Standard4x3,
/// 3:4 portrait (720x960)
Portrait3x4,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Seedance2p0BatchCount {
One,
Two,
Four,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct Seedance2p0MultiFunctionVideoGenResponse {
pub success: bool,
/// The first inference job token (always present).
pub inference_job_token: InferenceJobToken,
/// All inference job tokens for the batch (includes `inference_job_token` as the first entry).
pub all_inference_job_tokens: Vec<InferenceJobToken>,
}
@@ -35,6 +35,33 @@ pub struct GenerateVideoArgs<'a> {
pub reference_image_urls: Option<Vec<String>>,
}
impl GenerateVideoArgs<'_> {
/// Estimates the credit cost for this generation request.
///
/// Pricing rules:
/// - 40 credits per second of video (4s = 160, 15s = 600)
/// - Resolution has no effect on cost
/// - Input mode (text, keyframe, reference) has no effect on cost
/// - Batch 1 = 1×, Batch 2 = 2×, Batch 4 = 3× (not 4×)
pub fn estimate_credits(&self) -> u32 {
let per_video = u32::from(self.duration_seconds) * 40;
let batch_multiplier = match self.batch_count {
BatchCount::One => 1,
BatchCount::Two => 2,
BatchCount::Four => 3, // NB: 3x, not 4x.
};
per_video * batch_multiplier
}
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
// 25000 Credits costs $99.99 as of 2026-02-20
// 250 for $1.
let credits = self.estimate_credits() as f64;
let cost = credits / 250.0 * 100.0;
cost.round() as u64
}
}
// --- Public enums ---
/// Video resolution / aspect ratio.
@@ -220,6 +247,65 @@ mod tests {
use errors::AnyhowResult;
use log::LevelFilter;
fn dummy_session() -> Seedance2ProSession {
Seedance2ProSession::from_cookies_string(String::new())
}
fn args_with(duration_seconds: u8, batch_count: BatchCount) -> GenerateVideoArgs<'static> {
// Safety: the dummy session is leaked so the reference is 'static for test purposes.
let session = Box::leak(Box::new(dummy_session()));
GenerateVideoArgs {
session,
prompt: String::new(),
resolution: Resolution::Square1x1,
duration_seconds,
batch_count,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
}
}
#[test]
fn test_estimate_credits() {
// 40 credits per second, batch 1
assert_eq!(args_with(4, BatchCount::One).estimate_credits(), 160);
assert_eq!(args_with(5, BatchCount::One).estimate_credits(), 200);
assert_eq!(args_with(6, BatchCount::One).estimate_credits(), 240);
assert_eq!(args_with(7, BatchCount::One).estimate_credits(), 280);
assert_eq!(args_with(15, BatchCount::One).estimate_credits(), 600);
// Batch 2 = 2×
assert_eq!(args_with(4, BatchCount::Two).estimate_credits(), 320);
assert_eq!(args_with(5, BatchCount::Two).estimate_credits(), 400);
assert_eq!(args_with(15, BatchCount::Two).estimate_credits(), 1200);
// Batch 4 = 3× (not 4×)
assert_eq!(args_with(4, BatchCount::Four).estimate_credits(), 480);
assert_eq!(args_with(5, BatchCount::Four).estimate_credits(), 600);
assert_eq!(args_with(15, BatchCount::Four).estimate_credits(), 1800);
}
#[test]
fn test_estimate_cost_usd_cents() {
// 40 credits per second, batch 1
assert_eq!(args_with(4, BatchCount::One).estimate_cost_in_usd_cents(), 64);
assert_eq!(args_with(5, BatchCount::One).estimate_cost_in_usd_cents(), 80);
assert_eq!(args_with(6, BatchCount::One).estimate_cost_in_usd_cents(), 96);
assert_eq!(args_with(7, BatchCount::One).estimate_cost_in_usd_cents(), 112);
assert_eq!(args_with(15, BatchCount::One).estimate_cost_in_usd_cents(), 240);
// Batch 2 = 2×
assert_eq!(args_with(4, BatchCount::Two).estimate_cost_in_usd_cents(), 128);
assert_eq!(args_with(5, BatchCount::Two).estimate_cost_in_usd_cents(), 160);
assert_eq!(args_with(15, BatchCount::Two).estimate_cost_in_usd_cents(), 480);
// Batch 4 = 3× (not 4×)
assert_eq!(args_with(4, BatchCount::Four).estimate_cost_in_usd_cents(), 192);
assert_eq!(args_with(5, BatchCount::Four).estimate_cost_in_usd_cents(), 240);
assert_eq!(args_with(15, BatchCount::Four).estimate_cost_in_usd_cents(), 720);
}
fn test_session() -> AnyhowResult<Seedance2ProSession> {
let cookies = get_test_cookies()?;
Ok(Seedance2ProSession::from_cookies_string(cookies))
@@ -86,7 +86,6 @@ pub async fn insert_generic_inference_job_for_fal_queue_with_apriori_job_token<'
let maybe_external_third_party = InferenceJobExternalThirdParty::Fal;
let maybe_external_third_party_id = "";
const STATUS : JobStatusPlus = JobStatusPlus::Pending;
let query = sqlx::query!(
@@ -1,3 +1,4 @@
pub mod fal;
pub mod job;
pub mod seedance2pro;
pub mod web;
@@ -0,0 +1,137 @@
use anyhow::anyhow;
use log::info;
use sqlx::{Executor, MySql};
use std::marker::PhantomData;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::visibility::Visibility;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
pub struct InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c, E>
where E: 'e + Executor<'c, Database = MySql>
{
pub uuid_idempotency_token: &'e str,
// NOTE: We'll generate this ahead of time so we can save it with billing info!
pub apriori_job_token: &'e InferenceJobToken,
/// The external order_id returned by the seedance2pro generate_video call.
pub maybe_external_third_party_id: &'e str,
pub maybe_inference_args: Option<GenericInferenceArgs>,
pub maybe_prompt_token: Option<&'e PromptToken>,
pub maybe_creator_user_token: Option<&'e UserToken>,
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'e str,
pub creator_set_visibility: Visibility,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
pub async fn insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token<'e, 'c : 'e, E>(
args: InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c, E>
) -> Result<InferenceJobToken, DatabaseQueryError>
where E: 'e + Executor<'c, Database = MySql>
{
let serialized_args_payload = serde_json::ser::to_string(&args.maybe_inference_args)
.map_err(|_e| anyhow!("could not encode inference args"))?;
const JOB_TYPE: InferenceJobType = InferenceJobType::Seedance2ProQueue;
const INFERENCE_CATEGORY: InferenceCategory = InferenceCategory::VideoGeneration;
const PRODUCT_CATEGORY: InferenceJobProductCategory = InferenceJobProductCategory::Seedance2ProVideo;
const EXTERNAL_THIRD_PARTY: InferenceJobExternalThirdParty = InferenceJobExternalThirdParty::Seedance2Pro;
const STATUS: JobStatusPlus = JobStatusPlus::Pending;
let query = sqlx::query!(
r#"
INSERT INTO generic_inference_jobs
SET
token = ?,
uuid_idempotency_token = ?,
job_type = ?,
maybe_external_third_party = ?,
maybe_external_third_party_id = ?,
product_category = ?,
inference_category = ?,
maybe_model_type = NULL,
maybe_model_token = NULL,
maybe_input_source_token = NULL,
maybe_input_source_token_type = NULL,
maybe_download_url = NULL,
maybe_cover_image_media_file_token = NULL,
maybe_prompt_token = ?,
maybe_raw_inference_text = NULL,
maybe_inference_args = ?,
maybe_creator_user_token = ?,
maybe_creator_anonymous_visitor_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
priority_level = 0,
is_keepalive_required = FALSE,
max_duration_seconds = 0,
is_debug_request = FALSE,
maybe_routing_tag = NULL,
status = ?
"#,
args.apriori_job_token.as_str(),
args.uuid_idempotency_token,
JOB_TYPE.to_str(),
EXTERNAL_THIRD_PARTY.to_str(),
args.maybe_external_third_party_id,
PRODUCT_CATEGORY.to_str(),
INFERENCE_CATEGORY.to_str(),
args.maybe_prompt_token.map(|t| t.to_string()),
serialized_args_payload,
args.maybe_creator_user_token.map(|t| t.to_string()),
args.maybe_avt_token.map(|t| t.to_string()),
args.creator_ip_address,
args.creator_set_visibility.to_str(),
STATUS.to_str(),
);
let query_result = query.execute(args.mysql_executor)
.await;
let record_id = match query_result {
Err(err) => return Err(DatabaseQueryError::from(err)),
Ok(res) => res.last_insert_id(),
};
info!("Insert generic inference job for Seedance 2 Pro queue: {} with record ID {}", args.apriori_job_token, record_id);
Ok(args.apriori_job_token.clone())
}
@@ -0,0 +1 @@
pub mod insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token;
@@ -40,6 +40,7 @@ pub enum InferenceCategory {
ImageGeneration,
/// FAL-powered video generation
/// (Also Seedance2-Pro.com)
#[serde(rename = "video_generation")]
VideoGeneration,
@@ -16,6 +16,10 @@ pub enum InferenceJobExternalThirdParty {
#[serde(rename = "fal")]
#[default]
Fal,
/// Seedance 2 Pro jobs
#[serde(rename = "seedance2pro")]
Seedance2Pro,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
@@ -27,12 +31,14 @@ impl InferenceJobExternalThirdParty {
pub fn to_str(&self) -> &'static str {
match self {
Self::Fal => "fal",
Self::Seedance2Pro => "seedance2pro",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"fal" => Ok(Self::Fal),
"seedance2pro" => Ok(Self::Seedance2Pro),
_ => Err(format!("invalid value: {:?}", value)),
}
}
@@ -42,6 +48,7 @@ impl InferenceJobExternalThirdParty {
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::Fal,
Self::Seedance2Pro,
])
}
}
@@ -57,22 +64,25 @@ mod tests {
#[test]
fn test_serialization() {
assert_serialization(InferenceJobExternalThirdParty::Fal, "fal");
assert_serialization(InferenceJobExternalThirdParty::Seedance2Pro, "seedance2pro");
}
#[test]
fn to_str() {
assert_eq!(InferenceJobExternalThirdParty::Fal.to_str(), "fal");
assert_eq!(InferenceJobExternalThirdParty::Seedance2Pro.to_str(), "seedance2pro");
}
#[test]
fn from_str() {
assert_eq!(InferenceJobExternalThirdParty::from_str("fal").unwrap(), InferenceJobExternalThirdParty::Fal);
assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro").unwrap(), InferenceJobExternalThirdParty::Seedance2Pro);
}
#[test]
fn all_variants() {
// Static check
const EXPECTED_COUNT : usize = 1;
const EXPECTED_COUNT : usize = 2;
assert_eq!(InferenceJobExternalThirdParty::all_variants().len(), EXPECTED_COUNT);
assert_eq!(InferenceJobExternalThirdParty::iter().len(), EXPECTED_COUNT);
@@ -103,7 +113,7 @@ mod tests {
#[test]
fn serialized_length_ok_for_database() {
const MAX_LENGTH : usize = 32;
const MAX_LENGTH : usize = 16;
for variant in InferenceJobExternalThirdParty::all_variants() {
let serialized = variant.to_str();
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
@@ -30,6 +30,11 @@ pub enum InferenceJobProductCategory {
FalObject,
FalBgRemoval,
// =============== SEEDANCE 2 PRO ===============
#[serde(rename = "seedance2pro_video")]
Seedance2ProVideo,
// =============== TEXT TO SPEECH ===============
/// TTS: GptSoVits
@@ -125,6 +130,7 @@ impl InferenceJobProductCategory {
Self::FalVideo => "fal_video",
Self::FalObject => "fal_object",
Self::FalBgRemoval => "fal_bg_removal",
Self::Seedance2ProVideo => "seedance2pro_video",
Self::TtsGptSoVits => "tts_gpt_so_vits",
Self::TtsStyleTts2 => "tts_style_tts2",
Self::TtsTacotron2 => "tts_tacotron2",
@@ -157,6 +163,7 @@ impl InferenceJobProductCategory {
"fal_video" => Ok(Self::FalVideo),
"fal_object" => Ok(Self::FalObject),
"fal_bg_removal" => Ok(Self::FalBgRemoval),
"seedance2pro_video" => Ok(Self::Seedance2ProVideo),
"tts_gpt_so_vits" => Ok(Self::TtsGptSoVits),
"tts_style_tts2" => Ok(Self::TtsStyleTts2),
"tts_tacotron2" => Ok(Self::TtsTacotron2),
@@ -192,6 +199,7 @@ impl InferenceJobProductCategory {
Self::FalVideo,
Self::FalObject,
Self::FalBgRemoval,
Self::Seedance2ProVideo,
Self::TtsGptSoVits,
Self::TtsStyleTts2,
Self::TtsTacotron2,
@@ -233,6 +241,7 @@ mod tests {
assert_serialization(InferenceJobProductCategory::FalVideo, "fal_video");
assert_serialization(InferenceJobProductCategory::FalObject, "fal_object");
assert_serialization(InferenceJobProductCategory::FalBgRemoval, "fal_bg_removal");
assert_serialization(InferenceJobProductCategory::Seedance2ProVideo, "seedance2pro_video");
assert_serialization(InferenceJobProductCategory::TtsGptSoVits, "tts_gpt_so_vits");
assert_serialization(InferenceJobProductCategory::TtsStyleTts2, "tts_style_tts2");
assert_serialization(InferenceJobProductCategory::TtsTacotron2, "tts_tacotron2");
@@ -264,6 +273,7 @@ mod tests {
assert_eq!(InferenceJobProductCategory::FalVideo.to_str(), "fal_video");
assert_eq!(InferenceJobProductCategory::FalObject.to_str(), "fal_object");
assert_eq!(InferenceJobProductCategory::FalBgRemoval.to_str(), "fal_bg_removal");
assert_eq!(InferenceJobProductCategory::Seedance2ProVideo.to_str(), "seedance2pro_video");
assert_eq!(InferenceJobProductCategory::TtsGptSoVits.to_str(), "tts_gpt_so_vits");
assert_eq!(InferenceJobProductCategory::TtsStyleTts2.to_str(), "tts_style_tts2");
assert_eq!(InferenceJobProductCategory::TtsTacotron2.to_str(), "tts_tacotron2");
@@ -294,6 +304,7 @@ mod tests {
assert_eq!(InferenceJobProductCategory::from_str("fal_video").unwrap(), InferenceJobProductCategory::FalVideo);
assert_eq!(InferenceJobProductCategory::from_str("fal_object").unwrap(), InferenceJobProductCategory::FalObject);
assert_eq!(InferenceJobProductCategory::from_str("fal_bg_removal").unwrap(), InferenceJobProductCategory::FalBgRemoval);
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video").unwrap(), InferenceJobProductCategory::Seedance2ProVideo);
assert_eq!(InferenceJobProductCategory::from_str("tts_gpt_so_vits").unwrap(), InferenceJobProductCategory::TtsGptSoVits);
assert_eq!(InferenceJobProductCategory::from_str("tts_style_tts2").unwrap(), InferenceJobProductCategory::TtsStyleTts2);
assert_eq!(InferenceJobProductCategory::from_str("tts_tacotron2").unwrap(), InferenceJobProductCategory::TtsTacotron2);
@@ -320,7 +331,7 @@ mod tests {
#[test]
fn all_variants() {
// Static check
const EXPECTED_COUNT : usize = 27;
const EXPECTED_COUNT : usize = 28;
assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT);
assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT);
@@ -18,7 +18,11 @@ pub enum InferenceJobType {
/// Fal jobs do not run on our job runners.
/// Instead, we receive web hook updates from Fal.
FalQueue,
/// Seedance 2 Pro jobs. We poll for results.
#[serde(rename = "seedance2pro_queue")]
Seedance2ProQueue,
/// Storyteller Studio and Video Style Transfer Jobs (which we may want to split).
/// These run in Comfy.
/// TODO(bt,2024-07-15): We may segregate these two job types in the future
@@ -110,6 +114,7 @@ impl InferenceJobType {
pub fn to_str(&self) -> &'static str {
match self {
Self::FalQueue => "fal_queue",
Self::Seedance2ProQueue => "seedance2pro_queue",
Self::VideoRender => "video_render",
Self::LivePortrait => "live_portrait",
Self::FaceFusion => "face_fusion",
@@ -136,6 +141,7 @@ impl InferenceJobType {
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"fal_queue" => Ok(Self::FalQueue),
"seedance2pro_queue" => Ok(Self::Seedance2ProQueue),
"video_render" => Ok(Self::VideoRender),
"live_portrait" => Ok(Self::LivePortrait),
"face_fusion" => Ok(Self::FaceFusion),
@@ -165,6 +171,7 @@ impl InferenceJobType {
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::FalQueue,
Self::Seedance2ProQueue,
Self::VideoRender,
Self::LivePortrait,
Self::FaceFusion,
@@ -205,6 +212,7 @@ mod tests {
#[test]
fn test_serialization() {
assert_serialization(InferenceJobType::FalQueue, "fal_queue");
assert_serialization(InferenceJobType::Seedance2ProQueue, "seedance2pro_queue");
assert_serialization(InferenceJobType::VideoRender, "video_render");
assert_serialization(InferenceJobType::LivePortrait, "live_portrait");
assert_serialization(InferenceJobType::FaceFusion, "face_fusion");
@@ -230,6 +238,7 @@ mod tests {
#[test]
fn to_str() {
assert_eq!(InferenceJobType::FalQueue.to_str(), "fal_queue");
assert_eq!(InferenceJobType::Seedance2ProQueue.to_str(), "seedance2pro_queue");
assert_eq!(InferenceJobType::VideoRender.to_str(), "video_render");
assert_eq!(InferenceJobType::LivePortrait.to_str(), "live_portrait");
assert_eq!(InferenceJobType::FaceFusion.to_str(), "face_fusion");
@@ -255,6 +264,7 @@ mod tests {
#[test]
fn from_str() {
assert_eq!(InferenceJobType::from_str("fal_queue").unwrap(), InferenceJobType::FalQueue);
assert_eq!(InferenceJobType::from_str("seedance2pro_queue").unwrap(), InferenceJobType::Seedance2ProQueue);
assert_eq!(InferenceJobType::from_str("video_render").unwrap(), InferenceJobType::VideoRender);
assert_eq!(InferenceJobType::from_str("live_portrait").unwrap(), InferenceJobType::LivePortrait);
assert_eq!(InferenceJobType::from_str("face_fusion").unwrap(), InferenceJobType::FaceFusion);
@@ -280,7 +290,7 @@ mod tests {
#[test]
fn all_variants() {
// Static check
const EXPECTED_COUNT : usize = 21;
const EXPECTED_COUNT : usize = 22;
assert_eq!(InferenceJobType::all_variants().len(), EXPECTED_COUNT);
assert_eq!(InferenceJobType::iter().len(), EXPECTED_COUNT);
@@ -216,7 +216,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = MediaFileOriginModelType::all_variants();
assert_eq!(variants.len(), 15);
assert_eq!(variants.len(), 18);
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::FaceFusion));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::F5TTS));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::LivePortrait));
@@ -93,6 +93,8 @@ pub enum ModelType {
Seedance10Lite,
#[serde(rename = "seedance_1p0_pro")]
Seedance10Pro,
#[serde(rename = "seedance_2p0")]
Seedance2p0,
#[serde(rename = "sora_2")]
Sora2,
#[serde(rename = "sora_2_pro")]
@@ -163,6 +165,7 @@ impl ModelType {
Self::Kling2p6Pro => "kling_2p6_pro",
Self::Seedance10Lite => "seedance_1p0_lite",
Self::Seedance10Pro => "seedance_1p0_pro",
Self::Seedance2p0 => "seedance_2p0",
Self::Sora2 => "sora_2",
Self::Sora2Pro => "sora_2_pro",
Self::Veo2 => "veo_2",
@@ -217,6 +220,7 @@ impl ModelType {
"kling_2p6_pro" => Ok(Self::Kling2p6Pro),
"seedance_1p0_lite" => Ok(Self::Seedance10Lite),
"seedance_1p0_pro" => Ok(Self::Seedance10Pro),
"seedance_2p0" => Ok(Self::Seedance2p0),
"sora_2" => Ok(Self::Sora2),
"sora_2_pro" => Ok(Self::Sora2Pro),
"veo_2" => Ok(Self::Veo2),
@@ -275,6 +279,7 @@ impl ModelType {
Self::Kling2p6Pro,
Self::Seedance10Lite,
Self::Seedance10Pro,
Self::Seedance2p0,
Self::Sora2,
Self::Sora2Pro,
Self::Veo2,
@@ -337,6 +342,7 @@ mod tests {
assert_serialization(ModelType::Kling2p6Pro, "kling_2p6_pro");
assert_serialization(ModelType::Seedance10Lite, "seedance_1p0_lite");
assert_serialization(ModelType::Seedance10Pro, "seedance_1p0_pro");
assert_serialization(ModelType::Seedance2p0, "seedance_2p0");
assert_serialization(ModelType::Sora2, "sora_2");
assert_serialization(ModelType::Sora2Pro, "sora_2_pro");
assert_serialization(ModelType::Veo2, "veo_2");
@@ -389,6 +395,7 @@ mod tests {
assert_eq!(ModelType::Kling2p6Pro.to_str(), "kling_2p6_pro");
assert_eq!(ModelType::Seedance10Lite.to_str(), "seedance_1p0_lite");
assert_eq!(ModelType::Seedance10Pro.to_str(), "seedance_1p0_pro");
assert_eq!(ModelType::Seedance2p0.to_str(), "seedance_2p0");
assert_eq!(ModelType::Sora2.to_str(), "sora_2");
assert_eq!(ModelType::Sora2Pro.to_str(), "sora_2_pro");
assert_eq!(ModelType::Veo2.to_str(), "veo_2");
@@ -441,6 +448,7 @@ mod tests {
assert_eq!(ModelType::from_str("kling_2p6_pro").unwrap(), ModelType::Kling2p6Pro);
assert_eq!(ModelType::from_str("seedance_1p0_lite").unwrap(), ModelType::Seedance10Lite);
assert_eq!(ModelType::from_str("seedance_1p0_pro").unwrap(), ModelType::Seedance10Pro);
assert_eq!(ModelType::from_str("seedance_2p0").unwrap(), ModelType::Seedance2p0);
assert_eq!(ModelType::from_str("sora_2").unwrap(), ModelType::Sora2);
assert_eq!(ModelType::from_str("sora_2_pro").unwrap(), ModelType::Sora2Pro);
assert_eq!(ModelType::from_str("veo_2").unwrap(), ModelType::Veo2);
@@ -458,7 +466,7 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = ModelType::all_variants();
assert_eq!(variants.len(), 44);
assert_eq!(variants.len(), 45);
// Image models
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Dev));
assert_eq!(variants.pop_first(), Some(ModelType::Flux1Schnell));
@@ -495,6 +503,7 @@ mod tests {
assert_eq!(variants.pop_first(), Some(ModelType::Kling2p6Pro));
assert_eq!(variants.pop_first(), Some(ModelType::Seedance10Lite));
assert_eq!(variants.pop_first(), Some(ModelType::Seedance10Pro));
assert_eq!(variants.pop_first(), Some(ModelType::Seedance2p0));
assert_eq!(variants.pop_first(), Some(ModelType::Sora2));
assert_eq!(variants.pop_first(), Some(ModelType::Sora2Pro));
assert_eq!(variants.pop_first(), Some(ModelType::Veo2));
@@ -100,6 +100,10 @@ pub enum PublicInferenceJobType {
/// Sora GPT 4o image gen
#[serde(rename = "image_gen_api")]
ImageGenApi,
/// Seedance 2 Pro video generation jobs. We poll for results.
#[serde(rename = "seedance2pro_queue")]
Seedance2ProQueue,
}
/// NB: Legacy API for older code.
@@ -130,6 +134,7 @@ impl PublicInferenceJobType {
InferenceJobType::BevyToWorkflow => Self::BevyToWorkflow,
InferenceJobType::RerenderAVideo => Self::RerenderAVideo,
InferenceJobType::ImageGenApi => Self::ImageGenApi,
InferenceJobType::Seedance2ProQueue => Self::Seedance2ProQueue,
}
}
@@ -153,6 +158,7 @@ impl PublicInferenceJobType {
Self::SoVitsSvc => InferenceJobType::SoVitsSvc,
Self::StableDiffusion => InferenceJobType::StableDiffusion,
Self::ImageGenApi => InferenceJobType::ImageGenApi,
Self::Seedance2ProQueue => InferenceJobType::Seedance2ProQueue,
Self::StyleTTS2 => InferenceJobType::StyleTTS2,
Self::Tacotron2 => InferenceJobType::Tacotron2,
Self::Unknown => InferenceJobType::Unknown,
@@ -54,6 +54,7 @@ enums_public.workspace = true
errors.workspace = true
fal_client.workspace = true
filesys.workspace = true
seedance2pro.workspace = true
google_sign_in = { path = "../../../api_clients/google_sign_in" }
hashing = { path = "../../../lib/files/hashing" }
http_server_common = { path = "../../../lib/deprecated/http_server_common" }
@@ -1,5 +1,6 @@
pub mod kling_2p5_turbo_pro_multi_function_video_gen_handler;
pub mod kling_2p6_pro_multi_function_video_gen_handler;
pub mod seedance_2p0_multi_function_video_gen_handler;
pub mod sora_2_multi_function_video_gen_handler;
pub mod sora_2_pro_multi_function_video_gen_handler;
pub mod veo_3p1_fast_multi_function_video_gen_handler;
@@ -0,0 +1,390 @@
use std::sync::Arc;
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::generate::common::payments_error_test::payments_error_test;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
use crate::util::http_download_url_to_bytes::http_download_url_to_bytes;
use crate::util::lookup::lookup_image_urls_as_map::lookup_image_urls_as_map;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
Seedance2p0AspectRatio, Seedance2p0BatchCount, Seedance2p0MultiFunctionVideoGenRequest,
Seedance2p0MultiFunctionVideoGenResponse,
};
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::model_type::ModelType;
use enums::common::visibility::Visibility;
use http_server_common::request::get_request_ip::get_request_ip;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::{
insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token,
InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs,
};
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{
insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem,
};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro::requests::generate_video::generate_video::{
generate_video, BatchCount, GenerateVideoArgs, Resolution,
};
use seedance2pro::requests::prepare_image_upload::prepare_image_upload::{
prepare_image_upload, PrepareImageUploadArgs,
};
use seedance2pro::requests::upload_image::upload_image::{upload_image, UploadImageArgs};
use sqlx::Acquire;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
/// Seedance 2.0 Multi-Function video generation (text-to-video, keyframe, and reference).
#[utoipa::path(
post,
tag = "Generate Video (Multi-Function)",
path = "/v1/generate/video/multi_function/seedance_2p0",
responses(
(status = 200, description = "Success", body = Seedance2p0MultiFunctionVideoGenResponse),
),
params(
("request" = Seedance2p0MultiFunctionVideoGenRequest, description = "Payload for Request"),
)
)]
pub async fn seedance_2p0_multi_function_video_gen_handler(
http_request: HttpRequest,
request: Json<Seedance2p0MultiFunctionVideoGenRequest>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<Seedance2p0MultiFunctionVideoGenResponse>, CommonWebError> {
payments_error_test(&request.prompt.as_deref().unwrap_or(""))?;
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
let user_token = match maybe_user_session.as_ref() {
Some(session) => &session.user_token,
None => {
return Err(CommonWebError::NotAuthorized);
}
};
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(CommonWebError::BadInputWithSimpleMessage(reason));
}
// --- Collect all media tokens to look up ---
let mut all_media_tokens = Vec::new();
if let Some(token) = request.start_frame_media_token.as_ref() {
all_media_tokens.push(token.clone());
}
if let Some(token) = request.end_frame_media_token.as_ref() {
all_media_tokens.push(token.clone());
}
if let Some(tokens) = request.reference_image_media_tokens.as_ref() {
all_media_tokens.extend(tokens.iter().cloned());
}
let image_urls_by_token = if all_media_tokens.is_empty() {
std::collections::HashMap::new()
} else {
info!("Looking up image media tokens: {:?}", all_media_tokens);
lookup_image_urls_as_map(
&http_request,
&mut mysql_connection,
server_state.server_environment,
&all_media_tokens,
).await?
};
// --- Insert idempotency token ---
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
CommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string())
})?;
// --- Build seedance2pro session ---
let session = Seedance2ProSession::from_cookies_string(
server_state.seedance2pro.cookies.clone()
);
// --- Upload images to seedance2pro CDN ---
let start_frame_url = match request.start_frame_media_token.as_ref() {
None => None,
Some(token) => match image_urls_by_token.get(token) {
None => return Err(CommonWebError::BadInputWithSimpleMessage("Start frame media not found.".to_string())),
Some(url) => Some(upload_to_seedance2pro(&session, url).await?),
}
};
let end_frame_url = match request.end_frame_media_token.as_ref() {
None => None,
Some(token) => match image_urls_by_token.get(token) {
None => return Err(CommonWebError::BadInputWithSimpleMessage("End frame media not found.".to_string())),
Some(url) => Some(upload_to_seedance2pro(&session, url).await?),
}
};
let reference_image_urls: Option<Vec<String>> = match request.reference_image_media_tokens.as_ref() {
None => None,
Some(tokens) if tokens.is_empty() => None,
Some(tokens) => {
let mut urls = Vec::with_capacity(tokens.len());
for token in tokens {
match image_urls_by_token.get(token) {
None => return Err(CommonWebError::BadInputWithSimpleMessage("Reference image media not found.".to_string())),
Some(url) => {
let seedance_url = upload_to_seedance2pro(&session, url).await?;
urls.push(seedance_url);
}
}
}
Some(urls)
}
};
// --- Map request params to seedance2pro types ---
let resolution = match request.aspect_ratio {
Some(Seedance2p0AspectRatio::Landscape16x9) => Resolution::Landscape16x9,
Some(Seedance2p0AspectRatio::Portrait9x16) => Resolution::Portrait9x16,
Some(Seedance2p0AspectRatio::Square1x1) => Resolution::Square1x1,
Some(Seedance2p0AspectRatio::Standard4x3) => Resolution::Standard4x3,
Some(Seedance2p0AspectRatio::Portrait3x4) => Resolution::Portrait3x4,
None => Resolution::Landscape16x9,
};
let batch_count = match request.batch_count {
Some(Seedance2p0BatchCount::One) | None => BatchCount::One,
Some(Seedance2p0BatchCount::Two) => BatchCount::Two,
Some(Seedance2p0BatchCount::Four) => BatchCount::Four,
};
let duration_seconds = request.duration_seconds.unwrap_or(5).clamp(4, 15);
let prompt = request.prompt.clone().unwrap_or_else(|| "".to_string());
let video_gen_args = GenerateVideoArgs {
session: &session,
prompt,
resolution,
duration_seconds,
batch_count,
start_frame_url,
end_frame_url,
reference_image_urls,
};
// --- Calculate cost and charge wallet ---
let cost_in_cents = video_gen_args.estimate_cost_in_usd_cents(); // NB: ArtCraft credits are 1:1 with USD cents.
let apriori_job_token = InferenceJobToken::generate();
info!("Charging wallet: {} cents ({} credits)", cost_in_cents, cost_in_cents);
attempt_wallet_deduction_else_common_web_error(
user_token,
Some(apriori_job_token.as_str()),
cost_in_cents,
&mut mysql_connection,
).await?;
// --- Call seedance2pro API ---
let gen_response = generate_video(video_gen_args)
.await
.map_err(|err| {
warn!("Error calling seedance2pro generate_video: {:?}", err);
CommonWebError::ServerError
})?;
info!(
"Seedance2pro task_id={}, order_id={}",
gen_response.task_id, gen_response.order_id
);
// --- DB writes in a transaction ---
let ip_address = get_request_ip(&http_request);
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
// NB: Don't fail the job if the prompt insert fails.
let prompt_result = insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_model_type: Some(ModelType::Seedance2p0),
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: None,
maybe_other_args: None,
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await;
let prompt_token = match prompt_result {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None
}
};
if let Some(token) = prompt_token.as_ref() {
let mut context_items: Vec<PromptContextItem> = Vec::new();
if let Some(media_token) = &request.start_frame_media_token {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidStartFrame,
});
}
if let Some(media_token) = &request.end_frame_media_token {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidEndFrame,
});
}
if !context_items.is_empty() {
if let Err(err) = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: context_items,
transaction: &mut transaction,
}).await {
warn!("Error inserting batch prompt context items: {:?}", err);
}
}
}
// Insert one DB job per order_id in the batch.
let order_ids: Vec<String> = match gen_response.order_ids {
Some(ids) if !ids.is_empty() => ids,
_ => vec![gen_response.order_id],
};
let mut all_job_tokens: Vec<InferenceJobToken> = Vec::with_capacity(order_ids.len());
for (i, order_id) in order_ids.iter().enumerate() {
let job_token = if i == 0 {
apriori_job_token.clone()
} else {
InferenceJobToken::generate()
};
// For batch jobs beyond the first, generate a unique idempotency key.
let idempotency_str = if i == 0 {
request.uuid_idempotency_token.clone()
} else {
format!("{}-batch-{}", request.uuid_idempotency_token, i)
};
let db_result = insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token(
InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs {
apriori_job_token: &job_token,
uuid_idempotency_token: &idempotency_str,
maybe_external_third_party_id: order_id,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_creator_user_token: maybe_user_session.as_ref().map(|s| &s.user_token),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}
).await;
match db_result {
Ok(token) => {
all_job_tokens.push(token);
}
Err(err) => {
warn!("Error inserting seedance2pro inference job (order_id={}): {:?}", order_id, err);
if i == 0 {
return Err(CommonWebError::ServerError);
}
}
}
}
let first_job_token = all_job_tokens.first().cloned().ok_or_else(|| {
error!("No inference job token was created");
CommonWebError::ServerError
})?;
transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(Seedance2p0MultiFunctionVideoGenResponse {
success: true,
inference_job_token: first_job_token,
all_inference_job_tokens: all_job_tokens,
}))
}
async fn upload_to_seedance2pro(
session: &Seedance2ProSession,
our_cdn_url: &str,
) -> Result<String, CommonWebError> {
let image_bytes = http_download_url_to_bytes(our_cdn_url)
.await
.map_err(|err| {
warn!("Error downloading media file from CDN: {:?}", err);
CommonWebError::ServerError
})?
.to_vec();
let prepare_result = prepare_image_upload(PrepareImageUploadArgs { session })
.await
.map_err(|err| {
warn!("Error preparing seedance2pro image upload: {:?}", err);
CommonWebError::ServerError
})?;
let upload_result = upload_image(UploadImageArgs {
upload_url: prepare_result.upload_url,
image_bytes,
})
.await
.map_err(|err| {
warn!("Error uploading image to seedance2pro: {:?}", err);
CommonWebError::ServerError
})?;
Ok(upload_result.public_url)
}
@@ -22,6 +22,7 @@ pub mod trending;
pub mod tts;
pub mod user_bookmarks;
pub mod user_ratings;
pub mod user_wallets;
pub mod users;
pub mod voice_clone_requests;
pub mod voice_conversion;
@@ -0,0 +1,103 @@
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use enums::common::payments_namespace::PaymentsNamespace;
use log::{error, warn};
use mysql_queries::queries::wallets::create_new_artcraft_wallet_for_owner_user::create_new_artcraft_wallet_for_owner_user;
use mysql_queries::queries::wallets::find_primary_wallet_token_for_owner::find_primary_wallet_token_for_owner_using_connection;
use serde::{Deserialize, Serialize};
use sqlx::Acquire;
use tokens::tokens::wallets::WalletToken;
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GetOrCreateSessionArtcraftWalletResponse {
pub success: bool,
pub wallet_token: WalletToken,
}
/// NB: This endpoint was created primarily for local testing with new dev accounts
/// It should be harmless to have in production as wallet creation is idempotent
/// and (should be) side effect free.
#[utoipa::path(
get,
tag = "Wallets",
path = "/v1/wallets/session_artcraft_wallet",
responses(
(status = 200, description = "Success", body = GetOrCreateSessionArtcraftWalletResponse),
),
)]
pub async fn get_or_create_session_artcraft_wallet_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<GetOrCreateSessionArtcraftWalletResponse>, CommonWebError> {
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
error!("Error acquiring MySQL connection: {:?}", err);
CommonWebError::ServerError
})?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
CommonWebError::ServerError
})?;
let user_token = match maybe_user_session {
Some(session) => session.user_token,
None => return Err(CommonWebError::NotAuthorized),
};
let maybe_wallet_token = find_primary_wallet_token_for_owner_using_connection(
&user_token,
PaymentsNamespace::Artcraft,
&mut mysql_connection,
).await.map_err(|err| {
error!("Error finding primary artcraft wallet for user {:?}: {:?}", user_token, err);
CommonWebError::ServerError
})?;
if let Some(wallet_token) = maybe_wallet_token {
return Ok(Json(GetOrCreateSessionArtcraftWalletResponse {
success: true,
wallet_token,
}));
}
let mut transaction = mysql_connection
.begin()
.await
.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
let wallet_token = create_new_artcraft_wallet_for_owner_user(
&user_token,
&mut transaction,
).await.map_err(|err| {
error!("Error creating artcraft wallet for user {:?}: {:?}", user_token, err);
CommonWebError::ServerError
})?;
transaction
.commit()
.await
.map_err(|err| {
error!("Error committing MySQL transaction: {:?}", err);
CommonWebError::ServerError
})?;
Ok(Json(GetOrCreateSessionArtcraftWalletResponse {
success: true,
wallet_token,
}))
}
@@ -0,0 +1 @@
pub mod get_or_create_session_artcraft_wallet_handler;
@@ -13,6 +13,7 @@ use crate::http_server::routes::application_routes::tag_routes::add_tag_routes;
use crate::http_server::routes::application_routes::user_bookmarks_routes::add_user_bookmarks_routes;
use crate::http_server::routes::application_routes::user_rating_routes::add_user_rating_routes;
use crate::http_server::routes::application_routes::user_routes::add_user_routes;
use crate::http_server::routes::application_routes::wallet_routes::add_wallet_routes;
use crate::http_server::routes::application_routes::webhook_routes::add_webhook_routes;
use crate::http_server::routes::application_routes::weights_routes::add_weights_routes;
use actix_http::body::MessageBody;
@@ -62,6 +63,7 @@ where
app = add_user_routes(app); // /create_account, /session, /login, /logout, etc.
// Artcraft Billing pieces
app = add_wallet_routes(app); // /v1/wallets/...
app = add_credits_routes(app); // /v1/credits/...
app = add_stripe_artcraft_routes(app); // /v1/stripe_artcraft/...
app = add_subscription_routes(app); // /v1/subscriptions/...
@@ -29,6 +29,7 @@ use crate::http_server::endpoints::generate::video::generate_veo_3_fast_image_to
use crate::http_server::endpoints::generate::video::generate_veo_3_image_to_video_handler::generate_veo_3_image_to_video_handler;
use crate::http_server::endpoints::generate::video::multi_function::kling_2p5_turbo_pro_multi_function_video_gen_handler::kling_2p5_turbo_pro_multi_function_video_gen_handler;
use crate::http_server::endpoints::generate::video::multi_function::kling_2p6_pro_multi_function_video_gen_handler::kling_2p6_pro_multi_function_video_gen_handler;
use crate::http_server::endpoints::generate::video::multi_function::seedance_2p0_multi_function_video_gen_handler::seedance_2p0_multi_function_video_gen_handler;
use crate::http_server::endpoints::generate::video::multi_function::sora_2_multi_function_video_gen_handler::sora_2_multi_function_video_gen_handler;
use crate::http_server::endpoints::generate::video::multi_function::sora_2_pro_multi_function_video_gen_handler::sora_2_pro_multi_function_video_gen_handler;
use crate::http_server::endpoints::generate::video::multi_function::veo_3p1_fast_multi_function_video_gen_handler::veo_3p1_fast_multi_function_video_gen_handler;
@@ -140,6 +141,10 @@ where
.route(web::post().to(kling_2p6_pro_multi_function_video_gen_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/seedance_2p0")
.route(web::post().to(seedance_2p0_multi_function_video_gen_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/sora_2")
.route(web::post().to(sora_2_multi_function_video_gen_handler))
.route(web::head().to(|| HttpResponse::Ok()))
@@ -1,3 +1,4 @@
mod analytics_routes;
mod comments_routes;
mod credits_routes;
mod featured_item_routes;
@@ -5,14 +6,14 @@ mod generate_routes;
mod job_routes;
mod media_files_routes;
mod moderation_routes;
mod prompts_routes;
mod stripe_artcraft_routes;
mod subscription_routes;
mod tag_routes;
mod user_bookmarks_routes;
mod user_rating_routes;
mod user_routes;
mod wallet_routes;
mod webhook_routes;
mod weights_routes;
pub (super) mod add_application_routes;
mod prompts_routes;
mod analytics_routes;
pub (super) mod add_application_routes;
@@ -0,0 +1,24 @@
use crate::http_server::endpoints::user_wallets::get_or_create_session_artcraft_wallet_handler::get_or_create_session_artcraft_wallet_handler;
use actix_http::body::MessageBody;
use actix_service::ServiceFactory;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{web, App, Error, HttpResponse};
pub fn add_wallet_routes<T, B>(app: App<T>) -> App<T>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
{
app.service(web::scope("/v1/wallets")
.service(web::resource("/session_artcraft_wallet")
.route(web::get().to(get_or_create_session_artcraft_wallet_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
}
@@ -80,7 +80,7 @@ use crate::http_server::web_utils::handle_multipart_error::handle_multipart_erro
use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator;
use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache;
use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ResendData, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans};
use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ResendData, Seedance2ProData, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans};
use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus;
use crate::threads::db_health_checker_thread::db_health_checker_thread::db_health_checker_thread;
use crate::threads::poll_ip_banlist_thread::poll_ip_bans;
@@ -400,6 +400,8 @@ async fn main() -> AnyhowResult<()> {
let fal_api_key = FalApiKey::new(easyenv::get_env_string_required("FAL_API_KEY")?);
let fal_webhook_url = easyenv::get_env_string_required("FAL_WEBHOOK_URL")?;
let seedance2pro_cookies = easyenv::get_env_string_required("SEEDANCE2PRO_COOKIES")?;
let openai_api_key= easyenv::get_env_string_required("OPENAI_API_KEY")?;
let resend_api_key = easyenv::get_env_string_required("RESEND_API_KEY")?;
@@ -458,7 +460,10 @@ async fn main() -> AnyhowResult<()> {
fal: FalData {
api_key: fal_api_key,
webhook_url: fal_webhook_url,
},
},
seedance2pro: Seedance2ProData {
cookies: seedance2pro_cookies,
},
openai: OpenAiData {
api_key: openai_api_key,
},
@@ -92,7 +92,9 @@ pub struct ServerState {
pub auto_gc_bucket_client: BucketClient,
pub fal: FalData,
pub seedance2pro: Seedance2ProData,
pub openai: OpenAiData,
pub resend: ResendData,
@@ -311,6 +313,13 @@ pub struct FalData {
pub webhook_url: String,
}
/// Seedance 2 Pro integration
#[derive(Clone)]
pub struct Seedance2ProData {
/// Cookie string for seedance2-pro.com
pub cookies: String,
}
/// OpenAI integration
#[derive(Clone)]
pub struct OpenAiData {