Compare commits

...
Author SHA1 Message Date
Hanashi 0bc1c62c65 web + API for omni gen endpoints 2026-08-03 16:20:01 -04:00
hanashi a4d9a4d199 this fixes another set of negative margins (#1834) 2026-08-02 18:22:04 -04:00
hanashi 2b8cc5ef8d margins for video references were costing us money, fixing (#1833) 2026-08-02 17:29:06 -04:00
hanashi 9122be6752 we're losing money on seedance in this configuration (need to fix) (#1832) 2026-08-02 17:02:49 -04:00
hanashi 31054141cc costs for seedance 2.5 are costing us money (#1831)
we're losing money on this model, gotta plug it (there are other holes
to fix too)
2026-08-02 15:58:08 -04:00
27 changed files with 561 additions and 230 deletions
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users\nFROM user_sessions\nINNER JOIN users\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users,\n users.maybe_feature_flags,\n api_keys.token as `api_key_token: ApiKeyToken`\nFROM api_keys\nINNER JOIN users\n ON users.token = api_keys.owner_user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE api_keys.api_key = ?\n AND api_keys.maybe_deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
@@ -72,6 +72,26 @@
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 7,
"name": "maybe_feature_flags",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 8,
"name": "api_key_token: ApiKeyToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
@@ -84,8 +104,10 @@
false,
false,
false,
true
true,
true,
false
]
},
"hash": "c77126adb36f9e2f0248720eb4d8c1cd9d14719f64a02678d8021e26addd2c18"
}
"hash": "62b3e2e058331a6819acd98c634b3bf2d10aa38d43b49ab69fb8637497beec2f"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users,\n api_keys.token as `api_key_token: ApiKeyToken`\nFROM api_keys\nINNER JOIN users\n ON users.token = api_keys.owner_user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE api_keys.api_key = ?\n AND api_keys.maybe_deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"query": "\nSELECT\n users.token as `user_token: UserToken`,\n users.username,\n users.display_name,\n users.email_address,\n users.user_role_slug,\n users.is_banned,\n user_roles.can_ban_users,\n users.maybe_feature_flags\nFROM user_sessions\nINNER JOIN users\n ON users.token = user_sessions.user_token\nLEFT OUTER JOIN user_roles\n ON users.user_role_slug = user_roles.slug\nWHERE user_sessions.token = ?\n AND user_sessions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"describe": {
"columns": [
{
@@ -75,12 +75,12 @@
},
{
"ordinal": 7,
"name": "api_key_token: ApiKeyToken",
"name": "maybe_feature_flags",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
"max_size": 1020
}
}
],
@@ -95,8 +95,8 @@
false,
false,
true,
false
true
]
},
"hash": "9188e235caa439d50628333c2f4722b9f1a8f575c192d4d36a2039709c412ac0"
}
"hash": "cf2794c87868e77dfaa45bdbe630ae276dbb8c2d041442ba465af0b7a6d61af6"
}
@@ -16,7 +16,9 @@ impl ArtcraftQwenEdit2511AnglesCostState {
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
let cost_in_usd_cents = 4u64 * self.num_images as u64;
// 4.6¢ per output image (held in hundredths of a cent, rounded up to
// whole cents across the batch).
let cost_in_usd_cents = (460u64 * self.num_images as u64).div_ceil(100);
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
@@ -59,14 +61,14 @@ mod tests {
}
#[test]
fn one_image_costs_4_cents() { assert_eq!(cost_cents(1), 4); }
fn one_image_costs_5_cents() { assert_eq!(cost_cents(1), 5); }
#[test]
fn two_images_costs_8_cents() { assert_eq!(cost_cents(2), 8); }
fn two_images_costs_10_cents() { assert_eq!(cost_cents(2), 10); }
#[test]
fn three_images_costs_12_cents() { assert_eq!(cost_cents(3), 12); }
fn three_images_costs_14_cents() { assert_eq!(cost_cents(3), 14); }
#[test]
fn four_images_costs_16_cents() { assert_eq!(cost_cents(4), 16); }
fn four_images_costs_19_cents() { assert_eq!(cost_cents(4), 19); }
}
@@ -15,8 +15,8 @@ impl ArtcraftKling16ProCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// 5s = 52¢, 10s = 103¢.
let cost_in_usd_cents: u64 = if self.is_ten_seconds { 103 } else { 52 };
// 5s = 57¢, 10s = 113¢.
let cost_in_usd_cents: u64 = if self.is_ten_seconds { 113 } else { 57 };
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -51,11 +51,11 @@ mod tests {
}
#[test]
fn five_seconds_is_52() { assert_eq!(cost_cents(Some(5)), 52); }
fn five_seconds_is_57() { assert_eq!(cost_cents(Some(5)), 57); }
#[test]
fn ten_seconds_is_103() { assert_eq!(cost_cents(Some(10)), 103); }
fn ten_seconds_is_113() { assert_eq!(cost_cents(Some(10)), 113); }
#[test]
fn default_duration_is_5s_priced_at_52() { assert_eq!(cost_cents(None), 52); }
fn default_duration_is_5s_priced_at_57() { assert_eq!(cost_cents(None), 57); }
}
@@ -13,7 +13,7 @@ impl ArtcraftKling21MasterCostState {
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client kling_v2p1_master: 5s = $1.40, 10s = $2.80.
let cost_in_usd_cents: u64 = if self.is_ten_seconds { 280 } else { 140 };
let cost_in_usd_cents: u64 = if self.is_ten_seconds { 322 } else { 161 };
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -48,11 +48,11 @@ mod tests {
}
#[test]
fn five_seconds_is_140() { assert_eq!(cost_cents(Some(5)), 140); }
fn five_seconds_is_161() { assert_eq!(cost_cents(Some(5)), 161); }
#[test]
fn ten_seconds_is_280() { assert_eq!(cost_cents(Some(10)), 280); }
fn ten_seconds_is_322() { assert_eq!(cost_cents(Some(10)), 322); }
#[test]
fn default_duration_is_5s_priced_at_140() { assert_eq!(cost_cents(None), 140); }
fn default_duration_is_5s_priced_at_161() { assert_eq!(cost_cents(None), 161); }
}
@@ -13,7 +13,7 @@ impl ArtcraftKling21ProCostState {
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client kling_v2p1_pro: 5s = 45¢, 10s = 90¢.
let cost_in_usd_cents: u64 = if self.is_ten_seconds { 90 } else { 45 };
let cost_in_usd_cents: u64 = if self.is_ten_seconds { 104 } else { 52 };
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -48,11 +48,11 @@ mod tests {
}
#[test]
fn five_seconds_is_45() { assert_eq!(cost_cents(Some(5)), 45); }
fn five_seconds_is_52() { assert_eq!(cost_cents(Some(5)), 52); }
#[test]
fn ten_seconds_is_90() { assert_eq!(cost_cents(Some(10)), 90); }
fn ten_seconds_is_104() { assert_eq!(cost_cents(Some(10)), 104); }
#[test]
fn default_duration_is_5s_priced_at_45() { assert_eq!(cost_cents(None), 45); }
fn default_duration_is_5s_priced_at_52() { assert_eq!(cost_cents(None), 52); }
}
@@ -13,7 +13,7 @@ impl ArtcraftKling2p5TurboProCostState {
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client kling_v2p5_turbo_pro: 5s = 35¢, 10s = 70¢.
let cost_in_usd_cents: u64 = if self.is_ten_seconds { 70 } else { 35 };
let cost_in_usd_cents: u64 = if self.is_ten_seconds { 81 } else { 41 };
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -45,11 +45,11 @@ mod tests {
}
#[test]
fn five_seconds_is_35() { assert_eq!(cost_cents(Some(5)), 35); }
fn five_seconds_is_41() { assert_eq!(cost_cents(Some(5)), 41); }
#[test]
fn ten_seconds_is_70() { assert_eq!(cost_cents(Some(10)), 70); }
fn ten_seconds_is_81() { assert_eq!(cost_cents(Some(10)), 81); }
#[test]
fn default_duration_is_5s_priced_at_35() { assert_eq!(cost_cents(None), 35); }
fn default_duration_is_5s_priced_at_41() { assert_eq!(cost_cents(None), 41); }
}
@@ -21,10 +21,10 @@ impl ArtcraftKling2p6ProCostState {
// audio off: $0.07/sec → 5s=35¢, 10s=70¢
// audio on: $0.14/sec → 5s=70¢, 10s=140¢
let cost_in_usd_cents: u64 = match (self.generate_audio, self.is_ten_seconds) {
(false, false) => 35,
(false, true) => 70,
(true, false) => 70,
(true, true) => 140,
(false, false) => 41,
(false, true) => 81,
(true, false) => 81,
(true, true) => 161,
};
VideoGenerationCostEstimate {
@@ -58,16 +58,16 @@ mod tests {
}
#[test]
fn audio_off_5s_is_35() { assert_eq!(cost_cents(Some(5), Some(false)), 35); }
fn audio_off_5s_is_41() { assert_eq!(cost_cents(Some(5), Some(false)), 41); }
#[test]
fn audio_off_10s_is_70() { assert_eq!(cost_cents(Some(10), Some(false)), 70); }
fn audio_off_10s_is_81() { assert_eq!(cost_cents(Some(10), Some(false)), 81); }
#[test]
fn audio_on_5s_is_70() { assert_eq!(cost_cents(Some(5), Some(true)), 70); }
fn audio_on_5s_is_81() { assert_eq!(cost_cents(Some(5), Some(true)), 81); }
#[test]
fn audio_on_10s_is_140() { assert_eq!(cost_cents(Some(10), Some(true)), 140); }
fn audio_on_10s_is_161() { assert_eq!(cost_cents(Some(10), Some(true)), 161); }
#[test]
fn audio_default_is_on() {
@@ -1,4 +1,8 @@
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
// The markup is applied at cent granularity with ceiling rounding.
const MARKUP_NUMERATOR: u64 = 115;
const MARKUP_DENOMINATOR: u64 = 100;
use crate::generate::generate_video::providers::artcraft::kling_3p0_pro::request::ArtcraftKling3p0ProRequestState;
#[derive(Clone, Debug)]
@@ -17,12 +21,11 @@ impl ArtcraftKling3p0ProCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client kling_3p0_pro:
// audio off: $0.224/sec (rate=224 in tenths-of-cents)
// audio on: $0.336/sec (rate=336)
// ceiling-divided to whole cents.
// Base rates in tenths of a cent per second; the total is
// ceiling-divided to whole cents, then the markup applied.
let rate: u64 = if self.generate_audio { 336 } else { 224 };
let cost_in_usd_cents = (rate * self.duration_seconds + 9) / 10;
let base = (rate * self.duration_seconds + 9) / 10;
let cost_in_usd_cents = base.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -57,24 +60,24 @@ mod tests {
#[test]
fn audio_on_5s_is_168() {
// rate=336, (336*5 + 9) / 10 = 1689/10 = 168.
assert_eq!(cost_cents(Some(5), Some(true)), 168);
assert_eq!(cost_cents(Some(5), Some(true)), 194);
}
#[test]
fn audio_off_5s_is_112() {
// rate=224, (224*5 + 9) / 10 = 1129/10 = 112.
assert_eq!(cost_cents(Some(5), Some(false)), 112);
assert_eq!(cost_cents(Some(5), Some(false)), 129);
}
#[test]
fn audio_on_10s_is_336() {
assert_eq!(cost_cents(Some(10), Some(true)), 336);
assert_eq!(cost_cents(Some(10), Some(true)), 387);
}
#[test]
fn audio_on_15s_is_504() {
// rate=336, (336*15 + 9) / 10 = 5049/10 = 504.
assert_eq!(cost_cents(Some(15), Some(true)), 504);
assert_eq!(cost_cents(Some(15), Some(true)), 580);
}
#[test]
@@ -1,6 +1,10 @@
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::kling_3p0_standard::request::ArtcraftKling3p0StandardRequestState;
// The markup is applied at cent granularity with ceiling rounding.
const MARKUP_NUMERATOR: u64 = 115;
const MARKUP_DENOMINATOR: u64 = 100;
#[derive(Clone, Debug)]
pub struct ArtcraftKling3p0StandardCostState {
pub duration_seconds: u64,
@@ -16,11 +20,11 @@ impl ArtcraftKling3p0StandardCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Mirrors fal_client kling_3p0_standard:
// audio off: $0.168/sec (rate=168)
// audio on: $0.252/sec (rate=252)
// Base rates in tenths of a cent per second; the total is
// ceiling-divided to whole cents, then the markup applied.
let rate: u64 = if self.generate_audio { 252 } else { 168 };
let cost_in_usd_cents = (rate * self.duration_seconds + 9) / 10;
let base = (rate * self.duration_seconds + 9) / 10;
let cost_in_usd_cents = base.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -53,26 +57,26 @@ mod tests {
}
#[test]
fn audio_on_5s_is_126() {
// rate=252, (252*5 + 9) / 10 = 1269/10 = 126.
assert_eq!(cost_cents(Some(5), Some(true)), 126);
fn audio_on_5s_is_145() {
// rate=252, base (252*5 + 9) / 10 = 126, ×115/100 ceiled = 145.
assert_eq!(cost_cents(Some(5), Some(true)), 145);
}
#[test]
fn audio_off_5s_is_84() {
// rate=168, (168*5 + 9) / 10 = 849/10 = 84.
assert_eq!(cost_cents(Some(5), Some(false)), 84);
fn audio_off_5s_is_97() {
// rate=168, base (168*5 + 9) / 10 = 84, ×115/100 ceiled = 97.
assert_eq!(cost_cents(Some(5), Some(false)), 97);
}
#[test]
fn audio_on_10s_is_252() {
assert_eq!(cost_cents(Some(10), Some(true)), 252);
fn audio_on_10s_is_290() {
assert_eq!(cost_cents(Some(10), Some(true)), 290);
}
#[test]
fn audio_on_15s_is_378() {
// (252*15 + 9) / 10 = 3789/10 = 378.
assert_eq!(cost_cents(Some(15), Some(true)), 378);
fn audio_on_15s_is_435() {
// base (252*15 + 9) / 10 = 378, ×115/100 ceiled = 435.
assert_eq!(cost_cents(Some(15), Some(true)), 435);
}
#[test]
@@ -8,6 +8,10 @@ use fal_client::requests_old::webhook::video::text::enqueue_seedance_1p5_pro_tex
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::seedance_1p5_pro::request::ArtcraftSeedance1p5ProRequestState;
// The markup is applied at cent granularity with ceiling rounding.
const MARKUP_NUMERATOR: u64 = 115;
const MARKUP_DENOMINATOR: u64 = 100;
#[derive(Clone, Debug)]
pub struct ArtcraftSeedance1p5ProCostState {
pub duration: Option<EnqueueSeedance1p5ProTextToVideoDuration>,
@@ -37,7 +41,8 @@ impl ArtcraftSeedance1p5ProCostState {
aspect_ratio: None,
generate_audio: self.generate_audio,
};
let cost_in_usd_cents = req.calculate_cost_in_cents();
let base = req.calculate_cost_in_cents();
let cost_in_usd_cents = base.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -96,14 +101,14 @@ mod tests {
// Numeric literals mirror the v1 Artcraft Seedance 1.5 Pro tests we deleted:
// resolution=None + generate_audio=None → fal client defaults (720p, audio on).
#[test]
fn default_5s_is_26() { assert_eq!(cost_cents(Some(5)), 26); }
fn default_5s_is_30() { assert_eq!(cost_cents(Some(5)), 30); }
#[test]
fn default_4s_is_26() { assert_eq!(cost_cents(Some(4)), 26); }
fn default_4s_is_30() { assert_eq!(cost_cents(Some(4)), 30); }
#[test]
fn default_10s_is_65() { assert_eq!(cost_cents(Some(10)), 65); }
fn default_10s_is_75() { assert_eq!(cost_cents(Some(10)), 75); }
#[test]
fn default_12s_is_78() { assert_eq!(cost_cents(Some(12)), 78); }
fn default_12s_is_90() { assert_eq!(cost_cents(Some(12)), 90); }
}
@@ -8,25 +8,25 @@ use crate::generate::generate_video::video_generation_cost_estimate::VideoGenera
//
// ArtCraft credits: 100 credits = $1.00. Credits always equal USD cents.
//
// The per-second USD cost varies by resolution. We compute cents directly
// from the upstream credit rates and their credit-package prices,
// then set ArtCraft credits = cents.
// The per-second USD price varies by resolution.
/// USD cents per second by resolution, derived from upstream rates:
/// 480p: 15 upstream-credits/sec ÷ 193 upstream-credits/$1 × 100 ≈ 7.772 ¢/s
/// 720p: 40 upstream-credits/sec ÷ 250 upstream-credits/$1 × 100 = 16.0 ¢/s
/// 1080p: 90 upstream-credits/sec ÷ 193 upstream-credits/$1 × 100 ≈ 46.632 ¢/s
/// USD cents per second by resolution.
///
/// We keep these as f64 because per-second rates are fractional; rounding
/// happens once at the end after multiplying by duration × batch.
///
/// NB: Derivation is historical (193 credits/$1 package). The upstream
/// package is now 525,000 credits for $2,159.0909 (~243 credits/$1), but
/// user pricing is intentionally kept at these 193-derived rates.
/// Kept as f64 because per-second rates are fractional; rounding happens
/// once at the end after multiplying by duration × batch.
const CENTS_PER_SECOND_480P: f64 = 7.772;
const CENTS_PER_SECOND_720P: f64 = 16.0;
const CENTS_PER_SECOND_1080P: f64 = 46.632;
/// USD cents per second, in hundredths of a cent, when one or more
/// reference videos are attached. Held as integer hundredths so the math is
/// exact; rounded up to whole cents once, after multiplying by
/// duration × batch.
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P: u64 = 881;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P: u64 = 2_205;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_1080P: u64 = 5_110;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_4K: u64 = 11_380;
pub struct ArtcraftSeedance2p0CostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
@@ -48,6 +48,19 @@ impl ArtcraftSeedance2p0CostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.has_video_reference {
let usd_cents = self.with_video_reference_usd_cents();
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
if self.resolution == CommonResolution::FourK {
let usd_cents = seedance_2p0_four_k_usd_cents(
self.duration_seconds,
@@ -85,12 +98,31 @@ impl ArtcraftSeedance2p0CostState {
failures_are_refunded: None,
}
}
/// Price when one or more reference videos are attached.
fn with_video_reference_usd_cents(&self) -> u64 {
let centi_cents_per_second = match self.resolution {
CommonResolution::FourEightyP => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P,
CommonResolution::TenEightyP => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_1080P,
CommonResolution::FourK => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_4K,
// Everything else (including 720p) prices at 720p.
_ => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P,
};
let seconds = self.duration_seconds as u64 * self.batch_count as u64;
// Round up to whole cents.
(centi_cents_per_second * seconds).div_ceil(100)
}
}
#[cfg(test)]
mod tests {
use tokens::tokens::media_files::MediaFileToken;
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::video_list_ref::VideoListRef;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
// -- 720p pricing --
@@ -202,10 +234,110 @@ mod tests {
#[test]
fn explicit_4k_with_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
assert_eq!(artcraft_4k_cents(4, 1, true), 456);
assert_eq!(artcraft_4k_cents(5, 1, true), 569);
assert_eq!(artcraft_4k_cents(10, 1, true), 1138);
assert_eq!(artcraft_4k_cents(15, 1, true), 1707);
}
}
// -- With-video-reference pricing --
mod video_reference_pricing {
use super::*;
#[test]
fn table_480p() {
let expected = [36, 45, 53, 62, 71, 80, 89, 97, 106, 115, 124, 133];
for (i, cents) in expected.iter().enumerate() {
let duration = (i + 4) as u16;
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::FourEightyP), duration, 1),
*cents,
"480p {duration}s",
);
}
}
#[test]
fn table_720p() {
let expected = [89, 111, 133, 155, 177, 199, 221, 243, 265, 287, 309, 331];
for (i, cents) in expected.iter().enumerate() {
let duration = (i + 4) as u16;
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::SevenTwentyP), duration, 1),
*cents,
"720p {duration}s",
);
}
}
#[test]
fn table_1080p() {
let expected = [205, 256, 307, 358, 409, 460, 511, 563, 614, 665, 716, 767];
for (i, cents) in expected.iter().enumerate() {
let duration = (i + 4) as u16;
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::TenEightyP), duration, 1),
*cents,
"1080p {duration}s",
);
}
}
#[test]
fn table_4k() {
let expected = [456, 569, 683, 797, 911, 1025, 1138, 1252, 1366, 1480, 1594, 1707];
for (i, cents) in expected.iter().enumerate() {
let duration = (i + 4) as u16;
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::FourK), duration, 1),
*cents,
"4K {duration}s",
);
}
}
/// Batches price identically to the equivalent total seconds.
#[test]
fn batch_multiplies_total_seconds() {
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::FourEightyP), 5, 2),
cost_cents_with_video_ref(Some(RouterResolution::FourEightyP), 10, 1),
);
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::FourK), 5, 2),
cost_cents_with_video_ref(Some(RouterResolution::FourK), 10, 1),
);
}
/// Image and audio references do NOT trigger the with-reference pricing —
/// only reference videos do. (The non-reference price is unchanged.)
#[test]
fn image_and_audio_references_price_as_no_reference() {
use crate::api::audio_list_ref::AudioListRef;
use crate::api::image_list_ref::ImageListRef;
let builder = GenerateVideoRequestBuilder {
provider: RouterProvider::Artcraft,
resolution: Some(RouterResolution::SevenTwentyP),
duration_seconds: Some(5),
video_batch_count: Some(1),
reference_images: Some(ImageListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_img".to_string()),
])),
reference_audio: Some(AudioListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_audio".to_string()),
])),
..Default::default()
};
let cents = builder.build2()
.expect("build2 should succeed")
.estimate_cost()
.expect("estimate_cost should succeed")
.cost_in_usd_cents
.unwrap();
assert_eq!(cents, cost_cents(Some(RouterResolution::SevenTwentyP), 5, 1));
}
}
@@ -308,4 +440,28 @@ mod tests {
.cost_in_usd_cents
.unwrap()
}
/// Like [`cost_cents`], with one reference video attached.
fn cost_cents_with_video_ref(
resolution: Option<RouterResolution>,
duration_seconds: u16,
video_batch_count: u16,
) -> u64 {
let builder = GenerateVideoRequestBuilder {
provider: RouterProvider::Artcraft,
resolution,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
reference_videos: Some(VideoListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_ref".to_string()),
])),
..Default::default()
};
builder.build2()
.expect("build2 should succeed")
.estimate_cost()
.expect("estimate_cost should succeed")
.cost_in_usd_cents
.unwrap()
}
}
@@ -7,21 +7,22 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_fast::req
//
// ArtCraft credits: 100 credits = $1.00. Credits always equal USD cents.
//
// The per-second USD cost varies by resolution. We derive cents from the
// upstream credit rates and their credit-package prices, then set
// ArtCraft credits = cents.
// The per-second USD price varies by resolution.
/// USD cents per second by resolution, derived from upstream Fast rates:
/// 480p: 10 upstream-credits/sec / 193 upstream-credits/$1 * 100 ~= 5.181 c/s
/// (historical 193 credits/$1 derivation; the upstream package is now
/// ~243 credits/$1 but user pricing is intentionally unchanged)
/// 720p: 28 upstream-credits/sec / 220 upstream-credits/$1 * 100 ~= 12.727 c/s
/// USD cents per second by resolution.
///
/// We keep these as f64 because per-second rates are fractional; rounding
/// happens once at the end after multiplying by duration * batch.
/// Kept as f64 because per-second rates are fractional; rounding happens
/// once at the end after multiplying by duration * batch.
const CENTS_PER_SECOND_480P: f64 = 5.181;
const CENTS_PER_SECOND_720P: f64 = 12.727;
/// USD cents per second, in hundredths of a cent, when one or more
/// reference videos are attached. Held as integer hundredths so the math is
/// exact; rounded up to whole cents once, after multiplying by
/// duration * batch.
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P: u64 = 841;
const WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P: u64 = 1_560;
pub struct ArtcraftSeedance2p0FastCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
@@ -43,6 +44,19 @@ impl ArtcraftSeedance2p0FastCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.has_video_reference {
let usd_cents = self.with_video_reference_usd_cents();
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
let cents_per_second = match self.resolution {
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
// Everything else (including 720p and unsupported resolutions) prices at 720p.
@@ -62,13 +76,30 @@ impl ArtcraftSeedance2p0FastCostState {
failures_are_refunded: None,
}
}
/// Price when one or more reference videos are attached.
fn with_video_reference_usd_cents(&self) -> u64 {
let centi_cents_per_second = match self.resolution {
CommonResolution::FourEightyP => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_480P,
// Everything else (including 720p and unsupported resolutions) prices at 720p.
_ => WITH_VIDEO_REFERENCE_CENTI_CENTS_PER_SECOND_720P,
};
let seconds = self.duration_seconds as u64 * self.batch_count as u64;
// Round up to whole cents.
(centi_cents_per_second * seconds).div_ceil(100)
}
}
#[cfg(test)]
mod tests {
use tokens::tokens::media_files::MediaFileToken;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_provider::RouterProvider;
use crate::api::video_list_ref::VideoListRef;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
// -- 720p pricing --
@@ -128,6 +159,81 @@ mod tests {
}
}
// -- With-video-reference pricing --
mod video_reference_pricing {
use super::*;
#[test]
fn table_480p() {
let expected = [34, 43, 51, 59, 68, 76, 85, 93, 101, 110, 118, 127];
for (i, cents) in expected.iter().enumerate() {
let duration = (i + 4) as u16;
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::FourEightyP), duration, 1),
*cents,
"480p {duration}s",
);
}
}
#[test]
fn table_720p() {
let expected = [63, 78, 94, 110, 125, 141, 156, 172, 188, 203, 219, 234];
for (i, cents) in expected.iter().enumerate() {
let duration = (i + 4) as u16;
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::SevenTwentyP), duration, 1),
*cents,
"720p {duration}s",
);
}
}
/// Batches price identically to the equivalent total seconds.
#[test]
fn batch_multiplies_total_seconds() {
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::FourEightyP), 5, 2),
cost_cents_with_video_ref(Some(RouterResolution::FourEightyP), 10, 1),
);
assert_eq!(
cost_cents_with_video_ref(Some(RouterResolution::SevenTwentyP), 5, 2),
cost_cents_with_video_ref(Some(RouterResolution::SevenTwentyP), 10, 1),
);
}
/// Image and audio references do NOT trigger the with-reference pricing --
/// only reference videos do. (The non-reference price is unchanged.)
#[test]
fn image_and_audio_references_price_as_no_reference() {
use crate::api::audio_list_ref::AudioListRef;
use crate::api::image_list_ref::ImageListRef;
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Fast,
provider: RouterProvider::Artcraft,
resolution: Some(RouterResolution::SevenTwentyP),
duration_seconds: Some(5),
video_batch_count: Some(1),
reference_images: Some(ImageListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_img".to_string()),
])),
reference_audio: Some(AudioListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_audio".to_string()),
])),
..Default::default()
};
let cents = builder.build2()
.expect("build2 should succeed")
.estimate_cost()
.expect("estimate_cost should succeed")
.cost_in_usd_cents
.unwrap();
assert_eq!(cents, cost_cents(Some(RouterResolution::SevenTwentyP), 5, 1));
}
}
// -- Relative pricing --
mod relative_pricing_tests {
@@ -216,4 +322,29 @@ mod tests {
.cost_in_usd_cents
.unwrap()
}
/// Like [`cost_cents`], with one reference video attached.
fn cost_cents_with_video_ref(
resolution: Option<RouterResolution>,
duration_seconds: u16,
video_batch_count: u16,
) -> u64 {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Fast,
provider: RouterProvider::Artcraft,
resolution,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
reference_videos: Some(VideoListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_ref".to_string()),
])),
..Default::default()
};
builder.build2()
.expect("build2 should succeed")
.estimate_cost()
.expect("estimate_cost should succeed")
.cost_in_usd_cents
.unwrap()
}
}
@@ -55,12 +55,12 @@ mod tests {
#[test]
fn full_duration_table() {
assert_eq!(cents(Some(RouterResolution::FourEightyP), 4), 82);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 10), 205);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 15), 307);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 20), 409);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 25), 511);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 30), 613);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 4), 86);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 10), 214);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 15), 321);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 20), 428);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 25), 535);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 30), 642);
}
}
@@ -69,12 +69,12 @@ mod tests {
#[test]
fn full_duration_table() {
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 4), 164);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 10), 409);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 15), 613);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 20), 817);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 25), 1021);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 30), 1226);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 4), 172);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 10), 428);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 15), 642);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 20), 856);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 25), 1070);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 30), 1283);
}
#[test]
@@ -119,9 +119,9 @@ pub fn seedance_2p0_byteplus_mini_usd_cents(
// change the price.
/// Seedance 2.5 Preview — 480p price, USD cents per second.
const SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_480P: f64 = 20.41909465;
const SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_480P: f64 = 21.38234568;
/// Seedance 2.5 Preview — 720p price, USD cents per second.
const SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_720P: f64 = 40.8381893;
const SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_720P: f64 = 42.76469136;
/// ArtCraft's price (USD cents) for Seedance 2.5 Preview.
///
@@ -15,8 +15,9 @@ impl ArtcraftSora2CostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Sora 2: $0.10/second. Default duration 4s.
let cost_in_usd_cents = self.duration_seconds * 10;
// Sora 2: 11.5¢/second (held in hundredths of a cent, rounded up to
// whole cents). Default duration 4s.
let cost_in_usd_cents = (self.duration_seconds * 1_150).div_ceil(100);
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -48,14 +49,14 @@ mod tests {
}
#[test]
fn four_seconds_is_40() { assert_eq!(cost_cents(Some(4)), 40); }
fn four_seconds_is_46() { assert_eq!(cost_cents(Some(4)), 46); }
#[test]
fn eight_seconds_is_80() { assert_eq!(cost_cents(Some(8)), 80); }
fn eight_seconds_is_92() { assert_eq!(cost_cents(Some(8)), 92); }
#[test]
fn twelve_seconds_is_120() { assert_eq!(cost_cents(Some(12)), 120); }
fn twelve_seconds_is_138() { assert_eq!(cost_cents(Some(12)), 138); }
#[test]
fn default_duration_is_4s_priced_at_40() { assert_eq!(cost_cents(None), 40); }
fn default_duration_is_4s_priced_at_46() { assert_eq!(cost_cents(None), 46); }
}
@@ -19,9 +19,10 @@ impl ArtcraftSora2ProCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Sora 2 Pro: $0.30/sec @ 720p, $0.50/sec @ 1080p. Default 4s.
let per_second_cents: u64 = if self.is_ten_eighty_p { 50 } else { 30 };
let cost_in_usd_cents = per_second_cents * self.duration_seconds;
// Sora 2 Pro: 34.5¢/sec @ 720p, 57.5¢/sec @ 1080p (held in hundredths
// of a cent, rounded up to whole cents). Default 4s.
let per_second_centi_cents: u64 = if self.is_ten_eighty_p { 5_750 } else { 3_450 };
let cost_in_usd_cents = (per_second_centi_cents * self.duration_seconds).div_ceil(100);
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
@@ -77,30 +78,30 @@ mod tests {
use super::*;
#[test]
fn p720_4s_is_120() { assert_eq!(cost_cents(Some(4), Some(RouterResolution::SevenTwentyP), false), 120); }
fn p720_4s_is_138() { assert_eq!(cost_cents(Some(4), Some(RouterResolution::SevenTwentyP), false), 138); }
#[test]
fn p720_8s_is_240() { assert_eq!(cost_cents(Some(8), Some(RouterResolution::SevenTwentyP), false), 240); }
fn p720_8s_is_276() { assert_eq!(cost_cents(Some(8), Some(RouterResolution::SevenTwentyP), false), 276); }
#[test]
fn p720_12s_is_360() { assert_eq!(cost_cents(Some(12), Some(RouterResolution::SevenTwentyP), false), 360); }
fn p720_12s_is_414() { assert_eq!(cost_cents(Some(12), Some(RouterResolution::SevenTwentyP), false), 414); }
#[test]
fn p1080_4s_is_200() { assert_eq!(cost_cents(Some(4), Some(RouterResolution::TenEightyP), false), 200); }
fn p1080_4s_is_230() { assert_eq!(cost_cents(Some(4), Some(RouterResolution::TenEightyP), false), 230); }
#[test]
fn p1080_12s_is_600() { assert_eq!(cost_cents(Some(12), Some(RouterResolution::TenEightyP), false), 600); }
fn p1080_12s_is_690() { assert_eq!(cost_cents(Some(12), Some(RouterResolution::TenEightyP), false), 690); }
#[test]
fn t2v_default_resolution_priced_as_1080p() {
// T2V (no start_frame) with no resolution → 1080p default → 200¢ at 4s.
assert_eq!(cost_cents(Some(4), None, false), 200);
// T2V (no start_frame) with no resolution → 1080p default → 230¢ at 4s.
assert_eq!(cost_cents(Some(4), None, false), 230);
}
#[test]
fn i2v_default_resolution_priced_as_720p() {
// I2V (with start_frame) with no resolution → 720p default → 120¢ at 4s.
assert_eq!(cost_cents(Some(4), None, true), 120);
// I2V (with start_frame) with no resolution → 720p default → 138¢ at 4s.
assert_eq!(cost_cents(Some(4), None, true), 138);
}
}
@@ -4,10 +4,10 @@ use crate::generate::generate_video::video_generation_cost_estimate::VideoGenera
use crate::generate::generate_video::providers::artcraft::veo_3p1_lite::request::ArtcraftVeo3p1LiteRequestState;
/// Per-second rates in hundredths of a US cent.
const SEVEN_TWENTY_P_AUDIO_OFF_RATE: u64 = 315;
const SEVEN_TWENTY_P_AUDIO_ON_RATE: u64 = 525;
const TEN_EIGHTY_P_AUDIO_OFF_RATE: u64 = 525;
const TEN_EIGHTY_P_AUDIO_ON_RATE: u64 = 840;
const SEVEN_TWENTY_P_AUDIO_OFF_RATE: u64 = 345;
const SEVEN_TWENTY_P_AUDIO_ON_RATE: u64 = 575;
const TEN_EIGHTY_P_AUDIO_OFF_RATE: u64 = 575;
const TEN_EIGHTY_P_AUDIO_ON_RATE: u64 = 920;
#[derive(Clone, Debug)]
pub struct ArtcraftVeo3p1LiteCostState {
@@ -88,28 +88,28 @@ mod tests {
}
#[test]
fn defaults_8s_720p_audio_on_is_42() { assert_eq!(cost_cents(None, None, None), 42); }
fn defaults_8s_720p_audio_on_is_46() { assert_eq!(cost_cents(None, None, None), 46); }
#[test]
fn audio_off_8s_720p_is_26() {
// 315 * 8 = 2520 hundredth-cents → 26 cents.
assert_eq!(cost_cents(Some(8), None, Some(false)), 26);
fn audio_off_8s_720p_is_28() {
// 345 * 8 = 2760 hundredth-cents → 28 cents.
assert_eq!(cost_cents(Some(8), None, Some(false)), 28);
}
#[test]
fn audio_on_8s_1080p_is_68() {
// 840 * 8 = 6720 hundredth-cents → 68 cents.
assert_eq!(cost_cents(Some(8), Some(RouterResolution::TenEightyP), Some(true)), 68);
fn audio_on_8s_1080p_is_74() {
// 920 * 8 = 7360 hundredth-cents → 74 cents.
assert_eq!(cost_cents(Some(8), Some(RouterResolution::TenEightyP), Some(true)), 74);
}
#[test]
fn audio_off_8s_1080p_is_42() {
assert_eq!(cost_cents(Some(8), Some(RouterResolution::TenEightyP), Some(false)), 42);
fn audio_off_8s_1080p_is_46() {
assert_eq!(cost_cents(Some(8), Some(RouterResolution::TenEightyP), Some(false)), 46);
}
#[test]
fn audio_on_4s_720p_is_21() {
assert_eq!(cost_cents(Some(4), None, Some(true)), 21);
fn audio_on_4s_720p_is_23() {
assert_eq!(cost_cents(Some(4), None, Some(true)), 23);
}
#[test]
@@ -16,6 +16,9 @@ pub struct ApiOrWebSessionUserRecord {
pub is_banned: bool,
pub can_ban_users: bool,
/// Optional comma-separated list of parseable `UserFeatureFlag` enum features.
pub maybe_feature_flags: Option<String>,
/// Only set when the lookup was by API key.
pub maybe_api_key_token: Option<ApiKeyToken>,
}
@@ -40,6 +40,7 @@ SELECT
users.user_role_slug,
users.is_banned,
user_roles.can_ban_users,
users.maybe_feature_flags,
api_keys.token as `api_key_token: ApiKeyToken`
FROM api_keys
INNER JOIN users
@@ -65,6 +66,7 @@ LIMIT 1
user_role_slug: r.user_role_slug,
is_banned: i8_to_bool(r.is_banned),
can_ban_users: nullable_i8_to_bool_default_false(r.can_ban_users),
maybe_feature_flags: r.maybe_feature_flags,
maybe_api_key_token: Some(r.api_key_token),
}))
}
@@ -37,7 +37,8 @@ SELECT
users.email_address,
users.user_role_slug,
users.is_banned,
user_roles.can_ban_users
user_roles.can_ban_users,
users.maybe_feature_flags
FROM user_sessions
INNER JOIN users
ON users.token = user_sessions.user_token
@@ -62,6 +63,7 @@ LIMIT 1
user_role_slug: r.user_role_slug,
is_banned: i8_to_bool(r.is_banned),
can_ban_users: nullable_i8_to_bool_default_false(r.can_ban_users),
maybe_feature_flags: r.maybe_feature_flags,
maybe_api_key_token: None,
}))
}
@@ -16,6 +16,7 @@ use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
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::kinovi_web::insert_generic_inference_job_for_kinovi_web_queue_with_apriori_job_token::KinoviVersion;
@@ -38,12 +39,14 @@ use crate::http_server::endpoints::omni_gen::generate::audio::pipeline_v2::run_p
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::shared_job_args::SharedJobArgs;
use crate::http_server::endpoints::omni_gen::shared_utils::kinovi_account::KinoviAccount;
use crate::http_server::endpoints::omni_gen::shared_utils::audio::validate_audio_request::validate_audio_request;
use crate::http_server::user_lookup::api_or_web_session::require_api_or_web_session::{require_api_or_web_session, SessionType};
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::lookup_media_files_as_cdn_url_list_and_map;
/// Generate audio using the omni-gen unified endpoint.
/// Authenticates as either a web-session (cookie) user or an API-key (`Authorization` header) user.
#[utoipa::path(
post,
tag = "Omni Gen",
@@ -81,25 +84,18 @@ pub async fn omni_gen_audio_generate_handler(
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::from(e)
})?;
let session = match maybe_user_session.as_ref() {
Some(session) => session,
None => return Err(CommonWebError::NotAuthorized),
};
// Either an API-key user (Authorization header) or a web-session (cookie) user.
let session = require_api_or_web_session(
&http_request,
&server_state.session_checker,
&server_state.avt_cookie_manager,
&mut *mysql_connection,
).await?;
let user_token = &session.user_token;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// AVT tokens are web-session only; API-key sessions never carry one.
let maybe_avt_token = session.maybe_avt_token.clone();
// ==================== IDEMPOTENCY ==================== //
@@ -224,7 +220,10 @@ pub async fn omni_gen_audio_generate_handler(
// ==================== WRITE RESULT ==================== //
let maybe_platform_type = get_request_platform_type(&http_request);
let maybe_platform_type = match session.session_type {
SessionType::Api => Some(PlatformType::ApiKey),
SessionType::WebSession => get_request_platform_type(&http_request),
};
let mut transaction = mysql_connection.begin().await.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
@@ -15,6 +15,7 @@ use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_image_model::CommonImageModel;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use http_server_common::request::get_request_ip::get_request_ip;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
@@ -36,6 +37,7 @@ use crate::http_server::endpoints::omni_gen::generate::image::insert_db_job::ins
use crate::http_server::endpoints::omni_gen::generate::image::insert_db_job::shared_job_args::SharedJobArgs;
use crate::http_server::endpoints::omni_gen::generate::image::pipeline_v2::run_pipeline_v2::{run_pipeline_v2, RunPipelineV2Args};
use crate::http_server::endpoints::omni_gen::shared_utils::kinovi_account::KinoviAccount;
use crate::http_server::user_lookup::api_or_web_session::require_api_or_web_session::{require_api_or_web_session, SessionType};
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
@@ -43,6 +45,7 @@ use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::lookup_media_files_as_cdn_url_list_and_map;
/// Generate an image using the omni-gen unified endpoint.
/// Authenticates as either a web-session (cookie) user or an API-key (`Authorization` header) user.
#[utoipa::path(
post,
tag = "Omni Gen",
@@ -76,25 +79,18 @@ pub async fn omni_gen_image_generate_handler(
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::from(e)
})?;
let session = match maybe_user_session.as_ref() {
Some(session) => session,
None => return Err(CommonWebError::NotAuthorized),
};
// Either an API-key user (Authorization header) or a web-session (cookie) user.
let session = require_api_or_web_session(
&http_request,
&server_state.session_checker,
&server_state.avt_cookie_manager,
&mut *mysql_connection,
).await?;
let user_token = &session.user_token;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// AVT tokens are web-session only; API-key sessions never carry one.
let maybe_avt_token = session.maybe_avt_token.clone();
// ==================== MODEL ACCESS CHECK ==================== //
@@ -215,7 +211,10 @@ pub async fn omni_gen_image_generate_handler(
// ==================== WRITE RESULT ==================== //
let maybe_platform_type = get_request_platform_type(&http_request);
let maybe_platform_type = match session.session_type {
SessionType::Api => Some(PlatformType::ApiKey),
SessionType::WebSession => get_request_platform_type(&http_request),
};
let mut transaction = mysql_connection
.begin()
@@ -16,6 +16,7 @@ use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
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::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
@@ -35,12 +36,14 @@ use crate::http_server::endpoints::omni_gen::generate::mesh::insert_db_job::inse
use crate::http_server::endpoints::omni_gen::generate::mesh::pipeline_v2::run_pipeline_v2::{run_pipeline_v2, RunPipelineV2Args};
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::shared_job_args::SharedJobArgs;
use crate::http_server::endpoints::omni_gen::shared_utils::mesh::validate_mesh_request::validate_mesh_request;
use crate::http_server::user_lookup::api_or_web_session::require_api_or_web_session::{require_api_or_web_session, SessionType};
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::lookup_media_files_as_cdn_url_list_and_map;
/// Generate a mesh (3D object) using the omni-gen unified endpoint.
/// Authenticates as either a web-session (cookie) user or an API-key (`Authorization` header) user.
#[utoipa::path(
post,
tag = "Omni Gen",
@@ -78,25 +81,18 @@ pub async fn omni_gen_mesh_generate_handler(
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::from(e)
})?;
let session = match maybe_user_session.as_ref() {
Some(session) => session,
None => return Err(CommonWebError::NotAuthorized),
};
// Either an API-key user (Authorization header) or a web-session (cookie) user.
let session = require_api_or_web_session(
&http_request,
&server_state.session_checker,
&server_state.avt_cookie_manager,
&mut *mysql_connection,
).await?;
let user_token = &session.user_token;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// AVT tokens are web-session only; API-key sessions never carry one.
let maybe_avt_token = session.maybe_avt_token.clone();
// ==================== IDEMPOTENCY ==================== //
@@ -211,7 +207,10 @@ pub async fn omni_gen_mesh_generate_handler(
// ==================== WRITE RESULT ==================== //
let maybe_platform_type = get_request_platform_type(&http_request);
let maybe_platform_type = match session.session_type {
SessionType::Api => Some(PlatformType::ApiKey),
SessionType::WebSession => get_request_platform_type(&http_request),
};
let mut transaction = mysql_connection.begin().await.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
@@ -16,6 +16,7 @@ use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
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::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
@@ -36,12 +37,14 @@ use crate::http_server::endpoints::omni_gen::generate::splat::insert_db_job::ins
use crate::http_server::endpoints::omni_gen::generate::splat::pipeline_v2::run_pipeline_v2::{run_pipeline_v2, RunPipelineV2Args};
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::shared_job_args::SharedJobArgs;
use crate::http_server::endpoints::omni_gen::shared_utils::splat::validate_splat_request::validate_splat_request;
use crate::http_server::user_lookup::api_or_web_session::require_api_or_web_session::{require_api_or_web_session, SessionType};
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::lookup_media_files_as_cdn_url_list_and_map;
/// Generate a splat (3D world) using the omni-gen unified endpoint.
/// Authenticates as either a web-session (cookie) user or an API-key (`Authorization` header) user.
#[utoipa::path(
post,
tag = "Omni Gen",
@@ -79,25 +82,18 @@ pub async fn omni_gen_splat_generate_handler(
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::from(e)
})?;
let session = match maybe_user_session.as_ref() {
Some(session) => session,
None => return Err(CommonWebError::NotAuthorized),
};
// Either an API-key user (Authorization header) or a web-session (cookie) user.
let session = require_api_or_web_session(
&http_request,
&server_state.session_checker,
&server_state.avt_cookie_manager,
&mut *mysql_connection,
).await?;
let user_token = &session.user_token;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// AVT tokens are web-session only; API-key sessions never carry one.
let maybe_avt_token = session.maybe_avt_token.clone();
// ==================== IDEMPOTENCY ==================== //
@@ -216,7 +212,10 @@ pub async fn omni_gen_splat_generate_handler(
// ==================== WRITE RESULT ==================== //
let maybe_platform_type = get_request_platform_type(&http_request);
let maybe_platform_type = match session.session_type {
SessionType::Api => Some(PlatformType::ApiKey),
SessionType::WebSession => get_request_platform_type(&http_request),
};
let mut transaction = mysql_connection.begin().await.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
@@ -16,6 +16,7 @@ use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation::common_video_model::CommonVideoModel;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use http_server_common::request::get_request_ip::get_request_ip;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
@@ -43,6 +44,7 @@ use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::sha
use crate::http_server::endpoints::omni_gen::shared_utils::kinovi_account::KinoviAccount;
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::shared_utils::video::validate_video_request::validate_video_request;
use crate::http_server::user_lookup::api_or_web_session::require_api_or_web_session::{require_api_or_web_session, SessionType};
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_feature_flags::UserSessionFeatureFlags;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
@@ -50,6 +52,7 @@ use crate::state::server_state::ServerState;
use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::lookup_media_files_as_cdn_url_list_and_map;
/// Generate a video using the omni-gen unified endpoint.
/// Authenticates as either a web-session (cookie) user or an API-key (`Authorization` header) user.
#[utoipa::path(
post,
tag = "Omni Gen",
@@ -87,25 +90,18 @@ pub async fn omni_gen_video_generate_handler(
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::from(e)
})?;
let session = match maybe_user_session.as_ref() {
Some(session) => session,
None => return Err(CommonWebError::NotAuthorized),
};
// Either an API-key user (Authorization header) or a web-session (cookie) user.
let session = require_api_or_web_session(
&http_request,
&server_state.session_checker,
&server_state.avt_cookie_manager,
&mut *mysql_connection,
).await?;
let user_token = &session.user_token;
let maybe_avt_token = server_state
.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// AVT tokens are web-session only; API-key sessions never carry one.
let maybe_avt_token = session.maybe_avt_token.clone();
// ==================== MODEL ACCESS CHECK ==================== //
@@ -265,7 +261,10 @@ pub async fn omni_gen_video_generate_handler(
// ==================== WRITE RESULT ==================== //
let maybe_platform_type = get_request_platform_type(&http_request);
let maybe_platform_type = match session.session_type {
SessionType::Api => Some(PlatformType::ApiKey),
SessionType::WebSession => get_request_platform_type(&http_request),
};
let mut transaction = mysql_connection.begin().await.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
@@ -37,6 +37,9 @@ pub struct ApiOrWebSession {
pub user_role_slug: String,
pub can_ban_users: bool,
/// Optional comma-separated list of parseable `UserFeatureFlag` enum features.
pub maybe_feature_flags: Option<String>,
/// Only present for API-key sessions.
pub maybe_api_key_token: Option<ApiKeyToken>,
@@ -184,6 +187,7 @@ fn into_session_rejecting_banned(
email_address: record.email_address,
user_role_slug: record.user_role_slug,
can_ban_users: record.can_ban_users,
maybe_feature_flags: record.maybe_feature_flags,
maybe_api_key_token: record.maybe_api_key_token,
maybe_avt_token,
})