mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 15:55:41 -05:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2029524c77 | ||
|
|
0354b68f88 | ||
|
|
5b3327f919 | ||
|
|
34300089c8 | ||
|
|
9950dfebf0 |
@@ -38,7 +38,6 @@ script/socat_dev_database_connect.sh
|
||||
venv/
|
||||
|
||||
# LLM Testing
|
||||
/external/
|
||||
/prompts/
|
||||
/reports/
|
||||
/requests/
|
||||
@@ -103,4 +102,3 @@ windows/sqlite/sqlite3.lib
|
||||
# ArtCraft Windows DLLs
|
||||
crates/desktop/src-tauri/*.dll
|
||||
windows_dlls/
|
||||
|
||||
|
||||
Generated
+740
-640
File diff suppressed because it is too large
Load Diff
+13
-11
@@ -295,14 +295,16 @@ worldlabs_consumer_client = { path = "crates/api_clients/worldlabs_consumer_clie
|
||||
# External libs
|
||||
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
|
||||
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
|
||||
actix-cors = "0.7.1"
|
||||
actix-http = "3.12.1"
|
||||
actix-multipart = "0.7.2"
|
||||
actix-rt = "2.11.0"
|
||||
actix = "0.13.5"
|
||||
actix-cors = "0.6.4" # TODO(bt): Carefully upgrade this. CORS broke last time.
|
||||
actix-http = "3.7.0"
|
||||
actix-multipart = "0.6.2"
|
||||
actix-rt = "2.10.0"
|
||||
actix-service = "2.0.3"
|
||||
actix-utils = "3.0.1"
|
||||
actix-web = "4.13.0"
|
||||
actix-web-lab = "0.26.0"
|
||||
actix-web = "4.7.0" # TODO(bt): We're behind and need to bump versions.
|
||||
actix-web-actors = "4.3.1+deprecated"
|
||||
actix-web-lab = "0.20.2"
|
||||
anyhow = "1.0.102"
|
||||
assertor = "0.0.3"
|
||||
base64 = "0.22.1"
|
||||
@@ -342,11 +344,11 @@ sha2 = "0.11.0-rc.5"
|
||||
sha3 = "0.10.8"
|
||||
speculoos = "0.11.0"
|
||||
strum = { version = "0.28.0", features = ["derive"] }
|
||||
tauri = { version = "2.11.2", features = ["unstable", "devtools", "protocol-asset"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
tauri-plugin-fs = "2.5.1"
|
||||
tauri-plugin-log = "2.8.0"
|
||||
tauri-plugin-upload = "2.4.0"
|
||||
tauri = { version = "2.8.3", features = ["unstable", "devtools", "protocol-asset"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
|
||||
tauri-plugin-dialog = "2.3.3"
|
||||
tauri-plugin-fs = "2.4.2"
|
||||
tauri-plugin-log = "2.6.0"
|
||||
tauri-plugin-upload = "2.3.1"
|
||||
tempdir = "0.3.7"
|
||||
tempfile = "3.27.0"
|
||||
thiserror = "2.0.18"
|
||||
|
||||
+59
-5
@@ -11,11 +11,9 @@ use crate::generate::generate_image::providers::artcraft::midjourney_7::request:
|
||||
/// | 3 | 19 | 19¢ | (interpolated; Kinovi doesn't expose 3)
|
||||
/// | 4 | 25 | 25¢ |
|
||||
///
|
||||
/// Kinovi-side math (historical, 22,000-credit package at 193 credits/$1):
|
||||
/// 12 Kinovi credits per Midjourney task → batches 1/2/4 land on 6/12/25¢;
|
||||
/// batch 3 is `ceil((12 × 3 / 193) × 100) = 19`. Under the current
|
||||
/// 525,000-credit package (243 credits/$1) Kinovi costs only 5/10/20¢ —
|
||||
/// user pricing is intentionally kept at the 193-derived values.
|
||||
/// Kinovi-side math: 12 Kinovi credits per Midjourney task; 193 Kinovi
|
||||
/// credits per USD. Kinovi rounds to nearest cent, so batches 1/2/4 land
|
||||
/// on 6/12/25. Batch 3 is `ceil((12 × 3 / 193) × 100) = 19`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftMidjourney7CostState {
|
||||
pub num_images: u16,
|
||||
@@ -129,6 +127,62 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1:1 cost parity with Kinovi for batches 1/2/4 ──
|
||||
//
|
||||
// Kinovi midjourney_7 returns 6/12/25¢ for batches 1/2/4. The artcraft
|
||||
// provider must match those values byte-for-byte so users pay the same.
|
||||
|
||||
mod kinovi_parity {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::providers::kinovi::midjourney_7::cost::KinoviMidjourney7CostState;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount;
|
||||
|
||||
#[test]
|
||||
fn batch_1_matches_kinovi() {
|
||||
let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::One }
|
||||
.estimate_cost();
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost) while this artcraft user price rounds to
|
||||
// nearest — kinovi may exceed it by at most one cent.
|
||||
let artcraft_cents = cost_cents(1);
|
||||
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"batch 1: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2_matches_kinovi() {
|
||||
let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Two }
|
||||
.estimate_cost();
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost) while this artcraft user price rounds to
|
||||
// nearest — kinovi may exceed it by at most one cent.
|
||||
let artcraft_cents = cost_cents(2);
|
||||
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"batch 2: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4_matches_kinovi() {
|
||||
let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Four }
|
||||
.estimate_cost();
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost) while this artcraft user price rounds to
|
||||
// nearest — kinovi may exceed it by at most one cent.
|
||||
let artcraft_cents = cost_cents(4);
|
||||
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"batch 4: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Monotonicity ──
|
||||
|
||||
#[test]
|
||||
|
||||
+52
@@ -82,4 +82,56 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1:1 cost parity with Kinovi 7-niji ──
|
||||
|
||||
mod kinovi_parity {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::cost::KinoviMidjourney7NijiCostState;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount;
|
||||
|
||||
#[test]
|
||||
fn batch_1_matches_kinovi() {
|
||||
let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::One }
|
||||
.estimate_cost();
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost) while this artcraft user price rounds to
|
||||
// nearest — kinovi may exceed it by at most one cent.
|
||||
let artcraft_cents = cost_cents(1);
|
||||
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"batch 1: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2_matches_kinovi() {
|
||||
let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Two }
|
||||
.estimate_cost();
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost) while this artcraft user price rounds to
|
||||
// nearest — kinovi may exceed it by at most one cent.
|
||||
let artcraft_cents = cost_cents(2);
|
||||
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"batch 2: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4_matches_kinovi() {
|
||||
let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Four }
|
||||
.estimate_cost();
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost) while this artcraft user price rounds to
|
||||
// nearest — kinovi may exceed it by at most one cent.
|
||||
let artcraft_cents = cost_cents(4);
|
||||
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"batch 4: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+52
@@ -82,4 +82,56 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1:1 cost parity with Kinovi v8 ──
|
||||
|
||||
mod kinovi_parity {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::providers::kinovi::midjourney_8::cost::KinoviMidjourney8CostState;
|
||||
use seedance2pro_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount;
|
||||
|
||||
#[test]
|
||||
fn batch_1_matches_kinovi() {
|
||||
let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::One }
|
||||
.estimate_cost();
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost) while this artcraft user price rounds to
|
||||
// nearest — kinovi may exceed it by at most one cent.
|
||||
let artcraft_cents = cost_cents(1);
|
||||
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"batch 1: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2_matches_kinovi() {
|
||||
let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Two }
|
||||
.estimate_cost();
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost) while this artcraft user price rounds to
|
||||
// nearest — kinovi may exceed it by at most one cent.
|
||||
let artcraft_cents = cost_cents(2);
|
||||
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"batch 2: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4_matches_kinovi() {
|
||||
let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Four }
|
||||
.estimate_cost();
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost) while this artcraft user price rounds to
|
||||
// nearest — kinovi may exceed it by at most one cent.
|
||||
let artcraft_cents = cost_cents(4);
|
||||
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"batch 4: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -86,27 +86,27 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── USD pricing (243 credits per dollar; rounded up) ──
|
||||
// ── USD pricing (193 credits per dollar; rounded) ──
|
||||
|
||||
mod usd {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_one_is_five_cents() {
|
||||
// 12/243 × 100 = 4.94 → 5¢
|
||||
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::One), 5); // rounds UP
|
||||
fn batch_one_is_six_cents() {
|
||||
// 12 / 193 × 100 = 6.21 → 6¢
|
||||
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::One), 7); // rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_two_is_ten_cents() {
|
||||
// 24/243 × 100 = 9.88 → 10¢
|
||||
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Two), 10); // rounds UP
|
||||
fn batch_two_is_twelve_cents() {
|
||||
// 24 / 193 × 100 = 12.44 → 12¢
|
||||
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Two), 13); // rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_four_is_twenty_cents() {
|
||||
// 48/243 × 100 = 19.75 → 20¢
|
||||
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Four), 20);
|
||||
fn batch_four_is_twentyfive_cents() {
|
||||
// 48 / 193 × 100 = 24.87 → 25¢
|
||||
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Four), 25);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -56,11 +56,11 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_one_is_twelve_credits_and_five_cents() {
|
||||
fn batch_one_is_twelve_credits_and_six_cents() {
|
||||
let cost = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::One }
|
||||
.estimate_cost();
|
||||
assert_eq!(cost.cost_in_credits, Some(12));
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(5)); // 1200/243 = 4.94 -> rounds UP
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(7)); // 1200/193 = 6.22 -> rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -68,7 +68,7 @@ mod tests {
|
||||
let cost = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Two }
|
||||
.estimate_cost();
|
||||
assert_eq!(cost.cost_in_credits, Some(24));
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(10)); // 2400/243 = 9.88 -> rounds UP
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(13)); // 2400/193 = 12.44 -> rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -76,7 +76,7 @@ mod tests {
|
||||
let cost = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Four }
|
||||
.estimate_cost();
|
||||
assert_eq!(cost.cost_in_credits, Some(48));
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(20));
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+4
-4
@@ -56,11 +56,11 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_one_is_twelve_credits_and_five_cents() {
|
||||
fn batch_one_is_twelve_credits_and_six_cents() {
|
||||
let cost = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::One }
|
||||
.estimate_cost();
|
||||
assert_eq!(cost.cost_in_credits, Some(12));
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(5)); // 1200/243 = 4.94 -> rounds UP
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(7)); // 1200/193 = 6.22 -> rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -68,7 +68,7 @@ mod tests {
|
||||
let cost = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Two }
|
||||
.estimate_cost();
|
||||
assert_eq!(cost.cost_in_credits, Some(24));
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(10)); // 2400/243 = 9.88 -> rounds UP
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(13)); // 2400/193 = 12.44 -> rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -76,6 +76,6 @@ mod tests {
|
||||
let cost = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Four }
|
||||
.estimate_cost();
|
||||
assert_eq!(cost.cost_in_credits, Some(48));
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(20));
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(25));
|
||||
}
|
||||
}
|
||||
|
||||
+170
@@ -231,6 +231,176 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-provider margin: ArtCraft is consistently ~30% above GrokApi ──
|
||||
|
||||
mod small_margin {
|
||||
use super::*;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
|
||||
#[test]
|
||||
fn small_margin() {
|
||||
let resolutions = [
|
||||
None,
|
||||
Some(RouterResolution::FourEightyP),
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
Some(RouterResolution::TenEightyP), // clamps to 720p on both sides
|
||||
];
|
||||
let durations: &[u16] = &[1, 4, 5, 8, 10, 12, 15];
|
||||
let aspect_ratios = [
|
||||
None,
|
||||
Some(RouterAspectRatio::Auto),
|
||||
Some(RouterAspectRatio::Square),
|
||||
Some(RouterAspectRatio::WideSixteenByNine),
|
||||
Some(RouterAspectRatio::TallNineBySixteen),
|
||||
Some(RouterAspectRatio::WideFourByThree),
|
||||
Some(RouterAspectRatio::TallThreeByFour),
|
||||
Some(RouterAspectRatio::WideThreeByTwo),
|
||||
Some(RouterAspectRatio::TallTwoByThree),
|
||||
];
|
||||
// (has_start_frame, num_reference_images). At most one of the two is
|
||||
// non-default — start_frame and reference_images are mutually exclusive
|
||||
// on the GrokApi wire (image-to-video vs reference-to-video), so we
|
||||
// don't mix them here to keep the comparison apples-to-apples.
|
||||
let image_combos: &[(bool, usize)] = &[
|
||||
(false, 0), // text-to-video
|
||||
(true, 0), // image-to-video, 1 source image
|
||||
(false, 1), // reference-to-video, 1 ref image
|
||||
(false, 2),
|
||||
(false, 3),
|
||||
];
|
||||
|
||||
let mut total_cases = 0;
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for &res in &resolutions {
|
||||
for &dur in durations {
|
||||
for &ar in &aspect_ratios {
|
||||
for &(has_start, num_refs) in image_combos {
|
||||
total_cases += 1;
|
||||
let artcraft = artcraft_cents(res, dur, ar, has_start, num_refs);
|
||||
let grokapi = grokapi_cents(res, dur, ar, has_start, num_refs);
|
||||
|
||||
// Margin requirement: artcraft / max(grokapi - 1, 1) >= 1.29.
|
||||
// The (-1, 1) floor accounts for one cent of GrokApi-side
|
||||
// ceiling-rounding noise (see test doc comment above).
|
||||
let grokapi_compare = grokapi.saturating_sub(1).max(1);
|
||||
let lhs = artcraft.saturating_mul(100);
|
||||
let rhs = grokapi_compare.saturating_mul(129);
|
||||
|
||||
if lhs < rhs {
|
||||
let pct = if grokapi == 0 { 0.0 }
|
||||
else { (artcraft as f64 / grokapi as f64 - 1.0) * 100.0 };
|
||||
failures.push(format!(
|
||||
"res={:?} dur={}s ar={:?} start={} refs={} → \
|
||||
artcraft={}¢ vs grokapi={}¢ (margin {:.1}%, want ≥ 29% after 1¢ tolerance)",
|
||||
res, dur, ar, has_start, num_refs, artcraft, grokapi, pct,
|
||||
));
|
||||
}
|
||||
|
||||
// Defense in depth: ArtCraft must always cost at least as
|
||||
// much as GrokApi, regardless of rounding noise.
|
||||
assert!(
|
||||
artcraft >= grokapi,
|
||||
"ArtCraft ({artcraft}¢) cheaper than GrokApi ({grokapi}¢) for \
|
||||
res={res:?} dur={dur}s ar={ar:?} start={has_start} refs={num_refs}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
let summary = failures.iter().take(10).cloned().collect::<Vec<_>>().join("\n ");
|
||||
panic!(
|
||||
"{} / {} (res × duration × aspect × image_combo) tuples failed \
|
||||
the ≥29% (with 1¢ tolerance) margin check.\nFirst 10:\n {}",
|
||||
failures.len(), total_cases, summary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-provider helpers ──
|
||||
|
||||
fn artcraft_cents(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
aspect_ratio: Option<RouterAspectRatio>,
|
||||
has_start_frame: bool,
|
||||
num_reference_images: usize,
|
||||
) -> u64 {
|
||||
let start_frame = if has_start_frame {
|
||||
// ArtCraft's omni endpoint takes MediaFileTokens for image refs.
|
||||
Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string())))
|
||||
} else { None };
|
||||
let reference_images = if num_reference_images > 0 {
|
||||
Some(ImageListRef::MediaFileTokens(
|
||||
(0..num_reference_images)
|
||||
.map(|i| MediaFileToken::new(format!("mf_ref_{i}")))
|
||||
.collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
// Batch is fixed at 1 because GrokApi's per-request cost calculator
|
||||
// doesn't multiply by batch (Grok returns one video per call); the
|
||||
// markup test would compare apples to oranges otherwise.
|
||||
video_batch_count: Some(1),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 (artcraft) should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost (artcraft) should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn grokapi_cents(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
aspect_ratio: Option<RouterAspectRatio>,
|
||||
has_start_frame: bool,
|
||||
num_reference_images: usize,
|
||||
) -> u64 {
|
||||
let start_frame = if has_start_frame {
|
||||
// GrokApi (direct xAI) takes URLs for image refs.
|
||||
Some(ImageRef::Url("https://example.com/start.png".to_string()))
|
||||
} else { None };
|
||||
let reference_images = if num_reference_images > 0 {
|
||||
Some(ImageListRef::Urls(
|
||||
(0..num_reference_images)
|
||||
.map(|i| format!("https://example.com/ref_{i}.png"))
|
||||
.collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo,
|
||||
provider: RouterProvider::GrokApi,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(1),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 (grokapi) should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost (grokapi) should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn build_cost(
|
||||
|
||||
+179
@@ -144,6 +144,113 @@ mod tests {
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// ── Base cost parity with grok_api_client ─────────────────────────────
|
||||
//
|
||||
// The base cost (before ArtCraft markup, batch=1) must equal exactly what
|
||||
// grok_api_client's calculator returns for an equivalent direct-Grok
|
||||
// request. This protects against the artcraft and grok-api implementations
|
||||
// silently drifting apart.
|
||||
|
||||
mod grok_api_parity {
|
||||
use super::*;
|
||||
|
||||
// (resolution, duration, image_count)
|
||||
//
|
||||
// Durations are confined to [4, 15] because the ArtCraft pipeline clamps
|
||||
// outside that range (see `plan_duration` in artcraft/build_common.rs).
|
||||
// Every case carries exactly one image. v1.5 rejects text-to-video, AND
|
||||
// the GrokApi build step collapses multi-image reference-to-video
|
||||
// requests down to one `image` (promote-first; see grok_api side
|
||||
// build.rs). So the only request shape where both sides agree on the
|
||||
// input image count is the single-image case.
|
||||
const PARITY_CASES: &[(Option<RouterResolution>, u16, u64)] = &[
|
||||
// 480p variations
|
||||
(Some(RouterResolution::FourEightyP), 4, 1),
|
||||
(Some(RouterResolution::FourEightyP), 5, 1),
|
||||
(Some(RouterResolution::FourEightyP), 8, 1),
|
||||
(Some(RouterResolution::FourEightyP), 15, 1),
|
||||
// 720p variations
|
||||
(Some(RouterResolution::SevenTwentyP), 4, 1),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, 1),
|
||||
(Some(RouterResolution::SevenTwentyP), 8, 1),
|
||||
(Some(RouterResolution::SevenTwentyP), 15, 1),
|
||||
// None defaults to 720p in our `from_request`, which matches the
|
||||
// assumption Grok's calculator makes when fed a 720p request.
|
||||
(None, 5, 1),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn base_cost_for_one_video_matches_grok_api_client() {
|
||||
for &(router_res, duration, image_count) in PARITY_CASES {
|
||||
let artcraft = artcraft_state(router_res, duration, image_count, 1);
|
||||
let grok_cents = grok_cents(router_res, duration, image_count);
|
||||
|
||||
assert_eq!(
|
||||
artcraft.base_cost_in_cents_for_one_video(),
|
||||
grok_cents,
|
||||
"parity mismatch at res={router_res:?} dur={duration} images={image_count}: \
|
||||
artcraft_base={} grok={}",
|
||||
artcraft.base_cost_in_cents_for_one_video(),
|
||||
grok_cents,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_one_base_total_equals_one_video_base() {
|
||||
let s = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 1, 1);
|
||||
assert_eq!(s.base_cost_in_cents_for_batch(), s.base_cost_in_cents_for_one_video());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_scales_base_linearly() {
|
||||
// The ArtCraft pipeline only accepts batch counts of 1, 2, or 4 (see
|
||||
// `plan_batch_count` in artcraft/build_common.rs); anything else gets
|
||||
// clamped, so we can only meaningfully test those values.
|
||||
let per_one = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 1, 1)
|
||||
.base_cost_in_cents_for_one_video();
|
||||
for batch in [1u16, 2, 4] {
|
||||
let s = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 1, batch);
|
||||
assert_eq!(
|
||||
s.base_cost_in_cents_for_batch(),
|
||||
per_one * batch as u64,
|
||||
"batch={batch}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder-driven parity — exercises the full pipeline (build2 →
|
||||
/// estimate_cost) on both providers and compares ArtCraft's base cost
|
||||
/// (final / 1.05, accounting for ceiling) against GrokApi's direct
|
||||
/// cost.
|
||||
///
|
||||
/// We verify the markup rather than the base here because the public
|
||||
/// `estimate_cost()` returns the marked-up number; the explicit
|
||||
/// `base_cost_in_cents_*` API is tested above.
|
||||
#[test]
|
||||
fn end_to_end_markup_is_at_least_five_percent_over_grok() {
|
||||
// Single-image cases only — see PARITY_CASES comment above for why
|
||||
// multi-image rows can't be apples-to-apples between the two sides.
|
||||
let cases: &[(Option<RouterResolution>, u16, bool, usize)] = &[
|
||||
(Some(RouterResolution::FourEightyP), 5, true, 0),
|
||||
(Some(RouterResolution::FourEightyP), 10, true, 0),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, true, 0),
|
||||
(Some(RouterResolution::SevenTwentyP), 8, true, 0),
|
||||
(Some(RouterResolution::SevenTwentyP),15, true, 0),
|
||||
];
|
||||
for &(res, duration, has_start, refs) in cases {
|
||||
let artcraft_cents = artcraft_end_to_end_cents(res, duration, 1, has_start, refs);
|
||||
let grok_cents = grok_cents(res, duration, has_start as u64 + refs as u64);
|
||||
let want_minimum = grok_cents.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
|
||||
assert_eq!(
|
||||
artcraft_cents, want_minimum,
|
||||
"res={res:?} dur={duration} start={has_start} refs={refs}: \
|
||||
artcraft={artcraft_cents}¢ vs ceil(grok({grok_cents})×1.05)={want_minimum}¢",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Markup correctness ─────────────────────────────────────────────────
|
||||
|
||||
mod markup {
|
||||
@@ -377,6 +484,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn artcraft_end_to_end_cents(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
batch_count: u16,
|
||||
has_start_frame: bool,
|
||||
num_reference_images: usize,
|
||||
) -> u64 {
|
||||
let start_frame = if has_start_frame {
|
||||
Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string())))
|
||||
} else { None };
|
||||
let reference_images = if num_reference_images > 0 {
|
||||
Some(ImageListRef::MediaFileTokens(
|
||||
(0..num_reference_images)
|
||||
.map(|i| MediaFileToken::new(format!("mf_ref_{i}")))
|
||||
.collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo1p5,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(batch_count),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn build_cost_via_builder(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
@@ -399,4 +542,40 @@ mod tests {
|
||||
.expect("estimate_cost should succeed")
|
||||
}
|
||||
|
||||
/// Reference value: what GrokApi's own pipeline would charge for one
|
||||
/// equivalent video. Used by parity tests.
|
||||
///
|
||||
/// The Grok build step prefers `start_frame` over `reference_images` when
|
||||
/// both are supplied and drops the latter (see grok_api/grok_imagine_video_1p5/build.rs).
|
||||
/// To keep parity with the ArtCraft input-image *count*, we put all source
|
||||
/// images in `reference_images` only — the cost calculator counts the total
|
||||
/// either way.
|
||||
fn grok_cents(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
image_count: u64,
|
||||
) -> u64 {
|
||||
let reference_images = if image_count >= 1 {
|
||||
Some(ImageListRef::Urls(
|
||||
(0..image_count).map(|i| format!("https://example.com/ref_{i}.png")).collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo1p5,
|
||||
provider: RouterProvider::GrokApi,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(1),
|
||||
start_frame: None,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 (grokapi) should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost (grokapi) should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
+51
-4
@@ -14,16 +14,13 @@ use crate::generate::generate_video::providers::artcraft::happy_horse_1p0::reque
|
||||
//
|
||||
// Default resolution (None) is 720p.
|
||||
// Batch count multiplies the total cost.
|
||||
// Credit package (historical): 22,000 credits for $114 (~193 credits/$1).
|
||||
// The upstream package is now 525,000 credits for $2,159.0909 (~243 credits/$1),
|
||||
// but user pricing is intentionally kept at the 193-derived rates.
|
||||
// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193).
|
||||
//
|
||||
// ArtCraft credits: 100 credits = $1.00, so ArtCraft credits = USD cents.
|
||||
// We compute USD cents from upstream credits ÷ credits_per_dollar × 100.
|
||||
|
||||
const UPSTREAM_CREDITS_PER_SECOND_720P: u32 = 33;
|
||||
const UPSTREAM_CREDITS_PER_SECOND_1080P: u32 = 66;
|
||||
// Frozen at the historical rate so user pricing doesn't shift with the package deal.
|
||||
const UPSTREAM_CREDITS_PER_DOLLAR: f64 = 193.0;
|
||||
|
||||
pub struct ArtcraftHappyHorse1p0CostState {
|
||||
@@ -204,6 +201,56 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-check with Kinovi ──
|
||||
|
||||
mod cross_check_with_kinovi {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn artcraft_matches_kinovi_all_combos() {
|
||||
let resolutions = [
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
None,
|
||||
Some(RouterResolution::TenEightyP),
|
||||
];
|
||||
let durations: [u16; 4] = [4, 5, 10, 15];
|
||||
let batches: [u16; 3] = [1, 2, 4];
|
||||
|
||||
for res in &resolutions {
|
||||
for dur in &durations {
|
||||
for batch in &batches {
|
||||
let artcraft_cost = build_cost(*res, *dur, *batch);
|
||||
|
||||
let kinovi = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::HappyHorse1p0,
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
resolution: *res,
|
||||
duration_seconds: Some(*dur),
|
||||
video_batch_count: Some(*batch),
|
||||
..Default::default()
|
||||
};
|
||||
let kinovi_cost = kinovi.build2()
|
||||
.expect("kinovi build2")
|
||||
.estimate_cost()
|
||||
.expect("kinovi estimate_cost");
|
||||
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost), while the artcraft user-facing price
|
||||
// here rounds to nearest — so kinovi may exceed artcraft by at
|
||||
// most one cent.
|
||||
let artcraft_cents = artcraft_cost.cost_in_usd_cents.expect("artcraft cents");
|
||||
let kinovi_cents = kinovi_cost.cost_in_usd_cents.expect("kinovi cents");
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"USD cents mismatch: res={:?} dur={}s batch={} (artcraft={}, kinovi={})",
|
||||
res, dur, batch, artcraft_cents, kinovi_cents,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn build_cost(
|
||||
|
||||
+51
-4
@@ -18,10 +18,6 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0::request:
|
||||
///
|
||||
/// 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.
|
||||
const CENTS_PER_SECOND_480P: f64 = 7.772;
|
||||
const CENTS_PER_SECOND_720P: f64 = 16.0;
|
||||
const CENTS_PER_SECOND_1080P: f64 = 46.632;
|
||||
@@ -218,6 +214,57 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Cross-check with Kinovi via builder --
|
||||
|
||||
mod cross_check_with_kinovi {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn artcraft_matches_kinovi_all_combos() {
|
||||
let resolutions = [
|
||||
Some(RouterResolution::FourEightyP),
|
||||
Some(RouterResolution::TenEightyP),
|
||||
// NB: These are underpriced (for now)
|
||||
//Some(RouterResolution::SevenTwentyP),
|
||||
//None,
|
||||
];
|
||||
let durations: [u16; 4] = [4, 5, 10, 15];
|
||||
let batches: [u16; 3] = [1, 2, 4];
|
||||
|
||||
for res in &resolutions {
|
||||
for dur in &durations {
|
||||
for batch in &batches {
|
||||
let artcraft_cost = build_cost(*res, *dur, *batch);
|
||||
|
||||
let kinovi = GenerateVideoRequestBuilder {
|
||||
provider: RouterProvider::Seedance2Pro,
|
||||
resolution: *res,
|
||||
duration_seconds: Some(*dur),
|
||||
video_batch_count: Some(*batch),
|
||||
..Default::default()
|
||||
};
|
||||
let kinovi_cost = kinovi.build2()
|
||||
.expect("kinovi build2")
|
||||
.estimate_cost()
|
||||
.expect("kinovi estimate_cost");
|
||||
|
||||
// NB: The kinovi provider estimate rounds fractional cents UP
|
||||
// (KinoviGenerationCost), while the artcraft user-facing price
|
||||
// here rounds to nearest — so kinovi may exceed artcraft by at
|
||||
// most one cent.
|
||||
let artcraft_cents = artcraft_cost.cost_in_usd_cents.expect("artcraft cents");
|
||||
let kinovi_cents = kinovi_cost.cost_in_usd_cents.expect("kinovi cents");
|
||||
assert!(
|
||||
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
|
||||
"USD cents mismatch: res={:?} dur={}s batch={} (artcraft={}, kinovi={})",
|
||||
res, dur, batch, artcraft_cents, kinovi_cents,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
/// Build a cost estimate end-to-end: GenerateVideoRequestBuilder → build2 → estimate_cost.
|
||||
|
||||
-2
@@ -154,8 +154,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
|
||||
-2
@@ -125,8 +125,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
|
||||
-2
@@ -154,8 +154,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
|
||||
-2
@@ -125,8 +125,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
|
||||
+44
-2
@@ -13,8 +13,6 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_fast::req
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// We keep these as f64 because per-second rates are fractional; rounding
|
||||
@@ -186,6 +184,50 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Cross-check with Kinovi Fast via builder --
|
||||
|
||||
//mod cross_check_with_kinovi {
|
||||
// use super::*;
|
||||
//
|
||||
// #[test]
|
||||
// fn artcraft_fast_matches_kinovi_fast_all_combos() {
|
||||
// let resolutions = [
|
||||
// Some(RouterResolution::FourEightyP),
|
||||
// Some(RouterResolution::SevenTwentyP),
|
||||
// None,
|
||||
// ];
|
||||
// let durations: [u16; 4] = [4, 5, 10, 15];
|
||||
// let batches: [u16; 3] = [1, 2, 4];
|
||||
//
|
||||
// for res in &resolutions {
|
||||
// for dur in &durations {
|
||||
// for batch in &batches {
|
||||
// let artcraft_cost = build_cost(*res, *dur, *batch);
|
||||
//
|
||||
// let kinovi = GenerateVideoRequestBuilder {
|
||||
// model: RouterVideoModel::Seedance2p0Fast,
|
||||
// provider: RouterProvider::Seedance2Pro,
|
||||
// resolution: *res,
|
||||
// duration_seconds: Some(*dur),
|
||||
// video_batch_count: Some(*batch),
|
||||
// ..Default::default()
|
||||
// };
|
||||
// let kinovi_cost = kinovi.build2()
|
||||
// .expect("kinovi build2")
|
||||
// .estimate_cost()
|
||||
// .expect("kinovi estimate_cost");
|
||||
//
|
||||
// assert_eq!(
|
||||
// artcraft_cost.cost_in_usd_cents, kinovi_cost.cost_in_usd_cents,
|
||||
// "USD cents mismatch: res={:?} dur={}s batch={}",
|
||||
// res, dur, batch,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
fn build_cost(
|
||||
|
||||
-2
@@ -159,8 +159,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
|
||||
-2
@@ -144,8 +144,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
|
||||
+9
-7
@@ -98,8 +98,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn usd_cents_720p() {
|
||||
assert_eq!(usd_cents(None, 5, None), 68); // 16500/243 = 67.90 -> rounds UP
|
||||
assert_eq!(usd_cents(None, 15, None), 204); // 49500/243 = 203.70 -> rounds UP
|
||||
// 165 credits / 193 * 100 = 85.49 → 85¢
|
||||
assert_eq!(usd_cents(None, 5, None), 86); // 16500/193 = 85.49 -> rounds UP
|
||||
// 495 credits / 193 * 100 = 256.48 → 256¢
|
||||
assert_eq!(usd_cents(None, 15, None), 257); // 49500/193 = 256.48 -> rounds UP
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +131,10 @@ mod tests {
|
||||
#[test]
|
||||
fn usd_cents_1080p() {
|
||||
let r = Some(KinoviHappyHorse1p0OutputResolution::TenEightyP);
|
||||
// 330 credits; 33000/243 = 135.80 → rounds UP to 136¢
|
||||
assert_eq!(usd_cents(r, 5, None), 136);
|
||||
// 990 credits; 99000/243 = 407.41 → rounds UP to 408¢
|
||||
assert_eq!(usd_cents(r, 15, None), 408);
|
||||
// 330 credits / 193 * 100 = 170.98 → 171¢
|
||||
assert_eq!(usd_cents(r, 5, None), 171);
|
||||
// 990 credits / 193 * 100 = 512.95 → 513¢
|
||||
assert_eq!(usd_cents(r, 15, None), 513);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +238,7 @@ mod tests {
|
||||
);
|
||||
let cost = KinoviHappyHorse1p0CostState::from_request(&req);
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(330));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(136));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(171));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-38
@@ -66,8 +66,8 @@ impl KinoviSeedance2p0CostState {
|
||||
};
|
||||
|
||||
let costs = pricing_request.calculate_costs();
|
||||
let cost_in_credits = costs.total_cost.kinovi_credits;
|
||||
let cost_in_usd_cents = costs.total_cost.usd_cents_rounded_up;
|
||||
let cost_in_credits = costs.kinovi_credits;
|
||||
let cost_in_usd_cents = costs.usd_cents_rounded_up;
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_credits),
|
||||
@@ -103,26 +103,26 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cost_720p_batch_1() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 66);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 83);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 6, KinoviBatchCount::One), 99);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 7, KinoviBatchCount::One), 116);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 165);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 247);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 83);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 104);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 6, KinoviBatchCount::One), 125);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 7, KinoviBatchCount::One), 146);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 208);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 311);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_720p_batch_2() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Two), 132);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 165);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Two), 494);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Two), 166);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 208);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Two), 622);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_720p_batch_4() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Four), 264);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 330);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Four), 988);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Four), 332);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 415);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Four), 1244);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,20 +133,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cost_480p_batch_1() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 25);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 31);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 62);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 93);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 32);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 39);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 78);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 117);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_480p_batch_2() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 62);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 78);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_480p_batch_4() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 124);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 156);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,20 +157,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cost_1080p_batch_1() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 4, KinoviBatchCount::One), 149);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 186);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 10, KinoviBatchCount::One), 371);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 15, KinoviBatchCount::One), 556);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 4, KinoviBatchCount::One), 187);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 234);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 10, KinoviBatchCount::One), 467);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 15, KinoviBatchCount::One), 700);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_1080p_batch_2() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two), 371);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two), 467);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_1080p_batch_4() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Four), 741);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Four), 933);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,10 +219,10 @@ mod tests {
|
||||
};
|
||||
let without = base.estimate_cost();
|
||||
let with = KinoviSeedance2p0CostState { has_video_reference: true, ..base }.estimate_cost();
|
||||
// 720p surcharge is +8 credits/s: 200 -> 240 credits (24000/243 = 98.77 -> 99 cents).
|
||||
// 720p surcharge is +8 credits/s: 200 -> 240 credits (24000/193 = 124.35 -> 125 cents).
|
||||
assert_eq!(without.cost_in_credits, Some(200));
|
||||
assert_eq!(with.cost_in_credits, Some(240));
|
||||
assert_eq!(with.cost_in_usd_cents, Some(99));
|
||||
assert_eq!(with.cost_in_usd_cents, Some(125));
|
||||
}
|
||||
|
||||
// ── from_request() tests ──
|
||||
@@ -238,7 +238,7 @@ mod tests {
|
||||
assert_eq!(cost.duration_seconds, 5);
|
||||
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One)));
|
||||
assert!(!cost.has_video_reference);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(83));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(104));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -246,21 +246,21 @@ mod tests {
|
||||
let req = make_request_state(None, 5, KinoviBatchCount::One, false);
|
||||
let cost = KinoviSeedance2p0CostState::from_request(&req);
|
||||
assert!(cost.resolution.is_none()); // None defaults to 720p pricing
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(83));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(104));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_request_480p() {
|
||||
let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, false);
|
||||
let cost = KinoviSeedance2p0CostState::from_request(&req);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(31));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(39));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_request_1080p_batch_2() {
|
||||
let req = make_request_state(Some(KinoviOutputResolution::TenEightyP), 5, KinoviBatchCount::Two, false);
|
||||
let cost = KinoviSeedance2p0CostState::from_request(&req);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(371));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(467));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -270,7 +270,7 @@ mod tests {
|
||||
assert!(cost.has_video_reference);
|
||||
// Video refs add a +8 credits/s surcharge at 720p: 240 credits -> 125 cents.
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(240));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(99));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(125));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -294,7 +294,7 @@ mod tests {
|
||||
assert_eq!(cost.duration_seconds, 5);
|
||||
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One)));
|
||||
assert!(!cost.has_video_reference);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(83));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(104));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -302,7 +302,7 @@ mod tests {
|
||||
let draft = make_draft(5, 1, Some(RouterResolution::FourEightyP), false);
|
||||
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
|
||||
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourEightyP)));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(31));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(39));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -311,7 +311,7 @@ mod tests {
|
||||
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
|
||||
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::TenEightyP)));
|
||||
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::Four)));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(741));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(933));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -321,7 +321,7 @@ mod tests {
|
||||
assert!(cost.has_video_reference);
|
||||
// Video refs add a +8 credits/s surcharge at 720p: 240 credits -> 125 cents.
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(240));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(99));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(125));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -337,7 +337,7 @@ mod tests {
|
||||
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
|
||||
assert_eq!(cost.duration_seconds, 15);
|
||||
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::Two)));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(494));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(622));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-26
@@ -66,8 +66,8 @@ impl KinoviSeedance2p0FastCostState {
|
||||
};
|
||||
|
||||
let costs = pricing_request.calculate_costs();
|
||||
let cost_in_credits = costs.total_cost.kinovi_credits;
|
||||
let cost_in_usd_cents = costs.total_cost.usd_cents_rounded_up;
|
||||
let cost_in_credits = costs.kinovi_credits;
|
||||
let cost_in_usd_cents = costs.usd_cents_rounded_up;
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_credits),
|
||||
@@ -100,51 +100,51 @@ mod tests {
|
||||
// ── Direct estimate_cost() tests ──
|
||||
// These must match the reference implementation values exactly.
|
||||
|
||||
// -- 720p (28 credits/sec, 243 credits/$1) --
|
||||
// -- 720p (28 credits/sec, 193 credits/$1) --
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_720p_batch_1() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 47);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 58);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 116);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 173);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 59);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 73);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 146);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 218);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_720p_batch_2() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 116);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 146);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_720p_batch_4() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 231);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 291);
|
||||
}
|
||||
}
|
||||
|
||||
// -- 480p (14 credits/sec, 243 credits/$1) --
|
||||
// -- 480p (14 credits/sec, 193 credits/$1) --
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_480p_batch_1() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 24);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 29);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 58);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 87);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 30);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 37);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 73);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 109);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_480p_batch_2() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 58);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 73);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_480p_batch_4() {
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 116);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 146);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,10 +191,10 @@ mod tests {
|
||||
};
|
||||
let without = base.estimate_cost();
|
||||
let with = KinoviSeedance2p0FastCostState { has_video_reference: true, ..base }.estimate_cost();
|
||||
// Fast 720p surcharge is +6 credits/s: 140 -> 170 credits (17000/243 = 69.96 -> 70 cents).
|
||||
// Fast 720p surcharge is +6 credits/s: 140 -> 170 credits (17000/193 = 88.08 -> 89 cents).
|
||||
assert_eq!(without.cost_in_credits, Some(140));
|
||||
assert_eq!(with.cost_in_credits, Some(170));
|
||||
assert_eq!(with.cost_in_usd_cents, Some(70));
|
||||
assert_eq!(with.cost_in_usd_cents, Some(89));
|
||||
}
|
||||
|
||||
// ── from_request() tests ──
|
||||
@@ -210,7 +210,7 @@ mod tests {
|
||||
assert_eq!(cost.duration_seconds, 5);
|
||||
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One)));
|
||||
assert!(!cost.has_video_reference);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(58));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(73));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -218,14 +218,14 @@ mod tests {
|
||||
let req = make_request_state(None, 5, KinoviBatchCount::One, false);
|
||||
let cost = KinoviSeedance2p0FastCostState::from_request(&req);
|
||||
assert!(cost.resolution.is_none());
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(58));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(73));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_request_480p() {
|
||||
let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, false);
|
||||
let cost = KinoviSeedance2p0FastCostState::from_request(&req);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(29));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(37));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -235,7 +235,7 @@ mod tests {
|
||||
assert!(cost.has_video_reference);
|
||||
// Video refs add a +6 credits/s surcharge at 720p: 170 credits -> 89 cents.
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(170));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(70));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(89));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -259,7 +259,7 @@ mod tests {
|
||||
assert_eq!(cost.duration_seconds, 5);
|
||||
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One)));
|
||||
assert!(!cost.has_video_reference);
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(58));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(73));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -267,7 +267,7 @@ mod tests {
|
||||
let draft = make_draft(5, 1, Some(RouterResolution::FourEightyP), false);
|
||||
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
|
||||
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourEightyP)));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(29));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(37));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -277,7 +277,7 @@ mod tests {
|
||||
assert!(cost.has_video_reference);
|
||||
// Video refs add a +6 credits/s surcharge at 720p: 170 credits -> 89 cents.
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(170));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(70));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(89));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -293,7 +293,7 @@ mod tests {
|
||||
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
|
||||
assert_eq!(cost.duration_seconds, 15);
|
||||
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::Two)));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(346));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(436));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
Cost table built on 2026-06-11
|
||||
|
||||
Seedance 2.0 (~$0.19/sec)
|
||||
Seedance 2.0 Fast (~$0.13/sec)
|
||||
|
||||
Without Video Uploads
|
||||
=====================
|
||||
|
||||
### Seedance 2.0
|
||||
|
||||
4 sec 480p = 60 credits
|
||||
5 sec 480p = 75 credits
|
||||
10 sec 480p = 150 credits
|
||||
15 sec 480p = 225 credits
|
||||
|
||||
4 sec 720p = 160 credits
|
||||
5 sec 720p = 200 credits
|
||||
10 sec 720p = 400 credits
|
||||
15 sec 720p = 600 credits
|
||||
|
||||
4 sec 1080p = 360 credits
|
||||
5 sec 1080p = 450 credits
|
||||
10 sec 1080p = 900 credits
|
||||
15 sec 1080p = 1350 credits
|
||||
|
||||
### Seedance 2.0 Fast
|
||||
|
||||
fast 4 sec 480p = 56 credits
|
||||
fast 5 sec 480p = 70 credits
|
||||
fast 10 sec 480p = 140 credits
|
||||
fast 15 sec 480p = 210 credits
|
||||
|
||||
fast 4 sec 720p = 112 credits
|
||||
fast 5 sec 720p = 140 credits
|
||||
fast 10 sec 720p = 280 credits
|
||||
fast 15 sec 720p = 420 credits
|
||||
|
||||
With Video Uploads
|
||||
==================
|
||||
|
||||
### Seedance 2.0 With Video Refs
|
||||
|
||||
4 sec 480p + 10 sec video ref = 60 credits + 16 credits
|
||||
5 sec 480p + 10 sec video ref = 75 credits + 20 credits
|
||||
10 sec 480p + 10 sec video ref = 150 credits + 40 credits
|
||||
15 sec 480p + 10 sec video ref = 225 credits + 60 credits
|
||||
|
||||
4 sec 720p + 10 sec video ref = 160 credits + 32 credits
|
||||
5 sec 720p + 10 sec video ref = 200 credits + 40 credits
|
||||
10 sec 720p + 10 sec video ref = 400 credits + 80 credits
|
||||
15 sec 720p + 10 sec video ref = 600 credits + 120 credits
|
||||
|
||||
4 sec 1080p + 10 sec video ref = 360 credits + 72 credits
|
||||
5 sec 1080p + 10 sec video ref = 450 credits + 90 credits
|
||||
10 sec 1080p + 10 sec video ref = 900 credits + 180 credits
|
||||
15 sec 1080p + 10 sec video ref = 1350 credits + 270 credits
|
||||
|
||||
### Seedance 2.0 Fast With Video Refs
|
||||
|
||||
fast 4 sec 480p + 10 sec video ref = 56 credits + 16 credits
|
||||
fast 5 sec 480p + 10 sec video ref = 70 credits + 20 credits
|
||||
fast 10 sec 480p + 10 sec video ref = 140 credits + 40 credits
|
||||
fast 15 sec 480p + 10 sec video ref = 210 credits + 60 credits
|
||||
|
||||
fast 4 sec 720p + 10 sec video ref = 112 credits + 24 credits
|
||||
fast 5 sec 720p + 10 sec video ref = 140 credits + 30 credits
|
||||
fast 10 sec 720p + 10 sec video ref = 280 credits + 60 credits
|
||||
fast 15 sec 720p + 10 sec video ref = 420 credits + 90 credits
|
||||
@@ -1,9 +0,0 @@
|
||||
/// Kinovi credits per US dollar.
|
||||
///
|
||||
/// Current package: 525,000 credits for $2,159.0909 (~243.16 credits/$1,
|
||||
/// rounded down to 243).
|
||||
///
|
||||
/// Historical packages, for reference:
|
||||
/// - 500,000 credits for $2,159.09 (~231.58 credits/$1, rounded down to 231)
|
||||
/// - 22,000 credits for $114 (~192.98 credits/$1, rounded to 193)
|
||||
pub const CREDITS_PER_DOLLAR: u64 = 243;
|
||||
@@ -1,90 +0,0 @@
|
||||
use crate::cost::constants::CREDITS_PER_DOLLAR;
|
||||
|
||||
/// The cost of a Kinovi generation, in both native credits and USD cents.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KinoviGenerationCost {
|
||||
/// The cost of the generation in Kinovi credits.
|
||||
pub kinovi_credits: u64,
|
||||
|
||||
/// Estimated cost in cents, rounded up to the nearest whole cent.
|
||||
/// This does not account for discounts, prorations, etc.
|
||||
/// This is always rounded up when fractional.
|
||||
pub usd_cents_rounded_up: u64,
|
||||
|
||||
/// Estimated cost in cents, rounded down to the nearest whole cent.
|
||||
pub usd_cents_rounded_down: u64,
|
||||
|
||||
/// Estimated cost in cents without rounding (the exact fractional value).
|
||||
pub usd_cents_fractional: f64,
|
||||
}
|
||||
|
||||
impl KinoviGenerationCost {
|
||||
/// Build a cost from a Kinovi credit amount, deriving the USD cents via
|
||||
/// the credit-package rate.
|
||||
pub fn from_kinovi_credits(kinovi_credits: u64) -> Self {
|
||||
let total_hundredths = kinovi_credits * 100;
|
||||
Self {
|
||||
kinovi_credits,
|
||||
// Integer ceiling / floor of (credits * 100 / CREDITS_PER_DOLLAR).
|
||||
usd_cents_rounded_up: total_hundredths.div_ceil(CREDITS_PER_DOLLAR),
|
||||
usd_cents_rounded_down: total_hundredths / CREDITS_PER_DOLLAR,
|
||||
usd_cents_fractional: total_hundredths as f64 / CREDITS_PER_DOLLAR as f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const FLOAT_TOLERANCE: f64 = 1e-9;
|
||||
|
||||
#[test]
|
||||
fn zero_credits_is_zero_cents() {
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(0);
|
||||
assert_eq!(cost.kinovi_credits, 0);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 0);
|
||||
assert_eq!(cost.usd_cents_rounded_down, 0);
|
||||
assert!((cost.usd_cents_fractional - 0.0).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_dollar_boundaries_do_not_round() {
|
||||
// 243 credits = exactly $1.00 — all three representations agree.
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(243);
|
||||
assert_eq!(cost.kinovi_credits, 243);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 100);
|
||||
assert_eq!(cost.usd_cents_rounded_down, 100);
|
||||
assert!((cost.usd_cents_fractional - 100.0).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(486);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 200);
|
||||
assert_eq!(cost.usd_cents_rounded_down, 200);
|
||||
assert!((cost.usd_cents_fractional - 200.0).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fractional_cents_round_in_both_directions() {
|
||||
// 244 credits = 100.411... cents.
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(244);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 101);
|
||||
assert_eq!(cost.usd_cents_rounded_down, 100);
|
||||
assert!((cost.usd_cents_fractional - (24400.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// 1 credit = 0.432... cents.
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(1);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 1);
|
||||
assert_eq!(cost.usd_cents_rounded_down, 0);
|
||||
assert!((cost.usd_cents_fractional - (100.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounded_bounds_bracket_the_fractional_value() {
|
||||
for credits in [0, 1, 12, 75, 200, 243, 244, 450, 1350] {
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(credits);
|
||||
assert!(cost.usd_cents_rounded_down as f64 <= cost.usd_cents_fractional);
|
||||
assert!(cost.usd_cents_fractional <= cost.usd_cents_rounded_up as f64);
|
||||
assert!(cost.usd_cents_rounded_up - cost.usd_cents_rounded_down <= 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
use crate::cost::kinovi_generation_cost::KinoviGenerationCost;
|
||||
|
||||
/// The cost of a Kinovi Seedance generation, with the video-reference
|
||||
/// surcharge broken out from the base price.
|
||||
///
|
||||
/// `total_cost` covers base + surcharge. NB: the total's USD conversions are
|
||||
/// computed from the SUMMED credits (rounded once), so they may differ by a
|
||||
/// cent from adding the parts' rounded USD values together.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KinoviSeedanceGenerationCost {
|
||||
/// The full cost (base + any surcharge), in credits and USD cents.
|
||||
pub total_cost: KinoviGenerationCost,
|
||||
|
||||
/// The base generation cost (resolution rate × output duration × batch
|
||||
/// count), excluding any surcharges.
|
||||
pub base_cost: KinoviGenerationCost,
|
||||
|
||||
/// The video-reference surcharge, when one or more reference videos are
|
||||
/// attached. `None` when no reference videos are attached.
|
||||
pub video_reference_surcharge_cost: Option<KinoviGenerationCost>,
|
||||
}
|
||||
|
||||
impl KinoviSeedanceGenerationCost {
|
||||
/// Build a cost from base credits plus an optional video-reference
|
||||
/// surcharge (both in Kinovi credits). The per-part and total USD
|
||||
/// conversions are derived.
|
||||
pub fn from_base_and_surcharge(
|
||||
base_credits: u64,
|
||||
maybe_video_reference_surcharge_credits: Option<u64>,
|
||||
) -> Self {
|
||||
let total_credits = base_credits + maybe_video_reference_surcharge_credits.unwrap_or(0);
|
||||
Self {
|
||||
total_cost: KinoviGenerationCost::from_kinovi_credits(total_credits),
|
||||
base_cost: KinoviGenerationCost::from_kinovi_credits(base_credits),
|
||||
video_reference_surcharge_cost: maybe_video_reference_surcharge_credits
|
||||
.map(KinoviGenerationCost::from_kinovi_credits),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const FLOAT_TOLERANCE: f64 = 1e-9;
|
||||
|
||||
#[test]
|
||||
fn without_surcharge() {
|
||||
let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(200, None);
|
||||
|
||||
assert!(cost.video_reference_surcharge_cost.is_none());
|
||||
|
||||
// Base: 200 credits; 20000/243 = 82.30.
|
||||
assert_eq!(cost.base_cost.kinovi_credits, 200);
|
||||
assert_eq!(cost.base_cost.usd_cents_rounded_up, 83);
|
||||
assert_eq!(cost.base_cost.usd_cents_rounded_down, 82);
|
||||
assert!((cost.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Total equals base when there's no surcharge.
|
||||
assert_eq!(cost.total_cost, cost.base_cost);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_surcharge() {
|
||||
let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(200, Some(40));
|
||||
|
||||
// Base: 200 credits; 20000/243 = 82.30.
|
||||
assert_eq!(cost.base_cost.kinovi_credits, 200);
|
||||
assert_eq!(cost.base_cost.usd_cents_rounded_up, 83);
|
||||
assert_eq!(cost.base_cost.usd_cents_rounded_down, 82);
|
||||
assert!((cost.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Surcharge: 40 credits; 4000/243 = 16.46.
|
||||
let surcharge = cost.video_reference_surcharge_cost.expect("should have surcharge");
|
||||
assert_eq!(surcharge.kinovi_credits, 40);
|
||||
assert_eq!(surcharge.usd_cents_rounded_up, 17);
|
||||
assert_eq!(surcharge.usd_cents_rounded_down, 16);
|
||||
assert!((surcharge.usd_cents_fractional - (4000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Total: 240 credits; 24000/243 = 98.77.
|
||||
assert_eq!(cost.total_cost.kinovi_credits, 240);
|
||||
assert_eq!(cost.total_cost.usd_cents_rounded_up, 99);
|
||||
assert_eq!(cost.total_cost.usd_cents_rounded_down, 98);
|
||||
assert!((cost.total_cost.usd_cents_fractional - (24000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_credits_are_base_plus_surcharge() {
|
||||
let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(450, Some(90));
|
||||
assert_eq!(
|
||||
cost.total_cost.kinovi_credits,
|
||||
cost.base_cost.kinovi_credits + cost.video_reference_surcharge_cost.unwrap().kinovi_credits);
|
||||
}
|
||||
|
||||
/// The total's USD is rounded ONCE on the summed credits, so it can be a
|
||||
/// cent under the sum of the parts' rounded-up values.
|
||||
#[test]
|
||||
fn total_usd_is_rounded_once_not_per_part() {
|
||||
let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(200, Some(40));
|
||||
let surcharge = cost.video_reference_surcharge_cost.unwrap();
|
||||
|
||||
// Parts round up to 83 + 17 = 100, but the total (240 credits) rounds
|
||||
// once to 99 — one cent less than summing the rounded parts.
|
||||
assert_eq!(cost.base_cost.usd_cents_rounded_up + surcharge.usd_cents_rounded_up, 100);
|
||||
assert_eq!(cost.total_cost.usd_cents_rounded_up, 99);
|
||||
|
||||
// …but the fractional parts always sum exactly to the fractional total.
|
||||
let parts = cost.base_cost.usd_cents_fractional + surcharge.usd_cents_fractional;
|
||||
assert!((parts - cost.total_cost.usd_cents_fractional).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod constants;
|
||||
pub mod kinovi_generation_cost;
|
||||
pub mod kinovi_seedance_generation_cost;
|
||||
@@ -0,0 +1,61 @@
|
||||
/// Kinovi credits per US dollar.
|
||||
/// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193).
|
||||
const CREDITS_PER_DOLLAR: u64 = 193;
|
||||
|
||||
/// The cost of a Kinovi generation, in both native credits and USD cents.
|
||||
pub struct KinoviGenerationCost {
|
||||
/// The cost of the generation in Kinovi credits.
|
||||
pub kinovi_credits: u64,
|
||||
|
||||
/// Estimated cost in cents, rounded up to the nearest whole cent.
|
||||
/// This does not account for discounts, prorations, etc.
|
||||
/// This is always rounded up when fractional.
|
||||
pub usd_cents_rounded_up: u64,
|
||||
}
|
||||
|
||||
impl KinoviGenerationCost {
|
||||
/// Build a cost from a Kinovi credit amount, deriving the USD cents via
|
||||
/// the credit-package rate (always rounding up fractional cents).
|
||||
pub fn from_kinovi_credits(kinovi_credits: u64) -> Self {
|
||||
// Integer ceiling of (credits * 100 / CREDITS_PER_DOLLAR).
|
||||
let usd_cents_rounded_up = (kinovi_credits * 100).div_ceil(CREDITS_PER_DOLLAR);
|
||||
Self {
|
||||
kinovi_credits,
|
||||
usd_cents_rounded_up,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn zero_credits_is_zero_cents() {
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(0);
|
||||
assert_eq!(cost.kinovi_credits, 0);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_dollar_boundaries_do_not_round() {
|
||||
// 193 credits = exactly $1.00.
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(193);
|
||||
assert_eq!(cost.kinovi_credits, 193);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 100);
|
||||
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(386);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fractional_cents_round_up() {
|
||||
// 194 credits = 100.518... cents -> 101.
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(194);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 101);
|
||||
|
||||
// 1 credit = 0.518... cents -> 1.
|
||||
let cost = KinoviGenerationCost::from_kinovi_credits(1);
|
||||
assert_eq!(cost.usd_cents_rounded_up, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::cost::kinovi_generation_cost::KinoviGenerationCost;
|
||||
use crate::generate::cost::KinoviGenerationCost;
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::requests::generate_image::generate_image::{
|
||||
@@ -288,17 +288,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_batch_one_is_five() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 5); // 1200/243 = 4.94 -> rounds UP
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_down, 4);
|
||||
assert!((make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_fractional - (1200.0 / 243.0)).abs() < 1e-9);
|
||||
fn usd_cents_batch_one_is_six() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_batch_four_is_twenty() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 20); // 4800/243 = 19.75 -> rounds UP
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_down, 19);
|
||||
assert!((make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_fractional - (4800.0 / 243.0)).abs() < 1e-9);
|
||||
fn usd_cents_batch_four_is_twentyfive() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP
|
||||
}
|
||||
|
||||
/// Pricing must match the inner module byte-for-byte (sanity that no
|
||||
|
||||
+5
-9
@@ -1,4 +1,4 @@
|
||||
use crate::cost::kinovi_generation_cost::KinoviGenerationCost;
|
||||
use crate::generate::cost::KinoviGenerationCost;
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::requests::generate_image::generate_image::{
|
||||
@@ -286,17 +286,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_batch_one_is_five() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 5); // 1200/243 = 4.94 -> rounds UP
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_down, 4);
|
||||
assert!((make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_fractional - (1200.0 / 243.0)).abs() < 1e-9);
|
||||
fn usd_cents_batch_one_is_six() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_batch_four_is_twenty() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 20); // 4800/243 = 19.75 -> rounds UP
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_down, 19);
|
||||
assert!((make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_fractional - (4800.0 / 243.0)).abs() < 1e-9);
|
||||
fn usd_cents_batch_four_is_twentyfive() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP
|
||||
}
|
||||
|
||||
/// Pricing must match the inner module exactly.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::cost::kinovi_generation_cost::KinoviGenerationCost;
|
||||
use crate::generate::cost::KinoviGenerationCost;
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::requests::generate_image::generate_image::{
|
||||
@@ -283,17 +283,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_batch_one_is_five() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 5); // 1200/243 = 4.94 -> rounds UP
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_down, 4);
|
||||
assert!((make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_fractional - (1200.0 / 243.0)).abs() < 1e-9);
|
||||
fn usd_cents_batch_one_is_six() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_batch_four_is_twenty() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 20); // 4800/243 = 19.75 -> rounds UP
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_down, 19);
|
||||
assert!((make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_fractional - (4800.0 / 243.0)).abs() < 1e-9);
|
||||
fn usd_cents_batch_four_is_twentyfive() {
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP
|
||||
}
|
||||
|
||||
/// Pricing must match the inner module exactly.
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod cost;
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
|
||||
+12
-22
@@ -1,6 +1,6 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::cost::kinovi_generation_cost::KinoviGenerationCost;
|
||||
use crate::generate::cost::KinoviGenerationCost;
|
||||
use crate::requests::kinovi_host::KinoviHost;
|
||||
use crate::requests::workflow_run_task::workflow_run_task::{
|
||||
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw,
|
||||
@@ -63,7 +63,7 @@ pub enum KinoviHappyHorse1p0BatchCount {
|
||||
//
|
||||
// Default resolution (None) is 720p.
|
||||
// Batch count multiplies the total cost.
|
||||
// Credit package: 525,000 credits for $2,159.0909 (~243.16 credits/$1, rounded down to 243).
|
||||
// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193).
|
||||
|
||||
impl GenerateHappyHorse1p0Request {
|
||||
/// Estimate the credit cost for this generation request.
|
||||
@@ -334,52 +334,42 @@ mod tests {
|
||||
/// (USD cents are always rounded UP when fractional.)
|
||||
#[test]
|
||||
fn costs_720p_5s() {
|
||||
// 33 credits/s × 5s = 165 credits; 16500/243 = 67.90 → rounds UP to 68¢
|
||||
// 33 credits/s × 5s = 165 credits; 16500/193 = 85.49 → rounds UP to 86¢
|
||||
let costs = r720(5).calculate_costs();
|
||||
assert_eq!(costs.kinovi_credits, 165);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 68);
|
||||
assert_eq!(costs.usd_cents_rounded_down, 67);
|
||||
assert!((costs.usd_cents_fractional - (16500.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 86);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_1080p_5s() {
|
||||
// 66 credits/s × 5s = 330 credits; 33000/243 = 135.80 → 136¢
|
||||
// 66 credits/s × 5s = 330 credits; 33000/193 = 170.98 → 171¢
|
||||
let costs = r1080(5).calculate_costs();
|
||||
assert_eq!(costs.kinovi_credits, 330);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 136);
|
||||
assert_eq!(costs.usd_cents_rounded_down, 135);
|
||||
assert!((costs.usd_cents_fractional - (33000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 171);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_720p_15s() {
|
||||
// 33 credits/s × 15s = 495 credits; 49500/243 = 203.70 → rounds UP to 204¢
|
||||
// 33 credits/s × 15s = 495 credits; 49500/193 = 256.48 → rounds UP to 257¢
|
||||
let costs = r720(15).calculate_costs();
|
||||
assert_eq!(costs.kinovi_credits, 495);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 204);
|
||||
assert_eq!(costs.usd_cents_rounded_down, 203);
|
||||
assert!((costs.usd_cents_fractional - (49500.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 257);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_1080p_15s() {
|
||||
// 66 credits/s × 15s = 990 credits; 99000/243 = 407.41 → 408¢
|
||||
// 66 credits/s × 15s = 990 credits; 99000/193 = 512.95 → 513¢
|
||||
let costs = r1080(15).calculate_costs();
|
||||
assert_eq!(costs.kinovi_credits, 990);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 408);
|
||||
assert_eq!(costs.usd_cents_rounded_down, 407);
|
||||
assert!((costs.usd_cents_fractional - (99000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 513);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_batch_2_720p_5s() {
|
||||
// 165 credits × 2 = 330 credits; 33000/243 = 135.80 → 136¢
|
||||
// 165 credits × 2 = 330 credits; 33000/193 = 170.98 → 171¢
|
||||
let costs = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::Two)).calculate_costs();
|
||||
assert_eq!(costs.kinovi_credits, 330);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 136);
|
||||
assert_eq!(costs.usd_cents_rounded_down, 135);
|
||||
assert!((costs.usd_cents_fractional - (33000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 171);
|
||||
}
|
||||
|
||||
/// The deprecated shims return the corresponding struct fields.
|
||||
|
||||
+112
-340
@@ -1,6 +1,6 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost;
|
||||
use crate::generate::cost::KinoviGenerationCost;
|
||||
use crate::requests::kinovi_host::KinoviHost;
|
||||
use crate::requests::workflow_run_task::workflow_run_task::{
|
||||
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw,
|
||||
@@ -72,7 +72,7 @@ pub enum KinoviSeedance2p0BatchCount {
|
||||
//
|
||||
// Default resolution (None) is 720p.
|
||||
// Batch count multiplies the total cost.
|
||||
// Credit package: 525,000 credits for $2,159.0909 (~243.16 credits/$1, rounded down to 243).
|
||||
// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193).
|
||||
|
||||
impl GenerateSeedance2p0Request {
|
||||
/// Calculate the cost of this generation request, in Kinovi credits and
|
||||
@@ -80,7 +80,7 @@ impl GenerateSeedance2p0Request {
|
||||
///
|
||||
/// Attaching reference VIDEOS adds a per-output-second surcharge (see the
|
||||
/// pricing table below). Reference images and audio are free.
|
||||
pub fn calculate_costs(&self) -> KinoviSeedanceGenerationCost {
|
||||
pub fn calculate_costs(&self) -> KinoviGenerationCost {
|
||||
let credits_per_second: u64 = match self.output_resolution {
|
||||
Some(KinoviSeedance2p0OutputResolution::FourEightyP) => 15,
|
||||
Some(KinoviSeedance2p0OutputResolution::SevenTwentyP) | None => 40,
|
||||
@@ -108,24 +108,15 @@ impl GenerateSeedance2p0Request {
|
||||
0
|
||||
};
|
||||
|
||||
let per_video = u64::from(self.duration_seconds)
|
||||
* (credits_per_second + video_reference_surcharge_per_second);
|
||||
let batch_multiplier: u64 = match self.batch_count {
|
||||
None | Some(KinoviSeedance2p0BatchCount::One) => 1,
|
||||
Some(KinoviSeedance2p0BatchCount::Two) => 2,
|
||||
Some(KinoviSeedance2p0BatchCount::Four) => 4,
|
||||
};
|
||||
|
||||
let duration = u64::from(self.duration_seconds);
|
||||
let base_credits = duration * credits_per_second * batch_multiplier;
|
||||
let maybe_video_reference_surcharge_credits = if self.has_video_reference() {
|
||||
Some(duration * video_reference_surcharge_per_second * batch_multiplier)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
KinoviSeedanceGenerationCost::from_base_and_surcharge(
|
||||
base_credits,
|
||||
maybe_video_reference_surcharge_credits,
|
||||
)
|
||||
KinoviGenerationCost::from_kinovi_credits(per_video * batch_multiplier)
|
||||
}
|
||||
|
||||
fn has_video_reference(&self) -> bool {
|
||||
@@ -137,14 +128,14 @@ impl GenerateSeedance2p0Request {
|
||||
/// Estimate the credit cost for this generation request.
|
||||
#[deprecated(note = "Use calculate_costs() instead")]
|
||||
pub fn estimate_credits(&self) -> u32 {
|
||||
self.calculate_costs().total_cost.kinovi_credits as u32
|
||||
self.calculate_costs().kinovi_credits as u32
|
||||
}
|
||||
|
||||
/// Estimate the USD cost in cents for this generation request.
|
||||
/// NB: Rounds UP fractional cents (the historical behavior rounded to nearest).
|
||||
#[deprecated(note = "Use calculate_costs() instead")]
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
self.calculate_costs().total_cost.usd_cents_rounded_up
|
||||
self.calculate_costs().usd_cents_rounded_up
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,171 +262,6 @@ mod tests {
|
||||
build_request(dur, Some(KinoviSeedance2p0OutputResolution::TenEightyP), None)
|
||||
}
|
||||
|
||||
// ── Comprehensive per-resolution coverage ──
|
||||
//
|
||||
// Seedance 2.0: 480p 15/s (+4/s video ref), 720p 40/s (+8/s), 1080p 90/s (+18/s).
|
||||
// Every field and subfield is asserted: total_cost, base_cost,
|
||||
// video_reference_surcharge_cost × (kinovi_credits, usd_cents_rounded_up,
|
||||
// usd_cents_rounded_down, usd_cents_fractional). All cases use a 5 second
|
||||
// duration with no batching.
|
||||
|
||||
mod comprehensive {
|
||||
use super::*;
|
||||
|
||||
const FLOAT_TOLERANCE: f64 = 1e-9;
|
||||
|
||||
|
||||
mod resolution_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_without_reference_video() {
|
||||
let costs = r480(5).calculate_costs();
|
||||
|
||||
// Base: 15 credits/s × 5s = 75 credits; 7500/243 = 30.8642.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 75);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 31);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 30);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (7500.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// No reference video: no surcharge, and the total equals the base.
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 75);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 31);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 30);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (7500.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_reference_video() {
|
||||
let mut request = r480(5);
|
||||
request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]);
|
||||
let costs = request.calculate_costs();
|
||||
|
||||
// Base: 15 credits/s × 5s = 75 credits; 7500/243 = 30.8642.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 75);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 31);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 30);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (7500.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Surcharge: 4 credits/s × 5s = 20 credits; 2000/243 = 8.2305.
|
||||
let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge");
|
||||
assert_eq!(surcharge.kinovi_credits, 20);
|
||||
assert_eq!(surcharge.usd_cents_rounded_up, 9);
|
||||
assert_eq!(surcharge.usd_cents_rounded_down, 8);
|
||||
assert!((surcharge.usd_cents_fractional - (2000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Total: 75 + 20 = 95 credits; 9500/243 = 39.0947.
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 95);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 40);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 39);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (9500.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mod resolution_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_without_reference_video() {
|
||||
let costs = r720(5).calculate_costs();
|
||||
|
||||
// Base: 40 credits/s × 5s = 200 credits; 20000/243 = 82.3045.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 200);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 83);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 82);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// No reference video: no surcharge, and the total equals the base.
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 200);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 83);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 82);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_reference_video() {
|
||||
let mut request = r720(5);
|
||||
request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]);
|
||||
let costs = request.calculate_costs();
|
||||
|
||||
// Base: 40 credits/s × 5s = 200 credits; 20000/243 = 82.3045.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 200);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 83);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 82);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Surcharge: 8 credits/s × 5s = 40 credits; 4000/243 = 16.4609.
|
||||
let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge");
|
||||
assert_eq!(surcharge.kinovi_credits, 40);
|
||||
assert_eq!(surcharge.usd_cents_rounded_up, 17);
|
||||
assert_eq!(surcharge.usd_cents_rounded_down, 16);
|
||||
assert!((surcharge.usd_cents_fractional - (4000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Total: 200 + 40 = 240 credits; 24000/243 = 98.7654.
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 240);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 99);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 98);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (24000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mod resolution_1080p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_without_reference_video() {
|
||||
let costs = r1080(5).calculate_costs();
|
||||
|
||||
// Base: 90 credits/s × 5s = 450 credits; 45000/243 = 185.1852.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 450);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 186);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 185);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (45000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// No reference video: no surcharge, and the total equals the base.
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 450);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 186);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 185);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (45000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_reference_video() {
|
||||
let mut request = r1080(5);
|
||||
request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]);
|
||||
let costs = request.calculate_costs();
|
||||
|
||||
// Base: 90 credits/s × 5s = 450 credits; 45000/243 = 185.1852.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 450);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 186);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 185);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (45000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Surcharge: 18 credits/s × 5s = 90 credits; 9000/243 = 37.0370.
|
||||
let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge");
|
||||
assert_eq!(surcharge.kinovi_credits, 90);
|
||||
assert_eq!(surcharge.usd_cents_rounded_up, 38);
|
||||
assert_eq!(surcharge.usd_cents_rounded_down, 37);
|
||||
assert!((surcharge.usd_cents_fractional - (9000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Total: 450 + 90 = 540 credits; 54000/243 = 222.2222.
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 540);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 223);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 222);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (54000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ── 480p credits (15 credits/sec) ──
|
||||
|
||||
mod credits_480p {
|
||||
@@ -443,19 +269,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_duration() {
|
||||
assert_eq!(r480(3).calculate_costs().total_cost.kinovi_credits, 45);
|
||||
assert_eq!(r480(4).calculate_costs().total_cost.kinovi_credits, 60);
|
||||
assert_eq!(r480(5).calculate_costs().total_cost.kinovi_credits, 75);
|
||||
assert_eq!(r480(6).calculate_costs().total_cost.kinovi_credits, 90);
|
||||
assert_eq!(r480(7).calculate_costs().total_cost.kinovi_credits, 105);
|
||||
assert_eq!(r480(8).calculate_costs().total_cost.kinovi_credits, 120);
|
||||
assert_eq!(r480(9).calculate_costs().total_cost.kinovi_credits, 135);
|
||||
assert_eq!(r480(10).calculate_costs().total_cost.kinovi_credits, 150);
|
||||
assert_eq!(r480(11).calculate_costs().total_cost.kinovi_credits, 165);
|
||||
assert_eq!(r480(12).calculate_costs().total_cost.kinovi_credits, 180);
|
||||
assert_eq!(r480(13).calculate_costs().total_cost.kinovi_credits, 195);
|
||||
assert_eq!(r480(14).calculate_costs().total_cost.kinovi_credits, 210);
|
||||
assert_eq!(r480(15).calculate_costs().total_cost.kinovi_credits, 225);
|
||||
assert_eq!(r480(3).calculate_costs().kinovi_credits, 45);
|
||||
assert_eq!(r480(4).calculate_costs().kinovi_credits, 60);
|
||||
assert_eq!(r480(5).calculate_costs().kinovi_credits, 75);
|
||||
assert_eq!(r480(6).calculate_costs().kinovi_credits, 90);
|
||||
assert_eq!(r480(7).calculate_costs().kinovi_credits, 105);
|
||||
assert_eq!(r480(8).calculate_costs().kinovi_credits, 120);
|
||||
assert_eq!(r480(9).calculate_costs().kinovi_credits, 135);
|
||||
assert_eq!(r480(10).calculate_costs().kinovi_credits, 150);
|
||||
assert_eq!(r480(11).calculate_costs().kinovi_credits, 165);
|
||||
assert_eq!(r480(12).calculate_costs().kinovi_credits, 180);
|
||||
assert_eq!(r480(13).calculate_costs().kinovi_credits, 195);
|
||||
assert_eq!(r480(14).calculate_costs().kinovi_credits, 210);
|
||||
assert_eq!(r480(15).calculate_costs().kinovi_credits, 225);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,25 +292,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_duration() {
|
||||
assert_eq!(r720(3).calculate_costs().total_cost.kinovi_credits, 120);
|
||||
assert_eq!(r720(4).calculate_costs().total_cost.kinovi_credits, 160);
|
||||
assert_eq!(r720(5).calculate_costs().total_cost.kinovi_credits, 200);
|
||||
assert_eq!(r720(6).calculate_costs().total_cost.kinovi_credits, 240);
|
||||
assert_eq!(r720(7).calculate_costs().total_cost.kinovi_credits, 280);
|
||||
assert_eq!(r720(8).calculate_costs().total_cost.kinovi_credits, 320);
|
||||
assert_eq!(r720(9).calculate_costs().total_cost.kinovi_credits, 360);
|
||||
assert_eq!(r720(10).calculate_costs().total_cost.kinovi_credits, 400);
|
||||
assert_eq!(r720(11).calculate_costs().total_cost.kinovi_credits, 440);
|
||||
assert_eq!(r720(12).calculate_costs().total_cost.kinovi_credits, 480);
|
||||
assert_eq!(r720(13).calculate_costs().total_cost.kinovi_credits, 520);
|
||||
assert_eq!(r720(14).calculate_costs().total_cost.kinovi_credits, 560);
|
||||
assert_eq!(r720(15).calculate_costs().total_cost.kinovi_credits, 600);
|
||||
assert_eq!(r720(3).calculate_costs().kinovi_credits, 120);
|
||||
assert_eq!(r720(4).calculate_costs().kinovi_credits, 160);
|
||||
assert_eq!(r720(5).calculate_costs().kinovi_credits, 200);
|
||||
assert_eq!(r720(6).calculate_costs().kinovi_credits, 240);
|
||||
assert_eq!(r720(7).calculate_costs().kinovi_credits, 280);
|
||||
assert_eq!(r720(8).calculate_costs().kinovi_credits, 320);
|
||||
assert_eq!(r720(9).calculate_costs().kinovi_credits, 360);
|
||||
assert_eq!(r720(10).calculate_costs().kinovi_credits, 400);
|
||||
assert_eq!(r720(11).calculate_costs().kinovi_credits, 440);
|
||||
assert_eq!(r720(12).calculate_costs().kinovi_credits, 480);
|
||||
assert_eq!(r720(13).calculate_costs().kinovi_credits, 520);
|
||||
assert_eq!(r720(14).calculate_costs().kinovi_credits, 560);
|
||||
assert_eq!(r720(15).calculate_costs().kinovi_credits, 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_720p_same_as_default() {
|
||||
let default = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let explicit = build_request(5, Some(KinoviSeedance2p0OutputResolution::SevenTwentyP), None).calculate_costs().total_cost.kinovi_credits;
|
||||
let default = r720(5).calculate_costs().kinovi_credits;
|
||||
let explicit = build_request(5, Some(KinoviSeedance2p0OutputResolution::SevenTwentyP), None).calculate_costs().kinovi_credits;
|
||||
assert_eq!(default, explicit);
|
||||
}
|
||||
}
|
||||
@@ -496,19 +322,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_duration() {
|
||||
assert_eq!(r1080(3).calculate_costs().total_cost.kinovi_credits, 270);
|
||||
assert_eq!(r1080(4).calculate_costs().total_cost.kinovi_credits, 360);
|
||||
assert_eq!(r1080(5).calculate_costs().total_cost.kinovi_credits, 450);
|
||||
assert_eq!(r1080(6).calculate_costs().total_cost.kinovi_credits, 540);
|
||||
assert_eq!(r1080(7).calculate_costs().total_cost.kinovi_credits, 630);
|
||||
assert_eq!(r1080(8).calculate_costs().total_cost.kinovi_credits, 720);
|
||||
assert_eq!(r1080(9).calculate_costs().total_cost.kinovi_credits, 810);
|
||||
assert_eq!(r1080(10).calculate_costs().total_cost.kinovi_credits, 900);
|
||||
assert_eq!(r1080(11).calculate_costs().total_cost.kinovi_credits, 990);
|
||||
assert_eq!(r1080(12).calculate_costs().total_cost.kinovi_credits, 1080);
|
||||
assert_eq!(r1080(13).calculate_costs().total_cost.kinovi_credits, 1170);
|
||||
assert_eq!(r1080(14).calculate_costs().total_cost.kinovi_credits, 1260);
|
||||
assert_eq!(r1080(15).calculate_costs().total_cost.kinovi_credits, 1350);
|
||||
assert_eq!(r1080(3).calculate_costs().kinovi_credits, 270);
|
||||
assert_eq!(r1080(4).calculate_costs().kinovi_credits, 360);
|
||||
assert_eq!(r1080(5).calculate_costs().kinovi_credits, 450);
|
||||
assert_eq!(r1080(6).calculate_costs().kinovi_credits, 540);
|
||||
assert_eq!(r1080(7).calculate_costs().kinovi_credits, 630);
|
||||
assert_eq!(r1080(8).calculate_costs().kinovi_credits, 720);
|
||||
assert_eq!(r1080(9).calculate_costs().kinovi_credits, 810);
|
||||
assert_eq!(r1080(10).calculate_costs().kinovi_credits, 900);
|
||||
assert_eq!(r1080(11).calculate_costs().kinovi_credits, 990);
|
||||
assert_eq!(r1080(12).calculate_costs().kinovi_credits, 1080);
|
||||
assert_eq!(r1080(13).calculate_costs().kinovi_credits, 1170);
|
||||
assert_eq!(r1080(14).calculate_costs().kinovi_credits, 1260);
|
||||
assert_eq!(r1080(15).calculate_costs().kinovi_credits, 1350);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,39 +345,39 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn batch_1_is_base() {
|
||||
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let explicit = build_request(5, None, Some(KinoviSeedance2p0BatchCount::One)).calculate_costs().total_cost.kinovi_credits;
|
||||
let base = r720(5).calculate_costs().kinovi_credits;
|
||||
let explicit = build_request(5, None, Some(KinoviSeedance2p0BatchCount::One)).calculate_costs().kinovi_credits;
|
||||
assert_eq!(base, explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2_doubles() {
|
||||
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch2 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().total_cost.kinovi_credits;
|
||||
let base = r720(5).calculate_costs().kinovi_credits;
|
||||
let batch2 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().kinovi_credits;
|
||||
assert_eq!(batch2, base * 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4_quadruples() {
|
||||
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch4 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().total_cost.kinovi_credits;
|
||||
let base = r720(5).calculate_costs().kinovi_credits;
|
||||
let batch4 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().kinovi_credits;
|
||||
assert_eq!(batch4, base * 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_multiplier_applies_to_1080p() {
|
||||
let base = r1080(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().total_cost.kinovi_credits;
|
||||
let base = r1080(5).calculate_costs().kinovi_credits;
|
||||
let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().kinovi_credits;
|
||||
let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().kinovi_credits;
|
||||
assert_eq!(batch2, base * 2);
|
||||
assert_eq!(batch4, base * 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_multiplier_applies_to_480p() {
|
||||
let base = r480(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().total_cost.kinovi_credits;
|
||||
let base = r480(5).calculate_costs().kinovi_credits;
|
||||
let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().kinovi_credits;
|
||||
let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().kinovi_credits;
|
||||
assert_eq!(batch2, base * 2);
|
||||
assert_eq!(batch4, base * 4);
|
||||
}
|
||||
@@ -564,9 +390,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c3 = r720(3).calculate_costs().total_cost.kinovi_credits;
|
||||
let c10 = r720(10).calculate_costs().total_cost.kinovi_credits;
|
||||
let c15 = r720(15).calculate_costs().total_cost.kinovi_credits;
|
||||
let c3 = r720(3).calculate_costs().kinovi_credits;
|
||||
let c10 = r720(10).calculate_costs().kinovi_credits;
|
||||
let c15 = r720(15).calculate_costs().kinovi_credits;
|
||||
assert!(c3 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
@@ -574,9 +400,9 @@ mod tests {
|
||||
#[test]
|
||||
fn resolution_ordering() {
|
||||
for dur in 3..=15u8 {
|
||||
let c480 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::FourEightyP), None).calculate_costs().total_cost.kinovi_credits;
|
||||
let c720 = build_request(dur, None, None).calculate_costs().total_cost.kinovi_credits;
|
||||
let c1080 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::TenEightyP), None).calculate_costs().total_cost.kinovi_credits;
|
||||
let c480 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::FourEightyP), None).calculate_costs().kinovi_credits;
|
||||
let c720 = build_request(dur, None, None).calculate_costs().kinovi_credits;
|
||||
let c1080 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::TenEightyP), None).calculate_costs().kinovi_credits;
|
||||
assert!(c480 < c720, "480p should be cheaper than 720p at {}s", dur);
|
||||
assert!(c720 < c1080, "720p should be cheaper than 1080p at {}s", dur);
|
||||
}
|
||||
@@ -592,74 +418,50 @@ mod tests {
|
||||
/// (USD cents are always rounded UP when fractional.)
|
||||
#[test]
|
||||
fn costs_480p_5s() {
|
||||
// 15 credits/s × 5s = 75 credits; 7500/243 = 30.86 → 31¢
|
||||
// 15 credits/s × 5s = 75 credits; 7500/193 = 38.86 → 39¢
|
||||
let costs = r480(5).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 75);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 31);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 30);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (7500.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 75);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 75);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 39);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_720p_5s() {
|
||||
// 40 credits/s × 5s = 200 credits; 20000/243 = 82.30 → 83¢
|
||||
// 40 credits/s × 5s = 200 credits; 20000/193 = 103.63 → 104¢
|
||||
let costs = r720(5).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 200);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 83);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 82);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 200);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 200);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 104);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_1080p_5s() {
|
||||
// 90 credits/s × 5s = 450 credits; 45000/243 = 185.19 → rounds UP to 186¢
|
||||
// 90 credits/s × 5s = 450 credits; 45000/193 = 233.16 → rounds UP to 234¢
|
||||
let costs = r1080(5).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 450);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 186);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 185);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (45000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 450);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 450);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 234);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_720p_15s() {
|
||||
// 40 credits/s × 15s = 600 credits; 60000/243 = 246.91 → 247¢
|
||||
// 40 credits/s × 15s = 600 credits; 60000/193 = 310.88 → 311¢
|
||||
let costs = r720(15).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 600);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 247);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 246);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (60000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 600);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 600);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 311);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_1080p_15s() {
|
||||
// 90 credits/s × 15s = 1350 credits; 135000/243 = 555.56 → rounds UP to 556¢
|
||||
// 90 credits/s × 15s = 1350 credits; 135000/193 = 699.48 → rounds UP to 700¢
|
||||
let costs = r1080(15).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 1350);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 556);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 555);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (135000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 1350);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 1350);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 700);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_batch_4_720p_5s() {
|
||||
// 200 credits × 4 = 800 credits; 80000/243 = 329.22 → 330¢
|
||||
// 200 credits × 4 = 800 credits; 80000/193 = 414.51 → 415¢
|
||||
let costs = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 800);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 330);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 329);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (80000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 800);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 800);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 415);
|
||||
}
|
||||
|
||||
/// The deprecated shims return the corresponding struct fields.
|
||||
@@ -668,8 +470,8 @@ mod tests {
|
||||
fn deprecated_methods_delegate() {
|
||||
let request = r720(5);
|
||||
let costs = request.calculate_costs();
|
||||
assert_eq!(u64::from(request.estimate_credits()), costs.total_cost.kinovi_credits);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), costs.total_cost.usd_cents_rounded_up);
|
||||
assert_eq!(u64::from(request.estimate_credits()), costs.kinovi_credits);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), costs.usd_cents_rounded_up);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,74 +489,47 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The full base + surcharge table from Kinovi's pricing page
|
||||
/// ("With Video Uploads", 10 sec video ref). Asserts every field:
|
||||
/// base, surcharge, and the derived total.
|
||||
/// ("With Video Uploads", 10 sec video ref).
|
||||
#[test]
|
||||
fn kinovi_pricing_table_with_video_reference() {
|
||||
// (request, duration, base credits, surcharge credits)
|
||||
let cases: &[(fn(u8) -> GenerateSeedance2p0Request, u8, u64, u64)] = &[
|
||||
let cases: &[(fn(u8) -> GenerateSeedance2p0Request, u8, u64)] = &[
|
||||
// 480p: base + 4/s
|
||||
(r480, 4, 60, 16),
|
||||
(r480, 5, 75, 20),
|
||||
(r480, 10, 150, 40),
|
||||
(r480, 15, 225, 60),
|
||||
(r480, 4, 60 + 16),
|
||||
(r480, 5, 75 + 20),
|
||||
(r480, 10, 150 + 40),
|
||||
(r480, 15, 225 + 60),
|
||||
// 720p: base + 8/s
|
||||
(r720, 4, 160, 32),
|
||||
(r720, 5, 200, 40),
|
||||
(r720, 10, 400, 80),
|
||||
(r720, 15, 600, 120),
|
||||
(r720, 4, 160 + 32),
|
||||
(r720, 5, 200 + 40),
|
||||
(r720, 10, 400 + 80),
|
||||
(r720, 15, 600 + 120),
|
||||
// 1080p: base + 18/s
|
||||
(r1080, 4, 360, 72),
|
||||
(r1080, 5, 450, 90),
|
||||
(r1080, 10, 900, 180),
|
||||
(r1080, 15, 1350, 270),
|
||||
(r1080, 4, 360 + 72),
|
||||
(r1080, 5, 450 + 90),
|
||||
(r1080, 10, 900 + 180),
|
||||
(r1080, 15, 1350 + 270),
|
||||
];
|
||||
|
||||
for (make, duration, base, surcharge) in cases {
|
||||
for (make, duration, expected_credits) in cases {
|
||||
let costs = with_video_ref(make(*duration)).calculate_costs();
|
||||
assert_eq!(costs.base_cost.kinovi_credits, *base, "base for {duration}s");
|
||||
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(*surcharge), "surcharge for {duration}s");
|
||||
assert_eq!(costs.total_cost.kinovi_credits, base + surcharge, "total for {duration}s");
|
||||
assert_eq!(costs.kinovi_credits, *expected_credits,
|
||||
"duration {duration}s should cost {expected_credits} credits with a video reference");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surcharge_includes_usd_cents() {
|
||||
// 720p 5s + video ref = 200 + 40 = 240 credits; 24000/243 = 98.77 → 99¢
|
||||
// 720p 5s + video ref = 240 credits; 24000/193 = 124.35 → 125¢
|
||||
let costs = with_video_ref(r720(5)).calculate_costs();
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 200);
|
||||
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(40));
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 240);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 99);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 98);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (24000.0 / 243.0)).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// The base and surcharge parts each carry their own USD conversions.
|
||||
#[test]
|
||||
fn parts_have_their_own_usd_conversions() {
|
||||
let costs = with_video_ref(r720(5)).calculate_costs();
|
||||
|
||||
// Base: 200 credits; 20000/243 = 82.30.
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 83);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 82);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < 1e-9);
|
||||
|
||||
// Surcharge: 40 credits; 4000/243 = 16.46.
|
||||
let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge");
|
||||
assert_eq!(surcharge.kinovi_credits, 40);
|
||||
assert_eq!(surcharge.usd_cents_rounded_up, 17);
|
||||
assert_eq!(surcharge.usd_cents_rounded_down, 16);
|
||||
assert!((surcharge.usd_cents_fractional - (4000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.kinovi_credits, 240);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 125);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_video_reference_list_has_no_surcharge() {
|
||||
let mut request = r720(5);
|
||||
request.reference_video_urls = Some(vec![]);
|
||||
let costs = request.calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 200);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(request.calculate_costs().kinovi_credits, 200);
|
||||
}
|
||||
|
||||
/// Surcharge is flat per generation regardless of how many reference
|
||||
@@ -766,7 +541,7 @@ mod tests {
|
||||
"https://example.com/a.mp4".to_string(),
|
||||
"https://example.com/b.mp4".to_string(),
|
||||
]);
|
||||
assert_eq!(request.calculate_costs().total_cost.kinovi_credits, 240);
|
||||
assert_eq!(request.calculate_costs().kinovi_credits, 240);
|
||||
}
|
||||
|
||||
/// The surcharge applies per generated video, so batches multiply it.
|
||||
@@ -774,10 +549,7 @@ mod tests {
|
||||
fn batch_multiplies_surcharge() {
|
||||
let request = with_video_ref(build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)));
|
||||
// (200 base + 40 surcharge) × 2 = 480 credits
|
||||
let costs = request.calculate_costs();
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 400);
|
||||
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(80));
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 480);
|
||||
assert_eq!(request.calculate_costs().kinovi_credits, 480);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,7 +557,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_does_not_affect_credits() {
|
||||
let baseline = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let baseline = r720(5).calculate_costs().kinovi_credits;
|
||||
|
||||
let ratios = [
|
||||
KinoviSeedance2p0AspectRatio::Landscape16x9,
|
||||
@@ -812,7 +584,7 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
assert_eq!(
|
||||
req.calculate_costs().total_cost.kinovi_credits, baseline,
|
||||
req.calculate_costs().kinovi_credits, baseline,
|
||||
"Aspect ratio {:?} should not change credits from baseline {}", ar, baseline,
|
||||
);
|
||||
}
|
||||
|
||||
+88
-262
@@ -1,6 +1,6 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost;
|
||||
use crate::generate::cost::KinoviGenerationCost;
|
||||
use crate::requests::kinovi_host::KinoviHost;
|
||||
use crate::requests::workflow_run_task::workflow_run_task::{
|
||||
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw,
|
||||
@@ -70,7 +70,7 @@ pub enum KinoviSeedance2p0FastBatchCount {
|
||||
//
|
||||
// Default resolution (None) is 720p.
|
||||
// Batch count multiplies the total cost.
|
||||
// Credit package: 525,000 credits for $2,159.0909 (~243.16 credits/$1, rounded down to 243).
|
||||
// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193).
|
||||
|
||||
impl GenerateSeedance2p0FastRequest {
|
||||
/// Calculate the cost of this generation request, in Kinovi credits and
|
||||
@@ -78,7 +78,7 @@ impl GenerateSeedance2p0FastRequest {
|
||||
///
|
||||
/// Attaching reference VIDEOS adds a per-output-second surcharge (see the
|
||||
/// pricing table below). Reference images and audio are free.
|
||||
pub fn calculate_costs(&self) -> KinoviSeedanceGenerationCost {
|
||||
pub fn calculate_costs(&self) -> KinoviGenerationCost {
|
||||
let credits_per_second: u64 = match self.output_resolution {
|
||||
Some(KinoviSeedance2p0FastOutputResolution::FourEightyP) => 14,
|
||||
Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP) | None => 28,
|
||||
@@ -103,24 +103,15 @@ impl GenerateSeedance2p0FastRequest {
|
||||
0
|
||||
};
|
||||
|
||||
let per_video = u64::from(self.duration_seconds)
|
||||
* (credits_per_second + video_reference_surcharge_per_second);
|
||||
let batch_multiplier: u64 = match self.batch_count {
|
||||
None | Some(KinoviSeedance2p0FastBatchCount::One) => 1,
|
||||
Some(KinoviSeedance2p0FastBatchCount::Two) => 2,
|
||||
Some(KinoviSeedance2p0FastBatchCount::Four) => 4,
|
||||
};
|
||||
|
||||
let duration = u64::from(self.duration_seconds);
|
||||
let base_credits = duration * credits_per_second * batch_multiplier;
|
||||
let maybe_video_reference_surcharge_credits = if self.has_video_reference() {
|
||||
Some(duration * video_reference_surcharge_per_second * batch_multiplier)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
KinoviSeedanceGenerationCost::from_base_and_surcharge(
|
||||
base_credits,
|
||||
maybe_video_reference_surcharge_credits,
|
||||
)
|
||||
KinoviGenerationCost::from_kinovi_credits(per_video * batch_multiplier)
|
||||
}
|
||||
|
||||
fn has_video_reference(&self) -> bool {
|
||||
@@ -132,14 +123,14 @@ impl GenerateSeedance2p0FastRequest {
|
||||
/// Estimate the credit cost for this generation request.
|
||||
#[deprecated(note = "Use calculate_costs() instead")]
|
||||
pub fn estimate_credits(&self) -> u32 {
|
||||
self.calculate_costs().total_cost.kinovi_credits as u32
|
||||
self.calculate_costs().kinovi_credits as u32
|
||||
}
|
||||
|
||||
/// Estimate the USD cost in cents for this generation request.
|
||||
/// NB: Rounds UP fractional cents (the historical behavior rounded to nearest).
|
||||
#[deprecated(note = "Use calculate_costs() instead")]
|
||||
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
|
||||
self.calculate_costs().total_cost.usd_cents_rounded_up
|
||||
self.calculate_costs().usd_cents_rounded_up
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,121 +252,6 @@ mod tests {
|
||||
build_request(dur, None, None)
|
||||
}
|
||||
|
||||
// ── Comprehensive per-resolution coverage ──
|
||||
//
|
||||
// Seedance 2.0 Fast: 480p 14/s (+4/s video ref), 720p 28/s (+6/s). No 1080p.
|
||||
// Every field and subfield is asserted: total_cost, base_cost,
|
||||
// video_reference_surcharge_cost × (kinovi_credits, usd_cents_rounded_up,
|
||||
// usd_cents_rounded_down, usd_cents_fractional). All cases use a 5 second
|
||||
// duration with no batching.
|
||||
|
||||
mod comprehensive {
|
||||
use super::*;
|
||||
|
||||
const FLOAT_TOLERANCE: f64 = 1e-9;
|
||||
|
||||
|
||||
mod resolution_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_without_reference_video() {
|
||||
let costs = r480(5).calculate_costs();
|
||||
|
||||
// Base: 14 credits/s × 5s = 70 credits; 7000/243 = 28.8066.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 70);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 29);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 28);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (7000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// No reference video: no surcharge, and the total equals the base.
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 70);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 29);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 28);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (7000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_reference_video() {
|
||||
let mut request = r480(5);
|
||||
request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]);
|
||||
let costs = request.calculate_costs();
|
||||
|
||||
// Base: 14 credits/s × 5s = 70 credits; 7000/243 = 28.8066.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 70);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 29);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 28);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (7000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Surcharge: 4 credits/s × 5s = 20 credits; 2000/243 = 8.2305.
|
||||
let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge");
|
||||
assert_eq!(surcharge.kinovi_credits, 20);
|
||||
assert_eq!(surcharge.usd_cents_rounded_up, 9);
|
||||
assert_eq!(surcharge.usd_cents_rounded_down, 8);
|
||||
assert!((surcharge.usd_cents_fractional - (2000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Total: 70 + 20 = 90 credits; 9000/243 = 37.0370.
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 90);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 38);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 37);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (9000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mod resolution_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_without_reference_video() {
|
||||
let costs = r720(5).calculate_costs();
|
||||
|
||||
// Base: 28 credits/s × 5s = 140 credits; 14000/243 = 57.6132.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 140);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 58);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 57);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// No reference video: no surcharge, and the total equals the base.
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 140);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 58);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 57);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_reference_video() {
|
||||
let mut request = r720(5);
|
||||
request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]);
|
||||
let costs = request.calculate_costs();
|
||||
|
||||
// Base: 28 credits/s × 5s = 140 credits; 14000/243 = 57.6132.
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 140);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 58);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 57);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Surcharge: 6 credits/s × 5s = 30 credits; 3000/243 = 12.3457.
|
||||
let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge");
|
||||
assert_eq!(surcharge.kinovi_credits, 30);
|
||||
assert_eq!(surcharge.usd_cents_rounded_up, 13);
|
||||
assert_eq!(surcharge.usd_cents_rounded_down, 12);
|
||||
assert!((surcharge.usd_cents_fractional - (3000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
|
||||
// Total: 140 + 30 = 170 credits; 17000/243 = 69.9588.
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 170);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 70);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 69);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (17000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ── 480p credits (14 credits/sec) ──
|
||||
|
||||
mod credits_480p {
|
||||
@@ -383,19 +259,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_duration() {
|
||||
assert_eq!(r480(3).calculate_costs().total_cost.kinovi_credits, 42);
|
||||
assert_eq!(r480(4).calculate_costs().total_cost.kinovi_credits, 56);
|
||||
assert_eq!(r480(5).calculate_costs().total_cost.kinovi_credits, 70);
|
||||
assert_eq!(r480(6).calculate_costs().total_cost.kinovi_credits, 84);
|
||||
assert_eq!(r480(7).calculate_costs().total_cost.kinovi_credits, 98);
|
||||
assert_eq!(r480(8).calculate_costs().total_cost.kinovi_credits, 112);
|
||||
assert_eq!(r480(9).calculate_costs().total_cost.kinovi_credits, 126);
|
||||
assert_eq!(r480(10).calculate_costs().total_cost.kinovi_credits, 140);
|
||||
assert_eq!(r480(11).calculate_costs().total_cost.kinovi_credits, 154);
|
||||
assert_eq!(r480(12).calculate_costs().total_cost.kinovi_credits, 168);
|
||||
assert_eq!(r480(13).calculate_costs().total_cost.kinovi_credits, 182);
|
||||
assert_eq!(r480(14).calculate_costs().total_cost.kinovi_credits, 196);
|
||||
assert_eq!(r480(15).calculate_costs().total_cost.kinovi_credits, 210);
|
||||
assert_eq!(r480(3).calculate_costs().kinovi_credits, 42);
|
||||
assert_eq!(r480(4).calculate_costs().kinovi_credits, 56);
|
||||
assert_eq!(r480(5).calculate_costs().kinovi_credits, 70);
|
||||
assert_eq!(r480(6).calculate_costs().kinovi_credits, 84);
|
||||
assert_eq!(r480(7).calculate_costs().kinovi_credits, 98);
|
||||
assert_eq!(r480(8).calculate_costs().kinovi_credits, 112);
|
||||
assert_eq!(r480(9).calculate_costs().kinovi_credits, 126);
|
||||
assert_eq!(r480(10).calculate_costs().kinovi_credits, 140);
|
||||
assert_eq!(r480(11).calculate_costs().kinovi_credits, 154);
|
||||
assert_eq!(r480(12).calculate_costs().kinovi_credits, 168);
|
||||
assert_eq!(r480(13).calculate_costs().kinovi_credits, 182);
|
||||
assert_eq!(r480(14).calculate_costs().kinovi_credits, 196);
|
||||
assert_eq!(r480(15).calculate_costs().kinovi_credits, 210);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,25 +282,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn every_duration() {
|
||||
assert_eq!(r720(3).calculate_costs().total_cost.kinovi_credits, 84);
|
||||
assert_eq!(r720(4).calculate_costs().total_cost.kinovi_credits, 112);
|
||||
assert_eq!(r720(5).calculate_costs().total_cost.kinovi_credits, 140);
|
||||
assert_eq!(r720(6).calculate_costs().total_cost.kinovi_credits, 168);
|
||||
assert_eq!(r720(7).calculate_costs().total_cost.kinovi_credits, 196);
|
||||
assert_eq!(r720(8).calculate_costs().total_cost.kinovi_credits, 224);
|
||||
assert_eq!(r720(9).calculate_costs().total_cost.kinovi_credits, 252);
|
||||
assert_eq!(r720(10).calculate_costs().total_cost.kinovi_credits, 280);
|
||||
assert_eq!(r720(11).calculate_costs().total_cost.kinovi_credits, 308);
|
||||
assert_eq!(r720(12).calculate_costs().total_cost.kinovi_credits, 336);
|
||||
assert_eq!(r720(13).calculate_costs().total_cost.kinovi_credits, 364);
|
||||
assert_eq!(r720(14).calculate_costs().total_cost.kinovi_credits, 392);
|
||||
assert_eq!(r720(15).calculate_costs().total_cost.kinovi_credits, 420);
|
||||
assert_eq!(r720(3).calculate_costs().kinovi_credits, 84);
|
||||
assert_eq!(r720(4).calculate_costs().kinovi_credits, 112);
|
||||
assert_eq!(r720(5).calculate_costs().kinovi_credits, 140);
|
||||
assert_eq!(r720(6).calculate_costs().kinovi_credits, 168);
|
||||
assert_eq!(r720(7).calculate_costs().kinovi_credits, 196);
|
||||
assert_eq!(r720(8).calculate_costs().kinovi_credits, 224);
|
||||
assert_eq!(r720(9).calculate_costs().kinovi_credits, 252);
|
||||
assert_eq!(r720(10).calculate_costs().kinovi_credits, 280);
|
||||
assert_eq!(r720(11).calculate_costs().kinovi_credits, 308);
|
||||
assert_eq!(r720(12).calculate_costs().kinovi_credits, 336);
|
||||
assert_eq!(r720(13).calculate_costs().kinovi_credits, 364);
|
||||
assert_eq!(r720(14).calculate_costs().kinovi_credits, 392);
|
||||
assert_eq!(r720(15).calculate_costs().kinovi_credits, 420);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_720p_same_as_default() {
|
||||
let default = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let explicit = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP), None).calculate_costs().total_cost.kinovi_credits;
|
||||
let default = r720(5).calculate_costs().kinovi_credits;
|
||||
let explicit = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP), None).calculate_costs().kinovi_credits;
|
||||
assert_eq!(default, explicit);
|
||||
}
|
||||
}
|
||||
@@ -436,30 +312,30 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn batch_1_is_base() {
|
||||
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let explicit = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::One)).calculate_costs().total_cost.kinovi_credits;
|
||||
let base = r720(5).calculate_costs().kinovi_credits;
|
||||
let explicit = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::One)).calculate_costs().kinovi_credits;
|
||||
assert_eq!(base, explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2_doubles() {
|
||||
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch2 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().total_cost.kinovi_credits;
|
||||
let base = r720(5).calculate_costs().kinovi_credits;
|
||||
let batch2 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().kinovi_credits;
|
||||
assert_eq!(batch2, base * 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4_quadruples() {
|
||||
let base = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch4 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().total_cost.kinovi_credits;
|
||||
let base = r720(5).calculate_costs().kinovi_credits;
|
||||
let batch4 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().kinovi_credits;
|
||||
assert_eq!(batch4, base * 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_multiplier_applies_to_480p() {
|
||||
let base = r480(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch2 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().total_cost.kinovi_credits;
|
||||
let batch4 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().total_cost.kinovi_credits;
|
||||
let base = r480(5).calculate_costs().kinovi_credits;
|
||||
let batch2 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().kinovi_credits;
|
||||
let batch4 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().kinovi_credits;
|
||||
assert_eq!(batch2, base * 2);
|
||||
assert_eq!(batch4, base * 4);
|
||||
}
|
||||
@@ -472,9 +348,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c3 = r720(3).calculate_costs().total_cost.kinovi_credits;
|
||||
let c10 = r720(10).calculate_costs().total_cost.kinovi_credits;
|
||||
let c15 = r720(15).calculate_costs().total_cost.kinovi_credits;
|
||||
let c3 = r720(3).calculate_costs().kinovi_credits;
|
||||
let c10 = r720(10).calculate_costs().kinovi_credits;
|
||||
let c15 = r720(15).calculate_costs().kinovi_credits;
|
||||
assert!(c3 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
@@ -482,8 +358,8 @@ mod tests {
|
||||
#[test]
|
||||
fn resolution_ordering() {
|
||||
for dur in 3..=15u8 {
|
||||
let c480 = build_request(dur, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), None).calculate_costs().total_cost.kinovi_credits;
|
||||
let c720 = build_request(dur, None, None).calculate_costs().total_cost.kinovi_credits;
|
||||
let c480 = build_request(dur, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), None).calculate_costs().kinovi_credits;
|
||||
let c720 = build_request(dur, None, None).calculate_costs().kinovi_credits;
|
||||
assert!(c480 < c720, "480p should be cheaper than 720p at {}s", dur);
|
||||
}
|
||||
}
|
||||
@@ -498,62 +374,42 @@ mod tests {
|
||||
/// (USD cents are always rounded UP when fractional.)
|
||||
#[test]
|
||||
fn costs_480p_5s() {
|
||||
// 14 credits/s × 5s = 70 credits; 7000/243 = 28.81 → rounds UP to 29¢
|
||||
// 14 credits/s × 5s = 70 credits; 7000/193 = 36.27 → rounds UP to 37¢
|
||||
let costs = r480(5).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 70);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 29);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 28);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (7000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 70);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 70);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 37);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_720p_5s() {
|
||||
// 28 credits/s × 5s = 140 credits; 14000/243 = 57.61 → 58¢
|
||||
// 28 credits/s × 5s = 140 credits; 14000/193 = 72.54 → 73¢
|
||||
let costs = r720(5).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 140);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 58);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 57);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 140);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 140);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 73);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_720p_15s() {
|
||||
// 28 credits/s × 15s = 420 credits; 42000/243 = 172.84 → 173¢
|
||||
// 28 credits/s × 15s = 420 credits; 42000/193 = 217.62 → 218¢
|
||||
let costs = r720(15).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 420);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 173);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 172);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (42000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 420);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 420);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 218);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_480p_15s() {
|
||||
// 14 credits/s × 15s = 210 credits; 21000/243 = 86.42 → 87¢
|
||||
// 14 credits/s × 15s = 210 credits; 21000/193 = 108.81 → 109¢
|
||||
let costs = r480(15).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 210);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 87);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 86);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (21000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 210);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 210);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 109);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn costs_batch_2_720p_5s() {
|
||||
// 140 credits × 2 = 280 credits; 28000/243 = 115.23 → 116¢
|
||||
// 140 credits × 2 = 280 credits; 28000/193 = 145.07 → 146¢
|
||||
let costs = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 280);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 116);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 115);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (28000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 280);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(costs.kinovi_credits, 280);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 146);
|
||||
}
|
||||
|
||||
/// The deprecated shims return the corresponding struct fields.
|
||||
@@ -562,8 +418,8 @@ mod tests {
|
||||
fn deprecated_methods_delegate() {
|
||||
let request = r720(5);
|
||||
let costs = request.calculate_costs();
|
||||
assert_eq!(u64::from(request.estimate_credits()), costs.total_cost.kinovi_credits);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), costs.total_cost.usd_cents_rounded_up);
|
||||
assert_eq!(u64::from(request.estimate_credits()), costs.kinovi_credits);
|
||||
assert_eq!(request.estimate_cost_in_usd_cents(), costs.usd_cents_rounded_up);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,69 +438,42 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The full base + surcharge table from Kinovi's pricing page
|
||||
/// ("Seedance 2.0 Fast With Video Ref", 10 sec video ref). Asserts
|
||||
/// every field: base, surcharge, and the derived total.
|
||||
/// ("Seedance 2.0 Fast With Video Ref", 10 sec video ref).
|
||||
#[test]
|
||||
fn kinovi_pricing_table_with_video_reference() {
|
||||
// (request, duration, base credits, surcharge credits)
|
||||
let cases: &[(fn(u8) -> GenerateSeedance2p0FastRequest, u8, u64, u64)] = &[
|
||||
let cases: &[(fn(u8) -> GenerateSeedance2p0FastRequest, u8, u64)] = &[
|
||||
// 480p: base + 4/s
|
||||
(r480, 4, 56, 16),
|
||||
(r480, 5, 70, 20),
|
||||
(r480, 10, 140, 40),
|
||||
(r480, 15, 210, 60),
|
||||
(r480, 4, 56 + 16),
|
||||
(r480, 5, 70 + 20),
|
||||
(r480, 10, 140 + 40),
|
||||
(r480, 15, 210 + 60),
|
||||
// 720p: base + 6/s
|
||||
(r720, 4, 112, 24),
|
||||
(r720, 5, 140, 30),
|
||||
(r720, 10, 280, 60),
|
||||
(r720, 15, 420, 90),
|
||||
(r720, 4, 112 + 24),
|
||||
(r720, 5, 140 + 30),
|
||||
(r720, 10, 280 + 60),
|
||||
(r720, 15, 420 + 90),
|
||||
];
|
||||
|
||||
for (make, duration, base, surcharge) in cases {
|
||||
for (make, duration, expected_credits) in cases {
|
||||
let costs = with_video_ref(make(*duration)).calculate_costs();
|
||||
assert_eq!(costs.base_cost.kinovi_credits, *base, "base for {duration}s");
|
||||
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(*surcharge), "surcharge for {duration}s");
|
||||
assert_eq!(costs.total_cost.kinovi_credits, base + surcharge, "total for {duration}s");
|
||||
assert_eq!(costs.kinovi_credits, *expected_credits,
|
||||
"duration {duration}s should cost {expected_credits} credits with a video reference");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surcharge_includes_usd_cents() {
|
||||
// 720p 5s + video ref = 140 + 30 = 170 credits; 17000/243 = 69.96 → 70¢
|
||||
// 720p 5s + video ref = 170 credits; 17000/193 = 88.08 → 89¢
|
||||
let costs = with_video_ref(r720(5)).calculate_costs();
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 140);
|
||||
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(30));
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 170);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_up, 70);
|
||||
assert_eq!(costs.total_cost.usd_cents_rounded_down, 69);
|
||||
assert!((costs.total_cost.usd_cents_fractional - (17000.0 / 243.0)).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// The base and surcharge parts each carry their own USD conversions.
|
||||
#[test]
|
||||
fn parts_have_their_own_usd_conversions() {
|
||||
let costs = with_video_ref(r720(5)).calculate_costs();
|
||||
|
||||
// Base: 140 credits; 14000/243 = 57.61.
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_up, 58);
|
||||
assert_eq!(costs.base_cost.usd_cents_rounded_down, 57);
|
||||
assert!((costs.base_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < 1e-9);
|
||||
|
||||
// Surcharge: 30 credits; 3000/243 = 12.35.
|
||||
let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge");
|
||||
assert_eq!(surcharge.kinovi_credits, 30);
|
||||
assert_eq!(surcharge.usd_cents_rounded_up, 13);
|
||||
assert_eq!(surcharge.usd_cents_rounded_down, 12);
|
||||
assert!((surcharge.usd_cents_fractional - (3000.0 / 243.0)).abs() < 1e-9);
|
||||
assert_eq!(costs.kinovi_credits, 170);
|
||||
assert_eq!(costs.usd_cents_rounded_up, 89);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_video_reference_list_has_no_surcharge() {
|
||||
let mut request = r720(5);
|
||||
request.reference_video_urls = Some(vec![]);
|
||||
let costs = request.calculate_costs();
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 140);
|
||||
assert!(costs.video_reference_surcharge_cost.is_none());
|
||||
assert_eq!(request.calculate_costs().kinovi_credits, 140);
|
||||
}
|
||||
|
||||
/// Surcharge is flat per generation regardless of how many reference
|
||||
@@ -656,7 +485,7 @@ mod tests {
|
||||
"https://example.com/a.mp4".to_string(),
|
||||
"https://example.com/b.mp4".to_string(),
|
||||
]);
|
||||
assert_eq!(request.calculate_costs().total_cost.kinovi_credits, 170);
|
||||
assert_eq!(request.calculate_costs().kinovi_credits, 170);
|
||||
}
|
||||
|
||||
/// The surcharge applies per generated video, so batches multiply it.
|
||||
@@ -664,10 +493,7 @@ mod tests {
|
||||
fn batch_multiplies_surcharge() {
|
||||
let request = with_video_ref(build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)));
|
||||
// (140 base + 30 surcharge) × 2 = 340 credits
|
||||
let costs = request.calculate_costs();
|
||||
assert_eq!(costs.base_cost.kinovi_credits, 280);
|
||||
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(60));
|
||||
assert_eq!(costs.total_cost.kinovi_credits, 340);
|
||||
assert_eq!(request.calculate_costs().kinovi_credits, 340);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,7 +501,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_does_not_affect_credits() {
|
||||
let baseline = r720(5).calculate_costs().total_cost.kinovi_credits;
|
||||
let baseline = r720(5).calculate_costs().kinovi_credits;
|
||||
|
||||
let ratios = [
|
||||
KinoviSeedance2p0FastAspectRatio::Landscape16x9,
|
||||
@@ -702,7 +528,7 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
};
|
||||
assert_eq!(
|
||||
req.calculate_costs().total_cost.kinovi_credits, baseline,
|
||||
req.calculate_costs().kinovi_credits, baseline,
|
||||
"Aspect ratio {:?} should not change credits from baseline {}", ar, baseline,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
pub (crate) mod test_utils;
|
||||
|
||||
// User API
|
||||
pub mod cost;
|
||||
pub mod creds;
|
||||
pub mod error;
|
||||
pub mod generate;
|
||||
|
||||
+8
-14
@@ -1,4 +1,4 @@
|
||||
use crate::cost::kinovi_generation_cost::KinoviGenerationCost;
|
||||
use crate::generate::cost::KinoviGenerationCost;
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
@@ -22,7 +22,7 @@ const CREDITS_PER_MIDJOURNEY_TASK: u32 = 12;
|
||||
/// Newer Kinovi credit-package rate. Used to convert credits to USD for the
|
||||
/// Midjourney image generation flow.
|
||||
///
|
||||
/// 500,000 credits / $2,159.09 = 231.58 ≈ 243 credits per dollar (rounded down).
|
||||
/// 22,000 credits / $114 = 192.98 ≈ 193 credits per dollar.
|
||||
|
||||
/// Kinovi only currently exposes a single Midjourney resolution preset.
|
||||
const MIDJOURNEY_RESOLUTION: &str = "1k";
|
||||
@@ -416,26 +416,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn usd_cents_batch_one() {
|
||||
// 12/243 × 100 = 4.94¢ → 5¢
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 5); // 1200/243 = 4.94 -> rounds UP
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_down, 4);
|
||||
assert!((make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_fractional - (1200.0 / 243.0)).abs() < 1e-9);
|
||||
// 12 / 193 × 100 = 6.21¢ → 6¢
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_batch_two() {
|
||||
// 24/243 × 100 = 9.88¢ → 10¢
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().usd_cents_rounded_up, 10); // 2400/243 = 9.88 -> rounds UP
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().usd_cents_rounded_down, 9);
|
||||
assert!((make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().usd_cents_fractional - (2400.0 / 243.0)).abs() < 1e-9);
|
||||
// 24 / 193 × 100 = 12.43¢ → 12¢
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().usd_cents_rounded_up, 13); // 2400/193 = 12.44 -> rounds UP
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_batch_four() {
|
||||
// 48/243 × 100 = 19.75¢ → 20¢
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 20); // 4800/243 = 19.75 -> rounds UP
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_down, 19);
|
||||
assert!((make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_fractional - (4800.0 / 243.0)).abs() < 1e-9);
|
||||
// 48 / 193 × 100 = 24.87¢ → 25¢
|
||||
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP
|
||||
}
|
||||
|
||||
/// Batch 4 should be exactly 4× the credit cost of batch 1, and the USD
|
||||
|
||||
+18
-18
@@ -141,7 +141,7 @@ impl KinoviGenerateVideoRequest {
|
||||
/// Credits per dollar for billing conversion.
|
||||
///
|
||||
/// Legacy 720p pricing uses the original Kinovi credit package rates.
|
||||
/// All other model/resolution combos use the newer rate: 525,000 credits / $2,159.0909.
|
||||
/// All other model/resolution combos use the newer rate: 22,000 credits / $114.
|
||||
fn credits_per_dollar(&self) -> f64 {
|
||||
match (self.model_type, self.output_resolution) {
|
||||
// Legacy: Seedance 2.0 Pro @ 720p — 25,000 credits for $99.99
|
||||
@@ -152,8 +152,8 @@ impl KinoviGenerateVideoRequest {
|
||||
(KinoviModelType::Seedance2Fast, None)
|
||||
| (KinoviModelType::Seedance2Fast, Some(KinoviOutputResolution::SevenTwentyP)) => 220.0,
|
||||
|
||||
// New pricing: 525,000 credits for $2,159.0909 (~243.16 credits/$1)
|
||||
_ => 243.0,
|
||||
// New pricing: 22,000 credits for $114 (~192.98 credits/$1)
|
||||
_ => 193.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,7 +669,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn usd_cents_legacy_fast_720p() {
|
||||
// 220 credits/$1: 140 credits = 57.61 → 58¢
|
||||
// 220 credits/$1: 140 credits = 63.6 → 64¢
|
||||
assert_eq!(fast(5, KinoviBatchCount::One).estimate_cost_in_usd_cents(), 64);
|
||||
assert_eq!(fast(10, KinoviBatchCount::One).estimate_cost_in_usd_cents(), 127);
|
||||
}
|
||||
@@ -678,24 +678,24 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_pro_480p() {
|
||||
// 243 credits/$1: 75 credits = 30.86 → 31¢
|
||||
assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 31);
|
||||
assert_eq!(pro_res(10, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 62);
|
||||
// 192.98 credits/$1: 75 credits = 38.86 → 39¢
|
||||
assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 39);
|
||||
assert_eq!(pro_res(10, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 78);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_pro_1080p() {
|
||||
// 243 credits/$1: 450 credits = 185.19 → 185¢
|
||||
assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).estimate_cost_in_usd_cents(), 185);
|
||||
// 900 credits = 370.37 → 370¢
|
||||
assert_eq!(pro_res(10, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).estimate_cost_in_usd_cents(), 370);
|
||||
// 192.98 credits/$1: 450 credits = 233.18 → 233¢
|
||||
assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).estimate_cost_in_usd_cents(), 233);
|
||||
// 900 credits = 466.36 → 466¢
|
||||
assert_eq!(pro_res(10, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).estimate_cost_in_usd_cents(), 466);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_fast_480p() {
|
||||
// 243 credits/$1: 50 credits = 20.58 → 21¢
|
||||
assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 21);
|
||||
assert_eq!(fast_res(10, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 41);
|
||||
// 192.98 credits/$1: 50 credits = 25.91 → 26¢
|
||||
assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 26);
|
||||
assert_eq!(fast_res(10, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 52);
|
||||
}
|
||||
|
||||
// ── credits_per_dollar: verify the rates directly ──
|
||||
@@ -711,11 +711,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn credits_per_dollar_new_rate() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).credits_per_dollar(), 193.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).credits_per_dollar(), 193.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).credits_per_dollar(), 193.0);
|
||||
// Fast 1080p (not officially supported) also uses new rate
|
||||
assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).credits_per_dollar(), 193.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-20
@@ -150,7 +150,7 @@ impl WorkflowRunTaskRequest {
|
||||
/// Credits per dollar for billing conversion.
|
||||
///
|
||||
/// Legacy 720p pricing uses the original Kinovi credit package rates.
|
||||
/// All other model/resolution combos use the newer rate: 525,000 credits / $2,159.0909.
|
||||
/// All other model/resolution combos use the newer rate: 22,000 credits / $114.
|
||||
fn credits_per_dollar(&self) -> f64 {
|
||||
match (self.model_type, self.output_resolution) {
|
||||
// Legacy: Seedance 2.0 Pro @ 720p — 25,000 credits for $99.99
|
||||
@@ -161,8 +161,8 @@ impl WorkflowRunTaskRequest {
|
||||
(KinoviModelTypeRaw::Seedance2Fast, None)
|
||||
| (KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 220.0,
|
||||
|
||||
// New pricing: 525,000 credits for $2,159.0909 (~243.16 credits/$1)
|
||||
_ => 243.0,
|
||||
// New pricing: 22,000 credits for $114 (~192.98 credits/$1)
|
||||
_ => 193.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,20 +661,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_pro_480p() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 31);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 62);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 39);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 78);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_pro_1080p() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 185);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 370);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 233);
|
||||
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 466);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usd_cents_new_fast_480p() {
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 21);
|
||||
assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 41);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 26);
|
||||
assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 52);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -687,10 +687,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn credits_per_dollar_new_rate() {
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 193.0);
|
||||
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 193.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 193.0);
|
||||
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 193.0);
|
||||
}
|
||||
|
||||
// ── Happy Horse 1.0 ──
|
||||
@@ -724,17 +724,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn happy_horse_uses_new_pricing_rate() {
|
||||
// All Happy Horse resolutions use the new 243 credits/$1 rate (not legacy)
|
||||
assert_eq!(happy_horse(5, KinoviBatchCountRaw::One).credits_per_dollar(), 243.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0);
|
||||
// All Happy Horse resolutions use the new 193 credits/$1 rate (not legacy)
|
||||
assert_eq!(happy_horse(5, KinoviBatchCountRaw::One).credits_per_dollar(), 193.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 193.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 193.0);
|
||||
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 193.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_horse_usd_cents_1080p() {
|
||||
// 243 credits/$1: 360 credits (4s×90) = 148.15 → 148¢
|
||||
assert_eq!(happy_horse_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 148);
|
||||
// 193 credits/$1: 360 credits (4s×90) = 186.53 → 187¢
|
||||
assert_eq!(happy_horse_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 187);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ name = "artcraft_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.6.2", features = [] }
|
||||
tauri-build = { version = "2.1.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
|
||||
@@ -65,13 +65,7 @@ fn do_build_cors_config(is_production: bool) -> Cors {
|
||||
}
|
||||
|
||||
// Remaining setup
|
||||
//
|
||||
// NB: actix-cors 0.7 flipped the `block_on_origin_mismatch` default to
|
||||
// false (mismatched origins get a 200 without CORS headers and the browser
|
||||
// enforces the block). We keep the long-standing 0.6 behavior of rejecting
|
||||
// mismatched origins server-side with a 400.
|
||||
cors.block_on_origin_mismatch(true)
|
||||
.allowed_methods(vec!["GET", "POST", "PUT", "OPTIONS", "DELETE"])
|
||||
cors.allowed_methods(vec!["GET", "POST", "OPTIONS", "DELETE"])
|
||||
.supports_credentials()
|
||||
.allowed_headers(vec![
|
||||
actix_http::header::ACCEPT,
|
||||
@@ -88,70 +82,11 @@ fn do_build_cors_config(is_production: bool) -> Cors {
|
||||
mod tests {
|
||||
use server_environment::ServerEnvironment;
|
||||
|
||||
use crate::testing::assert_no_origin_header_ok;
|
||||
use crate::testing::assert_origin_invalid;
|
||||
use crate::testing::assert_origin_ok;
|
||||
use crate::testing::assert_preflight_method_invalid;
|
||||
use crate::testing::assert_preflight_method_ok;
|
||||
|
||||
use super::build_cors_config;
|
||||
|
||||
/// Every production origin that hosts a product (or IS a backend that
|
||||
/// serves same-origin pages like swagger) must be allowed. This is the
|
||||
/// guard against `block_on_origin_mismatch(true)` locking out a real
|
||||
/// frontend after a CORS-crate upgrade.
|
||||
#[actix_rt::test]
|
||||
async fn test_production_product_and_backend_origins_are_allowed() {
|
||||
let production_cors = build_cors_config(ServerEnvironment::Production);
|
||||
|
||||
let origins = [
|
||||
// Backends (same-origin browser requests still carry an Origin header).
|
||||
"https://api.fakeyou.com",
|
||||
"https://api.storyteller.ai",
|
||||
// Products
|
||||
"https://fakeyou.com",
|
||||
"https://storyteller.ai",
|
||||
"https://app.getartcraft.com",
|
||||
"https://getartcraft.com",
|
||||
"https://www.getartcraft.com",
|
||||
"https://artcraft.ai",
|
||||
"https://www.artcraft.ai",
|
||||
"https://artcraft-dashboard.netlify.app",
|
||||
];
|
||||
for origin in origins {
|
||||
assert_origin_ok(&production_cors, origin).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-browser clients (curl, python API clients, server-to-server) send
|
||||
/// no Origin header and must never be blocked, even with
|
||||
/// `block_on_origin_mismatch(true)`.
|
||||
#[actix_rt::test]
|
||||
async fn test_requests_without_origin_header_are_not_blocked() {
|
||||
let production_cors = build_cors_config(ServerEnvironment::Production);
|
||||
assert_no_origin_header_ok(&production_cors).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_preflight_allows_every_supported_method() {
|
||||
let production_cors = build_cors_config(ServerEnvironment::Production);
|
||||
|
||||
// PUT is used by e.g. the folders routes (rename/star/color_code/cover_image).
|
||||
for method in ["GET", "POST", "PUT", "DELETE"] {
|
||||
assert_preflight_method_ok(&production_cors, "https://app.getartcraft.com", method).await;
|
||||
assert_preflight_method_ok(&production_cors, "https://storyteller.ai", method).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_preflight_rejects_unsupported_methods() {
|
||||
let production_cors = build_cors_config(ServerEnvironment::Production);
|
||||
|
||||
for method in ["PATCH", "TRACE", "CONNECT"] {
|
||||
assert_preflight_method_invalid(&production_cors, "https://app.getartcraft.com", method).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_fakeyou_production() {
|
||||
let production_cors = build_cors_config(ServerEnvironment::Production);
|
||||
|
||||
@@ -2,7 +2,6 @@ use actix_cors::Cors;
|
||||
use actix_http::body::{BoxBody, EitherBody};
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::dev::{ServiceResponse, Transform};
|
||||
use actix_web::http::Method;
|
||||
use actix_web::test;
|
||||
use actix_web::test::TestRequest;
|
||||
use speculoos::asserting;
|
||||
@@ -21,39 +20,6 @@ pub (crate) async fn assert_origin_invalid(cors: &Cors, hostname: &str) {
|
||||
.is_equal_to(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
/// Assert that a CORS preflight (OPTIONS + Access-Control-Request-Method)
|
||||
/// for the given method is accepted from the given origin.
|
||||
pub (crate) async fn assert_preflight_method_ok(cors: &Cors, hostname: &str, method: &str) {
|
||||
let response = make_preflight_request(cors, hostname, method).await;
|
||||
asserting(&format!("Preflight for {} from {} is allowed", method, hostname))
|
||||
.that(&response.status())
|
||||
.is_equal_to(StatusCode::OK);
|
||||
}
|
||||
|
||||
/// Assert that a CORS preflight for the given method is rejected.
|
||||
pub (crate) async fn assert_preflight_method_invalid(cors: &Cors, hostname: &str, method: &str) {
|
||||
let response = make_preflight_request(cors, hostname, method).await;
|
||||
asserting(&format!("Preflight for {} from {} is rejected", method, hostname))
|
||||
.that(&response.status())
|
||||
.is_equal_to(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
/// Assert that a request WITHOUT an Origin header (curl, server-to-server,
|
||||
/// native API clients) is never blocked by CORS, regardless of
|
||||
/// `block_on_origin_mismatch`.
|
||||
pub (crate) async fn assert_no_origin_header_ok(cors: &Cors) {
|
||||
let cors = cors.new_transform(test::ok_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let request = TestRequest::default().to_srv_request();
|
||||
let response = test::call_service(&cors, request).await;
|
||||
|
||||
asserting("Requests without an Origin header are not blocked")
|
||||
.that(&response.status())
|
||||
.is_equal_to(StatusCode::OK);
|
||||
}
|
||||
|
||||
async fn make_test_request(cors: &Cors, hostname: &str) -> ServiceResponse<EitherBody<BoxBody>> {
|
||||
let cors= cors.new_transform(test::ok_service())
|
||||
.await
|
||||
@@ -65,21 +31,3 @@ async fn make_test_request(cors: &Cors, hostname: &str) -> ServiceResponse<Eithe
|
||||
|
||||
test::call_service(&cors, request).await
|
||||
}
|
||||
|
||||
async fn make_preflight_request(
|
||||
cors: &Cors,
|
||||
hostname: &str,
|
||||
method: &str,
|
||||
) -> ServiceResponse<EitherBody<BoxBody>> {
|
||||
let cors = cors.new_transform(test::ok_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let request = TestRequest::default()
|
||||
.method(Method::OPTIONS)
|
||||
.insert_header(("Origin", hostname))
|
||||
.insert_header(("Access-Control-Request-Method", method))
|
||||
.to_srv_request();
|
||||
|
||||
test::call_service(&cors, request).await
|
||||
}
|
||||
|
||||
+3
-2
@@ -84,8 +84,9 @@ pub struct SessionUserRecord {
|
||||
}
|
||||
|
||||
impl SessionUserRecord {
|
||||
pub fn get_user_token(&self) -> &UserToken {
|
||||
&self.user_token
|
||||
// TODO(bt, 2022-12-20): Convert all users of the bare record to using `UserToken`, then get rid of this method.
|
||||
pub fn get_strongly_typed_user_token(&self) -> UserToken {
|
||||
self.user_token.clone()
|
||||
}
|
||||
|
||||
pub fn is_mod(&self) -> bool {
|
||||
|
||||
+6
-7
@@ -1,7 +1,8 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::common::payments_namespace::PaymentsNamespace;
|
||||
use errors::AnyhowResult;
|
||||
use sqlx::{Executor, MySql};
|
||||
use sqlx::pool::PoolConnection;
|
||||
use sqlx::MySql;
|
||||
|
||||
pub struct ActiveUserSubscription {
|
||||
pub user_token: String,
|
||||
@@ -18,12 +19,10 @@ pub struct ActiveUserSubscription {
|
||||
pub subscription_expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub async fn list_active_user_subscriptions<'e, 'c : 'e, E>(
|
||||
mysql_executor: E,
|
||||
pub async fn list_active_user_subscriptions(
|
||||
mysql_connection: &mut PoolConnection<MySql>,
|
||||
user_token: &str
|
||||
) -> AnyhowResult<Vec<ActiveUserSubscription>>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
) -> AnyhowResult<Vec<ActiveUserSubscription>> {
|
||||
// NB: "status=incomplete" subscriptions can happen when a user submits credit card info,
|
||||
// but the CRC is wrong. Eg. what happened with ftx on 2022.11.18.
|
||||
//
|
||||
@@ -46,7 +45,7 @@ WHERE
|
||||
"#,
|
||||
user_token,
|
||||
)
|
||||
.fetch_all(mysql_executor)
|
||||
.fetch_all(&mut **mysql_connection)
|
||||
.await?;
|
||||
|
||||
let records = records.into_iter()
|
||||
|
||||
@@ -162,10 +162,12 @@ tempfile.workspace = true
|
||||
# https://github.com/durch/rust-s3/pull/254
|
||||
rust-s3.workspace = true
|
||||
|
||||
actix.workspace = true
|
||||
actix-http.workspace = true
|
||||
actix-multipart.workspace = true
|
||||
actix-service.workspace = true
|
||||
actix-web.workspace = true
|
||||
actix-web-actors.workspace = true
|
||||
actix-web-lab.workspace = true
|
||||
|
||||
# Crypto
|
||||
|
||||
@@ -1,36 +1,15 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use log::info;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
pub async fn connect_to_database() -> AnyhowResult<MySqlPool> {
|
||||
pub async fn connect_to_database() -> AnyhowResult<MySqlPool>{
|
||||
let db_connection_string = env_get_mysql_connection_string_or_default();
|
||||
|
||||
// NB: The managed DB's connection ceiling is far above what we use, so the binding
|
||||
// constraint is this per-pod pool. Keep `replicas * MYSQL_MAX_CONNECTIONS` (plus the
|
||||
// background jobs' small pools) comfortably under the server's `max_connections`.
|
||||
let max_connections = easyenv::get_env_num("MYSQL_MAX_CONNECTIONS", 20)?;
|
||||
let min_connections = easyenv::get_env_num("MYSQL_MIN_CONNECTIONS", 0)?;
|
||||
|
||||
// NB: Fail fast under pool saturation rather than stacking up on sqlx's 30s default — a
|
||||
// shorter timeout sheds load and surfaces the problem instead of hiding it behind long waits.
|
||||
let acquire_timeout_seconds = easyenv::get_env_num::<u64>("MYSQL_ACQUIRE_TIMEOUT_SECONDS", 10)?;
|
||||
|
||||
// NB: Log the settings before building/connecting so a connect failure has context. We do NOT
|
||||
// log the connection string (it contains credentials).
|
||||
info!(
|
||||
"Building MySQL pool: max_connections={}, min_connections={}, acquire_timeout_seconds={}",
|
||||
max_connections, min_connections, acquire_timeout_seconds,
|
||||
);
|
||||
|
||||
let pool_options = MySqlPoolOptions::new()
|
||||
.max_connections(max_connections)
|
||||
.min_connections(min_connections)
|
||||
.acquire_timeout(Duration::from_secs(acquire_timeout_seconds));
|
||||
.max_connections(easyenv::get_env_num("MYSQL_MAX_CONNECTIONS", 5)?);
|
||||
|
||||
// TODO(bt,2024-09-17): Configure logging, etc. after we've upgraded sqlx
|
||||
|
||||
let pool = pool_options
|
||||
.connect(&db_connection_string)
|
||||
|
||||
+27
-1
@@ -2,6 +2,7 @@ use std::fmt::{Display, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::session::session_checker_error::SessionCheckerError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_payload_error::AvtCookiePayloadError;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_payload_error::HttpUserSessionPayloadError;
|
||||
use actix_http::StatusCode;
|
||||
@@ -18,7 +19,7 @@ use serde::Serialize;
|
||||
/// ```ignore
|
||||
/// pub async fn my_handler(...) -> Result<Json<MyResponse>, CommonWebError> {
|
||||
/// // sqlx errors, anyhow errors, session errors — all convert via ?
|
||||
/// let user = require_user_session(&req, ...)?;
|
||||
/// let user = require_user_session_using_connection(&req, ...)?;
|
||||
/// let data = some_db_query(&pool).await?;
|
||||
///
|
||||
/// if data.is_none() {
|
||||
@@ -316,6 +317,15 @@ impl From<serde_json::Error> for CommonWebError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequireUserSessionError> for CommonWebError {
|
||||
fn from(value: RequireUserSessionError) -> Self {
|
||||
match value {
|
||||
RequireUserSessionError::NotAuthorized => Self::NotAuthorized,
|
||||
RequireUserSessionError::ServerError => Self::from_error(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpUserSessionPayloadError> for CommonWebError {
|
||||
fn from(err: HttpUserSessionPayloadError) -> Self {
|
||||
if err.is_server_error() {
|
||||
@@ -535,6 +545,22 @@ mod tests {
|
||||
assert!(source.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_user_session_not_authorized_maps_to_401() {
|
||||
let error: CommonWebError = RequireUserSessionError::NotAuthorized.into();
|
||||
assert_eq!(error.status_code(), StatusCode::UNAUTHORIZED);
|
||||
assert!(!error.is_server_error());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn require_user_session_server_error_wraps_cause() {
|
||||
let error: CommonWebError = RequireUserSessionError::ServerError.into();
|
||||
assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert!(error.is_server_error());
|
||||
assert!(error.cause().is_some());
|
||||
assert!(format!("{}", error.cause().unwrap()).contains("ServerError"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_json_error_converts_to_500() {
|
||||
let bad_json = "not json at all{{{";
|
||||
|
||||
+2
-3
@@ -120,8 +120,7 @@ pub async fn create_scene_handler(
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
// ==================== FILE DATA ==================== //
|
||||
|
||||
@@ -167,7 +166,7 @@ pub async fn create_scene_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(MediaFileClass::Dimensional),
|
||||
media_file_type: MediaFileType::SceneRon,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
|
||||
let mut field_name = None;
|
||||
let mut field_filename = None;
|
||||
|
||||
if let Some(content_disposition) = field.content_disposition() {
|
||||
if let content_disposition = field.content_disposition() {
|
||||
field_name = content_disposition.get_name()
|
||||
.map(|s| s.to_string());
|
||||
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
|
||||
|
||||
+2
-3
@@ -127,8 +127,7 @@ pub async fn process_upload_media_file(
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_file_size_bytes = upload_media_request.file_bytes
|
||||
.as_ref()
|
||||
@@ -326,7 +325,7 @@ pub async fn process_upload_media_file(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: None,
|
||||
media_file_type,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
|
||||
let mut field_name = None;
|
||||
let mut field_filename = None;
|
||||
|
||||
if let Some(content_disposition) = field.content_disposition() {
|
||||
if let content_disposition = field.content_disposition() {
|
||||
field_name = content_disposition.get_name()
|
||||
.map(|s| s.to_string());
|
||||
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
|
||||
|
||||
+4
-5
@@ -129,8 +129,7 @@ pub async fn handle_upload(
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_file_size_bytes = upload_media_request.file_bytes
|
||||
.as_ref()
|
||||
@@ -301,7 +300,7 @@ pub async fn handle_upload(
|
||||
checksum_sha2: &hash,
|
||||
public_upload_path: &public_upload_path,
|
||||
maybe_extra_file_modification_info: None,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: None,
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
@@ -318,12 +317,12 @@ pub async fn handle_upload(
|
||||
let firehose_result = match media_upload_source {
|
||||
MediaUploadSource::DeviceApi => {
|
||||
server_state.firehose_publisher.publish_device_media_recorded(
|
||||
maybe_user_token,
|
||||
maybe_user_token.as_ref(),
|
||||
&token).await
|
||||
}
|
||||
_ => {
|
||||
server_state.firehose_publisher.publish_media_uploaded(
|
||||
maybe_user_token,
|
||||
maybe_user_token.as_ref(),
|
||||
&token).await
|
||||
}
|
||||
};
|
||||
|
||||
+9
-3
@@ -28,7 +28,8 @@ use crate::http_server::requests::request_headers::get_routing_tag_header::get_r
|
||||
use crate::http_server::requests::request_headers::has_debug_header::has_debug_header;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_extended::require_user_session_extended;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_extended_using_connection::require_user_session_extended_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::cleaners::empty_media_file_token_to_null::empty_media_file_token_to_null;
|
||||
|
||||
@@ -113,10 +114,15 @@ pub async fn enqueue_face_fusion_workflow_handler(
|
||||
let maybe_avt_token = server_state.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
let user_session = require_user_session_extended(
|
||||
let user_session = require_user_session_extended_using_connection(
|
||||
&http_request,
|
||||
&server_state.session_checker,
|
||||
&mut *mysql_connection).await?;
|
||||
&mut mysql_connection)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
RequireUserSessionError::ServerError => CommonWebError::from_error(err),
|
||||
RequireUserSessionError::NotAuthorized => CommonWebError::NotAuthorized,
|
||||
})?;
|
||||
|
||||
// ==================== PAID PLAN + PRIORITY ==================== //
|
||||
|
||||
|
||||
+9
-3
@@ -29,7 +29,8 @@ use crate::http_server::requests::request_headers::get_routing_tag_header::get_r
|
||||
use crate::http_server::requests::request_headers::has_debug_header::has_debug_header;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_extended::require_user_session_extended;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::RequireUserSessionError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_extended_using_connection::require_user_session_extended_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::cleaners::empty_media_file_token_to_null::empty_media_file_token_to_null;
|
||||
|
||||
@@ -121,10 +122,15 @@ pub async fn enqueue_live_portrait_workflow_handler(
|
||||
let maybe_avt_token = server_state.avt_cookie_manager
|
||||
.get_avt_token_from_request(&http_request);
|
||||
|
||||
let user_session = require_user_session_extended(
|
||||
let user_session = require_user_session_extended_using_connection(
|
||||
&http_request,
|
||||
&server_state.session_checker,
|
||||
&mut *mysql_connection).await?;
|
||||
&mut mysql_connection)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
RequireUserSessionError::ServerError => CommonWebError::from_error(err),
|
||||
RequireUserSessionError::NotAuthorized => CommonWebError::NotAuthorized,
|
||||
})?;
|
||||
|
||||
// ==================== PAID PLAN + PRIORITY ==================== //
|
||||
|
||||
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use actix::*;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{
|
||||
web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
|
||||
};
|
||||
use actix_web_actors::ws;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::error;
|
||||
use redis::{Commands, RedisResult};
|
||||
use utoipa::ToSchema;
|
||||
use web::Data;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use mysql_queries::queries::generic_inference::web::get_inference_job_status::get_inference_job_status;
|
||||
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
|
||||
use redis_schema::keys::inference_job::style_transfer_progress_key::StyleTransferProgressKey;
|
||||
use redis_schema::payloads::inference_job::style_transfer_progress_state::InferenceProgressDetailsResponse;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
use crate::http_server::deprecated_endpoints::workflows::enqueue::{progress_tracker_server, progress_tracker_session};
|
||||
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
|
||||
use crate::http_server::endpoints::inference_job::utils::extractors::extract_polymorphic_inference_args::extract_polymorphic_inference_args;
|
||||
use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For certain jobs or job classes (eg. non-premium), we kill the jobs if the user hasn't
|
||||
/// maintained a keepalive. This prevents wasted work when users who are unlikely to return
|
||||
/// navigate away. Premium users have accounts and can always return to the site, so they
|
||||
/// typically do not require keepalive.
|
||||
const JOB_KEEPALIVE_TTL_SECONDS : usize = 60 * 3;
|
||||
|
||||
|
||||
|
||||
/// For the URL PathInfo
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct GetInferenceJobStatusPathInfo {
|
||||
token: InferenceJobToken,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct GetInferenceJobStatusSuccessResponse {
|
||||
pub success: bool,
|
||||
pub state: InferenceJobStatusResponsePayload,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct InferenceJobStatusResponsePayload {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
pub request: RequestDetailsResponse,
|
||||
pub status: StatusDetailsResponse,
|
||||
pub maybe_result: Option<InferenceProgressDetailsResponse>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
||||
/// Details about what the user requested for generation
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct RequestDetailsResponse {
|
||||
}
|
||||
|
||||
/// Details about the ongoing job status
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct StatusDetailsResponse {
|
||||
/// Primary status from the database (a state machine).
|
||||
pub status: JobStatusPlus,
|
||||
|
||||
/// Extra, temporary status from Redis.
|
||||
/// This can denote inference progress, and the Python code can write to it.
|
||||
pub maybe_extra_status_description: Option<String>,
|
||||
|
||||
pub maybe_assigned_worker: Option<String>,
|
||||
pub maybe_assigned_cluster: Option<String>,
|
||||
|
||||
pub maybe_first_started_at: Option<DateTime<Utc>>,
|
||||
|
||||
pub attempt_count: u8,
|
||||
|
||||
/// Whether the frontend needs to maintain a keepalive check.
|
||||
/// This is typically only for non-premium users.
|
||||
pub requires_keepalive: bool,
|
||||
|
||||
/// An enum the frontend can use to display localized/I18N error
|
||||
/// messages. These pertain to both transient and permanent failures.
|
||||
pub maybe_failure_category: Option<FrontendFailureCategory>,
|
||||
|
||||
/// This is an integer number between 0 and 100 (both inclusive) that
|
||||
/// reports the completeness.
|
||||
pub progress_percentage: u8,
|
||||
}
|
||||
|
||||
// #[derive(Serialize, ToSchema)]
|
||||
// pub struct InferenceStageDetails {
|
||||
// pub stage_progress: u8,
|
||||
// pub expected_frame_count: u8,
|
||||
// pub stage_complete: bool,
|
||||
// pub frames: Vec<Url>,
|
||||
// }
|
||||
|
||||
/// Details about the completed result (if any)
|
||||
// #[derive(Serialize, ToSchema)]
|
||||
// pub struct InferenceProgressDetailsResponse {
|
||||
// pub expected_stages: u8,
|
||||
// pub currently_active_stage: u8,
|
||||
// pub per_stage_frame_count: u8,
|
||||
// pub stages: Vec<InferenceStageDetails>,
|
||||
// }
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum GetInferenceJobStatusError {
|
||||
ServerError,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl ResponseError for GetInferenceJobStatusError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GetInferenceJobStatusError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
GetInferenceJobStatusError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
Self::ServerError => "server error".to_string(),
|
||||
Self::NotFound => "not found".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for GetInferenceJobStatusError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_inference_preview_status_handler_ws(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetInferenceJobStatusPathInfo>,
|
||||
server_state: Data<Arc<ServerState>>) -> Result<Json<GetInferenceJobStatusSuccessResponse>, GetInferenceJobStatusError>
|
||||
{
|
||||
if path.token.as_str().trim() == "None" {
|
||||
// NB: A bunch of Python clients use our API and can fail in this manner.
|
||||
// This was a large traffic driver during the 2023-03-08 outage.
|
||||
// Presumably it's this client: https://github.com/shards-7/fakeyou.py
|
||||
return Err(GetInferenceJobStatusError::NotFound);
|
||||
}
|
||||
|
||||
// NB: Since this is publicly exposed, we don't query sensitive data.
|
||||
let maybe_status = get_inference_job_status(
|
||||
&path.token,
|
||||
&server_state.mysql_pool
|
||||
).await;
|
||||
|
||||
let record = match maybe_status {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => return Err(GetInferenceJobStatusError::NotFound),
|
||||
Err(err) => {
|
||||
error!("tts job query error: {:?}", err);
|
||||
return Err(GetInferenceJobStatusError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
error!("redis error: {:?}", e);
|
||||
GetInferenceJobStatusError::ServerError
|
||||
})?;
|
||||
|
||||
let progress_key = StyleTransferProgressKey::new_for_job_id(record.job_token.clone());
|
||||
let progress_value: RedisResult<Option<String>> = redis.get(progress_key.to_string());
|
||||
|
||||
let maybe_result = match progress_value {
|
||||
Ok(Some(value)) => {
|
||||
let progress_details: InferenceProgressDetailsResponse = serde_json::from_str(&value)
|
||||
.map_err(|e| {
|
||||
error!("redis error: {:?}", e);
|
||||
GetInferenceJobStatusError::ServerError
|
||||
})?;
|
||||
|
||||
Some(progress_details)
|
||||
},
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
error!("redis error: {:?}", e);
|
||||
return Err(GetInferenceJobStatusError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
Ok(Json(GetInferenceJobStatusSuccessResponse {
|
||||
success: true,
|
||||
state: record_to_payload(record, maybe_result, None),
|
||||
}))
|
||||
}
|
||||
|
||||
fn record_to_payload(
|
||||
record: GenericInferenceJobStatus,
|
||||
progress_details: Option<InferenceProgressDetailsResponse>,
|
||||
maybe_extra_status_description: Option<String>,
|
||||
) -> InferenceJobStatusResponsePayload {
|
||||
let inference_category = record.request_details.inference_category;
|
||||
|
||||
// NB: Fail open. We don't want to fail the request if we can't extract the args.
|
||||
let maybe_polymorphic_args = extract_polymorphic_inference_args(&record)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
let progress_percentage = estimate_job_progress(&record, maybe_polymorphic_args.as_ref());
|
||||
|
||||
InferenceJobStatusResponsePayload {
|
||||
job_token: record.job_token,
|
||||
request: RequestDetailsResponse {
|
||||
// inference_category: record.request_details.inference_category,
|
||||
// maybe_model_type: maybe_filter_model_name(record.request_details.maybe_model_type.as_deref()),
|
||||
// maybe_model_token: record.request_details.maybe_model_token,
|
||||
// maybe_model_title: record.request_details.maybe_model_title,
|
||||
// maybe_raw_inference_text: record.request_details.maybe_raw_inference_text,
|
||||
// maybe_style_name: record.request_details.maybe_style_name,
|
||||
},
|
||||
status: StatusDetailsResponse {
|
||||
status: record.status,
|
||||
maybe_extra_status_description,
|
||||
maybe_assigned_worker: maybe_filter_model_name(record.maybe_assigned_worker.as_deref()),
|
||||
maybe_assigned_cluster: record.maybe_assigned_cluster,
|
||||
maybe_first_started_at: record.maybe_first_started_at,
|
||||
attempt_count: record.attempt_count as u8,
|
||||
requires_keepalive: record.is_keepalive_required,
|
||||
maybe_failure_category: record.maybe_frontend_failure_category,
|
||||
progress_percentage,
|
||||
},
|
||||
maybe_result: progress_details,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProgressTrackerState {
|
||||
|
||||
}
|
||||
pub async fn ws_progress_route(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
srv: web::Data<Addr<progress_tracker_server::ChatServer>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
ws::start(
|
||||
progress_tracker_session::WsChatSession {
|
||||
id: 0,
|
||||
hb: Instant::now(),
|
||||
room: "main".to_owned(),
|
||||
name: None,
|
||||
addr: srv.get_ref().clone(),
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
)
|
||||
}
|
||||
+3
@@ -3,4 +3,7 @@ pub mod enqueue_live_portrait_workflow_handler;
|
||||
pub mod enqueue_studio_workflow_handler;
|
||||
pub mod enqueue_video_style_transfer_workflow_handler;
|
||||
pub mod get_inference_preview_status;
|
||||
pub mod get_inference_preview_status_ws;
|
||||
pub mod progress_tracker_server;
|
||||
pub mod progress_tracker_session;
|
||||
pub mod vst_common;
|
||||
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
//! `ChatServer` is an actor. It maintains a list of connection client sessions.
|
||||
//! It also manages available rooms. Peers send messages to other peers in same
|
||||
//! room through `ChatServer`.
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use actix::prelude::*;
|
||||
use log::info;
|
||||
use rand::{rngs::ThreadRng, Rng};
|
||||
|
||||
/// Chat server sends this messages to session
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct Message(pub String);
|
||||
|
||||
/// Message for chat server communications
|
||||
|
||||
/// New chat session is created
|
||||
#[derive(Message)]
|
||||
#[rtype(usize)]
|
||||
pub struct Connect {
|
||||
pub addr: Recipient<Message>,
|
||||
}
|
||||
|
||||
/// Session is disconnected
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct Disconnect {
|
||||
pub id: usize,
|
||||
}
|
||||
|
||||
/// Send message to specific room
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct ClientMessage {
|
||||
/// Id of the client session
|
||||
pub id: usize,
|
||||
/// Peer message
|
||||
pub msg: String,
|
||||
/// Room name
|
||||
pub room: String,
|
||||
}
|
||||
|
||||
/// List of available rooms
|
||||
pub struct ListRooms;
|
||||
|
||||
impl actix::Message for ListRooms {
|
||||
type Result = Vec<String>;
|
||||
}
|
||||
|
||||
/// Join room, if room does not exists create new one.
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct Join {
|
||||
/// Client ID
|
||||
pub id: usize,
|
||||
|
||||
/// Room name
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat session.
|
||||
///
|
||||
/// Implementation is very naïve.
|
||||
#[derive(Debug)]
|
||||
pub struct ChatServer {
|
||||
sessions: HashMap<usize, Recipient<Message>>,
|
||||
rooms: HashMap<String, HashSet<usize>>,
|
||||
rng: ThreadRng,
|
||||
visitor_count: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl ChatServer {
|
||||
pub fn new(visitor_count: Arc<AtomicUsize>) -> ChatServer {
|
||||
// default room
|
||||
let mut rooms = HashMap::new();
|
||||
rooms.insert("main".to_owned(), HashSet::new());
|
||||
|
||||
ChatServer {
|
||||
sessions: HashMap::new(),
|
||||
rooms,
|
||||
rng: rand::thread_rng(),
|
||||
visitor_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatServer {
|
||||
/// Send message to all users in the room
|
||||
fn send_message(&self, room: &str, message: &str, skip_id: usize) {
|
||||
if let Some(sessions) = self.rooms.get(room) {
|
||||
for id in sessions {
|
||||
if *id != skip_id {
|
||||
if let Some(addr) = self.sessions.get(id) {
|
||||
addr.do_send(Message(message.to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make actor from `ChatServer`
|
||||
impl Actor for ChatServer {
|
||||
/// We are going to use simple Context, we just need ability to communicate
|
||||
/// with other actors.
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
/// Handler for Connect message.
|
||||
///
|
||||
/// Register new session and assign unique id to this session
|
||||
impl Handler<Connect> for ChatServer {
|
||||
type Result = usize;
|
||||
|
||||
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result {
|
||||
info!("Someone joined");
|
||||
|
||||
// notify all users in same room
|
||||
self.send_message("main", "Someone joined", 0);
|
||||
|
||||
// register session with random id
|
||||
let id = self.rng.random::<u64>() as usize;
|
||||
self.sessions.insert(id, msg.addr);
|
||||
|
||||
// auto join session to main room
|
||||
self.rooms.entry("main".to_owned()).or_default().insert(id);
|
||||
|
||||
let count = self.visitor_count.fetch_add(1, Ordering::SeqCst);
|
||||
self.send_message("main", &format!("Total visitors {count}"), 0);
|
||||
|
||||
// send id back
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for Disconnect message.
|
||||
impl Handler<Disconnect> for ChatServer {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) {
|
||||
info!("Someone disconnected");
|
||||
|
||||
let mut rooms: Vec<String> = Vec::new();
|
||||
|
||||
// remove address
|
||||
if self.sessions.remove(&msg.id).is_some() {
|
||||
// remove session from all rooms
|
||||
for (name, sessions) in &mut self.rooms {
|
||||
if sessions.remove(&msg.id) {
|
||||
rooms.push(name.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
// send message to other users
|
||||
for room in rooms {
|
||||
self.send_message(&room, "Someone disconnected", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for Message message.
|
||||
impl Handler<ClientMessage> for ChatServer {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ClientMessage, _: &mut Context<Self>) {
|
||||
self.send_message(&msg.room, msg.msg.as_str(), msg.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `ListRooms` message.
|
||||
impl Handler<ListRooms> for ChatServer {
|
||||
type Result = MessageResult<ListRooms>;
|
||||
|
||||
fn handle(&mut self, _: ListRooms, _: &mut Context<Self>) -> Self::Result {
|
||||
let mut rooms = Vec::new();
|
||||
|
||||
for key in self.rooms.keys() {
|
||||
rooms.push(key.to_owned())
|
||||
}
|
||||
|
||||
MessageResult(rooms)
|
||||
}
|
||||
}
|
||||
|
||||
/// Join room, send disconnect message to old room
|
||||
/// send join message to new room
|
||||
impl Handler<Join> for ChatServer {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: Join, _: &mut Context<Self>) {
|
||||
let Join { id, name } = msg;
|
||||
let mut rooms = Vec::new();
|
||||
|
||||
// remove session from all rooms
|
||||
for (n, sessions) in &mut self.rooms {
|
||||
if sessions.remove(&id) {
|
||||
rooms.push(n.to_owned());
|
||||
}
|
||||
}
|
||||
// send message to other users
|
||||
for room in rooms {
|
||||
self.send_message(&room, "Someone disconnected", 0);
|
||||
}
|
||||
|
||||
self.rooms.entry(name.clone()).or_default().insert(id);
|
||||
|
||||
self.send_message(&name, "Someone connected", id);
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use actix::prelude::*;
|
||||
use actix_web_actors::ws;
|
||||
use log::{debug, info, warn};
|
||||
|
||||
use crate::http_server::deprecated_endpoints::workflows::enqueue::progress_tracker_server;
|
||||
use crate::http_server::deprecated_endpoints::workflows::enqueue::progress_tracker_server::ChatServer;
|
||||
|
||||
/// How often heartbeat pings are sent
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
/// How long before lack of client response causes a timeout
|
||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WsChatSession {
|
||||
/// unique session id
|
||||
pub id: usize,
|
||||
|
||||
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
|
||||
/// otherwise we drop connection.
|
||||
pub hb: Instant,
|
||||
|
||||
/// joined room
|
||||
pub room: String,
|
||||
|
||||
/// peer name
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Chat server
|
||||
pub addr: Addr<ChatServer>,
|
||||
}
|
||||
|
||||
impl WsChatSession {
|
||||
/// helper method that sends ping to client every 5 seconds (HEARTBEAT_INTERVAL).
|
||||
///
|
||||
/// also this method checks heartbeats from client
|
||||
fn hb(&self, ctx: &mut ws::WebsocketContext<Self>) {
|
||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||
// check client heartbeats
|
||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||
// heartbeat timed out
|
||||
warn!("Websocket Client heartbeat failed, disconnecting!");
|
||||
|
||||
// notify chat server
|
||||
act.addr.do_send(progress_tracker_server::Disconnect { id: act.id });
|
||||
|
||||
// stop actor
|
||||
ctx.stop();
|
||||
|
||||
// don't try to send a ping
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ping(b"");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for WsChatSession {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
/// Method is called on actor start.
|
||||
/// We register ws session with ChatServer
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// we'll start heartbeat process on session start.
|
||||
self.hb(ctx);
|
||||
|
||||
// register self in chat server. `AsyncContext::wait` register
|
||||
// future within context, but context waits until this future resolves
|
||||
// before processing any other events.
|
||||
// HttpContext::state() is instance of WsChatSessionState, state is shared
|
||||
// across all routes within application
|
||||
let addr = ctx.address();
|
||||
self.addr
|
||||
.send(progress_tracker_server::Connect {
|
||||
addr: addr.recipient(),
|
||||
})
|
||||
.into_actor(self)
|
||||
.then(|res, act, ctx| {
|
||||
match res {
|
||||
Ok(res) => act.id = res,
|
||||
// something is wrong with chat server
|
||||
_ => ctx.stop(),
|
||||
}
|
||||
fut::ready(())
|
||||
})
|
||||
.wait(ctx);
|
||||
}
|
||||
|
||||
fn stopping(&mut self, _: &mut Self::Context) -> Running {
|
||||
// notify chat server
|
||||
self.addr.do_send(progress_tracker_server::Disconnect { id: self.id });
|
||||
Running::Stop
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle messages from chat server, we simply send it to peer websocket
|
||||
impl Handler<progress_tracker_server::Message> for WsChatSession {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: progress_tracker_server::Message, ctx: &mut Self::Context) {
|
||||
ctx.text(msg.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// WebSocket message handler
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsChatSession {
|
||||
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||
log::debug!("WEBSOCKET MESSAGE: {msg:?}");
|
||||
let msg = match msg {
|
||||
Err(_) => {
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
Ok(msg) => msg,
|
||||
};
|
||||
|
||||
log::debug!("WEBSOCKET MESSAGE: {msg:?}");
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
ctx.pong(&msg);
|
||||
}
|
||||
ws::Message::Pong(_) => {
|
||||
self.hb = Instant::now();
|
||||
}
|
||||
ws::Message::Text(text) => {
|
||||
let m = text.trim();
|
||||
// we check for /sss type of messages
|
||||
if m.starts_with('/') {
|
||||
let v: Vec<&str> = m.splitn(2, ' ').collect();
|
||||
match v[0] {
|
||||
"/list" => {
|
||||
// Send ListRooms message to chat server and wait for
|
||||
// response
|
||||
debug!("List rooms");
|
||||
self.addr
|
||||
.send(progress_tracker_server::ListRooms)
|
||||
.into_actor(self)
|
||||
.then(|res, _, ctx| {
|
||||
match res {
|
||||
Ok(rooms) => {
|
||||
for room in rooms {
|
||||
ctx.text(room);
|
||||
}
|
||||
}
|
||||
_ => warn!("Something is wrong"),
|
||||
}
|
||||
fut::ready(())
|
||||
})
|
||||
.wait(ctx)
|
||||
// .wait(ctx) pauses all events in context,
|
||||
// so actor wont receive any new messages until it get list
|
||||
// of rooms back
|
||||
}
|
||||
"/join" => {
|
||||
if v.len() == 2 {
|
||||
v[1].clone_into(&mut self.room);
|
||||
self.addr.do_send(progress_tracker_server::Join {
|
||||
id: self.id,
|
||||
name: self.room.clone(),
|
||||
});
|
||||
|
||||
ctx.text("joined");
|
||||
} else {
|
||||
ctx.text("!!! room name is required");
|
||||
}
|
||||
}
|
||||
"/name" => {
|
||||
if v.len() == 2 {
|
||||
self.name = Some(v[1].to_owned());
|
||||
} else {
|
||||
ctx.text("!!! name is required");
|
||||
}
|
||||
}
|
||||
_ => ctx.text(format!("!!! unknown command: {m:?}")),
|
||||
}
|
||||
} else {
|
||||
let msg = if let Some(ref name) = self.name {
|
||||
format!("{name}: {m}")
|
||||
} else {
|
||||
m.to_owned()
|
||||
};
|
||||
// send message to chat server
|
||||
self.addr.do_send(progress_tracker_server::ClientMessage {
|
||||
id: self.id,
|
||||
msg,
|
||||
room: self.room.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
ws::Message::Binary(_) => warn!("Unexpected binary"),
|
||||
ws::Message::Close(reason) => {
|
||||
ctx.close(reason);
|
||||
ctx.stop();
|
||||
}
|
||||
ws::Message::Continuation(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
ws::Message::Nop => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -14,7 +14,7 @@ use mysql_queries::queries::users::user_profiles::get_user_profile_by_username::
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAXIMUM_KEYS : u32 = 100;
|
||||
@@ -60,7 +60,12 @@ pub async fn create_beta_keys_handler(
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<HttpResponse, CommonWebError>
|
||||
{
|
||||
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
|
||||
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
RequireModeratorError::ServerError => CommonWebError::from_error(err),
|
||||
RequireModeratorError::NotAuthorized => CommonWebError::NotAuthorized,
|
||||
})?;
|
||||
|
||||
let mut maybe_referrer_user_token = None;
|
||||
|
||||
|
||||
+7
-2
@@ -14,7 +14,7 @@ use tokens::tokens::w2l_results::W2lResultToken;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
@@ -58,7 +58,12 @@ pub async fn edit_beta_key_distributed_flag_handler(
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<HttpResponse, CommonWebError>
|
||||
{
|
||||
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
|
||||
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
RequireModeratorError::ServerError => CommonWebError::from_error(err),
|
||||
RequireModeratorError::NotAuthorized => CommonWebError::NotAuthorized,
|
||||
})?;
|
||||
|
||||
edit_beta_key_distributed_flag(&path.token, request.is_distributed, &server_state.mysql_pool)
|
||||
.await
|
||||
|
||||
+7
-2
@@ -13,7 +13,7 @@ use tokens::tokens::beta_keys::BetaKeyToken;
|
||||
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// For the URL PathInfo
|
||||
@@ -57,7 +57,12 @@ pub async fn edit_beta_key_note_handler(
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<HttpResponse, CommonWebError>
|
||||
{
|
||||
let user_session = require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
|
||||
let user_session = require_moderator(&http_request, &server_state, UseDatabase::GrabNewConnection)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
RequireModeratorError::ServerError => CommonWebError::from_error(err),
|
||||
RequireModeratorError::NotAuthorized => CommonWebError::NotAuthorized,
|
||||
})?;
|
||||
|
||||
let maybe_note = request.note.as_deref()
|
||||
.map(|note| note.trim())
|
||||
|
||||
+7
-3
@@ -17,7 +17,7 @@ use tokens::tokens::beta_keys::BetaKeyToken;
|
||||
use crate::http_server::common_responses::pagination_page::PaginationPage;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::{require_user_session, RequireUserSessionError};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Copy, Clone, Deserialize, ToSchema)]
|
||||
@@ -108,8 +108,12 @@ pub async fn list_beta_keys_handler(
|
||||
query: Query<ListBetaKeysQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, CommonWebError> {
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &server_state.mysql_pool)
|
||||
.await?;
|
||||
let user_session = require_user_session(&http_request, &server_state)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
RequireUserSessionError::ServerError => CommonWebError::from_error(err),
|
||||
RequireUserSessionError::NotAuthorized => CommonWebError::NotAuthorized,
|
||||
})?;
|
||||
|
||||
let mut is_mod = user_session.can_ban_users;
|
||||
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ pub async fn redeem_beta_key_handler(
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<HttpResponse, CommonWebError>
|
||||
{
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
|
||||
let user_session = require_user_session(&http_request, &server_state).await?;
|
||||
|
||||
let rate_limiter = &server_state.redis_rate_limiters.logged_out;
|
||||
|
||||
|
||||
+6
-2
@@ -10,7 +10,7 @@ use mysql_queries::queries::characters::delete_character::delete_character;
|
||||
use mysql_queries::queries::characters::get_character_by_token_including_deleted::get_character_by_token_including_deleted;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Delete a character.
|
||||
@@ -40,7 +40,11 @@ pub async fn delete_character_handler(
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request,
|
||||
&server_state.session_checker,
|
||||
&mut mysql_connection,
|
||||
).await?;
|
||||
|
||||
let user_token = &user_session.user_token;
|
||||
let is_mod = user_session.is_mod();
|
||||
|
||||
+6
-2
@@ -11,7 +11,7 @@ use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::update_character::update_character::{update_character, UpdateCharacterArgs};
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Edit a character's name or description.
|
||||
@@ -38,7 +38,11 @@ pub async fn edit_character_handler(
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request,
|
||||
&server_state.session_checker,
|
||||
&mut mysql_connection,
|
||||
).await?;
|
||||
|
||||
let user_token = &user_session.user_token;
|
||||
let is_mod = user_session.is_mod();
|
||||
|
||||
+6
-2
@@ -16,7 +16,7 @@ use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// List characters for the current session.
|
||||
@@ -39,7 +39,11 @@ pub async fn list_characters_handler(
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *mysql_connection).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request,
|
||||
&server_state.session_checker,
|
||||
&mut mysql_connection,
|
||||
).await?;
|
||||
|
||||
let user_token = &user_session.user_token;
|
||||
|
||||
|
||||
+4
-2
@@ -17,7 +17,7 @@ use mysql_queries::queries::folders::folder::update_folder_color_code::{
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_COLOR_CODE_LEN: usize = 16;
|
||||
@@ -47,7 +47,9 @@ pub async fn color_code_folder_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
// Normalize: trim, treat empty after trim as `None`. This lets clients
|
||||
// clear by passing `""` or by omitting the field entirely.
|
||||
|
||||
+4
-2
@@ -23,7 +23,7 @@ use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Set or clear the custom cover image on a folder.
|
||||
@@ -64,7 +64,9 @@ pub async fn cover_image_folder_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
// Confirm the folder exists + is owned. Authoritative 404 source.
|
||||
get_folder_for_owner(GetFolderForOwnerArgs {
|
||||
|
||||
+4
-2
@@ -17,7 +17,7 @@ use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
|
||||
build_folder_thumbnails_lookup, folder_row_to_info,
|
||||
};
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_NAME_LEN: usize = 255;
|
||||
@@ -45,7 +45,9 @@ pub async fn create_folder_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let name = request.name.trim().to_string();
|
||||
|
||||
|
||||
+4
-2
@@ -12,7 +12,7 @@ use mysql_queries::queries::folders::folder::soft_delete_folder::{
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Soft-delete a folder. Children retain their parent pointer and become
|
||||
@@ -39,7 +39,9 @@ pub async fn delete_folder_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
|
||||
let rows_affected = soft_delete_folder(SoftDeleteFolderArgs {
|
||||
|
||||
+4
-2
@@ -16,7 +16,7 @@ use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
|
||||
build_folder_thumbnails_lookup, folder_row_to_info,
|
||||
};
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Fetch a single folder owned by the logged-in user.
|
||||
@@ -42,7 +42,9 @@ pub async fn get_folder_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let row = get_folder_for_owner(GetFolderForOwnerArgs {
|
||||
folder_token: &path.folder_token,
|
||||
|
||||
+4
-2
@@ -17,7 +17,7 @@ use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
|
||||
build_folder_thumbnails_lookup, folder_row_to_info,
|
||||
};
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const CURSOR_NAME: &str = "folders_all";
|
||||
@@ -48,7 +48,9 @@ pub async fn list_folders_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let limit = query.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
|
||||
|
||||
|
||||
+4
-2
@@ -17,7 +17,7 @@ use mysql_queries::queries::folders::folder::update_folder_name::{
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_NAME_LEN: usize = 255;
|
||||
@@ -47,7 +47,9 @@ pub async fn rename_folder_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let new_name = request.new_name.trim().to_string();
|
||||
|
||||
|
||||
+4
-2
@@ -17,7 +17,7 @@ use mysql_queries::queries::folders::folder::update_folder_has_star::{
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -44,7 +44,9 @@ pub async fn star_folder_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
// Confirm the folder exists + is owned before issuing the update.
|
||||
// A 404 here is the authoritative "not found" signal — the update
|
||||
|
||||
+4
-2
@@ -27,7 +27,7 @@ use mysql_queries::queries::folders::media_files::recompute_folder_last_media_fi
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_BULK: usize = 500;
|
||||
@@ -61,7 +61,9 @@ pub async fn bulk_add_folder_media_files_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
if request.media_file_tokens.len() > MAX_BULK {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
|
||||
+4
-2
@@ -30,7 +30,7 @@ use mysql_queries::queries::folders::media_files::recompute_folder_last_media_fi
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_BULK: usize = 500;
|
||||
@@ -72,7 +72,9 @@ pub async fn bulk_move_folder_media_files_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
if request.media_file_tokens.len() > MAX_BULK {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
|
||||
+4
-2
@@ -24,7 +24,7 @@ use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_BULK: usize = 500;
|
||||
@@ -56,7 +56,9 @@ pub async fn bulk_remove_folder_media_files_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
if request.media_file_tokens.len() > MAX_BULK {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
|
||||
+4
-2
@@ -32,7 +32,7 @@ use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const CURSOR_NAME: &str = "folder_mf";
|
||||
@@ -127,7 +127,9 @@ pub async fn list_folder_media_files_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let folder = get_folder_for_owner(GetFolderForOwnerArgs {
|
||||
folder_token: &path.folder_token,
|
||||
|
||||
+4
-2
@@ -25,7 +25,7 @@ use mysql_queries::queries::folders::subfolder::filter_existing_owned_folder_tok
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_BULK: usize = 500;
|
||||
@@ -58,7 +58,9 @@ pub async fn bulk_add_subfolders_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
if request.subfolder_tokens.len() > MAX_BULK {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
|
||||
+4
-2
@@ -14,7 +14,7 @@ use mysql_queries::queries::folders::subfolder::bulk_clear_parent_folder::{
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_BULK: usize = 500;
|
||||
@@ -45,7 +45,9 @@ pub async fn bulk_remove_subfolders_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
if request.subfolder_tokens.len() > MAX_BULK {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
|
||||
+4
-2
@@ -21,7 +21,7 @@ use crate::http_server::endpoints::folders::folder::folder_info_conversion::{
|
||||
build_folder_thumbnails_lookup, folder_row_to_info,
|
||||
};
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const CURSOR_NAME: &str = "subfolders";
|
||||
@@ -55,7 +55,9 @@ pub async fn list_subfolders_handler(
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *conn).await?;
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
|
||||
// Confirm the parent exists + is owned by the caller before listing.
|
||||
|
||||
+2
-2
@@ -119,7 +119,7 @@ pub async fn upload_snapshot_media_file_handler(
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
@@ -247,7 +247,7 @@ pub async fn upload_snapshot_media_file_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(MediaFileClass::Image),
|
||||
media_file_type,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
|
||||
+3
-2
@@ -8,7 +8,7 @@ use utoipa::ToSchema;
|
||||
use mysql_queries::queries::generic_inference::web::dismiss_finished_jobs_for_user::dismiss_finished_jobs_for_user;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session::require_user_session;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
@@ -40,7 +40,8 @@ pub async fn dismiss_finished_session_jobs_handler(
|
||||
CommonWebError::from_error(e)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &mut *mysql_connection)
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut mysql_connection)
|
||||
.await?;
|
||||
|
||||
dismiss_finished_jobs_for_user(&mut mysql_connection, &user_session.user_token)
|
||||
|
||||
+6
-2
@@ -56,8 +56,12 @@ pub async fn change_media_file_animation_type_handler(
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<SimpleResponse>, CommonWebError> {
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &server_state.mysql_pool)
|
||||
.await?;
|
||||
let user_session = require_user_session(&http_request, &server_state)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Not authorized: {:?}", e);
|
||||
CommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let media_file_token = path.token.clone();
|
||||
let is_mod = user_session.is_mod();
|
||||
|
||||
+6
-2
@@ -53,8 +53,12 @@ pub async fn change_media_file_engine_category_handler(
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<SimpleResponse>, CommonWebError> {
|
||||
|
||||
let user_session = require_user_session(&http_request, &server_state.session_checker, &server_state.mysql_pool)
|
||||
.await?;
|
||||
let user_session = require_user_session(&http_request, &server_state)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Not authorized: {:?}", e);
|
||||
CommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let media_file_token = path.token.clone();
|
||||
let is_mod = user_session.is_mod();
|
||||
|
||||
+2
-2
@@ -135,7 +135,7 @@ pub async fn upload_audio_media_file_handler(
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
@@ -334,7 +334,7 @@ pub async fn upload_audio_media_file_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(MediaFileClass::Audio),
|
||||
media_file_type: MediaFileType::Audio,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
|
||||
let mut field_name = None;
|
||||
let mut field_filename = None;
|
||||
|
||||
if let Some(content_disposition) = field.content_disposition() {
|
||||
if let content_disposition = field.content_disposition() {
|
||||
field_name = content_disposition.get_name()
|
||||
.map(|s| s.to_string());
|
||||
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
|
||||
|
||||
+2
-3
@@ -140,8 +140,7 @@ pub async fn upload_engine_asset_media_file_handler(
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
// ==================== FILE DATA ==================== //
|
||||
|
||||
@@ -206,7 +205,7 @@ pub async fn upload_engine_asset_media_file_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(MediaFileClass::Dimensional),
|
||||
media_file_type,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> Anyhow
|
||||
let mut field_name = None;
|
||||
let mut field_filename = None;
|
||||
|
||||
if let Some(content_disposition) = field.content_disposition() {
|
||||
if let content_disposition = field.content_disposition() {
|
||||
field_name = content_disposition.get_name()
|
||||
.map(|s| s.to_string());
|
||||
field_filename = content_disposition.get_filename() // NB: Only used for the file bytes.
|
||||
|
||||
+3
-4
@@ -142,8 +142,7 @@ pub async fn process_upload_media_file(
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_file_size_bytes = upload_media_request.file_bytes
|
||||
.as_ref()
|
||||
@@ -393,7 +392,7 @@ pub async fn process_upload_media_file(
|
||||
})?;
|
||||
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(maybe_user_token)
|
||||
.maybe_creator_user(maybe_user_token.as_ref())
|
||||
.maybe_creator_anonymous_visitor(maybe_avt_token.as_ref())
|
||||
.creator_ip_address(&ip_address)
|
||||
.public_bucket_directory_hash(&public_upload_path)
|
||||
@@ -418,7 +417,7 @@ pub async fn process_upload_media_file(
|
||||
//let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
// maybe_media_class: Some(media_file_class), // DONE
|
||||
// media_file_type, // DONE
|
||||
// maybe_creator_user_token: maybe_user_token, // DONE
|
||||
// maybe_creator_user_token: maybe_user_token.as_ref(), // DONE
|
||||
// maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(), // DONE
|
||||
// creator_ip_address: &ip_address, // DONE
|
||||
// creator_set_visibility, // DONE
|
||||
|
||||
+2
-2
@@ -145,7 +145,7 @@ pub async fn upload_image_media_file_handler(
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
@@ -310,7 +310,7 @@ pub async fn upload_image_media_file_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(MediaFileClass::Image),
|
||||
media_file_type,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
|
||||
+2
-2
@@ -179,7 +179,7 @@ pub async fn upload_new_engine_asset_media_file_handler(
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
// ==================== FILE DATA ==================== //
|
||||
|
||||
@@ -211,7 +211,7 @@ pub async fn upload_new_engine_asset_media_file_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(file_info.media_class),
|
||||
media_file_type: file_info.media_type,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: file_info.creator_set_visibility,
|
||||
|
||||
+2
-2
@@ -112,7 +112,7 @@ pub async fn upload_new_scene_media_file_handler(
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
@@ -226,7 +226,7 @@ pub async fn upload_new_scene_media_file_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(MediaFileClass::Dimensional),
|
||||
media_file_type: MediaFileType::SceneJson,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
|
||||
+6
-7
@@ -23,8 +23,7 @@ use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_pmx::extract_and_upload_pmx_files::{extract_and_upload_pmx_files, PmxError};
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::require_moderator;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Form-multipart request fields.
|
||||
@@ -116,11 +115,11 @@ pub async fn upload_pmx_media_file_handler(
|
||||
// ==================== READ SESSION ==================== //
|
||||
|
||||
// NB: We require a moderator to upload PMX files.
|
||||
let user_session = require_moderator(&http_request, &server_state.session_checker, &mut *mysql_connection)
|
||||
let user_session = require_moderator(&http_request, &server_state, UseDatabase::FromPool(&mut mysql_connection))
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
CommonWebError::NotAuthorized => MediaFileUploadError::NotAuthorized,
|
||||
_ => MediaFileUploadError::ServerError,
|
||||
RequireModeratorError::ServerError => MediaFileUploadError::ServerError,
|
||||
RequireModeratorError::NotAuthorized => MediaFileUploadError::NotAuthorized,
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
@@ -207,7 +206,7 @@ pub async fn upload_pmx_media_file_handler(
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
//let maybe_user_token = maybe_user_session
|
||||
// .map(|session| session.get_user_token());
|
||||
// .map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
// ==================== FILE DATA ==================== //
|
||||
|
||||
@@ -258,7 +257,7 @@ pub async fn upload_pmx_media_file_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(MediaFileClass::Dimensional),
|
||||
media_file_type: MediaFileType::Pmx,
|
||||
maybe_creator_user_token: Some(user_session.get_user_token()),
|
||||
maybe_creator_user_token: Some(&user_session.get_strongly_typed_user_token()),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
|
||||
+2
-2
@@ -109,7 +109,7 @@ pub async fn upload_saved_scene_media_file_handler(
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
@@ -148,7 +148,7 @@ pub async fn upload_saved_scene_media_file_handler(
|
||||
|
||||
let creator_check = check_creator_tokens(CheckCreatorTokenArgs {
|
||||
maybe_creator_user_token: media_file.maybe_creator_user_token.as_ref(),
|
||||
maybe_current_request_user_token: maybe_user_token,
|
||||
maybe_current_request_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: media_file.maybe_creator_anonymous_visitor_token.as_ref(),
|
||||
maybe_current_request_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
});
|
||||
|
||||
+2
-2
@@ -111,7 +111,7 @@ pub async fn upload_scene_snapshot_media_file_handler(
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
@@ -219,7 +219,7 @@ pub async fn upload_scene_snapshot_media_file_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(MediaFileClass::Dimensional),
|
||||
media_file_type: MediaFileType::SceneJson,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility: Visibility::Hidden,
|
||||
|
||||
+2
-2
@@ -119,7 +119,7 @@ pub async fn upload_spz_media_file_handler(
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
@@ -255,7 +255,7 @@ pub async fn upload_spz_media_file_handler(
|
||||
let media_token = MediaFileInsertBuilder::new()
|
||||
.media_file_class(MediaFileClass::Dimensional)
|
||||
.media_file_type(MediaFileType::Spz)
|
||||
.maybe_creator_user(maybe_user_token)
|
||||
.maybe_creator_user(maybe_user_token.as_ref())
|
||||
.maybe_creator_anonymous_visitor(maybe_avt_token.as_ref())
|
||||
.creator_ip_address(&ip_address)
|
||||
.creator_set_visibility(creator_set_visibility)
|
||||
|
||||
+5
-8
@@ -19,13 +19,13 @@ use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
|
||||
use mysql_queries::queries::media_files::create::insert_media_file_from_studio_scene_render::{insert_media_file_from_studio_scene_render, InsertStudioSceneRenderArgs};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::extract_frames_from_zip::{extract_frames_from_zip, ExtractFramesError};
|
||||
use crate::http_server::endpoints::media_files::upload::upload_studio_shot::ffmpeg_frames_to_mp4::ffmpeg_frames_to_mp4;
|
||||
use crate::http_server::requests::request_headers::has_debug_header::has_debug_header;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_extended::require_user_session_extended;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_extended_using_connection::require_user_session_extended_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const DEFAULT_FPS : u8 = 12;
|
||||
@@ -111,14 +111,11 @@ pub async fn upload_studio_shot_media_file_handler(
|
||||
// ==================== READ SESSION ==================== //
|
||||
|
||||
// NB: We require a moderator to upload PMX files.
|
||||
let user_session = require_user_session_extended(&http_request, &server_state.session_checker, &mut *mysql_connection)
|
||||
let user_session = require_user_session_extended_using_connection(&http_request, &server_state.session_checker, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("User session error: {:?}", e);
|
||||
match e {
|
||||
CommonWebError::NotAuthorized => MediaFileUploadError::NotAuthorized,
|
||||
_ => MediaFileUploadError::ServerError,
|
||||
}
|
||||
MediaFileUploadError::NotAuthorized
|
||||
})?;
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
@@ -179,7 +176,7 @@ pub async fn upload_studio_shot_media_file_handler(
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
//let maybe_user_token = maybe_user_session
|
||||
// .map(|session| session.get_user_token());
|
||||
// .map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
// ==================== FILE DATA ==================== //
|
||||
|
||||
|
||||
+2
-2
@@ -177,7 +177,7 @@ pub async fn upload_new_video_media_file_handler(
|
||||
|
||||
let maybe_user_token = maybe_user_session
|
||||
.as_ref()
|
||||
.map(|session| session.get_user_token());
|
||||
.map(|session| session.get_strongly_typed_user_token());
|
||||
|
||||
let maybe_avt_token = server_state
|
||||
.avt_cookie_manager
|
||||
@@ -507,7 +507,7 @@ pub async fn upload_new_video_media_file_handler(
|
||||
let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs {
|
||||
maybe_media_class: Some(MediaFileClass::Video),
|
||||
media_file_type: MediaFileType::Video,
|
||||
maybe_creator_user_token: maybe_user_token,
|
||||
maybe_creator_user_token: maybe_user_token.as_ref(),
|
||||
maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(),
|
||||
creator_ip_address: &ip_address,
|
||||
creator_set_visibility,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user