Compare commits

..
1 Commits
Author SHA1 Message Date
Kasisnu cb13100e3e temp switch ST cdn to FY 2024-11-22 23:30:43 +05:30
84 changed files with 83 additions and 3610 deletions
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO media_files\nSET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n origin_category = ?,\n origin_product_category = ?,\n maybe_origin_model_type = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n\n checksum_sha2 = ?,\n\n maybe_style_transfer_source_media_file_token = ?,\n maybe_scene_source_media_file_token = ?,\n\n maybe_title = ?,\n maybe_prompt_token = ?,\n\n public_bucket_directory_hash = ?,\n maybe_public_bucket_prefix = ?,\n maybe_public_bucket_extension = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n\n creator_set_visibility = ?,\n\n maybe_creator_file_synthetic_id = ?,\n maybe_creator_category_synthetic_id = ?,\n\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 25
},
"nullable": []
},
"hash": "039c24023c93deacc0b09fbbfe747821ba9f7e06bc17f651628edf1a82f5feb6"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE generic_inference_jobs\nSET\n status = 'cancelled_by_user',\n retry_at = NULL\nWHERE status IN (\n 'pending',\n 'started',\n 'attempt_failed'\n )\nLIMIT 100\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "84320887e8157a450ad46ec0843071c60c054eab61958a9148789ed76cd476e3"
}
@@ -63,9 +63,6 @@ pub enum InferenceJobProductCategory {
/// Video: Studio
VidStudio,
/// Video: Studio Gen 2
VidStudioGen2,
/// Video: Style Transfer
VidStyleTransfer,
@@ -125,7 +122,6 @@ impl InferenceJobProductCategory {
Self::VidLivePortrait => "vid_live_portrait",
Self::VidLivePortraitWebcam => "vid_live_portrait_webcam",
Self::VidStudio => "vid_studio",
Self::VidStudioGen2 => "vid_studio_gen2",
Self::VidStyleTransfer => "vid_style_transfer",
Self::LipsyncFaceFusion => "lipsync_face_fusion",
Self::LipsyncSadTalker => "lipsync_sad_talker",
@@ -153,7 +149,6 @@ impl InferenceJobProductCategory {
"vid_live_portrait" => Ok(Self::VidLivePortrait),
"vid_live_portrait_webcam" => Ok(Self::VidLivePortraitWebcam),
"vid_studio" => Ok(Self::VidStudio),
"vid_studio_gen2" => Ok(Self::VidStudioGen2),
"vid_style_transfer" => Ok(Self::VidStyleTransfer),
"lipsync_face_fusion" => Ok(Self::LipsyncFaceFusion),
"lipsync_sad_talker" => Ok(Self::LipsyncSadTalker),
@@ -184,7 +179,6 @@ impl InferenceJobProductCategory {
Self::VidLivePortrait,
Self::VidLivePortraitWebcam,
Self::VidStudio,
Self::VidStudioGen2,
Self::VidStyleTransfer,
Self::LipsyncFaceFusion,
Self::LipsyncSadTalker,
@@ -221,7 +215,6 @@ mod tests {
assert_serialization(InferenceJobProductCategory::VidLivePortrait, "vid_live_portrait");
assert_serialization(InferenceJobProductCategory::VidLivePortraitWebcam, "vid_live_portrait_webcam");
assert_serialization(InferenceJobProductCategory::VidStudio, "vid_studio");
assert_serialization(InferenceJobProductCategory::VidStudioGen2, "vid_studio_gen2");
assert_serialization(InferenceJobProductCategory::VidStyleTransfer, "vid_style_transfer");
assert_serialization(InferenceJobProductCategory::LipsyncFaceFusion, "lipsync_face_fusion");
assert_serialization(InferenceJobProductCategory::LipsyncSadTalker, "lipsync_sad_talker");
@@ -247,7 +240,6 @@ mod tests {
assert_eq!(InferenceJobProductCategory::VidLivePortrait.to_str(), "vid_live_portrait");
assert_eq!(InferenceJobProductCategory::VidLivePortraitWebcam.to_str(), "vid_live_portrait_webcam");
assert_eq!(InferenceJobProductCategory::VidStudio.to_str(), "vid_studio");
assert_eq!(InferenceJobProductCategory::VidStudioGen2.to_str(), "vid_studio_gen2");
assert_eq!(InferenceJobProductCategory::VidStyleTransfer.to_str(), "vid_style_transfer");
assert_eq!(InferenceJobProductCategory::LipsyncFaceFusion.to_str(), "lipsync_face_fusion");
assert_eq!(InferenceJobProductCategory::LipsyncSadTalker.to_str(), "lipsync_sad_talker");
@@ -273,7 +265,6 @@ mod tests {
assert_eq!(InferenceJobProductCategory::from_str("vid_live_portrait").unwrap(), InferenceJobProductCategory::VidLivePortrait);
assert_eq!(InferenceJobProductCategory::from_str("vid_live_portrait_webcam").unwrap(), InferenceJobProductCategory::VidLivePortraitWebcam);
assert_eq!(InferenceJobProductCategory::from_str("vid_studio").unwrap(), InferenceJobProductCategory::VidStudio);
assert_eq!(InferenceJobProductCategory::from_str("vid_studio_gen2").unwrap(), InferenceJobProductCategory::VidStudioGen2);
assert_eq!(InferenceJobProductCategory::from_str("vid_style_transfer").unwrap(), InferenceJobProductCategory::VidStyleTransfer);
assert_eq!(InferenceJobProductCategory::from_str("lipsync_face_fusion").unwrap(), InferenceJobProductCategory::LipsyncFaceFusion);
assert_eq!(InferenceJobProductCategory::from_str("lipsync_sad_talker").unwrap(), InferenceJobProductCategory::LipsyncSadTalker);
@@ -289,7 +280,7 @@ mod tests {
fn all_variants() {
// Static check
let mut variants = InferenceJobProductCategory::all_variants();
assert_eq!(variants.len(), 21);
assert_eq!(variants.len(), 20);
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::DownloadGptSoVits));
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::TtsGptSoVits));
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::TtsStyleTts2));
@@ -302,7 +293,6 @@ mod tests {
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidLivePortrait));
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidLivePortraitWebcam));
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidStudio));
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidStudioGen2));
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::VidStyleTransfer));
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::LipsyncFaceFusion));
assert_eq!(variants.pop_first(), Some(InferenceJobProductCategory::LipsyncSadTalker));
@@ -36,10 +36,6 @@ pub enum InferenceJobType {
#[deprecated(note = "Use VideoRender instead.")]
ComfyUi,
/// Second gen studio
#[serde(rename = "studio_gen2")]
StudioGen2,
/// A job that turns "FBX" game engine files into "GLTF" files (Bevy-compatible).
#[serde(rename = "convert_fbx_gltf")]
ConvertFbxToGltf,
@@ -107,7 +103,6 @@ impl InferenceJobType {
Self::F5TTS => "f5_tts",
Self::GptSovits => "gpt_sovits",
Self::ComfyUi => "comfy_ui",
Self::StudioGen2 => "studio_gen2",
Self::ConvertFbxToGltf => "convert_fbx_gltf",
Self::MocapNet => "mocap_net",
Self::RvcV2 => "rvc_v2",
@@ -131,7 +126,6 @@ impl InferenceJobType {
"f5_tts" => Ok(Self::F5TTS),
"gpt_sovits" => Ok(Self::GptSovits),
"comfy_ui" => Ok(Self::ComfyUi),
"studio_gen2" => Ok(Self::StudioGen2),
"convert_fbx_gltf" => Ok(Self::ConvertFbxToGltf),
"mocap_net" => Ok(Self::MocapNet),
"rvc_v2" => Ok(Self::RvcV2),
@@ -158,7 +152,6 @@ impl InferenceJobType {
Self::F5TTS,
Self::GptSovits,
Self::ComfyUi,
Self::StudioGen2,
Self::ConvertFbxToGltf,
Self::MocapNet,
Self::RvcV2,
@@ -196,7 +189,6 @@ mod tests {
assert_serialization(InferenceJobType::F5TTS, "f5_tts");
assert_serialization(InferenceJobType::GptSovits, "gpt_sovits");
assert_serialization(InferenceJobType::ComfyUi, "comfy_ui");
assert_serialization(InferenceJobType::StudioGen2, "studio_gen2");
assert_serialization(InferenceJobType::ConvertFbxToGltf, "convert_fbx_gltf");
assert_serialization(InferenceJobType::MocapNet, "mocap_net");
assert_serialization(InferenceJobType::RvcV2, "rvc_v2");
@@ -219,7 +211,6 @@ mod tests {
assert_eq!(InferenceJobType::F5TTS.to_str(), "f5_tts");
assert_eq!(InferenceJobType::GptSovits.to_str(), "gpt_sovits");
assert_eq!(InferenceJobType::ComfyUi.to_str(), "comfy_ui");
assert_eq!(InferenceJobType::StudioGen2.to_str(), "studio_gen2");
assert_eq!(InferenceJobType::ConvertFbxToGltf.to_str(), "convert_fbx_gltf");
assert_eq!(InferenceJobType::MocapNet.to_str(), "mocap_net");
assert_eq!(InferenceJobType::RvcV2.to_str(), "rvc_v2");
@@ -242,7 +233,6 @@ mod tests {
assert_eq!(InferenceJobType::from_str("f5_tts").unwrap(), InferenceJobType::F5TTS);
assert_eq!(InferenceJobType::from_str("gpt_sovits").unwrap(), InferenceJobType::GptSovits);
assert_eq!(InferenceJobType::from_str("comfy_ui").unwrap(), InferenceJobType::ComfyUi);
assert_eq!(InferenceJobType::from_str("studio_gen2").unwrap(), InferenceJobType::StudioGen2);
assert_eq!(InferenceJobType::from_str("convert_fbx_gltf").unwrap(), InferenceJobType::ConvertFbxToGltf);
assert_eq!(InferenceJobType::from_str("mocap_net").unwrap(), InferenceJobType::MocapNet);
assert_eq!(InferenceJobType::from_str("rvc_v2").unwrap(), InferenceJobType::RvcV2);
@@ -268,7 +258,6 @@ mod tests {
assert_eq!(variants.pop_first(), Some(InferenceJobType::F5TTS));
assert_eq!(variants.pop_first(), Some(InferenceJobType::GptSovits));
assert_eq!(variants.pop_first(), Some(InferenceJobType::ComfyUi));
assert_eq!(variants.pop_first(), Some(InferenceJobType::StudioGen2));
assert_eq!(variants.pop_first(), Some(InferenceJobType::ConvertFbxToGltf));
assert_eq!(variants.pop_first(), Some(InferenceJobType::MocapNet));
assert_eq!(variants.pop_first(), Some(InferenceJobType::RvcV2));
@@ -39,10 +39,6 @@ pub enum PublicInferenceJobType {
#[deprecated(note = "Use VideoRender instead.")]
ComfyUi,
/// Second gen studio
#[serde(rename = "studio_gen2")]
StudioGen2,
/// A job that turns "FBX" game engine files into "GLTF" files (Bevy-compatible).
#[serde(rename = "convert_fbx_gltf")]
ConvertFbxToGltf,
@@ -106,7 +102,6 @@ impl PublicInferenceJobType {
InferenceJobType::GptSovits => Self::GptSovits,
InferenceJobType::F5TTS => Self::F5TTS,
InferenceJobType::ComfyUi => Self::ComfyUi,
InferenceJobType::StudioGen2 => Self::StudioGen2,
InferenceJobType::ConvertFbxToGltf => Self::ConvertFbxToGltf,
InferenceJobType::MocapNet => Self::MocapNet,
InferenceJobType::RvcV2 => Self::RvcV2,
@@ -132,7 +127,6 @@ impl PublicInferenceJobType {
Self::GptSovits => InferenceJobType::GptSovits,
Self::F5TTS => InferenceJobType::F5TTS,
Self::ComfyUi => InferenceJobType::ComfyUi,
Self::StudioGen2 => InferenceJobType::StudioGen2,
Self::ConvertFbxToGltf => InferenceJobType::ConvertFbxToGltf,
Self::MocapNet => InferenceJobType::MocapNet,
Self::RvcV2 => InferenceJobType::RvcV2,
@@ -13,7 +13,6 @@ pub fn add_storyteller_studio(cors: Cors, _is_production: bool) -> Cors {
netlify_branch_domain_matches(origin, "pipeline-gottagofast.netlify.app")
})
.allowed_origin("http://localhost:5173")
.allowed_origin("https://animate.storyteller.ai") // NB: Gen2 Studio
}
#[cfg(test)]
@@ -4,13 +4,11 @@ use std::time::Duration;
use anyhow::anyhow;
use anyhow::bail;
use log::{error, warn};
use log::warn;
use log::{debug, info};
use s3::bucket::Bucket;
use s3::creds::Credentials;
use s3::error::S3Error;
use s3::region::Region;
use s3::request::ResponseData;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
@@ -145,13 +143,7 @@ impl BucketClient {
let object_name = self.get_rooted_object_name(object_name);
info!("Rooted filename for bucket: {}", object_name);
let response = self.bucket.put_object_with_content_type(&object_name, bytes, content_type).await;
if let Err(err) = &response {
error!("S3 Upload Error for bucket name {}: {:?}", &self.bucket.name, err);
}
let response = response?;
let response = self.bucket.put_object_with_content_type(&object_name, bytes, content_type).await?;
let body_bytes = response.bytes();
let code = response.status_code();
+1 -10
View File
@@ -15,11 +15,6 @@ pub struct BootstrapArgs<'a, P: AsRef<Path>> {
/// Where to look for env conf files.
pub config_search_directories: &'a [P],
/// If true, ignore the root '.env' file.
/// TODO(bt,2025-01-23): Make this the default behavior, then remove reading from '.env' entirely,
/// There are too many apps that share this file.
pub ignore_legacy_dot_env_file: bool,
}
/// Information about how the application is deployed.
@@ -50,11 +45,7 @@ pub struct ContainerEnvironment {
const SERVER_ENVIRONMENT : &str = "SERVER_ENVIRONMENT";
pub fn bootstrap<P: AsRef<Path>>(args: BootstrapArgs<'_, P>) -> AnyhowResult<ContainerEnvironment> {
if args.ignore_legacy_dot_env_file {
easyenv::init_env_logger(args.default_logging_override);
} else {
easyenv::init_all_with_default_logging(args.default_logging_override);
}
easyenv::init_all_with_default_logging(args.default_logging_override);
info!("Bootstrapping application {}", &args.app_name);
@@ -7,7 +7,6 @@ use crate::payloads::generic_inference_args::inner_payloads::live_portrait_paylo
use crate::payloads::generic_inference_args::inner_payloads::mocap_payload::MocapArgs;
use crate::payloads::generic_inference_args::inner_payloads::render_engine_scene_to_video_payload::RenderEngineSceneToVideoArgs;
use crate::payloads::generic_inference_args::inner_payloads::seed_vc_payload::SeedVcPayload;
use crate::payloads::generic_inference_args::inner_payloads::studio_gen2_payload::StudioGen2Payload;
use crate::payloads::generic_inference_args::inner_payloads::tts_payload::TTSArgs;
use crate::payloads::generic_inference_args::inner_payloads::videofilter_payload::RerenderArgs;
use crate::payloads::generic_inference_args::inner_payloads::workflow_payload::WorkflowArgs;
@@ -149,9 +148,6 @@ pub enum PolymorphicInferenceArgs {
/// ComfyUI (Short name to save space when serializing.)
Cu(WorkflowArgs),
/// Studio Gen2 (Short name to save space when serializing.)
S2(StudioGen2Payload),
/// ComfyUI (Short name to save space when serializing.)
Lp(LivePortraitPayload),
@@ -5,7 +5,6 @@ pub mod lipsync_payload;
pub mod live_portrait_payload;
pub mod mocap_payload;
pub mod render_engine_scene_to_video_payload;
pub mod studio_gen2_payload;
pub mod tts_payload;
pub mod videofilter_payload;
pub mod workflow_payload;
@@ -1,65 +0,0 @@
use crate::payloads::generic_inference_args::common::watermark_type::WatermarkType;
use enums::common::visibility::Visibility;
use std::time::Duration;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct StudioGen2Payload {
/// The input image media file (required)
#[serde(rename = "i")]
#[serde(skip_serializing_if = "Option::is_none")]
pub image_file: Option<MediaFileToken>,
/// The input video media file (required)
#[serde(rename = "v")]
#[serde(skip_serializing_if = "Option::is_none")]
pub video_file: Option<MediaFileToken>,
#[serde(rename = "wt")]
#[serde(skip_serializing_if = "Option::is_none")]
pub watermark_type: Option<WatermarkType>,
#[serde(rename = "cv")]
#[serde(skip_serializing_if = "Option::is_none")]
pub creator_visibility: Option<Visibility>,
#[serde(rename = "sm")]
#[serde(skip_serializing_if = "Option::is_none")]
pub after_job_debug_sleep_millis: Option<u64>,
#[serde(rename = "ow")]
#[serde(skip_serializing_if = "Option::is_none")]
pub output_width: Option<u64>,
#[serde(rename = "oh")]
#[serde(skip_serializing_if = "Option::is_none")]
pub output_height: Option<u64>,
#[serde(rename = "fp")]
#[serde(skip_serializing_if = "Option::is_none")]
pub fps: Option<u64>,
#[serde(rename = "mf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_frames: Option<u64>,
#[serde(rename = "rnd")]
#[serde(skip_serializing_if = "Option::is_none")]
pub rounds: Option<u64>,
#[serde(rename = "td")]
#[serde(skip_serializing_if = "Option::is_none")]
pub trim_duration_millis: Option<u64>,
#[serde(rename = "ski")]
#[serde(skip_serializing_if = "Option::is_none")]
pub skip_image_resize: Option<bool>,
#[serde(rename = "tw")]
#[serde(skip_serializing_if = "Option::is_none")]
pub tensor_image_width: Option<u64>,
#[serde(rename = "th")]
#[serde(skip_serializing_if = "Option::is_none")]
pub tensor_image_height: Option<u64>,
}
@@ -1,228 +0,0 @@
use std::collections::BTreeSet;
use anyhow::anyhow;
use chrono::Utc;
use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_input_source_token_type::InferenceInputSourceTokenType;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::job_status_plus::JobStatusPlus;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::helpers::boolean_converters::i8_to_bool;
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
use crate::queries::generic_inference::job::_keys::GenericInferenceJobId;
pub struct UntriedJobCount {
pub job_count: usize,
}
pub struct CountUntriedJobsOfTypeArgs<'a> {
pub maybe_scope_by_job_type: Option<&'a BTreeSet<InferenceJobType>>,
pub maybe_scope_by_model_type: Option<&'a BTreeSet<InferenceModelType>>,
pub maybe_scope_by_job_category: Option<&'a BTreeSet<InferenceCategory>>,
pub mysql_pool: &'a MySqlPool,
}
pub async fn count_untried_jobs_of_type(
args: CountUntriedJobsOfTypeArgs<'_>,
)
-> AnyhowResult<UntriedJobCount>
{
let query = perform_query(args).await;
let result = query?;
Ok(UntriedJobCount {
job_count: result.job_count as usize,
})
}
async fn perform_query(args: CountUntriedJobsOfTypeArgs<'_>) -> Result<UntriedJobCountRawInternal, sqlx::Error> {
// NB: Can't be type checked because of WHERE IN clause with dynamic contents
let mut query = core_query(&args);
let query = sqlx::query_as::<_, UntriedJobCountRawInternal>(&query);
query.fetch_one(args.mysql_pool)
.await
}
// TODO(bt,2024-01-25): Make QueryBuilder
fn core_query(args: &CountUntriedJobsOfTypeArgs<'_>) -> String {
// NB: Can't be type checked because of WHERE IN clause with dynamic contents
let mut query = r#"
SELECT
count(*) as job_count
FROM generic_inference_jobs
WHERE
(
status IN ("pending")
)
AND
(
retry_at IS NULL
)
AND
(
attempt_count = 0
)
"#.to_string();
if let Some(job_types) = args.maybe_scope_by_job_type {
if !job_types.is_empty() {
query.push_str(&format!(r#"
AND
(
job_type IN ({})
)
"#, job_type_predicate(job_types)));
}
}
if let Some(model_types) = args.maybe_scope_by_model_type {
if !model_types.is_empty() {
query.push_str(&format!(r#"
AND
(
maybe_model_type IN ({})
)
"#, model_type_predicate(model_types)));
}
}
if let Some(inference_categories) = args.maybe_scope_by_job_category {
if !inference_categories.is_empty() {
query.push_str(&format!(r#"
AND
(
inference_category IN ({})
)
"#, inference_category_predicate(&inference_categories)));
}
}
query
}
#[derive(Debug)]
#[derive(sqlx::FromRow)]
struct UntriedJobCountRawInternal {
pub job_count: u64,
}
/// Return a comma-separated predicate, since SQLx does not yet support WHERE IN(?) for Vec<T>, etc.
/// Issue: https://github.com/launchbadge/sqlx/issues/875
fn job_type_predicate(types: &BTreeSet<InferenceJobType>) -> String {
let mut vec = types.iter()
.map(|ty| ty.to_str())
.map(|ty| format!("\"{}\"", ty))
.collect::<Vec<String>>();
vec.sort(); // NB: For the benefit of tests.
vec.join(", ")
}
/// Return a comma-separated predicate, since SQLx does not yet support WHERE IN(?) for Vec<T>, etc.
/// Issue: https://github.com/launchbadge/sqlx/issues/875
fn model_type_predicate(types: &BTreeSet<InferenceModelType>) -> String {
let mut vec = types.iter()
.map(|ty| ty.to_str())
.map(|ty| format!("\"{}\"", ty))
.collect::<Vec<String>>();
vec.sort(); // NB: For the benefit of tests.
vec.join(", ")
}
/// Return a comma-separated predicate, since SQLx does not yet support WHERE IN(?) for Vec<T>, etc.
/// Issue: https://github.com/launchbadge/sqlx/issues/875
fn inference_category_predicate(categories: &BTreeSet<InferenceCategory>) -> String {
let mut vec = categories.iter()
.map(|ty| ty.to_str())
.map(|ty| format!("\"{}\"", ty))
.collect::<Vec<String>>();
vec.sort(); // NB: For the benefit of tests.
vec.join(", ")
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use crate::queries::generic_inference::job::count_untried_jobs_of_type::{inference_category_predicate, job_type_predicate, model_type_predicate};
#[test]
fn test_job_type_predicate() {
// None
let types = BTreeSet::from([]);
assert_eq!(job_type_predicate(&types), "".to_string());
// One
let types = BTreeSet::from([
InferenceJobType::RvcV2,
]);
assert_eq!(job_type_predicate(&types), "\"rvc_v2\"".to_string());
// Multiple
let types = BTreeSet::from([
InferenceJobType::MocapNet,
InferenceJobType::RvcV2,
]);
assert_eq!(job_type_predicate(&types), "\"mocap_net\", \"rvc_v2\"".to_string());
}
#[test]
fn test_model_type_predicate() {
// None
let types = BTreeSet::from([]);
assert_eq!(model_type_predicate(&types), "".to_string());
// One
let types = BTreeSet::from([
InferenceModelType::RvcV2,
]);
assert_eq!(model_type_predicate(&types), "\"rvc_v2\"".to_string());
// Multiple
let types = BTreeSet::from([
InferenceModelType::RvcV2,
InferenceModelType::SoVitsSvc,
]);
assert_eq!(model_type_predicate(&types), "\"rvc_v2\", \"so_vits_svc\"".to_string());
}
#[test]
fn test_inference_category_predicate() {
// None
let types = BTreeSet::from([]);
assert_eq!(inference_category_predicate(&types), "".to_string());
// Some
let types = BTreeSet::from([
InferenceCategory::VoiceConversion,
]);
assert_eq!(inference_category_predicate(&types), "\"voice_conversion\"".to_string());
// All
let types = BTreeSet::from([
InferenceCategory::TextToSpeech,
InferenceCategory::VoiceConversion,
]);
assert_eq!(inference_category_predicate(&types), "\"text_to_speech\", \"voice_conversion\"".to_string());
}
}
@@ -1,5 +1,4 @@
pub mod _keys;
pub mod count_untried_jobs_of_type;
pub mod list_available_generic_inference_jobs;
pub mod mark_generic_inference_job_completely_failed;
pub mod mark_generic_inference_job_failure;
@@ -1,48 +0,0 @@
use anyhow::anyhow;
use sqlx;
use sqlx::pool::PoolConnection;
use sqlx::{MySql, MySqlPool};
use errors::AnyhowResult;
/// Kill a handful of jobs when called.
/// This should only be used in development to keep the job queue empty.
pub async fn kill_jobs_in_development(mysql_pool: &MySqlPool) -> AnyhowResult<()>
{
let mut connection = mysql_pool.acquire().await?;
kill_jobs_in_development_using_connection(&mut connection).await
}
/// Kill a handful of jobs when called.
/// This should only be used in development to keep the job queue empty.
pub async fn kill_jobs_in_development_using_connection(
mysql_connection: &mut PoolConnection<MySql>,
) -> AnyhowResult<()> {
// We cancel any non-complete / non-terminal jobs
// We also mark "started" jobs in the event that they're left in a bad state.
// Any started jobs that are actually running should still run to completion, and
// the user should still see the final result even if the job system UI doesn't
// indicate it.
let query_result = sqlx::query!(
r#"
UPDATE generic_inference_jobs
SET
status = 'cancelled_by_user',
retry_at = NULL
WHERE status IN (
'pending',
'started',
'attempt_failed'
)
LIMIT 100
"#
)
.execute(&mut **mysql_connection)
.await;
if let Err(err) = query_result {
return Err(anyhow!("error with terminate job query (1): {:?}", err));
}
Ok(())
}
@@ -5,6 +5,5 @@ pub mod get_pending_inference_job_count;
pub mod insert_generic_inference_job;
pub mod job_status;
pub mod kill_generic_inference_jobs;
pub mod kill_jobs_in_development;
pub mod list_session_jobs;
pub mod mark_generic_inference_job_cancelled_by_user;
@@ -1,178 +0,0 @@
use anyhow::anyhow;
use sqlx;
use sqlx::MySqlPool;
use enums::by_table::generic_synthetic_ids::id_category::IdCategory;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use errors::AnyhowResult;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
use crate::queries::generic_synthetic_ids::transactional_increment_generic_synthetic_id::transactional_increment_generic_synthetic_id;
pub struct InsertArgs<'a> {
pub pool: &'a MySqlPool,
pub job: &'a AvailableInferenceJob,
pub maybe_mime_type: Option<&'a str>,
pub file_size_bytes: u64,
pub sha256_checksum: &'a str,
// TODO: Media duration.
//pub duration_millis: u64,
pub maybe_title: Option<&'a str>,
pub maybe_style_transfer_source_media_file_token: Option<&'a MediaFileToken>,
pub maybe_scene_source_media_file_token: Option<&'a MediaFileToken>,
pub maybe_prompt_token: Option<&'a PromptToken>,
pub public_bucket_directory_hash: &'a str,
pub maybe_public_bucket_prefix: Option<&'a str>,
pub maybe_public_bucket_extension: Option<&'a str>,
pub is_on_prem: bool,
pub worker_hostname: &'a str,
pub worker_cluster: &'a str,
}
pub async fn insert_media_file_from_studio_gen2(args: InsertArgs<'_>) -> AnyhowResult<(MediaFileToken, u64)>
{
let result_token = MediaFileToken::generate();
let mut maybe_creator_file_synthetic_id : Option<u64> = None;
let mut maybe_creator_category_synthetic_id : Option<u64> = None;
let mut transaction = args.pool.begin().await?;
if let Some(creator_user_token) = args.job.maybe_creator_user_token.as_deref() {
let user_token = UserToken::new_from_str(creator_user_token);
let next_media_file_id = transactional_increment_generic_synthetic_id(
&user_token,
IdCategory::MediaFile,
&mut transaction
).await?;
let next_comfy_ui_id = transactional_increment_generic_synthetic_id(
&user_token,
IdCategory::WorkflowResult,
&mut transaction
).await?;
maybe_creator_file_synthetic_id = Some(next_media_file_id);
maybe_creator_category_synthetic_id = Some(next_comfy_ui_id);
}
const ORIGIN_CATEGORY : MediaFileOriginCategory = MediaFileOriginCategory::Inference;
const PRODUCT : MediaFileOriginProductCategory = MediaFileOriginProductCategory::StorytellerStudio;
const MODEL_TYPE : MediaFileOriginModelType = MediaFileOriginModelType::StorytellerStudio;
let record_id = {
let query_result = sqlx::query!(
r#"
INSERT INTO media_files
SET
token = ?,
media_class = ?,
media_type = ?,
origin_category = ?,
origin_product_category = ?,
maybe_origin_model_type = ?,
maybe_mime_type = ?,
file_size_bytes = ?,
checksum_sha2 = ?,
maybe_style_transfer_source_media_file_token = ?,
maybe_scene_source_media_file_token = ?,
maybe_title = ?,
maybe_prompt_token = ?,
public_bucket_directory_hash = ?,
maybe_public_bucket_prefix = ?,
maybe_public_bucket_extension = ?,
maybe_creator_user_token = ?,
maybe_creator_anonymous_visitor_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
maybe_creator_file_synthetic_id = ?,
maybe_creator_category_synthetic_id = ?,
is_generated_on_prem = ?,
generated_by_worker = ?,
generated_by_cluster = ?
"#,
result_token.as_str(),
MediaFileClass::Video.to_str(),
MediaFileType::Video.to_str(), // TODO(bt,2024-04-30): This needs to become "mp4" after a frontend migration
ORIGIN_CATEGORY.to_str(),
PRODUCT.to_str(),
MODEL_TYPE.to_str(),
args.maybe_mime_type,
args.file_size_bytes,
args.sha256_checksum,
args.maybe_style_transfer_source_media_file_token.map(|t| t.as_str()),
args.maybe_scene_source_media_file_token.map(|t| t.as_str()),
args.maybe_title,
args.maybe_prompt_token.map(|e| e.as_str()),
args.public_bucket_directory_hash,
args.maybe_public_bucket_prefix,
args.maybe_public_bucket_extension,
args.job.maybe_creator_user_token,
args.job.maybe_creator_anonymous_visitor_token,
args.job.creator_ip_address,
args.job.creator_set_visibility.to_str(),
maybe_creator_file_synthetic_id,
maybe_creator_category_synthetic_id,
args.is_on_prem,
args.worker_hostname,
args.worker_cluster
)
.execute(&mut *transaction)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
// TODO: handle better
//transaction.rollback().await?;
return Err(anyhow!("Mysql error: {:?}", err));
}
};
record_id
};
transaction.commit().await?;
Ok((result_token, record_id))
}
@@ -6,7 +6,6 @@ pub mod insert_media_file_from_file_upload;
pub mod insert_media_file_from_mocapnet;
pub mod insert_media_file_from_rerender;
pub mod insert_media_file_from_seed_vc;
pub mod insert_media_file_from_studio_gen2;
pub mod insert_media_file_from_tacotron2;
pub mod insert_media_file_from_voice_conversion;
pub mod insert_media_file_from_zero_shot_tts;
-14
View File
@@ -52,14 +52,6 @@ impl ServerEnvironment {
ServerEnvironment::Production => true,
}
}
/// Reports whether the software is running in development.
pub fn is_development(&self) -> bool {
match self {
ServerEnvironment::Development => true,
ServerEnvironment::Production => false,
}
}
}
#[cfg(test)]
@@ -95,10 +87,4 @@ mod tests {
assert_eq!(true, ServerEnvironment::Production.is_deployed_in_production());
assert_eq!(false, ServerEnvironment::Development.is_deployed_in_production());
}
#[test]
fn test_is_development() {
assert_eq!(false, ServerEnvironment::Production.is_development());
assert_eq!(true, ServerEnvironment::Development.is_development());
}
}
@@ -78,7 +78,6 @@ async fn main() -> AnyhowResult<()> {
app_name: "download-job",
default_logging_override: Some(DEFAULT_RUST_LOG),
config_search_directories: &[".", "./config", "crates/service/job/download_job/config"],
ignore_legacy_dot_env_file: true,
})?;
info!("Hostname: {}", &container_environment.hostname);
@@ -54,7 +54,6 @@ async fn main() -> AnyhowResult<()> {
app_name: "email-sender-job",
default_logging_override: Some(DEFAULT_RUST_LOG),
config_search_directories: &[".", "./config", "crates/service/job/email_sender_job/config"],
ignore_legacy_dot_env_file: false,
})?;
info!("Hostname: {}", &container_environment.hostname);
@@ -44,7 +44,6 @@ async fn main() -> AnyhowResult<()> {
app_name: "es-update-job",
default_logging_override: Some(DEFAULT_RUST_LOG),
config_search_directories: &[".", "./config", "crates/service/job/es-update-job/config"],
ignore_legacy_dot_env_file: true,
})?;
info!("Hostname: {}", &container_environment.hostname);
@@ -1,11 +1,5 @@
# inference-job default .env configurations (these can be overridden with k8s)
# ================================= #
# #
# COMMON SETTINGS #
# #
# ================================= #
# NB: For my crapy internet connection
BUCKET_TIMEOUT_SECONDS=6000
@@ -18,9 +12,6 @@ REDIS_FOR_KEEPALIVE_URL="redis://redis/0"
# General model weights cache directory
MODEL_WEIGHTS_CACHE_DIR="/tmp/model_weights_cache"
FAKEYOU_WATERMARK_PATH="TODO_REPLACE_WATERMARK_PATH"
STORYTELLER_WATERMARK_PATH="TODO_REPLACE_WATERMARK_PATH"
# ================================= #
# #
# Vall-E-X #
@@ -169,31 +160,6 @@ COMFY_STYLES_DIRECTORY="/home/bt/dev/storyteller/V2V-StylesList/V2VWorkflows/sty
COMFY_WORKFLOWS_DIRECTORY="/home/bt/dev/storyteller/V2V-StylesList/V2VWorkflows/workflows"
COMFY_MAPPINGS_DIRECTORY="/home/bt/dev/storyteller/V2V-StylesList/V2VWorkflows/mappings"
# ================================= #
# #
# STUDIO GEN 2 #
# #
# ================================= #
# Feature Flags
STABLE_ANIMATOR_ENABLED="false"
ANIMATE_X_ENABLED="true"
# Animate-X
ANIMATE_X_MODEL_DIRECTORY_PATH="checkpoints/"
ANIMATE_X_ROOT_CODE_DIRECTORY="/home/bt/dev/storyteller/storyteller-ml/animation/animate-x"
ANIMATE_X_VENV_ACTIVATION_COMMAND="source venv/bin/activate"
# Stable Animator
STABLE_ANIMATOR_ROOT_CODE_DIRECTORY="/home/bt/dev/storyteller/storyteller-ml/animation/StableAnimator"
STABLE_ANIMATOR_VENV_ACTIVATION_COMMAND="source venv/bin/activate"
STABLE_ANIMATOR_PRETRAINED_MODEL_PATH="checkpoints/stable-video-diffusion-img2vid-xt"
STABLE_ANIMATOR_POSENET_MODEL_PATH="checkpoints/Animation/pose_net.pth"
STABLE_ANIMATOR_FACE_ENCODER_MODEL_PATH="checkpoints/Animation/face_encoder.pth"
STABLE_ANIMATOR_UNET_MODEL_PATH="checkpoints/Animation/unet.pth"
# ================================= #
# #
# Fbx2Gltf #
@@ -228,26 +194,20 @@ HF_DATASETS_CACHE="/tmp/huggingface"
NLTK_DATA="/tmp/nltk_data"
SKIP_TELEMETRY="true"
#SCOPED_EXECUTION_JOB_TYPES="gpt_sovits"
SCOPED_EXECUTION_JOB_TYPES="gpt_sovits"
DOWNLOAD_SCRIPT="/storyteller-rust/downloader/download_internet_file.py"
PRIVATE_BUCKET_NAME='brandon-dev-testing'
PUBLIC_BUCKET_NAME='brandon-dev-testing'
#PRIVATE_BUCKET_NAME=kasisnu-test-area-private
#PUBLIC_BUCKET_NAME=kasisnu-test-area-public
PRIVATE_BUCKET_NAME=kasisnu-test-area-private
PUBLIC_BUCKET_NAME=kasisnu-test-area-public
GC_ENABLED_PUBLIC_BUCKET_NAME="REPLACE_ME"
#ACCESS_KEY=bWDc7MntZ0sl7TcM
#SECRET_KEY=ixbQ1QFQ8tCh5A5V2Otp6V4ANuLSn8ww
#S3_COMPATIBLE_ENDPOINT_URL=http://minio.minio-operator
#REGION_NAME=us-east1
S3_COMPATIBLE_ENDPOINT_URL='https://396ddf89dc0e00d773764ac6baa669bd.r2.cloudflarestorage.com'
REGION_NAME='auto'
ACCESS_KEY=bWDc7MntZ0sl7TcM
SECRET_KEY=ixbQ1QFQ8tCh5A5V2Otp6V4ANuLSn8ww
S3_COMPATIBLE_ENDPOINT_URL=http://minio.minio-operator
REGION_NAME=us-east1
MYSQL_URL=mysql://storyteller:password@mysql/storyteller
#MYSQL_URL=mysql://storyteller:password@localhost/storyteller
REDIS_0_URL=redis://redis@redis/0
REDIS_1_URL=redis://redis@redis/1
GPT_SOVITS_CODE_DIRECTORY=/model_code/GPT-SoVITS
@@ -23,7 +23,6 @@ use crate::job::job_types::image_generation::process_single_ig_job::process_sing
use crate::job::job_types::lipsync::process_single_lipsync_job::process_single_lipsync_job;
use crate::job::job_types::mocap::process_single_mc_job::process_single_mc_job;
use crate::job::job_types::render_engine_scene::process_render_engine_scene::process_single_render_engine_scene_job;
use crate::job::job_types::studio_gen2::process_single_studio_gen2_job::process_single_studio_gen2_job;
use crate::job::job_types::tts::process_single_tts_job::process_single_tts_job;
use crate::job::job_types::vc::process_single_vc_job::process_single_vc_job;
use crate::job::job_types::vc::rvc_v2::dispatch_rvc_v2_job::dispatch_rvc_v2_job;
@@ -257,7 +256,6 @@ async fn do_process_single_job(
fn can_use_new_dispatch(job: &AvailableInferenceJob) -> bool {
match job.job_type {
InferenceJobType::StudioGen2 => true,
InferenceJobType::FaceFusion => true,
InferenceJobType::F5TTS => true,
InferenceJobType::GptSovits => true,
@@ -275,9 +273,6 @@ async fn new_dispatch(
) -> Result<JobSuccessResult, ProcessSingleJobError> {
let job_success_result = match job.job_type {
InferenceJobType::StudioGen2 => {
process_single_studio_gen2_job(job_dependencies, job).await?
},
InferenceJobType::VideoRender
| InferenceJobType::LivePortrait
| InferenceJobType::FaceFusion
@@ -1,12 +1,11 @@
pub mod f5_tts;
pub mod format_conversion;
pub mod gpt_sovits;
pub mod image_generation;
pub mod lipsync;
pub mod mocap;
pub mod render_engine_scene;
pub mod studio_gen2;
pub mod tts;
pub mod vc;
pub mod videofilter;
pub mod workflow;
pub mod gpt_sovits;
pub mod f5_tts;
@@ -1,21 +0,0 @@
use crate::job::job_types::studio_gen2::animate_x::animate_x_inference_command::AnimateXInferenceCommand;
use crate::job::job_types::studio_gen2::animate_x::animate_x_process_frames_command::AnimateXProcessFramesCommand;
use errors::AnyhowResult;
use std::path::PathBuf;
pub struct AnimateXDependencies {
pub process_frames_command: AnimateXProcessFramesCommand,
pub inference_command: AnimateXInferenceCommand,
pub model_directory_path: PathBuf,
}
impl AnimateXDependencies {
pub fn setup() -> AnyhowResult<Self> {
Ok(Self {
process_frames_command: AnimateXProcessFramesCommand::new_from_env()?,
inference_command: AnimateXInferenceCommand::new_from_env()?,
model_directory_path: easyenv::get_env_pathbuf_required("ANIMATE_X_MODEL_DIRECTORY_PATH")?,
})
}
}
@@ -1,349 +0,0 @@
use crate::util::get_filtered_env_vars::get_filtered_env_vars_hashmap;
use errors::AnyhowResult;
use filesys::path_to_string::path_to_string;
use log::{debug, info, warn};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use subprocess_common::command_exit_status::CommandExitStatus;
use subprocess_common::docker_options::DockerOptions;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
/*
python inference_cli.py \
--cfg configs/Animate_X_infer.yaml \
--image_file data_test_copy/images/man.png \
--pose_directory data_test_copy/saved_pose/dance_2 \
--frame_directory data_test_copy/saved_frames/dance_2 \
--pickle_data_file data_test_copy/saved_pkl/dance_2.pkl \
--model_checkpoints_directory checkpoints \
--generate_comparison_video \
--max_frames 4 \
--height 768 \
--width 512
*/
#[derive(Clone)]
pub struct AnimateXInferenceCommand {
/// Where the code lives
root_code_directory: PathBuf,
/// A single executable script or a much larger bash command.
executable_or_command: ExecutableOrCommand,
/// eg. `source python/bin/activate`
maybe_virtual_env_activation_command: Option<String>,
/// If this is run under Docker (eg. in development), these are the options.
maybe_docker_options: Option<DockerOptions>,
/// If the execution should be ended after a certain point.
maybe_execution_timeout: Option<Duration>,
}
#[derive(Clone)]
pub enum ExecutableOrCommand {
/// Eg. `inference.py`
Executable(PathBuf),
/// Eg. `python3 inference.py`
Command(String),
}
#[derive(Debug)]
pub struct AnimateXInferenceArgs<'s> {
pub stderr_output_file: &'s Path,
pub stdout_output_file: &'s Path,
pub model_directory: &'s Path,
pub result_filename: &'s Path,
pub image_file: &'s Path,
pub saved_pose_frames_dir: &'s Path,
pub saved_original_frames_dir: &'s Path,
pub saved_pose_pkl_file: &'s Path,
pub width: Option<u64>,
pub height: Option<u64>,
pub max_frames: Option<u64>,
}
impl AnimateXInferenceCommand {
pub fn new_from_env() -> AnyhowResult<Self> {
Ok(Self {
root_code_directory: easyenv::get_env_pathbuf_or_default("ANIMATE_X_ROOT_CODE_DIRECTORY", PathBuf::from("/model_code")),
executable_or_command: ExecutableOrCommand::Command("python inference_cli.py".to_string()),
maybe_virtual_env_activation_command: easyenv::get_env_string_optional("ANIMATE_X_VENV_ACTIVATION_COMMAND"),
maybe_docker_options: None,
maybe_execution_timeout: None,
})
}
pub async fn execute_inference<'a, 'b>(
&'a self,
args: AnimateXInferenceArgs<'b>,
) -> AnyhowResult<CommandExitStatus> {
info!("InferenceArgs: {:?}", &args);
let mut command = String::new();
command.push_str(&format!("cd {}", path_to_string(&self.root_code_directory)));
if let Some(venv_command) = self.maybe_virtual_env_activation_command.as_deref() {
command.push_str(" && ");
command.push_str(venv_command);
command.push_str(" ");
}
command.push_str(" && ");
match self.executable_or_command {
ExecutableOrCommand::Executable(ref executable) => {
command.push_str(&path_to_string(executable));
command.push_str(" ");
}
ExecutableOrCommand::Command(ref cmd) => {
command.push_str(cmd);
command.push_str(" ");
}
}
// NB: Hardcoded for now
command.push_str(" --cfg configs/Animate_X_infer.yaml ");
command.push_str(" --model_checkpoints_directory ");
command.push_str(&path_to_string(&args.model_directory));
command.push_str(" ");
command.push_str(" --image_file ");
command.push_str(&path_to_string(&args.image_file));
command.push_str(" ");
command.push_str(" --pose_directory ");
command.push_str(&path_to_string(&args.saved_pose_frames_dir));
command.push_str(" ");
command.push_str(" --frame_directory ");
command.push_str(&path_to_string(&args.saved_original_frames_dir));
command.push_str(" ");
command.push_str(" --pickle_data_file ");
command.push_str(&path_to_string(&args.saved_pose_pkl_file));
command.push_str(" ");
if let Some(width) = args.width {
command.push_str(" --width ");
command.push_str(&width.to_string());
command.push_str(" ");
}
if let Some(height) = args.height {
command.push_str(" --height ");
command.push_str(&height.to_string());
command.push_str(" ");
}
if let Some(max_frames) = args.max_frames {
command.push_str(" --max_frames ");
command.push_str(&max_frames.to_string());
command.push_str(" ");
}
command.push_str(" --result_filename ");
command.push_str(&path_to_string(&args.result_filename));
command.push_str(" ");
if let Some(docker_options) = self.maybe_docker_options.as_ref() {
command = docker_options.to_command_string(&command);
}
info!("Command: {:?}", command);
let env_vars = get_filtered_env_vars_hashmap();
info!("stderr will be written to file: {:?}", args.stderr_output_file.as_os_str());
let mut stderr_file = tokio::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&args.stderr_output_file)
.await?;
let mut stdout_file = tokio::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&args.stdout_output_file)
.await?;
let mut c = Command::new("bash")
.arg("-c")
.arg(&command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.envs(env_vars)
.spawn()
.expect("failed to execute process");
let stdout = c.stdout.take();
// (Kasisnu, 9/08/24) these are safe to leave dangling, when stdout is dropped,
// the reader will be dropped and the pipe will be closed
tokio::spawn(async move {
match stdout {
Some(stdout) => {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
loop {
let bytes_read = reader.read_line(&mut line).await;
match bytes_read {
Ok(bytes_read) => {
if bytes_read == 0 {
break;
}
let write_result = stdout_file.write_all(line.as_bytes()).await;
match write_result {
Ok(_) => {}
Err(e) => {
warn!("Error writing stdout: {:?}", e);
break;
}
}
print!("{}", line);
line.clear();
}
Err(e) => {
warn!("Error reading stdout: {:?}", e);
break;
}
}
}
}
None => {
warn!("No stdout available to read");
}
}
});
let stderr = c.stderr.take();
tokio::spawn(async move {
match stderr {
Some(stderr) => {
let mut reader = BufReader::new(stderr);
let mut line = String::new();
loop {
let bytes_read = reader.read_line(&mut line).await;
match bytes_read {
Ok(bytes_read) => {
if bytes_read == 0 {
break;
}
let write_result = stderr_file.write_all(line.as_bytes()).await;
match write_result {
Ok(_) => {}
Err(e) => {
warn!("Error writing stderr: {:?}", e);
break;
}
}
println!("here: {}", line);
line.clear();
}
Err(e) => {
warn!("Error reading stderr: {:?}", e);
break;
}
}
}
}
None => {
warn!("No stderr available to read");
}
}
});
let mut status = None;
let execution_start_time = std::time::Instant::now();
loop {
if let Some(execution_timeout) = self.maybe_execution_timeout {
let now = std::time::Instant::now();
if now.duration_since(execution_start_time) > execution_timeout {
info!("Execution timeout reached");
let res = c.kill().await;
match res {
Ok(_) => {
info!("Killed Studio Gen2 process");
}
Err(e) => {
info!("Error killing Studio Gen2 process: {:?}, this might leak resources", e);
}
}
status = Some(CommandExitStatus::Timeout);
break;
}
}
//// Check if the process has been cancelled
//match cancellation_receiver.try_recv() {
// Ok(_) => {
// info!("Cancelling Comfy process");
// let res = c.kill().await;
// match res {
// Ok(_) => {
// info!("Killed Comfy process");
// }
// Err(e) => {
// info!("Error killing Comfy process: {:?}, this might leak resources", e);
// }
// }
// status = Some(CommandExitStatus::Timeout);
// break;
// }
// Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
// // Do nothing
// }
// Err(tokio::sync::oneshot::error::TryRecvError::Closed) => {
// info!("Cancellation channel closed");
// break;
// }
//}
match c.try_wait() {
Ok(Some(exit_status)) => {
match exit_status.success() {
true => {
status = Some(CommandExitStatus::Success);
}
false => {
status = Some(CommandExitStatus::Failure);
}
}
}
Ok(None) => {
debug!("Studio Gen2 process is still running");
}
Err(e) => {
info!("Error attempting to wait: {:?}", e);
break;
}
}
if status.is_some() {
break;
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
Ok(status.unwrap())
}
}
@@ -1,305 +0,0 @@
use crate::util::get_filtered_env_vars::get_filtered_env_vars_hashmap;
use errors::AnyhowResult;
use filesys::path_to_string::path_to_string;
use log::{debug, info, warn};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use subprocess_common::command_exit_status::CommandExitStatus;
use subprocess_common::docker_options::DockerOptions;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
#[derive(Clone)]
pub struct AnimateXProcessFramesCommand {
/// Where the code lives
root_code_directory: PathBuf,
/// A single executable script or a much larger bash command.
executable_or_command: ExecutableOrCommand,
/// eg. `source python/bin/activate`
maybe_virtual_env_activation_command: Option<String>,
/// If this is run under Docker (eg. in development), these are the options.
maybe_docker_options: Option<DockerOptions>,
/// If the execution should be ended after a certain point.
maybe_execution_timeout: Option<Duration>,
}
#[derive(Clone)]
pub enum ExecutableOrCommand {
/// Eg. `inference.py`
Executable(PathBuf),
/// Eg. `python3 inference.py`
Command(String),
}
#[derive(Debug)]
pub struct ProcessFramesArgs<'s> {
pub stderr_output_file: &'s Path,
pub stdout_output_file: &'s Path,
pub model_directory: &'s Path,
pub source_video_path: &'s Path,
/// --saved_pose_dir, which denotes where the pkl files are stored (as pose.pkl within this directory).
pub saved_pose_pkl_dir: &'s Path,
/// --saved_pose, which is where the pose prediction frames are emitted
pub saved_pose_frames_dir: &'s Path,
/// --saved_frame_dir, which is where original frames from the video are emitted. Not sure why the model needs this.
pub saved_original_frames_dir: &'s Path,
}
impl AnimateXProcessFramesCommand {
pub fn new_from_env() -> AnyhowResult<Self> {
Ok(Self {
root_code_directory: easyenv::get_env_pathbuf_or_default("ANIMATE_X_ROOT_CODE_DIRECTORY", PathBuf::from("/model_code")),
executable_or_command: ExecutableOrCommand::Command("python process_data.py".to_string()),
maybe_virtual_env_activation_command: easyenv::get_env_string_optional("ANIMATE_X_VENV_ACTIVATION_COMMAND"),
maybe_docker_options: None,
maybe_execution_timeout: None,
})
}
pub async fn execute_inference<'a, 'b>(
&'a self,
args: ProcessFramesArgs<'b>,
) -> AnyhowResult<CommandExitStatus> {
info!("InferenceArgs: {:?}", &args);
let mut command = String::new();
command.push_str(&format!("cd {}", path_to_string(&self.root_code_directory)));
if let Some(venv_command) = self.maybe_virtual_env_activation_command.as_deref() {
command.push_str(" && ");
command.push_str(venv_command);
command.push_str(" ");
}
command.push_str(" && ");
match self.executable_or_command {
ExecutableOrCommand::Executable(ref executable) => {
command.push_str(&path_to_string(executable));
command.push_str(" ");
}
ExecutableOrCommand::Command(ref cmd) => {
command.push_str(cmd);
command.push_str(" ");
}
}
// NB: arg is "paths" not "path"
command.push_str(" --source_video_paths ");
command.push_str(&path_to_string(&args.source_video_path));
command.push_str(" ");
command.push_str(" --saved_pose_dir ");
command.push_str(&path_to_string(&args.saved_pose_pkl_dir));
command.push_str(" ");
command.push_str(" --saved_pose ");
command.push_str(&path_to_string(&args.saved_pose_frames_dir));
command.push_str(" ");
command.push_str(" --saved_frame_dir ");
command.push_str(&path_to_string(&args.saved_original_frames_dir));
command.push_str(" ");
command.push_str(" --model_directory ");
command.push_str(&path_to_string(&args.model_directory));
command.push_str(" ");
if let Some(docker_options) = self.maybe_docker_options.as_ref() {
command = docker_options.to_command_string(&command);
}
info!("Command: {:?}", command);
let env_vars = get_filtered_env_vars_hashmap();
info!("stderr will be written to file: {:?}", args.stderr_output_file.as_os_str());
let mut stderr_file = tokio::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&args.stderr_output_file)
.await?;
let mut stdout_file = tokio::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&args.stdout_output_file)
.await?;
let mut c = Command::new("bash")
.arg("-c")
.arg(&command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.envs(env_vars)
.spawn()
.expect("failed to execute process");
let stdout = c.stdout.take();
// (Kasisnu, 9/08/24) these are safe to leave dangling, when stdout is dropped,
// the reader will be dropped and the pipe will be closed
tokio::spawn(async move {
match stdout {
Some(stdout) => {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
loop {
let bytes_read = reader.read_line(&mut line).await;
match bytes_read {
Ok(bytes_read) => {
if bytes_read == 0 {
break;
}
let write_result = stdout_file.write_all(line.as_bytes()).await;
match write_result {
Ok(_) => {}
Err(e) => {
warn!("Error writing stdout: {:?}", e);
break;
}
}
print!("{}", line);
line.clear();
}
Err(e) => {
warn!("Error reading stdout: {:?}", e);
break;
}
}
}
}
None => {
warn!("No stdout available to read");
}
}
});
let stderr = c.stderr.take();
tokio::spawn(async move {
match stderr {
Some(stderr) => {
let mut reader = BufReader::new(stderr);
let mut line = String::new();
loop {
let bytes_read = reader.read_line(&mut line).await;
match bytes_read {
Ok(bytes_read) => {
if bytes_read == 0 {
break;
}
let write_result = stderr_file.write_all(line.as_bytes()).await;
match write_result {
Ok(_) => {}
Err(e) => {
warn!("Error writing stderr: {:?}", e);
break;
}
}
println!("here: {}", line);
line.clear();
}
Err(e) => {
warn!("Error reading stderr: {:?}", e);
break;
}
}
}
}
None => {
warn!("No stderr available to read");
}
}
});
let mut status = None;
let execution_start_time = std::time::Instant::now();
loop {
if let Some(execution_timeout) = self.maybe_execution_timeout {
let now = std::time::Instant::now();
if now.duration_since(execution_start_time) > execution_timeout {
info!("Execution timeout reached");
let res = c.kill().await;
match res {
Ok(_) => {
info!("Killed Studio Gen2 process");
}
Err(e) => {
info!("Error killing Studio Gen2 process: {:?}, this might leak resources", e);
}
}
status = Some(CommandExitStatus::Timeout);
break;
}
}
//// Check if the process has been cancelled
//match cancellation_receiver.try_recv() {
// Ok(_) => {
// info!("Cancelling Comfy process");
// let res = c.kill().await;
// match res {
// Ok(_) => {
// info!("Killed Comfy process");
// }
// Err(e) => {
// info!("Error killing Comfy process: {:?}, this might leak resources", e);
// }
// }
// status = Some(CommandExitStatus::Timeout);
// break;
// }
// Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
// // Do nothing
// }
// Err(tokio::sync::oneshot::error::TryRecvError::Closed) => {
// info!("Cancellation channel closed");
// break;
// }
//}
match c.try_wait() {
Ok(Some(exit_status)) => {
match exit_status.success() {
true => {
status = Some(CommandExitStatus::Success);
}
false => {
status = Some(CommandExitStatus::Failure);
}
}
}
Ok(None) => {
debug!("Studio Gen2 process is still running");
}
Err(e) => {
info!("Error attempting to wait: {:?}", e);
break;
}
}
if status.is_some() {
break;
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
Ok(status.unwrap())
}
}
@@ -1,3 +0,0 @@
pub mod animate_x_dependencies;
pub mod animate_x_inference_command;
pub mod animate_x_process_frames_command;
@@ -1,94 +0,0 @@
use std::path::{Path, PathBuf};
use anyhow::anyhow;
use log::{error, info};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use cloud_storage::remote_file_manager::remote_cloud_file_manager::RemoteCloudFileClient;
use filesys::path_to_string::path_to_string;
use mysql_queries::queries::media_files::get::get_media_file::{get_media_file_with_transactor, MediaFile};
use mysql_queries::utils::transactor::Transactor;
use tokens::tokens::media_files::MediaFileToken;
use crate::job::job_loop::process_single_job_error::ProcessSingleJobError;
use crate::job::job_types::studio_gen2::studio_gen2_dirs::StudioGen2Dirs;
const DEFAULT_SUFFIX : &str = ".jpg";
pub struct DownloadDetails {
pub media_file: MediaFile,
pub file_path: PathBuf,
}
pub struct DownloadFileForStudioArgs<'a> {
pub media_token: &'a MediaFileToken,
pub input_paths: &'a StudioGen2Dirs,
pub filename_without_extension: &'a str,
pub remote_cloud_file_client: &'a RemoteCloudFileClient,
}
pub async fn download_file_for_studio(
args: DownloadFileForStudioArgs<'_>,
transactor: Transactor<'_, '_>,
) -> Result<DownloadDetails, ProcessSingleJobError> {
info!("Querying input media file by token: {:?} ...", &args.media_token);
let input_media_file = get_media_file_with_transactor(
&args.media_token,
true,
transactor,
).await?.ok_or_else(|| {
error!("media_file not found: {:?}", &args.media_token);
ProcessSingleJobError::Other(anyhow!("media_file not found: {:?}", &args.media_token))
})?;
let media_file_bucket_path = MediaFileBucketPath::from_object_hash(
&input_media_file.public_bucket_directory_hash,
input_media_file.maybe_public_bucket_prefix.as_deref(),
input_media_file.maybe_public_bucket_extension.as_deref());
info!("media file cloud bucket path: {:?}", media_file_bucket_path.get_full_object_path_str());
let suffix = get_suffix(&input_media_file);
let filesystem_path = args.input_paths.input_dir.path()
.join(format!("{}{}", args.filename_without_extension, suffix));
info!("Downloading input file to {:?}", &filesystem_path);
args.remote_cloud_file_client.download_media_file(
&media_file_bucket_path,
path_to_string(&filesystem_path)
).await?;
info!("Downloaded file!");
Ok(DownloadDetails {
media_file: input_media_file,
file_path: filesystem_path,
})
}
fn get_suffix(input_media_file: &MediaFile) -> String {
let mut suffix;
if let Some(extension) = input_media_file.maybe_public_bucket_extension.as_deref() {
suffix = extension.to_string();
} else if let Some(mime_type) = input_media_file.maybe_mime_type.as_deref() {
suffix = match mime_type {
"image/jpeg" => ".jpg".to_string(),
"image/png" => ".png".to_string(),
"video/mp4" => ".mp4".to_string(),
_ => DEFAULT_SUFFIX.to_string(),
}
} else {
suffix = DEFAULT_SUFFIX.to_string();
}
if !suffix.starts_with(".") {
suffix = format!(".{}", suffix);
}
suffix
}
@@ -1,8 +0,0 @@
pub mod animate_x;
pub mod download_file_for_studio;
pub mod process_single_studio_gen2_job;
pub mod stable_animator;
pub mod studio_gen2_dependencies;
pub mod studio_gen2_dirs;
pub mod validate_and_save_results;
mod resize_image_for_studio;
@@ -1,532 +0,0 @@
use crate::job::job_loop::job_success_result::{JobSuccessResult, ResultEntity};
use crate::job::job_loop::process_single_job_error::ProcessSingleJobError;
use crate::job::job_types::studio_gen2::animate_x::animate_x_dependencies::AnimateXDependencies;
use crate::job::job_types::studio_gen2::animate_x::animate_x_inference_command::AnimateXInferenceArgs;
use crate::job::job_types::studio_gen2::animate_x::animate_x_process_frames_command::ProcessFramesArgs;
use crate::job::job_types::studio_gen2::download_file_for_studio::{download_file_for_studio, DownloadDetails, DownloadFileForStudioArgs};
use crate::job::job_types::studio_gen2::resize_image_for_studio::{resize_image_for_studio, ImageResizeType};
use crate::job::job_types::studio_gen2::stable_animator::stable_animator_command::InferenceArgs;
use crate::job::job_types::studio_gen2::stable_animator::stable_animator_dependencies::StableAnimatorDependencies;
use crate::job::job_types::studio_gen2::studio_gen2_dirs::StudioGen2Dirs;
use crate::job::job_types::studio_gen2::validate_and_save_results::{validate_and_save_results, SaveResultsArgs};
use crate::state::job_dependencies::JobDependencies;
use crate::util::common_commands::ffmpeg::ffmpeg_resample_duration::FfmpegResampleDurationArgs;
use crate::util::common_commands::ffmpeg::ffmpeg_resample_fps_and_duration::FfmpegResampleFpsAndDurationArgs;
use crate::util::common_commands::ffmpeg::ffmpeg_resample_fps_args::FfmpegResampleFpsArgs;
use anyhow::anyhow;
use cloud_storage::remote_file_manager::remote_cloud_file_manager::RemoteCloudFileClient;
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
use enums::by_table::media_files::media_file_type::MediaFileType;
use filesys::check_file_exists::check_file_exists;
use filesys::create_dir_all_if_missing::create_dir_all_if_missing;
use filesys::file_deletion::safe_delete_possible_files_and_directories::safe_delete_possible_files_and_directories;
use log::{error, info, warn};
use mysql_queries::payloads::generic_inference_args::generic_inference_args::PolymorphicInferenceArgs::{Cu, S2};
use mysql_queries::payloads::generic_inference_args::inner_payloads::studio_gen2_payload::StudioGen2Payload;
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
use mysql_queries::utils::transactor::Transactor;
use mysql_queries::queries::generic_inference::job::count_untried_jobs_of_type::count_untried_jobs_of_type;
use mysql_queries::queries::generic_inference::job::count_untried_jobs_of_type::CountUntriedJobsOfTypeArgs;
use std::fs::read_to_string;
use std::path::Path;
use std::time::{Duration, Instant};
use sqlx::MySqlPool;
use subprocess_common::command_runner::command_args::CommandArgs;
use subprocess_common::command_runner::command_runner_args::{RunAsSubprocessArgs, StreamRedirection};
use videos::ffprobe_get_dimensions::ffprobe_get_dimensions;
enum StudioModelPipeline<'a> {
None,
AnimateX(&'a AnimateXDependencies),
StableAnimator(&'a StableAnimatorDependencies),
}
pub async fn process_single_studio_gen2_job(
deps: &JobDependencies,
job: &AvailableInferenceJob
) -> Result<JobSuccessResult, ProcessSingleJobError> {
let mut job_progress_reporter = deps
.clients
.job_progress_reporter
.new_generic_inference(job.inference_job_token.as_str())
.map_err(|e| ProcessSingleJobError::Other(anyhow!(e)))?;
let gen2_deps = deps
.job
.job_specific_dependencies
.maybe_studio_gen2_dependencies
.as_ref()
.ok_or_else(|| ProcessSingleJobError::JobSystemMisconfiguration(Some("Missing Studio Gen2 dependencies".to_string())))?;
// ==================== UNPACK + VALIDATE INFERENCE ARGS ==================== //
let mut studio_job_type = StudioModelPipeline::None;
if let Some(deps) = gen2_deps.stable_animator.as_ref() {
studio_job_type = StudioModelPipeline::StableAnimator(deps);
} else if let Some(deps) = gen2_deps.animate_x.as_ref() {
studio_job_type = StudioModelPipeline::AnimateX(deps);
}
let work_paths = StudioGen2Dirs::new(&gen2_deps)?;
info!("Input path: {:?}", &work_paths.input_dir.path());
info!("Output path: {:?}", &work_paths.output_dir.path());
let remote_cloud_file_client = RemoteCloudFileClient::get_remote_cloud_file_client()
.await
.map_err(|e| ProcessSingleJobError::Other(anyhow!("failed to get remote cloud file client: {:?}", e)))?;
info!("Grabbing mysql connection from pool");
let mut mysql_connection = deps.db.mysql_pool.acquire()
.await
.map_err(|e| {
warn!("Could not acquire DB pool: {:?}", e);
ProcessSingleJobError::Other(anyhow!("Could not acquire DB pool: {:?}", e))
})?;
let studio_args = job.maybe_inference_args
.as_ref()
.map(|args| args.args.as_ref())
.flatten()
.ok_or_else(|| ProcessSingleJobError::Other(anyhow!("Job args not found")))
.map(|poly_args| match poly_args {
S2(args) => Ok(args),
_ => return Err(ProcessSingleJobError::Other(anyhow!("Studio Gen2 args not found"))),
})??;
info!("Studio args: {:?}", studio_args);
// ==================== DOWNLOAD IMAGE ==================== //
let unaltered_image_file;
match studio_args.image_file.as_ref() {
None => return Err(ProcessSingleJobError::Other(anyhow!("image_file not set"))),
Some(media_token) => {
unaltered_image_file = download_file_for_studio(DownloadFileForStudioArgs {
media_token,
input_paths: &work_paths,
remote_cloud_file_client: &remote_cloud_file_client,
filename_without_extension: "input_image",
}, Transactor::for_connection(&mut mysql_connection)).await?;
info!("Downloaded image to {:?}", &unaltered_image_file.file_path);
}
}
assert_file_is_image(&unaltered_image_file)?;
// ==================== DOWNLOAD VIDEO ==================== //
let unaltered_video_file;
match studio_args.video_file.as_ref() {
None => return Err(ProcessSingleJobError::Other(anyhow!("video_file not set"))),
Some(media_token) => {
unaltered_video_file = download_file_for_studio(DownloadFileForStudioArgs {
media_token,
input_paths: &work_paths,
remote_cloud_file_client: &remote_cloud_file_client,
filename_without_extension: "input_video",
}, Transactor::for_connection(&mut mysql_connection)).await?;
info!("Downloaded video to {:?}", &unaltered_video_file.file_path);
}
}
assert_file_is_video(&unaltered_video_file)?;
//if let Ok(Some(dimensions)) = ffprobe_get_dimensions(&videos.primary_video.original_download_path) {
// info!("Download video dimensions: {}x{}", dimensions.width, dimensions.height);
//}
// ========================= TRIM AND PREPROCESS VIDEO ======================== //
//const RESAMPLE_FRAME_RATE : u64 = 30; // TODO(bt,2025-02-02): Upstream we produce 60fps out of studio. We should do 24fps
// TODO REPLACE
let unaltered_video_path = unaltered_video_file.file_path.clone();
let resample_output_video_path = work_paths.output_dir.path().join("resampled_input.mp4");
let maybe_duration_and_fps
= (studio_args.trim_duration_millis, studio_args.fps);
let maybe_ffmpeg_args : Option<Box<dyn CommandArgs>> =
match maybe_duration_and_fps {
(Some(duration_millis), Some(fps)) => Some(Box::new(FfmpegResampleFpsAndDurationArgs {
input_video_file: &unaltered_video_path,
output_video_file: &resample_output_video_path,
fps: fps as usize,
trim_to_duration: Duration::from_millis(duration_millis),
})),
(Some(duration_millis), None) => Some(Box::new(FfmpegResampleDurationArgs {
input_video_file: &unaltered_video_path,
output_video_file: &resample_output_video_path,
trim_to_duration: Duration::from_millis(duration_millis),
})),
(None, Some(fps)) => Some(Box::new(FfmpegResampleFpsArgs {
input_video_file: &unaltered_video_path,
output_video_file: &resample_output_video_path,
fps: fps as usize,
})),
(None, None) => None,
};
let inference_input_video_path;
match maybe_ffmpeg_args.as_deref() {
None => {
// No new resampled file.
inference_input_video_path = unaltered_video_file.file_path.clone();
info!("Not resampling video file. Will use the following as input: {:?}", &inference_input_video_path);
}
Some(ffmpeg_args) => {
info!("Resampling video file. Will use the following args to ffmpeg: {:?}", &ffmpeg_args.to_command_string());
let command_exit_status = gen2_deps.ffmpeg
.run_with_subprocess(RunAsSubprocessArgs {
args: Box::new(ffmpeg_args),
stderr: StreamRedirection::None,
stdout: StreamRedirection::None,
});
if !command_exit_status.is_success() {
inference_input_video_path = unaltered_video_file.file_path.clone();
error!("Resample video failed: {:?} ; we'll revert to the original.", command_exit_status);
} else {
inference_input_video_path = resample_output_video_path.clone();
info!("Video resample successful. Will use the following as input: {:?}", &inference_input_video_path);
}
}
}
// ========================= RESIZE IMAGE ======================== //
let skip_image_resize = studio_args.skip_image_resize.unwrap_or(false);
let mut inference_input_image_path = unaltered_image_file.file_path.clone();
let mut maybe_dimensions = None;
if !skip_image_resize {
info!("Resizing input image...");
let resized_image_path = work_paths.output_dir.path().join("resized_image.png");
let image_resize_type = match studio_job_type {
StudioModelPipeline::None => return Err(ProcessSingleJobError::Other(anyhow!("Studio job type not set"))),
StudioModelPipeline::AnimateX(_) => ImageResizeType::AnimateX,
StudioModelPipeline::StableAnimator(_) => ImageResizeType::StableAnimator,
};
let dimensions = resize_image_for_studio(&unaltered_image_file.file_path, &resized_image_path, image_resize_type)?;
inference_input_image_path = resized_image_path;
maybe_dimensions = Some(dimensions);
}
// ==================== RUN INFERENCE ==================== //
info!("Preparing for studio inference...");
job_progress_reporter.log_status("running inference")
.map_err(|e| ProcessSingleJobError::Other(e))?;
let stderr_output_file = work_paths.output_dir.path().join("stderr.txt");
let stdout_output_file = work_paths.output_dir.path().join("stdout.txt");
let video_output_path = work_paths.output_dir.path().join("output_video.mp4");
let inference_duration;
match studio_job_type {
StudioModelPipeline::None => {
return Err(ProcessSingleJobError::Other(anyhow!("Studio job type not set")));
}
StudioModelPipeline::AnimateX(deps) => {
info!("Running Studio Gen2 pose frame generation (Animate-X)...");
let pose_pkl_dir= work_paths.output_dir.path().join("pose_pickle_data");
create_dir_all_if_missing(&pose_pkl_dir)?;
let pose_pkl_file = pose_pkl_dir.join("pose.pkl");
let pose_frames_dir = work_paths.output_dir.path().join("pose_frames");
create_dir_all_if_missing(&pose_frames_dir)?;
let original_frames_dir = work_paths.output_dir.path().join("original_frames");
create_dir_all_if_missing(&original_frames_dir)?;
let inference_start_time = Instant::now();
let command_exit_status = deps
.process_frames_command
.execute_inference(ProcessFramesArgs {
stderr_output_file: &stderr_output_file,
stdout_output_file: &stdout_output_file,
model_directory: &deps.model_directory_path,
source_video_path: &inference_input_video_path,
saved_pose_pkl_dir: &pose_pkl_dir,
saved_pose_frames_dir: &pose_frames_dir,
saved_original_frames_dir: &original_frames_dir,
}).await;
info!("Running Studio Gen2 inference (Animate-X)...");
let command_exit_status = deps
.inference_command
.execute_inference(AnimateXInferenceArgs {
stderr_output_file: &stderr_output_file,
stdout_output_file: &stdout_output_file,
model_directory: &deps.model_directory_path,
image_file: &inference_input_image_path,
saved_pose_pkl_file: &pose_pkl_file,
saved_pose_frames_dir: &pose_frames_dir,
saved_original_frames_dir: &original_frames_dir,
width: studio_args.tensor_image_width,
height: studio_args.tensor_image_height,
//width: maybe_dimensions.as_ref().map(|m| m.width as u64),
//height: maybe_dimensions.as_ref().map(|m| m.height as u64),
max_frames: studio_args.max_frames,
result_filename: &video_output_path,
}).await;
inference_duration = Instant::now().duration_since(inference_start_time);
}
StudioModelPipeline::StableAnimator(deps) => {
let pose_frames_dir = work_paths.output_dir.path().join("pose_frames");
create_dir_all_if_missing(&pose_frames_dir)?;
let video_frames_output_dir = work_paths.output_dir.path().join("final_frames");
create_dir_all_if_missing(&video_frames_output_dir)?;
info!("Running Studio Gen2 inference (Stable Animator)...");
let inference_start_time = Instant::now();
let command_exit_status = deps
.command
.execute_inference(
InferenceArgs {
stderr_output_file: &stderr_output_file,
stdout_output_file: &stdout_output_file,
start_image_path: &inference_input_image_path,
pre_pose_video_path: Some(inference_input_video_path.as_ref()),
pose_images_dir: &pose_frames_dir,
frame_output_dir: &video_frames_output_dir,
video_output_path: &video_output_path,
pretrained_model_name_or_path: &deps.pretrained_model_name_or_path,
posenet_model_name_or_path: &deps.posenet_model_name_or_path,
face_encoder_model_name_or_path: &deps.face_encoder_model_name_or_path,
unet_model_name_or_path: &deps.unet_model_name_or_path,
output_width: studio_args.output_width,
output_height: studio_args.output_height,
output_fps: studio_args.fps,
}).await;
inference_duration = Instant::now().duration_since(inference_start_time);
info!("Inference command exited with status: {:?}", command_exit_status);
info!("Inference took duration to complete: {:?}", &inference_duration);
}
}
// check stdout for success and check if file exists
if let Ok(contents) = read_to_string(&stdout_output_file) {
info!("Captured stdout output: {}", contents);
}
if let Ok(contents) = read_to_string(&stderr_output_file) {
info!("Captured stderr output: {}", contents);
}
//if let Ok(Some(dimensions)) = ffprobe_get_dimensions(&videos.primary_video.comfy_output_video_path) {
// info!("Comfy output video dimensions: {}x{}", dimensions.width, dimensions.height);
//}
// ==================== CHECK OUTPUT FILE ======================== //
if let Err(err) = check_file_exists(&video_output_path) {
error!("Output file does not exist: {:?}", err);
//error!("Inference failed: {:?}", command_exit_status);
if let Ok(contents) = read_to_string(&stderr_output_file) {
warn!("Captured stderr output: {}", contents);
}
maybe_debug_sleep(studio_args).await;
// NB: Forcing generic type to `&Path` with turbofish
safe_delete_possible_files_and_directories::<&Path>(&[
Some(work_paths.input_dir.path()),
Some(work_paths.output_dir.path()),
]);
return Err(ProcessSingleJobError::Other(anyhow!("Output file did not exist: {:?}",
&video_output_path)));
}
//// ==================== COPY BACK AUDIO ==================== //
//post_process_restore_audio(PostProcessRestoreVideoArgs {
// comfy_deps: gen2_deps,
// videos: &mut videos,
//});
//// ==================== OPTIONAL WATERMARK ==================== //
//post_process_add_watermark(PostProcessAddWatermarkArgs {
// comfy_deps: gen2_deps,
// videos: &mut videos,
//});
// ==================== DEBUG ======================== //
if let Ok(Some(dimensions)) = ffprobe_get_dimensions(&video_output_path) {
info!("Final video upload dimensions: {}x{}", dimensions.width, dimensions.height);
}
// ==================== VALIDATE AND SAVE RESULTS ======================== //
let result = validate_and_save_results(SaveResultsArgs {
job,
deps: &deps,
gen2_deps,
studio_args,
output_video_path: &video_output_path,
job_progress_reporter: &mut job_progress_reporter,
inference_duration,
}).await;
let media_file_token = match result {
Ok(token) => token,
Err(err) => {
error!("Error validating and saving results: {:?}", err);
print_work_dirs(&work_paths);
maybe_debug_sleep(studio_args).await;
// NB: Forcing generic type to `&Path` with turbofish
safe_delete_possible_files_and_directories::<&Path>(&[
Some(work_paths.input_dir.path()),
Some(work_paths.output_dir.path()),
]);
return Err(err);
}
};
// ==================== CLEANUP/ DELETE TEMP FILES ==================== //
info!("Inference took duration to complete: {:?}", &inference_duration);
print_work_dirs(&work_paths);
maybe_debug_sleep_polling_jobs(studio_args, &deps.db.mysql_pool).await;
info!("Cleaning up temporary files...");
// NB: Forcing generic type to `&Path` with turbofish
safe_delete_possible_files_and_directories::<&Path>(&[
Some(work_paths.input_dir.path()),
Some(work_paths.output_dir.path()),
]);
// ==================== DONE ==================== //
info!("Studio Gen2 Done.");
job_progress_reporter.log_status("done")
.map_err(|e| ProcessSingleJobError::Other(e))?;
info!("Result video media token: {:?}", &media_file_token);
info!("Job {:?} complete success!", job.id);
Ok(JobSuccessResult {
maybe_result_entity: Some(ResultEntity {
entity_type: InferenceResultType::MediaFile,
entity_token: media_file_token.to_string(),
}),
inference_duration,
})
}
async fn maybe_debug_sleep(args: &StudioGen2Payload) {
if let Some(sleep_millis) = args.after_job_debug_sleep_millis {
info!("Debug sleeping for millis: {sleep_millis}");
// We wait for the specified timeout. If a SIGTERM is received during this time,
// we'll panic and exit the program.
// https://tokio.rs/tokio/tutorial/select
// https://tokio.rs/tokio/topics/shutdown
tokio::select! {
_ = tokio::signal::ctrl_c() => {
panic!("Ctrl-C signal received, shutting down.");
},
_ = tokio::time::sleep(Duration::from_millis(sleep_millis)) => {},
}
}
}
async fn maybe_debug_sleep_polling_jobs(args: &StudioGen2Payload, mysql_pool: &MySqlPool) {
if let Some(sleep_millis) = args.after_job_debug_sleep_millis {
info!("Debug sleeping for millis: {sleep_millis}");
// We wait for the specified timeout. If a SIGTERM is received during this time,
// we'll panic and exit the program.
// https://tokio.rs/tokio/tutorial/select
// https://tokio.rs/tokio/topics/shutdown
tokio::select! {
_ = tokio::signal::ctrl_c() => {
panic!("Ctrl-C signal received, shutting down.");
},
_ = {
let wait_until = Instant::now().checked_add(Duration::from_millis(sleep_millis));
while wait_until.gt(Instant::now()) {
let result = count_untried_jobs_of_type(CountUntriedJobsOfTypeArgs {
maybe_scope_by_job_type: None,
maybe_scope_by_model_type: None,
maybe_scope_by_job_category: None,
mysql_pool: mysql_pool,
}).await;
if let Ok(res) = result {
if res.job_count > 0 {
return;
}
}
}
},
}
}
}
fn print_work_dirs(work_dirs: &StudioGen2Dirs) {
info!("Temp input directory: {:?}", work_dirs.input_dir.path());
info!("Temp output directory: {:?}", work_dirs.input_dir.path());
}
fn assert_file_is_image(file: &DownloadDetails) -> Result<(), ProcessSingleJobError> {
match file.media_file.media_type {
MediaFileType::Image |
MediaFileType::Jpg |
MediaFileType::Png |
MediaFileType::Gif => Ok(()),
_ => Err(ProcessSingleJobError::Other(anyhow!(
"Wrong file type for image: {:?} and token: {:?}",
file.media_file.media_type, file.media_file.token)))
}
}
fn assert_file_is_video(file: &DownloadDetails) -> Result<(), ProcessSingleJobError> {
match file.media_file.media_type {
MediaFileType::Video |
MediaFileType::Mp4 => Ok(()),
_ => Err(ProcessSingleJobError::Other(anyhow!(
"Wrong file type for video: {:?} and token: {:?}",
file.media_file.media_type, file.media_file.token)))
}
}
@@ -1,83 +0,0 @@
use crate::job::job_loop::process_single_job_error::ProcessSingleJobError;
use anyhow::anyhow;
use images::image::io::Reader;
use images::resize_preserving_aspect::resize_preserving_aspect;
use log::info;
use std::fs::{read_to_string, File};
use std::io::BufReader;
use std::path::{Path, PathBuf};
#[derive(Copy,Clone)]
pub enum ImageResizeType {
AnimateX,
StableAnimator,
}
pub struct Dimensions {
pub width: u32,
pub height: u32,
}
impl ImageResizeType {
/// The maximum for the large dimension of a horizontal or vertical image
fn max_large_dimension(&self) -> u32 {
match self {
ImageResizeType::AnimateX => 768,
ImageResizeType::StableAnimator => 1024,
}
}
/// The maximum for the small dimension of a horizontal or vertical image
fn max_small_dimension(&self) -> u32 {
match self {
ImageResizeType::AnimateX => 512,
ImageResizeType::StableAnimator => 576,
}
}
/// The maximum dimension for a square image
fn max_square_dimension(&self) -> u32 {
512
}
}
pub fn resize_image_for_studio(input_image_path: &Path, output_image_path: &Path, image_resize_type: ImageResizeType) -> Result<Dimensions, ProcessSingleJobError> {
// NB(bt,2025-01-31): Using non-tokio blocking reads for now due to better compatability
// with image processing libraries. We can update this in the future.
let file = File::open(input_image_path)?;
let reader = BufReader::new(file);
let reader = Reader::new(reader)
.with_guessed_format()?;
let image = reader.decode()
.map_err(|err| anyhow!("Could not decode image: {:?}", err))?;
info!("Original image is {}x{}", image.width(), image.height());
let width_bounds;
let height_bounds;
if image.width() > image.height() {
width_bounds = image_resize_type.max_large_dimension();;
height_bounds = image_resize_type.max_small_dimension();
} else if image.height() > image.width() {
width_bounds = image_resize_type.max_small_dimension();
height_bounds = image_resize_type.max_large_dimension();
} else {
width_bounds = image_resize_type.max_square_dimension();
height_bounds = image_resize_type.max_square_dimension();
}
info!("Desired resize bounds {}x{}", width_bounds, height_bounds);
let resized_image = resize_preserving_aspect(&image, width_bounds, height_bounds, false);
info!("Resized image to {}x{}", resized_image.width(), resized_image.height());
resized_image.save(output_image_path)
.map_err(|err| anyhow!("Could not save resized image to {:?}: {:?}", output_image_path, err))?;
Ok(Dimensions {
width: resized_image.width(),
height: resized_image.height(),
})
}
@@ -1,2 +0,0 @@
pub mod stable_animator_command;
pub mod stable_animator_dependencies;
@@ -1,359 +0,0 @@
use crate::util::get_filtered_env_vars::get_filtered_env_vars_hashmap;
use errors::AnyhowResult;
use filesys::path_to_string::path_to_string;
use log::{debug, info, warn};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::Duration;
use subprocess_common::command_exit_status::CommandExitStatus;
use subprocess_common::docker_options::DockerOptions;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
#[derive(Clone)]
pub struct StableAnimatorCommand {
/// Where the code lives
root_code_directory: PathBuf,
/// A single executable script or a much larger bash command.
executable_or_command: ExecutableOrCommand,
/// eg. `source python/bin/activate`
maybe_virtual_env_activation_command: Option<String>,
/// If this is run under Docker (eg. in development), these are the options.
maybe_docker_options: Option<DockerOptions>,
/// If the execution should be ended after a certain point.
maybe_execution_timeout: Option<Duration>,
}
#[derive(Clone)]
pub enum ExecutableOrCommand {
/// Eg. `inference.py`
Executable(PathBuf),
/// Eg. `python3 inference.py`
Command(String),
}
#[derive(Debug)]
pub struct InferenceArgs<'s> {
pub stderr_output_file: &'s Path,
pub stdout_output_file: &'s Path,
pub start_image_path: &'s Path,
pub pose_images_dir: &'s Path,
pub pre_pose_video_path: Option<&'s Path>,
pub frame_output_dir: &'s Path,
pub video_output_path: &'s Path,
pub pretrained_model_name_or_path: &'s Path,
pub posenet_model_name_or_path: &'s Path,
pub face_encoder_model_name_or_path: &'s Path,
pub unet_model_name_or_path: &'s Path,
pub output_width: Option<u64>,
pub output_height: Option<u64>,
pub output_fps: Option<u64>,
}
impl StableAnimatorCommand {
pub fn new_from_env() -> AnyhowResult<Self> {
Ok(Self {
root_code_directory: easyenv::get_env_pathbuf_or_default("STABLE_ANIMATOR_ROOT_CODE_DIRECTORY", PathBuf::from("/model_code")),
executable_or_command: ExecutableOrCommand::Command("python inference_advanced.py".to_string()),
// TODO(bt,2025-02-02): The default should be None, not Some(str).
maybe_virtual_env_activation_command: Some(easyenv::get_env_string_or_default(
"STABLE_ANIMATOR_VENV_ACTIVATION_COMMAND", "source /python_install/python/bin/activate")),
maybe_docker_options: None,
maybe_execution_timeout: None,
})
}
pub async fn execute_inference<'a, 'b>(
&'a self,
args: InferenceArgs<'b>,
) -> AnyhowResult<CommandExitStatus> {
info!("InferenceArgs: {:?}", &args);
let mut command = String::new();
command.push_str(&format!("cd {}", path_to_string(&self.root_code_directory)));
if let Some(venv_command) = self.maybe_virtual_env_activation_command.as_deref() {
command.push_str(" && ");
command.push_str(venv_command);
command.push_str(" ");
}
command.push_str(" && ");
match self.executable_or_command {
ExecutableOrCommand::Executable(ref executable) => {
command.push_str(&path_to_string(executable));
command.push_str(" ");
}
ExecutableOrCommand::Command(ref cmd) => {
command.push_str(cmd);
command.push_str(" ");
}
}
// TODO: Build command
command.push_str(" --start_image_path ");
command.push_str(&path_to_string(&args.start_image_path));
command.push_str(" ");
command.push_str(" --pose_images_dir ");
command.push_str(&path_to_string(&args.pose_images_dir));
command.push_str(" ");
if let Some(path) = args.pre_pose_video_path {
command.push_str(" --pre_pose_video_path ");
command.push_str(&path_to_string(path));
command.push_str(" ");
}
command.push_str(" --frame_output_dir ");
command.push_str(&path_to_string(&args.frame_output_dir));
command.push_str(" ");
command.push_str(" --video_output_path ");
command.push_str(&path_to_string(&args.video_output_path));
command.push_str(" ");
command.push_str(" --pretrained_model_name_or_path ");
command.push_str(&path_to_string(&args.pretrained_model_name_or_path));
command.push_str(" ");
command.push_str(" --posenet_model_name_or_path ");
command.push_str(&path_to_string(&args.posenet_model_name_or_path));
command.push_str(" ");
command.push_str(" --face_encoder_model_name_or_path ");
command.push_str(&path_to_string(&args.face_encoder_model_name_or_path));
command.push_str(" ");
command.push_str(" --unet_model_name_or_path ");
command.push_str(&path_to_string(&args.unet_model_name_or_path));
command.push_str(" ");
if let Some(width) = args.output_width {
command.push_str(" --width ");
command.push_str(&width.to_string());
command.push_str(" ");
}
if let Some(height) = args.output_height {
command.push_str(" --height ");
command.push_str(&height.to_string());
command.push_str(" ");
}
if let Some(fps) = args.output_fps {
command.push_str(" --fps ");
command.push_str(&fps.to_string());
command.push_str(" ");
}
// TODO(bt,2025-01-24): These should maybe be set as defaults upstream. These are from an
// inference shell script and are not the defaults in the python arg parser.
command.push_str(" --guidance_scale=3.0 ");
command.push_str(" --num_inference_steps=25 ");
command.push_str(" --tile_size=16 ");
command.push_str(" --overlap=4 ");
command.push_str(" --noise_aug_strength=0.02 ");
command.push_str(" --frames_overlap=4 ");
command.push_str(" --decode_chunk_size=4 ");
command.push_str(" --gradient_checkpointing ");;
if let Some(docker_options) = self.maybe_docker_options.as_ref() {
command = docker_options.to_command_string(&command);
}
info!("Command: {:?}", command);
let env_vars = get_filtered_env_vars_hashmap();
info!("stderr will be written to file: {:?}", args.stderr_output_file.as_os_str());
let mut stderr_file = tokio::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&args.stderr_output_file)
.await?;
let mut stdout_file = tokio::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&args.stdout_output_file)
.await?;
let mut c = Command::new("bash")
.arg("-c")
.arg(&command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.envs(env_vars)
.spawn()
.expect("failed to execute process");
let stdout = c.stdout.take();
// (Kasisnu, 9/08/24) these are safe to leave dangling, when stdout is dropped,
// the reader will be dropped and the pipe will be closed
tokio::spawn(async move {
match stdout {
Some(stdout) => {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
loop {
let bytes_read = reader.read_line(&mut line).await;
match bytes_read {
Ok(bytes_read) => {
if bytes_read == 0 {
break;
}
let write_result = stdout_file.write_all(line.as_bytes()).await;
match write_result {
Ok(_) => {}
Err(e) => {
warn!("Error writing stdout: {:?}", e);
break;
}
}
print!("{}", line);
line.clear();
}
Err(e) => {
warn!("Error reading stdout: {:?}", e);
break;
}
}
}
}
None => {
warn!("No stdout available to read");
}
}
});
let stderr = c.stderr.take();
tokio::spawn(async move {
match stderr {
Some(stderr) => {
let mut reader = BufReader::new(stderr);
let mut line = String::new();
loop {
let bytes_read = reader.read_line(&mut line).await;
match bytes_read {
Ok(bytes_read) => {
if bytes_read == 0 {
break;
}
let write_result = stderr_file.write_all(line.as_bytes()).await;
match write_result {
Ok(_) => {}
Err(e) => {
warn!("Error writing stderr: {:?}", e);
break;
}
}
println!("here: {}", line);
line.clear();
}
Err(e) => {
warn!("Error reading stderr: {:?}", e);
break;
}
}
}
}
None => {
warn!("No stderr available to read");
}
}
});
let mut status = None;
let execution_start_time = std::time::Instant::now();
loop {
if let Some(execution_timeout) = self.maybe_execution_timeout {
let now = std::time::Instant::now();
if now.duration_since(execution_start_time) > execution_timeout {
info!("Execution timeout reached");
let res = c.kill().await;
match res {
Ok(_) => {
info!("Killed Studio Gen2 process");
}
Err(e) => {
info!("Error killing Studio Gen2 process: {:?}, this might leak resources", e);
}
}
status = Some(CommandExitStatus::Timeout);
break;
}
}
//// Check if the process has been cancelled
//match cancellation_receiver.try_recv() {
// Ok(_) => {
// info!("Cancelling Comfy process");
// let res = c.kill().await;
// match res {
// Ok(_) => {
// info!("Killed Comfy process");
// }
// Err(e) => {
// info!("Error killing Comfy process: {:?}, this might leak resources", e);
// }
// }
// status = Some(CommandExitStatus::Timeout);
// break;
// }
// Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
// // Do nothing
// }
// Err(tokio::sync::oneshot::error::TryRecvError::Closed) => {
// info!("Cancellation channel closed");
// break;
// }
//}
match c.try_wait() {
Ok(Some(exit_status)) => {
match exit_status.success() {
true => {
status = Some(CommandExitStatus::Success);
}
false => {
status = Some(CommandExitStatus::Failure);
}
}
}
Ok(None) => {
debug!("Studio Gen2 process is still running");
}
Err(e) => {
info!("Error attempting to wait: {:?}", e);
break;
}
}
if status.is_some() {
break;
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
Ok(status.unwrap())
}
}
@@ -1,25 +0,0 @@
use crate::job::job_types::studio_gen2::stable_animator::stable_animator_command::StableAnimatorCommand;
use errors::AnyhowResult;
use std::path::PathBuf;
pub struct StableAnimatorDependencies {
pub command: StableAnimatorCommand,
pub pretrained_model_name_or_path: PathBuf,
pub posenet_model_name_or_path: PathBuf,
pub face_encoder_model_name_or_path: PathBuf,
pub unet_model_name_or_path: PathBuf,
}
impl StableAnimatorDependencies {
pub fn setup() -> AnyhowResult<Self> {
Ok(Self {
command: StableAnimatorCommand::new_from_env()?,
pretrained_model_name_or_path: easyenv::get_env_pathbuf_required("STABLE_ANIMATOR_PRETRAINED_MODEL_PATH")?,
posenet_model_name_or_path: easyenv::get_env_pathbuf_required("STABLE_ANIMATOR_POSENET_MODEL_PATH")?,
face_encoder_model_name_or_path: easyenv::get_env_pathbuf_required("STABLE_ANIMATOR_FACE_ENCODER_MODEL_PATH")?,
unet_model_name_or_path: easyenv::get_env_pathbuf_required("STABLE_ANIMATOR_UNET_MODEL_PATH")?,
})
}
}
@@ -1,49 +0,0 @@
use crate::job::job_types::studio_gen2::stable_animator::stable_animator_dependencies::StableAnimatorDependencies;
use crate::state::common::watermark_configs::WatermarkConfigs;
use crate::util::common_commands::ffmpeg::runner::ffmpeg_command_runner::FfmpegCommandRunner;
use errors::AnyhowResult;
use std::path::PathBuf;
use crate::job::job_types::studio_gen2::animate_x::animate_x_dependencies::AnimateXDependencies;
pub struct StudioGen2Dependencies {
/// Watermarks added to videos (or perhaps images in the future)
pub watermarks: WatermarkConfigs,
pub input_directory: PathBuf,
pub output_directory: PathBuf,
pub ffmpeg: FfmpegCommandRunner,
pub animate_x: Option<AnimateXDependencies>,
pub stable_animator: Option<StableAnimatorDependencies>,
}
impl StudioGen2Dependencies {
pub fn setup() -> AnyhowResult<Self> {
let animate_x =
match easyenv::get_env_bool_optional("ANIMATE_X_ENABLED") {
Some(true) => Some(AnimateXDependencies::setup()?),
_ => None,
};
let stable_animator =
match easyenv::get_env_bool_optional("STABLE_ANIMATOR_ENABLED") {
Some(true) => Some(StableAnimatorDependencies::setup()?),
_ => None,
};
Ok(Self {
watermarks: WatermarkConfigs::from_env()?,
// TODO: Configurability of input/output dirs
input_directory: PathBuf::from("/tmp/input"),
output_directory: PathBuf::from("/tmp/output"),
ffmpeg: FfmpegCommandRunner::from_env()?,
animate_x,
stable_animator,
})
}
}
@@ -1,38 +0,0 @@
use crate::job::job_loop::process_single_job_error::ProcessSingleJobError;
use crate::job::job_types::studio_gen2::studio_gen2_dependencies::StudioGen2Dependencies;
use std::path::PathBuf;
use tempdir::TempDir;
/// This is a temporary measure until we start using tempdirs for intermediate inputs and outputs.
pub struct StudioGen2Dirs {
pub input_dir: TempDir,
pub output_dir: TempDir,
}
impl StudioGen2Dirs {
pub fn new(deps: &StudioGen2Dependencies) -> Result<Self, ProcessSingleJobError> {
let input_dir = deps.input_directory.join("input");
let input_dir = create_dir(input_dir)?;
let output_dir = deps.output_directory.join("output");
let output_dir = create_dir(output_dir)?;
Ok(Self {
input_dir,
output_dir,
})
}
}
fn create_dir(path: PathBuf) -> Result<TempDir, ProcessSingleJobError> {
if !path.exists() {
std::fs::create_dir_all(&path)
.map_err(|err| ProcessSingleJobError::IoError(err))?;
}
let tempdir = TempDir::new_in(&path, "studio_gen2_")
.map_err(|err| ProcessSingleJobError::IoError(err))?;
Ok(tempdir)
}
@@ -1,304 +0,0 @@
use std::path::Path;
use std::thread;
use std::time::Duration;
use anyhow::anyhow;
use log::{debug, error, info};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::prompts::prompt_type::PromptType;
use filesys::file_size::file_size;
use filesys::path_to_string::path_to_string;
use hashing::sha256::sha256_hash_file::sha256_hash_file;
use jobs_common::job_progress_reporter::job_progress_reporter::JobProgressReporter;
use mimetypes::mimetype_for_file::get_mimetype_for_file;
use mysql_queries::payloads::generic_inference_args::inner_payloads::studio_gen2_payload::StudioGen2Payload;
use mysql_queries::payloads::generic_inference_args::inner_payloads::workflow_payload::{WorkflowArgs, WorkflowType};
use mysql_queries::payloads::prompt_args::prompt_inner_payload::PromptInnerPayloadBuilder;
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
use mysql_queries::queries::media_files::create::specialized_insert::insert_media_file_from_studio_gen2::{insert_media_file_from_studio_gen2, InsertArgs};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use thumbnail_generator::task_client::thumbnail_task::{ThumbnailTaskBuilder, ThumbnailTaskInputMimeType};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use crate::job::job_loop::process_single_job_error::ProcessSingleJobError;
use crate::job::job_types::studio_gen2::studio_gen2_dependencies::StudioGen2Dependencies;
use crate::job::job_types::workflow::comfy_ui_dependencies::ComfyDependencies;
use crate::job::job_types::workflow::video_style_transfer::steps::check_and_validate_job::JobArgs;
use crate::job::job_types::workflow::video_style_transfer::util::video_pathing::VideoPathing;
use crate::state::job_dependencies::JobDependencies;
fn get_file_extension(mimetype: &str) -> anyhow::Result<&'static str> {
let ext = match mimetype {
"video/mp4" => "mp4",
"image/png" => "png",
"image/jpeg" => "jpg",
"image/gif" => "gif",
_ => return Err(anyhow!("Mimetype not supported: {}", mimetype)),
};
Ok(ext)
}
pub struct SaveResultsArgs<'a> {
pub job: &'a AvailableInferenceJob,
pub deps: &'a JobDependencies,
pub gen2_deps: &'a StudioGen2Dependencies,
pub job_progress_reporter: &'a mut Box<dyn JobProgressReporter>,
//pub videos: &'a VideoPathing,
pub output_video_path: &'a Path,
pub inference_duration: Duration,
pub studio_args : &'a StudioGen2Payload,
}
pub async fn validate_and_save_results(args: SaveResultsArgs<'_>) -> Result<MediaFileToken, ProcessSingleJobError> {
// ==================== GET METADATA ==================== //
info!("Interrogating result file size ...");
let file_size_bytes = file_size(args.output_video_path)
.map_err(|err| ProcessSingleJobError::Other(err))?;
info!("Interrogating result mimetype ...");
let mimetype = get_mimetype_for_file(args.output_video_path)
.map_err(|err| ProcessSingleJobError::from_io_error(err))?
.map(|mime| mime.to_string())
.ok_or(ProcessSingleJobError::Other(anyhow!("Mimetype could not be determined")))?;
// create ext from mimetype
let ext = get_file_extension(mimetype.as_str())
.map_err(|e| ProcessSingleJobError::Other(e))?;
// Extension is really a "suffix" and should have the leading period to act as an extension.
let ext_suffix = if ext.starts_with(".") {
ext.to_string()
} else {
format!(".{ext}")
};
const PREFIX: &str = "storyteller_";
//// determine media type from mime type
//let media_type = match mimetype.as_str() {
// "video/mp4" => MediaFileType::Video,
// "image/png" => MediaFileType::Image,
// "image/jpeg" => MediaFileType::Image,
// _ => return Err(ProcessSingleJobError::Other(anyhow!("Mimetype not supported: {}", mimetype))),
//};
info!("Calculating sha256...");
let file_checksum = sha256_hash_file(args.output_video_path)
.map_err(|err| {
ProcessSingleJobError::Other(anyhow!("Error hashing file: {:?}", err))
})?;
// ==================== UPLOAD VIDEO TO BUCKET ==================== //
args.job_progress_reporter.log_status("uploading result")
.map_err(|e| ProcessSingleJobError::Other(e))?;
let result_bucket_location = MediaFileBucketPath::generate_new(
Some(PREFIX),
Some(&ext_suffix));
let result_bucket_object_pathbuf = result_bucket_location.to_full_object_pathbuf();
info!("Output file destination bucket path: {:?}", &result_bucket_object_pathbuf);
info!("Uploading media ...");
args.deps.buckets.public_bucket_client.upload_filename_with_content_type(
&result_bucket_object_pathbuf,
&args.output_video_path,
&mimetype) // TODO: We should check the mimetype to make sure bad payloads can't get uploaded
.await
.map_err(|e| ProcessSingleJobError::Other(e))?;
let thumbnail_task_result = ThumbnailTaskBuilder::new_for_source_mimetype(ThumbnailTaskInputMimeType::MP4)
.with_bucket(&*args.deps.buckets.public_bucket_client.bucket_name())
.with_path(&*path_to_string(result_bucket_object_pathbuf.clone()))
.with_output_suffix("thumb")
.with_event_id(&args.job.id.0.to_string())
.send_all()
.await;
match thumbnail_task_result {
Ok(thumbnail_task) => {
debug!("Thumbnail tasks sent: {:?}", thumbnail_task);
},
Err(e) => {
error!("Failed to create some/all thumbnail tasks: {:?}", e);
}
}
//// Also upload the non-watermarked copy
//info!("Uploading non-watermarked copy...");
//let result = args.deps.buckets.public_bucket_client.upload_filename_with_content_type(
// &result_bucket_object_pathbuf.with_extension("no_watermark.mp4"),
// &args.videos.primary_video.get_non_watermarked_video_to_upload(),
// &mimetype) // TODO: We should check the mimetype to make sure bad payloads can't get uploaded
// .await;
//if let Err(err) = result {
// error!("Failed to upload non-watermarked copy: {:?}", err);
//}
//if let Some(sleep_millis) = args.comfy_args.sleep_millis {
// info!("Sleeping for millis: {sleep_millis}");
// thread::sleep(Duration::from_millis(sleep_millis));
//}
// ==================== SAVE RECORDS ==================== //
info!("Saving Studio Gen2 result (media_files table record) ...");
//// NB: We do this to avoid deep-frying the video.
//// This also lets us hide the engine renders from users.
//// This shouldn't ever become a deeply nested tree of children, but rather a single root
//// with potentially many direct children.
//let style_transfer_source_media_file_token = args.videos
// .primary_video.record.maybe_style_transfer_source_media_file_token.as_ref()
// .unwrap_or_else(|| &args.videos.primary_video.record.token);
//let prompt_token = PromptToken::generate();
//let product = MediaFileOriginProductCategory::StorytellerStudio;
//let maybe_model_type = MediaFileOriginModelType::StorytellerStudio;
//let product = match args.comfy_args.workflow_type {
// Some(WorkflowType::StorytellerStudio) => Some(MediaFileOriginProductCategory::StorytellerStudio),
// Some(WorkflowType::VideoStyleTransfer) => Some(MediaFileOriginProductCategory::VideoStyleTransfer),
// None => None,
//};
//let maybe_model_type = match args.comfy_args.workflow_type {
// Some(WorkflowType::StorytellerStudio) => Some(MediaFileOriginModelType::StorytellerStudio),
// Some(WorkflowType::VideoStyleTransfer) => Some(MediaFileOriginModelType::VideoStyleTransfer),
// None => None,
//};
let (media_file_token, id) = insert_media_file_from_studio_gen2(InsertArgs{
pool: &args.deps.db.mysql_pool,
job: &args.job,
maybe_mime_type: Some(&mimetype),
maybe_title: Some("untitled video"),
// TODO: scene and source
maybe_style_transfer_source_media_file_token: None,
maybe_scene_source_media_file_token: None,
file_size_bytes,
sha256_checksum: &file_checksum,
maybe_prompt_token: None,
public_bucket_directory_hash: result_bucket_location.get_object_hash(),
maybe_public_bucket_prefix: Some(PREFIX),
maybe_public_bucket_extension: Some(&ext_suffix),
is_on_prem: args.deps.job.info.container.is_on_prem,
worker_hostname: &args.deps.job.info.container.hostname,
worker_cluster: &args.deps.job.info.container.cluster_name,
})
.await
.map_err(|e| {
error!("Error saving media file record: {:?}", e);
ProcessSingleJobError::Other(e)
})?;
// let should_insert_prompt_record =
// args.comfy_args.disable_lcm.is_some()
// || args.comfy_args.global_ip_adapter_token.is_some()
// || args.comfy_args.lipsync_enabled.is_some()
// || args.comfy_args.negative_prompt.is_some()
// || args.comfy_args.positive_prompt.is_some()
// || args.comfy_args.travel_prompt.is_some()
// || args.comfy_args.strength.is_some()
// || args.comfy_args.style_name.is_some()
// || args.comfy_args.use_cinematic.is_some()
// || args.comfy_args.use_face_detailer.is_some()
// || args.comfy_args.use_upscaler.is_some();
//
// if should_insert_prompt_record {
// info!("Saving prompt record");
//
// let mut other_args_builder = PromptInnerPayloadBuilder::new();
//
// other_args_builder.set_main_ipa_workflow(args.comfy_deps.configs.main_workflow.clone());
//
// if let Some(style_name) = args.comfy_args.style_name {
// info!("building PromptInnerPayload with style_name = {:?}", style_name);
// other_args_builder.set_style_name(style_name);
// }
//
// if args.comfy_args.use_face_detailer.unwrap_or(false) {
// other_args_builder.set_used_face_detailer(true);
// other_args_builder.set_face_detailer_workflow(args.comfy_deps.configs.face_detailer_workflow.clone());
// }
//
// if args.comfy_args.use_upscaler.unwrap_or(false) {
// other_args_builder.set_used_upscaler(true);
// other_args_builder.set_upscaler_workflow(args.comfy_deps.configs.upscaler_workflow.clone());
// }
//
// if args.comfy_args.lipsync_enabled.unwrap_or(false) {
// other_args_builder.set_lipsync_enabled(true);
// }
//
// if args.comfy_args.disable_lcm.unwrap_or(false) {
// other_args_builder.set_disable_lcm(true);
// }
//
// if args.comfy_args.use_cinematic.unwrap_or(false) {
// other_args_builder.set_use_cinematic(true);
// }
//
// other_args_builder.set_strength(args.comfy_args.strength);
//
// if let Ok(duration) = chrono::Duration::from_std(args.inference_duration) {
// // NB: Fail open.
// other_args_builder.set_inference_duration(Some(duration));
// }
//
// if args.comfy_args.global_ip_adapter_token.is_some() {
// other_args_builder.set_global_ipa_token(args.comfy_args.global_ip_adapter_token.clone());
// }
//
// if args.comfy_args.travel_prompt.is_some() {
// other_args_builder.set_travel_prompt(args.comfy_args.travel_prompt.clone());
// }
//
// if args.comfy_args.frame_skip.is_some() {
// other_args_builder.set_frame_skip(args.comfy_args.frame_skip.clone());
// }
//
// let maybe_other_args = other_args_builder.build();
//
// info!("maybe other prompt args: {:?}", maybe_other_args);
//
// // NB: Don't fail the job if the query fails.
// let prompt_result = insert_prompt(InsertPromptArgs {
// maybe_apriori_prompt_token: Some(&prompt_token),
// prompt_type: PromptType::ComfyUi,
// maybe_creator_user_token: args.job.maybe_creator_user_token_typed.as_ref(),
// maybe_positive_prompt: args.comfy_args.positive_prompt.as_deref(),
// maybe_negative_prompt: args.comfy_args.negative_prompt.as_deref(),
// maybe_other_args: maybe_other_args.as_ref(),
// creator_ip_address: &args.job.creator_ip_address,
// mysql_executor: &args.deps.db.mysql_pool,
// phantom: Default::default(),
// }).await;
//
// if let Err(err) = prompt_result {
// error!("No prompt result token? something has failed: {:?} (we'll ignore this error)", err);
// }
// }
info!("Result video media token: {:?}", &media_file_token);
Ok(media_file_token)
}
@@ -90,7 +90,6 @@ async fn main() -> AnyhowResult<()> {
app_name,
default_logging_override: Some(DEFAULT_RUST_LOG),
config_search_directories: &[".", "./config", "crates/service/job/inference_job/config"],
ignore_legacy_dot_env_file: true,
})?;
info!("Hostname: {}", &container_environment.hostname);
@@ -1,4 +1,3 @@
use log::info;
use crate::job::job_types::f5_tts::f5_tts_dependencies::F5TTSDependencies;
use crate::job::job_types::format_conversion::fbx_to_gltf::dependencies::FbxToGltfDependencies;
use crate::job::job_types::gpt_sovits::gpt_sovits_dependencies::GptSovitsDependencies;
@@ -20,7 +19,6 @@ use crate::state::scoped_model_type_execution::ScopedModelTypeExecution;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use errors::AnyhowResult;
use crate::job::job_types::studio_gen2::studio_gen2_dependencies::StudioGen2Dependencies;
pub struct JobSpecificDependencies {
pub maybe_rvc_v2_dependencies: Option<RvcV2Dependencies>,
@@ -34,7 +32,6 @@ pub struct JobSpecificDependencies {
pub maybe_mocapnet_dependencies: Option<MocapNetDependencies>,
pub maybe_styletts2_dependencies: Option<StyleTTS2Dependencies>,
pub maybe_comfy_ui_dependencies: Option<ComfyDependencies>,
pub maybe_studio_gen2_dependencies: Option<StudioGen2Dependencies>,
pub maybe_convert_fbx_to_gltf_dependencies: Option<FbxToGltfDependencies>,
pub maybe_convert_bvh_to_workflow_dependencies: Option<RenderEngineSceneToVideoDependencies>,
pub maybe_gpt_sovits_dependencies: Option<GptSovitsDependencies>,
@@ -59,7 +56,6 @@ impl JobSpecificDependencies {
let mut maybe_mocapnet_dependencies = None;
let mut maybe_styletts2_dependencies = None;
let mut maybe_comfy_ui_dependencies = None;
let mut maybe_studio_gen2_dependencies = None;
let mut maybe_convert_fbx_to_gltf_dependencies = None;
let mut maybe_convert_bvh_to_workflow_dependencies = None;
let mut maybe_gpt_sovits_dependencies = None;
@@ -76,11 +72,6 @@ impl JobSpecificDependencies {
maybe_comfy_ui_dependencies = Some(ComfyDependencies::setup().await?);
}
if scoped_job_type_execution.can_run_job(InferenceJobType::StudioGen2) {
print_with_space("Setting Studio Gen2 dependencies...");
maybe_studio_gen2_dependencies = Some(StudioGen2Dependencies::setup()?);
}
if scoped_job_type_execution.can_run_job(InferenceJobType::GptSovits) {
print_with_space("Setting GPT-SoViTS dependencies...");
maybe_gpt_sovits_dependencies = Some(GptSovitsDependencies::setup()?);
@@ -178,7 +169,6 @@ impl JobSpecificDependencies {
maybe_mocapnet_dependencies,
maybe_styletts2_dependencies,
maybe_comfy_ui_dependencies,
maybe_studio_gen2_dependencies,
maybe_convert_fbx_to_gltf_dependencies,
maybe_convert_bvh_to_workflow_dependencies,
maybe_gpt_sovits_dependencies,
@@ -1,33 +0,0 @@
use filesys::path_to_string::path_to_string;
use log::debug;
use std::path::Path;
use std::time::Duration;
use subprocess_common::command_runner::command_args::CommandArgs;
use videos::ffmpeg_timestamp_from_duration::ffmpeg_timestamp_from_duration;
/// Trim a video's duration
/// See: https://shotstack.io/learn/use-ffmpeg-to-trim-video/
pub struct FfmpegResampleDurationArgs<'a> {
pub input_video_file: &'a Path,
pub output_video_file: &'a Path,
pub trim_to_duration: Duration,
}
impl CommandArgs for FfmpegResampleDurationArgs<'_> {
fn to_command_string(&self) -> String {
let mut command = String::new();
command.push_str(" -i ");
command.push_str(&path_to_string(self.input_video_file));
command.push_str(" -t ");
command.push_str(&ffmpeg_timestamp_from_duration(self.trim_to_duration));
command.push_str(" ");
command.push_str(&path_to_string(self.output_video_file));
debug!("ffmpeg arguments: {:?}", &command);
command
}
}
@@ -1,41 +0,0 @@
use filesys::path_to_string::path_to_string;
use log::debug;
use std::path::Path;
use std::time::Duration;
use subprocess_common::command_runner::command_args::CommandArgs;
use videos::ffmpeg_timestamp_from_duration::ffmpeg_timestamp_from_duration;
/// Resample a video's frame rate
/// See: https://trac.ffmpeg.org/wiki/ChangingFrameRate
///
/// Trim a video's duration
/// See: https://shotstack.io/learn/use-ffmpeg-to-trim-video/
pub struct FfmpegResampleFpsAndDurationArgs<'a> {
pub input_video_file: &'a Path,
pub output_video_file: &'a Path,
pub fps: usize,
pub trim_to_duration: Duration,
}
impl CommandArgs for FfmpegResampleFpsAndDurationArgs<'_> {
fn to_command_string(&self) -> String {
let mut command = String::new();
command.push_str(" -i ");
command.push_str(&path_to_string(self.input_video_file));
command.push_str(" -t ");
command.push_str(&ffmpeg_timestamp_from_duration(self.trim_to_duration));
command.push_str(" ");
command.push_str(" -filter:v fps=");
command.push_str(&self.fps.to_string());
command.push_str(" ");
command.push_str(&path_to_string(self.output_video_file));
debug!("ffmpeg arguments: {:?}", &command);
command
}
}
@@ -1,31 +0,0 @@
use filesys::path_to_string::path_to_string;
use log::debug;
use std::path::Path;
use subprocess_common::command_runner::command_args::CommandArgs;
/// Resample a video's frame rate
/// See: https://trac.ffmpeg.org/wiki/ChangingFrameRate
pub struct FfmpegResampleFpsArgs<'a> {
pub input_video_file: &'a Path,
pub output_video_file: &'a Path,
pub fps: usize,
}
impl CommandArgs for FfmpegResampleFpsArgs<'_> {
fn to_command_string(&self) -> String {
let mut command = String::new();
command.push_str(" -i ");
command.push_str(&path_to_string(self.input_video_file));
command.push_str(" -filter:v fps=");
command.push_str(&self.fps.to_string());
command.push_str(" ");
command.push_str(&path_to_string(self.output_video_file));
debug!("ffmpeg arguments: {:?}", &command);
command
}
}
@@ -1,9 +1,6 @@
pub mod ffmpeg_audio_replace_args;
pub mod ffmpeg_audio_truncate_args;
pub mod ffmpeg_extract_audio_args;
pub mod ffmpeg_resample_duration;
pub mod ffmpeg_resample_fps_and_duration;
pub mod ffmpeg_resample_fps_args;
pub mod ffprobe_get_dimensions_args;
pub mod old;
pub mod runner;
@@ -1,88 +0,0 @@
use std::cmp::max;
pub struct Dimensions {
pub width: u32,
pub height: u32,
}
pub struct ResizeArgs {
pub source: Dimensions,
pub target: Dimensions,
pub resize_method: ResizeMethod,
}
pub enum ResizeMethod {
/// The frame or image must fit within maximum bounds.
KeepUnder,
/// The dimensions must be greater than the minimum bounds.
MustFill,
}
pub struct ResizeResult {
pub new_width: u32,
pub new_height: u32,
}
pub fn aspect_preserving_frame_resize(args: ResizeArgs) -> ResizeResult {
let width_ratio = args.target.width as f64 / args.source.width as f64;
let height_ratio = args.target.height as f64 / args.source.height as f64;
let resize_ratio= match args.resize_method {
ResizeMethod::KeepUnder => f64::min(width_ratio, height_ratio),
ResizeMethod::MustFill => f64::max(width_ratio, height_ratio),
};
let mut new_width = max((args.source.width as f64 * resize_ratio).round() as u64, 1);
let mut new_height = max((args.source.height as f64 * resize_ratio).round() as u64, 1);
// TODO(bt): u32 overflow.
ResizeResult {
new_width: new_width as u32,
new_height: new_height as u32,
}
}
#[cfg(test)]
mod tests {
use crate::util::frame_resize_math::{aspect_preserving_frame_resize, ResizeMethod, ResizeArgs, Dimensions};
#[test]
fn no_resize() {
for resize_method in [ResizeMethod::MustFill, ResizeMethod::KeepUnder] {
let result = aspect_preserving_frame_resize(ResizeArgs {
source: Dimensions {
width: 100,
height: 100,
},
target: Dimensions {
width: 100,
height: 100,
},
resize_method,
});
assert_eq!(result.new_width, 100);
assert_eq!(result.new_height, 100);
}
}
mod keep_under {
use super::*;
#[test]
fn above_constraints_width() {
let result = aspect_preserving_frame_resize(ResizeArgs {
source: Dimensions {
width: 1000,
height: 1000,
},
target: Dimensions {
width: 768,
height: 512,
},
resize_method: ResizeMethod::KeepUnder,
});
assert_eq!(result.new_width, 512);
assert_eq!(result.new_height, 512);
}
}
}
@@ -2,7 +2,6 @@ pub mod common_commands;
pub mod downloaders;
pub mod extractors;
pub mod filesystem;
pub mod frame_resize_math;
pub mod get_filtered_env_vars;
pub mod instrumentation;
pub mod model_weights_cache;
@@ -1,8 +1,5 @@
# storyteller-web default .env configurations (these can be overridden with k8s)
# Development
SERVER_ENVIRONMENT='development'
# We can't serve HTTPS locally without self-signed certificate nonsense.
COOKIE_SECURE=false
COOKIE_DOMAIN=.jungle.horse
@@ -37,7 +34,3 @@ REDIS_0_URL="redis://localhost/0"
#REDIS_1_URL="redis://localhost/1"
GC_ENABLED_PUBLIC_BUCKET_NAME="REPLACE_ME"
# Legacy configs
AUDIO_UPLOADS_BUCKET_ROOT=/user_uploaded_w2l_audio
@@ -117,7 +117,6 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_error::Medi
use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file::write_scene_file_media_file_handler::*;
use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*;
use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::*;
use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::*;
use crate::http_server::endpoints::prompts::get_prompt_handler::*;
use crate::http_server::endpoints::service::status_alert_handler::*;
use crate::http_server::endpoints::stats::get_unified_queue_stats_handler::*;
@@ -207,7 +206,6 @@ use crate::http_server::web_utils::response_success_helpers::*;
crate::http_server::endpoints::media_files::upload::upload_engine_asset::upload_engine_asset_media_file_handler::upload_engine_asset_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_generic::upload_media_file_handler::upload_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_image_media_file_handler::upload_image_media_file_handler,
crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::enqueue_studio_gen2_handler,
crate::http_server::endpoints::media_files::upload::upload_new_engine_asset_media_file_handler::upload_new_engine_asset_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::upload_new_scene_media_file_handler,
crate::http_server::endpoints::media_files::upload::upload_pmx::upload_pmx_media_file_handler::upload_pmx_media_file_handler,
@@ -420,9 +418,6 @@ use crate::http_server::web_utils::response_success_helpers::*;
EditUsernameResponse,
EnqueueFaceFusionCropDimensions,
EnqueueFaceFusionWorkflowError,
EnqueueStudioGen2Request,
EnqueueStudioGen2Response,
EnqueueStudioGen2Error,
EnqueueFaceFusionWorkflowRequest,
EnqueueFaceFusionWorkflowSuccessResponse,
EnqueueFbxToGltfRequest,
@@ -1,43 +0,0 @@
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use once_cell::sync::Lazy;
use server_environment::ServerEnvironment;
use url::Url;
const FAKEYOU_CDN_STR: &str = "https://cdn-2.fakeyou.com";
const STORYTELLER_CDN_STR: &str = "https://cdn-2.fakeyou.com"; // TODO(bt,2025-01-31): This doesn't work
const FAKEYOU_DEVELOPMENT_CDN_STR: &str = "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev";
const STORYTELLER_DEVELOPMENT_CDN_STR: &str = "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev";
const FAKEYOU_CDN: Lazy<Url> = Lazy::new(|| Url::parse(FAKEYOU_CDN_STR)
.expect("should never fail"));
const STORYTELLER_CDN: Lazy<Url> = Lazy::new(|| Url::parse(STORYTELLER_CDN_STR)
.expect("should never fail"));
const FAKEYOU_DEVELOPMENT_CDN: Lazy<Url> = Lazy::new(|| Url::parse(FAKEYOU_DEVELOPMENT_CDN_STR)
.expect("should never fail"));
const STORYTELLER_DEVELOPMENT_CDN: Lazy<Url> = Lazy::new(|| Url::parse(STORYTELLER_DEVELOPMENT_CDN_STR)
.expect("should never fail"));
// TODO(bt,2025-01-31): Perhaps this should be config driven and configurable at runtime instead of hardcoded.
pub fn get_cdn_host(media_domain: MediaDomain, server_environment: ServerEnvironment) -> &'static str {
match (media_domain, server_environment) {
(MediaDomain::FakeYou, ServerEnvironment::Development) => FAKEYOU_DEVELOPMENT_CDN_STR,
(MediaDomain::FakeYou, ServerEnvironment::Production) => FAKEYOU_CDN_STR,
(MediaDomain::Storyteller, ServerEnvironment::Development) => STORYTELLER_DEVELOPMENT_CDN_STR,
(MediaDomain::Storyteller, ServerEnvironment::Production) => STORYTELLER_CDN_STR,
}
}
pub fn new_cdn_url(media_domain: MediaDomain, server_environment: ServerEnvironment) -> Url {
match (media_domain, server_environment) {
(MediaDomain::FakeYou, ServerEnvironment::Development) => FAKEYOU_DEVELOPMENT_CDN.clone(),
(MediaDomain::FakeYou, ServerEnvironment::Production) => FAKEYOU_CDN.clone(),
(MediaDomain::Storyteller, ServerEnvironment::Development) => STORYTELLER_DEVELOPMENT_CDN.clone(),
(MediaDomain::Storyteller, ServerEnvironment::Production) => STORYTELLER_CDN.clone(),
}
}
@@ -3,8 +3,6 @@ use utoipa::ToSchema;
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use server_environment::ServerEnvironment;
use crate::http_server::common_responses::media::cdn_link::{get_cdn_host, new_cdn_url};
// TODO(bt,2024-09-05): Worth reducing the quality at all?
const QUALITY : u8 = 95;
@@ -43,41 +41,23 @@ pub struct VideoPreviews {
}
impl MediaLinks {
#[deprecated(note = "we need to start passing the domain and the server environment to generate the CDN")]
pub fn from_media_path(
domain: MediaDomain,
bucket_path: &MediaFileBucketPath,
) -> MediaLinks {
Self::from_media_path_and_env(domain, ServerEnvironment::Production, bucket_path)
let rooted_path = bucket_path.get_full_object_path_str();
Self::from_rooted_path(domain, rooted_path)
}
#[deprecated(note = "we need to start passing the domain and the server environment to generate the CDN")]
pub fn from_rooted_path(
domain: MediaDomain,
rooted_path: &str,
) -> MediaLinks {
Self::from_rooted_path_and_env(domain, ServerEnvironment::Production, rooted_path)
}
pub fn from_media_path_and_env(
domain: MediaDomain,
server_environment: ServerEnvironment,
bucket_path: &MediaFileBucketPath,
) -> MediaLinks {
let rooted_path = bucket_path.get_full_object_path_str();
Self::from_rooted_path_and_env(domain, server_environment, rooted_path)
}
pub fn from_rooted_path_and_env(
domain: MediaDomain,
server_environment: ServerEnvironment,
rooted_path: &str,
) -> MediaLinks {
let mut cdn_url = new_cdn_url(domain, server_environment);
let mut cdn_url = domain.new_cdn_url();
cdn_url.set_path(rooted_path);
MediaLinks {
cdn_url,
maybe_thumbnail_template: thumbnail_template(domain, server_environment, rooted_path),
maybe_thumbnail_template: thumbnail_template(domain, rooted_path),
maybe_video_previews: VideoPreviews::from_rooted_path(domain, rooted_path),
}
}
@@ -118,20 +98,14 @@ fn video_preview(media_domain: MediaDomain, rooted_path: &str, thumbnail_type: P
}
/// Returns a thumbnail template for image
fn thumbnail_template(media_domain: MediaDomain, server_environment: ServerEnvironment, rooted_path: &str) -> Option<String> {
fn thumbnail_template(media_domain: MediaDomain, rooted_path: &str) -> Option<String> {
if !rooted_path.ends_with(".jpg")
&& !rooted_path.ends_with(".png")
&& !rooted_path.ends_with(".gif") {
return None;
}
let host = get_cdn_host(media_domain, server_environment);
// NB(bt,2025-02-01): Development doesn't currently support thumbnails, so serve the full image.
match server_environment {
ServerEnvironment::Development => Some(format!("{host}{rooted_path}")), // NB(bt,2025-02-01): No thumbnails in development.
ServerEnvironment::Production => Some(format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}"))
}
let host = media_domain.cdn_url_str();
Some(format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}"))
}
/// Returns a thumbnail template for video
@@ -153,28 +127,17 @@ mod tests {
use crate::http_server::common_responses::media::media_links::MediaLinks;
mod fakeyou {
use server_environment::ServerEnvironment;
use super::*;
const DOMAIN : MediaDomain = MediaDomain::FakeYou;
// TODO(bt,2025-01-31): Write robust tests for this.
#[test]
fn spot_check_new_case() {
let links = MediaLinks::from_rooted_path_and_env(MediaDomain::FakeYou, ServerEnvironment::Production, "/foo/bar.wav");
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.wav");
let links = MediaLinks::from_rooted_path_and_env(MediaDomain::Storyteller, ServerEnvironment::Development, "/foo/bar.wav");
assert_eq!(links.cdn_url.as_str(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/foo/bar.wav");
}
mod rooted_path {
use super::*;
#[test]
fn wav_file() {
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.wav");
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.wav");
assert_eq!(links.cdn_url.as_str(), "https://cdn.fakeyou.com/foo/bar.wav");
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
}
@@ -182,7 +145,7 @@ mod tests {
#[test]
fn glb_file() {
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.glb");
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.glb");
assert_eq!(links.cdn_url.as_str(), "https://cdn.fakeyou.com/foo/bar.glb");
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
}
@@ -190,21 +153,21 @@ mod tests {
#[test]
fn jpg_image() {
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.jpg");
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.jpg");
assert_eq!(links.maybe_thumbnail_template, Some("https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.jpg".to_string()));
assert_eq!(links.cdn_url.as_str(), "https://cdn.fakeyou.com/foo/bar.jpg");
assert_eq!(links.maybe_thumbnail_template, Some("https://cdn.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.jpg".to_string()));
assert_eq!(links.maybe_video_previews, None);
}
#[test]
fn mp4_video() {
let links = MediaLinks::from_rooted_path(DOMAIN, "/foo/bar.mp4");
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.mp4");
assert_eq!(links.cdn_url.as_str(), "https://cdn.fakeyou.com/foo/bar.mp4");
assert_eq!(links.maybe_thumbnail_template, None);
let video_previews = links.maybe_video_previews.expect("should have previews");
assert_eq!(video_previews.animated.as_str(), "https://cdn-2.fakeyou.com/foo/bar.mp4-thumb.gif");
assert_eq!(video_previews.still.as_str(), "https://cdn-2.fakeyou.com/foo/bar.mp4-thumb.jpg");
assert_eq!(video_previews.animated_thumbnail_template, "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.mp4-thumb.gif");
assert_eq!(video_previews.still_thumbnail_template, "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.mp4-thumb.jpg");
assert_eq!(video_previews.animated.as_str(), "https://cdn.fakeyou.com/foo/bar.mp4-thumb.gif");
assert_eq!(video_previews.still.as_str(), "https://cdn.fakeyou.com/foo/bar.mp4-thumb.jpg");
assert_eq!(video_previews.animated_thumbnail_template, "https://cdn.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.mp4-thumb.gif");
assert_eq!(video_previews.still_thumbnail_template, "https://cdn.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/foo/bar.mp4-thumb.jpg");
}
}
@@ -216,7 +179,7 @@ mod tests {
// https://storage.googleapis.com/vocodes-public/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav
let media_path = MediaFileBucketPath::from_object_hash("94a27nmbd0bqmd10tg0pp3hz45zytf67", Some("fakeyou_"), Some(".wav"));
let links = MediaLinks::from_media_path(DOMAIN, &media_path);
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav");
assert_eq!(links.cdn_url.as_str(), "https://cdn.fakeyou.com/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav");
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
}
@@ -226,8 +189,8 @@ mod tests {
/// https://storage.googleapis.com/vocodes-public/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png
let media_path = MediaFileBucketPath::from_object_hash("37mb3gh8fmj85y21thvbv08bzv24atjt", Some("upload_"), Some(".png"));
let links = MediaLinks::from_media_path(DOMAIN, &media_path);
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png");
assert_eq!(links.maybe_thumbnail_template, Some("https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png".to_string()));
assert_eq!(links.cdn_url.as_str(), "https://cdn.fakeyou.com/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png");
assert_eq!(links.maybe_thumbnail_template, Some("https://cdn.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png".to_string()));
assert_eq!(links.maybe_video_previews, None);
}
@@ -236,13 +199,13 @@ mod tests {
// https://storage.googleapis.com/vocodes-public/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4
let media_path = MediaFileBucketPath::from_object_hash("t6cnyw4g3e8k7carkk2bvrt6nd3fycjv", Some("storyteller_"), Some(".mp4"));
let links = MediaLinks::from_media_path(DOMAIN, &media_path);
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4");
assert_eq!(links.cdn_url.as_str(), "https://cdn.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4");
assert_eq!(links.maybe_thumbnail_template, None);
let video_previews = links.maybe_video_previews.expect("should have previews");
assert_eq!(video_previews.animated.as_str(), "https://cdn-2.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.gif");
assert_eq!(video_previews.still.as_str(), "https://cdn-2.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg");
assert_eq!(video_previews.animated_thumbnail_template, "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.gif");
assert_eq!(video_previews.still_thumbnail_template, "https://cdn-2.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg");
assert_eq!(video_previews.animated.as_str(), "https://cdn.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.gif");
assert_eq!(video_previews.still.as_str(), "https://cdn.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg");
assert_eq!(video_previews.animated_thumbnail_template, "https://cdn.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.gif");
assert_eq!(video_previews.still_thumbnail_template, "https://cdn.fakeyou.com/cdn-cgi/image/width={WIDTH},quality=95/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg");
}
}
}
@@ -1,4 +1,3 @@
pub mod cdn_link;
pub mod cover_image_links;
pub mod media_domain;
pub mod media_file_cover_image_details;
@@ -189,7 +189,7 @@ pub async fn enqueue_face_animation_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
server_state.server_environment,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
@@ -130,8 +130,8 @@ pub async fn enqueue_rerender_animation_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref());
server_state.server_environment,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
@@ -146,8 +146,8 @@ pub async fn enqueue_fbx_to_gltf_handler(
// Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref()
server_state.server_environment,
maybe_user_session.as_ref()
);
// Separate priority for animation.
@@ -179,8 +179,8 @@ pub async fn enqueue_render_engine_scene_to_video_handler(
// Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref()
server_state.server_environment,
maybe_user_session.as_ref()
);
// Separate priority for animation.
@@ -236,8 +236,8 @@ pub async fn enqueue_image_generation_request(
// Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref()
server_state.server_environment,
maybe_user_session.as_ref()
);
// Separate priority for animation.
@@ -287,7 +287,7 @@ pub async fn get_media_file_handler(
&media_file_token,
show_deleted_results,
&server_state,
media_domain,
media_domain
).await?
} else {
modern_media_file_lookup(
@@ -365,7 +365,7 @@ async fn modern_media_file_lookup(
maybe_engine_extension,
maybe_batch_token: result.maybe_batch_token,
maybe_scene_source_media_file_token: result.maybe_scene_source_media_file_token,
media_links: MediaLinks::from_media_path_and_env(media_domain, server_state.server_environment, &public_bucket_path),
media_links: MediaLinks::from_media_path(media_domain, &public_bucket_path),
public_bucket_url: bucket_url_from_media_path(&public_bucket_path)
.map_err(|err| {
warn!("error creating URL: {:?}", err);
@@ -502,7 +502,7 @@ async fn emulate_media_file_with_legacy_tts_result_lookup(
maybe_engine_extension: None,
maybe_batch_token: None,
maybe_scene_source_media_file_token: None,
media_links: MediaLinks::from_rooted_path_and_env(media_domain, server_state.server_environment, &public_bucket_path),
media_links: MediaLinks::from_rooted_path(media_domain, &public_bucket_path),
public_bucket_url: bucket_url_from_str_path(&public_bucket_path)
.map_err(|err| {
warn!("error creating URL: {:?}", err);
@@ -21,12 +21,6 @@ pub fn get_scoped_media_classes(
return None;
}
if classes.len() == 1 && classes.contains(&MediaFileClass::Unknown) {
// NB(bt,2025-01-31): The frontend started passing simply "unknown" to the API in development
// for some reason. Not sure why. There's no product reason for this to be a filter.
return None;
}
Some(classes)
}
@@ -40,29 +34,23 @@ mod test {
#[test]
fn none() {
assert_eq!(get_scoped_media_classes(None), None);
assert_eq!(get_scoped_media_classes(None), None)
}
#[test]
fn empty() {
assert_eq!(get_scoped_media_classes(Some("")), None);
}
#[test]
fn not_just_unknown() {
assert_eq!(get_scoped_media_classes(Some("unknown")), None);
assert_eq!(get_scoped_media_classes(Some("unknown,,")), None);
assert_eq!(get_scoped_media_classes(Some("")), None)
}
#[test]
fn garbage() {
assert_eq!(get_scoped_media_classes(Some("foo,bar,baz")), None);
assert_eq!(get_scoped_media_classes(Some("foo,bar,baz")), None)
}
#[test]
fn valid_scope() {
assert_eq!(
get_scoped_media_classes(Some("image,video")),
Some(HashSet::from([MediaFileClass::Image, MediaFileClass::Video])));
Some(HashSet::from([MediaFileClass::Image, MediaFileClass::Video])))
}
}
@@ -340,8 +340,7 @@ pub async fn list_media_files_for_user_handler(
maybe_origin_model_type: record.maybe_origin_model_type
.map(|t| PublicMediaFileModelType::from_enum(t)),
maybe_origin_model_token: record.maybe_origin_model_token,
media_links: MediaLinks::from_media_path_and_env(
media_domain, server_state.server_environment, &public_bucket_path),
media_links: MediaLinks::from_media_path(media_domain, &public_bucket_path),
public_bucket_path: public_bucket_path
.get_full_object_path_str()
.to_string(),
@@ -125,8 +125,8 @@ pub async fn enqueue_mocapnet_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref());
server_state.server_environment,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
@@ -25,14 +25,13 @@ pub mod prompts;
pub mod service;
pub mod stats;
pub mod stubs;
pub mod studio_gen2;
pub mod subscriptions;
pub mod tags;
pub mod trending;
pub mod tts;
pub mod users;
pub mod user_bookmarks;
pub mod user_ratings;
pub mod users;
pub mod vocoders;
pub mod voice_clone_requests;
pub mod voice_conversion;
@@ -126,7 +126,7 @@ pub async fn enqueue_gptsovits_model_download_handler(
// ==================== PAID PLAN + PRIORITY ==================== //
let plan = get_correct_plan_for_session(server_state.server_environment_old, Some(&user_session));
let plan = get_correct_plan_for_session(server_state.server_environment, Some(&user_session));
let priority_level = plan.web_vc_base_priority_level();
// ==================== DEBUG MODE + ROUTING TAG ==================== //
@@ -1,341 +0,0 @@
// NB: Incrementally getting rid of build warnings...
// #![forbid(unused_imports)]
// #![forbid(unused_mut)]
// #![forbid(unused_variables)]
use std::sync::Arc;
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Json;
use actix_web::{web, HttpRequest, HttpResponse};
use log::{error, info, warn};
use utoipa::ToSchema;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::payloads::generic_inference_args::common::watermark_type::WatermarkType;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, InferenceCategoryAbbreviated, PolymorphicInferenceArgs};
use mysql_queries::payloads::generic_inference_args::inner_payloads::studio_gen2_payload::StudioGen2Payload;
use mysql_queries::payloads::generic_inference_args::inner_payloads::workflow_payload::{WorkflowArgs, WorkflowType};
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
use mysql_queries::queries::generic_inference::web::kill_jobs_in_development::kill_jobs_in_development;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use primitives::lazy_any_option_true::lazy_any_option_true;
use primitives::str_to_bool::str_to_bool;
use primitives::traits::trim_or_emptyable::TrimOrEmptyable;
use primitives::try_str_to_num::try_str_to_num;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::configs::plans::plan_category::PlanCategory;
use crate::http_server::endpoints::workflows::enqueue::vst_common::vst_request::VstRequest;
use crate::http_server::endpoints::workflows::enqueue::vst_common::vst_response::VstSuccessResponse;
use crate::http_server::requests::request_headers::get_routing_tag_header::get_routing_tag_header;
use crate::http_server::requests::request_headers::has_debug_header::has_debug_header;
use crate::http_server::session::lookup::user_session_extended::UserSessionExtended;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::state::server_state::ServerState;
use crate::util::allowed_video_style_transfer_access::allowed_video_style_transfer_access;
use crate::util::cleaners::empty_media_file_token_to_null::empty_media_file_token_to_null;
#[derive(Deserialize, ToSchema)]
pub struct EnqueueStudioGen2Request {
/// Entropy for request de-duplication (required)
pub uuid_idempotency_token: String,
/// The input image media file (required)
pub image_file: MediaFileToken,
/// The input video media file (required)
pub video_file: MediaFileToken,
/// Remove watermark from the output
/// Only for premium accounts
pub remove_watermark: Option<bool>,
/// Optional visibility setting override.
pub creator_set_visibility: Option<Visibility>,
/// Sleep for debugging
pub debug_sleep_millis: Option<u64>,
/// Kill old jobs (only in development)
pub debug_kill_old_jobs: Option<bool>,
// TODO
pub output_width: Option<u64>,
pub output_height: Option<u64>,
pub fps: Option<u64>,
// TODO
pub trim_duration_millis: Option<u64>,
// TODO
pub max_frames: Option<u64>,
pub rounds: Option<u64>,
pub skip_image_resize: Option<bool>,
/// Model image tensor width (we do not resize images to this!)
pub tensor_image_width: Option<u64>,
/// Model image tensor height (we do not resize images to this!)
pub tensor_image_height: Option<u64>,
}
#[derive(Serialize, ToSchema)]
pub struct EnqueueStudioGen2Response {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
#[derive(Debug, ToSchema)]
pub enum EnqueueStudioGen2Error {
BadInput(String),
NotAuthorized,
ServerError,
RateLimited,
}
impl ResponseError for EnqueueStudioGen2Error {
fn status_code(&self) -> StatusCode {
match *self {
EnqueueStudioGen2Error::BadInput(_) => StatusCode::BAD_REQUEST,
EnqueueStudioGen2Error::NotAuthorized => StatusCode::UNAUTHORIZED,
EnqueueStudioGen2Error::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
EnqueueStudioGen2Error::RateLimited => StatusCode::TOO_MANY_REQUESTS,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
EnqueueStudioGen2Error::BadInput(reason) => reason.to_string(),
EnqueueStudioGen2Error::NotAuthorized => "unauthorized".to_string(),
EnqueueStudioGen2Error::ServerError => "server error".to_string(),
EnqueueStudioGen2Error::RateLimited => "rate limited".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl std::fmt::Display for EnqueueStudioGen2Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
/// Enqueue Studio Gen2 jobs.
#[utoipa::path(
post,
tag = "Studio Gen2",
path = "/v1/studio_gen2/enqueue",
responses(
(status = 200, description = "Success", body = EnqueueStudioGen2Response),
(status = 400, description = "Bad input", body = EnqueueStudioGen2Error),
(status = 401, description = "Not authorized", body = EnqueueStudioGen2Error),
(status = 429, description = "Rate limited", body = EnqueueStudioGen2Error),
(status = 500, description = "Server error", body = EnqueueStudioGen2Error)
),
params(("request" = EnqueueStudioGen2Request, description = "Payload for request"))
)]
pub async fn enqueue_studio_gen2_handler(
http_request: HttpRequest,
request: Json<EnqueueStudioGen2Request>,
server_state: web::Data<Arc<ServerState>>
) -> Result<Json<EnqueueStudioGen2Response>, EnqueueStudioGen2Error> {
let a = 1;
// ==================== DB ==================== //
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
warn!("MySql pool error: {:?}", err);
EnqueueStudioGen2Error::ServerError
})?;
let maybe_avt_token = server_state.avt_cookie_manager
.get_avt_token_from_request(&http_request);
// ==================== USER SESSION ==================== //
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
EnqueueStudioGen2Error::ServerError
})?;
let maybe_user_token = maybe_user_session
.as_ref()
.map(|session| session.user_token_typed.clone());
// ==================== FEATURE FLAG CHECK ==================== //
//if !allowed_video_style_transfer_access(maybe_user_session.as_ref(), &server_state.flags) {
// warn!("Video style transfer access is not permitted for user");
// return Err(EnqueueStudioGen2Error::NotAuthorized);
//}
// ==================== PAID PLAN + PRIORITY ==================== //
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
let is_staff = maybe_user_session
.as_ref()
.map(|user| user.role.can_ban_users)
.unwrap_or(false);
// ==================== DEBUG MODE + ROUTING TAG ==================== //
let is_debug_request = has_debug_header(&http_request);
let maybe_routing_tag= get_routing_tag_header(&http_request);
// ==================== RATE LIMIT ==================== //
let rate_limiter = match maybe_user_session {
None => &server_state.redis_rate_limiters.logged_out,
Some(ref user) => {
if user.role.is_banned {
return Err(EnqueueStudioGen2Error::NotAuthorized);
}
&server_state.redis_rate_limiters.logged_in
},
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request) {
return Err(EnqueueStudioGen2Error::RateLimited);
}
// ==================== HANDLE IDEMPOTENCY ==================== //
if let Err(reason) = validate_idempotency_token_format(&request.uuid_idempotency_token) {
return Err(EnqueueStudioGen2Error::BadInput(reason));
}
insert_idempotency_token(&request.uuid_idempotency_token, &mut *mysql_connection)
.await
.map_err(|err| {
error!("Error inserting idempotency token: {:?}", err);
EnqueueStudioGen2Error::BadInput("invalid idempotency token".to_string())
})?;
// ==================== LOOK UP MODEL INFO ==================== //
// TODO(bt): CHECK DATABASE FOR TOKENS!
let ip_address = get_request_ip(&http_request);
let maybe_user_preferred_visibility : Option<Visibility> = maybe_user_session
.as_ref()
.map(|user_session: &UserSessionExtended| user_session.preferences.preferred_tts_result_visibility); // TODO: New setting for web-vc
let set_visibility = request.creator_set_visibility
.or(maybe_user_preferred_visibility)
.unwrap_or(Visibility::Public);
let has_paid_plan = plan.plan_slug() == "fakeyou_contributor" || plan.plan_category() == PlanCategory::Paid;
let watermark_type ;
let is_requested_to_remove_watermark = request.remove_watermark.unwrap_or(false);
let is_allowed_to_remove_watermark = is_staff; // TODO: Paid permission instead
if is_requested_to_remove_watermark && is_allowed_to_remove_watermark {
watermark_type = None;
} else {
watermark_type = Some(WatermarkType::Storyteller); // Studio is only on Storyteller.
}
if request.debug_kill_old_jobs.unwrap_or(false)
&& server_state.server_environment.is_development()
{
info!("Killing old jobs before inserting new job (DEVELOPMENT MODE ONLY!)");
let result = kill_jobs_in_development(&server_state.mysql_pool).await;
if let Err(err) = result {
warn!("Error killing old jobs: {:?}", err);
}
}
let inference_args = StudioGen2Payload {
image_file: Some(request.image_file.clone()),
video_file: Some(request.video_file.clone()),
creator_visibility: Some(set_visibility),
watermark_type,
after_job_debug_sleep_millis: request.debug_sleep_millis,
output_width: request.output_width,
output_height: request.output_height,
fps: request.fps,
max_frames: request.max_frames,
rounds: request.rounds,
trim_duration_millis: request.trim_duration_millis,
skip_image_resize: request.skip_image_resize,
tensor_image_width: request.tensor_image_width,
tensor_image_height: request.tensor_image_height,
};
info!("Creating ComfyUI job record...");
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
job_type: InferenceJobType::StudioGen2,
inference_category: InferenceCategory::DeprecatedField,
maybe_model_type: None,
maybe_product_category: None,
maybe_model_token: None,
maybe_input_source_token: None, // TODO: Introduce a second foreign key ?
maybe_input_source_token_type: None, // TODO: Introduce a second foreign key ?
maybe_download_url: None,
maybe_cover_image_media_file_token: None,
maybe_raw_inference_text: None, // No text
maybe_max_duration_seconds: None,
maybe_inference_args: Some(GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::Workflow),
args: Some(PolymorphicInferenceArgs::S2(inference_args)),
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: set_visibility,
priority_level,
requires_keepalive: false,
is_debug_request,
maybe_routing_tag: maybe_routing_tag.as_deref(),
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match query_result {
Ok((job_token, _id)) => job_token,
Err(err) => {
warn!("New generic inference job creation DB error: {:?}", err);
if err.had_duplicate_idempotency_token() {
return Err(EnqueueStudioGen2Error::BadInput("Duplicate idempotency token".to_string()));
}
return Err(EnqueueStudioGen2Error::ServerError);
}
};
Ok(Json(EnqueueStudioGen2Response {
success: true,
inference_job_token: job_token,
}))
}
@@ -1 +0,0 @@
pub mod enqueue_studio_gen2_handler;
@@ -170,7 +170,7 @@ pub async fn enqueue_infer_f5_tts_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
server_state.server_environment,
maybe_user_session.as_ref());
priority_level = plan.tts_base_priority_level();
@@ -187,7 +187,7 @@ pub async fn enqueue_infer_tts_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
server_state.server_environment,
maybe_user_session.as_ref());
priority_level = plan.tts_base_priority_level();
@@ -152,7 +152,7 @@ pub async fn enqueue_infer_seed_vc_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
server_state.server_environment,
maybe_user_session.as_ref());
priority_level = plan.tts_base_priority_level();
@@ -189,7 +189,7 @@ pub async fn enqueue_voice_conversion_inference_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
server_state.server_environment,
maybe_user_session.as_ref());
priority_level = plan.web_vc_base_priority_level();
@@ -142,8 +142,8 @@ pub async fn enqueue_tts_request(
// Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref()
server_state.server_environment,
maybe_user_session.as_ref()
);
// Separate priority for animation.
@@ -126,8 +126,8 @@ pub async fn create_voice_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref()
server_state.server_environment,
maybe_user_session.as_ref()
);
// TODO: Separate priority for animation.
@@ -168,7 +168,7 @@ pub async fn enqueue_face_fusion_workflow_handler(
// ==================== PAID PLAN + PRIORITY ==================== //
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(server_state.server_environment_old, Some(&user_session));
let plan = get_correct_plan_for_session(server_state.server_environment, Some(&user_session));
// TODO: Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
@@ -177,7 +177,7 @@ pub async fn enqueue_live_portrait_workflow_handler(
// ==================== PAID PLAN + PRIORITY ==================== //
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(server_state.server_environment_old, Some(&user_session));
let plan = get_correct_plan_for_session(server_state.server_environment, Some(&user_session));
// TODO: Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
@@ -130,7 +130,7 @@ pub async fn enqueue_studio_workflow_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
server_state.server_environment,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
@@ -131,7 +131,7 @@ pub async fn enqueue_video_style_transfer_workflow_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
server_state.server_environment,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
@@ -150,7 +150,7 @@ pub async fn enqueue_comfy_ui_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
server_state.server_environment,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
@@ -227,7 +227,7 @@ pub async fn enqueue_comfy_ui_handler(
// Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
server_state.server_environment,
maybe_user_session.as_ref()
);
@@ -272,8 +272,8 @@ pub async fn enqueue_video_style_transfer_handler(
// TODO: Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref());
server_state.server_environment,
maybe_user_session.as_ref());
// TODO: Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
@@ -157,8 +157,8 @@ pub async fn enqueue_workflow_upload_request(
// Plan should handle "first anonymous use" and "investor" cases.
let plan = get_correct_plan_for_session(
server_state.server_environment_old,
maybe_user_session.as_ref()
server_state.server_environment,
maybe_user_session.as_ref()
);
// Separate priority for animation.
@@ -1,7 +1,7 @@
use actix_helpers::extractors::get_request_origin_uri::get_request_origin_uri;
use actix_web::http::header::HOST;
use actix_web::HttpRequest;
use log::{debug, warn};
use log::{info, warn};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum DomainBranding {
@@ -26,7 +26,7 @@ pub fn get_request_domain_branding(http_request: &HttpRequest) -> Option<DomainB
match maybe_origin {
Ok(Some(uri)) => {
if let Some(branding) = match_possible_hostname(uri.host()) {
debug!("Origin header: {:?} Branding for hostname: {:?}", uri, branding);
info!("Origin header: {:?} Branding for hostname: {:?}", uri, branding);
return Some(branding);
}
}
@@ -46,7 +46,7 @@ pub fn get_request_domain_branding(http_request: &HttpRequest) -> Option<DomainB
.flatten();
if let Some(branding) = match_possible_hostname(maybe_host_header) {
debug!("Host header: {:?} Branding for hostname: {:?}", maybe_host_header, branding);
info!("Host header: {:?} Branding for hostname: {:?}", maybe_host_header, branding);
return Some(branding);
}
@@ -123,7 +123,6 @@ use crate::http_server::endpoints::w2l::get_w2l_template_use_count::get_w2l_temp
use crate::http_server::endpoints::w2l::get_w2l_upload_template_job_status::get_w2l_upload_template_job_status_handler;
use crate::http_server::endpoints::w2l::list_w2l_templates::list_w2l_templates_handler;
use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_template_mod_approval_handler;
use crate::http_server::routes::add_studio_gen2_routes::add_studio_gen2_routes;
use crate::http_server::routes::beta_key_routes::add_beta_key_routes;
use crate::http_server::routes::job_routes::add_job_routes;
use crate::http_server::routes::media_files_routes::add_media_file_routes;
@@ -170,7 +169,6 @@ pub fn add_routes<T, B> (app: App<T>, server_environment: ServerEnvironment) ->
app = add_tag_routes(app); /* /v1/tags */
app = add_model_download_routes(app);
app = add_workflow_routes(app);
app = add_studio_gen2_routes(app);
app = add_job_routes(app);
app = add_engine_routes(app); /* /v1/engine/... */
@@ -1,24 +0,0 @@
use actix_http::body::MessageBody;
use actix_service::ServiceFactory;
use actix_web::{App, Error, HttpResponse, web};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use crate::http_server::endpoints::studio_gen2::enqueue_studio_gen2_handler::enqueue_studio_gen2_handler;
pub fn add_studio_gen2_routes<T, B> (app: App<T>) -> App<T>
where
B: MessageBody,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
{
app.service(web::scope("/v1/studio_gen2")
.service(web::resource("/enqueue")
.route(web::post().to(enqueue_studio_gen2_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
}
@@ -1,5 +1,4 @@
pub mod add_routes;
pub mod add_studio_gen2_routes;
pub mod beta_key_routes;
pub mod job_routes;
pub mod media_files_routes;
@@ -26,7 +26,6 @@ mod tests {
// UUID with the dashes
assert!(validate_idempotency_token_format("bf12126a-2de0-4527-bbbf-156c4c45ae21").is_ok());
assert!(validate_idempotency_token_format("220ace1b-db32-42c4-bb87-49176c85a907").is_ok());
assert!(validate_idempotency_token_format("e0704f17-ee75-4f7c-ae4c-a1fda56a50cd").is_ok());
// UUID without the dashes
assert!(validate_idempotency_token_format("220ace1bdb3242c4bb8749176c85a907").is_ok());
@@ -132,7 +132,6 @@ async fn main() -> AnyhowResult<()> {
app_name: "storyteller-web",
default_logging_override: Some(DEFAULT_RUST_LOG),
config_search_directories: &[".", "./config", "crates/service/web/storyteller_web/config"],
ignore_legacy_dot_env_file: true,
})?;
let common_env = CommonEnv::read_from_env()?;
@@ -388,11 +387,6 @@ async fn main() -> AnyhowResult<()> {
"noreply@storyteller.ai".to_string(),
"FakeYouHanashi1".to_string())?;
let server_environment_typed = match server_environment {
ServerEnvironment::Production => server_environment::ServerEnvironment::Production,
ServerEnvironment::Development => server_environment::ServerEnvironment::Development,
};
let server_state = ServerState {
env_config: EnvConfig {
num_workers,
@@ -410,8 +404,7 @@ async fn main() -> AnyhowResult<()> {
client: stripe_client,
},
hostname: server_hostname,
server_environment_old: server_environment,
server_environment: server_environment_typed,
server_environment,
flags: service_feature_flags,
third_party_url_redirector,
health_check_status,
@@ -568,7 +561,7 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()>
let hostname = server_state.hostname.clone();
// TODO(bt,2023-11-12): Remove the old type.
let old_server_environment = server_state.server_environment_old;
let old_server_environment = server_state.server_environment;
let new_server_environment = match old_server_environment {
ServerEnvironment::Development => server_environment::ServerEnvironment::Development,
ServerEnvironment::Production => server_environment::ServerEnvironment::Production,
@@ -50,12 +50,8 @@ pub struct ServerState {
pub hostname: String,
#[deprecated(note = "Use `server_environment` instead")]
/// Knowing if we're in production will allow us to turn off development-only functionalities.
pub server_environment_old: ServerEnvironment,
/// Knowing if we're in production will allow us to turn off development-only functionalities.
pub server_environment: server_environment::ServerEnvironment,
pub server_environment: ServerEnvironment,
/// Feature flags will allow us to restart the service with different conditions embedded in the code.
pub flags: StaticFeatureFlags,