mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 11:49:30 -04:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d68c1db25 | ||
|
|
38d3e1d129 | ||
|
|
b1d506807d | ||
|
|
00b759c94e | ||
|
|
a90b03c31d | ||
|
|
c8e72f01f1 | ||
|
|
b94787e10c | ||
|
|
ee0a51b399 | ||
|
|
09af5d8f9c | ||
|
|
01db05c169 | ||
|
|
6a69c26fa2 | ||
|
|
a50ce98803 | ||
|
|
8d7b623a96 | ||
|
|
1cd2900b98 | ||
|
|
5bb6531009 | ||
|
|
87153de16d | ||
|
|
4982d3162d | ||
|
|
83cf580a9a | ||
|
|
246b189a2a | ||
|
|
b1187543c9 | ||
|
|
a9e26d1321 | ||
|
|
f7500a3026 | ||
|
|
fa4776bb21 |
@@ -0,0 +1,13 @@
|
||||
-- Daily ArtCraft generations since January 1st, 2026.
|
||||
|
||||
SELECT
|
||||
DATE(created_at) AS day,
|
||||
COUNT(*) AS generations
|
||||
FROM generic_inference_jobs
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY day
|
||||
ORDER BY day DESC;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
-- Daily ArtCraft generations by power users since January 1st, 2026.
|
||||
-- A "power user" is a user who created more than 10 ArtCraft jobs in the calendar month.
|
||||
|
||||
SELECT
|
||||
DATE(j.created_at) AS day,
|
||||
COUNT(*) AS generations
|
||||
FROM generic_inference_jobs j
|
||||
INNER JOIN (
|
||||
SELECT maybe_creator_user_token, DATE_FORMAT(created_at, '%Y-%m') AS month
|
||||
FROM generic_inference_jobs
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND maybe_creator_user_token IS NOT NULL
|
||||
AND job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY maybe_creator_user_token, month
|
||||
HAVING COUNT(*) > 10
|
||||
) power ON power.maybe_creator_user_token = j.maybe_creator_user_token
|
||||
AND power.month = DATE_FORMAT(j.created_at, '%Y-%m')
|
||||
WHERE j.created_at >= '2026-01-01'
|
||||
AND j.maybe_creator_user_token IS NOT NULL
|
||||
AND j.job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY day
|
||||
ORDER BY day DESC;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Monthly ArtCraft generations since January 1st, 2026.
|
||||
|
||||
SELECT
|
||||
DATE_FORMAT(created_at, '%Y-%m') AS month,
|
||||
COUNT(*) AS generations
|
||||
FROM generic_inference_jobs
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY month
|
||||
ORDER BY month DESC;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
-- Monthly ArtCraft generations by power users since January 1st, 2026.
|
||||
-- A "power user" is a user who created more than 10 ArtCraft jobs in the calendar month.
|
||||
|
||||
SELECT
|
||||
month,
|
||||
SUM(job_count) AS generations
|
||||
FROM (
|
||||
SELECT
|
||||
maybe_creator_user_token,
|
||||
DATE_FORMAT(created_at, '%Y-%m') AS month,
|
||||
COUNT(*) AS job_count
|
||||
FROM generic_inference_jobs
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND maybe_creator_user_token IS NOT NULL
|
||||
AND job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY maybe_creator_user_token, month
|
||||
HAVING COUNT(*) > 10
|
||||
) power
|
||||
GROUP BY month
|
||||
ORDER BY month DESC;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Weekly ArtCraft generations since January 1st, 2026.
|
||||
|
||||
SELECT
|
||||
DATE(DATE_SUB(created_at, INTERVAL WEEKDAY(created_at) DAY)) AS week_start,
|
||||
COUNT(*) AS generations
|
||||
FROM generic_inference_jobs
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY week_start
|
||||
ORDER BY week_start DESC;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
-- Weekly ArtCraft generations by power users since January 1st, 2026.
|
||||
-- A "power user" is a user who created more than 10 ArtCraft jobs in the calendar month.
|
||||
|
||||
SELECT
|
||||
DATE(DATE_SUB(j.created_at, INTERVAL WEEKDAY(j.created_at) DAY)) AS week_start,
|
||||
COUNT(*) AS generations
|
||||
FROM generic_inference_jobs j
|
||||
INNER JOIN (
|
||||
SELECT maybe_creator_user_token, DATE_FORMAT(created_at, '%Y-%m') AS month
|
||||
FROM generic_inference_jobs
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND maybe_creator_user_token IS NOT NULL
|
||||
AND job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY maybe_creator_user_token, month
|
||||
HAVING COUNT(*) > 10
|
||||
) power ON power.maybe_creator_user_token = j.maybe_creator_user_token
|
||||
AND power.month = DATE_FORMAT(j.created_at, '%Y-%m')
|
||||
WHERE j.created_at >= '2026-01-01'
|
||||
AND j.maybe_creator_user_token IS NOT NULL
|
||||
AND j.job_type NOT IN (
|
||||
'gpt_sovits', 'tacotron2', 'rvc_v2', 'f5_tts',
|
||||
'so_vits_svc', 'styletts2', 'face_fusion', 'seed_vc', 'comfy_ui'
|
||||
)
|
||||
GROUP BY week_start
|
||||
ORDER BY week_start DESC;
|
||||
@@ -41,11 +41,17 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
|
||||
#[serde(rename = "seedance_2p0_g")]
|
||||
Seedance2p0Global,
|
||||
#[serde(rename = "seedance_2p0_bp")]
|
||||
Seedance2p0BytePlus,
|
||||
|
||||
#[serde(rename = "seedance_2p0_fast_g")]
|
||||
Seedance2p0FastGlobal,
|
||||
#[serde(rename = "seedance_2p0_bp_fast")]
|
||||
Seedance2p0BytePlusFast,
|
||||
|
||||
#[serde(rename = "seedance_2p0_u")]
|
||||
Seedance2p0Ultra,
|
||||
|
||||
#[serde(rename = "seedance_2p0_u_fast")]
|
||||
Seedance2p0UltraFast,
|
||||
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
@@ -70,4 +76,10 @@ pub enum CommonVideoModel {
|
||||
|
||||
#[serde(rename = "veo_3p1_fast")]
|
||||
Veo3p1Fast,
|
||||
|
||||
#[serde(rename = "preview_model")]
|
||||
PreviewModel,
|
||||
|
||||
#[serde(rename = "preview_model_fast")]
|
||||
PreviewModelFast,
|
||||
}
|
||||
|
||||
+30
-12
@@ -48,14 +48,18 @@ use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_see
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::plan_generate_video_seedance2pro_seedance2p0_fast;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::build::build_artcraft_happy_horse_1p0;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model::build::build_artcraft_preview_model;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::build::build_artcraft_preview_model_fast;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::build::build_artcraft_seedance_2p0;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::build::build_artcraft_seedance_2p0_fast;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_g::build::build_artcraft_seedance_2p0_g;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast_g::build::build_artcraft_seedance_2p0_fast_g;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u::build::build_artcraft_seedance_2p0_u;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u_fast::build::build_artcraft_seedance_2p0_u_fast;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::build::build_artcraft_seedance_2p0_bp;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::build::build_artcraft_seedance_2p0_bp_fast;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::build::build_kinovi_happy_horse_1p0;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build::build_kinovi_seedance_2p0;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_g;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::build::build_gmicloud_seedance_2p0_fast_g;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_u;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::build::build_gmicloud_seedance_2p0_u_fast;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
@@ -148,13 +152,20 @@ impl GenerateVideoRequestBuilder {
|
||||
|
||||
pub fn use_new_builder(&self) -> bool {
|
||||
match (self.provider, self.model) {
|
||||
// Artcraft
|
||||
(Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Global) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0FastGlobal) => true,
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0Global) => true,
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0FastGlobal) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Ultra) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0UltraFast) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0BytePlus) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0BytePlusFast) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::PreviewModel) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::PreviewModelFast) => true,
|
||||
// GmiCloud
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0Ultra) => true,
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0UltraFast) => true,
|
||||
// Kinovi
|
||||
(Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => true,
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => true,
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => true,
|
||||
@@ -165,13 +176,20 @@ impl GenerateVideoRequestBuilder {
|
||||
// New builder (transitional state)
|
||||
pub fn build2(self) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
match (self.provider, self.model) {
|
||||
// Artcraft
|
||||
(Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => build_artcraft_happy_horse_1p0(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0) => build_artcraft_seedance_2p0(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => build_artcraft_seedance_2p0_fast(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Global) => build_artcraft_seedance_2p0_g(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0FastGlobal) => build_artcraft_seedance_2p0_fast_g(self),
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0Global) => build_gmicloud_seedance_2p0_g(self),
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0FastGlobal) => build_gmicloud_seedance_2p0_fast_g(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Ultra) => build_artcraft_seedance_2p0_u(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0UltraFast) => build_artcraft_seedance_2p0_u_fast(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0BytePlus) => build_artcraft_seedance_2p0_bp(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0BytePlusFast) => build_artcraft_seedance_2p0_bp_fast(self),
|
||||
(Provider::Artcraft, CommonVideoModel::PreviewModel) => build_artcraft_preview_model(self),
|
||||
(Provider::Artcraft, CommonVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self),
|
||||
// GmiCloud
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0Ultra) => build_gmicloud_seedance_2p0_u(self),
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0UltraFast) => build_gmicloud_seedance_2p0_u_fast(self),
|
||||
// Kinovi
|
||||
(Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
|
||||
|
||||
+92
-179
@@ -13,15 +13,40 @@ use crate::generate::generate_video_v2::providers::artcraft::resolve::{
|
||||
resolve_audio_list_ref, resolve_character_list_ref, resolve_image_list_ref,
|
||||
resolve_image_ref, resolve_video_list_ref,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_g::request::ArtcraftSeedance2p0GRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_g(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
/// Which output resolutions a model supports.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum SupportedResolutions {
|
||||
/// 480p, 720p, 1080p
|
||||
Full,
|
||||
/// 480p, 720p only (1080p downgrades to 720p)
|
||||
Fast,
|
||||
/// 720p, 1080p only (480p upgrades)
|
||||
NoFourEightyP,
|
||||
}
|
||||
|
||||
/// Which aspect ratios a model supports for the 21:9 ultra-wide slot.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum UltraWideSupport {
|
||||
/// 21:9 maps directly to WideTwentyOneByNine
|
||||
Supported,
|
||||
/// 21:9 is not a direct option; falls through to nearest_aspect_ratio
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
/// Build an `OmniGenVideoCostAndGenerateRequest` from the builder, using
|
||||
/// model-specific configuration for the model enum variant, resolution
|
||||
/// strategy, and ultra-wide aspect ratio support.
|
||||
pub fn build_artcraft_omni_request(
|
||||
mut builder: GenerateVideoRequestBuilder,
|
||||
model: CommonVideoModelEnum,
|
||||
resolutions: SupportedResolutions,
|
||||
ultra_wide: UltraWideSupport,
|
||||
) -> Result<OmniGenVideoCostAndGenerateRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy, ultra_wide)?;
|
||||
let resolution = plan_output_resolution(builder.resolution.take(), strategy, resolutions)?;
|
||||
let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let prompt = builder.prompt.take();
|
||||
@@ -34,8 +59,8 @@ pub fn build_artcraft_seedance_2p0_g(mut builder: GenerateVideoRequestBuilder) -
|
||||
let reference_characters = resolve_character_list_ref(builder.reference_character_tokens.take());
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
model: Some(CommonVideoModelEnum::Seedance2p0Global),
|
||||
Ok(OmniGenVideoCostAndGenerateRequest {
|
||||
model: Some(model),
|
||||
idempotency_token: Some(idempotency_token),
|
||||
prompt,
|
||||
start_frame_image_media_token: start_frame,
|
||||
@@ -51,17 +76,15 @@ pub fn build_artcraft_seedance_2p0_g(mut builder: GenerateVideoRequestBuilder) -
|
||||
negative_prompt: None,
|
||||
generate_audio: None,
|
||||
quality: None,
|
||||
};
|
||||
|
||||
let state = ArtcraftSeedance2p0GRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0G(state)))
|
||||
})
|
||||
}
|
||||
|
||||
// -- Plan helpers --
|
||||
// ── Plan helpers ──
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
ultra_wide: UltraWideSupport,
|
||||
) -> Result<Option<CommonAspectRatioEnum>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
@@ -80,7 +103,19 @@ fn plan_aspect_ratio(
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(CommonAspectRatioEnum::WideFourByThree)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(CommonAspectRatioEnum::TallThreeByFour)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(CommonAspectRatioEnum::WideTwentyOneByNine)),
|
||||
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => match ultra_wide {
|
||||
UltraWideSupport::Supported => Ok(Some(CommonAspectRatioEnum::WideTwentyOneByNine)),
|
||||
UltraWideSupport::Unsupported => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", CommonAspectRatio::WideTwentyOneByNine),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(nearest_aspect_ratio(CommonAspectRatio::WideTwentyOneByNine))),
|
||||
},
|
||||
},
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
@@ -101,6 +136,7 @@ fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnu
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideTwentyOneByNine => CommonAspectRatioEnum::WideSixteenByNine,
|
||||
CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineBySixteen,
|
||||
@@ -108,17 +144,46 @@ fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnu
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Pro supports output resolutions: 480p, 720p, 1080p.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
supported: SupportedResolutions,
|
||||
) -> Result<Option<CommonResolutionEnum>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonResolution::FourEightyP) => Ok(Some(CommonResolutionEnum::FourEightyP)),
|
||||
Some(CommonResolution::FourEightyP) => match supported {
|
||||
SupportedResolutions::Full | SupportedResolutions::Fast => {
|
||||
Ok(Some(CommonResolutionEnum::FourEightyP))
|
||||
}
|
||||
SupportedResolutions::NoFourEightyP => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", CommonResolution::FourEightyP),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(CommonResolutionEnum::TenEightyP)),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
},
|
||||
},
|
||||
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
Some(CommonResolution::TenEightyP) => Ok(Some(CommonResolutionEnum::TenEightyP)),
|
||||
|
||||
Some(CommonResolution::TenEightyP) => match supported {
|
||||
SupportedResolutions::Full | SupportedResolutions::NoFourEightyP => {
|
||||
Ok(Some(CommonResolutionEnum::TenEightyP))
|
||||
}
|
||||
SupportedResolutions::Fast => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", CommonResolution::TenEightyP),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
},
|
||||
},
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
@@ -128,14 +193,16 @@ fn plan_output_resolution(
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolutionEnum::FourEightyP,
|
||||
Ok(Some(match (unsupported, supported) {
|
||||
(CommonResolution::HalfK, _) => CommonResolutionEnum::FourEightyP,
|
||||
(_, SupportedResolutions::Fast) => CommonResolutionEnum::SevenTwentyP,
|
||||
_ => CommonResolutionEnum::TenEightyP,
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolutionEnum::FourEightyP,
|
||||
Ok(Some(match (unsupported, supported) {
|
||||
(CommonResolution::HalfK, _) => CommonResolutionEnum::FourEightyP,
|
||||
(_, SupportedResolutions::Fast) => CommonResolutionEnum::SevenTwentyP,
|
||||
_ => CommonResolutionEnum::TenEightyP,
|
||||
}))
|
||||
}
|
||||
@@ -143,8 +210,8 @@ fn plan_output_resolution(
|
||||
}
|
||||
}
|
||||
|
||||
// Batch counts: 1, 2, 4.
|
||||
fn plan_batch_count(
|
||||
/// Batch counts: 1, 2, 4.
|
||||
pub fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u16, ArtcraftRouterError> {
|
||||
@@ -167,8 +234,8 @@ fn plan_batch_count(
|
||||
}
|
||||
}
|
||||
|
||||
// Duration: 4-15 seconds.
|
||||
fn plan_duration(
|
||||
/// Duration: 4-15 seconds.
|
||||
pub fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u8>, ArtcraftRouterError> {
|
||||
@@ -188,157 +255,3 @@ fn plan_duration(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Field conversions ──
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_seedance_2p0() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0Global)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolution ──
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Media tokens ──
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_seedance_2p0_g(GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = base_builder();
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0GRequestState {
|
||||
let result = build_artcraft_seedance_2p0_g(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0G(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p0 request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-169
@@ -1,185 +1,25 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::resolve::{
|
||||
resolve_audio_list_ref, resolve_character_list_ref, resolve_image_list_ref,
|
||||
resolve_image_ref, resolve_video_list_ref,
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::request::ArtcraftHappyHorse1p0RequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_happy_horse_1p0(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
|
||||
let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let prompt = builder.prompt.take();
|
||||
|
||||
let start_frame = resolve_image_ref(builder.start_frame.take())?;
|
||||
let end_frame = resolve_image_ref(builder.end_frame.take())?;
|
||||
let reference_images = resolve_image_list_ref(builder.reference_images.take())?;
|
||||
let reference_videos = resolve_video_list_ref(builder.reference_videos.take())?;
|
||||
let reference_audio = resolve_audio_list_ref(builder.reference_audio.take())?;
|
||||
let reference_characters = resolve_character_list_ref(builder.reference_character_tokens.take());
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
model: Some(CommonVideoModelEnum::HappyHorse1p0),
|
||||
idempotency_token: Some(idempotency_token),
|
||||
prompt,
|
||||
start_frame_image_media_token: start_frame,
|
||||
end_frame_image_media_token: end_frame,
|
||||
reference_image_media_tokens: reference_images,
|
||||
reference_video_media_tokens: reference_videos,
|
||||
reference_audio_media_tokens: reference_audio,
|
||||
reference_character_tokens: reference_characters,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: duration_seconds.map(|d| d as u16),
|
||||
video_batch_count: Some(batch_count),
|
||||
negative_prompt: None,
|
||||
generate_audio: None,
|
||||
quality: None,
|
||||
};
|
||||
|
||||
pub fn build_artcraft_happy_horse_1p0(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::HappyHorse1p0,
|
||||
SupportedResolutions::NoFourEightyP,
|
||||
UltraWideSupport::Unsupported,
|
||||
)?;
|
||||
let state = ArtcraftHappyHorse1p0RequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftHappyHorse1p0(state)))
|
||||
}
|
||||
|
||||
// ── Plan helpers ──
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonAspectRatioEnum>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(CommonAspectRatioEnum::WideSixteenByNine))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(CommonAspectRatioEnum::TallNineBySixteen))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(CommonAspectRatioEnum::Square))
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(CommonAspectRatioEnum::WideFourByThree)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(CommonAspectRatioEnum::TallThreeByFour)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(nearest_aspect_ratio(unsupported)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnum {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideTwentyOneByNine => CommonAspectRatioEnum::WideSixteenByNine,
|
||||
CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineBySixteen,
|
||||
_ => CommonAspectRatioEnum::Square,
|
||||
}
|
||||
}
|
||||
|
||||
// Happy Horse supports 720p and 1080p only (no 480p).
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonResolutionEnum>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
Some(CommonResolution::TenEightyP) => Ok(Some(CommonResolutionEnum::TenEightyP)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(Some(CommonResolutionEnum::TenEightyP))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(CommonResolutionEnum::SevenTwentyP))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u16, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 | 2 | 4 => Ok(count),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(4),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(if count < 4 { 2 } else { 4 })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u8>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(d as u8)),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(d.clamp(MIN, MAX) as u8)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
|
||||
+3
-17
@@ -1,12 +1,9 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftHappyHorse1p0RequestState {
|
||||
@@ -15,18 +12,7 @@ pub struct ArtcraftHappyHorse1p0RequestState {
|
||||
|
||||
impl ArtcraftHappyHorse1p0RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_video_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
self.request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token: response.inference_job_token.clone(),
|
||||
all_inference_job_tokens: vec![response.inference_job_token],
|
||||
}))
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -1,6 +1,12 @@
|
||||
pub(crate) mod build_common;
|
||||
pub mod happy_horse_1p0;
|
||||
pub mod preview_model;
|
||||
pub mod preview_model_fast;
|
||||
pub(crate) mod request_common;
|
||||
pub(crate) mod resolve;
|
||||
pub mod seedance_2p0;
|
||||
pub mod seedance_2p0_fast;
|
||||
pub mod seedance_2p0_fast_g;
|
||||
pub mod seedance_2p0_g;
|
||||
pub mod seedance_2p0_u_fast;
|
||||
pub mod seedance_2p0_u;
|
||||
pub mod seedance_2p0_bp;
|
||||
pub mod seedance_2p0_bp_fast;
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model::request::ArtcraftPreviewModelRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_preview_model(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::PreviewModel,
|
||||
SupportedResolutions::Full,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftPreviewModelRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftPreviewModel(state)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Field conversions ──
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_preview_model() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::PreviewModel)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolution ──
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Media tokens ──
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_preview_model(GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = base_builder();
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftPreviewModelRequestState {
|
||||
let result = build_artcraft_preview_model(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftPreviewModel(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftPreviewModel request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model::request::ArtcraftPreviewModelRequestState;
|
||||
|
||||
// ── Pricing constants ──
|
||||
//
|
||||
// ArtCraft credits: 100 credits = $1.00. Credits always equal USD cents.
|
||||
|
||||
/// USD cents per second by resolution:
|
||||
/// 480p: $0.10/s = 10.0 ¢/s
|
||||
/// 720p: $0.25/s = 25.0 ¢/s
|
||||
/// 1080p: $0.50/s = 50.0 ¢/s
|
||||
const CENTS_PER_SECOND_480P: f64 = 10.0;
|
||||
const CENTS_PER_SECOND_720P: f64 = 25.0;
|
||||
const CENTS_PER_SECOND_1080P: f64 = 50.0;
|
||||
|
||||
pub struct ArtcraftPreviewModelCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftPreviewModelCostState {
|
||||
pub fn from_request(request: &ArtcraftPreviewModelRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
|
||||
Self { resolution, duration_seconds, batch_count }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let cents_per_second = match self.resolution {
|
||||
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
|
||||
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
|
||||
// Everything else (including 720p) prices at 720p.
|
||||
_ => CENTS_PER_SECOND_720P,
|
||||
};
|
||||
|
||||
let usd_cents = (self.duration_seconds as f64 * cents_per_second * self.batch_count as f64).round() as u64;
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
// -- 720p pricing --
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1), 100);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1), 125);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1), 250);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1), 375);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2), 250);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 2), 750);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 500);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 4), 1500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(cost_cents(None, 5, 1), cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// -- 480p pricing --
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 4, 1), 40);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 1), 50);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10, 1), 100);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15, 1), 150);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 2), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 4), 200);
|
||||
}
|
||||
}
|
||||
|
||||
// -- 1080p pricing --
|
||||
|
||||
mod pricing_1080p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 4, 1), 200);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 1), 250);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 10, 1), 500);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 15, 1), 750);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 2), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 4), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Relative pricing --
|
||||
|
||||
mod relative_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_480p_cheaper_than_720p_cheaper_than_1080p() {
|
||||
let c480 = cost_cents(Some(CommonResolution::FourEightyP), 5, 1);
|
||||
let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1);
|
||||
let c1080 = cost_cents(Some(CommonResolution::TenEightyP), 5, 1);
|
||||
assert!(c480 < c720);
|
||||
assert!(c720 < c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1);
|
||||
let c10 = cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1);
|
||||
let c15 = cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1);
|
||||
assert!(c4 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_batch() {
|
||||
let b1 = cost_cents(Some(CommonResolution::TenEightyP), 5, 1);
|
||||
let b2 = cost_cents(Some(CommonResolution::TenEightyP), 5, 2);
|
||||
let b4 = cost_cents(Some(CommonResolution::TenEightyP), 5, 4);
|
||||
assert!(b1 < b2);
|
||||
assert!(b2 < b4);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Credits equal cents --
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents_all_combos() {
|
||||
let resolutions = [
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
Some(CommonResolution::TenEightyP),
|
||||
None,
|
||||
];
|
||||
for res in resolutions {
|
||||
for dur in [4, 5, 10, 15] {
|
||||
for batch in [1, 2, 4] {
|
||||
let cost = build_cost(res, dur, batch);
|
||||
assert_eq!(
|
||||
cost.cost_in_credits, cost.cost_in_usd_cents,
|
||||
"credits should equal cents for res={:?} dur={}s batch={}",
|
||||
res, dur, batch,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::PreviewModel,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
}
|
||||
|
||||
fn cost_cents(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> u64 {
|
||||
build_cost(resolution, duration_seconds, video_batch_count)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftPreviewModelRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftPreviewModelRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::request::ArtcraftPreviewModelFastRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_preview_model_fast(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::PreviewModelFast,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftPreviewModelFastRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftPreviewModelFast(state)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Field conversions ──
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_preview_model_fast() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::PreviewModelFast)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolution ──
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_error_out() {
|
||||
let result = build_artcraft_preview_model_fast(GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::PreviewModelFast,
|
||||
provider: Provider::Artcraft,
|
||||
resolution: Some(CommonResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Media tokens ──
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_preview_model_fast(GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::PreviewModelFast,
|
||||
provider: Provider::Artcraft,
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::PreviewModelFast,
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftPreviewModelFastRequestState {
|
||||
let result = build_artcraft_preview_model_fast(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftPreviewModelFast(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftPreviewModelFast request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::request::ArtcraftPreviewModelFastRequestState;
|
||||
|
||||
// ── Pricing constants ──
|
||||
//
|
||||
// ArtCraft credits: 100 credits = $1.00. Credits always equal USD cents.
|
||||
|
||||
/// USD cents per second by resolution:
|
||||
/// 480p: $0.09/s = 9.0 ¢/s
|
||||
/// 720p: $0.20/s = 20.0 ¢/s
|
||||
const CENTS_PER_SECOND_480P: f64 = 9.0;
|
||||
const CENTS_PER_SECOND_720P: f64 = 20.0;
|
||||
|
||||
pub struct ArtcraftPreviewModelFastCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftPreviewModelFastCostState {
|
||||
pub fn from_request(request: &ArtcraftPreviewModelFastRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
|
||||
Self { resolution, duration_seconds, batch_count }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let cents_per_second = match self.resolution {
|
||||
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
|
||||
// Everything else (including 720p and unsupported resolutions) prices at 720p.
|
||||
_ => CENTS_PER_SECOND_720P,
|
||||
};
|
||||
|
||||
let usd_cents = (self.duration_seconds as f64 * cents_per_second * self.batch_count as f64).round() as u64;
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
// -- 720p pricing --
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1), 80);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1), 100);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1), 200);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1), 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2), 200);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 2), 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 400);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 4), 1200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(cost_cents(None, 5, 1), cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// -- 480p pricing --
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 4, 1), 36);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 1), 45);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10, 1), 90);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15, 1), 135);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 2), 90);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 4), 180);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Relative pricing --
|
||||
|
||||
mod relative_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_480p_cheaper_than_720p() {
|
||||
let c480 = cost_cents(Some(CommonResolution::FourEightyP), 5, 1);
|
||||
let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1);
|
||||
assert!(c480 < c720);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1);
|
||||
let c10 = cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1);
|
||||
let c15 = cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1);
|
||||
assert!(c4 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_batch() {
|
||||
let b1 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1);
|
||||
let b2 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2);
|
||||
let b4 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4);
|
||||
assert!(b1 < b2);
|
||||
assert!(b2 < b4);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Credits equal cents --
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents_all_combos() {
|
||||
let resolutions = [
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
None,
|
||||
];
|
||||
for res in resolutions {
|
||||
for dur in [4, 5, 10, 15] {
|
||||
for batch in [1, 2, 4] {
|
||||
let cost = build_cost(res, dur, batch);
|
||||
assert_eq!(
|
||||
cost.cost_in_credits, cost.cost_in_usd_cents,
|
||||
"credits should equal cents for res={:?} dur={}s batch={}",
|
||||
res, dur, batch,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::PreviewModelFast,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
}
|
||||
|
||||
fn cost_cents(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> u64 {
|
||||
build_cost(resolution, duration_seconds, video_batch_count)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftPreviewModelFastRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftPreviewModelFastRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
|
||||
/// Send a video generation request to the Artcraft omni-gen endpoint.
|
||||
/// All Artcraft model request states delegate to this function.
|
||||
pub async fn send_artcraft_omni_request(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_video_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token: response.inference_job_token.clone(),
|
||||
all_inference_job_tokens: vec![response.inference_job_token],
|
||||
}))
|
||||
}
|
||||
+9
-178
@@ -1,194 +1,25 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::resolve::{
|
||||
resolve_audio_list_ref, resolve_character_list_ref, resolve_image_list_ref,
|
||||
resolve_image_ref, resolve_video_list_ref,
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
|
||||
let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let prompt = builder.prompt.take();
|
||||
|
||||
let start_frame = resolve_image_ref(builder.start_frame.take())?;
|
||||
let end_frame = resolve_image_ref(builder.end_frame.take())?;
|
||||
let reference_images = resolve_image_list_ref(builder.reference_images.take())?;
|
||||
let reference_videos = resolve_video_list_ref(builder.reference_videos.take())?;
|
||||
let reference_audio = resolve_audio_list_ref(builder.reference_audio.take())?;
|
||||
let reference_characters = resolve_character_list_ref(builder.reference_character_tokens.take());
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
model: Some(CommonVideoModelEnum::Seedance2p0),
|
||||
idempotency_token: Some(idempotency_token),
|
||||
prompt,
|
||||
start_frame_image_media_token: start_frame,
|
||||
end_frame_image_media_token: end_frame,
|
||||
reference_image_media_tokens: reference_images,
|
||||
reference_video_media_tokens: reference_videos,
|
||||
reference_audio_media_tokens: reference_audio,
|
||||
reference_character_tokens: reference_characters,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: duration_seconds.map(|d| d as u16),
|
||||
video_batch_count: Some(batch_count),
|
||||
negative_prompt: None,
|
||||
generate_audio: None,
|
||||
quality: None,
|
||||
};
|
||||
|
||||
pub fn build_artcraft_seedance_2p0(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0,
|
||||
SupportedResolutions::Full,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0RequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0(state)))
|
||||
}
|
||||
|
||||
// -- Plan helpers --
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonAspectRatioEnum>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(CommonAspectRatioEnum::WideSixteenByNine))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(CommonAspectRatioEnum::TallNineBySixteen))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(CommonAspectRatioEnum::Square))
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(CommonAspectRatioEnum::WideFourByThree)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(CommonAspectRatioEnum::TallThreeByFour)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(CommonAspectRatioEnum::WideTwentyOneByNine)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(nearest_aspect_ratio(unsupported)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnum {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineBySixteen,
|
||||
_ => CommonAspectRatioEnum::Square,
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Pro supports output resolutions: 480p, 720p, 1080p.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonResolutionEnum>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonResolution::FourEightyP) => Ok(Some(CommonResolutionEnum::FourEightyP)),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
Some(CommonResolution::TenEightyP) => Ok(Some(CommonResolutionEnum::TenEightyP)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolutionEnum::FourEightyP,
|
||||
_ => CommonResolutionEnum::TenEightyP,
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolutionEnum::FourEightyP,
|
||||
_ => CommonResolutionEnum::TenEightyP,
|
||||
}))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Batch counts: 1, 2, 4.
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u16, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 | 2 | 4 => Ok(count),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(4),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(if count < 4 { 2 } else { 4 })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Duration: 4-15 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u8>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(d as u8)),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(d.clamp(MIN, MAX) as u8)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
|
||||
+3
-18
@@ -1,33 +1,18 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p0RequestState {
|
||||
/// Final materialized request; ready to fire via the omni-gen video endpoint.
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_video_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
self.request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token: response.inference_job_token.clone(),
|
||||
all_inference_job_tokens: vec![response.inference_job_token],
|
||||
}))
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_bp(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0BytePlus,
|
||||
SupportedResolutions::Full,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0BytePlusRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(state)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_seedance_2p0_plus() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0BytePlus)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_seedance_2p0_bp(GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = base_builder();
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0BytePlusRequestState {
|
||||
let result = build_artcraft_seedance_2p0_bp(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p0BytePlus request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState;
|
||||
|
||||
/// USD cents per second by resolution:
|
||||
/// 480p: $0.10/s = 10.0 ¢/s
|
||||
/// 720p: $0.25/s = 25.0 ¢/s
|
||||
/// 1080p: $0.50/s = 50.0 ¢/s
|
||||
const CENTS_PER_SECOND_480P: f64 = 10.0;
|
||||
const CENTS_PER_SECOND_720P: f64 = 25.0;
|
||||
const CENTS_PER_SECOND_1080P: f64 = 50.0;
|
||||
|
||||
pub struct ArtcraftSeedance2p0BytePlusCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p0BytePlusRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
Self { resolution, duration_seconds, batch_count }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let cents_per_second = match self.resolution {
|
||||
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
|
||||
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
|
||||
_ => CENTS_PER_SECOND_720P,
|
||||
};
|
||||
|
||||
let usd_cents = (self.duration_seconds as f64 * cents_per_second * self.batch_count as f64).round() as u64;
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1), 100);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1), 125);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1), 250);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1), 375);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2), 250);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(cost_cents(None, 5, 1), cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1));
|
||||
}
|
||||
}
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 4, 1), 40);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 1), 50);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10, 1), 100);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15, 1), 150);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 2), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 4), 200);
|
||||
}
|
||||
}
|
||||
|
||||
mod pricing_1080p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 4, 1), 200);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 1), 250);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 10, 1), 500);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 15, 1), 750);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 2), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 4), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
mod relative_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_480p_cheaper_than_720p_cheaper_than_1080p() {
|
||||
let c480 = cost_cents(Some(CommonResolution::FourEightyP), 5, 1);
|
||||
let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1);
|
||||
let c1080 = cost_cents(Some(CommonResolution::TenEightyP), 5, 1);
|
||||
assert!(c480 < c720);
|
||||
assert!(c720 < c1080);
|
||||
}
|
||||
}
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents() {
|
||||
for res in [Some(CommonResolution::FourEightyP), Some(CommonResolution::SevenTwentyP), Some(CommonResolution::TenEightyP), None] {
|
||||
for dur in [4, 5, 10, 15] {
|
||||
for batch in [1, 2, 4] {
|
||||
let cost = build_cost(res, dur, batch);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0BytePlus,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
..Default::default()
|
||||
}.build2().expect("build2").estimate_cost().expect("estimate_cost")
|
||||
}
|
||||
|
||||
fn cost_cents(resolution: Option<CommonResolution>, duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
build_cost(resolution, duration_seconds, video_batch_count).cost_in_usd_cents.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p0BytePlusRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::request::ArtcraftSeedance2p0BytePlusFastRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_bp_fast(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0BytePlusFast,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0BytePlusFastRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(state)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_seedance_2p0_plus_fast() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0BytePlusFast)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_error_out() {
|
||||
let result = build_artcraft_seedance_2p0_bp_fast(GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusFast,
|
||||
provider: Provider::Artcraft,
|
||||
resolution: Some(CommonResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_seedance_2p0_bp_fast(GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusFast,
|
||||
provider: Provider::Artcraft,
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusFast,
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0BytePlusFastRequestState {
|
||||
let result = build_artcraft_seedance_2p0_bp_fast(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p0BytePlusFast request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::request::ArtcraftSeedance2p0BytePlusFastRequestState;
|
||||
|
||||
/// USD cents per second by resolution:
|
||||
/// 480p: $0.09/s = 9.0 ¢/s
|
||||
/// 720p: $0.20/s = 20.0 ¢/s
|
||||
const CENTS_PER_SECOND_480P: f64 = 9.0;
|
||||
const CENTS_PER_SECOND_720P: f64 = 20.0;
|
||||
|
||||
pub struct ArtcraftSeedance2p0BytePlusFastCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusFastCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p0BytePlusFastRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
Self { resolution, duration_seconds, batch_count }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let cents_per_second = match self.resolution {
|
||||
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
|
||||
_ => CENTS_PER_SECOND_720P,
|
||||
};
|
||||
|
||||
let usd_cents = (self.duration_seconds as f64 * cents_per_second * self.batch_count as f64).round() as u64;
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1), 80);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1), 100);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1), 200);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1), 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 400);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(cost_cents(None, 5, 1), cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1));
|
||||
}
|
||||
}
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 4, 1), 36);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 1), 45);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10, 1), 90);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15, 1), 135);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 2), 90);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 4), 180);
|
||||
}
|
||||
}
|
||||
|
||||
mod relative_pricing_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_480p_cheaper_than_720p() {
|
||||
assert!(cost_cents(Some(CommonResolution::FourEightyP), 5, 1) < cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1));
|
||||
}
|
||||
}
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents() {
|
||||
for res in [Some(CommonResolution::FourEightyP), Some(CommonResolution::SevenTwentyP), None] {
|
||||
for dur in [4, 5, 10, 15] {
|
||||
for batch in [1, 2, 4] {
|
||||
let cost = build_cost(res, dur, batch);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0BytePlusFast,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
..Default::default()
|
||||
}.build2().expect("build2").estimate_cost().expect("estimate_cost")
|
||||
}
|
||||
|
||||
fn cost_cents(resolution: Option<CommonResolution>, duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
build_cost(resolution, duration_seconds, video_batch_count).cost_in_usd_cents.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p0BytePlusFastRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0BytePlusFastRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+9
-190
@@ -1,206 +1,25 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::resolve::{
|
||||
resolve_audio_list_ref, resolve_character_list_ref, resolve_image_list_ref,
|
||||
resolve_image_ref, resolve_video_list_ref,
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::request::ArtcraftSeedance2p0FastRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_fast(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
|
||||
let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let prompt = builder.prompt.take();
|
||||
|
||||
let start_frame = resolve_image_ref(builder.start_frame.take())?;
|
||||
let end_frame = resolve_image_ref(builder.end_frame.take())?;
|
||||
let reference_images = resolve_image_list_ref(builder.reference_images.take())?;
|
||||
let reference_videos = resolve_video_list_ref(builder.reference_videos.take())?;
|
||||
let reference_audio = resolve_audio_list_ref(builder.reference_audio.take())?;
|
||||
let reference_characters = resolve_character_list_ref(builder.reference_character_tokens.take());
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
model: Some(CommonVideoModelEnum::Seedance2p0Fast),
|
||||
idempotency_token: Some(idempotency_token),
|
||||
prompt,
|
||||
start_frame_image_media_token: start_frame,
|
||||
end_frame_image_media_token: end_frame,
|
||||
reference_image_media_tokens: reference_images,
|
||||
reference_video_media_tokens: reference_videos,
|
||||
reference_audio_media_tokens: reference_audio,
|
||||
reference_character_tokens: reference_characters,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: duration_seconds.map(|d| d as u16),
|
||||
video_batch_count: Some(batch_count),
|
||||
negative_prompt: None,
|
||||
generate_audio: None,
|
||||
quality: None,
|
||||
};
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_fast(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0Fast,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0FastRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Fast(state)))
|
||||
}
|
||||
|
||||
// -- Plan helpers --
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonAspectRatioEnum>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(None),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(CommonAspectRatioEnum::WideSixteenByNine))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(CommonAspectRatioEnum::TallNineBySixteen))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(CommonAspectRatioEnum::Square))
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(CommonAspectRatioEnum::WideFourByThree)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(CommonAspectRatioEnum::TallThreeByFour)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(CommonAspectRatioEnum::WideTwentyOneByNine)),
|
||||
|
||||
// Mismatches
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(nearest_aspect_ratio(unsupported)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnum {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineBySixteen,
|
||||
_ => CommonAspectRatioEnum::Square,
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Fast supports output resolutions: 480p and 720p only.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonResolutionEnum>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonResolution::FourEightyP) => Ok(Some(CommonResolutionEnum::FourEightyP)),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
|
||||
// 1080p not supported for Fast
|
||||
Some(CommonResolution::TenEightyP) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", CommonResolution::TenEightyP),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
},
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolutionEnum::FourEightyP,
|
||||
_ => CommonResolutionEnum::SevenTwentyP,
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolutionEnum::FourEightyP,
|
||||
_ => CommonResolutionEnum::SevenTwentyP,
|
||||
}))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Batch counts: 1, 2, 4.
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u16, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 | 2 | 4 => Ok(count),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(4),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(if count < 4 { 2 } else { 4 })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Duration: 4-15 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u8>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(d as u8)),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(d.clamp(MIN, MAX) as u8)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
|
||||
+3
-18
@@ -1,33 +1,18 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p0FastRequestState {
|
||||
/// Final materialized request; ready to fire via the omni-gen video endpoint.
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0FastRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_video_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
self.request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token: response.inference_job_token.clone(),
|
||||
all_inference_job_tokens: vec![response.inference_job_token],
|
||||
}))
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-426
@@ -1,426 +0,0 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::resolve::{
|
||||
resolve_audio_list_ref, resolve_character_list_ref, resolve_image_list_ref,
|
||||
resolve_image_ref, resolve_video_list_ref,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast_g::request::ArtcraftSeedance2p0FastGRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_fast_g(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
|
||||
let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let prompt = builder.prompt.take();
|
||||
|
||||
let start_frame = resolve_image_ref(builder.start_frame.take())?;
|
||||
let end_frame = resolve_image_ref(builder.end_frame.take())?;
|
||||
let reference_images = resolve_image_list_ref(builder.reference_images.take())?;
|
||||
let reference_videos = resolve_video_list_ref(builder.reference_videos.take())?;
|
||||
let reference_audio = resolve_audio_list_ref(builder.reference_audio.take())?;
|
||||
let reference_characters = resolve_character_list_ref(builder.reference_character_tokens.take());
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
model: Some(CommonVideoModelEnum::Seedance2p0FastGlobal),
|
||||
idempotency_token: Some(idempotency_token),
|
||||
prompt,
|
||||
start_frame_image_media_token: start_frame,
|
||||
end_frame_image_media_token: end_frame,
|
||||
reference_image_media_tokens: reference_images,
|
||||
reference_video_media_tokens: reference_videos,
|
||||
reference_audio_media_tokens: reference_audio,
|
||||
reference_character_tokens: reference_characters,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: duration_seconds.map(|d| d as u16),
|
||||
video_batch_count: Some(batch_count),
|
||||
negative_prompt: None,
|
||||
generate_audio: None,
|
||||
quality: None,
|
||||
};
|
||||
|
||||
let state = ArtcraftSeedance2p0FastGRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0FastG(state)))
|
||||
}
|
||||
|
||||
// -- Plan helpers --
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonAspectRatioEnum>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(None),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(CommonAspectRatioEnum::WideSixteenByNine))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(CommonAspectRatioEnum::TallNineBySixteen))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(CommonAspectRatioEnum::Square))
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(CommonAspectRatioEnum::WideFourByThree)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(CommonAspectRatioEnum::TallThreeByFour)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(CommonAspectRatioEnum::WideTwentyOneByNine)),
|
||||
|
||||
// Mismatches
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(nearest_aspect_ratio(unsupported)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnum {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineBySixteen,
|
||||
_ => CommonAspectRatioEnum::Square,
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Fast supports output resolutions: 480p and 720p only.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonResolutionEnum>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonResolution::FourEightyP) => Ok(Some(CommonResolutionEnum::FourEightyP)),
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
|
||||
// 1080p not supported for Fast
|
||||
Some(CommonResolution::TenEightyP) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", CommonResolution::TenEightyP),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
},
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolutionEnum::FourEightyP,
|
||||
_ => CommonResolutionEnum::SevenTwentyP,
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolutionEnum::FourEightyP,
|
||||
_ => CommonResolutionEnum::SevenTwentyP,
|
||||
}))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Batch counts: 1, 2, 4.
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u16, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 | 2 | 4 => Ok(count),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(4),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(if count < 4 { 2 } else { 4 })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Duration: 4-15 seconds.
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u8>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(d as u8)),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(d.clamp(MIN, MAX) as u8)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Field conversions ──
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_none_stays_none() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
|
||||
assert!(req.request.prompt.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_is_seedance_2p0_fast() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0FastGlobal)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aspect ratio ──
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wide() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.aspect_ratio = Some(CommonAspectRatio::WideSixteenByNine);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::WideSixteenByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.aspect_ratio = Some(CommonAspectRatio::TallNineBySixteen);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::TallNineBySixteen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.aspect_ratio = Some(CommonAspectRatio::Square);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::Square));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
|
||||
assert!(req.request.aspect_ratio.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolution ──
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.resolution = Some(CommonResolution::FourEightyP);
|
||||
}));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.resolution = Some(CommonResolution::SevenTwentyP);
|
||||
}));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.resolution = Some(CommonResolution::TenEightyP);
|
||||
}));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_error_out() {
|
||||
let result = build_artcraft_seedance_2p0_fast_g(GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0FastGlobal,
|
||||
provider: Provider::Artcraft,
|
||||
resolution: Some(CommonResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Media tokens ──
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_seedance_2p0_fast_g(GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0FastGlobal,
|
||||
provider: Provider::Artcraft,
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
MediaFileToken::new("mf_a".to_string()),
|
||||
MediaFileToken::new("mf_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
CharacterToken::new("char_a".to_string()),
|
||||
CharacterToken::new("char_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn artcraft_fast_builder_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0FastGlobal,
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0FastGRequestState {
|
||||
let result = build_artcraft_seedance_2p0_fast_g(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0FastG(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p0Fast request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u::request::ArtcraftSeedance2p0UltraRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_u(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0Ultra,
|
||||
SupportedResolutions::Full,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0UltraRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Ultra(state)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Field conversions ──
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_seedance_2p0() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0Ultra)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolution ──
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Media tokens ──
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_seedance_2p0_u(GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = base_builder();
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0UltraRequestState {
|
||||
let result = build_artcraft_seedance_2p0_u(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0Ultra(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p0 request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_g::request::ArtcraftSeedance2p0GRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u::request::ArtcraftSeedance2p0UltraRequestState;
|
||||
|
||||
// ── Pricing constants ──
|
||||
|
||||
@@ -9,14 +9,14 @@ const CENTS_PER_SECOND_480P: f64 = 5.4404;
|
||||
const CENTS_PER_SECOND_720P: f64 = 11.2;
|
||||
const CENTS_PER_SECOND_1080P: f64 = 32.6424;
|
||||
|
||||
pub struct ArtcraftSeedance2p0GCostState {
|
||||
pub struct ArtcraftSeedance2p0UltraCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0GCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p0GRequestState) -> Self {
|
||||
impl ArtcraftSeedance2p0UltraCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p0UltraRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
@@ -165,7 +165,7 @@ mod tests {
|
||||
video_batch_count: u16,
|
||||
) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0Global,
|
||||
model: CommonVideoModel::Seedance2p0Ultra,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+5
-20
@@ -1,33 +1,18 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p0GRequestState {
|
||||
/// Final materialized request; ready to fire via the omni-gen video endpoint.
|
||||
pub struct ArtcraftSeedance2p0UltraRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0GRequestState {
|
||||
impl ArtcraftSeedance2p0UltraRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_video_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
self.request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token: response.inference_job_token.clone(),
|
||||
all_inference_job_tokens: vec![response.inference_job_token],
|
||||
}))
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u_fast::request::ArtcraftSeedance2p0UltraFastRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_seedance_2p0_u_fast(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::Seedance2p0UltraFast,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Supported,
|
||||
)?;
|
||||
let state = ArtcraftSeedance2p0UltraFastRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(state)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Field conversions ──
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_is_passed_through() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_none_stays_none() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
|
||||
assert!(req.request.prompt.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_is_seedance_2p0_fast() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0UltraFast)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aspect ratio ──
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wide() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.aspect_ratio = Some(CommonAspectRatio::WideSixteenByNine);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::WideSixteenByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.aspect_ratio = Some(CommonAspectRatio::TallNineBySixteen);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::TallNineBySixteen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.aspect_ratio = Some(CommonAspectRatio::Square);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(CommonAspectRatioEnum::Square));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
|
||||
assert!(req.request.aspect_ratio.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolution ──
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.resolution = Some(CommonResolution::FourEightyP);
|
||||
}));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.resolution = Some(CommonResolution::SevenTwentyP);
|
||||
}));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.resolution = Some(CommonResolution::TenEightyP);
|
||||
}));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_error_out() {
|
||||
let result = build_artcraft_seedance_2p0_u_fast(GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0UltraFast,
|
||||
provider: Provider::Artcraft,
|
||||
resolution: Some(CommonResolution::TenEightyP),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Media tokens ──
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_seedance_2p0_u_fast(GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0UltraFast,
|
||||
provider: Provider::Artcraft,
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..Default::default()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
MediaFileToken::new("mf_a".to_string()),
|
||||
MediaFileToken::new("mf_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![
|
||||
CharacterToken::new("char_a".to_string()),
|
||||
CharacterToken::new("char_b".to_string()),
|
||||
];
|
||||
let req = unwrap_request(artcraft_fast_builder_with(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn artcraft_fast_builder_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0UltraFast,
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0UltraFastRequestState {
|
||||
let result = build_artcraft_seedance_2p0_u_fast(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftSeedance2p0Fast request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,21 +1,21 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast_g::request::ArtcraftSeedance2p0FastGRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u_fast::request::ArtcraftSeedance2p0UltraFastRequestState;
|
||||
|
||||
// ── Pricing constants ──
|
||||
|
||||
const CENTS_PER_SECOND_480P: f64 = 3.6267;
|
||||
const CENTS_PER_SECOND_720P: f64 = 8.9089;
|
||||
|
||||
pub struct ArtcraftSeedance2p0FastGCostState {
|
||||
pub struct ArtcraftSeedance2p0UltraFastCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0FastGCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p0FastGRequestState) -> Self {
|
||||
impl ArtcraftSeedance2p0UltraFastCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedance2p0UltraFastRequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
@@ -125,7 +125,7 @@ mod tests {
|
||||
let fast = cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1);
|
||||
let standard = {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0Global,
|
||||
model: CommonVideoModel::Seedance2p0Ultra,
|
||||
provider: Provider::Artcraft,
|
||||
resolution: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds: Some(10),
|
||||
@@ -150,7 +150,7 @@ mod tests {
|
||||
video_batch_count: u16,
|
||||
) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0FastGlobal,
|
||||
model: CommonVideoModel::Seedance2p0UltraFast,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+6
-21
@@ -1,33 +1,18 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedance2p0FastGRequestState {
|
||||
/// Final materialized request; ready to fire via the omni-gen video endpoint.
|
||||
pub struct ArtcraftSeedance2p0UltraFastRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedance2p0FastGRequestState {
|
||||
impl ArtcraftSeedance2p0UltraFastRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_video_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
self.request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
|
||||
inference_job_token: response.inference_job_token.clone(),
|
||||
all_inference_job_tokens: vec![response.inference_job_token],
|
||||
}))
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +185,7 @@ mod tests {
|
||||
|
||||
fn artcraft_fast_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0FastGlobal,
|
||||
model: CommonVideoModel::Seedance2p0UltraFast,
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(4),
|
||||
video_batch_count: Some(1),
|
||||
+9
-9
@@ -12,11 +12,11 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0FastGRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0UltraFastRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_gmicloud_seedance_2p0_fast_g(
|
||||
pub fn build_gmicloud_seedance_2p0_u_fast(
|
||||
mut builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
@@ -49,8 +49,8 @@ pub fn build_gmicloud_seedance_2p0_fast_g(
|
||||
reference_asset_ids: None,
|
||||
};
|
||||
|
||||
let state = GmiCloudSeedance2p0FastGRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0FastG(state)))
|
||||
let state = GmiCloudSeedance2p0UltraFastRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(state)))
|
||||
}
|
||||
|
||||
fn plan_ratio(
|
||||
@@ -247,7 +247,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn media_file_token_rejected() {
|
||||
let result = build_gmicloud_seedance_2p0_fast_g(GenerateVideoRequestBuilder {
|
||||
let result = build_gmicloud_seedance_2p0_u_fast(GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_x".to_string()))),
|
||||
..base_builder()
|
||||
});
|
||||
@@ -284,7 +284,7 @@ mod tests {
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0FastGlobal,
|
||||
model: CommonVideoModel::Seedance2p0UltraFast,
|
||||
provider: Provider::GmiCloud,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
@@ -298,10 +298,10 @@ mod tests {
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GmiCloudSeedance2p0FastGRequestState {
|
||||
let result = build_gmicloud_seedance_2p0_fast_g(builder).expect("build should succeed");
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GmiCloudSeedance2p0UltraFastRequestState {
|
||||
let result = build_gmicloud_seedance_2p0_u_fast(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0FastG(state)) => state,
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(state)) => state,
|
||||
_ => panic!("expected GmiCloudSeedance2p0FastG request"),
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,14 +1,14 @@
|
||||
use gmicloud_client::traits::gmicloud_request_cost_calculator_trait::GmiCloudRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0FastGRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0UltraFastRequestState;
|
||||
|
||||
pub struct GmiCloudSeedance2p0FastGCostState {
|
||||
request: GmiCloudSeedance2p0FastGRequestState,
|
||||
pub struct GmiCloudSeedance2p0UltraFastCostState {
|
||||
request: GmiCloudSeedance2p0UltraFastRequestState,
|
||||
}
|
||||
|
||||
impl GmiCloudSeedance2p0FastGCostState {
|
||||
pub fn from_request(request: &GmiCloudSeedance2p0FastGRequestState) -> Self {
|
||||
impl GmiCloudSeedance2p0UltraFastCostState {
|
||||
pub fn from_request(request: &GmiCloudSeedance2p0UltraFastRequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ mod tests {
|
||||
let fast = cost_cents(Some(CommonResolution::SevenTwentyP), 10);
|
||||
let standard = {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0Global,
|
||||
model: CommonVideoModel::Seedance2p0Ultra,
|
||||
provider: Provider::GmiCloud,
|
||||
resolution: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds: Some(10),
|
||||
@@ -108,7 +108,7 @@ mod tests {
|
||||
|
||||
fn cost_cents(resolution: Option<CommonResolution>, duration_seconds: u16) -> u64 {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0FastGlobal,
|
||||
model: CommonVideoModel::Seedance2p0UltraFast,
|
||||
provider: Provider::GmiCloud,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
+3
-3
@@ -8,11 +8,11 @@ use crate::generate::generate_video::generate_video_response::{
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GmiCloudSeedance2p0FastGRequestState {
|
||||
pub struct GmiCloudSeedance2p0UltraFastRequestState {
|
||||
pub request: Seedance20FastRequest,
|
||||
}
|
||||
|
||||
impl GmiCloudSeedance2p0FastGRequestState {
|
||||
impl GmiCloudSeedance2p0UltraFastRequestState {
|
||||
pub async fn send(&self, client: &RouterGmiCloudClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let response = self.request.send_request(&client.api_key)
|
||||
.await
|
||||
@@ -69,7 +69,7 @@ mod tests {
|
||||
|
||||
fn gmicloud_fast_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0FastGlobal,
|
||||
model: CommonVideoModel::Seedance2p0UltraFast,
|
||||
provider: Provider::GmiCloud,
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
|
||||
+10
-10
@@ -12,11 +12,11 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0GRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0UltraRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_gmicloud_seedance_2p0_g(
|
||||
pub fn build_gmicloud_seedance_2p0_u(
|
||||
mut builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
@@ -49,8 +49,8 @@ pub fn build_gmicloud_seedance_2p0_g(
|
||||
reference_asset_ids: None,
|
||||
};
|
||||
|
||||
let state = GmiCloudSeedance2p0GRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0G(state)))
|
||||
let state = GmiCloudSeedance2p0UltraRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0Ultra(state)))
|
||||
}
|
||||
|
||||
fn plan_ratio(
|
||||
@@ -270,7 +270,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn media_file_token_rejected_for_start_frame() {
|
||||
let result = build_gmicloud_seedance_2p0_g(GenerateVideoRequestBuilder {
|
||||
let result = build_gmicloud_seedance_2p0_u(GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test".to_string()))),
|
||||
..base_builder()
|
||||
});
|
||||
@@ -288,7 +288,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_rejected() {
|
||||
let result = build_gmicloud_seedance_2p0_g(GenerateVideoRequestBuilder {
|
||||
let result = build_gmicloud_seedance_2p0_u(GenerateVideoRequestBuilder {
|
||||
reference_images: Some(ImageListRef::MediaFileTokens(vec![MediaFileToken::new("mf_a".to_string())])),
|
||||
..base_builder()
|
||||
});
|
||||
@@ -316,7 +316,7 @@ mod tests {
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0Global,
|
||||
model: CommonVideoModel::Seedance2p0Ultra,
|
||||
provider: Provider::GmiCloud,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
@@ -330,10 +330,10 @@ mod tests {
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GmiCloudSeedance2p0GRequestState {
|
||||
let result = build_gmicloud_seedance_2p0_g(builder).expect("build should succeed");
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GmiCloudSeedance2p0UltraRequestState {
|
||||
let result = build_gmicloud_seedance_2p0_u(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0G(state)) => state,
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0Ultra(state)) => state,
|
||||
_ => panic!("expected GmiCloudSeedance2p0G request"),
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
use gmicloud_client::traits::gmicloud_request_cost_calculator_trait::GmiCloudRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0GRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0UltraRequestState;
|
||||
|
||||
pub struct GmiCloudSeedance2p0GCostState {
|
||||
request: GmiCloudSeedance2p0GRequestState,
|
||||
pub struct GmiCloudSeedance2p0UltraCostState {
|
||||
request: GmiCloudSeedance2p0UltraRequestState,
|
||||
}
|
||||
|
||||
impl GmiCloudSeedance2p0GCostState {
|
||||
pub fn from_request(request: &GmiCloudSeedance2p0GRequestState) -> Self {
|
||||
impl GmiCloudSeedance2p0UltraCostState {
|
||||
pub fn from_request(request: &GmiCloudSeedance2p0UltraRequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ mod tests {
|
||||
|
||||
fn cost_cents(resolution: Option<CommonResolution>, duration_seconds: u16) -> u64 {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0Global,
|
||||
model: CommonVideoModel::Seedance2p0Ultra,
|
||||
provider: Provider::GmiCloud,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
|
||||
+3
-3
@@ -8,11 +8,11 @@ use crate::generate::generate_video::generate_video_response::{
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GmiCloudSeedance2p0GRequestState {
|
||||
pub struct GmiCloudSeedance2p0UltraRequestState {
|
||||
pub request: Seedance20Request,
|
||||
}
|
||||
|
||||
impl GmiCloudSeedance2p0GRequestState {
|
||||
impl GmiCloudSeedance2p0UltraRequestState {
|
||||
pub async fn send(&self, client: &RouterGmiCloudClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let response = self.request.send_request(&client.api_key)
|
||||
.await
|
||||
@@ -67,7 +67,7 @@ mod tests {
|
||||
|
||||
fn gmicloud_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0Global,
|
||||
model: CommonVideoModel::Seedance2p0Ultra,
|
||||
provider: Provider::GmiCloud,
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
|
||||
+60
-24
@@ -5,18 +5,26 @@ use crate::generate::generate_video::generate_video_response::GenerateVideoRespo
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::cost::ArtcraftHappyHorse1p0CostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::request::ArtcraftHappyHorse1p0RequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model::cost::ArtcraftPreviewModelCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model::request::ArtcraftPreviewModelRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::cost::ArtcraftPreviewModelFastCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::request::ArtcraftPreviewModelFastRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::cost::ArtcraftSeedance2p0CostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::cost::ArtcraftSeedance2p0FastCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::request::ArtcraftSeedance2p0FastRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_g::cost::ArtcraftSeedance2p0GCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_g::request::ArtcraftSeedance2p0GRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast_g::cost::ArtcraftSeedance2p0FastGCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast_g::request::ArtcraftSeedance2p0FastGRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::cost::GmiCloudSeedance2p0GCostState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0GRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::cost::GmiCloudSeedance2p0FastGCostState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0FastGRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u::cost::ArtcraftSeedance2p0UltraCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u::request::ArtcraftSeedance2p0UltraRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u_fast::cost::ArtcraftSeedance2p0UltraFastCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u_fast::request::ArtcraftSeedance2p0UltraFastRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::cost::ArtcraftSeedance2p0BytePlusCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::cost::ArtcraftSeedance2p0BytePlusFastCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::request::ArtcraftSeedance2p0BytePlusFastRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::cost::GmiCloudSeedance2p0UltraCostState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0UltraRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::cost::GmiCloudSeedance2p0UltraFastCostState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0UltraFastRequestState;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::cost::KinoviHappyHorse1p0CostState;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::request::KinoviHappyHorse1p0RequestState;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::cost::KinoviSeedance2p0CostState;
|
||||
@@ -27,12 +35,16 @@ use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::re
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum VideoGenerationRequest {
|
||||
ArtcraftHappyHorse1p0(ArtcraftHappyHorse1p0RequestState),
|
||||
ArtcraftPreviewModel(ArtcraftPreviewModelRequestState),
|
||||
ArtcraftPreviewModelFast(ArtcraftPreviewModelFastRequestState),
|
||||
ArtcraftSeedance2p0(ArtcraftSeedance2p0RequestState),
|
||||
ArtcraftSeedance2p0Fast(ArtcraftSeedance2p0FastRequestState),
|
||||
ArtcraftSeedance2p0G(ArtcraftSeedance2p0GRequestState),
|
||||
ArtcraftSeedance2p0FastG(ArtcraftSeedance2p0FastGRequestState),
|
||||
GmiCloudSeedance2p0G(GmiCloudSeedance2p0GRequestState),
|
||||
GmiCloudSeedance2p0FastG(GmiCloudSeedance2p0FastGRequestState),
|
||||
ArtcraftSeedance2p0Ultra(ArtcraftSeedance2p0UltraRequestState),
|
||||
ArtcraftSeedance2p0UltraFast(ArtcraftSeedance2p0UltraFastRequestState),
|
||||
ArtcraftSeedance2p0BytePlus(ArtcraftSeedance2p0BytePlusRequestState),
|
||||
ArtcraftSeedance2p0BytePlusFast(ArtcraftSeedance2p0BytePlusFastRequestState),
|
||||
GmiCloudSeedance2p0Ultra(GmiCloudSeedance2p0UltraRequestState),
|
||||
GmiCloudSeedance2p0UltraFast(GmiCloudSeedance2p0UltraFastRequestState),
|
||||
KinoviHappyHorse1p0(KinoviHappyHorse1p0RequestState),
|
||||
KinoviSeedance2p0(KinoviSeedance2p0RequestState),
|
||||
KinoviSeedance2p0Fast(KinoviSeedance2p0FastRequestState),
|
||||
@@ -43,12 +55,16 @@ impl VideoGenerationRequest {
|
||||
pub fn get_provider(&self) -> Provider {
|
||||
match self {
|
||||
Self::ArtcraftHappyHorse1p0(_) => Provider::Artcraft,
|
||||
Self::ArtcraftPreviewModel(_) => Provider::Artcraft,
|
||||
Self::ArtcraftPreviewModelFast(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0Fast(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0G(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0FastG(_) => Provider::Artcraft,
|
||||
Self::GmiCloudSeedance2p0G(_) => Provider::GmiCloud,
|
||||
Self::GmiCloudSeedance2p0FastG(_) => Provider::GmiCloud,
|
||||
Self::ArtcraftSeedance2p0Ultra(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0UltraFast(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0BytePlus(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedance2p0BytePlusFast(_) => Provider::Artcraft,
|
||||
Self::GmiCloudSeedance2p0Ultra(_) => Provider::GmiCloud,
|
||||
Self::GmiCloudSeedance2p0UltraFast(_) => Provider::GmiCloud,
|
||||
Self::KinoviHappyHorse1p0(_) => Provider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0(_) => Provider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0Fast(_) => Provider::Seedance2Pro,
|
||||
@@ -59,12 +75,16 @@ impl VideoGenerationRequest {
|
||||
pub fn estimate_cost(&self) -> Result<VideoGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
VideoGenerationRequest::ArtcraftHappyHorse1p0(request) => Ok(ArtcraftHappyHorse1p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftPreviewModel(request) => Ok(ArtcraftPreviewModelCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftPreviewModelFast(request) => Ok(ArtcraftPreviewModelFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0(request) => Ok(ArtcraftSeedance2p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0Fast(request) => Ok(ArtcraftSeedance2p0FastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0G(request) => Ok(ArtcraftSeedance2p0GCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0FastG(request) => Ok(ArtcraftSeedance2p0FastGCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0G(request) => Ok(GmiCloudSeedance2p0GCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0FastG(request) => Ok(GmiCloudSeedance2p0FastGCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0Ultra(request) => Ok(ArtcraftSeedance2p0UltraCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(request) => Ok(ArtcraftSeedance2p0UltraFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(request) => Ok(ArtcraftSeedance2p0BytePlusCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(request) => Ok(ArtcraftSeedance2p0BytePlusFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => Ok(GmiCloudSeedance2p0UltraCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(request) => Ok(GmiCloudSeedance2p0UltraFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviHappyHorse1p0(request) => Ok(KinoviHappyHorse1p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviSeedance2p0(request) => Ok(KinoviSeedance2p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviSeedance2p0Fast(request) => Ok(KinoviSeedance2p0FastCostState::from_request(request).estimate_cost()),
|
||||
@@ -79,6 +99,14 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftPreviewModel(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftPreviewModelFast(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
@@ -87,19 +115,27 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0G(request) => {
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0Ultra(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0FastG(request) => {
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0UltraFast(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0G(request) => {
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlus(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => {
|
||||
let client_ref = client.get_gmicloud_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0FastG(request) => {
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(request) => {
|
||||
let client_ref = client.get_gmicloud_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
|
||||
@@ -9,6 +9,9 @@ pub enum GmiCloudSpecificApiError {
|
||||
/// The request was rejected due to content policy.
|
||||
ContentPolicyViolation(String),
|
||||
|
||||
/// The input image was rejected because it may contain a real person.
|
||||
ContentContainsRealPerson(String),
|
||||
|
||||
/// The account has insufficient credits or a billing issue.
|
||||
BillingError { status_code: u16, body: String },
|
||||
}
|
||||
|
||||
+15
-2
@@ -2,8 +2,10 @@ use crate::creds::gmicloud_api_key::GmiCloudApiKey;
|
||||
use crate::error::gmicloud_error::GmiCloudError;
|
||||
use crate::requests::api::video::seedance_2_0_260128::raw_request::Seedance20Payload;
|
||||
use crate::requests::common::create_request::{
|
||||
create_gmicloud_request, GmiCloudCreateRequest, GmiCloudCreateResponse,
|
||||
create_gmicloud_request, create_gmicloud_request_with_context,
|
||||
GmiCloudCreateRequest, GmiCloudCreateResponse,
|
||||
};
|
||||
use crate::requests::context::request_context::RequestContext;
|
||||
|
||||
const MODEL_ID: &str = "seedance-2-0-260128";
|
||||
|
||||
@@ -113,12 +115,23 @@ impl Seedance20Request {
|
||||
pub async fn send_request(
|
||||
&self,
|
||||
api_key: &GmiCloudApiKey,
|
||||
) -> Result<GmiCloudCreateResponse, GmiCloudError> {
|
||||
let context = RequestContext {
|
||||
api_key,
|
||||
maybe_timeout: None,
|
||||
};
|
||||
self.send_request_with_context(&context).await
|
||||
}
|
||||
|
||||
pub async fn send_request_with_context(
|
||||
&self,
|
||||
context: &RequestContext<'_>,
|
||||
) -> Result<GmiCloudCreateResponse, GmiCloudError> {
|
||||
let body = GmiCloudCreateRequest {
|
||||
model: MODEL_ID.to_string(),
|
||||
payload: self.to_raw_payload(),
|
||||
};
|
||||
create_gmicloud_request(api_key, &body).await
|
||||
create_gmicloud_request_with_context(context, &body).await
|
||||
}
|
||||
|
||||
pub fn model_id() -> &'static str {
|
||||
|
||||
+15
-2
@@ -2,8 +2,10 @@ use crate::creds::gmicloud_api_key::GmiCloudApiKey;
|
||||
use crate::error::gmicloud_error::GmiCloudError;
|
||||
use crate::requests::api::video::seedance_2_0_fast_260128::raw_request::Seedance20FastPayload;
|
||||
use crate::requests::common::create_request::{
|
||||
create_gmicloud_request, GmiCloudCreateRequest, GmiCloudCreateResponse,
|
||||
create_gmicloud_request, create_gmicloud_request_with_context,
|
||||
GmiCloudCreateRequest, GmiCloudCreateResponse,
|
||||
};
|
||||
use crate::requests::context::request_context::RequestContext;
|
||||
|
||||
const MODEL_ID: &str = "seedance-2-0-fast-260128";
|
||||
|
||||
@@ -111,12 +113,23 @@ impl Seedance20FastRequest {
|
||||
pub async fn send_request(
|
||||
&self,
|
||||
api_key: &GmiCloudApiKey,
|
||||
) -> Result<GmiCloudCreateResponse, GmiCloudError> {
|
||||
let context = RequestContext {
|
||||
api_key,
|
||||
maybe_timeout: None,
|
||||
};
|
||||
self.send_request_with_context(&context).await
|
||||
}
|
||||
|
||||
pub async fn send_request_with_context(
|
||||
&self,
|
||||
context: &RequestContext<'_>,
|
||||
) -> Result<GmiCloudCreateResponse, GmiCloudError> {
|
||||
let body = GmiCloudCreateRequest {
|
||||
model: MODEL_ID.to_string(),
|
||||
payload: self.to_raw_payload(),
|
||||
};
|
||||
create_gmicloud_request(api_key, &body).await
|
||||
create_gmicloud_request_with_context(context, &body).await
|
||||
}
|
||||
|
||||
pub fn model_id() -> &'static str {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -5,9 +7,14 @@ use crate::creds::gmicloud_api_key::GmiCloudApiKey;
|
||||
use crate::error::gmicloud_error::GmiCloudError;
|
||||
use crate::error::gmicloud_generic_api_error::GmiCloudGenericApiError;
|
||||
use crate::error::gmicloud_specific_api_error::GmiCloudSpecificApiError;
|
||||
use crate::requests::context::request_context::RequestContext;
|
||||
|
||||
const BASE_URL: &str = "https://console.gmicloud.ai/api/v1/ie/requestqueue/apikey";
|
||||
|
||||
/// Default request timeout. Video requests return quickly (async queue),
|
||||
/// but image requests block until completion and can take 60+ seconds.
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(180);
|
||||
|
||||
/// The top-level request body sent to `POST /requests`.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GmiCloudCreateRequest<P: Serialize> {
|
||||
@@ -23,17 +30,34 @@ pub struct GmiCloudCreateResponse {
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// Submit a video generation request to GmiCloud.
|
||||
/// Submit a request to GmiCloud using just an API key (default timeout).
|
||||
pub async fn create_gmicloud_request<P: Serialize>(
|
||||
api_key: &GmiCloudApiKey,
|
||||
body: &GmiCloudCreateRequest<P>,
|
||||
) -> Result<GmiCloudCreateResponse, GmiCloudError> {
|
||||
let context = RequestContext {
|
||||
api_key,
|
||||
maybe_timeout: None,
|
||||
};
|
||||
create_gmicloud_request_with_context(&context, body).await
|
||||
}
|
||||
|
||||
/// Submit a request to GmiCloud with explicit context (custom timeout, etc.).
|
||||
pub async fn create_gmicloud_request_with_context<P: Serialize>(
|
||||
context: &RequestContext<'_>,
|
||||
body: &GmiCloudCreateRequest<P>,
|
||||
) -> Result<GmiCloudCreateResponse, GmiCloudError> {
|
||||
let url = format!("{}/requests", BASE_URL);
|
||||
let timeout = context.maybe_timeout.unwrap_or(DEFAULT_TIMEOUT);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.map_err(GmiCloudGenericApiError::from)?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", api_key.as_str()))
|
||||
.header("Authorization", format!("Bearer {}", context.api_key.as_str()))
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
@@ -49,6 +73,12 @@ pub async fn create_gmicloud_request<P: Serialize>(
|
||||
|
||||
if !status.is_success() {
|
||||
warn!("GmiCloud API error: status={}, body={}", status, body_text);
|
||||
|
||||
// Detect specific error codes embedded in the response body.
|
||||
if let Some(specific) = classify_error_body(&body_text) {
|
||||
return Err(specific.into());
|
||||
}
|
||||
|
||||
return Err(GmiCloudGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status.as_u16(),
|
||||
body: body_text,
|
||||
@@ -60,3 +90,28 @@ pub async fn create_gmicloud_request<P: Serialize>(
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
/// Attempt to classify an error response body into a specific error type.
|
||||
/// GmiCloud sometimes wraps inner API errors as a JSON string within `{"error": "..."}`.
|
||||
fn classify_error_body(body: &str) -> Option<GmiCloudSpecificApiError> {
|
||||
if body.contains("InputImageSensitiveContentDetected") {
|
||||
let message = extract_inner_message(body)
|
||||
.unwrap_or_else(|| "Input image may contain a real person".to_string());
|
||||
return Some(GmiCloudSpecificApiError::ContentContainsRealPerson(message));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract the inner `message` field from a doubly-encoded GmiCloud error response.
|
||||
/// Expected shape: `{"error":"API request failed with status 400: {\"error\":{\"message\":\"...\"}}"}`
|
||||
fn extract_inner_message(body: &str) -> Option<String> {
|
||||
let outer: serde_json::Value = serde_json::from_str(body).ok()?;
|
||||
let error_str = outer.get("error")?.as_str()?;
|
||||
|
||||
// The inner JSON is embedded after "status NNN: "
|
||||
let inner_json_start = error_str.find('{')?;
|
||||
let inner_str = &error_str[inner_json_start..];
|
||||
let inner: serde_json::Value = serde_json::from_str(inner_str).ok()?;
|
||||
|
||||
inner.get("error")?.get("message")?.as_str().map(|s| s.to_string())
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod request_context;
|
||||
@@ -0,0 +1,8 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::creds::gmicloud_api_key::GmiCloudApiKey;
|
||||
|
||||
pub struct RequestContext<'a> {
|
||||
pub api_key: &'a GmiCloudApiKey,
|
||||
pub maybe_timeout: Option<Duration>,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod api;
|
||||
pub mod common;
|
||||
pub mod context;
|
||||
pub mod poll_request_queue;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use clap::Subcommand;
|
||||
use clap::{Args, Subcommand};
|
||||
use log::{info, warn};
|
||||
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
use super::state::ArtcraftState;
|
||||
use super::subcommands;
|
||||
@@ -10,6 +15,24 @@ use super::subcommands;
|
||||
/// Used by the underscore-insensitive arg normalizer.
|
||||
pub const SUBCOMMAND_NAMES: &[&str] = &["generate_video"];
|
||||
|
||||
const DEFAULT_COOKIE_FILE: &str = "artcraft_cookies.txt";
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ArtcraftArgs {
|
||||
/// Path to a file containing the session cookies.
|
||||
/// If not specified, reads from `artcraft_cookies.txt` in the current directory.
|
||||
#[arg(long, global = true)]
|
||||
pub cookie_file: Option<String>,
|
||||
|
||||
/// Target environment: "dev" / "development" (localhost:12345) or "prod" / "production" (api.storyteller.ai).
|
||||
/// [default: dev]
|
||||
#[arg(long, global = true, default_value = "dev")]
|
||||
pub environment: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: ArtcraftCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
#[command(rename_all = "snake_case")]
|
||||
pub enum ArtcraftCommand {
|
||||
@@ -17,17 +40,52 @@ pub enum ArtcraftCommand {
|
||||
GenerateVideo(subcommands::generate_video::GenerateVideoArgs),
|
||||
}
|
||||
|
||||
pub async fn run(command: ArtcraftCommand) -> anyhow::Result<()> {
|
||||
let cookies_str = easyenv::get_env_string_required("ARTCRAFT_COOKIES")
|
||||
.map_err(|err| anyhow!("Missing ARTCRAFT_COOKIES env var: {:?}", err))?;
|
||||
pub async fn dispatch(args: ArtcraftArgs) -> anyhow::Result<()> {
|
||||
let cookie_path = args.cookie_file.as_deref().unwrap_or(DEFAULT_COOKIE_FILE);
|
||||
let cookies_str = load_cookies(cookie_path)?;
|
||||
|
||||
let creds = StorytellerCredentialSet::parse_multi_cookie_header(&cookies_str)
|
||||
.map_err(|err| anyhow!("Failed to parse ARTCRAFT_COOKIES: {:?}", err))?
|
||||
.ok_or_else(|| anyhow!("ARTCRAFT_COOKIES contained no recognized cookies (expected session=... and/or visitor=...)"))?;
|
||||
.map_err(|err| anyhow!("Failed to parse cookies: {:?}", err))?
|
||||
.ok_or_else(|| anyhow!("Cookie file contained no recognized cookies (expected session=... and/or visitor=...)"))?;
|
||||
|
||||
let state = ArtcraftState { creds };
|
||||
let api_host = parse_api_host(&args.environment)?;
|
||||
let state = ArtcraftState { creds, api_host };
|
||||
|
||||
match command {
|
||||
ArtcraftCommand::GenerateVideo(args) => subcommands::generate_video::run(&state, args).await,
|
||||
match args.command {
|
||||
ArtcraftCommand::GenerateVideo(cmd_args) => subcommands::generate_video::run(&state, cmd_args).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_cookies(cookie_path: &str) -> anyhow::Result<String> {
|
||||
info!("Reading cookies from: {}", cookie_path);
|
||||
|
||||
if Path::new(cookie_path).exists() {
|
||||
let content = fs::read_to_string(cookie_path)
|
||||
.map_err(|err| anyhow!("Failed to read cookie file {:?}: {}", cookie_path, err))?;
|
||||
Ok(content.trim().to_string())
|
||||
} else {
|
||||
warn!("Cookie file {:?} does not exist.", cookie_path);
|
||||
Err(anyhow!(
|
||||
"Cookie file {:?} not found. Use --cookie-file to specify a path, \
|
||||
or create '{}' in the current directory.",
|
||||
cookie_path, DEFAULT_COOKIE_FILE
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_api_host(environment: &str) -> anyhow::Result<ApiHost> {
|
||||
match environment {
|
||||
"dev" | "development" => {
|
||||
info!("Environment: development (localhost:12345)");
|
||||
Ok(ApiHost::Localhost { port: 12345 })
|
||||
}
|
||||
"prod" | "production" => {
|
||||
info!("Environment: production (api.storyteller.ai)");
|
||||
Ok(ApiHost::Storyteller)
|
||||
}
|
||||
other => Err(anyhow!(
|
||||
"Unknown environment '{}'. Use 'dev', 'development', 'prod', or 'production'.",
|
||||
other
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@ pub mod dispatch;
|
||||
mod state;
|
||||
mod subcommands;
|
||||
|
||||
pub use dispatch::ArtcraftArgs;
|
||||
pub use dispatch::ArtcraftCommand;
|
||||
pub use dispatch::run;
|
||||
pub use dispatch::dispatch;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
|
||||
pub struct ArtcraftState {
|
||||
pub creds: StorytellerCredentialSet,
|
||||
pub api_host: ApiHost,
|
||||
}
|
||||
|
||||
+2
-5
@@ -7,7 +7,6 @@ use log::info;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::utils::parse_video_model::parse_video_model;
|
||||
@@ -51,9 +50,7 @@ pub struct GenerateVideoArgs {
|
||||
#[arg(long)]
|
||||
pub audio_reference_tokens: Option<String>,
|
||||
|
||||
/// Use localhost:12345 instead of production API.
|
||||
#[arg(long)]
|
||||
pub localhost: bool,
|
||||
|
||||
/// Video model to use. Accepts canonical names (eg. "seedance_2p0") or aliases
|
||||
/// (eg. "seedance2", "happyhorse", "veo3fast"). [default: seedance2p0]
|
||||
@@ -77,7 +74,7 @@ pub async fn run(state: &ArtcraftState, args: GenerateVideoArgs) -> anyhow::Resu
|
||||
.ok_or_else(|| anyhow!("Unknown model '{}'. Try --help for supported names.", model_str))?;
|
||||
|
||||
let prompt = resolve_prompt(&args.prompt)?;
|
||||
let api_host = if args.localhost { ApiHost::Localhost { port: 12345 } } else { ApiHost::Storyteller };
|
||||
let api_host = &state.api_host;
|
||||
let duration = args.duration.unwrap_or(5);
|
||||
|
||||
info!("Model: {:?}", model);
|
||||
@@ -131,7 +128,7 @@ pub async fn run(state: &ArtcraftState, args: GenerateVideoArgs) -> anyhow::Resu
|
||||
info!("Sending request to omni endpoint (idempotency_token={})...", idempotency_token);
|
||||
|
||||
let response = omni_gen_video_generate(
|
||||
&api_host,
|
||||
api_host,
|
||||
Some(&state.creds),
|
||||
request,
|
||||
).await.map_err(|err| anyhow!("Omni video generation failed: {}", err))?;
|
||||
|
||||
@@ -29,10 +29,7 @@ pub enum TopLevelCommand {
|
||||
},
|
||||
|
||||
/// ArtCraft support commands (omni API)
|
||||
Artcraft {
|
||||
#[command(subcommand)]
|
||||
command: artcraft::ArtcraftCommand,
|
||||
},
|
||||
Artcraft(artcraft::dispatch::ArtcraftArgs),
|
||||
}
|
||||
|
||||
pub async fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
@@ -40,8 +37,8 @@ pub async fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
TopLevelCommand::Seedance2pro { command } => {
|
||||
seedance2pro::run(command).await
|
||||
}
|
||||
TopLevelCommand::Artcraft { command } => {
|
||||
artcraft::run(command).await
|
||||
TopLevelCommand::Artcraft(args) => {
|
||||
artcraft::dispatch(args).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-6
@@ -22,6 +22,9 @@ use crate::payloads::generic_inference_args::generic_inference_args::GenericInfe
|
||||
pub struct InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c, E>
|
||||
where E: 'e + Executor<'c, Database = MySql>
|
||||
{
|
||||
/// Which kinovi queue to use: standard or alternate
|
||||
pub use_alternate_kinovi: bool,
|
||||
|
||||
pub uuid_idempotency_token: &'e str,
|
||||
|
||||
// NOTE: We'll generate this ahead of time so we can save it with billing info!
|
||||
@@ -56,12 +59,23 @@ pub async fn insert_generic_inference_job_for_seedance2pro_queue_with_apriori_jo
|
||||
let serialized_args_payload = serde_json::ser::to_string(&args.maybe_inference_args)
|
||||
.map_err(|_e| anyhow!("could not encode inference args"))?;
|
||||
|
||||
const JOB_TYPE: InferenceJobType = InferenceJobType::Seedance2ProQueue;
|
||||
const INFERENCE_CATEGORY: InferenceCategory = InferenceCategory::VideoGeneration;
|
||||
const PRODUCT_CATEGORY: InferenceJobProductCategory = InferenceJobProductCategory::Seedance2ProVideo;
|
||||
const EXTERNAL_THIRD_PARTY: InferenceJobExternalThirdParty = InferenceJobExternalThirdParty::Seedance2Pro;
|
||||
const STATUS: JobStatusPlus = JobStatusPlus::Pending;
|
||||
|
||||
let job_type;
|
||||
let external_third_party;
|
||||
let product_category;
|
||||
|
||||
if args.use_alternate_kinovi {
|
||||
job_type = InferenceJobType::Seedance2ProAltQueue;
|
||||
external_third_party = InferenceJobExternalThirdParty::Seedance2ProAlt;
|
||||
product_category = InferenceJobProductCategory::Seedance2ProVideoAlt;
|
||||
} else {
|
||||
job_type = InferenceJobType::Seedance2ProQueue;
|
||||
external_third_party = InferenceJobExternalThirdParty::Seedance2Pro;
|
||||
product_category = InferenceJobProductCategory::Seedance2ProVideo;
|
||||
}
|
||||
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO generic_inference_jobs
|
||||
@@ -113,12 +127,12 @@ SET
|
||||
args.apriori_job_token.as_str(),
|
||||
args.uuid_idempotency_token,
|
||||
|
||||
JOB_TYPE.to_str(),
|
||||
job_type.to_str(),
|
||||
|
||||
EXTERNAL_THIRD_PARTY.to_str(),
|
||||
external_third_party.to_str(),
|
||||
args.maybe_external_third_party_id,
|
||||
|
||||
PRODUCT_CATEGORY.to_str(),
|
||||
product_category.to_str(),
|
||||
INFERENCE_CATEGORY.to_str(),
|
||||
|
||||
args.maybe_prompt_token.map(|t| t.to_string()),
|
||||
|
||||
+7
-3
@@ -41,7 +41,11 @@ struct RawRecord {
|
||||
}
|
||||
|
||||
/// Returns all non-terminal Seedance2Pro video jobs that have an associated order_id.
|
||||
pub async fn list_pending_seedance2pro_video_jobs(pool: &MySqlPool) -> Result<Vec<PendingSeedance2ProJob>, sqlx::Error> {
|
||||
pub async fn list_pending_seedance2pro_video_jobs(
|
||||
pool: &MySqlPool,
|
||||
external_third_party: InferenceJobExternalThirdParty,
|
||||
job_type: InferenceJobType,
|
||||
) -> Result<Vec<PendingSeedance2ProJob>, sqlx::Error> {
|
||||
let records = sqlx::query_as!(
|
||||
RawRecord,
|
||||
r#"
|
||||
@@ -64,8 +68,8 @@ WHERE jobs.maybe_external_third_party = ?
|
||||
|
||||
LIMIT 25000
|
||||
"#,
|
||||
InferenceJobExternalThirdParty::Seedance2Pro.to_str(),
|
||||
InferenceJobType::Seedance2ProQueue.to_str(),
|
||||
external_third_party.to_str(),
|
||||
job_type.to_str(),
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
+11
-1
@@ -29,6 +29,10 @@ pub enum InferenceJobExternalThirdParty {
|
||||
#[serde(rename = "seedance2pro")]
|
||||
Seedance2Pro,
|
||||
|
||||
/// Seedance 2 Pro Alt jobs
|
||||
#[serde(rename = "seedance2pro_alt")]
|
||||
Seedance2ProAlt,
|
||||
|
||||
/// World Labs jobs
|
||||
#[serde(rename = "worldlabs")]
|
||||
Worldlabs,
|
||||
@@ -46,6 +50,7 @@ impl InferenceJobExternalThirdParty {
|
||||
Self::Fal => "fal",
|
||||
Self::GmiCloud => "gmicloud",
|
||||
Self::Seedance2Pro => "seedance2pro",
|
||||
Self::Seedance2ProAlt => "seedance2pro_alt",
|
||||
Self::Worldlabs => "worldlabs",
|
||||
}
|
||||
}
|
||||
@@ -56,6 +61,7 @@ impl InferenceJobExternalThirdParty {
|
||||
"fal" => Ok(Self::Fal),
|
||||
"gmicloud" => Ok(Self::GmiCloud),
|
||||
"seedance2pro" => Ok(Self::Seedance2Pro),
|
||||
"seedance2pro_alt" => Ok(Self::Seedance2ProAlt),
|
||||
"worldlabs" => Ok(Self::Worldlabs),
|
||||
_ => Err(format!("invalid value: {:?}", value)),
|
||||
}
|
||||
@@ -69,6 +75,7 @@ impl InferenceJobExternalThirdParty {
|
||||
Self::Fal,
|
||||
Self::GmiCloud,
|
||||
Self::Seedance2Pro,
|
||||
Self::Seedance2ProAlt,
|
||||
Self::Worldlabs,
|
||||
])
|
||||
}
|
||||
@@ -88,6 +95,7 @@ mod tests {
|
||||
assert_serialization(InferenceJobExternalThirdParty::Fal, "fal");
|
||||
assert_serialization(InferenceJobExternalThirdParty::GmiCloud, "gmicloud");
|
||||
assert_serialization(InferenceJobExternalThirdParty::Seedance2Pro, "seedance2pro");
|
||||
assert_serialization(InferenceJobExternalThirdParty::Seedance2ProAlt, "seedance2pro_alt");
|
||||
assert_serialization(InferenceJobExternalThirdParty::Worldlabs, "worldlabs");
|
||||
}
|
||||
|
||||
@@ -97,6 +105,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobExternalThirdParty::Fal.to_str(), "fal");
|
||||
assert_eq!(InferenceJobExternalThirdParty::GmiCloud.to_str(), "gmicloud");
|
||||
assert_eq!(InferenceJobExternalThirdParty::Seedance2Pro.to_str(), "seedance2pro");
|
||||
assert_eq!(InferenceJobExternalThirdParty::Seedance2ProAlt.to_str(), "seedance2pro_alt");
|
||||
assert_eq!(InferenceJobExternalThirdParty::Worldlabs.to_str(), "worldlabs");
|
||||
}
|
||||
|
||||
@@ -106,13 +115,14 @@ mod tests {
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("fal").unwrap(), InferenceJobExternalThirdParty::Fal);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("gmicloud").unwrap(), InferenceJobExternalThirdParty::GmiCloud);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro").unwrap(), InferenceJobExternalThirdParty::Seedance2Pro);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro_alt").unwrap(), InferenceJobExternalThirdParty::Seedance2ProAlt);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("worldlabs").unwrap(), InferenceJobExternalThirdParty::Worldlabs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 5;
|
||||
const EXPECTED_COUNT : usize = 6;
|
||||
|
||||
assert_eq!(InferenceJobExternalThirdParty::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobExternalThirdParty::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
+12
-1
@@ -48,6 +48,11 @@ pub enum InferenceJobProductCategory {
|
||||
#[serde(rename = "seedance2pro_character")]
|
||||
Seedance2ProCharacter,
|
||||
|
||||
// =============== SEEDANCE 2 PRO ALT ===============
|
||||
|
||||
#[serde(rename = "seedance2pro_video_alt")]
|
||||
Seedance2ProVideoAlt,
|
||||
|
||||
// =============== WORLD LABS ===============
|
||||
|
||||
/// World Labs: Gaussian Splat Generation
|
||||
@@ -153,6 +158,7 @@ impl InferenceJobProductCategory {
|
||||
Self::BeebleVideo => "beeble_video",
|
||||
Self::Seedance2ProVideo => "seedance2pro_video",
|
||||
Self::Seedance2ProCharacter => "seedance2pro_character",
|
||||
Self::Seedance2ProVideoAlt => "seedance2pro_video_alt",
|
||||
Self::WorldlabsSplat => "worldlabs_splat",
|
||||
Self::TtsGptSoVits => "tts_gpt_so_vits",
|
||||
Self::TtsStyleTts2 => "tts_style_tts2",
|
||||
@@ -190,6 +196,7 @@ impl InferenceJobProductCategory {
|
||||
"beeble_video" => Ok(Self::BeebleVideo),
|
||||
"seedance2pro_video" => Ok(Self::Seedance2ProVideo),
|
||||
"seedance2pro_character" => Ok(Self::Seedance2ProCharacter),
|
||||
"seedance2pro_video_alt" => Ok(Self::Seedance2ProVideoAlt),
|
||||
"worldlabs_splat" => Ok(Self::WorldlabsSplat),
|
||||
"tts_gpt_so_vits" => Ok(Self::TtsGptSoVits),
|
||||
"tts_style_tts2" => Ok(Self::TtsStyleTts2),
|
||||
@@ -230,6 +237,7 @@ impl InferenceJobProductCategory {
|
||||
Self::BeebleVideo,
|
||||
Self::Seedance2ProVideo,
|
||||
Self::Seedance2ProCharacter,
|
||||
Self::Seedance2ProVideoAlt,
|
||||
Self::WorldlabsSplat,
|
||||
Self::TtsGptSoVits,
|
||||
Self::TtsStyleTts2,
|
||||
@@ -276,6 +284,7 @@ mod tests {
|
||||
assert_serialization(InferenceJobProductCategory::BeebleVideo, "beeble_video");
|
||||
assert_serialization(InferenceJobProductCategory::Seedance2ProVideo, "seedance2pro_video");
|
||||
assert_serialization(InferenceJobProductCategory::Seedance2ProCharacter, "seedance2pro_character");
|
||||
assert_serialization(InferenceJobProductCategory::Seedance2ProVideoAlt, "seedance2pro_video_alt");
|
||||
assert_serialization(InferenceJobProductCategory::WorldlabsSplat, "worldlabs_splat");
|
||||
assert_serialization(InferenceJobProductCategory::TtsGptSoVits, "tts_gpt_so_vits");
|
||||
assert_serialization(InferenceJobProductCategory::TtsStyleTts2, "tts_style_tts2");
|
||||
@@ -312,6 +321,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobProductCategory::BeebleVideo.to_str(), "beeble_video");
|
||||
assert_eq!(InferenceJobProductCategory::Seedance2ProVideo.to_str(), "seedance2pro_video");
|
||||
assert_eq!(InferenceJobProductCategory::Seedance2ProCharacter.to_str(), "seedance2pro_character");
|
||||
assert_eq!(InferenceJobProductCategory::Seedance2ProVideoAlt.to_str(), "seedance2pro_video_alt");
|
||||
assert_eq!(InferenceJobProductCategory::WorldlabsSplat.to_str(), "worldlabs_splat");
|
||||
assert_eq!(InferenceJobProductCategory::TtsGptSoVits.to_str(), "tts_gpt_so_vits");
|
||||
assert_eq!(InferenceJobProductCategory::TtsStyleTts2.to_str(), "tts_style_tts2");
|
||||
@@ -347,6 +357,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobProductCategory::from_str("beeble_video").unwrap(), InferenceJobProductCategory::BeebleVideo);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video").unwrap(), InferenceJobProductCategory::Seedance2ProVideo);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_character").unwrap(), InferenceJobProductCategory::Seedance2ProCharacter);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video_alt").unwrap(), InferenceJobProductCategory::Seedance2ProVideoAlt);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("worldlabs_splat").unwrap(), InferenceJobProductCategory::WorldlabsSplat);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("tts_gpt_so_vits").unwrap(), InferenceJobProductCategory::TtsGptSoVits);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("tts_style_tts2").unwrap(), InferenceJobProductCategory::TtsStyleTts2);
|
||||
@@ -374,7 +385,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 32;
|
||||
const EXPECTED_COUNT : usize = 33;
|
||||
|
||||
assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
+11
-1
@@ -31,6 +31,10 @@ pub enum InferenceJobType {
|
||||
#[serde(rename = "seedance2pro_character")]
|
||||
Seedance2ProCharacter,
|
||||
|
||||
/// Seedance 2 Pro Alt jobs. We poll for results.
|
||||
#[serde(rename = "seedance2pro_alt_queue")]
|
||||
Seedance2ProAltQueue,
|
||||
|
||||
/// Beeble jobs. We poll for results.
|
||||
#[serde(rename = "beeble_queue")]
|
||||
BeebleQueue,
|
||||
@@ -133,6 +137,7 @@ impl InferenceJobType {
|
||||
Self::GmiCloudQueue => "gmicloud_queue",
|
||||
Self::BeebleQueue => "beeble_queue",
|
||||
Self::Seedance2ProQueue => "seedance2pro_queue",
|
||||
Self::Seedance2ProAltQueue => "seedance2pro_alt_queue",
|
||||
Self::Seedance2ProCharacter => "seedance2pro_character",
|
||||
Self::WorldlabsQueue => "worldlabs_queue",
|
||||
Self::VideoRender => "video_render",
|
||||
@@ -165,6 +170,7 @@ impl InferenceJobType {
|
||||
"beeble_queue" => Ok(Self::BeebleQueue),
|
||||
"seedance2pro_queue" => Ok(Self::Seedance2ProQueue),
|
||||
"seedance2pro_character" => Ok(Self::Seedance2ProCharacter),
|
||||
"seedance2pro_alt_queue" => Ok(Self::Seedance2ProAltQueue),
|
||||
"worldlabs_queue" => Ok(Self::WorldlabsQueue),
|
||||
"video_render" => Ok(Self::VideoRender),
|
||||
"live_portrait" => Ok(Self::LivePortrait),
|
||||
@@ -199,6 +205,7 @@ impl InferenceJobType {
|
||||
Self::BeebleQueue,
|
||||
Self::Seedance2ProQueue,
|
||||
Self::Seedance2ProCharacter,
|
||||
Self::Seedance2ProAltQueue,
|
||||
Self::WorldlabsQueue,
|
||||
Self::VideoRender,
|
||||
Self::LivePortrait,
|
||||
@@ -244,6 +251,7 @@ mod tests {
|
||||
assert_serialization(InferenceJobType::BeebleQueue, "beeble_queue");
|
||||
assert_serialization(InferenceJobType::Seedance2ProQueue, "seedance2pro_queue");
|
||||
assert_serialization(InferenceJobType::Seedance2ProCharacter, "seedance2pro_character");
|
||||
assert_serialization(InferenceJobType::Seedance2ProAltQueue, "seedance2pro_alt_queue");
|
||||
assert_serialization(InferenceJobType::WorldlabsQueue, "worldlabs_queue");
|
||||
assert_serialization(InferenceJobType::VideoRender, "video_render");
|
||||
assert_serialization(InferenceJobType::LivePortrait, "live_portrait");
|
||||
@@ -274,6 +282,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobType::BeebleQueue.to_str(), "beeble_queue");
|
||||
assert_eq!(InferenceJobType::Seedance2ProQueue.to_str(), "seedance2pro_queue");
|
||||
assert_eq!(InferenceJobType::Seedance2ProCharacter.to_str(), "seedance2pro_character");
|
||||
assert_eq!(InferenceJobType::Seedance2ProAltQueue.to_str(), "seedance2pro_alt_queue");
|
||||
assert_eq!(InferenceJobType::WorldlabsQueue.to_str(), "worldlabs_queue");
|
||||
assert_eq!(InferenceJobType::VideoRender.to_str(), "video_render");
|
||||
assert_eq!(InferenceJobType::LivePortrait.to_str(), "live_portrait");
|
||||
@@ -304,6 +313,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobType::from_str("beeble_queue").unwrap(), InferenceJobType::BeebleQueue);
|
||||
assert_eq!(InferenceJobType::from_str("seedance2pro_queue").unwrap(), InferenceJobType::Seedance2ProQueue);
|
||||
assert_eq!(InferenceJobType::from_str("seedance2pro_character").unwrap(), InferenceJobType::Seedance2ProCharacter);
|
||||
assert_eq!(InferenceJobType::from_str("seedance2pro_alt_queue").unwrap(), InferenceJobType::Seedance2ProAltQueue);
|
||||
assert_eq!(InferenceJobType::from_str("worldlabs_queue").unwrap(), InferenceJobType::WorldlabsQueue);
|
||||
assert_eq!(InferenceJobType::from_str("video_render").unwrap(), InferenceJobType::VideoRender);
|
||||
assert_eq!(InferenceJobType::from_str("live_portrait").unwrap(), InferenceJobType::LivePortrait);
|
||||
@@ -330,7 +340,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 26;
|
||||
const EXPECTED_COUNT : usize = 27;
|
||||
|
||||
assert_eq!(InferenceJobType::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobType::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
@@ -130,10 +130,14 @@ pub enum CommonModelType {
|
||||
Seedance2p0,
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
#[serde(rename = "seedance_2p0_g")]
|
||||
Seedance2p0Global,
|
||||
#[serde(rename = "seedance_2p0_fast_g")]
|
||||
Seedance2p0FastGlobal,
|
||||
#[serde(rename = "seedance_2p0_bp")]
|
||||
Seedance2p0BytePlus,
|
||||
#[serde(rename = "seedance_2p0_bp_fast")]
|
||||
Seedance2p0BytePlusFast,
|
||||
#[serde(rename = "seedance_2p0_u")]
|
||||
Seedance2p0Ultra,
|
||||
#[serde(rename = "seedance_2p0_u_fast")]
|
||||
Seedance2p0UltraFast,
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
#[serde(rename = "sora_2_pro")]
|
||||
@@ -148,6 +152,10 @@ pub enum CommonModelType {
|
||||
Veo3p1,
|
||||
#[serde(rename = "veo_3p1_fast")]
|
||||
Veo3p1Fast,
|
||||
#[serde(rename = "preview_model")]
|
||||
PreviewModel,
|
||||
#[serde(rename = "preview_model_fast")]
|
||||
PreviewModelFast,
|
||||
#[serde(rename = "switch_x")]
|
||||
SwitchX,
|
||||
|
||||
@@ -223,8 +231,10 @@ impl CommonModelType {
|
||||
Self::Seedance1p5Pro => "seedance_1p5_pro",
|
||||
Self::Seedance2p0 => "seedance_2p0",
|
||||
Self::Seedance2p0Fast => "seedance_2p0_fast",
|
||||
Self::Seedance2p0Global => "seedance_2p0_g",
|
||||
Self::Seedance2p0FastGlobal => "seedance_2p0_fast_g",
|
||||
Self::Seedance2p0BytePlus => "seedance_2p0_bp",
|
||||
Self::Seedance2p0BytePlusFast => "seedance_2p0_bp_fast",
|
||||
Self::Seedance2p0Ultra => "seedance_2p0_u",
|
||||
Self::Seedance2p0UltraFast => "seedance_2p0_u_fast",
|
||||
Self::Sora2 => "sora_2",
|
||||
Self::Sora2Pro => "sora_2_pro",
|
||||
Self::Veo2 => "veo_2",
|
||||
@@ -232,6 +242,8 @@ impl CommonModelType {
|
||||
Self::Veo3Fast => "veo_3_fast",
|
||||
Self::Veo3p1 => "veo_3p1",
|
||||
Self::Veo3p1Fast => "veo_3p1_fast",
|
||||
Self::PreviewModel => "preview_model",
|
||||
Self::PreviewModelFast => "preview_model_fast",
|
||||
Self::SwitchX => "switch_x",
|
||||
|
||||
// 3D Object generation models
|
||||
@@ -295,8 +307,10 @@ impl CommonModelType {
|
||||
"seedance_1p5_pro" => Ok(Self::Seedance1p5Pro),
|
||||
"seedance_2p0" => Ok(Self::Seedance2p0),
|
||||
"seedance_2p0_fast" => Ok(Self::Seedance2p0Fast),
|
||||
"seedance_2p0_g" => Ok(Self::Seedance2p0Global),
|
||||
"seedance_2p0_fast_g" => Ok(Self::Seedance2p0FastGlobal),
|
||||
"seedance_2p0_bp" => Ok(Self::Seedance2p0BytePlus),
|
||||
"seedance_2p0_bp_fast" => Ok(Self::Seedance2p0BytePlusFast),
|
||||
"seedance_2p0_u" => Ok(Self::Seedance2p0Ultra),
|
||||
"seedance_2p0_u_fast" => Ok(Self::Seedance2p0UltraFast),
|
||||
"sora_2" => Ok(Self::Sora2),
|
||||
"sora_2_pro" => Ok(Self::Sora2Pro),
|
||||
"veo_2" => Ok(Self::Veo2),
|
||||
@@ -304,6 +318,8 @@ impl CommonModelType {
|
||||
"veo_3_fast" => Ok(Self::Veo3Fast),
|
||||
"veo_3p1" => Ok(Self::Veo3p1),
|
||||
"veo_3p1_fast" => Ok(Self::Veo3p1Fast),
|
||||
"preview_model" => Ok(Self::PreviewModel),
|
||||
"preview_model_fast" => Ok(Self::PreviewModelFast),
|
||||
"switch_x" => Ok(Self::SwitchX),
|
||||
|
||||
// 3D Object generation models
|
||||
@@ -371,8 +387,10 @@ impl CommonModelType {
|
||||
Self::Seedance1p5Pro,
|
||||
Self::Seedance2p0,
|
||||
Self::Seedance2p0Fast,
|
||||
Self::Seedance2p0Global,
|
||||
Self::Seedance2p0FastGlobal,
|
||||
Self::Seedance2p0BytePlus,
|
||||
Self::Seedance2p0BytePlusFast,
|
||||
Self::Seedance2p0Ultra,
|
||||
Self::Seedance2p0UltraFast,
|
||||
Self::Sora2,
|
||||
Self::Sora2Pro,
|
||||
Self::Veo2,
|
||||
@@ -380,6 +398,8 @@ impl CommonModelType {
|
||||
Self::Veo3Fast,
|
||||
Self::Veo3p1,
|
||||
Self::Veo3p1Fast,
|
||||
Self::PreviewModel,
|
||||
Self::PreviewModelFast,
|
||||
Self::SwitchX,
|
||||
|
||||
// 3D Object generation models
|
||||
@@ -444,8 +464,10 @@ impl CommonModelType {
|
||||
Self::Seedance1p5Pro => CommonModelClass::Video,
|
||||
Self::Seedance2p0 => CommonModelClass::Video,
|
||||
Self::Seedance2p0Fast => CommonModelClass::Video,
|
||||
Self::Seedance2p0Global => CommonModelClass::Video,
|
||||
Self::Seedance2p0FastGlobal => CommonModelClass::Video,
|
||||
Self::Seedance2p0BytePlus => CommonModelClass::Video,
|
||||
Self::Seedance2p0BytePlusFast => CommonModelClass::Video,
|
||||
Self::Seedance2p0Ultra => CommonModelClass::Video,
|
||||
Self::Seedance2p0UltraFast => CommonModelClass::Video,
|
||||
Self::Sora2 => CommonModelClass::Video,
|
||||
Self::Sora2Pro => CommonModelClass::Video,
|
||||
Self::Veo2 => CommonModelClass::Video,
|
||||
@@ -453,6 +475,8 @@ impl CommonModelType {
|
||||
Self::Veo3Fast => CommonModelClass::Video,
|
||||
Self::Veo3p1 => CommonModelClass::Video,
|
||||
Self::Veo3p1Fast => CommonModelClass::Video,
|
||||
Self::PreviewModel => CommonModelClass::Video,
|
||||
Self::PreviewModelFast => CommonModelClass::Video,
|
||||
Self::SwitchX => CommonModelClass::Video,
|
||||
|
||||
// 3D Object generation models (mesh)
|
||||
@@ -526,8 +550,10 @@ mod tests {
|
||||
assert_serialization(CommonModelType::Seedance1p5Pro, "seedance_1p5_pro");
|
||||
assert_serialization(CommonModelType::Seedance2p0, "seedance_2p0");
|
||||
assert_serialization(CommonModelType::Seedance2p0Fast, "seedance_2p0_fast");
|
||||
assert_serialization(CommonModelType::Seedance2p0Global, "seedance_2p0_g");
|
||||
assert_serialization(CommonModelType::Seedance2p0FastGlobal, "seedance_2p0_fast_g");
|
||||
assert_serialization(CommonModelType::Seedance2p0BytePlus, "seedance_2p0_bp");
|
||||
assert_serialization(CommonModelType::Seedance2p0BytePlusFast, "seedance_2p0_bp_fast");
|
||||
assert_serialization(CommonModelType::Seedance2p0Ultra, "seedance_2p0_u");
|
||||
assert_serialization(CommonModelType::Seedance2p0UltraFast, "seedance_2p0_u_fast");
|
||||
assert_serialization(CommonModelType::Sora2, "sora_2");
|
||||
assert_serialization(CommonModelType::Sora2Pro, "sora_2_pro");
|
||||
assert_serialization(CommonModelType::Veo2, "veo_2");
|
||||
@@ -535,6 +561,8 @@ mod tests {
|
||||
assert_serialization(CommonModelType::Veo3Fast, "veo_3_fast");
|
||||
assert_serialization(CommonModelType::Veo3p1, "veo_3p1");
|
||||
assert_serialization(CommonModelType::Veo3p1Fast, "veo_3p1_fast");
|
||||
assert_serialization(CommonModelType::PreviewModel, "preview_model");
|
||||
assert_serialization(CommonModelType::PreviewModelFast, "preview_model_fast");
|
||||
assert_serialization(CommonModelType::SwitchX, "switch_x");
|
||||
// 3D Object generation models
|
||||
assert_serialization(CommonModelType::Hunyuan3d2_0, "hunyuan_3d_2p0");
|
||||
@@ -595,8 +623,10 @@ mod tests {
|
||||
assert_eq!(CommonModelType::Seedance1p5Pro.to_str(), "seedance_1p5_pro");
|
||||
assert_eq!(CommonModelType::Seedance2p0.to_str(), "seedance_2p0");
|
||||
assert_eq!(CommonModelType::Seedance2p0Fast.to_str(), "seedance_2p0_fast");
|
||||
assert_eq!(CommonModelType::Seedance2p0Global.to_str(), "seedance_2p0_g");
|
||||
assert_eq!(CommonModelType::Seedance2p0FastGlobal.to_str(), "seedance_2p0_fast_g");
|
||||
assert_eq!(CommonModelType::Seedance2p0BytePlus.to_str(), "seedance_2p0_bp");
|
||||
assert_eq!(CommonModelType::Seedance2p0BytePlusFast.to_str(), "seedance_2p0_bp_fast");
|
||||
assert_eq!(CommonModelType::Seedance2p0Ultra.to_str(), "seedance_2p0_u");
|
||||
assert_eq!(CommonModelType::Seedance2p0UltraFast.to_str(), "seedance_2p0_u_fast");
|
||||
assert_eq!(CommonModelType::Sora2.to_str(), "sora_2");
|
||||
assert_eq!(CommonModelType::Sora2Pro.to_str(), "sora_2_pro");
|
||||
assert_eq!(CommonModelType::Veo2.to_str(), "veo_2");
|
||||
@@ -604,6 +634,8 @@ mod tests {
|
||||
assert_eq!(CommonModelType::Veo3Fast.to_str(), "veo_3_fast");
|
||||
assert_eq!(CommonModelType::Veo3p1.to_str(), "veo_3p1");
|
||||
assert_eq!(CommonModelType::Veo3p1Fast.to_str(), "veo_3p1_fast");
|
||||
assert_eq!(CommonModelType::PreviewModel.to_str(), "preview_model");
|
||||
assert_eq!(CommonModelType::PreviewModelFast.to_str(), "preview_model_fast");
|
||||
assert_eq!(CommonModelType::SwitchX.to_str(), "switch_x");
|
||||
|
||||
// 3D Object generation models
|
||||
@@ -664,8 +696,10 @@ mod tests {
|
||||
assert_eq!(CommonModelType::from_str("seedance_1p5_pro").unwrap(), CommonModelType::Seedance1p5Pro);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0").unwrap(), CommonModelType::Seedance2p0);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_fast").unwrap(), CommonModelType::Seedance2p0Fast);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_g").unwrap(), CommonModelType::Seedance2p0Global);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_fast_g").unwrap(), CommonModelType::Seedance2p0FastGlobal);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_bp").unwrap(), CommonModelType::Seedance2p0BytePlus);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_bp_fast").unwrap(), CommonModelType::Seedance2p0BytePlusFast);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_u").unwrap(), CommonModelType::Seedance2p0Ultra);
|
||||
assert_eq!(CommonModelType::from_str("seedance_2p0_u_fast").unwrap(), CommonModelType::Seedance2p0UltraFast);
|
||||
assert_eq!(CommonModelType::from_str("sora_2").unwrap(), CommonModelType::Sora2);
|
||||
assert_eq!(CommonModelType::from_str("sora_2_pro").unwrap(), CommonModelType::Sora2Pro);
|
||||
assert_eq!(CommonModelType::from_str("veo_2").unwrap(), CommonModelType::Veo2);
|
||||
@@ -673,6 +707,8 @@ mod tests {
|
||||
assert_eq!(CommonModelType::from_str("veo_3_fast").unwrap(), CommonModelType::Veo3Fast);
|
||||
assert_eq!(CommonModelType::from_str("veo_3p1").unwrap(), CommonModelType::Veo3p1);
|
||||
assert_eq!(CommonModelType::from_str("veo_3p1_fast").unwrap(), CommonModelType::Veo3p1Fast);
|
||||
assert_eq!(CommonModelType::from_str("preview_model").unwrap(), CommonModelType::PreviewModel);
|
||||
assert_eq!(CommonModelType::from_str("preview_model_fast").unwrap(), CommonModelType::PreviewModelFast);
|
||||
assert_eq!(CommonModelType::from_str("switch_x").unwrap(), CommonModelType::SwitchX);
|
||||
|
||||
// 3D Object generation models
|
||||
@@ -687,7 +723,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonModelType::all_variants();
|
||||
assert_eq!(variants.len(), 60);
|
||||
assert_eq!(variants.len(), 64);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell));
|
||||
@@ -735,8 +771,10 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance1p5Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0Fast));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0Global));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0FastGlobal));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0BytePlus));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0BytePlusFast));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0Ultra));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0UltraFast));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Veo2));
|
||||
@@ -744,6 +782,8 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Veo3Fast));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Veo3p1));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Veo3p1Fast));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::PreviewModel));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::PreviewModelFast));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::SwitchX));
|
||||
// 3D Object generation models
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Hunyuan3d2_0));
|
||||
|
||||
@@ -44,11 +44,17 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
|
||||
#[serde(rename = "seedance_2p0_g")]
|
||||
Seedance2p0Global,
|
||||
#[serde(rename = "seedance_2p0_bp")]
|
||||
Seedance2p0BytePlus,
|
||||
|
||||
#[serde(rename = "seedance_2p0_fast_g")]
|
||||
Seedance2p0FastGlobal,
|
||||
#[serde(rename = "seedance_2p0_bp_fast")]
|
||||
Seedance2p0BytePlusFast,
|
||||
|
||||
#[serde(rename = "seedance_2p0_u")]
|
||||
Seedance2p0Ultra,
|
||||
|
||||
#[serde(rename = "seedance_2p0_u_fast")]
|
||||
Seedance2p0UltraFast,
|
||||
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
@@ -70,6 +76,16 @@ pub enum CommonVideoModel {
|
||||
|
||||
#[serde(rename = "veo_3p1_fast")]
|
||||
Veo3p1Fast,
|
||||
|
||||
// NB: Temporary model rollout. Previously used for "Seedance 2.0 BytePlus".
|
||||
// This can be reused in the future.
|
||||
#[serde(rename = "preview_model")]
|
||||
PreviewModel,
|
||||
|
||||
// NB: Temporary model rollout. Previously used for "Seedance 2.0 BytePlus Fast".
|
||||
// This can be reused in the future.
|
||||
#[serde(rename = "preview_model_fast")]
|
||||
PreviewModelFast,
|
||||
}
|
||||
|
||||
impl CommonVideoModel {
|
||||
@@ -89,8 +105,10 @@ impl CommonVideoModel {
|
||||
Self::Seedance1p5Pro => CommonModelType::Seedance1p5Pro,
|
||||
Self::Seedance2p0 => CommonModelType::Seedance2p0,
|
||||
Self::Seedance2p0Fast => CommonModelType::Seedance2p0Fast,
|
||||
Self::Seedance2p0Global => CommonModelType::Seedance2p0Global,
|
||||
Self::Seedance2p0FastGlobal => CommonModelType::Seedance2p0FastGlobal,
|
||||
Self::Seedance2p0BytePlus => CommonModelType::Seedance2p0BytePlus,
|
||||
Self::Seedance2p0BytePlusFast => CommonModelType::Seedance2p0BytePlusFast,
|
||||
Self::Seedance2p0Ultra => CommonModelType::Seedance2p0Ultra,
|
||||
Self::Seedance2p0UltraFast => CommonModelType::Seedance2p0UltraFast,
|
||||
Self::Sora2 => CommonModelType::Sora2,
|
||||
Self::Sora2Pro => CommonModelType::Sora2Pro,
|
||||
Self::Veo2 => CommonModelType::Veo2,
|
||||
@@ -98,6 +116,8 @@ impl CommonVideoModel {
|
||||
Self::Veo3Fast => CommonModelType::Veo3Fast,
|
||||
Self::Veo3p1 => CommonModelType::Veo3p1,
|
||||
Self::Veo3p1Fast => CommonModelType::Veo3p1Fast,
|
||||
Self::PreviewModel => CommonModelType::PreviewModel,
|
||||
Self::PreviewModelFast => CommonModelType::PreviewModelFast,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,8 +143,10 @@ mod tests {
|
||||
assert_serialization(CommonVideoModel::Seedance1p5Pro, "seedance_1p5_pro");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0, "seedance_2p0");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0Fast, "seedance_2p0_fast");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0Global, "seedance_2p0_g");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0FastGlobal, "seedance_2p0_fast_g");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0BytePlus, "seedance_2p0_bp");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0BytePlusFast, "seedance_2p0_bp_fast");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0Ultra, "seedance_2p0_u");
|
||||
assert_serialization(CommonVideoModel::Seedance2p0UltraFast, "seedance_2p0_u_fast");
|
||||
assert_serialization(CommonVideoModel::Sora2, "sora_2");
|
||||
assert_serialization(CommonVideoModel::Sora2Pro, "sora_2_pro");
|
||||
assert_serialization(CommonVideoModel::Veo2, "veo_2");
|
||||
@@ -132,6 +154,8 @@ mod tests {
|
||||
assert_serialization(CommonVideoModel::Veo3Fast, "veo_3_fast");
|
||||
assert_serialization(CommonVideoModel::Veo3p1, "veo_3p1");
|
||||
assert_serialization(CommonVideoModel::Veo3p1Fast, "veo_3p1_fast");
|
||||
assert_serialization(CommonVideoModel::PreviewModel, "preview_model");
|
||||
assert_serialization(CommonVideoModel::PreviewModelFast, "preview_model_fast");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -150,8 +174,10 @@ mod tests {
|
||||
("seedance_1p5_pro", CommonVideoModel::Seedance1p5Pro),
|
||||
("seedance_2p0", CommonVideoModel::Seedance2p0),
|
||||
("seedance_2p0_fast", CommonVideoModel::Seedance2p0Fast),
|
||||
("seedance_2p0_g", CommonVideoModel::Seedance2p0Global),
|
||||
("seedance_2p0_fast_g", CommonVideoModel::Seedance2p0FastGlobal),
|
||||
("seedance_2p0_bp", CommonVideoModel::Seedance2p0BytePlus),
|
||||
("seedance_2p0_bp_fast", CommonVideoModel::Seedance2p0BytePlusFast),
|
||||
("seedance_2p0_u", CommonVideoModel::Seedance2p0Ultra),
|
||||
("seedance_2p0_u_fast", CommonVideoModel::Seedance2p0UltraFast),
|
||||
("sora_2", CommonVideoModel::Sora2),
|
||||
("sora_2_pro", CommonVideoModel::Sora2Pro),
|
||||
("veo_2", CommonVideoModel::Veo2),
|
||||
@@ -159,6 +185,8 @@ mod tests {
|
||||
("veo_3_fast", CommonVideoModel::Veo3Fast),
|
||||
("veo_3p1", CommonVideoModel::Veo3p1),
|
||||
("veo_3p1_fast", CommonVideoModel::Veo3p1Fast),
|
||||
("preview_model", CommonVideoModel::PreviewModel),
|
||||
("preview_model_fast", CommonVideoModel::PreviewModelFast),
|
||||
];
|
||||
for (json_str, expected) in cases {
|
||||
let json = format!("\"{}\"", json_str);
|
||||
@@ -184,8 +212,10 @@ mod tests {
|
||||
CommonVideoModel::Seedance1p5Pro,
|
||||
CommonVideoModel::Seedance2p0,
|
||||
CommonVideoModel::Seedance2p0Fast,
|
||||
CommonVideoModel::Seedance2p0Global,
|
||||
CommonVideoModel::Seedance2p0FastGlobal,
|
||||
CommonVideoModel::Seedance2p0BytePlus,
|
||||
CommonVideoModel::Seedance2p0BytePlusFast,
|
||||
CommonVideoModel::Seedance2p0Ultra,
|
||||
CommonVideoModel::Seedance2p0UltraFast,
|
||||
CommonVideoModel::Sora2,
|
||||
CommonVideoModel::Sora2Pro,
|
||||
CommonVideoModel::Veo2,
|
||||
@@ -193,6 +223,8 @@ mod tests {
|
||||
CommonVideoModel::Veo3Fast,
|
||||
CommonVideoModel::Veo3p1,
|
||||
CommonVideoModel::Veo3p1Fast,
|
||||
CommonVideoModel::PreviewModel,
|
||||
CommonVideoModel::PreviewModelFast,
|
||||
];
|
||||
for variant in all {
|
||||
let json = serde_json::to_string(&variant).unwrap();
|
||||
@@ -217,8 +249,10 @@ mod tests {
|
||||
(CommonVideoModel::Seedance1p5Pro, CommonModelType::Seedance1p5Pro),
|
||||
(CommonVideoModel::Seedance2p0, CommonModelType::Seedance2p0),
|
||||
(CommonVideoModel::Seedance2p0Fast, CommonModelType::Seedance2p0Fast),
|
||||
(CommonVideoModel::Seedance2p0Global, CommonModelType::Seedance2p0Global),
|
||||
(CommonVideoModel::Seedance2p0FastGlobal, CommonModelType::Seedance2p0FastGlobal),
|
||||
(CommonVideoModel::Seedance2p0BytePlus, CommonModelType::Seedance2p0BytePlus),
|
||||
(CommonVideoModel::Seedance2p0BytePlusFast, CommonModelType::Seedance2p0BytePlusFast),
|
||||
(CommonVideoModel::Seedance2p0Ultra, CommonModelType::Seedance2p0Ultra),
|
||||
(CommonVideoModel::Seedance2p0UltraFast, CommonModelType::Seedance2p0UltraFast),
|
||||
(CommonVideoModel::Sora2, CommonModelType::Sora2),
|
||||
(CommonVideoModel::Sora2Pro, CommonModelType::Sora2Pro),
|
||||
(CommonVideoModel::Veo2, CommonModelType::Veo2),
|
||||
@@ -226,6 +260,8 @@ mod tests {
|
||||
(CommonVideoModel::Veo3Fast, CommonModelType::Veo3Fast),
|
||||
(CommonVideoModel::Veo3p1, CommonModelType::Veo3p1),
|
||||
(CommonVideoModel::Veo3p1Fast, CommonModelType::Veo3p1Fast),
|
||||
(CommonVideoModel::PreviewModel, CommonModelType::PreviewModel),
|
||||
(CommonVideoModel::PreviewModelFast, CommonModelType::PreviewModelFast),
|
||||
];
|
||||
for (video_model, expected) in models {
|
||||
assert_eq!(video_model.to_common_model_type(), expected);
|
||||
|
||||
@@ -13,7 +13,7 @@ use pager::client::pager::Pager;
|
||||
use crate::http_server::endpoints::health_check_handler::get_health_check_handler;
|
||||
use crate::http_server::http_server_shared_state::HttpServerSharedState;
|
||||
|
||||
const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0:12345";
|
||||
const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0:11223";
|
||||
const DEFAULT_NUM_WORKERS: usize = 2;
|
||||
|
||||
pub struct CreateServerArgs {
|
||||
|
||||
@@ -20,6 +20,10 @@ pub struct JobDependencies {
|
||||
/// Session credentials for polling seedance2-pro.com.
|
||||
pub seedance2pro_session: Seedance2ProSession,
|
||||
|
||||
/// When true, this instance polls for alternate-account jobs
|
||||
/// (Seedance2ProAltQueue / Seedance2ProAlt) instead of the primary ones.
|
||||
pub is_alternate_mode: bool,
|
||||
|
||||
pub server_environment: ServerEnvironment,
|
||||
|
||||
pub job_stats: JobStats,
|
||||
|
||||
+9
-1
@@ -2,6 +2,8 @@ use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use chrono::Utc;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
use log::{error, info, warn};
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
@@ -61,7 +63,13 @@ pub async fn video_polling_main_loop(job_dependencies: JobDependencies) {
|
||||
async fn run_poll_iteration(deps: &JobDependencies) -> anyhow::Result<()> {
|
||||
// 1. Query all (limit 25,000) non-terminal Seedance2Pro jobs from DB.
|
||||
// This is all non-(complete_success, complete_failure) jobs.
|
||||
let pending_jobs = match list_pending_seedance2pro_video_jobs(&deps.mysql_pool).await {
|
||||
let (third_party, job_type) = if deps.is_alternate_mode {
|
||||
(InferenceJobExternalThirdParty::Seedance2ProAlt, InferenceJobType::Seedance2ProAltQueue)
|
||||
} else {
|
||||
(InferenceJobExternalThirdParty::Seedance2Pro, InferenceJobType::Seedance2ProQueue)
|
||||
};
|
||||
|
||||
let pending_jobs = match list_pending_seedance2pro_video_jobs(&deps.mysql_pool, third_party, job_type).await {
|
||||
Ok(jobs) => jobs,
|
||||
Err(err) => {
|
||||
error!("Failed to list pending database jobs: {:?}", err);
|
||||
|
||||
@@ -45,7 +45,11 @@ const ENV_SECRET_KEY: &str = "SECRET_KEY";
|
||||
const ENV_REGION_NAME: &str = "REGION_NAME";
|
||||
const ENV_PUBLIC_BUCKET_NAME: &str = "PUBLIC_BUCKET_NAME";
|
||||
const ENV_S3_ENDPOINT: &str = "S3_COMPATIBLE_ENDPOINT_URL";
|
||||
const ENV_SEEDANCE2PRO_COOKIES : &str = "SEEDANCE2PRO_COOKIES";
|
||||
|
||||
const ENV_SEEDANCE2PRO_ALTERNATE: &str = "SEEDANCE2PRO_ALTERNATE";
|
||||
const ENV_SEEDANCE2PRO_COOKIES: &str = "SEEDANCE2PRO_COOKIES";
|
||||
const ENV_SEEDANCE2PRO_ALT_COOKIES: &str = "SEEDANCE2PRO_ALT_COOKIES";
|
||||
|
||||
const ENV_MAX_JOB_AGE_THRESHOLD_HOURS: &str = "MAX_JOB_AGE_THRESHOLD_HOURS";
|
||||
|
||||
#[tokio::main]
|
||||
@@ -101,8 +105,20 @@ async fn main() -> AnyhowResult<()> {
|
||||
Some(bucket_timeout),
|
||||
)?;
|
||||
|
||||
// Alternate mode: uses a different Kinovi account, different job types, skips character polling.
|
||||
let is_alternate_mode = easyenv::get_env_string_optional(ENV_SEEDANCE2PRO_ALTERNATE)
|
||||
.map(|v| v.trim().to_lowercase())
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_alternate_mode {
|
||||
info!("Running in ALTERNATE mode (SEEDANCE2PRO_ALTERNATE=true).");
|
||||
}
|
||||
|
||||
// Seedance2Pro session from cookie string
|
||||
let seedance2pro_cookies = easyenv::get_env_string_required(ENV_SEEDANCE2PRO_COOKIES)?;
|
||||
let cookie_env_var = if is_alternate_mode { ENV_SEEDANCE2PRO_ALT_COOKIES } else { ENV_SEEDANCE2PRO_COOKIES };
|
||||
info!("Reading Seedance2Pro cookies from environment variable: {}", cookie_env_var);
|
||||
let seedance2pro_cookies = easyenv::get_env_string_required(cookie_env_var)?;
|
||||
let seedance2pro_session = Seedance2ProSession::from_cookies_string(seedance2pro_cookies);
|
||||
|
||||
// How often to poll for results (default: 15 seconds)
|
||||
@@ -171,6 +187,7 @@ async fn main() -> AnyhowResult<()> {
|
||||
mysql_pool,
|
||||
public_bucket_client,
|
||||
seedance2pro_session,
|
||||
is_alternate_mode,
|
||||
server_environment,
|
||||
job_stats,
|
||||
poll_interval_millis,
|
||||
@@ -214,23 +231,27 @@ async fn main() -> AnyhowResult<()> {
|
||||
|
||||
// Spawn all polling loops as concurrent tasks.
|
||||
let video_deps = job_dependencies.clone();
|
||||
let character_deps = job_dependencies.clone();
|
||||
let credits_deps = job_dependencies;
|
||||
let credits_deps = job_dependencies.clone();
|
||||
|
||||
let video_handle = tokio::spawn(async move {
|
||||
video_polling_main_loop(video_deps).await;
|
||||
});
|
||||
|
||||
let character_handle = tokio::spawn(async move {
|
||||
character_polling_main_loop(character_deps).await;
|
||||
});
|
||||
|
||||
let credits_handle = tokio::spawn(async move {
|
||||
credits_checking_main_loop(credits_deps).await;
|
||||
});
|
||||
|
||||
// Wait for all to finish (they exit when application_shutdown is set).
|
||||
let _ = tokio::join!(video_handle, character_handle, credits_handle);
|
||||
if is_alternate_mode {
|
||||
// Alternate mode: skip character polling entirely.
|
||||
info!("Alternate mode: character polling is disabled.");
|
||||
let _ = tokio::join!(video_handle, credits_handle);
|
||||
} else {
|
||||
let character_deps = job_dependencies;
|
||||
let character_handle = tokio::spawn(async move {
|
||||
character_polling_main_loop(character_deps).await;
|
||||
});
|
||||
let _ = tokio::join!(video_handle, character_handle, credits_handle);
|
||||
}
|
||||
|
||||
info!("Shutting down pager worker...");
|
||||
pager_for_shutdown.shutdown_worker();
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
SERVER_ENVIRONMENT='development'
|
||||
|
||||
# Don't collide with storyteller-web in development
|
||||
HTTP_BIND_ADDRESS=0.0.0.0:13333
|
||||
HTTP_BIND_ADDRESS=0.0.0.0:13339
|
||||
|
||||
@@ -13,7 +13,7 @@ use pager::client::pager::Pager;
|
||||
use crate::http_server::endpoints::health_check_handler::get_health_check_handler;
|
||||
use crate::http_server::http_server_shared_state::HttpServerSharedState;
|
||||
|
||||
const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0:12345";
|
||||
const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0:13339";
|
||||
const DEFAULT_NUM_WORKERS: usize = 2;
|
||||
|
||||
pub struct CreateServerArgs {
|
||||
|
||||
@@ -155,10 +155,89 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// models.push(OmniGenVideoModelDetails {
|
||||
// model: CommonVideoModel::Seedance2p0Ultra,
|
||||
// model_creator: Some(ModelCreator::Bytedance),
|
||||
// full_name: Some("Seedance 2.0 (Global)".to_string()),
|
||||
// text_prompt_supported: Some(true),
|
||||
// starting_keyframe_supported: Some(true),
|
||||
// ending_keyframe_supported: Some(true),
|
||||
// image_references_supported: Some(true),
|
||||
// image_references_max: Some(9),
|
||||
// audio_references_supported: Some(true),
|
||||
// audio_references_max: Some(3),
|
||||
// audio_references_max_total_duration_seconds: Some(15),
|
||||
// video_references_supported: Some(true),
|
||||
// video_references_max: Some(3),
|
||||
// video_references_max_total_duration_seconds: Some(15),
|
||||
// character_references_supported: Some(true),
|
||||
// character_references_max: Some(9),
|
||||
// aspect_ratio_options: Some(vec![
|
||||
// CommonAspectRatio::WideTwentyOneByNine,
|
||||
// CommonAspectRatio::WideSixteenByNine,
|
||||
// CommonAspectRatio::WideFourByThree,
|
||||
// CommonAspectRatio::Square,
|
||||
// CommonAspectRatio::TallThreeByFour,
|
||||
// CommonAspectRatio::TallNineBySixteen,
|
||||
// ]),
|
||||
// aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
// resolution_options: Some(vec![
|
||||
// CommonResolution::FourEightyP,
|
||||
// CommonResolution::SevenTwentyP,
|
||||
// CommonResolution::TenEightyP,
|
||||
// ]),
|
||||
// resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
// batch_size_options: Some(vec![1, 2, 4]),
|
||||
// batch_size_default: Some(1),
|
||||
// duration_seconds_min: Some(4),
|
||||
// duration_seconds_max: Some(15),
|
||||
// duration_seconds_default: Some(5),
|
||||
// ..Default::default()
|
||||
// });
|
||||
//
|
||||
// models.push(OmniGenVideoModelDetails {
|
||||
// model: CommonVideoModel::Seedance2p0UltraFast,
|
||||
// model_creator: Some(ModelCreator::Bytedance),
|
||||
// full_name: Some("Seedance 2.0 Fast (Global)".to_string()),
|
||||
// text_prompt_supported: Some(true),
|
||||
// starting_keyframe_supported: Some(true),
|
||||
// ending_keyframe_supported: Some(true),
|
||||
// image_references_supported: Some(true),
|
||||
// image_references_max: Some(9),
|
||||
// audio_references_supported: Some(true),
|
||||
// audio_references_max: Some(3),
|
||||
// audio_references_max_total_duration_seconds: Some(15),
|
||||
// video_references_supported: Some(true),
|
||||
// video_references_max: Some(3),
|
||||
// video_references_max_total_duration_seconds: Some(15),
|
||||
// character_references_supported: Some(true),
|
||||
// character_references_max: Some(9),
|
||||
// aspect_ratio_options: Some(vec![
|
||||
// CommonAspectRatio::WideTwentyOneByNine,
|
||||
// CommonAspectRatio::WideSixteenByNine,
|
||||
// CommonAspectRatio::WideFourByThree,
|
||||
// CommonAspectRatio::Square,
|
||||
// CommonAspectRatio::TallThreeByFour,
|
||||
// CommonAspectRatio::TallNineBySixteen,
|
||||
// ]),
|
||||
// aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
// resolution_options: Some(vec![
|
||||
// CommonResolution::FourEightyP,
|
||||
// CommonResolution::SevenTwentyP,
|
||||
// ]),
|
||||
// resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
// batch_size_options: Some(vec![1, 2, 4]),
|
||||
// batch_size_default: Some(1),
|
||||
// duration_seconds_min: Some(4),
|
||||
// duration_seconds_max: Some(15),
|
||||
// duration_seconds_default: Some(5),
|
||||
// ..Default::default()
|
||||
// });
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0Global,
|
||||
model: CommonVideoModel::Seedance2p0BytePlus,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 (Global)".to_string()),
|
||||
full_name: Some("Seedance 2.0 Plus".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
@@ -196,9 +275,9 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::Seedance2p0FastGlobal,
|
||||
model: CommonVideoModel::Seedance2p0BytePlusFast,
|
||||
model_creator: Some(ModelCreator::Bytedance),
|
||||
full_name: Some("Seedance 2.0 Fast (Global)".to_string()),
|
||||
full_name: Some("Seedance 2.0 Plus Fast".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(true),
|
||||
|
||||
+6
-2
@@ -141,8 +141,10 @@ fn map_video_model(model: CommonVideoModel) -> Result<RouterVideoModel, HandlerE
|
||||
CommonVideoModel::Seedance1p5Pro => RouterVideoModel::Seedance1p5Pro,
|
||||
CommonVideoModel::Seedance2p0 => RouterVideoModel::Seedance2p0,
|
||||
CommonVideoModel::Seedance2p0Fast => RouterVideoModel::Seedance2p0Fast,
|
||||
CommonVideoModel::Seedance2p0Global => RouterVideoModel::Seedance2p0Global,
|
||||
CommonVideoModel::Seedance2p0FastGlobal => RouterVideoModel::Seedance2p0FastGlobal,
|
||||
CommonVideoModel::Seedance2p0Ultra => RouterVideoModel::Seedance2p0Ultra,
|
||||
CommonVideoModel::Seedance2p0UltraFast => RouterVideoModel::Seedance2p0UltraFast,
|
||||
CommonVideoModel::Seedance2p0BytePlus=> RouterVideoModel::Seedance2p0BytePlus,
|
||||
CommonVideoModel::Seedance2p0BytePlusFast=> RouterVideoModel::Seedance2p0BytePlusFast,
|
||||
CommonVideoModel::HappyHorse1p0 => RouterVideoModel::HappyHorse1p0,
|
||||
CommonVideoModel::Sora2 => RouterVideoModel::Sora2,
|
||||
CommonVideoModel::Sora2Pro => RouterVideoModel::Sora2Pro,
|
||||
@@ -151,6 +153,8 @@ fn map_video_model(model: CommonVideoModel) -> Result<RouterVideoModel, HandlerE
|
||||
CommonVideoModel::Veo3Fast => RouterVideoModel::Veo3Fast,
|
||||
CommonVideoModel::Veo3p1 => RouterVideoModel::Veo3p1,
|
||||
CommonVideoModel::Veo3p1Fast => RouterVideoModel::Veo3p1Fast,
|
||||
CommonVideoModel::PreviewModel => RouterVideoModel::PreviewModel,
|
||||
CommonVideoModel::PreviewModelFast => RouterVideoModel::PreviewModelFast,
|
||||
};
|
||||
Ok(router_model)
|
||||
}
|
||||
|
||||
+2
-1
@@ -192,7 +192,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
info!("User {:?} is seedance-whitelisted, trying whitelist session first", user_token);
|
||||
|
||||
let whitelist_session = Seedance2ProSession::from_cookies_string(
|
||||
server_state.seedance2pro.cookies_whitelist.clone()
|
||||
server_state.seedance2pro.cookies_byteplus.clone()
|
||||
);
|
||||
|
||||
let result = upload_and_generate(
|
||||
@@ -412,6 +412,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
|
||||
|
||||
let db_result = insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token(
|
||||
InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs {
|
||||
use_alternate_kinovi: false,
|
||||
apriori_job_token: &job_token,
|
||||
uuid_idempotency_token: &idempotency_str,
|
||||
maybe_external_third_party_id: order_id,
|
||||
|
||||
+18
-4
@@ -13,13 +13,11 @@ use crate::state::server_state::ServerState;
|
||||
pub fn build_router_client(
|
||||
provider: Provider,
|
||||
server_state: &ServerState,
|
||||
use_alternate_kinovi: bool,
|
||||
) -> Result<RouterClient, AdvancedCommonWebError> {
|
||||
match provider {
|
||||
Provider::Seedance2Pro => {
|
||||
let session = Seedance2ProSession::from_cookies_string(
|
||||
server_state.seedance2pro.cookies.clone()
|
||||
);
|
||||
Ok(RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session)))
|
||||
kinovi_provider(server_state, use_alternate_kinovi)
|
||||
}
|
||||
Provider::Fal => {
|
||||
let fal_client = RouterFalClient::new(
|
||||
@@ -40,3 +38,19 @@ pub fn build_router_client(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn kinovi_provider(server_state: &ServerState, use_alternate_kinovi: bool) -> Result<RouterClient, AdvancedCommonWebError> {
|
||||
let session = if use_alternate_kinovi {
|
||||
// Alternate Kinovi
|
||||
Seedance2ProSession::from_cookies_string(
|
||||
server_state.seedance2pro.cookies_byteplus.clone()
|
||||
)
|
||||
} else {
|
||||
// Standard Kinovi
|
||||
Seedance2ProSession::from_cookies_string(
|
||||
server_state.seedance2pro.cookies.clone()
|
||||
)
|
||||
};
|
||||
|
||||
Ok(RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session)))
|
||||
}
|
||||
|
||||
+3
@@ -16,6 +16,7 @@ pub struct InsertSeedance2proJobsArgs<'a, 'tx> {
|
||||
pub primary_order_id: &'a str,
|
||||
pub maybe_additional_order_ids: Option<&'a [String]>,
|
||||
pub maybe_wallet_ledger_entry_token: Option<&'a WalletLedgerEntryToken>,
|
||||
pub use_alternate_kinovi: bool,
|
||||
pub shared: SharedJobArgs<'a, 'tx>,
|
||||
}
|
||||
|
||||
@@ -29,6 +30,7 @@ pub async fn insert_seedance2pro_jobs(args: InsertSeedance2proJobsArgs<'_, '_>)
|
||||
primary_order_id,
|
||||
maybe_additional_order_ids,
|
||||
maybe_wallet_ledger_entry_token,
|
||||
use_alternate_kinovi,
|
||||
mut shared,
|
||||
} = args;
|
||||
|
||||
@@ -55,6 +57,7 @@ pub async fn insert_seedance2pro_jobs(args: InsertSeedance2proJobsArgs<'_, '_>)
|
||||
|
||||
let db_result = insert_generic_inference_job_for_seedance2pro_queue_with_apriori_job_token(
|
||||
InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs {
|
||||
use_alternate_kinovi,
|
||||
apriori_job_token: &job_token,
|
||||
uuid_idempotency_token: &idempotency_str,
|
||||
maybe_external_third_party_id: order_id,
|
||||
|
||||
+16
-2
@@ -187,8 +187,20 @@ pub async fn omni_gen_video_generate_handler(
|
||||
Some(CommonVideoModel::HappyHorse1p0) => true,
|
||||
Some(CommonVideoModel::Seedance2p0) => true,
|
||||
Some(CommonVideoModel::Seedance2p0Fast) => true,
|
||||
Some(CommonVideoModel::Seedance2p0Global) => true,
|
||||
Some(CommonVideoModel::Seedance2p0FastGlobal) => true,
|
||||
Some(CommonVideoModel::Seedance2p0Ultra) => true,
|
||||
Some(CommonVideoModel::Seedance2p0UltraFast) => true,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlus) => true,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusFast) => true,
|
||||
Some(CommonVideoModel::PreviewModel) => true,
|
||||
Some(CommonVideoModel::PreviewModelFast) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let use_alternate_kinovi = match request.model {
|
||||
Some(CommonVideoModel::Seedance2p0BytePlus) => true,
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusFast) => true,
|
||||
Some(CommonVideoModel::PreviewModel) => true,
|
||||
Some(CommonVideoModel::PreviewModelFast) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -201,6 +213,7 @@ pub async fn omni_gen_video_generate_handler(
|
||||
user_token,
|
||||
media_file_to_url_map: &media_file_to_url_map,
|
||||
kinovi_character_id_map: &kinovi_character_id_map,
|
||||
use_alternate_kinovi,
|
||||
}).await?
|
||||
} else {
|
||||
info!("Using pipeline v1");
|
||||
@@ -336,6 +349,7 @@ pub async fn omni_gen_video_generate_handler(
|
||||
primary_order_id: &payload.order_id,
|
||||
maybe_additional_order_ids: payload.maybe_order_ids.as_deref(),
|
||||
maybe_wallet_ledger_entry_token: pipeline_result.billing.maybe_wallet_ledger_entry_token.as_ref(),
|
||||
use_alternate_kinovi,
|
||||
shared: SharedJobArgs {
|
||||
apriori_job_token: &pipeline_result.billing.apriori_job_token,
|
||||
idempotency_token: &idempotency_token,
|
||||
|
||||
+3
-1
@@ -15,7 +15,9 @@ pub(crate) async fn execute_generation_fal(
|
||||
plan: &VideoGenerationPlan,
|
||||
server_state: &ServerState,
|
||||
) -> Result<GenerateVideoResponse, AdvancedCommonWebError> {
|
||||
let router_client = build_router_client(Provider::Fal, server_state)?;
|
||||
const USE_ALTERNATE_KINOVI: bool = false;
|
||||
|
||||
let router_client = build_router_client(Provider::Fal, server_state, USE_ALTERNATE_KINOVI)?;
|
||||
|
||||
let response = plan.generate_video(&router_client)
|
||||
.await
|
||||
|
||||
+21
-3
@@ -27,6 +27,7 @@ pub struct RunPipelineV2Args<'a> {
|
||||
pub user_token: &'a UserToken,
|
||||
pub media_file_to_url_map: &'a Option<HashMap<MediaFileToken, String>>,
|
||||
pub kinovi_character_id_map: &'a Option<HashMap<CharacterToken, String>>,
|
||||
pub use_alternate_kinovi: bool,
|
||||
}
|
||||
|
||||
pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResult, AdvancedCommonWebError> {
|
||||
@@ -37,14 +38,29 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
|
||||
user_token,
|
||||
media_file_to_url_map,
|
||||
kinovi_character_id_map,
|
||||
use_alternate_kinovi,
|
||||
} = args;
|
||||
|
||||
let mut router_builder = router_builder.clone();
|
||||
|
||||
match router_builder.model {
|
||||
CommonVideoModel::PreviewModel |
|
||||
CommonVideoModel::Seedance2p0BytePlus => {
|
||||
router_builder.model = CommonVideoModel::Seedance2p0;
|
||||
},
|
||||
CommonVideoModel::PreviewModelFast |
|
||||
CommonVideoModel::Seedance2p0BytePlusFast => {
|
||||
router_builder.model = CommonVideoModel::Seedance2p0Fast;
|
||||
},
|
||||
_ => {}, // Fall-through
|
||||
}
|
||||
|
||||
let provider = match router_builder.model {
|
||||
CommonVideoModel::HappyHorse1p0 => Provider::Seedance2Pro,
|
||||
CommonVideoModel::Seedance2p0 => Provider::Seedance2Pro,
|
||||
CommonVideoModel::Seedance2p0Fast => Provider::Seedance2Pro,
|
||||
CommonVideoModel::Seedance2p0Global => Provider::GmiCloud,
|
||||
CommonVideoModel::Seedance2p0FastGlobal => Provider::GmiCloud,
|
||||
CommonVideoModel::Seedance2p0Ultra => Provider::GmiCloud,
|
||||
CommonVideoModel::Seedance2p0UltraFast => Provider::GmiCloud,
|
||||
_ => Provider::Fal,
|
||||
};
|
||||
|
||||
@@ -97,6 +113,7 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
|
||||
server_state,
|
||||
media_file_to_url_map.as_ref(),
|
||||
kinovi_character_id_map.as_ref(),
|
||||
use_alternate_kinovi,
|
||||
).await;
|
||||
|
||||
// 5. On failure, refund wallet for Kinovi requests.
|
||||
@@ -129,10 +146,11 @@ async fn upload_and_generate(
|
||||
server_state: &ServerState,
|
||||
media_file_urls_by_token: Option<&HashMap<MediaFileToken, String>>,
|
||||
kinovi_character_ids: Option<&HashMap<CharacterToken, String>>,
|
||||
use_alternate_kinovi: bool,
|
||||
) -> Result<GenerateVideoResponse, AdvancedCommonWebError> {
|
||||
|
||||
let provider = draft_or_request.get_provider();
|
||||
let client = build_router_client(provider, server_state)?;
|
||||
let client = build_router_client(provider, server_state, use_alternate_kinovi)?;
|
||||
|
||||
let video_request = match draft_or_request {
|
||||
VideoGenerationDraftOrRequest::Request(request) => request,
|
||||
|
||||
@@ -492,7 +492,7 @@ async fn main() -> AnyhowResult<()> {
|
||||
},
|
||||
seedance2pro: Seedance2ProData {
|
||||
cookies: easyenv::get_env_string_required("SEEDANCE2PRO_COOKIES")?,
|
||||
cookies_whitelist: easyenv::get_env_string_required("SEEDANCE2PRO_WHITELIST_COOKIES")?,
|
||||
cookies_byteplus: easyenv::get_env_string_required("SEEDANCE2PRO_WHITELIST_COOKIES")?,
|
||||
},
|
||||
openai: OpenAiData {
|
||||
api_key: openai_api_key,
|
||||
|
||||
@@ -346,7 +346,7 @@ pub struct Seedance2ProData {
|
||||
pub cookies: String,
|
||||
|
||||
/// Cookie string for seedance2-pro.com (Whitelisted account)
|
||||
pub cookies_whitelist: String,
|
||||
pub cookies_byteplus: String,
|
||||
}
|
||||
|
||||
/// OpenAI integration
|
||||
|
||||
@@ -126,6 +126,11 @@ export function App() {
|
||||
<Route path="/edit-3d/:sceneToken" element={<PageScene />} />
|
||||
<Route path="/support" element={<Support />} />
|
||||
<Route path="/pricing" element={<Pricing />} />
|
||||
{/* Welcome is public so it stays reachable right after signup
|
||||
without losing the race against the session refresh that
|
||||
RequireAuth reads. The page itself drives the pricing/Stripe
|
||||
handoff via PricingTable. */}
|
||||
<Route path="/welcome" element={<Welcome />} />
|
||||
|
||||
{/* Protected — sign-in required (user-owned content / billing flows) */}
|
||||
<Route element={<RequireAuth />}>
|
||||
@@ -134,7 +139,6 @@ export function App() {
|
||||
<Route path="/library" element={<Library />} />
|
||||
<Route path="/library/:filter" element={<Library />} />
|
||||
<Route path="/referrals" element={<Referrals />} />
|
||||
<Route path="/welcome" element={<Welcome />} />
|
||||
<Route path="/onboarding" element={<Onboarding />} />
|
||||
<Route path="/checkout/success" element={<CheckoutSuccess />} />
|
||||
<Route path="/checkout/cancel" element={<CheckoutCancel />} />
|
||||
|
||||
@@ -267,7 +267,7 @@ export function LightboxDetails({
|
||||
return (
|
||||
<a
|
||||
key={contextImage.media_token}
|
||||
href={`/media/${contextImage.media_token}`}
|
||||
href={`${SHARE_URL_BASE}${contextImage.media_token}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative aspect-square overflow-hidden rounded-lg border border-white/10 hover:border-white/40 transition-colors block"
|
||||
|
||||
@@ -9,13 +9,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faImages,
|
||||
faPlus,
|
||||
faSpinner,
|
||||
faSpinnerThird,
|
||||
faXmark,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faImage } from "@fortawesome/pro-regular-svg-icons";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { UploaderStates } from "@storyteller/common";
|
||||
import {
|
||||
@@ -75,6 +75,7 @@ export const ImagePromptRow = ({
|
||||
const [uploadingImages, setUploadingImages] = useState<
|
||||
{ id: string; file: File }[]
|
||||
>([]);
|
||||
const [previewImage, setPreviewImage] = useState<RefImage | null>(null);
|
||||
|
||||
const referenceImagesRef = useRef(referenceImages);
|
||||
referenceImagesRef.current = referenceImages;
|
||||
@@ -273,6 +274,7 @@ export const ImagePromptRow = ({
|
||||
image={image}
|
||||
allowReorder={allowReorder}
|
||||
onRemove={handleRemoveReference}
|
||||
onPreview={(img) => setPreviewImage(img)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
@@ -285,6 +287,7 @@ export const ImagePromptRow = ({
|
||||
key={image.id}
|
||||
image={image}
|
||||
onRemove={handleRemoveReference}
|
||||
onPreview={(img) => setPreviewImage(img)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -371,14 +374,20 @@ export const ImagePromptRow = ({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{endFrameImage ? (
|
||||
<div className="group relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-all hover:border-white/80">
|
||||
<div
|
||||
className="group relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-all cursor-pointer hover:border-white/80"
|
||||
onClick={() => setPreviewImage(endFrameImage)}
|
||||
>
|
||||
<img
|
||||
src={endFrameImage.url}
|
||||
alt="End frame"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setEndFrameImage?.(undefined)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEndFrameImage?.(undefined);
|
||||
}}
|
||||
className="absolute right-[2px] top-[2px] flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-black/50 text-white sm:opacity-0 backdrop-blur-md transition-colors hover:bg-black sm:group-hover:opacity-100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="h-2.5 w-2.5" />
|
||||
@@ -389,7 +398,7 @@ export const ImagePromptRow = ({
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
spin
|
||||
className="h-6 w-6 text-white"
|
||||
className="h-5 w-5 text-white"
|
||||
/>
|
||||
</div>
|
||||
) : onPickEndFrameFromLibrary ? (
|
||||
@@ -445,6 +454,25 @@ export const ImagePromptRow = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={!!previewImage}
|
||||
onClose={() => setPreviewImage(null)}
|
||||
className="w-fit h-fit max-w-none bg-transparent border-0 p-0 shadow-none overflow-visible"
|
||||
backdropClassName="!bg-black/80"
|
||||
showClose={true}
|
||||
closeOnOutsideClick={true}
|
||||
>
|
||||
{previewImage && (
|
||||
<div className="relative flex items-center justify-center">
|
||||
<img
|
||||
src={previewImage.fullUrl || previewImage.url}
|
||||
alt="Preview"
|
||||
className="max-w-[90vw] max-h-[90vh] object-contain drop-shadow-2xl rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -454,11 +482,16 @@ export const ImagePromptRow = ({
|
||||
const ImageThumbnail = ({
|
||||
image,
|
||||
onRemove,
|
||||
onPreview,
|
||||
}: {
|
||||
image: RefImage;
|
||||
onRemove: (id: string) => void;
|
||||
onPreview?: (image: RefImage) => void;
|
||||
}) => (
|
||||
<div className="group glass relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-all hover:border-white/80">
|
||||
<div
|
||||
className="group glass relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-all cursor-pointer hover:border-white/80"
|
||||
onClick={() => onPreview?.(image)}
|
||||
>
|
||||
<img
|
||||
src={image.url}
|
||||
alt="Reference"
|
||||
@@ -480,10 +513,12 @@ const SortableImage = ({
|
||||
image,
|
||||
allowReorder,
|
||||
onRemove,
|
||||
onPreview,
|
||||
}: {
|
||||
image: RefImage;
|
||||
allowReorder: boolean;
|
||||
onRemove: (id: string) => void;
|
||||
onPreview?: (image: RefImage) => void;
|
||||
}) => {
|
||||
const {
|
||||
attributes,
|
||||
@@ -505,6 +540,7 @@ const SortableImage = ({
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...(allowReorder ? { ...attributes, ...listeners } : {})}
|
||||
onClick={() => onPreview?.(image)}
|
||||
className={twMerge(
|
||||
"group glass relative aspect-square w-10 sm:w-14 overflow-hidden rounded-lg border-2 border-white/30 transition-opacity",
|
||||
allowReorder
|
||||
@@ -546,7 +582,7 @@ const UploadingThumbnail = ({ file }: { file: File }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
|
||||
<FontAwesomeIcon icon={faSpinner} spin className="h-6 w-6 text-white" />
|
||||
<FontAwesomeIcon icon={faSpinnerThird} spin className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export interface RefImage {
|
||||
id: string;
|
||||
url: string;
|
||||
file: File;
|
||||
mediaToken: string;
|
||||
fullUrl?: string;
|
||||
file?: File;
|
||||
mediaToken?: string;
|
||||
isCustom?: boolean;
|
||||
}
|
||||
|
||||
export interface RefVideo {
|
||||
|
||||
@@ -358,8 +358,6 @@ function PasswordForm({ user, isEditing, onOpen, onClose }: SectionFormProps) {
|
||||
title="Password"
|
||||
collapsedDescription="Used to sign in to your account."
|
||||
editingDescription={`Choose a new password of at least ${PASSWORD_MIN} characters.`}
|
||||
currentLabel="Current"
|
||||
currentValue={"••••••••"}
|
||||
isEditing={isEditing}
|
||||
onOpen={handleOpen}
|
||||
>
|
||||
@@ -417,8 +415,8 @@ function SectionShell({
|
||||
title: string;
|
||||
collapsedDescription: string;
|
||||
editingDescription: string;
|
||||
currentLabel: string;
|
||||
currentValue: string;
|
||||
currentLabel?: string;
|
||||
currentValue?: string;
|
||||
isEditing: boolean;
|
||||
onOpen: () => void;
|
||||
children: React.ReactNode;
|
||||
@@ -444,7 +442,9 @@ function SectionShell({
|
||||
)}
|
||||
</div>
|
||||
{!isEditing ? (
|
||||
<CurrentValue label={currentLabel} value={currentValue} />
|
||||
currentLabel && currentValue ? (
|
||||
<CurrentValue label={currentLabel} value={currentValue} />
|
||||
) : null
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
|
||||
@@ -342,6 +342,13 @@ export function TopBar() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="hidden lg:flex h-8 items-center gap-1.5 px-3 rounded-lg text-sm font-medium text-white/85 hover:bg-white/[0.04] transition-colors"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGem} className="text-[11px]" />
|
||||
Pricing
|
||||
</Link>
|
||||
<Link
|
||||
to="/login"
|
||||
className="h-8 flex items-center px-3 rounded-lg text-sm font-medium text-white/80 hover:text-white hover:bg-white/[0.04] transition-all"
|
||||
|
||||
@@ -19,7 +19,7 @@ if (import.meta.env.DEV) {
|
||||
window.location.origin,
|
||||
);
|
||||
// NB: This is for Brandon to test with storyteller-web locally:
|
||||
StorytellerApiHostStore.getInstance().setDevelopment();
|
||||
//StorytellerApiHostStore.getInstance().setDevelopment();
|
||||
} catch (e) {
|
||||
console.warn("Failed to set dev API host override", e);
|
||||
}
|
||||
|
||||
@@ -2,64 +2,11 @@ import { faXmarkCircle } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import { SOCIAL_LINKS } from "../../config/links";
|
||||
import Seo from "../../components/seo";
|
||||
import { useEffect, useState } from "react";
|
||||
import { UsersApi } from "@storyteller/api";
|
||||
|
||||
const CheckoutCancel = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const skipOnboarding = searchParams.get("skip_onboarding") === "true";
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkOnboarding = async () => {
|
||||
// Skip onboarding check if we already went through it
|
||||
if (skipOnboarding) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const usersApi = new UsersApi();
|
||||
const sessionResponse = await usersApi.GetSession();
|
||||
|
||||
if (sessionResponse.success && sessionResponse.data?.loggedIn) {
|
||||
const onboarding = sessionResponse.data.onboarding;
|
||||
|
||||
// If onboarding is undefined or has any required fields not set, redirect to onboarding
|
||||
if (
|
||||
!onboarding ||
|
||||
onboarding.password_not_set ||
|
||||
onboarding.email_not_set ||
|
||||
onboarding.email_not_confirmed
|
||||
) {
|
||||
// Redirect to onboarding - it will add skip_onboarding=true after username step
|
||||
window.location.href =
|
||||
"/onboarding?redirect_to=" +
|
||||
encodeURIComponent("/checkout/cancel");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking onboarding status:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkOnboarding();
|
||||
}, [skipOnboarding]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="relative min-h-screen bg-[#101014] text-white flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden bg-[#101014] text-white">
|
||||
<Seo
|
||||
@@ -72,7 +19,7 @@ const CheckoutCancel = () => {
|
||||
<div className="w-[900px] h-[900px] rounded-full bg-gradient-to-br from-gray-500/20 via-gray-600/10 to-gray-700/5 opacity-40 blur-[120px]"></div>
|
||||
</div>
|
||||
|
||||
<main className="relative z-10 pt-28 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
<main className="relative z-10 pt-20 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
{/* Cancel Card */}
|
||||
<div className="max-w-lg w-full">
|
||||
<div className="bg-[#1A1A1E] border border-white/10 rounded-3xl p-8 md:p-12 text-center">
|
||||
|
||||
@@ -3,6 +3,11 @@ import {
|
||||
faDownload,
|
||||
faDesktop,
|
||||
faRocket,
|
||||
faImage,
|
||||
faVideo,
|
||||
faWandMagicSparkles,
|
||||
faCube,
|
||||
faArrowRight,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -72,20 +77,21 @@ const CheckoutSuccess = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden bg-[#101014] text-white">
|
||||
<div className="relative min-h-full bg-[#101014] text-white">
|
||||
<Seo
|
||||
title="Payment Successful - ArtCraft"
|
||||
description="Your ArtCraft subscription is now active. Download and start creating!"
|
||||
/>
|
||||
|
||||
{/* Background gradient - celebratory green/purple */}
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none z-0">
|
||||
{/* Background gradient — clipped to its own wrapper so the outer
|
||||
page can grow naturally and let SidebarInset handle scrolling. */}
|
||||
<div className="absolute inset-0 overflow-hidden flex items-center justify-center pointer-events-none z-0">
|
||||
<div className="w-[900px] h-[900px] rounded-full bg-gradient-to-br from-green-500/40 via-primary/30 to-purple-600/20 opacity-40 blur-[120px]"></div>
|
||||
</div>
|
||||
|
||||
<main className="relative z-10 pt-28 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
<main className="relative z-10 pt-12 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
{/* Success Card */}
|
||||
<div className="max-w-2xl w-full">
|
||||
<div className="max-w-4xl w-full">
|
||||
<div className="bg-[#1A1A1E] border border-white/10 rounded-3xl p-8 md:p-12 text-center">
|
||||
{/* Success Icon */}
|
||||
<div className="mb-6">
|
||||
@@ -107,6 +113,54 @@ const CheckoutSuccess = () => {
|
||||
create amazing AI-generated art!
|
||||
</p>
|
||||
|
||||
{/* Start creating in the browser */}
|
||||
<div className="bg-[#252529] rounded-2xl p-6 mb-8 text-left">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faWandMagicSparkles}
|
||||
className="text-primary text-sm"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-lg font-medium text-white">
|
||||
Start creating right here
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-white/60 text-sm mb-4">
|
||||
No download needed - jump straight in from your browser.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ to: "/create-image", icon: faImage, label: "Image" },
|
||||
{ to: "/create-video", icon: faVideo, label: "Video" },
|
||||
{
|
||||
to: "/background-change",
|
||||
icon: faWandMagicSparkles,
|
||||
label: "BG Change",
|
||||
},
|
||||
{ to: "/edit-3d", icon: faCube, label: "Edit 3D" },
|
||||
].map((item) => (
|
||||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className="group flex items-center gap-3 bg-[#1A1A1E] hover:bg-[#222227] border border-white/[0.06] hover:border-white/[0.12] rounded-xl px-4 py-3 transition-all"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={item.icon}
|
||||
className="text-primary text-base shrink-0"
|
||||
/>
|
||||
<span className="text-white/90 font-medium flex-1">
|
||||
{item.label}
|
||||
</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="text-white/30 text-[11px] transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Next Steps */}
|
||||
<div className="bg-[#252529] rounded-2xl p-6 mb-8 text-left">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
|
||||
@@ -268,6 +268,7 @@ export default function CreateImage() {
|
||||
.map((item) => ({
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: item.thumbnail || item.fullImage || "",
|
||||
fullUrl: item.fullImage || undefined,
|
||||
file: new File([], "library-image"),
|
||||
mediaToken: item.id,
|
||||
}));
|
||||
@@ -295,7 +296,7 @@ export default function CreateImage() {
|
||||
const imageMediaTokens = selectedModel.image_refs_supported
|
||||
? referenceImages
|
||||
.map((img) => img.mediaToken)
|
||||
.filter((t) => t.length > 0)
|
||||
.filter((t): t is string => typeof t === "string" && t.length > 0)
|
||||
: undefined;
|
||||
|
||||
const result = await enqueueImageGeneration({
|
||||
@@ -415,6 +416,7 @@ export default function CreateImage() {
|
||||
onSelect={handleModelChange}
|
||||
mode="toggle"
|
||||
panelTitle="Select Model"
|
||||
panelClassName="w-max"
|
||||
showIconsInList
|
||||
triggerIcon={
|
||||
<img
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faClock,
|
||||
faWaveformLines,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faClock, faWaveformLines } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { CharactersApi, FilterMediaClasses } from "@storyteller/api";
|
||||
import type { OmniGenVideoModelInfo } from "@storyteller/api";
|
||||
import { ToggleButton } from "@storyteller/ui-button";
|
||||
@@ -97,6 +94,13 @@ const LABEL_TO_RES: Record<string, string> = Object.fromEntries(
|
||||
|
||||
// ── Model lookup ─────────────────────────────────────────────────────────
|
||||
|
||||
// Placeholder entries shown in the picker as disabled "SOON" items. These are
|
||||
// UI-only — they are not in `_modelLookup`, so the click handler is a no-op
|
||||
// (the `disabled` flag also prevents the row from firing).
|
||||
const COMING_SOON_MODELS: ReadonlyArray<{ id: string; label: string }> = [
|
||||
{ id: "seedance_2p1", label: "Seedance 2.1" },
|
||||
];
|
||||
|
||||
let _modelLookup = new Map<string, OmniGenVideoModelInfo>();
|
||||
|
||||
function buildModelPopoverItems(
|
||||
@@ -104,7 +108,7 @@ function buildModelPopoverItems(
|
||||
selectedId: string,
|
||||
): PopoverItem[] {
|
||||
_modelLookup = new Map(models.map((m) => [m.model, m]));
|
||||
return models.map((model) => ({
|
||||
const apiItems: PopoverItem[] = models.map((model) => ({
|
||||
label: model.full_name || model.model,
|
||||
selected: model.model === selectedId,
|
||||
icon: (
|
||||
@@ -116,6 +120,27 @@ function buildModelPopoverItems(
|
||||
),
|
||||
action: model.model,
|
||||
}));
|
||||
const comingSoonItems: PopoverItem[] = COMING_SOON_MODELS.map(
|
||||
({ id, label }) => ({
|
||||
label,
|
||||
selected: false,
|
||||
disabled: true,
|
||||
className: "!ml-0",
|
||||
icon: (
|
||||
<img
|
||||
src={getModelCreatorIconPath(id)}
|
||||
alt={`${id} logo`}
|
||||
className="h-4 w-4 icon-auto-contrast"
|
||||
/>
|
||||
),
|
||||
trailing: (
|
||||
<span className="-ml-3.5 shrink-0 rounded-full bg-white/20 px-2 py-0 text-[10px] font-bold uppercase tracking-wider text-white">
|
||||
Soon
|
||||
</span>
|
||||
),
|
||||
}),
|
||||
);
|
||||
return [...apiItems, ...comingSoonItems];
|
||||
}
|
||||
|
||||
function buildSizePopoverItems(
|
||||
@@ -683,6 +708,7 @@ export default function CreateVideo() {
|
||||
.map((item) => ({
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: item.thumbnail || item.fullImage || "",
|
||||
fullUrl: item.fullImage || undefined,
|
||||
file: new File([], "library-image"),
|
||||
mediaToken: item.id,
|
||||
}));
|
||||
@@ -698,6 +724,7 @@ export default function CreateVideo() {
|
||||
setEndFrameImage({
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: item.thumbnail || item.fullImage || "",
|
||||
fullUrl: item.fullImage || undefined,
|
||||
file: new File([], "library-image"),
|
||||
mediaToken: item.id,
|
||||
});
|
||||
@@ -744,15 +771,19 @@ export default function CreateVideo() {
|
||||
isReferenceMode && referenceImages.length > 0
|
||||
? referenceImages
|
||||
.map((img) => img.mediaToken)
|
||||
.filter((t) => t.length > 0)
|
||||
.filter((t): t is string => typeof t === "string" && t.length > 0)
|
||||
: undefined;
|
||||
const referenceVideoTokens =
|
||||
isReferenceMode && referenceVideos.length > 0
|
||||
? referenceVideos.map((v) => v.mediaToken).filter((t) => t.length > 0)
|
||||
? referenceVideos
|
||||
.map((v) => v.mediaToken)
|
||||
.filter((t): t is string => typeof t === "string" && t.length > 0)
|
||||
: undefined;
|
||||
const referenceAudioTokens =
|
||||
isReferenceMode && referenceAudios.length > 0
|
||||
? referenceAudios.map((a) => a.mediaToken).filter((t) => t.length > 0)
|
||||
? referenceAudios
|
||||
.map((a) => a.mediaToken)
|
||||
.filter((t): t is string => typeof t === "string" && t.length > 0)
|
||||
: undefined;
|
||||
|
||||
// Extract character tokens from @-mentions in the prompt. Match longest
|
||||
@@ -807,7 +838,11 @@ export default function CreateVideo() {
|
||||
console.log("[generate-video] params", baseParams);
|
||||
|
||||
const modelLabel = selectedModel.full_name ?? selectedModel.model;
|
||||
const batchId = startBatch(prompt, modelLabel, numVideos > 1 ? numVideos : undefined);
|
||||
const batchId = startBatch(
|
||||
prompt,
|
||||
modelLabel,
|
||||
numVideos > 1 ? numVideos : undefined,
|
||||
);
|
||||
|
||||
try {
|
||||
console.log("[generate-video] enqueueing job...");
|
||||
@@ -973,6 +1008,7 @@ export default function CreateVideo() {
|
||||
onSelect={handleModelChange}
|
||||
mode="toggle"
|
||||
panelTitle="Select Model"
|
||||
panelClassName="w-max"
|
||||
showIconsInList
|
||||
triggerIcon={
|
||||
<img
|
||||
|
||||
@@ -307,7 +307,7 @@ const Onboarding = () => {
|
||||
<div className="w-[900px] h-[900px] rounded-full bg-gradient-to-br from-green-500/40 via-primary/30 to-purple-600/20 opacity-40 blur-[120px]"></div>
|
||||
</div>
|
||||
|
||||
<main className="relative z-10 pt-28 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
<main className="relative z-10 pt-20 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
<div className="max-w-md w-full text-center">
|
||||
<div className="mb-6">
|
||||
<div className="w-20 h-20 mx-auto rounded-full bg-green-500/20 flex items-center justify-center">
|
||||
@@ -338,7 +338,7 @@ const Onboarding = () => {
|
||||
<div className="w-[900px] h-[900px] rounded-full bg-gradient-to-br from-blue-700 via-blue-500 to-[#00AABA] opacity-20 blur-[120px]"></div>
|
||||
</div>
|
||||
|
||||
<main className="relative z-10 pt-28 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
<main className="relative z-10 pt-20 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
<div className="max-w-md w-full">
|
||||
{isNewAccount && (
|
||||
<div className="bg-blue-500/10 border border-blue-500/30 rounded-xl p-4 mb-6 flex items-start gap-3 animate-fade-in shadow-lg shadow-blue-900/10">
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCompress,
|
||||
faExpand,
|
||||
faImage,
|
||||
faVideo,
|
||||
faWandMagicSparkles,
|
||||
faXmark,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
@@ -11,96 +12,86 @@ import { MediaFilesApi, PromptsApi } from "@storyteller/api";
|
||||
import { addCorsParam, PLACEHOLDER_IMAGES } from "@storyteller/common";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
import { Viewer3D } from "@storyteller/ui-viewer-3d";
|
||||
import { is3DModelUrl, isVideoUrl } from "../../components/lightbox/shared";
|
||||
import { is3DModelUrl } from "../../components/lightbox/shared";
|
||||
|
||||
// Demo overlay rendered on top-right of the 3D editor when the URL carries
|
||||
// `?output=<media_token>` (alias: `?demo=<media_token>`). It resolves the
|
||||
// token to a media URL and renders the asset (image / video / 3D model)
|
||||
// in a 16:9 picture-in-picture card so the scene and its rendered output
|
||||
// can be shown side by side.
|
||||
// `?image=<media_token>` (or the legacy `?output=` / `?demo=` aliases) and
|
||||
// optionally `?video=<media_token>`. Each token is resolved independently
|
||||
// to a CDN URL and rendered in a 16:9 picture-in-picture card so the scene
|
||||
// and its rendered output can be shown side by side.
|
||||
//
|
||||
// UX details aimed at making the relationship obvious at a glance:
|
||||
// - "Rendered Output" header with a wand-sparkles icon and a
|
||||
// "Generated from this scene" subtitle so first-time viewers
|
||||
// immediately understand the card is the AI render of the scene
|
||||
// they're looking at.
|
||||
// - Slide-in entrance animation draws the eye to the corner on load
|
||||
// so the card isn't missed.
|
||||
// - The title-bar expand button promotes the card to a centered
|
||||
// larger view over the editor; clicking again (or the backdrop /
|
||||
// Esc) collapses it back to the corner.
|
||||
// UX details:
|
||||
// - "Rendered Output" header with a wand-sparkles icon and a "Generated
|
||||
// from this scene" subtitle so first-time viewers immediately understand
|
||||
// the card is the AI render of the scene they're looking at.
|
||||
// - Slide-in entrance animation draws the eye to the corner on load.
|
||||
// - The title-bar expand button promotes the card to a centered larger
|
||||
// view over the editor; clicking again (or backdrop / Esc) collapses it.
|
||||
// - When both an image and a video token are present, a small segmented
|
||||
// toggle appears in the header. Video shows first (the toggle defaults
|
||||
// to the video side); flipping the toggle swaps the rendered media
|
||||
// *and* the prompt caption shown below it.
|
||||
//
|
||||
// The component is fully self-contained: it fetches its own media and
|
||||
// renders nothing while the token is unresolved or invalid.
|
||||
// The component is fully self-contained: it fetches both assets and renders
|
||||
// nothing while everything is unresolved or invalid.
|
||||
|
||||
interface DemoOutputOverlayProps {
|
||||
outputToken: string;
|
||||
imageToken: string | null;
|
||||
videoToken: string | null;
|
||||
}
|
||||
|
||||
interface OverlayMedia {
|
||||
interface ResolvedAsset {
|
||||
url: string;
|
||||
isVideo: boolean;
|
||||
is3D: boolean;
|
||||
promptText: string | null;
|
||||
}
|
||||
|
||||
export function DemoOutputOverlay({ outputToken }: DemoOutputOverlayProps) {
|
||||
const [media, setMedia] = useState<OverlayMedia | null>(null);
|
||||
const [promptText, setPromptText] = useState<string | null>(null);
|
||||
type View = "video" | "image";
|
||||
|
||||
export function DemoOutputOverlay({
|
||||
imageToken,
|
||||
videoToken,
|
||||
}: DemoOutputOverlayProps) {
|
||||
const [image, setImage] = useState<ResolvedAsset | null>(null);
|
||||
const [video, setVideo] = useState<ResolvedAsset | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
// Video shows first when both tokens are provided. When only one token
|
||||
// is present, the effective view derivation below pins to that side
|
||||
// regardless of this value, so the initial pick only matters for the
|
||||
// both-present case.
|
||||
const [view, setView] = useState<View>(videoToken ? "video" : "image");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
setMedia(null);
|
||||
setPromptText(null);
|
||||
setImage(null);
|
||||
setVideo(null);
|
||||
setView(videoToken ? "video" : "image");
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const mediaResponse = await new MediaFilesApi().GetMediaFileByToken({
|
||||
mediaFileToken: outputToken,
|
||||
});
|
||||
if (cancelled) return;
|
||||
|
||||
const file = mediaResponse?.data;
|
||||
const url = file?.media_links?.cdn_url || null;
|
||||
if (!mediaResponse?.success || !file || !url) {
|
||||
setErrorMessage("Rendered output not found");
|
||||
return;
|
||||
}
|
||||
setMedia({
|
||||
url,
|
||||
isVideo: isVideoUrl(url),
|
||||
is3D: is3DModelUrl(url),
|
||||
});
|
||||
|
||||
// The prompt fetch is best-effort and decorative; failures or a
|
||||
// missing prompt_token should leave the card without a caption,
|
||||
// not surface an error to the user.
|
||||
if (file.maybe_prompt_token) {
|
||||
const promptResponse = await new PromptsApi().GetPromptsByToken({
|
||||
token: file.maybe_prompt_token,
|
||||
});
|
||||
if (cancelled) return;
|
||||
const text = promptResponse?.success
|
||||
? promptResponse.data?.maybe_positive_prompt || null
|
||||
: null;
|
||||
setPromptText(text);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setErrorMessage("Failed to load rendered output");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
const [imageResult, videoResult] = await Promise.all([
|
||||
imageToken ? resolveAsset(imageToken) : Promise.resolve(null),
|
||||
videoToken ? resolveAsset(videoToken) : Promise.resolve(null),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
setImage(imageResult);
|
||||
setVideo(videoResult);
|
||||
const anyRequested = !!(imageToken || videoToken);
|
||||
const anyResolved = !!(imageResult || videoResult);
|
||||
if (anyRequested && !anyResolved) {
|
||||
setErrorMessage("Rendered output not found");
|
||||
}
|
||||
setLoading(false);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [outputToken]);
|
||||
}, [imageToken, videoToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpanded) return;
|
||||
@@ -111,6 +102,22 @@ export function DemoOutputOverlay({ outputToken }: DemoOutputOverlayProps) {
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [isExpanded]);
|
||||
|
||||
const hasImage = !!image;
|
||||
const hasVideo = !!video;
|
||||
// The selected `view` reflects user intent, but render against whichever
|
||||
// side actually resolved — if a fetch fails, we silently fall back to
|
||||
// the other side rather than showing an empty card with a dead toggle.
|
||||
const effectiveView: View | null = useMemo(() => {
|
||||
if (view === "video" && hasVideo) return "video";
|
||||
if (view === "image" && hasImage) return "image";
|
||||
if (hasVideo) return "video";
|
||||
if (hasImage) return "image";
|
||||
return null;
|
||||
}, [view, hasImage, hasVideo]);
|
||||
const activeAsset = effectiveView === "video" ? video : image;
|
||||
const captionText = activeAsset?.promptText ?? null;
|
||||
const showToggle = hasImage && hasVideo;
|
||||
|
||||
if (isHidden) {
|
||||
return <ShowOutputPill onClick={() => setIsHidden(false)} />;
|
||||
}
|
||||
@@ -130,7 +137,13 @@ export function DemoOutputOverlay({ outputToken }: DemoOutputOverlayProps) {
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpanded={() => setIsExpanded((v) => !v)}
|
||||
onHide={handleStepDown}
|
||||
promptText={promptText}
|
||||
captionText={captionText}
|
||||
toggle={
|
||||
showToggle && effectiveView ? (
|
||||
<ViewToggle view={effectiveView} onChange={setView} />
|
||||
) : null
|
||||
}
|
||||
activeIsVideo={effectiveView === "video"}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
@@ -140,8 +153,8 @@ export function DemoOutputOverlay({ outputToken }: DemoOutputOverlayProps) {
|
||||
<div className="absolute inset-0 flex items-center justify-center px-3 text-center text-xs text-white/60">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : media ? (
|
||||
<OverlayMediaView media={media} />
|
||||
) : activeAsset && effectiveView ? (
|
||||
<OverlayMediaView asset={activeAsset} view={effectiveView} />
|
||||
) : null}
|
||||
</Card>
|
||||
);
|
||||
@@ -167,6 +180,36 @@ export function DemoOutputOverlay({ outputToken }: DemoOutputOverlayProps) {
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveAsset(token: string): Promise<ResolvedAsset | null> {
|
||||
try {
|
||||
const mediaResponse = await new MediaFilesApi().GetMediaFileByToken({
|
||||
mediaFileToken: token,
|
||||
});
|
||||
const file = mediaResponse?.data;
|
||||
const url = file?.media_links?.cdn_url;
|
||||
if (!mediaResponse?.success || !file || !url) return null;
|
||||
// Prompt fetch is best-effort and decorative; a missing or failed
|
||||
// prompt token leaves the asset without a caption rather than
|
||||
// failing the whole resolve.
|
||||
let promptText: string | null = null;
|
||||
if (file.maybe_prompt_token) {
|
||||
try {
|
||||
const promptResponse = await new PromptsApi().GetPromptsByToken({
|
||||
token: file.maybe_prompt_token,
|
||||
});
|
||||
promptText = promptResponse?.success
|
||||
? promptResponse.data?.maybe_positive_prompt || null
|
||||
: null;
|
||||
} catch {
|
||||
// leave promptText null
|
||||
}
|
||||
}
|
||||
return { url, is3D: is3DModelUrl(url), promptText };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function ShowOutputPill({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
@@ -181,11 +224,62 @@ function ShowOutputPill({ onClick }: { onClick: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface ViewToggleProps {
|
||||
view: View;
|
||||
onChange: (next: View) => void;
|
||||
}
|
||||
|
||||
// Single-button sliding toggle. The whole pill is the click target —
|
||||
// clicking anywhere on it flips between video and image. A primary-
|
||||
// coloured indicator translates between the two icons to show which
|
||||
// side is currently active.
|
||||
function ViewToggle({ view, onChange }: ViewToggleProps) {
|
||||
const next: View = view === "video" ? "image" : "video";
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={view === "video" ? "Switch to image" : "Switch to video"}
|
||||
onClick={() => onChange(next)}
|
||||
className="relative flex h-7 w-14 shrink-0 items-center rounded-full border border-ui-controls-border bg-black/30 p-0.5 transition-colors hover:bg-black/40"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute top-0.5 bottom-0.5 left-0.5 w-[calc(50%-0.125rem)] rounded-full bg-primary shadow-sm transition-transform duration-200 ease-out"
|
||||
style={{
|
||||
transform: view === "image" ? "translateX(100%)" : "translateX(0%)",
|
||||
}}
|
||||
/>
|
||||
<ToggleIcon icon={faVideo} active={view === "video"} />
|
||||
<ToggleIcon icon={faImage} active={view === "image"} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToggleIconProps {
|
||||
icon: typeof faVideo;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
function ToggleIcon({ icon, active }: ToggleIconProps) {
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
"relative z-10 flex h-full flex-1 items-center justify-center transition-colors " +
|
||||
(active ? "text-white" : "text-base-fg/60")
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} className="h-3 w-3" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
interface CardProps {
|
||||
isExpanded: boolean;
|
||||
onToggleExpanded: () => void;
|
||||
onHide: () => void;
|
||||
promptText: string | null;
|
||||
captionText: string | null;
|
||||
toggle: React.ReactNode;
|
||||
activeIsVideo: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -193,9 +287,22 @@ function Card({
|
||||
isExpanded,
|
||||
onToggleExpanded,
|
||||
onHide,
|
||||
promptText,
|
||||
captionText,
|
||||
toggle,
|
||||
activeIsVideo,
|
||||
children,
|
||||
}: CardProps) {
|
||||
// When the video element is active, clicks on the native control bar
|
||||
// must not bubble to the wrapper's expand handler — otherwise pressing
|
||||
// play would also fullscreen the card. Suppress the wrapper handler
|
||||
// for any click whose target lives inside a <video> element.
|
||||
const handleMediaClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (activeIsVideo) {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest("video")) return;
|
||||
}
|
||||
onToggleExpanded();
|
||||
};
|
||||
return (
|
||||
<div className="glass pointer-events-auto overflow-hidden rounded-xl shadow-xl border-2 border-primary">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-ui-controls-border/60 px-3 py-2">
|
||||
@@ -213,22 +320,27 @@ function Card({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onHide}
|
||||
aria-label={
|
||||
isExpanded ? "Collapse to picture-in-picture" : "Hide rendered output"
|
||||
}
|
||||
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-base-fg/60 transition-colors hover:bg-ui-controls hover:text-base-fg"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{toggle}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onHide}
|
||||
aria-label={
|
||||
isExpanded
|
||||
? "Collapse to picture-in-picture"
|
||||
: "Hide rendered output"
|
||||
}
|
||||
className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-base-fg/60 transition-colors hover:bg-ui-controls hover:text-base-fg"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={isExpanded ? "Collapse output view" : "Expand output view"}
|
||||
onClick={onToggleExpanded}
|
||||
onClick={handleMediaClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
@@ -254,10 +366,10 @@ function Card({
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{promptText && (
|
||||
{captionText && (
|
||||
<div
|
||||
className="border-t border-ui-controls-border/60 px-3 py-2 text-[11px] leading-[15px] text-base-fg/70"
|
||||
title={promptText}
|
||||
title={captionText}
|
||||
style={{
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 2,
|
||||
@@ -266,19 +378,24 @@ function Card({
|
||||
maxHeight: "47px",
|
||||
}}
|
||||
>
|
||||
{promptText}
|
||||
{captionText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OverlayMediaView({ media }: { media: OverlayMedia }) {
|
||||
const src = addCorsParam(media.url) || media.url;
|
||||
if (media.is3D) {
|
||||
interface OverlayMediaViewProps {
|
||||
asset: ResolvedAsset;
|
||||
view: View;
|
||||
}
|
||||
|
||||
function OverlayMediaView({ asset, view }: OverlayMediaViewProps) {
|
||||
const src = addCorsParam(asset.url) || asset.url;
|
||||
if (asset.is3D) {
|
||||
return <Viewer3D modelUrl={src} isActive className="h-full w-full" />;
|
||||
}
|
||||
if (media.isVideo) {
|
||||
if (view === "video") {
|
||||
return (
|
||||
<video
|
||||
src={src}
|
||||
@@ -287,6 +404,7 @@ function OverlayMediaView({ media }: { media: OverlayMedia }) {
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
controls
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCubes,
|
||||
faLayerGroup,
|
||||
faXmark,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { SceneSplashCard } from "./splash/SceneSplashCard";
|
||||
import { EXAMPLE_SCENES } from "./splash/example-scenes";
|
||||
|
||||
// Demo-mode affordance fed into Stage3D's `promptboxAboveStackSlot`
|
||||
// (lib slot rendered just above the promptbox stack). Collapsed: a
|
||||
// pill that fades in below the model. Expanded: an inline panel that
|
||||
// grows upward with the same horizontal lane as the promptbox, listing
|
||||
// the curated example scenes minus the one the user is on. Clicking a
|
||||
// card navigates to `/edit-3d/${sceneToken}?image=${outputToken}` —
|
||||
// same shape as the splash modal's pickExample.
|
||||
//
|
||||
// Rendering through the lib's slot (instead of as a separately
|
||||
// absolutely-positioned overlay) means the lib's `bottom-4` anchor
|
||||
// owns the vertical math: the slot is part of the same `flex flex-col
|
||||
// gap-3` stack as the prompt card and toolbar, so it never overlaps
|
||||
// the prompt input regardless of how tall the textarea has grown. The
|
||||
// host doesn't need to measure anything.
|
||||
|
||||
interface OtherScenesOverlayProps {
|
||||
currentSceneToken: string | undefined;
|
||||
demoOutputToken: string | null;
|
||||
}
|
||||
|
||||
export function OtherScenesOverlay({
|
||||
currentSceneToken,
|
||||
demoOutputToken,
|
||||
}: OtherScenesOverlayProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const otherScenes = useMemo(
|
||||
() => EXAMPLE_SCENES.filter((s) => s.sceneToken !== currentSceneToken),
|
||||
[currentSceneToken],
|
||||
);
|
||||
|
||||
if (!demoOutputToken || otherScenes.length === 0) return null;
|
||||
|
||||
const handlePick = (sceneToken: string, outputToken: string) => {
|
||||
setIsOpen(false);
|
||||
navigate(`/edit-3d/${sceneToken}?image=${outputToken}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
{isOpen ? (
|
||||
<ExpandedPanel
|
||||
scenes={otherScenes}
|
||||
onClose={() => setIsOpen(false)}
|
||||
onPick={handlePick}
|
||||
/>
|
||||
) : (
|
||||
<CollapsedPill onClick={() => setIsOpen(true)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsedPill({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex items-center gap-2.5 rounded-full border border-primary/40 bg-gradient-to-r from-primary/90 to-primary px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:from-primary hover:to-primary animate-in fade-in slide-in-from-bottom-2 duration-300"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCubes} className="h-3.5 w-3.5" />
|
||||
<span>See other demo scenes</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExpandedPanelProps {
|
||||
scenes: readonly (typeof EXAMPLE_SCENES)[number][];
|
||||
onClose: () => void;
|
||||
onPick: (sceneToken: string, outputToken: string) => void;
|
||||
}
|
||||
|
||||
function ExpandedPanel({ scenes, onClose, onPick }: ExpandedPanelProps) {
|
||||
return (
|
||||
<div className="glass w-[860px] max-w-[90vw] overflow-hidden rounded-xl border border-ui-controls-border shadow-2xl animate-in fade-in slide-in-from-bottom-2 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-ui-controls-border/60 px-4 py-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faLayerGroup}
|
||||
className="h-3 w-3 text-primary"
|
||||
/>
|
||||
<div className="text-xs font-semibold uppercase tracking-wider text-base-fg">
|
||||
Other demo scenes
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close other scenes"
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-base-fg/60 transition-colors hover:bg-ui-controls hover:text-base-fg"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 p-3 sm:grid-cols-3">
|
||||
{scenes.map((scene) => (
|
||||
<SceneSplashCard
|
||||
key={scene.id}
|
||||
variant="example"
|
||||
title={scene.title}
|
||||
description={scene.description}
|
||||
accentClass={scene.accentClass}
|
||||
outputToken={scene.outputToken}
|
||||
onClick={() => onPick(scene.sceneToken, scene.outputToken)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { useSidebar } from "../../components/ui/sidebar";
|
||||
import { useSignupCta } from "../../components/signup-cta-modal";
|
||||
import Seo from "../../components/seo";
|
||||
import { DemoOutputOverlay } from "./demo-output-overlay";
|
||||
import { OtherScenesOverlay } from "./other-scenes-overlay";
|
||||
import { usePromptPrefillFromOutput } from "./use-prompt-prefill-from-output";
|
||||
import { useSceneCacheStore } from "./scene-cache-store";
|
||||
import { useWebAppPageSceneAdapter } from "./web-adapter";
|
||||
@@ -30,6 +31,30 @@ import {
|
||||
useSceneSplashStore,
|
||||
} from "./splash";
|
||||
|
||||
// Mirror of the lib's private `DEFAULT_CAMERAS` in PageSceneStore.ts.
|
||||
// Re-applied to the store on every scene navigation so a stale
|
||||
// `cameras` array (from the previous scene) can't drive the new
|
||||
// viewport's FOV when the loaded scene's JSON omits camera data — see
|
||||
// the camera-reset effect below for the full mechanism.
|
||||
const DEFAULT_CAMERAS = [
|
||||
{
|
||||
id: "main",
|
||||
label: "Main View",
|
||||
focalLength: 17,
|
||||
position: { x: -2.5, y: 2.5, z: 2.5 },
|
||||
rotation: { x: 0, y: 0, z: 0 },
|
||||
lookAt: { x: 0, y: 0, z: 0 },
|
||||
},
|
||||
{
|
||||
id: "cam2",
|
||||
label: "Camera 2",
|
||||
focalLength: 10,
|
||||
position: { x: 0, y: 0.6, z: 1.5 },
|
||||
rotation: { x: 0, y: 0, z: 0 },
|
||||
lookAt: { x: 0, y: 0, z: 0 },
|
||||
},
|
||||
];
|
||||
|
||||
// Stage3D fills its parent box; this wrapper is that box. It clamps
|
||||
// the editor to the SidebarInset area and feeds the lib its rect:
|
||||
//
|
||||
@@ -69,11 +94,20 @@ export default function PageScene() {
|
||||
function PageSceneEditor() {
|
||||
const { sceneToken } = useParams<{ sceneToken?: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
// `?output=<media_token>` (alias `?demo=`) flips the editor into demo
|
||||
// mode: the named output media renders in a top-right picture-in-picture
|
||||
// card alongside the live scene. Empty string is normalized to null.
|
||||
const demoOutputToken =
|
||||
searchParams.get("output") || searchParams.get("demo") || null;
|
||||
// `?image=`, `?video=`, and the legacy `?output=` / `?demo=` aliases
|
||||
// flip the editor into demo mode: the named media renders in a
|
||||
// top-right picture-in-picture card alongside the live scene. `image`
|
||||
// and `output`/`demo` are equivalent — they name the image side of
|
||||
// the preview (the legacy aliases keep existing shared links working).
|
||||
// `video` names a video token shown on the video side. When both sides
|
||||
// are present, the card adds a toggle and starts on video.
|
||||
const imageToken =
|
||||
searchParams.get("image") ||
|
||||
searchParams.get("output") ||
|
||||
searchParams.get("demo") ||
|
||||
null;
|
||||
const videoToken = searchParams.get("video") || null;
|
||||
const demoToken = imageToken || videoToken;
|
||||
const { user } = useSession();
|
||||
const navigate = useNavigate();
|
||||
const { setOpen } = useSidebar();
|
||||
@@ -117,6 +151,26 @@ function PageSceneEditor() {
|
||||
}
|
||||
}, [sceneToken]);
|
||||
|
||||
// Reset the lib's camera Zustand state on every scene navigation.
|
||||
// The lib's camera tickPerFrame reads `cameras` + `selectedCameraId`
|
||||
// from this module-global store every frame and applies the matched
|
||||
// entry's `focalLength` to the viewport's THREE.PerspectiveCamera
|
||||
// (CameraController.ts). save_manager only emits CamerasReplacedEvent
|
||||
// when the loaded scene_json includes a `cameras` field — older
|
||||
// saves and some demo scenes don't, so without this reset an A→B
|
||||
// handoff leaves scene A's `cameras` array (and its 60mm focal
|
||||
// length) in the store and the new viewport renders at A's FOV
|
||||
// forever. Re-seeding with the same defaults the store ships with
|
||||
// mirrors the "fresh editor" state; if B's JSON has cameras, the
|
||||
// load path replaces these immediately.
|
||||
useEffect(() => {
|
||||
usePageSceneStore.setState({
|
||||
cameras: DEFAULT_CAMERAS,
|
||||
selectedCameraId: "main",
|
||||
focalLengthDragging: { isDragging: false, focalLength: 35 },
|
||||
});
|
||||
}, [sceneToken]);
|
||||
|
||||
// Mirror the URL-token scene's metadata into the lib store. Cache-hit
|
||||
// loads short-circuit the lib's loadScene path (Editor.initialize
|
||||
// prefers cacheJsonString over the sceneToken), so loadSceneViaApi —
|
||||
@@ -165,7 +219,7 @@ function PageSceneEditor() {
|
||||
// `?output=`, Stage3DBody's cold-sync handles the default letterbox by
|
||||
// reading the selected model's defaultAspectRatio (now 16:9 for the
|
||||
// InstructiveEdit-tagged models that populate STAGE_3D_PAGE_MODEL_LIST).
|
||||
usePromptPrefillFromOutput(demoOutputToken);
|
||||
usePromptPrefillFromOutput(imageToken);
|
||||
|
||||
const navigateToImageTo3D = useCallback(() => {
|
||||
navigate("/create-image");
|
||||
@@ -244,6 +298,12 @@ function PageSceneEditor() {
|
||||
showImageTo3DButton={false}
|
||||
showHelpMenu={false}
|
||||
modelSelectorPlacement="prompt-box"
|
||||
promptboxAboveStackSlot={
|
||||
<OtherScenesOverlay
|
||||
currentSceneToken={sceneToken}
|
||||
demoOutputToken={demoToken}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* Controls3D's "My Library" popup item flips the
|
||||
galleryModalVisibleViewMode signal — the modal below subscribes
|
||||
@@ -252,7 +312,9 @@ function PageSceneEditor() {
|
||||
adds. Both components portal themselves out of this wrapper. */}
|
||||
<GalleryModal mode="view" />
|
||||
<GalleryDragComponent />
|
||||
{demoOutputToken && <DemoOutputOverlay outputToken={demoOutputToken} />}
|
||||
{demoToken && (
|
||||
<DemoOutputOverlay imageToken={imageToken} videoToken={videoToken} />
|
||||
)}
|
||||
<SceneSplashModal currentSceneToken={sceneToken} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Example-scene data for the edit-3D splash. Each card opens
|
||||
// `/edit-3d/${sceneToken}?output=${outputToken}` so the editor mounts
|
||||
// `/edit-3d/${sceneToken}?image=${outputToken}` so the editor mounts
|
||||
// the scene and the demo-output overlay renders the AI-generated still
|
||||
// next to it. Real per-scene tokens land later; for now every entry
|
||||
// points at the same demo pair so the feature is exercised end-to-end.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user