mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 15:55:54 -04:00
Compare commits
23
Commits
jan-24-mithoo
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a938ceed1 | ||
|
|
62d708e357 | ||
|
|
72083eb030 | ||
|
|
62afa22d8c | ||
|
|
418b116469 | ||
|
|
42c7c70387 | ||
|
|
39c52d5377 | ||
|
|
cea3668cfc | ||
|
|
1caf1f38b8 | ||
|
|
f0b2a07613 | ||
|
|
3cd04414f8 | ||
|
|
f3776e953c | ||
|
|
d923699009 | ||
|
|
f842d795d6 | ||
|
|
7a1ce7c9fd | ||
|
|
d0a3c02c00 | ||
|
|
32bc400686 | ||
|
|
e3973e4ce1 | ||
|
|
1d7bc11be3 | ||
|
|
5321eb145d | ||
|
|
48043fb602 | ||
|
|
8af2035ee6 | ||
|
|
1e2f73e44a |
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -13,6 +13,7 @@ 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)]
|
||||
|
||||
+28
@@ -34,4 +34,32 @@ pub struct StudioGen2Payload {
|
||||
#[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>,
|
||||
}
|
||||
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
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,4 +1,5 @@
|
||||
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;
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
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,5 +5,6 @@ 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;
|
||||
@@ -52,6 +52,14 @@ 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)]
|
||||
@@ -87,4 +95,10 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,11 +175,25 @@ COMFY_MAPPINGS_DIRECTORY="/home/bt/dev/storyteller/V2V-StylesList/V2VWorkflows/m
|
||||
# #
|
||||
# ================================= #
|
||||
|
||||
# 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 #
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
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")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod animate_x_dependencies;
|
||||
pub mod animate_x_inference_command;
|
||||
pub mod animate_x_process_frames_command;
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod animate_x;
|
||||
pub mod download_file_for_studio;
|
||||
pub mod process_single_studio_gen2_job;
|
||||
pub mod stable_animator_command;
|
||||
pub mod stable_animator;
|
||||
pub mod studio_gen2_dependencies;
|
||||
pub mod studio_gen2_dirs;
|
||||
pub mod validate_and_save_results;
|
||||
pub mod validate_and_save_results;
|
||||
mod resize_image_for_studio;
|
||||
+304
-123
@@ -1,65 +1,51 @@
|
||||
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::download_file_for_studio::{download_file_for_studio, DownloadFileForStudioArgs};
|
||||
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::workflow::face_fusion::process_face_fusion_job::process_face_fusion_job;
|
||||
use crate::job::job_types::workflow::live_portrait::process_live_portrait_job::process_live_portrait_job;
|
||||
use crate::job::job_types::workflow::video_style_transfer::process_video_style_transfer_job::process_video_style_transfer_job;
|
||||
use crate::job::job_types::workflow::video_style_transfer::steps::check_and_validate_job::check_and_validate_job;
|
||||
use crate::job::job_types::workflow::video_style_transfer::steps::download_global_ipa_image::{download_global_ipa_image, DownloadGlobalIpaImageArgs};
|
||||
use crate::job::job_types::workflow::video_style_transfer::steps::download_input_videos::{download_input_videos, DownloadInputVideoArgs};
|
||||
use crate::job::job_types::workflow::video_style_transfer::steps::post_process_add_watermark::{post_process_add_watermark, PostProcessAddWatermarkArgs};
|
||||
use crate::job::job_types::workflow::video_style_transfer::steps::post_process_restore_audio::{post_process_restore_audio, PostProcessRestoreVideoArgs};
|
||||
use crate::job::job_types::workflow::video_style_transfer::steps::preprocess_save_audio::{preprocess_save_audio, ProcessSaveAudioArgs};
|
||||
use crate::job::job_types::workflow::video_style_transfer::steps::preprocess_trim_and_resample_videos::{preprocess_trim_and_resample_videos, ProcessTrimAndResampleVideoArgs};
|
||||
use crate::job::job_types::workflow::video_style_transfer::util::comfy_dirs::ComfyDirs;
|
||||
use crate::job::job_types::workflow::video_style_transfer::util::process_preview_updates::PreviewProcessor;
|
||||
use crate::job::job_types::workflow::video_style_transfer::util::video_pathing::{PrimaryInputVideoAndPaths, SecondaryInputVideoAndPaths, VideoPathing};
|
||||
use crate::job::job_types::workflow::video_style_transfer::util::write_workflow_prompt::{write_workflow_prompt, WorkflowPromptArgs};
|
||||
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 bucket_paths::legacy::remote_file_manager_paths::remote_cloud_bucket_details::RemoteCloudBucketDetails;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_directory::MediaFileBucketDirectory;
|
||||
use cloud_storage::remote_file_manager::remote_cloud_file_manager::RemoteCloudFileClient;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
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 filesys::file_deletion::safe_recursively_delete_files::safe_recursively_delete_files;
|
||||
use filesys::path_to_string::path_to_string;
|
||||
use log::{error, info, warn};
|
||||
use mysql_queries::payloads::generic_inference_args::generic_inference_args::PolymorphicInferenceArgs::{Cu, S2};
|
||||
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
|
||||
use mysql_queries::queries::model_weights::get::get_weight::get_weight_by_token_with_transactor;
|
||||
use mysql_queries::utils::transactor::Transactor;
|
||||
use std::fs::read_to_string;
|
||||
use std::io::stdout;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::oneshot::channel;
|
||||
use filesys::create_dir_all_if_missing::create_dir_all_if_missing;
|
||||
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;
|
||||
use crate::job::job_types::studio_gen2::stable_animator_command::InferenceArgs;
|
||||
use crate::job::job_types::studio_gen2::validate_and_save_results::{validate_and_save_results, SaveResultsArgs};
|
||||
// TODO(bt,2025-01-16): This is a stub for Studio Gen2
|
||||
// TODO(bt,2025-01-16): This is a stub for Studio Gen2
|
||||
// TODO(bt,2025-01-16): This is a stub for Studio Gen2
|
||||
// TODO(bt,2025-01-16): This is a stub for Studio Gen2
|
||||
|
||||
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> {
|
||||
|
||||
// TODO(bt,2025-01-16): This is a stub for Studio Gen2
|
||||
// TODO(bt,2025-01-16): This is a stub for Studio Gen2
|
||||
// TODO(bt,2025-01-16): This is a stub for Studio Gen2
|
||||
// TODO(bt,2025-01-16): This is a stub for Studio Gen2
|
||||
|
||||
let mut job_progress_reporter = deps
|
||||
.clients
|
||||
.job_progress_reporter
|
||||
@@ -73,36 +59,25 @@ pub async fn process_single_studio_gen2_job(
|
||||
.as_ref()
|
||||
.ok_or_else(|| ProcessSingleJobError::JobSystemMisconfiguration(Some("Missing Studio Gen2 dependencies".to_string())))?;
|
||||
|
||||
// // ==================== UNPACK + VALIDATE INFERENCE ARGS ==================== //
|
||||
//
|
||||
// let job_args = check_and_validate_job(job)?;
|
||||
|
||||
// ===================== DOWNLOAD REQUIRED MODELS IF NOT EXIST ===================== //
|
||||
// ==================== 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);
|
||||
}
|
||||
|
||||
//// TODO: Replace all other paths with this
|
||||
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;
|
||||
let remote_cloud_file_client = match remote_cloud_file_client {
|
||||
Ok(res) => {
|
||||
res
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(ProcessSingleJobError::from(anyhow!("failed to get remote cloud file client")));
|
||||
}
|
||||
};
|
||||
|
||||
// info!("Grabbing redis connection from pool");
|
||||
//
|
||||
// let redis_pool_dep = deps
|
||||
// .db
|
||||
// .maybe_keepalive_redis_pool.clone();
|
||||
//
|
||||
// let redis_pool = redis_pool_dep
|
||||
// .ok_or_else(|| ProcessSingleJobError::Other(anyhow!("failed to get redis pool")))?;
|
||||
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");
|
||||
|
||||
@@ -113,72 +88,146 @@ pub async fn process_single_studio_gen2_job(
|
||||
ProcessSingleJobError::Other(anyhow!("Could not acquire DB pool: {:?}", e))
|
||||
})?;
|
||||
|
||||
let maybe_args = job.maybe_inference_args
|
||||
let studio_args = job.maybe_inference_args
|
||||
.as_ref()
|
||||
.map(|args| args.args.as_ref())
|
||||
.flatten();
|
||||
|
||||
let poly_args = match maybe_args {
|
||||
None => return Err(ProcessSingleJobError::Other(anyhow!("Job args not found"))),
|
||||
Some(args) => args,
|
||||
};
|
||||
|
||||
let studio_args = match poly_args {
|
||||
S2(args) => args,
|
||||
_ => return Err(ProcessSingleJobError::Other(anyhow!("Studio Gen2 args not found"))),
|
||||
};
|
||||
.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 image_file;
|
||||
let unaltered_image_file;
|
||||
|
||||
match studio_args.image_file.as_ref() {
|
||||
None => return Err(ProcessSingleJobError::Other(anyhow!("image_file not set"))),
|
||||
Some(media_token) => {
|
||||
image_file = download_file_for_studio(DownloadFileForStudioArgs {
|
||||
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 {:?}", &image_file.file_path);
|
||||
info!("Downloaded image to {:?}", &unaltered_image_file.file_path);
|
||||
}
|
||||
}
|
||||
|
||||
assert_file_is_image(&unaltered_image_file)?;
|
||||
|
||||
// ==================== DOWNLOAD VIDEO ==================== //
|
||||
|
||||
let video_file;
|
||||
let unaltered_video_file;
|
||||
|
||||
match studio_args.video_file.as_ref() {
|
||||
None => return Err(ProcessSingleJobError::Other(anyhow!("video_file not set"))),
|
||||
Some(media_token) => {
|
||||
video_file = download_file_for_studio(DownloadFileForStudioArgs {
|
||||
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 {:?}", &video_file.file_path);
|
||||
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 ======================== //
|
||||
// ========================= TRIM AND PREPROCESS VIDEO ======================== //
|
||||
|
||||
//let expected_frame_count = preprocess_trim_and_resample_videos(ProcessTrimAndResampleVideoArgs {
|
||||
// comfy_args: studio_args,
|
||||
// comfy_deps: gen2_deps,
|
||||
// comfy_dirs: &work_paths,
|
||||
// videos: &mut videos,
|
||||
//})?;
|
||||
//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 ==================== //
|
||||
|
||||
@@ -190,41 +239,101 @@ pub async fn process_single_studio_gen2_job(
|
||||
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 pose_frames_dir = work_paths.output_dir.path().join("pose_frames");
|
||||
create_dir_all_if_missing(&pose_frames_dir)?;
|
||||
|
||||
let video_output_path = work_paths.output_dir.path().join("output_video.mp4");
|
||||
|
||||
let video_frames_output_dir = work_paths.output_dir.path().join("final_frames");
|
||||
create_dir_all_if_missing(&video_frames_output_dir)?;
|
||||
let inference_duration;
|
||||
|
||||
info!("Running Studio Gen2 inference...");
|
||||
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 inference_start_time = Instant::now();
|
||||
let pose_pkl_dir= work_paths.output_dir.path().join("pose_pickle_data");
|
||||
create_dir_all_if_missing(&pose_pkl_dir)?;
|
||||
|
||||
let command_exit_status = gen2_deps
|
||||
.command
|
||||
.execute_inference(
|
||||
InferenceArgs {
|
||||
stderr_output_file: &stderr_output_file,
|
||||
stdout_output_file: &stdout_output_file,
|
||||
start_image_path: &image_file.file_path,
|
||||
pre_pose_video_path: Some(video_file.file_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: &gen2_deps.pretrained_model_name_or_path,
|
||||
posenet_model_name_or_path: &gen2_deps.posenet_model_name_or_path,
|
||||
face_encoder_model_name_or_path: &gen2_deps.face_encoder_model_name_or_path,
|
||||
unet_model_name_or_path: &gen2_deps.unet_model_name_or_path,
|
||||
output_width: studio_args.output_width,
|
||||
output_height: studio_args.output_height,
|
||||
}).await;
|
||||
let pose_pkl_file = pose_pkl_dir.join("pose.pkl");
|
||||
|
||||
let inference_duration = Instant::now().duration_since(inference_start_time);
|
||||
let pose_frames_dir = work_paths.output_dir.path().join("pose_frames");
|
||||
create_dir_all_if_missing(&pose_frames_dir)?;
|
||||
|
||||
info!("Inference command exited with status: {:?}", command_exit_status);
|
||||
info!("Inference took duration to complete: {:?}", &inference_duration);
|
||||
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) {
|
||||
@@ -244,7 +353,7 @@ pub async fn process_single_studio_gen2_job(
|
||||
if let Err(err) = check_file_exists(&video_output_path) {
|
||||
error!("Output file does not exist: {:?}", err);
|
||||
|
||||
error!("Inference failed: {:?}", command_exit_status);
|
||||
//error!("Inference failed: {:?}", command_exit_status);
|
||||
|
||||
if let Ok(contents) = read_to_string(&stderr_output_file) {
|
||||
warn!("Captured stderr output: {}", contents);
|
||||
@@ -298,7 +407,8 @@ pub async fn process_single_studio_gen2_job(
|
||||
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
|
||||
@@ -312,8 +422,11 @@ pub async fn process_single_studio_gen2_job(
|
||||
};
|
||||
|
||||
// ==================== CLEANUP/ DELETE TEMP FILES ==================== //
|
||||
|
||||
info!("Inference took duration to complete: {:?}", &inference_duration);
|
||||
|
||||
maybe_debug_sleep(studio_args).await;
|
||||
print_work_dirs(&work_paths);
|
||||
maybe_debug_sleep_polling_jobs(studio_args, &deps.db.mysql_pool).await;
|
||||
|
||||
info!("Cleaning up temporary files...");
|
||||
|
||||
@@ -346,6 +459,74 @@ pub async fn process_single_studio_gen2_job(
|
||||
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}");
|
||||
tokio::time::sleep(Duration::from_millis(sleep_millis)).await;
|
||||
// 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)))
|
||||
}
|
||||
}
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
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(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod stable_animator_command;
|
||||
pub mod stable_animator_dependencies;
|
||||
+17
-34
@@ -1,40 +1,14 @@
|
||||
/*
|
||||
python inference_advanced.py \
|
||||
--start_image_path="${STARTING_IMAGE}" \
|
||||
--frame_output_dir="${OUTPUT_DIR}" \
|
||||
--pose_images_dir="/tmp/pose" \
|
||||
--pre_pose_video_path="inference/zeihan_trimmed.mp4" \
|
||||
|
||||
--video_output_path="out.mp4" \
|
||||
|
||||
--pretrained_model_name_or_path="checkpoints/stable-video-diffusion-img2vid-xt" \
|
||||
--posenet_model_name_or_path="checkpoints/Animation/pose_net.pth" \
|
||||
--face_encoder_model_name_or_path="checkpoints/Animation/face_encoder.pth" \
|
||||
--unet_model_name_or_path="checkpoints/Animation/unet.pth" \
|
||||
|
||||
--width=1024 \
|
||||
--height=576 \
|
||||
--guidance_scale=3.0 \
|
||||
--num_inference_steps=25 \
|
||||
--tile_size=16 \
|
||||
--overlap=4 \
|
||||
--noise_aug_strength=0.02 \
|
||||
--frames_overlap=4 \
|
||||
--decode_chunk_size=4 \
|
||||
--gradient_checkpointing
|
||||
*/
|
||||
|
||||
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 log::{debug, info, warn};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
use errors::AnyhowResult;
|
||||
use filesys::path_to_string::path_to_string;
|
||||
use subprocess_common::command_exit_status::CommandExitStatus;
|
||||
use subprocess_common::docker_options::DockerOptions;
|
||||
use crate::util::get_filtered_env_vars::get_filtered_env_vars_hashmap;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StableAnimatorCommand {
|
||||
@@ -83,14 +57,17 @@ pub struct InferenceArgs<'s> {
|
||||
|
||||
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: PathBuf::from("/home/bt/dev/storyteller/storyteller-ml/animation/StableAnimator"),
|
||||
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()),
|
||||
maybe_virtual_env_activation_command: Some(String::from("source venv/bin/activate")),
|
||||
// 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,
|
||||
})
|
||||
@@ -176,6 +153,12 @@ impl StableAnimatorCommand {
|
||||
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 ");
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
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")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
+25
-13
@@ -1,7 +1,9 @@
|
||||
use crate::job::job_types::studio_gen2::stable_animator_command::StableAnimatorCommand;
|
||||
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::{Path, PathBuf};
|
||||
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)
|
||||
@@ -10,28 +12,38 @@ pub struct StudioGen2Dependencies {
|
||||
pub input_directory: PathBuf,
|
||||
pub output_directory: PathBuf,
|
||||
|
||||
pub command: StableAnimatorCommand,
|
||||
pub ffmpeg: FfmpegCommandRunner,
|
||||
|
||||
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,
|
||||
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
|
||||
// TODO: Configurability of input/output dirs
|
||||
input_directory: PathBuf::from("/tmp/input"),
|
||||
output_directory: PathBuf::from("/tmp/output"),
|
||||
command: StableAnimatorCommand::new_from_env()?,
|
||||
|
||||
ffmpeg: FfmpegCommandRunner::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")?,
|
||||
animate_x,
|
||||
stable_animator,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
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,6 +1,9 @@
|
||||
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;
|
||||
@@ -0,0 +1,88 @@
|
||||
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,6 +2,7 @@ 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;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
+61
-24
@@ -3,6 +3,8 @@ 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;
|
||||
@@ -41,23 +43,41 @@ 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 {
|
||||
let rooted_path = bucket_path.get_full_object_path_str();
|
||||
Self::from_rooted_path(domain, rooted_path)
|
||||
Self::from_media_path_and_env(domain, ServerEnvironment::Production, bucket_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 {
|
||||
let mut cdn_url = domain.new_cdn_url();
|
||||
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);
|
||||
cdn_url.set_path(rooted_path);
|
||||
MediaLinks {
|
||||
cdn_url,
|
||||
maybe_thumbnail_template: thumbnail_template(domain, rooted_path),
|
||||
maybe_thumbnail_template: thumbnail_template(domain, server_environment, rooted_path),
|
||||
maybe_video_previews: VideoPreviews::from_rooted_path(domain, rooted_path),
|
||||
}
|
||||
}
|
||||
@@ -98,14 +118,20 @@ fn video_preview(media_domain: MediaDomain, rooted_path: &str, thumbnail_type: P
|
||||
}
|
||||
|
||||
/// Returns a thumbnail template for image
|
||||
fn thumbnail_template(media_domain: MediaDomain, rooted_path: &str) -> Option<String> {
|
||||
fn thumbnail_template(media_domain: MediaDomain, server_environment: ServerEnvironment, 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 = media_domain.cdn_url_str();
|
||||
Some(format!("{host}/cdn-cgi/image/width={{WIDTH}},quality={QUALITY}{rooted_path}"))
|
||||
|
||||
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}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a thumbnail template for video
|
||||
@@ -127,17 +153,28 @@ 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.fakeyou.com/foo/bar.wav");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.wav");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
}
|
||||
@@ -145,7 +182,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.fakeyou.com/foo/bar.glb");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.fakeyou.com/foo/bar.glb");
|
||||
assert_eq!(links.maybe_thumbnail_template, None);
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
}
|
||||
@@ -153,21 +190,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.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.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.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.fakeyou.com/foo/bar.mp4");
|
||||
assert_eq!(links.cdn_url.as_str(), "https://cdn-2.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.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");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +216,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.fakeyou.com/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav");
|
||||
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.maybe_thumbnail_template, None);
|
||||
assert_eq!(links.maybe_video_previews, None);
|
||||
}
|
||||
@@ -189,8 +226,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.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.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.maybe_video_previews, None);
|
||||
}
|
||||
|
||||
@@ -199,13 +236,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.fakeyou.com/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4");
|
||||
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.maybe_thumbnail_template, None);
|
||||
let video_previews = links.maybe_video_previews.expect("should have previews");
|
||||
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");
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod cdn_link;
|
||||
pub mod cover_image_links;
|
||||
pub mod media_domain;
|
||||
pub mod media_file_cover_image_details;
|
||||
|
||||
+1
-1
@@ -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,
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
|
||||
+2
-2
@@ -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,
|
||||
maybe_user_session.as_ref());
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
let priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
+2
-2
@@ -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,
|
||||
maybe_user_session.as_ref()
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref()
|
||||
);
|
||||
|
||||
// Separate priority for animation.
|
||||
|
||||
+2
-2
@@ -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,
|
||||
maybe_user_session.as_ref()
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref()
|
||||
);
|
||||
|
||||
// Separate priority for animation.
|
||||
|
||||
+2
-2
@@ -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,
|
||||
maybe_user_session.as_ref()
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref()
|
||||
);
|
||||
|
||||
// Separate priority for animation.
|
||||
|
||||
+3
-3
@@ -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(media_domain, &public_bucket_path),
|
||||
media_links: MediaLinks::from_media_path_and_env(media_domain, server_state.server_environment, &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(media_domain, &public_bucket_path),
|
||||
media_links: MediaLinks::from_rooted_path_and_env(media_domain, server_state.server_environment, &public_bucket_path),
|
||||
public_bucket_url: bucket_url_from_str_path(&public_bucket_path)
|
||||
.map_err(|err| {
|
||||
warn!("error creating URL: {:?}", err);
|
||||
|
||||
+16
-4
@@ -21,6 +21,12 @@ 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)
|
||||
}
|
||||
|
||||
@@ -34,23 +40,29 @@ 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)
|
||||
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);
|
||||
}
|
||||
|
||||
#[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])));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -340,7 +340,8 @@ 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(media_domain, &public_bucket_path),
|
||||
media_links: MediaLinks::from_media_path_and_env(
|
||||
media_domain, server_state.server_environment, &public_bucket_path),
|
||||
public_bucket_path: public_bucket_path
|
||||
.get_full_object_path_str()
|
||||
.to_string(),
|
||||
|
||||
+2
-2
@@ -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,
|
||||
maybe_user_session.as_ref());
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
let priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
+1
-1
@@ -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, Some(&user_session));
|
||||
let plan = get_correct_plan_for_session(server_state.server_environment_old, Some(&user_session));
|
||||
let priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
// ==================== DEBUG MODE + ROUTING TAG ==================== //
|
||||
|
||||
+37
-1
@@ -24,6 +24,7 @@ use mysql_queries::payloads::generic_inference_args::generic_inference_args::{Ge
|
||||
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;
|
||||
@@ -66,9 +67,27 @@ pub struct EnqueueStudioGen2Request {
|
||||
/// 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)]
|
||||
@@ -174,7 +193,7 @@ pub async fn enqueue_studio_gen2_handler(
|
||||
|
||||
// TODO: Plan should handle "first anonymous use" and "investor" cases.
|
||||
let plan = get_correct_plan_for_session(
|
||||
server_state.server_environment,
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
@@ -247,6 +266,16 @@ pub async fn enqueue_studio_gen2_handler(
|
||||
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()),
|
||||
@@ -255,6 +284,13 @@ pub async fn enqueue_studio_gen2_handler(
|
||||
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...");
|
||||
|
||||
+1
-1
@@ -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,
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
priority_level = plan.tts_base_priority_level();
|
||||
|
||||
+1
-1
@@ -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,
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
priority_level = plan.tts_base_priority_level();
|
||||
|
||||
+1
-1
@@ -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,
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
priority_level = plan.tts_base_priority_level();
|
||||
|
||||
+1
-1
@@ -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,
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
+2
-2
@@ -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,
|
||||
maybe_user_session.as_ref()
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref()
|
||||
);
|
||||
|
||||
// Separate priority for animation.
|
||||
|
||||
+2
-2
@@ -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,
|
||||
maybe_user_session.as_ref()
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref()
|
||||
);
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
|
||||
+1
-1
@@ -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, Some(&user_session));
|
||||
let plan = get_correct_plan_for_session(server_state.server_environment_old, Some(&user_session));
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
let priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
+1
-1
@@ -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, Some(&user_session));
|
||||
let plan = get_correct_plan_for_session(server_state.server_environment_old, Some(&user_session));
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
let priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
+1
-1
@@ -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,
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
|
||||
+1
-1
@@ -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,
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
|
||||
+2
-2
@@ -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,
|
||||
server_state.server_environment_old,
|
||||
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,
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref()
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
maybe_user_session.as_ref());
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
let priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
+2
-2
@@ -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,
|
||||
maybe_user_session.as_ref()
|
||||
server_state.server_environment_old,
|
||||
maybe_user_session.as_ref()
|
||||
);
|
||||
|
||||
// Separate priority for animation.
|
||||
|
||||
+3
-3
@@ -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::{info, warn};
|
||||
use log::{debug, 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()) {
|
||||
info!("Origin header: {:?} Branding for hostname: {:?}", uri, branding);
|
||||
debug!("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) {
|
||||
info!("Host header: {:?} Branding for hostname: {:?}", maybe_host_header, branding);
|
||||
debug!("Host header: {:?} Branding for hostname: {:?}", maybe_host_header, branding);
|
||||
return Some(branding);
|
||||
}
|
||||
|
||||
|
||||
@@ -388,6 +388,11 @@ 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,
|
||||
@@ -405,7 +410,8 @@ async fn main() -> AnyhowResult<()> {
|
||||
client: stripe_client,
|
||||
},
|
||||
hostname: server_hostname,
|
||||
server_environment,
|
||||
server_environment_old: server_environment,
|
||||
server_environment: server_environment_typed,
|
||||
flags: service_feature_flags,
|
||||
third_party_url_redirector,
|
||||
health_check_status,
|
||||
@@ -562,7 +568,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;
|
||||
let old_server_environment = server_state.server_environment_old;
|
||||
let new_server_environment = match old_server_environment {
|
||||
ServerEnvironment::Development => server_environment::ServerEnvironment::Development,
|
||||
ServerEnvironment::Production => server_environment::ServerEnvironment::Production,
|
||||
|
||||
@@ -50,8 +50,12 @@ 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: ServerEnvironment,
|
||||
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,
|
||||
|
||||
/// Feature flags will allow us to restart the service with different conditions embedded in the code.
|
||||
pub flags: StaticFeatureFlags,
|
||||
|
||||
Reference in New Issue
Block a user