Compare commits

...
20 changed files with 310 additions and 102 deletions
@@ -26,8 +26,8 @@ use crate::queries::generic_inference::api_providers::common::insert_generic_inf
pub struct InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c, E>
where E: 'e + Executor<'c, Database = MySql>
{
/// Which kinovi queue to use: standard or alternate
pub use_alternate_kinovi: bool,
/// Which kinovi queue to use
pub kinovi_version: KinoviVersion,
pub uuid_idempotency_token: &'e str,
@@ -57,23 +57,39 @@ pub struct InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c,
pub phantom: PhantomData<&'c E>,
}
#[derive(Debug, Clone, Copy)]
pub enum KinoviVersion {
Volcengine,
BytePlus,
BytePlusUltra,
}
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 (job_type, external_third_party, product_category) = if args.use_alternate_kinovi {
(
InferenceJobType::Seedance2ProAltQueue,
InferenceJobExternalThirdParty::Seedance2ProAlt,
InferenceJobProductCategory::Seedance2ProVideoAlt,
)
} else {
(
let (
job_type,
external_third_party,
product_category
) = match args.kinovi_version {
KinoviVersion::Volcengine => (
InferenceJobType::Seedance2ProQueue,
InferenceJobExternalThirdParty::Seedance2Pro,
InferenceJobProductCategory::Seedance2ProVideo,
)
),
KinoviVersion::BytePlus => (
InferenceJobType::Seedance2ProAltQueue,
InferenceJobExternalThirdParty::Seedance2ProAlt,
InferenceJobProductCategory::Seedance2ProVideoAlt,
),
KinoviVersion::BytePlusUltra => (
InferenceJobType::Seedance2ProBytePlusUltraQueue,
InferenceJobExternalThirdParty::Seedance2ProBytePlusUltra,
InferenceJobProductCategory::Seedance2ProVideoBytePlusUltra,
),
};
let record_id = insert_generic_inference_job_for_provider(InsertGenericInferenceJobForProviderArgs {
@@ -37,6 +37,10 @@ pub enum InferenceJobExternalThirdParty {
#[serde(rename = "seedance2pro_alt")]
Seedance2ProAlt,
/// Seedance 2 Pro BytePlus Ultra jobs
#[serde(rename = "seedance2pro_bpu")]
Seedance2ProBytePlusUltra,
/// World Labs jobs
#[serde(rename = "worldlabs")]
Worldlabs,
@@ -56,6 +60,7 @@ impl InferenceJobExternalThirdParty {
Self::GrokApi => "grok_api",
Self::Seedance2Pro => "seedance2pro",
Self::Seedance2ProAlt => "seedance2pro_alt",
Self::Seedance2ProBytePlusUltra => "seedance2pro_bpu",
Self::Worldlabs => "worldlabs",
}
}
@@ -68,6 +73,7 @@ impl InferenceJobExternalThirdParty {
"grok_api" => Ok(Self::GrokApi),
"seedance2pro" => Ok(Self::Seedance2Pro),
"seedance2pro_alt" => Ok(Self::Seedance2ProAlt),
"seedance2pro_bpu" => Ok(Self::Seedance2ProBytePlusUltra),
"worldlabs" => Ok(Self::Worldlabs),
_ => Err(format!("invalid value: {:?}", value)),
}
@@ -83,6 +89,7 @@ impl InferenceJobExternalThirdParty {
Self::GrokApi,
Self::Seedance2Pro,
Self::Seedance2ProAlt,
Self::Seedance2ProBytePlusUltra,
Self::Worldlabs,
])
}
@@ -104,6 +111,7 @@ mod tests {
assert_serialization(InferenceJobExternalThirdParty::GrokApi, "grok_api");
assert_serialization(InferenceJobExternalThirdParty::Seedance2Pro, "seedance2pro");
assert_serialization(InferenceJobExternalThirdParty::Seedance2ProAlt, "seedance2pro_alt");
assert_serialization(InferenceJobExternalThirdParty::Seedance2ProBytePlusUltra, "seedance2pro_bpu");
assert_serialization(InferenceJobExternalThirdParty::Worldlabs, "worldlabs");
}
@@ -115,6 +123,7 @@ mod tests {
assert_eq!(InferenceJobExternalThirdParty::GrokApi.to_str(), "grok_api");
assert_eq!(InferenceJobExternalThirdParty::Seedance2Pro.to_str(), "seedance2pro");
assert_eq!(InferenceJobExternalThirdParty::Seedance2ProAlt.to_str(), "seedance2pro_alt");
assert_eq!(InferenceJobExternalThirdParty::Seedance2ProBytePlusUltra.to_str(), "seedance2pro_bpu");
assert_eq!(InferenceJobExternalThirdParty::Worldlabs.to_str(), "worldlabs");
}
@@ -126,13 +135,14 @@ mod tests {
assert_eq!(InferenceJobExternalThirdParty::from_str("grok_api").unwrap(), InferenceJobExternalThirdParty::GrokApi);
assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro").unwrap(), InferenceJobExternalThirdParty::Seedance2Pro);
assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro_alt").unwrap(), InferenceJobExternalThirdParty::Seedance2ProAlt);
assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro_bpu").unwrap(), InferenceJobExternalThirdParty::Seedance2ProBytePlusUltra);
assert_eq!(InferenceJobExternalThirdParty::from_str("worldlabs").unwrap(), InferenceJobExternalThirdParty::Worldlabs);
}
#[test]
fn all_variants() {
// Static check
const EXPECTED_COUNT : usize = 7;
const EXPECTED_COUNT : usize = 8;
assert_eq!(InferenceJobExternalThirdParty::all_variants().len(), EXPECTED_COUNT);
assert_eq!(InferenceJobExternalThirdParty::iter().len(), EXPECTED_COUNT);
@@ -58,6 +58,11 @@ pub enum InferenceJobProductCategory {
#[serde(rename = "seedance2pro_video_alt")]
Seedance2ProVideoAlt,
// =============== SEEDANCE 2 PRO BYTEPLUS ULTRA ===============
#[serde(rename = "seedance2pro_video_bpu")]
Seedance2ProVideoBytePlusUltra,
// =============== WORLD LABS ===============
/// World Labs: Gaussian Splat Generation
@@ -165,6 +170,7 @@ impl InferenceJobProductCategory {
Self::Seedance2ProVideo => "seedance2pro_video",
Self::Seedance2ProCharacter => "seedance2pro_character",
Self::Seedance2ProVideoAlt => "seedance2pro_video_alt",
Self::Seedance2ProVideoBytePlusUltra => "seedance2pro_video_bpu",
Self::WorldlabsSplat => "worldlabs_splat",
Self::TtsGptSoVits => "tts_gpt_so_vits",
Self::TtsStyleTts2 => "tts_style_tts2",
@@ -204,6 +210,7 @@ impl InferenceJobProductCategory {
"seedance2pro_video" => Ok(Self::Seedance2ProVideo),
"seedance2pro_character" => Ok(Self::Seedance2ProCharacter),
"seedance2pro_video_alt" => Ok(Self::Seedance2ProVideoAlt),
"seedance2pro_video_bpu" => Ok(Self::Seedance2ProVideoBytePlusUltra),
"worldlabs_splat" => Ok(Self::WorldlabsSplat),
"tts_gpt_so_vits" => Ok(Self::TtsGptSoVits),
"tts_style_tts2" => Ok(Self::TtsStyleTts2),
@@ -246,6 +253,7 @@ impl InferenceJobProductCategory {
Self::Seedance2ProVideo,
Self::Seedance2ProCharacter,
Self::Seedance2ProVideoAlt,
Self::Seedance2ProVideoBytePlusUltra,
Self::WorldlabsSplat,
Self::TtsGptSoVits,
Self::TtsStyleTts2,
@@ -294,6 +302,7 @@ mod tests {
assert_serialization(InferenceJobProductCategory::Seedance2ProVideo, "seedance2pro_video");
assert_serialization(InferenceJobProductCategory::Seedance2ProCharacter, "seedance2pro_character");
assert_serialization(InferenceJobProductCategory::Seedance2ProVideoAlt, "seedance2pro_video_alt");
assert_serialization(InferenceJobProductCategory::Seedance2ProVideoBytePlusUltra, "seedance2pro_video_bpu");
assert_serialization(InferenceJobProductCategory::WorldlabsSplat, "worldlabs_splat");
assert_serialization(InferenceJobProductCategory::TtsGptSoVits, "tts_gpt_so_vits");
assert_serialization(InferenceJobProductCategory::TtsStyleTts2, "tts_style_tts2");
@@ -332,6 +341,7 @@ mod tests {
assert_eq!(InferenceJobProductCategory::Seedance2ProVideo.to_str(), "seedance2pro_video");
assert_eq!(InferenceJobProductCategory::Seedance2ProCharacter.to_str(), "seedance2pro_character");
assert_eq!(InferenceJobProductCategory::Seedance2ProVideoAlt.to_str(), "seedance2pro_video_alt");
assert_eq!(InferenceJobProductCategory::Seedance2ProVideoBytePlusUltra.to_str(), "seedance2pro_video_bpu");
assert_eq!(InferenceJobProductCategory::WorldlabsSplat.to_str(), "worldlabs_splat");
assert_eq!(InferenceJobProductCategory::TtsGptSoVits.to_str(), "tts_gpt_so_vits");
assert_eq!(InferenceJobProductCategory::TtsStyleTts2.to_str(), "tts_style_tts2");
@@ -369,6 +379,7 @@ mod tests {
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video").unwrap(), InferenceJobProductCategory::Seedance2ProVideo);
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_character").unwrap(), InferenceJobProductCategory::Seedance2ProCharacter);
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video_alt").unwrap(), InferenceJobProductCategory::Seedance2ProVideoAlt);
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video_bpu").unwrap(), InferenceJobProductCategory::Seedance2ProVideoBytePlusUltra);
assert_eq!(InferenceJobProductCategory::from_str("worldlabs_splat").unwrap(), InferenceJobProductCategory::WorldlabsSplat);
assert_eq!(InferenceJobProductCategory::from_str("tts_gpt_so_vits").unwrap(), InferenceJobProductCategory::TtsGptSoVits);
assert_eq!(InferenceJobProductCategory::from_str("tts_style_tts2").unwrap(), InferenceJobProductCategory::TtsStyleTts2);
@@ -396,7 +407,7 @@ mod tests {
#[test]
fn all_variants() {
// Static check
const EXPECTED_COUNT : usize = 34;
const EXPECTED_COUNT : usize = 35;
assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT);
assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT);
@@ -39,6 +39,10 @@ pub enum InferenceJobType {
#[serde(rename = "seedance2pro_alt_queue")]
Seedance2ProAltQueue,
/// Seedance 2 Pro BytePlus Ultra jobs. We poll for results.
#[serde(rename = "seedance2pro_bpu_queue")]
Seedance2ProBytePlusUltraQueue,
/// Beeble jobs. We poll for results.
#[serde(rename = "beeble_queue")]
BeebleQueue,
@@ -143,6 +147,7 @@ impl InferenceJobType {
Self::BeebleQueue => "beeble_queue",
Self::Seedance2ProQueue => "seedance2pro_queue",
Self::Seedance2ProAltQueue => "seedance2pro_alt_queue",
Self::Seedance2ProBytePlusUltraQueue => "seedance2pro_bpu_queue",
Self::Seedance2ProCharacter => "seedance2pro_character",
Self::WorldlabsQueue => "worldlabs_queue",
Self::VideoRender => "video_render",
@@ -177,6 +182,7 @@ impl InferenceJobType {
"seedance2pro_queue" => Ok(Self::Seedance2ProQueue),
"seedance2pro_character" => Ok(Self::Seedance2ProCharacter),
"seedance2pro_alt_queue" => Ok(Self::Seedance2ProAltQueue),
"seedance2pro_bpu_queue" => Ok(Self::Seedance2ProBytePlusUltraQueue),
"worldlabs_queue" => Ok(Self::WorldlabsQueue),
"video_render" => Ok(Self::VideoRender),
"live_portrait" => Ok(Self::LivePortrait),
@@ -213,6 +219,7 @@ impl InferenceJobType {
Self::Seedance2ProQueue,
Self::Seedance2ProCharacter,
Self::Seedance2ProAltQueue,
Self::Seedance2ProBytePlusUltraQueue,
Self::WorldlabsQueue,
Self::VideoRender,
Self::LivePortrait,
@@ -260,6 +267,7 @@ mod tests {
assert_serialization(InferenceJobType::Seedance2ProQueue, "seedance2pro_queue");
assert_serialization(InferenceJobType::Seedance2ProCharacter, "seedance2pro_character");
assert_serialization(InferenceJobType::Seedance2ProAltQueue, "seedance2pro_alt_queue");
assert_serialization(InferenceJobType::Seedance2ProBytePlusUltraQueue, "seedance2pro_bpu_queue");
assert_serialization(InferenceJobType::WorldlabsQueue, "worldlabs_queue");
assert_serialization(InferenceJobType::VideoRender, "video_render");
assert_serialization(InferenceJobType::LivePortrait, "live_portrait");
@@ -292,6 +300,7 @@ mod tests {
assert_eq!(InferenceJobType::Seedance2ProQueue.to_str(), "seedance2pro_queue");
assert_eq!(InferenceJobType::Seedance2ProCharacter.to_str(), "seedance2pro_character");
assert_eq!(InferenceJobType::Seedance2ProAltQueue.to_str(), "seedance2pro_alt_queue");
assert_eq!(InferenceJobType::Seedance2ProBytePlusUltraQueue.to_str(), "seedance2pro_bpu_queue");
assert_eq!(InferenceJobType::WorldlabsQueue.to_str(), "worldlabs_queue");
assert_eq!(InferenceJobType::VideoRender.to_str(), "video_render");
assert_eq!(InferenceJobType::LivePortrait.to_str(), "live_portrait");
@@ -324,6 +333,7 @@ mod tests {
assert_eq!(InferenceJobType::from_str("seedance2pro_queue").unwrap(), InferenceJobType::Seedance2ProQueue);
assert_eq!(InferenceJobType::from_str("seedance2pro_character").unwrap(), InferenceJobType::Seedance2ProCharacter);
assert_eq!(InferenceJobType::from_str("seedance2pro_alt_queue").unwrap(), InferenceJobType::Seedance2ProAltQueue);
assert_eq!(InferenceJobType::from_str("seedance2pro_bpu_queue").unwrap(), InferenceJobType::Seedance2ProBytePlusUltraQueue);
assert_eq!(InferenceJobType::from_str("worldlabs_queue").unwrap(), InferenceJobType::WorldlabsQueue);
assert_eq!(InferenceJobType::from_str("video_render").unwrap(), InferenceJobType::VideoRender);
assert_eq!(InferenceJobType::from_str("live_portrait").unwrap(), InferenceJobType::LivePortrait);
@@ -350,7 +360,7 @@ mod tests {
#[test]
fn all_variants() {
// Static check
const EXPECTED_COUNT : usize = 28;
const EXPECTED_COUNT : usize = 29;
assert_eq!(InferenceJobType::all_variants().len(), EXPECTED_COUNT);
assert_eq!(InferenceJobType::iter().len(), EXPECTED_COUNT);
@@ -1,5 +1,6 @@
use std::sync::Arc;
use crate::kinovi_version::KinoviVersion;
use chrono::Duration;
use cloud_storage::bucket_client::BucketClient;
use concurrency::relaxed_atomic_bool::RelaxedAtomicBool;
@@ -20,9 +21,8 @@ pub struct JobDependencies {
/// Session credentials for polling seedance2-pro.com.
pub seedance2pro_session: Seedance2ProSession,
/// When true, this instance polls for alternate-account jobs
/// (Seedance2ProAltQueue / Seedance2ProAlt) instead of the primary ones.
pub is_alternate_mode: bool,
/// The kinovi account/version we poll
pub kinovi_version: KinoviVersion,
pub server_environment: ServerEnvironment,
@@ -5,14 +5,15 @@ use chrono::Utc;
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use log::{error, info, warn};
use mysql_queries::queries::generic_inference::api_providers::seedance2pro::list_pending_seedance2pro_video_jobs::{list_pending_seedance2pro_video_jobs, PendingSeedance2ProJob};
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
use pager::notification::notification_urgency::NotificationUrgency;
use mysql_queries::queries::generic_inference::api_providers::seedance2pro::list_pending_seedance2pro_video_jobs::{list_pending_seedance2pro_video_jobs, PendingSeedance2ProJob};
use seedance2pro_client::requests::poll_orders::poll_orders::{poll_orders, OrderStatus, PollOrdersArgs, PollOrdersResponse, TaskStatus};
use crate::job_dependencies::JobDependencies;
use crate::jobs::video_polling_job::alert_on_error::alert_pager_and_return_err;
use crate::jobs::video_polling_job::process_orders_batch::process_orders_batch;
use crate::job_dependencies::JobDependencies;
use crate::kinovi_version::KinoviVersion;
const POLL_ALERT_THRESHOLD: Duration = Duration::from_mins(6);
@@ -61,14 +62,28 @@ pub async fn video_polling_main_loop(job_dependencies: JobDependencies) {
}
async fn run_poll_iteration(deps: &JobDependencies) -> anyhow::Result<()> {
info!("Querying database jobs for type: {:?}", deps.kinovi_version);
let (
job_type,
third_party,
) = match deps.kinovi_version {
KinoviVersion::Volcengine => (
InferenceJobType::Seedance2ProQueue,
InferenceJobExternalThirdParty::Seedance2Pro,
),
KinoviVersion::BytePlus => (
InferenceJobType::Seedance2ProAltQueue,
InferenceJobExternalThirdParty::Seedance2ProAlt,
),
KinoviVersion::BytePlusUltra => (
InferenceJobType::Seedance2ProBytePlusUltraQueue,
InferenceJobExternalThirdParty::Seedance2ProBytePlusUltra,
),
};
// 1. Query all (limit 25,000) non-terminal Seedance2Pro jobs from DB.
// This is all non-(complete_success, complete_failure) jobs.
let (third_party, job_type) = if deps.is_alternate_mode {
(InferenceJobExternalThirdParty::Seedance2ProAlt, InferenceJobType::Seedance2ProAltQueue)
} else {
(InferenceJobExternalThirdParty::Seedance2Pro, InferenceJobType::Seedance2ProQueue)
};
let pending_jobs = match list_pending_seedance2pro_video_jobs(&deps.mysql_pool, third_party, job_type).await {
Ok(jobs) => jobs,
Err(err) => {
@@ -0,0 +1,17 @@
#[derive(Debug, Clone, Copy)]
pub enum KinoviVersion {
Volcengine,
BytePlus,
BytePlusUltra,
}
impl KinoviVersion {
pub fn has_characters(&self) -> bool {
match self {
KinoviVersion::Volcengine => true,
KinoviVersion::BytePlus => false,
KinoviVersion::BytePlusUltra => false,
}
}
}
@@ -33,10 +33,12 @@ use crate::jobs::credits_checking_job::credits_checking_main_loop::credits_check
use crate::jobs::video_polling_job::video_polling_main_loop::video_polling_main_loop;
use crate::job_dependencies::JobDependencies;
use crate::startup::build_pager::build_pager;
use crate::startup::kinovi_setup::{get_kinovi_session, get_kinovi_version};
pub mod http_server;
pub mod job_dependencies;
pub mod jobs;
pub mod kinovi_version;
pub mod startup;
// Bucket config
@@ -46,10 +48,6 @@ const ENV_REGION_NAME: &str = "REGION_NAME";
const ENV_PUBLIC_BUCKET_NAME: &str = "PUBLIC_BUCKET_NAME";
const ENV_S3_ENDPOINT: &str = "S3_COMPATIBLE_ENDPOINT_URL";
const ENV_SEEDANCE2PRO_ALTERNATE: &str = "SEEDANCE2PRO_ALTERNATE";
const ENV_SEEDANCE2PRO_COOKIES: &str = "SEEDANCE2PRO_COOKIES";
const ENV_SEEDANCE2PRO_ALT_COOKIES: &str = "SEEDANCE2PRO_ALT_COOKIES";
const ENV_MAX_JOB_AGE_THRESHOLD_HOURS: &str = "MAX_JOB_AGE_THRESHOLD_HOURS";
#[tokio::main]
@@ -105,21 +103,8 @@ async fn main() -> AnyhowResult<()> {
Some(bucket_timeout),
)?;
// Alternate mode: uses a different Kinovi account, different job types, skips character polling.
let is_alternate_mode = easyenv::get_env_string_optional(ENV_SEEDANCE2PRO_ALTERNATE)
.map(|v| v.trim().to_lowercase())
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
if is_alternate_mode {
info!("Running in ALTERNATE mode (SEEDANCE2PRO_ALTERNATE=true).");
}
// Seedance2Pro session from cookie string
let cookie_env_var = if is_alternate_mode { ENV_SEEDANCE2PRO_ALT_COOKIES } else { ENV_SEEDANCE2PRO_COOKIES };
info!("Reading Seedance2Pro cookies from environment variable: {}", cookie_env_var);
let seedance2pro_cookies = easyenv::get_env_string_required(cookie_env_var)?;
let seedance2pro_session = Seedance2ProSession::from_cookies_string(seedance2pro_cookies);
let kinovi_version = get_kinovi_version()?;
let kinovi_session = get_kinovi_session(kinovi_version)?;
// How often to poll for results (default: 15 seconds)
let poll_interval_millis: u64 = easyenv::get_env_num(
@@ -186,8 +171,8 @@ async fn main() -> AnyhowResult<()> {
let job_dependencies = JobDependencies {
mysql_pool,
public_bucket_client,
seedance2pro_session,
is_alternate_mode,
seedance2pro_session: kinovi_session,
kinovi_version,
server_environment,
job_stats,
poll_interval_millis,
@@ -241,16 +226,16 @@ async fn main() -> AnyhowResult<()> {
credits_checking_main_loop(credits_deps).await;
});
if is_alternate_mode {
// Alternate mode: skip character polling entirely.
info!("Alternate mode: character polling is disabled.");
let _ = tokio::join!(video_handle, credits_handle);
} else {
if kinovi_version.has_characters() {
let character_deps = job_dependencies;
let character_handle = tokio::spawn(async move {
character_polling_main_loop(character_deps).await;
});
let _ = tokio::join!(video_handle, character_handle, credits_handle);
} else {
// skip character polling entirely.
info!("Alternate mode: character polling is disabled.");
let _ = tokio::join!(video_handle, credits_handle);
}
info!("Shutting down pager worker...");
@@ -260,3 +245,4 @@ async fn main() -> AnyhowResult<()> {
Ok(())
}
@@ -0,0 +1,123 @@
use crate::kinovi_version::KinoviVersion;
use anyhow::anyhow;
use log::info;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
// Configuration switch
const ENV_SEEDANCE2PRO_VERSION: &str = "SEEDANCE2PRO_VERSION";
// Cookies for Volcengine
#[deprecated]
const ENV_SEEDANCE2PRO_LEGACY_COOKIES: &str = "SEEDANCE2PRO_COOKIES";
const ENV_SEEDANCE2PRO_VOLCENGINE_COOKIES: &str = "SEEDANCE2PRO_VOLCENGINE_COOKIES";
// Cookies for BytePlus
#[deprecated]
const ENV_SEEDANCE2PRO_LEGACY_ALT_COOKIES: &str = "SEEDANCE2PRO_ALT_COOKIES";
const ENV_SEEDANCE2PRO_BYTEPLUS_COOKIES: &str = "SEEDANCE2PRO_BYTEPLUS_COOKIES";
// Cookies for BytePlus Ultra
const ENV_SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES: &str = "SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES";
pub fn get_kinovi_version() -> anyhow::Result<KinoviVersion> {
info!("Reading kinovi version from first CLI arg (optional - typical production config is via env vars):");
if let Some(arg) = std::env::args().nth(1) {
return parse_kinovi_version(&arg);
}
info!("Reading kinovi version from env var: {}", ENV_SEEDANCE2PRO_VERSION);
if let Some(version) = easyenv::get_env_string_optional(ENV_SEEDANCE2PRO_VERSION) {
return parse_kinovi_version(&version);
}
Err(anyhow!(
"kinovi version not specified: set env var {} or pass volcengine|byteplus|byteplusultra as the first CLI arg",
ENV_SEEDANCE2PRO_VERSION,
))
}
fn parse_kinovi_version(value: &str) -> anyhow::Result<KinoviVersion> {
match value.trim().to_lowercase().as_str() {
"volcengine" => Ok(KinoviVersion::Volcengine),
"byteplus" => Ok(KinoviVersion::BytePlus),
"byteplusultra" => Ok(KinoviVersion::BytePlusUltra),
other => Err(anyhow!(
"invalid kinovi version {:?} (expected volcengine|byteplus|byteplusultra)",
other,
)),
}
}
pub fn get_kinovi_session(version: KinoviVersion) -> anyhow::Result<Seedance2ProSession> {
let cookies = read_kinovi_cookies(version)?;
Ok(Seedance2ProSession::from_cookies_string(cookies))
}
fn read_kinovi_cookies(version: KinoviVersion) -> anyhow::Result<String> {
match version {
KinoviVersion::Volcengine => {
info!("Using Volcengine cookies from env var: {}", ENV_SEEDANCE2PRO_VOLCENGINE_COOKIES);
easyenv::get_env_string_optional(ENV_SEEDANCE2PRO_VOLCENGINE_COOKIES)
.or_else(|| easyenv::get_env_string_optional(ENV_SEEDANCE2PRO_LEGACY_COOKIES))
.ok_or_else(|| anyhow!("missing Seedance2Pro cookies in in env var {}", ENV_SEEDANCE2PRO_VOLCENGINE_COOKIES))
}
KinoviVersion::BytePlus => {
info!("Using BytePlus cookies from env var: {}", ENV_SEEDANCE2PRO_BYTEPLUS_COOKIES);
easyenv::get_env_string_optional(ENV_SEEDANCE2PRO_BYTEPLUS_COOKIES)
.or_else(|| easyenv::get_env_string_optional(ENV_SEEDANCE2PRO_LEGACY_ALT_COOKIES))
.ok_or_else(|| anyhow!("missing Seedance2Pro cookies in in env var {}", ENV_SEEDANCE2PRO_BYTEPLUS_COOKIES))
}
KinoviVersion::BytePlusUltra => {
info!("Using BytePlus Ultra cookies from env var: {}", ENV_SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES);
let cookies = easyenv::get_env_string_required(ENV_SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES)?;
Ok(cookies)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_volcengine() {
assert!(matches!(parse_kinovi_version("volcengine"), Ok(KinoviVersion::Volcengine)));
}
#[test]
fn parses_byteplus() {
assert!(matches!(parse_kinovi_version("byteplus"), Ok(KinoviVersion::BytePlus)));
}
#[test]
fn parses_byteplusultra() {
assert!(matches!(parse_kinovi_version("byteplusultra"), Ok(KinoviVersion::BytePlusUltra)));
}
#[test]
fn parse_is_case_insensitive() {
assert!(matches!(parse_kinovi_version("Volcengine"), Ok(KinoviVersion::Volcengine)));
assert!(matches!(parse_kinovi_version("BYTEPLUS"), Ok(KinoviVersion::BytePlus)));
assert!(matches!(parse_kinovi_version("BytePlusUltra"), Ok(KinoviVersion::BytePlusUltra)));
}
#[test]
fn parse_trims_whitespace() {
assert!(matches!(parse_kinovi_version(" volcengine\n"), Ok(KinoviVersion::Volcengine)));
}
#[test]
fn unknown_value_errors() {
let err = parse_kinovi_version("nope").expect_err("nope should not parse");
assert!(err.to_string().contains("nope"));
}
#[test]
fn empty_value_errors() {
assert!(parse_kinovi_version("").is_err());
assert!(parse_kinovi_version(" ").is_err());
}
}
@@ -1 +1,2 @@
pub mod build_pager;
pub mod kinovi_setup;
@@ -123,7 +123,7 @@ pub async fn create_character_handler(
// --- Upload image to Kinovi CDN ---
let seedance_image_url = upload_to_kinovi(
&server_state.inference_providers.seedance2pro.cookies,
&server_state.inference_providers.seedance2pro.cookies_volcengine,
image_cdn_url,
).await?;
@@ -132,7 +132,7 @@ pub async fn create_character_handler(
// --- Call Kinovi generate_character ---
let session = Seedance2ProSession::from_cookies_string(
server_state.inference_providers.seedance2pro.cookies.clone()
server_state.inference_providers.seedance2pro.cookies_volcengine.clone()
);
let gen_result = generate_character(GenerateCharacterArgs {
@@ -101,7 +101,7 @@ pub async fn edit_character_handler(
if has_name_change {
if let Some(ref kinovi_id) = character.kinovi_character_id {
let session = Seedance2ProSession::from_cookies_string(
server_state.inference_providers.seedance2pro.cookies.clone()
server_state.inference_providers.seedance2pro.cookies_volcengine.clone()
);
update_character(UpdateCharacterArgs {
@@ -26,10 +26,7 @@ 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::characters::batch_lookup_characters_by_token_for_prompting::batch_lookup_characters_by_token_for_prompting;
use mysql_queries::queries::generic_inference::api_providers::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::generic_inference::api_providers::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::{insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token, InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs, KinoviVersion};
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,
@@ -215,7 +212,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
warn!("Whitelist session failed for user {:?}: {:?}, falling back to regular session", user_token, err);
let regular_session = Seedance2ProSession::from_cookies_string(
server_state.inference_providers.seedance2pro.cookies.clone()
server_state.inference_providers.seedance2pro.cookies_volcengine.clone()
);
let result = upload_and_generate(
@@ -253,7 +250,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
info!("User {:?} using regular seedance session", user_token);
let regular_session = Seedance2ProSession::from_cookies_string(
server_state.inference_providers.seedance2pro.cookies.clone()
server_state.inference_providers.seedance2pro.cookies_volcengine.clone()
);
let result = upload_and_generate(
@@ -412,7 +409,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
let db_result = insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token(
InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs {
use_alternate_kinovi: false,
kinovi_version: KinoviVersion::Volcengine,
apriori_job_token: &job_token,
uuid_idempotency_token: &idempotency_str,
maybe_external_third_party_id: order_id,
@@ -9,16 +9,17 @@ use artcraft_router::client::router_seedance2pro_client::RouterSeedance2ProClien
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::omni_gen::generate::video::kinovi_account::KinoviAccount;
use crate::state::server_state::ServerState;
pub fn build_router_client(
provider: RouterProvider,
server_state: &ServerState,
use_alternate_kinovi: bool,
kinovi_account: KinoviAccount,
) -> Result<RouterClient, CommonWebError> {
match provider {
RouterProvider::Seedance2Pro => {
kinovi_provider(server_state, use_alternate_kinovi)
kinovi_provider(server_state, kinovi_account)
}
RouterProvider::Fal => {
let fal_client = RouterFalClient::new_with_webhook(
@@ -45,18 +46,16 @@ pub fn build_router_client(
}
}
fn kinovi_provider(server_state: &ServerState, use_alternate_kinovi: bool) -> Result<RouterClient, CommonWebError> {
let session = if use_alternate_kinovi {
// Alternate Kinovi
Seedance2ProSession::from_cookies_string(
server_state.inference_providers.seedance2pro.cookies_byteplus.clone()
)
} else {
// Standard Kinovi
Seedance2ProSession::from_cookies_string(
server_state.inference_providers.seedance2pro.cookies.clone()
)
fn kinovi_provider(server_state: &ServerState, kinovi_account: KinoviAccount) -> Result<RouterClient, CommonWebError> {
let seedance2pro = &server_state.inference_providers.seedance2pro;
let cookies = match kinovi_account {
KinoviAccount::Volcengine => seedance2pro.cookies_volcengine.clone(),
KinoviAccount::BytePlus => seedance2pro.cookies_byteplus.clone(),
KinoviAccount::BytePlusUltra => seedance2pro.cookies_byteplus_ultra.clone(),
};
let session = Seedance2ProSession::from_cookies_string(cookies);
Ok(RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session)))
}
@@ -2,10 +2,7 @@ use log::{error, warn};
use crate::http_server::common_responses::common_web_error::CommonWebError;
use enums::common::visibility::Visibility;
use mysql_queries::queries::generic_inference::api_providers::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::generic_inference::api_providers::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::{insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token, InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs, KinoviVersion};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
use uuid_utils::uuid::generate_random_uuid;
@@ -16,7 +13,7 @@ pub struct InsertSeedance2proJobsArgs<'a, 'tx> {
pub primary_order_id: &'a str,
pub maybe_additional_order_ids: Option<&'a [String]>,
pub maybe_wallet_ledger_entry_token: Option<&'a WalletLedgerEntryToken>,
pub use_alternate_kinovi: bool,
pub kinovi_version: KinoviVersion,
pub shared: SharedJobArgs<'a, 'tx>,
}
@@ -30,7 +27,7 @@ pub async fn insert_seedance2pro_jobs(args: InsertSeedance2proJobsArgs<'_, '_>)
primary_order_id,
maybe_additional_order_ids,
maybe_wallet_ledger_entry_token,
use_alternate_kinovi,
kinovi_version,
mut shared,
} = args;
@@ -57,7 +54,7 @@ pub async fn insert_seedance2pro_jobs(args: InsertSeedance2proJobsArgs<'_, '_>)
let db_result = insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token(
InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs {
use_alternate_kinovi,
kinovi_version,
apriori_job_token: &job_token,
uuid_idempotency_token: &idempotency_str,
maybe_external_third_party_id: order_id,
@@ -0,0 +1,7 @@
#[derive(Debug, Clone, Copy)]
pub enum KinoviAccount {
Volcengine,
BytePlus,
BytePlusUltra,
}
@@ -2,3 +2,4 @@ pub mod omni_gen_video_generate_handler;
pub mod helpers;
pub mod pipeline_v2;
pub mod insert_db_job;
pub mod kinovi_account;
@@ -18,6 +18,7 @@ use enums::common::generation::common_video_model::CommonVideoModel;
use enums::common::generation_provider::GenerationProvider;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use mysql_queries::queries::generic_inference::api_providers::seedance2pro::insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token::KinoviVersion;
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,
@@ -38,6 +39,7 @@ use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::ins
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::shared_job_args::SharedJobArgs;
use crate::http_server::endpoints::omni_gen::generate::video::pipeline_v2::run_pipeline_v2::{run_pipeline_v2, RunPipelineV2Args};
use crate::http_server::endpoints::omni_gen::generate::video::helpers::resolve_kinovi_character_ids::resolve_kinovi_character_ids;
use crate::http_server::endpoints::omni_gen::generate::video::kinovi_account::KinoviAccount;
use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::state::server_state::ServerState;
@@ -166,12 +168,17 @@ pub async fn omni_gen_video_generate_handler(
// ==================== PIPELINE DISPATCH ==================== //
let use_alternate_kinovi = match request.model {
Some(CommonVideoModel::Seedance2p0BytePlus) => true,
Some(CommonVideoModel::Seedance2p0BytePlusFast) => true,
Some(CommonVideoModel::PreviewModel) => true,
Some(CommonVideoModel::PreviewModelFast) => true,
_ => false,
let kinovi_account = match request.model {
// BytePlus Ultra
Some(CommonVideoModel::Seedance2p0BytePlusUltra) => KinoviAccount::BytePlusUltra,
Some(CommonVideoModel::Seedance2p0BytePlusUltraFast) => KinoviAccount::BytePlusUltra,
// BytePlus
Some(CommonVideoModel::Seedance2p0BytePlus) => KinoviAccount::BytePlus,
Some(CommonVideoModel::Seedance2p0BytePlusFast) => KinoviAccount::BytePlus,
Some(CommonVideoModel::PreviewModel) => KinoviAccount::BytePlus,
Some(CommonVideoModel::PreviewModelFast) => KinoviAccount::BytePlus,
// Everything else goes through Volcengine
_ => KinoviAccount::Volcengine,
};
// ==================== DEBUG LOG: HTTP REQUEST ==================== //
@@ -194,7 +201,7 @@ pub async fn omni_gen_video_generate_handler(
user_token,
media_file_to_url_map: &media_file_to_url_map,
kinovi_character_id_map: &kinovi_character_id_map,
use_alternate_kinovi,
kinovi_account,
}).await?;
// ==================== DEBUG LOG: FAL REQUEST ==================== //
@@ -318,11 +325,18 @@ pub async fn omni_gen_video_generate_handler(
let (primary_job_token, all_job_tokens) = match &pipeline_result.response {
GenerateVideoResponse::Seedance2Pro(payload) => {
info!("Inserting seedance2pro job(s) with token: {:?}", pipeline_result.billing.apriori_job_token);
let kinovi_version = match kinovi_account {
KinoviAccount::Volcengine => KinoviVersion::Volcengine,
KinoviAccount::BytePlus => KinoviVersion::BytePlus,
KinoviAccount::BytePlusUltra => KinoviVersion::BytePlusUltra,
};
let result = insert_seedance2pro_jobs(InsertSeedance2proJobsArgs {
primary_order_id: &payload.order_id,
maybe_additional_order_ids: payload.maybe_order_ids.as_deref(),
maybe_wallet_ledger_entry_token: pipeline_result.billing.maybe_wallet_ledger_entry_token.as_ref(),
use_alternate_kinovi,
kinovi_version,
shared: SharedJobArgs {
apriori_job_token: &pipeline_result.billing.apriori_job_token,
idempotency_token: &idempotency_token,
@@ -18,6 +18,7 @@ use crate::http_server::endpoints::omni_gen::generate::video::helpers::bill_wall
use crate::http_server::endpoints::omni_gen::generate::video::helpers::build_router_client::build_router_client;
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::state::server_state::ServerState;
pub struct RunPipelineV2Args<'a> {
@@ -27,7 +28,7 @@ pub struct RunPipelineV2Args<'a> {
pub user_token: &'a UserToken,
pub media_file_to_url_map: &'a Option<HashMap<MediaFileToken, String>>,
pub kinovi_character_id_map: &'a Option<HashMap<CharacterToken, String>>,
pub use_alternate_kinovi: bool,
pub kinovi_account: KinoviAccount,
}
pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResult, CommonWebError> {
@@ -38,18 +39,20 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
user_token,
media_file_to_url_map,
kinovi_character_id_map,
use_alternate_kinovi,
kinovi_account,
} = args;
let mut router_builder = router_builder.clone();
match router_builder.model {
RouterVideoModel::PreviewModel |
RouterVideoModel::Seedance2p0BytePlus => {
RouterVideoModel::Seedance2p0BytePlus |
RouterVideoModel::Seedance2p0BytePlusUltra => {
router_builder.model = RouterVideoModel::Seedance2p0;
},
RouterVideoModel::PreviewModelFast |
RouterVideoModel::Seedance2p0BytePlusFast => {
RouterVideoModel::Seedance2p0BytePlusFast |
RouterVideoModel::Seedance2p0BytePlusUltraFast => {
router_builder.model = RouterVideoModel::Seedance2p0Fast;
},
_ => {}, // Fall-through
@@ -59,8 +62,8 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
RouterVideoModel::HappyHorse1p0 => RouterProvider::Seedance2Pro,
RouterVideoModel::Seedance2p0 => RouterProvider::Seedance2Pro,
RouterVideoModel::Seedance2p0Fast => RouterProvider::Seedance2Pro,
RouterVideoModel::Seedance2p0Ultra => RouterProvider::GmiCloud,
RouterVideoModel::Seedance2p0UltraFast => RouterProvider::GmiCloud,
//RouterVideoModel::Seedance2p0Ultra => RouterProvider::GmiCloud,
//RouterVideoModel::Seedance2p0UltraFast => RouterProvider::GmiCloud,
RouterVideoModel::GrokImagineVideo => RouterProvider::GrokApi,
_ => RouterProvider::Fal,
};
@@ -114,7 +117,7 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
server_state,
media_file_to_url_map.as_ref(),
kinovi_character_id_map.as_ref(),
use_alternate_kinovi,
kinovi_account,
).await;
// 5. On failure, refund wallet for Kinovi requests.
@@ -147,11 +150,11 @@ async fn upload_and_generate(
server_state: &ServerState,
media_file_urls_by_token: Option<&HashMap<MediaFileToken, String>>,
kinovi_character_ids: Option<&HashMap<CharacterToken, String>>,
use_alternate_kinovi: bool,
kinovi_account: KinoviAccount,
) -> Result<GenerateVideoResponse, CommonWebError> {
let provider = draft_or_request.get_provider();
let client = build_router_client(provider, server_state, use_alternate_kinovi)?;
let client = build_router_client(provider, server_state, kinovi_account)?;
let video_request = match draft_or_request {
VideoGenerationDraftOrRequest::Request(request) => request,
@@ -42,11 +42,11 @@ pub struct BeebleData {
/// Seedance 2 Pro integration
#[derive(Clone)]
pub struct Seedance2ProData {
/// Cookie string for seedance2-pro.com
pub cookies: String,
pub cookies_volcengine: String,
/// Cookie string for seedance2-pro.com (Whitelisted account)
pub cookies_byteplus: String,
pub cookies_byteplus_ultra: String,
}
/// OpenAI integration
@@ -82,8 +82,9 @@ pub fn setup_inference_providers() -> AnyhowResult<InferenceProviders> {
webhook_url: easyenv::get_env_string_required("BEEBLE_WEBHOOK_URL")?,
},
seedance2pro: Seedance2ProData {
cookies: easyenv::get_env_string_required("SEEDANCE2PRO_COOKIES")?,
cookies_volcengine: easyenv::get_env_string_required("SEEDANCE2PRO_COOKIES")?,
cookies_byteplus: easyenv::get_env_string_required("SEEDANCE2PRO_WHITELIST_COOKIES")?,
cookies_byteplus_ultra: easyenv::get_env_string_required("SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES")?,
},
openai: OpenAiData {
api_key: easyenv::get_env_string_required("OPENAI_API_KEY")?,