Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1795e2b94 | ||
|
|
9de8fb8979 | ||
|
|
a61fcd50ed | ||
|
|
c16db8793c | ||
|
|
b3311d1574 |
@@ -0,0 +1,131 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Forward-compatible, self-contained API-client copy of the server's `CommonAspectRatio` enum.
|
||||
///
|
||||
/// Serialized as a string. Any value this client build does not recognize is preserved
|
||||
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
|
||||
/// still round-trip back to the original string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
pub enum ApiClientAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
WideThreeByTwo,
|
||||
WideFourByThree,
|
||||
WideFiveByFour,
|
||||
WideSixteenByNine,
|
||||
WideTwentyOneByNine,
|
||||
TallTwoByThree,
|
||||
TallThreeByFour,
|
||||
TallFourByFive,
|
||||
TallNineBySixteen,
|
||||
TallNineByTwentyOne,
|
||||
Wide,
|
||||
Tall,
|
||||
Auto2k,
|
||||
Auto3k,
|
||||
Auto4k,
|
||||
SquareHd,
|
||||
/// A value not known to this client build (preserved as-is).
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for ApiClientAspectRatio {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"auto" => Self::Auto,
|
||||
"square" => Self::Square,
|
||||
"wide_three_by_two" => Self::WideThreeByTwo,
|
||||
"wide_four_by_three" => Self::WideFourByThree,
|
||||
"wide_five_by_four" => Self::WideFiveByFour,
|
||||
"wide_sixteen_by_nine" => Self::WideSixteenByNine,
|
||||
"wide_twenty_one_by_nine" => Self::WideTwentyOneByNine,
|
||||
"tall_two_by_three" => Self::TallTwoByThree,
|
||||
"tall_three_by_four" => Self::TallThreeByFour,
|
||||
"tall_four_by_five" => Self::TallFourByFive,
|
||||
"tall_nine_by_sixteen" => Self::TallNineBySixteen,
|
||||
"tall_nine_by_twenty_one" => Self::TallNineByTwentyOne,
|
||||
"wide" => Self::Wide,
|
||||
"tall" => Self::Tall,
|
||||
"auto_2k" => Self::Auto2k,
|
||||
"auto_3k" => Self::Auto3k,
|
||||
"auto_4k" => Self::Auto4k,
|
||||
"square_hd" => Self::SquareHd,
|
||||
_ => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiClientAspectRatio> for String {
|
||||
fn from(value: ApiClientAspectRatio) -> Self {
|
||||
match value {
|
||||
ApiClientAspectRatio::Auto => "auto".to_string(),
|
||||
ApiClientAspectRatio::Square => "square".to_string(),
|
||||
ApiClientAspectRatio::WideThreeByTwo => "wide_three_by_two".to_string(),
|
||||
ApiClientAspectRatio::WideFourByThree => "wide_four_by_three".to_string(),
|
||||
ApiClientAspectRatio::WideFiveByFour => "wide_five_by_four".to_string(),
|
||||
ApiClientAspectRatio::WideSixteenByNine => "wide_sixteen_by_nine".to_string(),
|
||||
ApiClientAspectRatio::WideTwentyOneByNine => "wide_twenty_one_by_nine".to_string(),
|
||||
ApiClientAspectRatio::TallTwoByThree => "tall_two_by_three".to_string(),
|
||||
ApiClientAspectRatio::TallThreeByFour => "tall_three_by_four".to_string(),
|
||||
ApiClientAspectRatio::TallFourByFive => "tall_four_by_five".to_string(),
|
||||
ApiClientAspectRatio::TallNineBySixteen => "tall_nine_by_sixteen".to_string(),
|
||||
ApiClientAspectRatio::TallNineByTwentyOne => "tall_nine_by_twenty_one".to_string(),
|
||||
ApiClientAspectRatio::Wide => "wide".to_string(),
|
||||
ApiClientAspectRatio::Tall => "tall".to_string(),
|
||||
ApiClientAspectRatio::Auto2k => "auto_2k".to_string(),
|
||||
ApiClientAspectRatio::Auto3k => "auto_3k".to_string(),
|
||||
ApiClientAspectRatio::Auto4k => "auto_4k".to_string(),
|
||||
ApiClientAspectRatio::SquareHd => "square_hd".to_string(),
|
||||
ApiClientAspectRatio::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_variant_round_trips() {
|
||||
let parsed: ApiClientAspectRatio = serde_json::from_str("\"auto\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientAspectRatio::Auto);
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"auto\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_value_is_preserved_and_round_trips() {
|
||||
let parsed: ApiClientAspectRatio = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientAspectRatio::Unknown("zzz_not_a_real_value".to_string()));
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_known_variant_round_trips() {
|
||||
let all = [
|
||||
(ApiClientAspectRatio::Auto, "auto"),
|
||||
(ApiClientAspectRatio::Square, "square"),
|
||||
(ApiClientAspectRatio::WideThreeByTwo, "wide_three_by_two"),
|
||||
(ApiClientAspectRatio::WideFourByThree, "wide_four_by_three"),
|
||||
(ApiClientAspectRatio::WideFiveByFour, "wide_five_by_four"),
|
||||
(ApiClientAspectRatio::WideSixteenByNine, "wide_sixteen_by_nine"),
|
||||
(ApiClientAspectRatio::WideTwentyOneByNine, "wide_twenty_one_by_nine"),
|
||||
(ApiClientAspectRatio::TallTwoByThree, "tall_two_by_three"),
|
||||
(ApiClientAspectRatio::TallThreeByFour, "tall_three_by_four"),
|
||||
(ApiClientAspectRatio::TallFourByFive, "tall_four_by_five"),
|
||||
(ApiClientAspectRatio::TallNineBySixteen, "tall_nine_by_sixteen"),
|
||||
(ApiClientAspectRatio::TallNineByTwentyOne, "tall_nine_by_twenty_one"),
|
||||
(ApiClientAspectRatio::Wide, "wide"),
|
||||
(ApiClientAspectRatio::Tall, "tall"),
|
||||
(ApiClientAspectRatio::Auto2k, "auto_2k"),
|
||||
(ApiClientAspectRatio::Auto3k, "auto_3k"),
|
||||
(ApiClientAspectRatio::Auto4k, "auto_4k"),
|
||||
(ApiClientAspectRatio::SquareHd, "square_hd"),
|
||||
];
|
||||
for (variant, s) in all {
|
||||
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
|
||||
let parsed: ApiClientAspectRatio = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
|
||||
assert_eq!(parsed, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Forward-compatible, self-contained API-client copy of the server's `CommonBitrate` enum.
|
||||
///
|
||||
/// Serialized as a string. Any value this client build does not recognize is preserved
|
||||
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
|
||||
/// still round-trip back to the original string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
pub enum ApiClientBitrate {
|
||||
Normal,
|
||||
High,
|
||||
/// A value not known to this client build (preserved as-is).
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for ApiClientBitrate {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"normal" => Self::Normal,
|
||||
"high" => Self::High,
|
||||
_ => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiClientBitrate> for String {
|
||||
fn from(value: ApiClientBitrate) -> Self {
|
||||
match value {
|
||||
ApiClientBitrate::Normal => "normal".to_string(),
|
||||
ApiClientBitrate::High => "high".to_string(),
|
||||
ApiClientBitrate::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_variant_round_trips() {
|
||||
let parsed: ApiClientBitrate = serde_json::from_str("\"normal\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientBitrate::Normal);
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"normal\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_value_is_preserved_and_round_trips() {
|
||||
let parsed: ApiClientBitrate = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientBitrate::Unknown("zzz_not_a_real_value".to_string()));
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_known_variant_round_trips() {
|
||||
let all = [
|
||||
(ApiClientBitrate::Normal, "normal"),
|
||||
(ApiClientBitrate::High, "high"),
|
||||
];
|
||||
for (variant, s) in all {
|
||||
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
|
||||
let parsed: ApiClientBitrate = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
|
||||
assert_eq!(parsed, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Forward-compatible, self-contained API-client copy of the server's `GenerationProvider` enum.
|
||||
///
|
||||
/// Serialized as a string. Any value this client build does not recognize is preserved
|
||||
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
|
||||
/// still round-trip back to the original string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
pub enum ApiClientGenerationProvider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
Grok,
|
||||
Midjourney,
|
||||
Sora,
|
||||
WorldLabs,
|
||||
/// A value not known to this client build (preserved as-is).
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for ApiClientGenerationProvider {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"artcraft" => Self::Artcraft,
|
||||
"fal" => Self::Fal,
|
||||
"grok" => Self::Grok,
|
||||
"midjourney" => Self::Midjourney,
|
||||
"sora" => Self::Sora,
|
||||
"world_labs" => Self::WorldLabs,
|
||||
_ => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiClientGenerationProvider> for String {
|
||||
fn from(value: ApiClientGenerationProvider) -> Self {
|
||||
match value {
|
||||
ApiClientGenerationProvider::Artcraft => "artcraft".to_string(),
|
||||
ApiClientGenerationProvider::Fal => "fal".to_string(),
|
||||
ApiClientGenerationProvider::Grok => "grok".to_string(),
|
||||
ApiClientGenerationProvider::Midjourney => "midjourney".to_string(),
|
||||
ApiClientGenerationProvider::Sora => "sora".to_string(),
|
||||
ApiClientGenerationProvider::WorldLabs => "world_labs".to_string(),
|
||||
ApiClientGenerationProvider::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_variant_round_trips() {
|
||||
let parsed: ApiClientGenerationProvider = serde_json::from_str("\"artcraft\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientGenerationProvider::Artcraft);
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"artcraft\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_value_is_preserved_and_round_trips() {
|
||||
let parsed: ApiClientGenerationProvider = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientGenerationProvider::Unknown("zzz_not_a_real_value".to_string()));
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_known_variant_round_trips() {
|
||||
let all = [
|
||||
(ApiClientGenerationProvider::Artcraft, "artcraft"),
|
||||
(ApiClientGenerationProvider::Fal, "fal"),
|
||||
(ApiClientGenerationProvider::Grok, "grok"),
|
||||
(ApiClientGenerationProvider::Midjourney, "midjourney"),
|
||||
(ApiClientGenerationProvider::Sora, "sora"),
|
||||
(ApiClientGenerationProvider::WorldLabs, "world_labs"),
|
||||
];
|
||||
for (variant, s) in all {
|
||||
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
|
||||
let parsed: ApiClientGenerationProvider = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
|
||||
assert_eq!(parsed, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Forward-compatible, self-contained API-client copy of the server's `CommonImageModel` enum.
|
||||
///
|
||||
/// Serialized as a string. Any value this client build does not recognize is preserved
|
||||
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
|
||||
/// still round-trip back to the original string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
pub enum ApiClientImageModel {
|
||||
Flux1Dev,
|
||||
Flux1Schnell,
|
||||
FluxPro11,
|
||||
FluxPro11Ultra,
|
||||
GptImage1,
|
||||
GptImage1p5,
|
||||
GptImage2,
|
||||
GrokImagineImage,
|
||||
GrokImagineImageQuality,
|
||||
Midjourney7,
|
||||
Midjourney7Niji,
|
||||
Midjourney8,
|
||||
NanoBanana,
|
||||
NanoBanana2,
|
||||
NanoBananaPro,
|
||||
Seedream4,
|
||||
Seedream4p5,
|
||||
Seedream5Lite,
|
||||
QwenEdit2511Angles,
|
||||
Flux2LoraAngles,
|
||||
/// A value not known to this client build (preserved as-is).
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for ApiClientImageModel {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"flux_1_dev" => Self::Flux1Dev,
|
||||
"flux_1_schnell" => Self::Flux1Schnell,
|
||||
"flux_pro_1p1" => Self::FluxPro11,
|
||||
"flux_pro_1p1_ultra" => Self::FluxPro11Ultra,
|
||||
"gpt_image_1" => Self::GptImage1,
|
||||
"gpt_image_1p5" => Self::GptImage1p5,
|
||||
"gpt_image_2" => Self::GptImage2,
|
||||
"grok_imagine_image" => Self::GrokImagineImage,
|
||||
"grok_imagine_image_q" => Self::GrokImagineImageQuality,
|
||||
"midjourney_7" => Self::Midjourney7,
|
||||
"midjourney_7_niji" => Self::Midjourney7Niji,
|
||||
"midjourney_8" => Self::Midjourney8,
|
||||
"nano_banana" => Self::NanoBanana,
|
||||
"nano_banana_2" => Self::NanoBanana2,
|
||||
"nano_banana_pro" => Self::NanoBananaPro,
|
||||
"seedream_4" => Self::Seedream4,
|
||||
"seedream_4p5" => Self::Seedream4p5,
|
||||
"seedream_5_lite" => Self::Seedream5Lite,
|
||||
"qwen_edit_2511_angles" => Self::QwenEdit2511Angles,
|
||||
"flux_2_lora_angles" => Self::Flux2LoraAngles,
|
||||
_ => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiClientImageModel> for String {
|
||||
fn from(value: ApiClientImageModel) -> Self {
|
||||
match value {
|
||||
ApiClientImageModel::Flux1Dev => "flux_1_dev".to_string(),
|
||||
ApiClientImageModel::Flux1Schnell => "flux_1_schnell".to_string(),
|
||||
ApiClientImageModel::FluxPro11 => "flux_pro_1p1".to_string(),
|
||||
ApiClientImageModel::FluxPro11Ultra => "flux_pro_1p1_ultra".to_string(),
|
||||
ApiClientImageModel::GptImage1 => "gpt_image_1".to_string(),
|
||||
ApiClientImageModel::GptImage1p5 => "gpt_image_1p5".to_string(),
|
||||
ApiClientImageModel::GptImage2 => "gpt_image_2".to_string(),
|
||||
ApiClientImageModel::GrokImagineImage => "grok_imagine_image".to_string(),
|
||||
ApiClientImageModel::GrokImagineImageQuality => "grok_imagine_image_q".to_string(),
|
||||
ApiClientImageModel::Midjourney7 => "midjourney_7".to_string(),
|
||||
ApiClientImageModel::Midjourney7Niji => "midjourney_7_niji".to_string(),
|
||||
ApiClientImageModel::Midjourney8 => "midjourney_8".to_string(),
|
||||
ApiClientImageModel::NanoBanana => "nano_banana".to_string(),
|
||||
ApiClientImageModel::NanoBanana2 => "nano_banana_2".to_string(),
|
||||
ApiClientImageModel::NanoBananaPro => "nano_banana_pro".to_string(),
|
||||
ApiClientImageModel::Seedream4 => "seedream_4".to_string(),
|
||||
ApiClientImageModel::Seedream4p5 => "seedream_4p5".to_string(),
|
||||
ApiClientImageModel::Seedream5Lite => "seedream_5_lite".to_string(),
|
||||
ApiClientImageModel::QwenEdit2511Angles => "qwen_edit_2511_angles".to_string(),
|
||||
ApiClientImageModel::Flux2LoraAngles => "flux_2_lora_angles".to_string(),
|
||||
ApiClientImageModel::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_variant_round_trips() {
|
||||
let parsed: ApiClientImageModel = serde_json::from_str("\"flux_1_dev\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientImageModel::Flux1Dev);
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"flux_1_dev\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_value_is_preserved_and_round_trips() {
|
||||
let parsed: ApiClientImageModel = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientImageModel::Unknown("zzz_not_a_real_value".to_string()));
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_known_variant_round_trips() {
|
||||
let all = [
|
||||
(ApiClientImageModel::Flux1Dev, "flux_1_dev"),
|
||||
(ApiClientImageModel::Flux1Schnell, "flux_1_schnell"),
|
||||
(ApiClientImageModel::FluxPro11, "flux_pro_1p1"),
|
||||
(ApiClientImageModel::FluxPro11Ultra, "flux_pro_1p1_ultra"),
|
||||
(ApiClientImageModel::GptImage1, "gpt_image_1"),
|
||||
(ApiClientImageModel::GptImage1p5, "gpt_image_1p5"),
|
||||
(ApiClientImageModel::GptImage2, "gpt_image_2"),
|
||||
(ApiClientImageModel::GrokImagineImage, "grok_imagine_image"),
|
||||
(ApiClientImageModel::GrokImagineImageQuality, "grok_imagine_image_q"),
|
||||
(ApiClientImageModel::Midjourney7, "midjourney_7"),
|
||||
(ApiClientImageModel::Midjourney7Niji, "midjourney_7_niji"),
|
||||
(ApiClientImageModel::Midjourney8, "midjourney_8"),
|
||||
(ApiClientImageModel::NanoBanana, "nano_banana"),
|
||||
(ApiClientImageModel::NanoBanana2, "nano_banana_2"),
|
||||
(ApiClientImageModel::NanoBananaPro, "nano_banana_pro"),
|
||||
(ApiClientImageModel::Seedream4, "seedream_4"),
|
||||
(ApiClientImageModel::Seedream4p5, "seedream_4p5"),
|
||||
(ApiClientImageModel::Seedream5Lite, "seedream_5_lite"),
|
||||
(ApiClientImageModel::QwenEdit2511Angles, "qwen_edit_2511_angles"),
|
||||
(ApiClientImageModel::Flux2LoraAngles, "flux_2_lora_angles"),
|
||||
];
|
||||
for (variant, s) in all {
|
||||
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
|
||||
let parsed: ApiClientImageModel = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
|
||||
assert_eq!(parsed, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Forward-compatible, self-contained API-client copy of the server's `ModelCreator` enum.
|
||||
///
|
||||
/// Serialized as a string. Any value this client build does not recognize is preserved
|
||||
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
|
||||
/// still round-trip back to the original string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
pub enum ApiClientModelCreator {
|
||||
Alibaba,
|
||||
ArtCraft,
|
||||
BlackForestLabs,
|
||||
Bytedance,
|
||||
Fal,
|
||||
Google,
|
||||
Grok,
|
||||
Hailuo,
|
||||
Higgsfield,
|
||||
Kling,
|
||||
Krea,
|
||||
Midjourney,
|
||||
OpenAi,
|
||||
OpenArt,
|
||||
Recraft,
|
||||
Replicate,
|
||||
Runway,
|
||||
Stability,
|
||||
Tencent,
|
||||
TensorArt,
|
||||
Vidu,
|
||||
WorldLabs,
|
||||
/// A value not known to this client build (preserved as-is).
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for ApiClientModelCreator {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"alibaba" => Self::Alibaba,
|
||||
"artcraft" => Self::ArtCraft,
|
||||
"black_forest_labs" => Self::BlackForestLabs,
|
||||
"bytedance" => Self::Bytedance,
|
||||
"fal" => Self::Fal,
|
||||
"google" => Self::Google,
|
||||
"grok" => Self::Grok,
|
||||
"hailuo" => Self::Hailuo,
|
||||
"higgsfield" => Self::Higgsfield,
|
||||
"kling" => Self::Kling,
|
||||
"krea" => Self::Krea,
|
||||
"midjourney" => Self::Midjourney,
|
||||
"open_ai" => Self::OpenAi,
|
||||
"open_art" => Self::OpenArt,
|
||||
"recraft" => Self::Recraft,
|
||||
"replicate" => Self::Replicate,
|
||||
"runway" => Self::Runway,
|
||||
"stability" => Self::Stability,
|
||||
"tencent" => Self::Tencent,
|
||||
"tensor_art" => Self::TensorArt,
|
||||
"vidu" => Self::Vidu,
|
||||
"world_labs" => Self::WorldLabs,
|
||||
_ => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiClientModelCreator> for String {
|
||||
fn from(value: ApiClientModelCreator) -> Self {
|
||||
match value {
|
||||
ApiClientModelCreator::Alibaba => "alibaba".to_string(),
|
||||
ApiClientModelCreator::ArtCraft => "artcraft".to_string(),
|
||||
ApiClientModelCreator::BlackForestLabs => "black_forest_labs".to_string(),
|
||||
ApiClientModelCreator::Bytedance => "bytedance".to_string(),
|
||||
ApiClientModelCreator::Fal => "fal".to_string(),
|
||||
ApiClientModelCreator::Google => "google".to_string(),
|
||||
ApiClientModelCreator::Grok => "grok".to_string(),
|
||||
ApiClientModelCreator::Hailuo => "hailuo".to_string(),
|
||||
ApiClientModelCreator::Higgsfield => "higgsfield".to_string(),
|
||||
ApiClientModelCreator::Kling => "kling".to_string(),
|
||||
ApiClientModelCreator::Krea => "krea".to_string(),
|
||||
ApiClientModelCreator::Midjourney => "midjourney".to_string(),
|
||||
ApiClientModelCreator::OpenAi => "open_ai".to_string(),
|
||||
ApiClientModelCreator::OpenArt => "open_art".to_string(),
|
||||
ApiClientModelCreator::Recraft => "recraft".to_string(),
|
||||
ApiClientModelCreator::Replicate => "replicate".to_string(),
|
||||
ApiClientModelCreator::Runway => "runway".to_string(),
|
||||
ApiClientModelCreator::Stability => "stability".to_string(),
|
||||
ApiClientModelCreator::Tencent => "tencent".to_string(),
|
||||
ApiClientModelCreator::TensorArt => "tensor_art".to_string(),
|
||||
ApiClientModelCreator::Vidu => "vidu".to_string(),
|
||||
ApiClientModelCreator::WorldLabs => "world_labs".to_string(),
|
||||
ApiClientModelCreator::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_variant_round_trips() {
|
||||
let parsed: ApiClientModelCreator = serde_json::from_str("\"alibaba\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientModelCreator::Alibaba);
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"alibaba\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_value_is_preserved_and_round_trips() {
|
||||
let parsed: ApiClientModelCreator = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientModelCreator::Unknown("zzz_not_a_real_value".to_string()));
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_known_variant_round_trips() {
|
||||
let all = [
|
||||
(ApiClientModelCreator::Alibaba, "alibaba"),
|
||||
(ApiClientModelCreator::ArtCraft, "artcraft"),
|
||||
(ApiClientModelCreator::BlackForestLabs, "black_forest_labs"),
|
||||
(ApiClientModelCreator::Bytedance, "bytedance"),
|
||||
(ApiClientModelCreator::Fal, "fal"),
|
||||
(ApiClientModelCreator::Google, "google"),
|
||||
(ApiClientModelCreator::Grok, "grok"),
|
||||
(ApiClientModelCreator::Hailuo, "hailuo"),
|
||||
(ApiClientModelCreator::Higgsfield, "higgsfield"),
|
||||
(ApiClientModelCreator::Kling, "kling"),
|
||||
(ApiClientModelCreator::Krea, "krea"),
|
||||
(ApiClientModelCreator::Midjourney, "midjourney"),
|
||||
(ApiClientModelCreator::OpenAi, "open_ai"),
|
||||
(ApiClientModelCreator::OpenArt, "open_art"),
|
||||
(ApiClientModelCreator::Recraft, "recraft"),
|
||||
(ApiClientModelCreator::Replicate, "replicate"),
|
||||
(ApiClientModelCreator::Runway, "runway"),
|
||||
(ApiClientModelCreator::Stability, "stability"),
|
||||
(ApiClientModelCreator::Tencent, "tencent"),
|
||||
(ApiClientModelCreator::TensorArt, "tensor_art"),
|
||||
(ApiClientModelCreator::Vidu, "vidu"),
|
||||
(ApiClientModelCreator::WorldLabs, "world_labs"),
|
||||
];
|
||||
for (variant, s) in all {
|
||||
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
|
||||
let parsed: ApiClientModelCreator = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
|
||||
assert_eq!(parsed, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Forward-compatible, self-contained API-client copy of the server's `CommonQuality` enum.
|
||||
///
|
||||
/// Serialized as a string. Any value this client build does not recognize is preserved
|
||||
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
|
||||
/// still round-trip back to the original string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
pub enum ApiClientQuality {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
/// A value not known to this client build (preserved as-is).
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for ApiClientQuality {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"high" => Self::High,
|
||||
"medium" => Self::Medium,
|
||||
"low" => Self::Low,
|
||||
_ => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiClientQuality> for String {
|
||||
fn from(value: ApiClientQuality) -> Self {
|
||||
match value {
|
||||
ApiClientQuality::High => "high".to_string(),
|
||||
ApiClientQuality::Medium => "medium".to_string(),
|
||||
ApiClientQuality::Low => "low".to_string(),
|
||||
ApiClientQuality::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_variant_round_trips() {
|
||||
let parsed: ApiClientQuality = serde_json::from_str("\"high\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientQuality::High);
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"high\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_value_is_preserved_and_round_trips() {
|
||||
let parsed: ApiClientQuality = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientQuality::Unknown("zzz_not_a_real_value".to_string()));
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_known_variant_round_trips() {
|
||||
let all = [
|
||||
(ApiClientQuality::High, "high"),
|
||||
(ApiClientQuality::Medium, "medium"),
|
||||
(ApiClientQuality::Low, "low"),
|
||||
];
|
||||
for (variant, s) in all {
|
||||
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
|
||||
let parsed: ApiClientQuality = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
|
||||
assert_eq!(parsed, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Forward-compatible, self-contained API-client copy of the server's `CommonResolution` enum.
|
||||
///
|
||||
/// Serialized as a string. Any value this client build does not recognize is preserved
|
||||
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
|
||||
/// still round-trip back to the original string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
pub enum ApiClientResolution {
|
||||
OneK,
|
||||
TwoK,
|
||||
ThreeK,
|
||||
FourK,
|
||||
HalfK,
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
/// A value not known to this client build (preserved as-is).
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for ApiClientResolution {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"one_k" => Self::OneK,
|
||||
"two_k" => Self::TwoK,
|
||||
"three_k" => Self::ThreeK,
|
||||
"four_k" => Self::FourK,
|
||||
"half_k" => Self::HalfK,
|
||||
"four_eighty_p" => Self::FourEightyP,
|
||||
"seven_twenty_p" => Self::SevenTwentyP,
|
||||
"ten_eighty_p" => Self::TenEightyP,
|
||||
_ => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiClientResolution> for String {
|
||||
fn from(value: ApiClientResolution) -> Self {
|
||||
match value {
|
||||
ApiClientResolution::OneK => "one_k".to_string(),
|
||||
ApiClientResolution::TwoK => "two_k".to_string(),
|
||||
ApiClientResolution::ThreeK => "three_k".to_string(),
|
||||
ApiClientResolution::FourK => "four_k".to_string(),
|
||||
ApiClientResolution::HalfK => "half_k".to_string(),
|
||||
ApiClientResolution::FourEightyP => "four_eighty_p".to_string(),
|
||||
ApiClientResolution::SevenTwentyP => "seven_twenty_p".to_string(),
|
||||
ApiClientResolution::TenEightyP => "ten_eighty_p".to_string(),
|
||||
ApiClientResolution::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_variant_round_trips() {
|
||||
let parsed: ApiClientResolution = serde_json::from_str("\"one_k\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientResolution::OneK);
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"one_k\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_value_is_preserved_and_round_trips() {
|
||||
let parsed: ApiClientResolution = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientResolution::Unknown("zzz_not_a_real_value".to_string()));
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_known_variant_round_trips() {
|
||||
let all = [
|
||||
(ApiClientResolution::OneK, "one_k"),
|
||||
(ApiClientResolution::TwoK, "two_k"),
|
||||
(ApiClientResolution::ThreeK, "three_k"),
|
||||
(ApiClientResolution::FourK, "four_k"),
|
||||
(ApiClientResolution::HalfK, "half_k"),
|
||||
(ApiClientResolution::FourEightyP, "four_eighty_p"),
|
||||
(ApiClientResolution::SevenTwentyP, "seven_twenty_p"),
|
||||
(ApiClientResolution::TenEightyP, "ten_eighty_p"),
|
||||
];
|
||||
for (variant, s) in all {
|
||||
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
|
||||
let parsed: ApiClientResolution = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
|
||||
assert_eq!(parsed, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Forward-compatible, self-contained API-client copy of the server's `CommonVideoModel` enum.
|
||||
///
|
||||
/// Serialized as a string. Any value this client build does not recognize is preserved
|
||||
/// verbatim in [`Unknown`], so newer server variants never break deserialization and
|
||||
/// still round-trip back to the original string.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
pub enum ApiClientVideoModel {
|
||||
GrokVideo,
|
||||
GrokImagineVideo,
|
||||
GrokImagineVideo1p5,
|
||||
Kling16Pro,
|
||||
Kling21Pro,
|
||||
Kling21Master,
|
||||
Kling2p5TurboPro,
|
||||
Kling2p6Pro,
|
||||
Kling3p0Standard,
|
||||
Kling3p0Pro,
|
||||
HappyHorse1p0,
|
||||
Seedance10Lite,
|
||||
Seedance1p5Pro,
|
||||
Seedance2p0,
|
||||
Seedance2p0Fast,
|
||||
Seedance2p0BytePlus,
|
||||
Seedance2p0BytePlusFast,
|
||||
Seedance2p0Ultra,
|
||||
Seedance2p0UltraFast,
|
||||
Seedance2p0BytePlusUltra,
|
||||
Seedance2p0BytePlusUltraFast,
|
||||
Seedance2p0Mini,
|
||||
Seedance2p0BytePlusMini,
|
||||
Seedance2p0BytePlusUltraMini,
|
||||
Sora2,
|
||||
Sora2Pro,
|
||||
Veo2,
|
||||
Veo3,
|
||||
Veo3Fast,
|
||||
Veo3p1,
|
||||
Veo3p1Fast,
|
||||
PreviewModel,
|
||||
PreviewModelFast,
|
||||
/// A value not known to this client build (preserved as-is).
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<String> for ApiClientVideoModel {
|
||||
fn from(value: String) -> Self {
|
||||
match value.as_str() {
|
||||
"grok_video" => Self::GrokVideo,
|
||||
"grok_imagine_video" => Self::GrokImagineVideo,
|
||||
"grok_imagine_video_1p5" => Self::GrokImagineVideo1p5,
|
||||
"kling_1p6_pro" => Self::Kling16Pro,
|
||||
"kling_2p1_pro" => Self::Kling21Pro,
|
||||
"kling_2p1_master" => Self::Kling21Master,
|
||||
"kling_2p5_turbo_pro" => Self::Kling2p5TurboPro,
|
||||
"kling_2p6_pro" => Self::Kling2p6Pro,
|
||||
"kling_3p0_standard" => Self::Kling3p0Standard,
|
||||
"kling_3p0_pro" => Self::Kling3p0Pro,
|
||||
"happy_horse_1p0" => Self::HappyHorse1p0,
|
||||
"seedance_1p0_lite" => Self::Seedance10Lite,
|
||||
"seedance_1p5_pro" => Self::Seedance1p5Pro,
|
||||
"seedance_2p0" => Self::Seedance2p0,
|
||||
"seedance_2p0_fast" => Self::Seedance2p0Fast,
|
||||
"seedance_2p0_bp" => Self::Seedance2p0BytePlus,
|
||||
"seedance_2p0_bp_fast" => Self::Seedance2p0BytePlusFast,
|
||||
"seedance_2p0_u" => Self::Seedance2p0Ultra,
|
||||
"seedance_2p0_u_fast" => Self::Seedance2p0UltraFast,
|
||||
"seedance_2p0_bpu" => Self::Seedance2p0BytePlusUltra,
|
||||
"seedance_2p0_bpu_fast" => Self::Seedance2p0BytePlusUltraFast,
|
||||
"seedance_2p0_mini" => Self::Seedance2p0Mini,
|
||||
"seedance_2p0_bp_mini" => Self::Seedance2p0BytePlusMini,
|
||||
"seedance_2p0_bpu_mini" => Self::Seedance2p0BytePlusUltraMini,
|
||||
"sora_2" => Self::Sora2,
|
||||
"sora_2_pro" => Self::Sora2Pro,
|
||||
"veo_2" => Self::Veo2,
|
||||
"veo_3" => Self::Veo3,
|
||||
"veo_3_fast" => Self::Veo3Fast,
|
||||
"veo_3p1" => Self::Veo3p1,
|
||||
"veo_3p1_fast" => Self::Veo3p1Fast,
|
||||
"preview_model" => Self::PreviewModel,
|
||||
"preview_model_fast" => Self::PreviewModelFast,
|
||||
_ => Self::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApiClientVideoModel> for String {
|
||||
fn from(value: ApiClientVideoModel) -> Self {
|
||||
match value {
|
||||
ApiClientVideoModel::GrokVideo => "grok_video".to_string(),
|
||||
ApiClientVideoModel::GrokImagineVideo => "grok_imagine_video".to_string(),
|
||||
ApiClientVideoModel::GrokImagineVideo1p5 => "grok_imagine_video_1p5".to_string(),
|
||||
ApiClientVideoModel::Kling16Pro => "kling_1p6_pro".to_string(),
|
||||
ApiClientVideoModel::Kling21Pro => "kling_2p1_pro".to_string(),
|
||||
ApiClientVideoModel::Kling21Master => "kling_2p1_master".to_string(),
|
||||
ApiClientVideoModel::Kling2p5TurboPro => "kling_2p5_turbo_pro".to_string(),
|
||||
ApiClientVideoModel::Kling2p6Pro => "kling_2p6_pro".to_string(),
|
||||
ApiClientVideoModel::Kling3p0Standard => "kling_3p0_standard".to_string(),
|
||||
ApiClientVideoModel::Kling3p0Pro => "kling_3p0_pro".to_string(),
|
||||
ApiClientVideoModel::HappyHorse1p0 => "happy_horse_1p0".to_string(),
|
||||
ApiClientVideoModel::Seedance10Lite => "seedance_1p0_lite".to_string(),
|
||||
ApiClientVideoModel::Seedance1p5Pro => "seedance_1p5_pro".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0 => "seedance_2p0".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0Fast => "seedance_2p0_fast".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0BytePlus => "seedance_2p0_bp".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0BytePlusFast => "seedance_2p0_bp_fast".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0Ultra => "seedance_2p0_u".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0UltraFast => "seedance_2p0_u_fast".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0BytePlusUltra => "seedance_2p0_bpu".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0BytePlusUltraFast => "seedance_2p0_bpu_fast".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0Mini => "seedance_2p0_mini".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0BytePlusMini => "seedance_2p0_bp_mini".to_string(),
|
||||
ApiClientVideoModel::Seedance2p0BytePlusUltraMini => "seedance_2p0_bpu_mini".to_string(),
|
||||
ApiClientVideoModel::Sora2 => "sora_2".to_string(),
|
||||
ApiClientVideoModel::Sora2Pro => "sora_2_pro".to_string(),
|
||||
ApiClientVideoModel::Veo2 => "veo_2".to_string(),
|
||||
ApiClientVideoModel::Veo3 => "veo_3".to_string(),
|
||||
ApiClientVideoModel::Veo3Fast => "veo_3_fast".to_string(),
|
||||
ApiClientVideoModel::Veo3p1 => "veo_3p1".to_string(),
|
||||
ApiClientVideoModel::Veo3p1Fast => "veo_3p1_fast".to_string(),
|
||||
ApiClientVideoModel::PreviewModel => "preview_model".to_string(),
|
||||
ApiClientVideoModel::PreviewModelFast => "preview_model_fast".to_string(),
|
||||
ApiClientVideoModel::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_variant_round_trips() {
|
||||
let parsed: ApiClientVideoModel = serde_json::from_str("\"grok_video\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientVideoModel::GrokVideo);
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"grok_video\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_value_is_preserved_and_round_trips() {
|
||||
let parsed: ApiClientVideoModel = serde_json::from_str("\"zzz_not_a_real_value\"").unwrap();
|
||||
assert_eq!(parsed, ApiClientVideoModel::Unknown("zzz_not_a_real_value".to_string()));
|
||||
assert_eq!(serde_json::to_string(&parsed).unwrap(), "\"zzz_not_a_real_value\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_known_variant_round_trips() {
|
||||
let all = [
|
||||
(ApiClientVideoModel::GrokVideo, "grok_video"),
|
||||
(ApiClientVideoModel::GrokImagineVideo, "grok_imagine_video"),
|
||||
(ApiClientVideoModel::GrokImagineVideo1p5, "grok_imagine_video_1p5"),
|
||||
(ApiClientVideoModel::Kling16Pro, "kling_1p6_pro"),
|
||||
(ApiClientVideoModel::Kling21Pro, "kling_2p1_pro"),
|
||||
(ApiClientVideoModel::Kling21Master, "kling_2p1_master"),
|
||||
(ApiClientVideoModel::Kling2p5TurboPro, "kling_2p5_turbo_pro"),
|
||||
(ApiClientVideoModel::Kling2p6Pro, "kling_2p6_pro"),
|
||||
(ApiClientVideoModel::Kling3p0Standard, "kling_3p0_standard"),
|
||||
(ApiClientVideoModel::Kling3p0Pro, "kling_3p0_pro"),
|
||||
(ApiClientVideoModel::HappyHorse1p0, "happy_horse_1p0"),
|
||||
(ApiClientVideoModel::Seedance10Lite, "seedance_1p0_lite"),
|
||||
(ApiClientVideoModel::Seedance1p5Pro, "seedance_1p5_pro"),
|
||||
(ApiClientVideoModel::Seedance2p0, "seedance_2p0"),
|
||||
(ApiClientVideoModel::Seedance2p0Fast, "seedance_2p0_fast"),
|
||||
(ApiClientVideoModel::Seedance2p0BytePlus, "seedance_2p0_bp"),
|
||||
(ApiClientVideoModel::Seedance2p0BytePlusFast, "seedance_2p0_bp_fast"),
|
||||
(ApiClientVideoModel::Seedance2p0Ultra, "seedance_2p0_u"),
|
||||
(ApiClientVideoModel::Seedance2p0UltraFast, "seedance_2p0_u_fast"),
|
||||
(ApiClientVideoModel::Seedance2p0BytePlusUltra, "seedance_2p0_bpu"),
|
||||
(ApiClientVideoModel::Seedance2p0BytePlusUltraFast, "seedance_2p0_bpu_fast"),
|
||||
(ApiClientVideoModel::Seedance2p0Mini, "seedance_2p0_mini"),
|
||||
(ApiClientVideoModel::Seedance2p0BytePlusMini, "seedance_2p0_bp_mini"),
|
||||
(ApiClientVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini"),
|
||||
(ApiClientVideoModel::Sora2, "sora_2"),
|
||||
(ApiClientVideoModel::Sora2Pro, "sora_2_pro"),
|
||||
(ApiClientVideoModel::Veo2, "veo_2"),
|
||||
(ApiClientVideoModel::Veo3, "veo_3"),
|
||||
(ApiClientVideoModel::Veo3Fast, "veo_3_fast"),
|
||||
(ApiClientVideoModel::Veo3p1, "veo_3p1"),
|
||||
(ApiClientVideoModel::Veo3p1Fast, "veo_3p1_fast"),
|
||||
(ApiClientVideoModel::PreviewModel, "preview_model"),
|
||||
(ApiClientVideoModel::PreviewModelFast, "preview_model_fast"),
|
||||
];
|
||||
for (variant, s) in all {
|
||||
assert_eq!(String::from(variant.clone()), s, "serialize mismatch for {:?}", variant);
|
||||
let parsed: ApiClientVideoModel = serde_json::from_str(&format!("\"{}\"", s)).unwrap();
|
||||
assert_eq!(parsed, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod api_client_aspect_ratio;
|
||||
pub mod api_client_bitrate;
|
||||
pub mod api_client_generation_provider;
|
||||
pub mod api_client_image_model;
|
||||
pub mod api_client_model_creator;
|
||||
pub mod api_client_quality;
|
||||
pub mod api_client_resolution;
|
||||
pub mod api_client_video_model;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generation;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod common_enums;
|
||||
@@ -1 +1,2 @@
|
||||
pub mod generate;
|
||||
pub mod models;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod omni_gen_list_image_models;
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Self-contained, forward-compatible binding for `GET /v1/omni_gen/models/image`.
|
||||
//!
|
||||
//! IMPORTANT: This module deliberately does NOT reuse any types from
|
||||
//! `artcraft_api_defs`. Client builds may be deployed in the wild long after the
|
||||
//! server's models, enums, and response shapes have changed, so every request /
|
||||
//! response type is copied here and made permissive:
|
||||
//! - Unknown JSON fields are ignored (serde default behavior).
|
||||
//! - Missing collection/flag fields default rather than fail (`serde(default)`).
|
||||
//! - Every string-valued enum has an `Unknown(String)` catch-all so new server
|
||||
//! variants deserialize instead of erroring.
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::datatypes::common_enums::generation::api_client_aspect_ratio::ApiClientAspectRatio;
|
||||
use crate::datatypes::common_enums::generation::api_client_generation_provider::ApiClientGenerationProvider;
|
||||
use crate::datatypes::common_enums::generation::api_client_image_model::ApiClientImageModel;
|
||||
use crate::datatypes::common_enums::generation::api_client_model_creator::ApiClientModelCreator;
|
||||
use crate::datatypes::common_enums::generation::api_client_quality::ApiClientQuality;
|
||||
use crate::datatypes::common_enums::generation::api_client_resolution::ApiClientResolution;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_get_request::basic_json_get_request;
|
||||
|
||||
pub const OMNI_GEN_IMAGE_MODELS_PATH: &str = "/v1/omni_gen/models/image";
|
||||
|
||||
/// Arguments for [`omni_gen_list_image_models`].
|
||||
pub struct OmniGenListImageModelsArgs<'a> {
|
||||
pub api_host: &'a ApiHost,
|
||||
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
|
||||
/// Which provider's models to list. `None` lets the server default (artcraft).
|
||||
pub provider: Option<OmniGenImageModelsProvider>,
|
||||
}
|
||||
|
||||
/// List available image models.
|
||||
pub async fn omni_gen_list_image_models(
|
||||
args: OmniGenListImageModelsArgs<'_>,
|
||||
) -> Result<OmniGenImageModelsResponse, StorytellerError> {
|
||||
let path = match args.provider {
|
||||
Some(provider) => format!("{}?provider={}", OMNI_GEN_IMAGE_MODELS_PATH, provider.as_query_value()),
|
||||
None => OMNI_GEN_IMAGE_MODELS_PATH.to_string(),
|
||||
};
|
||||
|
||||
Ok(basic_json_get_request(args.api_host, &path, args.maybe_creds).await?)
|
||||
}
|
||||
|
||||
/// The provider filter for the models endpoint (a client-supplied request value).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OmniGenImageModelsProvider {
|
||||
/// Only models available through ArtCraft.
|
||||
Artcraft,
|
||||
/// All known models across all providers.
|
||||
All,
|
||||
}
|
||||
|
||||
impl OmniGenImageModelsProvider {
|
||||
pub fn as_query_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Artcraft => "artcraft",
|
||||
Self::All => "all",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OmniGenImageModelsProvider {
|
||||
fn default() -> Self {
|
||||
Self::Artcraft
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Response types ============================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OmniGenImageModelsResponse {
|
||||
#[serde(default)]
|
||||
pub success: bool,
|
||||
#[serde(default)]
|
||||
pub models: Vec<OmniGenImageModelDetails>,
|
||||
#[serde(default)]
|
||||
pub providers: Vec<OmniGenImageModelProviderDetails>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OmniGenImageModelProviderDetails {
|
||||
pub provider: ApiClientGenerationProvider,
|
||||
#[serde(default)]
|
||||
pub models: Vec<OmniGenImageProviderModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OmniGenImageProviderModelDetails {
|
||||
pub model: ApiClientImageModel,
|
||||
#[serde(default)]
|
||||
pub overrides: Option<OmniGenImageModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OmniGenImageModelDetails {
|
||||
pub model: ApiClientImageModel,
|
||||
#[serde(default)]
|
||||
pub model_creator: Option<ApiClientModelCreator>,
|
||||
#[serde(default)]
|
||||
pub full_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub text_prompt_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub text_prompt_max_length: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub negative_text_prompt_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub negative_text_prompt_max_length: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub image_refs_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub image_refs_max: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub has_fixed_editing_aspect_ratio: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub aspect_ratio_options: Option<Vec<ApiClientAspectRatio>>,
|
||||
#[serde(default)]
|
||||
pub aspect_ratio_default: Option<ApiClientAspectRatio>,
|
||||
#[serde(default)]
|
||||
pub aspect_ratio_default_when_editing: Option<ApiClientAspectRatio>,
|
||||
#[serde(default)]
|
||||
pub resolution_options: Option<Vec<ApiClientResolution>>,
|
||||
#[serde(default)]
|
||||
pub resolution_default: Option<ApiClientResolution>,
|
||||
#[serde(default)]
|
||||
pub quality_options: Option<Vec<ApiClientQuality>>,
|
||||
#[serde(default)]
|
||||
pub default_quality: Option<ApiClientQuality>,
|
||||
#[serde(default)]
|
||||
pub batch_size_min: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub batch_size_max: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub batch_size_options: Option<Vec<u16>>,
|
||||
#[serde(default)]
|
||||
pub batch_size_default: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn response_tolerates_unknown_fields_and_variants() {
|
||||
let json = r#"{
|
||||
"success": true,
|
||||
"models": [
|
||||
{
|
||||
"model": "brand_new_image_model",
|
||||
"model_creator": "some_new_studio",
|
||||
"aspect_ratio_options": ["square", "some_new_ratio"],
|
||||
"surprise_new_field": 123
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
{ "provider": "new_provider", "models": [ { "model": "nano_banana_pro" } ] }
|
||||
],
|
||||
"another_unexpected_top_level_field": "ignored"
|
||||
}"#;
|
||||
|
||||
let resp: OmniGenImageModelsResponse = serde_json::from_str(json).unwrap();
|
||||
assert!(resp.success);
|
||||
assert_eq!(resp.models.len(), 1);
|
||||
assert_eq!(resp.models[0].model, ApiClientImageModel::Unknown("brand_new_image_model".to_string()));
|
||||
assert_eq!(resp.models[0].model_creator, Some(ApiClientModelCreator::Unknown("some_new_studio".to_string())));
|
||||
let ratios = resp.models[0].aspect_ratio_options.as_ref().unwrap();
|
||||
assert_eq!(ratios[0], ApiClientAspectRatio::Square);
|
||||
assert_eq!(ratios[1], ApiClientAspectRatio::Unknown("some_new_ratio".to_string()));
|
||||
assert_eq!(resp.models[0].is_disabled, None);
|
||||
assert_eq!(resp.providers[0].provider, ApiClientGenerationProvider::Unknown("new_provider".to_string()));
|
||||
assert_eq!(resp.providers[0].models[0].model, ApiClientImageModel::NanoBananaPro);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_object_uses_defaults() {
|
||||
let resp: OmniGenImageModelsResponse = serde_json::from_str("{}").unwrap();
|
||||
assert!(!resp.success);
|
||||
assert!(resp.models.is_empty());
|
||||
assert!(resp.providers.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// Live tests that hit real servers. All `#[ignore]` so they never run by default.
|
||||
/// Run explicitly, e.g.:
|
||||
/// cargo test -p artcraft_client omni_gen_list_image_models::live_tests -- --ignored --nocapture
|
||||
#[cfg(test)]
|
||||
mod live_tests {
|
||||
mod localhost {
|
||||
use super::super::{omni_gen_list_image_models, OmniGenListImageModelsArgs};
|
||||
use crate::utils::api_host::ApiHost;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // live: hits http://localhost:12345
|
||||
async fn list_image_models() {
|
||||
let api_host = ApiHost::Localhost { port: 12345 };
|
||||
let response = omni_gen_list_image_models(OmniGenListImageModelsArgs {
|
||||
api_host: &api_host,
|
||||
maybe_creds: None,
|
||||
provider: None,
|
||||
})
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
println!("[localhost] image models response: {:#?}", response);
|
||||
assert!(response.success);
|
||||
assert!(!response.models.is_empty(), "expected at least one image model");
|
||||
}
|
||||
}
|
||||
|
||||
mod production {
|
||||
use super::super::{omni_gen_list_image_models, OmniGenListImageModelsArgs};
|
||||
use crate::utils::api_host::ApiHost;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // live: hits https://api.storyteller.ai
|
||||
async fn list_image_models() {
|
||||
let api_host = ApiHost::Storyteller;
|
||||
let response = omni_gen_list_image_models(OmniGenListImageModelsArgs {
|
||||
api_host: &api_host,
|
||||
maybe_creds: None,
|
||||
provider: None,
|
||||
})
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
println!("[production] image models response: {:#?}", response);
|
||||
assert!(response.success);
|
||||
assert!(!response.models.is_empty(), "expected at least one image model");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod omni_gen_list_video_models;
|
||||
@@ -0,0 +1,291 @@
|
||||
//! Self-contained, forward-compatible binding for `GET /v1/omni_gen/models/video`.
|
||||
//!
|
||||
//! IMPORTANT: This module deliberately does NOT reuse any types from
|
||||
//! `artcraft_api_defs`. Client builds may be deployed in the wild long after the
|
||||
//! server's models, enums, and response shapes have changed, so every request /
|
||||
//! response type is copied here and made permissive:
|
||||
//! - Unknown JSON fields are ignored (serde default behavior).
|
||||
//! - Missing collection/flag fields default rather than fail (`serde(default)`).
|
||||
//! - Every string-valued enum has an `Unknown(String)` catch-all so new server
|
||||
//! variants deserialize instead of erroring.
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_get_request::basic_json_get_request;
|
||||
use crate::datatypes::common_enums::generation::api_client_aspect_ratio::ApiClientAspectRatio;
|
||||
use crate::datatypes::common_enums::generation::api_client_bitrate::ApiClientBitrate;
|
||||
use crate::datatypes::common_enums::generation::api_client_generation_provider::ApiClientGenerationProvider;
|
||||
use crate::datatypes::common_enums::generation::api_client_model_creator::ApiClientModelCreator;
|
||||
use crate::datatypes::common_enums::generation::api_client_quality::ApiClientQuality;
|
||||
use crate::datatypes::common_enums::generation::api_client_resolution::ApiClientResolution;
|
||||
use crate::datatypes::common_enums::generation::api_client_video_model::ApiClientVideoModel;
|
||||
|
||||
pub const OMNI_GEN_VIDEO_MODELS_PATH: &str = "/v1/omni_gen/models/video";
|
||||
|
||||
/// Arguments for [`omni_gen_list_video_models`].
|
||||
pub struct OmniGenListVideoModelsArgs<'a> {
|
||||
pub api_host: &'a ApiHost,
|
||||
pub maybe_creds: Option<&'a StorytellerCredentialSet>,
|
||||
/// Which provider's models to list. `None` lets the server default (artcraft).
|
||||
pub provider: Option<OmniGenVideoModelsProvider>,
|
||||
}
|
||||
|
||||
/// List available video models.
|
||||
pub async fn omni_gen_list_video_models(
|
||||
args: OmniGenListVideoModelsArgs<'_>,
|
||||
) -> Result<OmniGenVideoModelsResponse, StorytellerError> {
|
||||
let path = match args.provider {
|
||||
Some(provider) => format!("{}?provider={}", OMNI_GEN_VIDEO_MODELS_PATH, provider.as_query_value()),
|
||||
None => OMNI_GEN_VIDEO_MODELS_PATH.to_string(),
|
||||
};
|
||||
|
||||
Ok(basic_json_get_request(args.api_host, &path, args.maybe_creds).await?)
|
||||
}
|
||||
|
||||
/// The provider filter for the models endpoint (a client-supplied request value).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OmniGenVideoModelsProvider {
|
||||
/// Only models available through ArtCraft.
|
||||
Artcraft,
|
||||
/// All known models across all providers.
|
||||
All,
|
||||
}
|
||||
|
||||
impl OmniGenVideoModelsProvider {
|
||||
pub fn as_query_value(self) -> &'static str {
|
||||
match self {
|
||||
Self::Artcraft => "artcraft",
|
||||
Self::All => "all",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OmniGenVideoModelsProvider {
|
||||
fn default() -> Self {
|
||||
Self::Artcraft
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Response types ============================
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OmniGenVideoModelsResponse {
|
||||
#[serde(default)]
|
||||
pub success: bool,
|
||||
#[serde(default)]
|
||||
pub models: Vec<OmniGenVideoModelDetails>,
|
||||
#[serde(default)]
|
||||
pub providers: Vec<OmniGenVideoModelProviderDetails>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OmniGenVideoModelProviderDetails {
|
||||
pub provider: ApiClientGenerationProvider,
|
||||
#[serde(default)]
|
||||
pub models: Vec<OmniGenVideoProviderModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OmniGenVideoProviderModelDetails {
|
||||
pub model: ApiClientVideoModel,
|
||||
#[serde(default)]
|
||||
pub overrides: Option<OmniGenVideoModelDetails>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct OmniGenVideoModelDetails {
|
||||
pub model: ApiClientVideoModel,
|
||||
#[serde(default)]
|
||||
pub model_creator: Option<ApiClientModelCreator>,
|
||||
#[serde(default)]
|
||||
pub full_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub extra_info: Option<String>,
|
||||
#[serde(default)]
|
||||
pub extra_info_short: Option<String>,
|
||||
#[serde(default)]
|
||||
pub text_to_video_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub text_prompt_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub text_prompt_max_length: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub negative_text_prompt_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub negative_text_prompt_max_length: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub starting_keyframe_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub starting_keyframe_required: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub ending_keyframe_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub image_references_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub image_references_max: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub video_references_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub video_references_max: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub video_references_max_total_duration_seconds: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub audio_references_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub audio_references_max: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub audio_references_max_total_duration_seconds: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub character_references_supported: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub character_references_max: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub show_generate_with_sound_toggle: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub aspect_ratio_options: Option<Vec<ApiClientAspectRatio>>,
|
||||
#[serde(default)]
|
||||
pub aspect_ratio_default: Option<ApiClientAspectRatio>,
|
||||
#[serde(default)]
|
||||
pub resolution_options: Option<Vec<ApiClientResolution>>,
|
||||
#[serde(default)]
|
||||
pub resolution_default: Option<ApiClientResolution>,
|
||||
#[serde(default)]
|
||||
pub bitrate_options: Option<Vec<ApiClientBitrate>>,
|
||||
#[serde(default)]
|
||||
pub bitrate_default: Option<ApiClientBitrate>,
|
||||
#[serde(default)]
|
||||
pub quality_options: Option<Vec<ApiClientQuality>>,
|
||||
#[serde(default)]
|
||||
pub default_quality: Option<ApiClientQuality>,
|
||||
#[serde(default)]
|
||||
pub duration_seconds_min: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub duration_seconds_max: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub duration_seconds_max_with_image_references: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub duration_seconds_options: Option<Vec<u16>>,
|
||||
#[serde(default)]
|
||||
pub duration_seconds_default: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub batch_size_min: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub batch_size_max: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub batch_size_options: Option<Vec<u16>>,
|
||||
#[serde(default)]
|
||||
pub batch_size_default: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unknown_enum_value_is_preserved_and_round_trips() {
|
||||
let known: ApiClientVideoModel = serde_json::from_str("\"seedance_2p0\"").unwrap();
|
||||
assert_eq!(known, ApiClientVideoModel::Seedance2p0);
|
||||
assert_eq!(serde_json::to_string(&known).unwrap(), "\"seedance_2p0\"");
|
||||
|
||||
// A value this build has never heard of does NOT fail — it is captured verbatim.
|
||||
let unknown: ApiClientVideoModel = serde_json::from_str("\"future_model_9000\"").unwrap();
|
||||
assert_eq!(unknown, ApiClientVideoModel::Unknown("future_model_9000".to_string()));
|
||||
// ...and serializes back to exactly what came in.
|
||||
assert_eq!(serde_json::to_string(&unknown).unwrap(), "\"future_model_9000\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_tolerates_unknown_fields_and_variants() {
|
||||
let json = r#"{
|
||||
"success": true,
|
||||
"models": [
|
||||
{
|
||||
"model": "brand_new_model",
|
||||
"model_creator": "some_new_studio",
|
||||
"aspect_ratio_options": ["wide_sixteen_by_nine", "some_new_ratio"],
|
||||
"surprise_new_field": 123
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
{ "provider": "new_provider", "models": [ { "model": "seedance_2p0" } ] }
|
||||
],
|
||||
"another_unexpected_top_level_field": "ignored"
|
||||
}"#;
|
||||
|
||||
let resp: OmniGenVideoModelsResponse = serde_json::from_str(json).unwrap();
|
||||
assert!(resp.success);
|
||||
assert_eq!(resp.models.len(), 1);
|
||||
assert_eq!(resp.models[0].model, ApiClientVideoModel::Unknown("brand_new_model".to_string()));
|
||||
assert_eq!(resp.models[0].model_creator, Some(ApiClientModelCreator::Unknown("some_new_studio".to_string())));
|
||||
let ratios = resp.models[0].aspect_ratio_options.as_ref().unwrap();
|
||||
assert_eq!(ratios[0], ApiClientAspectRatio::WideSixteenByNine);
|
||||
assert_eq!(ratios[1], ApiClientAspectRatio::Unknown("some_new_ratio".to_string()));
|
||||
assert_eq!(resp.models[0].is_disabled, None); // missing optional -> None
|
||||
assert_eq!(resp.providers[0].provider, ApiClientGenerationProvider::Unknown("new_provider".to_string()));
|
||||
assert_eq!(resp.providers[0].models[0].model, ApiClientVideoModel::Seedance2p0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_object_uses_defaults() {
|
||||
let resp: OmniGenVideoModelsResponse = serde_json::from_str("{}").unwrap();
|
||||
assert!(!resp.success);
|
||||
assert!(resp.models.is_empty());
|
||||
assert!(resp.providers.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
/// Live tests that hit real servers. All `#[ignore]` so they never run by default.
|
||||
/// Run explicitly, e.g.:
|
||||
/// cargo test -p artcraft_client omni_gen_list_video_models::live_tests -- --ignored --nocapture
|
||||
#[cfg(test)]
|
||||
mod live_tests {
|
||||
mod localhost {
|
||||
use super::super::{omni_gen_list_video_models, OmniGenListVideoModelsArgs};
|
||||
use crate::utils::api_host::ApiHost;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // live: hits http://localhost:12345
|
||||
async fn list_video_models() {
|
||||
let api_host = ApiHost::Localhost { port: 12345 };
|
||||
let response = omni_gen_list_video_models(OmniGenListVideoModelsArgs {
|
||||
api_host: &api_host,
|
||||
maybe_creds: None,
|
||||
provider: None,
|
||||
})
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
println!("[localhost] video models response: {:#?}", response);
|
||||
assert!(response.success);
|
||||
assert!(!response.models.is_empty(), "expected at least one video model");
|
||||
}
|
||||
}
|
||||
|
||||
mod production {
|
||||
use super::super::{omni_gen_list_video_models, OmniGenListVideoModelsArgs};
|
||||
use crate::utils::api_host::ApiHost;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // live: hits https://api.storyteller.ai
|
||||
async fn list_video_models() {
|
||||
let api_host = ApiHost::Storyteller;
|
||||
let response = omni_gen_list_video_models(OmniGenListVideoModelsArgs {
|
||||
api_host: &api_host,
|
||||
maybe_creds: None,
|
||||
provider: None,
|
||||
})
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
println!("[production] video models response: {:#?}", response);
|
||||
assert!(response.success);
|
||||
assert!(!response.models.is_empty(), "expected at least one video model");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! An API client for Artcraft / Storyteller / FakeYou
|
||||
|
||||
pub mod credentials;
|
||||
pub mod datatypes;
|
||||
pub mod endpoints;
|
||||
pub mod error;
|
||||
pub mod recipes;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod tauri_image_model_to_common_model_type;
|
||||
pub mod tauri_image_model_to_enums_model;
|
||||
pub mod tauri_image_model_to_generation_model;
|
||||
pub mod tauri_image_model_to_router_model;
|
||||
pub mod tauri_image_model_to_router_model;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
|
||||
use crate::core::commands::generate::generate_image::tauri_image_model::TauriImageModel;
|
||||
|
||||
/// Map TauriImageModel to the legacy CommonModelType (enums crate).
|
||||
pub fn tauri_image_model_to_common_model_type(model: TauriImageModel) -> CommonModelType {
|
||||
match model {
|
||||
TauriImageModel::Flux1Dev => CommonModelType::Flux1Dev,
|
||||
TauriImageModel::Flux1Schnell => CommonModelType::Flux1Schnell,
|
||||
TauriImageModel::FluxPro11 => CommonModelType::FluxPro11,
|
||||
TauriImageModel::FluxPro11Ultra => CommonModelType::FluxPro11Ultra,
|
||||
TauriImageModel::GrokImage => CommonModelType::GrokImage,
|
||||
TauriImageModel::Recraft3 => CommonModelType::Recraft3,
|
||||
TauriImageModel::GptImage1 => CommonModelType::GptImage1,
|
||||
TauriImageModel::GptImage1p5 => CommonModelType::GptImage1p5,
|
||||
TauriImageModel::GptImage2 => CommonModelType::GptImage2,
|
||||
TauriImageModel::Gemini25Flash => CommonModelType::NanoBanana,
|
||||
TauriImageModel::NanoBanana => CommonModelType::NanoBanana,
|
||||
TauriImageModel::NanoBanana2 => CommonModelType::NanoBanana2,
|
||||
TauriImageModel::NanoBananaPro => CommonModelType::NanoBananaPro,
|
||||
TauriImageModel::Seedream4 => CommonModelType::Seedream4,
|
||||
TauriImageModel::Seedream4p5 => CommonModelType::Seedream4p5,
|
||||
TauriImageModel::Seedream5Lite => CommonModelType::Seedream5Lite,
|
||||
TauriImageModel::Midjourney => CommonModelType::Midjourney,
|
||||
TauriImageModel::Midjourney7 => CommonModelType::Midjourney7,
|
||||
TauriImageModel::Midjourney7Niji => CommonModelType::Midjourney7Niji,
|
||||
TauriImageModel::Midjourney8 => CommonModelType::Midjourney8,
|
||||
TauriImageModel::FluxProKontextMax => CommonModelType::FluxProKontextMax,
|
||||
TauriImageModel::QwenEdit2511Angles => CommonModelType::QwenEdit2511Angles,
|
||||
TauriImageModel::Flux2LoraAngles => CommonModelType::Flux2LoraAngles,
|
||||
TauriImageModel::FluxDevJuggernaut => CommonModelType::FluxDevJuggernaut,
|
||||
TauriImageModel::FluxPro1 => CommonModelType::FluxPro1,
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,13 @@ pub fn tauri_image_model_to_enums_model(model: TauriImageModel) -> Option<Common
|
||||
TauriImageModel::Seedream5Lite => Some(CommonImageModel::Seedream5Lite),
|
||||
TauriImageModel::QwenEdit2511Angles => Some(CommonImageModel::QwenEdit2511Angles),
|
||||
TauriImageModel::Flux2LoraAngles => Some(CommonImageModel::Flux2LoraAngles),
|
||||
TauriImageModel::Midjourney7 => Some(CommonImageModel::Midjourney7),
|
||||
TauriImageModel::Midjourney7Niji => Some(CommonImageModel::Midjourney7Niji),
|
||||
TauriImageModel::Midjourney8 => Some(CommonImageModel::Midjourney8),
|
||||
// Not accounted for yet
|
||||
TauriImageModel::GrokImage => None,
|
||||
TauriImageModel::Recraft3 => None,
|
||||
TauriImageModel::Midjourney => None,
|
||||
TauriImageModel::Midjourney => None, // NB: Generic Midjourney is served by the native Midjourney provider.
|
||||
TauriImageModel::FluxProKontextMax => None,
|
||||
TauriImageModel::FluxDevJuggernaut => None,
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ pub fn tauri_image_model_to_generation_model(model: TauriImageModel) -> Generati
|
||||
TauriImageModel::Seedream4 => GenerationModel::Seedream4,
|
||||
TauriImageModel::Seedream4p5 => GenerationModel::Seedream4p5,
|
||||
TauriImageModel::Seedream5Lite => GenerationModel::Seedream5Lite,
|
||||
TauriImageModel::Midjourney => GenerationModel::Flux1Dev, // Fallback
|
||||
TauriImageModel::Midjourney => GenerationModel::Midjourney,
|
||||
TauriImageModel::Midjourney7 => GenerationModel::Midjourney7,
|
||||
TauriImageModel::Midjourney7Niji => GenerationModel::Midjourney7Niji,
|
||||
TauriImageModel::Midjourney8 => GenerationModel::Midjourney8,
|
||||
TauriImageModel::FluxProKontextMax => GenerationModel::FluxProKontextMax,
|
||||
TauriImageModel::QwenEdit2511Angles => GenerationModel::QwenEdit2511Angles,
|
||||
TauriImageModel::Flux2LoraAngles => GenerationModel::Flux2LoraAngles,
|
||||
|
||||
@@ -23,10 +23,13 @@ pub fn tauri_image_model_to_router_model(model: TauriImageModel) -> Option<Route
|
||||
TauriImageModel::Seedream5Lite => Some(RouterImageModel::Seedream5Lite), // Text-to-Image
|
||||
TauriImageModel::QwenEdit2511Angles => Some(RouterImageModel::QwenEdit2511Angles),
|
||||
TauriImageModel::Flux2LoraAngles => Some(RouterImageModel::Flux2LoraAngles),
|
||||
TauriImageModel::Midjourney7 => Some(RouterImageModel::Midjourney7),
|
||||
TauriImageModel::Midjourney7Niji => Some(RouterImageModel::Midjourney7Niji),
|
||||
TauriImageModel::Midjourney8 => Some(RouterImageModel::Midjourney8),
|
||||
// Not accounted for yet
|
||||
TauriImageModel::GrokImage => None,
|
||||
TauriImageModel::Recraft3 => None,
|
||||
TauriImageModel::Midjourney => None,
|
||||
TauriImageModel::Midjourney => None, // NB: Generic Midjourney is served by the native Midjourney provider.
|
||||
TauriImageModel::FluxProKontextMax => None,
|
||||
TauriImageModel::FluxDevJuggernaut => None,
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod image;
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod tauri_video_model_to_generation_model;
|
||||
pub mod tauri_video_model_to_router_model;
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::core::commands::generate::generate_video::request::TauriVideoModel;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
|
||||
/// Map TauriVideoModel to the GenerationModel used in events and the task database.
|
||||
/// NB: GenerationModel serializations are stored in the tasks database — they keep
|
||||
/// their legacy spellings for compatibility.
|
||||
pub fn tauri_video_model_to_generation_model(model: TauriVideoModel) -> GenerationModel {
|
||||
match model {
|
||||
TauriVideoModel::GrokVideo => GenerationModel::GrokVideo,
|
||||
TauriVideoModel::GrokImagineVideo1p5 => GenerationModel::GrokImagineVideo1p5,
|
||||
TauriVideoModel::HappyHorse1p0 => GenerationModel::HappyHorse1p0,
|
||||
TauriVideoModel::Kling16Pro => GenerationModel::Kling1_6,
|
||||
TauriVideoModel::Kling21Master => GenerationModel::Kling21Master,
|
||||
TauriVideoModel::Kling21Pro => GenerationModel::Kling21Pro,
|
||||
TauriVideoModel::Kling2p5TurboPro => GenerationModel::Kling2p5TurboPro,
|
||||
TauriVideoModel::Kling2p6Pro => GenerationModel::Kling2p6Pro,
|
||||
TauriVideoModel::Kling3p0Pro => GenerationModel::Kling3p0Pro,
|
||||
TauriVideoModel::Kling3p0Standard => GenerationModel::Kling3p0Standard,
|
||||
TauriVideoModel::Seedance10Lite => GenerationModel::Seedance10Lite,
|
||||
TauriVideoModel::Seedance1p5Pro => GenerationModel::Seedance1p5Pro,
|
||||
TauriVideoModel::Seedance2p0 => GenerationModel::Seedance2p0,
|
||||
TauriVideoModel::Seedance2p0Fast => GenerationModel::Seedance2p0Fast,
|
||||
TauriVideoModel::Sora2 => GenerationModel::Sora2,
|
||||
TauriVideoModel::Sora2Pro => GenerationModel::Sora2Pro,
|
||||
TauriVideoModel::Veo2 => GenerationModel::Veo2,
|
||||
TauriVideoModel::Veo3 => GenerationModel::Veo3,
|
||||
TauriVideoModel::Veo3Fast => GenerationModel::Veo3Fast,
|
||||
TauriVideoModel::Veo3p1 => GenerationModel::Veo3p1,
|
||||
TauriVideoModel::Veo3p1Fast => GenerationModel::Veo3p1Fast,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use artcraft_router::api::router_video_model::RouterVideoModel;
|
||||
|
||||
use crate::core::commands::generate::generate_video::request::TauriVideoModel;
|
||||
|
||||
/// Map TauriVideoModel to the artcraft_router's RouterVideoModel.
|
||||
pub fn tauri_video_model_to_router_model(model: TauriVideoModel) -> RouterVideoModel {
|
||||
match model {
|
||||
TauriVideoModel::GrokVideo => RouterVideoModel::GrokImagineVideo,
|
||||
TauriVideoModel::GrokImagineVideo1p5 => RouterVideoModel::GrokImagineVideo1p5,
|
||||
TauriVideoModel::HappyHorse1p0 => RouterVideoModel::HappyHorse1p0,
|
||||
TauriVideoModel::Kling16Pro => RouterVideoModel::Kling16Pro,
|
||||
TauriVideoModel::Kling21Master => RouterVideoModel::Kling21Master,
|
||||
TauriVideoModel::Kling21Pro => RouterVideoModel::Kling21Pro,
|
||||
TauriVideoModel::Kling2p5TurboPro => RouterVideoModel::Kling2p5TurboPro,
|
||||
TauriVideoModel::Kling2p6Pro => RouterVideoModel::Kling2p6Pro,
|
||||
TauriVideoModel::Kling3p0Pro => RouterVideoModel::Kling3p0Pro,
|
||||
TauriVideoModel::Kling3p0Standard => RouterVideoModel::Kling3p0Standard,
|
||||
TauriVideoModel::Seedance10Lite => RouterVideoModel::Seedance10Lite,
|
||||
TauriVideoModel::Seedance1p5Pro => RouterVideoModel::Seedance1p5Pro,
|
||||
TauriVideoModel::Seedance2p0 => RouterVideoModel::Seedance2p0,
|
||||
TauriVideoModel::Seedance2p0Fast => RouterVideoModel::Seedance2p0Fast,
|
||||
TauriVideoModel::Sora2 => RouterVideoModel::Sora2,
|
||||
TauriVideoModel::Sora2Pro => RouterVideoModel::Sora2Pro,
|
||||
TauriVideoModel::Veo2 => RouterVideoModel::Veo2,
|
||||
TauriVideoModel::Veo3 => RouterVideoModel::Veo3,
|
||||
TauriVideoModel::Veo3Fast => RouterVideoModel::Veo3Fast,
|
||||
TauriVideoModel::Veo3p1 => RouterVideoModel::Veo3p1,
|
||||
TauriVideoModel::Veo3p1Fast => RouterVideoModel::Veo3p1Fast,
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@ impl TaskEnqueueSuccess{
|
||||
Some(GenerationModel::GrokImage) => Some(TaskModelType::GrokImage),
|
||||
Some(GenerationModel::Recraft3) => Some(TaskModelType::Recraft3),
|
||||
Some(GenerationModel::GrokVideo) => Some(TaskModelType::GrokVideo),
|
||||
Some(GenerationModel::GrokImagineVideo1p5) => Some(TaskModelType::GrokImagineVideo1p5),
|
||||
Some(GenerationModel::Kling21Pro) => Some(TaskModelType::Kling21Pro),
|
||||
Some(GenerationModel::Kling21Master) => Some(TaskModelType::Kling21Master),
|
||||
Some(GenerationModel::Kling2p5TurboPro) => Some(TaskModelType::Kling2p5TurboPro),
|
||||
@@ -110,6 +111,9 @@ impl TaskEnqueueSuccess{
|
||||
Some(GenerationModel::WorldlabsMarble0p1Mini) => Some(TaskModelType::WorldlabsMarble0p1Mini),
|
||||
Some(GenerationModel::WorldlabsMarble0p1Plus) => Some(TaskModelType::WorldlabsMarble0p1Plus),
|
||||
Some(GenerationModel::Midjourney) => Some(TaskModelType::Midjourney), // NB: This is a generic Midjourney model, version unknown.
|
||||
Some(GenerationModel::Midjourney7) => Some(TaskModelType::Midjourney7),
|
||||
Some(GenerationModel::Midjourney7Niji) => Some(TaskModelType::Midjourney7Niji),
|
||||
Some(GenerationModel::Midjourney8) => Some(TaskModelType::Midjourney8),
|
||||
|
||||
// TODO: These seem wrong -
|
||||
Some(GenerationModel::Kling1_6) => Some(TaskModelType::Kling16Pro), // NB: `VideoModel::Kling16Pro`.
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
/// Unified image model enum covering text-to-image, image edit, and inpainting.
|
||||
///
|
||||
/// This is used in the Tauri command bridge.
|
||||
/// Don't change the serializations without coordinating with the frontend.
|
||||
///
|
||||
/// MIGRATION (2026-07): we're moving the frontend to send storyteller-web omni
|
||||
/// identifiers (`CommonImageModel` serde strings). Variants whose legacy Tauri
|
||||
/// id differs carry the omni id as a `#[serde(alias)]` so BOTH deserialize.
|
||||
/// Once the frontend is 100% omni ids, the legacy renames can be dropped.
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TauriImageModel {
|
||||
@@ -14,11 +18,11 @@ pub enum TauriImageModel {
|
||||
Flux1Dev,
|
||||
#[serde(rename = "flux_1_schnell")]
|
||||
Flux1Schnell,
|
||||
#[serde(rename = "flux_pro_11")]
|
||||
#[serde(rename = "flux_pro_11", alias = "flux_pro_1p1")]
|
||||
FluxPro11,
|
||||
#[serde(rename = "flux_pro_11_ultra")]
|
||||
#[serde(rename = "flux_pro_11_ultra", alias = "flux_pro_1p1_ultra")]
|
||||
FluxPro11Ultra,
|
||||
#[serde(rename = "grok_image")]
|
||||
#[serde(rename = "grok_image", alias = "grok_imagine_image")]
|
||||
GrokImage,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
@@ -42,9 +46,19 @@ pub enum TauriImageModel {
|
||||
Seedream4p5,
|
||||
#[serde(rename = "seedream_5_lite")]
|
||||
Seedream5Lite,
|
||||
// Generic Midjourney experience, served via the native Midjourney provider.
|
||||
#[serde(rename = "midjourney")]
|
||||
Midjourney,
|
||||
|
||||
// Versioned Midjourney models, served by storyteller-web via the
|
||||
// Artcraft provider (dispatched through artcraft_router).
|
||||
#[serde(rename = "midjourney_7")]
|
||||
Midjourney7,
|
||||
#[serde(rename = "midjourney_7_niji")]
|
||||
Midjourney7Niji,
|
||||
#[serde(rename = "midjourney_8")]
|
||||
Midjourney8,
|
||||
|
||||
// Image edit models
|
||||
|
||||
#[serde(rename = "flux_pro_kontext_max")]
|
||||
@@ -61,32 +75,3 @@ pub enum TauriImageModel {
|
||||
#[serde(rename = "flux_pro_1")]
|
||||
FluxPro1,
|
||||
}
|
||||
|
||||
impl TauriImageModel {
|
||||
pub fn to_common_model_type(&self) -> CommonModelType {
|
||||
match self {
|
||||
Self::Flux1Dev => CommonModelType::Flux1Dev,
|
||||
Self::Flux1Schnell => CommonModelType::Flux1Schnell,
|
||||
Self::FluxPro11 => CommonModelType::FluxPro11,
|
||||
Self::FluxPro11Ultra => CommonModelType::FluxPro11Ultra,
|
||||
Self::GrokImage => CommonModelType::GrokImage,
|
||||
Self::Recraft3 => CommonModelType::Recraft3,
|
||||
Self::GptImage1 => CommonModelType::GptImage1,
|
||||
Self::GptImage1p5 => CommonModelType::GptImage1p5,
|
||||
Self::GptImage2 => CommonModelType::GptImage2,
|
||||
Self::Gemini25Flash => CommonModelType::NanoBanana,
|
||||
Self::NanoBanana => CommonModelType::NanoBanana,
|
||||
Self::NanoBanana2 => CommonModelType::NanoBanana2,
|
||||
Self::NanoBananaPro => CommonModelType::NanoBananaPro,
|
||||
Self::Seedream4 => CommonModelType::Seedream4,
|
||||
Self::Seedream4p5 => CommonModelType::Seedream4p5,
|
||||
Self::Seedream5Lite => CommonModelType::Seedream5Lite,
|
||||
Self::Midjourney => CommonModelType::Midjourney,
|
||||
Self::FluxProKontextMax => CommonModelType::FluxProKontextMax,
|
||||
Self::QwenEdit2511Angles => CommonModelType::QwenEdit2511Angles,
|
||||
Self::Flux2LoraAngles => CommonModelType::Flux2LoraAngles,
|
||||
Self::FluxDevJuggernaut => CommonModelType::FluxDevJuggernaut,
|
||||
Self::FluxPro1 => CommonModelType::FluxPro1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::core::api_adapters::models::video::tauri_video_model_to_generation_model::tauri_video_model_to_generation_model;
|
||||
use crate::core::api_adapters::models::video::tauri_video_model_to_router_model::tauri_video_model_to_router_model;
|
||||
use crate::core::commands::enqueue::generate_error::GenerateError;
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::commands::generate::generate_video::artcraft::get_storyteller_creds_or_error::get_storyteller_creds_or_error;
|
||||
use crate::core::commands::generate::generate_video::artcraft::handle_artcraft_video_via_router::handle_artcraft_video_via_router;
|
||||
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, TauriVideoModel};
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
use crate::core::commands::generate::generate_video::request::TauriGenerateVideoRequest;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use anyhow::anyhow;
|
||||
use artcraft_router::api::router_video_model::RouterVideoModel;
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn handle_video_artcraft(
|
||||
@@ -19,30 +18,13 @@ pub async fn handle_video_artcraft(
|
||||
|
||||
let creds = get_storyteller_creds_or_error(app, storyteller_creds_manager)?;
|
||||
|
||||
let (router_model, generation_model) = match request.model {
|
||||
let model = match request.model {
|
||||
None => return Err(GenerateError::no_model_specified()),
|
||||
Some(TauriVideoModel::HappyHorse1p0) => (RouterVideoModel::HappyHorse1p0, GenerationModel::HappyHorse1p0),
|
||||
Some(TauriVideoModel::Kling16Pro) => (RouterVideoModel::Kling16Pro, GenerationModel::Kling1_6),
|
||||
Some(TauriVideoModel::Kling21Master) => (RouterVideoModel::Kling21Master, GenerationModel::Kling21Master),
|
||||
Some(TauriVideoModel::Kling21Pro) => (RouterVideoModel::Kling21Pro, GenerationModel::Kling21Pro),
|
||||
Some(TauriVideoModel::Kling2p5TurboPro) => (RouterVideoModel::Kling2p5TurboPro, GenerationModel::Kling2p5TurboPro),
|
||||
Some(TauriVideoModel::Kling2p6Pro) => (RouterVideoModel::Kling2p6Pro, GenerationModel::Kling2p6Pro),
|
||||
Some(TauriVideoModel::Kling3p0Pro) => (RouterVideoModel::Kling3p0Pro, GenerationModel::Kling3p0Pro),
|
||||
Some(TauriVideoModel::Kling3p0Standard) => (RouterVideoModel::Kling3p0Standard, GenerationModel::Kling3p0Standard),
|
||||
Some(TauriVideoModel::Seedance10Lite) => (RouterVideoModel::Seedance10Lite, GenerationModel::Seedance10Lite),
|
||||
Some(TauriVideoModel::Seedance1p5Pro) => (RouterVideoModel::Seedance1p5Pro, GenerationModel::Seedance1p5Pro),
|
||||
Some(TauriVideoModel::Seedance2p0) => (RouterVideoModel::Seedance2p0, GenerationModel::Seedance2p0),
|
||||
Some(TauriVideoModel::Seedance2p0Fast) => (RouterVideoModel::Seedance2p0Fast, GenerationModel::Seedance2p0Fast),
|
||||
Some(TauriVideoModel::Sora2) => (RouterVideoModel::Sora2, GenerationModel::Sora2),
|
||||
Some(TauriVideoModel::Sora2Pro) => (RouterVideoModel::Sora2Pro, GenerationModel::Sora2Pro),
|
||||
Some(TauriVideoModel::Veo2) => (RouterVideoModel::Veo2, GenerationModel::Veo2),
|
||||
Some(TauriVideoModel::Veo3) => (RouterVideoModel::Veo3, GenerationModel::Veo3),
|
||||
Some(TauriVideoModel::Veo3Fast) => (RouterVideoModel::Veo3Fast, GenerationModel::Veo3Fast),
|
||||
Some(TauriVideoModel::Veo3p1) => (RouterVideoModel::Veo3p1, GenerationModel::Veo3p1),
|
||||
Some(TauriVideoModel::Veo3p1Fast) => (RouterVideoModel::Veo3p1Fast, GenerationModel::Veo3p1Fast),
|
||||
Some(other) => return Err(GenerateError::AnyhowError(
|
||||
anyhow!("wrong logic: another branch should handle this: {:?}", other))),
|
||||
Some(model) => model,
|
||||
};
|
||||
|
||||
let router_model = tauri_video_model_to_router_model(model);
|
||||
let generation_model = tauri_video_model_to_generation_model(model);
|
||||
|
||||
handle_artcraft_video_via_router(request, app_env_configs, &creds, router_model, generation_model).await
|
||||
}
|
||||
|
||||
@@ -150,6 +150,10 @@ async fn handle_request(
|
||||
|
||||
let provider = match (model, request.provider) {
|
||||
(TauriVideoModel::GrokVideo, _) => GenerationProvider::Grok,
|
||||
// NB: Grok Imagine 1.5 is served via Artcraft (storyteller-web / artcraft_router)
|
||||
// for now — force it regardless of the requested provider. In the future we
|
||||
// may serve it natively via the Grok provider.
|
||||
(TauriVideoModel::GrokImagineVideo1p5, _) => GenerationProvider::Artcraft,
|
||||
_ => request.provider.unwrap_or(GenerationProvider::Artcraft),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError};
|
||||
use crate::core::commands::deprecated::image_edit::enqueue_edit_image_command::EnqueueEditImageCommand;
|
||||
use crate::core::commands::deprecated::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand;
|
||||
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, GrokAspectRatio, SoraOrientation};
|
||||
use crate::core::commands::generate::generate_video::request::{TauriGenerateVideoRequest, TauriVideoModel, GrokAspectRatio, SoraOrientation};
|
||||
use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess;
|
||||
use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent;
|
||||
use crate::core::events::generation_events::common::GenerationModel;
|
||||
@@ -91,9 +91,16 @@ pub async fn handle_grok_video(
|
||||
}
|
||||
};
|
||||
|
||||
// NB: Grok's consumer API doesn't take a version selector; this only affects
|
||||
// which model is recorded for the task queue / events.
|
||||
let generation_model = match request.model {
|
||||
Some(TauriVideoModel::GrokImagineVideo1p5) => GenerationModel::GrokImagineVideo1p5,
|
||||
_ => GenerationModel::GrokVideo,
|
||||
};
|
||||
|
||||
Ok(TaskEnqueueSuccess {
|
||||
provider: GenerationProvider::Grok,
|
||||
model: Some(GenerationModel::GrokVideo),
|
||||
model: Some(generation_model),
|
||||
provider_job_id: Some(post_id.to_string()),
|
||||
task_type: TaskType::VideoGeneration,
|
||||
maybe_queue_status_url: None,
|
||||
|
||||
@@ -10,19 +10,27 @@ use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
|
||||
/// This is used in the Tauri command bridge.
|
||||
/// Don't change the serializations without coordinating with the frontend.
|
||||
///
|
||||
/// MIGRATION (2026-07): we're moving the frontend to send storyteller-web omni
|
||||
/// identifiers (`CommonVideoModel` serde strings). Variants whose legacy Tauri
|
||||
/// id differs carry the omni id as a `#[serde(alias)]` so BOTH deserialize.
|
||||
/// Once the frontend is 100% omni ids, the legacy renames can be dropped.
|
||||
#[derive(Deserialize, Debug, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TauriVideoModel {
|
||||
#[serde(rename = "grok_video")]
|
||||
#[serde(rename = "grok_video", alias = "grok_imagine_video")]
|
||||
GrokVideo,
|
||||
|
||||
#[serde(rename = "kling_1.6_pro")]
|
||||
#[serde(rename = "grok_imagine_video_1p5")]
|
||||
GrokImagineVideo1p5,
|
||||
|
||||
#[serde(rename = "kling_1.6_pro", alias = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
|
||||
#[serde(rename = "kling_2.1_pro")]
|
||||
#[serde(rename = "kling_2.1_pro", alias = "kling_2p1_pro")]
|
||||
Kling21Pro,
|
||||
|
||||
#[serde(rename = "kling_2.1_master")]
|
||||
#[serde(rename = "kling_2.1_master", alias = "kling_2p1_master")]
|
||||
Kling21Master,
|
||||
|
||||
#[serde(rename = "kling_2p5_turbo_pro")]
|
||||
@@ -40,7 +48,7 @@ pub enum TauriVideoModel {
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
|
||||
#[serde(rename = "seedance_1.0_lite")]
|
||||
#[serde(rename = "seedance_1.0_lite", alias = "seedance_1p0_lite")]
|
||||
Seedance10Lite,
|
||||
|
||||
#[serde(rename = "seedance_1p5_pro")]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod generate_image;
|
||||
pub mod generate_video;
|
||||
pub mod common;
|
||||
pub mod models;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Tauri command: list available image models.
|
||||
//!
|
||||
//! Maps the API-client response into command-specific types (see `response_types`),
|
||||
//! retries the request up to `MAX_ATTEMPTS` times, and caches successful responses
|
||||
//! in-memory for `CACHE_TTL`.
|
||||
|
||||
use std::sync::RwLock;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use log::{debug, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use tauri::State;
|
||||
|
||||
use artcraft_client::endpoints::omni_gen::models::image::omni_gen_list_image_models::{omni_gen_list_image_models, OmniGenListImageModelsArgs, OmniGenImageModelsResponse};
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
use crate::core::commands::generate::models::image::response_types::ListImageModelsResponse;
|
||||
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
const MAX_ATTEMPTS: u32 = 3;
|
||||
const RETRY_BACKOFF: Duration = Duration::from_millis(250);
|
||||
const CACHE_TTL: Duration = Duration::from_secs(60);
|
||||
|
||||
struct CacheEntry {
|
||||
response: ListImageModelsResponse,
|
||||
loaded_at: Instant,
|
||||
}
|
||||
|
||||
static CACHE: Lazy<RwLock<Option<CacheEntry>>> = Lazy::new(|| RwLock::new(None));
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_image_models_command(
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
) -> ResponseOrErrorMessage<ListImageModelsResponse> {
|
||||
if let Some(cached) = cached_response() {
|
||||
debug!("list_image_models_command: serving cached response");
|
||||
return Ok(cached.into());
|
||||
}
|
||||
|
||||
match fetch_with_retry(&app_env_configs.storyteller_host).await {
|
||||
Ok(api_response) => {
|
||||
let response: ListImageModelsResponse = api_response.into();
|
||||
store_response(response.clone());
|
||||
Ok(response.into())
|
||||
}
|
||||
Err(error_message) => {
|
||||
warn!("list_image_models_command failed after {} attempts: {}", MAX_ATTEMPTS, error_message);
|
||||
if let Some(stale) = any_cached_response() {
|
||||
warn!("list_image_models_command: refresh failed; serving stale cached response");
|
||||
return Ok(stale.into());
|
||||
}
|
||||
Err(error_message.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_response() -> Option<ListImageModelsResponse> {
|
||||
let guard = CACHE.read().ok()?;
|
||||
let entry = guard.as_ref()?;
|
||||
if entry.loaded_at.elapsed() < CACHE_TTL {
|
||||
Some(entry.response.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The cached response regardless of age. Used as a fallback so that a failed
|
||||
/// refresh still returns the last-known-good data instead of an error.
|
||||
fn any_cached_response() -> Option<ListImageModelsResponse> {
|
||||
let guard = CACHE.read().ok()?;
|
||||
guard.as_ref().map(|entry| entry.response.clone())
|
||||
}
|
||||
|
||||
fn store_response(response: ListImageModelsResponse) {
|
||||
if let Ok(mut guard) = CACHE.write() {
|
||||
*guard = Some(CacheEntry { response, loaded_at: Instant::now() });
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the models list, retrying up to `MAX_ATTEMPTS` times on failure.
|
||||
async fn fetch_with_retry(api_host: &ApiHost) -> Result<OmniGenImageModelsResponse, String> {
|
||||
let mut last_error = "unknown error".to_string();
|
||||
for attempt in 1..=MAX_ATTEMPTS {
|
||||
match omni_gen_list_image_models(OmniGenListImageModelsArgs { api_host, maybe_creds: None, provider: None }).await {
|
||||
Ok(response) => return Ok(response),
|
||||
Err(err) => {
|
||||
last_error = err.to_string();
|
||||
warn!("list_image_models attempt {}/{} failed: {}", attempt, MAX_ATTEMPTS, last_error);
|
||||
if attempt < MAX_ATTEMPTS {
|
||||
tokio::time::sleep(RETRY_BACKOFF).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_error)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod list_image_models_command;
|
||||
pub mod response_types;
|
||||
@@ -0,0 +1,524 @@
|
||||
//! Command-specific response types for `list_image_models_command`.
|
||||
//!
|
||||
//! These are duplicated (never `artcraft_client` types), and every enum has an
|
||||
//! `Unknown(String)` catch-all so newer server variants are preserved instead of
|
||||
//! breaking an older desktop build. `From` impls map the API-client response over.
|
||||
|
||||
use serde_derive::Serialize;
|
||||
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_generation_provider::ApiClientGenerationProvider;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_model_creator::ApiClientModelCreator;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_image_model::ApiClientImageModel;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_aspect_ratio::ApiClientAspectRatio;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_resolution::ApiClientResolution;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_quality::ApiClientQuality;
|
||||
use artcraft_client::endpoints::omni_gen::models::image::omni_gen_list_image_models::{
|
||||
OmniGenImageModelDetails, OmniGenImageModelProviderDetails, OmniGenImageProviderModelDetails, OmniGenImageModelsResponse,
|
||||
};
|
||||
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
|
||||
impl SerializeMarker for ListImageModelsResponse {}
|
||||
|
||||
// ============================ Enums ============================
|
||||
|
||||
/// Command-specific copy of `ApiClientGenerationProvider` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListImageModelsProvider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
Grok,
|
||||
Midjourney,
|
||||
Sora,
|
||||
WorldLabs,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientGenerationProvider> for ListImageModelsProvider {
|
||||
fn from(value: ApiClientGenerationProvider) -> Self {
|
||||
match value {
|
||||
ApiClientGenerationProvider::Artcraft => Self::Artcraft,
|
||||
ApiClientGenerationProvider::Fal => Self::Fal,
|
||||
ApiClientGenerationProvider::Grok => Self::Grok,
|
||||
ApiClientGenerationProvider::Midjourney => Self::Midjourney,
|
||||
ApiClientGenerationProvider::Sora => Self::Sora,
|
||||
ApiClientGenerationProvider::WorldLabs => Self::WorldLabs,
|
||||
ApiClientGenerationProvider::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListImageModelsProvider> for String {
|
||||
fn from(value: ListImageModelsProvider) -> Self {
|
||||
match value {
|
||||
ListImageModelsProvider::Artcraft => "artcraft".to_string(),
|
||||
ListImageModelsProvider::Fal => "fal".to_string(),
|
||||
ListImageModelsProvider::Grok => "grok".to_string(),
|
||||
ListImageModelsProvider::Midjourney => "midjourney".to_string(),
|
||||
ListImageModelsProvider::Sora => "sora".to_string(),
|
||||
ListImageModelsProvider::WorldLabs => "world_labs".to_string(),
|
||||
ListImageModelsProvider::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientModelCreator` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListImageModelsModelCreator {
|
||||
Alibaba,
|
||||
ArtCraft,
|
||||
BlackForestLabs,
|
||||
Bytedance,
|
||||
Fal,
|
||||
Google,
|
||||
Grok,
|
||||
Hailuo,
|
||||
Higgsfield,
|
||||
Kling,
|
||||
Krea,
|
||||
Midjourney,
|
||||
OpenAi,
|
||||
OpenArt,
|
||||
Recraft,
|
||||
Replicate,
|
||||
Runway,
|
||||
Stability,
|
||||
Tencent,
|
||||
TensorArt,
|
||||
Vidu,
|
||||
WorldLabs,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientModelCreator> for ListImageModelsModelCreator {
|
||||
fn from(value: ApiClientModelCreator) -> Self {
|
||||
match value {
|
||||
ApiClientModelCreator::Alibaba => Self::Alibaba,
|
||||
ApiClientModelCreator::ArtCraft => Self::ArtCraft,
|
||||
ApiClientModelCreator::BlackForestLabs => Self::BlackForestLabs,
|
||||
ApiClientModelCreator::Bytedance => Self::Bytedance,
|
||||
ApiClientModelCreator::Fal => Self::Fal,
|
||||
ApiClientModelCreator::Google => Self::Google,
|
||||
ApiClientModelCreator::Grok => Self::Grok,
|
||||
ApiClientModelCreator::Hailuo => Self::Hailuo,
|
||||
ApiClientModelCreator::Higgsfield => Self::Higgsfield,
|
||||
ApiClientModelCreator::Kling => Self::Kling,
|
||||
ApiClientModelCreator::Krea => Self::Krea,
|
||||
ApiClientModelCreator::Midjourney => Self::Midjourney,
|
||||
ApiClientModelCreator::OpenAi => Self::OpenAi,
|
||||
ApiClientModelCreator::OpenArt => Self::OpenArt,
|
||||
ApiClientModelCreator::Recraft => Self::Recraft,
|
||||
ApiClientModelCreator::Replicate => Self::Replicate,
|
||||
ApiClientModelCreator::Runway => Self::Runway,
|
||||
ApiClientModelCreator::Stability => Self::Stability,
|
||||
ApiClientModelCreator::Tencent => Self::Tencent,
|
||||
ApiClientModelCreator::TensorArt => Self::TensorArt,
|
||||
ApiClientModelCreator::Vidu => Self::Vidu,
|
||||
ApiClientModelCreator::WorldLabs => Self::WorldLabs,
|
||||
ApiClientModelCreator::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListImageModelsModelCreator> for String {
|
||||
fn from(value: ListImageModelsModelCreator) -> Self {
|
||||
match value {
|
||||
ListImageModelsModelCreator::Alibaba => "alibaba".to_string(),
|
||||
ListImageModelsModelCreator::ArtCraft => "artcraft".to_string(),
|
||||
ListImageModelsModelCreator::BlackForestLabs => "black_forest_labs".to_string(),
|
||||
ListImageModelsModelCreator::Bytedance => "bytedance".to_string(),
|
||||
ListImageModelsModelCreator::Fal => "fal".to_string(),
|
||||
ListImageModelsModelCreator::Google => "google".to_string(),
|
||||
ListImageModelsModelCreator::Grok => "grok".to_string(),
|
||||
ListImageModelsModelCreator::Hailuo => "hailuo".to_string(),
|
||||
ListImageModelsModelCreator::Higgsfield => "higgsfield".to_string(),
|
||||
ListImageModelsModelCreator::Kling => "kling".to_string(),
|
||||
ListImageModelsModelCreator::Krea => "krea".to_string(),
|
||||
ListImageModelsModelCreator::Midjourney => "midjourney".to_string(),
|
||||
ListImageModelsModelCreator::OpenAi => "open_ai".to_string(),
|
||||
ListImageModelsModelCreator::OpenArt => "open_art".to_string(),
|
||||
ListImageModelsModelCreator::Recraft => "recraft".to_string(),
|
||||
ListImageModelsModelCreator::Replicate => "replicate".to_string(),
|
||||
ListImageModelsModelCreator::Runway => "runway".to_string(),
|
||||
ListImageModelsModelCreator::Stability => "stability".to_string(),
|
||||
ListImageModelsModelCreator::Tencent => "tencent".to_string(),
|
||||
ListImageModelsModelCreator::TensorArt => "tensor_art".to_string(),
|
||||
ListImageModelsModelCreator::Vidu => "vidu".to_string(),
|
||||
ListImageModelsModelCreator::WorldLabs => "world_labs".to_string(),
|
||||
ListImageModelsModelCreator::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientImageModel` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListImageModelsImageModel {
|
||||
Flux1Dev,
|
||||
Flux1Schnell,
|
||||
FluxPro11,
|
||||
FluxPro11Ultra,
|
||||
GptImage1,
|
||||
GptImage1p5,
|
||||
GptImage2,
|
||||
GrokImagineImage,
|
||||
GrokImagineImageQuality,
|
||||
Midjourney7,
|
||||
Midjourney7Niji,
|
||||
Midjourney8,
|
||||
NanoBanana,
|
||||
NanoBanana2,
|
||||
NanoBananaPro,
|
||||
Seedream4,
|
||||
Seedream4p5,
|
||||
Seedream5Lite,
|
||||
QwenEdit2511Angles,
|
||||
Flux2LoraAngles,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientImageModel> for ListImageModelsImageModel {
|
||||
fn from(value: ApiClientImageModel) -> Self {
|
||||
match value {
|
||||
ApiClientImageModel::Flux1Dev => Self::Flux1Dev,
|
||||
ApiClientImageModel::Flux1Schnell => Self::Flux1Schnell,
|
||||
ApiClientImageModel::FluxPro11 => Self::FluxPro11,
|
||||
ApiClientImageModel::FluxPro11Ultra => Self::FluxPro11Ultra,
|
||||
ApiClientImageModel::GptImage1 => Self::GptImage1,
|
||||
ApiClientImageModel::GptImage1p5 => Self::GptImage1p5,
|
||||
ApiClientImageModel::GptImage2 => Self::GptImage2,
|
||||
ApiClientImageModel::GrokImagineImage => Self::GrokImagineImage,
|
||||
ApiClientImageModel::GrokImagineImageQuality => Self::GrokImagineImageQuality,
|
||||
ApiClientImageModel::Midjourney7 => Self::Midjourney7,
|
||||
ApiClientImageModel::Midjourney7Niji => Self::Midjourney7Niji,
|
||||
ApiClientImageModel::Midjourney8 => Self::Midjourney8,
|
||||
ApiClientImageModel::NanoBanana => Self::NanoBanana,
|
||||
ApiClientImageModel::NanoBanana2 => Self::NanoBanana2,
|
||||
ApiClientImageModel::NanoBananaPro => Self::NanoBananaPro,
|
||||
ApiClientImageModel::Seedream4 => Self::Seedream4,
|
||||
ApiClientImageModel::Seedream4p5 => Self::Seedream4p5,
|
||||
ApiClientImageModel::Seedream5Lite => Self::Seedream5Lite,
|
||||
ApiClientImageModel::QwenEdit2511Angles => Self::QwenEdit2511Angles,
|
||||
ApiClientImageModel::Flux2LoraAngles => Self::Flux2LoraAngles,
|
||||
ApiClientImageModel::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListImageModelsImageModel> for String {
|
||||
fn from(value: ListImageModelsImageModel) -> Self {
|
||||
match value {
|
||||
ListImageModelsImageModel::Flux1Dev => "flux_1_dev".to_string(),
|
||||
ListImageModelsImageModel::Flux1Schnell => "flux_1_schnell".to_string(),
|
||||
ListImageModelsImageModel::FluxPro11 => "flux_pro_1p1".to_string(),
|
||||
ListImageModelsImageModel::FluxPro11Ultra => "flux_pro_1p1_ultra".to_string(),
|
||||
ListImageModelsImageModel::GptImage1 => "gpt_image_1".to_string(),
|
||||
ListImageModelsImageModel::GptImage1p5 => "gpt_image_1p5".to_string(),
|
||||
ListImageModelsImageModel::GptImage2 => "gpt_image_2".to_string(),
|
||||
ListImageModelsImageModel::GrokImagineImage => "grok_imagine_image".to_string(),
|
||||
ListImageModelsImageModel::GrokImagineImageQuality => "grok_imagine_image_q".to_string(),
|
||||
ListImageModelsImageModel::Midjourney7 => "midjourney_7".to_string(),
|
||||
ListImageModelsImageModel::Midjourney7Niji => "midjourney_7_niji".to_string(),
|
||||
ListImageModelsImageModel::Midjourney8 => "midjourney_8".to_string(),
|
||||
ListImageModelsImageModel::NanoBanana => "nano_banana".to_string(),
|
||||
ListImageModelsImageModel::NanoBanana2 => "nano_banana_2".to_string(),
|
||||
ListImageModelsImageModel::NanoBananaPro => "nano_banana_pro".to_string(),
|
||||
ListImageModelsImageModel::Seedream4 => "seedream_4".to_string(),
|
||||
ListImageModelsImageModel::Seedream4p5 => "seedream_4p5".to_string(),
|
||||
ListImageModelsImageModel::Seedream5Lite => "seedream_5_lite".to_string(),
|
||||
ListImageModelsImageModel::QwenEdit2511Angles => "qwen_edit_2511_angles".to_string(),
|
||||
ListImageModelsImageModel::Flux2LoraAngles => "flux_2_lora_angles".to_string(),
|
||||
ListImageModelsImageModel::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientAspectRatio` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListImageModelsAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
WideThreeByTwo,
|
||||
WideFourByThree,
|
||||
WideFiveByFour,
|
||||
WideSixteenByNine,
|
||||
WideTwentyOneByNine,
|
||||
TallTwoByThree,
|
||||
TallThreeByFour,
|
||||
TallFourByFive,
|
||||
TallNineBySixteen,
|
||||
TallNineByTwentyOne,
|
||||
Wide,
|
||||
Tall,
|
||||
Auto2k,
|
||||
Auto3k,
|
||||
Auto4k,
|
||||
SquareHd,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientAspectRatio> for ListImageModelsAspectRatio {
|
||||
fn from(value: ApiClientAspectRatio) -> Self {
|
||||
match value {
|
||||
ApiClientAspectRatio::Auto => Self::Auto,
|
||||
ApiClientAspectRatio::Square => Self::Square,
|
||||
ApiClientAspectRatio::WideThreeByTwo => Self::WideThreeByTwo,
|
||||
ApiClientAspectRatio::WideFourByThree => Self::WideFourByThree,
|
||||
ApiClientAspectRatio::WideFiveByFour => Self::WideFiveByFour,
|
||||
ApiClientAspectRatio::WideSixteenByNine => Self::WideSixteenByNine,
|
||||
ApiClientAspectRatio::WideTwentyOneByNine => Self::WideTwentyOneByNine,
|
||||
ApiClientAspectRatio::TallTwoByThree => Self::TallTwoByThree,
|
||||
ApiClientAspectRatio::TallThreeByFour => Self::TallThreeByFour,
|
||||
ApiClientAspectRatio::TallFourByFive => Self::TallFourByFive,
|
||||
ApiClientAspectRatio::TallNineBySixteen => Self::TallNineBySixteen,
|
||||
ApiClientAspectRatio::TallNineByTwentyOne => Self::TallNineByTwentyOne,
|
||||
ApiClientAspectRatio::Wide => Self::Wide,
|
||||
ApiClientAspectRatio::Tall => Self::Tall,
|
||||
ApiClientAspectRatio::Auto2k => Self::Auto2k,
|
||||
ApiClientAspectRatio::Auto3k => Self::Auto3k,
|
||||
ApiClientAspectRatio::Auto4k => Self::Auto4k,
|
||||
ApiClientAspectRatio::SquareHd => Self::SquareHd,
|
||||
ApiClientAspectRatio::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListImageModelsAspectRatio> for String {
|
||||
fn from(value: ListImageModelsAspectRatio) -> Self {
|
||||
match value {
|
||||
ListImageModelsAspectRatio::Auto => "auto".to_string(),
|
||||
ListImageModelsAspectRatio::Square => "square".to_string(),
|
||||
ListImageModelsAspectRatio::WideThreeByTwo => "wide_three_by_two".to_string(),
|
||||
ListImageModelsAspectRatio::WideFourByThree => "wide_four_by_three".to_string(),
|
||||
ListImageModelsAspectRatio::WideFiveByFour => "wide_five_by_four".to_string(),
|
||||
ListImageModelsAspectRatio::WideSixteenByNine => "wide_sixteen_by_nine".to_string(),
|
||||
ListImageModelsAspectRatio::WideTwentyOneByNine => "wide_twenty_one_by_nine".to_string(),
|
||||
ListImageModelsAspectRatio::TallTwoByThree => "tall_two_by_three".to_string(),
|
||||
ListImageModelsAspectRatio::TallThreeByFour => "tall_three_by_four".to_string(),
|
||||
ListImageModelsAspectRatio::TallFourByFive => "tall_four_by_five".to_string(),
|
||||
ListImageModelsAspectRatio::TallNineBySixteen => "tall_nine_by_sixteen".to_string(),
|
||||
ListImageModelsAspectRatio::TallNineByTwentyOne => "tall_nine_by_twenty_one".to_string(),
|
||||
ListImageModelsAspectRatio::Wide => "wide".to_string(),
|
||||
ListImageModelsAspectRatio::Tall => "tall".to_string(),
|
||||
ListImageModelsAspectRatio::Auto2k => "auto_2k".to_string(),
|
||||
ListImageModelsAspectRatio::Auto3k => "auto_3k".to_string(),
|
||||
ListImageModelsAspectRatio::Auto4k => "auto_4k".to_string(),
|
||||
ListImageModelsAspectRatio::SquareHd => "square_hd".to_string(),
|
||||
ListImageModelsAspectRatio::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientResolution` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListImageModelsResolution {
|
||||
OneK,
|
||||
TwoK,
|
||||
ThreeK,
|
||||
FourK,
|
||||
HalfK,
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientResolution> for ListImageModelsResolution {
|
||||
fn from(value: ApiClientResolution) -> Self {
|
||||
match value {
|
||||
ApiClientResolution::OneK => Self::OneK,
|
||||
ApiClientResolution::TwoK => Self::TwoK,
|
||||
ApiClientResolution::ThreeK => Self::ThreeK,
|
||||
ApiClientResolution::FourK => Self::FourK,
|
||||
ApiClientResolution::HalfK => Self::HalfK,
|
||||
ApiClientResolution::FourEightyP => Self::FourEightyP,
|
||||
ApiClientResolution::SevenTwentyP => Self::SevenTwentyP,
|
||||
ApiClientResolution::TenEightyP => Self::TenEightyP,
|
||||
ApiClientResolution::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListImageModelsResolution> for String {
|
||||
fn from(value: ListImageModelsResolution) -> Self {
|
||||
match value {
|
||||
ListImageModelsResolution::OneK => "one_k".to_string(),
|
||||
ListImageModelsResolution::TwoK => "two_k".to_string(),
|
||||
ListImageModelsResolution::ThreeK => "three_k".to_string(),
|
||||
ListImageModelsResolution::FourK => "four_k".to_string(),
|
||||
ListImageModelsResolution::HalfK => "half_k".to_string(),
|
||||
ListImageModelsResolution::FourEightyP => "four_eighty_p".to_string(),
|
||||
ListImageModelsResolution::SevenTwentyP => "seven_twenty_p".to_string(),
|
||||
ListImageModelsResolution::TenEightyP => "ten_eighty_p".to_string(),
|
||||
ListImageModelsResolution::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientQuality` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListImageModelsQuality {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientQuality> for ListImageModelsQuality {
|
||||
fn from(value: ApiClientQuality) -> Self {
|
||||
match value {
|
||||
ApiClientQuality::High => Self::High,
|
||||
ApiClientQuality::Medium => Self::Medium,
|
||||
ApiClientQuality::Low => Self::Low,
|
||||
ApiClientQuality::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListImageModelsQuality> for String {
|
||||
fn from(value: ListImageModelsQuality) -> Self {
|
||||
match value {
|
||||
ListImageModelsQuality::High => "high".to_string(),
|
||||
ListImageModelsQuality::Medium => "medium".to_string(),
|
||||
ListImageModelsQuality::Low => "low".to_string(),
|
||||
ListImageModelsQuality::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Structs ============================
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ListImageModelsResponse {
|
||||
pub success: bool,
|
||||
pub models: Vec<ListImageModelsModelDetails>,
|
||||
pub providers: Vec<ListImageModelsProviderDetails>,
|
||||
}
|
||||
|
||||
impl From<OmniGenImageModelsResponse> for ListImageModelsResponse {
|
||||
fn from(v: OmniGenImageModelsResponse) -> Self {
|
||||
Self {
|
||||
success: v.success,
|
||||
models: v.models.into_iter().map(Into::into).collect(),
|
||||
providers: v.providers.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ListImageModelsProviderDetails {
|
||||
pub provider: ListImageModelsProvider,
|
||||
pub models: Vec<ListImageModelsProviderModelDetails>,
|
||||
}
|
||||
|
||||
impl From<OmniGenImageModelProviderDetails> for ListImageModelsProviderDetails {
|
||||
fn from(v: OmniGenImageModelProviderDetails) -> Self {
|
||||
Self {
|
||||
provider: v.provider.into(),
|
||||
models: v.models.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ListImageModelsProviderModelDetails {
|
||||
pub model: ListImageModelsImageModel,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub overrides: Option<ListImageModelsModelDetails>,
|
||||
}
|
||||
|
||||
impl From<OmniGenImageProviderModelDetails> for ListImageModelsProviderModelDetails {
|
||||
fn from(v: OmniGenImageProviderModelDetails) -> Self {
|
||||
Self {
|
||||
model: v.model.into(),
|
||||
overrides: v.overrides.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ListImageModelsModelDetails {
|
||||
pub model: ListImageModelsImageModel,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ListImageModelsModelCreator>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_prompt_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_prompt_max_length: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_text_prompt_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_text_prompt_max_length: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_refs_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_refs_max: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub has_fixed_editing_aspect_ratio: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio_options: Option<Vec<ListImageModelsAspectRatio>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio_default: Option<ListImageModelsAspectRatio>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio_default_when_editing: Option<ListImageModelsAspectRatio>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution_options: Option<Vec<ListImageModelsResolution>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution_default: Option<ListImageModelsResolution>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality_options: Option<Vec<ListImageModelsQuality>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_quality: Option<ListImageModelsQuality>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_min: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_max: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_options: Option<Vec<u16>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_default: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<OmniGenImageModelDetails> for ListImageModelsModelDetails {
|
||||
fn from(v: OmniGenImageModelDetails) -> Self {
|
||||
Self {
|
||||
model: v.model.into(),
|
||||
model_creator: v.model_creator.map(Into::into),
|
||||
full_name: v.full_name,
|
||||
text_prompt_supported: v.text_prompt_supported,
|
||||
text_prompt_max_length: v.text_prompt_max_length,
|
||||
negative_text_prompt_supported: v.negative_text_prompt_supported,
|
||||
negative_text_prompt_max_length: v.negative_text_prompt_max_length,
|
||||
image_refs_supported: v.image_refs_supported,
|
||||
image_refs_max: v.image_refs_max,
|
||||
has_fixed_editing_aspect_ratio: v.has_fixed_editing_aspect_ratio,
|
||||
aspect_ratio_options: v.aspect_ratio_options.map(|items| items.into_iter().map(Into::into).collect()),
|
||||
aspect_ratio_default: v.aspect_ratio_default.map(Into::into),
|
||||
aspect_ratio_default_when_editing: v.aspect_ratio_default_when_editing.map(Into::into),
|
||||
resolution_options: v.resolution_options.map(|items| items.into_iter().map(Into::into).collect()),
|
||||
resolution_default: v.resolution_default.map(Into::into),
|
||||
quality_options: v.quality_options.map(|items| items.into_iter().map(Into::into).collect()),
|
||||
default_quality: v.default_quality.map(Into::into),
|
||||
batch_size_min: v.batch_size_min,
|
||||
batch_size_max: v.batch_size_max,
|
||||
batch_size_options: v.batch_size_options,
|
||||
batch_size_default: v.batch_size_default,
|
||||
is_disabled: v.is_disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Tauri command: list available video models.
|
||||
//!
|
||||
//! Maps the API-client response into command-specific types (see `response_types`),
|
||||
//! retries the request up to `MAX_ATTEMPTS` times, and caches successful responses
|
||||
//! in-memory for `CACHE_TTL`.
|
||||
|
||||
use std::sync::RwLock;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use log::{debug, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use tauri::State;
|
||||
|
||||
use artcraft_client::endpoints::omni_gen::models::video::omni_gen_list_video_models::{omni_gen_list_video_models, OmniGenListVideoModelsArgs, OmniGenVideoModelsResponse};
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
use crate::core::commands::generate::models::video::response_types::ListVideoModelsResponse;
|
||||
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
|
||||
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
|
||||
|
||||
const MAX_ATTEMPTS: u32 = 3;
|
||||
const RETRY_BACKOFF: Duration = Duration::from_millis(250);
|
||||
const CACHE_TTL: Duration = Duration::from_secs(60);
|
||||
|
||||
struct CacheEntry {
|
||||
response: ListVideoModelsResponse,
|
||||
loaded_at: Instant,
|
||||
}
|
||||
|
||||
static CACHE: Lazy<RwLock<Option<CacheEntry>>> = Lazy::new(|| RwLock::new(None));
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_video_models_command(
|
||||
app_env_configs: State<'_, AppEnvConfigs>,
|
||||
) -> ResponseOrErrorMessage<ListVideoModelsResponse> {
|
||||
if let Some(cached) = cached_response() {
|
||||
debug!("list_video_models_command: serving cached response");
|
||||
return Ok(cached.into());
|
||||
}
|
||||
|
||||
match fetch_with_retry(&app_env_configs.storyteller_host).await {
|
||||
Ok(api_response) => {
|
||||
let response: ListVideoModelsResponse = api_response.into();
|
||||
store_response(response.clone());
|
||||
Ok(response.into())
|
||||
}
|
||||
Err(error_message) => {
|
||||
warn!("list_video_models_command failed after {} attempts: {}", MAX_ATTEMPTS, error_message);
|
||||
if let Some(stale) = any_cached_response() {
|
||||
warn!("list_video_models_command: refresh failed; serving stale cached response");
|
||||
return Ok(stale.into());
|
||||
}
|
||||
Err(error_message.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_response() -> Option<ListVideoModelsResponse> {
|
||||
let guard = CACHE.read().ok()?;
|
||||
let entry = guard.as_ref()?;
|
||||
if entry.loaded_at.elapsed() < CACHE_TTL {
|
||||
Some(entry.response.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The cached response regardless of age. Used as a fallback so that a failed
|
||||
/// refresh still returns the last-known-good data instead of an error.
|
||||
fn any_cached_response() -> Option<ListVideoModelsResponse> {
|
||||
let guard = CACHE.read().ok()?;
|
||||
guard.as_ref().map(|entry| entry.response.clone())
|
||||
}
|
||||
|
||||
fn store_response(response: ListVideoModelsResponse) {
|
||||
if let Ok(mut guard) = CACHE.write() {
|
||||
*guard = Some(CacheEntry { response, loaded_at: Instant::now() });
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the models list, retrying up to `MAX_ATTEMPTS` times on failure.
|
||||
async fn fetch_with_retry(api_host: &ApiHost) -> Result<OmniGenVideoModelsResponse, String> {
|
||||
let mut last_error = "unknown error".to_string();
|
||||
for attempt in 1..=MAX_ATTEMPTS {
|
||||
match omni_gen_list_video_models(OmniGenListVideoModelsArgs { api_host, maybe_creds: None, provider: None }).await {
|
||||
Ok(response) => return Ok(response),
|
||||
Err(err) => {
|
||||
last_error = err.to_string();
|
||||
warn!("list_video_models attempt {}/{} failed: {}", attempt, MAX_ATTEMPTS, last_error);
|
||||
if attempt < MAX_ATTEMPTS {
|
||||
tokio::time::sleep(RETRY_BACKOFF).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_error)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod list_video_models_command;
|
||||
pub mod response_types;
|
||||
@@ -0,0 +1,654 @@
|
||||
//! Command-specific response types for `list_video_models_command`.
|
||||
//!
|
||||
//! These are duplicated (never `artcraft_client` types), and every enum has an
|
||||
//! `Unknown(String)` catch-all so newer server variants are preserved instead of
|
||||
//! breaking an older desktop build. `From` impls map the API-client response over.
|
||||
|
||||
use serde_derive::Serialize;
|
||||
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_generation_provider::ApiClientGenerationProvider;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_model_creator::ApiClientModelCreator;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_video_model::ApiClientVideoModel;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_aspect_ratio::ApiClientAspectRatio;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_resolution::ApiClientResolution;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_bitrate::ApiClientBitrate;
|
||||
use artcraft_client::datatypes::common_enums::generation::api_client_quality::ApiClientQuality;
|
||||
use artcraft_client::endpoints::omni_gen::models::video::omni_gen_list_video_models::{
|
||||
OmniGenVideoModelDetails, OmniGenVideoModelProviderDetails, OmniGenVideoProviderModelDetails, OmniGenVideoModelsResponse,
|
||||
};
|
||||
|
||||
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
|
||||
|
||||
impl SerializeMarker for ListVideoModelsResponse {}
|
||||
|
||||
// ============================ Enums ============================
|
||||
|
||||
/// Command-specific copy of `ApiClientGenerationProvider` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListVideoModelsProvider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
Grok,
|
||||
Midjourney,
|
||||
Sora,
|
||||
WorldLabs,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientGenerationProvider> for ListVideoModelsProvider {
|
||||
fn from(value: ApiClientGenerationProvider) -> Self {
|
||||
match value {
|
||||
ApiClientGenerationProvider::Artcraft => Self::Artcraft,
|
||||
ApiClientGenerationProvider::Fal => Self::Fal,
|
||||
ApiClientGenerationProvider::Grok => Self::Grok,
|
||||
ApiClientGenerationProvider::Midjourney => Self::Midjourney,
|
||||
ApiClientGenerationProvider::Sora => Self::Sora,
|
||||
ApiClientGenerationProvider::WorldLabs => Self::WorldLabs,
|
||||
ApiClientGenerationProvider::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListVideoModelsProvider> for String {
|
||||
fn from(value: ListVideoModelsProvider) -> Self {
|
||||
match value {
|
||||
ListVideoModelsProvider::Artcraft => "artcraft".to_string(),
|
||||
ListVideoModelsProvider::Fal => "fal".to_string(),
|
||||
ListVideoModelsProvider::Grok => "grok".to_string(),
|
||||
ListVideoModelsProvider::Midjourney => "midjourney".to_string(),
|
||||
ListVideoModelsProvider::Sora => "sora".to_string(),
|
||||
ListVideoModelsProvider::WorldLabs => "world_labs".to_string(),
|
||||
ListVideoModelsProvider::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientModelCreator` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListVideoModelsModelCreator {
|
||||
Alibaba,
|
||||
ArtCraft,
|
||||
BlackForestLabs,
|
||||
Bytedance,
|
||||
Fal,
|
||||
Google,
|
||||
Grok,
|
||||
Hailuo,
|
||||
Higgsfield,
|
||||
Kling,
|
||||
Krea,
|
||||
Midjourney,
|
||||
OpenAi,
|
||||
OpenArt,
|
||||
Recraft,
|
||||
Replicate,
|
||||
Runway,
|
||||
Stability,
|
||||
Tencent,
|
||||
TensorArt,
|
||||
Vidu,
|
||||
WorldLabs,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientModelCreator> for ListVideoModelsModelCreator {
|
||||
fn from(value: ApiClientModelCreator) -> Self {
|
||||
match value {
|
||||
ApiClientModelCreator::Alibaba => Self::Alibaba,
|
||||
ApiClientModelCreator::ArtCraft => Self::ArtCraft,
|
||||
ApiClientModelCreator::BlackForestLabs => Self::BlackForestLabs,
|
||||
ApiClientModelCreator::Bytedance => Self::Bytedance,
|
||||
ApiClientModelCreator::Fal => Self::Fal,
|
||||
ApiClientModelCreator::Google => Self::Google,
|
||||
ApiClientModelCreator::Grok => Self::Grok,
|
||||
ApiClientModelCreator::Hailuo => Self::Hailuo,
|
||||
ApiClientModelCreator::Higgsfield => Self::Higgsfield,
|
||||
ApiClientModelCreator::Kling => Self::Kling,
|
||||
ApiClientModelCreator::Krea => Self::Krea,
|
||||
ApiClientModelCreator::Midjourney => Self::Midjourney,
|
||||
ApiClientModelCreator::OpenAi => Self::OpenAi,
|
||||
ApiClientModelCreator::OpenArt => Self::OpenArt,
|
||||
ApiClientModelCreator::Recraft => Self::Recraft,
|
||||
ApiClientModelCreator::Replicate => Self::Replicate,
|
||||
ApiClientModelCreator::Runway => Self::Runway,
|
||||
ApiClientModelCreator::Stability => Self::Stability,
|
||||
ApiClientModelCreator::Tencent => Self::Tencent,
|
||||
ApiClientModelCreator::TensorArt => Self::TensorArt,
|
||||
ApiClientModelCreator::Vidu => Self::Vidu,
|
||||
ApiClientModelCreator::WorldLabs => Self::WorldLabs,
|
||||
ApiClientModelCreator::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListVideoModelsModelCreator> for String {
|
||||
fn from(value: ListVideoModelsModelCreator) -> Self {
|
||||
match value {
|
||||
ListVideoModelsModelCreator::Alibaba => "alibaba".to_string(),
|
||||
ListVideoModelsModelCreator::ArtCraft => "artcraft".to_string(),
|
||||
ListVideoModelsModelCreator::BlackForestLabs => "black_forest_labs".to_string(),
|
||||
ListVideoModelsModelCreator::Bytedance => "bytedance".to_string(),
|
||||
ListVideoModelsModelCreator::Fal => "fal".to_string(),
|
||||
ListVideoModelsModelCreator::Google => "google".to_string(),
|
||||
ListVideoModelsModelCreator::Grok => "grok".to_string(),
|
||||
ListVideoModelsModelCreator::Hailuo => "hailuo".to_string(),
|
||||
ListVideoModelsModelCreator::Higgsfield => "higgsfield".to_string(),
|
||||
ListVideoModelsModelCreator::Kling => "kling".to_string(),
|
||||
ListVideoModelsModelCreator::Krea => "krea".to_string(),
|
||||
ListVideoModelsModelCreator::Midjourney => "midjourney".to_string(),
|
||||
ListVideoModelsModelCreator::OpenAi => "open_ai".to_string(),
|
||||
ListVideoModelsModelCreator::OpenArt => "open_art".to_string(),
|
||||
ListVideoModelsModelCreator::Recraft => "recraft".to_string(),
|
||||
ListVideoModelsModelCreator::Replicate => "replicate".to_string(),
|
||||
ListVideoModelsModelCreator::Runway => "runway".to_string(),
|
||||
ListVideoModelsModelCreator::Stability => "stability".to_string(),
|
||||
ListVideoModelsModelCreator::Tencent => "tencent".to_string(),
|
||||
ListVideoModelsModelCreator::TensorArt => "tensor_art".to_string(),
|
||||
ListVideoModelsModelCreator::Vidu => "vidu".to_string(),
|
||||
ListVideoModelsModelCreator::WorldLabs => "world_labs".to_string(),
|
||||
ListVideoModelsModelCreator::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientVideoModel` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListVideoModelsVideoModel {
|
||||
GrokVideo,
|
||||
GrokImagineVideo,
|
||||
GrokImagineVideo1p5,
|
||||
Kling16Pro,
|
||||
Kling21Pro,
|
||||
Kling21Master,
|
||||
Kling2p5TurboPro,
|
||||
Kling2p6Pro,
|
||||
Kling3p0Standard,
|
||||
Kling3p0Pro,
|
||||
HappyHorse1p0,
|
||||
Seedance10Lite,
|
||||
Seedance1p5Pro,
|
||||
Seedance2p0,
|
||||
Seedance2p0Fast,
|
||||
Seedance2p0BytePlus,
|
||||
Seedance2p0BytePlusFast,
|
||||
Seedance2p0Ultra,
|
||||
Seedance2p0UltraFast,
|
||||
Seedance2p0BytePlusUltra,
|
||||
Seedance2p0BytePlusUltraFast,
|
||||
Seedance2p0Mini,
|
||||
Seedance2p0BytePlusMini,
|
||||
Seedance2p0BytePlusUltraMini,
|
||||
Sora2,
|
||||
Sora2Pro,
|
||||
Veo2,
|
||||
Veo3,
|
||||
Veo3Fast,
|
||||
Veo3p1,
|
||||
Veo3p1Fast,
|
||||
PreviewModel,
|
||||
PreviewModelFast,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientVideoModel> for ListVideoModelsVideoModel {
|
||||
fn from(value: ApiClientVideoModel) -> Self {
|
||||
match value {
|
||||
ApiClientVideoModel::GrokVideo => Self::GrokVideo,
|
||||
ApiClientVideoModel::GrokImagineVideo => Self::GrokImagineVideo,
|
||||
ApiClientVideoModel::GrokImagineVideo1p5 => Self::GrokImagineVideo1p5,
|
||||
ApiClientVideoModel::Kling16Pro => Self::Kling16Pro,
|
||||
ApiClientVideoModel::Kling21Pro => Self::Kling21Pro,
|
||||
ApiClientVideoModel::Kling21Master => Self::Kling21Master,
|
||||
ApiClientVideoModel::Kling2p5TurboPro => Self::Kling2p5TurboPro,
|
||||
ApiClientVideoModel::Kling2p6Pro => Self::Kling2p6Pro,
|
||||
ApiClientVideoModel::Kling3p0Standard => Self::Kling3p0Standard,
|
||||
ApiClientVideoModel::Kling3p0Pro => Self::Kling3p0Pro,
|
||||
ApiClientVideoModel::HappyHorse1p0 => Self::HappyHorse1p0,
|
||||
ApiClientVideoModel::Seedance10Lite => Self::Seedance10Lite,
|
||||
ApiClientVideoModel::Seedance1p5Pro => Self::Seedance1p5Pro,
|
||||
ApiClientVideoModel::Seedance2p0 => Self::Seedance2p0,
|
||||
ApiClientVideoModel::Seedance2p0Fast => Self::Seedance2p0Fast,
|
||||
ApiClientVideoModel::Seedance2p0BytePlus => Self::Seedance2p0BytePlus,
|
||||
ApiClientVideoModel::Seedance2p0BytePlusFast => Self::Seedance2p0BytePlusFast,
|
||||
ApiClientVideoModel::Seedance2p0Ultra => Self::Seedance2p0Ultra,
|
||||
ApiClientVideoModel::Seedance2p0UltraFast => Self::Seedance2p0UltraFast,
|
||||
ApiClientVideoModel::Seedance2p0BytePlusUltra => Self::Seedance2p0BytePlusUltra,
|
||||
ApiClientVideoModel::Seedance2p0BytePlusUltraFast => Self::Seedance2p0BytePlusUltraFast,
|
||||
ApiClientVideoModel::Seedance2p0Mini => Self::Seedance2p0Mini,
|
||||
ApiClientVideoModel::Seedance2p0BytePlusMini => Self::Seedance2p0BytePlusMini,
|
||||
ApiClientVideoModel::Seedance2p0BytePlusUltraMini => Self::Seedance2p0BytePlusUltraMini,
|
||||
ApiClientVideoModel::Sora2 => Self::Sora2,
|
||||
ApiClientVideoModel::Sora2Pro => Self::Sora2Pro,
|
||||
ApiClientVideoModel::Veo2 => Self::Veo2,
|
||||
ApiClientVideoModel::Veo3 => Self::Veo3,
|
||||
ApiClientVideoModel::Veo3Fast => Self::Veo3Fast,
|
||||
ApiClientVideoModel::Veo3p1 => Self::Veo3p1,
|
||||
ApiClientVideoModel::Veo3p1Fast => Self::Veo3p1Fast,
|
||||
ApiClientVideoModel::PreviewModel => Self::PreviewModel,
|
||||
ApiClientVideoModel::PreviewModelFast => Self::PreviewModelFast,
|
||||
ApiClientVideoModel::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListVideoModelsVideoModel> for String {
|
||||
fn from(value: ListVideoModelsVideoModel) -> Self {
|
||||
match value {
|
||||
ListVideoModelsVideoModel::GrokVideo => "grok_video".to_string(),
|
||||
ListVideoModelsVideoModel::GrokImagineVideo => "grok_imagine_video".to_string(),
|
||||
ListVideoModelsVideoModel::GrokImagineVideo1p5 => "grok_imagine_video_1p5".to_string(),
|
||||
ListVideoModelsVideoModel::Kling16Pro => "kling_1p6_pro".to_string(),
|
||||
ListVideoModelsVideoModel::Kling21Pro => "kling_2p1_pro".to_string(),
|
||||
ListVideoModelsVideoModel::Kling21Master => "kling_2p1_master".to_string(),
|
||||
ListVideoModelsVideoModel::Kling2p5TurboPro => "kling_2p5_turbo_pro".to_string(),
|
||||
ListVideoModelsVideoModel::Kling2p6Pro => "kling_2p6_pro".to_string(),
|
||||
ListVideoModelsVideoModel::Kling3p0Standard => "kling_3p0_standard".to_string(),
|
||||
ListVideoModelsVideoModel::Kling3p0Pro => "kling_3p0_pro".to_string(),
|
||||
ListVideoModelsVideoModel::HappyHorse1p0 => "happy_horse_1p0".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance10Lite => "seedance_1p0_lite".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance1p5Pro => "seedance_1p5_pro".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0 => "seedance_2p0".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0Fast => "seedance_2p0_fast".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0BytePlus => "seedance_2p0_bp".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0BytePlusFast => "seedance_2p0_bp_fast".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0Ultra => "seedance_2p0_u".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0UltraFast => "seedance_2p0_u_fast".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0BytePlusUltra => "seedance_2p0_bpu".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0BytePlusUltraFast => "seedance_2p0_bpu_fast".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0Mini => "seedance_2p0_mini".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0BytePlusMini => "seedance_2p0_bp_mini".to_string(),
|
||||
ListVideoModelsVideoModel::Seedance2p0BytePlusUltraMini => "seedance_2p0_bpu_mini".to_string(),
|
||||
ListVideoModelsVideoModel::Sora2 => "sora_2".to_string(),
|
||||
ListVideoModelsVideoModel::Sora2Pro => "sora_2_pro".to_string(),
|
||||
ListVideoModelsVideoModel::Veo2 => "veo_2".to_string(),
|
||||
ListVideoModelsVideoModel::Veo3 => "veo_3".to_string(),
|
||||
ListVideoModelsVideoModel::Veo3Fast => "veo_3_fast".to_string(),
|
||||
ListVideoModelsVideoModel::Veo3p1 => "veo_3p1".to_string(),
|
||||
ListVideoModelsVideoModel::Veo3p1Fast => "veo_3p1_fast".to_string(),
|
||||
ListVideoModelsVideoModel::PreviewModel => "preview_model".to_string(),
|
||||
ListVideoModelsVideoModel::PreviewModelFast => "preview_model_fast".to_string(),
|
||||
ListVideoModelsVideoModel::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientAspectRatio` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListVideoModelsAspectRatio {
|
||||
Auto,
|
||||
Square,
|
||||
WideThreeByTwo,
|
||||
WideFourByThree,
|
||||
WideFiveByFour,
|
||||
WideSixteenByNine,
|
||||
WideTwentyOneByNine,
|
||||
TallTwoByThree,
|
||||
TallThreeByFour,
|
||||
TallFourByFive,
|
||||
TallNineBySixteen,
|
||||
TallNineByTwentyOne,
|
||||
Wide,
|
||||
Tall,
|
||||
Auto2k,
|
||||
Auto3k,
|
||||
Auto4k,
|
||||
SquareHd,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientAspectRatio> for ListVideoModelsAspectRatio {
|
||||
fn from(value: ApiClientAspectRatio) -> Self {
|
||||
match value {
|
||||
ApiClientAspectRatio::Auto => Self::Auto,
|
||||
ApiClientAspectRatio::Square => Self::Square,
|
||||
ApiClientAspectRatio::WideThreeByTwo => Self::WideThreeByTwo,
|
||||
ApiClientAspectRatio::WideFourByThree => Self::WideFourByThree,
|
||||
ApiClientAspectRatio::WideFiveByFour => Self::WideFiveByFour,
|
||||
ApiClientAspectRatio::WideSixteenByNine => Self::WideSixteenByNine,
|
||||
ApiClientAspectRatio::WideTwentyOneByNine => Self::WideTwentyOneByNine,
|
||||
ApiClientAspectRatio::TallTwoByThree => Self::TallTwoByThree,
|
||||
ApiClientAspectRatio::TallThreeByFour => Self::TallThreeByFour,
|
||||
ApiClientAspectRatio::TallFourByFive => Self::TallFourByFive,
|
||||
ApiClientAspectRatio::TallNineBySixteen => Self::TallNineBySixteen,
|
||||
ApiClientAspectRatio::TallNineByTwentyOne => Self::TallNineByTwentyOne,
|
||||
ApiClientAspectRatio::Wide => Self::Wide,
|
||||
ApiClientAspectRatio::Tall => Self::Tall,
|
||||
ApiClientAspectRatio::Auto2k => Self::Auto2k,
|
||||
ApiClientAspectRatio::Auto3k => Self::Auto3k,
|
||||
ApiClientAspectRatio::Auto4k => Self::Auto4k,
|
||||
ApiClientAspectRatio::SquareHd => Self::SquareHd,
|
||||
ApiClientAspectRatio::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListVideoModelsAspectRatio> for String {
|
||||
fn from(value: ListVideoModelsAspectRatio) -> Self {
|
||||
match value {
|
||||
ListVideoModelsAspectRatio::Auto => "auto".to_string(),
|
||||
ListVideoModelsAspectRatio::Square => "square".to_string(),
|
||||
ListVideoModelsAspectRatio::WideThreeByTwo => "wide_three_by_two".to_string(),
|
||||
ListVideoModelsAspectRatio::WideFourByThree => "wide_four_by_three".to_string(),
|
||||
ListVideoModelsAspectRatio::WideFiveByFour => "wide_five_by_four".to_string(),
|
||||
ListVideoModelsAspectRatio::WideSixteenByNine => "wide_sixteen_by_nine".to_string(),
|
||||
ListVideoModelsAspectRatio::WideTwentyOneByNine => "wide_twenty_one_by_nine".to_string(),
|
||||
ListVideoModelsAspectRatio::TallTwoByThree => "tall_two_by_three".to_string(),
|
||||
ListVideoModelsAspectRatio::TallThreeByFour => "tall_three_by_four".to_string(),
|
||||
ListVideoModelsAspectRatio::TallFourByFive => "tall_four_by_five".to_string(),
|
||||
ListVideoModelsAspectRatio::TallNineBySixteen => "tall_nine_by_sixteen".to_string(),
|
||||
ListVideoModelsAspectRatio::TallNineByTwentyOne => "tall_nine_by_twenty_one".to_string(),
|
||||
ListVideoModelsAspectRatio::Wide => "wide".to_string(),
|
||||
ListVideoModelsAspectRatio::Tall => "tall".to_string(),
|
||||
ListVideoModelsAspectRatio::Auto2k => "auto_2k".to_string(),
|
||||
ListVideoModelsAspectRatio::Auto3k => "auto_3k".to_string(),
|
||||
ListVideoModelsAspectRatio::Auto4k => "auto_4k".to_string(),
|
||||
ListVideoModelsAspectRatio::SquareHd => "square_hd".to_string(),
|
||||
ListVideoModelsAspectRatio::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientResolution` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListVideoModelsResolution {
|
||||
OneK,
|
||||
TwoK,
|
||||
ThreeK,
|
||||
FourK,
|
||||
HalfK,
|
||||
FourEightyP,
|
||||
SevenTwentyP,
|
||||
TenEightyP,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientResolution> for ListVideoModelsResolution {
|
||||
fn from(value: ApiClientResolution) -> Self {
|
||||
match value {
|
||||
ApiClientResolution::OneK => Self::OneK,
|
||||
ApiClientResolution::TwoK => Self::TwoK,
|
||||
ApiClientResolution::ThreeK => Self::ThreeK,
|
||||
ApiClientResolution::FourK => Self::FourK,
|
||||
ApiClientResolution::HalfK => Self::HalfK,
|
||||
ApiClientResolution::FourEightyP => Self::FourEightyP,
|
||||
ApiClientResolution::SevenTwentyP => Self::SevenTwentyP,
|
||||
ApiClientResolution::TenEightyP => Self::TenEightyP,
|
||||
ApiClientResolution::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListVideoModelsResolution> for String {
|
||||
fn from(value: ListVideoModelsResolution) -> Self {
|
||||
match value {
|
||||
ListVideoModelsResolution::OneK => "one_k".to_string(),
|
||||
ListVideoModelsResolution::TwoK => "two_k".to_string(),
|
||||
ListVideoModelsResolution::ThreeK => "three_k".to_string(),
|
||||
ListVideoModelsResolution::FourK => "four_k".to_string(),
|
||||
ListVideoModelsResolution::HalfK => "half_k".to_string(),
|
||||
ListVideoModelsResolution::FourEightyP => "four_eighty_p".to_string(),
|
||||
ListVideoModelsResolution::SevenTwentyP => "seven_twenty_p".to_string(),
|
||||
ListVideoModelsResolution::TenEightyP => "ten_eighty_p".to_string(),
|
||||
ListVideoModelsResolution::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientBitrate` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListVideoModelsBitrate {
|
||||
Normal,
|
||||
High,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientBitrate> for ListVideoModelsBitrate {
|
||||
fn from(value: ApiClientBitrate) -> Self {
|
||||
match value {
|
||||
ApiClientBitrate::Normal => Self::Normal,
|
||||
ApiClientBitrate::High => Self::High,
|
||||
ApiClientBitrate::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListVideoModelsBitrate> for String {
|
||||
fn from(value: ListVideoModelsBitrate) -> Self {
|
||||
match value {
|
||||
ListVideoModelsBitrate::Normal => "normal".to_string(),
|
||||
ListVideoModelsBitrate::High => "high".to_string(),
|
||||
ListVideoModelsBitrate::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command-specific copy of `ApiClientQuality` with an `Unknown` catch-all so newer
|
||||
/// server variants are preserved rather than dropped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(into = "String")]
|
||||
pub enum ListVideoModelsQuality {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl From<ApiClientQuality> for ListVideoModelsQuality {
|
||||
fn from(value: ApiClientQuality) -> Self {
|
||||
match value {
|
||||
ApiClientQuality::High => Self::High,
|
||||
ApiClientQuality::Medium => Self::Medium,
|
||||
ApiClientQuality::Low => Self::Low,
|
||||
ApiClientQuality::Unknown(other) => Self::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListVideoModelsQuality> for String {
|
||||
fn from(value: ListVideoModelsQuality) -> Self {
|
||||
match value {
|
||||
ListVideoModelsQuality::High => "high".to_string(),
|
||||
ListVideoModelsQuality::Medium => "medium".to_string(),
|
||||
ListVideoModelsQuality::Low => "low".to_string(),
|
||||
ListVideoModelsQuality::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Structs ============================
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ListVideoModelsResponse {
|
||||
pub success: bool,
|
||||
pub models: Vec<ListVideoModelsModelDetails>,
|
||||
pub providers: Vec<ListVideoModelsProviderDetails>,
|
||||
}
|
||||
|
||||
impl From<OmniGenVideoModelsResponse> for ListVideoModelsResponse {
|
||||
fn from(v: OmniGenVideoModelsResponse) -> Self {
|
||||
Self {
|
||||
success: v.success,
|
||||
models: v.models.into_iter().map(Into::into).collect(),
|
||||
providers: v.providers.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ListVideoModelsProviderDetails {
|
||||
pub provider: ListVideoModelsProvider,
|
||||
pub models: Vec<ListVideoModelsProviderModelDetails>,
|
||||
}
|
||||
|
||||
impl From<OmniGenVideoModelProviderDetails> for ListVideoModelsProviderDetails {
|
||||
fn from(v: OmniGenVideoModelProviderDetails) -> Self {
|
||||
Self {
|
||||
provider: v.provider.into(),
|
||||
models: v.models.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ListVideoModelsProviderModelDetails {
|
||||
pub model: ListVideoModelsVideoModel,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub overrides: Option<ListVideoModelsModelDetails>,
|
||||
}
|
||||
|
||||
impl From<OmniGenVideoProviderModelDetails> for ListVideoModelsProviderModelDetails {
|
||||
fn from(v: OmniGenVideoProviderModelDetails) -> Self {
|
||||
Self {
|
||||
model: v.model.into(),
|
||||
overrides: v.overrides.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct ListVideoModelsModelDetails {
|
||||
pub model: ListVideoModelsVideoModel,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ListVideoModelsModelCreator>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_info_short: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_to_video_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_prompt_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_prompt_max_length: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_text_prompt_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_text_prompt_max_length: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub starting_keyframe_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub starting_keyframe_required: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ending_keyframe_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_references_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_references_max: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub video_references_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub video_references_max: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub video_references_max_total_duration_seconds: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audio_references_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audio_references_max: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audio_references_max_total_duration_seconds: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub character_references_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub character_references_max: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub show_generate_with_sound_toggle: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio_options: Option<Vec<ListVideoModelsAspectRatio>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aspect_ratio_default: Option<ListVideoModelsAspectRatio>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution_options: Option<Vec<ListVideoModelsResolution>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resolution_default: Option<ListVideoModelsResolution>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bitrate_options: Option<Vec<ListVideoModelsBitrate>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bitrate_default: Option<ListVideoModelsBitrate>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub quality_options: Option<Vec<ListVideoModelsQuality>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_quality: Option<ListVideoModelsQuality>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_seconds_min: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_seconds_max: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_seconds_max_with_image_references: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_seconds_options: Option<Vec<u16>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_seconds_default: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_min: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_max: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_options: Option<Vec<u16>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub batch_size_default: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_disabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<OmniGenVideoModelDetails> for ListVideoModelsModelDetails {
|
||||
fn from(v: OmniGenVideoModelDetails) -> Self {
|
||||
Self {
|
||||
model: v.model.into(),
|
||||
model_creator: v.model_creator.map(Into::into),
|
||||
full_name: v.full_name,
|
||||
extra_info: v.extra_info,
|
||||
extra_info_short: v.extra_info_short,
|
||||
text_to_video_supported: v.text_to_video_supported,
|
||||
text_prompt_supported: v.text_prompt_supported,
|
||||
text_prompt_max_length: v.text_prompt_max_length,
|
||||
negative_text_prompt_supported: v.negative_text_prompt_supported,
|
||||
negative_text_prompt_max_length: v.negative_text_prompt_max_length,
|
||||
starting_keyframe_supported: v.starting_keyframe_supported,
|
||||
starting_keyframe_required: v.starting_keyframe_required,
|
||||
ending_keyframe_supported: v.ending_keyframe_supported,
|
||||
image_references_supported: v.image_references_supported,
|
||||
image_references_max: v.image_references_max,
|
||||
video_references_supported: v.video_references_supported,
|
||||
video_references_max: v.video_references_max,
|
||||
video_references_max_total_duration_seconds: v.video_references_max_total_duration_seconds,
|
||||
audio_references_supported: v.audio_references_supported,
|
||||
audio_references_max: v.audio_references_max,
|
||||
audio_references_max_total_duration_seconds: v.audio_references_max_total_duration_seconds,
|
||||
character_references_supported: v.character_references_supported,
|
||||
character_references_max: v.character_references_max,
|
||||
show_generate_with_sound_toggle: v.show_generate_with_sound_toggle,
|
||||
aspect_ratio_options: v.aspect_ratio_options.map(|items| items.into_iter().map(Into::into).collect()),
|
||||
aspect_ratio_default: v.aspect_ratio_default.map(Into::into),
|
||||
resolution_options: v.resolution_options.map(|items| items.into_iter().map(Into::into).collect()),
|
||||
resolution_default: v.resolution_default.map(Into::into),
|
||||
bitrate_options: v.bitrate_options.map(|items| items.into_iter().map(Into::into).collect()),
|
||||
bitrate_default: v.bitrate_default.map(Into::into),
|
||||
quality_options: v.quality_options.map(|items| items.into_iter().map(Into::into).collect()),
|
||||
default_quality: v.default_quality.map(Into::into),
|
||||
duration_seconds_min: v.duration_seconds_min,
|
||||
duration_seconds_max: v.duration_seconds_max,
|
||||
duration_seconds_max_with_image_references: v.duration_seconds_max_with_image_references,
|
||||
duration_seconds_options: v.duration_seconds_options,
|
||||
duration_seconds_default: v.duration_seconds_default,
|
||||
batch_size_min: v.batch_size_min,
|
||||
batch_size_max: v.batch_size_max,
|
||||
batch_size_options: v.batch_size_options,
|
||||
batch_size_default: v.batch_size_default,
|
||||
is_disabled: v.is_disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,10 +74,18 @@ pub enum GenerationModel {
|
||||
// Generic Midjourney model, version unknown.
|
||||
#[serde(rename = "midjourney")]
|
||||
Midjourney,
|
||||
#[serde(rename = "midjourney_7")]
|
||||
Midjourney7,
|
||||
#[serde(rename = "midjourney_7_niji")]
|
||||
Midjourney7Niji,
|
||||
#[serde(rename = "midjourney_8")]
|
||||
Midjourney8,
|
||||
|
||||
// Generic Grok video model, version unknown.
|
||||
#[serde(rename = "grok_video")]
|
||||
GrokVideo,
|
||||
#[serde(rename = "grok_imagine_video_1p5")]
|
||||
GrokImagineVideo1p5,
|
||||
// TODO: Should be Kling16Pro
|
||||
#[serde(rename = "kling_1.6")]
|
||||
Kling1_6,
|
||||
|
||||
@@ -299,7 +299,11 @@ fn task_model_type_to_generation_model(model: TaskModelType) -> Option<Generatio
|
||||
TaskModelType::QwenEdit2511Angles => Some(GenerationModel::QwenEdit2511Angles),
|
||||
TaskModelType::GrokImage => Some(GenerationModel::GrokImage),
|
||||
TaskModelType::Recraft3 => Some(GenerationModel::Recraft3),
|
||||
TaskModelType::Midjourney7 => Some(GenerationModel::Midjourney7),
|
||||
TaskModelType::Midjourney7Niji => Some(GenerationModel::Midjourney7Niji),
|
||||
TaskModelType::Midjourney8 => Some(GenerationModel::Midjourney8),
|
||||
TaskModelType::GrokVideo => Some(GenerationModel::GrokVideo),
|
||||
TaskModelType::GrokImagineVideo1p5 => Some(GenerationModel::GrokImagineVideo1p5),
|
||||
TaskModelType::Kling16Pro => Some(GenerationModel::Kling1_6),
|
||||
TaskModelType::Kling21Pro => Some(GenerationModel::Kling21Pro),
|
||||
TaskModelType::Kling21Master => Some(GenerationModel::Kling21Master),
|
||||
|
||||
@@ -9,6 +9,8 @@ use crate::core::commands::app_preferences::update_app_preference_command::updat
|
||||
use crate::core::commands::cost_estimate::estimate_image_cost_command::estimate_image_cost_command;
|
||||
use crate::core::commands::cost_estimate::estimate_splat_cost_command::estimate_splat_cost_command;
|
||||
use crate::core::commands::cost_estimate::estimate_video_cost_command::estimate_video_cost_command;
|
||||
use crate::core::commands::generate::models::image::list_image_models_command::list_image_models_command;
|
||||
use crate::core::commands::generate::models::video::list_video_models_command::list_video_models_command;
|
||||
use crate::core::commands::download::download_directory_reveal_command::download_directory_reveal_command;
|
||||
use crate::core::commands::download::download_media_file_command::download_media_file_command;
|
||||
use crate::core::commands::download::download_url_command::download_url_command;
|
||||
@@ -219,6 +221,8 @@ pub fn run() {
|
||||
estimate_image_cost_command,
|
||||
estimate_splat_cost_command,
|
||||
estimate_video_cost_command,
|
||||
list_image_models_command,
|
||||
list_video_models_command,
|
||||
flip_image,
|
||||
generate_image_command,
|
||||
generate_video_command,
|
||||
|
||||
@@ -59,10 +59,18 @@ pub enum TaskModelType {
|
||||
// Generic Midjourney model, version unknown.
|
||||
#[serde(rename = "midjourney")]
|
||||
Midjourney,
|
||||
#[serde(rename = "midjourney_7")]
|
||||
Midjourney7,
|
||||
#[serde(rename = "midjourney_7_niji")]
|
||||
Midjourney7Niji,
|
||||
#[serde(rename = "midjourney_8")]
|
||||
Midjourney8,
|
||||
|
||||
// Video models
|
||||
#[serde(rename = "grok_video")]
|
||||
GrokVideo, // Video version unspecified/unknown
|
||||
#[serde(rename = "grok_imagine_video_1p5")]
|
||||
GrokImagineVideo1p5,
|
||||
#[serde(rename = "kling_1.6_pro")]
|
||||
Kling16Pro,
|
||||
#[serde(rename = "kling_2.1_pro")]
|
||||
@@ -149,8 +157,12 @@ impl TaskModelType {
|
||||
Self::GrokImage => "grok_image",
|
||||
Self::Recraft3 => "recraft_3",
|
||||
Self::Midjourney => "midjourney",
|
||||
Self::Midjourney7 => "midjourney_7",
|
||||
Self::Midjourney7Niji => "midjourney_7_niji",
|
||||
Self::Midjourney8 => "midjourney_8",
|
||||
// Video models
|
||||
Self::GrokVideo => "grok_video",
|
||||
Self::GrokImagineVideo1p5 => "grok_imagine_video_1p5",
|
||||
Self::Kling16Pro => "kling_1.6_pro",
|
||||
Self::Kling21Pro => "kling_2.1_pro",
|
||||
Self::Kling21Master => "kling_2.1_master",
|
||||
@@ -205,8 +217,12 @@ impl TaskModelType {
|
||||
"grok_image" => Ok(Self::GrokImage),
|
||||
"recraft_3" => Ok(Self::Recraft3),
|
||||
"midjourney" => Ok(Self::Midjourney),
|
||||
"midjourney_7" => Ok(Self::Midjourney7),
|
||||
"midjourney_7_niji" => Ok(Self::Midjourney7Niji),
|
||||
"midjourney_8" => Ok(Self::Midjourney8),
|
||||
// Video models
|
||||
"grok_video" => Ok(Self::GrokVideo),
|
||||
"grok_imagine_video_1p5" => Ok(Self::GrokImagineVideo1p5),
|
||||
"kling_1.6_pro" => Ok(Self::Kling16Pro),
|
||||
"kling_2.1_pro" => Ok(Self::Kling21Pro),
|
||||
"kling_2.1_master" => Ok(Self::Kling21Master),
|
||||
@@ -264,8 +280,12 @@ impl TaskModelType {
|
||||
Self::GrokImage,
|
||||
Self::Recraft3,
|
||||
Self::Midjourney,
|
||||
Self::Midjourney7,
|
||||
Self::Midjourney7Niji,
|
||||
Self::Midjourney8,
|
||||
// Video models
|
||||
Self::GrokVideo,
|
||||
Self::GrokImagineVideo1p5,
|
||||
Self::Kling16Pro,
|
||||
Self::Kling21Pro,
|
||||
Self::Kling21Master,
|
||||
@@ -331,7 +351,11 @@ mod tests {
|
||||
assert_serialization(TaskModelType::Recraft3, "recraft_3");
|
||||
assert_serialization(TaskModelType::Midjourney, "midjourney");
|
||||
// Video models
|
||||
assert_serialization(TaskModelType::Midjourney7, "midjourney_7");
|
||||
assert_serialization(TaskModelType::Midjourney7Niji, "midjourney_7_niji");
|
||||
assert_serialization(TaskModelType::Midjourney8, "midjourney_8");
|
||||
assert_serialization(TaskModelType::GrokVideo, "grok_video");
|
||||
assert_serialization(TaskModelType::GrokImagineVideo1p5, "grok_imagine_video_1p5");
|
||||
assert_serialization(TaskModelType::Kling16Pro, "kling_1.6_pro");
|
||||
assert_serialization(TaskModelType::Kling21Pro, "kling_2.1_pro");
|
||||
assert_serialization(TaskModelType::Kling21Master, "kling_2.1_master");
|
||||
@@ -387,6 +411,7 @@ mod tests {
|
||||
assert_eq!(TaskModelType::Midjourney.to_str(), "midjourney");
|
||||
// Video models
|
||||
assert_eq!(TaskModelType::GrokVideo.to_str(), "grok_video");
|
||||
assert_eq!(TaskModelType::GrokImagineVideo1p5.to_str(), "grok_imagine_video_1p5");
|
||||
assert_eq!(TaskModelType::Kling16Pro.to_str(), "kling_1.6_pro");
|
||||
assert_eq!(TaskModelType::Kling21Pro.to_str(), "kling_2.1_pro");
|
||||
assert_eq!(TaskModelType::Kling21Master.to_str(), "kling_2.1_master");
|
||||
@@ -442,6 +467,7 @@ mod tests {
|
||||
assert_eq!(TaskModelType::from_str("midjourney").unwrap(), TaskModelType::Midjourney);
|
||||
// Video models
|
||||
assert_eq!(TaskModelType::from_str("grok_video").unwrap(), TaskModelType::GrokVideo);
|
||||
assert_eq!(TaskModelType::from_str("grok_imagine_video_1p5").unwrap(), TaskModelType::GrokImagineVideo1p5);
|
||||
assert_eq!(TaskModelType::from_str("kling_1.6_pro").unwrap(), TaskModelType::Kling16Pro);
|
||||
assert_eq!(TaskModelType::from_str("kling_2.1_pro").unwrap(), TaskModelType::Kling21Pro);
|
||||
assert_eq!(TaskModelType::from_str("kling_2.1_master").unwrap(), TaskModelType::Kling21Master);
|
||||
@@ -484,7 +510,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = TaskModelType::all_variants();
|
||||
assert_eq!(variants.len(), 48);
|
||||
assert_eq!(variants.len(), 52);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Flux1Schnell));
|
||||
@@ -508,8 +534,12 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::GrokImage));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Recraft3));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Midjourney));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Midjourney7));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Midjourney7Niji));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Midjourney8));
|
||||
// Video models
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::GrokVideo));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::GrokImagineVideo1p5));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling16Pro));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Pro));
|
||||
assert_eq!(variants.pop_first(), Some(TaskModelType::Kling21Master));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #2d81ff;
|
||||
fill: #ef4444;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
|
Before Width: | Height: | Size: 885 B After Width: | Height: | Size: 885 B |
@@ -6,7 +6,6 @@ import {
|
||||
IconCamera,
|
||||
IconCompass,
|
||||
IconCreditCard,
|
||||
IconDashboard,
|
||||
IconFlag,
|
||||
IconKey,
|
||||
IconClipboardList,
|
||||
@@ -39,86 +38,46 @@ import {
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
navGroups: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/",
|
||||
icon: IconDashboard,
|
||||
label: "Media",
|
||||
items: [
|
||||
{ title: "Explore Media", url: "/explore/media", icon: IconCompass },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Explore Media",
|
||||
url: "/explore/media",
|
||||
icon: IconCompass,
|
||||
label: "Users",
|
||||
items: [
|
||||
{ title: "User Search", url: "/user/search", icon: IconUsers },
|
||||
{ title: "Stripe Lookup", url: "/stripe-lookup", icon: IconBrandStripe },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "User Search",
|
||||
url: "/user/search",
|
||||
icon: IconUsers,
|
||||
label: "User Growth",
|
||||
items: [
|
||||
{ title: "User Signups", url: "/user-signups", icon: IconUserPlus },
|
||||
{ title: "Subscriber Signups", url: "/subscriber-signups", icon: IconCreditCard },
|
||||
{ title: "Referrals", url: "/referrals", icon: IconShare },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Stripe Lookup",
|
||||
url: "/stripe-lookup",
|
||||
icon: IconBrandStripe,
|
||||
label: "Business Growth",
|
||||
items: [
|
||||
{ title: "Spend Events", url: "/spend-events", icon: IconCoin },
|
||||
{ title: "Reengagement List", url: "/reengagement-list", icon: IconHeartHandshake },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Search Job by Token",
|
||||
url: "/moderation/job-search",
|
||||
icon: IconBriefcase,
|
||||
},
|
||||
{
|
||||
title: "Search Debug Logs",
|
||||
url: "/moderation/debug-logs-search",
|
||||
icon: IconBug,
|
||||
},
|
||||
{
|
||||
title: "User Signups",
|
||||
url: "/user-signups",
|
||||
icon: IconUserPlus,
|
||||
},
|
||||
{
|
||||
title: "Subscriber Signups",
|
||||
url: "/subscriber-signups",
|
||||
icon: IconCreditCard,
|
||||
},
|
||||
{
|
||||
title: "Spend Events",
|
||||
url: "/spend-events",
|
||||
icon: IconCoin,
|
||||
},
|
||||
{
|
||||
title: "Reengagement List",
|
||||
url: "/reengagement-list",
|
||||
icon: IconHeartHandshake,
|
||||
},
|
||||
{
|
||||
title: "Impersonation Logs",
|
||||
url: "/impersonation",
|
||||
icon: IconKey,
|
||||
},
|
||||
{
|
||||
title: "Staff Audit Logs",
|
||||
url: "/staff-audit-logs",
|
||||
icon: IconClipboardList,
|
||||
},
|
||||
{
|
||||
title: "Send Pager",
|
||||
url: "/send-pager",
|
||||
icon: IconBellRinging,
|
||||
},
|
||||
{
|
||||
title: "Feature Flags",
|
||||
url: "/feature-flags",
|
||||
icon: IconFlag,
|
||||
},
|
||||
{
|
||||
title: "Email Changes",
|
||||
url: "/email-changes",
|
||||
icon: IconMail,
|
||||
},
|
||||
{
|
||||
title: "Referrals",
|
||||
url: "/referrals",
|
||||
icon: IconShare,
|
||||
label: "Oncall",
|
||||
items: [
|
||||
{ title: "Page Oncall", url: "/send-pager", icon: IconBellRinging },
|
||||
{ title: "Search Job by Token", url: "/moderation/job-search", icon: IconBriefcase },
|
||||
{ title: "Search Debug Logs", url: "/moderation/debug-logs-search", icon: IconBug },
|
||||
{ title: "Impersonation Logs", url: "/impersonation", icon: IconKey },
|
||||
{ title: "Staff Audit Logs", url: "/staff-audit-logs", icon: IconClipboardList },
|
||||
{ title: "Feature Flags", url: "/feature-flags", icon: IconFlag },
|
||||
{ title: "Email Changes", url: "/email-changes", icon: IconMail },
|
||||
],
|
||||
},
|
||||
],
|
||||
navClouds: [
|
||||
@@ -237,7 +196,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<NavMain items={data.navMain} />
|
||||
{data.navGroups.map((group) => (
|
||||
<NavMain key={group.label} label={group.label} items={group.items} />
|
||||
))}
|
||||
{/* <NavDocuments items={data.documents} /> */}
|
||||
<NavSecondary items={navSecondaryConfig} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type Icon } from "@tabler/icons-react";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
@@ -12,12 +13,14 @@ import {
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
label,
|
||||
}: {
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
icon?: Icon;
|
||||
}[];
|
||||
label?: string;
|
||||
}) {
|
||||
const { pathname } = useLocation();
|
||||
const { isMobile, setOpenMobile } = useSidebar();
|
||||
@@ -30,6 +33,7 @@ export function NavMain({
|
||||
|
||||
return (
|
||||
<SidebarGroup>
|
||||
{label && <SidebarGroupLabel>{label}</SidebarGroupLabel>}
|
||||
<SidebarGroupContent className="flex flex-col gap-2">
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
|
||||
@@ -14,6 +14,7 @@ import { pageHeight, pageWidth, persistLogin } from "~/signals";
|
||||
import { SyncStorytellerApiConfig } from "./api/SyncStorytellerApiConfig";
|
||||
import { posthog } from "posthog-js";
|
||||
import { SoundManager } from "@storyteller/soundboard";
|
||||
import { useModelsStore } from "@storyteller/tauri-api";
|
||||
|
||||
config.autoAddCss = false; /* eslint-disable import/first */
|
||||
|
||||
@@ -89,6 +90,13 @@ const GlobalSettingsManager = ({ env }: { env: Record<string, string> }) => {
|
||||
SoundManager.install();
|
||||
}, []);
|
||||
|
||||
// Reconcile the model dropdowns against the backend omni listing once on boot.
|
||||
// The store is already seeded with the static overlay, so a failure here is a
|
||||
// no-op (the UI keeps the overlay models).
|
||||
useEffect(() => {
|
||||
void useModelsStore.getState().loadModelsFromBackend();
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import { SliderV2 } from "@storyteller/ui-sliderv2";
|
||||
import { GenerateImage } from "@storyteller/tauri-api";
|
||||
import {
|
||||
ClassyModelSelector,
|
||||
ANGLES_PAGE_MODEL_LIST,
|
||||
useAnglesPageModelList,
|
||||
ModelPage,
|
||||
useSelectedModel,
|
||||
useSelectedProviderForModel,
|
||||
@@ -58,6 +58,7 @@ const ANGLES_MODEL_PAGE = ModelPage.Angles;
|
||||
// ─── Main Angles Component ─────────────────────────────────────────────────────
|
||||
|
||||
export const Angles = () => {
|
||||
const anglesModelList = useAnglesPageModelList();
|
||||
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
|
||||
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
|
||||
[],
|
||||
@@ -733,7 +734,7 @@ export const Angles = () => {
|
||||
|
||||
<ClassyModelSelector
|
||||
variant="embedded"
|
||||
items={ANGLES_PAGE_MODEL_LIST}
|
||||
items={anglesModelList}
|
||||
page={ANGLES_MODEL_PAGE}
|
||||
/>
|
||||
<GenerateButton
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PromptBoxImage } from "@storyteller/ui-promptbox";
|
||||
import { UploadImageMedia, FilterMediaClasses } from "@storyteller/api";
|
||||
import BackgroundGallery from "./BackgroundGallery";
|
||||
import {
|
||||
TEXT_TO_IMAGE_PAGE_MODEL_LIST,
|
||||
useTextToImagePageModelList,
|
||||
ModelPage,
|
||||
ClassyModelSelector,
|
||||
useSelectedImageModel,
|
||||
@@ -44,6 +44,7 @@ interface TextToImageProps {
|
||||
}
|
||||
|
||||
const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
const textToImageModelList = useTextToImagePageModelList();
|
||||
const startBatch = useTextToImageStore((s) => s.startBatch);
|
||||
const promptContentRef = useRef<HTMLDivElement>(null);
|
||||
const [promptHeight, setPromptHeight] = useState<number>(138);
|
||||
@@ -187,7 +188,7 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
||||
modelSelector={
|
||||
<ClassyModelSelector
|
||||
variant="embedded"
|
||||
items={TEXT_TO_IMAGE_PAGE_MODEL_LIST}
|
||||
items={textToImageModelList}
|
||||
page={PAGE_ID}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import BackgroundGallery from "./BackgroundGallery";
|
||||
import {
|
||||
ClassyModelSelector,
|
||||
IMAGE_TO_VIDEO_PAGE_MODEL_LIST,
|
||||
useImageToVideoPageModelList,
|
||||
ModelPage,
|
||||
useSelectedVideoModel,
|
||||
useSelectedProviderForModel,
|
||||
@@ -56,6 +56,7 @@ interface ImageToVideoProps {
|
||||
}
|
||||
|
||||
const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
const imageToVideoModelList = useImageToVideoPageModelList();
|
||||
const startBatch = useImageToVideoStore((s) => s.startBatch);
|
||||
const completeBatch = useImageToVideoStore((s) => s.completeBatch);
|
||||
const promptContentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -215,7 +216,7 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
modelSelector={
|
||||
<ClassyModelSelector
|
||||
variant="embedded"
|
||||
items={IMAGE_TO_VIDEO_PAGE_MODEL_LIST}
|
||||
items={imageToVideoModelList}
|
||||
page={PAGE_ID}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ interface ClassyModelSelectorProps {
|
||||
showProviderSelection?: boolean;
|
||||
}
|
||||
|
||||
// Model instances are rebuilt when the backend listing hydrates, so compare by
|
||||
// tauriId rather than object identity.
|
||||
const isSameModel = (a: Model | undefined, b: Model | undefined): boolean =>
|
||||
a !== undefined && b !== undefined && a.tauriId === b.tauriId;
|
||||
|
||||
const DEFAULT_PROVIDER_OPTIONS: GenerationProvider[] = [GenerationProvider.Artcraft];
|
||||
|
||||
function ProviderTooltipContent({
|
||||
@@ -143,6 +148,20 @@ export function ClassyModelSelector({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// The backend listing hydrates asynchronously and rebuilds the model
|
||||
// instances with API capabilities. Swap a stale selected instance for the
|
||||
// fresh one so capability-driven UI (keyframes, references, pickers)
|
||||
// reflects the API data without needing a manual re-select.
|
||||
useEffect(() => {
|
||||
const selected = selectedModels[page];
|
||||
if (!selected) return;
|
||||
const fresh = itemModels.find((m) => m.tauriId === selected.tauriId);
|
||||
if (fresh !== undefined && fresh !== selected) {
|
||||
setSelectedModel(page, fresh);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [items, selectedModels, page]);
|
||||
|
||||
// Initialize a default provider for each model so we can render icons even when not selected
|
||||
useEffect(() => {
|
||||
for (const item of items) {
|
||||
@@ -175,7 +194,7 @@ export function ClassyModelSelector({
|
||||
|
||||
return {
|
||||
...item,
|
||||
selected: item.model === selectedModel,
|
||||
selected: isSameModel(item.model as Model | undefined, selectedModel),
|
||||
hoverTooltip: hasMultipleProviders
|
||||
? (close: () => void) => (
|
||||
<ProviderTooltipContent
|
||||
@@ -190,7 +209,7 @@ export function ClassyModelSelector({
|
||||
: undefined,
|
||||
tooltipDelayMs: providerTooltipDelayMs,
|
||||
trailing:
|
||||
item.model !== selectedModel && hasMultipleProviders
|
||||
!isSameModel(item.model as Model | undefined, selectedModel) && hasMultipleProviders
|
||||
? (() => {
|
||||
const prov = modelId
|
||||
? selectedProvidersByModel[modelId]
|
||||
@@ -206,7 +225,7 @@ export function ClassyModelSelector({
|
||||
})()
|
||||
: undefined,
|
||||
selectedRight:
|
||||
item.model === selectedModel &&
|
||||
isSameModel(item.model as Model | undefined, selectedModel) &&
|
||||
selectedProvider &&
|
||||
hasMultipleProviders ? (
|
||||
<div className="mr-1 rounded-md p-1.5 bg-primary/60 group-hover:bg-primary/80 transition-colors">
|
||||
@@ -265,7 +284,7 @@ export function ClassyModelSelector({
|
||||
{...popoverProps}
|
||||
buttonClassName="rounded-xl bg-ui-controls/90 hover:bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
|
||||
renderTrigger={(selectedItem) => {
|
||||
const modelTitle = selectedItem?.label ?? "";
|
||||
const modelTitle = selectedItem?.label ?? selectedModel?.selectorName ?? "";
|
||||
const providerIcon = selectedProvider
|
||||
? getProviderIcon(selectedProvider)
|
||||
: null;
|
||||
|
||||
@@ -6,35 +6,37 @@ import {
|
||||
} from "@storyteller/model-list";
|
||||
import { ModelPage } from "./model-pages";
|
||||
|
||||
const DEFAULT_MODEL_ID_FOR_PAGE: Partial<Record<ModelPage, string>> = {
|
||||
[ModelPage.TextToImage]: "nano_banana_pro",
|
||||
[ModelPage.ImageToVideo]: "seedance_2p0",
|
||||
[ModelPage.Canvas2D]: "gpt_image_1p5",
|
||||
[ModelPage.Stage3D]: "gpt_image_1p5",
|
||||
[ModelPage.ImageEditor]: "nano_banana_pro",
|
||||
[ModelPage.ImageTo3DWorld]: "marble_0p1_mini",
|
||||
[ModelPage.Angles]: "flux_2_lora_angles",
|
||||
};
|
||||
|
||||
export const defaultModelForPage = (
|
||||
models: Model[],
|
||||
page: ModelPage,
|
||||
): Model => {
|
||||
let imageModel: Model | undefined;
|
||||
const defaultId = DEFAULT_MODEL_ID_FOR_PAGE[page];
|
||||
|
||||
switch (page) {
|
||||
case ModelPage.TextToImage:
|
||||
imageModel = IMAGE_MODELS_BY_ID.get("nano_banana_pro");
|
||||
break;
|
||||
case ModelPage.ImageToVideo:
|
||||
imageModel = VIDEO_MODELS_BY_ID.get("seedance_2p0");
|
||||
break;
|
||||
case ModelPage.Canvas2D:
|
||||
imageModel = IMAGE_MODELS_BY_ID.get("gpt_image_1p5");
|
||||
break;
|
||||
case ModelPage.Stage3D:
|
||||
imageModel = IMAGE_MODELS_BY_ID.get("gpt_image_1p5");
|
||||
break;
|
||||
case ModelPage.ImageEditor:
|
||||
imageModel = IMAGE_MODELS_BY_ID.get("nano_banana_pro");
|
||||
break;
|
||||
case ModelPage.ImageTo3DWorld:
|
||||
imageModel = SPLAT_MODELS_BY_ID.get("marble_0p1_mini");
|
||||
break;
|
||||
case ModelPage.Angles:
|
||||
imageModel = IMAGE_MODELS_BY_ID.get("flux_2_lora_angles");
|
||||
break;
|
||||
if (defaultId) {
|
||||
// Prefer the instance from the caller's (backend-hydrated) list — the
|
||||
// static map instances are presentation-only fallbacks without the API's
|
||||
// capability data.
|
||||
const fromList = models.find(
|
||||
(m) => m.id === defaultId || m.tauriId === defaultId,
|
||||
);
|
||||
if (fromList) return fromList;
|
||||
|
||||
const fromStaticMaps =
|
||||
IMAGE_MODELS_BY_ID.get(defaultId) ??
|
||||
VIDEO_MODELS_BY_ID.get(defaultId) ??
|
||||
SPLAT_MODELS_BY_ID.get(defaultId);
|
||||
if (fromStaticMaps) return fromStaticMaps;
|
||||
}
|
||||
|
||||
return imageModel || models[0];
|
||||
return models[0];
|
||||
};
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { useMemo } from "react";
|
||||
import type { PopoverItem } from "@storyteller/ui-popover";
|
||||
import { faCube, faFilm, faImage } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
getCreatorIcon,
|
||||
Model,
|
||||
ImageModel,
|
||||
VideoModel,
|
||||
VIDEO_MODELS,
|
||||
IMAGE_MODELS,
|
||||
SPLAT_MODELS,
|
||||
OBJECT_3D_MODELS,
|
||||
} from "@storyteller/model-list";
|
||||
import { ModelTag } from "@storyteller/model-list";
|
||||
import { useImageModels, useVideoModels } from "@storyteller/tauri-api";
|
||||
|
||||
export type ModelList = Omit<PopoverItem, "selected">[];
|
||||
|
||||
const withIcon = (creatorIcon: any, fallback: any) => creatorIcon || fallback;
|
||||
|
||||
const buildItems = (
|
||||
models: Model[],
|
||||
fallbackIcon: any
|
||||
) =>
|
||||
const buildItems = (models: Model[], fallbackIcon: any) =>
|
||||
models.map((model: Model) => ({
|
||||
label: model.selectorName,
|
||||
icon: withIcon(getCreatorIcon(model.creator), fallbackIcon),
|
||||
@@ -27,113 +28,137 @@ const buildItems = (
|
||||
model: model,
|
||||
}));
|
||||
|
||||
const sortedBySelectorName = <T extends Model>(models: T[]): T[] => {
|
||||
// De-dupe while preserving instances, then sort by selector name.
|
||||
const list = Array.from(new Set(models));
|
||||
list.sort((a, b) => a.selectorName?.localeCompare(b.selectorName));
|
||||
return list;
|
||||
};
|
||||
|
||||
const imageIcon = <FontAwesomeIcon icon={faImage} className="h-4 w-4" />;
|
||||
const filmIcon = <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />;
|
||||
const cubeIcon = <FontAwesomeIcon icon={faCube} className="h-4 w-4" />;
|
||||
|
||||
/**
|
||||
* IN-PROGRESS MIGRATION (messy for now)
|
||||
* We're gradually going to phase out ModelList, ModelConfig, etc.
|
||||
* We won't index by name, but rather id, or simply will always have full
|
||||
* access to the object directly.
|
||||
* Per-page model subsetting. These pages show a slice of the full model set
|
||||
* based on model CAPABILITY FLAGS (canTextToImage, canEditImages, canEditAngles,
|
||||
* the InstructiveEdit tag, etc.). Those flags do not exist in the storyteller-web
|
||||
* omni backend, so this subsetting is maintained here in the frontend overlay.
|
||||
*
|
||||
* Each builder is a pure function over a model list, so it can run against either
|
||||
* the static overlay (the `*_PAGE_MODEL_LIST` constants below) or the live,
|
||||
* backend-reconciled list from `useModelsStore` (the `use*PageModelList` hooks).
|
||||
*/
|
||||
|
||||
export const TEXT_TO_IMAGE_PAGE_MODEL_LIST: ModelList =
|
||||
export const buildTextToImagePageList = (imageModels: ImageModel[]): ModelList =>
|
||||
buildItems(
|
||||
(function (): Model[] {
|
||||
const set: Set<Model> = new Set();
|
||||
IMAGE_MODELS
|
||||
.filter((model) => model.canTextToImage)
|
||||
.forEach((m) => set.add(m));
|
||||
const list = Array.from(set);
|
||||
list.sort((a, b) => a.selectorName?.localeCompare(b.selectorName));
|
||||
return list;
|
||||
})(),
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
sortedBySelectorName(imageModels.filter((m) => m.canTextToImage)),
|
||||
imageIcon,
|
||||
);
|
||||
|
||||
export const buildCanvas2dPageList = (imageModels: ImageModel[]): ModelList =>
|
||||
buildItems(
|
||||
sortedBySelectorName(
|
||||
imageModels.filter(
|
||||
(m) => m.canEditImages || m.tags?.includes(ModelTag.InstructiveEdit),
|
||||
),
|
||||
),
|
||||
imageIcon,
|
||||
);
|
||||
|
||||
export const buildStage3dPageList = (imageModels: ImageModel[]): ModelList =>
|
||||
buildItems(
|
||||
sortedBySelectorName(
|
||||
imageModels.filter((m) => m.tags?.includes(ModelTag.InstructiveEdit)),
|
||||
),
|
||||
imageIcon,
|
||||
);
|
||||
|
||||
export const buildImageEditorPageList = (imageModels: ImageModel[]): ModelList =>
|
||||
buildItems(
|
||||
sortedBySelectorName(imageModels.filter((m) => m.canEditImages)),
|
||||
imageIcon,
|
||||
);
|
||||
|
||||
export const buildAnglesPageList = (imageModels: ImageModel[]): ModelList =>
|
||||
buildItems(
|
||||
sortedBySelectorName(imageModels.filter((m) => m.canEditAngles)),
|
||||
imageIcon,
|
||||
);
|
||||
|
||||
export const buildImageToVideoPageList = (videoModels: VideoModel[]): ModelList =>
|
||||
buildItems(
|
||||
// SwitchX is a VFX/background-change model with its own page; keep it
|
||||
// out of the general video selector.
|
||||
sortedBySelectorName(videoModels.filter((m) => m.id !== "switch_x")),
|
||||
filmIcon,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Backward-compatible static constants (computed from the frontend overlay).
|
||||
// Prefer the `use*PageModelList` hooks in React components so the dropdowns
|
||||
// reflect the backend-reconciled model set.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const TEXT_TO_IMAGE_PAGE_MODEL_LIST: ModelList =
|
||||
buildTextToImagePageList(IMAGE_MODELS);
|
||||
|
||||
export const CANVAS_2D_PAGE_MODEL_LIST: ModelList =
|
||||
buildItems(
|
||||
(function (): Model[] {
|
||||
const set: Set<Model> = new Set();
|
||||
IMAGE_MODELS
|
||||
.filter((m) => m.canEditImages || m.tags?.includes(ModelTag.InstructiveEdit))
|
||||
.forEach((m) => set.add(m));
|
||||
const list = Array.from(set);
|
||||
list.sort((a, b) => a.selectorName?.localeCompare(b.selectorName));
|
||||
return list;
|
||||
})(),
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
);
|
||||
buildCanvas2dPageList(IMAGE_MODELS);
|
||||
|
||||
export const STAGE_3D_PAGE_MODEL_LIST: ModelList =
|
||||
buildItems(
|
||||
(function (): Model[] {
|
||||
const set: Set<Model> = new Set();
|
||||
IMAGE_MODELS
|
||||
.filter((m) => m.tags?.includes(ModelTag.InstructiveEdit))
|
||||
.forEach((m) => set.add(m));
|
||||
const list = Array.from(set);
|
||||
list.sort((a, b) => a.selectorName?.localeCompare(b.selectorName));
|
||||
return list;
|
||||
})(),
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
);
|
||||
buildStage3dPageList(IMAGE_MODELS);
|
||||
|
||||
export const IMAGE_EDITOR_PAGE_MODEL_LIST: ModelList =
|
||||
buildItems(
|
||||
//[
|
||||
// ALL_MODELS_BY_ID.get("flux_pro_inpaint")!,
|
||||
// ALL_MODELS_BY_ID.get("flux_dev_juggernaut_inpaint")!,
|
||||
// ALL_MODELS_BY_ID.get("flux_pro_kontext_max")!,
|
||||
//],
|
||||
(function (): Model[] {
|
||||
const set: Set<Model> = new Set();
|
||||
//set.add(IMAGE_MODELS_BY_ID.get("gpt_image_1")!); // Place gpt_image_1 first.
|
||||
IMAGE_MODELS
|
||||
.filter((m) => m.canEditImages)
|
||||
.forEach((m) => set.add(m));
|
||||
const list = Array.from(set);
|
||||
list.sort((a, b) => a.selectorName?.localeCompare(b.selectorName));
|
||||
return list;
|
||||
})(),
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
);
|
||||
buildImageEditorPageList(IMAGE_MODELS);
|
||||
|
||||
export const IMAGE_TO_VIDEO_PAGE_MODEL_LIST: ModelList =
|
||||
buildItems(
|
||||
(function (): Model[] {
|
||||
const set: Set<Model> = new Set();
|
||||
// SwitchX is a VFX/background-change model with its own page; keep it
|
||||
// out of the general video selector.
|
||||
VIDEO_MODELS.filter((m) => m.id !== "switch_x").forEach((m) =>
|
||||
set.add(m),
|
||||
);
|
||||
const list = Array.from(set);
|
||||
list.sort((a, b) => a.selectorName?.localeCompare(b.selectorName));
|
||||
return list;
|
||||
})(),
|
||||
<FontAwesomeIcon icon={faFilm} className="h-4 w-4" />
|
||||
);
|
||||
buildImageToVideoPageList(VIDEO_MODELS);
|
||||
|
||||
export const ANGLES_PAGE_MODEL_LIST: ModelList =
|
||||
buildItems(
|
||||
(function (): Model[] {
|
||||
const set: Set<Model> = new Set();
|
||||
IMAGE_MODELS
|
||||
.filter((m) => m.canEditAngles)
|
||||
.forEach((m) => set.add(m));
|
||||
const list = Array.from(set);
|
||||
list.sort((a, b) => a.selectorName?.localeCompare(b.selectorName));
|
||||
return list;
|
||||
})(),
|
||||
<FontAwesomeIcon icon={faImage} className="h-4 w-4" />
|
||||
);
|
||||
buildAnglesPageList(IMAGE_MODELS);
|
||||
|
||||
export const IMAGE_TO_3D_WORLD_PAGE_MODEL_LIST: ModelList =
|
||||
buildItems(
|
||||
SPLAT_MODELS as Model[],
|
||||
<FontAwesomeIcon icon={faCube} className="h-4 w-4" />
|
||||
);
|
||||
export const IMAGE_TO_3D_WORLD_PAGE_MODEL_LIST: ModelList = buildItems(
|
||||
SPLAT_MODELS as Model[],
|
||||
cubeIcon,
|
||||
);
|
||||
|
||||
export const IMAGE_TO_3D_OBJECT_PAGE_MODEL_LIST: ModelList =
|
||||
buildItems(
|
||||
OBJECT_3D_MODELS as Model[],
|
||||
<FontAwesomeIcon icon={faCube} className="h-4 w-4" />
|
||||
);
|
||||
export const IMAGE_TO_3D_OBJECT_PAGE_MODEL_LIST: ModelList = buildItems(
|
||||
OBJECT_3D_MODELS as Model[],
|
||||
cubeIcon,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Live, backend-reconciled hooks. Use these in React components; they fall back
|
||||
// to the overlay until `loadModelsFromBackend()` completes at app startup.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const useTextToImagePageModelList = (): ModelList => {
|
||||
const imageModels = useImageModels();
|
||||
return useMemo(() => buildTextToImagePageList(imageModels), [imageModels]);
|
||||
};
|
||||
|
||||
export const useCanvas2dPageModelList = (): ModelList => {
|
||||
const imageModels = useImageModels();
|
||||
return useMemo(() => buildCanvas2dPageList(imageModels), [imageModels]);
|
||||
};
|
||||
|
||||
export const useStage3dPageModelList = (): ModelList => {
|
||||
const imageModels = useImageModels();
|
||||
return useMemo(() => buildStage3dPageList(imageModels), [imageModels]);
|
||||
};
|
||||
|
||||
export const useImageEditorPageModelList = (): ModelList => {
|
||||
const imageModels = useImageModels();
|
||||
return useMemo(() => buildImageEditorPageList(imageModels), [imageModels]);
|
||||
};
|
||||
|
||||
export const useAnglesPageModelList = (): ModelList => {
|
||||
const imageModels = useImageModels();
|
||||
return useMemo(() => buildAnglesPageList(imageModels), [imageModels]);
|
||||
};
|
||||
|
||||
export const useImageToVideoPageModelList = (): ModelList => {
|
||||
const videoModels = useVideoModels();
|
||||
return useMemo(() => buildImageToVideoPageList(videoModels), [videoModels]);
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ContextMenuContainer } from "./components/ui/ContextMenu";
|
||||
import InpaintToolBar from "./components/ui/InpaintToolBar";
|
||||
import { ImageModel } from "@storyteller/model-list";
|
||||
import {
|
||||
CANVAS_2D_PAGE_MODEL_LIST,
|
||||
useCanvas2dPageModelList,
|
||||
ClassyModelSelector,
|
||||
ModelPage,
|
||||
useSelectedImageModel,
|
||||
@@ -293,6 +293,7 @@ const PageDraw = ({
|
||||
fillParentHeight = false,
|
||||
showBottomRightControls = true,
|
||||
}: PageDrawProps) => {
|
||||
const canvas2dModelList = useCanvas2dPageModelList();
|
||||
const canvasWidth = useRef<number>(1024);
|
||||
const canvasHeight = useRef<number>(1024);
|
||||
const [isSelecting, setIsSelecting] = useState<boolean>(false);
|
||||
@@ -1215,7 +1216,7 @@ const PageDraw = ({
|
||||
modelSelector={
|
||||
<ClassyModelSelector
|
||||
variant="embedded"
|
||||
items={CANVAS_2D_PAGE_MODEL_LIST}
|
||||
items={canvas2dModelList}
|
||||
page={PAGE_ID}
|
||||
showProviderSelection={false}
|
||||
/>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
removeImageDropListener,
|
||||
} from "@storyteller/ui-gallery-modal";
|
||||
import {
|
||||
STAGE_3D_PAGE_MODEL_LIST,
|
||||
useStage3dPageModelList,
|
||||
ModelPage,
|
||||
defaultModelForPage,
|
||||
useClassyModelSelectorStore,
|
||||
@@ -109,6 +109,7 @@ export const Stage3DBody = ({
|
||||
topBarStartSlot,
|
||||
topBarEndSlot,
|
||||
}: Stage3DBodyProps = {}) => {
|
||||
const stage3dModelList = useStage3dPageModelList();
|
||||
const camAspect = usePageSceneStore((s) => s.cameraAspectRatio);
|
||||
const outlinerShowing = usePageSceneStore((s) => s.outlinerShowing);
|
||||
const editorLoader = usePageSceneStore((s) => s.editorLoader);
|
||||
@@ -165,20 +166,25 @@ export const Stage3DBody = ({
|
||||
useEffect(() => {
|
||||
if (modelSelectorPlacement !== "prompt-box") return;
|
||||
if (selectedImageModel) return;
|
||||
const models = STAGE_3D_PAGE_MODEL_LIST.map((i) => i.model).filter(
|
||||
const models = stage3dModelList.map((i) => i.model).filter(
|
||||
(m): m is NonNullable<typeof m> => m !== undefined,
|
||||
);
|
||||
const def = defaultModelForPage(models, PAGE_ID);
|
||||
if (def) setSelectedModel(PAGE_ID, def);
|
||||
}, [modelSelectorPlacement, selectedImageModel, setSelectedModel]);
|
||||
}, [
|
||||
modelSelectorPlacement,
|
||||
selectedImageModel,
|
||||
setSelectedModel,
|
||||
stage3dModelList,
|
||||
]);
|
||||
|
||||
const inlineModelItems: PopoverItem[] = useMemo(
|
||||
() =>
|
||||
STAGE_3D_PAGE_MODEL_LIST.map((item) => ({
|
||||
stage3dModelList.map((item) => ({
|
||||
...item,
|
||||
selected: item.model === selectedImageModel,
|
||||
})),
|
||||
[selectedImageModel],
|
||||
[selectedImageModel, stage3dModelList],
|
||||
);
|
||||
const handleInlineModelSelect = useCallback(
|
||||
(item: PopoverItem) => {
|
||||
@@ -188,9 +194,9 @@ export const Stage3DBody = ({
|
||||
);
|
||||
const selectedModelIcon = useMemo(
|
||||
() =>
|
||||
STAGE_3D_PAGE_MODEL_LIST.find((i) => i.model === selectedImageModel)
|
||||
stage3dModelList.find((i) => i.model === selectedImageModel)
|
||||
?.icon,
|
||||
[selectedImageModel],
|
||||
[selectedImageModel, stage3dModelList],
|
||||
);
|
||||
const inlineModelSelector =
|
||||
modelSelectorPlacement === "prompt-box" ? (
|
||||
@@ -536,7 +542,7 @@ export const Stage3DBody = ({
|
||||
{modelSelectorPlacement === "bottom-left" && (
|
||||
<div className="absolute bottom-6 left-6 z-20 flex items-center gap-3">
|
||||
<ClassyModelSelector
|
||||
items={STAGE_3D_PAGE_MODEL_LIST}
|
||||
items={stage3dModelList}
|
||||
page={PAGE_ID}
|
||||
panelTitle="Select Model"
|
||||
panelClassName="min-w-[300px]"
|
||||
|
||||
@@ -1,41 +1,26 @@
|
||||
import { CommonImageModel } from "@storyteller/api-enums";
|
||||
|
||||
// TODO(bt): This shouldn't exist. We need to standardize types throughout the frontend.
|
||||
// MIGRATION (2026-07): model `tauriId`s are now the storyteller-web omni
|
||||
// identifiers, which are exactly the CommonImageModel values — so the common
|
||||
// case is a direct pass-through. The switch below only remains to translate
|
||||
// LEGACY tauri ids (old bookmarks/state); do not add new cases.
|
||||
|
||||
const COMMON_IMAGE_MODEL_VALUES: Set<string> = new Set(
|
||||
Object.values(CommonImageModel),
|
||||
);
|
||||
|
||||
export function imageModelToCommonImageModel(
|
||||
tauriId: string,
|
||||
): CommonImageModel | null {
|
||||
if (COMMON_IMAGE_MODEL_VALUES.has(tauriId)) {
|
||||
return tauriId as CommonImageModel;
|
||||
}
|
||||
|
||||
switch (tauriId) {
|
||||
case "flux_1_dev":
|
||||
return CommonImageModel.Flux1Dev;
|
||||
case "flux_1_schnell":
|
||||
return CommonImageModel.Flux1Schnell;
|
||||
case "flux_pro_11":
|
||||
return CommonImageModel.FluxPro11;
|
||||
case "flux_pro_11_ultra":
|
||||
return CommonImageModel.FluxPro11Ultra;
|
||||
case "gpt_image_1":
|
||||
return CommonImageModel.GptImage1;
|
||||
case "gpt_image_1p5":
|
||||
return CommonImageModel.GptImage1p5;
|
||||
case "gpt_image_2":
|
||||
return CommonImageModel.GptImage2;
|
||||
case "nano_banana":
|
||||
return CommonImageModel.NanaBanana;
|
||||
case "nano_banana_2":
|
||||
return CommonImageModel.NanaBanana2;
|
||||
case "nano_banana_pro":
|
||||
return CommonImageModel.NanaBananaPro;
|
||||
case "seedream_4":
|
||||
return CommonImageModel.Seedream4;
|
||||
case "seedream_4p5":
|
||||
return CommonImageModel.Seedream4p5;
|
||||
case "seedream_5_lite":
|
||||
return CommonImageModel.Seedream5Lite;
|
||||
case "qwen_edit_2511_angles":
|
||||
return CommonImageModel.QwenEdit2511Angles;
|
||||
case "flux_2_lora_angles":
|
||||
return CommonImageModel.Flux2LoraAngles;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,49 +1,34 @@
|
||||
import { CommonVideoModel } from "@storyteller/api-enums";
|
||||
|
||||
// TODO(bt): This shouldn't exist. We need to standardize types throughout the frontend.
|
||||
// MIGRATION (2026-07): model `tauriId`s are now the storyteller-web omni
|
||||
// identifiers, which are exactly the CommonVideoModel values — so the common
|
||||
// case is a direct pass-through. The switch below only remains to translate
|
||||
// LEGACY tauri ids (old bookmarks/state); do not add new cases.
|
||||
|
||||
const COMMON_VIDEO_MODEL_VALUES: Set<string> = new Set(
|
||||
Object.values(CommonVideoModel),
|
||||
);
|
||||
|
||||
export function videoModelToCommonVideoModel(
|
||||
tauriId: string,
|
||||
): CommonVideoModel | null {
|
||||
if (COMMON_VIDEO_MODEL_VALUES.has(tauriId)) {
|
||||
return tauriId as CommonVideoModel;
|
||||
}
|
||||
|
||||
switch (tauriId) {
|
||||
case "grok_video":
|
||||
return CommonVideoModel.GrokVideo;
|
||||
case "kling_1.6_pro":
|
||||
return CommonVideoModel.Kling16Pro;
|
||||
case "kling_2.1_pro":
|
||||
return CommonVideoModel.Kling21Pro;
|
||||
case "kling_2.1_master":
|
||||
return CommonVideoModel.Kling21Master;
|
||||
case "kling_2p5_turbo_pro":
|
||||
return CommonVideoModel.Kling2p5TurboPro;
|
||||
case "kling_2p6_pro":
|
||||
return CommonVideoModel.Kling2p6Pro;
|
||||
case "kling_3p0_standard":
|
||||
return CommonVideoModel.Kling3p0Standard;
|
||||
case "kling_3p0_pro":
|
||||
return CommonVideoModel.Kling3p0Pro;
|
||||
case "seedance_1.0_lite":
|
||||
return CommonVideoModel.Seedance10Lite;
|
||||
case "seedance_1p5_pro":
|
||||
return CommonVideoModel.Seedance1p5Pro;
|
||||
case "seedance_2p0":
|
||||
return CommonVideoModel.Seedance2p0;
|
||||
case "seedance_2p0_fast":
|
||||
return CommonVideoModel.Seedance2p0Fast;
|
||||
case "sora_2":
|
||||
return CommonVideoModel.Sora2;
|
||||
case "sora_2_pro":
|
||||
return CommonVideoModel.Sora2Pro;
|
||||
case "veo_2":
|
||||
return CommonVideoModel.Veo2;
|
||||
case "veo_3":
|
||||
return CommonVideoModel.Veo3;
|
||||
case "veo_3_fast":
|
||||
return CommonVideoModel.Veo3Fast;
|
||||
case "veo_3p1":
|
||||
return CommonVideoModel.Veo3p1;
|
||||
case "veo_3p1_fast":
|
||||
return CommonVideoModel.Veo3p1Fast;
|
||||
case "grok_imagine_video":
|
||||
// The frontend id for the generic Grok video experience; the (stale)
|
||||
// CommonVideoModel enum still spells it "grok_video".
|
||||
return CommonVideoModel.GrokVideo;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ import {
|
||||
useSelectedModel,
|
||||
useSelectedProviderForModel,
|
||||
defaultModelForPage,
|
||||
TEXT_TO_IMAGE_PAGE_MODEL_LIST,
|
||||
IMAGE_TO_VIDEO_PAGE_MODEL_LIST,
|
||||
CANVAS_2D_PAGE_MODEL_LIST,
|
||||
STAGE_3D_PAGE_MODEL_LIST,
|
||||
IMAGE_EDITOR_PAGE_MODEL_LIST,
|
||||
useTextToImagePageModelList,
|
||||
useImageToVideoPageModelList,
|
||||
useCanvas2dPageModelList,
|
||||
useStage3dPageModelList,
|
||||
useImageEditorPageModelList,
|
||||
useAnglesPageModelList,
|
||||
IMAGE_TO_3D_WORLD_PAGE_MODEL_LIST,
|
||||
ANGLES_PAGE_MODEL_LIST,
|
||||
} from "@storyteller/ui-model-selector";
|
||||
import {
|
||||
usePrompt2DStore,
|
||||
@@ -49,41 +49,11 @@ const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
||||
worldlabs: "World Labs",
|
||||
};
|
||||
|
||||
// Get models list for a page
|
||||
const getModelsForPage = (page: ModelPage | null): Model[] => {
|
||||
switch (page) {
|
||||
case ModelPage.TextToImage:
|
||||
return TEXT_TO_IMAGE_PAGE_MODEL_LIST.map((item) => item.model).filter(
|
||||
(m): m is Model => m !== undefined,
|
||||
);
|
||||
case ModelPage.ImageToVideo:
|
||||
return IMAGE_TO_VIDEO_PAGE_MODEL_LIST.map((item) => item.model).filter(
|
||||
(m): m is Model => m !== undefined,
|
||||
);
|
||||
case ModelPage.Canvas2D:
|
||||
return CANVAS_2D_PAGE_MODEL_LIST.map((item) => item.model).filter(
|
||||
(m): m is Model => m !== undefined,
|
||||
);
|
||||
case ModelPage.Stage3D:
|
||||
return STAGE_3D_PAGE_MODEL_LIST.map((item) => item.model).filter(
|
||||
(m): m is Model => m !== undefined,
|
||||
);
|
||||
case ModelPage.ImageEditor:
|
||||
return IMAGE_EDITOR_PAGE_MODEL_LIST.map((item) => item.model).filter(
|
||||
(m): m is Model => m !== undefined,
|
||||
);
|
||||
case ModelPage.ImageTo3DWorld:
|
||||
return IMAGE_TO_3D_WORLD_PAGE_MODEL_LIST.map((item) => item.model).filter(
|
||||
(m): m is Model => m !== undefined,
|
||||
);
|
||||
case ModelPage.Angles:
|
||||
return ANGLES_PAGE_MODEL_LIST.map((item) => item.model).filter(
|
||||
(m): m is Model => m !== undefined,
|
||||
);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
// Extract the concrete `Model` instances out of a page's selectable item list.
|
||||
const modelsFromList = (
|
||||
list: { model?: Model }[],
|
||||
): Model[] =>
|
||||
list.map((item) => item.model).filter((m): m is Model => m !== undefined);
|
||||
|
||||
export interface CostBreakdownModalProps {
|
||||
/** The current active tab ID from the app (e.g. "IMAGE", "VIDEO", "2D", "3D") */
|
||||
@@ -106,11 +76,47 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) {
|
||||
return TAB_TO_MODEL_PAGE[activeTabId] ?? ModelPage.TextToImage;
|
||||
}, [activeTabId]);
|
||||
|
||||
// Backend-reconciled model lists per page (fall back to the overlay until the
|
||||
// startup fetch completes).
|
||||
const textToImageList = useTextToImagePageModelList();
|
||||
const imageToVideoList = useImageToVideoPageModelList();
|
||||
const canvas2dList = useCanvas2dPageModelList();
|
||||
const stage3dList = useStage3dPageModelList();
|
||||
const imageEditorList = useImageEditorPageModelList();
|
||||
const anglesList = useAnglesPageModelList();
|
||||
|
||||
// Get the selected model for the active page
|
||||
const selectedModelFromStore = useSelectedModel(activePage);
|
||||
|
||||
// If no model selected in store, use the default for this page
|
||||
const modelsForPage = getModelsForPage(activePage);
|
||||
const modelsForPage = useMemo<Model[]>(() => {
|
||||
switch (activePage) {
|
||||
case ModelPage.TextToImage:
|
||||
return modelsFromList(textToImageList);
|
||||
case ModelPage.ImageToVideo:
|
||||
return modelsFromList(imageToVideoList);
|
||||
case ModelPage.Canvas2D:
|
||||
return modelsFromList(canvas2dList);
|
||||
case ModelPage.Stage3D:
|
||||
return modelsFromList(stage3dList);
|
||||
case ModelPage.ImageEditor:
|
||||
return modelsFromList(imageEditorList);
|
||||
case ModelPage.ImageTo3DWorld:
|
||||
return modelsFromList(IMAGE_TO_3D_WORLD_PAGE_MODEL_LIST);
|
||||
case ModelPage.Angles:
|
||||
return modelsFromList(anglesList);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}, [
|
||||
activePage,
|
||||
textToImageList,
|
||||
imageToVideoList,
|
||||
canvas2dList,
|
||||
stage3dList,
|
||||
imageEditorList,
|
||||
anglesList,
|
||||
]);
|
||||
const selectedModel =
|
||||
selectedModelFromStore ?? defaultModelForPage(modelsForPage, activePage);
|
||||
|
||||
@@ -273,8 +279,10 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) {
|
||||
const MODELS_WITH_COST_DATA = new Set([
|
||||
"flux_1_dev",
|
||||
"flux_1_schnell",
|
||||
"flux_pro_11",
|
||||
"flux_pro_11_ultra",
|
||||
"flux_pro_11", // Legacy id
|
||||
"flux_pro_1p1",
|
||||
"flux_pro_11_ultra", // Legacy id
|
||||
"flux_pro_1p1_ultra",
|
||||
"gpt_image_1p5",
|
||||
"gpt_image_2",
|
||||
"nano_banana",
|
||||
|
||||
@@ -826,7 +826,8 @@ export const PromptBoxVideo = ({
|
||||
}
|
||||
|
||||
switch (selectedModel?.tauriId) {
|
||||
case "grok_video":
|
||||
case "grok_video": // Legacy id
|
||||
case "grok_imagine_video":
|
||||
request.grok_aspect_ratio = getGrokAspectRatio();
|
||||
break;
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ export * from "./lib/classes/SplatModel.js";
|
||||
export * from "./lib/lists/SplatModels.js";
|
||||
export * from "./lib/classes/Object3DModel.js";
|
||||
export * from "./lib/lists/Object3DModels.js";
|
||||
|
||||
|
||||
export * from "./lib/loader/modelReconciliation.js";
|
||||
export * from "./lib/loader/buildModelsFromListing.js";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CommonQuality } from "../classes/properties/CommonQuality.js";
|
||||
export const IMAGE_MODELS: ImageModel[] = [
|
||||
new ImageModel({
|
||||
id: "grok_image",
|
||||
tauriId: "grok_image",
|
||||
tauriId: "grok_imagine_image",
|
||||
fullName: "Grok",
|
||||
category: "image",
|
||||
creator: ModelCreator.Grok,
|
||||
@@ -58,7 +58,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
}),
|
||||
new ImageModel({
|
||||
id: "flux_pro_1_1_ultra",
|
||||
tauriId: "flux_pro_11_ultra",
|
||||
tauriId: "flux_pro_1p1_ultra",
|
||||
fullName: "Flux Pro 1.1 Ultra",
|
||||
category: "image",
|
||||
creator: ModelCreator.BlackForestLabs,
|
||||
@@ -87,7 +87,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
}),
|
||||
new ImageModel({
|
||||
id: "flux_pro_1_1",
|
||||
tauriId: "flux_pro_11",
|
||||
tauriId: "flux_pro_1p1",
|
||||
fullName: "Flux Pro 1.1",
|
||||
category: "image",
|
||||
creator: ModelCreator.BlackForestLabs,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SizeIconOption } from "../classes/metadata/SizeOption.js";
|
||||
export const VIDEO_MODELS: VideoModel[] = [
|
||||
new VideoModel({
|
||||
id: "grok_video",
|
||||
tauriId: "grok_video",
|
||||
tauriId: "grok_imagine_video",
|
||||
fullName: "Grok Video",
|
||||
category: "video",
|
||||
creator: ModelCreator.Grok,
|
||||
@@ -43,7 +43,9 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
fullName: "Grok Imagine 1.5",
|
||||
category: "video",
|
||||
creator: ModelCreator.Grok,
|
||||
providers: [GenerationProvider.Grok],
|
||||
// Served via Artcraft (storyteller-web / artcraft_router) for now, even
|
||||
// though the model is made by xAI. See generate_video_command.rs.
|
||||
providers: [GenerationProvider.Artcraft],
|
||||
selectorName: "Grok Imagine 1.5",
|
||||
selectorDescription: "Image-to-video by xAI",
|
||||
selectorBadges: ["Preview"],
|
||||
@@ -75,7 +77,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "kling_1_6_pro",
|
||||
tauriId: "kling_1.6_pro",
|
||||
tauriId: "kling_1p6_pro",
|
||||
fullName: "Kling 1.6 Pro",
|
||||
category: "video",
|
||||
creator: ModelCreator.Kling,
|
||||
@@ -91,7 +93,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "kling_2_1_pro",
|
||||
tauriId: "kling_2.1_pro",
|
||||
tauriId: "kling_2p1_pro",
|
||||
fullName: "Kling 2.1 Pro",
|
||||
category: "video",
|
||||
creator: ModelCreator.Kling,
|
||||
@@ -107,7 +109,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "kling_2_1_master",
|
||||
tauriId: "kling_2.1_master",
|
||||
tauriId: "kling_2p1_master",
|
||||
fullName: "Kling 2.1 Master",
|
||||
category: "video",
|
||||
creator: ModelCreator.Kling,
|
||||
@@ -274,7 +276,7 @@ export const VIDEO_MODELS: VideoModel[] = [
|
||||
}),
|
||||
new VideoModel({
|
||||
id: "seedance_1_0_lite",
|
||||
tauriId: "seedance_1.0_lite",
|
||||
tauriId: "seedance_1p0_lite",
|
||||
fullName: "Seedance 1.0 Lite",
|
||||
category: "video",
|
||||
creator: ModelCreator.Bytedance,
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
// Builds the picker model lists FROM the backend omni listing (the Tauri
|
||||
// command response). Membership + order come from the backend, and every
|
||||
// capability the API expresses is mapped 1:1 — the static overlay lists
|
||||
// (`IMAGE_MODELS` / `VIDEO_MODELS`) only contribute what the API does not
|
||||
// return:
|
||||
// - presentation (selectorName/Description/Badges, tags, progressBarTime)
|
||||
// - page-subsetting flags (canTextToImage, canEditImages, canEditAngles, …)
|
||||
// - desktop-native provider knowledge (`providers`)
|
||||
// - models the backend registry doesn't know (switch_x, inpaint models, …)
|
||||
//
|
||||
// API capability fields ALWAYS win over the overlay's copies, so the overlay's
|
||||
// capability data is dead at runtime for backend-known models and can be
|
||||
// deleted from the lists.
|
||||
//
|
||||
// Pure module (no tauri-api import). The store in `@storyteller/tauri-api`
|
||||
// feeds it the command payload's `models[]`; the DTO is accepted structurally.
|
||||
|
||||
import { ImageModel } from "../classes/ImageModel.js";
|
||||
import { VideoModel } from "../classes/VideoModel.js";
|
||||
import { ModelCreator } from "../classes/metadata/ModelCreator.js";
|
||||
import { SizeIconOption, SizeOption } from "../classes/metadata/SizeOption.js";
|
||||
import { CommonAspectRatio } from "../classes/properties/CommonAspectRatio.js";
|
||||
import { CommonResolution } from "../classes/properties/CommonResolution.js";
|
||||
import { CommonQuality } from "../classes/properties/CommonQuality.js";
|
||||
import {
|
||||
BACKEND_TO_TAURI_IMAGE_ID,
|
||||
BACKEND_TO_TAURI_VIDEO_ID,
|
||||
modelCreatorFromBackend,
|
||||
} from "./modelReconciliation.js";
|
||||
|
||||
// ── Structural shapes of the backend listing (subset we read) ──────────────
|
||||
|
||||
export interface ListingModelBase {
|
||||
model: string;
|
||||
model_creator?: string | null;
|
||||
full_name?: string | null;
|
||||
text_prompt_supported?: boolean | null;
|
||||
text_prompt_max_length?: number | null;
|
||||
batch_size_max?: number | null;
|
||||
batch_size_default?: number | null;
|
||||
batch_size_options?: number[] | null;
|
||||
aspect_ratio_options?: string[] | null;
|
||||
aspect_ratio_default?: string | null;
|
||||
resolution_options?: string[] | null;
|
||||
resolution_default?: string | null;
|
||||
quality_options?: string[] | null;
|
||||
default_quality?: string | null;
|
||||
is_disabled?: boolean | null;
|
||||
}
|
||||
|
||||
export interface ListingImageModel extends ListingModelBase {
|
||||
image_refs_supported?: boolean | null;
|
||||
image_refs_max?: number | null;
|
||||
}
|
||||
|
||||
export interface ListingVideoModel extends ListingModelBase {
|
||||
extra_info?: string | null;
|
||||
extra_info_short?: string | null;
|
||||
text_to_video_supported?: boolean | null;
|
||||
starting_keyframe_supported?: boolean | null;
|
||||
starting_keyframe_required?: boolean | null;
|
||||
ending_keyframe_supported?: boolean | null;
|
||||
image_references_supported?: boolean | null;
|
||||
image_references_max?: number | null;
|
||||
video_references_max?: number | null;
|
||||
video_references_max_total_duration_seconds?: number | null;
|
||||
audio_references_max?: number | null;
|
||||
audio_references_max_total_duration_seconds?: number | null;
|
||||
show_generate_with_sound_toggle?: boolean | null;
|
||||
duration_seconds_options?: number[] | null;
|
||||
duration_seconds_default?: number | null;
|
||||
}
|
||||
|
||||
// ── Public builders ────────────────────────────────────────────────────────
|
||||
|
||||
export const buildImageModelsFromListing = (
|
||||
overlay: ImageModel[],
|
||||
listing: ListingImageModel[],
|
||||
offeredModelIds: string[] = [],
|
||||
): ImageModel[] =>
|
||||
build(overlay, listing, offeredModelIds, BACKEND_TO_TAURI_IMAGE_ID, mergedImageModel);
|
||||
|
||||
export const buildVideoModelsFromListing = (
|
||||
overlay: VideoModel[],
|
||||
listing: ListingVideoModel[],
|
||||
offeredModelIds: string[] = [],
|
||||
): VideoModel[] =>
|
||||
build(overlay, listing, offeredModelIds, BACKEND_TO_TAURI_VIDEO_ID, mergedVideoModel);
|
||||
|
||||
// ── Core assembly ──────────────────────────────────────────────────────────
|
||||
|
||||
// Membership: a model appears in the picker when it is
|
||||
// 1. OFFERED by a provider in the response's `providers[]` (the backend's
|
||||
// publish switch — `models[]` details also contain disabled entries and
|
||||
// internal variants that must NOT surface), or
|
||||
// 2. an ENABLED `models[]` detail that the frontend knows (overlay entry) —
|
||||
// covers desktop-native models like grok video that no backend provider
|
||||
// offers yet, or
|
||||
// 3. a frontend-only overlay model the backend has never heard of
|
||||
// (switch_x, inpaint models, …).
|
||||
// `models[]` details are the capability source either way (the enabled entry
|
||||
// wins when a model is detailed twice).
|
||||
const build = <T extends { tauriId: string }, L extends ListingModelBase>(
|
||||
overlay: T[],
|
||||
listing: L[],
|
||||
offeredModelIds: string[],
|
||||
alias: Record<string, string>,
|
||||
merge: (m: L, tauriId: string, overlayEntry: T | undefined) => T,
|
||||
): T[] => {
|
||||
const toTauriId = (backendId: string) => alias[backendId] ?? backendId;
|
||||
const overlayByTauriId = new Map(overlay.map((m) => [m.tauriId, m]));
|
||||
const knownTauriIds = new Set(listing.map((m) => toTauriId(m.model)));
|
||||
const offeredTauriIds = new Set(offeredModelIds.map(toTauriId));
|
||||
|
||||
// Capability lookup — prefer the enabled detail entry when duplicated.
|
||||
const detailsByTauriId = new Map<string, L>();
|
||||
for (const m of listing) {
|
||||
const tauriId = toTauriId(m.model);
|
||||
const existing = detailsByTauriId.get(tauriId);
|
||||
if (existing === undefined || existing.is_disabled === true) {
|
||||
detailsByTauriId.set(tauriId, m);
|
||||
}
|
||||
}
|
||||
|
||||
const result: T[] = [];
|
||||
const seenTauriIds = new Set<string>();
|
||||
// Backend details order drives ordering.
|
||||
for (const m of listing) {
|
||||
const tauriId = toTauriId(m.model);
|
||||
if (seenTauriIds.has(tauriId)) continue;
|
||||
|
||||
const overlayEntry = overlayByTauriId.get(tauriId);
|
||||
const detail = detailsByTauriId.get(tauriId) ?? m;
|
||||
const enabled = detail.is_disabled !== true;
|
||||
const show =
|
||||
offeredTauriIds.has(tauriId) || (enabled && overlayEntry !== undefined);
|
||||
if (!show) continue;
|
||||
|
||||
seenTauriIds.add(tauriId);
|
||||
result.push(merge(detail, tauriId, overlayEntry));
|
||||
}
|
||||
// Offered models with no detail entry at all (unusual, but the publish
|
||||
// switch wins): surface minimally.
|
||||
for (const backendId of offeredModelIds) {
|
||||
const tauriId = toTauriId(backendId);
|
||||
if (seenTauriIds.has(tauriId)) continue;
|
||||
seenTauriIds.add(tauriId);
|
||||
result.push(
|
||||
merge({ model: backendId } as L, tauriId, overlayByTauriId.get(tauriId)),
|
||||
);
|
||||
}
|
||||
// Append frontend-only models the backend has never heard of.
|
||||
for (const m of overlay) {
|
||||
if (!knownTauriIds.has(m.tauriId) && !seenTauriIds.has(m.tauriId)) {
|
||||
result.push(m);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// ── 1:1 merge constructors (API capability > overlay; presentation ← overlay)
|
||||
|
||||
const mergedImageModel = (
|
||||
m: ListingImageModel,
|
||||
tauriId: string,
|
||||
o: ImageModel | undefined,
|
||||
): ImageModel => {
|
||||
const aspectRatios = knownValues(m.aspect_ratio_options, COMMON_ASPECT_RATIO_VALUES);
|
||||
const resolutions = knownValues(m.resolution_options, COMMON_RESOLUTION_VALUES);
|
||||
const qualityOptions = knownValues(m.quality_options, COMMON_QUALITY_VALUES);
|
||||
const fullName = m.full_name ?? o?.fullName ?? m.model;
|
||||
|
||||
return new ImageModel({
|
||||
// Identity. Keep the overlay's `id` (BY_ID lookups / history) when known.
|
||||
id: o?.id ?? tauriId,
|
||||
tauriId,
|
||||
fullName,
|
||||
category: "image",
|
||||
creator: creatorFor(m.model_creator, m.model, o?.creator),
|
||||
|
||||
// Presentation — the omni image API has no display fields yet.
|
||||
selectorName: o?.selectorName ?? fullName,
|
||||
selectorDescription: o?.selectorDescription ?? "",
|
||||
selectorBadges: o?.selectorBadges ?? [],
|
||||
tags: o?.tags ?? [],
|
||||
progressBarTime: o?.progressBarTime,
|
||||
|
||||
// Page-subsetting flags — frontend-owned concepts.
|
||||
canTextToImage: o ? o.canTextToImage : m.text_prompt_supported !== false,
|
||||
canEditImages: o?.canEditImages ?? false,
|
||||
usesInpaintingMask: o?.usesInpaintingMask ?? false,
|
||||
editingIsInpainting: o?.editingIsInpainting ?? false,
|
||||
canEditAngles: o?.canEditAngles ?? false,
|
||||
|
||||
// Desktop-native provider knowledge.
|
||||
providers: o?.getProviders(),
|
||||
|
||||
// Capabilities — served by the API, overlay only as a transitional
|
||||
// fallback where the backend config leaves a field unset.
|
||||
maxPromptLength: m.text_prompt_max_length ?? o?.maxPromptLength,
|
||||
maxGenerationCount: m.batch_size_max ?? o?.maxGenerationCount ?? 4,
|
||||
defaultGenerationCount: m.batch_size_default ?? o?.defaultGenerationCount ?? 1,
|
||||
predefinedGenerationCounts: m.batch_size_options ?? o?.predefinedGenerationCounts,
|
||||
canUseImagePrompt: m.image_refs_supported ?? o?.canUseImagePrompt ?? false,
|
||||
maxImagePromptCount: m.image_refs_max ?? o?.maxImagePromptCount ?? 1,
|
||||
canChangeAspectRatio:
|
||||
aspectRatios.length > 0 || (o?.canChangeAspectRatio ?? false),
|
||||
aspectRatios:
|
||||
aspectRatios.length > 0 ? (aspectRatios as CommonAspectRatio[]) : o?.aspectRatios,
|
||||
defaultAspectRatio:
|
||||
(knownValue(m.aspect_ratio_default, COMMON_ASPECT_RATIO_VALUES) as
|
||||
| CommonAspectRatio
|
||||
| undefined) ?? o?.defaultAspectRatio,
|
||||
canChangeResolution:
|
||||
resolutions.length > 0 || (o?.canChangeResolution ?? false),
|
||||
resolutions:
|
||||
resolutions.length > 0 ? (resolutions as CommonResolution[]) : o?.resolutions,
|
||||
defaultResolution:
|
||||
(knownValue(m.resolution_default, COMMON_RESOLUTION_VALUES) as
|
||||
| CommonResolution
|
||||
| undefined) ?? o?.defaultResolution,
|
||||
qualityOptions:
|
||||
qualityOptions.length > 0 ? (qualityOptions as CommonQuality[]) : o?.qualityOptions,
|
||||
defaultQuality:
|
||||
(knownValue(m.default_quality, COMMON_QUALITY_VALUES) as
|
||||
| CommonQuality
|
||||
| undefined) ?? o?.defaultQuality,
|
||||
});
|
||||
};
|
||||
|
||||
const mergedVideoModel = (
|
||||
m: ListingVideoModel,
|
||||
tauriId: string,
|
||||
o: VideoModel | undefined,
|
||||
): VideoModel => {
|
||||
const aspectRatios = knownValues(m.aspect_ratio_options, COMMON_ASPECT_RATIO_VALUES);
|
||||
const fullName = m.full_name ?? o?.fullName ?? m.model;
|
||||
|
||||
return new VideoModel({
|
||||
// Identity. Keep the overlay's `id` (BY_ID lookups / history) when known.
|
||||
id: o?.id ?? tauriId,
|
||||
tauriId,
|
||||
fullName,
|
||||
category: "video",
|
||||
creator: creatorFor(m.model_creator, m.model, o?.creator),
|
||||
|
||||
// Presentation. The video API DOES have descriptions (extra_info_short /
|
||||
// extra_info) — prefer them so backend copy reaches the picker.
|
||||
selectorName: o?.selectorName ?? fullName,
|
||||
selectorDescription:
|
||||
m.extra_info_short ?? m.extra_info ?? o?.selectorDescription ?? "",
|
||||
selectorBadges: o?.selectorBadges ?? [],
|
||||
tags: o?.tags ?? [],
|
||||
progressBarTime: o?.progressBarTime,
|
||||
supportsSystemPrompt: o?.supportsSystemPrompt,
|
||||
|
||||
// Desktop-native provider knowledge.
|
||||
providers: o?.getProviders(),
|
||||
|
||||
// Capabilities — served by the API, overlay only as a transitional
|
||||
// fallback where the backend config leaves a field unset.
|
||||
maxPromptLength: m.text_prompt_max_length ?? o?.maxPromptLength,
|
||||
startFrame: m.starting_keyframe_supported ?? o?.startFrame ?? false,
|
||||
endFrame: m.ending_keyframe_supported ?? o?.endFrame ?? false,
|
||||
requiresImage: m.starting_keyframe_required ?? o?.requiresImage ?? false,
|
||||
textToVideoSupported: m.text_to_video_supported ?? o?.textToVideoSupported,
|
||||
generateWithSound: m.show_generate_with_sound_toggle ?? o?.generateWithSound,
|
||||
durationOptions: m.duration_seconds_options ?? o?.durationOptions,
|
||||
defaultDuration: m.duration_seconds_default ?? o?.defaultDuration,
|
||||
supportsReferenceMode: m.image_references_supported ?? o?.supportsReferenceMode,
|
||||
maxReferenceImages: m.image_references_max ?? o?.maxReferenceImages,
|
||||
maxReferenceVideos: m.video_references_max ?? o?.maxReferenceVideos,
|
||||
maxVideoRefDuration:
|
||||
m.video_references_max_total_duration_seconds ?? o?.maxVideoRefDuration,
|
||||
maxReferenceAudios: m.audio_references_max ?? o?.maxReferenceAudios,
|
||||
maxAudioRefDuration:
|
||||
m.audio_references_max_total_duration_seconds ?? o?.maxAudioRefDuration,
|
||||
resolutionOptions:
|
||||
m.resolution_options?.map(resolutionLabel) ?? o?.resolutionOptions,
|
||||
defaultResolution: m.resolution_default
|
||||
? resolutionLabel(m.resolution_default)
|
||||
: o?.defaultResolution,
|
||||
// Aspect handling: API aspect ratios drive the modern picker; legacy
|
||||
// models (grok/sora native size UI) keep their overlay sizeOptions.
|
||||
sizeOptions:
|
||||
aspectRatios.length > 0
|
||||
? aspectRatios.map(sizeOptionForAspectRatio)
|
||||
: o?.sizeOptions,
|
||||
supportsCommonAspectRatio:
|
||||
aspectRatios.length > 0 || (o?.supportsCommonAspectRatio ?? false),
|
||||
});
|
||||
};
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
const COMMON_ASPECT_RATIO_VALUES: Set<string> = new Set(Object.values(CommonAspectRatio));
|
||||
const COMMON_RESOLUTION_VALUES: Set<string> = new Set(Object.values(CommonResolution));
|
||||
const COMMON_QUALITY_VALUES: Set<string> = new Set(Object.values(CommonQuality));
|
||||
|
||||
const knownValues = (values: string[] | null | undefined, known: Set<string>): string[] =>
|
||||
(values ?? []).filter((v) => known.has(v));
|
||||
|
||||
const knownValue = (
|
||||
value: string | null | undefined,
|
||||
known: Set<string>,
|
||||
): string | undefined => (value != null && known.has(value) ? value : undefined);
|
||||
|
||||
/**
|
||||
* Derive a video size picker option from a CommonAspectRatio value.
|
||||
* `tauriValue` is the CommonAspectRatio serde string sent in the request.
|
||||
*/
|
||||
const sizeOptionForAspectRatio = (value: string): SizeOption => {
|
||||
switch (value) {
|
||||
case "square":
|
||||
return { tauriValue: value, textLabel: "1:1", icon: SizeIconOption.Square };
|
||||
case "square_hd":
|
||||
return { tauriValue: value, textLabel: "1:1 HD", icon: SizeIconOption.Square };
|
||||
case "wide_sixteen_by_nine":
|
||||
return { tauriValue: value, textLabel: "16:9", icon: SizeIconOption.Landscape16x9 };
|
||||
case "tall_nine_by_sixteen":
|
||||
return { tauriValue: value, textLabel: "9:16", icon: SizeIconOption.Portrait9x16 };
|
||||
case "wide_four_by_three":
|
||||
return { tauriValue: value, textLabel: "4:3", icon: SizeIconOption.Standard4x3 };
|
||||
case "tall_three_by_four":
|
||||
return { tauriValue: value, textLabel: "3:4", icon: SizeIconOption.Portrait3x4 };
|
||||
case "wide_three_by_two":
|
||||
return { tauriValue: value, textLabel: "3:2", icon: SizeIconOption.Landscape };
|
||||
case "tall_two_by_three":
|
||||
return { tauriValue: value, textLabel: "2:3", icon: SizeIconOption.Portrait };
|
||||
case "wide_five_by_four":
|
||||
return { tauriValue: value, textLabel: "5:4", icon: SizeIconOption.Landscape };
|
||||
case "tall_four_by_five":
|
||||
return { tauriValue: value, textLabel: "4:5", icon: SizeIconOption.Portrait };
|
||||
case "wide_twenty_one_by_nine":
|
||||
return { tauriValue: value, textLabel: "21:9", icon: SizeIconOption.Landscape16x9 };
|
||||
case "tall_nine_by_twenty_one":
|
||||
return { tauriValue: value, textLabel: "9:21", icon: SizeIconOption.Portrait9x16 };
|
||||
case "wide":
|
||||
return { tauriValue: value, textLabel: "Landscape", icon: SizeIconOption.Landscape };
|
||||
case "tall":
|
||||
return { tauriValue: value, textLabel: "Portrait", icon: SizeIconOption.Portrait };
|
||||
case "auto":
|
||||
return { tauriValue: value, textLabel: "Auto", icon: SizeIconOption.Square };
|
||||
default:
|
||||
return { tauriValue: value, textLabel: value, icon: SizeIconOption.Square };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CommonResolution serde value -> the UI label form the video promptbox uses.
|
||||
* (PromptBoxVideo's RESOLUTION_STRING_TO_COMMON accepts both forms; labels
|
||||
* display better.)
|
||||
*/
|
||||
const resolutionLabel = (value: string): string => {
|
||||
switch (value) {
|
||||
case "four_eighty_p":
|
||||
return "480p";
|
||||
case "seven_twenty_p":
|
||||
return "720p";
|
||||
case "ten_eighty_p":
|
||||
return "1080p";
|
||||
default:
|
||||
return value; // Raw enum value; still request-mappable.
|
||||
}
|
||||
};
|
||||
|
||||
// Guess a creator from the API's `model_creator`, then the overlay, then the
|
||||
// model-id prefix, then ArtCraft.
|
||||
const CREATOR_BY_PREFIX: Array<[string, ModelCreator]> = [
|
||||
["flux", ModelCreator.BlackForestLabs],
|
||||
["nano_banana", ModelCreator.Google],
|
||||
["gpt_image", ModelCreator.OpenAi],
|
||||
["seedream", ModelCreator.Bytedance],
|
||||
["seedance", ModelCreator.Bytedance],
|
||||
["kling", ModelCreator.Kling],
|
||||
["sora", ModelCreator.OpenAi],
|
||||
["veo", ModelCreator.Google],
|
||||
["grok", ModelCreator.Grok],
|
||||
["happy_horse", ModelCreator.Alibaba],
|
||||
["midjourney", ModelCreator.Midjourney],
|
||||
["qwen", ModelCreator.Alibaba],
|
||||
];
|
||||
|
||||
const creatorFor = (
|
||||
raw: string | null | undefined,
|
||||
modelId: string,
|
||||
overlayCreator: ModelCreator | undefined,
|
||||
): ModelCreator => {
|
||||
const mapped = modelCreatorFromBackend(raw ?? undefined);
|
||||
if (mapped) return mapped;
|
||||
if (overlayCreator) return overlayCreator;
|
||||
for (const [prefix, creator] of CREATOR_BY_PREFIX) {
|
||||
if (modelId.startsWith(prefix)) return creator;
|
||||
}
|
||||
return ModelCreator.ArtCraft;
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
// Reconciles the frontend model OVERLAY (the hand-maintained `IMAGE_MODELS` /
|
||||
// `VIDEO_MODELS`, which carry all the UI/product metadata + page-capability flags)
|
||||
// against the backend omni listing (`ListImageModels` / `ListVideoModels`).
|
||||
//
|
||||
// The backend is authoritative for WHICH models are offered; the overlay is
|
||||
// authoritative for the metadata the backend doesn't have (selectorName, badges,
|
||||
// tags, canTextToImage/canEditImages/canEditAngles, sizeOptions, durations, and
|
||||
// the frontend-only models switch_x / inpaint / angle / kontext).
|
||||
//
|
||||
// This module is intentionally PURE (no tauri-api import) so `model-list` stays
|
||||
// free of a circular dependency. The async loader that calls the Tauri commands
|
||||
// lives in `tauri-api` and feeds `knownBackendIds` / `offeredBackendIds` here.
|
||||
|
||||
import { ImageModel } from "../classes/ImageModel.js";
|
||||
import { VideoModel } from "../classes/VideoModel.js";
|
||||
import { ModelCreator } from "../classes/metadata/ModelCreator.js";
|
||||
|
||||
/**
|
||||
* Backend omni model id (the `CommonImageModel` serde string) -> frontend `tauriId`.
|
||||
*
|
||||
* MIGRATION (2026-07): RETIRED. The overlay `tauriId`s now use the
|
||||
* storyteller-web omni identifiers directly, and the Tauri generate commands
|
||||
* accept them, so no aliasing remains. Do not add entries — fix ids at the
|
||||
* source instead. (Kept exported until the reconcile helpers are deleted.)
|
||||
* See external/reports/2026-07/storyteller-web-omni-vs-tauri-model-differences.md.
|
||||
*/
|
||||
export const BACKEND_TO_TAURI_IMAGE_ID: Record<string, string> = {};
|
||||
|
||||
export const BACKEND_TO_TAURI_VIDEO_ID: Record<string, string> = {};
|
||||
|
||||
/** Backend `model_creator` snake_case string -> frontend `ModelCreator` enum. */
|
||||
export const modelCreatorFromBackend = (raw?: string): ModelCreator | undefined => {
|
||||
switch (raw) {
|
||||
case "alibaba": return ModelCreator.Alibaba;
|
||||
case "artcraft": return ModelCreator.ArtCraft;
|
||||
case "black_forest_labs": return ModelCreator.BlackForestLabs;
|
||||
case "bytedance": return ModelCreator.Bytedance;
|
||||
case "fal": return ModelCreator.Fal;
|
||||
case "google": return ModelCreator.Google;
|
||||
case "grok": return ModelCreator.Grok;
|
||||
case "hailuo": return ModelCreator.Hailuo;
|
||||
case "higgsfield": return ModelCreator.Higgsfield;
|
||||
case "kling": return ModelCreator.Kling;
|
||||
case "krea": return ModelCreator.Krea;
|
||||
case "midjourney": return ModelCreator.Midjourney;
|
||||
case "open_ai": return ModelCreator.OpenAi;
|
||||
case "open_art": return ModelCreator.OpenArt;
|
||||
case "recraft": return ModelCreator.Recraft;
|
||||
case "replicate": return ModelCreator.Replicate;
|
||||
case "runway": return ModelCreator.Runway;
|
||||
case "stability": return ModelCreator.Stability;
|
||||
case "tencent": return ModelCreator.Tencent;
|
||||
case "tensor_art": return ModelCreator.TensorArt;
|
||||
case "vidu": return ModelCreator.Vidu;
|
||||
case "world_labs": return ModelCreator.WorldLabs;
|
||||
default: return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const toTauriIds = (backendIds: string[], alias: Record<string, string>): Set<string> =>
|
||||
new Set(backendIds.map((id) => alias[id] ?? id));
|
||||
|
||||
/**
|
||||
* Membership is BACKEND-DRIVEN, mirroring the web apps' `omni-gen-hooks` (which
|
||||
* render `res.models.filter((m) => m.is_disabled !== true)`):
|
||||
*
|
||||
* - Keep an overlay model when the backend returns it ENABLED.
|
||||
* - Drop it when the backend KNOWS the model but has it disabled (`is_disabled`).
|
||||
* - Keep it when the backend has never heard of it — a frontend-only model like
|
||||
* `switch_x` / inpaint / angles that has no omni counterpart.
|
||||
*
|
||||
* The overlay only supplies the UI metadata the backend lacks (selectorName,
|
||||
* badges, tags, capability flags); the backend response decides what appears.
|
||||
*/
|
||||
const reconcile = <T extends { tauriId: string }>(
|
||||
overlay: T[],
|
||||
knownBackendIds: string[],
|
||||
enabledBackendIds: string[],
|
||||
alias: Record<string, string>,
|
||||
): T[] => {
|
||||
const known = toTauriIds(knownBackendIds, alias);
|
||||
const enabled = toTauriIds(enabledBackendIds, alias);
|
||||
return overlay.filter((m) => enabled.has(m.tauriId) || !known.has(m.tauriId));
|
||||
};
|
||||
|
||||
export const reconcileImageModels = (
|
||||
overlay: ImageModel[],
|
||||
knownBackendIds: string[],
|
||||
enabledBackendIds: string[],
|
||||
): ImageModel[] => reconcile(overlay, knownBackendIds, enabledBackendIds, BACKEND_TO_TAURI_IMAGE_ID);
|
||||
|
||||
export const reconcileVideoModels = (
|
||||
overlay: VideoModel[],
|
||||
knownBackendIds: string[],
|
||||
enabledBackendIds: string[],
|
||||
): VideoModel[] => reconcile(overlay, knownBackendIds, enabledBackendIds, BACKEND_TO_TAURI_VIDEO_ID);
|
||||
@@ -16,6 +16,8 @@ export * from "./lib/enqueue/EnqueueImageToVideo.js";
|
||||
export * from "./lib/enqueue/EnqueueTextToImage.js";
|
||||
export * from "./lib/generate/GenerateImage.js";
|
||||
export * from "./lib/generate/GenerateVideo.js";
|
||||
export * from "./lib/generate/models/image/ListImageModels.js";
|
||||
export * from "./lib/generate/models/video/ListVideoModels.js";
|
||||
export * from "./lib/events/BasicEventWrapper.js";
|
||||
export * from "./lib/events/CanvasBgRemovedEvent.js";
|
||||
export * from "./lib/fal/FalBackgroundRemoval.js";
|
||||
@@ -45,4 +47,5 @@ export * from "./lib/util/LoadWithoutCors.js";
|
||||
export * from "./lib/worldlabs/WorldLabsGetCredentialInfo.js";
|
||||
export * from "./lib/cost_estimate/EstimateImageCost.js";
|
||||
export * from "./lib/cost_estimate/EstimateSplatCost.js";
|
||||
export * from "./lib/cost_estimate/EstimateVideoCost.js";
|
||||
export * from "./lib/cost_estimate/EstimateVideoCost.js";
|
||||
export * from "./lib/models/modelsStore.js";
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../../../common/CommandStatus";
|
||||
|
||||
// NB: enum-valued fields are typed as plain strings on purpose. The backend
|
||||
// enums carry an `Unknown(String)` catch-all, so a newer server may return a
|
||||
// value this build doesn't know; keeping them as strings keeps us forward-compatible.
|
||||
|
||||
export interface ListImageModelsSuccess extends CommandResult {
|
||||
payload: ListImageModelsPayload;
|
||||
}
|
||||
|
||||
export interface ListImageModelsPayload {
|
||||
success: boolean;
|
||||
models: OmniGenImageModelDetails[];
|
||||
providers: OmniGenImageModelProviderDetails[];
|
||||
}
|
||||
|
||||
export interface OmniGenImageModelProviderDetails {
|
||||
provider: string;
|
||||
models: OmniGenImageProviderModelDetails[];
|
||||
}
|
||||
|
||||
export interface OmniGenImageProviderModelDetails {
|
||||
model: string;
|
||||
overrides?: OmniGenImageModelDetails;
|
||||
}
|
||||
|
||||
export interface OmniGenImageModelDetails {
|
||||
model: string;
|
||||
model_creator?: string;
|
||||
full_name?: string;
|
||||
text_prompt_supported?: boolean;
|
||||
text_prompt_max_length?: number;
|
||||
negative_text_prompt_supported?: boolean;
|
||||
negative_text_prompt_max_length?: number;
|
||||
image_refs_supported?: boolean;
|
||||
image_refs_max?: number;
|
||||
has_fixed_editing_aspect_ratio?: boolean;
|
||||
aspect_ratio_options?: string[];
|
||||
aspect_ratio_default?: string;
|
||||
aspect_ratio_default_when_editing?: string;
|
||||
resolution_options?: string[];
|
||||
resolution_default?: string;
|
||||
quality_options?: string[];
|
||||
default_quality?: string;
|
||||
batch_size_min?: number;
|
||||
batch_size_max?: number;
|
||||
batch_size_options?: number[];
|
||||
batch_size_default?: number;
|
||||
is_disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* List available image models from the Tauri backend
|
||||
* (`list_image_models_command` — retried + cached 60s in the Rust layer).
|
||||
*/
|
||||
export const ListImageModels = async (): Promise<ListImageModelsSuccess> => {
|
||||
try {
|
||||
return (await invoke("list_image_models_command")) as ListImageModelsSuccess;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { CommandResult } from "../../../common/CommandStatus";
|
||||
|
||||
// NB: enum-valued fields are typed as plain strings for forward-compatibility
|
||||
// (the backend enums have an `Unknown(String)` catch-all).
|
||||
|
||||
export interface ListVideoModelsSuccess extends CommandResult {
|
||||
payload: ListVideoModelsPayload;
|
||||
}
|
||||
|
||||
export interface ListVideoModelsPayload {
|
||||
success: boolean;
|
||||
models: OmniGenVideoModelDetails[];
|
||||
providers: OmniGenVideoModelProviderDetails[];
|
||||
}
|
||||
|
||||
export interface OmniGenVideoModelProviderDetails {
|
||||
provider: string;
|
||||
models: OmniGenVideoProviderModelDetails[];
|
||||
}
|
||||
|
||||
export interface OmniGenVideoProviderModelDetails {
|
||||
model: string;
|
||||
overrides?: OmniGenVideoModelDetails;
|
||||
}
|
||||
|
||||
export interface OmniGenVideoModelDetails {
|
||||
model: string;
|
||||
model_creator?: string;
|
||||
full_name?: string;
|
||||
extra_info?: string;
|
||||
extra_info_short?: string;
|
||||
text_to_video_supported?: boolean;
|
||||
text_prompt_supported?: boolean;
|
||||
text_prompt_max_length?: number;
|
||||
negative_text_prompt_supported?: boolean;
|
||||
negative_text_prompt_max_length?: number;
|
||||
starting_keyframe_supported?: boolean;
|
||||
starting_keyframe_required?: boolean;
|
||||
ending_keyframe_supported?: boolean;
|
||||
image_references_supported?: boolean;
|
||||
image_references_max?: number;
|
||||
video_references_supported?: boolean;
|
||||
video_references_max?: number;
|
||||
video_references_max_total_duration_seconds?: number;
|
||||
audio_references_supported?: boolean;
|
||||
audio_references_max?: number;
|
||||
audio_references_max_total_duration_seconds?: number;
|
||||
character_references_supported?: boolean;
|
||||
character_references_max?: number;
|
||||
show_generate_with_sound_toggle?: boolean;
|
||||
aspect_ratio_options?: string[];
|
||||
aspect_ratio_default?: string;
|
||||
resolution_options?: string[];
|
||||
resolution_default?: string;
|
||||
bitrate_options?: string[];
|
||||
bitrate_default?: string;
|
||||
quality_options?: string[];
|
||||
default_quality?: string;
|
||||
duration_seconds_min?: number;
|
||||
duration_seconds_max?: number;
|
||||
duration_seconds_max_with_image_references?: number;
|
||||
duration_seconds_options?: number[];
|
||||
duration_seconds_default?: number;
|
||||
batch_size_min?: number;
|
||||
batch_size_max?: number;
|
||||
batch_size_options?: number[];
|
||||
batch_size_default?: number;
|
||||
is_disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* List available video models from the Tauri backend
|
||||
* (`list_video_models_command` — retried + cached 60s in the Rust layer).
|
||||
*/
|
||||
export const ListVideoModels = async (): Promise<ListVideoModelsSuccess> => {
|
||||
try {
|
||||
return (await invoke("list_video_models_command")) as ListVideoModelsSuccess;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
// Shared model store: the single source of truth for the model dropdowns.
|
||||
//
|
||||
// Seeded synchronously from the frontend OVERLAY (`IMAGE_MODELS` / `VIDEO_MODELS`
|
||||
// in `@storyteller/model-list`) so the UI is never empty. On app boot,
|
||||
// `loadModelsFromBackend()` fetches the authoritative omni listing via the Tauri
|
||||
// commands and rebuilds the lists FROM that response (membership + order come
|
||||
// from the backend; the overlay only enriches UI metadata; backend models with
|
||||
// no overlay entry are built minimally so NEW models appear). If the fetch fails
|
||||
// the store keeps the overlay.
|
||||
|
||||
import { create } from "zustand";
|
||||
import {
|
||||
ImageModel,
|
||||
VideoModel,
|
||||
IMAGE_MODELS,
|
||||
VIDEO_MODELS,
|
||||
buildImageModelsFromListing,
|
||||
buildVideoModelsFromListing,
|
||||
} from "@storyteller/model-list";
|
||||
import { ListImageModels } from "../generate/models/image/ListImageModels.js";
|
||||
import { ListVideoModels } from "../generate/models/video/ListVideoModels.js";
|
||||
|
||||
export interface ModelsStoreState {
|
||||
imageModels: ImageModel[];
|
||||
videoModels: VideoModel[];
|
||||
// True once a backend reconciliation has completed at least once.
|
||||
loaded: boolean;
|
||||
isLoading: boolean;
|
||||
loadModelsFromBackend: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useModelsStore = create<ModelsStoreState>((set, get) => ({
|
||||
imageModels: IMAGE_MODELS,
|
||||
videoModels: VIDEO_MODELS,
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
loadModelsFromBackend: async () => {
|
||||
if (get().isLoading) return;
|
||||
console.log("[models] loadModelsFromBackend() — calling Tauri commands…");
|
||||
set({ isLoading: true });
|
||||
|
||||
const [imageResult, videoResult] = await Promise.allSettled([
|
||||
ListImageModels(),
|
||||
ListVideoModels(),
|
||||
]);
|
||||
|
||||
const next: Partial<ModelsStoreState> = { isLoading: false, loaded: true };
|
||||
|
||||
if (imageResult.status === "fulfilled") {
|
||||
const { models, providers } = imageResult.value.payload;
|
||||
const offered = providers.flatMap((p) => p.models.map((pm) => pm.model));
|
||||
const built = buildImageModelsFromListing(IMAGE_MODELS, models, offered);
|
||||
next.imageModels = built;
|
||||
console.log(
|
||||
`[models] image: backend returned ${models.length} → ${built.length} models shown`,
|
||||
built.map((m) => m.tauriId),
|
||||
);
|
||||
} else {
|
||||
console.error("[models] failed to load image models from backend:", imageResult.reason);
|
||||
}
|
||||
|
||||
if (videoResult.status === "fulfilled") {
|
||||
const { models, providers } = videoResult.value.payload;
|
||||
const offered = providers.flatMap((p) => p.models.map((pm) => pm.model));
|
||||
const built = buildVideoModelsFromListing(VIDEO_MODELS, models, offered);
|
||||
next.videoModels = built;
|
||||
console.log(
|
||||
`[models] video: backend returned ${models.length} → ${built.length} models shown`,
|
||||
built.map((m) => m.tauriId),
|
||||
);
|
||||
} else {
|
||||
console.error("[models] failed to load video models from backend:", videoResult.reason);
|
||||
}
|
||||
|
||||
set(next);
|
||||
},
|
||||
}));
|
||||
|
||||
// Hook selectors for React consumers.
|
||||
export const useImageModels = (): ImageModel[] => useModelsStore((s) => s.imageModels);
|
||||
export const useVideoModels = (): VideoModel[] => useModelsStore((s) => s.videoModels);
|
||||
|
Before Width: | Height: | Size: 6.2 MiB After Width: | Height: | Size: 6.2 MiB |
|
Before Width: | Height: | Size: 7.6 MiB After Width: | Height: | Size: 7.6 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 297 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 183 KiB |