mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-07 02:21:23 +00:00
Merge pull request #63 from storytold/bt-twitter-dimensions
SadTalker: Final API shape. Twitter dimensions, non-default options are opt-in, premium watermark
This commit is contained in:
13 files changed
+313
-98
No files matched your search
Generated
+2
@@ -2941,6 +2941,7 @@ dependencies = [
|
||||
"hashing",
|
||||
"hostname",
|
||||
"hyper 0.14.24",
|
||||
"images",
|
||||
"jobs_common",
|
||||
"lfu",
|
||||
"log",
|
||||
@@ -2959,6 +2960,7 @@ dependencies = [
|
||||
"subprocess",
|
||||
"subprocess_common",
|
||||
"tempdir",
|
||||
"testing",
|
||||
"tokens",
|
||||
"tokio 1.25.0",
|
||||
"tts_common",
|
||||
|
||||
@@ -19,4 +19,8 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod resize_image_file_preserving_aspect;
|
||||
pub mod resize_preserving_aspect;
|
||||
|
||||
// Re-export image to everyone importing this library
|
||||
pub use image;
|
||||
@@ -0,0 +1,43 @@
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
|
||||
use image::DynamicImage;
|
||||
use image::io::Reader;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
|
||||
use crate::resize_preserving_aspect::resize_preserving_aspect;
|
||||
|
||||
pub fn resize_image_file_preserving_aspect<P: AsRef<Path>>(
|
||||
source_image_file: P,
|
||||
new_width: u32,
|
||||
new_height: u32,
|
||||
exact_fit: bool,
|
||||
) -> AnyhowResult<DynamicImage> {
|
||||
let file = File::open(source_image_file)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let reader = Reader::new(reader)
|
||||
.with_guessed_format()?;
|
||||
|
||||
let img = reader.decode()?;
|
||||
|
||||
Ok(resize_preserving_aspect(&img, new_width, new_height, exact_fit))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
use crate::resize_image_file_preserving_aspect::resize_image_file_preserving_aspect;
|
||||
|
||||
#[test]
|
||||
fn test_happy_path() {
|
||||
// NB: The other function does most of the testing
|
||||
let path = test_file_path("test_data/image/mochi.jpg").unwrap();
|
||||
let image = resize_image_file_preserving_aspect(path, 100, 100, true).unwrap();
|
||||
assert_eq!(image.width(), 100);
|
||||
assert_eq!(image.height(), 100);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,21 @@
|
||||
use image::DynamicImage;
|
||||
use image::imageops::FilterType;
|
||||
|
||||
pub fn resize_preserving_aspect(source_image: &DynamicImage, new_width: u32, new_height: u32) -> DynamicImage {
|
||||
source_image.resize(new_width, new_height, FilterType::Lanczos3)
|
||||
/// Resize an image preserving the exact aspect ratio of the source image.
|
||||
///
|
||||
/// If `exact_fit` is true, the image will be resized to fit the largest dimension,
|
||||
/// then cropped to fit the precise dimensions specified.
|
||||
pub fn resize_preserving_aspect(
|
||||
source_image: &DynamicImage,
|
||||
new_width: u32,
|
||||
new_height: u32,
|
||||
exact_fit: bool,
|
||||
) -> DynamicImage {
|
||||
if exact_fit {
|
||||
source_image.resize_to_fill(new_width, new_height, FilterType::Lanczos3)
|
||||
} else {
|
||||
source_image.resize(new_width, new_height, FilterType::Lanczos3)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -38,9 +51,7 @@ mod tests {
|
||||
#[test]
|
||||
fn downsize_wide_image_preserving_aspect() {
|
||||
let image = open_wide_image().unwrap();
|
||||
assert!(image.width() > image.height());
|
||||
|
||||
let resized = resize_preserving_aspect(&image, 500, 500);
|
||||
let resized = resize_preserving_aspect(&image, 500, 500, false);
|
||||
assert!(resized.width() > resized.height());
|
||||
assert_eq!(resized.width(), 500);
|
||||
assert_eq!(resized.height(), 375);
|
||||
@@ -49,9 +60,7 @@ mod tests {
|
||||
#[test]
|
||||
fn downsize_tall_image_preserving_aspect() {
|
||||
let image = open_tall_image().unwrap();
|
||||
assert!(image.width() < image.height());
|
||||
|
||||
let resized = resize_preserving_aspect(&image, 500, 500);
|
||||
let resized = resize_preserving_aspect(&image, 500, 500, false);
|
||||
assert!(resized.width() < resized.height());
|
||||
assert_eq!(resized.width(), 377);
|
||||
assert_eq!(resized.height(), 500);
|
||||
@@ -60,9 +69,7 @@ mod tests {
|
||||
#[test]
|
||||
fn enlarge_wide_image_preserving_aspect() {
|
||||
let image = open_wide_image().unwrap();
|
||||
assert!(image.width() > image.height());
|
||||
|
||||
let resized = resize_preserving_aspect(&image, 1200, 1200);
|
||||
let resized = resize_preserving_aspect(&image, 1200, 1200, false);
|
||||
assert!(resized.width() > resized.height());
|
||||
assert_eq!(resized.width(), 1200);
|
||||
assert_eq!(resized.height(), 900);
|
||||
@@ -71,11 +78,57 @@ mod tests {
|
||||
#[test]
|
||||
fn enlarge_tall_image_preserving_aspect() {
|
||||
let image = open_tall_image().unwrap();
|
||||
assert!(image.width() < image.height());
|
||||
|
||||
let resized = resize_preserving_aspect(&image, 1200, 1200);
|
||||
let resized = resize_preserving_aspect(&image, 1200, 1200, false);
|
||||
assert!(resized.width() < resized.height());
|
||||
assert_eq!(resized.width(), 904);
|
||||
assert_eq!(resized.height(), 1200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_wide_image_preserving_aspect_exact() {
|
||||
// Downscale
|
||||
let image = open_wide_image().unwrap();
|
||||
let resized = resize_preserving_aspect(&image, 500, 500, true);
|
||||
assert_eq!(resized.width(), 500);
|
||||
assert_eq!(resized.height(), 500);
|
||||
|
||||
// Upscale
|
||||
let resized = resize_preserving_aspect(&image, 1200, 1200, true);
|
||||
assert_eq!(resized.width(), 1200);
|
||||
assert_eq!(resized.height(), 1200);
|
||||
|
||||
// Really wide
|
||||
let resized = resize_preserving_aspect(&image, 1200, 100, true);
|
||||
assert_eq!(resized.width(), 1200);
|
||||
assert_eq!(resized.height(), 100);
|
||||
|
||||
// Really tall
|
||||
let resized = resize_preserving_aspect(&image, 100, 1200, true);
|
||||
assert_eq!(resized.width(), 100);
|
||||
assert_eq!(resized.height(), 1200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_tall_image_preserving_aspect_exact() {
|
||||
// Downscale
|
||||
let image = open_tall_image().unwrap();
|
||||
let resized = resize_preserving_aspect(&image, 500, 500, true);
|
||||
assert_eq!(resized.width(), 500);
|
||||
assert_eq!(resized.height(), 500);
|
||||
|
||||
// Upscale
|
||||
let resized = resize_preserving_aspect(&image, 1200, 1200, true);
|
||||
assert_eq!(resized.width(), 1200);
|
||||
assert_eq!(resized.height(), 1200);
|
||||
|
||||
// Really wide
|
||||
let resized = resize_preserving_aspect(&image, 1200, 100, true);
|
||||
assert_eq!(resized.width(), 1200);
|
||||
assert_eq!(resized.height(), 100);
|
||||
|
||||
// Really tall
|
||||
let resized = resize_preserving_aspect(&image, 100, 1200, true);
|
||||
assert_eq!(resized.width(), 100);
|
||||
assert_eq!(resized.height(), 1200);
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,8 @@ mod tests {
|
||||
maybe_preprocess: None,
|
||||
maybe_make_still: None,
|
||||
maybe_remove_watermark: None,
|
||||
maybe_resize_width: None,
|
||||
maybe_resize_height: None,
|
||||
})),
|
||||
};
|
||||
|
||||
|
||||
@@ -34,9 +34,21 @@ pub struct LipsyncArgs {
|
||||
pub maybe_make_still: Option<bool>,
|
||||
|
||||
/// Omit adding the watermark
|
||||
#[serde(rename = "w")] // NB: DO NOT CHANGE. It could break live jobs. Renamed to be fewer bytes.
|
||||
#[serde(rename = "m")] // NB: DO NOT CHANGE. It could break live jobs. Renamed to be fewer bytes.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub maybe_remove_watermark: Option<bool>,
|
||||
|
||||
/// Resize width
|
||||
/// NB: FOR TESTING ONLY
|
||||
#[serde(rename = "w")] // NB: DO NOT CHANGE. It could break live jobs. Renamed to be fewer bytes.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub maybe_resize_width: Option<u32>,
|
||||
|
||||
/// Resize height
|
||||
/// NB: FOR TESTING ONLY
|
||||
#[serde(rename = "h")] // NB: DO NOT CHANGE. It could break live jobs. Renamed to be fewer bytes.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub maybe_resize_height: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -152,6 +164,8 @@ mod tests {
|
||||
maybe_preprocess: None,
|
||||
maybe_make_still: None,
|
||||
maybe_remove_watermark: None,
|
||||
maybe_resize_width: None,
|
||||
maybe_resize_height: None,
|
||||
};
|
||||
let json = serde_json::ser::to_string(&args).unwrap();
|
||||
assert_eq!(json, r#"{"a":{"F":"audio_media_file"},"i":{"F":"image_media_file"}}"#.to_string());
|
||||
@@ -167,6 +181,8 @@ mod tests {
|
||||
maybe_preprocess: None,
|
||||
maybe_make_still: None,
|
||||
maybe_remove_watermark: None,
|
||||
maybe_resize_width: None,
|
||||
maybe_resize_height: None,
|
||||
};
|
||||
let json = serde_json::ser::to_string(&args).unwrap();
|
||||
assert_eq!(json, r#"{"a":{"U":"audio_media_upload"},"i":{"U":"image_media_upload"}}"#.to_string());
|
||||
@@ -182,6 +198,8 @@ mod tests {
|
||||
maybe_preprocess: None,
|
||||
maybe_make_still: None,
|
||||
maybe_remove_watermark: None,
|
||||
maybe_resize_width: None,
|
||||
maybe_resize_height: None,
|
||||
};
|
||||
let json = serde_json::ser::to_string(&args).unwrap();
|
||||
assert_eq!(json, r#"{"a":{"T":"audio_tts_result"},"i":{"U":"image_media_upload"}}"#.to_string());
|
||||
@@ -197,6 +215,8 @@ mod tests {
|
||||
maybe_preprocess: None,
|
||||
maybe_make_still: None,
|
||||
maybe_remove_watermark: None,
|
||||
maybe_resize_width: None,
|
||||
maybe_resize_height: None,
|
||||
};
|
||||
let json = serde_json::ser::to_string(&args).unwrap();
|
||||
assert_eq!(json, r#"{"a":{"V":"audio_voice_conversion_result"},"i":{"U":"image_media_upload"}}"#.to_string());
|
||||
@@ -212,6 +232,8 @@ mod tests {
|
||||
maybe_preprocess: None,
|
||||
maybe_make_still: None,
|
||||
maybe_remove_watermark: None,
|
||||
maybe_resize_width: None,
|
||||
maybe_resize_height: None,
|
||||
};
|
||||
let json = serde_json::ser::to_string(&args).unwrap();
|
||||
assert_eq!(json, r#"{"f":"G"}"#.to_string());
|
||||
@@ -227,6 +249,8 @@ mod tests {
|
||||
maybe_preprocess: None,
|
||||
maybe_make_still: None,
|
||||
maybe_remove_watermark: None,
|
||||
maybe_resize_width: None,
|
||||
maybe_resize_height: None,
|
||||
};
|
||||
let json = serde_json::ser::to_string(&args).unwrap();
|
||||
assert_eq!(json, r#"{"f":"R"}"#.to_string());
|
||||
|
||||
@@ -28,6 +28,7 @@ envvar = { path = "../../../lib/envvar" }
|
||||
errors = { path = "../../../lib/errors" }
|
||||
filesys = { path = "../../../lib/files/filesys" }
|
||||
hashing = { path = "../../../lib/files/hashing" }
|
||||
images = { path = "../../../lib/files/images" }
|
||||
jobs_common = { path = "../../../lib/jobs_common" }
|
||||
media = { path = "../../../lib/files/media" }
|
||||
memory_caching = { path = "../../../lib/caching/memory_caching" }
|
||||
@@ -35,6 +36,7 @@ mimetypes = { path = "../../../lib/files/mimetypes" }
|
||||
mysql_queries = { path = "../../../lib/mysql_queries" }
|
||||
redis_common = { path = "../../../lib/redis_common" }
|
||||
subprocess_common = { path = "../../../lib/subprocess_common" }
|
||||
testing = { path = "../../../lib/testing" }
|
||||
tokens = { path = "../../../_types/tokens" }
|
||||
tts_common = { path = "../../../lib/tts_common" }
|
||||
|
||||
|
||||
+50
-4
@@ -20,11 +20,15 @@ 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::lipsync::sad_talker::download_audio_file::download_audio_file;
|
||||
use crate::job::job_types::lipsync::sad_talker::download_image_file::download_image_file;
|
||||
use crate::job::job_types::lipsync::sad_talker::resize_image::resize_image;
|
||||
use crate::job::job_types::lipsync::sad_talker::sad_talker_inference_command::InferenceArgs;
|
||||
use crate::job::job_types::lipsync::sad_talker::validate_job::validate_job;
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
use crate::util::common_commands::ffmpeg_logo_watermark_command;
|
||||
|
||||
/// The maximum that either width or height can be
|
||||
const MAX_DIMENSION : u32 = 1500;
|
||||
|
||||
pub struct SadTalkerProcessJobArgs<'a> {
|
||||
pub job_dependencies: &'a JobDependencies,
|
||||
pub job: &'a AvailableInferenceJob,
|
||||
@@ -102,11 +106,50 @@ pub async fn process_job(args: SadTalkerProcessJobArgs<'_>) -> Result<JobSuccess
|
||||
&deps.mysql_pool
|
||||
).await?;
|
||||
|
||||
info!("Image file: {:?}", image_path.filesystem_path);
|
||||
info!("Downloaded image file: {:?}", image_path.filesystem_path);
|
||||
|
||||
// ==================== TRANSCODE MEDIA (IF NECESSARY) ==================== //
|
||||
|
||||
// TODO
|
||||
let mut usable_image_path = image_path.filesystem_path.clone();
|
||||
|
||||
if let Some((width, height)) = job_args.width.zip(job_args.height) {
|
||||
info!("Requested image resize to {}x{} ...", width, height);
|
||||
|
||||
let mut width = width.min(MAX_DIMENSION);
|
||||
let mut height = height.min(MAX_DIMENSION);
|
||||
|
||||
if job_args.enhancer.is_some() {
|
||||
// The enhancer will double the size of the image, so we'll downsize the image by half
|
||||
// if it's enabled.
|
||||
width = width / 2;
|
||||
height = height / 2;
|
||||
info!("Enhancer will be doubling image size, so cutting frame size to {}x{}.", width, height)
|
||||
}
|
||||
|
||||
info!("Resizing image to {}x{} ...", width, height);
|
||||
|
||||
let result = resize_image(
|
||||
&image_path.filesystem_path,
|
||||
&work_temp_dir,
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
error!("Image resize failure: {:?}", err);
|
||||
safe_delete_temp_file(&audio_path.filesystem_path);
|
||||
safe_delete_temp_file(&image_path.filesystem_path);
|
||||
safe_delete_temp_directory(&work_temp_dir);
|
||||
return Err(ProcessSingleJobError::Other(anyhow!("image resize failure: {:?}", err)));
|
||||
}
|
||||
Ok(resized_image_path) => {
|
||||
usable_image_path = resized_image_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Used image file: {:?}", usable_image_path);
|
||||
|
||||
// ==================== SETUP FOR INFERENCE ==================== //
|
||||
|
||||
@@ -122,7 +165,7 @@ pub async fn process_job(args: SadTalkerProcessJobArgs<'_>) -> Result<JobSuccess
|
||||
|
||||
info!("Expected output video filename: {:?}", &output_video_fs_path);
|
||||
|
||||
// TODO: Limit output length for premium.
|
||||
// TODO: Limit output length for non-premium (???)
|
||||
|
||||
let maybe_args = job.maybe_inference_args
|
||||
.as_ref()
|
||||
@@ -143,7 +186,7 @@ pub async fn process_job(args: SadTalkerProcessJobArgs<'_>) -> Result<JobSuccess
|
||||
.inference_command
|
||||
.execute_inference(InferenceArgs {
|
||||
input_audio: &audio_path.filesystem_path,
|
||||
input_image: &image_path.filesystem_path,
|
||||
input_image: &usable_image_path,
|
||||
work_dir: &workdir,
|
||||
output_file: &output_video_fs_path,
|
||||
stderr_output_file: &stderr_output_file,
|
||||
@@ -170,6 +213,7 @@ pub async fn process_job(args: SadTalkerProcessJobArgs<'_>) -> Result<JobSuccess
|
||||
|
||||
safe_delete_temp_file(&audio_path.filesystem_path);
|
||||
safe_delete_temp_file(&image_path.filesystem_path);
|
||||
safe_delete_temp_file(&usable_image_path);
|
||||
safe_delete_temp_file(&output_video_fs_path);
|
||||
safe_delete_temp_file(&stderr_output_file);
|
||||
safe_delete_temp_directory(&work_temp_dir);
|
||||
@@ -207,6 +251,7 @@ pub async fn process_job(args: SadTalkerProcessJobArgs<'_>) -> Result<JobSuccess
|
||||
error!("Watermark failed: {:?}", command_exit_status);
|
||||
safe_delete_temp_file(&audio_path.filesystem_path);
|
||||
safe_delete_temp_file(&image_path.filesystem_path);
|
||||
safe_delete_temp_file(&usable_image_path);
|
||||
safe_delete_temp_file(&output_video_fs_path);
|
||||
safe_delete_temp_file(&output_video_fs_path_watermark);
|
||||
safe_delete_temp_directory(&work_temp_dir);
|
||||
@@ -263,6 +308,7 @@ pub async fn process_job(args: SadTalkerProcessJobArgs<'_>) -> Result<JobSuccess
|
||||
|
||||
safe_delete_temp_file(&output_video_fs_path);
|
||||
safe_delete_temp_file(&output_video_fs_path_watermark);
|
||||
safe_delete_temp_file(&usable_image_path);
|
||||
|
||||
// NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk...
|
||||
safe_delete_temp_directory(&work_temp_dir);
|
||||
|
||||
+23
-3
@@ -1,8 +1,28 @@
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::info;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use images::image::ImageFormat;
|
||||
use images::resize_image_file_preserving_aspect::resize_image_file_preserving_aspect;
|
||||
|
||||
pub fn resize_image<P: AsRef<Path>, Q: AsRef<Path>>(input_file: P, output_file: Q) -> AnyhowResult<()> {
|
||||
|
||||
pub fn resize_image<P: AsRef<Path>>(
|
||||
input_file: P,
|
||||
tempdir: &TempDir,
|
||||
new_width: u32,
|
||||
new_height: u32
|
||||
) -> AnyhowResult<PathBuf> {
|
||||
let image = resize_image_file_preserving_aspect(
|
||||
input_file,
|
||||
new_width,
|
||||
new_height,
|
||||
true
|
||||
)?;
|
||||
let output_file = tempdir.path().join("resized_image.png");
|
||||
|
||||
info!("File successfully resized to {}x{} !", image.width(), image.height());
|
||||
|
||||
image.save_with_format(&output_file, ImageFormat::Png)?;
|
||||
Ok(output_file)
|
||||
}
|
||||
+10
-2
@@ -15,6 +15,9 @@ pub struct JobArgs<'a> {
|
||||
pub preprocess: Option<String>,
|
||||
// NB: Enhancer controls quality
|
||||
pub enhancer: Option<String>,
|
||||
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn validate_job(job: &AvailableInferenceJob) -> Result<JobArgs, ProcessSingleJobError> {
|
||||
@@ -70,7 +73,7 @@ pub fn validate_job(job: &AvailableInferenceJob) -> Result<JobArgs, ProcessSingl
|
||||
let make_still = inference_args.maybe_make_still.unwrap_or(false);
|
||||
|
||||
let preprocess = match inference_args.maybe_preprocess {
|
||||
None => None,
|
||||
None => Some("full".to_string()),
|
||||
Some(Preprocess::F) => Some("full".to_string()),
|
||||
Some(Preprocess::EF) => Some("extfull".to_string()),
|
||||
Some(Preprocess::C) => Some("crop".to_string()),
|
||||
@@ -83,6 +86,9 @@ pub fn validate_job(job: &AvailableInferenceJob) -> Result<JobArgs, ProcessSingl
|
||||
Some(FaceEnhancer::R) => Some("RestoreFormer".to_string()),
|
||||
};
|
||||
|
||||
let mut width = inference_args.maybe_resize_width;
|
||||
let mut height = inference_args.maybe_resize_height;
|
||||
|
||||
Ok(JobArgs {
|
||||
audio_source,
|
||||
image_source,
|
||||
@@ -90,5 +96,7 @@ pub fn validate_job(job: &AvailableInferenceJob) -> Result<JobArgs, ProcessSingl
|
||||
make_still,
|
||||
preprocess,
|
||||
enhancer,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,8 @@ pub struct Plan {
|
||||
|
||||
lipsync_requires_frontend_keepalive: bool,
|
||||
|
||||
can_remove_visual_watermarks: bool,
|
||||
|
||||
// ========== Features for TTS ==========
|
||||
|
||||
tts_base_priority_level: u8,
|
||||
@@ -89,6 +91,7 @@ impl Plan {
|
||||
is_development_plan: builder.is_development_plan,
|
||||
is_synthetic_plan: builder.is_synthetic_plan,
|
||||
lipsync_requires_frontend_keepalive: builder.lipsync_requires_frontend_keepalive,
|
||||
can_remove_visual_watermarks: builder.can_remove_visual_watermarks,
|
||||
tts_base_priority_level: builder.tts_base_priority_level,
|
||||
tts_max_duration: builder.tts_max_duration,
|
||||
tts_max_character_length: builder.tts_max_character_length,
|
||||
@@ -143,6 +146,10 @@ impl Plan {
|
||||
self.lipsync_requires_frontend_keepalive
|
||||
}
|
||||
|
||||
pub fn can_remove_visual_watermarks(&self) -> bool {
|
||||
self.can_remove_visual_watermarks
|
||||
}
|
||||
|
||||
pub fn tts_base_priority_level(&self) -> u8 {
|
||||
self.tts_base_priority_level
|
||||
}
|
||||
@@ -183,6 +190,8 @@ pub struct PlanBuilder {
|
||||
|
||||
lipsync_requires_frontend_keepalive: bool,
|
||||
|
||||
can_remove_visual_watermarks: bool,
|
||||
|
||||
// ========== Features for TTS ==========
|
||||
|
||||
tts_base_priority_level: u8,
|
||||
@@ -228,6 +237,7 @@ impl PlanBuilder {
|
||||
|
||||
// Lipsync (SadTalker, not Wav2Lip)
|
||||
lipsync_requires_frontend_keepalive: true,
|
||||
can_remove_visual_watermarks: false,
|
||||
|
||||
// TTS
|
||||
tts_base_priority_level : TTS_DEFAULT_PRIORITY_LEVEL,
|
||||
@@ -294,6 +304,11 @@ impl PlanBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn can_remove_visual_watermarks(mut self, value: bool) -> Self {
|
||||
self.can_remove_visual_watermarks = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tts_base_priority_level(mut self, value: u8) -> Self {
|
||||
self.tts_base_priority_level = value;
|
||||
self
|
||||
|
||||
@@ -146,6 +146,7 @@ pub static PRODUCTION_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
.web_vc_base_priority_level(30)
|
||||
.web_vc_requires_frontend_keepalive(false)
|
||||
.lipsync_requires_frontend_keepalive(false)
|
||||
.can_remove_visual_watermarks(true)
|
||||
.build());
|
||||
|
||||
plans.insert(PlanBuilder::new("fakeyou_elite")
|
||||
@@ -159,6 +160,7 @@ pub static PRODUCTION_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
.web_vc_base_priority_level(40)
|
||||
.web_vc_requires_frontend_keepalive(false)
|
||||
.lipsync_requires_frontend_keepalive(false)
|
||||
.can_remove_visual_watermarks(true)
|
||||
.build());
|
||||
|
||||
// ========== Spanish plans ==========
|
||||
|
||||
+68
-74
@@ -16,17 +16,15 @@ use enums::common::visibility::Visibility;
|
||||
use http_server_common::request::get_request_header_optional::get_request_header_optional;
|
||||
use http_server_common::request::get_request_ip::get_request_ip;
|
||||
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, InferenceCategoryAbbreviated, PolymorphicInferenceArgs};
|
||||
use mysql_queries::payloads::generic_inference_args::lipsync_payload::{FaceEnhancer, LipsyncAnimationAudioSource, LipsyncAnimationImageSource, LipsyncArgs, Preprocess};
|
||||
use mysql_queries::payloads::generic_inference_args::lipsync_payload::{FaceEnhancer, LipsyncAnimationAudioSource, LipsyncAnimationImageSource, LipsyncArgs};
|
||||
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
|
||||
use tokens::files::media_upload::MediaUploadToken;
|
||||
use tokens::jobs::inference::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::voice_conversion_results::VoiceConversionResultToken;
|
||||
use tokens::users::user::UserToken;
|
||||
use tts_common::priority::FAKEYOU_INVESTOR_PRIORITY_LEVEL;
|
||||
|
||||
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
|
||||
use crate::http_server::endpoints::investor_demo::demo_cookie::request_has_demo_cookie;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::server_state::ServerState;
|
||||
|
||||
@@ -46,30 +44,60 @@ pub struct EnqueueLipsyncAnimationRequest {
|
||||
image_source: ImageSource,
|
||||
|
||||
creator_set_visibility: Option<Visibility>,
|
||||
is_storyteller_demo: Option<bool>,
|
||||
|
||||
/// SadTalker: cropping
|
||||
crop: Option<CropMode>,
|
||||
/// Width/height of the video to generate (presets)
|
||||
dimensions: Option<FrameSize>,
|
||||
|
||||
/// SadTalker: parameter to make the animation more still.
|
||||
make_still: Option<bool>,
|
||||
|
||||
/// SadTalker: parameter to enhance the face using gfpgan.
|
||||
high_quality: Option<bool>,
|
||||
/// SadTalker: we use gfpgan face enhancement by default; this disables it.
|
||||
disable_face_enhancement: Option<bool>,
|
||||
|
||||
/// Remove the watermark
|
||||
/// Remove the watermark (premium only)
|
||||
remove_watermark: Option<bool>,
|
||||
|
||||
///// SadTalker: cropping
|
||||
//crop: Option<CropMode>,
|
||||
}
|
||||
|
||||
//#[derive(Deserialize, Copy, Clone)]
|
||||
//#[serde(rename_all = "snake_case")]
|
||||
//pub enum CropMode {
|
||||
// /// SadTalker: "extcrop", which appears to be a wider crop than the "crop" option
|
||||
// Crop,
|
||||
// /// SadTalker: "crop" (the default)
|
||||
// CloseCrop,
|
||||
// /// SadTalker: "full" (can't tell that "extfull" does any differently)
|
||||
// Full,
|
||||
//}
|
||||
|
||||
#[derive(Deserialize, Copy, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CropMode {
|
||||
/// SadTalker: "extcrop", which appears to be a wider crop than the "crop" option
|
||||
Crop,
|
||||
/// SadTalker: "crop" (the default)
|
||||
CloseCrop,
|
||||
/// SadTalker: "full" (can't tell that "extfull" does any differently)
|
||||
Full,
|
||||
pub enum FrameSize {
|
||||
/// Twitter horizontal = 1280x720
|
||||
/// See: https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/uploading-media/media-best-practices
|
||||
TwitterLandscape,
|
||||
|
||||
/// Twitter vertical = 720x1280
|
||||
TwitterPortrait,
|
||||
|
||||
/// Twitter square = 720x720
|
||||
TwitterSquare,
|
||||
|
||||
// D-ID: 1000x1000
|
||||
// Runway: 896x512
|
||||
// Pika: 1024x576
|
||||
}
|
||||
|
||||
impl FrameSize {
|
||||
pub fn get_width_and_height(&self) -> (u32, u32) {
|
||||
match self {
|
||||
FrameSize::TwitterLandscape => (1280, 720),
|
||||
FrameSize::TwitterPortrait => (720, 1280),
|
||||
FrameSize::TwitterSquare => (720, 720),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Treated as an enum. Only one of these may be set.
|
||||
@@ -137,8 +165,6 @@ pub async fn enqueue_lipsync_animation_handler(
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, EnqueueLipsyncAnimationError>
|
||||
{
|
||||
let mut maybe_user_token : Option<UserToken> = None;
|
||||
let mut priority_level ;
|
||||
let disable_rate_limiter = false; // NB: Careful!
|
||||
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
@@ -169,33 +195,7 @@ pub async fn enqueue_lipsync_animation_handler(
|
||||
maybe_user_session.as_ref());
|
||||
|
||||
// TODO: Separate priority for animation.
|
||||
priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
// ==================== INVESTOR PRIORITY ==================== //
|
||||
|
||||
// TODO/TEMP: Give investors even more priority
|
||||
let mut is_investor = false;
|
||||
|
||||
{
|
||||
// TODO/TEMP: The storyteller.io website's AJAX calls will set this.
|
||||
// This is just for the YCombinator demo.
|
||||
match request.is_storyteller_demo {
|
||||
Some(true) => {
|
||||
is_investor = true;
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
|
||||
// TODO/TEMP: The storyteller.io website will redirect and establish this cookie.
|
||||
// This is just for the YCombinator demo.
|
||||
if request_has_demo_cookie(&http_request) {
|
||||
is_investor = true;
|
||||
}
|
||||
|
||||
if is_investor {
|
||||
priority_level = FAKEYOU_INVESTOR_PRIORITY_LEVEL;
|
||||
}
|
||||
}
|
||||
let priority_level = plan.web_vc_base_priority_level();
|
||||
|
||||
// ==================== DEBUG MODE + ROUTING TAG ==================== //
|
||||
|
||||
@@ -209,25 +209,18 @@ pub async fn enqueue_lipsync_animation_handler(
|
||||
|
||||
// ==================== RATE LIMIT ==================== //
|
||||
|
||||
if !disable_rate_limiter {
|
||||
let mut rate_limiter = match maybe_user_session {
|
||||
None => &server_state.redis_rate_limiters.logged_out,
|
||||
Some(ref user) => {
|
||||
if user.role.is_banned {
|
||||
return Err(EnqueueLipsyncAnimationError::NotAuthorized);
|
||||
}
|
||||
&server_state.redis_rate_limiters.logged_in
|
||||
},
|
||||
};
|
||||
let rate_limiter = match maybe_user_session {
|
||||
None => &server_state.redis_rate_limiters.logged_out,
|
||||
Some(ref user) => {
|
||||
if user.role.is_banned {
|
||||
return Err(EnqueueLipsyncAnimationError::NotAuthorized);
|
||||
}
|
||||
&server_state.redis_rate_limiters.logged_in
|
||||
},
|
||||
};
|
||||
|
||||
// TODO/TEMP
|
||||
if is_investor {
|
||||
rate_limiter = &server_state.redis_rate_limiters.logged_in;
|
||||
}
|
||||
|
||||
if let Err(_err) = rate_limiter.rate_limit_request(&http_request) {
|
||||
return Err(EnqueueLipsyncAnimationError::RateLimited);
|
||||
}
|
||||
if let Err(_err) = rate_limiter.rate_limit_request(&http_request) {
|
||||
return Err(EnqueueLipsyncAnimationError::RateLimited);
|
||||
}
|
||||
|
||||
// ==================== LOOK UP MODEL INFO ==================== //
|
||||
@@ -276,22 +269,16 @@ pub async fn enqueue_lipsync_animation_handler(
|
||||
maybe_preprocess: None,
|
||||
maybe_make_still: None,
|
||||
maybe_remove_watermark: None,
|
||||
maybe_resize_width: None,
|
||||
maybe_resize_height: None,
|
||||
};
|
||||
|
||||
if request.high_quality.unwrap_or(false) {
|
||||
let enable_face_enhancement = !request.disable_face_enhancement.unwrap_or(false);
|
||||
|
||||
if enable_face_enhancement {
|
||||
inference_args.maybe_face_enhancer = Some(FaceEnhancer::G); // "--enhancer gfpgan"
|
||||
}
|
||||
|
||||
if let Some(crop_mode) = request.crop {
|
||||
let crop_mode = match crop_mode {
|
||||
CropMode::Crop => Preprocess::EC, // NB: "extcrop" is less of a close crop than "crop"
|
||||
CropMode::CloseCrop => Preprocess::C,
|
||||
CropMode::Full => Preprocess::F,
|
||||
};
|
||||
|
||||
inference_args.maybe_preprocess = Some(crop_mode);
|
||||
}
|
||||
|
||||
if request.make_still.unwrap_or(false) {
|
||||
inference_args.maybe_make_still = Some(true);
|
||||
}
|
||||
@@ -300,6 +287,13 @@ pub async fn enqueue_lipsync_animation_handler(
|
||||
inference_args.maybe_remove_watermark = Some(true);
|
||||
}
|
||||
|
||||
let (width, height) = request.dimensions
|
||||
.unwrap_or(FrameSize::TwitterLandscape)
|
||||
.get_width_and_height();
|
||||
|
||||
inference_args.maybe_resize_width = Some(width);
|
||||
inference_args.maybe_resize_height = Some(height);
|
||||
|
||||
info!("Creating lipsync animation job record...");
|
||||
|
||||
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
|
||||
|
||||
Reference in new issue
Block a user