mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
Added new models and modes: GPT Image 2.5, Wan, Output Formats (#1954)
This commit is contained in:
+6
@@ -6,6 +6,7 @@ use enums::common::generation::common_bitrate::CommonBitrate;
|
|||||||
use enums::common::generation::common_quality::CommonQuality;
|
use enums::common::generation::common_quality::CommonQuality;
|
||||||
use enums::common::generation::common_resolution::CommonResolution;
|
use enums::common::generation::common_resolution::CommonResolution;
|
||||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
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::characters::CharacterToken;
|
||||||
use tokens::tokens::media_files::MediaFileToken;
|
use tokens::tokens::media_files::MediaFileToken;
|
||||||
|
|
||||||
@@ -55,6 +56,11 @@ pub struct OmniGenVideoCostAndGenerateRequest {
|
|||||||
/// Not all models support this; models that don't simply ignore it.
|
/// Not all models support this; models that don't simply ignore it.
|
||||||
pub bitrate: Option<CommonBitrate>,
|
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.
|
/// The quality to use.
|
||||||
pub quality: Option<CommonQuality>,
|
pub quality: Option<CommonQuality>,
|
||||||
|
|
||||||
|
|||||||
+9
@@ -2,6 +2,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
|||||||
use enums::common::generation::common_quality::CommonQuality;
|
use enums::common::generation::common_quality::CommonQuality;
|
||||||
use enums::common::generation::common_resolution::CommonResolution;
|
use enums::common::generation::common_resolution::CommonResolution;
|
||||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
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::model_creator::ModelCreator;
|
||||||
use enums::common::generation_provider::GenerationProvider;
|
use enums::common::generation_provider::GenerationProvider;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
@@ -156,6 +157,12 @@ pub struct OmniGenVideoModelDetails {
|
|||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub bitrate_default: Option<CommonBitrate>,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub quality_options: Option<Vec<CommonQuality>>,
|
pub quality_options: Option<Vec<CommonQuality>>,
|
||||||
@@ -229,6 +236,8 @@ impl Default for OmniGenVideoModelDetails {
|
|||||||
resolution_default: None,
|
resolution_default: None,
|
||||||
bitrate_options: None,
|
bitrate_options: None,
|
||||||
bitrate_default: None,
|
bitrate_default: None,
|
||||||
|
output_format_options: None,
|
||||||
|
output_format_default: None,
|
||||||
quality_options: None,
|
quality_options: None,
|
||||||
default_quality: None,
|
default_quality: None,
|
||||||
duration_seconds_min: None,
|
duration_seconds_min: None,
|
||||||
|
|||||||
+4
@@ -158,6 +158,10 @@ pub struct OmniGenVideoModelDetails {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub bitrate_default: Option<ApiClientBitrate>,
|
pub bitrate_default: Option<ApiClientBitrate>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub output_format_options: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub output_format_default: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub quality_options: Option<Vec<ApiClientQuality>>,
|
pub quality_options: Option<Vec<ApiClientQuality>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub default_quality: Option<ApiClientQuality>,
|
pub default_quality: Option<ApiClientQuality>,
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Common quality levels you can specify when enqueuing a generation.
|
/// Common quality levels you can specify when enqueuing a generation.
|
||||||
/// Not every model will use this; models that don't simply ignore it.
|
/// Not every model will use this; models that don't simply ignore it, and models that only
|
||||||
|
/// offer a subset of tiers clamp to the nearest tier they support.
|
||||||
|
///
|
||||||
|
/// Serialized names mirror the server's `CommonQuality` so the two convert via serde.
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum RouterQuality {
|
pub enum RouterQuality {
|
||||||
|
/// Let the model choose. Models without a native "auto" use their default tier.
|
||||||
|
Auto,
|
||||||
|
/// Highest tier (GPT Image 2.5 `max`).
|
||||||
|
Max,
|
||||||
|
/// Between High and Max (GPT Image 2.5 `xhigh`).
|
||||||
|
#[serde(rename = "xhigh")]
|
||||||
|
XHigh,
|
||||||
High,
|
High,
|
||||||
Medium,
|
Medium,
|
||||||
Low,
|
Low,
|
||||||
|
|||||||
+5
-2
@@ -151,8 +151,11 @@ fn to_resolution_enum(r: RouterResolution) -> CommonResolutionEnum {
|
|||||||
|
|
||||||
fn to_quality_enum(q: RouterQuality) -> CommonQualityEnum {
|
fn to_quality_enum(q: RouterQuality) -> CommonQualityEnum {
|
||||||
match q {
|
match q {
|
||||||
RouterQuality::Low => CommonQualityEnum::Low,
|
RouterQuality::Auto => CommonQualityEnum::Auto,
|
||||||
RouterQuality::Medium => CommonQualityEnum::Medium,
|
RouterQuality::Max => CommonQualityEnum::Max,
|
||||||
|
RouterQuality::XHigh => CommonQualityEnum::XHigh,
|
||||||
RouterQuality::High => CommonQualityEnum::High,
|
RouterQuality::High => CommonQualityEnum::High,
|
||||||
|
RouterQuality::Medium => CommonQualityEnum::Medium,
|
||||||
|
RouterQuality::Low => CommonQualityEnum::Low,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -36,7 +36,7 @@ impl ArtcraftGptImage1CostState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||||
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
|
let quality = clamp_to_supported_quality(self.quality);
|
||||||
let is_square = is_square_or_none(self.aspect_ratio);
|
let is_square = is_square_or_none(self.aspect_ratio);
|
||||||
|
|
||||||
let output_cost_per_image: u64 = match (quality, is_square) {
|
let output_cost_per_image: u64 = match (quality, is_square) {
|
||||||
@@ -45,6 +45,8 @@ impl ArtcraftGptImage1CostState {
|
|||||||
(CommonQualityEnum::Medium, false) => 7,
|
(CommonQualityEnum::Medium, false) => 7,
|
||||||
(CommonQualityEnum::High, true) => 17,
|
(CommonQualityEnum::High, true) => 17,
|
||||||
(CommonQualityEnum::High, false) => 25,
|
(CommonQualityEnum::High, false) => 25,
|
||||||
|
// Clamped away above.
|
||||||
|
(CommonQualityEnum::Auto | CommonQualityEnum::XHigh | CommonQualityEnum::Max, _) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let output_cost = output_cost_per_image * self.num_images as u64;
|
let output_cost = output_cost_per_image * self.num_images as u64;
|
||||||
@@ -63,6 +65,20 @@ impl ArtcraftGptImage1CostState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GPT Image 1 only offers low / medium / high. `auto` prices as the default (high) and the
|
||||||
|
/// higher GPT Image 2.5 tiers clamp down to high, matching the Fal request planner.
|
||||||
|
fn clamp_to_supported_quality(quality: Option<CommonQualityEnum>) -> CommonQualityEnum {
|
||||||
|
match quality {
|
||||||
|
Some(CommonQualityEnum::Low) => CommonQualityEnum::Low,
|
||||||
|
Some(CommonQualityEnum::Medium) => CommonQualityEnum::Medium,
|
||||||
|
Some(CommonQualityEnum::High)
|
||||||
|
| Some(CommonQualityEnum::XHigh)
|
||||||
|
| Some(CommonQualityEnum::Max)
|
||||||
|
| Some(CommonQualityEnum::Auto)
|
||||||
|
| None => CommonQualityEnum::High,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_square_or_none(aspect_ratio: Option<CommonAspectRatioEnum>) -> bool {
|
fn is_square_or_none(aspect_ratio: Option<CommonAspectRatioEnum>) -> bool {
|
||||||
use CommonAspectRatioEnum as Ar;
|
use CommonAspectRatioEnum as Ar;
|
||||||
matches!(
|
matches!(
|
||||||
|
|||||||
+17
-1
@@ -37,7 +37,7 @@ impl ArtcraftGptImage1p5CostState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||||
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
|
let quality = clamp_to_supported_quality(self.quality);
|
||||||
let size = size_bucket(self.aspect_ratio);
|
let size = size_bucket(self.aspect_ratio);
|
||||||
|
|
||||||
let cost_per_image: u64 = match (quality, size) {
|
let cost_per_image: u64 = match (quality, size) {
|
||||||
@@ -50,6 +50,8 @@ impl ArtcraftGptImage1p5CostState {
|
|||||||
|
|
||||||
(CommonQualityEnum::High, SizeBucket::Square) => 14,
|
(CommonQualityEnum::High, SizeBucket::Square) => 14,
|
||||||
(CommonQualityEnum::High, _) => 20,
|
(CommonQualityEnum::High, _) => 20,
|
||||||
|
// Clamped away above.
|
||||||
|
(CommonQualityEnum::Auto | CommonQualityEnum::XHigh | CommonQualityEnum::Max, _) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
|
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
|
||||||
@@ -65,6 +67,20 @@ impl ArtcraftGptImage1p5CostState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GPT Image 1.5 only offers low / medium / high. `auto` prices as the default (high) and the
|
||||||
|
/// higher GPT Image 2.5 tiers clamp down to high, matching the Fal request planner.
|
||||||
|
fn clamp_to_supported_quality(quality: Option<CommonQualityEnum>) -> CommonQualityEnum {
|
||||||
|
match quality {
|
||||||
|
Some(CommonQualityEnum::Low) => CommonQualityEnum::Low,
|
||||||
|
Some(CommonQualityEnum::Medium) => CommonQualityEnum::Medium,
|
||||||
|
Some(CommonQualityEnum::High)
|
||||||
|
| Some(CommonQualityEnum::XHigh)
|
||||||
|
| Some(CommonQualityEnum::Max)
|
||||||
|
| Some(CommonQualityEnum::Auto)
|
||||||
|
| None => CommonQualityEnum::High,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn size_bucket(aspect_ratio: Option<CommonAspectRatioEnum>) -> SizeBucket {
|
fn size_bucket(aspect_ratio: Option<CommonAspectRatioEnum>) -> SizeBucket {
|
||||||
use CommonAspectRatioEnum as Ar;
|
use CommonAspectRatioEnum as Ar;
|
||||||
match aspect_ratio {
|
match aspect_ratio {
|
||||||
|
|||||||
+17
-1
@@ -42,7 +42,7 @@ impl ArtcraftGptImage2CostState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||||
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
|
let quality = clamp_to_supported_quality(self.quality);
|
||||||
let size = size_bucket(self.aspect_ratio);
|
let size = size_bucket(self.aspect_ratio);
|
||||||
|
|
||||||
let cost_per_image: u64 = match (quality, size) {
|
let cost_per_image: u64 = match (quality, size) {
|
||||||
@@ -62,6 +62,8 @@ impl ArtcraftGptImage2CostState {
|
|||||||
| (CommonQualityEnum::High, SizeBucket::Portrait4x3) => 15,
|
| (CommonQualityEnum::High, SizeBucket::Portrait4x3) => 15,
|
||||||
(CommonQualityEnum::High, SizeBucket::Landscape16x9)
|
(CommonQualityEnum::High, SizeBucket::Landscape16x9)
|
||||||
| (CommonQualityEnum::High, SizeBucket::Portrait16x9) => 16,
|
| (CommonQualityEnum::High, SizeBucket::Portrait16x9) => 16,
|
||||||
|
// Clamped away above.
|
||||||
|
(CommonQualityEnum::Auto | CommonQualityEnum::XHigh | CommonQualityEnum::Max, _) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
|
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
|
||||||
@@ -77,6 +79,20 @@ impl ArtcraftGptImage2CostState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GPT Image 2 only offers low / medium / high. `auto` prices as the default (high) and the
|
||||||
|
/// higher GPT Image 2.5 tiers clamp down to high, matching the Fal request planner.
|
||||||
|
fn clamp_to_supported_quality(quality: Option<CommonQualityEnum>) -> CommonQualityEnum {
|
||||||
|
match quality {
|
||||||
|
Some(CommonQualityEnum::Low) => CommonQualityEnum::Low,
|
||||||
|
Some(CommonQualityEnum::Medium) => CommonQualityEnum::Medium,
|
||||||
|
Some(CommonQualityEnum::High)
|
||||||
|
| Some(CommonQualityEnum::XHigh)
|
||||||
|
| Some(CommonQualityEnum::Max)
|
||||||
|
| Some(CommonQualityEnum::Auto)
|
||||||
|
| None => CommonQualityEnum::High,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Mirrors v1's `plan_image_size` for GPT Image 2 — see the v1 file
|
/// Mirrors v1's `plan_image_size` for GPT Image 2 — see the v1 file
|
||||||
/// `plan_generate_image_artcraft_gpt_image_2.rs::plan_image_size`.
|
/// `plan_generate_image_artcraft_gpt_image_2.rs::plan_image_size`.
|
||||||
fn size_bucket(aspect_ratio: Option<CommonAspectRatioEnum>) -> SizeBucket {
|
fn size_bucket(aspect_ratio: Option<CommonAspectRatioEnum>) -> SizeBucket {
|
||||||
|
|||||||
+58
-20
@@ -8,25 +8,27 @@ use fal_client::requests::api::image::common::gpt_image_2p5::gpt_image_2p5_image
|
|||||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||||
|
|
||||||
/// Artcraft price per output image, in hundredths of a US cent, by output dimensions and
|
/// Artcraft price per output image, in hundredths of a US cent, by output dimensions and
|
||||||
/// quality (`[low, medium, high]`). Rows are stored landscape-first; portrait orientations of
|
/// quality (`[low, medium, high, xhigh, max]`). Each cell is the `fal_client` GPT Image 2.5
|
||||||
/// the same dimensions price identically. Dimensions come from the aspect ratio preset and
|
/// estimate for that size and tier with a 10% markup, rounded up. Rows are stored
|
||||||
/// resolution tier the same way the Fal request builder derives them.
|
/// landscape-first; portrait orientations of the same dimensions price identically.
|
||||||
const OUTPUT_IMAGE_PRICE_TABLE: &[(u32, u32, [u64; 3])] = &[
|
/// Dimensions come from the aspect ratio preset and resolution tier the same way the Fal
|
||||||
|
/// request builder derives them.
|
||||||
|
const OUTPUT_IMAGE_PRICE_TABLE: &[(u32, u32, [u64; 5])] = &[
|
||||||
// 1:1
|
// 1:1
|
||||||
(1024, 1024, [ 65, 152, 582]),
|
(1024, 1024, [ 65, 152, 582, 1034, 2327]),
|
||||||
(2048, 2048, [103, 239, 919]),
|
(2048, 2048, [103, 239, 919, 1633, 3672]),
|
||||||
(2880, 2880, [151, 352, 1357]),
|
(2880, 2880, [151, 352, 1357, 2412, 5426]),
|
||||||
// 4:3
|
// 4:3
|
||||||
(1024, 768, [ 46, 105, 400]),
|
(1024, 768, [ 46, 105, 400, 710, 1597]),
|
||||||
(2048, 1536, [ 73, 170, 652]),
|
(2048, 1536, [ 73, 170, 652, 1159, 2605]),
|
||||||
(3072, 2304, [120, 279, 1073]),
|
(3072, 2304, [120, 279, 1073, 1906, 4287]),
|
||||||
(3312, 2480, [134, 311, 1194]),
|
(3312, 2480, [134, 311, 1194, 2122, 4773]),
|
||||||
// 16:9
|
// 16:9
|
||||||
(1088, 608, [ 32, 75, 285]),
|
(1088, 608, [ 32, 75, 285, 506, 1139]),
|
||||||
(1920, 1080, [ 49, 114, 436]),
|
(1920, 1080, [ 49, 114, 436, 775, 1743]),
|
||||||
(2048, 1152, [ 53, 121, 467]),
|
(2048, 1152, [ 53, 121, 467, 830, 1865]),
|
||||||
(3072, 1728, [ 87, 204, 783]),
|
(3072, 1728, [ 87, 204, 783, 1390, 3127]),
|
||||||
(3840, 2160, [124, 286, 1102]),
|
(3840, 2160, [124, 286, 1102, 1957, 4404]),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Artcraft price per reference image on an edit request, in hundredths of a US cent.
|
/// Artcraft price per reference image on an edit request, in hundredths of a US cent.
|
||||||
@@ -55,7 +57,8 @@ impl ArtcraftGptImage2p5CostInputs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Per-image price rounded up to whole cents, times the batch size. Quality defaults to
|
/// Per-image price rounded up to whole cents, times the batch size. Quality defaults to
|
||||||
/// high. Without an aspect ratio, text-to-image prices as landscape 4:3 and edit as auto,
|
/// high, and `auto` is priced as high (the documented default the model falls back to).
|
||||||
|
/// Without an aspect ratio, text-to-image prices as landscape 4:3 and edit as auto,
|
||||||
/// matching the Fal defaults the request would fall back to.
|
/// matching the Fal defaults the request would fall back to.
|
||||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||||
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
|
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
|
||||||
@@ -91,11 +94,13 @@ fn output_image_price(quality: CommonQualityEnum, width: u32, height: u32) -> u6
|
|||||||
match quality {
|
match quality {
|
||||||
CommonQualityEnum::Low => prices[0],
|
CommonQualityEnum::Low => prices[0],
|
||||||
CommonQualityEnum::Medium => prices[1],
|
CommonQualityEnum::Medium => prices[1],
|
||||||
CommonQualityEnum::High => prices[2],
|
CommonQualityEnum::High | CommonQualityEnum::Auto => prices[2],
|
||||||
|
CommonQualityEnum::XHigh => prices[3],
|
||||||
|
CommonQualityEnum::Max => prices[4],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lookup_price_row(width: u32, height: u32) -> Option<[u64; 3]> {
|
fn lookup_price_row(width: u32, height: u32) -> Option<[u64; 5]> {
|
||||||
let long_edge = width.max(height);
|
let long_edge = width.max(height);
|
||||||
let short_edge = width.min(height);
|
let short_edge = width.min(height);
|
||||||
OUTPUT_IMAGE_PRICE_TABLE.iter()
|
OUTPUT_IMAGE_PRICE_TABLE.iter()
|
||||||
@@ -176,6 +181,18 @@ mod tests {
|
|||||||
(Some(Ar::Auto4k), Some(R::OneK), [2, 3, 12]),
|
(Some(Ar::Auto4k), Some(R::OneK), [2, 3, 12]),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// (aspect_ratio, resolution, [xhigh, max]) — cents per image, text-to-image
|
||||||
|
const TEXT_GRID_UPPER_TIERS: &[(Option<Ar>, Option<R>, [u64; 2])] = &[
|
||||||
|
(None, None, [ 8, 16]), // landscape 4:3 default
|
||||||
|
(Some(Ar::Square), None, [11, 24]),
|
||||||
|
(Some(Ar::Square), Some(R::TwoK), [17, 37]),
|
||||||
|
(Some(Ar::Square), Some(R::FourK), [25, 55]),
|
||||||
|
(Some(Ar::WideSixteenByNine), None, [ 8, 18]),
|
||||||
|
(Some(Ar::WideSixteenByNine), Some(R::OneK), [ 6, 12]),
|
||||||
|
(Some(Ar::TallNineBySixteen), Some(R::FourK), [20, 45]),
|
||||||
|
(Some(Ar::Auto), None, [20, 45]),
|
||||||
|
];
|
||||||
|
|
||||||
mod text_to_image_tests {
|
mod text_to_image_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -189,11 +206,32 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn upper_tier_grid() {
|
||||||
|
for &(aspect_ratio, resolution, expected) in TEXT_GRID_UPPER_TIERS {
|
||||||
|
for (quality, expected) in [Q::XHigh, Q::Max].into_iter().zip(expected) {
|
||||||
|
let actual = cents(make_inputs(0, Some(quality), aspect_ratio, resolution, 1));
|
||||||
|
assert_eq!(actual, expected, "{aspect_ratio:?} {resolution:?} {quality:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quality_defaults_to_high() {
|
fn quality_defaults_to_high() {
|
||||||
assert_eq!(cents(make_inputs(0, None, Some(Ar::Square), None, 1)), 6);
|
assert_eq!(cents(make_inputs(0, None, Some(Ar::Square), None, 1)), 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_prices_as_high() {
|
||||||
|
for &(aspect_ratio, resolution, _) in TEXT_GRID {
|
||||||
|
assert_eq!(
|
||||||
|
cents(make_inputs(0, Some(Q::Auto), aspect_ratio, resolution, 1)),
|
||||||
|
cents(make_inputs(0, Some(Q::High), aspect_ratio, resolution, 1)),
|
||||||
|
"{aspect_ratio:?} {resolution:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn batch_multiplies_per_image_cents() {
|
fn batch_multiplies_per_image_cents() {
|
||||||
for &(aspect_ratio, resolution, _) in TEXT_GRID {
|
for &(aspect_ratio, resolution, _) in TEXT_GRID {
|
||||||
@@ -265,7 +303,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn prices_rise_with_quality_and_size() {
|
fn prices_rise_with_quality_and_size() {
|
||||||
for &(_, _, prices) in OUTPUT_IMAGE_PRICE_TABLE {
|
for &(_, _, prices) in OUTPUT_IMAGE_PRICE_TABLE {
|
||||||
assert!(prices[0] < prices[1] && prices[1] < prices[2], "{prices:?}");
|
assert!(prices.windows(2).all(|pair| pair[0] < pair[1]), "{prices:?}");
|
||||||
}
|
}
|
||||||
assert!(output_image_price(Q::High, 1024, 1024) < output_image_price(Q::High, 2048, 2048));
|
assert!(output_image_price(Q::High, 1024, 1024) < output_image_price(Q::High, 2048, 2048));
|
||||||
assert!(output_image_price(Q::High, 2048, 2048) < output_image_price(Q::High, 2880, 2880));
|
assert!(output_image_price(Q::High, 2048, 2048) < output_image_price(Q::High, 2880, 2880));
|
||||||
|
|||||||
+11
-1
@@ -100,11 +100,17 @@ fn plan_num_images(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This endpoint only offers low / medium / high. `auto` falls back to Fal's default (`high`)
|
||||||
|
/// and the higher GPT Image 2.5 tiers (`xhigh`, `max`) clamp down to `high`.
|
||||||
fn plan_quality(quality: Option<RouterQuality>) -> PlannedQuality {
|
fn plan_quality(quality: Option<RouterQuality>) -> PlannedQuality {
|
||||||
match quality {
|
match quality {
|
||||||
Some(RouterQuality::Low) => PlannedQuality::Low,
|
Some(RouterQuality::Low) => PlannedQuality::Low,
|
||||||
Some(RouterQuality::Medium) => PlannedQuality::Medium,
|
Some(RouterQuality::Medium) => PlannedQuality::Medium,
|
||||||
Some(RouterQuality::High) | None => PlannedQuality::High,
|
Some(RouterQuality::High)
|
||||||
|
| Some(RouterQuality::XHigh)
|
||||||
|
| Some(RouterQuality::Max)
|
||||||
|
| Some(RouterQuality::Auto)
|
||||||
|
| None => PlannedQuality::High,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,6 +343,10 @@ mod tests {
|
|||||||
(Some(RouterQuality::Low), "Some(Low)"),
|
(Some(RouterQuality::Low), "Some(Low)"),
|
||||||
(Some(RouterQuality::Medium), "Some(Medium)"),
|
(Some(RouterQuality::Medium), "Some(Medium)"),
|
||||||
(Some(RouterQuality::High), "Some(High)"),
|
(Some(RouterQuality::High), "Some(High)"),
|
||||||
|
// Tiers this endpoint lacks clamp to high.
|
||||||
|
(Some(RouterQuality::Auto), "Some(High)"),
|
||||||
|
(Some(RouterQuality::XHigh), "Some(High)"),
|
||||||
|
(Some(RouterQuality::Max), "Some(High)"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (quality, expected) in cases {
|
for (quality, expected) in cases {
|
||||||
|
|||||||
+11
-1
@@ -100,11 +100,17 @@ fn plan_num_images(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This endpoint only offers low / medium / high. `auto` falls back to Fal's default (`high`)
|
||||||
|
/// and the higher GPT Image 2.5 tiers (`xhigh`, `max`) clamp down to `high`.
|
||||||
fn plan_quality(quality: Option<RouterQuality>) -> PlannedQuality {
|
fn plan_quality(quality: Option<RouterQuality>) -> PlannedQuality {
|
||||||
match quality {
|
match quality {
|
||||||
Some(RouterQuality::Low) => PlannedQuality::Low,
|
Some(RouterQuality::Low) => PlannedQuality::Low,
|
||||||
Some(RouterQuality::Medium) => PlannedQuality::Medium,
|
Some(RouterQuality::Medium) => PlannedQuality::Medium,
|
||||||
Some(RouterQuality::High) | None => PlannedQuality::High,
|
Some(RouterQuality::High)
|
||||||
|
| Some(RouterQuality::XHigh)
|
||||||
|
| Some(RouterQuality::Max)
|
||||||
|
| Some(RouterQuality::Auto)
|
||||||
|
| None => PlannedQuality::High,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,6 +343,10 @@ mod tests {
|
|||||||
(Some(RouterQuality::Low), "Some(Low)"),
|
(Some(RouterQuality::Low), "Some(Low)"),
|
||||||
(Some(RouterQuality::Medium), "Some(Medium)"),
|
(Some(RouterQuality::Medium), "Some(Medium)"),
|
||||||
(Some(RouterQuality::High), "Some(High)"),
|
(Some(RouterQuality::High), "Some(High)"),
|
||||||
|
// Tiers this endpoint lacks clamp to high.
|
||||||
|
(Some(RouterQuality::Auto), "Some(High)"),
|
||||||
|
(Some(RouterQuality::XHigh), "Some(High)"),
|
||||||
|
(Some(RouterQuality::Max), "Some(High)"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (quality, expected) in cases {
|
for (quality, expected) in cases {
|
||||||
|
|||||||
+11
-1
@@ -106,11 +106,17 @@ fn plan_num_images(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This endpoint only offers low / medium / high. `auto` falls back to Fal's default (`high`)
|
||||||
|
/// and the higher GPT Image 2.5 tiers (`xhigh`, `max`) clamp down to `high`.
|
||||||
fn plan_quality(quality: Option<RouterQuality>) -> PlannedQuality {
|
fn plan_quality(quality: Option<RouterQuality>) -> PlannedQuality {
|
||||||
match quality {
|
match quality {
|
||||||
Some(RouterQuality::Low) => PlannedQuality::Low,
|
Some(RouterQuality::Low) => PlannedQuality::Low,
|
||||||
Some(RouterQuality::Medium) => PlannedQuality::Medium,
|
Some(RouterQuality::Medium) => PlannedQuality::Medium,
|
||||||
Some(RouterQuality::High) | None => PlannedQuality::High,
|
Some(RouterQuality::High)
|
||||||
|
| Some(RouterQuality::XHigh)
|
||||||
|
| Some(RouterQuality::Max)
|
||||||
|
| Some(RouterQuality::Auto)
|
||||||
|
| None => PlannedQuality::High,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +368,10 @@ mod tests {
|
|||||||
(Some(RouterQuality::Low), "Some(Low)"),
|
(Some(RouterQuality::Low), "Some(Low)"),
|
||||||
(Some(RouterQuality::Medium), "Some(Medium)"),
|
(Some(RouterQuality::Medium), "Some(Medium)"),
|
||||||
(Some(RouterQuality::High), "Some(High)"),
|
(Some(RouterQuality::High), "Some(High)"),
|
||||||
|
// Tiers this endpoint lacks clamp to high.
|
||||||
|
(Some(RouterQuality::Auto), "Some(High)"),
|
||||||
|
(Some(RouterQuality::XHigh), "Some(High)"),
|
||||||
|
(Some(RouterQuality::Max), "Some(High)"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (quality, expected) in cases {
|
for (quality, expected) in cases {
|
||||||
|
|||||||
+12
-6
@@ -82,13 +82,16 @@ fn plan_num_images(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The router only exposes low / medium / high; GPT Image 2.5's `xhigh` and `max` tiers are
|
/// GPT Image 2.5 supports every router tier natively. Fal's default (`high`) applies when
|
||||||
/// not reachable through it. Fal's default (`high`) applies when unspecified.
|
/// unspecified.
|
||||||
fn plan_quality(quality: Option<RouterQuality>) -> GptImage2p5Quality {
|
fn plan_quality(quality: Option<RouterQuality>) -> GptImage2p5Quality {
|
||||||
match quality {
|
match quality {
|
||||||
Some(RouterQuality::Low) => GptImage2p5Quality::Low,
|
Some(RouterQuality::Auto) => GptImage2p5Quality::Auto,
|
||||||
Some(RouterQuality::Medium) => GptImage2p5Quality::Medium,
|
Some(RouterQuality::Max) => GptImage2p5Quality::Max,
|
||||||
|
Some(RouterQuality::XHigh) => GptImage2p5Quality::XHigh,
|
||||||
Some(RouterQuality::High) | None => GptImage2p5Quality::High,
|
Some(RouterQuality::High) | None => GptImage2p5Quality::High,
|
||||||
|
Some(RouterQuality::Medium) => GptImage2p5Quality::Medium,
|
||||||
|
Some(RouterQuality::Low) => GptImage2p5Quality::Low,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,9 +223,12 @@ mod tests {
|
|||||||
fn quality_maps_exhaustively_for_text_and_edit() {
|
fn quality_maps_exhaustively_for_text_and_edit() {
|
||||||
let cases = [
|
let cases = [
|
||||||
(None, "Some(High)"),
|
(None, "Some(High)"),
|
||||||
(Some(RouterQuality::Low), "Some(Low)"),
|
(Some(RouterQuality::Auto), "Some(Auto)"),
|
||||||
(Some(RouterQuality::Medium), "Some(Medium)"),
|
(Some(RouterQuality::Max), "Some(Max)"),
|
||||||
|
(Some(RouterQuality::XHigh), "Some(XHigh)"),
|
||||||
(Some(RouterQuality::High), "Some(High)"),
|
(Some(RouterQuality::High), "Some(High)"),
|
||||||
|
(Some(RouterQuality::Medium), "Some(Medium)"),
|
||||||
|
(Some(RouterQuality::Low), "Some(Low)"),
|
||||||
];
|
];
|
||||||
for (quality, expected) in cases {
|
for (quality, expected) in cases {
|
||||||
let text = unwrap_t2i(plan_gpt_image_2p5_params(&GenerateImageRequestBuilder { quality, ..base_builder() }));
|
let text = unwrap_t2i(plan_gpt_image_2p5_params(&GenerateImageRequestBuilder { quality, ..base_builder() }));
|
||||||
|
|||||||
+3
@@ -53,6 +53,9 @@ mod tests {
|
|||||||
(Some(Q::Low), Some(Ar::Square), None, 1, 1),
|
(Some(Q::Low), Some(Ar::Square), None, 1, 1),
|
||||||
(Some(Q::Medium), Some(Ar::Square), None, 1, 2),
|
(Some(Q::Medium), Some(Ar::Square), None, 1, 2),
|
||||||
(Some(Q::High), Some(Ar::Square), None, 1, 6),
|
(Some(Q::High), Some(Ar::Square), None, 1, 6),
|
||||||
|
(Some(Q::Auto), Some(Ar::Square), None, 1, 6), // auto is priced as high
|
||||||
|
(Some(Q::XHigh), Some(Ar::Square), None, 1, 10),
|
||||||
|
(Some(Q::Max), Some(Ar::Square), None, 1, 22),
|
||||||
(Some(Q::High), Some(Ar::Square), None, 4, 24),
|
(Some(Q::High), Some(Ar::Square), None, 4, 24),
|
||||||
(Some(Q::High), Some(Ar::SquareHd), None, 1, 9),
|
(Some(Q::High), Some(Ar::SquareHd), None, 1, 9),
|
||||||
(Some(Q::High), Some(Ar::Square), Some(R::TwoK), 1, 9),
|
(Some(Q::High), Some(Ar::Square), Some(R::TwoK), 1, 9),
|
||||||
|
|||||||
+3
@@ -53,6 +53,9 @@ mod tests {
|
|||||||
(Some(Q::Low), Some(Ar::Square), None, 1, 1),
|
(Some(Q::Low), Some(Ar::Square), None, 1, 1),
|
||||||
(Some(Q::Medium), Some(Ar::Square), None, 1, 2),
|
(Some(Q::Medium), Some(Ar::Square), None, 1, 2),
|
||||||
(Some(Q::High), Some(Ar::Square), None, 1, 6),
|
(Some(Q::High), Some(Ar::Square), None, 1, 6),
|
||||||
|
(Some(Q::Auto), Some(Ar::Square), None, 1, 6), // auto is priced as high
|
||||||
|
(Some(Q::XHigh), Some(Ar::Square), None, 1, 10),
|
||||||
|
(Some(Q::Max), Some(Ar::Square), None, 1, 22),
|
||||||
(Some(Q::High), Some(Ar::Square), None, 4, 24),
|
(Some(Q::High), Some(Ar::Square), None, 4, 24),
|
||||||
(Some(Q::High), Some(Ar::SquareHd), None, 1, 9),
|
(Some(Q::High), Some(Ar::SquareHd), None, 1, 9),
|
||||||
(Some(Q::High), Some(Ar::Square), Some(R::TwoK), 1, 9),
|
(Some(Q::High), Some(Ar::Square), Some(R::TwoK), 1, 9),
|
||||||
|
|||||||
+6
-2
@@ -122,10 +122,14 @@ pub(crate) fn plan_quality(quality: Option<RouterQuality>) -> Option<GenerateMid
|
|||||||
// Kinovi pricing — pass through whatever the caller asked for, and let
|
// Kinovi pricing — pass through whatever the caller asked for, and let
|
||||||
// `None` ride Midjourney's server-side default.
|
// `None` ride Midjourney's server-side default.
|
||||||
match quality {
|
match quality {
|
||||||
None => None,
|
// `auto` has no Midjourney equivalent; leave it to the provider default.
|
||||||
|
None | Some(RouterQuality::Auto) => None,
|
||||||
Some(RouterQuality::Low) => Some(GenerateMidjourneyV7Quality::Quarter),
|
Some(RouterQuality::Low) => Some(GenerateMidjourneyV7Quality::Quarter),
|
||||||
Some(RouterQuality::Medium) => Some(GenerateMidjourneyV7Quality::Half),
|
Some(RouterQuality::Medium) => Some(GenerateMidjourneyV7Quality::Half),
|
||||||
Some(RouterQuality::High) => Some(GenerateMidjourneyV7Quality::Full),
|
// Midjourney tops out at full quality; the higher GPT Image tiers clamp to it.
|
||||||
|
Some(RouterQuality::High)
|
||||||
|
| Some(RouterQuality::XHigh)
|
||||||
|
| Some(RouterQuality::Max) => Some(GenerateMidjourneyV7Quality::Full),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -110,10 +110,14 @@ pub(crate) fn plan_aspect_ratio(
|
|||||||
|
|
||||||
pub(crate) fn plan_quality(quality: Option<RouterQuality>) -> Option<GenerateMidjourneyV7NijiQuality> {
|
pub(crate) fn plan_quality(quality: Option<RouterQuality>) -> Option<GenerateMidjourneyV7NijiQuality> {
|
||||||
match quality {
|
match quality {
|
||||||
None => None,
|
// `auto` has no Midjourney equivalent; leave it to the provider default.
|
||||||
|
None | Some(RouterQuality::Auto) => None,
|
||||||
Some(RouterQuality::Low) => Some(GenerateMidjourneyV7NijiQuality::Quarter),
|
Some(RouterQuality::Low) => Some(GenerateMidjourneyV7NijiQuality::Quarter),
|
||||||
Some(RouterQuality::Medium) => Some(GenerateMidjourneyV7NijiQuality::Half),
|
Some(RouterQuality::Medium) => Some(GenerateMidjourneyV7NijiQuality::Half),
|
||||||
Some(RouterQuality::High) => Some(GenerateMidjourneyV7NijiQuality::Full),
|
// Midjourney tops out at full quality; the higher GPT Image tiers clamp to it.
|
||||||
|
Some(RouterQuality::High)
|
||||||
|
| Some(RouterQuality::XHigh)
|
||||||
|
| Some(RouterQuality::Max) => Some(GenerateMidjourneyV7NijiQuality::Full),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -110,10 +110,14 @@ pub(crate) fn plan_aspect_ratio(
|
|||||||
|
|
||||||
pub(crate) fn plan_quality(quality: Option<RouterQuality>) -> Option<GenerateMidjourneyV8Quality> {
|
pub(crate) fn plan_quality(quality: Option<RouterQuality>) -> Option<GenerateMidjourneyV8Quality> {
|
||||||
match quality {
|
match quality {
|
||||||
None => None,
|
// `auto` has no Midjourney equivalent; leave it to the provider default.
|
||||||
|
None | Some(RouterQuality::Auto) => None,
|
||||||
Some(RouterQuality::Low) => Some(GenerateMidjourneyV8Quality::Quarter),
|
Some(RouterQuality::Low) => Some(GenerateMidjourneyV8Quality::Quarter),
|
||||||
Some(RouterQuality::Medium) => Some(GenerateMidjourneyV8Quality::Half),
|
Some(RouterQuality::Medium) => Some(GenerateMidjourneyV8Quality::Half),
|
||||||
Some(RouterQuality::High) => Some(GenerateMidjourneyV8Quality::Full),
|
// Midjourney tops out at full quality; the higher GPT Image tiers clamp to it.
|
||||||
|
Some(RouterQuality::High)
|
||||||
|
| Some(RouterQuality::XHigh)
|
||||||
|
| Some(RouterQuality::Max) => Some(GenerateMidjourneyV8Quality::Full),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
@@ -77,6 +77,7 @@ pub fn build_artcraft_omni_video_request(
|
|||||||
resolution,
|
resolution,
|
||||||
aspect_ratio,
|
aspect_ratio,
|
||||||
bitrate,
|
bitrate,
|
||||||
|
maybe_output_format: None,
|
||||||
duration_seconds: duration_seconds.map(|d| d as u16),
|
duration_seconds: duration_seconds.map(|d| d as u16),
|
||||||
video_batch_count: Some(batch_count),
|
video_batch_count: Some(batch_count),
|
||||||
negative_prompt: None,
|
negative_prompt: None,
|
||||||
|
|||||||
+1
@@ -59,6 +59,7 @@ mod tests {
|
|||||||
resolution: None,
|
resolution: None,
|
||||||
aspect_ratio: None,
|
aspect_ratio: None,
|
||||||
bitrate: None,
|
bitrate: None,
|
||||||
|
maybe_output_format: None,
|
||||||
quality: None,
|
quality: None,
|
||||||
duration_seconds: Some(5),
|
duration_seconds: Some(5),
|
||||||
video_batch_count: Some(1),
|
video_batch_count: Some(1),
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ use artcraft_router::api::router_quality::RouterQuality;
|
|||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum CommonQuality {
|
pub enum CommonQuality {
|
||||||
|
Auto,
|
||||||
|
Max,
|
||||||
|
#[serde(rename = "xhigh")]
|
||||||
|
XHigh,
|
||||||
High,
|
High,
|
||||||
Medium,
|
Medium,
|
||||||
Low,
|
Low,
|
||||||
@@ -13,6 +17,9 @@ pub enum CommonQuality {
|
|||||||
impl CommonQuality {
|
impl CommonQuality {
|
||||||
pub fn to_artcraft_router_type(&self) -> RouterQuality {
|
pub fn to_artcraft_router_type(&self) -> RouterQuality {
|
||||||
match self {
|
match self {
|
||||||
|
CommonQuality::Auto => RouterQuality::Auto,
|
||||||
|
CommonQuality::Max => RouterQuality::Max,
|
||||||
|
CommonQuality::XHigh => RouterQuality::XHigh,
|
||||||
CommonQuality::High => RouterQuality::High,
|
CommonQuality::High => RouterQuality::High,
|
||||||
CommonQuality::Medium => RouterQuality::Medium,
|
CommonQuality::Medium => RouterQuality::Medium,
|
||||||
CommonQuality::Low => RouterQuality::Low,
|
CommonQuality::Low => RouterQuality::Low,
|
||||||
|
|||||||
+3
@@ -30,6 +30,9 @@ pub fn convert_aspect_ratio(value: EnumsAspectRatio) -> RouterAspectRatio {
|
|||||||
|
|
||||||
pub fn convert_quality(value: EnumsQuality) -> RouterQuality {
|
pub fn convert_quality(value: EnumsQuality) -> RouterQuality {
|
||||||
match value {
|
match value {
|
||||||
|
EnumsQuality::Auto => RouterQuality::Auto,
|
||||||
|
EnumsQuality::Max => RouterQuality::Max,
|
||||||
|
EnumsQuality::XHigh => RouterQuality::XHigh,
|
||||||
EnumsQuality::High => RouterQuality::High,
|
EnumsQuality::High => RouterQuality::High,
|
||||||
EnumsQuality::Medium => RouterQuality::Medium,
|
EnumsQuality::Medium => RouterQuality::Medium,
|
||||||
EnumsQuality::Low => RouterQuality::Low,
|
EnumsQuality::Low => RouterQuality::Low,
|
||||||
|
|||||||
@@ -552,3 +552,25 @@ impl From<OmniGenImageModelDetails> for ListImageModelsModelDetails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod catalog_parity_tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gpt_image_2p5_quality_tiers_survive_the_desktop_listing_boundary() {
|
||||||
|
for model in ["gpt_image_2p5_flare", "gpt_image_2p5_sunburst"] {
|
||||||
|
let source = json!({
|
||||||
|
"model": model,
|
||||||
|
"quality_options": ["auto", "max", "xhigh", "high", "medium", "low", "future_quality"],
|
||||||
|
"default_quality": "high",
|
||||||
|
"resolution_options": ["one_k", "two_k", "three_k", "four_k"],
|
||||||
|
"resolution_default": "one_k"
|
||||||
|
});
|
||||||
|
let client: OmniGenImageModelDetails = serde_json::from_value(source.clone()).unwrap();
|
||||||
|
let desktop = serde_json::to_value(ListImageModelsModelDetails::from(client)).unwrap();
|
||||||
|
assert_eq!(desktop, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -597,6 +597,10 @@ pub struct ListVideoModelsModelDetails {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub bitrate_default: Option<ListVideoModelsBitrate>,
|
pub bitrate_default: Option<ListVideoModelsBitrate>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub output_format_options: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub output_format_default: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub quality_options: Option<Vec<ListVideoModelsQuality>>,
|
pub quality_options: Option<Vec<ListVideoModelsQuality>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub default_quality: Option<ListVideoModelsQuality>,
|
pub default_quality: Option<ListVideoModelsQuality>,
|
||||||
@@ -655,6 +659,8 @@ impl From<OmniGenVideoModelDetails> for ListVideoModelsModelDetails {
|
|||||||
resolution_default: v.resolution_default.map(Into::into),
|
resolution_default: v.resolution_default.map(Into::into),
|
||||||
bitrate_options: v.bitrate_options.map(|items| items.into_iter().map(Into::into).collect()),
|
bitrate_options: v.bitrate_options.map(|items| items.into_iter().map(Into::into).collect()),
|
||||||
bitrate_default: v.bitrate_default.map(Into::into),
|
bitrate_default: v.bitrate_default.map(Into::into),
|
||||||
|
output_format_options: v.output_format_options,
|
||||||
|
output_format_default: v.output_format_default,
|
||||||
quality_options: v.quality_options.map(|items| items.into_iter().map(Into::into).collect()),
|
quality_options: v.quality_options.map(|items| items.into_iter().map(Into::into).collect()),
|
||||||
default_quality: v.default_quality.map(Into::into),
|
default_quality: v.default_quality.map(Into::into),
|
||||||
duration_seconds_min: v.duration_seconds_min,
|
duration_seconds_min: v.duration_seconds_min,
|
||||||
@@ -670,3 +676,26 @@ impl From<OmniGenVideoModelDetails> for ListVideoModelsModelDetails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod catalog_parity_tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wan_and_output_formats_survive_the_desktop_listing_boundary() {
|
||||||
|
for model in ["wan_3p0", "wan_3p0_prime", "seedance_2p5", "seedance_2p5_u", "future_video"] {
|
||||||
|
let source = json!({
|
||||||
|
"model": model,
|
||||||
|
"output_format_options": ["mp4", "mov", "future_container"],
|
||||||
|
"output_format_default": "mp4",
|
||||||
|
"batch_size_options": [1, 2, 3, 4],
|
||||||
|
"duration_seconds_max": 30,
|
||||||
|
"video_references_max": 5
|
||||||
|
});
|
||||||
|
let client: OmniGenVideoModelDetails = serde_json::from_value(source.clone()).unwrap();
|
||||||
|
let desktop = serde_json::to_value(ListVideoModelsModelDetails::from(client)).unwrap();
|
||||||
|
assert_eq!(desktop, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,6 +123,31 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn catalog_parity_new_models_and_options_reach_the_api_unchanged() {
|
||||||
|
for model in ["gpt_image_2p5_flare", "gpt_image_2p5_sunburst"] {
|
||||||
|
for quality in ["auto", "max", "xhigh", "high", "medium", "low"] {
|
||||||
|
let source = json!({"model": model, "quality": quality, "resolution": "four_k"});
|
||||||
|
let request: OmniRequest = serde_json::from_value(source.clone()).unwrap();
|
||||||
|
assert!(request.uses_artcraft());
|
||||||
|
assert!(!request.uses_legacy_image_endpoint());
|
||||||
|
assert_eq!(Value::Object(request.api_fields(Modality::Image)), source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for model in ["wan_3p0", "wan_3p0_prime", "seedance_2p5", "seedance_2p5_u"] {
|
||||||
|
let source = json!({
|
||||||
|
"model": model, "duration_seconds": 15, "resolution": "ten_eighty_p",
|
||||||
|
"output_format": "mov", "generate_audio": true,
|
||||||
|
"reference_image_media_tokens": ["mf_image"],
|
||||||
|
"reference_video_media_tokens": ["mf_video"],
|
||||||
|
"reference_audio_media_tokens": ["mf_audio"]
|
||||||
|
});
|
||||||
|
let request: OmniRequest = serde_json::from_value(source.clone()).unwrap();
|
||||||
|
assert!(request.uses_artcraft());
|
||||||
|
assert_eq!(Value::Object(request.api_fields(Modality::Video)), source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn future_model_and_options_survive_without_desktop_metadata() {
|
fn future_model_and_options_survive_without_desktop_metadata() {
|
||||||
let request: OmniRequest = serde_json::from_value(json!({
|
let request: OmniRequest = serde_json::from_value(json!({
|
||||||
|
|||||||
+20
@@ -192,6 +192,12 @@ pub enum InferenceModelType {
|
|||||||
Kling3p0Pro,
|
Kling3p0Pro,
|
||||||
#[serde(rename = "happy_horse_1p0")]
|
#[serde(rename = "happy_horse_1p0")]
|
||||||
HappyHorse1p0,
|
HappyHorse1p0,
|
||||||
|
|
||||||
|
#[serde(rename = "wan_3p0")]
|
||||||
|
Wan3p0,
|
||||||
|
|
||||||
|
#[serde(rename = "wan_3p0_prime")]
|
||||||
|
Wan3p0Prime,
|
||||||
#[serde(rename = "minimax_h3")]
|
#[serde(rename = "minimax_h3")]
|
||||||
MinimaxH3,
|
MinimaxH3,
|
||||||
#[serde(rename = "minimax_h3_turbo")]
|
#[serde(rename = "minimax_h3_turbo")]
|
||||||
@@ -394,6 +400,8 @@ impl InferenceModelType {
|
|||||||
Self::Kling3p0Standard => "kling_3p0_standard",
|
Self::Kling3p0Standard => "kling_3p0_standard",
|
||||||
Self::Kling3p0Pro => "kling_3p0_pro",
|
Self::Kling3p0Pro => "kling_3p0_pro",
|
||||||
Self::HappyHorse1p0 => "happy_horse_1p0",
|
Self::HappyHorse1p0 => "happy_horse_1p0",
|
||||||
|
Self::Wan3p0 => "wan_3p0",
|
||||||
|
Self::Wan3p0Prime => "wan_3p0_prime",
|
||||||
Self::MinimaxH3 => "minimax_h3",
|
Self::MinimaxH3 => "minimax_h3",
|
||||||
Self::MinimaxH3Turbo => "minimax_h3_turbo",
|
Self::MinimaxH3Turbo => "minimax_h3_turbo",
|
||||||
Self::MinimaxH3Ultra => "minimax_h3_ultra",
|
Self::MinimaxH3Ultra => "minimax_h3_ultra",
|
||||||
@@ -534,6 +542,8 @@ impl InferenceModelType {
|
|||||||
"kling_3p0_standard" => Ok(Self::Kling3p0Standard),
|
"kling_3p0_standard" => Ok(Self::Kling3p0Standard),
|
||||||
"kling_3p0_pro" => Ok(Self::Kling3p0Pro),
|
"kling_3p0_pro" => Ok(Self::Kling3p0Pro),
|
||||||
"happy_horse_1p0" => Ok(Self::HappyHorse1p0),
|
"happy_horse_1p0" => Ok(Self::HappyHorse1p0),
|
||||||
|
"wan_3p0" => Ok(Self::Wan3p0),
|
||||||
|
"wan_3p0_prime" => Ok(Self::Wan3p0Prime),
|
||||||
"minimax_h3" => Ok(Self::MinimaxH3),
|
"minimax_h3" => Ok(Self::MinimaxH3),
|
||||||
"minimax_h3_turbo" => Ok(Self::MinimaxH3Turbo),
|
"minimax_h3_turbo" => Ok(Self::MinimaxH3Turbo),
|
||||||
"minimax_h3_ultra" => Ok(Self::MinimaxH3Ultra),
|
"minimax_h3_ultra" => Ok(Self::MinimaxH3Ultra),
|
||||||
@@ -678,6 +688,8 @@ impl InferenceModelType {
|
|||||||
Self::Kling3p0Standard,
|
Self::Kling3p0Standard,
|
||||||
Self::Kling3p0Pro,
|
Self::Kling3p0Pro,
|
||||||
Self::HappyHorse1p0,
|
Self::HappyHorse1p0,
|
||||||
|
Self::Wan3p0,
|
||||||
|
Self::Wan3p0Prime,
|
||||||
Self::MinimaxH3,
|
Self::MinimaxH3,
|
||||||
Self::MinimaxH3Turbo,
|
Self::MinimaxH3Turbo,
|
||||||
Self::MinimaxH3Ultra,
|
Self::MinimaxH3Ultra,
|
||||||
@@ -805,6 +817,8 @@ impl InferenceModelType {
|
|||||||
CommonModelType::Kling3p0Standard => Self::Kling3p0Standard,
|
CommonModelType::Kling3p0Standard => Self::Kling3p0Standard,
|
||||||
CommonModelType::Kling3p0Pro => Self::Kling3p0Pro,
|
CommonModelType::Kling3p0Pro => Self::Kling3p0Pro,
|
||||||
CommonModelType::HappyHorse1p0 => Self::HappyHorse1p0,
|
CommonModelType::HappyHorse1p0 => Self::HappyHorse1p0,
|
||||||
|
CommonModelType::Wan3p0 => Self::Wan3p0,
|
||||||
|
CommonModelType::Wan3p0Prime => Self::Wan3p0Prime,
|
||||||
CommonModelType::MinimaxH3 => Self::MinimaxH3,
|
CommonModelType::MinimaxH3 => Self::MinimaxH3,
|
||||||
CommonModelType::MinimaxH3Turbo => Self::MinimaxH3Turbo,
|
CommonModelType::MinimaxH3Turbo => Self::MinimaxH3Turbo,
|
||||||
CommonModelType::MinimaxH3Ultra => Self::MinimaxH3Ultra,
|
CommonModelType::MinimaxH3Ultra => Self::MinimaxH3Ultra,
|
||||||
@@ -954,6 +968,8 @@ mod tests {
|
|||||||
assert_serialization(InferenceModelType::Kling3p0Standard, "kling_3p0_standard");
|
assert_serialization(InferenceModelType::Kling3p0Standard, "kling_3p0_standard");
|
||||||
assert_serialization(InferenceModelType::Kling3p0Pro, "kling_3p0_pro");
|
assert_serialization(InferenceModelType::Kling3p0Pro, "kling_3p0_pro");
|
||||||
assert_serialization(InferenceModelType::HappyHorse1p0, "happy_horse_1p0");
|
assert_serialization(InferenceModelType::HappyHorse1p0, "happy_horse_1p0");
|
||||||
|
assert_serialization(InferenceModelType::Wan3p0, "wan_3p0");
|
||||||
|
assert_serialization(InferenceModelType::Wan3p0Prime, "wan_3p0_prime");
|
||||||
assert_serialization(InferenceModelType::MinimaxH3, "minimax_h3");
|
assert_serialization(InferenceModelType::MinimaxH3, "minimax_h3");
|
||||||
assert_serialization(InferenceModelType::MinimaxH3Turbo, "minimax_h3_turbo");
|
assert_serialization(InferenceModelType::MinimaxH3Turbo, "minimax_h3_turbo");
|
||||||
assert_serialization(InferenceModelType::MinimaxH3Ultra, "minimax_h3_ultra");
|
assert_serialization(InferenceModelType::MinimaxH3Ultra, "minimax_h3_ultra");
|
||||||
@@ -1090,6 +1106,8 @@ mod tests {
|
|||||||
assert_eq!(InferenceModelType::Kling3p0Standard.to_str(), "kling_3p0_standard");
|
assert_eq!(InferenceModelType::Kling3p0Standard.to_str(), "kling_3p0_standard");
|
||||||
assert_eq!(InferenceModelType::Kling3p0Pro.to_str(), "kling_3p0_pro");
|
assert_eq!(InferenceModelType::Kling3p0Pro.to_str(), "kling_3p0_pro");
|
||||||
assert_eq!(InferenceModelType::HappyHorse1p0.to_str(), "happy_horse_1p0");
|
assert_eq!(InferenceModelType::HappyHorse1p0.to_str(), "happy_horse_1p0");
|
||||||
|
assert_eq!(InferenceModelType::Wan3p0.to_str(), "wan_3p0");
|
||||||
|
assert_eq!(InferenceModelType::Wan3p0Prime.to_str(), "wan_3p0_prime");
|
||||||
assert_eq!(InferenceModelType::MinimaxH3.to_str(), "minimax_h3");
|
assert_eq!(InferenceModelType::MinimaxH3.to_str(), "minimax_h3");
|
||||||
assert_eq!(InferenceModelType::MinimaxH3Turbo.to_str(), "minimax_h3_turbo");
|
assert_eq!(InferenceModelType::MinimaxH3Turbo.to_str(), "minimax_h3_turbo");
|
||||||
assert_eq!(InferenceModelType::MinimaxH3Ultra.to_str(), "minimax_h3_ultra");
|
assert_eq!(InferenceModelType::MinimaxH3Ultra.to_str(), "minimax_h3_ultra");
|
||||||
@@ -1228,6 +1246,8 @@ mod tests {
|
|||||||
assert_eq!(InferenceModelType::from_str("kling_3p0_standard").unwrap(), InferenceModelType::Kling3p0Standard);
|
assert_eq!(InferenceModelType::from_str("kling_3p0_standard").unwrap(), InferenceModelType::Kling3p0Standard);
|
||||||
assert_eq!(InferenceModelType::from_str("kling_3p0_pro").unwrap(), InferenceModelType::Kling3p0Pro);
|
assert_eq!(InferenceModelType::from_str("kling_3p0_pro").unwrap(), InferenceModelType::Kling3p0Pro);
|
||||||
assert_eq!(InferenceModelType::from_str("happy_horse_1p0").unwrap(), InferenceModelType::HappyHorse1p0);
|
assert_eq!(InferenceModelType::from_str("happy_horse_1p0").unwrap(), InferenceModelType::HappyHorse1p0);
|
||||||
|
assert_eq!(InferenceModelType::from_str("wan_3p0").unwrap(), InferenceModelType::Wan3p0);
|
||||||
|
assert_eq!(InferenceModelType::from_str("wan_3p0_prime").unwrap(), InferenceModelType::Wan3p0Prime);
|
||||||
assert_eq!(InferenceModelType::from_str("minimax_h3").unwrap(), InferenceModelType::MinimaxH3);
|
assert_eq!(InferenceModelType::from_str("minimax_h3").unwrap(), InferenceModelType::MinimaxH3);
|
||||||
assert_eq!(InferenceModelType::from_str("minimax_h3_turbo").unwrap(), InferenceModelType::MinimaxH3Turbo);
|
assert_eq!(InferenceModelType::from_str("minimax_h3_turbo").unwrap(), InferenceModelType::MinimaxH3Turbo);
|
||||||
assert_eq!(InferenceModelType::from_str("minimax_h3_ultra").unwrap(), InferenceModelType::MinimaxH3Ultra);
|
assert_eq!(InferenceModelType::from_str("minimax_h3_ultra").unwrap(), InferenceModelType::MinimaxH3Ultra);
|
||||||
|
|||||||
@@ -163,6 +163,12 @@ pub enum CommonModelType {
|
|||||||
Kling3p0Pro,
|
Kling3p0Pro,
|
||||||
#[serde(rename = "happy_horse_1p0")]
|
#[serde(rename = "happy_horse_1p0")]
|
||||||
HappyHorse1p0,
|
HappyHorse1p0,
|
||||||
|
|
||||||
|
#[serde(rename = "wan_3p0")]
|
||||||
|
Wan3p0,
|
||||||
|
|
||||||
|
#[serde(rename = "wan_3p0_prime")]
|
||||||
|
Wan3p0Prime,
|
||||||
#[serde(rename = "minimax_h3")]
|
#[serde(rename = "minimax_h3")]
|
||||||
MinimaxH3,
|
MinimaxH3,
|
||||||
#[serde(rename = "minimax_h3_turbo")]
|
#[serde(rename = "minimax_h3_turbo")]
|
||||||
@@ -349,6 +355,8 @@ impl CommonModelType {
|
|||||||
Self::Kling3p0Standard => "kling_3p0_standard",
|
Self::Kling3p0Standard => "kling_3p0_standard",
|
||||||
Self::Kling3p0Pro => "kling_3p0_pro",
|
Self::Kling3p0Pro => "kling_3p0_pro",
|
||||||
Self::HappyHorse1p0 => "happy_horse_1p0",
|
Self::HappyHorse1p0 => "happy_horse_1p0",
|
||||||
|
Self::Wan3p0 => "wan_3p0",
|
||||||
|
Self::Wan3p0Prime => "wan_3p0_prime",
|
||||||
Self::MinimaxH3 => "minimax_h3",
|
Self::MinimaxH3 => "minimax_h3",
|
||||||
Self::MinimaxH3Turbo => "minimax_h3_turbo",
|
Self::MinimaxH3Turbo => "minimax_h3_turbo",
|
||||||
Self::MinimaxH3Ultra => "minimax_h3_ultra",
|
Self::MinimaxH3Ultra => "minimax_h3_ultra",
|
||||||
@@ -472,6 +480,8 @@ impl CommonModelType {
|
|||||||
"kling_3p0_standard" => Ok(Self::Kling3p0Standard),
|
"kling_3p0_standard" => Ok(Self::Kling3p0Standard),
|
||||||
"kling_3p0_pro" => Ok(Self::Kling3p0Pro),
|
"kling_3p0_pro" => Ok(Self::Kling3p0Pro),
|
||||||
"happy_horse_1p0" => Ok(Self::HappyHorse1p0),
|
"happy_horse_1p0" => Ok(Self::HappyHorse1p0),
|
||||||
|
"wan_3p0" => Ok(Self::Wan3p0),
|
||||||
|
"wan_3p0_prime" => Ok(Self::Wan3p0Prime),
|
||||||
"minimax_h3" => Ok(Self::MinimaxH3),
|
"minimax_h3" => Ok(Self::MinimaxH3),
|
||||||
"minimax_h3_turbo" => Ok(Self::MinimaxH3Turbo),
|
"minimax_h3_turbo" => Ok(Self::MinimaxH3Turbo),
|
||||||
"minimax_h3_ultra" => Ok(Self::MinimaxH3Ultra),
|
"minimax_h3_ultra" => Ok(Self::MinimaxH3Ultra),
|
||||||
@@ -599,6 +609,8 @@ impl CommonModelType {
|
|||||||
Self::Kling3p0Standard,
|
Self::Kling3p0Standard,
|
||||||
Self::Kling3p0Pro,
|
Self::Kling3p0Pro,
|
||||||
Self::HappyHorse1p0,
|
Self::HappyHorse1p0,
|
||||||
|
Self::Wan3p0,
|
||||||
|
Self::Wan3p0Prime,
|
||||||
Self::MinimaxH3,
|
Self::MinimaxH3,
|
||||||
Self::MinimaxH3Turbo,
|
Self::MinimaxH3Turbo,
|
||||||
Self::MinimaxH3Ultra,
|
Self::MinimaxH3Ultra,
|
||||||
@@ -723,6 +735,8 @@ impl CommonModelType {
|
|||||||
Self::Kling3p0Standard => CommonModelClass::Video,
|
Self::Kling3p0Standard => CommonModelClass::Video,
|
||||||
Self::Kling3p0Pro => CommonModelClass::Video,
|
Self::Kling3p0Pro => CommonModelClass::Video,
|
||||||
Self::HappyHorse1p0 => CommonModelClass::Video,
|
Self::HappyHorse1p0 => CommonModelClass::Video,
|
||||||
|
Self::Wan3p0 => CommonModelClass::Video,
|
||||||
|
Self::Wan3p0Prime => CommonModelClass::Video,
|
||||||
Self::MinimaxH3 => CommonModelClass::Video,
|
Self::MinimaxH3 => CommonModelClass::Video,
|
||||||
Self::MinimaxH3Turbo => CommonModelClass::Video,
|
Self::MinimaxH3Turbo => CommonModelClass::Video,
|
||||||
Self::MinimaxH3Ultra => CommonModelClass::Video,
|
Self::MinimaxH3Ultra => CommonModelClass::Video,
|
||||||
@@ -856,6 +870,8 @@ mod tests {
|
|||||||
assert_serialization(CommonModelType::Kling3p0Standard, "kling_3p0_standard");
|
assert_serialization(CommonModelType::Kling3p0Standard, "kling_3p0_standard");
|
||||||
assert_serialization(CommonModelType::Kling3p0Pro, "kling_3p0_pro");
|
assert_serialization(CommonModelType::Kling3p0Pro, "kling_3p0_pro");
|
||||||
assert_serialization(CommonModelType::HappyHorse1p0, "happy_horse_1p0");
|
assert_serialization(CommonModelType::HappyHorse1p0, "happy_horse_1p0");
|
||||||
|
assert_serialization(CommonModelType::Wan3p0, "wan_3p0");
|
||||||
|
assert_serialization(CommonModelType::Wan3p0Prime, "wan_3p0_prime");
|
||||||
assert_serialization(CommonModelType::MinimaxH3, "minimax_h3");
|
assert_serialization(CommonModelType::MinimaxH3, "minimax_h3");
|
||||||
assert_serialization(CommonModelType::MinimaxH3Turbo, "minimax_h3_turbo");
|
assert_serialization(CommonModelType::MinimaxH3Turbo, "minimax_h3_turbo");
|
||||||
assert_serialization(CommonModelType::MinimaxH3Ultra, "minimax_h3_ultra");
|
assert_serialization(CommonModelType::MinimaxH3Ultra, "minimax_h3_ultra");
|
||||||
@@ -974,6 +990,8 @@ mod tests {
|
|||||||
assert_eq!(CommonModelType::Kling3p0Standard.to_str(), "kling_3p0_standard");
|
assert_eq!(CommonModelType::Kling3p0Standard.to_str(), "kling_3p0_standard");
|
||||||
assert_eq!(CommonModelType::Kling3p0Pro.to_str(), "kling_3p0_pro");
|
assert_eq!(CommonModelType::Kling3p0Pro.to_str(), "kling_3p0_pro");
|
||||||
assert_eq!(CommonModelType::HappyHorse1p0.to_str(), "happy_horse_1p0");
|
assert_eq!(CommonModelType::HappyHorse1p0.to_str(), "happy_horse_1p0");
|
||||||
|
assert_eq!(CommonModelType::Wan3p0.to_str(), "wan_3p0");
|
||||||
|
assert_eq!(CommonModelType::Wan3p0Prime.to_str(), "wan_3p0_prime");
|
||||||
assert_eq!(CommonModelType::MinimaxH3.to_str(), "minimax_h3");
|
assert_eq!(CommonModelType::MinimaxH3.to_str(), "minimax_h3");
|
||||||
assert_eq!(CommonModelType::MinimaxH3Turbo.to_str(), "minimax_h3_turbo");
|
assert_eq!(CommonModelType::MinimaxH3Turbo.to_str(), "minimax_h3_turbo");
|
||||||
assert_eq!(CommonModelType::MinimaxH3Ultra.to_str(), "minimax_h3_ultra");
|
assert_eq!(CommonModelType::MinimaxH3Ultra.to_str(), "minimax_h3_ultra");
|
||||||
@@ -1094,6 +1112,8 @@ mod tests {
|
|||||||
assert_eq!(CommonModelType::from_str("kling_3p0_standard").unwrap(), CommonModelType::Kling3p0Standard);
|
assert_eq!(CommonModelType::from_str("kling_3p0_standard").unwrap(), CommonModelType::Kling3p0Standard);
|
||||||
assert_eq!(CommonModelType::from_str("kling_3p0_pro").unwrap(), CommonModelType::Kling3p0Pro);
|
assert_eq!(CommonModelType::from_str("kling_3p0_pro").unwrap(), CommonModelType::Kling3p0Pro);
|
||||||
assert_eq!(CommonModelType::from_str("happy_horse_1p0").unwrap(), CommonModelType::HappyHorse1p0);
|
assert_eq!(CommonModelType::from_str("happy_horse_1p0").unwrap(), CommonModelType::HappyHorse1p0);
|
||||||
|
assert_eq!(CommonModelType::from_str("wan_3p0").unwrap(), CommonModelType::Wan3p0);
|
||||||
|
assert_eq!(CommonModelType::from_str("wan_3p0_prime").unwrap(), CommonModelType::Wan3p0Prime);
|
||||||
assert_eq!(CommonModelType::from_str("minimax_h3").unwrap(), CommonModelType::MinimaxH3);
|
assert_eq!(CommonModelType::from_str("minimax_h3").unwrap(), CommonModelType::MinimaxH3);
|
||||||
assert_eq!(CommonModelType::from_str("minimax_h3_turbo").unwrap(), CommonModelType::MinimaxH3Turbo);
|
assert_eq!(CommonModelType::from_str("minimax_h3_turbo").unwrap(), CommonModelType::MinimaxH3Turbo);
|
||||||
assert_eq!(CommonModelType::from_str("minimax_h3_ultra").unwrap(), CommonModelType::MinimaxH3Ultra);
|
assert_eq!(CommonModelType::from_str("minimax_h3_ultra").unwrap(), CommonModelType::MinimaxH3Ultra);
|
||||||
@@ -1158,7 +1178,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn all_variants() {
|
fn all_variants() {
|
||||||
let mut variants = CommonModelType::all_variants();
|
let mut variants = CommonModelType::all_variants();
|
||||||
assert_eq!(variants.len(), 108);
|
assert_eq!(variants.len(), 110);
|
||||||
// Image models
|
// Image models
|
||||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev));
|
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev));
|
||||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell));
|
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell));
|
||||||
@@ -1214,6 +1234,8 @@ mod tests {
|
|||||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling3p0Standard));
|
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling3p0Standard));
|
||||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling3p0Pro));
|
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling3p0Pro));
|
||||||
assert_eq!(variants.pop_first(), Some(CommonModelType::HappyHorse1p0));
|
assert_eq!(variants.pop_first(), Some(CommonModelType::HappyHorse1p0));
|
||||||
|
assert_eq!(variants.pop_first(), Some(CommonModelType::Wan3p0));
|
||||||
|
assert_eq!(variants.pop_first(), Some(CommonModelType::Wan3p0Prime));
|
||||||
assert_eq!(variants.pop_first(), Some(CommonModelType::MinimaxH3));
|
assert_eq!(variants.pop_first(), Some(CommonModelType::MinimaxH3));
|
||||||
assert_eq!(variants.pop_first(), Some(CommonModelType::MinimaxH3Turbo));
|
assert_eq!(variants.pop_first(), Some(CommonModelType::MinimaxH3Turbo));
|
||||||
assert_eq!(variants.pop_first(), Some(CommonModelType::MinimaxH3Ultra));
|
assert_eq!(variants.pop_first(), Some(CommonModelType::MinimaxH3Ultra));
|
||||||
|
|||||||
@@ -13,11 +13,22 @@ pub const MAX_LENGTH: usize = 16;
|
|||||||
|
|
||||||
/// Common quality levels for generation.
|
/// Common quality levels for generation.
|
||||||
///
|
///
|
||||||
|
/// Declared from "let the model decide" through the highest tier down to the lowest, so the
|
||||||
|
/// derived `Ord` (and `all_variants()`) reads top-down. Not every model supports every tier;
|
||||||
|
/// the router clamps unsupported tiers to the nearest one the model offers.
|
||||||
|
///
|
||||||
/// NB: Keep the max serialized length to 16 characters.
|
/// NB: Keep the max serialized length to 16 characters.
|
||||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum CommonQuality {
|
pub enum CommonQuality {
|
||||||
|
/// Let the model choose. Models without a native "auto" treat this as their default tier.
|
||||||
|
Auto,
|
||||||
|
/// Highest tier (GPT Image 2.5 `max`).
|
||||||
|
Max,
|
||||||
|
/// Between High and Max (GPT Image 2.5 `xhigh`).
|
||||||
|
#[serde(rename = "xhigh")]
|
||||||
|
XHigh,
|
||||||
High,
|
High,
|
||||||
Medium,
|
Medium,
|
||||||
Low,
|
Low,
|
||||||
@@ -30,6 +41,9 @@ impl_mysql_from_row!(CommonQuality);
|
|||||||
impl CommonQuality {
|
impl CommonQuality {
|
||||||
pub fn to_str(&self) -> &'static str {
|
pub fn to_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Auto => "auto",
|
||||||
|
Self::Max => "max",
|
||||||
|
Self::XHigh => "xhigh",
|
||||||
Self::High => "high",
|
Self::High => "high",
|
||||||
Self::Medium => "medium",
|
Self::Medium => "medium",
|
||||||
Self::Low => "low",
|
Self::Low => "low",
|
||||||
@@ -38,6 +52,9 @@ impl CommonQuality {
|
|||||||
|
|
||||||
pub fn from_str(value: &str) -> Result<Self, EnumError> {
|
pub fn from_str(value: &str) -> Result<Self, EnumError> {
|
||||||
match value {
|
match value {
|
||||||
|
"auto" => Ok(Self::Auto),
|
||||||
|
"max" => Ok(Self::Max),
|
||||||
|
"xhigh" => Ok(Self::XHigh),
|
||||||
"high" => Ok(Self::High),
|
"high" => Ok(Self::High),
|
||||||
"medium" => Ok(Self::Medium),
|
"medium" => Ok(Self::Medium),
|
||||||
"low" => Ok(Self::Low),
|
"low" => Ok(Self::Low),
|
||||||
@@ -47,6 +64,9 @@ impl CommonQuality {
|
|||||||
|
|
||||||
pub fn all_variants() -> BTreeSet<Self> {
|
pub fn all_variants() -> BTreeSet<Self> {
|
||||||
BTreeSet::from([
|
BTreeSet::from([
|
||||||
|
Self::Auto,
|
||||||
|
Self::Max,
|
||||||
|
Self::XHigh,
|
||||||
Self::High,
|
Self::High,
|
||||||
Self::Medium,
|
Self::Medium,
|
||||||
Self::Low,
|
Self::Low,
|
||||||
@@ -66,6 +86,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serialization() {
|
fn test_serialization() {
|
||||||
|
assert_serialization(CommonQuality::Auto, "auto");
|
||||||
|
assert_serialization(CommonQuality::Max, "max");
|
||||||
|
assert_serialization(CommonQuality::XHigh, "xhigh");
|
||||||
assert_serialization(CommonQuality::High, "high");
|
assert_serialization(CommonQuality::High, "high");
|
||||||
assert_serialization(CommonQuality::Medium, "medium");
|
assert_serialization(CommonQuality::Medium, "medium");
|
||||||
assert_serialization(CommonQuality::Low, "low");
|
assert_serialization(CommonQuality::Low, "low");
|
||||||
@@ -73,6 +96,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn to_str() {
|
fn to_str() {
|
||||||
|
assert_eq!(CommonQuality::Auto.to_str(), "auto");
|
||||||
|
assert_eq!(CommonQuality::Max.to_str(), "max");
|
||||||
|
assert_eq!(CommonQuality::XHigh.to_str(), "xhigh");
|
||||||
assert_eq!(CommonQuality::High.to_str(), "high");
|
assert_eq!(CommonQuality::High.to_str(), "high");
|
||||||
assert_eq!(CommonQuality::Medium.to_str(), "medium");
|
assert_eq!(CommonQuality::Medium.to_str(), "medium");
|
||||||
assert_eq!(CommonQuality::Low.to_str(), "low");
|
assert_eq!(CommonQuality::Low.to_str(), "low");
|
||||||
@@ -80,6 +106,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_str() {
|
fn from_str() {
|
||||||
|
assert_eq!(CommonQuality::from_str("auto").unwrap(), CommonQuality::Auto);
|
||||||
|
assert_eq!(CommonQuality::from_str("max").unwrap(), CommonQuality::Max);
|
||||||
|
assert_eq!(CommonQuality::from_str("xhigh").unwrap(), CommonQuality::XHigh);
|
||||||
assert_eq!(CommonQuality::from_str("high").unwrap(), CommonQuality::High);
|
assert_eq!(CommonQuality::from_str("high").unwrap(), CommonQuality::High);
|
||||||
assert_eq!(CommonQuality::from_str("medium").unwrap(), CommonQuality::Medium);
|
assert_eq!(CommonQuality::from_str("medium").unwrap(), CommonQuality::Medium);
|
||||||
assert_eq!(CommonQuality::from_str("low").unwrap(), CommonQuality::Low);
|
assert_eq!(CommonQuality::from_str("low").unwrap(), CommonQuality::Low);
|
||||||
@@ -99,7 +128,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn all_variants() {
|
fn all_variants() {
|
||||||
let mut variants = CommonQuality::all_variants();
|
let mut variants = CommonQuality::all_variants();
|
||||||
assert_eq!(variants.len(), 3);
|
assert_eq!(variants.len(), 6);
|
||||||
|
assert_eq!(variants.pop_first(), Some(CommonQuality::Auto));
|
||||||
|
assert_eq!(variants.pop_first(), Some(CommonQuality::Max));
|
||||||
|
assert_eq!(variants.pop_first(), Some(CommonQuality::XHigh));
|
||||||
assert_eq!(variants.pop_first(), Some(CommonQuality::High));
|
assert_eq!(variants.pop_first(), Some(CommonQuality::High));
|
||||||
assert_eq!(variants.pop_first(), Some(CommonQuality::Medium));
|
assert_eq!(variants.pop_first(), Some(CommonQuality::Medium));
|
||||||
assert_eq!(variants.pop_first(), Some(CommonQuality::Low));
|
assert_eq!(variants.pop_first(), Some(CommonQuality::Low));
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ pub enum CommonVideoModel {
|
|||||||
#[serde(rename = "happy_horse_1p0")]
|
#[serde(rename = "happy_horse_1p0")]
|
||||||
HappyHorse1p0,
|
HappyHorse1p0,
|
||||||
|
|
||||||
|
#[serde(rename = "wan_3p0")]
|
||||||
|
Wan3p0,
|
||||||
|
|
||||||
|
#[serde(rename = "wan_3p0_prime")]
|
||||||
|
Wan3p0Prime,
|
||||||
|
|
||||||
#[serde(rename = "minimax_h3")]
|
#[serde(rename = "minimax_h3")]
|
||||||
MinimaxH3,
|
MinimaxH3,
|
||||||
|
|
||||||
@@ -168,6 +174,8 @@ impl CommonVideoModel {
|
|||||||
Self::Kling3p0Standard => CommonModelType::Kling3p0Standard,
|
Self::Kling3p0Standard => CommonModelType::Kling3p0Standard,
|
||||||
Self::Kling3p0Pro => CommonModelType::Kling3p0Pro,
|
Self::Kling3p0Pro => CommonModelType::Kling3p0Pro,
|
||||||
Self::HappyHorse1p0 => CommonModelType::HappyHorse1p0,
|
Self::HappyHorse1p0 => CommonModelType::HappyHorse1p0,
|
||||||
|
Self::Wan3p0 => CommonModelType::Wan3p0,
|
||||||
|
Self::Wan3p0Prime => CommonModelType::Wan3p0Prime,
|
||||||
Self::MinimaxH3 => CommonModelType::MinimaxH3,
|
Self::MinimaxH3 => CommonModelType::MinimaxH3,
|
||||||
Self::MinimaxH3Turbo => CommonModelType::MinimaxH3Turbo,
|
Self::MinimaxH3Turbo => CommonModelType::MinimaxH3Turbo,
|
||||||
Self::MinimaxH3Ultra => CommonModelType::MinimaxH3Ultra,
|
Self::MinimaxH3Ultra => CommonModelType::MinimaxH3Ultra,
|
||||||
@@ -224,6 +232,8 @@ mod tests {
|
|||||||
assert_serialization(CommonVideoModel::Kling3p0Standard, "kling_3p0_standard");
|
assert_serialization(CommonVideoModel::Kling3p0Standard, "kling_3p0_standard");
|
||||||
assert_serialization(CommonVideoModel::Kling3p0Pro, "kling_3p0_pro");
|
assert_serialization(CommonVideoModel::Kling3p0Pro, "kling_3p0_pro");
|
||||||
assert_serialization(CommonVideoModel::HappyHorse1p0, "happy_horse_1p0");
|
assert_serialization(CommonVideoModel::HappyHorse1p0, "happy_horse_1p0");
|
||||||
|
assert_serialization(CommonVideoModel::Wan3p0, "wan_3p0");
|
||||||
|
assert_serialization(CommonVideoModel::Wan3p0Prime, "wan_3p0_prime");
|
||||||
assert_serialization(CommonVideoModel::MinimaxH3, "minimax_h3");
|
assert_serialization(CommonVideoModel::MinimaxH3, "minimax_h3");
|
||||||
assert_serialization(CommonVideoModel::MinimaxH3Turbo, "minimax_h3_turbo");
|
assert_serialization(CommonVideoModel::MinimaxH3Turbo, "minimax_h3_turbo");
|
||||||
assert_serialization(CommonVideoModel::MinimaxH3Ultra, "minimax_h3_ultra");
|
assert_serialization(CommonVideoModel::MinimaxH3Ultra, "minimax_h3_ultra");
|
||||||
@@ -273,6 +283,8 @@ mod tests {
|
|||||||
("kling_3p0_standard", CommonVideoModel::Kling3p0Standard),
|
("kling_3p0_standard", CommonVideoModel::Kling3p0Standard),
|
||||||
("kling_3p0_pro", CommonVideoModel::Kling3p0Pro),
|
("kling_3p0_pro", CommonVideoModel::Kling3p0Pro),
|
||||||
("happy_horse_1p0", CommonVideoModel::HappyHorse1p0),
|
("happy_horse_1p0", CommonVideoModel::HappyHorse1p0),
|
||||||
|
("wan_3p0", CommonVideoModel::Wan3p0),
|
||||||
|
("wan_3p0_prime", CommonVideoModel::Wan3p0Prime),
|
||||||
("minimax_h3", CommonVideoModel::MinimaxH3),
|
("minimax_h3", CommonVideoModel::MinimaxH3),
|
||||||
("minimax_h3_turbo", CommonVideoModel::MinimaxH3Turbo),
|
("minimax_h3_turbo", CommonVideoModel::MinimaxH3Turbo),
|
||||||
("minimax_h3_ultra", CommonVideoModel::MinimaxH3Ultra),
|
("minimax_h3_ultra", CommonVideoModel::MinimaxH3Ultra),
|
||||||
@@ -329,6 +341,8 @@ mod tests {
|
|||||||
CommonVideoModel::Kling3p0Standard,
|
CommonVideoModel::Kling3p0Standard,
|
||||||
CommonVideoModel::Kling3p0Pro,
|
CommonVideoModel::Kling3p0Pro,
|
||||||
CommonVideoModel::HappyHorse1p0,
|
CommonVideoModel::HappyHorse1p0,
|
||||||
|
CommonVideoModel::Wan3p0,
|
||||||
|
CommonVideoModel::Wan3p0Prime,
|
||||||
CommonVideoModel::MinimaxH3,
|
CommonVideoModel::MinimaxH3,
|
||||||
CommonVideoModel::MinimaxH3Turbo,
|
CommonVideoModel::MinimaxH3Turbo,
|
||||||
CommonVideoModel::MinimaxH3Ultra,
|
CommonVideoModel::MinimaxH3Ultra,
|
||||||
@@ -384,6 +398,8 @@ mod tests {
|
|||||||
(CommonVideoModel::Kling3p0Standard, CommonModelType::Kling3p0Standard),
|
(CommonVideoModel::Kling3p0Standard, CommonModelType::Kling3p0Standard),
|
||||||
(CommonVideoModel::Kling3p0Pro, CommonModelType::Kling3p0Pro),
|
(CommonVideoModel::Kling3p0Pro, CommonModelType::Kling3p0Pro),
|
||||||
(CommonVideoModel::HappyHorse1p0, CommonModelType::HappyHorse1p0),
|
(CommonVideoModel::HappyHorse1p0, CommonModelType::HappyHorse1p0),
|
||||||
|
(CommonVideoModel::Wan3p0, CommonModelType::Wan3p0),
|
||||||
|
(CommonVideoModel::Wan3p0Prime, CommonModelType::Wan3p0Prime),
|
||||||
(CommonVideoModel::MinimaxH3, CommonModelType::MinimaxH3),
|
(CommonVideoModel::MinimaxH3, CommonModelType::MinimaxH3),
|
||||||
(CommonVideoModel::MinimaxH3Turbo, CommonModelType::MinimaxH3Turbo),
|
(CommonVideoModel::MinimaxH3Turbo, CommonModelType::MinimaxH3Turbo),
|
||||||
(CommonVideoModel::MinimaxH3Ultra, CommonModelType::MinimaxH3Ultra),
|
(CommonVideoModel::MinimaxH3Ultra, CommonModelType::MinimaxH3Ultra),
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
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,3 +15,4 @@ pub mod model_creator;
|
|||||||
pub mod common_resolution;
|
pub mod common_resolution;
|
||||||
pub mod common_splat_model;
|
pub mod common_splat_model;
|
||||||
pub mod common_video_model;
|
pub mod common_video_model;
|
||||||
|
pub mod common_video_output_format;
|
||||||
|
|||||||
@@ -12,12 +12,19 @@ export enum CommonImageModel {
|
|||||||
GptImage2 = "gpt_image_2",
|
GptImage2 = "gpt_image_2",
|
||||||
GptImage2p5Flare = "gpt_image_2p5_flare",
|
GptImage2p5Flare = "gpt_image_2p5_flare",
|
||||||
GptImage2p5Sunburst = "gpt_image_2p5_sunburst",
|
GptImage2p5Sunburst = "gpt_image_2p5_sunburst",
|
||||||
|
GrokImagineImage = "grok_imagine_image",
|
||||||
|
GrokImagineImageQuality = "grok_imagine_image_q",
|
||||||
|
Midjourney7 = "midjourney_7",
|
||||||
|
Midjourney7Niji = "midjourney_7_niji",
|
||||||
|
Midjourney8 = "midjourney_8",
|
||||||
NanaBanana = "nano_banana",
|
NanaBanana = "nano_banana",
|
||||||
NanaBanana2 = "nano_banana_2",
|
NanaBanana2 = "nano_banana_2",
|
||||||
NanaBananaPro = "nano_banana_pro",
|
NanaBananaPro = "nano_banana_pro",
|
||||||
Seedream4 = "seedream_4",
|
Seedream4 = "seedream_4",
|
||||||
Seedream4p5 = "seedream_4p5",
|
Seedream4p5 = "seedream_4p5",
|
||||||
Seedream5Lite = "seedream_5_lite",
|
Seedream5Lite = "seedream_5_lite",
|
||||||
|
Seedream5p0Pro = "seedream_5p0_pro",
|
||||||
|
Seedream5p0ProUltra = "seedream_5p0_pro_u",
|
||||||
QwenEdit2511Angles = "qwen_edit_2511_angles",
|
QwenEdit2511Angles = "qwen_edit_2511_angles",
|
||||||
Flux2LoraAngles = "flux_2_lora_angles",
|
Flux2LoraAngles = "flux_2_lora_angles",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
|
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
|
||||||
|
|
||||||
export enum CommonQuality {
|
export enum CommonQuality {
|
||||||
|
Auto = "auto",
|
||||||
|
Max = "max",
|
||||||
|
XHigh = "xhigh",
|
||||||
High = "high",
|
High = "high",
|
||||||
Medium = "medium",
|
Medium = "medium",
|
||||||
Low = "low",
|
Low = "low",
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
export enum CommonVideoModel {
|
export enum CommonVideoModel {
|
||||||
GrokVideo = "grok_video",
|
GrokVideo = "grok_video",
|
||||||
|
GrokImagineVideo = "grok_imagine_video",
|
||||||
|
GrokImagineVideo1p5 = "grok_imagine_video_1p5",
|
||||||
|
Flux3 = "flux_3",
|
||||||
|
Flux3Draft = "flux_3_draft",
|
||||||
Kling16Pro = "kling_1p6_pro",
|
Kling16Pro = "kling_1p6_pro",
|
||||||
Kling21Pro = "kling_2p1_pro",
|
Kling21Pro = "kling_2p1_pro",
|
||||||
Kling21Master = "kling_2p1_master",
|
Kling21Master = "kling_2p1_master",
|
||||||
@@ -11,10 +15,28 @@ export enum CommonVideoModel {
|
|||||||
Kling2p6Pro = "kling_2p6_pro",
|
Kling2p6Pro = "kling_2p6_pro",
|
||||||
Kling3p0Standard = "kling_3p0_standard",
|
Kling3p0Standard = "kling_3p0_standard",
|
||||||
Kling3p0Pro = "kling_3p0_pro",
|
Kling3p0Pro = "kling_3p0_pro",
|
||||||
|
HappyHorse1p0 = "happy_horse_1p0",
|
||||||
|
Wan3p0 = "wan_3p0",
|
||||||
|
Wan3p0Prime = "wan_3p0_prime",
|
||||||
|
MinimaxH3 = "minimax_h3",
|
||||||
|
MinimaxH3Turbo = "minimax_h3_turbo",
|
||||||
|
MinimaxH3Ultra = "minimax_h3_ultra",
|
||||||
Seedance10Lite = "seedance_1p0_lite",
|
Seedance10Lite = "seedance_1p0_lite",
|
||||||
Seedance1p5Pro = "seedance_1p5_pro",
|
Seedance1p5Pro = "seedance_1p5_pro",
|
||||||
Seedance2p0 = "seedance_2p0",
|
Seedance2p0 = "seedance_2p0",
|
||||||
Seedance2p0Fast = "seedance_2p0_fast",
|
Seedance2p0Fast = "seedance_2p0_fast",
|
||||||
|
Seedance2p0BytePlus = "seedance_2p0_bp",
|
||||||
|
Seedance2p0BytePlusFast = "seedance_2p0_bp_fast",
|
||||||
|
Seedance2p0Ultra = "seedance_2p0_u",
|
||||||
|
Seedance2p0UltraFast = "seedance_2p0_u_fast",
|
||||||
|
Seedance2p0BytePlusUltra = "seedance_2p0_bpu",
|
||||||
|
Seedance2p0BytePlusUltraFast = "seedance_2p0_bpu_fast",
|
||||||
|
Seedance2p0Mini = "seedance_2p0_mini",
|
||||||
|
Seedance2p0BytePlusMini = "seedance_2p0_bp_mini",
|
||||||
|
Seedance2p0BytePlusUltraMini = "seedance_2p0_bpu_mini",
|
||||||
|
Seedance2p5Preview = "seedance_2p5_preview",
|
||||||
|
Seedance2p5 = "seedance_2p5",
|
||||||
|
Seedance2p5Ultra = "seedance_2p5_u",
|
||||||
Sora2 = "sora_2",
|
Sora2 = "sora_2",
|
||||||
Sora2Pro = "sora_2_pro",
|
Sora2Pro = "sora_2_pro",
|
||||||
Veo2 = "veo_2",
|
Veo2 = "veo_2",
|
||||||
@@ -22,4 +44,9 @@ export enum CommonVideoModel {
|
|||||||
Veo3Fast = "veo_3_fast",
|
Veo3Fast = "veo_3_fast",
|
||||||
Veo3p1 = "veo_3p1",
|
Veo3p1 = "veo_3p1",
|
||||||
Veo3p1Fast = "veo_3p1_fast",
|
Veo3p1Fast = "veo_3p1_fast",
|
||||||
|
Veo3p1Lite = "veo_3p1_lite",
|
||||||
|
ViduQ3 = "vidu_q3",
|
||||||
|
ViduQ3Turbo = "vidu_q3_turbo",
|
||||||
|
PreviewModel = "preview_model",
|
||||||
|
PreviewModelFast = "preview_model_fast",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ import { CommonQuality } from "@storyteller/api-enums";
|
|||||||
/**
|
/**
|
||||||
* Convert a quality value to CommonQuality for the image cost estimate API.
|
* Convert a quality value to CommonQuality for the image cost estimate API.
|
||||||
*
|
*
|
||||||
* Accepts a CommonQuality string value from @storyteller/model-list ("high", "medium", "low").
|
* Preserves all API quality tiers, including values added after this build.
|
||||||
*/
|
*/
|
||||||
export function stringToCommonQuality(
|
export function stringToCommonQuality(
|
||||||
quality: string | undefined,
|
quality: string | undefined,
|
||||||
): string | null {
|
): string | null {
|
||||||
switch (quality) {
|
switch (quality) {
|
||||||
|
case "auto":
|
||||||
|
return CommonQuality.Auto;
|
||||||
|
case "max":
|
||||||
|
return CommonQuality.Max;
|
||||||
|
case "xhigh":
|
||||||
|
return CommonQuality.XHigh;
|
||||||
case "high":
|
case "high":
|
||||||
return CommonQuality.High;
|
return CommonQuality.High;
|
||||||
case "medium":
|
case "medium":
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export function useImageCostEstimate(
|
|||||||
// Match GenerateImage exactly; omitted values use the API defaults.
|
// Match GenerateImage exactly; omitted values use the API defaults.
|
||||||
aspectRatioStr = imageModel.supportsNewAspectRatio() ? imageAspectRatio : undefined;
|
aspectRatioStr = imageModel.supportsNewAspectRatio() ? imageAspectRatio : undefined;
|
||||||
resolutionStr = imageModel.supportsNewResolution() ? imageResolution : undefined;
|
resolutionStr = imageModel.supportsNewResolution() ? imageResolution : undefined;
|
||||||
qualityStr = imageModel.supportsQuality() ? imageQuality ?? imageModel.defaultQuality : undefined;
|
qualityStr = imageModel.resolveQuality(imageQuality);
|
||||||
referenceImageCount = imageModel.canUseImagePrompt
|
referenceImageCount = imageModel.canUseImagePrompt
|
||||||
? imageReferenceImages.filter((image) => image.mediaToken.length > 0).length : 0;
|
? imageReferenceImages.filter((image) => image.mediaToken.length > 0).length : 0;
|
||||||
generationCount = imageGenerationCount;
|
generationCount = imageGenerationCount;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({
|
|||||||
referenceVideos: [{ mediaToken: "mf_video", duration: 7.25 }],
|
referenceVideos: [{ mediaToken: "mf_video", duration: 7.25 }],
|
||||||
referenceAudios: [{ mediaToken: "mf_audio", duration: 3.5 }],
|
referenceAudios: [{ mediaToken: "mf_audio", duration: 3.5 }],
|
||||||
bitrate: "high",
|
bitrate: "high",
|
||||||
|
outputFormat: "mov",
|
||||||
generationCount: 1,
|
generationCount: 1,
|
||||||
generateWithSound: true,
|
generateWithSound: true,
|
||||||
},
|
},
|
||||||
@@ -38,6 +39,7 @@ vi.mock("./cost-breakdown-modal-store", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mocks.state.generationCount = 1;
|
||||||
mocks.estimate.mockReset();
|
mocks.estimate.mockReset();
|
||||||
mocks.setCredits.mockReset();
|
mocks.setCredits.mockReset();
|
||||||
mocks.estimate.mockResolvedValue({
|
mocks.estimate.mockResolvedValue({
|
||||||
@@ -61,6 +63,7 @@ it("quotes a future model with the chosen bitrate and reference durations in mil
|
|||||||
model: "future_video",
|
model: "future_video",
|
||||||
resolution: "four_k",
|
resolution: "four_k",
|
||||||
bitrate: "high",
|
bitrate: "high",
|
||||||
|
output_format: "mov",
|
||||||
duration_seconds: 30,
|
duration_seconds: 30,
|
||||||
reference_video_media_tokens: ["mf_video"],
|
reference_video_media_tokens: ["mf_video"],
|
||||||
reference_audio_media_tokens: ["mf_audio"],
|
reference_audio_media_tokens: ["mf_audio"],
|
||||||
@@ -72,6 +75,20 @@ it("quotes a future model with the chosen bitrate and reference durations in mil
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prices all selected generations for any model with API batch options", async () => {
|
||||||
|
mocks.state.generationCount = 3;
|
||||||
|
renderHook(() => useVideoCostEstimate(ModelPage.ImageToVideo, model, "artcraft"));
|
||||||
|
await waitFor(() => expect(mocks.setCredits).toHaveBeenLastCalledWith(ModelPage.ImageToVideo, 126));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears format options and clamps stale batch counts for unsupported models", async () => {
|
||||||
|
mocks.state.generationCount = 4;
|
||||||
|
const single = { ...model, generationCountOptions: [1], outputFormatOptions: undefined } as VideoModel;
|
||||||
|
renderHook(() => useVideoCostEstimate(ModelPage.ImageToVideo, single, "artcraft"));
|
||||||
|
await waitFor(() => expect(mocks.setCredits).toHaveBeenLastCalledWith(ModelPage.ImageToVideo, 42));
|
||||||
|
expect(mocks.estimate).toHaveBeenCalledWith(expect.objectContaining({ output_format: undefined }));
|
||||||
|
});
|
||||||
|
|
||||||
it("does not overwrite a newer model's quote with a slow earlier response", async () => {
|
it("does not overwrite a newer model's quote with a slow earlier response", async () => {
|
||||||
let finishOld: (result: unknown) => void = () => {};
|
let finishOld: (result: unknown) => void = () => {};
|
||||||
mocks.estimate.mockImplementationOnce(
|
mocks.estimate.mockImplementationOnce(
|
||||||
@@ -111,6 +128,10 @@ const model = {
|
|||||||
resolutionOptions: ["4K"],
|
resolutionOptions: ["4K"],
|
||||||
bitrateOptions: ["normal", "high"],
|
bitrateOptions: ["normal", "high"],
|
||||||
defaultBitrate: "normal",
|
defaultBitrate: "normal",
|
||||||
|
outputFormatOptions: ["mp4", "mov"],
|
||||||
|
defaultOutputFormat: "mp4",
|
||||||
|
generationCountOptions: [1, 2, 3, 4],
|
||||||
|
defaultGenerationCount: 1,
|
||||||
generateWithSound: true,
|
generateWithSound: true,
|
||||||
sizeOptions: [{ tauriValue: "wide_sixteen_by_nine", textLabel: "16:9" }],
|
sizeOptions: [{ tauriValue: "wide_sixteen_by_nine", textLabel: "16:9" }],
|
||||||
} as VideoModel;
|
} as VideoModel;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ModelPage } from "@storyteller/ui-model-selector";
|
import { ModelPage } from "@storyteller/ui-model-selector";
|
||||||
import { Model, VideoModel, resolveVideoDuration, videoResolutionValue } from "@storyteller/model-list";
|
import { Model, VideoModel, resolveVideoDuration, resolveVideoGenerationCount, resolveVideoOutputFormat, videoResolutionValue } from "@storyteller/model-list";
|
||||||
import { GenerationProvider } from "@storyteller/api-enums";
|
import { GenerationProvider } from "@storyteller/api-enums";
|
||||||
import { usePromptVideoStore } from "@storyteller/ui-promptbox";
|
import { usePromptVideoStore } from "@storyteller/ui-promptbox";
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +32,7 @@ export function useVideoCostEstimate(
|
|||||||
const referenceVideos = usePromptVideoStore((s) => s.referenceVideos);
|
const referenceVideos = usePromptVideoStore((s) => s.referenceVideos);
|
||||||
const referenceAudios = usePromptVideoStore((s) => s.referenceAudios);
|
const referenceAudios = usePromptVideoStore((s) => s.referenceAudios);
|
||||||
const bitrate = usePromptVideoStore((s) => s.bitrate);
|
const bitrate = usePromptVideoStore((s) => s.bitrate);
|
||||||
|
const outputFormat = usePromptVideoStore((s) => s.outputFormat);
|
||||||
const generationCount = usePromptVideoStore((s) => s.generationCount);
|
const generationCount = usePromptVideoStore((s) => s.generationCount);
|
||||||
const generateWithSound = usePromptVideoStore((s) => s.generateWithSound);
|
const generateWithSound = usePromptVideoStore((s) => s.generateWithSound);
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ export function useVideoCostEstimate(
|
|||||||
duration_seconds: resolveVideoDuration(videoModel, duration, isReferenceMode) ?? undefined,
|
duration_seconds: resolveVideoDuration(videoModel, duration, isReferenceMode) ?? undefined,
|
||||||
generate_audio: videoModel.generateWithSound ? generateWithSound : undefined,
|
generate_audio: videoModel.generateWithSound ? generateWithSound : undefined,
|
||||||
bitrate: videoModel.bitrateOptions?.length ? bitrate ?? videoModel.defaultBitrate ?? videoModel.bitrateOptions[0] : undefined,
|
bitrate: videoModel.bitrateOptions?.length ? bitrate ?? videoModel.defaultBitrate ?? videoModel.bitrateOptions[0] : undefined,
|
||||||
|
output_format: resolveVideoOutputFormat(videoModel, outputFormat),
|
||||||
start_frame_image_media_token: !isReferenceMode ? referenceImages[0]?.mediaToken : undefined,
|
start_frame_image_media_token: !isReferenceMode ? referenceImages[0]?.mediaToken : undefined,
|
||||||
end_frame_image_media_token: !isReferenceMode ? endFrameImage?.mediaToken : undefined,
|
end_frame_image_media_token: !isReferenceMode ? endFrameImage?.mediaToken : undefined,
|
||||||
reference_image_media_tokens: isReferenceMode ? referenceImages.map((image) => image.mediaToken) : undefined,
|
reference_image_media_tokens: isReferenceMode ? referenceImages.map((image) => image.mediaToken) : undefined,
|
||||||
@@ -88,7 +90,7 @@ export function useVideoCostEstimate(
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (isEstimateVideoCostSuccess(result)) {
|
if (isEstimateVideoCostSuccess(result)) {
|
||||||
const unitCredits = result.payload.cost_in_credits;
|
const unitCredits = result.payload.cost_in_credits;
|
||||||
const count = videoModel.id === "seedance_2p0" ? generationCount : 1;
|
const count = resolveVideoGenerationCount(videoModel, generationCount);
|
||||||
const credits = unitCredits == null ? null : unitCredits * count;
|
const credits = unitCredits == null ? null : unitCredits * count;
|
||||||
setEstimatedCreditsForPage(ModelPage.ImageToVideo, credits);
|
setEstimatedCreditsForPage(ModelPage.ImageToVideo, credits);
|
||||||
} else {
|
} else {
|
||||||
@@ -115,6 +117,7 @@ export function useVideoCostEstimate(
|
|||||||
referenceVideos,
|
referenceVideos,
|
||||||
referenceAudios,
|
referenceAudios,
|
||||||
bitrate,
|
bitrate,
|
||||||
|
outputFormat,
|
||||||
generationCount,
|
generationCount,
|
||||||
endFrameImage,
|
endFrameImage,
|
||||||
generateWithSound,
|
generateWithSound,
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ export const PromptBoxImage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedModel?.supportsQuality()) {
|
if (selectedModel?.supportsQuality()) {
|
||||||
request.quality = commonQuality ?? selectedModel.defaultQuality;
|
request.quality = selectedModel.resolveQuality(commonQuality);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { DynamicIcon } from "@storyteller/icons";
|
|||||||
import { arrayMove } from "@dnd-kit/sortable";
|
import { arrayMove } from "@dnd-kit/sortable";
|
||||||
import {
|
import {
|
||||||
resolveVideoDuration,
|
resolveVideoDuration,
|
||||||
|
resolveVideoGenerationCount,
|
||||||
|
resolveVideoOutputFormat,
|
||||||
|
videoGenerationCounts,
|
||||||
videoDurationRange,
|
videoDurationRange,
|
||||||
videoResolutionValue,
|
videoResolutionValue,
|
||||||
effectivePromptMaxLength,
|
effectivePromptMaxLength,
|
||||||
@@ -143,6 +146,8 @@ export const PromptBoxVideo = ({
|
|||||||
const duration = usePromptVideoStore((s) => s.duration);
|
const duration = usePromptVideoStore((s) => s.duration);
|
||||||
const bitrate = usePromptVideoStore((s) => s.bitrate);
|
const bitrate = usePromptVideoStore((s) => s.bitrate);
|
||||||
const setBitrate = usePromptVideoStore((s) => s.setBitrate);
|
const setBitrate = usePromptVideoStore((s) => s.setBitrate);
|
||||||
|
const outputFormat = usePromptVideoStore((s) => s.outputFormat);
|
||||||
|
const setOutputFormat = usePromptVideoStore((s) => s.setOutputFormat);
|
||||||
const setDuration = usePromptVideoStore((s) => s.setDuration);
|
const setDuration = usePromptVideoStore((s) => s.setDuration);
|
||||||
const inputMode = usePromptVideoStore((s) => s.inputMode);
|
const inputMode = usePromptVideoStore((s) => s.inputMode);
|
||||||
const setInputMode = usePromptVideoStore((s) => s.setInputMode);
|
const setInputMode = usePromptVideoStore((s) => s.setInputMode);
|
||||||
@@ -361,6 +366,14 @@ export const PromptBoxVideo = ({
|
|||||||
}
|
}
|
||||||
}, [selectedModel, setBitrate]);
|
}, [selectedModel, setBitrate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const current = usePromptVideoStore.getState().outputFormat;
|
||||||
|
const options = selectedModel?.outputFormatOptions;
|
||||||
|
if (!current || !options?.includes(current)) {
|
||||||
|
setOutputFormat(selectedModel?.defaultOutputFormat ?? options?.[0] ?? null);
|
||||||
|
}
|
||||||
|
}, [selectedModel, setOutputFormat]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const current = usePromptVideoStore.getState().aspectRatio;
|
const current = usePromptVideoStore.getState().aspectRatio;
|
||||||
if (selectedModel?.sizeOptions.some((option) => option.textLabel === current)) return;
|
if (selectedModel?.sizeOptions.some((option) => option.textLabel === current)) return;
|
||||||
@@ -396,15 +409,16 @@ export const PromptBoxVideo = ({
|
|||||||
}
|
}
|
||||||
}, [selectedModel]);
|
}, [selectedModel]);
|
||||||
|
|
||||||
// Reset generation count when switching away from seedance 2.0.
|
// Keep the selected count within the API's options when switching models.
|
||||||
// Read from store directly to avoid stale closure (same as duration above).
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentGenerationCount =
|
const current = usePromptVideoStore.getState().generationCount;
|
||||||
usePromptVideoStore.getState().generationCount;
|
const next = selectedModel ? resolveVideoGenerationCount(selectedModel, current) : 1;
|
||||||
if (selectedModel?.id !== "seedance_2p0" && currentGenerationCount > 1) {
|
if (next !== current) setGenerationCount(next);
|
||||||
setGenerationCount(1);
|
}, [selectedModel, setGenerationCount]);
|
||||||
}
|
|
||||||
}, [selectedModel]);
|
const generationCounts = selectedModel ? videoGenerationCounts(selectedModel) : [1];
|
||||||
|
const effectiveGenerationCount = selectedModel
|
||||||
|
? resolveVideoGenerationCount(selectedModel, generationCount) : 1;
|
||||||
|
|
||||||
const durationRange = selectedModel
|
const durationRange = selectedModel
|
||||||
? videoDurationRange(selectedModel, inputMode === "reference")
|
? videoDurationRange(selectedModel, inputMode === "reference")
|
||||||
@@ -1049,8 +1063,7 @@ export const PromptBoxVideo = ({
|
|||||||
|
|
||||||
gtagEvent("enqueue_video");
|
gtagEvent("enqueue_video");
|
||||||
|
|
||||||
const isSeedance2 = selectedModel.id === "seedance_2p0";
|
const count = effectiveGenerationCount;
|
||||||
const count = isSeedance2 ? generationCount : 1;
|
|
||||||
|
|
||||||
const isRefMode =
|
const isRefMode =
|
||||||
inputMode === "reference" && !!selectedModel.supportsReferenceMode;
|
inputMode === "reference" && !!selectedModel.supportsReferenceMode;
|
||||||
@@ -1145,6 +1158,9 @@ export const PromptBoxVideo = ({
|
|||||||
if (selectedModel.bitrateOptions?.length) {
|
if (selectedModel.bitrateOptions?.length) {
|
||||||
request.bitrate = bitrate ?? selectedModel.defaultBitrate ?? selectedModel.bitrateOptions[0];
|
request.bitrate = bitrate ?? selectedModel.defaultBitrate ?? selectedModel.bitrateOptions[0];
|
||||||
}
|
}
|
||||||
|
if (selectedModel.outputFormatOptions?.length) {
|
||||||
|
request.output_format = resolveVideoOutputFormat(selectedModel, outputFormat);
|
||||||
|
}
|
||||||
|
|
||||||
// Pass the chosen resolution when the model exposes a resolution picker.
|
// Pass the chosen resolution when the model exposes a resolution picker.
|
||||||
// Guarded on `resolutionOptions` so a stale store value (left over from a
|
// Guarded on `resolutionOptions` so a stale store value (left over from a
|
||||||
@@ -1454,6 +1470,21 @@ export const PromptBoxVideo = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!!selectedModel?.outputFormatOptions?.length && (
|
||||||
|
<Tooltip content="Output format" position="top" className="z-50">
|
||||||
|
<PopoverMenu
|
||||||
|
panelTitle="Output format"
|
||||||
|
mode="toggle"
|
||||||
|
items={selectedModel.outputFormatOptions.map((value) => ({
|
||||||
|
label: value.toUpperCase(),
|
||||||
|
action: value,
|
||||||
|
selected: value === outputFormat,
|
||||||
|
}))}
|
||||||
|
onSelect={(item) => setOutputFormat(item.action ?? item.label)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
{durationRange && durationRange.max > durationRange.min && (
|
{durationRange && durationRange.max > durationRange.min && (
|
||||||
<Tooltip content="Duration" position="top" className="z-50">
|
<Tooltip content="Duration" position="top" className="z-50">
|
||||||
<PopoverMenu
|
<PopoverMenu
|
||||||
@@ -1522,10 +1553,10 @@ export const PromptBoxVideo = ({
|
|||||||
disabled={!hasClearableContent}
|
disabled={!hasClearableContent}
|
||||||
confirmClear={hasAttachedRefs}
|
confirmClear={hasAttachedRefs}
|
||||||
/>
|
/>
|
||||||
{selectedModel?.id === "seedance_2p0" && (
|
{generationCounts.length > 1 && (
|
||||||
<VideoGenerationCountPicker
|
<VideoGenerationCountPicker
|
||||||
maxCount={4}
|
counts={generationCounts}
|
||||||
currentCount={generationCount}
|
currentCount={effectiveGenerationCount}
|
||||||
handleCountChange={setGenerationCount}
|
handleCountChange={setGenerationCount}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1541,9 +1572,7 @@ export const PromptBoxVideo = ({
|
|||||||
onClick={handleEnqueue}
|
onClick={handleEnqueue}
|
||||||
disabled={!prompt.trim()}
|
disabled={!prompt.trim()}
|
||||||
loading={isEnqueueing}
|
loading={isEnqueueing}
|
||||||
credits={
|
credits={credits}
|
||||||
credits != null ? credits * generationCount : credits
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ interface QualityPickerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QUALITY_LABELS: Record<CommonQuality, string> = {
|
const QUALITY_LABELS: Record<CommonQuality, string> = {
|
||||||
|
[CommonQuality.Auto]: "Auto",
|
||||||
|
[CommonQuality.Max]: "Max",
|
||||||
|
[CommonQuality.XHigh]: "Extra high",
|
||||||
[CommonQuality.High]: "High",
|
[CommonQuality.High]: "High",
|
||||||
[CommonQuality.Medium]: "Medium",
|
[CommonQuality.Medium]: "Medium",
|
||||||
[CommonQuality.Low]: "Low",
|
[CommonQuality.Low]: "Low",
|
||||||
@@ -17,7 +20,7 @@ const QUALITY_LABELS: Record<CommonQuality, string> = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stateless picker for image generation "quality" (used by OpenAI image
|
* Stateless picker for image generation "quality" (used by OpenAI image
|
||||||
* models — gpt_image_1, gpt_image_1p5, gpt_image_2). Models that don't
|
* models, including GPT Image 2.5). Models that don't
|
||||||
* declare `qualities` should not render this picker.
|
* declare `qualities` should not render this picker.
|
||||||
*/
|
*/
|
||||||
export const QualityPicker = ({
|
export const QualityPicker = ({
|
||||||
@@ -25,7 +28,7 @@ export const QualityPicker = ({
|
|||||||
currentQuality,
|
currentQuality,
|
||||||
handleCommonQualitySelect,
|
handleCommonQualitySelect,
|
||||||
}: QualityPickerProps) => {
|
}: QualityPickerProps) => {
|
||||||
const useQuality = currentQuality ?? model.defaultQuality ?? undefined;
|
const useQuality = model.resolveQuality(currentQuality);
|
||||||
|
|
||||||
const handleSelectAdapter = (item: PopoverItem) => {
|
const handleSelectAdapter = (item: PopoverItem) => {
|
||||||
const quality = (item.action ?? item.label) as CommonQuality;
|
const quality = (item.action ?? item.label) as CommonQuality;
|
||||||
|
|||||||
@@ -4,27 +4,24 @@ import { Tooltip } from "@storyteller/ui-tooltip";
|
|||||||
import { PROMPT_TOOLBAR_ICON_BUTTON_CLASSES } from "../PromptClearAllButton";
|
import { PROMPT_TOOLBAR_ICON_BUTTON_CLASSES } from "../PromptClearAllButton";
|
||||||
|
|
||||||
interface VideoGenerationCountPickerProps {
|
interface VideoGenerationCountPickerProps {
|
||||||
maxCount: number;
|
counts: number[];
|
||||||
currentCount: number;
|
currentCount: number;
|
||||||
handleCountChange: (count: number) => void;
|
handleCountChange: (count: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VideoGenerationCountPicker = ({
|
export const VideoGenerationCountPicker = ({
|
||||||
maxCount,
|
counts,
|
||||||
currentCount,
|
currentCount,
|
||||||
handleCountChange,
|
handleCountChange,
|
||||||
}: VideoGenerationCountPickerProps) => {
|
}: VideoGenerationCountPickerProps) => {
|
||||||
const options: PopoverItem[] = [];
|
const options: PopoverItem[] = counts.map((count) => ({
|
||||||
for (let i = 1; i <= maxCount; i++) {
|
label: String(count),
|
||||||
options.push({
|
selected: count === currentCount,
|
||||||
label: String(i),
|
}));
|
||||||
selected: i === currentCount,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSelect = (item: PopoverItem) => {
|
const onSelect = (item: PopoverItem) => {
|
||||||
const count = parseInt(item.label, 10);
|
const count = parseInt(item.label, 10);
|
||||||
if (!isNaN(count) && count >= 1 && count <= maxCount) {
|
if (counts.includes(count)) {
|
||||||
handleCountChange(count);
|
handleCountChange(count);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ interface PromptVideoStore {
|
|||||||
generateWithSound: boolean;
|
generateWithSound: boolean;
|
||||||
duration: number | null;
|
duration: number | null;
|
||||||
bitrate: string | null;
|
bitrate: string | null;
|
||||||
|
outputFormat: string | null;
|
||||||
inputMode: VideoInputMode;
|
inputMode: VideoInputMode;
|
||||||
generationCount: number;
|
generationCount: number;
|
||||||
setPrompt: (prompt: string) => void;
|
setPrompt: (prompt: string) => void;
|
||||||
@@ -178,6 +179,7 @@ interface PromptVideoStore {
|
|||||||
setGenerateWithSound: (value: boolean) => void;
|
setGenerateWithSound: (value: boolean) => void;
|
||||||
setDuration: (duration: number | null) => void;
|
setDuration: (duration: number | null) => void;
|
||||||
setBitrate: (bitrate: string | null) => void;
|
setBitrate: (bitrate: string | null) => void;
|
||||||
|
setOutputFormat: (outputFormat: string | null) => void;
|
||||||
setInputMode: (mode: VideoInputMode) => void;
|
setInputMode: (mode: VideoInputMode) => void;
|
||||||
setGenerationCount: (count: number) => void;
|
setGenerationCount: (count: number) => void;
|
||||||
}
|
}
|
||||||
@@ -194,6 +196,7 @@ export const usePromptVideoStore = create<PromptVideoStore>()((set) => ({
|
|||||||
generateWithSound: true,
|
generateWithSound: true,
|
||||||
duration: null,
|
duration: null,
|
||||||
bitrate: null,
|
bitrate: null,
|
||||||
|
outputFormat: null,
|
||||||
inputMode: "reference",
|
inputMode: "reference",
|
||||||
generationCount: 1,
|
generationCount: 1,
|
||||||
setPrompt: (prompt) => set({ prompt }),
|
setPrompt: (prompt) => set({ prompt }),
|
||||||
@@ -207,6 +210,7 @@ export const usePromptVideoStore = create<PromptVideoStore>()((set) => ({
|
|||||||
setGenerateWithSound: (generateWithSound) => set({ generateWithSound }),
|
setGenerateWithSound: (generateWithSound) => set({ generateWithSound }),
|
||||||
setDuration: (duration) => set({ duration }),
|
setDuration: (duration) => set({ duration }),
|
||||||
setBitrate: (bitrate) => set({ bitrate }),
|
setBitrate: (bitrate) => set({ bitrate }),
|
||||||
|
setOutputFormat: (outputFormat) => set({ outputFormat }),
|
||||||
setInputMode: (inputMode) => set({ inputMode }),
|
setInputMode: (inputMode) => set({ inputMode }),
|
||||||
setGenerationCount: (generationCount) => set({ generationCount }),
|
setGenerationCount: (generationCount) => set({ generationCount }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -189,6 +189,13 @@ export class ImageModel extends Model {
|
|||||||
return this.qualityOptions && this.qualityOptions.length > 0;
|
return this.qualityOptions && this.qualityOptions.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A saved tier from another model may not be supported by this model.
|
||||||
|
resolveQuality(current?: CommonQuality): CommonQuality | undefined {
|
||||||
|
if (!this.supportsQuality()) return undefined;
|
||||||
|
return current && this.qualityOptions.includes(current)
|
||||||
|
? current : this.defaultQuality ?? this.qualityOptions[0];
|
||||||
|
}
|
||||||
|
|
||||||
// Return whether the count of generations is valid for this model
|
// Return whether the count of generations is valid for this model
|
||||||
isValidGenerationCount(count: number): boolean {
|
isValidGenerationCount(count: number): boolean {
|
||||||
if (this.predefinedGenerationCounts) {
|
if (this.predefinedGenerationCounts) {
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export class VideoModel extends Model {
|
|||||||
readonly maxDurationWithImageReferences?: number;
|
readonly maxDurationWithImageReferences?: number;
|
||||||
readonly bitrateOptions?: string[];
|
readonly bitrateOptions?: string[];
|
||||||
readonly defaultBitrate?: string;
|
readonly defaultBitrate?: string;
|
||||||
|
readonly outputFormatOptions?: string[];
|
||||||
|
readonly defaultOutputFormat?: string;
|
||||||
|
readonly generationCountOptions?: number[];
|
||||||
|
readonly defaultGenerationCount?: number;
|
||||||
readonly defaultAspectRatio?: string;
|
readonly defaultAspectRatio?: string;
|
||||||
|
|
||||||
|
|
||||||
@@ -102,6 +106,10 @@ export class VideoModel extends Model {
|
|||||||
maxDurationWithImageReferences?: number;
|
maxDurationWithImageReferences?: number;
|
||||||
bitrateOptions?: string[];
|
bitrateOptions?: string[];
|
||||||
defaultBitrate?: string;
|
defaultBitrate?: string;
|
||||||
|
outputFormatOptions?: string[];
|
||||||
|
defaultOutputFormat?: string;
|
||||||
|
generationCountOptions?: number[];
|
||||||
|
defaultGenerationCount?: number;
|
||||||
defaultAspectRatio?: string;
|
defaultAspectRatio?: string;
|
||||||
|
|
||||||
supportsReferenceMode?: boolean;
|
supportsReferenceMode?: boolean;
|
||||||
@@ -130,6 +138,10 @@ export class VideoModel extends Model {
|
|||||||
this.maxDurationWithImageReferences = args.maxDurationWithImageReferences;
|
this.maxDurationWithImageReferences = args.maxDurationWithImageReferences;
|
||||||
this.bitrateOptions = args.bitrateOptions;
|
this.bitrateOptions = args.bitrateOptions;
|
||||||
this.defaultBitrate = args.defaultBitrate;
|
this.defaultBitrate = args.defaultBitrate;
|
||||||
|
this.outputFormatOptions = args.outputFormatOptions;
|
||||||
|
this.defaultOutputFormat = args.defaultOutputFormat;
|
||||||
|
this.generationCountOptions = args.generationCountOptions;
|
||||||
|
this.defaultGenerationCount = args.defaultGenerationCount;
|
||||||
this.defaultAspectRatio = args.defaultAspectRatio;
|
this.defaultAspectRatio = args.defaultAspectRatio;
|
||||||
|
|
||||||
this.supportsReferenceMode = args.supportsReferenceMode;
|
this.supportsReferenceMode = args.supportsReferenceMode;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const MODEL_ID_PREFIX_CREATORS: Array<[string, ModelCreator]> = [
|
|||||||
["vidu", ModelCreator.Vidu],
|
["vidu", ModelCreator.Vidu],
|
||||||
["recraft", ModelCreator.Recraft],
|
["recraft", ModelCreator.Recraft],
|
||||||
["happy_horse", ModelCreator.Alibaba],
|
["happy_horse", ModelCreator.Alibaba],
|
||||||
|
["wan", ModelCreator.Alibaba],
|
||||||
["minimax", ModelCreator.Hailuo],
|
["minimax", ModelCreator.Hailuo],
|
||||||
["qwen", ModelCreator.Alibaba],
|
["qwen", ModelCreator.Alibaba],
|
||||||
["suno", ModelCreator.Suno],
|
["suno", ModelCreator.Suno],
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const MODEL_ID_PREFIX_FAMILIES: Array<[string, string]> = [
|
|||||||
["minimax", "MiniMax"],
|
["minimax", "MiniMax"],
|
||||||
["hailuo", "MiniMax"],
|
["hailuo", "MiniMax"],
|
||||||
["happy_horse", "Happy Horse"],
|
["happy_horse", "Happy Horse"],
|
||||||
|
["wan", "Wan"],
|
||||||
["vidu", "Vidu"],
|
["vidu", "Vidu"],
|
||||||
// ── Image ── (flux also covers the Flux 3 video model)
|
// ── Image ── (flux also covers the Flux 3 video model)
|
||||||
["flux", "Flux"],
|
["flux", "Flux"],
|
||||||
@@ -50,6 +51,7 @@ export const FAMILY_ORDER: string[] = [
|
|||||||
"Grok",
|
"Grok",
|
||||||
"Vidu",
|
"Vidu",
|
||||||
"Happy Horse",
|
"Happy Horse",
|
||||||
|
"Wan",
|
||||||
// Image families.
|
// Image families.
|
||||||
"Nano Banana",
|
"Nano Banana",
|
||||||
"Seedream",
|
"Seedream",
|
||||||
|
|||||||
@@ -96,7 +96,10 @@ export const getModelDisplayName = (
|
|||||||
seedance_2p0_bpu_fast: "Seedance 2.0 Plus Ultra Fast",
|
seedance_2p0_bpu_fast: "Seedance 2.0 Plus Ultra Fast",
|
||||||
seedance_2p0_bpu_mini: "Seedance 2.0 Plus Ultra Mini",
|
seedance_2p0_bpu_mini: "Seedance 2.0 Plus Ultra Mini",
|
||||||
seedance_2p5: "Seedance 2.5",
|
seedance_2p5: "Seedance 2.5",
|
||||||
|
seedance_2p5_u: "Seedance 2.5 Ultra",
|
||||||
seedance_2p5_preview: "Seedance 2.5 Preview",
|
seedance_2p5_preview: "Seedance 2.5 Preview",
|
||||||
|
wan_3p0: "Wan 3.0",
|
||||||
|
wan_3p0_prime: "Wan 3.0 Prime",
|
||||||
// Seedance — dot-normalized aliases
|
// Seedance — dot-normalized aliases
|
||||||
seedance_1_0_lite: "Seedance 1.0 Lite",
|
seedance_1_0_lite: "Seedance 1.0 Lite",
|
||||||
// Anonymous preview aliases the backend used pre-announce
|
// Anonymous preview aliases the backend used pre-announce
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
/// Not all models have each of these qualities,
|
/// Not all models have each of these qualities,
|
||||||
/// but Tauri will interpolate if a mistake is sent.
|
/// but Tauri will interpolate if a mistake is sent.
|
||||||
export enum CommonQuality {
|
export enum CommonQuality {
|
||||||
|
Auto = "auto",
|
||||||
|
Max = "max",
|
||||||
|
XHigh = "xhigh",
|
||||||
High = "high",
|
High = "high",
|
||||||
Medium = "medium",
|
Medium = "medium",
|
||||||
Low = "low",
|
Low = "low",
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
import type { VideoModel } from "./VideoModel.js";
|
import type { VideoModel } from "./VideoModel.js";
|
||||||
|
|
||||||
|
export function resolveVideoOutputFormat(model: VideoModel, current?: string | null): string | undefined {
|
||||||
|
const options = model.outputFormatOptions;
|
||||||
|
if (!options?.length) return undefined;
|
||||||
|
if (current && options.includes(current)) return current;
|
||||||
|
return model.defaultOutputFormat ?? options[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function videoGenerationCounts(model: VideoModel): number[] {
|
||||||
|
return model.generationCountOptions?.length ? model.generationCountOptions : [1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveVideoGenerationCount(model: VideoModel, current: number): number {
|
||||||
|
const options = videoGenerationCounts(model);
|
||||||
|
if (options.includes(current)) return current;
|
||||||
|
return model.defaultGenerationCount != null && options.includes(model.defaultGenerationCount)
|
||||||
|
? model.defaultGenerationCount : options[0];
|
||||||
|
}
|
||||||
|
|
||||||
/** Normalize old display values while preserving future API values. */
|
/** Normalize old display values while preserving future API values. */
|
||||||
export function videoResolutionValue(value: string): string {
|
export function videoResolutionValue(value: string): string {
|
||||||
const legacy: Record<string, string> = {
|
const legacy: Record<string, string> = {
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
|||||||
progressBarTime: 60000,
|
progressBarTime: 60000,
|
||||||
maxPromptLength: Infinity,
|
maxPromptLength: Infinity,
|
||||||
canChangeAspectRatio: true,
|
canChangeAspectRatio: true,
|
||||||
defaultAspectRatio: CommonAspectRatio.WideSixteenByNine,
|
defaultAspectRatio: CommonAspectRatio.Square,
|
||||||
aspectRatios: [
|
aspectRatios: [
|
||||||
CommonAspectRatio.Auto,
|
CommonAspectRatio.Auto,
|
||||||
CommonAspectRatio.Square,
|
CommonAspectRatio.Square,
|
||||||
@@ -413,8 +413,19 @@ export const IMAGE_MODELS: ImageModel[] = [
|
|||||||
CommonAspectRatio.WideFourByThree,
|
CommonAspectRatio.WideFourByThree,
|
||||||
CommonAspectRatio.WideSixteenByNine,
|
CommonAspectRatio.WideSixteenByNine,
|
||||||
],
|
],
|
||||||
|
canChangeResolution: true,
|
||||||
|
resolutions: [
|
||||||
|
CommonResolution.OneK,
|
||||||
|
CommonResolution.TwoK,
|
||||||
|
CommonResolution.ThreeK,
|
||||||
|
CommonResolution.FourK,
|
||||||
|
],
|
||||||
|
defaultResolution: CommonResolution.OneK,
|
||||||
defaultQuality: CommonQuality.High,
|
defaultQuality: CommonQuality.High,
|
||||||
qualityOptions: [
|
qualityOptions: [
|
||||||
|
CommonQuality.Auto,
|
||||||
|
CommonQuality.Max,
|
||||||
|
CommonQuality.XHigh,
|
||||||
CommonQuality.High,
|
CommonQuality.High,
|
||||||
CommonQuality.Medium,
|
CommonQuality.Medium,
|
||||||
CommonQuality.Low,
|
CommonQuality.Low,
|
||||||
@@ -438,7 +449,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
|||||||
progressBarTime: 120000,
|
progressBarTime: 120000,
|
||||||
maxPromptLength: Infinity,
|
maxPromptLength: Infinity,
|
||||||
canChangeAspectRatio: true,
|
canChangeAspectRatio: true,
|
||||||
defaultAspectRatio: CommonAspectRatio.WideSixteenByNine,
|
defaultAspectRatio: CommonAspectRatio.Square,
|
||||||
aspectRatios: [
|
aspectRatios: [
|
||||||
CommonAspectRatio.Auto,
|
CommonAspectRatio.Auto,
|
||||||
CommonAspectRatio.Square,
|
CommonAspectRatio.Square,
|
||||||
@@ -448,8 +459,19 @@ export const IMAGE_MODELS: ImageModel[] = [
|
|||||||
CommonAspectRatio.WideFourByThree,
|
CommonAspectRatio.WideFourByThree,
|
||||||
CommonAspectRatio.WideSixteenByNine,
|
CommonAspectRatio.WideSixteenByNine,
|
||||||
],
|
],
|
||||||
|
canChangeResolution: true,
|
||||||
|
resolutions: [
|
||||||
|
CommonResolution.OneK,
|
||||||
|
CommonResolution.TwoK,
|
||||||
|
CommonResolution.ThreeK,
|
||||||
|
CommonResolution.FourK,
|
||||||
|
],
|
||||||
|
defaultResolution: CommonResolution.OneK,
|
||||||
defaultQuality: CommonQuality.High,
|
defaultQuality: CommonQuality.High,
|
||||||
qualityOptions: [
|
qualityOptions: [
|
||||||
|
CommonQuality.Auto,
|
||||||
|
CommonQuality.Max,
|
||||||
|
CommonQuality.XHigh,
|
||||||
CommonQuality.High,
|
CommonQuality.High,
|
||||||
CommonQuality.Medium,
|
CommonQuality.Medium,
|
||||||
CommonQuality.Low,
|
CommonQuality.Low,
|
||||||
|
|||||||
@@ -345,6 +345,8 @@ export const VIDEO_MODELS: VideoModel[] = [
|
|||||||
new VideoModel({
|
new VideoModel({
|
||||||
id: "seedance_2p0",
|
id: "seedance_2p0",
|
||||||
tauriId: "seedance_2p0",
|
tauriId: "seedance_2p0",
|
||||||
|
generationCountOptions: [1, 2, 3, 4],
|
||||||
|
defaultGenerationCount: 1,
|
||||||
fullName: "Seedance 2.0",
|
fullName: "Seedance 2.0",
|
||||||
category: "video",
|
category: "video",
|
||||||
creator: ModelCreator.Bytedance,
|
creator: ModelCreator.Bytedance,
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ import {
|
|||||||
} from "./buildModelsFromListing.js";
|
} from "./buildModelsFromListing.js";
|
||||||
import {
|
import {
|
||||||
resolveVideoDuration,
|
resolveVideoDuration,
|
||||||
|
resolveVideoGenerationCount,
|
||||||
|
resolveVideoOutputFormat,
|
||||||
|
videoGenerationCounts,
|
||||||
videoDurationRange,
|
videoDurationRange,
|
||||||
videoResolutionValue,
|
videoResolutionValue,
|
||||||
} from "../classes/videoOptions.js";
|
} from "../classes/videoOptions.js";
|
||||||
import { IMAGE_MODELS } from "../lists/ImageModels.js";
|
import { IMAGE_MODELS } from "../lists/ImageModels.js";
|
||||||
|
import { CommonQuality } from "../classes/properties/CommonQuality.js";
|
||||||
import { VIDEO_MODELS } from "../lists/VideoModels.js";
|
import { VIDEO_MODELS } from "../lists/VideoModels.js";
|
||||||
import { SPLAT_MODELS } from "../lists/SplatModels.js";
|
import { SPLAT_MODELS } from "../lists/SplatModels.js";
|
||||||
|
|
||||||
@@ -24,6 +28,8 @@ describe("desktop catalog", () => {
|
|||||||
"flux_3",
|
"flux_3",
|
||||||
"minimax_h3",
|
"minimax_h3",
|
||||||
"vidu_q3",
|
"vidu_q3",
|
||||||
|
"wan_3p0",
|
||||||
|
"wan_3p0_prime",
|
||||||
"future_video_v99",
|
"future_video_v99",
|
||||||
];
|
];
|
||||||
const models = buildVideoModelsFromListing(
|
const models = buildVideoModelsFromListing(
|
||||||
@@ -97,6 +103,61 @@ describe("desktop catalog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("API video options", () => {
|
describe("API video options", () => {
|
||||||
|
it.each(["wan_3p0", "wan_3p0_prime"])("hydrates %s with Wan's reference and duration capabilities", (id) => {
|
||||||
|
const [model] = buildVideoModelsFromListing([], [{
|
||||||
|
model: id,
|
||||||
|
model_creator: "alibaba",
|
||||||
|
text_to_video_supported: true,
|
||||||
|
text_prompt_max_length: 20000,
|
||||||
|
starting_keyframe_supported: true,
|
||||||
|
ending_keyframe_supported: true,
|
||||||
|
image_references_supported: true,
|
||||||
|
image_references_max: 10,
|
||||||
|
video_references_max: 5,
|
||||||
|
video_references_max_total_duration_seconds: 15,
|
||||||
|
audio_references_max: 5,
|
||||||
|
audio_references_max_total_duration_seconds: 15,
|
||||||
|
show_generate_with_sound_toggle: true,
|
||||||
|
resolution_options: ["four_eighty_p", "seven_twenty_p", "ten_eighty_p"],
|
||||||
|
resolution_default: "seven_twenty_p",
|
||||||
|
duration_seconds_min: 2,
|
||||||
|
duration_seconds_max: 30,
|
||||||
|
duration_seconds_default: 5,
|
||||||
|
batch_size_options: [1],
|
||||||
|
batch_size_default: 1,
|
||||||
|
}]);
|
||||||
|
expect(model.getProviders()).toEqual([GenerationProvider.Artcraft]);
|
||||||
|
expect(model).toMatchObject({
|
||||||
|
startFrame: true, endFrame: true, textToVideoSupported: true,
|
||||||
|
supportsReferenceMode: true, maxReferenceImages: 10,
|
||||||
|
maxReferenceVideos: 5, maxVideoRefDuration: 15,
|
||||||
|
maxReferenceAudios: 5, maxAudioRefDuration: 15,
|
||||||
|
generateWithSound: true, maxPromptLength: 20000,
|
||||||
|
resolutionOptions: ["480p", "720p", "1080p"], defaultResolution: "720p",
|
||||||
|
});
|
||||||
|
expect(videoDurationRange(model, true)).toEqual({ min: 2, max: 30 });
|
||||||
|
expect(resolveVideoDuration(model, null, false)).toBe(5);
|
||||||
|
expect(videoGenerationCounts(model)).toEqual([1]);
|
||||||
|
expect(resolveVideoGenerationCount(model, 4)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["seedance_2p5", "seedance_2p5_u"])("preserves %s output formats and batch choices", (id) => {
|
||||||
|
const [model] = buildVideoModelsFromListing([], [{
|
||||||
|
model: id,
|
||||||
|
output_format_options: ["mp4", "mov", "future_format"],
|
||||||
|
output_format_default: "mp4",
|
||||||
|
batch_size_options: [1, 2, 3, 4],
|
||||||
|
batch_size_default: 1,
|
||||||
|
}]);
|
||||||
|
expect(model.outputFormatOptions).toEqual(["mp4", "mov", "future_format"]);
|
||||||
|
expect(model.defaultOutputFormat).toBe("mp4");
|
||||||
|
expect(resolveVideoOutputFormat(model, "mov")).toBe("mov");
|
||||||
|
expect(resolveVideoOutputFormat(model, "unsupported")).toBe("mp4");
|
||||||
|
expect(videoGenerationCounts(model)).toEqual([1, 2, 3, 4]);
|
||||||
|
expect(resolveVideoGenerationCount(model, 3)).toBe(3);
|
||||||
|
expect(resolveVideoGenerationCount(model, 5)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses range durations, reference-mode caps, bitrate, and future resolutions", () => {
|
it("uses range durations, reference-mode caps, bitrate, and future resolutions", () => {
|
||||||
const [model] = buildVideoModelsFromListing(
|
const [model] = buildVideoModelsFromListing(
|
||||||
[],
|
[],
|
||||||
@@ -134,6 +195,37 @@ describe("API video options", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(["gpt_image_2p5_flare", "gpt_image_2p5_sunburst"])("preserves every %s quality and resolution option", (id) => {
|
||||||
|
const qualities = ["auto", "max", "xhigh", "high", "medium", "low"];
|
||||||
|
const resolutions = ["one_k", "two_k", "three_k", "four_k"];
|
||||||
|
const model = buildImageModelsFromListing(IMAGE_MODELS, [{
|
||||||
|
model: id,
|
||||||
|
quality_options: qualities,
|
||||||
|
default_quality: "high",
|
||||||
|
resolution_options: resolutions,
|
||||||
|
resolution_default: "one_k",
|
||||||
|
image_refs_supported: true,
|
||||||
|
}]).find((m) => m.tauriId === id)!;
|
||||||
|
expect(model.qualityOptions).toEqual(qualities);
|
||||||
|
expect(model.defaultQuality).toBe("high");
|
||||||
|
expect(model.resolutions).toEqual(resolutions);
|
||||||
|
expect(model.defaultResolution).toBe("one_k");
|
||||||
|
expect(model.canEditImages).toBe(true);
|
||||||
|
expect(model.resolveQuality(CommonQuality.Max)).toBe("max");
|
||||||
|
expect(model.resolveQuality(CommonQuality.Auto)).toBe("auto");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the new model's default when a saved quality tier is unsupported", () => {
|
||||||
|
const [model] = buildImageModelsFromListing([], [{
|
||||||
|
model: "gpt_image_2",
|
||||||
|
quality_options: ["high", "medium", "low"],
|
||||||
|
default_quality: "high",
|
||||||
|
}]);
|
||||||
|
expect(model.resolveQuality(CommonQuality.Max)).toBe("high");
|
||||||
|
expect(model.resolveQuality(CommonQuality.XHigh)).toBe("high");
|
||||||
|
expect(model.resolveQuality(CommonQuality.Low)).toBe("low");
|
||||||
|
});
|
||||||
|
|
||||||
it("offers ArtCraft and direct Midjourney only for supported Midjourney models", () => {
|
it("offers ArtCraft and direct Midjourney only for supported Midjourney models", () => {
|
||||||
const models = buildImageModelsFromListing([], [
|
const models = buildImageModelsFromListing([], [
|
||||||
{ model: "midjourney_7" }, { model: "midjourney_7_niji" },
|
{ model: "midjourney_7" }, { model: "midjourney_7_niji" },
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export interface ListingVideoModel extends ListingModelBase {
|
|||||||
duration_seconds_max_with_image_references?: number | null;
|
duration_seconds_max_with_image_references?: number | null;
|
||||||
bitrate_options?: string[] | null;
|
bitrate_options?: string[] | null;
|
||||||
bitrate_default?: string | null;
|
bitrate_default?: string | null;
|
||||||
|
output_format_options?: string[] | null;
|
||||||
|
output_format_default?: string | null;
|
||||||
duration_seconds_default?: number | null;
|
duration_seconds_default?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +271,10 @@ const mergedVideoModel = (
|
|||||||
maxDurationWithImageReferences: m.duration_seconds_max_with_image_references ?? o?.maxDurationWithImageReferences,
|
maxDurationWithImageReferences: m.duration_seconds_max_with_image_references ?? o?.maxDurationWithImageReferences,
|
||||||
bitrateOptions: m.bitrate_options ?? o?.bitrateOptions,
|
bitrateOptions: m.bitrate_options ?? o?.bitrateOptions,
|
||||||
defaultBitrate: m.bitrate_default ?? o?.defaultBitrate,
|
defaultBitrate: m.bitrate_default ?? o?.defaultBitrate,
|
||||||
|
outputFormatOptions: m.output_format_options ?? o?.outputFormatOptions,
|
||||||
|
defaultOutputFormat: m.output_format_default ?? o?.defaultOutputFormat,
|
||||||
|
generationCountOptions: m.batch_size_options ?? o?.generationCountOptions,
|
||||||
|
defaultGenerationCount: m.batch_size_default ?? o?.defaultGenerationCount,
|
||||||
defaultAspectRatio: m.aspect_ratio_default ?? o?.defaultAspectRatio,
|
defaultAspectRatio: m.aspect_ratio_default ?? o?.defaultAspectRatio,
|
||||||
supportsReferenceMode: m.image_references_supported ?? o?.supportsReferenceMode,
|
supportsReferenceMode: m.image_references_supported ?? o?.supportsReferenceMode,
|
||||||
maxReferenceImages: m.image_references_max ?? o?.maxReferenceImages,
|
maxReferenceImages: m.image_references_max ?? o?.maxReferenceImages,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface EstimateVideoCostRequest {
|
|||||||
video_batch_count?: number;
|
video_batch_count?: number;
|
||||||
generate_audio?: boolean;
|
generate_audio?: boolean;
|
||||||
bitrate?: string;
|
bitrate?: string;
|
||||||
|
output_format?: string;
|
||||||
start_frame_image_media_token?: string;
|
start_frame_image_media_token?: string;
|
||||||
end_frame_image_media_token?: string;
|
end_frame_image_media_token?: string;
|
||||||
reference_image_media_tokens?: string[];
|
reference_image_media_tokens?: string[];
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ it("passes unknown model and option strings through image and video commands", a
|
|||||||
const video = {
|
const video = {
|
||||||
model: "future_video",
|
model: "future_video",
|
||||||
bitrate: "high",
|
bitrate: "high",
|
||||||
|
output_format: "mov",
|
||||||
resolution: "eight_k",
|
resolution: "eight_k",
|
||||||
duration_seconds: 30,
|
duration_seconds: 30,
|
||||||
generate_audio: false,
|
generate_audio: false,
|
||||||
@@ -54,6 +55,25 @@ it("dispatches every added modality and its pricing through Tauri", async () =>
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(["gpt_image_2p5_flare", "gpt_image_2p5_sunburst"])("forwards every %s quality tier", async (model) => {
|
||||||
|
for (const quality of ["auto", "max", "xhigh", "high", "medium", "low"]) {
|
||||||
|
const request = { model, quality, resolution: "four_k" };
|
||||||
|
await GenerateImage(request);
|
||||||
|
expect(invoke).toHaveBeenLastCalledWith("generate_image_command", { request });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["wan_3p0", "wan_3p0_prime"])("forwards %s with reference media", async (model) => {
|
||||||
|
const request = {
|
||||||
|
model, duration_seconds: 15, resolution: "ten_eighty_p", generate_audio: true,
|
||||||
|
reference_image_media_tokens: ["mf_image"],
|
||||||
|
reference_video_media_tokens: ["mf_video"],
|
||||||
|
reference_audio_media_tokens: ["mf_audio"],
|
||||||
|
};
|
||||||
|
await GenerateVideo(request);
|
||||||
|
expect(invoke).toHaveBeenLastCalledWith("generate_video_command", { request });
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves reference duration hints in video pricing", async () => {
|
it("preserves reference duration hints in video pricing", async () => {
|
||||||
const request = {
|
const request = {
|
||||||
model: "future_video",
|
model: "future_video",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface GenerateVideoRequest {
|
|||||||
resolution?: CommonResolution | string;
|
resolution?: CommonResolution | string;
|
||||||
|
|
||||||
bitrate?: string;
|
bitrate?: string;
|
||||||
|
output_format?: string;
|
||||||
duration_seconds?: number;
|
duration_seconds?: number;
|
||||||
generate_audio?: boolean;
|
generate_audio?: boolean;
|
||||||
video_batch_count?: number;
|
video_batch_count?: number;
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export interface OmniGenVideoModelDetails {
|
|||||||
resolution_default?: string;
|
resolution_default?: string;
|
||||||
bitrate_options?: string[];
|
bitrate_options?: string[];
|
||||||
bitrate_default?: string;
|
bitrate_default?: string;
|
||||||
|
output_format_options?: string[];
|
||||||
|
output_format_default?: string;
|
||||||
quality_options?: string[];
|
quality_options?: string[];
|
||||||
default_quality?: string;
|
default_quality?: string;
|
||||||
duration_seconds_min?: number;
|
duration_seconds_min?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user