mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 13:02:37 -05:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
425bfcba86 | ||
|
|
a99fab1d45 | ||
|
|
323cbc8076 | ||
|
|
93190d2a66 | ||
|
|
c3444ad439 |
Generated
+3
@@ -4710,7 +4710,10 @@ dependencies = [
|
||||
"jobs_common",
|
||||
"log",
|
||||
"mysql_queries",
|
||||
"pager",
|
||||
"reqwest 0.12.23",
|
||||
"rootly_client",
|
||||
"rootly_config",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
||||
@@ -28,6 +28,9 @@ gmicloud_client.workspace = true
|
||||
hashing = { path = "../../../lib/files/hashing" }
|
||||
jobs_common.workspace = true
|
||||
mysql_queries.workspace = true
|
||||
pager = { path = "../../../lib/pager" }
|
||||
rootly_client.workspace = true
|
||||
rootly_config.workspace = true
|
||||
server_environment = { path = "../../../lib/server_environment" }
|
||||
shared_env_var_config.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::sync::Arc;
|
||||
use log::error;
|
||||
use mysql_queries::queries::generic_inference::gmicloud::list_pending_gmicloud_jobs::PendingGmiCloudJob;
|
||||
use pager::client::pager::Pager;
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
|
||||
/// Enqueue a pager alert for the error, then return it as `Err`.
|
||||
pub fn alert_pager_and_return_err<T>(
|
||||
pager: &Pager,
|
||||
title: &str,
|
||||
error: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
job: Option<&PendingGmiCloudJob>,
|
||||
) -> anyhow::Result<T> {
|
||||
let err_message = format!("{:#}", error);
|
||||
|
||||
let mut builder = NotificationDetailsBuilder::from_boxed_error(error)
|
||||
.set_title(title.to_string())
|
||||
.set_urgency(Some(NotificationUrgency::Medium));
|
||||
|
||||
if let Some(job) = job {
|
||||
builder = builder
|
||||
.set_inference_job_token(Some(job.job_token.to_string()))
|
||||
.set_third_party_id(Some(job.request_id.clone()))
|
||||
.set_user_token(job.maybe_creator_user_token.as_ref().map(|t| t.to_string()));
|
||||
}
|
||||
|
||||
let notification = builder.build();
|
||||
|
||||
if let Err(pager_err) = pager.enqueue_page(notification) {
|
||||
error!("Failed to enqueue pager alert: {:?}", pager_err);
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(err_message))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use cloud_storage::bucket_client::BucketClient;
|
||||
use concurrency::relaxed_atomic_bool::RelaxedAtomicBool;
|
||||
use gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey;
|
||||
use jobs_common::job_stats::JobStats;
|
||||
use pager::client::pager::Pager;
|
||||
use server_environment::ServerEnvironment;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
@@ -16,10 +17,15 @@ pub struct JobDependencies {
|
||||
|
||||
pub server_environment: ServerEnvironment,
|
||||
|
||||
pub pager: Pager,
|
||||
|
||||
pub job_stats: JobStats,
|
||||
|
||||
/// How long to sleep between poll iterations (milliseconds).
|
||||
pub poll_interval_millis: u64,
|
||||
/// How long to sleep after a successful poll iteration (milliseconds).
|
||||
pub poll_interval_success_millis: u64,
|
||||
|
||||
/// How long to sleep after a failed poll iteration (milliseconds).
|
||||
pub poll_interval_failure_millis: u64,
|
||||
|
||||
/// Set to `true` from another thread to trigger graceful shutdown.
|
||||
pub application_shutdown: RelaxedAtomicBool,
|
||||
|
||||
@@ -28,11 +28,14 @@ use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
|
||||
use crate::http_server::run_http_server::{launch_http_server, CreateServerArgs};
|
||||
use crate::main_loop::main_loop;
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
use crate::startup::build_pager::build_pager;
|
||||
|
||||
pub mod alert_on_error;
|
||||
pub mod http_server;
|
||||
pub mod job_dependencies;
|
||||
pub mod main_loop;
|
||||
pub mod process_job;
|
||||
pub mod startup;
|
||||
|
||||
// Bucket config
|
||||
const ENV_ACCESS_KEY: &str = "ACCESS_KEY";
|
||||
@@ -99,15 +102,31 @@ async fn main() -> AnyhowResult<()> {
|
||||
let gmicloud_api_key_str = easyenv::get_env_string_required(ENV_GMICLOUD_API_KEY)?;
|
||||
let gmicloud_api_key = GmiCloudApiKey::new(gmicloud_api_key_str);
|
||||
|
||||
// How often to poll for results (default: 15 seconds)
|
||||
let poll_interval_millis: u64 = easyenv::get_env_num(
|
||||
"GMICLOUD_POLL_INTERVAL_MILLIS",
|
||||
// How often to poll after a successful iteration (default: 3 seconds)
|
||||
let poll_interval_success_millis: u64 = easyenv::get_env_num(
|
||||
"GMICLOUD_POLL_INTERVAL_SUCCESS_MILLIS",
|
||||
3_000,
|
||||
)?;
|
||||
|
||||
// How often to poll after a failed iteration (default: 15 seconds)
|
||||
let poll_interval_failure_millis: u64 = easyenv::get_env_num(
|
||||
"GMICLOUD_POLL_INTERVAL_FAILURE_MILLIS",
|
||||
15_000,
|
||||
)?;
|
||||
|
||||
let application_shutdown = RelaxedAtomicBool::new(false);
|
||||
let job_stats = JobStats::new();
|
||||
|
||||
// Pager setup
|
||||
let (pager, pager_worker) = build_pager(server_environment, &container_environment.hostname);
|
||||
|
||||
info!("Spawning pager worker.");
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().expect("pager worker tokio runtime");
|
||||
rt.block_on(pager_worker.run());
|
||||
});
|
||||
|
||||
let create_server_args = CreateServerArgs {
|
||||
container_environment: container_environment.clone(),
|
||||
job_stats: job_stats.clone(),
|
||||
@@ -118,8 +137,10 @@ async fn main() -> AnyhowResult<()> {
|
||||
public_bucket_client,
|
||||
gmicloud_api_key,
|
||||
server_environment,
|
||||
pager,
|
||||
job_stats,
|
||||
poll_interval_millis,
|
||||
poll_interval_success_millis,
|
||||
poll_interval_failure_millis,
|
||||
application_shutdown: application_shutdown.clone(),
|
||||
};
|
||||
|
||||
|
||||
@@ -12,12 +12,16 @@ pub async fn main_loop(job_dependencies: JobDependencies) {
|
||||
while !job_dependencies.application_shutdown.get() {
|
||||
let result = run_poll_iteration(&job_dependencies).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Error in poll iteration: {:?}", err);
|
||||
let _ = job_dependencies.job_stats.increment_failure_count();
|
||||
}
|
||||
let sleep_millis = match result {
|
||||
Ok(_) => job_dependencies.poll_interval_success_millis,
|
||||
Err(err) => {
|
||||
error!("Error in poll iteration: {:?}", err);
|
||||
let _ = job_dependencies.job_stats.increment_failure_count();
|
||||
job_dependencies.poll_interval_failure_millis
|
||||
}
|
||||
};
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(job_dependencies.poll_interval_millis)).await;
|
||||
tokio::time::sleep(Duration::from_millis(sleep_millis)).await;
|
||||
}
|
||||
|
||||
warn!("GmiCloud job runner main loop is shut down.");
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use log::{error, info, warn};
|
||||
use log::{error, warn};
|
||||
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use mysql_queries::queries::generic_inference::job::mark_job_failed_by_token::{mark_job_failed_by_token, MarkJobFailedByTokenArgs};
|
||||
use mysql_queries::queries::generic_inference::gmicloud::list_pending_gmicloud_jobs::PendingGmiCloudJob;
|
||||
use mysql_queries::queries::wallets::refund::try_to_refund_ledger_entry::{try_to_refund_ledger_entry, WalletRefundOutcome};
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
@@ -12,72 +11,6 @@ pub async fn process_failed_job(
|
||||
job: &PendingGmiCloudJob,
|
||||
reason: &str,
|
||||
) {
|
||||
// --- Step 1: Attempt the refund before touching the job status. ---
|
||||
//
|
||||
// Refund first so a crash between refund and status update never leaves
|
||||
// a failed job without a refund. If the refund fails, bail out early:
|
||||
// the job stays pending and will be retried on the next poll cycle.
|
||||
|
||||
match &job.maybe_wallet_ledger_entry_token {
|
||||
None => {
|
||||
warn!(
|
||||
"Job {} has no wallet ledger entry token; skipping refund.",
|
||||
job.job_token.as_str()
|
||||
);
|
||||
}
|
||||
Some(ledger_token) => {
|
||||
let mut transaction = match deps.mysql_pool.begin().await {
|
||||
Ok(tx) => tx,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to begin refund transaction for job {} (ledger {}): {:?}. \
|
||||
Job will NOT be marked failed yet and will be retried next poll.",
|
||||
job.job_token.as_str(), ledger_token.as_str(), err
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match try_to_refund_ledger_entry(ledger_token, &mut transaction).await {
|
||||
Ok(WalletRefundOutcome::Refunded(summary)) => {
|
||||
info!(
|
||||
"Refunded {} credits for failed job {} (ledger {} -> refund ledger {}).",
|
||||
summary.refund_amount,
|
||||
job.job_token.as_str(),
|
||||
ledger_token.as_str(),
|
||||
summary.refund_ledger_entry_token.as_str(),
|
||||
);
|
||||
if let Err(err) = transaction.commit().await {
|
||||
error!(
|
||||
"Failed to commit refund transaction for job {} (ledger {}): {:?}. \
|
||||
Job will NOT be marked failed yet and will be retried next poll.",
|
||||
job.job_token.as_str(), ledger_token.as_str(), err
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Ok(WalletRefundOutcome::AlreadyRefunded) => {
|
||||
info!(
|
||||
"Ledger entry {} for job {} was already refunded; proceeding to mark job failed.",
|
||||
ledger_token.as_str(), job.job_token.as_str(),
|
||||
);
|
||||
let _ = transaction.rollback().await;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to refund ledger entry {} for job {}: {:?}. \
|
||||
Job will NOT be marked failed yet and will be retried next poll.",
|
||||
ledger_token.as_str(), job.job_token.as_str(), err,
|
||||
);
|
||||
let _ = transaction.rollback().await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 2: Mark the job record as failed. ---
|
||||
|
||||
let reason_lower = reason.to_lowercase();
|
||||
|
||||
let platform_rules_violation = reason_lower.contains("violates")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use anyhow::anyhow;
|
||||
use log::{error, info, warn};
|
||||
|
||||
@@ -14,6 +15,7 @@ use mysql_queries::queries::media_files::create::insert_builder::media_file_inse
|
||||
use mysql_queries::queries::generic_inference::web::mark_generic_inference_job_successfully_done_by_token::mark_generic_inference_job_successfully_done_by_token;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::alert_on_error::alert_pager_and_return_err;
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
const VIDEO_PREFIX: &str = "artcraft_";
|
||||
@@ -47,20 +49,38 @@ pub async fn process_successful_job(
|
||||
None => None,
|
||||
};
|
||||
|
||||
// --- Step 2: Download and upload the video. ---
|
||||
// --- Step 2: Download the video. ---
|
||||
|
||||
info!("Downloading video for job {} from: {}", job.job_token.as_str(), video_url);
|
||||
|
||||
let video_bytes: Vec<u8> = reqwest::get(video_url)
|
||||
.await
|
||||
.map_err(|err| anyhow!("reqwest error downloading video: {:?}", err))?
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|err| anyhow!("error reading video bytes: {:?}", err))?
|
||||
.to_vec();
|
||||
let video_bytes: Vec<u8> = match reqwest::get(video_url).await {
|
||||
Ok(resp) => match resp.bytes().await {
|
||||
Ok(bytes) => bytes.to_vec(),
|
||||
Err(err) => {
|
||||
error!("Error reading video bytes for job {}: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"GmiCloud video download failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error downloading video for job {}: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"GmiCloud video download failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
info!("Downloaded {} bytes for job {}", video_bytes.len(), job.job_token.as_str());
|
||||
|
||||
// --- Step 3: Hash and upload. ---
|
||||
|
||||
let checksum = sha256_hash_bytes(&video_bytes)
|
||||
.map_err(|err| anyhow!("error hashing video: {:?}", err))?;
|
||||
|
||||
@@ -69,15 +89,26 @@ pub async fn process_successful_job(
|
||||
|
||||
info!("Uploading video to public bucket at path: {}", object_path);
|
||||
|
||||
deps
|
||||
let upload_result = deps
|
||||
.public_bucket_client
|
||||
.upload_file_with_content_type_process(object_path, &video_bytes, "video/mp4")
|
||||
.await
|
||||
.map_err(|err| anyhow!("error uploading video to bucket: {:?}", err))?;
|
||||
.await;
|
||||
|
||||
if let Err(err) = upload_result {
|
||||
error!("Error uploading video for job {}: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"GmiCloud bucket upload failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
|
||||
info!("Uploaded video for job {}. Creating media file record.", job.job_token.as_str());
|
||||
|
||||
let media_file_token = MediaFileInsertBuilder::new()
|
||||
// --- Step 4: Create media file record. ---
|
||||
|
||||
let media_file_result = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.creator_ip_address(&job.creator_ip_address)
|
||||
@@ -93,27 +124,44 @@ pub async fn process_successful_job(
|
||||
.maybe_cover_image_media_file_token(maybe_cover_token.as_ref())
|
||||
.public_bucket_directory_hash(&bucket_path)
|
||||
.insert_pool(&deps.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error inserting media file record: {:?}", err))?;
|
||||
.await;
|
||||
|
||||
let media_file_token = match media_file_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
error!("Error inserting media file record for job {}: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"GmiCloud media file insert failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
"Created media file {} for job {}. Marking job complete.",
|
||||
media_file_token.as_str(), job.job_token.as_str()
|
||||
);
|
||||
|
||||
mark_generic_inference_job_successfully_done_by_token(
|
||||
// --- Step 5: Mark job as done. ---
|
||||
|
||||
if let Err(err) = mark_generic_inference_job_successfully_done_by_token(
|
||||
&deps.mysql_pool,
|
||||
&job.job_token,
|
||||
Some(InferenceResultType::MediaFile),
|
||||
Some(media_file_token.as_str()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error marking job {} done: {:?}", job.job_token.as_str(), err);
|
||||
anyhow!("error marking job done: {:?}", err)
|
||||
})?;
|
||||
).await {
|
||||
error!("Error marking job {} done: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"GmiCloud job completion update failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
|
||||
info!("Job {} completed successfully.", job.job_token.as_str());
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
use log::{info, warn};
|
||||
use pager::client::pager::Pager;
|
||||
use pager::client::pager_builder::PagerBuilder;
|
||||
use pager::worker::pager_worker::PagerWorker;
|
||||
use rootly_client::creds::rootly_api_key::RootlyApiKey;
|
||||
use rootly_config::services::ROOTLY_SERVICE_ID_SEEDANCE2_PRO_JOB;
|
||||
use rootly_config::urgencies::{ROOTLY_URGENCY_ID_HIGH, ROOTLY_URGENCY_ID_LOW, ROOTLY_URGENCY_ID_MEDIUM};
|
||||
use shared_env_var_config::paging::{env_enable_paging_default_false, env_optional_rootly_api_key, env_optional_rootly_notification_target_id, env_optional_rootly_notification_target_type};
|
||||
|
||||
pub fn build_pager(
|
||||
server_environment: server_environment::ServerEnvironment,
|
||||
hostname: &str,
|
||||
) -> (Pager, PagerWorker) {
|
||||
let is_paging_enabled = env_enable_paging_default_false();
|
||||
|
||||
info!("Paging enabled: {}", is_paging_enabled);
|
||||
|
||||
let environment = if server_environment.is_deployed_in_production() {
|
||||
"production"
|
||||
} else {
|
||||
"development"
|
||||
};
|
||||
|
||||
let builder = PagerBuilder::new()
|
||||
.application_name("gmicloud-job".to_string())
|
||||
.environment(environment.to_string())
|
||||
.hostname(hostname.to_string())
|
||||
.service_id(ROOTLY_SERVICE_ID_SEEDANCE2_PRO_JOB.to_string());
|
||||
|
||||
if !is_paging_enabled {
|
||||
warn!("ENABLE_PAGING is false. Pager will be NoOp.");
|
||||
return builder.build_with_worker();
|
||||
}
|
||||
|
||||
let maybe_api_key = env_optional_rootly_api_key();
|
||||
|
||||
match maybe_api_key {
|
||||
Some(api_key) => {
|
||||
info!("Rootly API key found. Configuring pager with Rootly backend.");
|
||||
build_rootly_pager(builder, api_key)
|
||||
}
|
||||
None => {
|
||||
warn!("ROOTLY_API_KEY not set. Pager will not send real pages.");
|
||||
builder.build_with_worker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_rootly_pager(builder: PagerBuilder, api_key: String) -> (Pager, PagerWorker) {
|
||||
let mut rootly_builder = builder
|
||||
.rootly(RootlyApiKey::new(api_key))
|
||||
.urgency_id_high(ROOTLY_URGENCY_ID_HIGH.to_string())
|
||||
.urgency_id_medium(ROOTLY_URGENCY_ID_MEDIUM.to_string())
|
||||
.urgency_id_low(ROOTLY_URGENCY_ID_LOW.to_string());
|
||||
|
||||
let target_type = env_optional_rootly_notification_target_type();
|
||||
let target_id = env_optional_rootly_notification_target_id();
|
||||
|
||||
if let (Some(t_type), Some(t_id)) = (target_type, target_id) {
|
||||
rootly_builder = rootly_builder.notification_target(t_type, t_id);
|
||||
}
|
||||
|
||||
rootly_builder.build_with_worker()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod build_pager;
|
||||
+8
-6
@@ -94,11 +94,12 @@ pub async fn process_successful_job(
|
||||
);
|
||||
|
||||
// Upload to public bucket.
|
||||
if let Err(err) = deps
|
||||
let upload_result = deps
|
||||
.public_bucket_client
|
||||
.upload_file_with_content_type_process(object_path, &video_bytes, "video/mp4")
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
|
||||
if let Err(err) = upload_result {
|
||||
error!("Error uploading video for order {}: {:?}", order.order_id, err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
@@ -118,7 +119,7 @@ pub async fn process_successful_job(
|
||||
let maybe_frame_height = order.results.first().map(|r| r.height);
|
||||
|
||||
// Insert media file record.
|
||||
let media_file_token = match MediaFileInsertBuilder::new()
|
||||
let media_file_result = MediaFileInsertBuilder::new()
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.creator_ip_address(&job.creator_ip_address)
|
||||
@@ -135,8 +136,9 @@ pub async fn process_successful_job(
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.public_bucket_directory_hash(&bucket_path)
|
||||
.insert_pool(&deps.mysql_pool)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
|
||||
let media_file_token = match media_file_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
error!("Error inserting media file record for order {}: {:?}", order.order_id, err);
|
||||
|
||||
+9
-1
@@ -8,7 +8,7 @@ use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use artcraft_router::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use log::warn;
|
||||
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::helpers::hydrate_router_request::hydrate_to_router_request;
|
||||
use crate::state::server_state::ServerState;
|
||||
@@ -31,8 +31,16 @@ pub async fn omni_gen_video_cost_handler(
|
||||
_server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<OmniGenVideoCostResponse>, AdvancedCommonWebError> {
|
||||
let mut builder = hydrate_to_router_request(&request)?;
|
||||
|
||||
builder.provider = Provider::Artcraft;
|
||||
|
||||
// We bill global Seedance 2.0 at the same rate (for now) as regular Seedance 2.0
|
||||
match builder.model {
|
||||
CommonVideoModel::Seedance2p0Global => builder.model = CommonVideoModel::Seedance2p0,
|
||||
CommonVideoModel::Seedance2p0FastGlobal => builder.model = CommonVideoModel::Seedance2p0Fast,
|
||||
_ => {},
|
||||
}
|
||||
|
||||
let estimate = if builder.use_new_builder() {
|
||||
builder.build2()
|
||||
.map_err(|e| {
|
||||
|
||||
+1
@@ -3,3 +3,4 @@ pub mod build_router_client;
|
||||
pub(crate) mod hydrate_router_request;
|
||||
pub mod pipeline_result;
|
||||
pub mod resolve_kinovi_character_ids;
|
||||
pub mod resolve_media_tokens_to_urls;
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use artcraft_router::api::audio_list_ref::AudioListRef;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::video_list_ref::VideoListRef;
|
||||
use artcraft_router::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// For providers that take URLs directly (GmiCloud, Fal), swap
|
||||
/// `ImageRef::MediaFileToken` → `ImageRef::Url` using the resolved map.
|
||||
pub fn resolve_media_tokens_to_urls(
|
||||
builder: &mut GenerateVideoRequestBuilder,
|
||||
url_map: Option<&HashMap<MediaFileToken, String>>,
|
||||
) {
|
||||
let map = match url_map {
|
||||
Some(m) => m,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// start_frame
|
||||
if let Some(ImageRef::MediaFileToken(ref token)) = builder.start_frame {
|
||||
if let Some(url) = map.get(token) {
|
||||
builder.start_frame = Some(ImageRef::Url(url.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// end_frame
|
||||
if let Some(ImageRef::MediaFileToken(ref token)) = builder.end_frame {
|
||||
if let Some(url) = map.get(token) {
|
||||
builder.end_frame = Some(ImageRef::Url(url.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// reference_images
|
||||
if let Some(ImageListRef::MediaFileTokens(ref tokens)) = builder.reference_images {
|
||||
let urls: Vec<String> = tokens.iter()
|
||||
.filter_map(|t| map.get(t).cloned())
|
||||
.collect();
|
||||
if !urls.is_empty() {
|
||||
builder.reference_images = Some(ImageListRef::Urls(urls));
|
||||
}
|
||||
}
|
||||
|
||||
// reference_videos
|
||||
if let Some(VideoListRef::MediaFileTokens(ref tokens)) = builder.reference_videos {
|
||||
let urls: Vec<String> = tokens.iter()
|
||||
.filter_map(|t| map.get(t).cloned())
|
||||
.collect();
|
||||
if !urls.is_empty() {
|
||||
builder.reference_videos = Some(VideoListRef::Urls(urls));
|
||||
}
|
||||
}
|
||||
|
||||
// reference_audio
|
||||
if let Some(AudioListRef::MediaFileTokens(ref tokens)) = builder.reference_audio {
|
||||
let urls: Vec<String> = tokens.iter()
|
||||
.filter_map(|t| map.get(t).cloned())
|
||||
.collect();
|
||||
if !urls.is_empty() {
|
||||
builder.reference_audio = Some(AudioListRef::Urls(urls));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-69
@@ -2,12 +2,8 @@ use std::collections::HashMap;
|
||||
|
||||
use log::{error, info, warn};
|
||||
use sqlx::pool::PoolConnection;
|
||||
use artcraft_router::api::audio_list_ref::AudioListRef;
|
||||
use artcraft_router::api::common_video_model::CommonVideoModel;
|
||||
use artcraft_router::api::image_list_ref::ImageListRef;
|
||||
use artcraft_router::api::image_ref::ImageRef;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::api::video_list_ref::VideoListRef;
|
||||
use artcraft_router::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use artcraft_router::generate::generate_video_v2::video_generation_draft_context::VideoGenerationDraftContext;
|
||||
@@ -21,6 +17,7 @@ use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refun
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::helpers::bill_wallet::bill_wallet;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::helpers::build_router_client::build_router_client;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::helpers::pipeline_result::PipelineResult;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::helpers::resolve_media_tokens_to_urls::resolve_media_tokens_to_urls;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
pub struct RunPipelineV2Args<'a> {
|
||||
@@ -70,18 +67,19 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
|
||||
// 2. Calculate cost.
|
||||
// For Artcraft-billable models, swap provider to Artcraft so credits = cents.
|
||||
// For GmiCloud, use the execution request's cost directly (no Artcraft equivalent).
|
||||
let cost = if matches!(provider, Provider::GmiCloud) {
|
||||
draft_or_request.estimate_cost()
|
||||
.map_err(|e| {
|
||||
warn!("Failed to estimate cost for v2 (GmiCloud): {}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
})?
|
||||
.cost_in_usd_cents
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
let cost = {
|
||||
let mut cost_builder = router_builder.clone();
|
||||
cost_builder.provider = Provider::Artcraft;
|
||||
|
||||
if matches!(provider, Provider::GmiCloud) {
|
||||
// We bill global Seedance 2.0 at the same rate (for now) as regular Seedance 2.0
|
||||
match cost_builder.model {
|
||||
CommonVideoModel::Seedance2p0Global => cost_builder.model = CommonVideoModel::Seedance2p0,
|
||||
CommonVideoModel::Seedance2p0FastGlobal => cost_builder.model = CommonVideoModel::Seedance2p0Fast,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
cost_builder.build2()
|
||||
.map_err(|e| {
|
||||
warn!("Failed to build2 cost estimate for v2: {}", e);
|
||||
@@ -170,59 +168,3 @@ async fn upload_and_generate(
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})
|
||||
}
|
||||
|
||||
/// For providers that take URLs directly (GmiCloud, Fal), swap
|
||||
/// `ImageRef::MediaFileToken` → `ImageRef::Url` using the resolved map.
|
||||
fn resolve_media_tokens_to_urls(
|
||||
builder: &mut GenerateVideoRequestBuilder,
|
||||
url_map: Option<&HashMap<MediaFileToken, String>>,
|
||||
) {
|
||||
let map = match url_map {
|
||||
Some(m) => m,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// start_frame
|
||||
if let Some(ImageRef::MediaFileToken(ref token)) = builder.start_frame {
|
||||
if let Some(url) = map.get(token) {
|
||||
builder.start_frame = Some(ImageRef::Url(url.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// end_frame
|
||||
if let Some(ImageRef::MediaFileToken(ref token)) = builder.end_frame {
|
||||
if let Some(url) = map.get(token) {
|
||||
builder.end_frame = Some(ImageRef::Url(url.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// reference_images
|
||||
if let Some(ImageListRef::MediaFileTokens(ref tokens)) = builder.reference_images {
|
||||
let urls: Vec<String> = tokens.iter()
|
||||
.filter_map(|t| map.get(t).cloned())
|
||||
.collect();
|
||||
if !urls.is_empty() {
|
||||
builder.reference_images = Some(ImageListRef::Urls(urls));
|
||||
}
|
||||
}
|
||||
|
||||
// reference_videos
|
||||
if let Some(VideoListRef::MediaFileTokens(ref tokens)) = builder.reference_videos {
|
||||
let urls: Vec<String> = tokens.iter()
|
||||
.filter_map(|t| map.get(t).cloned())
|
||||
.collect();
|
||||
if !urls.is_empty() {
|
||||
builder.reference_videos = Some(VideoListRef::Urls(urls));
|
||||
}
|
||||
}
|
||||
|
||||
// reference_audio
|
||||
if let Some(AudioListRef::MediaFileTokens(ref tokens)) = builder.reference_audio {
|
||||
let urls: Vec<String> = tokens.iter()
|
||||
.filter_map(|t| map.get(t).cloned())
|
||||
.collect();
|
||||
if !urls.is_empty() {
|
||||
builder.reference_audio = Some(AudioListRef::Urls(urls));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user