mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-27 12:10:08 -04:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47c4e2d8f0 | ||
|
|
63a4779094 | ||
|
|
2c9c29066a | ||
|
|
839cb56b9d |
-11
@@ -183,17 +183,6 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let tokens = vec![make_token()];
|
||||
|
||||
-11
@@ -185,17 +185,6 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let tokens = vec![make_token()];
|
||||
|
||||
+97
-80
@@ -65,25 +65,27 @@ mod tests {
|
||||
|
||||
fn text_to_image_price_cases() -> Vec<TextPriceCase> {
|
||||
let mut cases = vec![
|
||||
(None, None, None, [22, 44, 66, 88]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None, [2, 3, 4, 5]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), None, [2, 3, 4, 5]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), None, [2, 3, 5, 6]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), None, [2, 4, 6, 7]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), None, [2, 4, 6, 7]),
|
||||
// Default: High quality, Square
|
||||
(None, None, None, [16, 32, 48, 64]),
|
||||
// Presets (no resolution)
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), None, [5, 9, 13, 18]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), None, [5, 9, 13, 18]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), None, [7, 13, 19, 25]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), None, [6, 11, 16, 22]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), None, [6, 11, 16, 22]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), None, [7, 14, 21, 28]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), None, [16, 31, 46, 61]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), None, [16, 31, 46, 61]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), None, [22, 44, 66, 88]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), None, [24, 47, 71, 94]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), None, [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), None, [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), None, [8, 16, 24, 32]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), None, [20, 40, 60, 80]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), None, [20, 40, 60, 80]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), None, [27, 54, 81, 108]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in text_to_image_resolution_price_cases() {
|
||||
@@ -97,31 +99,34 @@ mod tests {
|
||||
|
||||
fn edit_image_price_cases() -> Vec<EditPriceCase> {
|
||||
let mut cases = vec![
|
||||
(None, None, None, [22, 44, 66, 88]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), None, [3, 5, 8, 10]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), None, [12, 23, 34, 46]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), None, [42, 83, 124, 166]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [3, 5, 8, 10]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [12, 23, 34, 46]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [42, 83, 124, 166]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None, [2, 3, 4, 5]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), None, [2, 3, 4, 5]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), None, [2, 3, 5, 6]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), None, [2, 4, 6, 7]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), None, [2, 4, 6, 7]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), None, [5, 9, 13, 18]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), None, [5, 9, 13, 18]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), None, [7, 13, 19, 25]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), None, [6, 11, 16, 22]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), None, [6, 11, 16, 22]),
|
||||
// Default: High quality, Square
|
||||
(None, None, None, [15, 30, 45, 60]),
|
||||
// Auto (with and without resolution — same cost, conservative max)
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), None, [10, 20, 30, 40]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), None, [41, 82, 123, 164]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [10, 20, 30, 40]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [41, 82, 123, 164]),
|
||||
// Presets (no resolution)
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd), None, [7, 14, 21, 28]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), None, [16, 31, 46, 61]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), None, [16, 31, 46, 61]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), None, [22, 44, 66, 88]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), None, [24, 47, 71, 94]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), None, [14, 28, 42, 56]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), None, [14, 28, 42, 56]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), None, [19, 38, 57, 76]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), None, [19, 38, 57, 76]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), None, [26, 52, 78, 104]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in edit_image_resolution_price_cases() {
|
||||
@@ -135,48 +140,60 @@ mod tests {
|
||||
|
||||
fn text_to_image_resolution_price_cases() -> Vec<(GptImage2TextToImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
vec![
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, [2, 4, 6, 7], [6, 11, 16, 22], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, [2, 3, 4, 5], [5, 9, 13, 18], [16, 31, 46, 61]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, [2, 4, 6, 7], [6, 11, 16, 22], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, [2, 3, 5, 6], [7, 13, 19, 25], [22, 44, 66, 88]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, [2, 3, 5, 6], [7, 13, 19, 25], [22, 44, 66, 88]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 47, 71, 94]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, [3, 5, 8, 10], [12, 23, 34, 46], [42, 83, 124, 166]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
]
|
||||
}
|
||||
|
||||
fn edit_image_resolution_price_cases() -> Vec<(GptImage2EditImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
text_to_image_resolution_price_cases()
|
||||
.into_iter()
|
||||
.map(|(size, resolution, low, medium, high)| {
|
||||
let edit_size = match size {
|
||||
GptImage2TextToImageSize::SquareHd => GptImage2EditImageSize::SquareHd,
|
||||
GptImage2TextToImageSize::Square => GptImage2EditImageSize::Square,
|
||||
GptImage2TextToImageSize::Portrait4x3 => GptImage2EditImageSize::Portrait4x3,
|
||||
GptImage2TextToImageSize::Portrait16x9 => GptImage2EditImageSize::Portrait16x9,
|
||||
GptImage2TextToImageSize::Landscape4x3 => GptImage2EditImageSize::Landscape4x3,
|
||||
GptImage2TextToImageSize::Landscape16x9 => GptImage2EditImageSize::Landscape16x9,
|
||||
};
|
||||
(edit_size, resolution, low, medium, high)
|
||||
})
|
||||
.collect()
|
||||
vec![
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
]
|
||||
}
|
||||
|
||||
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage2TextToImageNumImages, u64); 4] {
|
||||
|
||||
+379
-154
@@ -1,67 +1,60 @@
|
||||
use crate::requests::api::image::common::gpt_image_2_resolution::{compute_custom_image_size, GptImage2AspectRatio, GptImage2Resolution};
|
||||
use crate::requests::api::image::edit::gpt_image_2_edit_image::api::{GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageRequest, GptImage2EditImageSize};
|
||||
use crate::requests::api::image::common::gpt_image_2_resolution::{
|
||||
compute_custom_image_size, GptImage2AspectRatio, GptImage2Resolution,
|
||||
};
|
||||
use crate::requests::api::image::edit::gpt_image_2_edit_image::api::{
|
||||
GptImage2EditImageNumImages, GptImage2EditImageQuality,
|
||||
GptImage2EditImageRequest, GptImage2EditImageSize,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
|
||||
const PRICING_TABLE: &[GptImage2PriceRow] = &[GptImage2PriceRow { width: 1024, height: 768, low: 11, medium: 43, high: 151 }, GptImage2PriceRow { width: 1024, height: 1024, low: 15, medium: 61, high: 219 }, GptImage2PriceRow { width: 1024, height: 1536, low: 18, medium: 54, high: 178 }, GptImage2PriceRow { width: 1920, height: 1080, low: 17, medium: 53, high: 158 }, GptImage2PriceRow { width: 2560, height: 1440, low: 19, medium: 68, high: 234 }, GptImage2PriceRow { width: 3840, height: 2160, low: 24, medium: 113, high: 413 }];
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct GptImage2PriceRow {
|
||||
width: u32,
|
||||
height: u32,
|
||||
low: u64,
|
||||
medium: u64,
|
||||
high: u64,
|
||||
}
|
||||
|
||||
impl GptImage2PriceRow {
|
||||
fn price_in_tenths_of_a_cent(self, quality: GptImage2EditImageQuality) -> u64 {
|
||||
match quality {
|
||||
GptImage2EditImageQuality::Low => self.low,
|
||||
GptImage2EditImageQuality::Medium => self.medium,
|
||||
GptImage2EditImageQuality::High => self.high,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalRequestCostCalculator for GptImage2EditImageRequest {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
let quality = self.quality.unwrap_or(GptImage2EditImageQuality::High);
|
||||
let price = match dimensions_for_request(self) {
|
||||
Some((width, height)) => closest_price_row(width, height).price_in_tenths_of_a_cent(quality),
|
||||
None => max_price_for_quality(quality),
|
||||
let total_pixels = estimate_total_pixels(self.image_size, self.resolution);
|
||||
let megapixels = total_pixels as f64 / 1_000_000.0;
|
||||
|
||||
// Fal's GPT Image 2 edit-image pricing has two components:
|
||||
//
|
||||
// 1. A base request cost (prompt processing, API overhead)
|
||||
// 2. A per-pixel generation cost that scales with output resolution
|
||||
//
|
||||
// Both components increase with quality level. All rates are in
|
||||
// tenths of a US cent, derived from Fal's published pricing.
|
||||
//
|
||||
// Edit-image is slightly cheaper than text-to-image at the base level,
|
||||
// but scales at a similar per-pixel rate at medium and high quality.
|
||||
let (base_tenths, per_megapixel_tenths): (f64, f64) = match quality {
|
||||
GptImage2EditImageQuality::Low => ( 3.0, 1.0),
|
||||
GptImage2EditImageQuality::Medium => (25.0, 9.0),
|
||||
GptImage2EditImageQuality::High => (110.0, 35.0),
|
||||
};
|
||||
let total_tenths_of_a_cent = price * num_images(self.num_images);
|
||||
|
||||
total_tenths_of_a_cent.div_ceil(10) as UsdCents
|
||||
let tenths_per_image = (base_tenths + per_megapixel_tenths * megapixels).ceil() as u64;
|
||||
let cents_per_image = tenths_per_image.div_ceil(10);
|
||||
let num = num_images_u64(self.num_images);
|
||||
|
||||
cents_per_image * num
|
||||
}
|
||||
}
|
||||
|
||||
fn dimensions_for_request(request: &GptImage2EditImageRequest) -> Option<(u32, u32)> {
|
||||
match (request.image_size, request.resolution) {
|
||||
(Some(GptImage2EditImageSize::Auto), _) => None,
|
||||
(Some(size), Some(resolution)) => {
|
||||
fn estimate_total_pixels(
|
||||
image_size: Option<GptImage2EditImageSize>,
|
||||
resolution: Option<GptImage2Resolution>,
|
||||
) -> u64 {
|
||||
match (image_size, resolution) {
|
||||
// Auto: no meaningful aspect ratio, use conservative max-size estimate
|
||||
(Some(GptImage2EditImageSize::Auto), _) => 3840 * 2160, // 8,294,400
|
||||
(Some(size), Some(res)) => {
|
||||
let aspect = size_to_aspect(size);
|
||||
let custom = compute_custom_image_size(aspect, resolution);
|
||||
Some((custom.width, custom.height))
|
||||
},
|
||||
(None, Some(resolution)) => {
|
||||
let custom = compute_custom_image_size(GptImage2AspectRatio::Square, resolution);
|
||||
Some((custom.width, custom.height))
|
||||
},
|
||||
(Some(size), None) => Some(preset_dimensions(size)),
|
||||
(None, None) => Some(preset_dimensions(GptImage2EditImageSize::Square)),
|
||||
}
|
||||
}
|
||||
|
||||
fn preset_dimensions(size: GptImage2EditImageSize) -> (u32, u32) {
|
||||
match size {
|
||||
GptImage2EditImageSize::SquareHd => (2048, 2048),
|
||||
GptImage2EditImageSize::Square => (1024, 1024),
|
||||
GptImage2EditImageSize::Portrait4x3 => (768, 1024),
|
||||
GptImage2EditImageSize::Portrait16x9 => (1080, 1920),
|
||||
GptImage2EditImageSize::Landscape4x3 => (1024, 768),
|
||||
GptImage2EditImageSize::Landscape16x9 => (1920, 1080),
|
||||
GptImage2EditImageSize::Auto => (3840, 2160),
|
||||
let dims = compute_custom_image_size(aspect, res);
|
||||
dims.width as u64 * dims.height as u64
|
||||
}
|
||||
(None, Some(res)) => {
|
||||
let dims = compute_custom_image_size(GptImage2AspectRatio::Square, res);
|
||||
dims.width as u64 * dims.height as u64
|
||||
}
|
||||
(Some(size), None) => preset_pixel_count(size),
|
||||
(None, None) => preset_pixel_count(GptImage2EditImageSize::Square),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,24 +70,21 @@ fn size_to_aspect(size: GptImage2EditImageSize) -> GptImage2AspectRatio {
|
||||
}
|
||||
}
|
||||
|
||||
fn closest_price_row(width: u32, height: u32) -> GptImage2PriceRow {
|
||||
let pixels = width as u64 * height as u64;
|
||||
PRICING_TABLE
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|row| {
|
||||
let row_pixels = row.width as u64 * row.height as u64;
|
||||
row_pixels.abs_diff(pixels)
|
||||
})
|
||||
.expect("GPT Image 2 pricing table cannot be empty")
|
||||
/// Standard preset pixel counts when no custom resolution is specified.
|
||||
fn preset_pixel_count(size: GptImage2EditImageSize) -> u64 {
|
||||
match size {
|
||||
GptImage2EditImageSize::Square => 1024 * 1024, // 1,048,576
|
||||
GptImage2EditImageSize::SquareHd => 2048 * 2048, // 4,194,304
|
||||
GptImage2EditImageSize::Landscape4x3 => 1024 * 768, // 786,432
|
||||
GptImage2EditImageSize::Landscape16x9 => 1920 * 1080, // 2,073,600
|
||||
GptImage2EditImageSize::Portrait4x3 => 768 * 1024, // 786,432
|
||||
GptImage2EditImageSize::Portrait16x9 => 1080 * 1920, // 2,073,600
|
||||
GptImage2EditImageSize::Auto => 3840 * 2160, // 8,294,400 (conservative)
|
||||
}
|
||||
}
|
||||
|
||||
fn max_price_for_quality(quality: GptImage2EditImageQuality) -> u64 {
|
||||
PRICING_TABLE.iter().copied().map(|row| row.price_in_tenths_of_a_cent(quality)).max().expect("GPT Image 2 pricing table cannot be empty")
|
||||
}
|
||||
|
||||
fn num_images(num_images: GptImage2EditImageNumImages) -> u64 {
|
||||
match num_images {
|
||||
fn num_images_u64(num: GptImage2EditImageNumImages) -> u64 {
|
||||
match num {
|
||||
GptImage2EditImageNumImages::One => 1,
|
||||
GptImage2EditImageNumImages::Two => 2,
|
||||
GptImage2EditImageNumImages::Three => 3,
|
||||
@@ -106,113 +96,348 @@ fn num_images(num_images: GptImage2EditImageNumImages) -> u64 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const QUALITIES: &[(GptImage2EditImageQuality, u64, u64, u64, u64, u64, u64)] = &[(GptImage2EditImageQuality::Low, 11, 15, 18, 17, 19, 24), (GptImage2EditImageQuality::Medium, 43, 61, 54, 53, 68, 113), (GptImage2EditImageQuality::High, 151, 219, 178, 158, 234, 413)];
|
||||
|
||||
const PRESET_CASES: &[(GptImage2EditImageSize, u64)] = &[(GptImage2EditImageSize::Landscape4x3, 0), (GptImage2EditImageSize::Portrait4x3, 0), (GptImage2EditImageSize::Square, 1), (GptImage2EditImageSize::Landscape16x9, 3), (GptImage2EditImageSize::Portrait16x9, 3), (GptImage2EditImageSize::SquareHd, 4)];
|
||||
|
||||
const RESOLUTION_CASES: &[(GptImage2EditImageSize, GptImage2Resolution, u64)] = &[
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::OneK, 0),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::TwoK, 4),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::ThreeK, 5),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::OneK, 0),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::TwoK, 3),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::ThreeK, 4),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::OneK, 0),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::TwoK, 4),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::ThreeK, 5),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::OneK, 0),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::TwoK, 3),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::ThreeK, 4),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::OneK, 1),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::TwoK, 4),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::ThreeK, 5),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::OneK, 1),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::TwoK, 4),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::ThreeK, 5),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::FourK, 5),
|
||||
];
|
||||
|
||||
const SPOT_PRICE_CASES: &[(GptImage2EditImageSize, GptImage2Resolution, GptImage2EditImageNumImages, u64)] = &[
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::OneK, GptImage2EditImageNumImages::One, 16),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::TwoK, GptImage2EditImageNumImages::Two, 47),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::ThreeK, GptImage2EditImageNumImages::Three, 124),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::TwoK, GptImage2EditImageNumImages::Four, 64),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::FourK, GptImage2EditImageNumImages::Two, 83),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::TwoK, GptImage2EditImageNumImages::One, 24),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::ThreeK, GptImage2EditImageNumImages::Three, 71),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::OneK, GptImage2EditImageNumImages::Four, 88),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::FourK, GptImage2EditImageNumImages::Four, 166),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::TwoK, GptImage2EditImageNumImages::Three, 71),
|
||||
];
|
||||
|
||||
fn make_request(num_images: GptImage2EditImageNumImages, quality: Option<GptImage2EditImageQuality>, image_size: Option<GptImage2EditImageSize>, resolution: Option<GptImage2Resolution>) -> GptImage2EditImageRequest {
|
||||
GptImage2EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_url: None, image_size, quality, resolution, output_format: None }
|
||||
fn make_request(
|
||||
num_images: GptImage2EditImageNumImages,
|
||||
quality: Option<GptImage2EditImageQuality>,
|
||||
image_size: Option<GptImage2EditImageSize>,
|
||||
resolution: Option<GptImage2Resolution>,
|
||||
) -> GptImage2EditImageRequest {
|
||||
GptImage2EditImageRequest {
|
||||
prompt: "test".to_string(),
|
||||
image_urls: vec!["https://example.com/image.png".to_string()],
|
||||
num_images,
|
||||
mask_url: None,
|
||||
image_size,
|
||||
resolution,
|
||||
quality,
|
||||
output_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_to_high_square() {
|
||||
assert_eq!(make_request(GptImage2EditImageNumImages::One, None, None, None).calculate_cost_in_cents(), 22);
|
||||
use GptImage2EditImageNumImages::*;
|
||||
use GptImage2EditImageQuality::*;
|
||||
use GptImage2EditImageSize::*;
|
||||
use GptImage2Resolution::*;
|
||||
|
||||
mod preset_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
// Table: (size, low, medium, high) — expected cents per image
|
||||
const PRESET_CASES: &[(GptImage2EditImageSize, u64, u64, u64)] = &[
|
||||
// Low Med High
|
||||
(Landscape4x3, 1, 4, 14),
|
||||
(Portrait4x3, 1, 4, 14),
|
||||
(Square, 1, 4, 15),
|
||||
(Landscape16x9, 1, 5, 19),
|
||||
(Portrait16x9, 1, 5, 19),
|
||||
(SquareHd, 1, 7, 26),
|
||||
(Auto, 2, 10, 41),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn preset_costs_at_each_quality() {
|
||||
for &(size, expected_low, expected_med, expected_high) in PRESET_CASES {
|
||||
let cases = [
|
||||
(Low, expected_low),
|
||||
(Medium, expected_med),
|
||||
(High, expected_high),
|
||||
];
|
||||
for (quality, expected) in cases {
|
||||
let actual = make_request(One, Some(quality), Some(size), None)
|
||||
.calculate_cost_in_cents();
|
||||
assert_eq!(actual, expected, "{size:?} {quality:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_to_high_quality_square() {
|
||||
assert_eq!(
|
||||
make_request(One, None, None, None).calculate_cost_in_cents(),
|
||||
15,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_rounds_final_request_total_up_to_whole_cents() {
|
||||
assert_eq!(make_request(GptImage2EditImageNumImages::Four, Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None,).calculate_cost_in_cents(), 5,);
|
||||
mod custom_resolution_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
// Table: (size, resolution, low, medium, high) — expected cents per image
|
||||
const RESOLUTION_CASES: &[(GptImage2EditImageSize, GptImage2Resolution, u64, u64, u64)] = &[
|
||||
// Square aspect
|
||||
(Square, OneK, 1, 4, 15),
|
||||
(Square, TwoK, 1, 7, 26),
|
||||
(Square, ThreeK, 2, 10, 41),
|
||||
(Square, FourK, 2, 10, 41), // capped at max pixels
|
||||
|
||||
// SquareHd (same aspect as Square)
|
||||
(SquareHd, OneK, 1, 4, 15),
|
||||
(SquareHd, TwoK, 1, 7, 26),
|
||||
(SquareHd, ThreeK, 2, 10, 41),
|
||||
(SquareHd, FourK, 2, 10, 41),
|
||||
|
||||
// Landscape 4:3
|
||||
(Landscape4x3, OneK, 1, 4, 14),
|
||||
(Landscape4x3, TwoK, 1, 6, 23),
|
||||
(Landscape4x3, ThreeK, 2, 9, 36),
|
||||
(Landscape4x3, FourK, 2, 10, 40),
|
||||
|
||||
// Landscape 16:9
|
||||
(Landscape16x9, OneK, 1, 4, 14),
|
||||
(Landscape16x9, TwoK, 1, 5, 20),
|
||||
(Landscape16x9, ThreeK, 1, 8, 30),
|
||||
(Landscape16x9, FourK, 2, 10, 41),
|
||||
|
||||
// Portrait 4:3
|
||||
(Portrait4x3, OneK, 1, 4, 14),
|
||||
(Portrait4x3, TwoK, 1, 6, 23),
|
||||
(Portrait4x3, ThreeK, 2, 9, 36),
|
||||
(Portrait4x3, FourK, 2, 10, 40),
|
||||
|
||||
// Portrait 16:9
|
||||
(Portrait16x9, OneK, 1, 4, 14),
|
||||
(Portrait16x9, TwoK, 1, 5, 20),
|
||||
(Portrait16x9, ThreeK, 1, 8, 30),
|
||||
(Portrait16x9, FourK, 2, 10, 41),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn resolution_costs_at_each_quality() {
|
||||
for &(size, res, expected_low, expected_med, expected_high) in RESOLUTION_CASES {
|
||||
let cases = [
|
||||
(Low, expected_low),
|
||||
(Medium, expected_med),
|
||||
(High, expected_high),
|
||||
];
|
||||
for (quality, expected) in cases {
|
||||
let actual = make_request(One, Some(quality), Some(size), Some(res))
|
||||
.calculate_cost_in_cents();
|
||||
assert_eq!(actual, expected, "{size:?} {res:?} {quality:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_without_size_defaults_to_square() {
|
||||
assert_eq!(
|
||||
make_request(One, Some(High), None, Some(TwoK)).calculate_cost_in_cents(),
|
||||
make_request(One, Some(High), Some(Square), Some(TwoK)).calculate_cost_in_cents(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_ignores_resolution_and_uses_max_estimate() {
|
||||
// Auto always uses the conservative 3840x2160 estimate
|
||||
let auto_cost = make_request(One, Some(High), Some(Auto), Some(OneK))
|
||||
.calculate_cost_in_cents();
|
||||
let auto_no_res = make_request(One, Some(High), Some(Auto), None)
|
||||
.calculate_cost_in_cents();
|
||||
assert_eq!(auto_cost, auto_no_res);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_pricing_matches_published_table() {
|
||||
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
|
||||
let expected_by_row = [landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k];
|
||||
mod num_images_tests {
|
||||
use super::*;
|
||||
|
||||
for &(size, row_index) in PRESET_CASES {
|
||||
let expected = expected_by_row[row_index as usize].div_ceil(10);
|
||||
assert_eq!(make_request(GptImage2EditImageNumImages::One, Some(quality), Some(size), None).calculate_cost_in_cents(), expected, "quality={quality:?} size={size:?}",);
|
||||
const ALL_BATCH_SIZES: &[(GptImage2EditImageNumImages, u64)] = &[
|
||||
(One, 1), (Two, 2), (Three, 3), (Four, 4),
|
||||
];
|
||||
|
||||
const ALL_SIZES: &[GptImage2EditImageSize] = &[
|
||||
Square, SquareHd, Landscape4x3, Landscape16x9, Portrait4x3, Portrait16x9, Auto,
|
||||
];
|
||||
|
||||
const ALL_RESOLUTIONS: &[GptImage2Resolution] = &[OneK, TwoK, ThreeK, FourK];
|
||||
|
||||
#[test]
|
||||
fn batch_scales_linearly_for_all_presets() {
|
||||
for &size in ALL_SIZES {
|
||||
let per_image = make_request(One, Some(High), Some(size), None)
|
||||
.calculate_cost_in_cents();
|
||||
for &(num, n) in ALL_BATCH_SIZES {
|
||||
let actual = make_request(num, Some(High), Some(size), None)
|
||||
.calculate_cost_in_cents();
|
||||
assert_eq!(actual, per_image * n, "{size:?} x{n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_scales_linearly_for_all_resolutions() {
|
||||
for &size in &[Square, SquareHd, Landscape4x3, Landscape16x9, Portrait4x3, Portrait16x9] {
|
||||
for &res in ALL_RESOLUTIONS {
|
||||
let per_image = make_request(One, Some(High), Some(size), Some(res))
|
||||
.calculate_cost_in_cents();
|
||||
for &(num, n) in ALL_BATCH_SIZES {
|
||||
let actual = make_request(num, Some(High), Some(size), Some(res))
|
||||
.calculate_cost_in_cents();
|
||||
assert_eq!(actual, per_image * n, "{size:?} {res:?} x{n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_published_rows_match_all_quality_prices() {
|
||||
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
|
||||
let cases = [(1024, 768, landscape_4x3), (1024, 1024, square), (1024, 1536, portrait), (1920, 1080, landscape_16x9), (2560, 1440, square_hd), (3840, 2160, four_k)];
|
||||
mod exhaustive_preset_batch_tests {
|
||||
use super::*;
|
||||
|
||||
for (width, height, expected) in cases {
|
||||
assert_eq!(closest_price_row(width, height).price_in_tenths_of_a_cent(quality), expected, "quality={quality:?} dimensions={width}x{height}",);
|
||||
// (size, quality, num_images, expected_cents)
|
||||
const CASES: &[(GptImage2EditImageSize, GptImage2EditImageQuality, GptImage2EditImageNumImages, u64)] = &[
|
||||
(Square, Low, One, 1), (Square, Low, Two, 2), (Square, Low, Three, 3), (Square, Low, Four, 4),
|
||||
(Square, Medium, One, 4), (Square, Medium, Two, 8), (Square, Medium, Three, 12), (Square, Medium, Four, 16),
|
||||
(Square, High, One, 15), (Square, High, Two, 30), (Square, High, Three, 45), (Square, High, Four, 60),
|
||||
(SquareHd, Low, One, 1), (SquareHd, Low, Two, 2), (SquareHd, Low, Three, 3), (SquareHd, Low, Four, 4),
|
||||
(SquareHd, Medium, One, 7), (SquareHd, Medium, Two, 14), (SquareHd, Medium, Three, 21), (SquareHd, Medium, Four, 28),
|
||||
(SquareHd, High, One, 26), (SquareHd, High, Two, 52), (SquareHd, High, Three, 78), (SquareHd, High, Four, 104),
|
||||
(Landscape4x3, Low, One, 1), (Landscape4x3, Low, Two, 2), (Landscape4x3, Low, Three, 3), (Landscape4x3, Low, Four, 4),
|
||||
(Landscape4x3, Medium, One, 4), (Landscape4x3, Medium, Two, 8), (Landscape4x3, Medium, Three, 12), (Landscape4x3, Medium, Four, 16),
|
||||
(Landscape4x3, High, One, 14), (Landscape4x3, High, Two, 28), (Landscape4x3, High, Three, 42), (Landscape4x3, High, Four, 56),
|
||||
(Landscape16x9, Low, One, 1), (Landscape16x9, Low, Two, 2), (Landscape16x9, Low, Three, 3), (Landscape16x9, Low, Four, 4),
|
||||
(Landscape16x9, Medium, One, 5), (Landscape16x9, Medium, Two, 10), (Landscape16x9, Medium, Three, 15), (Landscape16x9, Medium, Four, 20),
|
||||
(Landscape16x9, High, One, 19), (Landscape16x9, High, Two, 38), (Landscape16x9, High, Three, 57), (Landscape16x9, High, Four, 76),
|
||||
(Portrait4x3, Low, One, 1), (Portrait4x3, Low, Two, 2), (Portrait4x3, Low, Three, 3), (Portrait4x3, Low, Four, 4),
|
||||
(Portrait4x3, Medium, One, 4), (Portrait4x3, Medium, Two, 8), (Portrait4x3, Medium, Three, 12), (Portrait4x3, Medium, Four, 16),
|
||||
(Portrait4x3, High, One, 14), (Portrait4x3, High, Two, 28), (Portrait4x3, High, Three, 42), (Portrait4x3, High, Four, 56),
|
||||
(Portrait16x9, Low, One, 1), (Portrait16x9, Low, Two, 2), (Portrait16x9, Low, Three, 3), (Portrait16x9, Low, Four, 4),
|
||||
(Portrait16x9, Medium, One, 5), (Portrait16x9, Medium, Two, 10), (Portrait16x9, Medium, Three, 15), (Portrait16x9, Medium, Four, 20),
|
||||
(Portrait16x9, High, One, 19), (Portrait16x9, High, Two, 38), (Portrait16x9, High, Three, 57), (Portrait16x9, High, Four, 76),
|
||||
(Auto, Low, One, 2), (Auto, Low, Two, 4), (Auto, Low, Three, 6), (Auto, Low, Four, 8),
|
||||
(Auto, Medium, One, 10), (Auto, Medium, Two, 20), (Auto, Medium, Three, 30), (Auto, Medium, Four, 40),
|
||||
(Auto, High, One, 41), (Auto, High, Two, 82), (Auto, High, Three, 123), (Auto, High, Four, 164),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn all_preset_batch_costs() {
|
||||
for &(size, quality, num, expected) in CASES {
|
||||
let actual = make_request(num, Some(quality), Some(size), None).calculate_cost_in_cents();
|
||||
assert_eq!(actual, expected, "{size:?} {quality:?} {num:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_pricing_covers_aspect_size_resolution_combinations() {
|
||||
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
|
||||
let expected_by_row = [landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k];
|
||||
mod exhaustive_resolution_batch_tests {
|
||||
use super::*;
|
||||
|
||||
for &(size, resolution, row_index) in RESOLUTION_CASES {
|
||||
let expected = expected_by_row[row_index as usize].div_ceil(10);
|
||||
assert_eq!(make_request(GptImage2EditImageNumImages::One, Some(quality), Some(size), Some(resolution)).calculate_cost_in_cents(), expected, "quality={quality:?} size={size:?} resolution={resolution:?}",);
|
||||
// (size, resolution, quality, num_images, expected_cents)
|
||||
const CASES: &[(GptImage2EditImageSize, GptImage2Resolution, GptImage2EditImageQuality, GptImage2EditImageNumImages, u64)] = &[
|
||||
// Square
|
||||
(Square, OneK, Low, One, 1), (Square, OneK, Low, Two, 2), (Square, OneK, Low, Three, 3), (Square, OneK, Low, Four, 4),
|
||||
(Square, OneK, Medium, One, 4), (Square, OneK, Medium, Two, 8), (Square, OneK, Medium, Three, 12), (Square, OneK, Medium, Four, 16),
|
||||
(Square, OneK, High, One, 15), (Square, OneK, High, Two, 30), (Square, OneK, High, Three, 45), (Square, OneK, High, Four, 60),
|
||||
(Square, TwoK, Low, One, 1), (Square, TwoK, Low, Two, 2), (Square, TwoK, Low, Three, 3), (Square, TwoK, Low, Four, 4),
|
||||
(Square, TwoK, Medium, One, 7), (Square, TwoK, Medium, Two, 14), (Square, TwoK, Medium, Three, 21), (Square, TwoK, Medium, Four, 28),
|
||||
(Square, TwoK, High, One, 26), (Square, TwoK, High, Two, 52), (Square, TwoK, High, Three, 78), (Square, TwoK, High, Four, 104),
|
||||
(Square, ThreeK, Low, One, 2), (Square, ThreeK, Low, Two, 4), (Square, ThreeK, Low, Three, 6), (Square, ThreeK, Low, Four, 8),
|
||||
(Square, ThreeK, Medium, One, 10), (Square, ThreeK, Medium, Two, 20), (Square, ThreeK, Medium, Three, 30), (Square, ThreeK, Medium, Four, 40),
|
||||
(Square, ThreeK, High, One, 41), (Square, ThreeK, High, Two, 82), (Square, ThreeK, High, Three, 123), (Square, ThreeK, High, Four, 164),
|
||||
(Square, FourK, Low, One, 2), (Square, FourK, Low, Two, 4), (Square, FourK, Low, Three, 6), (Square, FourK, Low, Four, 8),
|
||||
(Square, FourK, Medium, One, 10), (Square, FourK, Medium, Two, 20), (Square, FourK, Medium, Three, 30), (Square, FourK, Medium, Four, 40),
|
||||
(Square, FourK, High, One, 41), (Square, FourK, High, Two, 82), (Square, FourK, High, Three, 123), (Square, FourK, High, Four, 164),
|
||||
// SquareHd
|
||||
(SquareHd, OneK, Low, One, 1), (SquareHd, OneK, Low, Two, 2), (SquareHd, OneK, Low, Three, 3), (SquareHd, OneK, Low, Four, 4),
|
||||
(SquareHd, OneK, Medium, One, 4), (SquareHd, OneK, Medium, Two, 8), (SquareHd, OneK, Medium, Three, 12), (SquareHd, OneK, Medium, Four, 16),
|
||||
(SquareHd, OneK, High, One, 15), (SquareHd, OneK, High, Two, 30), (SquareHd, OneK, High, Three, 45), (SquareHd, OneK, High, Four, 60),
|
||||
(SquareHd, TwoK, Low, One, 1), (SquareHd, TwoK, Low, Two, 2), (SquareHd, TwoK, Low, Three, 3), (SquareHd, TwoK, Low, Four, 4),
|
||||
(SquareHd, TwoK, Medium, One, 7), (SquareHd, TwoK, Medium, Two, 14), (SquareHd, TwoK, Medium, Three, 21), (SquareHd, TwoK, Medium, Four, 28),
|
||||
(SquareHd, TwoK, High, One, 26), (SquareHd, TwoK, High, Two, 52), (SquareHd, TwoK, High, Three, 78), (SquareHd, TwoK, High, Four, 104),
|
||||
(SquareHd, ThreeK, Low, One, 2), (SquareHd, ThreeK, Low, Two, 4), (SquareHd, ThreeK, Low, Three, 6), (SquareHd, ThreeK, Low, Four, 8),
|
||||
(SquareHd, ThreeK, Medium, One, 10), (SquareHd, ThreeK, Medium, Two, 20), (SquareHd, ThreeK, Medium, Three, 30), (SquareHd, ThreeK, Medium, Four, 40),
|
||||
(SquareHd, ThreeK, High, One, 41), (SquareHd, ThreeK, High, Two, 82), (SquareHd, ThreeK, High, Three, 123), (SquareHd, ThreeK, High, Four, 164),
|
||||
(SquareHd, FourK, Low, One, 2), (SquareHd, FourK, Low, Two, 4), (SquareHd, FourK, Low, Three, 6), (SquareHd, FourK, Low, Four, 8),
|
||||
(SquareHd, FourK, Medium, One, 10), (SquareHd, FourK, Medium, Two, 20), (SquareHd, FourK, Medium, Three, 30), (SquareHd, FourK, Medium, Four, 40),
|
||||
(SquareHd, FourK, High, One, 41), (SquareHd, FourK, High, Two, 82), (SquareHd, FourK, High, Three, 123), (SquareHd, FourK, High, Four, 164),
|
||||
// Landscape4x3
|
||||
(Landscape4x3, OneK, Low, One, 1), (Landscape4x3, OneK, Low, Two, 2), (Landscape4x3, OneK, Low, Three, 3), (Landscape4x3, OneK, Low, Four, 4),
|
||||
(Landscape4x3, OneK, Medium, One, 4), (Landscape4x3, OneK, Medium, Two, 8), (Landscape4x3, OneK, Medium, Three, 12), (Landscape4x3, OneK, Medium, Four, 16),
|
||||
(Landscape4x3, OneK, High, One, 14), (Landscape4x3, OneK, High, Two, 28), (Landscape4x3, OneK, High, Three, 42), (Landscape4x3, OneK, High, Four, 56),
|
||||
(Landscape4x3, TwoK, Low, One, 1), (Landscape4x3, TwoK, Low, Two, 2), (Landscape4x3, TwoK, Low, Three, 3), (Landscape4x3, TwoK, Low, Four, 4),
|
||||
(Landscape4x3, TwoK, Medium, One, 6), (Landscape4x3, TwoK, Medium, Two, 12), (Landscape4x3, TwoK, Medium, Three, 18), (Landscape4x3, TwoK, Medium, Four, 24),
|
||||
(Landscape4x3, TwoK, High, One, 23), (Landscape4x3, TwoK, High, Two, 46), (Landscape4x3, TwoK, High, Three, 69), (Landscape4x3, TwoK, High, Four, 92),
|
||||
(Landscape4x3, ThreeK, Low, One, 2), (Landscape4x3, ThreeK, Low, Two, 4), (Landscape4x3, ThreeK, Low, Three, 6), (Landscape4x3, ThreeK, Low, Four, 8),
|
||||
(Landscape4x3, ThreeK, Medium, One, 9), (Landscape4x3, ThreeK, Medium, Two, 18), (Landscape4x3, ThreeK, Medium, Three, 27), (Landscape4x3, ThreeK, Medium, Four, 36),
|
||||
(Landscape4x3, ThreeK, High, One, 36), (Landscape4x3, ThreeK, High, Two, 72), (Landscape4x3, ThreeK, High, Three, 108), (Landscape4x3, ThreeK, High, Four, 144),
|
||||
(Landscape4x3, FourK, Low, One, 2), (Landscape4x3, FourK, Low, Two, 4), (Landscape4x3, FourK, Low, Three, 6), (Landscape4x3, FourK, Low, Four, 8),
|
||||
(Landscape4x3, FourK, Medium, One, 10), (Landscape4x3, FourK, Medium, Two, 20), (Landscape4x3, FourK, Medium, Three, 30), (Landscape4x3, FourK, Medium, Four, 40),
|
||||
(Landscape4x3, FourK, High, One, 40), (Landscape4x3, FourK, High, Two, 80), (Landscape4x3, FourK, High, Three, 120), (Landscape4x3, FourK, High, Four, 160),
|
||||
// Landscape16x9
|
||||
(Landscape16x9, OneK, Low, One, 1), (Landscape16x9, OneK, Low, Two, 2), (Landscape16x9, OneK, Low, Three, 3), (Landscape16x9, OneK, Low, Four, 4),
|
||||
(Landscape16x9, OneK, Medium, One, 4), (Landscape16x9, OneK, Medium, Two, 8), (Landscape16x9, OneK, Medium, Three, 12), (Landscape16x9, OneK, Medium, Four, 16),
|
||||
(Landscape16x9, OneK, High, One, 14), (Landscape16x9, OneK, High, Two, 28), (Landscape16x9, OneK, High, Three, 42), (Landscape16x9, OneK, High, Four, 56),
|
||||
(Landscape16x9, TwoK, Low, One, 1), (Landscape16x9, TwoK, Low, Two, 2), (Landscape16x9, TwoK, Low, Three, 3), (Landscape16x9, TwoK, Low, Four, 4),
|
||||
(Landscape16x9, TwoK, Medium, One, 5), (Landscape16x9, TwoK, Medium, Two, 10), (Landscape16x9, TwoK, Medium, Three, 15), (Landscape16x9, TwoK, Medium, Four, 20),
|
||||
(Landscape16x9, TwoK, High, One, 20), (Landscape16x9, TwoK, High, Two, 40), (Landscape16x9, TwoK, High, Three, 60), (Landscape16x9, TwoK, High, Four, 80),
|
||||
(Landscape16x9, ThreeK, Low, One, 1), (Landscape16x9, ThreeK, Low, Two, 2), (Landscape16x9, ThreeK, Low, Three, 3), (Landscape16x9, ThreeK, Low, Four, 4),
|
||||
(Landscape16x9, ThreeK, Medium, One, 8), (Landscape16x9, ThreeK, Medium, Two, 16), (Landscape16x9, ThreeK, Medium, Three, 24), (Landscape16x9, ThreeK, Medium, Four, 32),
|
||||
(Landscape16x9, ThreeK, High, One, 30), (Landscape16x9, ThreeK, High, Two, 60), (Landscape16x9, ThreeK, High, Three, 90), (Landscape16x9, ThreeK, High, Four, 120),
|
||||
(Landscape16x9, FourK, Low, One, 2), (Landscape16x9, FourK, Low, Two, 4), (Landscape16x9, FourK, Low, Three, 6), (Landscape16x9, FourK, Low, Four, 8),
|
||||
(Landscape16x9, FourK, Medium, One, 10), (Landscape16x9, FourK, Medium, Two, 20), (Landscape16x9, FourK, Medium, Three, 30), (Landscape16x9, FourK, Medium, Four, 40),
|
||||
(Landscape16x9, FourK, High, One, 41), (Landscape16x9, FourK, High, Two, 82), (Landscape16x9, FourK, High, Three, 123), (Landscape16x9, FourK, High, Four, 164),
|
||||
// Portrait4x3
|
||||
(Portrait4x3, OneK, Low, One, 1), (Portrait4x3, OneK, Low, Two, 2), (Portrait4x3, OneK, Low, Three, 3), (Portrait4x3, OneK, Low, Four, 4),
|
||||
(Portrait4x3, OneK, Medium, One, 4), (Portrait4x3, OneK, Medium, Two, 8), (Portrait4x3, OneK, Medium, Three, 12), (Portrait4x3, OneK, Medium, Four, 16),
|
||||
(Portrait4x3, OneK, High, One, 14), (Portrait4x3, OneK, High, Two, 28), (Portrait4x3, OneK, High, Three, 42), (Portrait4x3, OneK, High, Four, 56),
|
||||
(Portrait4x3, TwoK, Low, One, 1), (Portrait4x3, TwoK, Low, Two, 2), (Portrait4x3, TwoK, Low, Three, 3), (Portrait4x3, TwoK, Low, Four, 4),
|
||||
(Portrait4x3, TwoK, Medium, One, 6), (Portrait4x3, TwoK, Medium, Two, 12), (Portrait4x3, TwoK, Medium, Three, 18), (Portrait4x3, TwoK, Medium, Four, 24),
|
||||
(Portrait4x3, TwoK, High, One, 23), (Portrait4x3, TwoK, High, Two, 46), (Portrait4x3, TwoK, High, Three, 69), (Portrait4x3, TwoK, High, Four, 92),
|
||||
(Portrait4x3, ThreeK, Low, One, 2), (Portrait4x3, ThreeK, Low, Two, 4), (Portrait4x3, ThreeK, Low, Three, 6), (Portrait4x3, ThreeK, Low, Four, 8),
|
||||
(Portrait4x3, ThreeK, Medium, One, 9), (Portrait4x3, ThreeK, Medium, Two, 18), (Portrait4x3, ThreeK, Medium, Three, 27), (Portrait4x3, ThreeK, Medium, Four, 36),
|
||||
(Portrait4x3, ThreeK, High, One, 36), (Portrait4x3, ThreeK, High, Two, 72), (Portrait4x3, ThreeK, High, Three, 108), (Portrait4x3, ThreeK, High, Four, 144),
|
||||
(Portrait4x3, FourK, Low, One, 2), (Portrait4x3, FourK, Low, Two, 4), (Portrait4x3, FourK, Low, Three, 6), (Portrait4x3, FourK, Low, Four, 8),
|
||||
(Portrait4x3, FourK, Medium, One, 10), (Portrait4x3, FourK, Medium, Two, 20), (Portrait4x3, FourK, Medium, Three, 30), (Portrait4x3, FourK, Medium, Four, 40),
|
||||
(Portrait4x3, FourK, High, One, 40), (Portrait4x3, FourK, High, Two, 80), (Portrait4x3, FourK, High, Three, 120), (Portrait4x3, FourK, High, Four, 160),
|
||||
// Portrait16x9
|
||||
(Portrait16x9, OneK, Low, One, 1), (Portrait16x9, OneK, Low, Two, 2), (Portrait16x9, OneK, Low, Three, 3), (Portrait16x9, OneK, Low, Four, 4),
|
||||
(Portrait16x9, OneK, Medium, One, 4), (Portrait16x9, OneK, Medium, Two, 8), (Portrait16x9, OneK, Medium, Three, 12), (Portrait16x9, OneK, Medium, Four, 16),
|
||||
(Portrait16x9, OneK, High, One, 14), (Portrait16x9, OneK, High, Two, 28), (Portrait16x9, OneK, High, Three, 42), (Portrait16x9, OneK, High, Four, 56),
|
||||
(Portrait16x9, TwoK, Low, One, 1), (Portrait16x9, TwoK, Low, Two, 2), (Portrait16x9, TwoK, Low, Three, 3), (Portrait16x9, TwoK, Low, Four, 4),
|
||||
(Portrait16x9, TwoK, Medium, One, 5), (Portrait16x9, TwoK, Medium, Two, 10), (Portrait16x9, TwoK, Medium, Three, 15), (Portrait16x9, TwoK, Medium, Four, 20),
|
||||
(Portrait16x9, TwoK, High, One, 20), (Portrait16x9, TwoK, High, Two, 40), (Portrait16x9, TwoK, High, Three, 60), (Portrait16x9, TwoK, High, Four, 80),
|
||||
(Portrait16x9, ThreeK, Low, One, 1), (Portrait16x9, ThreeK, Low, Two, 2), (Portrait16x9, ThreeK, Low, Three, 3), (Portrait16x9, ThreeK, Low, Four, 4),
|
||||
(Portrait16x9, ThreeK, Medium, One, 8), (Portrait16x9, ThreeK, Medium, Two, 16), (Portrait16x9, ThreeK, Medium, Three, 24), (Portrait16x9, ThreeK, Medium, Four, 32),
|
||||
(Portrait16x9, ThreeK, High, One, 30), (Portrait16x9, ThreeK, High, Two, 60), (Portrait16x9, ThreeK, High, Three, 90), (Portrait16x9, ThreeK, High, Four, 120),
|
||||
(Portrait16x9, FourK, Low, One, 2), (Portrait16x9, FourK, Low, Two, 4), (Portrait16x9, FourK, Low, Three, 6), (Portrait16x9, FourK, Low, Four, 8),
|
||||
(Portrait16x9, FourK, Medium, One, 10), (Portrait16x9, FourK, Medium, Two, 20), (Portrait16x9, FourK, Medium, Three, 30), (Portrait16x9, FourK, Medium, Four, 40),
|
||||
(Portrait16x9, FourK, High, One, 41), (Portrait16x9, FourK, High, Two, 82), (Portrait16x9, FourK, High, Three, 123), (Portrait16x9, FourK, High, Four, 164),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn all_resolution_batch_costs() {
|
||||
for &(size, res, quality, num, expected) in CASES {
|
||||
let actual = make_request(num, Some(quality), Some(size), Some(res)).calculate_cost_in_cents();
|
||||
assert_eq!(actual, expected, "{size:?} {res:?} {quality:?} {num:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_quality_spot_prices_match_expected_cents() {
|
||||
for &(size, resolution, num_images, expected) in SPOT_PRICE_CASES {
|
||||
assert_eq!(make_request(num_images, Some(GptImage2EditImageQuality::High), Some(size), Some(resolution)).calculate_cost_in_cents(), expected, "size={size:?} resolution={resolution:?} num_images={num_images:?}",);
|
||||
mod monotonicity_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn higher_quality_costs_more() {
|
||||
for &size in &[Square, Landscape16x9, Portrait4x3, Auto] {
|
||||
let low = make_request(One, Some(Low), Some(size), None).calculate_cost_in_cents();
|
||||
let med = make_request(One, Some(Medium), Some(size), None).calculate_cost_in_cents();
|
||||
let high = make_request(One, Some(High), Some(size), None).calculate_cost_in_cents();
|
||||
assert!(low <= med, "{size:?}: low ({low}) should be <= medium ({med})");
|
||||
assert!(med <= high, "{size:?}: medium ({med}) should be <= high ({high})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_uses_highest_published_price_for_quality() {
|
||||
assert_eq!(make_request(GptImage2EditImageNumImages::Two, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK),).calculate_cost_in_cents(), 83,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_without_image_size_defaults_to_square_aspect() {
|
||||
assert_eq!(make_request(GptImage2EditImageNumImages::Two, Some(GptImage2EditImageQuality::High), None, Some(GptImage2Resolution::FourK),).calculate_cost_in_cents(), 83,);
|
||||
#[test]
|
||||
fn higher_resolution_costs_at_least_as_much() {
|
||||
for &size in &[Square, Landscape16x9, Portrait4x3] {
|
||||
let resolutions = [OneK, TwoK, ThreeK, FourK];
|
||||
for pair in resolutions.windows(2) {
|
||||
let lower = make_request(One, Some(High), Some(size), Some(pair[0]))
|
||||
.calculate_cost_in_cents();
|
||||
let higher = make_request(One, Some(High), Some(size), Some(pair[1]))
|
||||
.calculate_cost_in_cents();
|
||||
assert!(
|
||||
lower <= higher,
|
||||
"{size:?}: {pair:?} — lower res ({lower}¢) should be <= higher res ({higher}¢)",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+362
-154
@@ -1,63 +1,58 @@
|
||||
use crate::requests::api::image::common::gpt_image_2_resolution::{compute_custom_image_size, GptImage2AspectRatio, GptImage2Resolution};
|
||||
use crate::requests::api::image::text::gpt_image_2_text_to_image::api::{GptImage2TextToImageNumImages, GptImage2TextToImageQuality, GptImage2TextToImageRequest, GptImage2TextToImageSize};
|
||||
use crate::requests::api::image::common::gpt_image_2_resolution::{
|
||||
compute_custom_image_size, GptImage2AspectRatio, GptImage2Resolution,
|
||||
};
|
||||
use crate::requests::api::image::text::gpt_image_2_text_to_image::api::{
|
||||
GptImage2TextToImageNumImages, GptImage2TextToImageQuality,
|
||||
GptImage2TextToImageRequest, GptImage2TextToImageSize,
|
||||
};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
|
||||
const PRICING_TABLE: &[GptImage2PriceRow] = &[GptImage2PriceRow { width: 1024, height: 768, low: 11, medium: 43, high: 151 }, GptImage2PriceRow { width: 1024, height: 1024, low: 15, medium: 61, high: 219 }, GptImage2PriceRow { width: 1024, height: 1536, low: 18, medium: 54, high: 178 }, GptImage2PriceRow { width: 1920, height: 1080, low: 17, medium: 53, high: 158 }, GptImage2PriceRow { width: 2560, height: 1440, low: 19, medium: 68, high: 234 }, GptImage2PriceRow { width: 3840, height: 2160, low: 24, medium: 113, high: 413 }];
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct GptImage2PriceRow {
|
||||
width: u32,
|
||||
height: u32,
|
||||
low: u64,
|
||||
medium: u64,
|
||||
high: u64,
|
||||
}
|
||||
|
||||
impl GptImage2PriceRow {
|
||||
fn price_in_tenths_of_a_cent(self, quality: GptImage2TextToImageQuality) -> u64 {
|
||||
match quality {
|
||||
GptImage2TextToImageQuality::Low => self.low,
|
||||
GptImage2TextToImageQuality::Medium => self.medium,
|
||||
GptImage2TextToImageQuality::High => self.high,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalRequestCostCalculator for GptImage2TextToImageRequest {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
let quality = self.quality.unwrap_or(GptImage2TextToImageQuality::High);
|
||||
let (width, height) = dimensions_for_request(self);
|
||||
let price = closest_price_row(width, height).price_in_tenths_of_a_cent(quality);
|
||||
let total_tenths_of_a_cent = price * num_images(self.num_images);
|
||||
let total_pixels = estimate_total_pixels(self.image_size, self.resolution);
|
||||
let megapixels = total_pixels as f64 / 1_000_000.0;
|
||||
|
||||
total_tenths_of_a_cent.div_ceil(10) as UsdCents
|
||||
// Fal's GPT Image 2 text-to-image pricing has two components:
|
||||
//
|
||||
// 1. A base request cost (prompt processing, API overhead)
|
||||
// 2. A per-pixel generation cost that scales with output resolution
|
||||
//
|
||||
// Both components increase with quality level. All rates are in
|
||||
// tenths of a US cent, derived from Fal's published pricing.
|
||||
//
|
||||
// The base cost dominates at small resolutions (~1K); the per-pixel
|
||||
// cost dominates at large resolutions (~4K).
|
||||
let (base_tenths, per_megapixel_tenths): (f64, f64) = match quality {
|
||||
GptImage2TextToImageQuality::Low => ( 8.0, 1.5),
|
||||
GptImage2TextToImageQuality::Medium => (35.0, 9.5),
|
||||
GptImage2TextToImageQuality::High => (120.0, 35.0),
|
||||
};
|
||||
|
||||
let tenths_per_image = (base_tenths + per_megapixel_tenths * megapixels).ceil() as u64;
|
||||
let cents_per_image = tenths_per_image.div_ceil(10);
|
||||
let num = num_images_u64(self.num_images);
|
||||
|
||||
cents_per_image * num
|
||||
}
|
||||
}
|
||||
|
||||
fn dimensions_for_request(request: &GptImage2TextToImageRequest) -> (u32, u32) {
|
||||
match (request.image_size, request.resolution) {
|
||||
(Some(size), Some(resolution)) => {
|
||||
fn estimate_total_pixels(
|
||||
image_size: Option<GptImage2TextToImageSize>,
|
||||
resolution: Option<GptImage2Resolution>,
|
||||
) -> u64 {
|
||||
match (image_size, resolution) {
|
||||
(Some(size), Some(res)) => {
|
||||
let aspect = size_to_aspect(size);
|
||||
let custom = compute_custom_image_size(aspect, resolution);
|
||||
(custom.width, custom.height)
|
||||
},
|
||||
(None, Some(resolution)) => {
|
||||
let custom = compute_custom_image_size(GptImage2AspectRatio::Square, resolution);
|
||||
(custom.width, custom.height)
|
||||
},
|
||||
(Some(size), None) => preset_dimensions(size),
|
||||
(None, None) => preset_dimensions(GptImage2TextToImageSize::Square),
|
||||
}
|
||||
}
|
||||
|
||||
fn preset_dimensions(size: GptImage2TextToImageSize) -> (u32, u32) {
|
||||
match size {
|
||||
GptImage2TextToImageSize::SquareHd => (2048, 2048),
|
||||
GptImage2TextToImageSize::Square => (1024, 1024),
|
||||
GptImage2TextToImageSize::Portrait4x3 => (768, 1024),
|
||||
GptImage2TextToImageSize::Portrait16x9 => (1080, 1920),
|
||||
GptImage2TextToImageSize::Landscape4x3 => (1024, 768),
|
||||
GptImage2TextToImageSize::Landscape16x9 => (1920, 1080),
|
||||
let dims = compute_custom_image_size(aspect, res);
|
||||
dims.width as u64 * dims.height as u64
|
||||
}
|
||||
(None, Some(res)) => {
|
||||
let dims = compute_custom_image_size(GptImage2AspectRatio::Square, res);
|
||||
dims.width as u64 * dims.height as u64
|
||||
}
|
||||
(Some(size), None) => preset_pixel_count(size),
|
||||
(None, None) => preset_pixel_count(GptImage2TextToImageSize::Square),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,20 +67,20 @@ fn size_to_aspect(size: GptImage2TextToImageSize) -> GptImage2AspectRatio {
|
||||
}
|
||||
}
|
||||
|
||||
fn closest_price_row(width: u32, height: u32) -> GptImage2PriceRow {
|
||||
let pixels = width as u64 * height as u64;
|
||||
PRICING_TABLE
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|row| {
|
||||
let row_pixels = row.width as u64 * row.height as u64;
|
||||
row_pixels.abs_diff(pixels)
|
||||
})
|
||||
.expect("GPT Image 2 pricing table cannot be empty")
|
||||
/// Standard preset pixel counts when no custom resolution is specified.
|
||||
fn preset_pixel_count(size: GptImage2TextToImageSize) -> u64 {
|
||||
match size {
|
||||
GptImage2TextToImageSize::Square => 1024 * 1024, // 1,048,576
|
||||
GptImage2TextToImageSize::SquareHd => 2048 * 2048, // 4,194,304
|
||||
GptImage2TextToImageSize::Landscape4x3 => 1024 * 768, // 786,432
|
||||
GptImage2TextToImageSize::Landscape16x9 => 1920 * 1080, // 2,073,600
|
||||
GptImage2TextToImageSize::Portrait4x3 => 768 * 1024, // 786,432
|
||||
GptImage2TextToImageSize::Portrait16x9 => 1080 * 1920, // 2,073,600
|
||||
}
|
||||
}
|
||||
|
||||
fn num_images(num_images: GptImage2TextToImageNumImages) -> u64 {
|
||||
match num_images {
|
||||
fn num_images_u64(num: GptImage2TextToImageNumImages) -> u64 {
|
||||
match num {
|
||||
GptImage2TextToImageNumImages::One => 1,
|
||||
GptImage2TextToImageNumImages::Two => 2,
|
||||
GptImage2TextToImageNumImages::Three => 3,
|
||||
@@ -97,119 +92,332 @@ fn num_images(num_images: GptImage2TextToImageNumImages) -> u64 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const QUALITIES: &[(GptImage2TextToImageQuality, u64, u64, u64, u64, u64, u64)] = &[
|
||||
(GptImage2TextToImageQuality::Low, 11, 15, 18, 17, 19, 24),
|
||||
(GptImage2TextToImageQuality::Medium, 43, 61, 54, 53, 68, 113),
|
||||
(GptImage2TextToImageQuality::High, 151, 219, 178, 158, 234, 413)
|
||||
];
|
||||
|
||||
const PRESET_CASES: &[(GptImage2TextToImageSize, u64)] = &[
|
||||
(GptImage2TextToImageSize::Landscape4x3, 0),
|
||||
(GptImage2TextToImageSize::Portrait4x3, 0),
|
||||
(GptImage2TextToImageSize::Square, 1),
|
||||
(GptImage2TextToImageSize::Landscape16x9, 3),
|
||||
(GptImage2TextToImageSize::Portrait16x9, 3),
|
||||
(GptImage2TextToImageSize::SquareHd, 4),
|
||||
];
|
||||
|
||||
const RESOLUTION_CASES: &[(GptImage2TextToImageSize, GptImage2Resolution, u64)] = &[
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, 0),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, 4),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, 5),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, 0),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, 3),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, 4),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, 0),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, 4),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, 5),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, 0),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, 3),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, 4),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, 1),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, 4),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, 5),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, 5),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, 1),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, 4),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, 5),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, 5),
|
||||
];
|
||||
|
||||
const SPOT_PRICE_CASES: &[(GptImage2TextToImageSize, GptImage2Resolution, GptImage2TextToImageNumImages, u64)] = &[
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, GptImage2TextToImageNumImages::One, 16),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, GptImage2TextToImageNumImages::Two, 47),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, GptImage2TextToImageNumImages::Three, 124),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, GptImage2TextToImageNumImages::Four, 64),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, GptImage2TextToImageNumImages::Two, 83),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, GptImage2TextToImageNumImages::One, 24),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, GptImage2TextToImageNumImages::Three, 71),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, GptImage2TextToImageNumImages::Four, 88),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, GptImage2TextToImageNumImages::Four, 166),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, GptImage2TextToImageNumImages::Three, 71),
|
||||
];
|
||||
|
||||
fn make_request(num_images: GptImage2TextToImageNumImages, quality: Option<GptImage2TextToImageQuality>, image_size: Option<GptImage2TextToImageSize>, resolution: Option<GptImage2Resolution>) -> GptImage2TextToImageRequest {
|
||||
GptImage2TextToImageRequest { prompt: "test".to_string(), num_images, image_size, quality, resolution, output_format: None }
|
||||
fn make_request(
|
||||
num_images: GptImage2TextToImageNumImages,
|
||||
quality: Option<GptImage2TextToImageQuality>,
|
||||
image_size: Option<GptImage2TextToImageSize>,
|
||||
resolution: Option<GptImage2Resolution>,
|
||||
) -> GptImage2TextToImageRequest {
|
||||
GptImage2TextToImageRequest {
|
||||
prompt: "test".to_string(),
|
||||
num_images,
|
||||
image_size,
|
||||
resolution,
|
||||
quality,
|
||||
output_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_defaults_to_high_square() {
|
||||
assert_eq!(make_request(GptImage2TextToImageNumImages::One, None, None, None).calculate_cost_in_cents(), 22);
|
||||
use GptImage2TextToImageNumImages::*;
|
||||
use GptImage2TextToImageQuality::*;
|
||||
use GptImage2TextToImageSize::*;
|
||||
use GptImage2Resolution::*;
|
||||
|
||||
mod preset_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
// Table: (size, low, medium, high) — expected cents per image
|
||||
const PRESET_CASES: &[(GptImage2TextToImageSize, u64, u64, u64)] = &[
|
||||
// Low Med High
|
||||
(Landscape4x3, 1, 5, 15),
|
||||
(Portrait4x3, 1, 5, 15),
|
||||
(Square, 1, 5, 16),
|
||||
(Landscape16x9, 2, 6, 20),
|
||||
(Portrait16x9, 2, 6, 20),
|
||||
(SquareHd, 2, 8, 27),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn preset_costs_at_each_quality() {
|
||||
for &(size, expected_low, expected_med, expected_high) in PRESET_CASES {
|
||||
let cases = [
|
||||
(Low, expected_low),
|
||||
(Medium, expected_med),
|
||||
(High, expected_high),
|
||||
];
|
||||
for (quality, expected) in cases {
|
||||
let actual = make_request(One, Some(quality), Some(size), None)
|
||||
.calculate_cost_in_cents();
|
||||
assert_eq!(actual, expected, "{size:?} {quality:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_to_high_quality_square() {
|
||||
assert_eq!(
|
||||
make_request(One, None, None, None).calculate_cost_in_cents(),
|
||||
16,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_rounds_final_request_total_up_to_whole_cents() {
|
||||
assert_eq!(make_request(GptImage2TextToImageNumImages::Four, Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None,).calculate_cost_in_cents(), 5,);
|
||||
mod custom_resolution_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
// Table: (size, resolution, low, medium, high) — expected cents per image
|
||||
const RESOLUTION_CASES: &[(GptImage2TextToImageSize, GptImage2Resolution, u64, u64, u64)] = &[
|
||||
// Square aspect
|
||||
(Square, OneK, 1, 5, 16),
|
||||
(Square, TwoK, 2, 8, 27),
|
||||
(Square, ThreeK, 3, 12, 42),
|
||||
(Square, FourK, 3, 12, 42), // capped at max pixels
|
||||
|
||||
// SquareHd (same aspect as Square)
|
||||
(SquareHd, OneK, 1, 5, 16),
|
||||
(SquareHd, TwoK, 2, 8, 27),
|
||||
(SquareHd, ThreeK, 3, 12, 42),
|
||||
(SquareHd, FourK, 3, 12, 42),
|
||||
|
||||
// Landscape 4:3
|
||||
(Landscape4x3, OneK, 1, 5, 15),
|
||||
(Landscape4x3, TwoK, 2, 7, 24),
|
||||
(Landscape4x3, ThreeK, 2, 11, 37),
|
||||
(Landscape4x3, FourK, 3, 12, 41),
|
||||
|
||||
// Landscape 16:9
|
||||
(Landscape16x9, OneK, 1, 5, 15),
|
||||
(Landscape16x9, TwoK, 2, 6, 21),
|
||||
(Landscape16x9, ThreeK, 2, 9, 31),
|
||||
(Landscape16x9, FourK, 3, 12, 42),
|
||||
|
||||
// Portrait 4:3
|
||||
(Portrait4x3, OneK, 1, 5, 15),
|
||||
(Portrait4x3, TwoK, 2, 7, 24),
|
||||
(Portrait4x3, ThreeK, 2, 11, 37),
|
||||
(Portrait4x3, FourK, 3, 12, 41),
|
||||
|
||||
// Portrait 16:9
|
||||
(Portrait16x9, OneK, 1, 5, 15),
|
||||
(Portrait16x9, TwoK, 2, 6, 21),
|
||||
(Portrait16x9, ThreeK, 2, 9, 31),
|
||||
(Portrait16x9, FourK, 3, 12, 42),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn resolution_costs_at_each_quality() {
|
||||
for &(size, res, expected_low, expected_med, expected_high) in RESOLUTION_CASES {
|
||||
let cases = [
|
||||
(Low, expected_low),
|
||||
(Medium, expected_med),
|
||||
(High, expected_high),
|
||||
];
|
||||
for (quality, expected) in cases {
|
||||
let actual = make_request(One, Some(quality), Some(size), Some(res))
|
||||
.calculate_cost_in_cents();
|
||||
assert_eq!(actual, expected, "{size:?} {res:?} {quality:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_without_size_defaults_to_square() {
|
||||
assert_eq!(
|
||||
make_request(One, Some(High), None, Some(TwoK)).calculate_cost_in_cents(),
|
||||
make_request(One, Some(High), Some(Square), Some(TwoK)).calculate_cost_in_cents(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_pricing_matches_published_table() {
|
||||
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
|
||||
let expected_by_row = [landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k];
|
||||
mod num_images_tests {
|
||||
use super::*;
|
||||
|
||||
for &(size, row_index) in PRESET_CASES {
|
||||
let expected = expected_by_row[row_index as usize].div_ceil(10);
|
||||
assert_eq!(make_request(GptImage2TextToImageNumImages::One, Some(quality), Some(size), None).calculate_cost_in_cents(), expected, "quality={quality:?} size={size:?}",);
|
||||
const ALL_BATCH_SIZES: &[(GptImage2TextToImageNumImages, u64)] = &[
|
||||
(One, 1), (Two, 2), (Three, 3), (Four, 4),
|
||||
];
|
||||
|
||||
const ALL_SIZES: &[GptImage2TextToImageSize] = &[
|
||||
Square, SquareHd, Landscape4x3, Landscape16x9, Portrait4x3, Portrait16x9,
|
||||
];
|
||||
|
||||
const ALL_RESOLUTIONS: &[GptImage2Resolution] = &[OneK, TwoK, ThreeK, FourK];
|
||||
|
||||
#[test]
|
||||
fn batch_scales_linearly_for_all_presets() {
|
||||
for &size in ALL_SIZES {
|
||||
let per_image = make_request(One, Some(High), Some(size), None)
|
||||
.calculate_cost_in_cents();
|
||||
for &(num, n) in ALL_BATCH_SIZES {
|
||||
let actual = make_request(num, Some(High), Some(size), None)
|
||||
.calculate_cost_in_cents();
|
||||
assert_eq!(actual, per_image * n, "{size:?} x{n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_scales_linearly_for_all_resolutions() {
|
||||
for &size in ALL_SIZES {
|
||||
for &res in ALL_RESOLUTIONS {
|
||||
let per_image = make_request(One, Some(High), Some(size), Some(res))
|
||||
.calculate_cost_in_cents();
|
||||
for &(num, n) in ALL_BATCH_SIZES {
|
||||
let actual = make_request(num, Some(High), Some(size), Some(res))
|
||||
.calculate_cost_in_cents();
|
||||
assert_eq!(actual, per_image * n, "{size:?} {res:?} x{n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_published_rows_match_all_quality_prices() {
|
||||
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
|
||||
let cases = [(1024, 768, landscape_4x3), (1024, 1024, square), (1024, 1536, portrait), (1920, 1080, landscape_16x9), (2560, 1440, square_hd), (3840, 2160, four_k)];
|
||||
mod exhaustive_preset_batch_tests {
|
||||
use super::*;
|
||||
|
||||
for (width, height, expected) in cases {
|
||||
assert_eq!(closest_price_row(width, height).price_in_tenths_of_a_cent(quality), expected, "quality={quality:?} dimensions={width}x{height}",);
|
||||
// (size, quality, num_images, expected_cents)
|
||||
const CASES: &[(GptImage2TextToImageSize, GptImage2TextToImageQuality, GptImage2TextToImageNumImages, u64)] = &[
|
||||
(Square, Low, One, 1), (Square, Low, Two, 2), (Square, Low, Three, 3), (Square, Low, Four, 4),
|
||||
(Square, Medium, One, 5), (Square, Medium, Two, 10), (Square, Medium, Three, 15), (Square, Medium, Four, 20),
|
||||
(Square, High, One, 16), (Square, High, Two, 32), (Square, High, Three, 48), (Square, High, Four, 64),
|
||||
(SquareHd, Low, One, 2), (SquareHd, Low, Two, 4), (SquareHd, Low, Three, 6), (SquareHd, Low, Four, 8),
|
||||
(SquareHd, Medium, One, 8), (SquareHd, Medium, Two, 16), (SquareHd, Medium, Three, 24), (SquareHd, Medium, Four, 32),
|
||||
(SquareHd, High, One, 27), (SquareHd, High, Two, 54), (SquareHd, High, Three, 81), (SquareHd, High, Four, 108),
|
||||
(Landscape4x3, Low, One, 1), (Landscape4x3, Low, Two, 2), (Landscape4x3, Low, Three, 3), (Landscape4x3, Low, Four, 4),
|
||||
(Landscape4x3, Medium, One, 5), (Landscape4x3, Medium, Two, 10), (Landscape4x3, Medium, Three, 15), (Landscape4x3, Medium, Four, 20),
|
||||
(Landscape4x3, High, One, 15), (Landscape4x3, High, Two, 30), (Landscape4x3, High, Three, 45), (Landscape4x3, High, Four, 60),
|
||||
(Landscape16x9, Low, One, 2), (Landscape16x9, Low, Two, 4), (Landscape16x9, Low, Three, 6), (Landscape16x9, Low, Four, 8),
|
||||
(Landscape16x9, Medium, One, 6), (Landscape16x9, Medium, Two, 12), (Landscape16x9, Medium, Three, 18), (Landscape16x9, Medium, Four, 24),
|
||||
(Landscape16x9, High, One, 20), (Landscape16x9, High, Two, 40), (Landscape16x9, High, Three, 60), (Landscape16x9, High, Four, 80),
|
||||
(Portrait4x3, Low, One, 1), (Portrait4x3, Low, Two, 2), (Portrait4x3, Low, Three, 3), (Portrait4x3, Low, Four, 4),
|
||||
(Portrait4x3, Medium, One, 5), (Portrait4x3, Medium, Two, 10), (Portrait4x3, Medium, Three, 15), (Portrait4x3, Medium, Four, 20),
|
||||
(Portrait4x3, High, One, 15), (Portrait4x3, High, Two, 30), (Portrait4x3, High, Three, 45), (Portrait4x3, High, Four, 60),
|
||||
(Portrait16x9, Low, One, 2), (Portrait16x9, Low, Two, 4), (Portrait16x9, Low, Three, 6), (Portrait16x9, Low, Four, 8),
|
||||
(Portrait16x9, Medium, One, 6), (Portrait16x9, Medium, Two, 12), (Portrait16x9, Medium, Three, 18), (Portrait16x9, Medium, Four, 24),
|
||||
(Portrait16x9, High, One, 20), (Portrait16x9, High, Two, 40), (Portrait16x9, High, Three, 60), (Portrait16x9, High, Four, 80),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn all_preset_batch_costs() {
|
||||
for &(size, quality, num, expected) in CASES {
|
||||
let actual = make_request(num, Some(quality), Some(size), None).calculate_cost_in_cents();
|
||||
assert_eq!(actual, expected, "{size:?} {quality:?} {num:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_pricing_covers_aspect_size_resolution_combinations() {
|
||||
for &(quality, landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k) in QUALITIES {
|
||||
let expected_by_row = [landscape_4x3, square, portrait, landscape_16x9, square_hd, four_k];
|
||||
mod exhaustive_resolution_batch_tests {
|
||||
use super::*;
|
||||
|
||||
for &(size, resolution, row_index) in RESOLUTION_CASES {
|
||||
let expected = expected_by_row[row_index as usize].div_ceil(10);
|
||||
assert_eq!(make_request(GptImage2TextToImageNumImages::One, Some(quality), Some(size), Some(resolution)).calculate_cost_in_cents(), expected, "quality={quality:?} size={size:?} resolution={resolution:?}",);
|
||||
// (size, resolution, quality, num_images, expected_cents)
|
||||
const CASES: &[(GptImage2TextToImageSize, GptImage2Resolution, GptImage2TextToImageQuality, GptImage2TextToImageNumImages, u64)] = &[
|
||||
// Square
|
||||
(Square, OneK, Low, One, 1), (Square, OneK, Low, Two, 2), (Square, OneK, Low, Three, 3), (Square, OneK, Low, Four, 4),
|
||||
(Square, OneK, Medium, One, 5), (Square, OneK, Medium, Two, 10), (Square, OneK, Medium, Three, 15), (Square, OneK, Medium, Four, 20),
|
||||
(Square, OneK, High, One, 16), (Square, OneK, High, Two, 32), (Square, OneK, High, Three, 48), (Square, OneK, High, Four, 64),
|
||||
(Square, TwoK, Low, One, 2), (Square, TwoK, Low, Two, 4), (Square, TwoK, Low, Three, 6), (Square, TwoK, Low, Four, 8),
|
||||
(Square, TwoK, Medium, One, 8), (Square, TwoK, Medium, Two, 16), (Square, TwoK, Medium, Three, 24), (Square, TwoK, Medium, Four, 32),
|
||||
(Square, TwoK, High, One, 27), (Square, TwoK, High, Two, 54), (Square, TwoK, High, Three, 81), (Square, TwoK, High, Four, 108),
|
||||
(Square, ThreeK, Low, One, 3), (Square, ThreeK, Low, Two, 6), (Square, ThreeK, Low, Three, 9), (Square, ThreeK, Low, Four, 12),
|
||||
(Square, ThreeK, Medium, One, 12), (Square, ThreeK, Medium, Two, 24), (Square, ThreeK, Medium, Three, 36), (Square, ThreeK, Medium, Four, 48),
|
||||
(Square, ThreeK, High, One, 42), (Square, ThreeK, High, Two, 84), (Square, ThreeK, High, Three, 126), (Square, ThreeK, High, Four, 168),
|
||||
(Square, FourK, Low, One, 3), (Square, FourK, Low, Two, 6), (Square, FourK, Low, Three, 9), (Square, FourK, Low, Four, 12),
|
||||
(Square, FourK, Medium, One, 12), (Square, FourK, Medium, Two, 24), (Square, FourK, Medium, Three, 36), (Square, FourK, Medium, Four, 48),
|
||||
(Square, FourK, High, One, 42), (Square, FourK, High, Two, 84), (Square, FourK, High, Three, 126), (Square, FourK, High, Four, 168),
|
||||
// SquareHd
|
||||
(SquareHd, OneK, Low, One, 1), (SquareHd, OneK, Low, Two, 2), (SquareHd, OneK, Low, Three, 3), (SquareHd, OneK, Low, Four, 4),
|
||||
(SquareHd, OneK, Medium, One, 5), (SquareHd, OneK, Medium, Two, 10), (SquareHd, OneK, Medium, Three, 15), (SquareHd, OneK, Medium, Four, 20),
|
||||
(SquareHd, OneK, High, One, 16), (SquareHd, OneK, High, Two, 32), (SquareHd, OneK, High, Three, 48), (SquareHd, OneK, High, Four, 64),
|
||||
(SquareHd, TwoK, Low, One, 2), (SquareHd, TwoK, Low, Two, 4), (SquareHd, TwoK, Low, Three, 6), (SquareHd, TwoK, Low, Four, 8),
|
||||
(SquareHd, TwoK, Medium, One, 8), (SquareHd, TwoK, Medium, Two, 16), (SquareHd, TwoK, Medium, Three, 24), (SquareHd, TwoK, Medium, Four, 32),
|
||||
(SquareHd, TwoK, High, One, 27), (SquareHd, TwoK, High, Two, 54), (SquareHd, TwoK, High, Three, 81), (SquareHd, TwoK, High, Four, 108),
|
||||
(SquareHd, ThreeK, Low, One, 3), (SquareHd, ThreeK, Low, Two, 6), (SquareHd, ThreeK, Low, Three, 9), (SquareHd, ThreeK, Low, Four, 12),
|
||||
(SquareHd, ThreeK, Medium, One, 12), (SquareHd, ThreeK, Medium, Two, 24), (SquareHd, ThreeK, Medium, Three, 36), (SquareHd, ThreeK, Medium, Four, 48),
|
||||
(SquareHd, ThreeK, High, One, 42), (SquareHd, ThreeK, High, Two, 84), (SquareHd, ThreeK, High, Three, 126), (SquareHd, ThreeK, High, Four, 168),
|
||||
(SquareHd, FourK, Low, One, 3), (SquareHd, FourK, Low, Two, 6), (SquareHd, FourK, Low, Three, 9), (SquareHd, FourK, Low, Four, 12),
|
||||
(SquareHd, FourK, Medium, One, 12), (SquareHd, FourK, Medium, Two, 24), (SquareHd, FourK, Medium, Three, 36), (SquareHd, FourK, Medium, Four, 48),
|
||||
(SquareHd, FourK, High, One, 42), (SquareHd, FourK, High, Two, 84), (SquareHd, FourK, High, Three, 126), (SquareHd, FourK, High, Four, 168),
|
||||
// Landscape4x3
|
||||
(Landscape4x3, OneK, Low, One, 1), (Landscape4x3, OneK, Low, Two, 2), (Landscape4x3, OneK, Low, Three, 3), (Landscape4x3, OneK, Low, Four, 4),
|
||||
(Landscape4x3, OneK, Medium, One, 5), (Landscape4x3, OneK, Medium, Two, 10), (Landscape4x3, OneK, Medium, Three, 15), (Landscape4x3, OneK, Medium, Four, 20),
|
||||
(Landscape4x3, OneK, High, One, 15), (Landscape4x3, OneK, High, Two, 30), (Landscape4x3, OneK, High, Three, 45), (Landscape4x3, OneK, High, Four, 60),
|
||||
(Landscape4x3, TwoK, Low, One, 2), (Landscape4x3, TwoK, Low, Two, 4), (Landscape4x3, TwoK, Low, Three, 6), (Landscape4x3, TwoK, Low, Four, 8),
|
||||
(Landscape4x3, TwoK, Medium, One, 7), (Landscape4x3, TwoK, Medium, Two, 14), (Landscape4x3, TwoK, Medium, Three, 21), (Landscape4x3, TwoK, Medium, Four, 28),
|
||||
(Landscape4x3, TwoK, High, One, 24), (Landscape4x3, TwoK, High, Two, 48), (Landscape4x3, TwoK, High, Three, 72), (Landscape4x3, TwoK, High, Four, 96),
|
||||
(Landscape4x3, ThreeK, Low, One, 2), (Landscape4x3, ThreeK, Low, Two, 4), (Landscape4x3, ThreeK, Low, Three, 6), (Landscape4x3, ThreeK, Low, Four, 8),
|
||||
(Landscape4x3, ThreeK, Medium, One, 11), (Landscape4x3, ThreeK, Medium, Two, 22), (Landscape4x3, ThreeK, Medium, Three, 33), (Landscape4x3, ThreeK, Medium, Four, 44),
|
||||
(Landscape4x3, ThreeK, High, One, 37), (Landscape4x3, ThreeK, High, Two, 74), (Landscape4x3, ThreeK, High, Three, 111), (Landscape4x3, ThreeK, High, Four, 148),
|
||||
(Landscape4x3, FourK, Low, One, 3), (Landscape4x3, FourK, Low, Two, 6), (Landscape4x3, FourK, Low, Three, 9), (Landscape4x3, FourK, Low, Four, 12),
|
||||
(Landscape4x3, FourK, Medium, One, 12), (Landscape4x3, FourK, Medium, Two, 24), (Landscape4x3, FourK, Medium, Three, 36), (Landscape4x3, FourK, Medium, Four, 48),
|
||||
(Landscape4x3, FourK, High, One, 41), (Landscape4x3, FourK, High, Two, 82), (Landscape4x3, FourK, High, Three, 123), (Landscape4x3, FourK, High, Four, 164),
|
||||
// Landscape16x9
|
||||
(Landscape16x9, OneK, Low, One, 1), (Landscape16x9, OneK, Low, Two, 2), (Landscape16x9, OneK, Low, Three, 3), (Landscape16x9, OneK, Low, Four, 4),
|
||||
(Landscape16x9, OneK, Medium, One, 5), (Landscape16x9, OneK, Medium, Two, 10), (Landscape16x9, OneK, Medium, Three, 15), (Landscape16x9, OneK, Medium, Four, 20),
|
||||
(Landscape16x9, OneK, High, One, 15), (Landscape16x9, OneK, High, Two, 30), (Landscape16x9, OneK, High, Three, 45), (Landscape16x9, OneK, High, Four, 60),
|
||||
(Landscape16x9, TwoK, Low, One, 2), (Landscape16x9, TwoK, Low, Two, 4), (Landscape16x9, TwoK, Low, Three, 6), (Landscape16x9, TwoK, Low, Four, 8),
|
||||
(Landscape16x9, TwoK, Medium, One, 6), (Landscape16x9, TwoK, Medium, Two, 12), (Landscape16x9, TwoK, Medium, Three, 18), (Landscape16x9, TwoK, Medium, Four, 24),
|
||||
(Landscape16x9, TwoK, High, One, 21), (Landscape16x9, TwoK, High, Two, 42), (Landscape16x9, TwoK, High, Three, 63), (Landscape16x9, TwoK, High, Four, 84),
|
||||
(Landscape16x9, ThreeK, Low, One, 2), (Landscape16x9, ThreeK, Low, Two, 4), (Landscape16x9, ThreeK, Low, Three, 6), (Landscape16x9, ThreeK, Low, Four, 8),
|
||||
(Landscape16x9, ThreeK, Medium, One, 9), (Landscape16x9, ThreeK, Medium, Two, 18), (Landscape16x9, ThreeK, Medium, Three, 27), (Landscape16x9, ThreeK, Medium, Four, 36),
|
||||
(Landscape16x9, ThreeK, High, One, 31), (Landscape16x9, ThreeK, High, Two, 62), (Landscape16x9, ThreeK, High, Three, 93), (Landscape16x9, ThreeK, High, Four, 124),
|
||||
(Landscape16x9, FourK, Low, One, 3), (Landscape16x9, FourK, Low, Two, 6), (Landscape16x9, FourK, Low, Three, 9), (Landscape16x9, FourK, Low, Four, 12),
|
||||
(Landscape16x9, FourK, Medium, One, 12), (Landscape16x9, FourK, Medium, Two, 24), (Landscape16x9, FourK, Medium, Three, 36), (Landscape16x9, FourK, Medium, Four, 48),
|
||||
(Landscape16x9, FourK, High, One, 42), (Landscape16x9, FourK, High, Two, 84), (Landscape16x9, FourK, High, Three, 126), (Landscape16x9, FourK, High, Four, 168),
|
||||
// Portrait4x3
|
||||
(Portrait4x3, OneK, Low, One, 1), (Portrait4x3, OneK, Low, Two, 2), (Portrait4x3, OneK, Low, Three, 3), (Portrait4x3, OneK, Low, Four, 4),
|
||||
(Portrait4x3, OneK, Medium, One, 5), (Portrait4x3, OneK, Medium, Two, 10), (Portrait4x3, OneK, Medium, Three, 15), (Portrait4x3, OneK, Medium, Four, 20),
|
||||
(Portrait4x3, OneK, High, One, 15), (Portrait4x3, OneK, High, Two, 30), (Portrait4x3, OneK, High, Three, 45), (Portrait4x3, OneK, High, Four, 60),
|
||||
(Portrait4x3, TwoK, Low, One, 2), (Portrait4x3, TwoK, Low, Two, 4), (Portrait4x3, TwoK, Low, Three, 6), (Portrait4x3, TwoK, Low, Four, 8),
|
||||
(Portrait4x3, TwoK, Medium, One, 7), (Portrait4x3, TwoK, Medium, Two, 14), (Portrait4x3, TwoK, Medium, Three, 21), (Portrait4x3, TwoK, Medium, Four, 28),
|
||||
(Portrait4x3, TwoK, High, One, 24), (Portrait4x3, TwoK, High, Two, 48), (Portrait4x3, TwoK, High, Three, 72), (Portrait4x3, TwoK, High, Four, 96),
|
||||
(Portrait4x3, ThreeK, Low, One, 2), (Portrait4x3, ThreeK, Low, Two, 4), (Portrait4x3, ThreeK, Low, Three, 6), (Portrait4x3, ThreeK, Low, Four, 8),
|
||||
(Portrait4x3, ThreeK, Medium, One, 11), (Portrait4x3, ThreeK, Medium, Two, 22), (Portrait4x3, ThreeK, Medium, Three, 33), (Portrait4x3, ThreeK, Medium, Four, 44),
|
||||
(Portrait4x3, ThreeK, High, One, 37), (Portrait4x3, ThreeK, High, Two, 74), (Portrait4x3, ThreeK, High, Three, 111), (Portrait4x3, ThreeK, High, Four, 148),
|
||||
(Portrait4x3, FourK, Low, One, 3), (Portrait4x3, FourK, Low, Two, 6), (Portrait4x3, FourK, Low, Three, 9), (Portrait4x3, FourK, Low, Four, 12),
|
||||
(Portrait4x3, FourK, Medium, One, 12), (Portrait4x3, FourK, Medium, Two, 24), (Portrait4x3, FourK, Medium, Three, 36), (Portrait4x3, FourK, Medium, Four, 48),
|
||||
(Portrait4x3, FourK, High, One, 41), (Portrait4x3, FourK, High, Two, 82), (Portrait4x3, FourK, High, Three, 123), (Portrait4x3, FourK, High, Four, 164),
|
||||
// Portrait16x9
|
||||
(Portrait16x9, OneK, Low, One, 1), (Portrait16x9, OneK, Low, Two, 2), (Portrait16x9, OneK, Low, Three, 3), (Portrait16x9, OneK, Low, Four, 4),
|
||||
(Portrait16x9, OneK, Medium, One, 5), (Portrait16x9, OneK, Medium, Two, 10), (Portrait16x9, OneK, Medium, Three, 15), (Portrait16x9, OneK, Medium, Four, 20),
|
||||
(Portrait16x9, OneK, High, One, 15), (Portrait16x9, OneK, High, Two, 30), (Portrait16x9, OneK, High, Three, 45), (Portrait16x9, OneK, High, Four, 60),
|
||||
(Portrait16x9, TwoK, Low, One, 2), (Portrait16x9, TwoK, Low, Two, 4), (Portrait16x9, TwoK, Low, Three, 6), (Portrait16x9, TwoK, Low, Four, 8),
|
||||
(Portrait16x9, TwoK, Medium, One, 6), (Portrait16x9, TwoK, Medium, Two, 12), (Portrait16x9, TwoK, Medium, Three, 18), (Portrait16x9, TwoK, Medium, Four, 24),
|
||||
(Portrait16x9, TwoK, High, One, 21), (Portrait16x9, TwoK, High, Two, 42), (Portrait16x9, TwoK, High, Three, 63), (Portrait16x9, TwoK, High, Four, 84),
|
||||
(Portrait16x9, ThreeK, Low, One, 2), (Portrait16x9, ThreeK, Low, Two, 4), (Portrait16x9, ThreeK, Low, Three, 6), (Portrait16x9, ThreeK, Low, Four, 8),
|
||||
(Portrait16x9, ThreeK, Medium, One, 9), (Portrait16x9, ThreeK, Medium, Two, 18), (Portrait16x9, ThreeK, Medium, Three, 27), (Portrait16x9, ThreeK, Medium, Four, 36),
|
||||
(Portrait16x9, ThreeK, High, One, 31), (Portrait16x9, ThreeK, High, Two, 62), (Portrait16x9, ThreeK, High, Three, 93), (Portrait16x9, ThreeK, High, Four, 124),
|
||||
(Portrait16x9, FourK, Low, One, 3), (Portrait16x9, FourK, Low, Two, 6), (Portrait16x9, FourK, Low, Three, 9), (Portrait16x9, FourK, Low, Four, 12),
|
||||
(Portrait16x9, FourK, Medium, One, 12), (Portrait16x9, FourK, Medium, Two, 24), (Portrait16x9, FourK, Medium, Three, 36), (Portrait16x9, FourK, Medium, Four, 48),
|
||||
(Portrait16x9, FourK, High, One, 42), (Portrait16x9, FourK, High, Two, 84), (Portrait16x9, FourK, High, Three, 126), (Portrait16x9, FourK, High, Four, 168),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn all_resolution_batch_costs() {
|
||||
for &(size, res, quality, num, expected) in CASES {
|
||||
let actual = make_request(num, Some(quality), Some(size), Some(res)).calculate_cost_in_cents();
|
||||
assert_eq!(actual, expected, "{size:?} {res:?} {quality:?} {num:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_quality_spot_prices_match_expected_cents() {
|
||||
for &(size, resolution, num_images, expected) in SPOT_PRICE_CASES {
|
||||
assert_eq!(make_request(num_images, Some(GptImage2TextToImageQuality::High), Some(size), Some(resolution)).calculate_cost_in_cents(), expected, "size={size:?} resolution={resolution:?} num_images={num_images:?}",);
|
||||
}
|
||||
}
|
||||
mod monotonicity_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn resolution_without_image_size_defaults_to_square_aspect() {
|
||||
assert_eq!(make_request(GptImage2TextToImageNumImages::Two, Some(GptImage2TextToImageQuality::High), None, Some(GptImage2Resolution::FourK),).calculate_cost_in_cents(), 83,);
|
||||
#[test]
|
||||
fn higher_quality_costs_more() {
|
||||
for &size in &[Square, Landscape16x9, Portrait4x3] {
|
||||
let low = make_request(One, Some(Low), Some(size), None).calculate_cost_in_cents();
|
||||
let med = make_request(One, Some(Medium), Some(size), None).calculate_cost_in_cents();
|
||||
let high = make_request(One, Some(High), Some(size), None).calculate_cost_in_cents();
|
||||
assert!(low <= med, "{size:?}: low ({low}) should be <= medium ({med})");
|
||||
assert!(med <= high, "{size:?}: medium ({med}) should be <= high ({high})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_resolution_costs_at_least_as_much() {
|
||||
for &size in &[Square, Landscape16x9, Portrait4x3] {
|
||||
let resolutions = [OneK, TwoK, ThreeK, FourK];
|
||||
for pair in resolutions.windows(2) {
|
||||
let lower = make_request(One, Some(High), Some(size), Some(pair[0]))
|
||||
.calculate_cost_in_cents();
|
||||
let higher = make_request(One, Some(High), Some(size), Some(pair[1]))
|
||||
.calculate_cost_in_cents();
|
||||
assert!(
|
||||
lower <= higher,
|
||||
"{size:?}: {pair:?} — lower res ({lower}¢) should be <= higher res ({higher}¢)",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user