Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a43d929ec | ||
|
|
437bbadd0f | ||
|
|
d181ce5089 | ||
|
|
dab29e1cc0 | ||
|
|
3e6a02c29c | ||
|
|
16b87f3e07 | ||
|
|
fa56bb1c9c |
No files matched your search
+11
-11
@@ -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]
|
||||
|
||||
+11
-11
@@ -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]
|
||||
|
||||
+10
-8
@@ -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.
|
||||
///
|
||||
|
||||
+10
-10
@@ -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
|
||||
|
||||
+62
-3
@@ -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::*;
|
||||
|
||||
|
||||
+66
-2
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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);
|
||||
|
||||
+356
-356
@@ -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">
|
||||
© {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>
|
||||
);
|
||||
}
|
||||
+7
-24
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
+3
-1
@@ -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">
|
||||
© {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">
|
||||
© {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">
|
||||
© {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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-1
@@ -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" }}
|
||||
|
||||
+12
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-5
@@ -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 & continue
|
||||
|
||||
+6
-6
@@ -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
|
||||
|
||||
+10
-6
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user