Compare commits

..
62 changed files with 1001 additions and 949 deletions

No files matched your search

@@ -94,11 +94,11 @@ mod tests {
#[test]
fn table_1080p() {
// 45.85869386 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 184);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 230);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 459);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 1376);
// 61.62732357 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 247);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 309);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 617);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 1849);
}
#[test]
@@ -121,16 +121,16 @@ mod tests {
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(10)), 290);
// 15.84362140 ¢/s × 40 = 633.74 → 634¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::SevenTwentyP), 30, Some(10)), 634);
// 27.39973680 ¢/s × 40 = 1095.99 → 1096¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1096);
// 38.49728574 ¢/s × 40 = 1539.89 → 1540¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1540);
}
#[test]
fn table_1080p_with_ten_input_seconds() {
// 27.39973680 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 411);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 548);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1096);
// 38.49728574 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 578);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 770);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1540);
}
#[test]
@@ -95,11 +95,11 @@ mod tests {
#[test]
fn table_1080p() {
// 50.10486922 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 201);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 251);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 502);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 1504);
// 67.22980753 ¢/s, rounded up.
assert_eq!(cents(Some(RouterResolution::TenEightyP), 4), 269);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 5), 337);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 10), 673);
assert_eq!(cents(Some(RouterResolution::TenEightyP), 30), 2017);
}
#[test]
@@ -141,16 +141,16 @@ mod tests {
assert_eq!(cents_with_video_refs(Some(RouterResolution::FourEightyP), 30, Some(10)), 343);
// 18.72427984 ¢/s × 40 = 748.97 → 749¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::SevenTwentyP), 30, Some(10)), 749);
// 29.93674947 ¢/s × 40 = 1197.47 → 1198¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1198);
// 41.84487580 ¢/s × 40 = 1673.80 → 1674¢.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1674);
}
#[test]
fn table_1080p_with_ten_input_seconds() {
// 29.93674947 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 450);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 599);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1198);
// 41.84487580 ¢/s over (output + 10) billed seconds, rounded up.
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 5, Some(10)), 628);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 10, Some(10)), 837);
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1674);
}
#[test]
@@ -137,14 +137,15 @@ pub fn seedance_2p5_preview_usd_cents(
const SEEDANCE_2P5_CENTS_PER_SECOND_480P: f64 = 11.76954733;
/// Seedance 2.5 — 720p price, USD cents per second.
const SEEDANCE_2P5_CENTS_PER_SECOND_720P: f64 = 26.70781893;
/// Seedance 2.5 — 1080p price, USD cents per second.
const SEEDANCE_2P5_CENTS_PER_SECOND_1080P: f64 = 45.85869386;
/// Seedance 2.5 — 1080p price, USD cents per second (updated 2026-09-17).
const SEEDANCE_2P5_CENTS_PER_SECOND_1080P: f64 = 61.62732357;
/// Seedance 2.5 — 480p price with video references, USD cents per second.
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_480P: f64 = 7.24279835;
/// Seedance 2.5 — 720p price with video references, USD cents per second.
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_720P: f64 = 15.84362140;
/// Seedance 2.5 — 1080p price with video references, USD cents per second.
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 27.39973680;
/// Seedance 2.5 — 1080p price with video references, USD cents per second
/// (updated 2026-09-17).
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 38.49728574;
/// ArtCraft's price (USD cents) for Seedance 2.5.
///
@@ -199,14 +200,15 @@ pub fn seedance_2p5_usd_cents(
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_480P: f64 = 13.90946502;
/// Seedance 2.5 Ultra — 720p price, USD cents per second.
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_720P: f64 = 31.56378601;
/// Seedance 2.5 Ultra — 1080p price, USD cents per second.
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_1080P: f64 = 50.10486922;
/// Seedance 2.5 Ultra — 1080p price, USD cents per second (updated 2026-09-17).
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_1080P: f64 = 67.22980753;
/// Seedance 2.5 Ultra — 480p price with video references, USD cents per second.
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_480P: f64 = 8.55967078;
/// Seedance 2.5 Ultra — 720p price with video references, USD cents per second.
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_720P: f64 = 18.72427984;
/// Seedance 2.5 Ultra — 1080p price with video references, USD cents per second.
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 29.93674947;
/// Seedance 2.5 Ultra — 1080p price with video references, USD cents per
/// second (updated 2026-09-17).
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 41.84487580;
/// ArtCraft's price (USD cents) for Seedance 2.5 Ultra.
///
@@ -100,7 +100,7 @@ impl KinoviSeedance2p5CostState {
// per-model credit rate at our bulk credit purchase rate).
let costs = pricing_request.calculate_enterprise_costs();
// 2.5 bills whole credits at 480p/720p, so the rounding below is exact
// there; 1080p bills fractional credits (103.25/sec) and rounds to the
// there; 1080p bills fractional credits (136.23/sec) and rounds to the
// nearest credit. The USD cents (the authoritative charge) are rounded
// up.
let cost_in_credits = costs.kinovi_credits.round() as u64;
@@ -125,7 +125,7 @@ mod tests {
use super::*;
// ── Credits without video references (26/sec at 480p, 59/sec at 720p,
// 103.25/sec at 1080p enterprise) ──
// 136.23/sec at 1080p enterprise) ──
mod credits_without_video_references {
use super::*;
@@ -145,11 +145,11 @@ mod tests {
#[test]
fn credits_1080p() {
// 103.25/sec enterprise; fractional totals round to the nearest credit
// (516.25 → 516, 1032.5 → 1033, 3097.5 → 3098).
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 5, false, None), 516);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 10, false, None), 1033);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, false, None), 3098);
// 136.23/sec enterprise; fractional totals round to the nearest credit
// (681.15 → 681, 1362.3 → 1362, 4086.9 → 4087).
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 5, false, None), 681);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 10, false, None), 1362);
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, false, None), 4087);
}
#[test]
@@ -164,7 +164,7 @@ mod tests {
}
// ── Credits with video references (16/sec at 480p, 35/sec at 720p,
// 61.69/sec at 1080p enterprise, over output duration + input seconds) ──
// 81.4/sec at 1080p enterprise, over output duration + input seconds) ──
mod credits_with_video_references {
use super::*;
@@ -173,8 +173,8 @@ mod tests {
fn thirty_second_output_with_ten_input_seconds_bills_forty() {
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 30, true, Some(10)), 16 * 40);
assert_eq!(credits(Some(KinoviOutputResolution::SevenTwentyP), 30, true, Some(10)), 35 * 40);
// 61.69/sec enterprise × 40 = 2467.6 → 2468 credits.
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, true, Some(10)), 2468);
// 81.4/sec enterprise × 40 = 3256 credits.
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, true, Some(10)), 3256);
}
#[test]
@@ -147,7 +147,7 @@ pub enum KinoviSeedance2p5OutputFormat {
// |------------|----------------------|------------------------|
// | 480p | 26 | (no discount) |
// | 720p | 59 | (no discount) |
// | 1080p | 107.55 | 103.25 |
// | 1080p | 143.4 | 136.23 |
//
// With video references, the rate drops but the billed seconds are the
// output duration PLUS the total seconds of reference video input
@@ -160,11 +160,10 @@ pub enum KinoviSeedance2p5OutputFormat {
// |------------|----------------------|------------------------|
// | 480p | 16 | (no discount) |
// | 720p | 35 | (no discount) |
// | 1080p | 64.26 | 61.69 |
// | 1080p | 85.68 | 81.4 |
//
// The 1080p enterprise rates were spot-checked against the Kinovi site
// (2026-08-17): 4s / 10s / 30s without a reference video showed 413 /
// 1,032.5 / 3,097.5 credits. 480p and 720p have no separately negotiated
// The 1080p rates were last updated 2026-09-17 (previously 107.55 / 103.25
// and 64.26 / 61.69 credits/sec). 480p and 720p have no separately negotiated
// enterprise credit rate, so enterprise bills the same credits as consumer
// (the tiers still convert to USD at their own purchase rates). Default
// resolution (None) is 720p.
@@ -180,8 +179,8 @@ const SEEDANCE_2P5_720P: KinoviPricingRate = KinoviPricingRate {
maybe_enterprise_credits: None,
};
const SEEDANCE_2P5_1080P: KinoviPricingRate = KinoviPricingRate {
consumer_credits: 107.55,
maybe_enterprise_credits: Some(103.25),
consumer_credits: 143.4,
maybe_enterprise_credits: Some(136.23),
};
/// Per-second credit rates when video references are attached. These REPLACE
@@ -196,8 +195,8 @@ const SEEDANCE_2P5_720P_VIDEO_REF: KinoviPricingRate = KinoviPricingRate {
maybe_enterprise_credits: None,
};
const SEEDANCE_2P5_1080P_VIDEO_REF: KinoviPricingRate = KinoviPricingRate {
consumer_credits: 64.26,
maybe_enterprise_credits: Some(61.69),
consumer_credits: 85.68,
maybe_enterprise_credits: Some(81.4),
};
impl KinoviCostCalculatorTrait for GenerateSeedance2p5Request {
@@ -375,7 +374,7 @@ mod tests {
// ── No video references: output-duration billing ──
//
// 480p = 26 credits/sec and 720p = 59 credits/sec at both tiers (no
// negotiated enterprise discount); 1080p = 107.55 consumer / 103.25
// negotiated enterprise discount); 1080p = 143.4 consumer / 136.23
// enterprise credits/sec.
mod without_video_references {
@@ -401,24 +400,23 @@ mod tests {
#[test]
fn table_1080p() {
// Enterprise 103.25 credits/sec — the 4s / 10s / 30s values were
// verified against the Kinovi site (413 / 1,032.5 / 3,097.5).
assert_eq!(t2v_1080(4).calculate_enterprise_costs().kinovi_credits, 413.0);
assert_eq!(t2v_1080(5).calculate_enterprise_costs().kinovi_credits, 516.25);
assert_eq!(t2v_1080(10).calculate_enterprise_costs().kinovi_credits, 1032.5);
assert_eq!(t2v_1080(15).calculate_enterprise_costs().kinovi_credits, 1548.75);
assert_eq!(t2v_1080(20).calculate_enterprise_costs().kinovi_credits, 2065.0);
assert_eq!(t2v_1080(25).calculate_enterprise_costs().kinovi_credits, 2581.25);
assert_eq!(t2v_1080(30).calculate_enterprise_costs().kinovi_credits, 3097.5);
// Enterprise 136.23 credits/sec (2026-09-17).
assert_eq!(t2v_1080(4).calculate_enterprise_costs().kinovi_credits, 544.92);
assert_eq!(t2v_1080(5).calculate_enterprise_costs().kinovi_credits, 681.15);
assert_eq!(t2v_1080(10).calculate_enterprise_costs().kinovi_credits, 1362.3);
assert_eq!(t2v_1080(15).calculate_enterprise_costs().kinovi_credits, 2043.45);
assert_eq!(t2v_1080(20).calculate_enterprise_costs().kinovi_credits, 2724.6);
assert_eq!(t2v_1080(25).calculate_enterprise_costs().kinovi_credits, 3405.75);
assert_eq!(t2v_1080(30).calculate_enterprise_costs().kinovi_credits, 4086.9);
// Consumer 107.55 credits/sec.
assert_eq!(t2v_1080(4).calculate_consumer_costs().kinovi_credits, 430.2);
assert_eq!(t2v_1080(5).calculate_consumer_costs().kinovi_credits, 537.75);
assert_eq!(t2v_1080(10).calculate_consumer_costs().kinovi_credits, 1075.5);
assert_eq!(t2v_1080(15).calculate_consumer_costs().kinovi_credits, 1613.25);
assert_eq!(t2v_1080(20).calculate_consumer_costs().kinovi_credits, 2151.0);
assert_eq!(t2v_1080(25).calculate_consumer_costs().kinovi_credits, 2688.75);
assert_eq!(t2v_1080(30).calculate_consumer_costs().kinovi_credits, 3226.5);
// Consumer 143.4 credits/sec.
assert_eq!(t2v_1080(4).calculate_consumer_costs().kinovi_credits, 573.6);
assert_eq!(t2v_1080(5).calculate_consumer_costs().kinovi_credits, 717.0);
assert_eq!(t2v_1080(10).calculate_consumer_costs().kinovi_credits, 1434.0);
assert_eq!(t2v_1080(15).calculate_consumer_costs().kinovi_credits, 2151.0);
assert_eq!(t2v_1080(20).calculate_consumer_costs().kinovi_credits, 2868.0);
assert_eq!(t2v_1080(25).calculate_consumer_costs().kinovi_credits, 3585.0);
assert_eq!(t2v_1080(30).calculate_consumer_costs().kinovi_credits, 4302.0);
}
#[test]
@@ -456,7 +454,7 @@ mod tests {
let mut request_1080 = keyframe_request(10);
request_1080.output_resolution = Some(KinoviSeedance2p5OutputResolution::TenEightyP);
assert_eq!(request_1080.calculate_enterprise_costs().kinovi_credits, 1032.5);
assert_eq!(request_1080.calculate_enterprise_costs().kinovi_credits, 1362.3);
}
#[test]
@@ -489,7 +487,7 @@ mod tests {
// ── With video references: input seconds are billed too ──
//
// 480p = 16 credits/sec and 720p = 35 credits/sec at both tiers; 1080p
// = 64.26 consumer / 61.69 enterprise credits/sec. All over
// = 85.68 consumer / 81.4 enterprise credits/sec. All over
// (output duration + total_input_seconds) billed seconds.
mod with_video_references {
@@ -541,21 +539,21 @@ mod tests {
// seconds: billed seconds = 20 + clamp(input, 4, 30). E.g. a 10s
// input video + 20s generation = 30 billed seconds.
//
// Enterprise 61.69 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_enterprise_costs().kinovi_credits, 1480.56);
assert_eq!(video_ref_1080(20, Some(5)).calculate_enterprise_costs().kinovi_credits, 1542.25);
assert_eq!(video_ref_1080(20, Some(10)).calculate_enterprise_costs().kinovi_credits, 1850.7);
assert_eq!(video_ref_1080(20, Some(15)).calculate_enterprise_costs().kinovi_credits, 2159.15);
assert_eq!(video_ref_1080(20, Some(20)).calculate_enterprise_costs().kinovi_credits, 2467.6);
assert_eq!(video_ref_1080(20, Some(30)).calculate_enterprise_costs().kinovi_credits, 3084.5);
// Enterprise 81.4 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_enterprise_costs().kinovi_credits, 1953.6);
assert_eq!(video_ref_1080(20, Some(5)).calculate_enterprise_costs().kinovi_credits, 2035.0);
assert_eq!(video_ref_1080(20, Some(10)).calculate_enterprise_costs().kinovi_credits, 2442.0);
assert_eq!(video_ref_1080(20, Some(15)).calculate_enterprise_costs().kinovi_credits, 2849.0);
assert_eq!(video_ref_1080(20, Some(20)).calculate_enterprise_costs().kinovi_credits, 3256.0);
assert_eq!(video_ref_1080(20, Some(30)).calculate_enterprise_costs().kinovi_credits, 4070.0);
// Consumer 64.26 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_consumer_costs().kinovi_credits, 1542.24);
assert_eq!(video_ref_1080(20, Some(5)).calculate_consumer_costs().kinovi_credits, 1606.5);
assert_eq!(video_ref_1080(20, Some(10)).calculate_consumer_costs().kinovi_credits, 1927.8);
assert_eq!(video_ref_1080(20, Some(15)).calculate_consumer_costs().kinovi_credits, 2249.1);
assert_eq!(video_ref_1080(20, Some(20)).calculate_consumer_costs().kinovi_credits, 2570.4);
assert_eq!(video_ref_1080(20, Some(30)).calculate_consumer_costs().kinovi_credits, 3213.0);
// Consumer 85.68 credits/sec.
assert_eq!(video_ref_1080(20, Some(1)).calculate_consumer_costs().kinovi_credits, 2056.32);
assert_eq!(video_ref_1080(20, Some(5)).calculate_consumer_costs().kinovi_credits, 2142.0);
assert_eq!(video_ref_1080(20, Some(10)).calculate_consumer_costs().kinovi_credits, 2570.4);
assert_eq!(video_ref_1080(20, Some(15)).calculate_consumer_costs().kinovi_credits, 2998.8);
assert_eq!(video_ref_1080(20, Some(20)).calculate_consumer_costs().kinovi_credits, 3427.2);
assert_eq!(video_ref_1080(20, Some(30)).calculate_consumer_costs().kinovi_credits, 4284.0);
}
#[test]
@@ -574,9 +572,9 @@ mod tests {
// 30-second maximum so the estimate never undershoots the charge.
assert_eq!(video_ref_480(10, None).calculate_consumer_costs().kinovi_credits, 640.0);
assert_eq!(video_ref_720(10, None).calculate_consumer_costs().kinovi_credits, 1400.0);
// 1080p: 40 billed seconds at 61.69 enterprise / 64.26 consumer.
assert_eq!(video_ref_1080(10, None).calculate_enterprise_costs().kinovi_credits, 2467.6);
assert_eq!(video_ref_1080(10, None).calculate_consumer_costs().kinovi_credits, 2570.4);
// 1080p: 40 billed seconds at 81.4 enterprise / 85.68 consumer.
assert_eq!(video_ref_1080(10, None).calculate_enterprise_costs().kinovi_credits, 3256.0);
assert_eq!(video_ref_1080(10, None).calculate_consumer_costs().kinovi_credits, 3427.2);
}
#[test]
@@ -649,47 +647,47 @@ mod tests {
#[test]
fn enterprise_cents_1080p_4s() {
// 413 credits; 41300/243.16 = 169.8470 cents.
// 544.92 credits; 54492/243.16 = 224.0994 cents.
let cost = t2v_1080(4).calculate_enterprise_costs();
assert_eq!(cost.usd_cents_rounded_up, 170);
assert_eq!(cost.usd_cents_rounded_down, 169);
assert!((cost.usd_cents_fractional - 169.8470).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 225);
assert_eq!(cost.usd_cents_rounded_down, 224);
assert!((cost.usd_cents_fractional - 224.0994).abs() < FLOAT_TOLERANCE);
}
#[test]
fn consumer_cents_1080p_4s() {
// 430.2 credits; 43020/192.98 = 222.9247 cents.
// 573.6 credits; 57360/192.98 = 297.2329 cents.
let cost = t2v_1080(4).calculate_consumer_costs();
assert_eq!(cost.usd_cents_rounded_up, 223);
assert_eq!(cost.usd_cents_rounded_down, 222);
assert!((cost.usd_cents_fractional - 222.9247).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 298);
assert_eq!(cost.usd_cents_rounded_down, 297);
assert!((cost.usd_cents_fractional - 297.2329).abs() < FLOAT_TOLERANCE);
}
#[test]
fn enterprise_cents_1080p_30s() {
// 3097.5 credits; 309750/243.16 = 1273.8526 cents.
// 4086.9 credits; 408690/243.16 = 1680.7452 cents.
let cost = t2v_1080(30).calculate_enterprise_costs();
assert_eq!(cost.usd_cents_rounded_up, 1274);
assert_eq!(cost.usd_cents_rounded_down, 1273);
assert!((cost.usd_cents_fractional - 1273.8526).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 1681);
assert_eq!(cost.usd_cents_rounded_down, 1680);
assert!((cost.usd_cents_fractional - 1680.7452).abs() < FLOAT_TOLERANCE);
}
#[test]
fn enterprise_cents_1080p_video_ref_30_billed_seconds() {
// 61.69 × 30 = 1850.7 credits; 185070/243.16 = 761.1038 cents.
// 81.4 × 30 = 2442 credits; 244200/243.16 = 1004.2770 cents.
let cost = video_ref_1080(20, Some(10)).calculate_enterprise_costs();
assert_eq!(cost.usd_cents_rounded_up, 762);
assert_eq!(cost.usd_cents_rounded_down, 761);
assert!((cost.usd_cents_fractional - 761.1038).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 1005);
assert_eq!(cost.usd_cents_rounded_down, 1004);
assert!((cost.usd_cents_fractional - 1004.2770).abs() < FLOAT_TOLERANCE);
}
#[test]
fn consumer_cents_1080p_video_ref_30_billed_seconds() {
// 64.26 × 30 = 1927.8 credits; 192780/192.98 = 998.9636 cents.
// 85.68 × 30 = 2570.4 credits; 257040/192.98 = 1331.9515 cents.
let cost = video_ref_1080(20, Some(10)).calculate_consumer_costs();
assert_eq!(cost.usd_cents_rounded_up, 999);
assert_eq!(cost.usd_cents_rounded_down, 998);
assert!((cost.usd_cents_fractional - 998.9636).abs() < FLOAT_TOLERANCE);
assert_eq!(cost.usd_cents_rounded_up, 1332);
assert_eq!(cost.usd_cents_rounded_down, 1331);
assert!((cost.usd_cents_fractional - 1331.9515).abs() < FLOAT_TOLERANCE);
}
}
@@ -700,7 +698,7 @@ mod tests {
#[test]
fn video_reference_rate_is_cheaper_per_second() {
// 16 < 26, 35 < 59, and 61.69 < 103.25: the with-references rate is
// 16 < 26, 35 < 59, and 81.4 < 136.23: the with-references rate is
// lower per billed second (the input seconds are where the money
// goes). Compare at the minimum billed input (1s clamps to 4) —
// unknown or zero input assumes the 30-second maximum, which would
@@ -75,7 +75,10 @@ pub fn hydrate_to_router_request(
},
duration_seconds: request.duration_seconds,
video_batch_count: request.video_batch_count,
generate_audio: request.generate_audio,
// Seedance 2.0 / 2.5 always generate sound. Kinovi honours an explicit
// `generate_audio: false`, and the frontend was sending one, so the flag
// is dropped here and never reaches Kinovi for these models.
generate_audio: if is_seedance_2x(&model) { None } else { request.generate_audio },
// Set by the handler after probing reference video durations (only
// models that bill input seconds use it).
total_reference_video_input_seconds: None,
@@ -84,6 +87,24 @@ pub fn hydrate_to_router_request(
})
}
fn is_seedance_2x(model: &RouterVideoModel) -> bool {
matches!(
model,
RouterVideoModel::Seedance2p0
| RouterVideoModel::Seedance2p0Fast
| RouterVideoModel::Seedance2p0Mini
| RouterVideoModel::Seedance2p0BytePlus
| RouterVideoModel::Seedance2p0BytePlusFast
| RouterVideoModel::Seedance2p0BytePlusMini
| RouterVideoModel::Seedance2p0BytePlusUltra
| RouterVideoModel::Seedance2p0BytePlusUltraFast
| RouterVideoModel::Seedance2p0BytePlusUltraMini
| RouterVideoModel::Seedance2p5Preview
| RouterVideoModel::Seedance2p5
| RouterVideoModel::Seedance2p5Ultra
)
}
fn convert_model(
model: &CommonVideoModelEnum,
) -> Result<RouterVideoModel, CommonWebError> {
@@ -144,7 +165,7 @@ mod tests {
use serde_json::{json, Value};
#[tokio::test]
async fn artcraft_http_request_reaches_kinovi_with_format_and_audio() {
async fn artcraft_http_request_reaches_kinovi_with_format_and_without_audio_flag() {
// No media inputs or send: finalization needs a client but makes no HTTP calls.
let client = RouterClient::KinoviWeb(RouterKinoviWebClient::new(
KinoviWebSession::from_cookies_string(String::new()),
@@ -183,7 +204,7 @@ mod tests {
});
let expected_format = output_format.as_ref().and_then(Value::as_str).unwrap_or("mp4");
assert_eq!(actual_format, Some(expected_format), "model: {model}");
assert_eq!(request.maybe_generate_audio, generate_audio);
assert_eq!(request.maybe_generate_audio, None, "generate_audio: {generate_audio:?}");
}
}
}
@@ -210,6 +231,44 @@ mod tests {
}
}
mod generate_audio {
use super::*;
#[test]
fn seedance_2x_drops_generate_audio() {
for model in [
CommonVideoModelEnum::Seedance2p0,
CommonVideoModelEnum::Seedance2p0Fast,
CommonVideoModelEnum::Seedance2p0Mini,
CommonVideoModelEnum::Seedance2p5,
CommonVideoModelEnum::Seedance2p5Ultra,
] {
for generate_audio in [None, Some(true), Some(false)] {
let request = OmniGenVideoCostAndGenerateRequest {
model: Some(model),
generate_audio,
..base_request()
};
let builder = hydrate_to_router_request(&request).expect("hydrate should succeed");
assert_eq!(builder.generate_audio, None, "model: {model:?}, generate_audio: {generate_audio:?}");
}
}
}
#[test]
fn other_models_keep_generate_audio() {
for generate_audio in [None, Some(true), Some(false)] {
let request = OmniGenVideoCostAndGenerateRequest {
model: Some(CommonVideoModelEnum::Veo3p1),
generate_audio,
..base_request()
};
let builder = hydrate_to_router_request(&request).expect("hydrate should succeed");
assert_eq!(builder.generate_audio, generate_audio);
}
}
}
mod bitrate_hydration {
use super::*;
@@ -12,8 +12,8 @@ use enums::common::generation::common_video_model::CommonVideoModel;
use artcraft_api_keys::ArtcraftApiKey;
use crate::http_server::endpoints::omni_gen::generate::video::tests::support::{
base_generate_request, to_omni_api_request, ExpectedCredits, Seconds, TestHarness,
STARTING_CREDITS,
base_generate_request, stub_traffic, to_omni_api_request, ExpectedCredits, Seconds,
TestHarness, STARTING_CREDITS,
};
mod billing_parity {
@@ -46,6 +46,26 @@ mod billing_parity {
}
}
mod generate_audio {
use super::*;
/// Seedance 2.0 / 2.5 always produce sound. Even when a caller explicitly
/// sends `generate_audio: false`, neither endpoint forwards the flag to
/// Kinovi — the key must be absent from the captured request body so Kinovi
/// falls back to its default of sound on.
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn seedance_2p0_never_forwards_generate_audio_on_either_endpoint() {
assert_endpoints_omit_generate_audio(CommonVideoModel::Seedance2p0).await;
}
#[tokio::test]
#[cfg_attr(feature = "skip_database_tests", ignore)]
async fn seedance_2p5_never_forwards_generate_audio_on_either_endpoint() {
assert_endpoints_omit_generate_audio(CommonVideoModel::Seedance2p5).await;
}
}
mod url_ingestion {
use super::*;
@@ -195,3 +215,47 @@ async fn assert_endpoints_bill_identically(
"{:?}: omni_api debited the wrong amount", model,
);
}
/// Run `model` with an explicit `generate_audio: false` through omni_gen
/// (session cookie) and omni_api (API key); assert both succeed and neither
/// captured Kinovi request carries a `generate_audio` key.
async fn assert_endpoints_omit_generate_audio(model: CommonVideoModel) {
let harness = TestHarness::create().await;
// omni_gen, session-cookie user.
let web_user = harness.create_funded_user(STARTING_CREDITS).await;
let mut request = base_generate_request(model);
let web_prompt = format!("audio flag test web {}", request.idempotency_token.as_deref().unwrap());
request.prompt = Some(web_prompt.clone());
request.generate_audio = Some(false);
let response = harness
.post_generate(&web_user, request)
.await
.unwrap_or_else(|err| panic!("{:?}: omni_gen generation failed: {:?}", model, err));
assert!(response.success);
// omni_api, API-key user.
let api_user = harness.create_funded_user(STARTING_CREDITS).await;
let api_key = harness.create_api_key(&api_user).await;
let mut request = base_generate_request(model);
let api_prompt = format!("audio flag test api {}", request.idempotency_token.as_deref().unwrap());
request.prompt = Some(api_prompt.clone());
request.generate_audio = Some(false);
let response = harness
.post_generate_via_api_key(&api_key, request)
.await
.unwrap_or_else(|err| panic!("{:?}: omni_api generation failed: {:?}", model, err));
assert!(response.success);
let traffic = stub_traffic().lock().unwrap();
for (endpoint, prompt) in [("omni_gen", &web_prompt), ("omni_api", &api_prompt)] {
let params = traffic.workflows.iter()
.map(|request| &request["0"]["json"]["apiParams"])
.find(|params| params["prompt"].as_str() == Some(prompt))
.unwrap_or_else(|| panic!("{:?}: no captured Kinovi request for {}", model, endpoint));
assert!(
params.get("generate_audio").is_none(),
"{:?}: {} forwarded generate_audio to Kinovi: {}", model, endpoint, params,
);
}
}
@@ -74,7 +74,8 @@ async fn quicktime_uploads_are_video_references_with_mp4_or_mov_output() {
.find(|params| params["prompt"].as_str() == Some(&prompt))
.expect("captured Kinovi request");
assert_eq!(params["output_format"], expected);
assert_eq!(params["generate_audio"], true);
// Seedance 2.x never forwards the audio flag; Kinovi defaults to sound on.
assert!(params.get("generate_audio").is_none(), "{}", params);
assert_eq!(params["mode"], "reference");
assert!(params["videoUrls"][0].as_str().is_some_and(|url| url.ends_with(".mov")), "{}", params);
assert_eq!(traffic.uploads.get("fixture.mov").unwrap(), &bytes);
@@ -4,12 +4,12 @@
//!
//! Rate cards (ceil-rounded once):
//! - Seedance 2.5: 480p 11.76954733 ¢/s, 720p 26.70781893 ¢/s,
//! 1080p 45.85869386 ¢/s; with video refs 480p 7.24279835 ¢/s,
//! 720p 15.84362140 ¢/s, 1080p 27.39973680 ¢/s over (output + input)
//! 1080p 61.62732357 ¢/s; with video refs 480p 7.24279835 ¢/s,
//! 720p 15.84362140 ¢/s, 1080p 38.49728574 ¢/s over (output + input)
//! seconds
//! - Seedance 2.5 Ultra: 480p 13.90946502 ¢/s, 720p 31.56378601 ¢/s,
//! 1080p 50.10486922 ¢/s; with video refs 480p 8.55967078 ¢/s,
//! 720p 18.72427984 ¢/s, 1080p 29.93674947 ¢/s
//! 1080p 67.22980753 ¢/s; with video refs 480p 8.55967078 ¢/s,
//! 720p 18.72427984 ¢/s, 1080p 41.84487580 ¢/s
//!
//! Both models offer 480p/720p/1080p (4K downgrades to 1080p), durations
//! run 4-30s, and every request generates a single video regardless of the
@@ -90,60 +90,60 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(748)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(775)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(184)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(276)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(322)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(367)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(413)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(459)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(505)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(551)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(597)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(643)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(688)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(734)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(780)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(826)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(872)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(918)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(964)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1055)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1101)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1147)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1193)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1239)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1285)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1330)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1376)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(184)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(276)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(322)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(367)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(413)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(459)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(505)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(551)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(597)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(643)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(688)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(734)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(780)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(826)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(872)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(918)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(964)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1055)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1101)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1147)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1193)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1239)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1285)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1330)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1376)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(247)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(370)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(432)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(494)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(555)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(617)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(678)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(863)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(925)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1110)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1171)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1295)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1356)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1418)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1541)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1603)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1664)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1726)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1788)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1849)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(247)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(370)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(432)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(494)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(555)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(617)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(678)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(863)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(925)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1110)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1171)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1295)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1356)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1418)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1541)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1603)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1664)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1726)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1788)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1849)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -182,26 +182,26 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(134)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(134)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(230)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(230)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(309)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -230,18 +230,18 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(294)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(348)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(401)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(276)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(322)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(413)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(505)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(597)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(688)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(276)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(322)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(413)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(505)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(597)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(688)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(370)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(432)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(555)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(678)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(925)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(370)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(432)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(555)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(678)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(802)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(925)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -313,60 +313,60 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(919)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(935)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(951)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(932)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1014)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1042)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1069)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1096)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1124)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1151)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1179)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1206)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1261)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1316)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1343)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1370)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1398)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1453)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1535)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1562)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1590)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1644)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(932)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(987)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1014)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1042)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1069)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1096)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1124)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1151)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1179)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1206)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1261)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1316)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1343)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1370)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1398)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1453)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1535)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1562)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1590)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1644)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(1309)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(1386)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1463)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1502)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1540)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1579)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1656)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1694)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1733)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1771)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1810)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1848)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1887)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1964)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(2002)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(2041)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(2079)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(2118)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(2156)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(2195)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(2233)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(2272)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(2310)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(1309)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(1386)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1463)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1502)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1540)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1579)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1656)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1694)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1733)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1771)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1810)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1848)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1887)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1964)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(2002)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(2041)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(2079)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(2118)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(2156)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(2195)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(2233)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(2272)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(2310)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -405,26 +405,26 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(555)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(555)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(959)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(959)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1348)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -454,18 +454,18 @@ mod seedance_2p5 {
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(650)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(682)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(713)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(987)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1014)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1069)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1124)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1179)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1233)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(987)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1014)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1069)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1124)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1179)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1233)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(1386)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1425)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1502)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1579)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1656)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1733)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1386)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1425)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1502)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1579)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1656)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1733)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -546,60 +546,60 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(884)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(916)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(947)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(201)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(301)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(351)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(401)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(451)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(502)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(552)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(602)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(652)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(702)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(752)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(852)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(902)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(952)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1003)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1053)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1103)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1153)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1203)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1253)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1303)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1353)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1403)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1454)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1504)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(201)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(301)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(351)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(401)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(451)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(502)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(552)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(602)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(652)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(702)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(752)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(802)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(852)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(902)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(952)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1003)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1053)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1103)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1153)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1203)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1253)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1303)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1353)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1403)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1454)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1504)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(269)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(404)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(471)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(538)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(606)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(673)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(807)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(874)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(942)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1076)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1143)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1211)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1278)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1345)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1412)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1547)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1614)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1681)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1748)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1816)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1883)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1950)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(2017)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(269)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(404)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(471)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(538)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(606)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(673)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(807)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(874)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(942)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1076)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1143)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1211)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1278)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1345)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1412)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1480)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1547)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1614)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1681)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1748)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1816)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1883)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1950)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(2017)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -638,26 +638,26 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(158)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(158)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(251)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(251)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(337)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(337)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(337)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -686,18 +686,18 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(348)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(411)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(474)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(301)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(351)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(451)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(552)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(652)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(752)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(301)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(351)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(451)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(552)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(652)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(752)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(404)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(471)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(606)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(740)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(874)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1009)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(404)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(471)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(606)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(740)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(874)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1009)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -769,60 +769,60 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(1087)),
(Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(1105)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(1124)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(1018)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(1078)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1108)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1138)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1168)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1198)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1228)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1258)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1318)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1378)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1408)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(1437)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(1467)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(1497)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(1527)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(1557)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(1587)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1647)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1677)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1707)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1737)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1767)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1797)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(1018)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(1078)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1108)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1138)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1168)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1198)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1228)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1258)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1288)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1318)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1378)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1408)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(1437)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(1467)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(1497)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(1527)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(1557)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(1587)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1617)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1647)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1677)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1707)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1737)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1767)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1797)),
(Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(1423)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(1549)),
(Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(1591)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(1632)),
(Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(1674)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(1716)),
(Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(1758)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(1800)),
(Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(1842)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(1884)),
(Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(1967)),
(Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(2009)),
(Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(2051)),
(Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(2093)),
(Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(2135)),
(Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(2176)),
(Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(2218)),
(Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(2260)),
(Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(2302)),
(Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(2344)),
(Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(2386)),
(Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(2428)),
(Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(2469)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(2511)),
(Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(1423)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(1507)),
(Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(1549)),
(Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(1591)),
(Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(1632)),
(Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(1674)),
(Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(1716)),
(Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(1758)),
(Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(1800)),
(Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(1842)),
(Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(1884)),
(Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(1925)),
(Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(1967)),
(Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(2009)),
(Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(2051)),
(Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(2093)),
(Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(2135)),
(Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(2176)),
(Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(2218)),
(Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(2260)),
(Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(2302)),
(Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(2344)),
(Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(2386)),
(Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(2428)),
(Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(2469)),
(Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(2511)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -861,26 +861,26 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(656)),
(Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(656)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1048)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1048)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(1465)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(1465)),
(Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(1465)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -910,18 +910,18 @@ mod seedance_2p5_u {
(Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(768)),
(Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(806)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(843)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(1078)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1108)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1168)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1228)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1288)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1348)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1078)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1108)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1168)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1228)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1288)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1348)),
(Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(1507)),
(Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(1549)),
(Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(1632)),
(Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(1716)),
(Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(1800)),
(Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(1884)),
(Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(1507)),
(Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(1549)),
(Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(1632)),
(Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(1716)),
(Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(1800)),
(Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(1884)),
];
for (resolution, seconds, batch, expected) in cases {
@@ -1059,7 +1059,7 @@ mod input_video_duration_billing {
// 6.5s → 7 → 37 billed × 7.24279835 = 267.98 → 268
// 10.5s → 11 → 41 billed × 7.24279835 = 296.95 → 297
// 6.5s → 7 → 37 billed × 15.84362140 = 586.21 → 587 (720p)
// 6.5s → 7 → 37 billed × 27.39973680 = 1013.79 → 1014 (1080p)
// 6.5s → 7 → 37 billed × 38.49728574 = 1424.40 → 1425 (1080p)
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5, Some(CommonResolution::FourEightyP),
Seconds(30), &[4_500], ExpectedCredits(254),
@@ -1078,7 +1078,7 @@ mod input_video_duration_billing {
).await;
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5, Some(CommonResolution::TenEightyP),
Seconds(30), &[6_500], ExpectedCredits(1014),
Seconds(30), &[6_500], ExpectedCredits(1425),
).await;
}
@@ -1122,12 +1122,12 @@ mod input_video_duration_billing {
let harness = TestHarness::create().await;
// 30s output, ultra with-refs 480p 8.55967078 ¢/s / 720p 18.72427984 ¢/s
// / 1080p 29.93674947 ¢/s:
// / 1080p 41.84487580 ¢/s:
// 2.5s → total 3 → clamps to 4 → 34 billed → 291.03 → 292
// 10.5s → 11 → 41 billed → 350.95 → 351
// 2.5s + 6.5s → 3+7=10 → 40 billed → 342.39 → 343 (total-based, no per-video clamp)
// 6.5s → 7 → 37 billed × 18.72427984 = 692.80 → 693 (720p)
// 6.5s → 7 → 37 billed × 29.93674947 = 1107.66 → 1108 (1080p)
// 6.5s → 7 → 37 billed × 41.84487580 = 1548.26 → 1549 (1080p)
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5Ultra, Some(CommonResolution::FourEightyP),
Seconds(30), &[2_500], ExpectedCredits(292),
@@ -1146,7 +1146,7 @@ mod input_video_duration_billing {
).await;
assert_real_input_videos_charge(
&harness, CommonVideoModel::Seedance2p5Ultra, Some(CommonResolution::TenEightyP),
Seconds(30), &[6_500], ExpectedCredits(1108),
Seconds(30), &[6_500], ExpectedCredits(1549),
).await;
}
}
@@ -1172,10 +1172,10 @@ mod premium {
(Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(268), ExpectedCredits(316), CreditsDelta(48)),
(Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(401), ExpectedCredits(474), CreditsDelta(73)),
(Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(802), ExpectedCredits(947), CreditsDelta(145)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(230), ExpectedCredits(251), CreditsDelta(21)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1376), ExpectedCredits(1504), CreditsDelta(128)),
(Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(309), ExpectedCredits(337), CreditsDelta(28)),
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1849), ExpectedCredits(2017), CreditsDelta(168)),
// 4K downgrades to 1080p on both models, so the premium matches 1080p.
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230), ExpectedCredits(251), CreditsDelta(21)),
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309), ExpectedCredits(337), CreditsDelta(28)),
];
for (resolution, seconds, batch, base, variant, delta) in cases {
@@ -0,0 +1,9 @@
export const AUTH_LABEL_CLASSES =
"block font-mono text-[11px] font-semibold leading-4 uppercase tracking-[0.12em] text-white/70";
export const AUTH_INPUT_CLASSES =
"h-10 w-full rounded-[3px] border border-white/15 bg-white/5 px-4 py-2 text-sm font-normal leading-5 text-white placeholder-white/20 outline-none transition-colors focus:border-white/40";
export const AUTH_PASSWORD_INPUT_CLASSES = `${AUTH_INPUT_CLASSES} pr-12`;
export const AUTH_FORM_PADDING = "px-6 py-10 sm:px-10 sm:py-12";
@@ -1,11 +1,11 @@
import { AUTH_FORM_PADDING } from "./auth-form-styles";
import { ReactNode } from "react";
import { Outlet, useNavigate, Navigate } from "react-router-dom";
import { ArrowLeftIcon, LoaderCircleIcon } from "lucide-react";
import { TruchetPattern } from "@storyteller/ui-vfx";
import { Outlet, Navigate, Link } from "react-router-dom";
import { LoaderCircleIcon } from "lucide-react";
import { AuthShowcase } from "./auth-showcase";
import { AuthPageFrame } from "./auth-page-frame";
import { useMediaQuery } from "../ui/use-media-query";
import { useSession } from "../../lib/session";
import { Reveal, RevealGroup } from "../motion/reveal";
/**
* Persistent shell for the auth pages. Rendered as a layout route so the
@@ -18,52 +18,21 @@ export const AuthLayout = () => {
// Only mount the showcase on wide screens (matches the `lg` breakpoint) so
// mobile never downloads the demo videos.
const showShowcase = useMediaQuery("(min-width: 1024px)");
const navigate = useNavigate();
const { loggedIn, authChecked } = useSession();
const handleBack = () => {
navigate("/");
};
// Already signed in? Never show the auth form — go straight home.
if (loggedIn) {
return <Navigate to="/" replace />;
}
return (
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-ui-background p-4 text-white">
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0"
style={{
maskImage:
"radial-gradient(ellipse 70% 60% at 50% 40%, black 25%, transparent 80%)",
WebkitMaskImage:
"radial-gradient(ellipse 70% 60% at 50% 40%, black 25%, transparent 80%)",
}}
>
<TruchetPattern
intensity={0.5}
className="absolute inset-0 h-full w-full"
/>
</div>
<div className="relative z-10 flex w-full max-w-5xl overflow-hidden border border-white/15 bg-[#101014] lg:min-h-[640px]">
<AuthPageFrame wide>
{/* Form pane — only the inner content swaps per route (via Outlet), so
this pane and the showcase beside it stay mounted across the
login/signup toggle. */}
<div className="relative flex w-full flex-col lg:w-1/2">
<button
type="button"
onClick={handleBack}
aria-label="Go back"
className="absolute left-5 top-5 z-20 flex h-9 w-9 items-center justify-center text-white/60 transition-colors hover:bg-white/10 hover:text-white"
>
<ArrowLeftIcon />
</button>
<div className="flex flex-1 flex-col justify-center px-6 pb-10 pt-16 sm:px-10">
<div className="mx-auto w-full max-w-sm">
<div className={`flex flex-1 flex-col justify-center ${AUTH_FORM_PADDING}`}>
<div className="w-full">
{authChecked ? (
<Outlet />
) : (
@@ -76,44 +45,39 @@ export const AuthLayout = () => {
</div>
</div>
<div className="px-8 pb-8 text-center text-xs text-white/60">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
{/* Showcase pane (desktop only) */}
{showShowcase && (
<div className="relative lg:w-1/2">
<div className="relative border-l border-white/15 lg:w-1/2">
<AuthShowcase />
</div>
)}
</div>
</div>
</AuthPageFrame>
);
};
interface AuthHeaderProps {
title: string;
title: ReactNode;
subtitle: string;
}
export const AuthHeader = ({ title, subtitle }: AuthHeaderProps) => (
<RevealGroup inView={false} stagger={0.08} className="mb-8 text-center">
<Reveal y={10}>
<img
src="/artcraft-icon.svg"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
</Reveal>
<Reveal as="h1" y={10} className="mb-2 text-2xl font-semibold">
<div className="mb-8 text-left">
<Link
to="/"
aria-label="ArtCraft home"
className="mb-8 inline-flex rounded-sm transition-opacity hover:opacity-80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white/60"
>
<img src="/artcraft-icon.svg" alt="ArtCraft" className="h-8 w-8" />
</Link>
<h1 className="mb-3 text-balance font-display text-3xl leading-[1.05] tracking-tight sm:text-4xl">
{title}
</Reveal>
<Reveal as="p" y={10} className="text-sm text-white/60">
</h1>
<p className="text-sm leading-relaxed text-white/60">
{subtitle}
</Reveal>
</RevealGroup>
</p>
</div>
);
export const AuthFooter = ({ children }: { children: ReactNode }) => (
@@ -0,0 +1,22 @@
import { type ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import { FrameCornerMarks } from "../ui/frame-corner-marks";
/** A contained section frame shared by the auth pages. */
export function AuthPageFrame({ children, wide = false }: {
children: ReactNode;
wide?: boolean;
}) {
return (
<div className="relative isolate flex min-h-dvh items-center justify-center overflow-hidden bg-ui-background p-4 text-white sm:py-10">
<div className={twMerge("relative w-full", wide ? "max-w-lg lg:max-w-5xl" : "max-w-lg")}>
<div className="relative border border-white/20 bg-ui-controls">
<div className={twMerge("flex", wide ? "lg:min-h-[560px]" : "flex-col")}>
{children}
</div>
</div>
<FrameCornerMarks />
</div>
</div>
);
}
@@ -3,7 +3,7 @@ const FEATURED_VIDEO_URL =
export const AuthShowcase = () => {
return (
<div className="absolute inset-2 overflow-hidden bg-black">
<div className="absolute inset-0 overflow-hidden bg-ui-controls">
{/* Muted background video (no controls) cropped to cover the pane */}
<video
src={FEATURED_VIDEO_URL}
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_PASSWORD_INPUT_CLASSES } from "./auth-form-styles";
import { EyeIcon, EyeOffIcon, LoaderCircleIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
@@ -14,7 +15,6 @@ import {
import { refreshSession } from "../../lib/session";
import { hasActiveSubscription } from "../../lib/billing";
import { GoogleLoginButton } from "./GoogleLoginButton";
import { Reveal, RevealGroup } from "../motion/reveal";
interface SignupFormProps {
onSuccess: (isNewUser?: boolean) => void;
@@ -98,7 +98,7 @@ export const SignupForm = ({
};
return (
<div className={`space-y-4 ${className}`}>
<div className={className}>
<form
className="space-y-4"
onSubmit={(e) => {
@@ -111,19 +111,11 @@ export const SignupForm = ({
{error}
</div>
)}
{/* Fields cascade in as their own beat, leading with the email field on
an almost-zero delay so the primary input lands immediately — the
entrance never gates typing (inputs stay focusable mid-cascade).
Mirrors the login page. */}
<RevealGroup
inView={false}
delayChildren={0.04}
stagger={0.08}
<div
className="space-y-4"
>
<Reveal className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
<div className="space-y-2">
<label className={AUTH_LABEL_CLASSES}>
Email
</label>
<Input
@@ -132,13 +124,13 @@ export const SignupForm = ({
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
autoFocus={autoFocus}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 rounded-[3px] px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
</Reveal>
</div>
<Reveal className="space-y-2">
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
<label className={AUTH_LABEL_CLASSES}>
Password
</label>
</div>
@@ -148,7 +140,7 @@ export const SignupForm = ({
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 rounded-[3px] px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -159,9 +151,9 @@ export const SignupForm = ({
<DynamicIcon icon={showPassword ? EyeOffIcon : EyeIcon} />
</button>
</div>
</Reveal>
</div>
<Reveal className="pt-2">
<div className="pt-2">
<Button
className="w-full justify-center h-10"
type="submit"
@@ -173,36 +165,25 @@ export const SignupForm = ({
"Create account"
)}
</Button>
</Reveal>
</RevealGroup>
</div>
</div>
</form>
{/* Secondary sign-up options continue the same cascade just after the
fields (delay continues from the form group's three children above:
0.04 + 3 × 0.08). */}
<RevealGroup
inView={false}
delayChildren={0.28}
stagger={0.08}
className="space-y-4"
>
<Reveal className="relative flex items-center justify-center py-2">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/10" />
</div>
<span className="relative bg-[#101014] px-4 font-mono text-[11px] uppercase tracking-[0.12em] text-white/40">
<div>
<div className="my-6 flex items-center gap-4 before:h-px before:flex-1 before:bg-white/15 after:h-px after:flex-1 after:bg-white/15">
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
or
</span>
</Reveal>
</div>
<Reveal>
<div>
<GoogleLoginButton
mode="signup"
onSuccess={handleGoogleSuccess}
onError={handleGoogleError}
/>
</Reveal>
</RevealGroup>
</div>
</div>
</div>
);
};
@@ -0,0 +1,25 @@
import { type ReactNode } from "react";
import { FrameCornerMarks } from "../ui/frame-corner-marks";
interface CreateEmptyStateProps {
title: string;
subtitle: string;
children?: ReactNode;
compact?: boolean;
}
/** A quiet editorial section with a fine border and corner registration marks. */
export function CreateEmptyState({ title, subtitle, children, compact = false }: CreateEmptyStateProps) {
return (
<section className={`create-empty-section relative w-full max-w-3xl px-6 text-left sm:px-10 ${compact ? "py-8" : "py-10 sm:py-14"}`}>
<h1 className={`text-balance font-display leading-[1.05] tracking-[-0.035em] text-ui-ink ${compact ? "text-2xl" : "text-3xl md:text-5xl"}`}>
{title}
</h1>
<p className="mt-4 max-w-xl text-pretty text-sm leading-relaxed text-white/60 sm:text-base">
{subtitle}
</p>
{children && <div className="mt-6">{children}</div>}
<FrameCornerMarks />
</section>
);
}
@@ -1,3 +1,4 @@
import { CreateEmptyState } from "./CreateEmptyState";
import { type ReactNode, useState } from "react";
import { LoaderCircleIcon } from "lucide-react";
import { type PopoverItem } from "@storyteller/ui-popover";
@@ -127,14 +128,8 @@ export function CreateMediaPageShell({
<div className="px-3">{gridContent}</div>
</div>
) : (
<div className="relative isolate flex h-full flex-col items-center justify-center px-6 text-center">
<div aria-hidden="true" className="create-empty-backdrop" />
<span className="relative z-[1] font-display text-lg text-ui-ink">
{emptyStateTitle}
</span>
<span className="relative z-[1] pt-1 text-sm text-white/60">
Your generations will appear here.
</span>
<div className="flex h-full items-center justify-center overflow-y-auto px-6 py-8">
<CreateEmptyState compact title={emptyStateTitle} subtitle="Your generations will appear here." />
</div>
)}
</div>
@@ -149,25 +144,13 @@ export function CreateMediaPageShell({
<div className="relative isolate flex h-full w-full bg-ui-background text-white">
<Seo title={title} description={description} />
{/* Static landing-style rails stay inside the workspace and never
intercept input or cover the gallery once results arrive. */}
{!hasContent && (
<div aria-hidden="true" className="create-empty-backdrop" />
)}
<div className="relative z-[1] h-full w-full">
<div className="flex h-full w-full flex-col">
{!hasContent && (
<div className="flex flex-1 items-center justify-center">
<div className="animate-fade-in-up relative z-20 mb-32 flex max-w-3xl flex-col items-center justify-center px-6 text-center">
<h1 className="font-display text-3xl leading-tight text-ui-ink md:text-5xl">
{emptyStateTitle}
</h1>
<span className="max-w-xl pt-3 text-sm leading-relaxed text-base-fg/70 md:text-base">
{emptyStateSubtitle}
</span>
{emptyStateCta && <div className="pt-6">{emptyStateCta}</div>}
</div>
<div className="flex flex-1 items-center justify-center px-6" style={{ paddingBottom: bottomOffset }}>
<CreateEmptyState title={emptyStateTitle} subtitle={emptyStateSubtitle}>
{emptyStateCta}
</CreateEmptyState>
</div>
)}
@@ -1,5 +1,6 @@
import { type ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import { NEUTRAL_BUTTON_HOVER_CLASSES } from "@storyteller/ui-button";
import { ChevronRightIcon } from "lucide-react";
interface MobileFieldButtonProps {
@@ -27,7 +28,8 @@ export function MobileFieldButton({
disabled={disabled}
className={twMerge(
"flex w-full items-center gap-3 border border-ui-panel-border bg-ui-controls px-3.5 py-3 text-left transition-colors",
"hover:bg-ui-controls/80 active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-50",
NEUTRAL_BUTTON_HOVER_CLASSES,
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-ui-controls disabled:hover:border-ui-panel-border",
className,
)}
>
@@ -0,0 +1,8 @@
const CORNERS = ["top-0 left-0", "top-0 right-0", "bottom-0 left-0", "bottom-0 right-0"];
/** Decorative registration marks for a positioned panel. */
export function FrameCornerMarks() {
return CORNERS.map((corner) => (
<span key={corner} aria-hidden="true" className={`frame-corner-mark ${corner}`} />
));
}
@@ -1,5 +1,6 @@
import { CreateEmptyState } from "../../components/generation-gallery/CreateEmptyState";
import { useCallback, useEffect, useRef, useState } from "react";
import { LoaderCircleIcon, SparklesIcon } from "lucide-react";
import { LoaderCircleIcon } from "lucide-react";
import {
PromptBoxVFX,
VFXResultCard,
@@ -267,7 +268,7 @@ export default function CreateVFX() {
className="relative z-10 flex h-full items-center justify-center px-3 sm:px-6"
style={{ paddingBottom: Math.max(promptBoxHeight + 32, 240) }}
>
<EmptyState
<CreateEmptyState
title="No background changes yet"
subtitle="Upload a source video and a reference image, then optionally add a prompt."
/>
@@ -330,18 +331,3 @@ export default function CreateVFX() {
</div>
);
}
interface EmptyStateProps {
title: string;
subtitle: string;
}
const EmptyState = ({ title, subtitle }: EmptyStateProps) => (
<div className="flex max-w-md flex-col items-center gap-4 text-center">
<div className="flex h-12 w-12 items-center justify-center border border-white/15 bg-white/5">
<SparklesIcon className="text-2xl text-white/40" />
</div>
<h3 className="text-2xl font-bold text-white">{title}</h3>
<p className="text-sm text-white/60 max-w-xs">{subtitle}</p>
</div>
);
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_FORM_PADDING } from "../../components/auth/auth-form-styles";
import { ArrowLeftIcon, LoaderCircleIcon, MailIcon } from "lucide-react";
import { Button } from "@storyteller/ui-button";
import { Input } from "@storyteller/ui-input";
@@ -5,6 +6,8 @@ import { useState } from "react";
import { Link } from "react-router-dom";
import { PasswordResetApi } from "@storyteller/api";
import { AuthHeader } from "../../components/auth/auth-layout";
import { AuthPageFrame } from "../../components/auth/auth-page-frame";
import Seo from "../../components/seo";
const ForgotPassword = () => {
@@ -41,28 +44,17 @@ const ForgotPassword = () => {
};
return (
<div className="relative min-h-screen bg-ui-background text-white overflow-hidden flex flex-col">
<AuthPageFrame>
<Seo
title="Reset Password - ArtCraft"
description="Reset your ArtCraft password."
/>
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#101014] border border-white/15 p-6 py-8">
<main className={AUTH_FORM_PADDING}>
<div className="w-full">
{!submitted ? (
<>
<div className="text-center mb-8">
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
<h1 className="text-2xl font-semibold mb-2">Reset Password</h1>
<p className="text-white/60 text-sm">
Enter your email to receive reset instructions
</p>
</div>
<AuthHeader title={<>Reset your <span className="font-serif-italic">password.</span></>} subtitle="Enter your email or username to receive a reset code." />
<form
className="space-y-4"
@@ -78,7 +70,7 @@ const ForgotPassword = () => {
)}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Email or Username
</label>
<Input
@@ -87,7 +79,7 @@ const ForgotPassword = () => {
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
</div>
@@ -134,18 +126,14 @@ const ForgotPassword = () => {
<div className="mt-8 text-center text-sm">
<Link
to="/login"
className="text-white/40 hover:text-white transition-colors flex items-center justify-center gap-2"
className="text-white/60 hover:text-white transition-colors flex items-center justify-center gap-2"
>
<ArrowLeftIcon /> Back to Log in
</Link>
</div>
</div>
</main>
<div className="relative z-10 py-6 text-center text-white/20 text-xs">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
</AuthPageFrame>
);
};
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_PASSWORD_INPUT_CLASSES, AUTH_FORM_PADDING } from "../../components/auth/auth-form-styles";
import {
ArrowLeftIcon,
CircleCheckIcon,
@@ -12,6 +13,8 @@ import { useState } from "react";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { PasswordResetApi, BillingApi } from "@storyteller/api";
import { AuthHeader } from "../../components/auth/auth-layout";
import { AuthPageFrame } from "../../components/auth/auth-page-frame";
import Seo from "../../components/seo";
const VerifyReset = () => {
@@ -102,30 +105,17 @@ const VerifyReset = () => {
};
return (
<div className="relative min-h-screen bg-ui-background text-white overflow-hidden flex flex-col">
<AuthPageFrame>
<Seo
title="Verify Password Reset - ArtCraft"
description="Enter your verification code and new password."
/>
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#101014] border border-white/15 p-6 py-8">
<main className={AUTH_FORM_PADDING}>
<div className="w-full">
{!success ? (
<>
<div className="text-center mb-8">
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
<h1 className="text-2xl font-semibold mb-2">
Password Reset Verification
</h1>
<p className="text-white/60 text-sm">
Enter the code sent to your email address.
</p>
</div>
<AuthHeader title={<>Set a new <span className="font-serif-italic">password.</span></>} subtitle="Enter your reset code and choose a new password." />
<form
className="space-y-4"
@@ -142,7 +132,7 @@ const VerifyReset = () => {
{/* Verification Code */}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Verification Code
</label>
<Input
@@ -152,10 +142,10 @@ const VerifyReset = () => {
onChange={(e) => setVerificationCode(e.target.value)}
placeholder="Enter verification code"
isError={!!fieldErrors.verificationCode}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
{fieldErrors.verificationCode && (
<p className="text-red-400 text-xs ml-1">
<p className="text-red-400 text-xs">
{fieldErrors.verificationCode}
</p>
)}
@@ -163,7 +153,7 @@ const VerifyReset = () => {
{/* New Password */}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
New Password
</label>
<div className="relative">
@@ -174,7 +164,7 @@ const VerifyReset = () => {
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password"
isError={!!fieldErrors.newPassword}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -187,7 +177,7 @@ const VerifyReset = () => {
</button>
</div>
{fieldErrors.newPassword && (
<p className="text-red-400 text-xs ml-1">
<p className="text-red-400 text-xs">
{fieldErrors.newPassword}
</p>
)}
@@ -195,7 +185,7 @@ const VerifyReset = () => {
{/* Verify New Password */}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Verify New Password
</label>
<div className="relative">
@@ -206,7 +196,7 @@ const VerifyReset = () => {
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Enter new password again"
isError={!!fieldErrors.confirmPassword}
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -221,7 +211,7 @@ const VerifyReset = () => {
</button>
</div>
{fieldErrors.confirmPassword && (
<p className="text-red-400 text-xs ml-1">
<p className="text-red-400 text-xs">
{fieldErrors.confirmPassword}
</p>
)}
@@ -246,7 +236,7 @@ const VerifyReset = () => {
<div className="mt-8 text-center text-sm">
<Link
to="/forgot-password"
className="text-white/40 hover:text-white transition-colors flex items-center justify-center gap-2"
className="text-white/60 hover:text-white transition-colors flex items-center justify-center gap-2"
>
<ArrowLeftIcon /> Request a new code
</Link>
@@ -277,11 +267,7 @@ const VerifyReset = () => {
)}
</div>
</main>
<div className="relative z-10 py-6 text-center text-white/20 text-xs">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
</AuthPageFrame>
);
};
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_INPUT_CLASSES, AUTH_PASSWORD_INPUT_CLASSES } from "../../components/auth/auth-form-styles";
import { EyeIcon, EyeOffIcon, LoaderCircleIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
@@ -11,7 +12,6 @@ import {
GoogleLoginButton,
} from "../../components/auth";
import Seo from "../../components/seo";
import { Reveal, RevealGroup } from "../../components/motion/reveal";
import { refreshSession } from "../../lib/session";
import { hasActiveSubscription } from "../../lib/billing";
@@ -70,7 +70,7 @@ const Login = () => {
title="Login - ArtCraft"
description="Login to your ArtCraft account."
/>
<AuthHeader title="Welcome Back" subtitle="Log in to your account" />
<AuthHeader title={<>Welcome <span className="font-serif-italic">back.</span></>} subtitle="Log in to your creative workspace." />
<form
className="space-y-4"
@@ -84,20 +84,11 @@ const Login = () => {
{error}
</div>
)}
{/* The form fields cascade in as their own beat, distinct from the
shell/header settling. We lead with the email field on an almost-zero
delay so the primary input lands immediately — the entrance never
gates typing: inputs stay focusable and accept keystrokes while the
password/button below are still cascading in. */}
<RevealGroup
inView={false}
delayChildren={0.04}
stagger={0.08}
<div
className="space-y-4"
>
<Reveal className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70 ml-1">
<div className="space-y-2">
<label className={AUTH_LABEL_CLASSES}>
Email or Username
</label>
<Input
@@ -105,12 +96,12 @@ const Login = () => {
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com or username"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
inputClassName={AUTH_INPUT_CLASSES}
/>
</Reveal>
<Reveal className="space-y-2">
<div className="flex justify-between items-center ml-1">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className={AUTH_LABEL_CLASSES}>
Password
</label>
<Link
@@ -126,7 +117,7 @@ const Login = () => {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -137,9 +128,9 @@ const Login = () => {
<DynamicIcon icon={showPassword ? EyeOffIcon : EyeIcon} />
</button>
</div>
</Reveal>
</div>
<Reveal className="pt-2">
<div className="pt-2">
<Button
className="w-full justify-center h-10"
type="submit"
@@ -151,32 +142,25 @@ const Login = () => {
"Log in"
)}
</Button>
</Reveal>
</RevealGroup>
</form>
{/* Secondary sign-in options pick up the same cascade just after the
fields (delay continues from the form group's three children above:
0.04 + 3 × 0.08). */}
<RevealGroup inView={false} delayChildren={0.28} stagger={0.08}>
<Reveal className="relative my-6 flex items-center justify-center">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/15" />
</div>
<span className="relative bg-[#101014] px-4 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
</div>
</form>
<div>
<div className="my-6 flex items-center gap-4 before:h-px before:flex-1 before:bg-white/15 after:h-px after:flex-1 after:bg-white/15">
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
or
</span>
</Reveal>
</div>
<Reveal>
<div>
<GoogleLoginButton
mode="login"
onSuccess={handleGoogleSuccess}
onError={handleGoogleError}
/>
</Reveal>
</div>
<Reveal>
<div>
<AuthFooter>
Don't have an account?{" "}
<Link
@@ -186,8 +170,8 @@ const Login = () => {
Sign up
</Link>
</AuthFooter>
</Reveal>
</RevealGroup>
</div>
</div>
</>
);
};
@@ -22,7 +22,7 @@ export function SceneSplashModal({
<Modal
isOpen={isOpen}
onClose={close}
className="w-full max-w-3xl overflow-hidden border border-white/15 bg-ui-modal p-0"
className="w-full max-w-3xl overflow-hidden border border-white/15 bg-ui-controls p-0"
childPadding={false}
backdropClassName="!bg-black/80"
closeOnOutsideClick
@@ -88,7 +88,7 @@ const Pricing = () => {
{hasPlan && (
<Button
variant="secondary"
className="mt-6 gap-2 rounded-none"
className="mt-6 gap-2 rounded-none text-[11px]"
onClick={() => setCreditsModalOpen(true)}
>
<CoinsIcon aria-hidden className="h-4 w-4" />
@@ -164,7 +164,7 @@ const Pricing = () => {
{isLoggedIn ? (
<Button
variant="secondary"
className="mt-6 gap-2 rounded-none"
className="mt-6 gap-2 rounded-none text-[11px]"
onClick={() => setCreditsModalOpen(true)}
>
<CoinsIcon aria-hidden className="h-4 w-4 text-primary" />
@@ -1,3 +1,4 @@
import { AUTH_LABEL_CLASSES, AUTH_PASSWORD_INPUT_CLASSES, AUTH_FORM_PADDING } from "../../components/auth/auth-form-styles";
import { EyeIcon, EyeOffIcon, LoaderCircleIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
@@ -6,6 +7,8 @@ import { useState } from "react";
import { useNavigate, Navigate } from "react-router-dom";
import { UsersApi } from "@storyteller/api";
import { useSession, refreshSession } from "../../lib/session";
import { AuthHeader } from "../../components/auth/auth-layout";
import { AuthPageFrame } from "../../components/auth/auth-page-frame";
import Seo from "../../components/seo";
import { toast } from "../../components/toast/toast";
@@ -66,26 +69,15 @@ const SetPassword = () => {
};
return (
<div className="relative min-h-screen bg-ui-background text-white overflow-hidden flex flex-col">
<AuthPageFrame>
<Seo
title="Set Password - ArtCraft"
description="Set a password for your ArtCraft account."
/>
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-[#101014] border border-white/15 p-6 py-8">
<div className="text-center mb-8">
<img
src="/images/artcraft-icon.png"
alt="ArtCraft"
className="mx-auto mb-6 h-12 w-auto select-none pointer-events-none"
draggable={false}
/>
<h1 className="text-2xl font-semibold mb-2">Set a password</h1>
<p className="text-white/60 text-sm">
Create a password so you can also log into the desktop app
</p>
</div>
<main className={AUTH_FORM_PADDING}>
<div className="w-full">
<AuthHeader title={<>Set your <span className="font-serif-italic">password.</span></>} subtitle="Create a password to sign in on the web and desktop." />
<form
className="space-y-4"
@@ -101,7 +93,7 @@ const SetPassword = () => {
)}
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Password
</label>
<div className="relative">
@@ -111,7 +103,7 @@ const SetPassword = () => {
onChange={(e) => setPassword(e.target.value)}
placeholder="Min. 8 characters"
autoFocus
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -125,7 +117,7 @@ const SetPassword = () => {
</div>
<div className="space-y-2">
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 ml-1">
<label className={AUTH_LABEL_CLASSES}>
Confirm password
</label>
<div className="relative">
@@ -134,7 +126,7 @@ const SetPassword = () => {
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Re-enter your password"
inputClassName="w-full bg-ui-controls border border-white/15 focus:border-white/40 px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
/>
<button
type="button"
@@ -165,11 +157,7 @@ const SetPassword = () => {
</form>
</div>
</main>
<div className="relative z-10 py-6 text-center text-white/20 text-xs">
&copy; {new Date().getFullYear()} ArtCraft. All rights reserved.
</div>
</div>
</AuthPageFrame>
);
};
@@ -1,7 +1,6 @@
import { Link, useNavigate } from "react-router-dom";
import { AuthHeader, AuthFooter, SignupForm } from "../../components/auth";
import Seo from "../../components/seo";
import { Reveal } from "../../components/motion/reveal";
const Signup = () => {
const navigate = useNavigate();
@@ -12,17 +11,13 @@ const Signup = () => {
title="Sign Up - ArtCraft"
description="Create your ArtCraft account."
/>
<AuthHeader title="Create an Account" subtitle="Join thousands of creators" />
{/* SignupForm runs its own field cascade internally (see signup-form.tsx).
The footer picks up the tail of that cascade — the form's six staggered
steps land by ~0.36s, so the footer follows at ~0.44s. */}
<AuthHeader title={<>Make it <span className="font-serif-italic">yours.</span></>} subtitle="Create your account and start crafting." />
<SignupForm
onSuccess={() => navigate("/welcome")}
signupSource="artcraft"
/>
<Reveal inView={false} delay={0.44}>
<div>
<AuthFooter>
Already have an account?{" "}
<Link
@@ -32,7 +27,7 @@ const Signup = () => {
Log in
</Link>
</AuthFooter>
</Reveal>
</div>
</>
);
};
+17 -29
View File
@@ -527,37 +527,25 @@
}
}
/* Quiet drafting grid and side rails echo the landing's section frames.
No masks, filters, animation, or viewport-sized layers on mobile. */
.create-empty-backdrop {
/* Fine registration marks echo the landing page section corners. */
.frame-corner-mark {
position: absolute;
inset: 0;
z-index: 0;
overflow: hidden;
pointer-events: none;
background-image:
linear-gradient(rgba(242, 241, 238, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(242, 241, 238, 0.025) 1px, transparent 1px);
background-size: 96px 96px;
background-position: center;
}
.create-empty-backdrop::before {
content: "";
position: absolute;
inset: 12% clamp(24px, 6%, 96px) 20%;
border: 1px solid rgba(242, 241, 238, 0.1);
}
.create-empty-backdrop::after {
content: "";
position: absolute;
top: 12%;
left: clamp(24px, 6%, 96px);
width: 11px;
height: 11px;
transform: translate(-5px, -5px);
margin: -5px;
pointer-events: none;
background:
linear-gradient(#74aaff, #74aaff) center / 1px 11px no-repeat,
linear-gradient(#74aaff, #74aaff) center / 11px 1px no-repeat;
linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 1px 11px no-repeat,
linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 11px 1px no-repeat;
}
/* Contained section borders match the landing without decorative extensions. */
.create-empty-section {
margin-inline: 12px;
border: 1px solid rgba(242, 241, 238, 0.2);
background: rgba(242, 241, 238, 0.015);
}
@media (min-width: 640px) {
.create-empty-section { margin-inline: 32px; }
}
@@ -101,7 +101,7 @@ export const ButtonDropdown = ({
<Menu.Items
static
className={twMerge(
"absolute z-20 mt-1 w-max divide-y divide-gray-100 overflow-hidden rounded-lg bg-brand-secondary py-1.5 shadow-xl focus:outline-none",
"absolute z-20 mt-2 w-max min-w-48 overflow-hidden rounded-[3px] border border-ui-panel-border bg-ui-controls p-1.5 text-base-fg focus:outline-none",
align === "left" ? "left-0" : "right-0",
)}
>
@@ -109,19 +109,19 @@ export const ButtonDropdown = ({
{options.map((option, index) => (
<Fragment key={index}>
{option.divider && (
<div className="my-1.5 border-t border-white/5" />
<div className="my-1.5 border-t border-white/15" />
)}
<Menu.Item>
{({ active }) => (
<button
disabled={option.disabled}
className={twMerge(
"duration-50 bg-brand-secondary font-medium text-white transition-all",
active ? "bg-brand-secondary-800" : "",
"rounded-[3px] bg-transparent font-medium text-base-fg transition-colors duration-150",
active ? "bg-white/10" : "",
option.disabled
? "pointer-events-none opacity-40"
: "",
"group flex w-full items-center py-1.5 pl-7 pr-4 text-sm",
"group flex w-full items-center gap-2 px-2.5 py-2 text-sm",
option.className,
)}
onClick={() => handleOptionClick(index)}
@@ -130,11 +130,11 @@ export const ButtonDropdown = ({
{option.icon && (
<DynamicIcon
icon={option.icon}
className="mr-2"
className="mr-2 h-4 w-4 shrink-0"
/>
)}
<div className="grow text-start">{option.label}</div>
<div className="ml-10 font-normal text-white/75">
<div className="ml-6 font-mono text-[11px] font-normal text-white/45">
{option.description && option.description}
</div>
{showSelected && (
@@ -15,12 +15,14 @@ interface ButtonIconSelectProps {
options: Option[];
onOptionChange?: (value: string) => void;
selectedOption?: string;
buttonClassName?: string;
}
export function ButtonIconSelect({
options,
onOptionChange,
selectedOption,
buttonClassName,
}: ButtonIconSelectProps) {
const [internalSelectedOption, setInternalSelectedOption] = useState<string>(
selectedOption || options[0].value
@@ -54,7 +56,8 @@ export function ButtonIconSelect({
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
internalSelectedOption === value
? "border-brand-primary bg-brand-primary/20"
: "border-transparent hover:bg-ui-panel/[0.4]"
: "border-transparent hover:bg-ui-panel/[0.4]",
buttonClassName,
)}
onClick={() => handleOptionChange(value)}
>
@@ -72,7 +75,8 @@ export function ButtonIconSelect({
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
internalSelectedOption === value
? "border-brand-primary bg-brand-primary/20"
: "border-transparent hover:bg-ui-panel/[0.4]"
: "border-transparent hover:bg-ui-panel/[0.4]",
buttonClassName,
)}
onClick={() => handleOptionChange(value)}
>
@@ -30,11 +30,11 @@ export const ToggleButton = ({
// PopoverMenu triggers). Flat control surface + hairline border,
// same idiom as the PopoverMenu triggers beside it — the old glassy
// backdrop-blur look predates the brutalist system.
"flex h-[34px] items-center justify-center rounded-[3px] border border-ui-controls-border bg-ui-controls text-sm text-white transition-colors",
hasLabel ? "px-3" : "w-[34px]",
"flex h-[34px] items-center justify-center rounded-[3px] border border-ui-controls-border bg-ui-controls text-white transition-colors",
hasLabel ? "px-3" : "w-[34px] p-0",
isActive
? "border-white/30 bg-brand-primary/40 hover:bg-brand-primary/40"
: "hover:border-white/30 hover:bg-ui-controls/80",
? "border-white/30 bg-brand-primary/40 hover:border-white/30 hover:bg-brand-primary/40"
: "hover:bg-ui-controls hover:bg-[linear-gradient(rgba(255,255,255,0.07),rgba(255,255,255,0.07))]",
className,
)}
variant="secondary"
@@ -48,7 +48,7 @@ export const ToggleButton = ({
/>
)}
{label && (
<span className="text-sm font-medium text-white/90">{label}</span>
<span className="whitespace-nowrap text-white/90">{label}</span>
)}
</span>
</Button>
@@ -4,6 +4,9 @@ import { DynamicIcon } from "@storyteller/icons";
import { twMerge } from "tailwind-merge";
import { ButtonHTMLAttributes, AnchorHTMLAttributes } from "react";
export const NEUTRAL_BUTTON_HOVER_CLASSES =
"hover:bg-white/[0.07] hover:border-white/25";
type AnchorProps = Omit<
AnchorHTMLAttributes<HTMLAnchorElement>,
keyof ButtonHTMLAttributes<HTMLButtonElement>
@@ -41,20 +44,20 @@ export const Button = ({
case "secondary": {
// Resting bg-white/5 tint (matching the home grid cards) so the
// button reads as a surface, not a bare outline on the page bg.
return "bg-white/5 text-base-fg border border-ui-controls-border hover:bg-white/10 hover:border-white/30";
return "bg-white/5 text-base-fg border border-ui-controls-border";
}
case "action": {
return "bg-ui-controls text-base-fg border border-ui-controls-border hover:bg-ui-controls/80";
return "bg-ui-controls text-base-fg border border-ui-controls-border";
}
case "destructive": {
return "bg-red-500 hover:bg-red-400 text-white";
}
case "ghost": {
return "bg-transparent text-base-fg/70 hover:bg-white/10 hover:text-base-fg";
return "bg-transparent text-base-fg/70 hover:text-base-fg";
}
case "primary":
default: {
return "bg-white hover:bg-white/80 text-black font-bold";
return "bg-white hover:bg-white/90 text-black font-bold";
}
}
}
@@ -66,6 +69,8 @@ export const Button = ({
const className = twMerge(
"w-fit rounded-[3px] font-mono text-xs font-semibold uppercase tracking-[0.12em] px-3.5 py-2 border border-transparent focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-white/60 transition-colors duration-150 flex gap-2 items-center justify-center",
getVariantClassNames(propsVariant),
(propsVariant === "secondary" || propsVariant === "action" || propsVariant === "ghost") &&
NEUTRAL_BUTTON_HOVER_CLASSES,
propsClassName,
disabledClass,
);
@@ -17,7 +17,7 @@ import {
getCreatorIcon,
getModelFamilyName,
} from "@storyteller/model-list";
import { ChevronUpIcon, CircleCheckIcon } from "lucide-react";
import { ChevronUpIcon, CheckIcon } from "lucide-react";
import { GenerationProvider } from "@storyteller/api-enums";
import { Tooltip } from "@storyteller/ui-tooltip";
import { defaultModelForPage } from "./defaultModelForPage";
@@ -118,8 +118,8 @@ function ProviderTooltipContent({
{getProviderDisplayName(p)}
</span>
{selectedProvider === p && (
<span className="text-primary text-xl font-bold bg-white rounded-full p-0 h-4 w-4 flex items-center justify-center">
<CircleCheckIcon />
<span className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-white">
<CheckIcon aria-hidden="true" className="h-4 w-4" strokeWidth={2.5} />
</span>
)}
</button>
@@ -313,7 +313,7 @@ export function ClassyModelSelector({
mode="hoverSelect"
maxListHeight={maxListHeight}
{...popoverProps}
buttonClassName="rounded-[3px] bg-ui-controls/90 hover:bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
buttonClassName="rounded-[3px] bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
renderTrigger={(selectedItem) => {
const modelTitle =
selectedItem?.label ?? selectedModel?.selectorName ?? "";
@@ -43,7 +43,6 @@ import type { PopoverItem } from "@storyteller/ui-popover";
import { v4 as uuidv4 } from "uuid";
import { EngineContext } from "./contexts/EngineContext/EngineContext";
import { AnonHintChip } from "./comps/AnonHintChip";
import { AspectRatioMenu } from "./comps/AspectRatioMenu";
import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
import { Controls3D } from "./comps/Controls3D";
@@ -488,26 +487,25 @@ export const Stage3DBody = ({
className="absolute left-0 top-0 w-full"
onClick={handleOverlayClick}
>
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col items-start gap-2">
<div className="grid grid-cols-2 gap-x-4 gap-y-1 2xl:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]">
<div className="min-w-0 flex flex-col items-start gap-2">
{!isRecord && <ControlsTopButtons />}
{!isRecord && topBarStartSlot && (
<div className="pl-3">{topBarStartSlot}</div>
)}
</div>
<div className="flex flex-col items-center">
<div className="col-span-2 row-start-2 flex flex-col items-center 2xl:col-span-1 2xl:col-start-2 2xl:row-start-1">
<SceneModePill />
{!isRecord && (
<Controls3D showImageTo3DButton={showImageTo3DButton} />
)}
</div>
<div className="flex items-start justify-end gap-2 pr-3 pt-3">
<div className="col-start-2 row-start-1 flex min-w-0 flex-wrap items-start justify-end gap-2 pr-3 pt-3 2xl:col-start-3">
{/* Camera cluster: which camera drives the viewport +
the render-frame aspect ratio. */}
{!isRecord && <CameraStatusPill />}
{!isRecord && <AspectRatioMenu />}
{!isRecord && topBarEndSlot}
{!isRecord && <AnonHintChip />}
</div>
</div>
</div>
@@ -21,7 +21,7 @@ export const AnonHintChip = () => {
<button
type="button"
onClick={handleClick}
className="glass flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs font-medium text-white/80 shadow-md transition-colors hover:bg-ui-controls/100 hover:text-white"
className="flex shrink-0 items-center gap-2 whitespace-nowrap rounded-[3px] border border-ui-panel-border bg-ui-controls px-3 py-1.5 text-xs font-medium text-white/80 transition-colors hover:bg-white/10 hover:text-white"
>
<LogInIcon className="opacity-70" />
Sign up to save
@@ -6,6 +6,7 @@ import { CameraAspectRatio } from "../../enums";
import { setCameraAspect } from "../../actions";
import { usePageSceneStore } from "../../PageSceneStore";
import { EngineContext } from "../../contexts/EngineContext";
import { TOOLBAR_BUTTON_CLASS_NAME } from "../toolbarStyles";
// Render-frame aspect ratio picker. Same popover pattern as the prompt
// boxes' AspectRatioPicker, but driving the 3D editor's camera letterbox
@@ -52,7 +53,7 @@ export const AspectRatioMenu = () => {
position="bottom"
panelTitle="Aspect Ratio"
showIconsInList
buttonClassName="glass glass-no-hover h-[34px] rounded-full px-3 text-xs font-medium shadow-xl"
buttonClassName={TOOLBAR_BUTTON_CLASS_NAME}
triggerIcon={<AspectRatioIcon ratio={current.proportions} />}
/>
</Tooltip>
@@ -209,7 +209,7 @@ export const AnimationsModal = () => {
<Modal
isOpen={animationsModalVisible}
onClose={handleClose}
className="relative h-[640px] max-w-4xl"
className="relative h-[640px] max-w-4xl bg-ui-controls"
childPadding={false}
showClose={false}
backdropClassName="bg-transparent"
@@ -228,8 +228,8 @@ export const AnimationsModal = () => {
</Modal.DragHandle>
<div className="grid h-full grid-cols-12 gap-3">
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10">
<div className="flex items-center justify-between gap-2.5 py-0.5">
<h2 className="text-[18px] font-semibold opacity-80">
<div className="flex h-10 items-center justify-between gap-2.5">
<h2 className="font-display text-lg tracking-tight text-white">
Animations
</h2>
</div>
@@ -240,9 +240,9 @@ export const AnimationsModal = () => {
key={tab.id}
variant={activeTab === tab.id ? "primary" : "secondary"}
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
"w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-left hover:bg-white/15",
activeTab === tab.id &&
"border-primary bg-primary/10 hover:bg-primary/10",
"border-white/15 bg-white/10 text-white hover:bg-white/10",
)}
onClick={() => setActiveTab(tab.id)}
>
@@ -259,7 +259,7 @@ export const AnimationsModal = () => {
variant="secondary"
icon={ArrowUpFromLineIcon}
iconClassName="opacity-70"
className="w-full justify-center rounded-xl border border-white/10 bg-white/[6%] px-3.5 py-2.5 hover:bg-white/15"
className="w-full justify-center rounded-[3px] border border-white/10 bg-white/[6%] px-3.5 py-2.5 hover:bg-white/15"
onClick={handleOpenUpload}
>
Upload animation
@@ -281,13 +281,13 @@ export const AnimationsModal = () => {
ref={searchInputRef}
placeholder="Search animations"
className="relative z-[51] grow"
inputClassName="pr-2.5"
inputClassName="bg-white/5 pr-10 placeholder-white/40"
icon={SearchIcon}
value={searchTerm}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setSearchTerm(e.target.value)
}
iconClassName="text-white/60"
iconClassName="pointer-events-none left-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50"
/>
{searchTerm && (
<CloseButton
@@ -56,12 +56,12 @@ const AllTabSection = ({
<div className="mb-2 flex items-center justify-between">
<h3 className="text-md ml-2 font-semibold opacity-90">{label}</h3>
<Button
variant="secondary"
className="mr-3 flex items-center gap-1 px-2 py-1 text-xs"
variant="ghost"
className="mr-3 h-6 gap-1 px-1.5 py-0 text-[10px] tracking-[0.08em] leading-none"
onClick={onViewAll}
>
View all
<ChevronRightIcon className="text-xs opacity-70" />
<ChevronRightIcon className="h-3 w-3 shrink-0 opacity-70" />
</Button>
</div>
<div className="h-[170px]">
@@ -487,7 +487,7 @@ export const AssetModal = () => {
<Modal
isOpen={assetModalVisible}
onClose={handleClose}
className="relative h-[640px] max-w-4xl"
className="relative h-[640px] max-w-4xl bg-ui-controls"
childPadding={false}
showClose={false}
backdropClassName="bg-transparent"
@@ -507,8 +507,8 @@ export const AssetModal = () => {
</Modal.DragHandle>
<div className="grid h-full grid-cols-12 gap-3">
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10">
<div className="flex items-center justify-between gap-2.5 py-0.5">
<h2 className="text-[18px] font-semibold opacity-80">
<div className="flex h-10 items-center justify-between gap-2.5">
<h2 className="font-display text-lg tracking-tight text-white">
ArtCraft Presets
</h2>
</div>
@@ -519,9 +519,9 @@ export const AssetModal = () => {
key={tab.id}
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
"w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-left hover:bg-white/15",
activeAssetTab === tab.id &&
"border-primary bg-primary/10 hover:bg-primary/10",
"border-white/15 bg-white/10 text-white hover:bg-white/10",
)}
onClick={() => setActiveAssetTab(tab.id)}
>
@@ -551,13 +551,13 @@ export const AssetModal = () => {
ref={searchInputRef}
placeholder="Search"
className="relative z-[51] grow"
inputClassName="pr-2.5"
inputClassName="bg-white/5 pr-10 placeholder-white/40"
icon={SearchIcon}
value={searchTerm}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setSearchTerm(e.target.value)
}
iconClassName="text-white/60"
iconClassName="pointer-events-none left-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50"
/>
{searchTerm && (
<CloseButton
@@ -110,7 +110,7 @@ export const ItemElement = ({ item }: Props) => {
)}
<div
className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-xl border-[3px] border-white/5 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary"
className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-none border border-white/15 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary"
onPointerDown={(event) => dragAndDrop.onPointerDown(event, item, editor)}
onClick={handleClick}
style={{ cursor: "grab", pointerEvents: "auto" }}
@@ -1,11 +1,13 @@
import { useContext } from "react";
import { MoveIcon, VideoIcon } from "lucide-react";
import { DynamicIcon } from "@storyteller/icons";
import { Button } from "@storyteller/ui-button";
import { twMerge } from "tailwind-merge";
import { Tooltip } from "@storyteller/ui-tooltip";
import { EngineContext } from "../../contexts/EngineContext";
import { toggleCameraView } from "../../actions";
import { usePageSceneStore } from "../../PageSceneStore";
import { EditorStates } from "../../enums";
import { TOOLBAR_BUTTON_CLASS_NAME } from "../toolbarStyles";
// Small status pill (top-left) showing which camera the viewport is driving:
// the free "Viewport" camera vs looking through a render camera. Clicking it
@@ -28,21 +30,19 @@ export const CameraStatusPill = () => {
position="bottom"
delay={300}
>
<button
<Button
type="button"
variant="secondary"
icon={inCameraView ? VideoIcon : MoveIcon}
aria-pressed={inCameraView}
onClick={() => editor && toggleCameraView(editor)}
className={
inCameraView
? "flex h-[34px] items-center gap-2 rounded-full bg-brand-primary px-3 text-xs font-medium text-white shadow-xl transition-all duration-200"
: "glass glass-no-hover flex h-[34px] items-center gap-2 rounded-full px-3 text-xs font-medium text-base-fg/70 shadow-xl transition-all duration-200 hover:text-base-fg"
}
className={twMerge(
TOOLBAR_BUTTON_CLASS_NAME,
inCameraView && "border-white/30 bg-white/10",
)}
>
<DynamicIcon
icon={inCameraView ? VideoIcon : MoveIcon}
className="h-3 w-3"
/>
{inCameraView ? renderCameraLabel : "Viewport"}
</button>
</Button>
</Tooltip>
);
};
@@ -82,12 +82,12 @@ export const CompletionModal = () => {
return (
<div className="absolute inset-0 z-[70] flex items-center justify-center bg-black/60 p-6 backdrop-blur-sm">
<div className="glass glass-no-hover flex w-full max-w-xl flex-col gap-4 rounded-2xl p-5 text-white shadow-2xl">
<div className="border border-ui-panel-border bg-ui-controls flex w-full max-w-xl flex-col gap-4 rounded-none p-5 text-white">
<div className="text-sm font-semibold text-base-fg/90">
{isVideo ? "Recording complete" : "Capture complete"}
</div>
<div className="flex max-h-[52vh] items-center justify-center overflow-hidden rounded-xl bg-black/30">
<div className="flex max-h-[52vh] items-center justify-center overflow-hidden rounded-none bg-white/5">
{isVideo ? (
<video
src={artifact.objectUrl}
@@ -129,7 +129,7 @@ export const CompletionModal = () => {
type="button"
onClick={() => clearProducedArtifact()}
disabled={busy}
className="flex h-9 items-center gap-2 rounded-full px-3 text-sm text-base-fg/60 transition-colors hover:bg-white/10 hover:text-base-fg disabled:opacity-50"
className="flex h-9 items-center gap-2 rounded-[3px] px-3 text-sm text-base-fg/60 transition-colors hover:bg-white/10 hover:text-base-fg disabled:opacity-50"
>
<TrashIcon className="h-3.5 w-3.5" />
Discard
@@ -139,7 +139,7 @@ export const CompletionModal = () => {
<button
type="button"
onClick={() => void upload()}
className="flex h-9 items-center gap-2 rounded-full bg-brand-primary px-4 text-sm font-medium text-white transition-transform hover:scale-[1.03]"
className="flex h-9 items-center gap-2 rounded-[3px] bg-brand-primary px-4 text-sm font-medium text-white transition-colors hover:brightness-110"
>
<RotateCwIcon className="h-3.5 w-3.5" />
Retry
@@ -148,7 +148,7 @@ export const CompletionModal = () => {
<button
type="button"
onClick={() => void upload()}
className="flex h-9 items-center gap-2 rounded-full bg-brand-primary px-4 text-sm font-medium text-white transition-transform hover:scale-[1.03]"
className="flex h-9 items-center gap-2 rounded-[3px] bg-brand-primary px-4 text-sm font-medium text-white transition-colors hover:brightness-110"
>
<CloudUploadIcon className="h-3.5 w-3.5" />
Upload &amp; continue
@@ -305,7 +305,7 @@ export const ControlPanelSceneObject = () => {
as="div"
show={isShowing}
className={twMerge(
"glass absolute bottom-16 right-0 mb-4 mr-4 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-lg border border-ui-panel-border p-3.5 text-white shadow-lg",
"border border-ui-panel-border bg-ui-controls absolute bottom-16 right-0 mb-4 mr-4 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-none p-3.5 text-white",
)}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
@@ -315,7 +315,7 @@ export const ControlPanelSceneObject = () => {
leaveTo="opacity-0"
style={{ transform: `scale(${getScale()})` }}
>
<div className="mb-1 flex justify-between">
<div className="mb-1 flex justify-between border-b border-ui-panel-border pb-3">
<div className="flex items-center gap-2">
<BoxIcon />
<p className="max-w-36 truncate font-semibold">
@@ -342,7 +342,7 @@ export const ControlPanelSceneObject = () => {
className={"flex flex-col gap-2 overflow-y-auto"}
>
<div className="flex flex-col gap-1">
<p className="font-semibold text-sm">Color</p>
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.12em] text-white/60">Color</p>
{/* Native <input type="color"> is nested INSIDE the Button's
<label> so Safari opens the picker reliably. Safari refuses
to open a color picker triggered via label[htmlFor]→input
@@ -385,7 +385,7 @@ export const ControlPanelSceneObject = () => {
</Button>
</div>
<div className="flex flex-col gap-2">
<p className="font-semibold text-sm">Location</p>
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.12em] text-white/60">Location</p>
<InputVector
x={localPosition.x.toString()}
y={localPosition.y.toString()}
@@ -398,7 +398,7 @@ export const ControlPanelSceneObject = () => {
</div>
<div className="flex flex-col gap-2">
<p className="font-semibold text-sm">Rotation</p>
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.12em] text-white/60">Rotation</p>
<InputVector
x={localRotation.x.toString()}
y={localRotation.y.toString()}
@@ -413,7 +413,7 @@ export const ControlPanelSceneObject = () => {
<div className="mb-1 flex flex-col gap-2">
<DraggablePrecisionMutator onChange={handleUniformScaleChange}>
<p className="font-semibold text-sm">Scale</p>
<p className="font-mono text-[11px] font-medium uppercase tracking-[0.12em] text-white/60">Scale</p>
</DraggablePrecisionMutator>
<InputVector
x={localScale.x.toString()}
@@ -169,14 +169,14 @@ export const Controls3D = ({
return (
<>
<div className="flex justify-center pt-3">
<div className="glass rounded-2xl p-1.5 text-white shadow-xl h-fit">
<div className="border border-ui-panel-border bg-ui-controls rounded-none p-1.5 text-white h-fit">
<div className="flex items-center justify-center gap-1.5">
<div className="flex items-center gap-1.5">
<div className="relative">
{showEmptySceneTooltip && (
<div className="absolute -bottom-14 left-1/2 -translate-x-1/2 transform whitespace-nowrap">
<div className="animate-bounce rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white shadow-lg">
Click + to add your first 3D asset!
<div className="rounded-[3px] bg-primary px-4 py-2 text-sm font-medium text-white">
Click + to add your first 3D asset
<div className="absolute -top-1.5 left-1/2 h-3 w-3 -translate-x-1/2 rotate-45 transform bg-primary" />
</div>
</div>
@@ -236,7 +236,7 @@ export const Controls3D = ({
]}
onPanelAction={handleAddAssetAction}
showIconsInList
buttonClassName={`h-9 w-9 p-2 rounded-xl text-lg ${
buttonClassName={`h-9 w-9 p-2 rounded-[3px] text-lg ${
showEmptySceneTooltip
? "bg-primary/90 hover:bg-primary/70"
: "border-transparent bg-primary/90 hover:bg-primary/70"
@@ -254,7 +254,7 @@ export const Controls3D = ({
>
<Button
icon={WandSparklesIcon}
className="text-md h-9 w-9 rounded-[10px] bg-white/15 transition-colors hover:bg-white/25"
className="text-md h-9 w-9 rounded-[3px] bg-white/15 transition-colors hover:bg-white/25"
variant="secondary"
onClick={handleOpenCreate3dModal}
/>
@@ -262,13 +262,14 @@ export const Controls3D = ({
)}
</div>
<span className="opacity-10">|</span>
<span aria-hidden="true" className="h-5 w-px bg-white/15" />
<ButtonIconSelect
options={modes}
onOptionChange={handleModeChange}
selectedOption={selectedMode}
buttonClassName="rounded-[3px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-300"
/>
<span className="opacity-10">|</span>
<span aria-hidden="true" className="h-5 w-px bg-white/15" />
{selectedMode === "scale" ? (
<Tooltip
content="Scale is always in local space"
@@ -277,7 +278,7 @@ export const Controls3D = ({
>
<button
disabled
className="flex min-w-[92px] cursor-not-allowed items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-3 py-1.5 text-sm font-medium text-base-fg opacity-40 transition-colors duration-150"
className="flex min-w-[92px] cursor-not-allowed items-center justify-center gap-2 rounded-[3px] border border-white/10 bg-white/[0.04] px-3 py-1.5 font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-base-fg opacity-40 transition-colors duration-150"
>
<BoxIcon className="h-3 w-3 text-base-fg/60" />
Local
@@ -290,7 +291,7 @@ export const Controls3D = ({
delay={300}
>
<button
className="flex min-w-[92px] items-center justify-center gap-2 rounded-xl border border-white/10 bg-white/[0.04] px-3 py-1.5 text-sm font-medium text-base-fg transition-colors duration-150 hover:bg-white/[0.08]"
className="flex min-w-[92px] items-center justify-center gap-2 rounded-[3px] border border-white/10 bg-white/[0.04] px-3 py-1.5 font-mono text-[11px] font-medium uppercase tracking-[0.08em] text-base-fg transition-colors duration-150 hover:bg-white/[0.08]"
onClick={() => editor?.gizmo.toggleTransformSpace()}
>
<DynamicIcon
@@ -15,6 +15,8 @@ import { DEFAULT_CAMERA_ASPECT_RATIO, ToastTypes } from "../../enums";
import { getSceneGenerationMetaData } from "../../sceneMetadata";
import { LoadUserScenes } from "./LoadUserScenes";
import { TOOLBAR_BUTTON_CLASS_NAME } from "../toolbarStyles";
const isNumberString = (s: string): boolean => /^\d+$/.test(s);
export const ControlsTopButtons = () => {
@@ -170,7 +172,7 @@ export const ControlsTopButtons = () => {
// visitors). We deliberately do NOT require sceneMeta.isModified;
// dirty-tracking isn't plumbed end-to-end and saving shouldn't depend
// on it. Anon still falls through to the signup CTA branch below.
const canSave = !!currentUserToken;
const canSave = !!currentUserToken || !!editor?.adapter.promptSignup;
// A scene needs a name prompt before saving when it's brand-new (no
// token) or has no usable title yet. Already-named saved scenes save
@@ -239,11 +241,11 @@ export const ControlsTopButtons = () => {
return (
<div className="flex flex-col gap-2 pl-3 pt-3">
<div className="flex gap-1.5">
<div className="flex items-center gap-1.5">
<ButtonDropdown
label="File"
icon={FileIcon}
className="shadow-xl"
className={TOOLBAR_BUTTON_CLASS_NAME}
options={[
{
label: "New scene",
@@ -311,7 +313,9 @@ export const ControlsTopButtons = () => {
// that branch.
{
disabled: !canSave,
label: isVisitingOthersScene ? "Save copy" : "Save scene",
label: !currentUserToken
? "Sign up to save"
: isVisitingOthersScene ? "Save copy" : "Save scene",
description: "Ctrl+S",
...(!currentUserToken
? {
@@ -379,7 +383,7 @@ export const ControlsTopButtons = () => {
<Button
icon={outlinerShowing ? SquareCheckIcon : SquareIcon}
className="shadow-xl"
className={TOOLBAR_BUTTON_CLASS_NAME}
iconClassName={twMerge(
"text-[16px]",
outlinerShowing ? "text-white" : "text-white/20",
@@ -397,7 +401,7 @@ export const ControlsTopButtons = () => {
<Button
icon={KeyboardIcon}
variant="secondary"
className="shadow-xl"
className={TOOLBAR_BUTTON_CLASS_NAME}
iconClassName={twMerge(
"text-[16px]",
cheatsheetPinned ? "text-white" : "text-white/20",
@@ -25,7 +25,7 @@ export const ExitCameraView = ({ className }: { className?: string }) => {
type="button"
onClick={() => editor && toggleCameraView(editor)}
className={twMerge(
"flex h-11 items-center gap-2 whitespace-nowrap rounded-xl bg-brand-primary px-4 text-sm font-medium text-white shadow-xl transition-colors hover:bg-brand-primary/90",
"flex h-11 items-center gap-2 whitespace-nowrap rounded-[3px] bg-brand-primary px-4 text-sm font-medium text-white transition-colors hover:bg-brand-primary/90",
className,
)}
>
@@ -15,7 +15,7 @@ export const FocalLengthDisplay = () => {
leaveTo="opacity-0"
>
<div className="absolute left-1/2 top-16 z-10 -translate-x-1/2 transform">
<div className="glass rounded-xl px-5 py-2.5 text-center text-2xl font-bold text-white">
<div className="border border-ui-panel-border bg-ui-controls rounded-none px-5 py-2.5 text-center text-2xl font-bold text-white">
{focalLengthDragging.focalLength}mm
</div>
</div>
@@ -58,9 +58,9 @@ const OutlinerRow = ({ item }: { item: OutlinerItem }) => {
<div
role="button"
className={twMerge(
"flex cursor-pointer items-center justify-between gap-2 rounded-xl border border-transparent px-3 py-2 outline-none transition-colors duration-100 hover:bg-white/5 focus:outline-none",
"flex cursor-pointer items-center justify-between gap-2 rounded-[3px] border border-transparent px-3 py-2 transition-colors duration-100 hover:bg-white/5 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-300",
isSelected &&
"border-brand-primary bg-brand-primary/15 hover:bg-brand-primary/15",
"border-white/20 bg-white/10 hover:bg-white/10",
)}
onDoubleClick={handleDoubleClick}
onClick={handleSelect}
@@ -146,7 +146,7 @@ export const Outliner = () => {
as="div"
show={showing}
className={twMerge(
"flex h-[45vh] w-[260px] flex-col overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel shadow-lg",
"flex h-[45vh] w-[260px] flex-col overflow-hidden rounded-[3px] border border-ui-panel-border bg-ui-controls",
)}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
@@ -155,18 +155,23 @@ export const Outliner = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="flex items-center px-4 pb-1 pt-3">
<h1 className="grow text-base font-semibold">Scene</h1>
<div className="flex items-center border-b border-ui-panel-border px-3 py-2.5">
<h2 className="grow font-mono text-[11px] font-semibold uppercase tracking-[0.12em]">Scene</h2>
<Button
icon={PlusIcon}
className="h-6 bg-transparent px-1.5 text-sm font-medium text-white/80 hover:bg-transparent hover:text-white"
variant="ghost"
iconClassName="h-3.5 w-3.5 shrink-0"
className="h-7 gap-1 px-1.5 py-0 text-[11px] leading-none text-white/70 hover:text-white"
onClick={openAssetModal}
>
Add
</Button>
<Button
icon={XIcon}
className="h-5 bg-transparent p-0 text-xl opacity-50 hover:bg-transparent hover:opacity-90"
variant="ghost"
aria-label="Close scene panel"
iconClassName="h-4 w-4 shrink-0"
className="h-7 w-7 p-0 text-white/50 hover:text-white"
onClick={() => {
usePageSceneStore.getState().setOutlinerShowing(false);
}}
@@ -44,7 +44,7 @@ export const PoseModeSelector: React.FC<PoseModeSelectorProps> = () => {
<Button
icon={FootprintsIcon}
onClick={handleModeChange}
className="rounded-xl shadow-xl outline-none focus-visible:outline-none"
className="rounded-[3px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-300"
>
Enter Pose Mode
</Button>
@@ -52,7 +52,7 @@ export const PoseModeSelector: React.FC<PoseModeSelectorProps> = () => {
<Button
icon={CheckIcon}
onClick={handleModeChange}
className="rounded-xl outline-none focus-visible:outline-none"
className="rounded-[3px] outline-none focus-visible:outline-none"
>
Done
</Button>
@@ -71,7 +71,7 @@ export const PoseModeSelector: React.FC<PoseModeSelectorProps> = () => {
<Button
icon={FilmIcon}
onClick={() => openAnimationsModal()}
className="rounded-xl shadow-xl outline-none focus-visible:outline-none"
className="rounded-[3px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-300"
>
Add Animation
</Button>
@@ -27,7 +27,7 @@ export const PrecisionSelector = () => {
<span
onMouseEnter={() => handleMouseEnterItem(scale)}
key={index}
className="flex bg-ui-panel text-sm justify-center align-middle justify-items-center px-2 hover:bg-ui-controls"
className="flex bg-ui-controls text-sm justify-center align-middle justify-items-center px-2 hover:bg-white/10"
>
{scale}
</span>
@@ -84,7 +84,7 @@ export const PreviewEngineCamera = () => {
>
<div
className={twMerge(
"origin -z-10 flex h-auto w-full flex-wrap items-center gap-1.5 rounded-t-lg bg-ui-panel p-2 text-white",
"origin -z-10 flex h-auto w-full flex-wrap items-center gap-1.5 rounded-t-lg bg-ui-controls p-2 text-white",
camAspect !== CameraAspectRatio.VERTICAL_9_16
? "justify-between"
: "flex-col justify-center",
@@ -134,7 +134,7 @@ export const PreviewEngineCamera = () => {
: "aspect-video",
)}
>
<div className="flex h-full w-full items-center justify-center bg-ui-panel">
<div className="flex h-full w-full items-center justify-center bg-ui-controls">
<LoaderCircleIcon size="3em" className="animate-spin" />
</div>
<div className="absolute left-0 top-0 h-full w-full overflow-hidden">
@@ -13,7 +13,7 @@ export const PreviewImages = () => {
return (
<div className="flex justify-center gap-1">
<div
className="block w-full overflow-hidden rounded-lg border border-ui-controls/25 bg-ui-panel"
className="block w-full overflow-hidden rounded-lg border border-ui-controls/25 bg-ui-controls"
style={{ height: imageHeight, width: imageWidth }}
>
<canvas id="raw-preview" width={imageWidth} height={imageHeight} />
@@ -24,7 +24,7 @@ export const PreviewImages = () => {
className="text-3xl opacity-60" />
</div>
<div
className="block w-full overflow-hidden rounded-lg border border-ui-controls/25 bg-ui-panel"
className="block w-full overflow-hidden rounded-lg border border-ui-controls/25 bg-ui-controls"
style={{ height: imageHeight, width: imageWidth }}
>
<img
@@ -31,7 +31,7 @@ export const RecordControls = () => {
type="button"
onClick={() => editor && captureStill(editor)}
disabled={busy}
className="glass glass-no-hover flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-medium text-base-fg shadow-xl hover:bg-white/10 disabled:opacity-50"
className="border border-ui-panel-border bg-ui-controls flex items-center gap-2 rounded-[3px] px-5 py-2.5 text-sm font-medium text-base-fg hover:bg-white/10 disabled:opacity-50"
>
<CameraIcon className="h-3.5 w-3.5" />
Capture
@@ -49,7 +49,7 @@ export const RecordControls = () => {
type="button"
onClick={() => editor && void recordVideo(editor)}
disabled={busy || !timelineExists}
className="flex items-center gap-2 rounded-full bg-red px-5 py-2.5 text-sm font-semibold text-white shadow-xl transition-transform hover:scale-[1.03] disabled:opacity-50 disabled:hover:scale-100"
className="flex items-center gap-2 rounded-[3px] bg-red px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:brightness-110 disabled:opacity-50"
>
<CircleIcon className="h-3 w-3" />
Record
@@ -22,9 +22,9 @@ export const SceneModePill = () => {
activeTab={sceneMode}
onTabChange={(id) => setSceneMode(id as SceneMode)}
className="w-auto"
listClassName="rounded-full px-1"
tabClassName="rounded-full px-5 py-1"
indicatorClassName="rounded-full bg-white/95"
listClassName="rounded-none border border-ui-panel-border bg-ui-controls px-1"
tabClassName="rounded-[3px] px-5 py-1 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary-300"
indicatorClassName="rounded-[3px] bg-white/95"
selectedTabClassName="text-black"
/>
</div>
@@ -75,7 +75,7 @@ export const MotionPopover = ({
return (
<div
id="motion-popover"
className="glass glass-no-hover pointer-events-auto absolute bottom-full z-50 mb-2 w-[180px] -translate-x-1/2 select-none rounded-2xl p-3 shadow-xl backdrop-blur-md"
className="border border-ui-panel-border bg-ui-controls pointer-events-auto absolute bottom-full z-50 mb-2 w-[180px] -translate-x-1/2 select-none rounded-none p-3"
style={{ left: `${Math.max(12, Math.min(88, leftPercent))}%` }}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
@@ -95,7 +95,7 @@ export const MotionPopover = ({
type="button"
title={p.label}
onClick={() => commit({ ...preset })}
className={`flex h-8 items-center justify-center rounded-md border transition-colors ${
className={`flex h-8 items-center justify-center rounded-[3px] border transition-colors ${
active
? "border-brand-primary bg-brand-primary/20"
: "border-ui-controls-border/60 hover:bg-ui-controls/40"
@@ -118,7 +118,7 @@ export const MotionPopover = ({
<svg
ref={svgRef}
viewBox={`0 0 ${BOX} ${BOX}`}
className="w-full touch-none rounded-lg bg-black/20"
className="w-full touch-none rounded-[3px] bg-white/5"
onPointerMove={onPointerMove}
onPointerUp={() => setDragging(null)}
onPointerLeave={() => setDragging(null)}
@@ -25,22 +25,24 @@ export const TimelineBar = ({ readOnly = false }: { readOnly?: boolean }) => {
return (
<div
className="glass glass-no-hover flex w-full select-none items-center gap-3 rounded-2xl px-4 py-2 text-white shadow-xl"
className="border border-ui-panel-border bg-ui-controls flex w-full select-none items-center gap-3 rounded-none px-4 py-2 text-white"
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<button
type="button"
onClick={togglePlay}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/80 hover:bg-white/10"
aria-label={isPlaying ? "Pause timeline" : "Play timeline"}
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/80 hover:bg-white/10"
>
<DynamicIcon icon={isPlaying ? PauseIcon : PlayIcon} className="h-3.5 w-3.5" />
</button>
<span className="w-9 shrink-0 tabular-nums text-xs text-base-fg/70">
<span className="w-9 shrink-0 font-mono tabular-nums text-xs text-base-fg/70">
{formatTimecode(playhead)}
</span>
<input
type="range"
aria-label="Timeline playhead"
min={0}
max={duration}
step={0.01}
@@ -53,7 +55,7 @@ export const TimelineBar = ({ readOnly = false }: { readOnly?: boolean }) => {
className="h-1 flex-1 cursor-pointer accent-white"
/>
{readOnly ? (
<span className="w-9 shrink-0 text-right tabular-nums text-xs text-base-fg/70">
<span className="w-9 shrink-0 text-right font-mono tabular-nums text-xs text-base-fg/70">
{formatTimecode(duration)}
</span>
) : (
@@ -64,7 +66,7 @@ export const TimelineBar = ({ readOnly = false }: { readOnly?: boolean }) => {
type="button"
title="Expand timeline"
onClick={() => setExpanded(true)}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/60 hover:bg-white/10"
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/60 hover:bg-white/10"
>
<ChevronUpIcon className="h-3 w-3" />
</button>
@@ -232,7 +232,7 @@ export const TimelineClipRow = ({
/* overflow-hidden: strips are engine-clamped into the lane, but any
transient out-of-range placement must clip here rather than paint
over the add-button column. */
className="relative h-6 flex-1 overflow-hidden rounded-md bg-black/20"
className="relative h-6 flex-1 overflow-hidden rounded-md bg-white/5"
onPointerMove={onPointerMove}
onPointerUp={endDrag}
onPointerLeave={endDrag}
@@ -370,7 +370,7 @@ export const TimelineClipRow = ({
className={`absolute top-1/2 z-10 flex h-4 w-4 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border transition-colors ${
active
? "border-brand-primary bg-brand-primary text-white"
: "border-dashed border-white/30 bg-black/30 text-white/40 hover:border-white/60 hover:text-white/80"
: "border-dashed border-white/30 bg-white/5 text-white/40 hover:border-white/60 hover:text-white/80"
}`}
style={{ left: `${midPct}%` }}
onPointerDown={(e) => e.stopPropagation()}
@@ -317,24 +317,25 @@ export const TimelineEditor = () => {
return (
<div
id="timeline-editor"
className="glass glass-no-hover absolute bottom-4 left-1/2 w-[90vw] max-w-5xl -translate-x-1/2 select-none rounded-2xl p-3 text-white shadow-xl"
className="border border-ui-panel-border bg-ui-controls absolute bottom-4 left-1/2 w-[90vw] max-w-5xl -translate-x-1/2 select-none rounded-none p-3 text-white"
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
{/* transport + collapse */}
<div className="mb-2 flex items-center gap-2">
<div className="mb-2 flex items-center gap-2 border-b border-ui-panel-border pb-2">
<button
type="button"
title="Go to start"
onClick={() => editor && seekTimeline(editor, 0)}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/70 hover:bg-white/10"
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/70 hover:bg-white/10"
>
<SkipBackIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
onClick={togglePlay}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/90 hover:bg-white/10"
aria-label={isPlaying ? "Pause timeline" : "Play timeline"}
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/90 hover:bg-white/10"
>
<DynamicIcon
icon={isPlaying ? PauseIcon : PlayIcon}
@@ -345,11 +346,11 @@ export const TimelineEditor = () => {
type="button"
title="Go to end"
onClick={() => editor && seekTimeline(editor, duration)}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/70 hover:bg-white/10"
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/70 hover:bg-white/10"
>
<SkipForwardIcon className="h-3.5 w-3.5" />
</button>
<span className="ml-2 tabular-nums text-xs text-base-fg/60">
<span className="ml-2 font-mono tabular-nums text-xs text-base-fg/60">
{formatTimecodeFrames(playhead, fps)} / {formatTimecode(duration)}
</span>
{/* ml-auto lives on a wrapper: Tooltip's className styles the popup
@@ -368,7 +369,7 @@ export const TimelineEditor = () => {
<button
type="button"
onClick={() => setFocusSelected(!focusSelected)}
className={`flex h-7 w-7 items-center justify-center rounded-full transition-colors ${
className={`flex h-7 w-7 items-center justify-center rounded-[3px] transition-colors ${
focusSelected
? "bg-primary/20 text-white"
: "text-base-fg/60 hover:bg-white/10"
@@ -382,7 +383,7 @@ export const TimelineEditor = () => {
type="button"
title="Collapse timeline"
onClick={() => setExpanded(false)}
className="flex h-7 w-7 items-center justify-center rounded-full text-base-fg/60 hover:bg-white/10"
className="flex h-7 w-7 items-center justify-center rounded-[3px] text-base-fg/60 hover:bg-white/10"
>
<ChevronDownIcon className="h-3 w-3" />
</button>
@@ -552,7 +553,7 @@ export const TimelineEditor = () => {
footer={
<button
type="button"
className="mt-2 w-full rounded-md border border-ui-controls-border/60 px-2 py-1.5 text-[11px] text-base-fg/80 transition-colors hover:bg-ui-controls/40"
className="mt-2 w-full rounded-[3px] border border-ui-controls-border/60 px-2 py-1.5 text-[11px] text-base-fg/80 transition-colors hover:bg-ui-controls/40"
onClick={() => {
if (editor) {
setClipTransitionEasing(
@@ -90,7 +90,7 @@ export const TimelineTrackRow = ({
<div
ref={laneRef}
className="relative h-9 flex-1 rounded-lg bg-black/30"
className="relative h-9 flex-1 rounded-lg bg-white/5"
onPointerMove={(e) => {
if (!draggingId.current || !editor) return;
moveKeyframe(
@@ -129,7 +129,7 @@ export const TimelineTrackRow = ({
type="button"
title="Edit easing"
data-easing-chip
className={`absolute top-1/2 flex h-5 w-6 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-md border bg-black/60 ${
className={`absolute top-1/2 flex h-5 w-6 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-md border bg-ui-controls ${
active
? "border-brand-primary text-white"
: "border-white/40 text-white/80 hover:border-white/80 hover:text-white"
@@ -0,0 +1,2 @@
export const TOOLBAR_BUTTON_CLASS_NAME =
"h-9 rounded-[3px] border border-ui-panel-border bg-ui-controls px-3 py-0 font-mono text-xs font-semibold uppercase tracking-[0.12em] leading-none shadow-none [&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:shrink-0";
@@ -14,13 +14,12 @@ import {
PopoverPanel,
} from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import { Button } from "@storyteller/ui-button";
import { Button, NEUTRAL_BUTTON_HOVER_CLASSES } from "@storyteller/ui-button";
import {
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
ChevronUpIcon,
CircleCheckIcon,
InfoIcon,
} from "lucide-react";
import { Model, ModelInfo } from "@storyteller/model-list";
@@ -143,7 +142,7 @@ function PortalTooltip({
zIndex: 9999,
}}
className={twMerge(
"pointer-events-auto rounded-[3px] bg-ui-panel p-3 border border-ui-panel-border text-base-fg",
"pointer-events-auto rounded-[3px] bg-ui-controls p-3 border border-ui-panel-border text-base-fg",
className,
)}
>
@@ -260,7 +259,7 @@ function InfoHint({ content }: { content: ReactNode }) {
transform: "translate(-50%, -100%)",
zIndex: 10000,
}}
className="pointer-events-auto rounded-[3px] border border-ui-panel-border bg-ui-panel px-3 py-1.5 text-center text-xs leading-relaxed text-base-fg"
className="pointer-events-auto rounded-[3px] border border-ui-panel-border bg-ui-controls px-3 py-1.5 text-center text-xs leading-relaxed text-base-fg"
onClick={(e) => e.stopPropagation()}
onMouseEnter={() => {
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
@@ -357,8 +356,8 @@ function RichListRow({
}}
className={twMerge(
"group flex cursor-pointer items-center gap-3 rounded-[3px] px-2 py-2 transition-colors",
item.selected ? "bg-ui-controls/70" : "hover:bg-ui-controls/50",
!item.selected && active ? "bg-ui-controls/50" : "",
item.selected ? "bg-white/10" : "hover:bg-white/5",
!item.selected && active ? "bg-white/5" : "",
item.disabled ? "!cursor-not-allowed opacity-50" : "",
)}
>
@@ -367,7 +366,7 @@ function RichListRow({
"flex h-9 w-9 shrink-0 items-center justify-center rounded-none border text-base-fg/80 transition-colors",
item.selected
? "border-white bg-white/10"
: "border-ui-controls-border bg-ui-controls/60",
: "border-ui-controls-border bg-white/5",
)}
>
{item.icon}
@@ -411,8 +410,8 @@ function RichListRow({
{rightNode ??
(item.selected &&
(item.selectedRight ?? (
<span className="ml-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-[3px] bg-white">
<CheckIcon className="text-[11px] font-bold text-black" />
<span className="ml-1 flex h-5 w-5 shrink-0 items-center justify-center">
<CheckIcon aria-hidden="true" className="h-4 w-4 text-white" strokeWidth={2.5} />
</span>
)))}
</div>
@@ -634,7 +633,7 @@ function SubmenuFlyout({
transform: "translateY(-50%)",
zIndex: 9999,
}}
className="pointer-events-auto w-[340px] rounded-[3px] border border-ui-panel-border bg-ui-panel p-1.5 text-base-fg"
className="pointer-events-auto w-[340px] rounded-[3px] border border-ui-panel-border bg-ui-controls p-1.5 text-base-fg"
>
<div className="mb-1 mt-0.5 px-1.5 text-sm font-normal text-base-fg opacity-70">
{item.label}
@@ -644,7 +643,7 @@ function SubmenuFlyout({
slow bouncing arrow hinting to scroll up. */}
<div
className={twMerge(
"pointer-events-none absolute inset-x-0 top-0 z-20 flex h-10 items-start justify-center bg-gradient-to-b from-ui-panel to-transparent pt-1 transition-opacity duration-200",
"pointer-events-none absolute inset-x-0 top-0 z-20 flex h-10 items-start justify-center bg-gradient-to-b from-ui-controls to-transparent pt-1 transition-opacity duration-200",
canScrollUp ? "opacity-100" : "opacity-0",
)}
>
@@ -694,7 +693,7 @@ function SubmenuFlyout({
slow bouncing arrow hinting to scroll down. */}
<div
className={twMerge(
"pointer-events-none absolute inset-x-0 bottom-0 z-20 flex h-10 items-end justify-center bg-gradient-to-t from-ui-panel to-transparent pb-1 transition-opacity duration-200",
"pointer-events-none absolute inset-x-0 bottom-0 z-20 flex h-10 items-end justify-center bg-gradient-to-t from-ui-controls to-transparent pb-1 transition-opacity duration-200",
canScrollDown ? "opacity-100" : "opacity-0",
)}
>
@@ -852,9 +851,10 @@ export const PopoverMenu = ({
const className = twMerge(
"text-sm font-medium rounded-[3px] px-2.5 py-1.5",
"flex gap-2 items-center justify-center outline-none",
"flex gap-2 items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-white/60",
"transition-colors duration-150",
"bg-ui-controls px-3 text-base-fg hover:bg-ui-controls/80 border border-ui-controls-border",
"bg-ui-controls px-3 text-base-fg border border-ui-controls-border",
NEUTRAL_BUTTON_HOVER_CLASSES,
buttonClassName,
);
@@ -1090,7 +1090,7 @@ export const PopoverMenu = ({
<div
ref={panelContentRef}
className={twMerge(
"z-10 min-w-48 mt-2 rounded-[3px] bg-ui-panel p-1.5 border border-ui-panel-border overflow-visible",
"z-10 min-w-48 mt-2 rounded-[3px] bg-ui-controls p-1.5 border border-ui-panel-border overflow-visible",
position === "top" ? "mb-2" : "mt-2",
panelClassName,
)}
@@ -1098,7 +1098,7 @@ export const PopoverMenu = ({
onMouseLeave={() => handlePanelMouseLeave(close)}
>
{panelTitle && (
<div className="mb-2 mt-0.5 flex justify-between px-1.5 text-sm font-normal text-base-fg opacity-70">
<div className="mb-1 flex items-center justify-between border-b border-ui-panel-border px-2 pb-2 pt-1 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
{panelTitle}
{panelActionLabel && (
<button
@@ -1125,7 +1125,7 @@ export const PopoverMenu = ({
with a slow bouncing arrow hinting to scroll up. */}
<div
className={twMerge(
"pointer-events-none absolute inset-x-0 top-0 z-20 flex h-10 items-start justify-center bg-gradient-to-b from-ui-panel to-transparent pt-1 transition-opacity duration-200",
"pointer-events-none absolute inset-x-0 top-0 z-20 flex h-10 items-start justify-center bg-gradient-to-b from-ui-controls to-transparent pt-1 transition-opacity duration-200",
canScrollUp ? "opacity-100" : "opacity-0",
)}
>
@@ -1222,7 +1222,7 @@ export const PopoverMenu = ({
with a slow bouncing arrow hinting to scroll down. */}
<div
className={twMerge(
"pointer-events-none absolute inset-x-0 bottom-0 z-20 flex h-10 items-end justify-center bg-gradient-to-t from-ui-panel to-transparent pb-1 transition-opacity duration-200",
"pointer-events-none absolute inset-x-0 bottom-0 z-20 flex h-10 items-end justify-center bg-gradient-to-t from-ui-controls to-transparent pb-1 transition-opacity duration-200",
canScrollDown ? "opacity-100" : "opacity-0",
)}
>
@@ -1232,7 +1232,7 @@ export const PopoverMenu = ({
) : mode === "hoverSelect" ? (
<div className="relative flex flex-col text-sm text-base-fg overflow-visible">
{maxListHeight && canScrollUp && (
<div className="absolute top-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-b from-ui-panel via-ui-panel/80 to-transparent py-1.5 pointer-events-none">
<div className="absolute top-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-b from-ui-controls via-ui-controls/80 to-transparent py-1.5 pointer-events-none">
<ChevronUpIcon className="text-base-fg/60 text-xs animate-bounce" />
</div>
)}
@@ -1274,9 +1274,9 @@ export const PopoverMenu = ({
"group flex cursor-pointer items-start gap-2 rounded-[3px] px-2 py-2 transition-all",
item.selected
? "bg-white/10 border-l-2 border-white"
: "hover:bg-ui-controls/50",
: "hover:bg-white/5",
!item.selected && openTooltipIdx === index
? "bg-ui-controls/50"
? "bg-white/5"
: "",
item.disabled
? "!cursor-not-allowed opacity-50"
@@ -1333,7 +1333,7 @@ export const PopoverMenu = ({
{item.selected &&
(item.selectedRight ?? (
<span className="text-white text-xl flex items-center justify-center mr-1">
<CircleCheckIcon />
<CheckIcon aria-hidden="true" className="h-4 w-4 shrink-0" strokeWidth={2.5} />
</span>
))}
</div>
@@ -1366,7 +1366,7 @@ export const PopoverMenu = ({
position="right"
delay={item.tooltipDelayMs ?? 300}
interactive
className="!pointer-events-auto z-50 min-w-48 rounded-[3px] bg-ui-panel p-1.5 border border-ui-panel-border"
className="!pointer-events-auto z-50 min-w-48 rounded-[3px] bg-ui-controls p-1.5 border border-ui-panel-border"
onOpenChange={(open) =>
setOpenTooltipIdx((prev) =>
open
@@ -1391,7 +1391,7 @@ export const PopoverMenu = ({
})}
</div>
{maxListHeight && canScrollDown && (
<div className="absolute bottom-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-t from-ui-panel via-ui-panel/80 to-transparent py-1.5 pointer-events-none">
<div className="absolute bottom-0 left-0 right-0 z-20 flex justify-center bg-gradient-to-t from-ui-controls via-ui-controls/80 to-transparent py-1.5 pointer-events-none">
<ChevronDownIcon className="text-base-fg/60 text-xs animate-bounce" />
</div>
)}
@@ -1429,7 +1429,7 @@ export const PopoverMenu = ({
position="right"
delay={item.tooltipDelayMs ?? 1000}
interactive
className="!pointer-events-auto z-50 min-w-48 rounded-[3px] bg-ui-panel p-1.5 border border-ui-panel-border"
className="!pointer-events-auto z-50 min-w-48 rounded-[3px] bg-ui-controls p-1.5 border border-ui-panel-border"
onOpenChange={(open) =>
setOpenTooltipIdx((prev) =>
open ? index : prev === index ? null : prev,
@@ -1440,9 +1440,9 @@ export const PopoverMenu = ({
className={twMerge(
"flex w-full items-center shadow-none justify-between px-1.5",
"font-sans text-sm font-medium normal-case tracking-normal",
"bg-transparent hover:bg-ui-controls/60",
"bg-transparent hover:bg-white/5",
openTooltipIdx === index
? "bg-ui-controls/60"
? "bg-white/5"
: "",
mode === "toggle" && item.selected
? "hover:bg-ui-controls/80"
@@ -1493,15 +1493,10 @@ export const PopoverMenu = ({
{mode === "toggle" && (
<span
className={twMerge(
"ml-2 h-5 w-5 rounded-[3px] border flex items-center justify-center transition-colors",
item.selected
? "border-white bg-white"
: "border-transparent bg-transparent",
)}
className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center"
>
{item.selected && (
<CheckIcon className="text-black text-xs font-bold" />
<CheckIcon aria-hidden="true" className="h-4 w-4 text-white" strokeWidth={2.5} />
)}
</span>
)}
@@ -1512,7 +1507,7 @@ export const PopoverMenu = ({
className={twMerge(
"flex w-full items-center shadow-none justify-between px-1.5",
"font-sans text-sm font-medium normal-case tracking-normal",
"bg-transparent hover:bg-ui-controls/60",
"bg-transparent hover:bg-white/5",
mode === "toggle" && item.selected
? "hover:bg-ui-controls/80"
: "",
@@ -1555,15 +1550,10 @@ export const PopoverMenu = ({
{mode === "toggle" && (
<span
className={twMerge(
"ml-2 h-5 w-5 rounded-[3px] border flex items-center justify-center transition-colors",
item.selected
? "border-white bg-white"
: "border-transparent bg-transparent",
)}
className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center"
>
{item.selected && (
<CheckIcon className="text-black text-xs font-bold" />
<CheckIcon aria-hidden="true" className="h-4 w-4 text-white" strokeWidth={2.5} />
)}
</span>
)}
@@ -1,10 +1,16 @@
.pricing-hud {
font-family: ui-monospace, monospace;
font-size: 10px;
font-family: var(--font-geist-mono, "Geist Mono"), ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.6875rem;
font-weight: 500;
line-height: 1.5;
text-transform: uppercase;
letter-spacing: 0.15em;
}
.pricing-sale-badge {
font-size: 10px;
letter-spacing: 0.1em;
}
.website-plan-card {
--plan: #555;
--plan-ink: #ddd;
@@ -32,7 +32,7 @@ const PLAN_STYLE: Record<
},
};
const PLAN_BUTTON_CLASSES =
"h-11 w-full rounded-none bg-[var(--plan)] hover:bg-[var(--plan)] text-white hover:opacity-90";
"h-11 w-full text-[11px] rounded-none bg-[var(--plan)] hover:bg-[var(--plan)] text-white hover:opacity-90";
const CONTACT_EMAIL = "hello@storyteller.ai";
const ENTERPRISE_FEATURES = [
"Bespoke credit allocation",
@@ -89,9 +89,9 @@ export function WebsitePricingTable({
className="w-fit"
listClassName="rounded-none"
indicatorClassName="rounded-none"
tabClassName="w-24"
tabClassName="w-24 font-mono text-[11px] uppercase tracking-[0.1em]"
/>
<span className="pricing-hud bg-primary px-2 py-1 text-white">
<span className="pricing-hud pricing-sale-badge bg-primary px-2 py-1 text-white">
{PROMO_PCT}% off · ends soon
</span>
<p className="pricing-hud hidden text-primary sm:block">
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Modal } from "@storyteller/ui-modal";
import { Button } from "@storyteller/ui-button";
import { Button, NEUTRAL_BUTTON_HOVER_CLASSES } from "@storyteller/ui-button";
import { EraserIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
@@ -16,7 +16,7 @@ import { twMerge } from "tailwind-merge";
// `buttonClassName` to PopoverMenu triggers (e.g. the generation count
// picker) so they match this button.
export const PROMPT_TOOLBAR_ICON_BUTTON_CLASSES =
"flex h-9 w-9 items-center justify-center rounded-[3px] border-0 bg-transparent px-0 text-base-fg/50 shadow-none transition-colors hover:bg-base-fg/10 hover:text-base-fg/90";
`flex h-9 w-9 items-center justify-center rounded-[3px] border border-transparent bg-transparent px-0 text-base-fg/50 shadow-none transition-colors ${NEUTRAL_BUTTON_HOVER_CLASSES} hover:text-base-fg/90`;
interface PromptClearAllButtonProps {
onClick: () => void;