mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 17:08:13 +00:00
stronger typing
This commit is contained in:
1 parent
bfd572a530
commit
3cc8b39b08
6 files changed
+433
-167
No files matched your search
@@ -11,6 +11,10 @@ pub enum Seedance2ProClientError {
|
||||
|
||||
/// An error was encountered in building the Wreq client.
|
||||
WreqClientError(wreq::Error),
|
||||
|
||||
/// A request field couldn't be parsed into the value expected on the wire.
|
||||
/// Carries the field name and the raw input the caller supplied.
|
||||
InvalidRequestField { field: &'static str, raw_value: String, reason: String },
|
||||
}
|
||||
|
||||
impl Error for Seedance2ProClientError {}
|
||||
@@ -21,6 +25,9 @@ impl Display for Seedance2ProClientError {
|
||||
Self::NoCookiesPresent => write!(f, "No cookies present in the session."),
|
||||
Self::UrlParseError(err) => write!(f, "URL parse error: {}", err),
|
||||
Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err),
|
||||
Self::InvalidRequestField { field, raw_value, reason } => {
|
||||
write!(f, "Invalid value for request field `{}`: {:?} ({})", field, raw_value, reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+112
-17
@@ -5,11 +5,11 @@ use crate::requests::generate_image::generate_image::{
|
||||
};
|
||||
use crate::requests::kinovi_host::KinoviHost;
|
||||
|
||||
// Re-export shared enums so callers can use this module without dipping into
|
||||
// the lower-level `requests::generate_image` module path.
|
||||
pub use crate::requests::generate_image::generate_image::{
|
||||
KinoviMidjourneyAspectRatio, KinoviMidjourneyBatchCount, KinoviMidjourneyQuality,
|
||||
};
|
||||
// `KinoviMidjourneyBatchCount` is shared across all Midjourney models, so we
|
||||
// re-export it from the base. Aspect ratio and quality, however, are pinned
|
||||
// to model-specific enums on this wrapper to keep their valid values typed
|
||||
// at the call site.
|
||||
pub use crate::requests::generate_image::generate_image::KinoviMidjourneyBatchCount;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
@@ -24,12 +24,12 @@ pub struct GenerateMidjourneyV7Args<'a> {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateMidjourneyV7Request {
|
||||
pub prompt: String,
|
||||
pub aspect_ratio: KinoviMidjourneyAspectRatio,
|
||||
pub aspect_ratio: GenerateMidjourneyV7AspectRatio,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub stylize: Option<u16>,
|
||||
pub weird: Option<u16>,
|
||||
pub chaos: Option<u8>,
|
||||
pub quality: Option<KinoviMidjourneyQuality>,
|
||||
pub quality: Option<GenerateMidjourneyV7Quality>,
|
||||
pub raw_mode: bool,
|
||||
pub batch_count: KinoviMidjourneyBatchCount,
|
||||
pub reference_image_urls: Option<Vec<String>>,
|
||||
@@ -48,12 +48,12 @@ impl GenerateMidjourneyV7Request {
|
||||
KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V7,
|
||||
prompt: self.prompt.clone(),
|
||||
aspect_ratio: self.aspect_ratio,
|
||||
aspect_ratio: self.aspect_ratio.as_api_str().to_string(),
|
||||
negative_prompt: self.negative_prompt.clone(),
|
||||
stylize: self.stylize,
|
||||
weird: self.weird,
|
||||
chaos: self.chaos,
|
||||
quality: self.quality,
|
||||
quality: self.quality.map(|q| q.as_api_str().to_string()),
|
||||
raw_mode: self.raw_mode,
|
||||
batch_count: self.batch_count,
|
||||
reference_image_urls: self.reference_image_urls.clone(),
|
||||
@@ -61,6 +61,64 @@ impl GenerateMidjourneyV7Request {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Model-specific enums ──
|
||||
|
||||
/// Aspect ratios supported by Midjourney v7.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum GenerateMidjourneyV7AspectRatio {
|
||||
Square1x1,
|
||||
Landscape16x9,
|
||||
Portrait9x16,
|
||||
UltraWide21x9,
|
||||
UltraTall9x21,
|
||||
Standard4x3,
|
||||
Portrait3x4,
|
||||
Wide5x4,
|
||||
Tall4x5,
|
||||
Wide3x2,
|
||||
Tall2x3,
|
||||
}
|
||||
|
||||
impl GenerateMidjourneyV7AspectRatio {
|
||||
fn as_api_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Square1x1 => "1:1",
|
||||
Self::Landscape16x9 => "16:9",
|
||||
Self::Portrait9x16 => "9:16",
|
||||
Self::UltraWide21x9 => "21:9",
|
||||
Self::UltraTall9x21 => "9:21",
|
||||
Self::Standard4x3 => "4:3",
|
||||
Self::Portrait3x4 => "3:4",
|
||||
Self::Wide5x4 => "5:4",
|
||||
Self::Tall4x5 => "4:5",
|
||||
Self::Wide3x2 => "3:2",
|
||||
Self::Tall2x3 => "2:3",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Quality presets supported by Midjourney v7. Higher = more compute and
|
||||
/// slower; Kinovi credit pricing is flat regardless.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum GenerateMidjourneyV7Quality {
|
||||
/// 0.25
|
||||
Quarter,
|
||||
/// 0.5
|
||||
Half,
|
||||
/// 1 (default)
|
||||
Full,
|
||||
}
|
||||
|
||||
impl GenerateMidjourneyV7Quality {
|
||||
fn as_api_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Quarter => "0.25",
|
||||
Self::Half => "0.5",
|
||||
Self::Full => "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateMidjourneyV7Response {
|
||||
@@ -107,7 +165,7 @@ mod tests {
|
||||
fn make_request(batch_count: KinoviMidjourneyBatchCount) -> GenerateMidjourneyV7Request {
|
||||
GenerateMidjourneyV7Request {
|
||||
prompt: "test".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: GenerateMidjourneyV7AspectRatio::Square1x1,
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
@@ -134,12 +192,12 @@ mod tests {
|
||||
fn inner_request_preserves_all_fields() {
|
||||
let req = GenerateMidjourneyV7Request {
|
||||
prompt: "a corgi in space".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Landscape16x9,
|
||||
aspect_ratio: GenerateMidjourneyV7AspectRatio::Landscape16x9,
|
||||
negative_prompt: Some("dark".to_string()),
|
||||
stylize: Some(500),
|
||||
weird: Some(1500),
|
||||
chaos: Some(50),
|
||||
quality: Some(KinoviMidjourneyQuality::Half),
|
||||
quality: Some(GenerateMidjourneyV7Quality::Half),
|
||||
raw_mode: true,
|
||||
batch_count: KinoviMidjourneyBatchCount::Four,
|
||||
reference_image_urls: Some(vec!["https://example.com/x.png".to_string()]),
|
||||
@@ -147,12 +205,12 @@ mod tests {
|
||||
let inner = req.to_inner_request();
|
||||
assert_eq!(inner.model, KinoviMidjourneyModel::V7);
|
||||
assert_eq!(inner.prompt, "a corgi in space");
|
||||
assert_eq!(inner.aspect_ratio, KinoviMidjourneyAspectRatio::Landscape16x9);
|
||||
assert_eq!(inner.aspect_ratio, "16:9");
|
||||
assert_eq!(inner.negative_prompt.as_deref(), Some("dark"));
|
||||
assert_eq!(inner.stylize, Some(500));
|
||||
assert_eq!(inner.weird, Some(1500));
|
||||
assert_eq!(inner.chaos, Some(50));
|
||||
assert_eq!(inner.quality, Some(KinoviMidjourneyQuality::Half));
|
||||
assert_eq!(inner.quality.as_deref(), Some("0.5"));
|
||||
assert!(inner.raw_mode);
|
||||
assert_eq!(inner.batch_count, KinoviMidjourneyBatchCount::Four);
|
||||
assert_eq!(
|
||||
@@ -160,6 +218,43 @@ mod tests {
|
||||
Some(&["https://example.com/x.png".to_string()][..]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Every wrapper-side aspect ratio must serialize to the exact wire
|
||||
/// string Midjourney expects. Catches silent drift between the enum
|
||||
/// surface and the API.
|
||||
#[test]
|
||||
fn every_aspect_ratio_maps_to_canonical_wire_string() {
|
||||
let cases = [
|
||||
(GenerateMidjourneyV7AspectRatio::Square1x1, "1:1"),
|
||||
(GenerateMidjourneyV7AspectRatio::Landscape16x9, "16:9"),
|
||||
(GenerateMidjourneyV7AspectRatio::Portrait9x16, "9:16"),
|
||||
(GenerateMidjourneyV7AspectRatio::UltraWide21x9, "21:9"),
|
||||
(GenerateMidjourneyV7AspectRatio::UltraTall9x21, "9:21"),
|
||||
(GenerateMidjourneyV7AspectRatio::Standard4x3, "4:3"),
|
||||
(GenerateMidjourneyV7AspectRatio::Portrait3x4, "3:4"),
|
||||
(GenerateMidjourneyV7AspectRatio::Wide5x4, "5:4"),
|
||||
(GenerateMidjourneyV7AspectRatio::Tall4x5, "4:5"),
|
||||
(GenerateMidjourneyV7AspectRatio::Wide3x2, "3:2"),
|
||||
(GenerateMidjourneyV7AspectRatio::Tall2x3, "2:3"),
|
||||
];
|
||||
for (variant, expected) in cases {
|
||||
let req = GenerateMidjourneyV7Request { aspect_ratio: variant, ..make_request(KinoviMidjourneyBatchCount::One) };
|
||||
assert_eq!(req.to_inner_request().aspect_ratio, expected, "variant={:?}", variant);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_quality_maps_to_canonical_wire_string() {
|
||||
let cases = [
|
||||
(GenerateMidjourneyV7Quality::Quarter, "0.25"),
|
||||
(GenerateMidjourneyV7Quality::Half, "0.5"),
|
||||
(GenerateMidjourneyV7Quality::Full, "1"),
|
||||
];
|
||||
for (variant, expected) in cases {
|
||||
let req = GenerateMidjourneyV7Request { quality: Some(variant), ..make_request(KinoviMidjourneyBatchCount::One) };
|
||||
assert_eq!(req.to_inner_request().quality.as_deref(), Some(expected), "variant={:?}", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pricing (delegates to the inner module, but pinned via explicit values) ──
|
||||
@@ -224,7 +319,7 @@ mod tests {
|
||||
host_override: None,
|
||||
request: GenerateMidjourneyV7Request {
|
||||
prompt: "A corgi astronaut floating among stars".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: GenerateMidjourneyV7AspectRatio::Square1x1,
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
@@ -251,12 +346,12 @@ mod tests {
|
||||
host_override: None,
|
||||
request: GenerateMidjourneyV7Request {
|
||||
prompt: "abandoned skyscrapers".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: GenerateMidjourneyV7AspectRatio::Square1x1,
|
||||
negative_prompt: Some("dark, gloomy, night".to_string()),
|
||||
stylize: Some(1000),
|
||||
weird: Some(3000),
|
||||
chaos: Some(100),
|
||||
quality: Some(KinoviMidjourneyQuality::Half),
|
||||
quality: Some(GenerateMidjourneyV7Quality::Half),
|
||||
raw_mode: true,
|
||||
batch_count: KinoviMidjourneyBatchCount::One,
|
||||
reference_image_urls: None,
|
||||
|
||||
+108
-15
@@ -5,11 +5,11 @@ use crate::requests::generate_image::generate_image::{
|
||||
};
|
||||
use crate::requests::kinovi_host::KinoviHost;
|
||||
|
||||
// Re-export shared enums so callers can use this module without dipping into
|
||||
// the lower-level `requests::generate_image` module path.
|
||||
pub use crate::requests::generate_image::generate_image::{
|
||||
KinoviMidjourneyAspectRatio, KinoviMidjourneyBatchCount, KinoviMidjourneyQuality,
|
||||
};
|
||||
// `KinoviMidjourneyBatchCount` is shared across all Midjourney models, so we
|
||||
// re-export it from the base. Aspect ratio and quality, however, are pinned
|
||||
// to model-specific enums on this wrapper to keep their valid values typed
|
||||
// at the call site.
|
||||
pub use crate::requests::generate_image::generate_image::KinoviMidjourneyBatchCount;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
@@ -24,12 +24,12 @@ pub struct GenerateMidjourneyV7NijiArgs<'a> {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateMidjourneyV7NijiRequest {
|
||||
pub prompt: String,
|
||||
pub aspect_ratio: KinoviMidjourneyAspectRatio,
|
||||
pub aspect_ratio: GenerateMidjourneyV7NijiAspectRatio,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub stylize: Option<u16>,
|
||||
pub weird: Option<u16>,
|
||||
pub chaos: Option<u8>,
|
||||
pub quality: Option<KinoviMidjourneyQuality>,
|
||||
pub quality: Option<GenerateMidjourneyV7NijiQuality>,
|
||||
pub raw_mode: bool,
|
||||
pub batch_count: KinoviMidjourneyBatchCount,
|
||||
pub reference_image_urls: Option<Vec<String>>,
|
||||
@@ -48,12 +48,12 @@ impl GenerateMidjourneyV7NijiRequest {
|
||||
KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V7Niji,
|
||||
prompt: self.prompt.clone(),
|
||||
aspect_ratio: self.aspect_ratio,
|
||||
aspect_ratio: self.aspect_ratio.as_api_str().to_string(),
|
||||
negative_prompt: self.negative_prompt.clone(),
|
||||
stylize: self.stylize,
|
||||
weird: self.weird,
|
||||
chaos: self.chaos,
|
||||
quality: self.quality,
|
||||
quality: self.quality.map(|q| q.as_api_str().to_string()),
|
||||
raw_mode: self.raw_mode,
|
||||
batch_count: self.batch_count,
|
||||
reference_image_urls: self.reference_image_urls.clone(),
|
||||
@@ -61,6 +61,64 @@ impl GenerateMidjourneyV7NijiRequest {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Model-specific enums ──
|
||||
|
||||
/// Aspect ratios supported by Midjourney v7 Niji.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum GenerateMidjourneyV7NijiAspectRatio {
|
||||
Square1x1,
|
||||
Landscape16x9,
|
||||
Portrait9x16,
|
||||
UltraWide21x9,
|
||||
UltraTall9x21,
|
||||
Standard4x3,
|
||||
Portrait3x4,
|
||||
Wide5x4,
|
||||
Tall4x5,
|
||||
Wide3x2,
|
||||
Tall2x3,
|
||||
}
|
||||
|
||||
impl GenerateMidjourneyV7NijiAspectRatio {
|
||||
fn as_api_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Square1x1 => "1:1",
|
||||
Self::Landscape16x9 => "16:9",
|
||||
Self::Portrait9x16 => "9:16",
|
||||
Self::UltraWide21x9 => "21:9",
|
||||
Self::UltraTall9x21 => "9:21",
|
||||
Self::Standard4x3 => "4:3",
|
||||
Self::Portrait3x4 => "3:4",
|
||||
Self::Wide5x4 => "5:4",
|
||||
Self::Tall4x5 => "4:5",
|
||||
Self::Wide3x2 => "3:2",
|
||||
Self::Tall2x3 => "2:3",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Quality presets supported by Midjourney v7 Niji. Higher = more compute
|
||||
/// and slower; Kinovi credit pricing is flat regardless.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum GenerateMidjourneyV7NijiQuality {
|
||||
/// 0.25
|
||||
Quarter,
|
||||
/// 0.5
|
||||
Half,
|
||||
/// 1 (default)
|
||||
Full,
|
||||
}
|
||||
|
||||
impl GenerateMidjourneyV7NijiQuality {
|
||||
fn as_api_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Quarter => "0.25",
|
||||
Self::Half => "0.5",
|
||||
Self::Full => "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateMidjourneyV7NijiResponse {
|
||||
@@ -107,7 +165,7 @@ mod tests {
|
||||
fn make_request(batch_count: KinoviMidjourneyBatchCount) -> GenerateMidjourneyV7NijiRequest {
|
||||
GenerateMidjourneyV7NijiRequest {
|
||||
prompt: "test".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: GenerateMidjourneyV7NijiAspectRatio::Square1x1,
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
@@ -134,12 +192,12 @@ mod tests {
|
||||
fn inner_request_preserves_all_fields() {
|
||||
let req = GenerateMidjourneyV7NijiRequest {
|
||||
prompt: "anime mecha duel".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::UltraWide21x9,
|
||||
aspect_ratio: GenerateMidjourneyV7NijiAspectRatio::UltraWide21x9,
|
||||
negative_prompt: Some("blurry".to_string()),
|
||||
stylize: Some(750),
|
||||
weird: Some(0),
|
||||
chaos: Some(25),
|
||||
quality: Some(KinoviMidjourneyQuality::Full),
|
||||
quality: Some(GenerateMidjourneyV7NijiQuality::Full),
|
||||
raw_mode: false,
|
||||
batch_count: KinoviMidjourneyBatchCount::Two,
|
||||
reference_image_urls: Some(vec![
|
||||
@@ -150,16 +208,51 @@ mod tests {
|
||||
let inner = req.to_inner_request();
|
||||
assert_eq!(inner.model, KinoviMidjourneyModel::V7Niji);
|
||||
assert_eq!(inner.prompt, "anime mecha duel");
|
||||
assert_eq!(inner.aspect_ratio, KinoviMidjourneyAspectRatio::UltraWide21x9);
|
||||
assert_eq!(inner.aspect_ratio, "21:9");
|
||||
assert_eq!(inner.negative_prompt.as_deref(), Some("blurry"));
|
||||
assert_eq!(inner.stylize, Some(750));
|
||||
assert_eq!(inner.weird, Some(0));
|
||||
assert_eq!(inner.chaos, Some(25));
|
||||
assert_eq!(inner.quality, Some(KinoviMidjourneyQuality::Full));
|
||||
assert_eq!(inner.quality.as_deref(), Some("1"));
|
||||
assert!(!inner.raw_mode);
|
||||
assert_eq!(inner.batch_count, KinoviMidjourneyBatchCount::Two);
|
||||
assert_eq!(inner.reference_image_urls.as_ref().map(|v| v.len()), Some(2));
|
||||
}
|
||||
|
||||
/// Every wrapper-side aspect ratio must serialize to the canonical wire string.
|
||||
#[test]
|
||||
fn every_aspect_ratio_maps_to_canonical_wire_string() {
|
||||
let cases = [
|
||||
(GenerateMidjourneyV7NijiAspectRatio::Square1x1, "1:1"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::Landscape16x9, "16:9"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::Portrait9x16, "9:16"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::UltraWide21x9, "21:9"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::UltraTall9x21, "9:21"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::Standard4x3, "4:3"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::Portrait3x4, "3:4"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::Wide5x4, "5:4"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::Tall4x5, "4:5"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::Wide3x2, "3:2"),
|
||||
(GenerateMidjourneyV7NijiAspectRatio::Tall2x3, "2:3"),
|
||||
];
|
||||
for (variant, expected) in cases {
|
||||
let req = GenerateMidjourneyV7NijiRequest { aspect_ratio: variant, ..make_request(KinoviMidjourneyBatchCount::One) };
|
||||
assert_eq!(req.to_inner_request().aspect_ratio, expected, "variant={:?}", variant);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_quality_maps_to_canonical_wire_string() {
|
||||
let cases = [
|
||||
(GenerateMidjourneyV7NijiQuality::Quarter, "0.25"),
|
||||
(GenerateMidjourneyV7NijiQuality::Half, "0.5"),
|
||||
(GenerateMidjourneyV7NijiQuality::Full, "1"),
|
||||
];
|
||||
for (variant, expected) in cases {
|
||||
let req = GenerateMidjourneyV7NijiRequest { quality: Some(variant), ..make_request(KinoviMidjourneyBatchCount::One) };
|
||||
assert_eq!(req.to_inner_request().quality.as_deref(), Some(expected), "variant={:?}", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
@@ -223,7 +316,7 @@ mod tests {
|
||||
host_override: None,
|
||||
request: GenerateMidjourneyV7NijiRequest {
|
||||
prompt: "A magical shiba inu sorcerer casting spells in a crystal cave".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::UltraWide21x9,
|
||||
aspect_ratio: GenerateMidjourneyV7NijiAspectRatio::UltraWide21x9,
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
|
||||
+113
-20
@@ -5,11 +5,11 @@ use crate::requests::generate_image::generate_image::{
|
||||
};
|
||||
use crate::requests::kinovi_host::KinoviHost;
|
||||
|
||||
// Re-export shared enums so callers can use this module without dipping into
|
||||
// the lower-level `requests::generate_image` module path.
|
||||
pub use crate::requests::generate_image::generate_image::{
|
||||
KinoviMidjourneyAspectRatio, KinoviMidjourneyBatchCount, KinoviMidjourneyQuality,
|
||||
};
|
||||
// `KinoviMidjourneyBatchCount` is shared across all Midjourney models, so we
|
||||
// re-export it from the base. Aspect ratio and quality, however, are pinned
|
||||
// to model-specific enums on this wrapper to keep their valid values typed
|
||||
// at the call site.
|
||||
pub use crate::requests::generate_image::generate_image::KinoviMidjourneyBatchCount;
|
||||
|
||||
// ── Args ──
|
||||
|
||||
@@ -24,12 +24,12 @@ pub struct GenerateMidjourneyV8Args<'a> {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateMidjourneyV8Request {
|
||||
pub prompt: String,
|
||||
pub aspect_ratio: KinoviMidjourneyAspectRatio,
|
||||
pub aspect_ratio: GenerateMidjourneyV8AspectRatio,
|
||||
pub negative_prompt: Option<String>,
|
||||
pub stylize: Option<u16>,
|
||||
pub weird: Option<u16>,
|
||||
pub chaos: Option<u8>,
|
||||
pub quality: Option<KinoviMidjourneyQuality>,
|
||||
pub quality: Option<GenerateMidjourneyV8Quality>,
|
||||
pub raw_mode: bool,
|
||||
pub batch_count: KinoviMidjourneyBatchCount,
|
||||
pub reference_image_urls: Option<Vec<String>>,
|
||||
@@ -48,12 +48,12 @@ impl GenerateMidjourneyV8Request {
|
||||
KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V8,
|
||||
prompt: self.prompt.clone(),
|
||||
aspect_ratio: self.aspect_ratio,
|
||||
aspect_ratio: self.aspect_ratio.as_api_str().to_string(),
|
||||
negative_prompt: self.negative_prompt.clone(),
|
||||
stylize: self.stylize,
|
||||
weird: self.weird,
|
||||
chaos: self.chaos,
|
||||
quality: self.quality,
|
||||
quality: self.quality.map(|q| q.as_api_str().to_string()),
|
||||
raw_mode: self.raw_mode,
|
||||
batch_count: self.batch_count,
|
||||
reference_image_urls: self.reference_image_urls.clone(),
|
||||
@@ -61,6 +61,64 @@ impl GenerateMidjourneyV8Request {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Model-specific enums ──
|
||||
|
||||
/// Aspect ratios supported by Midjourney v8.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum GenerateMidjourneyV8AspectRatio {
|
||||
Square1x1,
|
||||
Landscape16x9,
|
||||
Portrait9x16,
|
||||
UltraWide21x9,
|
||||
UltraTall9x21,
|
||||
Standard4x3,
|
||||
Portrait3x4,
|
||||
Wide5x4,
|
||||
Tall4x5,
|
||||
Wide3x2,
|
||||
Tall2x3,
|
||||
}
|
||||
|
||||
impl GenerateMidjourneyV8AspectRatio {
|
||||
fn as_api_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Square1x1 => "1:1",
|
||||
Self::Landscape16x9 => "16:9",
|
||||
Self::Portrait9x16 => "9:16",
|
||||
Self::UltraWide21x9 => "21:9",
|
||||
Self::UltraTall9x21 => "9:21",
|
||||
Self::Standard4x3 => "4:3",
|
||||
Self::Portrait3x4 => "3:4",
|
||||
Self::Wide5x4 => "5:4",
|
||||
Self::Tall4x5 => "4:5",
|
||||
Self::Wide3x2 => "3:2",
|
||||
Self::Tall2x3 => "2:3",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Quality presets supported by Midjourney v8. Higher = more compute and
|
||||
/// slower; Kinovi credit pricing is flat regardless.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum GenerateMidjourneyV8Quality {
|
||||
/// 0.25
|
||||
Quarter,
|
||||
/// 0.5
|
||||
Half,
|
||||
/// 1 (default)
|
||||
Full,
|
||||
}
|
||||
|
||||
impl GenerateMidjourneyV8Quality {
|
||||
fn as_api_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Quarter => "0.25",
|
||||
Self::Half => "0.5",
|
||||
Self::Full => "1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response ──
|
||||
|
||||
pub struct GenerateMidjourneyV8Response {
|
||||
@@ -107,7 +165,7 @@ mod tests {
|
||||
fn make_request(batch_count: KinoviMidjourneyBatchCount) -> GenerateMidjourneyV8Request {
|
||||
GenerateMidjourneyV8Request {
|
||||
prompt: "test".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: GenerateMidjourneyV8AspectRatio::Square1x1,
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
@@ -134,12 +192,12 @@ mod tests {
|
||||
fn inner_request_preserves_all_fields() {
|
||||
let req = GenerateMidjourneyV8Request {
|
||||
prompt: "abandoned k-mart, overgrown vines".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Portrait9x16,
|
||||
aspect_ratio: GenerateMidjourneyV8AspectRatio::Portrait9x16,
|
||||
negative_prompt: Some("dark, gloomy".to_string()),
|
||||
stylize: Some(730),
|
||||
weird: Some(2050),
|
||||
chaos: Some(70),
|
||||
quality: Some(KinoviMidjourneyQuality::Half),
|
||||
quality: Some(GenerateMidjourneyV8Quality::Half),
|
||||
raw_mode: true,
|
||||
batch_count: KinoviMidjourneyBatchCount::Four,
|
||||
reference_image_urls: Some(vec!["https://example.com/ref.jpg".to_string()]),
|
||||
@@ -147,16 +205,51 @@ mod tests {
|
||||
let inner = req.to_inner_request();
|
||||
assert_eq!(inner.model, KinoviMidjourneyModel::V8);
|
||||
assert_eq!(inner.prompt, "abandoned k-mart, overgrown vines");
|
||||
assert_eq!(inner.aspect_ratio, KinoviMidjourneyAspectRatio::Portrait9x16);
|
||||
assert_eq!(inner.aspect_ratio, "9:16");
|
||||
assert_eq!(inner.negative_prompt.as_deref(), Some("dark, gloomy"));
|
||||
assert_eq!(inner.stylize, Some(730));
|
||||
assert_eq!(inner.weird, Some(2050));
|
||||
assert_eq!(inner.chaos, Some(70));
|
||||
assert_eq!(inner.quality, Some(KinoviMidjourneyQuality::Half));
|
||||
assert_eq!(inner.quality.as_deref(), Some("0.5"));
|
||||
assert!(inner.raw_mode);
|
||||
assert_eq!(inner.batch_count, KinoviMidjourneyBatchCount::Four);
|
||||
assert_eq!(inner.reference_image_urls.as_ref().map(|v| v.len()), Some(1));
|
||||
}
|
||||
|
||||
/// Every wrapper-side aspect ratio must serialize to the canonical wire string.
|
||||
#[test]
|
||||
fn every_aspect_ratio_maps_to_canonical_wire_string() {
|
||||
let cases = [
|
||||
(GenerateMidjourneyV8AspectRatio::Square1x1, "1:1"),
|
||||
(GenerateMidjourneyV8AspectRatio::Landscape16x9, "16:9"),
|
||||
(GenerateMidjourneyV8AspectRatio::Portrait9x16, "9:16"),
|
||||
(GenerateMidjourneyV8AspectRatio::UltraWide21x9, "21:9"),
|
||||
(GenerateMidjourneyV8AspectRatio::UltraTall9x21, "9:21"),
|
||||
(GenerateMidjourneyV8AspectRatio::Standard4x3, "4:3"),
|
||||
(GenerateMidjourneyV8AspectRatio::Portrait3x4, "3:4"),
|
||||
(GenerateMidjourneyV8AspectRatio::Wide5x4, "5:4"),
|
||||
(GenerateMidjourneyV8AspectRatio::Tall4x5, "4:5"),
|
||||
(GenerateMidjourneyV8AspectRatio::Wide3x2, "3:2"),
|
||||
(GenerateMidjourneyV8AspectRatio::Tall2x3, "2:3"),
|
||||
];
|
||||
for (variant, expected) in cases {
|
||||
let req = GenerateMidjourneyV8Request { aspect_ratio: variant, ..make_request(KinoviMidjourneyBatchCount::One) };
|
||||
assert_eq!(req.to_inner_request().aspect_ratio, expected, "variant={:?}", variant);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_quality_maps_to_canonical_wire_string() {
|
||||
let cases = [
|
||||
(GenerateMidjourneyV8Quality::Quarter, "0.25"),
|
||||
(GenerateMidjourneyV8Quality::Half, "0.5"),
|
||||
(GenerateMidjourneyV8Quality::Full, "1"),
|
||||
];
|
||||
for (variant, expected) in cases {
|
||||
let req = GenerateMidjourneyV8Request { quality: Some(variant), ..make_request(KinoviMidjourneyBatchCount::One) };
|
||||
assert_eq!(req.to_inner_request().quality.as_deref(), Some(expected), "variant={:?}", variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pricing ──
|
||||
@@ -220,12 +313,12 @@ mod tests {
|
||||
host_override: None,
|
||||
request: GenerateMidjourneyV8Request {
|
||||
prompt: "spooky horror scene, ghosts".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Portrait9x16,
|
||||
aspect_ratio: GenerateMidjourneyV8AspectRatio::Portrait9x16,
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
chaos: None,
|
||||
quality: Some(KinoviMidjourneyQuality::Full),
|
||||
quality: Some(GenerateMidjourneyV8Quality::Full),
|
||||
raw_mode: false,
|
||||
batch_count: KinoviMidjourneyBatchCount::One,
|
||||
reference_image_urls: None,
|
||||
@@ -246,12 +339,12 @@ mod tests {
|
||||
host_override: None,
|
||||
request: GenerateMidjourneyV8Request {
|
||||
prompt: "desolate cliff overlooking the ocean".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Portrait9x16,
|
||||
aspect_ratio: GenerateMidjourneyV8AspectRatio::Portrait9x16,
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
chaos: None,
|
||||
quality: Some(KinoviMidjourneyQuality::Full),
|
||||
quality: Some(GenerateMidjourneyV8Quality::Full),
|
||||
raw_mode: false,
|
||||
batch_count: KinoviMidjourneyBatchCount::Four,
|
||||
reference_image_urls: None,
|
||||
@@ -272,12 +365,12 @@ mod tests {
|
||||
host_override: None,
|
||||
request: GenerateMidjourneyV8Request {
|
||||
prompt: "spooky horror scene, ghosts".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Portrait9x16,
|
||||
aspect_ratio: GenerateMidjourneyV8AspectRatio::Portrait9x16,
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
chaos: None,
|
||||
quality: Some(KinoviMidjourneyQuality::Full),
|
||||
quality: Some(GenerateMidjourneyV8Quality::Full),
|
||||
raw_mode: false,
|
||||
batch_count: KinoviMidjourneyBatchCount::One,
|
||||
reference_image_urls: Some(vec![
|
||||
|
||||
+92
-114
@@ -39,6 +39,12 @@ pub struct GenerateImageArgs<'a> {
|
||||
}
|
||||
|
||||
/// Image generation parameters (no session/host info).
|
||||
///
|
||||
/// `aspect_ratio` and `quality` are loosely-typed strings here so this
|
||||
/// function can serve image-generation flows that aren't Midjourney without
|
||||
/// being constrained to Midjourney's particular set of values. The thin
|
||||
/// per-model wrappers (`generate_midjourney_v7`, etc.) own strongly-typed
|
||||
/// enums and convert them to strings before calling in.
|
||||
#[derive(Clone)]
|
||||
pub struct KinoviGenerateImageRequest {
|
||||
/// Which Midjourney variant to run.
|
||||
@@ -47,8 +53,9 @@ pub struct KinoviGenerateImageRequest {
|
||||
/// User prompt.
|
||||
pub prompt: String,
|
||||
|
||||
/// Output aspect ratio.
|
||||
pub aspect_ratio: KinoviMidjourneyAspectRatio,
|
||||
/// Output aspect ratio, e.g. `"16:9"`, `"1:1"`, `"9:16"`. Forwarded
|
||||
/// verbatim into the request's `aspectRatio` field.
|
||||
pub aspect_ratio: String,
|
||||
|
||||
/// Optional negative prompt. Sent as the `"no"` field on the wire.
|
||||
pub negative_prompt: Option<String>,
|
||||
@@ -66,8 +73,14 @@ pub struct KinoviGenerateImageRequest {
|
||||
/// Midjourney "chaos" parameter. Valid range 0–100.
|
||||
pub chaos: Option<u8>,
|
||||
|
||||
/// Quality preset. `None` omits the field.
|
||||
pub quality: Option<KinoviMidjourneyQuality>,
|
||||
/// Quality preset, e.g. `"0.25"`, `"0.5"`, `"1"`. Parsed as an `f32` and
|
||||
/// emitted as a JSON number on the wire. `None` omits the field.
|
||||
///
|
||||
/// If the string isn't a valid `f32`, `generate_image()` returns a
|
||||
/// `Seedance2ProClientError::InvalidRequestField` error rather than
|
||||
/// panicking. The typed wrappers always supply well-formed values, so
|
||||
/// errors here only occur for callers that bypass them.
|
||||
pub quality: Option<String>,
|
||||
|
||||
/// Enables Midjourney "raw" style mode when true. Sent as `"style":"raw"`.
|
||||
pub raw_mode: bool,
|
||||
@@ -147,64 +160,6 @@ impl KinoviMidjourneyModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Output aspect ratio. Kinovi/Midjourney accepts a wide range; the values
|
||||
/// here cover every ratio seen in the captured request samples plus the
|
||||
/// common Midjourney-supported variants.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum KinoviMidjourneyAspectRatio {
|
||||
Square1x1,
|
||||
Landscape16x9,
|
||||
Portrait9x16,
|
||||
UltraWide21x9,
|
||||
UltraTall9x21,
|
||||
Standard4x3,
|
||||
Portrait3x4,
|
||||
Wide5x4,
|
||||
Tall4x5,
|
||||
Wide3x2,
|
||||
Tall2x3,
|
||||
}
|
||||
|
||||
impl KinoviMidjourneyAspectRatio {
|
||||
fn as_api_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Square1x1 => "1:1",
|
||||
Self::Landscape16x9 => "16:9",
|
||||
Self::Portrait9x16 => "9:16",
|
||||
Self::UltraWide21x9 => "21:9",
|
||||
Self::UltraTall9x21 => "9:21",
|
||||
Self::Standard4x3 => "4:3",
|
||||
Self::Portrait3x4 => "3:4",
|
||||
Self::Wide5x4 => "5:4",
|
||||
Self::Tall4x5 => "4:5",
|
||||
Self::Wide3x2 => "3:2",
|
||||
Self::Tall2x3 => "2:3",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Midjourney quality presets. Higher = more compute and slower; pricing is
|
||||
/// flat regardless.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum KinoviMidjourneyQuality {
|
||||
/// 0.25
|
||||
Quarter,
|
||||
/// 0.5
|
||||
Half,
|
||||
/// 1 (default)
|
||||
Full,
|
||||
}
|
||||
|
||||
impl KinoviMidjourneyQuality {
|
||||
fn as_api_value(&self) -> f32 {
|
||||
match self {
|
||||
Self::Quarter => 0.25,
|
||||
Self::Half => 0.5,
|
||||
Self::Full => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of distinct Midjourney generations to enqueue.
|
||||
/// Each task itself returns a 4-image grid; this knob just enqueues N tasks.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
@@ -257,19 +212,21 @@ pub async fn generate_image(args: GenerateImageArgs<'_>) -> Result<GenerateImage
|
||||
// wire — Midjourney rejects empty arrays.
|
||||
let uploaded_urls = req.reference_image_urls.filter(|urls| !urls.is_empty());
|
||||
|
||||
let quality = req.quality.as_deref().map(parse_quality).transpose()?;
|
||||
|
||||
let request_body = BatchRequest {
|
||||
zero: BatchRequestInner {
|
||||
json: BatchRequestJson {
|
||||
business_type: BUSINESS_TYPE,
|
||||
api_params: ApiParams {
|
||||
prompt: req.prompt,
|
||||
aspect_ratio: req.aspect_ratio.as_api_str(),
|
||||
aspect_ratio: req.aspect_ratio,
|
||||
resolution: MIDJOURNEY_RESOLUTION,
|
||||
model: req.model.as_api_str(),
|
||||
stylize: req.stylize,
|
||||
chaos: req.chaos,
|
||||
weird: req.weird,
|
||||
quality: req.quality.map(|q| q.as_api_value()),
|
||||
quality,
|
||||
style,
|
||||
negative_prompt: req.negative_prompt,
|
||||
batch_count,
|
||||
@@ -347,6 +304,20 @@ pub async fn generate_image(args: GenerateImageArgs<'_>) -> Result<GenerateImage
|
||||
})
|
||||
}
|
||||
|
||||
/// The wire format for `quality` is a JSON number, but the public API
|
||||
/// accepts a string so future models with different quality semantics can
|
||||
/// hand in whatever they like. Callers funnel through strongly-typed
|
||||
/// wrapper enums whose string output is always parseable; arbitrary
|
||||
/// callers might not, so we surface a structured client error instead of
|
||||
/// panicking.
|
||||
fn parse_quality(raw: &str) -> Result<f32, Seedance2ProClientError> {
|
||||
raw.parse::<f32>().map_err(|err| Seedance2ProClientError::InvalidRequestField {
|
||||
field: "quality",
|
||||
raw_value: raw.to_string(),
|
||||
reason: format!("not a valid f32: {}", err),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -360,7 +331,7 @@ mod tests {
|
||||
KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V7,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: "1:1".to_string(),
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
@@ -407,20 +378,11 @@ mod tests {
|
||||
#[test]
|
||||
fn pricing_is_aspect_ratio_agnostic() {
|
||||
let baseline = make_request(KinoviMidjourneyBatchCount::One).estimate_credits();
|
||||
for ar in [
|
||||
KinoviMidjourneyAspectRatio::Square1x1,
|
||||
KinoviMidjourneyAspectRatio::Landscape16x9,
|
||||
KinoviMidjourneyAspectRatio::Portrait9x16,
|
||||
KinoviMidjourneyAspectRatio::UltraWide21x9,
|
||||
KinoviMidjourneyAspectRatio::UltraTall9x21,
|
||||
KinoviMidjourneyAspectRatio::Standard4x3,
|
||||
KinoviMidjourneyAspectRatio::Portrait3x4,
|
||||
KinoviMidjourneyAspectRatio::Wide5x4,
|
||||
KinoviMidjourneyAspectRatio::Tall4x5,
|
||||
KinoviMidjourneyAspectRatio::Wide3x2,
|
||||
KinoviMidjourneyAspectRatio::Tall2x3,
|
||||
] {
|
||||
let req = KinoviGenerateImageRequest { aspect_ratio: ar, ..make_request(KinoviMidjourneyBatchCount::One) };
|
||||
for ar in ["1:1", "16:9", "9:16", "21:9", "9:21", "4:3", "3:4", "5:4", "4:5", "3:2", "2:3"] {
|
||||
let req = KinoviGenerateImageRequest {
|
||||
aspect_ratio: ar.to_string(),
|
||||
..make_request(KinoviMidjourneyBatchCount::One)
|
||||
};
|
||||
assert_eq!(req.estimate_credits(), baseline, "aspect_ratio={:?}", ar);
|
||||
}
|
||||
}
|
||||
@@ -433,7 +395,7 @@ mod tests {
|
||||
stylize: Some(1000),
|
||||
weird: Some(3000),
|
||||
chaos: Some(100),
|
||||
quality: Some(KinoviMidjourneyQuality::Full),
|
||||
quality: Some("1".to_string()),
|
||||
raw_mode: true,
|
||||
negative_prompt: Some("ugly, blurry".to_string()),
|
||||
..make_request(KinoviMidjourneyBatchCount::One)
|
||||
@@ -485,12 +447,17 @@ mod tests {
|
||||
use serde_json::Value;
|
||||
|
||||
fn build_wire_json(req: KinoviGenerateImageRequest) -> Value {
|
||||
try_build_wire_json(req).expect("test helper: malformed quality string")
|
||||
}
|
||||
|
||||
fn try_build_wire_json(req: KinoviGenerateImageRequest) -> Result<Value, Seedance2ProClientError> {
|
||||
// Mirrors the body assembly in `generate_image()` minus the actual
|
||||
// network call, so we can introspect the JSON the server would see.
|
||||
let batch_count_value = req.batch_count.as_u8();
|
||||
let batch_count = if batch_count_value > 1 { Some(batch_count_value) } else { None };
|
||||
let style = if req.raw_mode { Some("raw") } else { None };
|
||||
let uploaded_urls = req.reference_image_urls.filter(|urls| !urls.is_empty());
|
||||
let quality = req.quality.as_deref().map(parse_quality).transpose()?;
|
||||
|
||||
let request_body = BatchRequest {
|
||||
zero: BatchRequestInner {
|
||||
@@ -498,13 +465,13 @@ mod tests {
|
||||
business_type: BUSINESS_TYPE,
|
||||
api_params: ApiParams {
|
||||
prompt: req.prompt,
|
||||
aspect_ratio: req.aspect_ratio.as_api_str(),
|
||||
aspect_ratio: req.aspect_ratio,
|
||||
resolution: MIDJOURNEY_RESOLUTION,
|
||||
model: req.model.as_api_str(),
|
||||
stylize: req.stylize,
|
||||
chaos: req.chaos,
|
||||
weird: req.weird,
|
||||
quality: req.quality.map(|q| q.as_api_value()),
|
||||
quality,
|
||||
style,
|
||||
negative_prompt: req.negative_prompt,
|
||||
batch_count,
|
||||
@@ -513,7 +480,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
};
|
||||
serde_json::to_value(&request_body).unwrap()
|
||||
Ok(serde_json::to_value(&request_body).unwrap())
|
||||
}
|
||||
|
||||
fn api_params(req: KinoviGenerateImageRequest) -> Value {
|
||||
@@ -524,7 +491,7 @@ mod tests {
|
||||
KinoviGenerateImageRequest {
|
||||
model,
|
||||
prompt: prompt.to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: "1:1".to_string(),
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
@@ -578,24 +545,16 @@ mod tests {
|
||||
// ── Aspect ratio strings ──
|
||||
|
||||
#[test]
|
||||
fn all_aspect_ratios_serialize_correctly() {
|
||||
let cases = [
|
||||
(KinoviMidjourneyAspectRatio::Square1x1, "1:1"),
|
||||
(KinoviMidjourneyAspectRatio::Landscape16x9, "16:9"),
|
||||
(KinoviMidjourneyAspectRatio::Portrait9x16, "9:16"),
|
||||
(KinoviMidjourneyAspectRatio::UltraWide21x9, "21:9"),
|
||||
(KinoviMidjourneyAspectRatio::UltraTall9x21, "9:21"),
|
||||
(KinoviMidjourneyAspectRatio::Standard4x3, "4:3"),
|
||||
(KinoviMidjourneyAspectRatio::Portrait3x4, "3:4"),
|
||||
(KinoviMidjourneyAspectRatio::Wide5x4, "5:4"),
|
||||
(KinoviMidjourneyAspectRatio::Tall4x5, "4:5"),
|
||||
(KinoviMidjourneyAspectRatio::Wide3x2, "3:2"),
|
||||
(KinoviMidjourneyAspectRatio::Tall2x3, "2:3"),
|
||||
];
|
||||
for (ar, expected) in cases {
|
||||
let req = KinoviGenerateImageRequest { aspect_ratio: ar, ..minimal_request(KinoviMidjourneyModel::V7, "p") };
|
||||
fn aspect_ratio_passes_through_verbatim() {
|
||||
// The base API is stringly-typed for aspect_ratio — whatever the caller
|
||||
// (typically a per-model wrapper) hands in must appear on the wire as-is.
|
||||
for input in ["1:1", "16:9", "9:16", "21:9", "9:21", "4:3", "3:4", "5:4", "4:5", "3:2", "2:3"] {
|
||||
let req = KinoviGenerateImageRequest {
|
||||
aspect_ratio: input.to_string(),
|
||||
..minimal_request(KinoviMidjourneyModel::V7, "p")
|
||||
};
|
||||
let p = api_params(req);
|
||||
assert_eq!(p["aspectRatio"], expected, "aspect_ratio={:?}", ar);
|
||||
assert_eq!(p["aspectRatio"], input, "aspect_ratio={:?}", input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,25 +606,44 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn quality_quarter_serializes_as_0_25() {
|
||||
let req = KinoviGenerateImageRequest { quality: Some(KinoviMidjourneyQuality::Quarter), ..minimal_request(KinoviMidjourneyModel::V7, "p") };
|
||||
let req = KinoviGenerateImageRequest { quality: Some("0.25".to_string()), ..minimal_request(KinoviMidjourneyModel::V7, "p") };
|
||||
let q = api_params(req)["quality"].as_f64().unwrap();
|
||||
assert!((q - 0.25).abs() < 1e-6, "got {}", q);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_half_serializes_as_0_5() {
|
||||
let req = KinoviGenerateImageRequest { quality: Some(KinoviMidjourneyQuality::Half), ..minimal_request(KinoviMidjourneyModel::V7, "p") };
|
||||
let req = KinoviGenerateImageRequest { quality: Some("0.5".to_string()), ..minimal_request(KinoviMidjourneyModel::V7, "p") };
|
||||
let q = api_params(req)["quality"].as_f64().unwrap();
|
||||
assert!((q - 0.5).abs() < 1e-6, "got {}", q);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_full_serializes_as_1_0() {
|
||||
let req = KinoviGenerateImageRequest { quality: Some(KinoviMidjourneyQuality::Full), ..minimal_request(KinoviMidjourneyModel::V7, "p") };
|
||||
let req = KinoviGenerateImageRequest { quality: Some("1".to_string()), ..minimal_request(KinoviMidjourneyModel::V7, "p") };
|
||||
let q = api_params(req)["quality"].as_f64().unwrap();
|
||||
assert!((q - 1.0).abs() < 1e-6, "got {}", q);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_with_non_numeric_string_returns_client_error() {
|
||||
// The base function accepts strings; malformed input surfaces as a
|
||||
// structured `Seedance2ProClientError::InvalidRequestField` rather
|
||||
// than panicking.
|
||||
let req = KinoviGenerateImageRequest {
|
||||
quality: Some("not-a-number".to_string()),
|
||||
..minimal_request(KinoviMidjourneyModel::V7, "p")
|
||||
};
|
||||
let err = try_build_wire_json(req).expect_err("expected an error");
|
||||
match err {
|
||||
Seedance2ProClientError::InvalidRequestField { field, raw_value, .. } => {
|
||||
assert_eq!(field, "quality");
|
||||
assert_eq!(raw_value, "not-a-number");
|
||||
}
|
||||
other => panic!("expected InvalidRequestField, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Raw mode ──
|
||||
|
||||
#[test]
|
||||
@@ -737,12 +715,12 @@ mod tests {
|
||||
let req = KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V7,
|
||||
prompt: "abandoned skyscrapers".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: "1:1".to_string(),
|
||||
negative_prompt: Some("dark, gloomy, night".to_string()),
|
||||
stylize: Some(1000),
|
||||
weird: Some(3000),
|
||||
chaos: Some(100),
|
||||
quality: Some(KinoviMidjourneyQuality::Half),
|
||||
quality: Some("0.5".to_string()),
|
||||
raw_mode: true,
|
||||
batch_count: KinoviMidjourneyBatchCount::One,
|
||||
reference_image_urls: None,
|
||||
@@ -760,12 +738,12 @@ mod tests {
|
||||
let req = KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V8,
|
||||
prompt: "desolate cliff overlooking the ocean".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Portrait9x16,
|
||||
aspect_ratio: "9:16".to_string(),
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
chaos: None,
|
||||
quality: Some(KinoviMidjourneyQuality::Full),
|
||||
quality: Some("1".to_string()),
|
||||
raw_mode: false,
|
||||
batch_count: KinoviMidjourneyBatchCount::Four,
|
||||
reference_image_urls: None,
|
||||
@@ -804,7 +782,7 @@ mod tests {
|
||||
request: KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V7,
|
||||
prompt: "A corgi astronaut floating among stars".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: "1:1".to_string(),
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
@@ -834,7 +812,7 @@ mod tests {
|
||||
request: KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V7Niji,
|
||||
prompt: "A magical shiba inu sorcerer casting spells in a crystal cave".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::UltraWide21x9,
|
||||
aspect_ratio: "21:9".to_string(),
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
@@ -863,12 +841,12 @@ mod tests {
|
||||
request: KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V8,
|
||||
prompt: "A magical shiba inu sorcerer casting spells in a crystal cave".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Square1x1,
|
||||
aspect_ratio: "1:1".to_string(),
|
||||
negative_prompt: Some("dark, gloomy".to_string()),
|
||||
stylize: Some(730),
|
||||
weird: Some(2050),
|
||||
chaos: Some(70),
|
||||
quality: Some(KinoviMidjourneyQuality::Half),
|
||||
quality: Some("0.5".to_string()),
|
||||
raw_mode: true,
|
||||
batch_count: KinoviMidjourneyBatchCount::One,
|
||||
reference_image_urls: None,
|
||||
@@ -892,12 +870,12 @@ mod tests {
|
||||
request: KinoviGenerateImageRequest {
|
||||
model: KinoviMidjourneyModel::V8,
|
||||
prompt: "desolate cliff overlooking the ocean".to_string(),
|
||||
aspect_ratio: KinoviMidjourneyAspectRatio::Portrait9x16,
|
||||
aspect_ratio: "9:16".to_string(),
|
||||
negative_prompt: None,
|
||||
stylize: None,
|
||||
weird: None,
|
||||
chaos: None,
|
||||
quality: Some(KinoviMidjourneyQuality::Full),
|
||||
quality: Some("1".to_string()),
|
||||
raw_mode: false,
|
||||
batch_count: KinoviMidjourneyBatchCount::Four,
|
||||
reference_image_urls: None,
|
||||
|
||||
@@ -23,7 +23,7 @@ pub (super) struct BatchRequestJson {
|
||||
pub (super) struct ApiParams {
|
||||
pub prompt: String,
|
||||
#[serde(rename = "aspectRatio")]
|
||||
pub aspect_ratio: &'static str,
|
||||
pub aspect_ratio: String,
|
||||
pub resolution: &'static str,
|
||||
pub model: &'static str,
|
||||
|
||||
|
||||
Reference in new issue
Block a user