Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b369cfae3 | ||
|
|
d11a9dc65d | ||
|
|
26f1e29c23 | ||
|
|
af55f7eb6f | ||
|
|
505c45ffdb |
No files matched your search
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nINSERT INTO media_files\nSET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n maybe_project_type = ?,\n\n origin_category = ?,\n origin_product_category = ?,\n\n maybe_engine_category = ?,\n maybe_animation_type = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n\n checksum_sha2 = ?,\n\n maybe_prompt_token = ?,\n maybe_batch_token = ?,\n\n maybe_title = ?,\n maybe_duration_millis = ?,\n maybe_frame_width = ?,\n maybe_frame_height = ?,\n\n public_bucket_directory_hash = ?,\n maybe_public_bucket_prefix = ?,\n maybe_public_bucket_extension = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n\n creator_set_visibility = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n is_intermediate_system_file = ?,\n is_user_upload = ?,\n\n maybe_generation_provider = ?,\n\n maybe_creator_file_synthetic_id = ?,\n maybe_creator_category_synthetic_id = ?,\n\n maybe_origin_model_type = NULL,\n maybe_origin_model_token = NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 30
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "026a3ab39a1ba09a891cebd840ab1c956e1bcedd07006ee34473bd4f1d0ff67a"
|
||||
}
|
||||
Generated
-4
@@ -1348,7 +1348,6 @@ dependencies = [
|
||||
"hostname 0.4.0",
|
||||
"log",
|
||||
"server_environment",
|
||||
"tempfile",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -3240,7 +3239,6 @@ dependencies = [
|
||||
"ffprobe",
|
||||
"log",
|
||||
"tempdir",
|
||||
"tempfile",
|
||||
"test_utils",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
@@ -7921,12 +7919,10 @@ dependencies = [
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
"ffmpeg_utils",
|
||||
"hashing",
|
||||
"jobs_common",
|
||||
"kinovi_web_client",
|
||||
"log",
|
||||
"mimetypes",
|
||||
"mysql_queries",
|
||||
"pager",
|
||||
"rand 0.9.2",
|
||||
|
||||
-6
@@ -6,7 +6,6 @@ use enums::common::generation::common_bitrate::CommonBitrate;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
@@ -80,11 +79,6 @@ pub struct OmniApiVideoGenerateRequest {
|
||||
/// Not all models support this; models that don't simply ignore it.
|
||||
pub bitrate: Option<CommonBitrate>,
|
||||
|
||||
/// Output container for Seedance 2.5 (including Ultra). Other models ignore it.
|
||||
/// Use "mp4" or "mov" (QuickTime). Omitted or null defaults to MP4.
|
||||
#[serde(rename = "output_format", skip_serializing_if = "Option::is_none")]
|
||||
pub maybe_output_format: Option<CommonVideoOutputFormat>,
|
||||
|
||||
/// The quality to use.
|
||||
pub quality: Option<CommonQuality>,
|
||||
|
||||
|
||||
-6
@@ -6,7 +6,6 @@ use enums::common::generation::common_bitrate::CommonBitrate;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
@@ -56,11 +55,6 @@ pub struct OmniGenVideoCostAndGenerateRequest {
|
||||
/// Not all models support this; models that don't simply ignore it.
|
||||
pub bitrate: Option<CommonBitrate>,
|
||||
|
||||
/// Output container for Seedance 2.5 (including Ultra). Other models ignore it.
|
||||
/// Use "mp4" or "mov" (QuickTime). Omitted or null defaults to MP4.
|
||||
#[serde(rename = "output_format", skip_serializing_if = "Option::is_none")]
|
||||
pub maybe_output_format: Option<CommonVideoOutputFormat>,
|
||||
|
||||
/// The quality to use.
|
||||
pub quality: Option<CommonQuality>,
|
||||
|
||||
|
||||
-9
@@ -2,7 +2,6 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
@@ -157,12 +156,6 @@ pub struct OmniGenVideoModelDetails {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bitrate_default: Option<CommonBitrate>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format_options: Option<Vec<CommonVideoOutputFormat>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format_default: Option<CommonVideoOutputFormat>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality_options: Option<Vec<CommonQuality>>,
|
||||
@@ -236,8 +229,6 @@ impl Default for OmniGenVideoModelDetails {
|
||||
resolution_default: None,
|
||||
bitrate_options: None,
|
||||
bitrate_default: None,
|
||||
output_format_options: None,
|
||||
output_format_default: None,
|
||||
quality_options: None,
|
||||
default_quality: None,
|
||||
duration_seconds_min: None,
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ pub async fn upload_media_file_from_file<P: AsRef<Path>>(
|
||||
Err(err) => Err(StorytellerError::Api(err)),
|
||||
}
|
||||
}
|
||||
Some(FileExtension::Mp4 | FileExtension::Mov) => {
|
||||
Some(FileExtension::Mp4) => {
|
||||
match upload_video_media_file_from_file(UploadVideoFromFileArgs {
|
||||
api_host,
|
||||
maybe_creds,
|
||||
|
||||
@@ -13,6 +13,5 @@ pub mod router_quality;
|
||||
pub mod router_resolution;
|
||||
pub mod router_splat_model;
|
||||
pub mod router_video_model;
|
||||
pub mod router_video_output_format;
|
||||
pub mod video_list_ref;
|
||||
pub mod video_ref;
|
||||
@@ -1,10 +0,0 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Output video container. Supported by Seedance 2.5 on Kinovi and Artcraft.
|
||||
/// Other models/providers ignore this option.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RouterVideoOutputFormat {
|
||||
Mp4,
|
||||
Mov,
|
||||
}
|
||||
-6
@@ -4,7 +4,6 @@ use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_bitrate::RouterBitrate;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::router_video_output_format::RouterVideoOutputFormat;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
@@ -126,10 +125,6 @@ pub struct GenerateVideoRequestBuilder {
|
||||
/// Not all models support this; models that don't simply ignore it.
|
||||
pub bitrate: Option<RouterBitrate>,
|
||||
|
||||
/// Output container for Seedance 2.5 on Kinovi and Artcraft.
|
||||
/// None keeps the provider default; unsupported models/providers ignore it.
|
||||
pub maybe_output_format: Option<RouterVideoOutputFormat>,
|
||||
|
||||
/// How many seconds to generate.
|
||||
pub duration_seconds: Option<u16>,
|
||||
|
||||
@@ -173,7 +168,6 @@ impl Default for GenerateVideoRequestBuilder {
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
|
||||
-75
@@ -3,12 +3,10 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonA
|
||||
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
|
||||
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_bitrate::RouterBitrate;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_output_format::RouterVideoOutputFormat;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
@@ -57,13 +55,6 @@ pub fn build_artcraft_omni_video_request(
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let bitrate = plan_bitrate(builder.bitrate.take());
|
||||
let prompt = builder.prompt.take();
|
||||
let supports_output_format = matches!(model, CommonVideoModelEnum::Seedance2p5 | CommonVideoModelEnum::Seedance2p5Ultra);
|
||||
let maybe_output_format = builder.maybe_output_format
|
||||
.filter(|_| supports_output_format)
|
||||
.map(|format| match format {
|
||||
RouterVideoOutputFormat::Mp4 => CommonVideoOutputFormat::Mp4,
|
||||
RouterVideoOutputFormat::Mov => CommonVideoOutputFormat::Mov,
|
||||
});
|
||||
|
||||
let start_frame = resolve_image_ref(builder.start_frame.take())?;
|
||||
let end_frame = resolve_image_ref(builder.end_frame.take())?;
|
||||
@@ -86,7 +77,6 @@ pub fn build_artcraft_omni_video_request(
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
bitrate,
|
||||
maybe_output_format,
|
||||
duration_seconds: duration_seconds.map(|d| d as u16),
|
||||
video_batch_count: Some(batch_count),
|
||||
negative_prompt: None,
|
||||
@@ -327,71 +317,6 @@ pub fn plan_duration(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod output_format {
|
||||
use super::*;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
#[test]
|
||||
fn seedance_2p5_preserves_output_format_and_audio_over_http() {
|
||||
for model in [RouterVideoModel::Seedance2p5, RouterVideoModel::Seedance2p5Ultra] {
|
||||
for maybe_output_format in [None, Some(RouterVideoOutputFormat::Mp4), Some(RouterVideoOutputFormat::Mov)] {
|
||||
for generate_audio in [None, Some(true), Some(false)] {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
provider: RouterProvider::Artcraft,
|
||||
model,
|
||||
maybe_output_format,
|
||||
generate_audio,
|
||||
..Default::default()
|
||||
};
|
||||
let request = match builder.build2().unwrap() {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5(state)) => state.request,
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Ultra(state)) => state.request,
|
||||
other => panic!("Unexpected request: {:?}", other),
|
||||
};
|
||||
let json = serde_json::to_value(&request).unwrap();
|
||||
let expected = match maybe_output_format {
|
||||
None => None,
|
||||
Some(RouterVideoOutputFormat::Mp4) => Some("mp4"),
|
||||
Some(RouterVideoOutputFormat::Mov) => Some("mov"),
|
||||
};
|
||||
assert_eq!(json.get("output_format").and_then(|v| v.as_str()), expected);
|
||||
if expected.is_none() {
|
||||
assert!(json.get("output_format").is_none());
|
||||
}
|
||||
let decoded: OmniGenVideoCostAndGenerateRequest = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(decoded.maybe_output_format, request.maybe_output_format);
|
||||
assert_eq!(decoded.generate_audio, generate_audio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_models_ignore_output_format_even_with_error_strategy() {
|
||||
for model in [RouterVideoModel::Seedance2p0, RouterVideoModel::Seedance2p0Fast, RouterVideoModel::Seedance2p5Preview] {
|
||||
for format in [RouterVideoOutputFormat::Mp4, RouterVideoOutputFormat::Mov] {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
provider: RouterProvider::Artcraft,
|
||||
model,
|
||||
maybe_output_format: Some(format),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..Default::default()
|
||||
};
|
||||
let request = match builder.build2().unwrap() {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0(state)) => state.request,
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Fast(state)) => state.request,
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Preview(state)) => state.request,
|
||||
other => panic!("Unexpected request: {:?}", other),
|
||||
};
|
||||
assert!(serde_json::to_value(request).unwrap().get("output_format").is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod bitrate_translation {
|
||||
use super::*;
|
||||
|
||||
|
||||
-1
@@ -59,7 +59,6 @@ mod tests {
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
quality: None,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
|
||||
-2
@@ -49,7 +49,6 @@ pub fn build_artcraft_seedance_2p5(mut builder: GenerateVideoRequestBuilder) ->
|
||||
plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
|
||||
let total_input_seconds = builder.total_reference_video_input_seconds.take();
|
||||
let generate_audio = builder.generate_audio.take();
|
||||
|
||||
let mut request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
@@ -59,7 +58,6 @@ pub fn build_artcraft_seedance_2p5(mut builder: GenerateVideoRequestBuilder) ->
|
||||
)?;
|
||||
request.duration_seconds = duration_seconds;
|
||||
request.video_batch_count = Some(1);
|
||||
request.generate_audio = generate_audio;
|
||||
|
||||
let state = ArtcraftSeedance2p5RequestState { request, total_input_seconds };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5(state)))
|
||||
|
||||
+11
-11
@@ -94,11 +94,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn table_1080p() {
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
// 38.49728574 ¢/s × 40 = 1539.89 → 1540¢.
|
||||
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1540);
|
||||
// 27.39973680 ¢/s × 40 = 1095.99 → 1096¢.
|
||||
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1096);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_1080p_with_ten_input_seconds() {
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
-2
@@ -49,7 +49,6 @@ pub fn build_artcraft_seedance_2p5_u(mut builder: GenerateVideoRequestBuilder) -
|
||||
plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
|
||||
let total_input_seconds = builder.total_reference_video_input_seconds.take();
|
||||
let generate_audio = builder.generate_audio.take();
|
||||
|
||||
let mut request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
@@ -59,7 +58,6 @@ pub fn build_artcraft_seedance_2p5_u(mut builder: GenerateVideoRequestBuilder) -
|
||||
)?;
|
||||
request.duration_seconds = duration_seconds;
|
||||
request.video_batch_count = Some(1);
|
||||
request.generate_audio = generate_audio;
|
||||
|
||||
let state = ArtcraftSeedance2p5UltraRequestState { request, total_input_seconds };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Ultra(state)))
|
||||
|
||||
+11
-11
@@ -95,11 +95,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn table_1080p() {
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
// 41.84487580 ¢/s × 40 = 1673.80 → 1674¢.
|
||||
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1674);
|
||||
// 29.93674947 ¢/s × 40 = 1197.47 → 1198¢.
|
||||
assert_eq!(cents_with_video_refs(Some(RouterResolution::TenEightyP), 30, Some(10)), 1198);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_1080p_with_ten_input_seconds() {
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+8
-10
@@ -137,15 +137,14 @@ 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 (updated 2026-09-17).
|
||||
const SEEDANCE_2P5_CENTS_PER_SECOND_1080P: f64 = 61.62732357;
|
||||
/// Seedance 2.5 — 1080p price, USD cents per second.
|
||||
const SEEDANCE_2P5_CENTS_PER_SECOND_1080P: f64 = 45.85869386;
|
||||
/// 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
|
||||
/// (updated 2026-09-17).
|
||||
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 38.49728574;
|
||||
/// Seedance 2.5 — 1080p price with video references, USD cents per second.
|
||||
const SEEDANCE_2P5_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 27.39973680;
|
||||
|
||||
/// ArtCraft's price (USD cents) for Seedance 2.5.
|
||||
///
|
||||
@@ -200,15 +199,14 @@ 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 (updated 2026-09-17).
|
||||
const SEEDANCE_2P5_ULTRA_CENTS_PER_SECOND_1080P: f64 = 67.22980753;
|
||||
/// 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 — 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 (updated 2026-09-17).
|
||||
const SEEDANCE_2P5_ULTRA_VIDEO_REFERENCE_CENTS_PER_SECOND_1080P: f64 = 41.84487580;
|
||||
/// 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;
|
||||
|
||||
/// ArtCraft's price (USD cents) for Seedance 2.5 Ultra.
|
||||
///
|
||||
|
||||
-1
@@ -47,7 +47,6 @@ fn do_build_kinovi_seedance_2p0(mut builder: GenerateVideoRequestBuilder) -> Res
|
||||
duration_seconds,
|
||||
bitrate,
|
||||
prompt,
|
||||
maybe_generate_audio: builder.generate_audio,
|
||||
unhandled_request_state: Some(unhandled_request_state),
|
||||
})
|
||||
}
|
||||
|
||||
-3
@@ -65,7 +65,6 @@ impl KinoviSeedance2p0CostState {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
};
|
||||
|
||||
// Enterprise tier: what generations actually cost us (our discounted
|
||||
@@ -277,7 +276,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
.calculate_enterprise_costs();
|
||||
(costs.total_cost.kinovi_credits.round() as u64, costs.total_cost.usd_cents_rounded_up)
|
||||
@@ -656,7 +654,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
-3
@@ -28,8 +28,6 @@ pub struct KinoviSeedance2p0DraftState {
|
||||
pub batch_count: KinoviSeedance2p0BatchCount,
|
||||
pub bitrate: Option<KinoviSeedance2p0Bitrate>,
|
||||
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
|
||||
// Pending types that need to be queried.
|
||||
pub unhandled_request_state: Option<KinoviSeedance2p0RemainingItems>,
|
||||
}
|
||||
@@ -98,7 +96,6 @@ impl KinoviSeedance2p0DraftState {
|
||||
character_ids,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: self.bitrate,
|
||||
maybe_generate_audio: self.maybe_generate_audio,
|
||||
};
|
||||
|
||||
Ok(KinoviSeedance2p0RequestState { request })
|
||||
|
||||
-1
@@ -47,7 +47,6 @@ fn do_build_kinovi_seedance_2p0_fast(mut builder: GenerateVideoRequestBuilder) -
|
||||
duration_seconds,
|
||||
bitrate,
|
||||
prompt,
|
||||
maybe_generate_audio: builder.generate_audio,
|
||||
unhandled_request_state: Some(unhandled_request_state),
|
||||
})
|
||||
}
|
||||
|
||||
-2
@@ -65,7 +65,6 @@ impl KinoviSeedance2p0FastCostState {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
};
|
||||
|
||||
// Enterprise tier: what generations actually cost us (our discounted
|
||||
@@ -451,7 +450,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -25,7 +25,6 @@ pub struct KinoviSeedance2p0FastDraftState {
|
||||
pub duration_seconds: u8,
|
||||
pub batch_count: KinoviSeedance2p0FastBatchCount,
|
||||
pub bitrate: Option<KinoviSeedance2p0FastBitrate>,
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
|
||||
pub unhandled_request_state: Option<KinoviSeedance2p0FastRemainingItems>,
|
||||
}
|
||||
@@ -94,7 +93,6 @@ impl KinoviSeedance2p0FastDraftState {
|
||||
character_ids,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: self.bitrate,
|
||||
maybe_generate_audio: self.maybe_generate_audio,
|
||||
};
|
||||
|
||||
Ok(KinoviSeedance2p0FastRequestState { request })
|
||||
|
||||
-1
@@ -47,7 +47,6 @@ fn do_build_kinovi_seedance_2p0_mini(mut builder: GenerateVideoRequestBuilder) -
|
||||
duration_seconds,
|
||||
bitrate,
|
||||
prompt,
|
||||
maybe_generate_audio: builder.generate_audio,
|
||||
unhandled_request_state: Some(unhandled_request_state),
|
||||
})
|
||||
}
|
||||
|
||||
-2
@@ -65,7 +65,6 @@ impl KinoviSeedance2p0MiniCostState {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
};
|
||||
|
||||
// Enterprise tier: what generations actually cost us (our discounted
|
||||
@@ -381,7 +380,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -25,7 +25,6 @@ pub struct KinoviSeedance2p0MiniDraftState {
|
||||
pub duration_seconds: u8,
|
||||
pub batch_count: KinoviSeedance2p0MiniBatchCount,
|
||||
pub bitrate: Option<KinoviSeedance2p0MiniBitrate>,
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
|
||||
pub unhandled_request_state: Option<KinoviSeedance2p0MiniRemainingItems>,
|
||||
}
|
||||
@@ -94,7 +93,6 @@ impl KinoviSeedance2p0MiniDraftState {
|
||||
character_ids,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: self.bitrate,
|
||||
maybe_generate_audio: self.maybe_generate_audio,
|
||||
};
|
||||
|
||||
Ok(KinoviSeedance2p0MiniRequestState { request })
|
||||
|
||||
-44
@@ -1,14 +1,12 @@
|
||||
use kinovi_web_client::generate::video::generate_seedance_2p5::{
|
||||
KinoviSeedance2p5AspectRatio as KinoviAspectRatio,
|
||||
KinoviSeedance2p5Bitrate,
|
||||
KinoviSeedance2p5OutputFormat,
|
||||
KinoviSeedance2p5OutputResolution as KinoviOutputResolution,
|
||||
};
|
||||
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_bitrate::RouterBitrate;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_output_format::RouterVideoOutputFormat;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
@@ -88,11 +86,6 @@ fn do_build_kinovi_seedance_2p5(mut builder: GenerateVideoRequestBuilder) -> Res
|
||||
prompt,
|
||||
total_input_seconds,
|
||||
maybe_bitrate,
|
||||
maybe_output_format: builder.maybe_output_format.map(|format| match format {
|
||||
RouterVideoOutputFormat::Mp4 => KinoviSeedance2p5OutputFormat::Mp4,
|
||||
RouterVideoOutputFormat::Mov => KinoviSeedance2p5OutputFormat::Mov,
|
||||
}),
|
||||
maybe_generate_audio: builder.generate_audio,
|
||||
unhandled_request_state: Some(unhandled_request_state),
|
||||
})
|
||||
}
|
||||
@@ -282,7 +275,6 @@ mod tests {
|
||||
use crate::generate::generate_video::video_generation_draft::VideoGenerationDraftRequest;
|
||||
use crate::generate::generate_video::video_generation_draft_context::VideoGenerationDraftContext;
|
||||
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -319,42 +311,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn output_options_survive_router_finalization() {
|
||||
let client = RouterClient::KinoviWeb(RouterKinoviWebClient::new(
|
||||
KinoviWebSession::from_cookies_string(String::new()),
|
||||
));
|
||||
let context = VideoGenerationDraftContext { client: Some(&client), ..Default::default() };
|
||||
for model in [RouterVideoModel::Seedance2p0, RouterVideoModel::Seedance2p0Fast, RouterVideoModel::Seedance2p0Mini, RouterVideoModel::Seedance2p5] {
|
||||
for generate_audio in [None, Some(false), Some(true)] {
|
||||
for maybe_output_format in [None, Some(RouterVideoOutputFormat::Mp4), Some(RouterVideoOutputFormat::Mov)] {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model, generate_audio, maybe_output_format, ..base_builder()
|
||||
};
|
||||
let VideoGenerationDraftOrRequest::Draft(draft) = builder.build2().unwrap() else {
|
||||
panic!("expected Kinovi draft");
|
||||
};
|
||||
let finalized = draft.finalize(context.clone()).await.unwrap();
|
||||
let actual_audio = match finalized {
|
||||
VideoGenerationRequest::KinoviSeedance2p0(state) => state.request.maybe_generate_audio,
|
||||
VideoGenerationRequest::KinoviSeedance2p0Fast(state) => state.request.maybe_generate_audio,
|
||||
VideoGenerationRequest::KinoviSeedance2p0Mini(state) => state.request.maybe_generate_audio,
|
||||
VideoGenerationRequest::KinoviSeedance2p5(state) => {
|
||||
let actual_format = state.request.maybe_output_format.map(|format| match format {
|
||||
KinoviSeedance2p5OutputFormat::Mp4 => RouterVideoOutputFormat::Mp4,
|
||||
KinoviSeedance2p5OutputFormat::Mov => RouterVideoOutputFormat::Mov,
|
||||
});
|
||||
assert_eq!(actual_format, maybe_output_format);
|
||||
state.request.maybe_generate_audio
|
||||
}
|
||||
_ => panic!("expected a Seedance request"),
|
||||
};
|
||||
assert_eq!(actual_audio, generate_audio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod materialized_field_conversions {
|
||||
use super::*;
|
||||
|
||||
|
||||
+10
-12
@@ -92,15 +92,13 @@ impl KinoviSeedance2p5CostState {
|
||||
total_input_seconds,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
};
|
||||
|
||||
// Enterprise tier: what generations actually cost us (our discounted
|
||||
// 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 (136.23/sec) and rounds to the
|
||||
// there; 1080p bills fractional credits (103.25/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 +123,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── Credits without video references (26/sec at 480p, 59/sec at 720p,
|
||||
// 136.23/sec at 1080p enterprise) ──
|
||||
// 103.25/sec at 1080p enterprise) ──
|
||||
|
||||
mod credits_without_video_references {
|
||||
use super::*;
|
||||
@@ -145,11 +143,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn credits_1080p() {
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -164,7 +162,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// ── Credits with video references (16/sec at 480p, 35/sec at 720p,
|
||||
// 81.4/sec at 1080p enterprise, over output duration + input seconds) ──
|
||||
// 61.69/sec at 1080p enterprise, over output duration + input seconds) ──
|
||||
|
||||
mod credits_with_video_references {
|
||||
use super::*;
|
||||
@@ -173,8 +171,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);
|
||||
// 81.4/sec enterprise × 40 = 3256 credits.
|
||||
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, true, Some(10)), 3256);
|
||||
// 61.69/sec enterprise × 40 = 2467.6 → 2468 credits.
|
||||
assert_eq!(credits(Some(KinoviOutputResolution::TenEightyP), 30, true, Some(10)), 2468);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+1
-5
@@ -1,6 +1,6 @@
|
||||
use kinovi_web_client::generate::video::generate_seedance_2p5::{
|
||||
GenerateSeedance2p5Request, KinoviSeedance2p5AspectRatio, KinoviSeedance2p5Bitrate, KinoviSeedance2p5Modality,
|
||||
KinoviSeedance2p5OutputFormat, KinoviSeedance2p5OutputResolution,
|
||||
KinoviSeedance2p5OutputResolution,
|
||||
};
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
@@ -33,8 +33,6 @@ pub struct KinoviSeedance2p5DraftState {
|
||||
pub total_input_seconds: Option<u8>,
|
||||
|
||||
pub maybe_bitrate: Option<KinoviSeedance2p5Bitrate>,
|
||||
pub maybe_output_format: Option<KinoviSeedance2p5OutputFormat>,
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
|
||||
pub unhandled_request_state: Option<KinoviSeedance2p5RemainingItems>,
|
||||
}
|
||||
@@ -116,8 +114,6 @@ impl KinoviSeedance2p5DraftState {
|
||||
total_input_seconds: self.total_input_seconds,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: self.maybe_bitrate,
|
||||
maybe_output_format: self.maybe_output_format,
|
||||
maybe_generate_audio: self.maybe_generate_audio,
|
||||
};
|
||||
|
||||
Ok(KinoviSeedance2p5RequestState { request })
|
||||
|
||||
+3
-23
@@ -19,7 +19,9 @@ pub(crate) async fn upload_to_kinovi_web(
|
||||
source_url: &str,
|
||||
maybe_local_path: Option<&Path>,
|
||||
) -> Result<String, ArtcraftRouterError> {
|
||||
let extension = upload_extension(source_url, maybe_local_path);
|
||||
let extension = extract_extension_from_url_str(source_url, &ExtractExtensions::All)
|
||||
.map(|ext| ext.without_period().to_string())
|
||||
.unwrap_or_else(|| "jpg".to_string());
|
||||
|
||||
let file_bytes = match maybe_local_path {
|
||||
Some(local_path) => match std::fs::read(local_path) {
|
||||
@@ -56,25 +58,3 @@ pub(crate) async fn upload_to_kinovi_web(
|
||||
Ok(upload_response.public_url)
|
||||
}
|
||||
|
||||
/// Keep the reference container when uploading, including QuickTime/MOV.
|
||||
fn upload_extension(source_url: &str, maybe_local_path: Option<&Path>) -> String {
|
||||
extract_extension_from_url_str(source_url, &ExtractExtensions::All)
|
||||
.map(|ext| ext.without_period().to_string())
|
||||
.or_else(|| maybe_local_path
|
||||
.and_then(Path::extension)
|
||||
.and_then(|ext| ext.to_str())
|
||||
.map(str::to_ascii_lowercase))
|
||||
.unwrap_or_else(|| "jpg".to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn quicktime_upload_keeps_mov_extension() {
|
||||
assert_eq!(upload_extension("https://example.com/reference.MOV?token=test", None), "mov");
|
||||
assert_eq!(upload_extension("https://example.com/reference.mov", Some(Path::new("/tmp/cached.bin"))), "mov");
|
||||
assert_eq!(upload_extension("https://example.com/reference", Some(Path::new("/tmp/reference.MOV"))), "mov");
|
||||
}
|
||||
}
|
||||
@@ -352,7 +352,6 @@ pub fn base_video_request() -> GenerateVideoRequestBuilder {
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
|
||||
@@ -130,8 +130,6 @@ pub async fn generate_happy_horse_1p0(
|
||||
character_ids: None,
|
||||
use_face_blur_hack: Some(false),
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
};
|
||||
|
||||
let raw_response = workflow_run_task(WorkflowRunTaskArgs {
|
||||
|
||||
@@ -38,8 +38,6 @@ pub struct GenerateSeedance2p0Request {
|
||||
/// Output video bitrate. None defaults to "standard"; `High` requests a
|
||||
/// higher bitrate. Does not affect cost.
|
||||
pub bitrate: Option<KinoviSeedance2p0Bitrate>,
|
||||
/// Whether to generate audio. None leaves the provider default unchanged.
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
@@ -223,8 +221,6 @@ pub async fn generate_seedance_2p0(
|
||||
character_ids: req.character_ids,
|
||||
use_face_blur_hack: req.use_face_blur_hack,
|
||||
bitrate: map_bitrate(req.bitrate),
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: req.maybe_generate_audio,
|
||||
};
|
||||
|
||||
let raw_response = workflow_run_task(WorkflowRunTaskArgs {
|
||||
@@ -732,7 +728,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -805,7 +800,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("t2v default — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -837,7 +831,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("t2v 1080p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -872,7 +865,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("t2v 21:9 — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -905,7 +897,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("keyframe 21:9 — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -942,7 +933,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("keyframe — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -976,7 +966,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("keyframe start+end — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -1014,7 +1003,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("image ref — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -1050,7 +1038,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("4K image ref — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -1089,7 +1076,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("video ref — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -1125,7 +1111,6 @@ mod tests {
|
||||
character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string()]),
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("character ref — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -1157,7 +1142,6 @@ mod tests {
|
||||
character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string(), MOCHI_ID.to_string()]),
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("two characters — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
|
||||
@@ -38,8 +38,6 @@ pub struct GenerateSeedance2p0FastRequest {
|
||||
/// Output video bitrate. None defaults to "standard"; `High` requests a
|
||||
/// higher bitrate. Does not affect cost.
|
||||
pub bitrate: Option<KinoviSeedance2p0FastBitrate>,
|
||||
/// Whether to generate audio. None leaves the provider default unchanged.
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
@@ -197,8 +195,6 @@ pub async fn generate_seedance_2p0_fast(
|
||||
character_ids: req.character_ids,
|
||||
use_face_blur_hack: req.use_face_blur_hack,
|
||||
bitrate: map_bitrate(req.bitrate),
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: req.maybe_generate_audio,
|
||||
};
|
||||
|
||||
let raw_response = workflow_run_task(WorkflowRunTaskArgs {
|
||||
@@ -652,7 +648,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,7 +712,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("t2v fast default — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -749,7 +743,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("t2v fast 480p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -784,7 +777,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("fast t2v 21:9 480p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -817,7 +809,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("fast keyframe 21:9 — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -854,7 +845,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("fast keyframe — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -892,7 +882,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("fast image ref — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -930,7 +919,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("fast video ref — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -966,7 +954,6 @@ mod tests {
|
||||
character_ids: Some(vec![MOCHI_ID.to_string()]),
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("fast character — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -998,7 +985,6 @@ mod tests {
|
||||
character_ids: Some(vec![STEAMPUNK_CLOWN_ID.to_string(), MOCHI_ID.to_string()]),
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("fast two characters — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
|
||||
@@ -38,8 +38,6 @@ pub struct GenerateSeedance2p0MiniRequest {
|
||||
/// Output video bitrate. None defaults to "normal"; `High` requests a
|
||||
/// higher bitrate. Does not affect cost.
|
||||
pub bitrate: Option<KinoviSeedance2p0MiniBitrate>,
|
||||
/// Whether to generate audio. None leaves the provider default unchanged.
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
// ── Enums ──
|
||||
@@ -224,8 +222,6 @@ fn to_raw_request(req: GenerateSeedance2p0MiniRequest) -> WorkflowRunTaskRequest
|
||||
character_ids: req.character_ids,
|
||||
use_face_blur_hack: req.use_face_blur_hack,
|
||||
bitrate: map_bitrate(req.bitrate),
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: req.maybe_generate_audio,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,7 +642,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,7 +695,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: Some(KinoviSeedance2p0MiniBitrate::High),
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -775,7 +769,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -812,7 +805,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("mini t2v 720p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -844,7 +836,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("mini t2v 480p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -877,7 +868,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("mini reference-to-video 720p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -910,7 +900,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("mini video ref 480p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
|
||||
use crate::requests::kinovi_host::KinoviHost;
|
||||
use crate::requests::workflow_run_task::workflow_run_task::{
|
||||
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, KinoviBitrateRaw, KinoviModelTypeRaw,
|
||||
KinoviOutputFormatRaw, KinoviOutputResolutionRaw, WorkflowRunTaskArgs, WorkflowRunTaskRequest,
|
||||
KinoviOutputResolutionRaw, WorkflowRunTaskArgs, WorkflowRunTaskRequest,
|
||||
};
|
||||
|
||||
// ── Constants ──
|
||||
@@ -64,12 +64,6 @@ pub struct GenerateSeedance2p5Request {
|
||||
/// Output video bitrate. None keeps the standard bitrate; `High` requests
|
||||
/// a higher bitrate. This does not select a container or lossless codec.
|
||||
pub maybe_bitrate: Option<KinoviSeedance2p5Bitrate>,
|
||||
|
||||
/// Output container. None leaves the provider default unchanged.
|
||||
pub maybe_output_format: Option<KinoviSeedance2p5OutputFormat>,
|
||||
|
||||
/// Whether to generate audio. None leaves the provider default unchanged.
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
// ── Modality ──
|
||||
@@ -130,13 +124,6 @@ pub enum KinoviSeedance2p5Bitrate {
|
||||
High,
|
||||
}
|
||||
|
||||
/// Output container for Seedance 2.5.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum KinoviSeedance2p5OutputFormat {
|
||||
Mp4,
|
||||
Mov,
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
//
|
||||
// Seedance 2.5 credit pricing (credits per billed second). Without video
|
||||
@@ -147,7 +134,7 @@ pub enum KinoviSeedance2p5OutputFormat {
|
||||
// |------------|----------------------|------------------------|
|
||||
// | 480p | 26 | (no discount) |
|
||||
// | 720p | 59 | (no discount) |
|
||||
// | 1080p | 143.4 | 136.23 |
|
||||
// | 1080p | 107.55 | 103.25 |
|
||||
//
|
||||
// With video references, the rate drops but the billed seconds are the
|
||||
// output duration PLUS the total seconds of reference video input
|
||||
@@ -160,10 +147,11 @@ pub enum KinoviSeedance2p5OutputFormat {
|
||||
// |------------|----------------------|------------------------|
|
||||
// | 480p | 16 | (no discount) |
|
||||
// | 720p | 35 | (no discount) |
|
||||
// | 1080p | 85.68 | 81.4 |
|
||||
// | 1080p | 64.26 | 61.69 |
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
@@ -179,8 +167,8 @@ const SEEDANCE_2P5_720P: KinoviPricingRate = KinoviPricingRate {
|
||||
maybe_enterprise_credits: None,
|
||||
};
|
||||
const SEEDANCE_2P5_1080P: KinoviPricingRate = KinoviPricingRate {
|
||||
consumer_credits: 143.4,
|
||||
maybe_enterprise_credits: Some(136.23),
|
||||
consumer_credits: 107.55,
|
||||
maybe_enterprise_credits: Some(103.25),
|
||||
};
|
||||
|
||||
/// Per-second credit rates when video references are attached. These REPLACE
|
||||
@@ -195,8 +183,8 @@ const SEEDANCE_2P5_720P_VIDEO_REF: KinoviPricingRate = KinoviPricingRate {
|
||||
maybe_enterprise_credits: None,
|
||||
};
|
||||
const SEEDANCE_2P5_1080P_VIDEO_REF: KinoviPricingRate = KinoviPricingRate {
|
||||
consumer_credits: 85.68,
|
||||
maybe_enterprise_credits: Some(81.4),
|
||||
consumer_credits: 64.26,
|
||||
maybe_enterprise_credits: Some(61.69),
|
||||
};
|
||||
|
||||
impl KinoviCostCalculatorTrait for GenerateSeedance2p5Request {
|
||||
@@ -317,11 +305,6 @@ fn to_raw_request(req: GenerateSeedance2p5Request) -> WorkflowRunTaskRequest {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: req.use_face_blur_hack,
|
||||
bitrate: map_bitrate(req.maybe_bitrate),
|
||||
maybe_output_format: req.maybe_output_format.map(|format| match format {
|
||||
KinoviSeedance2p5OutputFormat::Mp4 => KinoviOutputFormatRaw::Mp4,
|
||||
KinoviSeedance2p5OutputFormat::Mov => KinoviOutputFormatRaw::Mov,
|
||||
}),
|
||||
maybe_generate_audio: req.maybe_generate_audio,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +357,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 = 143.4 consumer / 136.23
|
||||
// negotiated enterprise discount); 1080p = 107.55 consumer / 103.25
|
||||
// enterprise credits/sec.
|
||||
|
||||
mod without_video_references {
|
||||
@@ -400,23 +383,24 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn table_1080p() {
|
||||
// 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);
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -454,7 +438,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, 1362.3);
|
||||
assert_eq!(request_1080.calculate_enterprise_costs().kinovi_credits, 1032.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -487,7 +471,7 @@ mod tests {
|
||||
// ── With video references: input seconds are billed too ──
|
||||
//
|
||||
// 480p = 16 credits/sec and 720p = 35 credits/sec at both tiers; 1080p
|
||||
// = 85.68 consumer / 81.4 enterprise credits/sec. All over
|
||||
// = 64.26 consumer / 61.69 enterprise credits/sec. All over
|
||||
// (output duration + total_input_seconds) billed seconds.
|
||||
|
||||
mod with_video_references {
|
||||
@@ -539,21 +523,21 @@ mod tests {
|
||||
// seconds: billed seconds = 20 + clamp(input, 4, 30). E.g. a 10s
|
||||
// input video + 20s generation = 30 billed seconds.
|
||||
//
|
||||
// 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);
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -572,9 +556,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 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -647,47 +631,47 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn enterprise_cents_1080p_4s() {
|
||||
// 544.92 credits; 54492/243.16 = 224.0994 cents.
|
||||
// 413 credits; 41300/243.16 = 169.8470 cents.
|
||||
let cost = t2v_1080(4).calculate_enterprise_costs();
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumer_cents_1080p_4s() {
|
||||
// 573.6 credits; 57360/192.98 = 297.2329 cents.
|
||||
// 430.2 credits; 43020/192.98 = 222.9247 cents.
|
||||
let cost = t2v_1080(4).calculate_consumer_costs();
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enterprise_cents_1080p_30s() {
|
||||
// 4086.9 credits; 408690/243.16 = 1680.7452 cents.
|
||||
// 3097.5 credits; 309750/243.16 = 1273.8526 cents.
|
||||
let cost = t2v_1080(30).calculate_enterprise_costs();
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enterprise_cents_1080p_video_ref_30_billed_seconds() {
|
||||
// 81.4 × 30 = 2442 credits; 244200/243.16 = 1004.2770 cents.
|
||||
// 61.69 × 30 = 1850.7 credits; 185070/243.16 = 761.1038 cents.
|
||||
let cost = video_ref_1080(20, Some(10)).calculate_enterprise_costs();
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumer_cents_1080p_video_ref_30_billed_seconds() {
|
||||
// 85.68 × 30 = 2570.4 credits; 257040/192.98 = 1331.9515 cents.
|
||||
// 64.26 × 30 = 1927.8 credits; 192780/192.98 = 998.9636 cents.
|
||||
let cost = video_ref_1080(20, Some(10)).calculate_consumer_costs();
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,7 +682,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn video_reference_rate_is_cheaper_per_second() {
|
||||
// 16 < 26, 35 < 59, and 81.4 < 136.23: the with-references rate is
|
||||
// 16 < 26, 35 < 59, and 61.69 < 103.25: 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
|
||||
@@ -781,8 +765,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,8 +792,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,8 +852,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
});
|
||||
|
||||
assert!(matches!(raw.model_type, KinoviModelTypeRaw::Seedance2p5));
|
||||
@@ -900,14 +878,10 @@ mod tests {
|
||||
total_input_seconds: Some(7),
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: Some(KinoviSeedance2p5Bitrate::High),
|
||||
maybe_output_format: Some(KinoviSeedance2p5OutputFormat::Mov),
|
||||
maybe_generate_audio: Some(false),
|
||||
});
|
||||
|
||||
assert!(matches!(raw.aspect_ratio, KinoviAspectRatioRaw::UltraWide21x9));
|
||||
assert!(matches!(raw.bitrate, Some(KinoviBitrateRaw::High)));
|
||||
assert!(matches!(raw.maybe_output_format, Some(KinoviOutputFormatRaw::Mov)));
|
||||
assert_eq!(raw.maybe_generate_audio, Some(false));
|
||||
assert!(raw.start_frame_url.is_none());
|
||||
assert!(raw.end_frame_url.is_none());
|
||||
assert_eq!(raw.reference_image_urls.as_deref().map(|urls| urls.len()), Some(1));
|
||||
@@ -930,8 +904,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
});
|
||||
|
||||
assert!(matches!(raw.output_resolution, Some(KinoviOutputResolutionRaw::TenEightyP)));
|
||||
@@ -952,8 +924,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
});
|
||||
|
||||
assert!(matches!(raw.aspect_ratio, KinoviAspectRatioRaw::Landscape16x9));
|
||||
@@ -984,8 +954,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("t2v 480p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -1018,8 +986,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("t2v 1080p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -1048,8 +1014,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("keyframe 1080p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -1078,8 +1042,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("keyframe 480p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
@@ -1113,8 +1075,6 @@ mod tests {
|
||||
total_input_seconds: None,
|
||||
use_face_blur_hack: None,
|
||||
maybe_bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}).await?;
|
||||
println!("reference 480p — task_id={}, order_id={}", result.task_id, result.order_id);
|
||||
|
||||
-2
@@ -148,8 +148,6 @@ fn to_raw_request(req: GenerateSeedance2p5PreviewRequest) -> WorkflowRunTaskRequ
|
||||
character_ids: None,
|
||||
use_face_blur_hack: req.use_face_blur_hack,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,10 +68,6 @@ pub (super) struct ApiParams {
|
||||
pub batch_count: Option<u8>,
|
||||
#[serde(rename = "bitrate_mode", skip_serializing_if = "Option::is_none")]
|
||||
pub bitrate_mode: Option<&'static str>,
|
||||
#[serde(rename = "output_format", skip_serializing_if = "Option::is_none")]
|
||||
pub maybe_output_format: Option<&'static str>,
|
||||
#[serde(rename = "generate_audio", skip_serializing_if = "Option::is_none")]
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
||||
-128
@@ -74,12 +74,6 @@ pub struct WorkflowRunTaskRequest {
|
||||
/// Output video bitrate. None defaults to "standard" (the field is omitted);
|
||||
/// `High` sends `bitrate_mode: "high"`. Does not affect cost.
|
||||
pub bitrate: Option<KinoviBitrateRaw>,
|
||||
|
||||
/// Output container, supported only by Seedance 2.5. None omits the field.
|
||||
pub maybe_output_format: Option<KinoviOutputFormatRaw>,
|
||||
|
||||
/// Audio generation for Seedance 2.0 and 2.5. None keeps the provider default.
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WorkflowRunTaskRequest {
|
||||
@@ -99,8 +93,6 @@ impl std::fmt::Debug for WorkflowRunTaskRequest {
|
||||
.field("output_resolution", &self.output_resolution)
|
||||
.field("use_face_blur_hack", &self.use_face_blur_hack)
|
||||
.field("bitrate", &self.bitrate)
|
||||
.field("maybe_output_format", &self.maybe_output_format)
|
||||
.field("maybe_generate_audio", &self.maybe_generate_audio)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -116,22 +108,6 @@ impl std::fmt::Debug for WorkflowRunTaskArgs<'_> {
|
||||
|
||||
// --- Public enums ---
|
||||
|
||||
/// Output container for Seedance 2.5.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum KinoviOutputFormatRaw {
|
||||
Mp4,
|
||||
Mov,
|
||||
}
|
||||
|
||||
impl KinoviOutputFormatRaw {
|
||||
fn as_api_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Mp4 => "mp4",
|
||||
Self::Mov => "mov",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Video resolution / aspect ratio.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum KinoviAspectRatioRaw {
|
||||
@@ -613,15 +589,6 @@ fn build_batch_request(req: WorkflowRunTaskRequest) -> BatchRequest {
|
||||
audio_urls,
|
||||
batch_count,
|
||||
bitrate_mode: req.bitrate.and_then(|bitrate| bitrate.as_api_str()),
|
||||
maybe_output_format: match req.model_type {
|
||||
KinoviModelTypeRaw::Seedance2p5 => req.maybe_output_format.map(KinoviOutputFormatRaw::as_api_str),
|
||||
_ => None,
|
||||
},
|
||||
maybe_generate_audio: match req.model_type {
|
||||
KinoviModelTypeRaw::Seedance2Pro | KinoviModelTypeRaw::Seedance2Fast
|
||||
| KinoviModelTypeRaw::Seedance2Mini | KinoviModelTypeRaw::Seedance2p5 => req.maybe_generate_audio,
|
||||
_ => None,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -683,8 +650,6 @@ mod tests {
|
||||
audio_urls: None,
|
||||
batch_count: None,
|
||||
bitrate_mode,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -736,8 +701,6 @@ mod tests {
|
||||
audio_urls: None,
|
||||
batch_count: None,
|
||||
bitrate_mode: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -772,8 +735,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,8 +842,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -988,51 +947,6 @@ mod tests {
|
||||
mod seedance_2p5_shape_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn optional_output_settings_preserve_omission_and_explicit_values() {
|
||||
for (format, expected) in [(None, None), (Some(KinoviOutputFormatRaw::Mp4), Some("mp4")), (Some(KinoviOutputFormatRaw::Mov), Some("mov"))] {
|
||||
for audio in [None, Some(false), Some(true)] {
|
||||
let mut request = base_request("A boat on a lake", 4);
|
||||
request.maybe_output_format = format;
|
||||
request.maybe_generate_audio = audio;
|
||||
let body = serde_json::to_value(build_batch_request(request)).unwrap();
|
||||
let params = &body["0"]["json"]["apiParams"];
|
||||
assert_eq!(params.get("output_format"), expected.map(serde_json::Value::from).as_ref());
|
||||
assert_eq!(params.get("generate_audio"), audio.map(serde_json::Value::from).as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_setting_applies_to_seedance_2p0_without_output_format() {
|
||||
for model in [KinoviModelTypeRaw::Seedance2Pro, KinoviModelTypeRaw::Seedance2Fast, KinoviModelTypeRaw::Seedance2Mini] {
|
||||
for audio in [None, Some(false), Some(true)] {
|
||||
let mut request = base_request("A boat on a lake", 4);
|
||||
request.model_type = model;
|
||||
request.maybe_output_format = Some(KinoviOutputFormatRaw::Mov);
|
||||
request.maybe_generate_audio = audio;
|
||||
let body = serde_json::to_value(build_batch_request(request)).unwrap();
|
||||
let params = &body["0"]["json"]["apiParams"];
|
||||
assert!(params.get("output_format").is_none());
|
||||
assert_eq!(params.get("generate_audio"), audio.map(serde_json::Value::from).as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_models_omit_unsupported_output_settings() {
|
||||
for model in [KinoviModelTypeRaw::HappyHorse1p0, KinoviModelTypeRaw::Seedance2p5Preview] {
|
||||
let mut request = base_request("A boat on a lake", 4);
|
||||
request.model_type = model;
|
||||
request.maybe_output_format = Some(KinoviOutputFormatRaw::Mov);
|
||||
request.maybe_generate_audio = Some(false);
|
||||
let body = serde_json::to_value(build_batch_request(request)).unwrap();
|
||||
let params = &body["0"]["json"]["apiParams"];
|
||||
assert!(params.get("output_format").is_none());
|
||||
assert!(params.get("generate_audio").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
fn base_request(prompt: &str, duration_seconds: u8) -> WorkflowRunTaskRequest {
|
||||
WorkflowRunTaskRequest {
|
||||
model_type: KinoviModelTypeRaw::Seedance2p5,
|
||||
@@ -1049,8 +963,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1203,8 +1115,6 @@ mod tests {
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1295,8 +1205,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1331,8 +1239,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1369,8 +1275,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1406,8 +1310,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1445,8 +1347,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1501,8 +1401,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1556,8 +1454,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1611,8 +1507,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1675,8 +1569,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1732,8 +1624,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1774,8 +1664,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1810,8 +1698,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1848,8 +1734,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1887,8 +1771,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1927,8 +1809,6 @@ mod tests {
|
||||
use_face_blur_hack: Some(false),
|
||||
bitrate: None,
|
||||
output_resolution: Some(KinoviOutputResolutionRaw::TenEightyP),
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -1981,8 +1861,6 @@ mod tests {
|
||||
use_face_blur_hack: Some(false),
|
||||
bitrate: None,
|
||||
output_resolution: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -2035,8 +1913,6 @@ mod tests {
|
||||
use_face_blur_hack: Some(false),
|
||||
bitrate: None,
|
||||
output_resolution: Some(KinoviOutputResolutionRaw::TenEightyP),
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
@@ -2081,8 +1957,6 @@ mod tests {
|
||||
output_resolution,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2189,8 +2063,6 @@ mod tests {
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
output_resolution: Some(KinoviOutputResolutionRaw::FourK),
|
||||
maybe_output_format: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
};
|
||||
let result = workflow_run_task(args).await?;
|
||||
|
||||
@@ -4,8 +4,7 @@ use serde_json::{json, Value};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThumbnailTaskInputMimeType {
|
||||
MP4,
|
||||
MOV,
|
||||
MP4
|
||||
}
|
||||
|
||||
pub enum ThumbnailTaskOutputMimeType {
|
||||
@@ -18,7 +17,6 @@ impl ThumbnailTaskInputMimeType {
|
||||
fn as_str(&self) -> &str {
|
||||
match self {
|
||||
ThumbnailTaskInputMimeType::MP4 => "video/mp4",
|
||||
ThumbnailTaskInputMimeType::MOV => "video/quicktime",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +26,7 @@ impl ThumbnailTaskInputMimeType {
|
||||
|
||||
fn to_output_mimetypes(&self) -> Vec<ThumbnailTaskOutputMimeType> {
|
||||
match self {
|
||||
ThumbnailTaskInputMimeType::MP4 | ThumbnailTaskInputMimeType::MOV => vec![
|
||||
ThumbnailTaskInputMimeType::MP4 => vec![
|
||||
ThumbnailTaskOutputMimeType::JPEG,
|
||||
ThumbnailTaskOutputMimeType::GIF,
|
||||
],
|
||||
@@ -213,21 +211,3 @@ impl ThumbnailTask{
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn quicktime_thumbnail_uses_correct_source_mime_type() {
|
||||
let task = ThumbnailTaskBuilder::new_for_source_mimetype(ThumbnailTaskInputMimeType::MOV)
|
||||
.with_bucket("test-bucket")
|
||||
.with_path("video.mov")
|
||||
.with_event_id("test-event")
|
||||
.build(ThumbnailTaskOutputMimeType::JPEG)
|
||||
.unwrap();
|
||||
let json = task.request_json();
|
||||
assert_eq!(json["input"]["type"], "video/quicktime");
|
||||
assert_eq!(json["input"]["path"], "video.mov");
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,6 @@ pub async fn run(state: &ArtcraftState, args: GenerateVideoArgs) -> anyhow::Resu
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
quality: None,
|
||||
duration_seconds: Some(duration),
|
||||
video_batch_count: None,
|
||||
|
||||
-2
@@ -428,7 +428,6 @@ async fn generate(
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
session,
|
||||
host_override: None,
|
||||
@@ -459,7 +458,6 @@ async fn generate(
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
bitrate: None,
|
||||
maybe_generate_audio: None,
|
||||
},
|
||||
session,
|
||||
host_override: None,
|
||||
|
||||
@@ -31,4 +31,4 @@ hostname.workspace = true
|
||||
log = "0.4.14"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
# None yet
|
||||
@@ -5,8 +5,6 @@ use log::{info, warn};
|
||||
use errors::{anyhow, AnyhowResult};
|
||||
use server_environment::ServerEnvironment;
|
||||
|
||||
use crate::config_search_directories::{config_search_directories, find_repository_root};
|
||||
|
||||
pub struct BootstrapArgs<'a, P: AsRef<Path>> {
|
||||
/// The name of the application or service
|
||||
pub app_name: &'a str,
|
||||
@@ -16,7 +14,6 @@ pub struct BootstrapArgs<'a, P: AsRef<Path>> {
|
||||
pub default_logging_override: Option<&'a str>,
|
||||
|
||||
/// Where to look for env conf files.
|
||||
/// Relative paths are also searched from an enclosing artcraft or artcraft-services checkout.
|
||||
pub config_search_directories: &'a [P],
|
||||
|
||||
/// If true, ignore the root '.env' file.
|
||||
@@ -116,16 +113,12 @@ pub fn bootstrap<P: AsRef<Path>>(args: BootstrapArgs<'_, P>) -> AnyhowResult<Con
|
||||
}
|
||||
|
||||
fn load_env_config_files<P: AsRef<Path>>(server_environment: ServerEnvironment, args: &BootstrapArgs<'_, P>) -> AnyhowResult<()> {
|
||||
let maybe_current_directory = std::env::current_dir().ok();
|
||||
let maybe_repository_root = maybe_current_directory.as_deref().and_then(find_repository_root);
|
||||
let search_directories = config_search_directories(args.config_search_directories, maybe_repository_root);
|
||||
let secrets_filename = format!("{}.development-secrets.env", &args.app_name);
|
||||
|
||||
let env_config_file_names = match server_environment {
|
||||
ServerEnvironment::Development => vec![
|
||||
format!("{}.common.env", &args.app_name),
|
||||
format!("{}.development.env", &args.app_name),
|
||||
secrets_filename.clone(), // NB: .gitignore these files
|
||||
format!("{}.development-secrets.env", &args.app_name), // NB: .gitignore these files
|
||||
],
|
||||
ServerEnvironment::Production => vec![
|
||||
format!("{}.common.env", &args.app_name),
|
||||
@@ -136,19 +129,9 @@ fn load_env_config_files<P: AsRef<Path>>(server_environment: ServerEnvironment,
|
||||
for env_config_file in env_config_file_names.into_iter() {
|
||||
info!("Loading environment variable config file: {}", &env_config_file);
|
||||
|
||||
let mut was_read = easyenv::maybe_read_env_config_from_filename_and_paths(
|
||||
let was_read = easyenv::maybe_read_env_config_from_filename_and_paths(
|
||||
&env_config_file,
|
||||
&search_directories)?;
|
||||
|
||||
// Development backups fill missing variables, even when a local config file exists.
|
||||
// dotenv preserves values already set in the environment or earlier config files.
|
||||
if server_environment == ServerEnvironment::Development && env_config_file == secrets_filename {
|
||||
if let Some(root) = maybe_repository_root {
|
||||
was_read |= easyenv::maybe_read_env_config_from_filename_and_paths(
|
||||
&env_config_file,
|
||||
&[root.join("secrets/artcraft-backup")])?;
|
||||
}
|
||||
}
|
||||
args.config_search_directories)?;
|
||||
|
||||
if was_read {
|
||||
info!("Environment config file `{}` was read.", &env_config_file);
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Keep caller-supplied paths first, then resolve relative paths from this checkout.
|
||||
pub(crate) fn config_search_directories<P: AsRef<Path>>(
|
||||
paths: &[P],
|
||||
maybe_repository_root: Option<&Path>,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut directories = paths.iter()
|
||||
.map(|path| path.as_ref().to_path_buf())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(root) = maybe_repository_root {
|
||||
for path in paths {
|
||||
if path.as_ref().is_relative() {
|
||||
let directory = root.join(path);
|
||||
if !directories.contains(&directory) {
|
||||
directories.push(directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
directories
|
||||
}
|
||||
|
||||
/// Use the enclosing checkout, never a sibling repository's configuration.
|
||||
pub(crate) fn find_repository_root(current_directory: &Path) -> Option<&Path> {
|
||||
current_directory.ancestors().find(|directory| {
|
||||
matches!(directory.file_name().and_then(|name| name.to_str()), Some("artcraft" | "artcraft-services"))
|
||||
&& directory.join("Cargo.toml").is_file()
|
||||
&& directory.join("crates").is_dir()
|
||||
})
|
||||
}
|
||||
@@ -18,6 +18,4 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod bootstrap;
|
||||
|
||||
mod config_search_directories;
|
||||
pub mod bootstrap;
|
||||
@@ -1,131 +0,0 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use bootstrap::bootstrap::{bootstrap, BootstrapArgs};
|
||||
use tempfile::TempDir;
|
||||
|
||||
const CONFIG_DIRECTORY: &str = "crates/service/job/test_job/config";
|
||||
const BACKUP_DIRECTORY: &str = "secrets/artcraft-backup";
|
||||
const CHILD_PROCESS: &str = "BOOTSTRAP_CONFIG_TEST_CHILD";
|
||||
const VALUE: &str = "BOOTSTRAP_CONFIG_TEST_VALUE";
|
||||
const MISSING_VALUE: &str = "BOOTSTRAP_CONFIG_TEST_MISSING_VALUE";
|
||||
const COMMON_VALUE: &str = "BOOTSTRAP_CONFIG_TEST_COMMON_VALUE";
|
||||
|
||||
mod development {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn loads_checkout_configs_and_backup_secrets_from_roots_and_subdirectories() {
|
||||
for name in ["artcraft", "artcraft-services"] {
|
||||
let (_temporary, root) = checkout(name);
|
||||
write_config(&root, CONFIG_DIRECTORY, "common", &format!("{COMMON_VALUE}=common\n"));
|
||||
write_config(&root, BACKUP_DIRECTORY, "development-secrets", &format!("{VALUE}=backup\n"));
|
||||
|
||||
// The other checkout must not supply this process's secrets.
|
||||
let sibling_name = if name == "artcraft" { "artcraft-services" } else { "artcraft" };
|
||||
let sibling = root.parent().unwrap().join(sibling_name);
|
||||
write_config(&sibling, BACKUP_DIRECTORY, "development-secrets", &format!("{VALUE}=wrong-checkout\n"));
|
||||
|
||||
// Nested directories can themselves be named artcraft without being the repo root.
|
||||
let nested = root.join("crates/api_clients/artcraft/test_client");
|
||||
fs::create_dir_all(&nested).unwrap();
|
||||
for directory in [&root, &root.join(CONFIG_DIRECTORY), &nested] {
|
||||
run_bootstrap(directory, "development", "backup", "common", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_and_local_configs_take_precedence_but_backup_fills_missing_values() {
|
||||
let (_temporary, root) = checkout("artcraft-services");
|
||||
write_config(&root, CONFIG_DIRECTORY, "development-secrets", &format!("{VALUE}=local\n"));
|
||||
write_config(&root, BACKUP_DIRECTORY, "development-secrets",
|
||||
&format!("{VALUE}=backup\n{MISSING_VALUE}=backup-only\n"));
|
||||
|
||||
run_bootstrap(&root, "development", "local", "", true);
|
||||
run_bootstrap(&root, "development", "environment", "", true);
|
||||
}
|
||||
}
|
||||
|
||||
mod deployment {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn production_does_not_read_development_secrets() {
|
||||
let (_temporary, root) = checkout("artcraft-services");
|
||||
write_config(&root, CONFIG_DIRECTORY, "production", &format!("{VALUE}=production\n"));
|
||||
write_config(&root, BACKUP_DIRECTORY, "development-secrets", &format!("{MISSING_VALUE}=backup-only\n"));
|
||||
|
||||
run_bootstrap(&root.join(CONFIG_DIRECTORY), "production", "production", "", false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_paths_still_work_outside_named_checkouts() {
|
||||
let temporary = TempDir::new().unwrap();
|
||||
let root = temporary.path().join("deployment");
|
||||
write_config(&root, "config", "development-secrets", &format!("{VALUE}=deployment\n"));
|
||||
write_config(&root, BACKUP_DIRECTORY, "development-secrets", &format!("{MISSING_VALUE}=backup-only\n"));
|
||||
|
||||
run_bootstrap(&root, "development", "deployment", "", false);
|
||||
}
|
||||
}
|
||||
|
||||
// Run bootstrap in a subprocess so environment and logger changes cannot affect other tests.
|
||||
#[test]
|
||||
fn bootstrap_child_process() {
|
||||
if env::var_os(CHILD_PROCESS).is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
bootstrap(BootstrapArgs {
|
||||
app_name: "bootstrap-test",
|
||||
default_logging_override: None,
|
||||
config_search_directories: &[".", "./config", CONFIG_DIRECTORY],
|
||||
ignore_legacy_dot_env_file: true,
|
||||
}).unwrap();
|
||||
|
||||
assert_eq!(env::var(VALUE).unwrap(), env::var("EXPECTED_VALUE").unwrap());
|
||||
assert_eq!(env::var(COMMON_VALUE).unwrap_or_default(), env::var("EXPECTED_COMMON_VALUE").unwrap());
|
||||
if env::var("EXPECT_BACKUP_VALUE").unwrap() == "true" {
|
||||
assert_eq!(env::var(MISSING_VALUE).unwrap(), "backup-only");
|
||||
} else {
|
||||
assert!(env::var_os(MISSING_VALUE).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
fn run_bootstrap(directory: &Path, environment: &str, expected: &str, expected_common: &str, expect_backup: bool) {
|
||||
let mut command = Command::new(env::current_exe().unwrap());
|
||||
command.args(["--exact", "bootstrap_child_process", "--nocapture"])
|
||||
.current_dir(directory)
|
||||
.env_clear()
|
||||
.env(CHILD_PROCESS, "1")
|
||||
.env("RUST_LOG", "error")
|
||||
.env("SERVER_ENVIRONMENT", environment)
|
||||
.env("EXPECTED_VALUE", expected)
|
||||
.env("EXPECTED_COMMON_VALUE", expected_common)
|
||||
.env("EXPECT_BACKUP_VALUE", expect_backup.to_string());
|
||||
|
||||
if expected == "environment" {
|
||||
command.env(VALUE, "environment");
|
||||
}
|
||||
|
||||
let output = command.output().unwrap();
|
||||
assert!(output.status.success(), "bootstrap failed in {directory:?}:\n{}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
fn checkout(name: &str) -> (TempDir, PathBuf) {
|
||||
let temporary = TempDir::new().unwrap();
|
||||
let root = temporary.path().join(name);
|
||||
fs::create_dir_all(root.join(CONFIG_DIRECTORY)).unwrap();
|
||||
fs::write(root.join("Cargo.toml"), "[workspace]\n").unwrap();
|
||||
(temporary, root)
|
||||
}
|
||||
|
||||
fn write_config(root: &Path, directory: &str, suffix: &str, contents: &str) {
|
||||
let directory = root.join(directory);
|
||||
fs::create_dir_all(&directory).unwrap();
|
||||
fs::write(directory.join(format!("bootstrap-test.{suffix}.env")), contents).unwrap();
|
||||
}
|
||||
@@ -27,7 +27,6 @@ errors.workspace = true
|
||||
anyhow.workspace = true
|
||||
ffprobe = "0.4.0"
|
||||
log.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir.workspace = true
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
use std::io::Write;
|
||||
use errors::AnyhowResult;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use ffprobe::FfProbe;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VideoInfo {
|
||||
pub dimensions: Option<VideoDimensions>,
|
||||
pub duration: Option<VideoDuration>,
|
||||
pub frame_rate: Option<VideoFrameRate>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VideoDimensions {
|
||||
pub width: u64,
|
||||
pub height: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VideoDuration {
|
||||
/// We have to convert ffprobe's seconds (with decimal) into milliseconds.
|
||||
/// We use a u32 as this can hold 49 days of milliseconds.
|
||||
@@ -29,7 +22,6 @@ pub struct VideoDuration {
|
||||
pub seconds_original: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VideoFrameRate {
|
||||
pub fps: f32,
|
||||
/// This is the original value returned by ffprobe (for debugging).
|
||||
@@ -41,41 +33,56 @@ pub fn ffprobe_get_info(
|
||||
) -> AnyhowResult<VideoInfo>
|
||||
{
|
||||
let result = ffprobe::ffprobe(video_path)?;
|
||||
Ok(info_from_probe(result))
|
||||
}
|
||||
|
||||
/// Probe downloaded/uploaded bytes without changing their container or audio.
|
||||
/// A seekable temporary file lets ffprobe inspect MOV metadata at the end of the file.
|
||||
pub fn ffprobe_get_info_from_bytes(bytes: &[u8]) -> AnyhowResult<VideoInfo> {
|
||||
let mut file = NamedTempFile::new()?;
|
||||
file.write_all(bytes)?;
|
||||
ffprobe_get_info(file.path())
|
||||
}
|
||||
let mut maybe_dimensions = None;
|
||||
let mut maybe_duration = None;
|
||||
let mut maybe_frame_rate = None;
|
||||
|
||||
fn info_from_probe(result: FfProbe) -> VideoInfo {
|
||||
let Some(stream) = result.streams.iter().find(|stream| {
|
||||
stream.codec_type.as_deref() == Some("video") && stream.disposition.attached_pic == 0
|
||||
}) else {
|
||||
return VideoInfo::default();
|
||||
let video_streams = result.streams.iter()
|
||||
.filter(|stream| stream.codec_type.as_deref() == Some("video"));
|
||||
|
||||
for stream in video_streams {
|
||||
// Grab the first video stream with dimensions and duration.
|
||||
if let (Some(width), Some(height), Some(duration)) =
|
||||
(stream.width, stream.height, stream.duration.as_deref())
|
||||
{
|
||||
maybe_dimensions = Some(VideoDimensions {
|
||||
width: width.unsigned_abs(),
|
||||
height: height.unsigned_abs(),
|
||||
});
|
||||
maybe_duration = Some(duration.to_string());
|
||||
// avg_frame_rate vs r_frame_rate: https://github.com/eugeneware/ffprobe/issues/7
|
||||
maybe_frame_rate = Some(stream.avg_frame_rate.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let maybe_duration = match maybe_duration {
|
||||
None => None,
|
||||
Some(duration) => {
|
||||
let millis = parse_seconds(&duration)?;
|
||||
Some(VideoDuration {
|
||||
millis,
|
||||
seconds_original: duration,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// MOV files can carry duration only at the container level. Keep dimensions
|
||||
// even when the stream-level duration is absent.
|
||||
let dimensions = stream.width.zip(stream.height)
|
||||
.filter(|(width, height)| *width > 0 && *height > 0)
|
||||
.map(|(width, height)| VideoDimensions { width: width as u64, height: height as u64 });
|
||||
let duration = [stream.duration.as_deref(), result.format.duration.as_deref()]
|
||||
.into_iter().flatten()
|
||||
.find_map(|seconds| parse_seconds(seconds).ok().map(|millis| VideoDuration {
|
||||
millis,
|
||||
seconds_original: seconds.to_string(),
|
||||
}));
|
||||
let frame_rate = [&stream.avg_frame_rate, &stream.r_frame_rate].into_iter()
|
||||
.find_map(|rate| parse_fps(rate).ok()
|
||||
.filter(|fps| fps.is_finite() && *fps > 0.0)
|
||||
.map(|fps| VideoFrameRate { fps, fps_original: rate.to_string() }));
|
||||
let maybe_frame_rate = match maybe_frame_rate {
|
||||
None => None,
|
||||
Some(frame_rate) => {
|
||||
let fps = parse_fps(&frame_rate)?;
|
||||
Some(VideoFrameRate {
|
||||
fps,
|
||||
fps_original: frame_rate,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
VideoInfo { dimensions, duration, frame_rate }
|
||||
Ok(VideoInfo {
|
||||
dimensions: maybe_dimensions,
|
||||
duration: maybe_duration,
|
||||
frame_rate: maybe_frame_rate,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_seconds(ffprobe_seconds: &str) -> AnyhowResult<u32> {
|
||||
@@ -125,56 +132,6 @@ pub mod tests {
|
||||
assert_eq!(info.frame_rate.unwrap().fps, 30.0);
|
||||
}
|
||||
|
||||
mod quicktime {
|
||||
use super::super::{ffprobe_get_info_from_bytes, info_from_probe};
|
||||
use ffprobe::{FfProbe, Format, Stream};
|
||||
use std::process::Command;
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[test]
|
||||
fn container_duration_does_not_discard_dimensions() {
|
||||
let info = info_from_probe(FfProbe {
|
||||
streams: vec![Stream {
|
||||
codec_type: Some("video".into()),
|
||||
width: Some(320),
|
||||
height: Some(180),
|
||||
duration: None,
|
||||
avg_frame_rate: "0/0".into(),
|
||||
r_frame_rate: "24/1".into(),
|
||||
..Default::default()
|
||||
}],
|
||||
format: Format { duration: Some("1.250".into()), ..Default::default() },
|
||||
});
|
||||
let dimensions = info.dimensions.unwrap();
|
||||
assert_eq!((dimensions.width, dimensions.height), (320, 180));
|
||||
assert_eq!(info.duration.unwrap().millis, 1250);
|
||||
assert_eq!(info.frame_rate.unwrap().fps, 24.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probes_mov_with_pcm_audio_without_modifying_bytes() {
|
||||
let dir = TempDir::new("quicktime-metadata-test").unwrap();
|
||||
let path = dir.path().join("reference.mov");
|
||||
let output = Command::new("ffmpeg").args([
|
||||
"-hide_banner", "-loglevel", "error", "-nostdin",
|
||||
"-f", "lavfi", "-i", "color=c=blue:s=160x90:r=24",
|
||||
"-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000",
|
||||
"-t", "1", "-c:v", "libx264", "-pix_fmt", "yuv420p",
|
||||
"-c:a", "pcm_s16le", "-ac", "2",
|
||||
]).arg(&path).output().unwrap();
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
let info = ffprobe_get_info_from_bytes(&bytes).unwrap();
|
||||
let dimensions = info.dimensions.unwrap();
|
||||
assert_eq!((dimensions.width, dimensions.height), (160, 90));
|
||||
assert_eq!(info.duration.unwrap().millis, 1000);
|
||||
assert_eq!(info.frame_rate.unwrap().fps, 24.0);
|
||||
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
||||
let probe = ffprobe::ffprobe(&path).unwrap();
|
||||
assert!(probe.streams.iter().any(|s| s.codec_name.as_deref() == Some("pcm_s16le")));
|
||||
}
|
||||
}
|
||||
|
||||
mod parse_seconds {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ const KNOWN_AUDIO_EXTENSIONS: &[Extension] = &[
|
||||
/// Known video extensions.
|
||||
const KNOWN_VIDEO_EXTENSIONS: &[Extension] = &[
|
||||
Extension::from_static("mp4", ".mp4"),
|
||||
Extension::from_static("mov", ".mov"),
|
||||
Extension::from_static("webm", ".webm"),
|
||||
];
|
||||
|
||||
@@ -40,7 +39,7 @@ pub enum ExtractExtensions {
|
||||
/// Accept only known audio extensions (wav, mp3).
|
||||
KnownAudio,
|
||||
|
||||
/// Accept only known video extensions (mp4, mov, webm).
|
||||
/// Accept only known video extensions (mp4, webm).
|
||||
KnownVideo,
|
||||
|
||||
/// Accept any known media extension (image, audio, or video).
|
||||
@@ -159,14 +158,6 @@ mod tests {
|
||||
assert_eq!(ext.unwrap().without_period(), "mp4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quicktime_is_a_known_video_and_media_extension() {
|
||||
for accept in [ExtractExtensions::KnownVideo, ExtractExtensions::KnownMedia] {
|
||||
let ext = extract_extension_from_url_str("https://example.com/clip.MOV?token=test", &accept);
|
||||
assert_eq!(ext.unwrap().without_period(), "mov");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_video_rejects_mp3() {
|
||||
let ext = extract_extension_from_url_str("https://example.com/song.mp3", &ExtractExtensions::KnownVideo);
|
||||
|
||||
-6
@@ -62,8 +62,6 @@ pub struct InsertMediaFileFromUploadArgs<'a> {
|
||||
|
||||
pub maybe_title: Option<&'a str>,
|
||||
pub maybe_duration_millis: Option<u64>,
|
||||
pub maybe_frame_width: Option<u32>,
|
||||
pub maybe_frame_height: Option<u32>,
|
||||
|
||||
pub maybe_scene_source_media_file_token: Option<&'a MediaFileToken>,
|
||||
pub is_intermediate_system_file: bool,
|
||||
@@ -157,8 +155,6 @@ SET
|
||||
|
||||
maybe_title = ?,
|
||||
maybe_duration_millis = ?,
|
||||
maybe_frame_width = ?,
|
||||
maybe_frame_height = ?,
|
||||
|
||||
public_bucket_directory_hash = ?,
|
||||
maybe_public_bucket_prefix = ?,
|
||||
@@ -209,8 +205,6 @@ SET
|
||||
|
||||
args.maybe_title,
|
||||
args.maybe_duration_millis,
|
||||
args.maybe_frame_width,
|
||||
args.maybe_frame_height,
|
||||
|
||||
args.public_bucket_directory_hash,
|
||||
args.maybe_public_bucket_prefix,
|
||||
|
||||
@@ -57,10 +57,6 @@ pub enum UserFeatureFlag {
|
||||
/// Access to Minimax with debugging priority (admins)
|
||||
#[serde(rename = "minimax_p")]
|
||||
MinimaxPriority,
|
||||
|
||||
/// Access to QuickTime video output options
|
||||
#[serde(rename = "use_qt")]
|
||||
CanUseQuicktime,
|
||||
}
|
||||
|
||||
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
|
||||
@@ -83,7 +79,6 @@ impl UserFeatureFlag {
|
||||
Self::ApiKey => "api_key",
|
||||
Self::Minimax => "minimax",
|
||||
Self::MinimaxPriority => "minimax_p",
|
||||
Self::CanUseQuicktime => "use_qt",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +95,6 @@ impl UserFeatureFlag {
|
||||
"api_key" => Ok(Self::ApiKey),
|
||||
"minimax" => Ok(Self::Minimax),
|
||||
"minimax_p" => Ok(Self::MinimaxPriority),
|
||||
"use_qt" => Ok(Self::CanUseQuicktime),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -118,7 +112,6 @@ impl UserFeatureFlag {
|
||||
Self::ApiKey => "API Keys",
|
||||
Self::Minimax => "Minimax",
|
||||
Self::MinimaxPriority => "Minimax (Debugging Priority)",
|
||||
Self::CanUseQuicktime => "QuickTime Video Output",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +128,6 @@ impl UserFeatureFlag {
|
||||
Self::ApiKey => "Access to API key creation",
|
||||
Self::Minimax => "Access to Minimax",
|
||||
Self::MinimaxPriority => "Access to Minimax with debugging priority (admins)",
|
||||
Self::CanUseQuicktime => "Access to QuickTime video output options for Seedance 2.5",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +146,6 @@ impl UserFeatureFlag {
|
||||
Self::ApiKey,
|
||||
Self::Minimax,
|
||||
Self::MinimaxPriority,
|
||||
Self::CanUseQuicktime,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -180,7 +171,6 @@ mod tests {
|
||||
assert_serialization(UserFeatureFlag::ApiKey, "api_key");
|
||||
assert_serialization(UserFeatureFlag::Minimax, "minimax");
|
||||
assert_serialization(UserFeatureFlag::MinimaxPriority, "minimax_p");
|
||||
assert_serialization(UserFeatureFlag::CanUseQuicktime, "use_qt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -196,7 +186,6 @@ mod tests {
|
||||
assert_eq!(UserFeatureFlag::ApiKey.to_str(), "api_key");
|
||||
assert_eq!(UserFeatureFlag::Minimax.to_str(), "minimax");
|
||||
assert_eq!(UserFeatureFlag::MinimaxPriority.to_str(), "minimax_p");
|
||||
assert_eq!(UserFeatureFlag::CanUseQuicktime.to_str(), "use_qt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -212,14 +201,13 @@ mod tests {
|
||||
assert_eq!(UserFeatureFlag::from_str("api_key").unwrap(), UserFeatureFlag::ApiKey);
|
||||
assert_eq!(UserFeatureFlag::from_str("minimax").unwrap(), UserFeatureFlag::Minimax);
|
||||
assert_eq!(UserFeatureFlag::from_str("minimax_p").unwrap(), UserFeatureFlag::MinimaxPriority);
|
||||
assert_eq!(UserFeatureFlag::from_str("use_qt").unwrap(), UserFeatureFlag::CanUseQuicktime);
|
||||
assert!(UserFeatureFlag::from_str("foo").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = UserFeatureFlag::all_variants();
|
||||
assert_eq!(variants.len(), 12);
|
||||
assert_eq!(variants.len(), 11);
|
||||
assert_eq!(variants.pop_first(), Some(UserFeatureFlag::ExploreMedia));
|
||||
assert_eq!(variants.pop_first(), Some(UserFeatureFlag::Studio));
|
||||
assert_eq!(variants.pop_first(), Some(UserFeatureFlag::Upload3d));
|
||||
@@ -231,7 +219,6 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(UserFeatureFlag::ApiKey));
|
||||
assert_eq!(variants.pop_first(), Some(UserFeatureFlag::Minimax));
|
||||
assert_eq!(variants.pop_first(), Some(UserFeatureFlag::MinimaxPriority));
|
||||
assert_eq!(variants.pop_first(), Some(UserFeatureFlag::CanUseQuicktime));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
/// Maximum serialized string length for database storage.
|
||||
pub const MAX_LENGTH: usize = 16;
|
||||
|
||||
/// Output video containers for generation. Unset options retain the provider default.
|
||||
/// Models without output-format support ignore this option.
|
||||
///
|
||||
/// NB: Keep the max serialized length to 16 characters.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonVideoOutputFormat {
|
||||
#[serde(rename = "mp4")]
|
||||
Mp4,
|
||||
#[serde(rename = "mov")]
|
||||
Mov,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(CommonVideoOutputFormat);
|
||||
impl_mysql_enum_coders!(CommonVideoOutputFormat);
|
||||
impl_mysql_from_row!(CommonVideoOutputFormat);
|
||||
|
||||
impl CommonVideoOutputFormat {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mp4 => "mp4",
|
||||
Self::Mov => "mov",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, EnumError> {
|
||||
match value {
|
||||
"mp4" => Ok(Self::Mp4),
|
||||
"mov" => Ok(Self::Mov),
|
||||
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
BTreeSet::from([
|
||||
Self::Mp4,
|
||||
Self::Mov,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::generation::common_video_output_format::CommonVideoOutputFormat;
|
||||
use crate::common::generation::common_video_output_format::MAX_LENGTH;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod explicit_checks {
|
||||
use super::*;
|
||||
use crate::error::enum_error::EnumError;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(CommonVideoOutputFormat::Mp4, "mp4");
|
||||
assert_serialization(CommonVideoOutputFormat::Mov, "mov");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(CommonVideoOutputFormat::Mp4.to_str(), "mp4");
|
||||
assert_eq!(CommonVideoOutputFormat::Mov.to_str(), "mov");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(CommonVideoOutputFormat::from_str("mp4").unwrap(), CommonVideoOutputFormat::Mp4);
|
||||
assert_eq!(CommonVideoOutputFormat::from_str("mov").unwrap(), CommonVideoOutputFormat::Mov);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_err() {
|
||||
let result = CommonVideoOutputFormat::from_str("invalid");
|
||||
assert!(result.is_err());
|
||||
if let Err(EnumError::CouldNotConvertFromString(value)) = result {
|
||||
assert_eq!(value, "invalid");
|
||||
} else {
|
||||
panic!("Expected EnumError::CouldNotConvertFromString");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonVideoOutputFormat::all_variants();
|
||||
assert_eq!(variants.len(), 2);
|
||||
assert_eq!(variants.pop_first(), Some(CommonVideoOutputFormat::Mp4));
|
||||
assert_eq!(variants.pop_first(), Some(CommonVideoOutputFormat::Mov));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(CommonVideoOutputFormat::all_variants().len(), CommonVideoOutputFormat::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in CommonVideoOutputFormat::all_variants() {
|
||||
assert_eq!(variant, CommonVideoOutputFormat::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, CommonVideoOutputFormat::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, CommonVideoOutputFormat::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_length_ok_for_database() {
|
||||
for variant in CommonVideoOutputFormat::all_variants() {
|
||||
let serialized = variant.to_str();
|
||||
assert!(serialized.len() > 0, "variant {:?} is too short", variant);
|
||||
assert!(serialized.len() <= MAX_LENGTH, "variant {:?} is too long via to_str()", variant);
|
||||
}
|
||||
for variant in CommonVideoOutputFormat::all_variants() {
|
||||
let json = serde_json::to_string(&variant).unwrap().replace('"', "");
|
||||
assert!(json.len() <= MAX_LENGTH, "variant {:?} is too long via JSON: {:?}", variant, json);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_names_must_not_contain_dots() {
|
||||
for variant in CommonVideoOutputFormat::all_variants() {
|
||||
let to_str_value = variant.to_str();
|
||||
assert!(!to_str_value.contains('.'), "to_str() for {:?} contains a dot: {:?}", variant, to_str_value);
|
||||
|
||||
let json_value = serde_json::to_string(&variant).unwrap().replace('"', "");
|
||||
assert!(!json_value.contains('.'), "JSON serialization for {:?} contains a dot: {:?}", variant, json_value);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_names_must_only_contain_lowercase_alphanumeric_and_underscore() {
|
||||
let valid_pattern = regex::Regex::new(r"^[a-z0-9_]+$").unwrap();
|
||||
|
||||
for variant in CommonVideoOutputFormat::all_variants() {
|
||||
let to_str_value = variant.to_str();
|
||||
assert!(valid_pattern.is_match(to_str_value),
|
||||
"to_str() for {:?} contains invalid characters: {:?} (only a-z, 0-9, _ allowed)", variant, to_str_value);
|
||||
|
||||
let json_value = serde_json::to_string(&variant).unwrap().replace('"', "");
|
||||
assert!(valid_pattern.is_match(&json_value),
|
||||
"JSON serialization for {:?} contains invalid characters: {:?} (only a-z, 0-9, _ allowed)", variant, json_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,3 @@ pub mod model_creator;
|
||||
pub mod common_resolution;
|
||||
pub mod common_splat_model;
|
||||
pub mod common_video_model;
|
||||
pub mod common_video_output_format;
|
||||
@@ -27,8 +27,6 @@ cloud_storage.workspace = true
|
||||
concurrency.workspace = true
|
||||
easyenv.workspace = true
|
||||
enums.workspace = true
|
||||
mimetypes.workspace = true
|
||||
ffmpeg_utils.workspace = true
|
||||
errors.workspace = true
|
||||
hashing = { path = "../../../lib/files/hashing" }
|
||||
jobs_common = { path = "../../../lib/jobs_common" }
|
||||
|
||||
+10
-58
@@ -9,8 +9,6 @@ use enums::by_table::media_files::media_file_origin_product_category::MediaFileO
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use errors::AnyhowResult;
|
||||
use ffmpeg_utils::ffprobe::ffprobe_get_info::{ffprobe_get_info_from_bytes, VideoInfo};
|
||||
use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use mysql_queries::queries::generic_inference::api_providers::kinovi_web::list_pending_kinovi_web_video_jobs::PendingKinoviWebJob;
|
||||
use mysql_queries::queries::generic_inference::job::select_inference_job_status_for_update::select_inference_job_status_for_update;
|
||||
@@ -23,6 +21,7 @@ use crate::job_dependencies::JobDependencies;
|
||||
use crate::jobs::order_processing_job::is_job_status_terminal::is_job_status_terminal;
|
||||
|
||||
const PREFIX: &str = "artcraft_";
|
||||
const SUFFIX: &str = ".mp4";
|
||||
|
||||
/// Download the completed video, upload to bucket, create media file record, and mark job done.
|
||||
pub async fn process_successful_video_job(
|
||||
@@ -53,7 +52,7 @@ pub async fn process_successful_video_job(
|
||||
);
|
||||
|
||||
// Download the video bytes.
|
||||
let video_bytes: Vec<u8> = match deps.download_client.get(video_url).send().await.and_then(|resp| resp.error_for_status()) {
|
||||
let video_bytes: Vec<u8> = match deps.download_client.get(video_url).send().await {
|
||||
Ok(resp) => match resp.bytes().await {
|
||||
Ok(bytes) => bytes.to_vec(),
|
||||
Err(err) => {
|
||||
@@ -83,22 +82,12 @@ pub async fn process_successful_video_job(
|
||||
order.order_id
|
||||
);
|
||||
|
||||
let (suffix, mimetype, media_file_type) = video_metadata(&video_bytes)?;
|
||||
let (video_bytes, probe_result) = tokio::task::spawn_blocking(move || {
|
||||
let info = ffprobe_get_info_from_bytes(&video_bytes);
|
||||
(video_bytes, info)
|
||||
}).await?;
|
||||
let video_info = probe_result.unwrap_or_else(|err| {
|
||||
warn!("Could not probe video for order {}: {:?}", order.order_id, err);
|
||||
VideoInfo::default()
|
||||
});
|
||||
|
||||
// Hash the video.
|
||||
let checksum = sha256_hash_bytes(&video_bytes)
|
||||
.map_err(|err| anyhow!("error hashing video: {:?}", err))?;
|
||||
|
||||
// Build the bucket path.
|
||||
let bucket_path = MediaFileBucketPath::generate_new(Some(PREFIX), Some(suffix));
|
||||
let bucket_path = MediaFileBucketPath::generate_new(Some(PREFIX), Some(SUFFIX));
|
||||
|
||||
let object_path = bucket_path.get_full_object_path_str();
|
||||
|
||||
@@ -110,7 +99,7 @@ pub async fn process_successful_video_job(
|
||||
// Upload to public bucket.
|
||||
let upload_result = deps
|
||||
.public_bucket_client
|
||||
.upload_file_with_content_type_process(object_path, &video_bytes, mimetype)
|
||||
.upload_file_with_content_type_process(object_path, &video_bytes, "video/mp4")
|
||||
.await;
|
||||
|
||||
if let Err(err) = upload_result {
|
||||
@@ -128,15 +117,10 @@ pub async fn process_successful_video_job(
|
||||
order.order_id
|
||||
);
|
||||
|
||||
// Prefer the downloaded file's dimensions; provider fields are a fallback.
|
||||
let maybe_result = order.results.iter().find(|result| result.url == video_url);
|
||||
let maybe_frame_width = video_info.dimensions.as_ref()
|
||||
.and_then(|d| u32::try_from(d.width).ok())
|
||||
.or_else(|| maybe_result.and_then(|result| result.maybe_width));
|
||||
let maybe_frame_height = video_info.dimensions.as_ref()
|
||||
.and_then(|d| u32::try_from(d.height).ok())
|
||||
.or_else(|| maybe_result.and_then(|result| result.maybe_height));
|
||||
let maybe_duration_millis = video_info.duration.map(|duration| u64::from(duration.millis));
|
||||
// Optionally extract frame dimensions from the order results.
|
||||
// Persist the integer dimensions when the API provides them, NULL otherwise.
|
||||
let maybe_frame_width = order.results.first().and_then(|r| r.maybe_width);
|
||||
let maybe_frame_height = order.results.first().and_then(|r| r.maybe_height);
|
||||
|
||||
// Insert media file record.
|
||||
let media_file_result = MediaFileInsertBuilder::new()
|
||||
@@ -146,7 +130,6 @@ pub async fn process_successful_video_job(
|
||||
.file_size_bytes(video_bytes.len() as u64)
|
||||
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.maybe_duration_millis(maybe_duration_millis)
|
||||
.maybe_frame_height(maybe_frame_height)
|
||||
.maybe_frame_width(maybe_frame_width)
|
||||
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
|
||||
@@ -156,8 +139,8 @@ pub async fn process_successful_video_job(
|
||||
.media_file_class(MediaFileClass::Video)
|
||||
.media_file_origin_category(MediaFileOriginCategory::Inference)
|
||||
.media_file_origin_product_category(MediaFileOriginProductCategory::VideoGeneration)
|
||||
.media_file_type(media_file_type)
|
||||
.mime_type(mimetype)
|
||||
.media_file_type(MediaFileType::Mp4)
|
||||
.mime_type("video/mp4")
|
||||
.public_bucket_directory_hash(&bucket_path)
|
||||
.insert_pool(&deps.mysql_pool)
|
||||
.await;
|
||||
@@ -263,34 +246,3 @@ async fn finalize_success(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Detect the actual container instead of labeling every provider result as MP4.
|
||||
fn video_metadata(bytes: &[u8]) -> AnyhowResult<(&'static str, &'static str, MediaFileType)> {
|
||||
match get_mimetype_for_bytes(bytes) {
|
||||
Some("video/quicktime") => Ok((".mov", "video/quicktime", MediaFileType::Mov)),
|
||||
Some("video/mp4") => Ok((".mp4", "video/mp4", MediaFileType::Mp4)),
|
||||
other => Err(anyhow!("Unsupported Kinovi video result MIME type: {:?}", other)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn detects_quicktime_result() {
|
||||
let bytes = b"\x00\x00\x00\x14ftypqt \x00\x00\x02\x00qt ";
|
||||
assert_eq!(video_metadata(bytes).unwrap(), (".mov", "video/quicktime", MediaFileType::Mov));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_mp4_result() {
|
||||
let bytes = b"\x00\x00\x00\x20ftypisom\x00\x00\x02\x00isomiso2avc1mp41";
|
||||
assert_eq!(video_metadata(bytes).unwrap(), (".mp4", "video/mp4", MediaFileType::Mp4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_error_page_as_video() {
|
||||
assert!(video_metadata(b"<html>Download failed</html>").is_err());
|
||||
}
|
||||
}
|
||||
-28
@@ -3,7 +3,6 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_bitrate::CommonBitrate;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
|
||||
/// The Seedance 2.5 family of video models.
|
||||
@@ -49,8 +48,6 @@ pub fn seedance_2p5_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
output_format_options: Some(vec![CommonVideoOutputFormat::Mp4, CommonVideoOutputFormat::Mov]),
|
||||
output_format_default: Some(CommonVideoOutputFormat::Mp4),
|
||||
batch_size_options: Some(vec![1]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
@@ -98,11 +95,6 @@ pub fn seedance_2p5_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
CommonBitrate::High,
|
||||
]),
|
||||
bitrate_default: Some(CommonBitrate::Normal),
|
||||
output_format_options: Some(vec![
|
||||
CommonVideoOutputFormat::Mp4,
|
||||
CommonVideoOutputFormat::Mov
|
||||
]),
|
||||
output_format_default: Some(CommonVideoOutputFormat::Mp4),
|
||||
batch_size_options: Some(vec![1]),
|
||||
batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(4),
|
||||
@@ -159,23 +151,3 @@ pub fn seedance_2p5_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
|
||||
models
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn output_format_config_matches_supported_models() {
|
||||
for model in seedance_2p5_video_models() {
|
||||
let json = serde_json::to_value(&model).unwrap();
|
||||
if matches!(model.model, CommonVideoModel::Seedance2p5 | CommonVideoModel::Seedance2p5Ultra) {
|
||||
assert_eq!(json["output_format_options"], json!(["mp4", "mov"]));
|
||||
assert_eq!(json["output_format_default"], "mp4");
|
||||
} else {
|
||||
assert!(json.get("output_format_options").is_none());
|
||||
assert!(json.get("output_format_default").is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,7 +310,6 @@ use enums::common::generation::common_model_class::CommonModelClass;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_bitrate::CommonBitrate;
|
||||
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
@@ -817,7 +816,6 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
CommonBitrate,
|
||||
CommonGenerationMode,
|
||||
CommonResolution,
|
||||
CommonVideoOutputFormat,
|
||||
PromptContextSemanticType,
|
||||
PromptType,
|
||||
MediaFileOriginModelType,
|
||||
|
||||
-1
@@ -52,7 +52,6 @@ pub async fn estimate_video_cost_handler(
|
||||
resolution: router_resolution,
|
||||
aspect_ratio: router_aspect_ratio,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
duration_seconds: request.duration_seconds,
|
||||
video_batch_count: request.video_batch_count,
|
||||
generate_audio: request.generate_audio,
|
||||
|
||||
-2
@@ -258,8 +258,6 @@ pub async fn upload_snapshot_media_file_handler(
|
||||
maybe_mime_type: Some(&mimetype),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis: None,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: Some("Snapshot"),
|
||||
maybe_scene_source_media_file_token: None,
|
||||
|
||||
-2
@@ -153,8 +153,6 @@ pub async fn save_new_project(args: SaveNewProjectArgs<'_>) -> Result<MediaFileT
|
||||
maybe_mime_type: Some(PROJECT_MIMETYPE),
|
||||
file_size_bytes: file_bytes.len() as u64,
|
||||
maybe_duration_millis: None,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &sha256_checksum,
|
||||
maybe_title: maybe_title.as_deref(),
|
||||
maybe_scene_source_media_file_token: None,
|
||||
|
||||
-2
@@ -332,8 +332,6 @@ pub async fn upload_audio_media_file_handler(
|
||||
maybe_mime_type: Some(&mimetype),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: maybe_title.as_deref(),
|
||||
maybe_scene_source_media_file_token: None,
|
||||
|
||||
+1
-17
@@ -1,4 +1,3 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -12,7 +11,6 @@ use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCat
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use ffmpeg_utils::ffprobe::ffprobe_get_info::{ffprobe_get_info_from_bytes, VideoInfo};
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use media::decode_basic_audio_info::decode_basic_audio_bytes_info;
|
||||
use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes;
|
||||
@@ -196,7 +194,7 @@ pub async fn process_upload_media_file(
|
||||
|
||||
let do_audio_decode = match mimetype {
|
||||
// TODO: Revisit when Safari can send us this metadata consistently
|
||||
"audio/mp4" | "video/mp4" | "video/quicktime" => false,
|
||||
"audio/mp4" | "video/mp4" => false,
|
||||
"audio/opus" => {
|
||||
// TODO/FIXME(bt, 2023-05-19): Symphonia is currently broken for Firefox's opus.
|
||||
// We're on an off-master branch that may resolve the problem in the future, but for now
|
||||
@@ -282,18 +280,6 @@ pub async fn process_upload_media_file(
|
||||
};
|
||||
|
||||
let media_file_class = media_file_type.to_media_class();
|
||||
let video_info = if media_file_class == MediaFileClass::Video {
|
||||
ffprobe_get_info_from_bytes(&bytes).unwrap_or_else(|err| {
|
||||
warn!("Could not probe uploaded video: {:?}", err);
|
||||
VideoInfo::default()
|
||||
})
|
||||
} else {
|
||||
VideoInfo::default()
|
||||
};
|
||||
if let Some(duration) = &video_info.duration {
|
||||
maybe_duration_millis = Some(u64::from(duration.millis));
|
||||
}
|
||||
|
||||
|
||||
let upload_type = match upload_media_request.media_source {
|
||||
MediaFileUploadSource::Unknown => UploadType::Filesystem,
|
||||
@@ -363,8 +349,6 @@ pub async fn process_upload_media_file(
|
||||
.mime_type(mime_type)
|
||||
.file_size_bytes(file_size_bytes as u64)
|
||||
.maybe_duration_millis(maybe_duration_millis)
|
||||
.maybe_frame_width(video_info.dimensions.as_ref().and_then(|d| u32::try_from(d.width).ok()))
|
||||
.maybe_frame_height(video_info.dimensions.as_ref().and_then(|d| u32::try_from(d.height).ok()))
|
||||
.checksum_sha2(&hash)
|
||||
.maybe_title(upload_media_request.title.as_deref())
|
||||
.maybe_origin_filename(upload_media_request.file_name.as_deref())
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ static ALLOWED_MIME_TYPES : Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
// Video
|
||||
"video/mp4",
|
||||
"video/webm",
|
||||
"video/quicktime",
|
||||
//"video/quicktime",
|
||||
// Image
|
||||
"image/gif",
|
||||
"image/jpeg",
|
||||
|
||||
-2
@@ -304,8 +304,6 @@ pub async fn upload_image_media_file_handler(
|
||||
maybe_batch_token: maybe_batch_token.as_ref(),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis: None,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: maybe_title.as_deref(),
|
||||
maybe_scene_source_media_file_token: None,
|
||||
|
||||
-2
@@ -224,8 +224,6 @@ pub async fn upload_new_engine_asset_media_file_handler(
|
||||
maybe_mime_type: Some(file_info.mimetype),
|
||||
file_size_bytes: file_info.file_size_bytes as u64,
|
||||
maybe_duration_millis: file_info.maybe_duration_millis,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &file_info.sha256_checksum,
|
||||
maybe_scene_source_media_file_token: None,
|
||||
is_intermediate_system_file: false, // NB: is_user_upload = TRUE
|
||||
|
||||
-2
@@ -240,8 +240,6 @@ pub async fn upload_new_scene_media_file_handler(
|
||||
maybe_mime_type: Some(MIMETYPE),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis: None,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: maybe_title.as_deref(),
|
||||
maybe_scene_source_media_file_token: None,
|
||||
|
||||
-2
@@ -271,8 +271,6 @@ pub async fn upload_pmx_media_file_handler(
|
||||
maybe_mime_type: Some("application/octet-stream"),
|
||||
file_size_bytes: pmx_details.file_size_bytes,
|
||||
maybe_duration_millis,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &pmx_details.sha256_checksum,
|
||||
maybe_scene_source_media_file_token: None,
|
||||
is_intermediate_system_file: false, // NB: is_user_upload = TRUE
|
||||
|
||||
-2
@@ -233,8 +233,6 @@ pub async fn upload_scene_snapshot_media_file_handler(
|
||||
maybe_mime_type: Some(MIMETYPE),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis: None,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: maybe_title.as_deref(),
|
||||
maybe_scene_source_media_file_token: maybe_scene_source_media_file_token.as_ref(),
|
||||
|
||||
+73
-42
@@ -1,4 +1,3 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::collections::HashSet;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
@@ -18,8 +17,9 @@ use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path:
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use ffmpeg_utils::ffmpeg::ffmpeg_transcode_to_mp4::{ffmpeg_transcode_to_mp4, FfmpegTranscodeToMp4Args};
|
||||
use ffmpeg_utils::ffmpeg::ffmpeg_trim_and_resample::{ffmpeg_trim_and_resample, Args};
|
||||
use ffmpeg_utils::ffprobe::ffprobe_get_info::{ffprobe_get_info, VideoInfo};
|
||||
use ffmpeg_utils::ffprobe::ffprobe_get_info::ffprobe_get_info;
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use filesys::path_to_string::path_to_string;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
@@ -35,7 +35,6 @@ use tokens::tokens::prompts::PromptToken;
|
||||
use crate::http_server::endpoints::media_files::upload::common_utils::read_upload_session::read_upload_session;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileUploadError;
|
||||
use crate::http_server::endpoints::media_files::upload::common_utils::try_parse_generation_provider::try_parse_generation_provider;
|
||||
use crate::http_server::user_lookup::user_session::session_utils::lookup::user_session_feature_flags::UserSessionFeatureFlags;
|
||||
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -120,8 +119,14 @@ pub struct UploadNewVideoMediaFileSuccessResponse {
|
||||
static ALLOWED_MIME_TYPES : Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
HashSet::from([
|
||||
// Video
|
||||
"video/mp4",
|
||||
"video/quicktime", // Preserve MOV video and audio without transcoding.
|
||||
"video/mp4", // NB: Only mp4 for now.
|
||||
])
|
||||
});
|
||||
|
||||
static TRANSCODE_MIME_TYPES : Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
HashSet::from([
|
||||
// Video
|
||||
"video/quicktime", // .qt quicktime files
|
||||
])
|
||||
});
|
||||
|
||||
@@ -231,7 +236,9 @@ pub async fn upload_new_video_media_file_handler(
|
||||
MediaFileUploadError::BadInput("Could not determine mimetype for file".to_string())
|
||||
})?;
|
||||
|
||||
if !ALLOWED_MIME_TYPES.contains(mimetype.as_str()) {
|
||||
let needs_transcode = TRANSCODE_MIME_TYPES.contains(mimetype.as_str());
|
||||
|
||||
if !ALLOWED_MIME_TYPES.contains(mimetype.as_str()) && !needs_transcode {
|
||||
// NB: Don't let our error message inject malicious strings
|
||||
let filtered_mimetype = mimetype
|
||||
.chars()
|
||||
@@ -241,24 +248,10 @@ pub async fn upload_new_video_media_file_handler(
|
||||
return Err(MediaFileUploadError::BadInput(format!("unpermitted mime type: {}", &filtered_mimetype)));
|
||||
}
|
||||
|
||||
if mimetype == "video/quicktime" {
|
||||
let maybe_feature_flags = match session_auth.maybe_header_session.as_ref() {
|
||||
Some(session) => session.maybe_feature_flags.as_deref(),
|
||||
None => session_auth.maybe_cookie_session.as_ref()
|
||||
.and_then(|session| session.maybe_feature_flags.as_deref()),
|
||||
};
|
||||
let user_feature_flags = UserSessionFeatureFlags::new(maybe_feature_flags);
|
||||
if !user_feature_flags.can_use_quicktime() {
|
||||
return Err(MediaFileUploadError::NotAuthorizedVerbose(
|
||||
"QuickTime uploads are not enabled for this account".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ORIGINAL FILE BOOKKEEPING ==================== //
|
||||
// Hold on to the user's original file (path + mimetype + extension) so we
|
||||
// can upload it as a `_original.{ext}` sibling later if we end up
|
||||
// trimming or resampling it.
|
||||
// transcoding and/or resampling it.
|
||||
|
||||
let original_file_path: PathBuf = form.file.file.path().to_path_buf();
|
||||
let original_mimetype: String = mimetype.clone();
|
||||
@@ -274,10 +267,49 @@ pub async fn upload_new_video_media_file_handler(
|
||||
MediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// Preserve the source container and audio unless the caller requests an edit.
|
||||
let mut final_upload_file_path = form.file.file.path().to_path_buf();
|
||||
// ==================== OPTIONAL TRANSCODE TO MP4 ==================== //
|
||||
// Some accepted source mime types (e.g. QuickTime .mov) are not the canonical
|
||||
// distribution format we want for the media file record. Transcode them to
|
||||
// mp4 in a tempdir so we can resample / upload mp4 instead.
|
||||
|
||||
// Tempdirs we need to keep alive across the upload step. They get dropped at
|
||||
// function exit which cleans up the on-disk files.
|
||||
let mut _transcode_tempdir_ref = None;
|
||||
let mut _resample_tempdir_ref = None;
|
||||
|
||||
let mut final_upload_file_path = form.file.file.path().to_path_buf();
|
||||
|
||||
if needs_transcode {
|
||||
let transcode_tempdir = server_state.temp_dir_creator.new_tempdir("ffmpeg_transcode")
|
||||
.map_err(|err| {
|
||||
error!("Problem creating transcode temp dir: {:?}", err);
|
||||
MediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
let transcoded_path = transcode_tempdir.path().join("transcoded.mp4");
|
||||
|
||||
info!("Transcoding {} → mp4", &original_mimetype);
|
||||
ffmpeg_transcode_to_mp4(FfmpegTranscodeToMp4Args {
|
||||
video_input_path: form.file.file.path(),
|
||||
video_output_path: &transcoded_path,
|
||||
}).map_err(|err| {
|
||||
error!("Problem transcoding video to mp4: {:?}", err);
|
||||
MediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// The canonical file is now the transcoded mp4. Read its bytes and pin
|
||||
// its mimetype to mp4 so all the downstream metadata reflects the
|
||||
// transcoded file rather than the user's original upload.
|
||||
file_bytes = file_read_bytes(&transcoded_path)
|
||||
.map_err(|e| {
|
||||
error!("Problem reading transcoded mp4: {:?}", e);
|
||||
MediaFileUploadError::ServerError
|
||||
})?;
|
||||
mimetype = "video/mp4".to_string();
|
||||
final_upload_file_path = transcoded_path;
|
||||
_transcode_tempdir_ref = Some(transcode_tempdir);
|
||||
}
|
||||
|
||||
// ==================== OPTIONAL VIDEO RESAMPLE ==================== //
|
||||
|
||||
let should_resample = form.maybe_resample_fps.is_some()
|
||||
@@ -298,7 +330,8 @@ pub async fn upload_new_video_media_file_handler(
|
||||
let maybe_start_offset = form.maybe_trim_start_millis.map(|millis| Duration::from_millis(millis.0));
|
||||
let maybe_end_offset = form.maybe_trim_end_millis.map(|millis| Duration::from_millis(millis.0));
|
||||
|
||||
// Explicit trimming/resampling produces MP4; the original is retained below.
|
||||
// NB: Resample reads from `final_upload_file_path` (which is the transcoded
|
||||
// mp4 if we transcoded above, or the user's original upload otherwise).
|
||||
ffmpeg_trim_and_resample(Args {
|
||||
video_input_path: &final_upload_file_path,
|
||||
video_output_path: &video_output_path,
|
||||
@@ -327,18 +360,23 @@ pub async fn upload_new_video_media_file_handler(
|
||||
_resample_tempdir_ref = Some(frame_temp_dir); // NB: Keep from going out of scope
|
||||
}
|
||||
|
||||
// Retain the unmodified original when the caller requests an edit.
|
||||
let should_upload_original = should_resample;
|
||||
// True if we touched the user's file in any way (transcoded, resampled, or both).
|
||||
// We'll upload their unmodified original alongside the canonical file when so.
|
||||
let should_upload_original = needs_transcode || should_resample;
|
||||
|
||||
// ==================== OTHER FILE METADATA ==================== //
|
||||
|
||||
let video_info = ffprobe_get_info(&final_upload_file_path).unwrap_or_else(|err| {
|
||||
warn!("Could not probe uploaded video: {:?}", err);
|
||||
VideoInfo::default()
|
||||
});
|
||||
let maybe_duration_millis = video_info.duration.map(|duration| u64::from(duration.millis));
|
||||
let maybe_frame_width = video_info.dimensions.as_ref().and_then(|d| u32::try_from(d.width).ok());
|
||||
let maybe_frame_height = video_info.dimensions.as_ref().and_then(|d| u32::try_from(d.height).ok());
|
||||
let mut maybe_duration_millis = None;
|
||||
|
||||
match ffprobe_get_info(&final_upload_file_path) {
|
||||
Ok(video_info) => {
|
||||
maybe_duration_millis = video_info.duration
|
||||
.map(|duration| duration.millis as u64);
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Error reading video dimensions with ffprobe: {:?}", error);
|
||||
}
|
||||
}
|
||||
|
||||
let maybe_filename = form.file.file_name.as_deref()
|
||||
.as_deref()
|
||||
@@ -385,7 +423,7 @@ pub async fn upload_new_video_media_file_handler(
|
||||
})?;
|
||||
|
||||
// ==================== UPLOAD ORIGINAL SIBLING ==================== //
|
||||
// If we trimmed or resampled the user's upload, also upload the
|
||||
// If we transcoded and/or resampled the user's upload, also upload the
|
||||
// unmodified original alongside the canonical file as `_original.{ext}`
|
||||
// (sharing the same object hash / directory). The media file record points
|
||||
// at the canonical file, not this sibling.
|
||||
@@ -469,8 +507,6 @@ pub async fn upload_new_video_media_file_handler(
|
||||
maybe_mime_type: Some(&mimetype),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis,
|
||||
maybe_frame_width,
|
||||
maybe_frame_height,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: maybe_title.as_deref(),
|
||||
maybe_scene_source_media_file_token,
|
||||
@@ -489,12 +525,7 @@ pub async fn upload_new_video_media_file_handler(
|
||||
|
||||
info!("new media file id: {} token: {:?}", record_id, &token);
|
||||
|
||||
let thumbnail_mimetype = if mimetype == "video/quicktime" {
|
||||
ThumbnailTaskInputMimeType::MOV
|
||||
} else {
|
||||
ThumbnailTaskInputMimeType::MP4
|
||||
};
|
||||
let thumbnail_task_result = ThumbnailTaskBuilder::new_for_source_mimetype(thumbnail_mimetype)
|
||||
let thumbnail_task_result = ThumbnailTaskBuilder::new_for_source_mimetype(ThumbnailTaskInputMimeType::MP4)
|
||||
.with_bucket(server_state.public_bucket_client.bucket_name().as_str())
|
||||
.with_path(&*path_to_string(public_upload_path.to_full_object_pathbuf()))
|
||||
.with_output_suffix("thumb")
|
||||
|
||||
-2
@@ -236,8 +236,6 @@ pub async fn upload_video_media_file_handler(
|
||||
maybe_mime_type: Some(mimetype),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis: Some(mp4_info.duration_millis as u64),
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: upload_media_request.title.as_deref(),
|
||||
maybe_scene_source_media_file_token: None,
|
||||
|
||||
-1
@@ -128,7 +128,6 @@ fn to_omni_gen_request(request: OmniApiVideoGenerateRequest) -> OmniGenVideoCost
|
||||
resolution: request.resolution,
|
||||
aspect_ratio: request.aspect_ratio,
|
||||
bitrate: request.bitrate,
|
||||
maybe_output_format: request.maybe_output_format,
|
||||
quality: request.quality,
|
||||
duration_seconds: request.duration_seconds,
|
||||
video_batch_count: request.video_batch_count,
|
||||
|
||||
+1
-15
@@ -1,5 +1,3 @@
|
||||
use std::convert::TryFrom;
|
||||
use ffmpeg_utils::ffprobe::ffprobe_get_info::{ffprobe_get_info_from_bytes, VideoInfo};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use log::{info, warn};
|
||||
@@ -73,15 +71,6 @@ pub async fn upload_url_to_media_file(
|
||||
format!("Unpermitted mime type for URL {}: {}", url, filtered_mimetype)));
|
||||
}
|
||||
|
||||
let video_info = if matches!(kind, MediaUploadKind::Video) {
|
||||
ffprobe_get_info_from_bytes(&file_bytes).unwrap_or_else(|err| {
|
||||
warn!("Could not probe uploaded video URL: {:?}", err);
|
||||
VideoInfo::default()
|
||||
})
|
||||
} else {
|
||||
VideoInfo::default()
|
||||
};
|
||||
|
||||
// ==================== OTHER FILE METADATA ==================== //
|
||||
|
||||
let extension = mimetype_to_extension(&mimetype)
|
||||
@@ -136,9 +125,7 @@ pub async fn upload_url_to_media_file(
|
||||
maybe_batch_token: None,
|
||||
file_size_bytes,
|
||||
// NB: Duration would require probing the file on disk; URL inputs skip it.
|
||||
maybe_duration_millis: video_info.duration.map(|duration| u64::from(duration.millis)),
|
||||
maybe_frame_width: video_info.dimensions.as_ref().and_then(|d| u32::try_from(d.width).ok()),
|
||||
maybe_frame_height: video_info.dimensions.as_ref().and_then(|d| u32::try_from(d.height).ok()),
|
||||
maybe_duration_millis: None,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: None,
|
||||
maybe_scene_source_media_file_token: None,
|
||||
@@ -172,7 +159,6 @@ static IMAGE_MIME_TYPES: Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
static VIDEO_MIME_TYPES: Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
HashSet::from([
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
])
|
||||
});
|
||||
|
||||
|
||||
-1
@@ -88,7 +88,6 @@ mod tests {
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
quality: None,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
|
||||
-1
@@ -70,7 +70,6 @@ mod tests {
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
quality: None,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
|
||||
-2
@@ -296,8 +296,6 @@ pub async fn omni_upload_audio_media_file_handler(
|
||||
maybe_mime_type: Some(&mimetype),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: maybe_title.as_deref(),
|
||||
maybe_scene_source_media_file_token: None,
|
||||
|
||||
-2
@@ -282,8 +282,6 @@ pub async fn omni_upload_image_media_file_handler(
|
||||
maybe_batch_token: maybe_batch_token.as_ref(),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis: None,
|
||||
maybe_frame_width: None,
|
||||
maybe_frame_height: None,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: maybe_title.as_deref(),
|
||||
maybe_scene_source_media_file_token: None,
|
||||
|
||||
+65
-26
@@ -1,4 +1,3 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::collections::HashSet;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
@@ -19,8 +18,9 @@ use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path:
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use ffmpeg_utils::ffmpeg::ffmpeg_transcode_to_mp4::{ffmpeg_transcode_to_mp4, FfmpegTranscodeToMp4Args};
|
||||
use ffmpeg_utils::ffmpeg::ffmpeg_trim_and_resample::{ffmpeg_trim_and_resample, Args};
|
||||
use ffmpeg_utils::ffprobe::ffprobe_get_info::{ffprobe_get_info, VideoInfo};
|
||||
use ffmpeg_utils::ffprobe::ffprobe_get_info::ffprobe_get_info;
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use filesys::path_to_string::path_to_string;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
@@ -108,8 +108,14 @@ pub struct OmniUploadVideoMediaFileForm {
|
||||
static ALLOWED_MIME_TYPES : Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
HashSet::from([
|
||||
// Video
|
||||
"video/mp4",
|
||||
"video/quicktime", // Preserve MOV video and audio without transcoding.
|
||||
"video/mp4", // NB: Only mp4 for now.
|
||||
])
|
||||
});
|
||||
|
||||
static TRANSCODE_MIME_TYPES : Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
HashSet::from([
|
||||
// Video
|
||||
"video/quicktime", // .qt quicktime files
|
||||
])
|
||||
});
|
||||
|
||||
@@ -212,7 +218,9 @@ pub async fn omni_upload_video_media_file_handler(
|
||||
MediaFileUploadError::BadInput("Could not determine mimetype for file".to_string())
|
||||
})?;
|
||||
|
||||
if !ALLOWED_MIME_TYPES.contains(mimetype.as_str()) {
|
||||
let needs_transcode = TRANSCODE_MIME_TYPES.contains(mimetype.as_str());
|
||||
|
||||
if !ALLOWED_MIME_TYPES.contains(mimetype.as_str()) && !needs_transcode {
|
||||
// NB: Don't let our error message inject malicious strings
|
||||
let filtered_mimetype = mimetype
|
||||
.chars()
|
||||
@@ -225,7 +233,7 @@ pub async fn omni_upload_video_media_file_handler(
|
||||
// ==================== ORIGINAL FILE BOOKKEEPING ==================== //
|
||||
// Hold on to the user's original file (path + mimetype + extension) so we
|
||||
// can upload it as a `_original.{ext}` sibling later if we end up
|
||||
// trimming or resampling it.
|
||||
// transcoding and/or resampling it.
|
||||
|
||||
let original_file_path: PathBuf = form.file.file.path().to_path_buf();
|
||||
let original_mimetype: String = mimetype.clone();
|
||||
@@ -241,10 +249,44 @@ pub async fn omni_upload_video_media_file_handler(
|
||||
MediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
// Preserve the source container and audio unless the caller requests an edit.
|
||||
let mut final_upload_file_path = form.file.file.path().to_path_buf();
|
||||
// ==================== OPTIONAL TRANSCODE TO MP4 ==================== //
|
||||
// Some accepted source mime types (e.g. QuickTime .mov) are not the canonical
|
||||
// distribution format we want for the media file record. Transcode them to
|
||||
// mp4 in a tempdir so we can resample / upload mp4 instead.
|
||||
|
||||
let mut _transcode_tempdir_ref = None;
|
||||
let mut _resample_tempdir_ref = None;
|
||||
|
||||
let mut final_upload_file_path = form.file.file.path().to_path_buf();
|
||||
|
||||
if needs_transcode {
|
||||
let transcode_tempdir = server_state.temp_dir_creator.new_tempdir("ffmpeg_transcode")
|
||||
.map_err(|err| {
|
||||
error!("Problem creating transcode temp dir: {:?}", err);
|
||||
MediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
let transcoded_path = transcode_tempdir.path().join("transcoded.mp4");
|
||||
|
||||
info!("Transcoding {} → mp4", &original_mimetype);
|
||||
ffmpeg_transcode_to_mp4(FfmpegTranscodeToMp4Args {
|
||||
video_input_path: form.file.file.path(),
|
||||
video_output_path: &transcoded_path,
|
||||
}).map_err(|err| {
|
||||
error!("Problem transcoding video to mp4: {:?}", err);
|
||||
MediaFileUploadError::ServerError
|
||||
})?;
|
||||
|
||||
file_bytes = file_read_bytes(&transcoded_path)
|
||||
.map_err(|e| {
|
||||
error!("Problem reading transcoded mp4: {:?}", e);
|
||||
MediaFileUploadError::ServerError
|
||||
})?;
|
||||
mimetype = "video/mp4".to_string();
|
||||
final_upload_file_path = transcoded_path;
|
||||
_transcode_tempdir_ref = Some(transcode_tempdir);
|
||||
}
|
||||
|
||||
// ==================== OPTIONAL VIDEO RESAMPLE ==================== //
|
||||
|
||||
let should_resample = form.maybe_resample_fps.is_some()
|
||||
@@ -292,18 +334,22 @@ pub async fn omni_upload_video_media_file_handler(
|
||||
_resample_tempdir_ref = Some(frame_temp_dir); // NB: Keep from going out of scope
|
||||
}
|
||||
|
||||
// Retain the unmodified original when the caller requests an edit.
|
||||
let should_upload_original = should_resample;
|
||||
// True if we touched the user's file in any way (transcoded, resampled, or both).
|
||||
let should_upload_original = needs_transcode || should_resample;
|
||||
|
||||
// ==================== OTHER FILE METADATA ==================== //
|
||||
|
||||
let video_info = ffprobe_get_info(&final_upload_file_path).unwrap_or_else(|err| {
|
||||
warn!("Could not probe uploaded video: {:?}", err);
|
||||
VideoInfo::default()
|
||||
});
|
||||
let maybe_duration_millis = video_info.duration.map(|duration| u64::from(duration.millis));
|
||||
let maybe_frame_width = video_info.dimensions.as_ref().and_then(|d| u32::try_from(d.width).ok());
|
||||
let maybe_frame_height = video_info.dimensions.as_ref().and_then(|d| u32::try_from(d.height).ok());
|
||||
let mut maybe_duration_millis = None;
|
||||
|
||||
match ffprobe_get_info(&final_upload_file_path) {
|
||||
Ok(video_info) => {
|
||||
maybe_duration_millis = video_info.duration
|
||||
.map(|duration| duration.millis as u64);
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("Error reading video dimensions with ffprobe: {:?}", error);
|
||||
}
|
||||
}
|
||||
|
||||
let maybe_filename = form.file.file_name.as_deref()
|
||||
.as_deref()
|
||||
@@ -350,7 +396,7 @@ pub async fn omni_upload_video_media_file_handler(
|
||||
})?;
|
||||
|
||||
// ==================== UPLOAD ORIGINAL SIBLING ==================== //
|
||||
// If we trimmed or resampled the user's upload, also upload the
|
||||
// If we transcoded and/or resampled the user's upload, also upload the
|
||||
// unmodified original alongside the canonical file as `_original.{ext}`.
|
||||
|
||||
if should_upload_original {
|
||||
@@ -431,8 +477,6 @@ pub async fn omni_upload_video_media_file_handler(
|
||||
maybe_mime_type: Some(&mimetype),
|
||||
file_size_bytes: file_size_bytes as u64,
|
||||
maybe_duration_millis,
|
||||
maybe_frame_width,
|
||||
maybe_frame_height,
|
||||
sha256_checksum: &hash,
|
||||
maybe_title: maybe_title.as_deref(),
|
||||
maybe_scene_source_media_file_token,
|
||||
@@ -451,12 +495,7 @@ pub async fn omni_upload_video_media_file_handler(
|
||||
|
||||
info!("new media file id: {} token: {:?}", record_id, &token);
|
||||
|
||||
let thumbnail_mimetype = if mimetype == "video/quicktime" {
|
||||
ThumbnailTaskInputMimeType::MOV
|
||||
} else {
|
||||
ThumbnailTaskInputMimeType::MP4
|
||||
};
|
||||
let thumbnail_task_result = ThumbnailTaskBuilder::new_for_source_mimetype(thumbnail_mimetype)
|
||||
let thumbnail_task_result = ThumbnailTaskBuilder::new_for_source_mimetype(ThumbnailTaskInputMimeType::MP4)
|
||||
.with_bucket(server_state.public_bucket_client.bucket_name().as_str())
|
||||
.with_path(&*path_to_string(public_upload_path.to_full_object_pathbuf()))
|
||||
.with_output_suffix("thumb")
|
||||
|
||||
-1
@@ -271,7 +271,6 @@ mod tests {
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
quality: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
|
||||
+1
-149
@@ -6,7 +6,6 @@ use artcraft_router::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use artcraft_router::api::router_bitrate::RouterBitrate;
|
||||
use artcraft_router::api::router_resolution::RouterResolution;
|
||||
use artcraft_router::api::router_video_model::RouterVideoModel;
|
||||
use artcraft_router::api::router_video_output_format::RouterVideoOutputFormat;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::router_provider::RouterProvider;
|
||||
@@ -17,7 +16,6 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonA
|
||||
use enums::common::generation::common_bitrate::CommonBitrate as CommonBitrateEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
|
||||
|
||||
pub fn hydrate_to_router_request(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
@@ -65,20 +63,9 @@ pub fn hydrate_to_router_request(
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
bitrate,
|
||||
maybe_output_format: if matches!(model, RouterVideoModel::Seedance2p5 | RouterVideoModel::Seedance2p5Ultra) {
|
||||
Some(match request.maybe_output_format.unwrap_or(CommonVideoOutputFormat::Mp4) {
|
||||
CommonVideoOutputFormat::Mp4 => RouterVideoOutputFormat::Mp4,
|
||||
CommonVideoOutputFormat::Mov => RouterVideoOutputFormat::Mov,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
duration_seconds: request.duration_seconds,
|
||||
video_batch_count: request.video_batch_count,
|
||||
// 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 },
|
||||
generate_audio: 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,
|
||||
@@ -87,24 +74,6 @@ 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> {
|
||||
@@ -153,122 +122,6 @@ fn convert_bitrate(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod output_format {
|
||||
use super::*;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::client::router_kinovi_web_client::RouterKinoviWebClient;
|
||||
use artcraft_router::generate::generate_video::video_generation_draft_context::VideoGenerationDraftContext;
|
||||
use artcraft_router::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use artcraft_router::generate::generate_video::video_generation_request::VideoGenerationRequest;
|
||||
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
|
||||
use kinovi_web_client::generate::video::generate_seedance_2p5::KinoviSeedance2p5OutputFormat;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[tokio::test]
|
||||
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()),
|
||||
));
|
||||
let context = VideoGenerationDraftContext { client: Some(&client), ..Default::default() };
|
||||
for model in ["seedance_2p5", "seedance_2p5_u"] {
|
||||
for output_format in [None, Some(Value::Null), Some(json!("mp4")), Some(json!("mov"))] {
|
||||
for generate_audio in [None, Some(true), Some(false)] {
|
||||
let mut body = json!({
|
||||
"model": model,
|
||||
"generate_audio": generate_audio,
|
||||
});
|
||||
if let Some(format) = &output_format {
|
||||
body["output_format"] = format.clone();
|
||||
}
|
||||
let request: OmniGenVideoCostAndGenerateRequest = serde_json::from_value(body).unwrap();
|
||||
let mut builder = hydrate_to_router_request(&request).unwrap();
|
||||
builder.provider = RouterProvider::KinoviWeb;
|
||||
// Mirror run_pipeline_v2: Ultra executes as the base model on
|
||||
// its own account, while billing retains the original model.
|
||||
if matches!(builder.model, RouterVideoModel::Seedance2p5Ultra) {
|
||||
builder.model = RouterVideoModel::Seedance2p5;
|
||||
}
|
||||
let draft = match builder.build2().unwrap() {
|
||||
VideoGenerationDraftOrRequest::Draft(draft) => draft,
|
||||
other => panic!("Unexpected request: {:?}", other),
|
||||
};
|
||||
let finalized = draft.finalize(context.clone()).await.unwrap();
|
||||
let request = match finalized {
|
||||
VideoGenerationRequest::KinoviSeedance2p5(state) => state.request,
|
||||
other => panic!("Unexpected request: {:?}", other),
|
||||
};
|
||||
let actual_format = request.maybe_output_format.map(|format| match format {
|
||||
KinoviSeedance2p5OutputFormat::Mp4 => "mp4",
|
||||
KinoviSeedance2p5OutputFormat::Mov => "mov",
|
||||
});
|
||||
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, None, "generate_audio: {generate_audio:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_model_ignores_format() {
|
||||
for model in ["seedance_2p0", "seedance_2p5_preview"] {
|
||||
let request: OmniGenVideoCostAndGenerateRequest = serde_json::from_value(json!({
|
||||
"model": model, "output_format": "mov"
|
||||
})).unwrap();
|
||||
assert!(hydrate_to_router_request(&request).unwrap().maybe_output_format.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_format_is_rejected() {
|
||||
for output_format in [json!("webm"), json!("MP4"), json!(42)] {
|
||||
let request = serde_json::from_value::<OmniGenVideoCostAndGenerateRequest>(json!({
|
||||
"model": "seedance_2p5", "output_format": output_format,
|
||||
}));
|
||||
assert!(request.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
@@ -314,7 +167,6 @@ mod tests {
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
quality: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
|
||||
-2
@@ -29,5 +29,3 @@ mod mcp_session_tests;
|
||||
mod omni_api_parity_tests;
|
||||
mod seedance_2p0;
|
||||
mod seedance_2p5;
|
||||
|
||||
mod quicktime_tests;
|
||||
+2
-66
@@ -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, stub_traffic, to_omni_api_request, ExpectedCredits, Seconds,
|
||||
TestHarness, STARTING_CREDITS,
|
||||
base_generate_request, to_omni_api_request, ExpectedCredits, Seconds, TestHarness,
|
||||
STARTING_CREDITS,
|
||||
};
|
||||
|
||||
mod billing_parity {
|
||||
@@ -46,26 +46,6 @@ 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::*;
|
||||
|
||||
@@ -215,47 +195,3 @@ 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
use std::process::Command;
|
||||
|
||||
use actix_web::cookie::Cookie;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{test, web, App};
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::common_video_output_format::CommonVideoOutputFormat;
|
||||
use mysql_queries::queries::media_files::get::get_media_file::get_media_file;
|
||||
use mysql_queries::queries::users::user::update::set_user_feature_flags::{set_user_feature_flags, SetUserFeatureFlagArgs};
|
||||
use mysql_testing::fixtures::users::{create_test_user, TestUser};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::http_server::endpoints::media_files::upload::upload_generic::upload_media_file_handler::upload_media_file_handler;
|
||||
use crate::http_server::endpoints::media_files::upload::upload_video_new::upload_new_video_media_file_handler::upload_new_video_media_file_handler;
|
||||
use crate::http_server::endpoints::omni_api::upload::omni_upload_video_media_file_handler::omni_upload_video_media_file_handler;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::tests::support::{base_generate_request, stub_traffic, TestHarness};
|
||||
|
||||
const BOUNDARY: &str = "artcraft-quicktime-test-boundary";
|
||||
|
||||
#[actix_web::test]
|
||||
#[cfg_attr(feature = "skip_database_tests", ignore)]
|
||||
async fn quicktime_uploads_are_video_references_with_mp4_or_mov_output() {
|
||||
let harness = TestHarness::create().await;
|
||||
let user = harness.create_funded_user(10_000).await;
|
||||
enable_quicktime(&harness, &user).await;
|
||||
let api_key = harness.create_api_key(&user).await;
|
||||
let bytes = quicktime_fixture();
|
||||
let app = test::init_service(App::new()
|
||||
.app_data(web::Data::new(harness.server_state.clone()))
|
||||
.route("/new-video", web::post().to(upload_new_video_media_file_handler))
|
||||
.route("/omni-upload", web::post().to(omni_upload_video_media_file_handler))
|
||||
.route("/generic-upload", web::post().to(upload_media_file_handler))
|
||||
).await;
|
||||
|
||||
for route in ["/new-video", "/omni-upload", "/generic-upload"] {
|
||||
let idempotency = base_generate_request(CommonVideoModel::Seedance2p5).idempotency_token.unwrap();
|
||||
let body = multipart_body(&idempotency, &bytes);
|
||||
let request = test::TestRequest::post().uri(route)
|
||||
.insert_header(("Authorization", format!("Bearer {}", api_key.as_str_be_careful())))
|
||||
.insert_header(("Content-Type", format!("multipart/form-data; boundary={BOUNDARY}")))
|
||||
.peer_addr("127.0.0.1:9999".parse().unwrap())
|
||||
.set_payload(body).to_request();
|
||||
let response = test::call_service(&app, request).await;
|
||||
let status = response.status();
|
||||
let body = test::read_body(response).await;
|
||||
assert!(status.is_success(), "{}: {}", route, String::from_utf8_lossy(&body));
|
||||
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||
let token = MediaFileToken::new_from_str(json["media_file_token"].as_str().unwrap());
|
||||
let media = get_media_file(&token, false, &harness.pool).await.unwrap().unwrap();
|
||||
assert_eq!(media.media_class, MediaFileClass::Video);
|
||||
assert_eq!(media.media_type, MediaFileType::Mov);
|
||||
assert_eq!(media.maybe_mime_type.as_deref(), Some("video/quicktime"));
|
||||
assert_eq!(media.maybe_public_bucket_extension.as_deref(), Some(".mov"));
|
||||
assert_eq!(media.maybe_duration_millis, Some(1000));
|
||||
|
||||
for (format, expected) in [(None, "mp4"), (Some(CommonVideoOutputFormat::Mp4), "mp4"), (Some(CommonVideoOutputFormat::Mov), "mov")] {
|
||||
let mut request = base_generate_request(CommonVideoModel::Seedance2p5);
|
||||
let prompt = format!("QuickTime reference test {}", request.idempotency_token.as_deref().unwrap());
|
||||
request.prompt = Some(prompt.clone());
|
||||
request.reference_video_media_tokens = Some(vec![token.clone()]);
|
||||
request.maybe_output_format = format;
|
||||
request.generate_audio = Some(true);
|
||||
request.duration_seconds = Some(4);
|
||||
request.resolution = Some(CommonResolution::FourEightyP);
|
||||
let response = harness.post_generate(&user, request).await.expect("submit MOV video reference");
|
||||
assert!(response.success);
|
||||
|
||||
let traffic = stub_traffic().lock().unwrap();
|
||||
let params = traffic.workflows.iter()
|
||||
.map(|request| &request["0"]["json"]["apiParams"])
|
||||
.find(|params| params["prompt"].as_str() == Some(&prompt))
|
||||
.expect("captured Kinovi request");
|
||||
assert_eq!(params["output_format"], expected);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
#[cfg_attr(feature = "skip_database_tests", ignore)]
|
||||
async fn new_video_quicktime_uploads_require_feature_flag_for_every_auth_type() {
|
||||
let harness = TestHarness::create().await;
|
||||
let mov_bytes = quicktime_fixture();
|
||||
let mp4_bytes = video_fixture("mp4", "aac");
|
||||
let app = test::init_service(App::new()
|
||||
.app_data(web::Data::new(harness.server_state.clone()))
|
||||
.route("/new-video", web::post().to(upload_new_video_media_file_handler))
|
||||
).await;
|
||||
|
||||
for enabled in [false, true] {
|
||||
let user = create_test_user(&harness.pool).await.unwrap();
|
||||
if enabled {
|
||||
enable_quicktime(&harness, &user).await;
|
||||
}
|
||||
let api_key = harness.create_api_key(&user).await;
|
||||
let mcp_session = harness.create_mcp_session(&user).await;
|
||||
let cookie = harness.server_state.session_cookie_manager
|
||||
.create_cookie(&user.session_token, &user.user_token).unwrap();
|
||||
|
||||
for auth in ["anonymous", "cookie", "api_key", "mcp"] {
|
||||
for (is_quicktime, bytes) in [(true, &mov_bytes), (false, &mp4_bytes)] {
|
||||
let idempotency = base_generate_request(CommonVideoModel::Seedance2p5).idempotency_token.unwrap();
|
||||
// Even MOV bytes claim to be MP4: authorization must use the detected
|
||||
// container, not the filename or multipart Content-Type.
|
||||
let body = multipart_body_for_file(&idempotency, bytes, "reference.mp4", "video/mp4");
|
||||
let mut request = test::TestRequest::post().uri("/new-video")
|
||||
.insert_header(("Content-Type", format!("multipart/form-data; boundary={BOUNDARY}")))
|
||||
.peer_addr("127.0.0.1:9999".parse().unwrap())
|
||||
.set_payload(body);
|
||||
request = match auth {
|
||||
"cookie" => request.cookie(Cookie::new("session", cookie.value().to_string())),
|
||||
"api_key" => request.insert_header(("Authorization", format!("Bearer {}", api_key.as_str_be_careful()))),
|
||||
"mcp" => request.insert_header(("Authorization", format!("Bearer {}", mcp_session.as_str()))),
|
||||
_ => request,
|
||||
};
|
||||
|
||||
let response = test::call_service(&app, request.to_request()).await;
|
||||
let status = response.status();
|
||||
let body = test::read_body(response).await;
|
||||
let expected = if is_quicktime && (!enabled || auth == "anonymous") {
|
||||
StatusCode::UNAUTHORIZED
|
||||
} else {
|
||||
StatusCode::OK
|
||||
};
|
||||
assert_eq!(status, expected,
|
||||
"enabled={}, auth={}, quicktime={}: {}", enabled, auth, is_quicktime, String::from_utf8_lossy(&body));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn enable_quicktime(harness: &TestHarness, user: &TestUser) {
|
||||
set_user_feature_flags(SetUserFeatureFlagArgs {
|
||||
subject_user_token: &user.user_token,
|
||||
maybe_feature_flags: Some("use_qt"),
|
||||
ip_address: "127.0.0.1",
|
||||
maybe_mod_user_token: None,
|
||||
mysql_pool: &harness.pool,
|
||||
}).await.unwrap();
|
||||
}
|
||||
|
||||
fn quicktime_fixture() -> Vec<u8> {
|
||||
video_fixture("mov", "pcm_s16le")
|
||||
}
|
||||
|
||||
fn video_fixture(extension: &str, audio_codec: &str) -> Vec<u8> {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join(format!("reference.{extension}"));
|
||||
let output = Command::new("ffmpeg").args([
|
||||
"-hide_banner", "-loglevel", "error", "-nostdin",
|
||||
"-f", "lavfi", "-i", "color=c=blue:s=160x90:r=24",
|
||||
"-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000",
|
||||
"-t", "1", "-c:v", "libx264", "-pix_fmt", "yuv420p",
|
||||
"-c:a", audio_codec, "-ac", "2",
|
||||
]).arg(&path).output().unwrap();
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
std::fs::read(path).unwrap()
|
||||
}
|
||||
|
||||
fn multipart_body(idempotency_token: &str, bytes: &[u8]) -> Vec<u8> {
|
||||
multipart_body_for_file(idempotency_token, bytes, "reference.mov", "video/quicktime")
|
||||
}
|
||||
|
||||
fn multipart_body_for_file(idempotency_token: &str, bytes: &[u8], filename: &str, mimetype: &str) -> Vec<u8> {
|
||||
let mut body = format!(
|
||||
"--{BOUNDARY}\r\nContent-Disposition: form-data; name=\"uuid_idempotency_token\"\r\n\r\n{idempotency_token}\r\n\
|
||||
--{BOUNDARY}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\nContent-Type: {mimetype}\r\n\r\n"
|
||||
).into_bytes();
|
||||
body.extend_from_slice(bytes);
|
||||
body.extend_from_slice(format!("\r\n--{BOUNDARY}--\r\n").as_bytes());
|
||||
body
|
||||
}
|
||||
+356
-356
@@ -4,12 +4,12 @@
|
||||
//!
|
||||
//! Rate cards (ceil-rounded once):
|
||||
//! - Seedance 2.5: 480p 11.76954733 ¢/s, 720p 26.70781893 ¢/s,
|
||||
//! 1080p 61.62732357 ¢/s; with video refs 480p 7.24279835 ¢/s,
|
||||
//! 720p 15.84362140 ¢/s, 1080p 38.49728574 ¢/s over (output + input)
|
||||
//! 1080p 45.85869386 ¢/s; with video refs 480p 7.24279835 ¢/s,
|
||||
//! 720p 15.84362140 ¢/s, 1080p 27.39973680 ¢/s over (output + input)
|
||||
//! seconds
|
||||
//! - Seedance 2.5 Ultra: 480p 13.90946502 ¢/s, 720p 31.56378601 ¢/s,
|
||||
//! 1080p 67.22980753 ¢/s; with video refs 480p 8.55967078 ¢/s,
|
||||
//! 720p 18.72427984 ¢/s, 1080p 41.84487580 ¢/s
|
||||
//! 1080p 50.10486922 ¢/s; with video refs 480p 8.55967078 ¢/s,
|
||||
//! 720p 18.72427984 ¢/s, 1080p 29.93674947 ¢/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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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(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)),
|
||||
(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)),
|
||||
];
|
||||
|
||||
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 × 38.49728574 = 1424.40 → 1425 (1080p)
|
||||
// 6.5s → 7 → 37 billed × 27.39973680 = 1013.79 → 1014 (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(1425),
|
||||
Seconds(30), &[6_500], ExpectedCredits(1014),
|
||||
).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 41.84487580 ¢/s:
|
||||
// / 1080p 29.93674947 ¢/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 × 41.84487580 = 1548.26 → 1549 (1080p)
|
||||
// 6.5s → 7 → 37 billed × 29.93674947 = 1107.66 → 1108 (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(1549),
|
||||
Seconds(30), &[6_500], ExpectedCredits(1108),
|
||||
).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(309), ExpectedCredits(337), CreditsDelta(28)),
|
||||
(Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1849), ExpectedCredits(2017), CreditsDelta(168)),
|
||||
(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)),
|
||||
// 4K downgrades to 1080p on both models, so the premium matches 1080p.
|
||||
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(309), ExpectedCredits(337), CreditsDelta(28)),
|
||||
(Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(230), ExpectedCredits(251), CreditsDelta(21)),
|
||||
];
|
||||
|
||||
for (resolution, seconds, batch, base, variant, delta) in cases {
|
||||
|
||||
+8
-45
@@ -3,10 +3,10 @@
|
||||
//! ever leave the process), and helpers for driving the generate handler
|
||||
//! with dummy Actix HTTP requests as a fixture user.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_web::cookie::Cookie;
|
||||
@@ -15,7 +15,6 @@ use actix_web::web::{Data, Json};
|
||||
use actix_web::HttpRequest;
|
||||
use chrono::Utc;
|
||||
use sqlx::MySqlPool;
|
||||
use serde_json::Value;
|
||||
|
||||
use actix_artcraft::sessions::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager;
|
||||
use actix_artcraft::sessions::user_sessions::http_user_session_manager::HttpUserSessionManager;
|
||||
@@ -324,7 +323,6 @@ pub fn base_generate_request(model: CommonVideoModel) -> OmniGenVideoCostAndGene
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
quality: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
@@ -350,29 +348,16 @@ fn uuid_v4_like() -> String {
|
||||
/// BOTH the Kinovi host-override and media-CDN-override env vars exactly
|
||||
/// once, before returning.
|
||||
fn process_stub_kinovi_base_url() -> &'static String {
|
||||
use std::sync::OnceLock;
|
||||
static STUB: OnceLock<String> = OnceLock::new();
|
||||
STUB.get_or_init(|| {
|
||||
let base_url = spawn_stub_server();
|
||||
std::env::set_var(ENV_KINOVI_CUSTOM_API_HOST, &base_url);
|
||||
std::env::set_var(ENV_KINOVI_CUSTOM_CDN_HOST, &base_url);
|
||||
std::env::set_var("THUMBNAIL_GENERATOR_API_URL", format!("{base_url}/thumbnails"));
|
||||
std::env::set_var("THUMBNAIL_GENERATOR_API_BASIC_AUTH", "");
|
||||
base_url
|
||||
})
|
||||
}
|
||||
|
||||
/// Captured local stub traffic for assertions about provider payloads and exact upload bytes.
|
||||
#[derive(Default)]
|
||||
pub struct StubTraffic {
|
||||
pub uploads: HashMap<String, Vec<u8>>,
|
||||
pub workflows: Vec<Value>,
|
||||
}
|
||||
|
||||
pub fn stub_traffic() -> &'static Mutex<StubTraffic> {
|
||||
static TRAFFIC: OnceLock<Mutex<StubTraffic>> = OnceLock::new();
|
||||
TRAFFIC.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// A stub for BOTH the Kinovi API and the media CDN. Runs on plain OS
|
||||
/// threads so it is independent of any tokio runtime and serves connections
|
||||
/// until the test process exits.
|
||||
@@ -419,15 +404,8 @@ fn spawn_stub_server() -> String {
|
||||
match stream.read(&mut buffer[read_total..]) {
|
||||
Ok(n) if n > 0 => {
|
||||
read_total += n;
|
||||
if let Some(header_end) = buffer[..read_total].windows(4).position(|w| w == b"\r\n\r\n") {
|
||||
let headers = String::from_utf8_lossy(&buffer[..header_end]);
|
||||
let content_length = headers.lines().find_map(|line| {
|
||||
let (name, value) = line.split_once(':')?;
|
||||
name.eq_ignore_ascii_case("content-length").then(|| value.trim().parse::<usize>().ok()).flatten()
|
||||
}).unwrap_or(0);
|
||||
if read_total >= header_end + 4 + content_length {
|
||||
break;
|
||||
}
|
||||
if buffer[..read_total].windows(4).any(|w| w == b"\r\n\r\n") && read_total > 4 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
@@ -435,19 +413,9 @@ fn spawn_stub_server() -> String {
|
||||
}
|
||||
let head = String::from_utf8_lossy(&buffer[..read_total]).to_string();
|
||||
let request_line = head.lines().next().unwrap_or_default().to_string();
|
||||
let body_start = buffer[..read_total].windows(4).position(|w| w == b"\r\n\r\n")
|
||||
.map(|index| index + 4).unwrap_or(read_total);
|
||||
let request_body = &buffer[body_start..read_total];
|
||||
let filename = request_line.split_whitespace().nth(1).unwrap_or("")
|
||||
.split('?').next().unwrap_or("").rsplit('/').next().unwrap_or("").to_string();
|
||||
let maybe_uploaded = stub_traffic().lock().unwrap().uploads.get(&filename).cloned();
|
||||
|
||||
|
||||
let (status, content_type, body): (&str, &str, Vec<u8>) =
|
||||
if request_line.starts_with("GET") && maybe_uploaded.is_some() {
|
||||
let mime = if filename.ends_with(".mov") { "video/quicktime" } else { "video/mp4" };
|
||||
("200 OK", mime, maybe_uploaded.unwrap())
|
||||
} else if request_line.starts_with("GET") {
|
||||
if request_line.starts_with("GET") {
|
||||
match extract_fixture_duration_millis(&request_line) {
|
||||
Some(millis) => ("200 OK", "video/mp4", fixture_video_bytes(millis)),
|
||||
// URL-ingested uploads land under the `video_` bucket prefix
|
||||
@@ -460,17 +428,12 @@ fn spawn_stub_server() -> String {
|
||||
None => ("404 Not Found", "text/plain", b"not found".to_vec()),
|
||||
}
|
||||
} else if request_line.starts_with("PUT") {
|
||||
stub_traffic().lock().unwrap().uploads.insert(filename, request_body.to_vec());
|
||||
("200 OK", "text/plain", Vec::new())
|
||||
} else if request_line.contains("uploads.signedUploadUrl") {
|
||||
let extension = if head.contains("mov") { "mov" } else { "mp4" };
|
||||
let upload_url = format!("{base_url}/upload-target/materials/test/fixture.{extension}");
|
||||
let upload_url = format!("{base_url}/upload-target/materials/test/fixture.mp4");
|
||||
let json = format!(r#"[{{"result":{{"data":{{"json":"{upload_url}"}}}}}}]"#);
|
||||
("200 OK", "application/json", json.into_bytes())
|
||||
} else {
|
||||
if let Ok(payload) = serde_json::from_slice(request_body) {
|
||||
stub_traffic().lock().unwrap().workflows.push(payload);
|
||||
}
|
||||
let order_number = ORDER_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let json = format!(
|
||||
r#"[{{"result":{{"data":{{"json":{{"taskId":"task_test_{process_prefix:016x}_{order_number:04}","orderId":"ord_test_{process_prefix:016x}_{order_number:04}","violationWarning":false}}}}}}}}]"#,
|
||||
@@ -582,6 +545,7 @@ fn build_test_server_state(pool: MySqlPool, media_cdn_override_url: String) -> S
|
||||
auto_gc_bucket_client: LegacyBucketClient,
|
||||
redis_rate_limiters: crate::state::server_state::RedisRateLimiters,
|
||||
}
|
||||
use std::sync::OnceLock;
|
||||
static DUMMIES: OnceLock<ExpensiveDummies> = OnceLock::new();
|
||||
let dummies = DUMMIES.get_or_init(|| retry_keychain_flake(|| {
|
||||
// Bucket uploads (e.g. URL-input ingestion) PUT to the stub, which
|
||||
@@ -1024,7 +988,6 @@ pub fn to_omni_api_request(request: OmniGenVideoCostAndGenerateRequest) -> OmniA
|
||||
resolution: request.resolution,
|
||||
aspect_ratio: request.aspect_ratio,
|
||||
bitrate: request.bitrate,
|
||||
maybe_output_format: request.maybe_output_format,
|
||||
quality: request.quality,
|
||||
duration_seconds: request.duration_seconds,
|
||||
video_batch_count: request.video_batch_count,
|
||||
|
||||
-37
@@ -42,15 +42,6 @@ pub async fn omni_gen_video_models_handler(
|
||||
remove_models(&mut response, &MINIMAX_GATED_MODELS);
|
||||
}
|
||||
|
||||
if !can_use_quicktime(&http_request, &server_state).await? {
|
||||
for model in &mut response.models {
|
||||
if matches!(model.model, CommonVideoModel::Seedance2p5 | CommonVideoModel::Seedance2p5Ultra) {
|
||||
model.output_format_options = None;
|
||||
model.output_format_default = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
@@ -86,34 +77,6 @@ pub async fn can_see_minimax(
|
||||
Ok(can_use_minimax || can_use_minimax_priority)
|
||||
}
|
||||
|
||||
/// Whether the (optional) logged-in user holds the QuickTime feature flag.
|
||||
/// Anonymous requests do not expose the output-format options.
|
||||
pub async fn can_use_quicktime(
|
||||
http_request: &HttpRequest,
|
||||
server_state: &ServerState,
|
||||
) -> Result<bool, CommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool.acquire().await?;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
CommonWebError::from(e)
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
let user_feature_flags =
|
||||
UserSessionFeatureFlags::new(user_session.maybe_feature_flags.as_deref());
|
||||
|
||||
Ok(user_feature_flags.can_use_quicktime())
|
||||
}
|
||||
|
||||
/// Strip the given models from both the top-level model list and every
|
||||
/// provider's model list.
|
||||
fn remove_models(response: &mut OmniGenVideoModelsResponse, hidden_models: &[CommonVideoModel]) {
|
||||
|
||||
-1
@@ -77,7 +77,6 @@ mod tests {
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
quality: None,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
|
||||
-1
@@ -57,7 +57,6 @@ mod tests {
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
bitrate: None,
|
||||
maybe_output_format: None,
|
||||
quality: None,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
|
||||
-4
@@ -109,10 +109,6 @@ impl UserSessionFeatureFlags {
|
||||
pub fn can_use_minimax_priority(&self) -> bool {
|
||||
self.feature_flags.contains(&UserFeatureFlag::MinimaxPriority)
|
||||
}
|
||||
|
||||
pub fn can_use_quicktime(&self) -> bool {
|
||||
self.feature_flags.contains(&UserFeatureFlag::CanUseQuicktime)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+1
-9
@@ -35,19 +35,11 @@ environment, install dependencies, and free the dev port first):
|
||||
./script/website/unix_frontend_webapp_dev.sh # artcraft-webapp (user dashboard), port 4201
|
||||
```
|
||||
|
||||
Run the webapp from **artcraft-services**, not the separate `artcraft` desktop
|
||||
checkout. The webapp launcher also works from another directory when invoked
|
||||
by its full or relative path:
|
||||
|
||||
```bash
|
||||
../artcraft-services/script/website/unix_frontend_webapp_dev.sh
|
||||
```
|
||||
|
||||
Or directly from this directory:
|
||||
|
||||
```bash
|
||||
nx dev artcraft-website
|
||||
npx nx dev @frontend/artcraft-webapp
|
||||
nx dev artcraft-webapp
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/artcraft-icon.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/src/styles.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 116.34 97.5">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #4e7bfb;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path class="cls-1" d="M104.28,49.49L81.55,0h-31.23l-3.17,4.76L14.75,53.63,0,75.85l21.55,21.55,63.79-36.94,16.99,37.04,14.01-21.74-12.06-26.27ZM32.89,65.66l32.42-48.87,10.91,23.77-43.32,25.09Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 483 B |
@@ -1,9 +0,0 @@
|
||||
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, Navigate, Link } from "react-router-dom";
|
||||
import { LoaderCircleIcon } from "lucide-react";
|
||||
import { Outlet, useNavigate, Navigate } from "react-router-dom";
|
||||
import { ArrowLeftIcon, LoaderCircleIcon } from "lucide-react";
|
||||
import { TruchetPattern } from "@storyteller/ui-vfx";
|
||||
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,21 +18,52 @@ 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 (
|
||||
<AuthPageFrame wide>
|
||||
<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]">
|
||||
{/* 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">
|
||||
<div className={`flex flex-1 flex-col justify-center ${AUTH_FORM_PADDING}`}>
|
||||
<div className="w-full">
|
||||
<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-8 py-10 sm:px-10">
|
||||
<div className="mx-auto w-full max-w-sm">
|
||||
{authChecked ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
@@ -45,39 +76,44 @@ export const AuthLayout = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-8 text-center text-xs text-white/20">
|
||||
© {new Date().getFullYear()} ArtCraft. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Showcase pane (desktop only) */}
|
||||
{showShowcase && (
|
||||
<div className="relative border-l border-white/15 lg:w-1/2">
|
||||
<div className="relative lg:w-1/2">
|
||||
<AuthShowcase />
|
||||
</div>
|
||||
)}
|
||||
</AuthPageFrame>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface AuthHeaderProps {
|
||||
title: ReactNode;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
export const AuthHeader = ({ title, subtitle }: AuthHeaderProps) => (
|
||||
<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">
|
||||
<RevealGroup inView={false} stagger={0.08} className="mb-8 text-center">
|
||||
<Reveal y={10}>
|
||||
<img
|
||||
src="/images/artcraft-icon.png"
|
||||
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">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-sm leading-relaxed text-white/60">
|
||||
</Reveal>
|
||||
<Reveal as="p" y={10} className="text-sm text-white/60">
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
</RevealGroup>
|
||||
);
|
||||
|
||||
export const AuthFooter = ({ children }: { children: ReactNode }) => (
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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-0 overflow-hidden bg-ui-controls">
|
||||
<div className="absolute inset-2 overflow-hidden bg-black">
|
||||
{/* Muted background video (no controls) cropped to cover the pane */}
|
||||
<video
|
||||
src={FEATURED_VIDEO_URL}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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";
|
||||
@@ -15,6 +14,7 @@ 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={className}>
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
@@ -111,11 +111,19 @@ export const SignupForm = ({
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<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}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<label className={AUTH_LABEL_CLASSES}>
|
||||
<Reveal className="space-y-2">
|
||||
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
@@ -124,13 +132,13 @@ export const SignupForm = ({
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
autoFocus={autoFocus}
|
||||
inputClassName={AUTH_INPUT_CLASSES}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Reveal className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className={AUTH_LABEL_CLASSES}>
|
||||
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
@@ -140,7 +148,7 @@ export const SignupForm = ({
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Min. 8 characters"
|
||||
inputClassName={AUTH_PASSWORD_INPUT_CLASSES}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -151,9 +159,9 @@ export const SignupForm = ({
|
||||
<DynamicIcon icon={showPassword ? EyeOffIcon : EyeIcon} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div className="pt-2">
|
||||
<Reveal className="pt-2">
|
||||
<Button
|
||||
className="w-full justify-center h-10"
|
||||
type="submit"
|
||||
@@ -165,25 +173,36 @@ export const SignupForm = ({
|
||||
"Create account"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</RevealGroup>
|
||||
</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">
|
||||
{/* 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">
|
||||
or
|
||||
</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div>
|
||||
<Reveal>
|
||||
<GoogleLoginButton
|
||||
mode="signup"
|
||||
onSuccess={handleGoogleSuccess}
|
||||
onError={handleGoogleError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</RevealGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
+55
-11
@@ -1,8 +1,9 @@
|
||||
import { CreateEmptyState } from "./CreateEmptyState";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { LoaderCircleIcon } from "lucide-react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { type PopoverItem } from "@storyteller/ui-popover";
|
||||
import { TabSelector } from "@storyteller/ui-tab-selector";
|
||||
import { TruchetPattern } from "@storyteller/ui-vfx";
|
||||
import Seo from "../../components/seo";
|
||||
import { useIsMobile } from "../ui/use-mobile";
|
||||
import {
|
||||
@@ -19,7 +20,7 @@ interface CreateMediaPageShellProps {
|
||||
// SEO
|
||||
title: string;
|
||||
description: string;
|
||||
// Auth state - `authChecked` gates the initial spinner so we don't flash
|
||||
// Auth state — `authChecked` gates the initial spinner so we don't flash
|
||||
// logged-out chrome while the session resolves. Pages stay viewable for
|
||||
// logged-out users; the signup CTA modal is triggered at generate time.
|
||||
authChecked: boolean;
|
||||
@@ -40,6 +41,8 @@ interface CreateMediaPageShellProps {
|
||||
// Model selector
|
||||
modelItems: PopoverItem[];
|
||||
onModelChange: (item: PopoverItem) => void;
|
||||
// Glow orb overrides (optional - defaults provided)
|
||||
glowOrbs?: ReactNode;
|
||||
// Children slots
|
||||
gridContent: ReactNode;
|
||||
promptBox: ReactNode;
|
||||
@@ -66,6 +69,7 @@ export function CreateMediaPageShell({
|
||||
emptyStateSubtitle,
|
||||
emptyStateCta,
|
||||
bottomOffset,
|
||||
glowOrbs,
|
||||
gridContent,
|
||||
promptBox,
|
||||
promptForm,
|
||||
@@ -99,9 +103,9 @@ export function CreateMediaPageShell({
|
||||
<Seo title={title} description={description} />
|
||||
|
||||
{/* Flex (not absolute) keeps the toggle cluster from overlapping
|
||||
the tabs - the tabs stay centered via the equal flex-1 sides and
|
||||
the tabs — the tabs stay centered via the equal flex-1 sides and
|
||||
just shift left if the cluster outgrows its half. */}
|
||||
<div className="flex flex-wrap items-center gap-y-2 border-b border-ui-panel-border bg-ui-panel px-3 py-2">
|
||||
<div className="flex items-center border-b border-ui-panel-border px-3 py-2">
|
||||
<div className="flex-1" />
|
||||
<TabSelector
|
||||
tabs={MOBILE_TABS}
|
||||
@@ -128,8 +132,13 @@ export function CreateMediaPageShell({
|
||||
<div className="px-3">{gridContent}</div>
|
||||
</div>
|
||||
) : (
|
||||
<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 className="flex h-full flex-col items-center justify-center px-6 text-center">
|
||||
<span className="text-lg font-semibold text-white">
|
||||
{emptyStateTitle}
|
||||
</span>
|
||||
<span className="pt-1 text-sm text-white/60">
|
||||
Your generations will appear here.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -141,16 +150,51 @@ export function CreateMediaPageShell({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative isolate flex h-full w-full bg-ui-background text-white">
|
||||
<div className="flex h-full w-full bg-ui-background text-white">
|
||||
<Seo title={title} description={description} />
|
||||
|
||||
{/* Decorative background, empty state only. Not rendered on mobile
|
||||
devices: the truchet uses mask-image, a full-screen GPU layer that
|
||||
iOS Safari must re-rasterize when an overlay (mobile menu, modal)
|
||||
composites over it, causing multi-second hangs. (This is distinct
|
||||
from backdrop-filter.) We gate on the actual device
|
||||
(react-device-detect's user-agent check) rather than a viewport
|
||||
breakpoint so the layers never mount on a real phone/tablet, and a
|
||||
narrow desktop window still gets the decoration. */}
|
||||
{!hasContent && !isMobile && (
|
||||
<>
|
||||
{glowOrbs}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-0 z-0"
|
||||
style={{
|
||||
maskImage:
|
||||
"radial-gradient(ellipse 70% 60% at 50% 50%, black 20%, transparent 80%)",
|
||||
WebkitMaskImage:
|
||||
"radial-gradient(ellipse 70% 60% at 50% 50%, black 20%, transparent 80%)",
|
||||
}}
|
||||
>
|
||||
<TruchetPattern
|
||||
intensity={0.5}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<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 px-6" style={{ paddingBottom: bottomOffset }}>
|
||||
<CreateEmptyState title={emptyStateTitle} subtitle={emptyStateSubtitle}>
|
||||
{emptyStateCta}
|
||||
</CreateEmptyState>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="animate-fade-in-up relative z-20 mb-32 flex flex-col items-center justify-center text-center">
|
||||
<h1 className="text-5xl font-semibold text-white md:text-7xl">
|
||||
{emptyStateTitle}
|
||||
</h1>
|
||||
<span className="pt-2 text-lg text-white/80 md:text-xl">
|
||||
{emptyStateSubtitle}
|
||||
</span>
|
||||
{emptyStateCta && <div className="pt-6">{emptyStateCta}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -45,11 +45,11 @@ import {
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4 px-4 py-3 border-b border-white/10 last:border-0">
|
||||
<span className="shrink-0 pt-0.5 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10 last:border-0">
|
||||
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60">
|
||||
{label}
|
||||
</span>
|
||||
<span className="min-w-0 text-right text-sm text-white font-medium flex items-center justify-end gap-2">
|
||||
<span className="text-sm text-white font-medium flex items-center gap-2">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
@@ -426,14 +426,7 @@ export function LightboxDetails({
|
||||
{createdAt && (
|
||||
<InfoRow
|
||||
label="Created"
|
||||
value={
|
||||
<time dateTime={createdAt} className="flex flex-col items-end gap-0.5 tabular-nums">
|
||||
<span>{dayjs(createdAt).format("MMM D, YYYY")}</span>
|
||||
<span className="text-xs text-white/60 whitespace-nowrap">
|
||||
{dayjs(createdAt).format("h:mm:ss A")}
|
||||
</span>
|
||||
</time>
|
||||
}
|
||||
value={dayjs(createdAt).format("MMMM D, YYYY h:mm:ss A")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -357,7 +357,7 @@ export function Lightbox({
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="h-[90dvh] sm:h-[92dvh] w-full sm:w-[1440px] max-w-[96vw] max-h-[92dvh] p-0 border-white/15"
|
||||
className="h-[90vh] sm:h-[680px] w-full sm:w-[1100px] max-w-[95vw] max-h-[90vh] p-0 border-white/15"
|
||||
backdropClassName="!bg-black/80"
|
||||
showClose={false}
|
||||
>
|
||||
|
||||
Loaded 100 of 335 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user