Compare commits

...
Author SHA1 Message Date
Michael Chung 8a5e4538a0 All good! going to merge now. 2023-10-25 11:32:22 -04:00
Michael Chung 3f70ca72dd Dev environment
Fixed various code problems, going to merge soon.
2023-10-24 13:09:36 -04:00
Michael Chung d8a4b6c735 Get everything to build again we have to merge soon. 2023-10-24 11:26:12 -04:00
Michael Chung 48ed6d118c Merge issues with VSCode? this is strange .... 2023-10-24 11:15:31 -04:00
Michael Chung 4c3ac3edda Merge branch 'master' into feature/enqueue_tts_request 2023-10-24 10:20:37 -04:00
Brandon Thomas dae5764658 codegen queries 2023-10-24 05:40:33 -04:00
Brandon Thomas 98547df063 propagate avt from jobs to media_files 2023-10-24 05:12:42 -04:00
Brandon Thomas 9c856235fc write avt cookie token to inference jobs 2023-10-24 05:04:00 -04:00
Brandon Thomas 205f6de1b2 fix lint failures 2023-10-24 04:20:38 -04:00
Brandon Thomas a9fa45575e additional comment 2023-10-24 04:07:02 -04:00
Brandon Thomas 1b77974a0e change user token prefix 2023-10-24 04:06:33 -04:00
Brandon Thomas 724ae2570e How did this ever work!? 2023-10-24 03:56:38 -04:00
Brandon Thomas dc7b5b7e60 Merge pull request #120 from storytold/echelon-terminate-job-endpoint
endpoint to terminate jobs
2023-10-24 02:41:03 -04:00
Brandon Thomas 3259433717 endpoint to terminate jobs 2023-10-24 02:40:00 -04:00
Brandon Thomas bc5d84a306 use the new job status enum 2023-10-24 01:22:48 -04:00
Brandon Thomas 2357f00944 Merge pull request #119 from storytold/echelon-job-system-new-states
Introduce two new job status types to generic inference jobs
2023-10-24 01:08:15 -04:00
Brandon Thomas 24127f20e4 Introduce two new job status types to generic inference jobs 2023-10-24 01:06:14 -04:00
Brandon Thomas 4c8f14d4d5 Migration to make password_resets.ip_address_redemption nullable 2023-10-23 22:11:26 -04:00
Brandon Thomas e14440ba02 undo edit to previous migration (should be harmless in prod) 2023-10-23 22:05:11 -04:00
Michael Chung 7295a3295f Fix prefixes for create and inference 2023-10-23 22:03:37 -04:00
Michael Chung 10176079c7 remove variables not used 2023-10-23 21:59:26 -04:00
Michael Chung 8999a80e92 Change root ml dir 2023-10-23 21:34:58 -04:00
Michael Chung 41b07049ff Remove comments 2023-10-23 21:29:41 -04:00
Michael Chung b45f6ed691 Restore storyteller rust root for dev_database_seed 2023-10-23 21:25:04 -04:00
Michael Chung 9d16eb05c1 Fix data seed env-secrets path. 2023-10-23 21:13:27 -04:00
Michael Chung b414540251 Fixing issue with the origin model type along side tests. 2023-10-23 21:11:00 -04:00
Michael Chung 2949456adc Restore bad merge on id_category.rs 2023-10-23 21:07:35 -04:00
Michael Chung 3a72292898 Some minor comment fix 2023-10-23 16:54:27 -04:00
Michael Chung 07c640ce84 Retest the endpoints with the correct credentials
One more integration test with multiple wav files.
2023-10-23 16:52:31 -04:00
Michael Chung 49a15fd1f1 Cleaning up some of the end points. 2023-10-23 16:22:37 -04:00
Michael Chung 3267f9949d Added ip address and creator token. 2023-10-23 16:02:07 -04:00
Michael Chung 47df6930d2 Basically works well!
Few things to checkout.
2023-10-22 23:38:31 -04:00
Michael Chung 913d282d60 Fixed extensions on create
Checking whether media uploads.
2023-10-22 20:52:57 -04:00
Michael Chung 743934ff50 Need to do a clean up then check media upload and table 2023-10-22 20:36:32 -04:00
Michael Chung bc209fe939 So the prefix wasn't populated in the seed I think! 2023-10-22 17:57:35 -04:00
Michael Chung 302975ddfd Seeded db testing again 2023-10-22 11:57:29 -04:00
Michael Chung d7664e8e1c Merge branch 'master' into feature/enqueue_tts_request 2023-10-21 02:41:43 -04:00
Michael Chung 9f7fc09e9f Builds testing. 2023-10-21 01:13:39 -04:00
Michael Chung 2686cb94e0 Testing create command now. 2023-10-21 00:28:11 -04:00
Michael Chung fd7f1e1893 testing now. 2023-10-20 23:27:31 -04:00
Brandon Thomas eb3499596e always output stderr 2023-10-20 14:45:03 -04:00
Michael Chung 00bfdad3d0 Test pass! 2023-10-20 12:13:57 -04:00
Michael Chung ff3821956a Builds again 2023-10-20 11:43:16 -04:00
Michael Chung a5a5f5473b Testing! 2023-10-20 10:13:23 -04:00
Brandon Thomas 7c553ffcee fix rvc jobs 2023-10-20 07:00:09 -04:00
Brandon Thomas 52f0b44c26 codegen db 2023-10-20 06:14:49 -04:00
Brandon Thomas fbed7cd530 Merge pull request #113 from storytold/echelon-voice-conversion-media-files
voice conversion --> media files
2023-10-20 06:14:19 -04:00
Brandon Thomas 4ce8752a6a codegen db 2023-10-20 06:12:41 -04:00
Brandon Thomas 3239e318b1 fix inference job status links 2023-10-20 05:37:51 -04:00
Brandon Thomas 471dcff697 RVC -> media_files 2023-10-20 05:14:00 -04:00
Brandon Thomas 2af57fcf3e Write voice conversion results to media_files tables 2023-10-20 05:14:00 -04:00
Brandon Thomas f2b0e77210 fix stupid "1" bug on generic jobs 2023-10-20 05:13:44 -04:00
Michael Chung 10c8bc1ef3 Just need to run inference and pass the insert the record and upload the voice once it infers.
Run the path test.
2023-10-20 01:46:13 -04:00
Brandon Thomas 35dfe6a7f2 fix lints 2023-10-20 01:09:56 -04:00
Brandon Thomas 48ecc44978 Merge pull request #112 from storytold/echelon-seed-samples
Update seed tool to seed and upload samples
2023-10-19 22:36:30 -04:00
Brandon Thomas 4f0e2d5f72 Update seed tool to seed and upload samples 2023-10-19 22:33:24 -04:00
Brandon Thomas 96c99fd102 Merge pull request #111 from storytold/echelon-media-file-upload-cli
CLI tool to upload media files and save them to the DB
2023-10-19 21:56:38 -04:00
Brandon Thomas 26794b3d2e CLI tool to upload media files and save them to the DB 2023-10-19 21:52:32 -04:00
Michael Chung c3a60ed8ab Merge branch 'master' into feature/enqueue_tts_request 2023-10-19 20:12:40 -04:00
Michael Chung 13964901f0 update process job created a fork for creating voice 2023-10-19 19:19:53 -04:00
Brandon Thomas 593fb50fcb Merge pull request #110 from storytold/echelon-list-dataset-samples
Enrich list dataset samples query + endpoint with bucket information
2023-10-19 18:24:39 -04:00
Michael Chung 57eecb3757 Creating the create voice job 2023-10-19 16:26:43 -04:00
Michael Chung 25cdbf2f1d Adding create voice command 2023-10-19 02:09:16 -04:00
Michael Chung 0f4cab9ca3 Added missing new file 2023-10-18 19:50:24 -04:00
Michael Chung 6de4d451ac Fixed import issues 2023-10-18 19:50:07 -04:00
Michael Chung 9d4e426ace It is now being tested then we work on the command side. 2023-10-18 18:02:30 -04:00
Michael Chung bfb5103f46 Merge branch 'master' into feature/enqueue_tts_request 2023-10-18 13:37:23 -04:00
Michael Chung 3308c05836 Need to pull master 2023-10-18 13:26:19 -04:00
Michael Chung be19eefa1a Upload and create record and were good to do the other command and clean up. 2023-10-18 02:13:59 -04:00
Michael Chung 34e2b1e544 It works runs inference_job
Need to upload the file then create the record for download
2023-10-18 02:03:41 -04:00
Michael Chung cbe8aaa1ba More fixes 2023-10-18 00:04:33 -04:00
Michael Chung 11185584a7 Fixing typos and virtual env 2023-10-17 22:56:30 -04:00
Michael Chung 76559cf783 fix the env 2023-10-17 22:11:22 -04:00
Michael Chung 2e7bd622f3 fixed a typo 2023-10-17 22:06:42 -04:00
Michael Chung 68048659d4 Testing inference_job 2023-10-17 22:00:14 -04:00
Michael Chung 17d51f8d87 Merge branch 'master' into feature/enqueue_tts_request 2023-10-17 13:37:35 -04:00
Michael Chung 5f7a10a54c Merge branch 'master' into feature/enqueue_tts_request 2023-10-16 09:34:46 -04:00
Michael Chung 7eee225296 Builds again need to clearify what happened to the download function 2023-10-13 15:36:21 -04:00
Michael Chung 1319471c04 Merge branch 'master' into feature/enqueue_tts_request 2023-10-13 10:02:17 -04:00
Brandon Thomas 3e8b6f39fe Merge branch 'master' into feature/enqueue_tts_request 2023-10-12 23:28:08 -04:00
Michael Chung a0b8849806 Trying to build again. 2023-10-12 00:56:50 -04:00
Michael Chung bb0a4958ef Need to build and fix the container. 2023-10-11 18:19:40 -04:00
Michael Chung a256681f6d Merge branch 'master' into feature/enqueue_tts_request 2023-10-11 12:58:57 -04:00
Michael Chung ec03420efc Fixed issue with typo in job.
Need to fix process job and run it.
2023-10-11 02:51:47 -04:00
Michael Chung 264058ea87 Just a quick commit gotta modify the seeding script to get the bucket hashes for downloading 2023-10-10 20:20:56 -04:00
Michael Chung 3845c632b2 Turns out we don't support folder downloads only files. 2023-10-10 17:40:33 -04:00
Michael Chung 22518d9164 Fixed pathing typos 2023-10-10 16:24:16 -04:00
Michael Chung 5dbed7afbb Fixed typo on downloading the vall-e-x model 2023-10-10 16:18:35 -04:00
Michael Chung a485666b1c Test pass!
Payload delivered and picked up.
Need to ensure the job now runs locally.
2023-10-10 16:03:27 -04:00
Michael Chung 54e9b8af70 Fixed routing issue with the job need to move tts text to maybe tts text out of the json. 2023-10-10 15:32:51 -04:00
95 changed files with 4792 additions and 1954 deletions
+5
View File
@@ -75,3 +75,8 @@ TTS_HIFIGAN_SUPERRES_VOCODER_MODEL_FILENAME=hifigan_superres_twilight_33000.pt
# Development key. Permissible to check in.
NEWRELIC_API_KEY=c402be1c4c4216ec83d0cfc0851520cfFFFFNRAL
STORYTELLER_ROOT = "/home/tensor/code/storyteller" # your path here!
STORYTELLER_FRONTEND = "storyteller-frontend"
STORYTELLER_ML = "storyteller-ml"
STORYTELLER_RUST = "storyteller-rust"
Generated
+27
View File
@@ -1829,12 +1829,39 @@ dependencies = [
name = "dev-database-seed"
version = "0.0.1"
dependencies = [
"buckets",
"cloud_storage",
"config",
"easyenv",
"enums",
"errors",
"filesys",
"hashing",
"idempotency",
"log",
"mimetypes",
"mysql_queries",
"sqlx",
"storyteller_root",
"tokens",
"tokio 1.25.0",
]
[[package]]
name = "dev-upload-media-file"
version = "0.0.1"
dependencies = [
"buckets",
"clap",
"cloud_storage",
"config",
"easyenv",
"enums",
"errors",
"filesys",
"hashing",
"log",
"mimetypes",
"mysql_queries",
"sqlx",
"tokens",
+1
View File
@@ -12,6 +12,7 @@ members = [
"crates/cli/database_backup",
"crates/cli/database_cleanup",
"crates/cli/dev_database_seed",
"crates/cli/dev_upload_media_file",
"crates/cli/kubectl_pod_cleanup",
"crates/cli/scrape_test",
@@ -1,6 +1,14 @@
FakeYou Development Setup (bare metal)
======================================
Set your Environment .env File with the following paths, copy this in
The root path and the folders associated with the path.
This will change the base paths for development.
STORYTELLER_ROOT = "/home/tensor/code/storyteller" # change this
STORYTELLER_FRONTEND = "storyteller-frontend"
STORYTELLER_ML = "storyteller-ml"
STORYTELLER_RUST = "storyteller-rust"
This document describes how to run the FakeYou infrastructure on your development
machine's bare metal (no containers).
@@ -58,9 +58,6 @@ CREATE TABLE password_resets (
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (user_token, secret_key),
KEY fk_user_token (user_token),
KEY index_secret_key (secret_key),
KEY index_expires_at (expires_at)
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE password_resets
MODIFY ip_address_redemption VARCHAR(40) NOT NULL;
@@ -0,0 +1,6 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE password_resets
MODIFY ip_address_redemption VARCHAR(40) DEFAULT NULL;
@@ -0,0 +1,13 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
MODIFY status ENUM(
'pending',
'started',
'complete_success',
'complete_failure',
'attempt_failed',
'dead'
) NOT NULL DEFAULT 'pending';
@@ -0,0 +1,16 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Add two states to allow us to distinguish user-cancelled jobs and system-cancelled jobs.
ALTER TABLE generic_inference_jobs
MODIFY status ENUM(
'pending',
'started',
'complete_success',
'complete_failure',
'attempt_failed',
'dead',
'cancelled_by_user',
'cancelled_by_system'
) NOT NULL DEFAULT 'pending';
@@ -145,9 +145,11 @@ CREATE TABLE generic_inference_jobs (
-- * Pending = job is ready to go
-- * Started = job is running
-- * Complete_Success = job is done (success)
-- * Complete_Failure = job is done (failure)
-- * Complete_Failure = job is done (permanent failure, eg. a Face Animator job without a face)
-- * Attempt_Failed = job failed but may retry.
-- * Dead = job failed permanently.
-- * Dead = job failed permanently (ie. exhausted retries)
-- * Cancelled_By_User = the user canceled their own job
-- * Cancelled_By_System = the system terminated the job (ie. load shedding, user account deletion, etc.)
--
-- Pending -> Started -> Complete_Success
-- |-> Complete_Failure
@@ -158,7 +160,10 @@ CREATE TABLE generic_inference_jobs (
'complete_success',
'complete_failure',
'attempt_failed',
'dead') NOT NULL DEFAULT 'pending',
'dead',
'cancelled_by_user',
'cancelled_by_system'
) NOT NULL DEFAULT 'pending',
-- We can track this against a "max_attempt_count"
attempt_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
@@ -0,0 +1,68 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
-- Per-user password resets.
-- Password resets are valid until any of the following are true:
--
-- 1) database NOW() > password_rests.expires_at
-- 2) password_rests.is_redeemed == TRUE
-- 3) users.password_version > password_resets.current_password_version
--
-- When creating a new password reset, send the user an email with the `secret_key` in the URL.
-- The user will have to supply their username and/or email address, plus the secret key to
-- redeem the password reset.
--
-- Also when creating the password hash, be sure to set `expires_at` to be "NOW() + 3 hours", or
-- some other small, reasonable time frame.
--
CREATE TABLE password_resets (
-- Not used for anything except replication.
id BIGINT(20) NOT NULL AUTO_INCREMENT,
-- Password hash "primary key"
-- (ie. for the future when we have a support dashboard)
token VARCHAR(32) NOT NULL,
-- Foreign key to user
user_token VARCHAR(32) NOT NULL,
-- Secret "key" for the password reset
-- This will be shared via URL or user text input.
secret_key VARCHAR(32) NOT NULL,
-- Copied from the user record at the time of password reset issuance.
-- If the user's password version is greater than this value, then this
-- reset is no longer valid.
current_password_version INT NOT NULL DEFAULT 0,
-- Whether the password reset has been consumed.
is_redeemed BOOLEAN NOT NULL DEFAULT false,
-- For abuse tracking.
-- Wide enough for IPv4/6
ip_address_creation VARCHAR(40) NOT NULL,
ip_address_redemption VARCHAR(40) DEFAULT NULL,
-- Incremented with every update.
version INT NOT NULL DEFAULT 0,
-- ========== TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- When the password reset expires
expires_at TIMESTAMP NOT NULL,
-- INDICES --
PRIMARY KEY (id),
UNIQUE KEY (token),
UNIQUE KEY (user_token, secret_key),
KEY fk_user_token (user_token),
KEY index_secret_key (secret_key),
KEY index_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,15 @@ pub enum IdCategory {
#[serde(rename = "lipsync_animation")]
LipsyncAnimation,
/// Results from voice conversion (which may live in the media_files table)
/// Applies for RVC and SVC
#[serde(rename = "voice_conversion")]
VoiceConversion,
/// Results from the zero shot tts
#[serde(rename = "zs_audio_tts")]
ZeroShotTTS,
/// Zs dataset which lives in the zs_voice_datasets table
#[serde(rename = "zs_dataset")]
ZsDataset,
@@ -40,8 +49,10 @@ impl IdCategory {
match self {
Self::MediaFile => "media_file",
Self::LipsyncAnimation => "lipsync_animation",
Self::VoiceConversion => "voice_conversion",
Self::ZsDataset => "zs_dataset",
Self::ZsVoice => "zs_voice",
Self::ZeroShotTTS => "zs_audio_tts"
}
}
@@ -49,8 +60,10 @@ impl IdCategory {
match value {
"media_file" => Ok(Self::MediaFile),
"lipsync_animation" => Ok(Self::LipsyncAnimation),
"voice_conversion" => Ok(Self::VoiceConversion),
"zs_dataset" => Ok(Self::ZsDataset),
"zs_voice" => Ok(Self::ZsVoice),
"zs_audio_tts" => Ok(Self::ZeroShotTTS),
_ => Err(format!("invalid value: {:?}", value)),
}
}
@@ -61,8 +74,10 @@ impl IdCategory {
BTreeSet::from([
Self::MediaFile,
Self::LipsyncAnimation,
Self::VoiceConversion,
Self::ZsDataset,
Self::ZsVoice,
Self::ZeroShotTTS
])
}
}
@@ -72,41 +87,73 @@ mod tests {
use crate::by_table::generic_synthetic_ids::id_category::IdCategory;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(IdCategory::MediaFile, "media_file");
assert_serialization(IdCategory::LipsyncAnimation, "lipsync_animation");
assert_serialization(IdCategory::VoiceConversion, "voice_conversion");
assert_serialization(IdCategory::ZsDataset, "zs_dataset");
assert_serialization(IdCategory::ZsVoice, "zs_voice");
}
#[test]
fn to_str() {
assert_eq!(IdCategory::MediaFile.to_str(), "media_file");
assert_eq!(IdCategory::LipsyncAnimation.to_str(), "lipsync_animation");
assert_eq!(IdCategory::ZsDataset.to_str(), "zs_dataset");
assert_eq!(IdCategory::ZsVoice.to_str(), "zs_voice");
#[test]
fn to_str() {
assert_eq!(IdCategory::MediaFile.to_str(), "media_file");
assert_eq!(IdCategory::LipsyncAnimation.to_str(), "lipsync_animation");
assert_eq!(IdCategory::VoiceConversion.to_str(), "voice_conversion");
assert_eq!(IdCategory::ZsDataset.to_str(), "zs_dataset");
assert_eq!(IdCategory::ZsVoice.to_str(), "zs_voice");
assert_eq!(IdCategory::ZeroShotTTS.to_str(),"zs_audio_tts");
}
#[test]
fn from_str() {
assert_eq!(IdCategory::from_str("media_file").unwrap(), IdCategory::MediaFile);
assert_eq!(IdCategory::from_str("lipsync_animation").unwrap(), IdCategory::LipsyncAnimation);
assert_eq!(IdCategory::from_str("voice_conversion").unwrap(), IdCategory::VoiceConversion);
assert_eq!(IdCategory::from_str("zs_dataset").unwrap(), IdCategory::ZsDataset);
assert_eq!(IdCategory::from_str("zs_voice").unwrap(), IdCategory::ZsVoice);
assert_eq!(IdCategory::from_str("zs_audio_tts").unwrap(), IdCategory::ZeroShotTTS);
}
#[test]
fn all_variants() {
// Static check
let mut variants = IdCategory::all_variants();
assert_eq!(variants.len(), 5);
assert_eq!(variants.pop_first(), Some(IdCategory::MediaFile));
assert_eq!(variants.pop_first(), Some(IdCategory::LipsyncAnimation));
assert_eq!(variants.pop_first(), Some(IdCategory::VoiceConversion));
assert_eq!(variants.pop_first(), Some(IdCategory::ZsDataset));
assert_eq!(variants.pop_first(), Some(IdCategory::ZsVoice));
assert_eq!(variants.pop_first(), Some(IdCategory::ZeroShotTTS));
assert_eq!(variants.pop_first(), None);
// Generated check
use strum::IntoEnumIterator;
assert_eq!(IdCategory::all_variants().len(), IdCategory::iter().len());
}
}
#[test]
fn from_str() {
assert_eq!(IdCategory::from_str("media_file").unwrap(), IdCategory::MediaFile);
assert_eq!(IdCategory::from_str("lipsync_animation").unwrap(), IdCategory::LipsyncAnimation);
assert_eq!(IdCategory::from_str("zs_dataset").unwrap(), IdCategory::ZsDataset);
assert_eq!(IdCategory::from_str("zs_voice").unwrap(), IdCategory::ZsVoice);
}
mod mechanical_checks {
use super::*;
#[test]
fn all_variants() {
// Static check
let mut variants = IdCategory::all_variants();
assert_eq!(variants.len(), 4);
assert_eq!(variants.pop_first(), Some(IdCategory::MediaFile));
assert_eq!(variants.pop_first(), Some(IdCategory::LipsyncAnimation));
assert_eq!(variants.pop_first(), Some(IdCategory::ZsDataset));
assert_eq!(variants.pop_first(), Some(IdCategory::ZsVoice));
assert_eq!(variants.pop_first(), None);
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(IdCategory::all_variants().len(), IdCategory::iter().len());
}
// Generated check
use strum::IntoEnumIterator;
assert_eq!(IdCategory::all_variants().len(), IdCategory::iter().len());
#[test]
fn round_trip() {
for variant in IdCategory::all_variants() {
assert_eq!(variant, IdCategory::from_str(variant.to_str()).unwrap());
assert_eq!(variant, IdCategory::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, IdCategory::from_str(&format!("{:?}", variant)).unwrap());
}
}
}
}
@@ -1,3 +1,5 @@
use std::collections::BTreeSet;
use serde::Deserialize;
use serde::Serialize;
#[cfg(test)]
@@ -9,11 +11,21 @@ use strum::EnumIter;
///
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum MediaFileOriginModelType {
/// RVC (v2) voice conversion models
#[serde(rename = "rvc_v2")]
RvcV2,
/// SadTalker -- v1, we may add another enum value for future versions
SadTalker,
/// so-vits-svc voice conversion models
#[serde(rename = "so_vits_svc")]
SoVitsSvc,
#[serde(rename = "vall_e_x")]
VallEX,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
@@ -24,16 +36,33 @@ impl_mysql_enum_coders!(MediaFileOriginModelType);
impl MediaFileOriginModelType {
pub fn to_str(&self) -> &'static str {
match self {
Self::RvcV2 => "rvc_v2",
Self::SadTalker => "sad_talker",
Self::SoVitsSvc => "so_vits_svc",
Self::VallEX => "vall_e_x"
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"rvc_v2" => Ok(Self::RvcV2),
"sad_talker" => Ok(Self::SadTalker),
"so_vits_svc" => Ok(Self::SoVitsSvc),
"vall_e_x" => Ok(Self::VallEX),
_ => Err(format!("invalid value: {:?}", value)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::RvcV2,
Self::SadTalker,
Self::SoVitsSvc,
Self::VallEX
])
}
}
#[cfg(test)]
@@ -41,19 +70,60 @@ mod tests {
use crate::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(MediaFileOriginModelType::SadTalker, "sad_talker");
}
#[test]
fn test_to_str() {
assert_eq!(MediaFileOriginModelType::SadTalker.to_str(), "sad_talker");
#[test]
fn test_to_str() {
assert_eq!(MediaFileOriginModelType::RvcV2.to_str(), "rvc_v2");
assert_eq!(MediaFileOriginModelType::SadTalker.to_str(), "sad_talker");
assert_eq!(MediaFileOriginModelType::SoVitsSvc.to_str(), "so_vits_svc");
assert_eq!(MediaFileOriginModelType::VallEX.to_str(), "vall_e_x");
}
#[test]
fn test_from_str() {
assert_eq!(MediaFileOriginModelType::from_str("sad_talker").unwrap(), MediaFileOriginModelType::SadTalker);
#[test]
fn test_from_str() {
assert_eq!(MediaFileOriginModelType::from_str("rvc_v2").unwrap(), MediaFileOriginModelType::RvcV2);
assert_eq!(MediaFileOriginModelType::from_str("sad_talker").unwrap(), MediaFileOriginModelType::SadTalker);
assert_eq!(MediaFileOriginModelType::from_str("so_vits_svc").unwrap(), MediaFileOriginModelType::SoVitsSvc);
assert_eq!(MediaFileOriginModelType::from_str("vall_e_x").unwrap(), MediaFileOriginModelType::VallEX);
assert!(MediaFileOriginModelType::from_str("foo").is_err());
}
#[test]
fn all_variants() {
let mut variants = MediaFileOriginModelType::all_variants();
assert_eq!(variants.len(), 4);
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::RvcV2));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SadTalker));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::SoVitsSvc));
assert_eq!(variants.pop_first(), Some(MediaFileOriginModelType::VallEX));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(MediaFileOriginModelType::all_variants().len(), MediaFileOriginModelType::iter().len());
}
#[test]
fn round_trip() {
for variant in MediaFileOriginModelType::all_variants() {
assert_eq!(variant, MediaFileOriginModelType::from_str(variant.to_str()).unwrap());
assert_eq!(variant, MediaFileOriginModelType::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, MediaFileOriginModelType::from_str(&format!("{:?}", variant)).unwrap());
}
}
}
}
@@ -27,6 +27,10 @@ pub enum MediaFileOriginProductCategory {
#[serde(rename = "unknown")]
Unknown,
/// Voice conversion (either RVC or SVC)
#[serde(rename = "voice_conversion")]
VoiceConversion,
/// Media files created by (or uploaded for) the Zero Shot voice product.
#[serde(rename = "zs_voice")]
ZeroShotVoice,
@@ -42,6 +46,7 @@ impl MediaFileOriginProductCategory {
match self {
Self::FaceAnimator => "face_animator",
Self::Unknown => "unknown",
Self::VoiceConversion => "voice_conversion",
Self::ZeroShotVoice => "zs_voice",
}
}
@@ -50,6 +55,7 @@ impl MediaFileOriginProductCategory {
match value {
"face_animator" => Ok(Self::FaceAnimator),
"unknown" => Ok(Self::Unknown),
"voice_conversion" => Ok(Self::VoiceConversion),
"zs_voice" => Ok(Self::ZeroShotVoice),
_ => Err(format!("invalid value: {:?}", value)),
}
@@ -61,6 +67,7 @@ impl MediaFileOriginProductCategory {
BTreeSet::from([
Self::FaceAnimator,
Self::Unknown,
Self::VoiceConversion,
Self::ZeroShotVoice,
])
}
@@ -78,6 +85,7 @@ mod tests {
fn test_serialization() {
assert_serialization(MediaFileOriginProductCategory::FaceAnimator, "face_animator");
assert_serialization(MediaFileOriginProductCategory::Unknown, "unknown");
assert_serialization(MediaFileOriginProductCategory::VoiceConversion, "voice_conversion");
assert_serialization(MediaFileOriginProductCategory::ZeroShotVoice, "zs_voice");
}
@@ -85,6 +93,7 @@ mod tests {
fn to_str() {
assert_eq!(MediaFileOriginProductCategory::FaceAnimator.to_str(), "face_animator");
assert_eq!(MediaFileOriginProductCategory::Unknown.to_str(), "unknown");
assert_eq!(MediaFileOriginProductCategory::VoiceConversion.to_str(), "voice_conversion");
assert_eq!(MediaFileOriginProductCategory::ZeroShotVoice.to_str(), "zs_voice");
}
@@ -92,15 +101,17 @@ mod tests {
fn from_str() {
assert_eq!(MediaFileOriginProductCategory::from_str("face_animator").unwrap(), MediaFileOriginProductCategory::FaceAnimator);
assert_eq!(MediaFileOriginProductCategory::from_str("unknown").unwrap(), MediaFileOriginProductCategory::Unknown);
assert_eq!(MediaFileOriginProductCategory::from_str("voice_conversion").unwrap(), MediaFileOriginProductCategory::VoiceConversion);
assert_eq!(MediaFileOriginProductCategory::from_str("zs_voice").unwrap(), MediaFileOriginProductCategory::ZeroShotVoice);
}
#[test]
fn all_variants() {
let mut variants = MediaFileOriginProductCategory::all_variants();
assert_eq!(variants.len(), 3);
assert_eq!(variants.len(), 4);
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceAnimator));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::Unknown));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VoiceConversion));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::ZeroShotVoice));
assert_eq!(variants.pop_first(), None);
}
@@ -0,0 +1,147 @@
use std::collections::BTreeSet;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
/// This is used in nearly every job system as an actual MySQL ENUM value:
///
/// - tts_download_job
/// - tts_inference_job
/// - w2l_download_job
/// - w2l_inference_job
/// - generic_download_job
/// - (NOT generic_inference_job, which uses JobStatusPlus)
///
/// See the documentation on the table for usage.
///
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, sqlx::Type)]
#[serde(rename_all = "snake_case")]
#[sqlx(rename_all = "snake_case")]
pub enum JobStatus {
Pending,
Started,
CompleteSuccess,
CompleteFailure,
AttemptFailed,
Dead,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
impl_enum_display_and_debug_using_to_str!(JobStatus);
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
impl JobStatus {
pub fn to_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Started => "started",
Self::CompleteSuccess => "complete_success",
Self::CompleteFailure => "complete_failure",
Self::AttemptFailed => "attempt_failed",
Self::Dead => "dead",
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
"pending" => Ok(Self::Pending),
"started" => Ok(Self::Started),
"complete_success" => Ok(Self::CompleteSuccess),
"complete_failure" => Ok(Self::CompleteFailure),
"attempt_failed" => Ok(Self::AttemptFailed),
"dead" => Ok(Self::Dead),
_ => Err(format!("invalid job_status: {:?}", job_status)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::Pending,
Self::Started,
Self::CompleteSuccess,
Self::CompleteFailure,
Self::AttemptFailed,
Self::Dead,
])
}
}
#[cfg(test)]
mod tests {
use crate::common::job_status::JobStatus;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(JobStatus::Pending, "pending");
assert_serialization(JobStatus::Started, "started");
assert_serialization(JobStatus::CompleteSuccess, "complete_success");
assert_serialization(JobStatus::CompleteFailure, "complete_failure");
assert_serialization(JobStatus::AttemptFailed, "attempt_failed");
assert_serialization(JobStatus::Dead, "dead");
}
#[test]
fn to_str() {
assert_eq!(JobStatus::Pending.to_str(), "pending");
assert_eq!(JobStatus::Started.to_str(), "started");
assert_eq!(JobStatus::CompleteSuccess.to_str(), "complete_success");
assert_eq!(JobStatus::CompleteFailure.to_str(), "complete_failure");
assert_eq!(JobStatus::AttemptFailed.to_str(), "attempt_failed");
assert_eq!(JobStatus::Dead.to_str(), "dead");
}
#[test]
fn from_str() {
assert_eq!(JobStatus::from_str("pending").unwrap(), JobStatus::Pending);
assert_eq!(JobStatus::from_str("started").unwrap(), JobStatus::Started);
assert_eq!(JobStatus::from_str("complete_success").unwrap(), JobStatus::CompleteSuccess);
assert_eq!(JobStatus::from_str("complete_failure").unwrap(), JobStatus::CompleteFailure);
assert_eq!(JobStatus::from_str("attempt_failed").unwrap(), JobStatus::AttemptFailed);
assert_eq!(JobStatus::from_str("dead").unwrap(), JobStatus::Dead);
}
#[test]
fn all_variants() {
let mut variants = JobStatus::all_variants();
assert_eq!(variants.len(), 6);
assert_eq!(variants.pop_first(), Some(JobStatus::Pending));
assert_eq!(variants.pop_first(), Some(JobStatus::Started));
assert_eq!(variants.pop_first(), Some(JobStatus::CompleteSuccess));
assert_eq!(variants.pop_first(), Some(JobStatus::CompleteFailure));
assert_eq!(variants.pop_first(), Some(JobStatus::AttemptFailed));
assert_eq!(variants.pop_first(), Some(JobStatus::Dead));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(JobStatus::all_variants().len(), JobStatus::iter().len());
}
#[test]
fn round_trip() {
for variant in JobStatus::all_variants() {
assert_eq!(variant, JobStatus::from_str(variant.to_str()).unwrap());
assert_eq!(variant, JobStatus::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, JobStatus::from_str(&format!("{:?}", variant)).unwrap());
}
}
}
}
@@ -0,0 +1,160 @@
use std::collections::BTreeSet;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
/// This is used in newer jobs (that add additional enum states)
///
/// - generic_inference_job
/// - (no other jobs yet)
///
/// See the documentation on the table for usage.
///
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, sqlx::Type)]
#[serde(rename_all = "snake_case")]
#[sqlx(rename_all = "snake_case")]
pub enum JobStatusPlus {
Pending,
Started,
CompleteSuccess,
CompleteFailure,
AttemptFailed,
Dead,
CancelledByUser,
CancelledBySystem,
}
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
impl_enum_display_and_debug_using_to_str!(JobStatusPlus);
// NB: We can derive `sqlx::Type` instead of using `impl_mysql_enum_coders`
impl JobStatusPlus {
pub fn to_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Started => "started",
Self::CompleteSuccess => "complete_success",
Self::CompleteFailure => "complete_failure",
Self::AttemptFailed => "attempt_failed",
Self::Dead => "dead",
Self::CancelledByUser => "cancelled_by_user",
Self::CancelledBySystem => "cancelled_by_system",
}
}
pub fn from_str(job_status: &str) -> Result<Self, String> {
match job_status {
"pending" => Ok(Self::Pending),
"started" => Ok(Self::Started),
"complete_success" => Ok(Self::CompleteSuccess),
"complete_failure" => Ok(Self::CompleteFailure),
"attempt_failed" => Ok(Self::AttemptFailed),
"dead" => Ok(Self::Dead),
"cancelled_by_user" => Ok(Self::CancelledByUser),
"cancelled_by_system" => Ok(Self::CancelledBySystem),
_ => Err(format!("invalid job_status: {:?}", job_status)),
}
}
pub fn all_variants() -> BTreeSet<Self> {
// NB: BTreeSet is sorted
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
BTreeSet::from([
Self::Pending,
Self::Started,
Self::CompleteSuccess,
Self::CompleteFailure,
Self::AttemptFailed,
Self::Dead,
Self::CancelledByUser,
Self::CancelledBySystem,
])
}
}
#[cfg(test)]
mod tests {
use crate::common::job_status_plus::JobStatusPlus;
use crate::test_helpers::assert_serialization;
mod explicit_checks {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(JobStatusPlus::Pending, "pending");
assert_serialization(JobStatusPlus::Started, "started");
assert_serialization(JobStatusPlus::CompleteSuccess, "complete_success");
assert_serialization(JobStatusPlus::CompleteFailure, "complete_failure");
assert_serialization(JobStatusPlus::AttemptFailed, "attempt_failed");
assert_serialization(JobStatusPlus::Dead, "dead");
assert_serialization(JobStatusPlus::CancelledByUser, "cancelled_by_user");
assert_serialization(JobStatusPlus::CancelledBySystem, "cancelled_by_system");
}
#[test]
fn to_str() {
assert_eq!(JobStatusPlus::Pending.to_str(), "pending");
assert_eq!(JobStatusPlus::Started.to_str(), "started");
assert_eq!(JobStatusPlus::CompleteSuccess.to_str(), "complete_success");
assert_eq!(JobStatusPlus::CompleteFailure.to_str(), "complete_failure");
assert_eq!(JobStatusPlus::AttemptFailed.to_str(), "attempt_failed");
assert_eq!(JobStatusPlus::Dead.to_str(), "dead");
assert_eq!(JobStatusPlus::CancelledByUser.to_str(), "cancelled_by_user");
assert_eq!(JobStatusPlus::CancelledBySystem.to_str(), "cancelled_by_system");
}
#[test]
fn from_str() {
assert_eq!(JobStatusPlus::from_str("pending").unwrap(), JobStatusPlus::Pending);
assert_eq!(JobStatusPlus::from_str("started").unwrap(), JobStatusPlus::Started);
assert_eq!(JobStatusPlus::from_str("complete_success").unwrap(), JobStatusPlus::CompleteSuccess);
assert_eq!(JobStatusPlus::from_str("complete_failure").unwrap(), JobStatusPlus::CompleteFailure);
assert_eq!(JobStatusPlus::from_str("attempt_failed").unwrap(), JobStatusPlus::AttemptFailed);
assert_eq!(JobStatusPlus::from_str("dead").unwrap(), JobStatusPlus::Dead);
assert_eq!(JobStatusPlus::from_str("cancelled_by_user").unwrap(), JobStatusPlus::CancelledByUser);
assert_eq!(JobStatusPlus::from_str("cancelled_by_system").unwrap(), JobStatusPlus::CancelledBySystem);
}
#[test]
fn all_variants() {
let mut variants = JobStatusPlus::all_variants();
assert_eq!(variants.len(), 8);
assert_eq!(variants.pop_first(), Some(JobStatusPlus::Pending));
assert_eq!(variants.pop_first(), Some(JobStatusPlus::Started));
assert_eq!(variants.pop_first(), Some(JobStatusPlus::CompleteSuccess));
assert_eq!(variants.pop_first(), Some(JobStatusPlus::CompleteFailure));
assert_eq!(variants.pop_first(), Some(JobStatusPlus::AttemptFailed));
assert_eq!(variants.pop_first(), Some(JobStatusPlus::Dead));
assert_eq!(variants.pop_first(), Some(JobStatusPlus::CancelledByUser));
assert_eq!(variants.pop_first(), Some(JobStatusPlus::CancelledBySystem));
assert_eq!(variants.pop_first(), None);
}
}
mod mechanical_checks {
use super::*;
#[test]
fn variant_length() {
use strum::IntoEnumIterator;
assert_eq!(JobStatusPlus::all_variants().len(), JobStatusPlus::iter().len());
}
#[test]
fn round_trip() {
for variant in JobStatusPlus::all_variants() {
// Test to_str(), from_str(), Display, and Debug.
assert_eq!(variant, JobStatusPlus::from_str(variant.to_str()).unwrap());
assert_eq!(variant, JobStatusPlus::from_str(&format!("{}", variant)).unwrap());
assert_eq!(variant, JobStatusPlus::from_str(&format!("{:?}", variant)).unwrap());
}
}
}
}
+2
View File
@@ -1,3 +1,5 @@
pub mod job_status;
pub mod job_status_plus;
pub mod sqlite;
pub mod visibility;
pub mod vocoder_type;
+3 -1
View File
@@ -26,6 +26,7 @@ pub(crate) enum EntityType {
TtsModel,
TtsRenderTask, // NB: aichatbot / sqlite
User,
UserDeprecatedDoNotUse, // NB: Users prior to 2023-10-24 used this prefix.
VoiceConversionModel,
VoiceConversionResult,
W2lTemplate,
@@ -49,7 +50,8 @@ impl EntityType {
Self::NewsStory => "news_story_",
Self::TtsModel => "TM:", // NB: Old-style prefix, do not use for future tokens.
Self::TtsRenderTask => "tts_task_",
Self::User => "U:", // NB: Old-style prefix, do not use for future tokens.
Self::User => "user_",
Self::UserDeprecatedDoNotUse => "U:", // NB: Users prior to 2023-10-24 used this prefix.
Self::VoiceConversionModel => "vcm_",
Self::VoiceConversionResult => "vcr_",
Self::W2lTemplate => "WT:", // NB: Old-style prefix, do not use for future tokens.
+2 -1
View File
@@ -11,4 +11,5 @@ use crate::prefixes::EntityType;
pub struct UserToken(pub String);
impl_string_token!(UserToken);
impl_crockford_generator!(UserToken, 15usize, EntityType::User, CrockfordUpper);
// NB: Older user tokens were under this regime: 15 characters, "U:" prefix, Crockford Upper.
impl_crockford_generator!(UserToken, 18usize, EntityType::User, CrockfordLower);
+25 -25
View File
@@ -18,31 +18,31 @@ mod interface {
fn generate() {
let token = UserToken::generate();
assert!(!token.to_string().is_empty());
assert!(token.to_string().starts_with("U:"));
assert!(token.to_string().starts_with("user_"));
}
#[test]
fn new() {
let token = UserToken::new("U:foo".to_string());
assert_eq!(token, UserToken("U:foo".to_string()));
let token = UserToken::new("user_foo".to_string());
assert_eq!(token, UserToken("user_foo".to_string()));
}
#[test]
fn new_from_str() {
let token = UserToken::new_from_str("U:foo");
assert_eq!(token, UserToken("U:foo".to_string()));
let token = UserToken::new_from_str("user_foo");
assert_eq!(token, UserToken("user_foo".to_string()));
}
#[test]
fn as_str() {
let token = UserToken("U:foo".to_string());
assert_eq!(token.as_str(), "U:foo");
let token = UserToken("user_foo".to_string());
assert_eq!(token.as_str(), "user_foo");
}
#[test]
fn to_string() {
let token = UserToken("U:foo".to_string());
assert_eq!(token.to_string(), "U:foo".to_string());
let token = UserToken("user_foo".to_string());
assert_eq!(token.to_string(), "user_foo".to_string());
}
}
@@ -51,14 +51,14 @@ mod traits {
#[test]
fn display() {
let token = UserToken("U:foo".to_string());
assert_eq!(format!("{}", token), "U:foo".to_string());
let token = UserToken("user_foo".to_string());
assert_eq!(format!("{}", token), "user_foo".to_string());
}
#[test]
fn debug() {
let token = UserToken("U:foo".to_string());
assert_eq!(format!("{:?}", token), "UserToken(\"U:foo\")".to_string());
let token = UserToken("user_foo".to_string());
assert_eq!(format!("{:?}", token), "UserToken(\"user_foo\")".to_string());
}
}
@@ -69,19 +69,19 @@ mod serialization {
#[test]
fn serialize() {
let expected = "\"U:foo\"".to_string(); // NB: Quoted
let expected = "\"user_foo\"".to_string(); // NB: Quoted
let token = UserToken("U:foo".to_string());
let token = UserToken("user_foo".to_string());
assert_eq!(expected, toml::to_string(&token).unwrap());
// Just to show this serializes the same as a string
assert_eq!(expected, toml::to_string("U:foo").unwrap());
assert_eq!(expected, toml::to_string("user_foo").unwrap());
}
#[test]
fn nested_serialize() {
let value = CompositeType { user_token: UserToken("U:foo".to_string()), string: "bar".to_string() };
let expected = r#"{"user_token":"U:foo","string":"bar"}"#.to_string();
let value = CompositeType { user_token: UserToken("user_foo".to_string()), string: "bar".to_string() };
let expected = r#"{"user_token":"user_foo","string":"bar"}"#.to_string();
assert_eq!(expected, serde_json::to_string(&value).unwrap());
}
}
@@ -93,8 +93,8 @@ mod deserialization {
#[test]
fn deserialize() {
let payload = "\"U:foo\""; // NB: Quoted
let expected = "U:foo".to_string();
let payload = "\"user_foo\""; // NB: Quoted
let expected = "user_foo".to_string();
let value: UserToken = serde_json::from_str(payload).unwrap();
assert_eq!(value, UserToken(expected.clone()));
@@ -106,9 +106,9 @@ mod deserialization {
#[test]
fn nested_deserialize() {
let payload = r#"{"user_token":"U:foo","string":"bar"}"#.to_string();
let payload = r#"{"user_token":"user_foo","string":"bar"}"#.to_string();
let expected = CompositeType {
user_token: UserToken("U:foo".to_string()),
user_token: UserToken("user_foo".to_string()),
string: "bar".to_string(),
};
@@ -129,7 +129,7 @@ mod crockford_traits {
#[test]
fn token_length() {
assert_eq!(UserToken::generate().as_str().len(), 15);
assert_eq!(UserToken::generate().as_str().len(), 18);
}
#[test]
@@ -148,7 +148,7 @@ mod crockford_traits {
let random_part = token_string.replace(prefix, "");
assert!(random_part.len() > ENTROPIC_CHARACTERS_MINIMUM);
assert!(random_part.chars().all(|c| c.is_numeric() || c.is_uppercase()));
assert!(random_part.chars().all(|c| c.is_numeric() || c.is_lowercase()));
}
#[test]
@@ -169,7 +169,7 @@ mod crockford_traits {
#[test]
fn entropy_suffix() {
let token = UserToken::new_from_str("U:foo");
let token = UserToken::new_from_str("user_foo");
assert_eq!(token.entropy_suffix(), "foo");
let token = UserToken::new_from_str("bar");
+6
View File
@@ -14,12 +14,18 @@ path = "src/main.rs"
[dependencies]
# Internal
buckets = { path = "../../_types/buckets" }
cloud_storage = { path = "../../lib/api_clients/cloud_storage" }
config = { path = "../../lib/deprecated/config" }
easyenv = { path = "../../lib/easyenv" }
enums = { path = "../../_types/enums" }
errors = { path = "../../lib/errors" }
filesys = { path = "../../lib/files/filesys" }
hashing = { path = "../../lib/files/hashing" }
idempotency = { path = "../../lib/idempotency" }
mimetypes = { path = "../../lib/files/mimetypes" }
mysql_queries = { path = "../../lib/mysql_queries" }
storyteller_root = { path = "../../lib/storyteller_root" }
tokens = { path = "../../_types/tokens" }
# External
+32 -1
View File
@@ -1,6 +1,9 @@
use std::time::Duration;
use log::info;
use sqlx::mysql::MySqlPoolOptions;
use cloud_storage::bucket_client::BucketClient;
use config::shared_constants::{DEFAULT_MYSQL_CONNECTION_STRING, DEFAULT_RUST_LOG};
use errors::AnyhowResult;
@@ -16,6 +19,9 @@ pub async fn main() -> AnyhowResult<()> {
info!("Database seed CLI script.");
// NB: Read secrets (eg. ACCESS_KEY)
easyenv::from_filename("env-secrets")?;
let db_connection_string =
easyenv::get_env_string_or_default(
"MYSQL_URL",
@@ -26,10 +32,35 @@ pub async fn main() -> AnyhowResult<()> {
.connect(&db_connection_string)
.await?;
let public_bucket_client = get_bucket_client()?;
seed_user_accounts(&pool).await?;
seed_zero_shot_tts(&pool).await?;
seed_zero_shot_tts(&pool, &public_bucket_client).await?;
seed_voice_conversion(&pool).await?;
info!("Done!");
Ok(())
}
fn get_bucket_client() -> AnyhowResult<BucketClient> {
let access_key = easyenv::get_env_string_required("ACCESS_KEY")?;
let secret_key = easyenv::get_env_string_required("SECRET_KEY")?;
let region_name = easyenv::get_env_string_required("REGION_NAME")?;
let public_bucket_name = easyenv::get_env_string_required("PUBLIC_BUCKET_NAME")?;
let bucket_timeout = easyenv::get_env_duration_seconds_or_default(
"BUCKET_TIMEOUT_SECONDS", Duration::from_secs(60 * 5));
info!("Configuring GCS bucket...");
let public_bucket_client = BucketClient::create(
&access_key,
&secret_key,
&region_name,
&public_bucket_name,
None,
Some(bucket_timeout),
)?;
Ok(public_bucket_client)
}
@@ -12,6 +12,7 @@ pub const HANASHI_USERNAME : &str = "hanashi";
pub async fn seed_user_accounts(mysql_pool: &Pool<MySql>) -> AnyhowResult<()> {
info!("Seeding user accounts...");
// NB: This is idempotent and will only install the accounts once.
let users = [
(ADMIN_USERNAME, "password"),
(HANASHI_USERNAME, "password"),
@@ -1,15 +1,29 @@
use std::path::PathBuf;
use log::info;
use sqlx::{MySql, Pool};
use buckets::public::media_files::original_file::MediaFileBucketPath;
use cloud_storage::bucket_client::BucketClient;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_uploads::media_upload_type::MediaUploadType;
use enums::common::visibility::Visibility;
use errors::{anyhow, AnyhowResult};
use filesys::file_read_bytes::file_read_bytes;
use idempotency::uuid::generate_random_uuid;
use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes;
use mysql_queries::queries::users::user::get_user_token_by_username::get_user_token_by_username;
use mysql_queries::queries::voice_designer::datasets::create_dataset::{create_dataset, CreateDatasetArgs};
use mysql_queries::queries::voice_designer::voice_samples::insert_dataset_sample_and_media_file::{insert_dataset_sample_and_media_file, InsertDatasetSampleAndMediaFileArgs};
use mysql_queries::queries::voice_designer::voices::create_voice::{create_voice, CreateVoiceArgs};
use storyteller_root::get_storyteller_rust_root;
use tokens::tokens::users::UserToken;
use tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken;
use tokens::tokens::zs_voices::ZsVoiceToken;
use crate::seeding::users::HANASHI_USERNAME;
pub async fn seed_zero_shot_tts(mysql_pool: &Pool<MySql>) -> AnyhowResult<()> {
pub async fn seed_zero_shot_tts(mysql_pool: &Pool<MySql>, bucket_client: &BucketClient) -> AnyhowResult<()> {
info!("Seeding zero shot TTS...");
let user_token = match get_user_token_by_username(HANASHI_USERNAME, mysql_pool).await? {
@@ -20,16 +34,16 @@ pub async fn seed_zero_shot_tts(mysql_pool: &Pool<MySql>) -> AnyhowResult<()> {
let records = [
// NB: The bucket hashes here are already uploaded to the development Google Cloud Storage
// bucket and should be usable if you have the development secrets on your machine.
("Alice", "qtqaprnd5shtybve4fqpvcfp50yjw238", &user_token),
("Biden", "n945w0xsq15xrh16hc147a5mc1a91gwh", &user_token),
("Goku", "cnnv05yjst2m737dpmxazgfpksjf4y7c", &user_token),
("Hilary", "7wav68ba2yy86491jk36cgk36tkmzesr", &user_token),
("Obama", "z3gy4v56sgtfrxfrpvaj7v74sqc67rcq", &user_token),
("Trump", "qcy7pv3rph0ntkqnpz5cfg9ksyh7kkz5", &user_token),
("Alice", "qtqaprnd5shtybve4fqpvcfp50yjw238", "alice.wav", &user_token),
("Biden", "n945w0xsq15xrh16hc147a5mc1a91gwh", "biden.wav", &user_token),
("Goku", "cnnv05yjst2m737dpmxazgfpksjf4y7c", "goku.wav", &user_token),
("Hilary", "7wav68ba2yy86491jk36cgk36tkmzesr", "hilary.wav", &user_token),
("Obama", "z3gy4v56sgtfrxfrpvaj7v74sqc67rcq", "obama.wav", &user_token),
("Trump", "qcy7pv3rph0ntkqnpz5cfg9ksyh7kkz5", "trump.wav", &user_token),
];
for (voice_name, bucket_hash, user_token) in records {
create_voice_records(voice_name, bucket_hash, user_token, mysql_pool).await?;
for (voice_name, bucket_hash, wav_file, user_token) in records {
create_voice_records(voice_name, bucket_hash, wav_file, user_token, mysql_pool, bucket_client).await?;
}
Ok(())
@@ -38,12 +52,14 @@ pub async fn seed_zero_shot_tts(mysql_pool: &Pool<MySql>) -> AnyhowResult<()> {
async fn create_voice_records(
voice_name: &str,
bucket_hash: &str,
wav_file: &str,
creator_user_token: &UserToken,
mysql_pool: &Pool<MySql>,
) -> AnyhowResult<()> {
bucket_client: &BucketClient,
) -> AnyhowResult<(ZsVoiceToken, ZsVoiceDatasetToken)> {
info!("Creating voice records for voice {} ...", voice_name);
let dataset_title = format!("{} dataset", voice_name);
let dataset_title = format!("{} Dataset", voice_name);
info!("Creating dataset...");
@@ -51,11 +67,53 @@ async fn create_voice_records(
dataset_title: &dataset_title,
maybe_creator_user_token: Some(creator_user_token.as_str()),
creator_ip_address: "127.0.0.1",
creator_set_visibility: &Default::default(),
creator_set_visibility: Visibility::Public,
mysql_pool,
}).await?;
info!("Creating voice...");
info!("Uploading wav file {} ...", wav_file);
let public_upload_path = MediaFileBucketPath::generate_new(
Some("dev_zs_"),
Some(".bin"));
info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str());
let mut file_path = get_storyteller_rust_root();
file_path.push(format!("assets/seed/{}", wav_file));
info!("Reading seed file: {:?}", file_path);
let bytes = file_read_bytes(file_path)?;
let mimetype = get_mimetype_for_bytes(&bytes).unwrap_or("audio/wav");
let _r = bucket_client.upload_file_with_content_type(
public_upload_path.get_full_object_path_str(),
bytes.as_ref(),
mimetype)
.await?;
info!("Creating dataset sample record...");
let uuid_idempotency_token = generate_random_uuid();
let (_sample_token, _media_file_token, _id) =
insert_dataset_sample_and_media_file(InsertDatasetSampleAndMediaFileArgs {
uuid_idempotency_token: &uuid_idempotency_token,
dataset_token: &dataset_token,
media_type: MediaUploadType::Audio,
origin_category: MediaFileOriginCategory::Upload,
checksum_sha2: "", // TODO
media_file_path: &public_upload_path,
maybe_creator_user_token: Some(&creator_user_token),
maybe_creator_anonymous_visitor_token: None,
creator_ip_address: "127.0.0.1",
creator_set_visibility: Visibility::Public,
mysql_pool,
}).await?;
info!("Creating voice record...");
let voice_token = create_voice(CreateVoiceArgs {
dataset_token: &dataset_token,
@@ -67,9 +125,9 @@ async fn create_voice_records(
bucket_hash: &bucket_hash,
maybe_creator_user_token: Some(&creator_user_token),
creator_ip_address: "127.0.0.1",
creator_set_visibility: &Default::default(),
creator_set_visibility: Visibility::Public,
mysql_pool,
}).await?;
Ok(())
Ok((voice_token, dataset_token))
}
@@ -0,0 +1,44 @@
[package]
name = "dev-upload-media-file"
edition = "2018"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[[bin]]
name = "dev-upload-media-file"
path = "src/main.rs"
[dependencies]
# Internal
buckets = { path = "../../_types/buckets" }
cloud_storage = { path = "../../lib/api_clients/cloud_storage" }
config = { path = "../../lib/deprecated/config" }
easyenv = { path = "../../lib/easyenv" }
enums = { path = "../../_types/enums" }
errors = { path = "../../lib/errors" }
filesys = { path = "../../lib/files/filesys" }
hashing = { path = "../../lib/files/hashing" }
mimetypes = { path = "../../lib/files/mimetypes" }
mysql_queries = { path = "../../lib/mysql_queries" }
tokens = { path = "../../_types/tokens" }
# External
clap = "2.34.0" # TODO: This is really out of date, but has conflicts when updated
log = "0.4.14"
# Runtime
# NB: We switched to Tokio from `async-std` for broader ecosystem support.
tokio = { version = "1.6.0", features = ["macros"] }
# MySql Database
# "offline" supports typesafe builds without a live database
# "rustls" doesn't appear to work with IP addresses, per:
# https://github.com/launchbadge/sqlx/issues/1095#issuecomment-798050828
sqlx = { version = "0.6.2", features = [ "mysql", "runtime-actix-native-tls", "offline", "chrono" ] }
[dev-dependencies]
# None yet
@@ -0,0 +1,34 @@
use std::path::PathBuf;
use clap::{App, Arg};
use errors::{anyhow, AnyhowResult};
pub struct CliArgs {
pub file_path: PathBuf,
}
pub fn parse_cli_args() -> AnyhowResult<CliArgs> {
let matches = App::new("scrape_test")
.arg(Arg::with_name("file")
.short("f")
.long("file")
.value_name("FILE")
.help("File to upload")
.takes_value(true)
.required(true))
//.arg(Arg::with_name("print_html")
// .long("html")
// .help("Print the HTML scraped")
// .takes_value(false)
// .required(false))
.get_matches();
let file_path = matches.value_of("file")
.map(|val| val.trim().to_string())
.ok_or(anyhow!("no url supplied"))?;
//let print_html = matches.is_present("print_html");
Ok(CliArgs {
file_path: PathBuf::from(file_path),
})
}
@@ -0,0 +1,154 @@
use std::time::Duration;
use log::info;
use sqlx::mysql::MySqlPoolOptions;
use buckets::public::media_files::original_file::MediaFileBucketPath;
use cloud_storage::bucket_client::BucketClient;
use config::shared_constants::{DEFAULT_MYSQL_CONNECTION_STRING, DEFAULT_RUST_LOG};
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use errors::{AnyhowResult, bail};
use filesys::file_read_bytes::file_read_bytes;
use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes;
use mysql_queries::queries::media_files::insert_media_file_from_cli_tool::{insert_media_file_from_cli_tool, InsertArgs};
use crate::cli_args::parse_cli_args;
pub mod cli_args;
#[tokio::main]
pub async fn main() -> AnyhowResult<()> {
easyenv::init_all_with_default_logging(Some(DEFAULT_RUST_LOG));
info!("Media file upload");
// NB: Read secrets (eg. ACCESS_KEY)
easyenv::from_filename(".env-secrets")?;
let db_connection_string =
easyenv::get_env_string_or_default(
"MYSQL_URL",
DEFAULT_MYSQL_CONNECTION_STRING);
info!("Connecting to MySQL...");
let pool = MySqlPoolOptions::new()
.max_connections(easyenv::get_env_num("MYSQL_MAX_CONNECTIONS", 3)?)
.connect(&db_connection_string)
.await?;
let access_key = easyenv::get_env_string_required("ACCESS_KEY")?;
let secret_key = easyenv::get_env_string_required("SECRET_KEY")?;
let region_name = easyenv::get_env_string_required("REGION_NAME")?;
let public_bucket_name = easyenv::get_env_string_required("PUBLIC_BUCKET_NAME")?;
let bucket_timeout = easyenv::get_env_duration_seconds_or_default(
"BUCKET_TIMEOUT_SECONDS", Duration::from_secs(60 * 5));
info!("Configuring GCS bucket...");
let public_bucket_client = BucketClient::create(
&access_key,
&secret_key,
&region_name,
&public_bucket_name,
None,
Some(bucket_timeout),
)?;
let args = parse_cli_args()?;
info!("Reading file to upload: {:?} ...", args.file_path);
let bytes = file_read_bytes(args.file_path)?;
let mimetype = get_mimetype_for_bytes(&bytes);
let mut media_file_type;
let mut maybe_mime_type;
let mut maybe_public_bucket_prefix;
let mut maybe_public_bucket_extension;
match mimetype {
Some("audio/wav") |
Some("audio/x-wav") => {
media_file_type = MediaFileType::Video;
maybe_mime_type = Some("audio/wav");
maybe_public_bucket_prefix = Some("dev_upload_");
maybe_public_bucket_extension = Some(".wav");
}
Some("video/mp4") => {
media_file_type = MediaFileType::Video;
maybe_mime_type = mimetype;
maybe_public_bucket_prefix = Some("dev_upload_");
maybe_public_bucket_extension = Some(".mp4");
}
Some("image/jpeg") => {
media_file_type = MediaFileType::Image;
maybe_mime_type = mimetype;
maybe_public_bucket_prefix = Some("dev_upload_");
maybe_public_bucket_extension = Some(".jpg");
}
Some("image/png") => {
media_file_type = MediaFileType::Image;
maybe_mime_type = mimetype;
maybe_public_bucket_prefix = Some("dev_upload_");
maybe_public_bucket_extension = Some(".png");
}
_ => {
bail!("Invalid mime type: {:?}", mimetype);
}
}
let mimetype = mimetype.unwrap_or("text/plain");
let creator_set_visibility = Visibility::Public;
let public_upload_path = MediaFileBucketPath::generate_new(
maybe_public_bucket_prefix,
maybe_public_bucket_extension);
info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str());
let _r = public_bucket_client.upload_file_with_content_type(
public_upload_path.get_full_object_path_str(),
bytes.as_ref(),
mimetype)
.await?;
let (media_file_token, _id) = insert_media_file_from_cli_tool(InsertArgs {
pool: &pool,
media_file_type,
maybe_mime_type,
file_size_bytes: 0, // TODO
sha256_checksum: "", // TODO
maybe_creator_user_token: None,
creator_set_visibility,
public_bucket_directory_hash: public_upload_path.get_object_hash(),
maybe_public_bucket_prefix,
maybe_public_bucket_extension,
}).await?;
info!("New media file token: {:?}", media_file_token);
let bucket_url = format!(
"https://storage.cloud.google.com/{}{}",
public_bucket_name,
public_upload_path.get_full_object_path_str()
);
let gcp_url = format!(
"https://console.cloud.google.com/storage/browser/_details/{}{}",
public_bucket_name,
public_upload_path.get_full_object_path_str()
);
println!("\n\n ========== FILE INFO ==========\n\n");
println!(" token: {}", media_file_token);
println!(" URL: {}", bucket_url);
println!(" GCP Admin: {}", gcp_url);
println!("\n");
Ok(())
}
@@ -75,4 +75,26 @@ impl <T, B> RouteBuilder<T, B>
);
self
}
/// Add an HTTP DELETE route.
pub fn add_delete<F, Args>(mut self, path: &str, handler: F, add_head_route: bool) -> Self
where
F: Handler<Args>,
Args: FromRequest + 'static,
F::Output: Responder + 'static,
{
if add_head_route {
self.app = self.app.service(
web::resource(path)
.route(web::delete().to(handler))
.route(web::head().to(|| HttpResponse::Ok())) // NB: For XHR/CORS HEAD requests.
);
} else {
self.app = self.app.service(
web::resource(path)
.route(web::delete().to(handler))
);
}
self
}
}
@@ -192,6 +192,7 @@ impl BucketClient {
object_path: P,
filesystem_path: Q,
) -> anyhow::Result<()> {
// TODO I think we might want to return an error here? but there has to a reason why we are just returning ok?
let object_path_str = object_path.as_ref()
.to_str()
.map(|s| s.to_string())
@@ -213,7 +213,6 @@ impl BucketPathUnifier {
.join(speaker_encoding_filename)
}
// ==================== VOICE CONVERSION MODELS ==================== //
// NB: Entropic hash is not based on file hash and is shared between .pth and .index files.
+10
View File
@@ -11,6 +11,16 @@ pub fn init_all_with_default_logging(default_if_absent: Option<&str>) {
init_env_logger(default_if_absent)
}
/// Read env configs from a filename.
pub fn from_filename<P: AsRef<Path>>(filename: P) -> Result<(), InitError> {
let _path = dotenv::from_filename(&filename)
.map_err(|err| {
error!("Could not read env config from path {:?} : {:?}", filename.as_ref(), err);
InitError::DotEnvError
})?;
Ok(())
}
/// Try loading in an environment file across many search paths (first found wins).
/// Returns an error if no file could be read or if in attempting to read a file there was an error.
pub fn read_env_config_from_filename_and_paths<P: AsRef<Path>, Q: AsRef<Path>>(filename: P, paths: &[Q]) -> Result<(), InitError> {
+1
View File
@@ -32,6 +32,7 @@
// It's easy to just keep them exported here to maintain compatibility.
pub use error::EnvError;
pub use error::InitError;
pub use init::from_filename;
pub use init::init_all_with_default_logging;
pub use init::maybe_read_env_config_from_filename_and_paths;
pub use init::read_env_config_from_filename_and_paths;
@@ -0,0 +1,16 @@
use std::fs::File;
use std::io;
use std::io::{BufReader, Read};
use std::path::Path;
/// Read a file into a byte buffer
pub fn file_read_bytes<P: AsRef<Path>>(file_path: P) -> io::Result<Vec<u8>> {
let f = File::open(file_path)?;
let mut reader = BufReader::new(f);
let mut buffer = Vec::new();
// Read file into vector.
reader.read_to_end(&mut buffer)?;
Ok(buffer)
}
+1
View File
@@ -21,6 +21,7 @@
pub mod create_dir_all_if_missing;
pub mod directory_exists;
pub mod file_exists;
pub mod file_read_bytes;
pub mod file_size;
pub mod filename_concat;
pub mod path_to_string;
File diff suppressed because it is too large Load Diff
@@ -1,45 +0,0 @@
//! These are columns where users can control the visibility of their data.
use anyhow::anyhow;
use errors::AnyhowResult;
/// To use this in a query, the query must have type annotations.
/// See: https://www.gitmemory.com/issue/launchbadge/sqlx/1241/847154375
/// eg. job_status as `job_status: crate::column_types::job_status::JobStatus`
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, sqlx::Type)]
#[serde(rename_all = "snake_case")]
#[sqlx(rename_all = "snake_case")]
pub enum JobStatus {
Pending,
Started,
CompleteSuccess,
CompleteFailure,
AttemptFailed,
Dead,
}
impl JobStatus {
pub fn to_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Started => "started",
Self::CompleteSuccess => "complete_success",
Self::CompleteFailure => "complete_failure",
Self::AttemptFailed => "attempt_failed",
Self::Dead => "dead",
}
}
pub fn from_str(job_status: &str) -> AnyhowResult<Self> {
match job_status {
"pending" => Ok(Self::Pending),
"started" => Ok(Self::Started),
"complete_success" => Ok(Self::CompleteSuccess),
"complete_failure" => Ok(Self::CompleteFailure),
"attempt_failed" => Ok(Self::AttemptFailed),
"dead" => Ok(Self::Dead),
_ => Err(anyhow!("invalid job_status: {:?}", job_status)),
}
}
}
@@ -1,3 +1,2 @@
pub mod job_status;
pub mod twitch_event_category;
pub mod vocoder_type;
@@ -4,6 +4,7 @@ use errors::AnyhowResult;
use crate::payloads::generic_inference_args::lipsync_payload::LipsyncArgs;
use crate::payloads::generic_inference_args::tts_payload::TTSArgs;
/// Used to encode extra state for the `generic_inference_jobs` table.
/// This should act somewhat like a serialized protobuf stored inside a record.
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -55,10 +56,10 @@ pub enum PolymorphicInferenceArgs {
La (LipsyncArgs),
/// Text to speech. (Short name to save space when serializing.)
Tts(TTSArgs)
Tts(TTSArgs),
// No arguments yet.
// It might be best to just not include this when not used.
,
/// Voice conversion. (Short name to save space when serializing.)
Vc {
/// Argument for so-vits-svc
@@ -147,14 +148,11 @@ mod tests {
assert!(json.len() < 1000);
}
// TODO FIX THIS TEST! MICHAEL
#[test]
fn typical_tts_args_serialize() {
let args = GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::TextToSpeech),
args: Some(PolymorphicInferenceArgs::Tts(TTSArgs {
text: "hello world".to_string(),
voice_token: "token".to_string(),
})),
};
@@ -162,7 +160,7 @@ mod tests {
let json = serde_json::ser::to_string(&args).unwrap();
// NB: Assert the serialized form. If this changes and the test breaks, be careful about migrating.
assert_eq!(json, r#"{"cat":"tts","args":{"Tts":{"t":"hello world","e":"token"}}}"#.to_string());
assert_eq!(json, r#"{"cat":"tts","args":{"Tts":{"e":"token"}}}"#.to_string());
// NB: Make sure we don't overflow the DB field capacity (TEXT column).
assert!(json.len() < 1000);
@@ -1,17 +1,8 @@
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TTSArgs {
#[serde(rename = "t")]
pub text: String,
#[serde(rename = "e")]
pub voice_token: String // varchar 32
}
#[cfg(test)]
mod tests {
use crate::payloads::generic_inference_args::tts_payload::TTSArgs;
#[test]
fn test_tts_result() {
}
#[serde(rename = "vt")]
pub voice_token: Option<String>, // varchar 32
#[serde(rename = "dt")]
pub dataset_token: Option<String>
}
@@ -4,11 +4,11 @@ use chrono::Utc;
use sqlx::MySqlPool;
use enums::by_table::generic_download_jobs::generic_download_type::GenericDownloadType;
use enums::common::job_status::JobStatus;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::generic_download_jobs::DownloadJobToken;
use crate::column_types::job_status::JobStatus;
use crate::queries::generic_download::job::_keys::GenericDownloadJobId;
/// table: generic_download_jobs
@@ -6,11 +6,11 @@ use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
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::generic_inference_jobs::InferenceJobToken;
use crate::column_types::job_status::JobStatus;
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;
@@ -38,11 +38,12 @@ pub struct AvailableInferenceJob {
// User information to propagate downstream
pub maybe_creator_user_token: Option<String>,
pub maybe_creator_anonymous_visitor_token: Option<String>,
pub creator_ip_address: String,
pub creator_set_visibility: Visibility,
// Job information
pub status: JobStatus,
pub status: JobStatusPlus,
pub attempt_count: u16,
pub priority_level: u16,
@@ -111,6 +112,7 @@ pub async fn list_available_generic_inference_jobs(
uuid_idempotency_token: record.uuid_idempotency_token,
creator_ip_address: record.creator_ip_address,
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_creator_anonymous_visitor_token: record.maybe_creator_anonymous_visitor_token,
creator_set_visibility: Visibility::from_str(&record.creator_set_visibility)
.map_err(|e| anyhow!("error: {:?}", e))?, // TODO/FIXME: This is a gross fix.
inference_category: InferenceCategory::from_str(&record.inference_category)
@@ -128,7 +130,8 @@ pub async fn list_available_generic_inference_jobs(
.map(|args| GenericInferenceArgs::from_json(args))
.transpose()?,
maybe_raw_inference_text: record.maybe_raw_inference_text,
status: JobStatus::from_str(&record.status)?,
status: JobStatusPlus::from_str(&record.status)
.map_err(|err| anyhow!("JobStatus failure to parse: {:?}", err))?,
attempt_count: record.attempt_count,
priority_level: record.priority_level,
is_keepalive_required: i8_to_bool(record.is_keepalive_required),
@@ -180,6 +183,7 @@ SELECT
maybe_raw_inference_text,
maybe_creator_user_token,
maybe_creator_anonymous_visitor_token,
creator_ip_address,
creator_set_visibility,
@@ -273,6 +277,7 @@ SELECT
maybe_raw_inference_text,
maybe_creator_user_token,
maybe_creator_anonymous_visitor_token,
creator_ip_address,
creator_set_visibility,
@@ -363,6 +368,7 @@ struct AvailableInferenceJobRawInternal {
// User information to propagate downstream
pub maybe_creator_user_token: Option<String>,
pub maybe_creator_anonymous_visitor_token: Option<String>,
pub creator_ip_address: String,
//pub creator_set_visibility: Visibility,
pub creator_set_visibility: String,
@@ -0,0 +1,33 @@
use anyhow::anyhow;
use sqlx;
use sqlx::MySql;
use sqlx::pool::PoolConnection;
use errors::AnyhowResult;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
/// Mark a single job as cancelled by the user.
pub async fn mark_generic_inference_job_cancelled_by_user(
mysql_connection: &mut PoolConnection<MySql>,
job_token: &InferenceJobToken
) -> AnyhowResult<()> {
let query_result = sqlx::query!(
r#"
UPDATE generic_inference_jobs
SET
status = 'cancelled_by_user',
retry_at = NULL
WHERE token = ?
AND status NOT IN ('complete_success', 'complete_failure', 'dead', 'cancelled_by_system')
"#,
job_token,
)
.execute(mysql_connection)
.await;
match query_result {
Err(err) => Err(anyhow!("error with query: {:?}", err)),
Ok(_r) => Ok(()),
}
}
@@ -62,6 +62,8 @@ FOR UPDATE
"complete_success" => false, // Job already complete
"complete_failure" => false, // Job already complete (permanently dead; no need to retry)
"dead" => false, // Job already complete (permanently dead; retries exhausted)
"cancelled_by_user" => false, // Job already complete (permanently dead; killed by user)
"cancelled_by_system" => false, // Job already complete (permanently dead; killed by system)
_ => false, // Future-proof
};
@@ -1,6 +1,7 @@
pub mod _keys;
pub mod list_available_generic_inference_jobs;
pub mod mark_generic_inference_job_cancelled_by_user;
pub mod mark_generic_inference_job_completely_failed;
pub mod mark_generic_inference_job_failure;
pub mod mark_generic_inference_job_pending_and_grab_lock;
pub mod mark_generic_inference_job_successfully_done;
pub mod list_available_generic_inference_jobs;
@@ -1,19 +1,24 @@
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use log::warn;
use sqlx::MySqlPool;
use sqlx::{MySql, MySqlPool};
use sqlx::pool::PoolConnection;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::common::job_status_plus::JobStatusPlus;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::users::UserToken;
use crate::helpers::boolean_converters::i8_to_bool;
#[derive(Debug)]
pub struct GenericInferenceJobStatus {
pub job_token: InferenceJobToken,
pub status: String,
pub status: JobStatusPlus,
pub attempt_count: u16,
pub maybe_assigned_worker: Option<String>,
@@ -25,6 +30,7 @@ pub struct GenericInferenceJobStatus {
pub request_details: RequestDetails,
pub maybe_result_details: Option<ResultDetails>,
pub user_details: UserDetails,
pub is_keepalive_required: bool,
@@ -34,6 +40,7 @@ pub struct GenericInferenceJobStatus {
/// Details about the user's original inference request
/// (We may want to present it in the "pending" UI.)
#[derive(Debug)]
pub struct RequestDetails {
pub inference_category: InferenceCategory,
pub maybe_model_type: Option<String>, // TODO: Strongly type
@@ -45,6 +52,7 @@ pub struct RequestDetails {
}
/// Details about the generated result
#[derive(Debug)]
pub struct ResultDetails {
pub entity_type: String,
pub entity_token: String,
@@ -61,10 +69,29 @@ pub struct ResultDetails {
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
}
/// NB: DO NOT EXPOSE TO FRONTEND.
/// This is used to gate access to job termination
#[derive(Debug)]
pub struct UserDetails {
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
pub creator_ip_address: String,
}
/// Look up job status.
/// Returns Ok(None) when the record cannot be found.
pub async fn get_inference_job_status(job_token: &InferenceJobToken, mysql_pool: &MySqlPool)
-> AnyhowResult<Option<GenericInferenceJobStatus>>
{
let mut connection = mysql_pool.acquire().await?;
get_inference_job_status_from_connection(job_token, &mut connection).await
}
/// Look up job status.
/// Returns Ok(None) when the record cannot be found.
pub async fn get_inference_job_status_from_connection(job_token: &InferenceJobToken, mysql_connection: &mut PoolConnection<MySql>)
-> AnyhowResult<Option<GenericInferenceJobStatus>>
{
// NB(bt): jobs.uuid_idempotency_token is the current way to reconstruct the hash of the
// TTS result since we don't store a bucket hash on the table. This is an ugly hack :(
@@ -76,9 +103,13 @@ pub async fn get_inference_job_status(job_token: &InferenceJobToken, mysql_pool:
SELECT
jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,
jobs.status,
jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,
jobs.attempt_count,
jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,
jobs.creator_ip_address,
jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,
jobs.maybe_model_type,
jobs.maybe_model_token,
@@ -94,6 +125,7 @@ SELECT
tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,
voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,
media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,
media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,
media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,
@@ -122,7 +154,7 @@ WHERE jobs.token = ?
"#,
job_token
)
.fetch_one(mysql_pool)
.fetch_one(mysql_connection)
.await;
let record = match maybe_status {
@@ -144,12 +176,22 @@ WHERE jobs.token = ?
// NB: A bit of a hack. We store TTS results with a full path.
// Going forward, all other record types will store a hash.
let (bucket_path_is_hash, maybe_public_bucket_hash) = match record.inference_category {
let (mut bucket_path_is_hash, mut maybe_public_bucket_hash) = match record.inference_category {
InferenceCategory::LipsyncAnimation => (true, record.maybe_media_file_public_bucket_directory_hash.as_deref()),
InferenceCategory::TextToSpeech => (false, record.maybe_tts_public_bucket_path.as_deref()),
InferenceCategory::VoiceConversion => (true, record.maybe_voice_conversion_public_bucket_hash.as_deref()),
};
// NB: We've moved voice conversion out of their own table and into media_files
// This check should allow for graceful migration to the new end-state.
match record.maybe_result_entity_type.as_deref() {
Some("media_file") => {
bucket_path_is_hash = true;
maybe_public_bucket_hash = record.maybe_media_file_public_bucket_directory_hash.as_deref();
}
_ => {}
}
let maybe_result_details = record
.maybe_result_entity_type
.as_deref()
@@ -187,6 +229,11 @@ WHERE jobs.token = ?
maybe_raw_inference_text: record.maybe_raw_inference_text,
},
maybe_result_details,
user_details: UserDetails {
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_creator_anonymous_visitor_token: record.maybe_creator_anonymous_visitor_token,
creator_ip_address: record.creator_ip_address,
},
is_keepalive_required: i8_to_bool(record.is_keepalive_required),
created_at: record.created_at,
updated_at: record.updated_at,
@@ -196,9 +243,13 @@ WHERE jobs.token = ?
struct RawGenericInferenceJobStatus {
pub job_token: InferenceJobToken,
pub status: String,
pub status: JobStatusPlus,
pub attempt_count: u16,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
pub creator_ip_address: String,
pub inference_category: InferenceCategory,
pub maybe_model_type: Option<String>,
pub maybe_model_token: Option<String>,
@@ -6,6 +6,7 @@ use enums::by_table::generic_inference_jobs::inference_input_source_token_type::
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
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::users::UserToken;
@@ -26,6 +27,7 @@ pub struct InsertGenericInferenceArgs<'a> {
pub maybe_inference_args: Option<GenericInferenceArgs>,
pub maybe_creator_user_token: Option<&'a UserToken>,
pub maybe_avt_token: Option<&'a AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'a str,
pub creator_set_visibility: Visibility,
@@ -71,7 +73,9 @@ SET
maybe_inference_args = ?,
maybe_creator_user_token = ?,
maybe_creator_anonymous_visitor_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
priority_level = ?,
@@ -98,7 +102,9 @@ SET
serialized_args_payload,
args.maybe_creator_user_token.map(|t| t.to_string()),
args.maybe_avt_token.map(|t| t.to_string()),
args.creator_ip_address,
args.creator_set_visibility.to_str(),
args.priority_level,
@@ -1,4 +1,3 @@
pub mod get_inference_job_status;
pub mod get_pending_inference_job_count;
pub mod insert_generic_inference_job;
pub mod result_details;
@@ -0,0 +1,110 @@
use anyhow::anyhow;
use log::error;
use sqlx;
use sqlx::MySqlPool;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
pub struct InsertArgs<'a> {
pub pool: &'a MySqlPool,
pub media_file_type: MediaFileType,
pub maybe_mime_type: Option<&'a str>,
pub file_size_bytes: u64,
pub sha256_checksum: &'a str,
pub maybe_creator_user_token: Option<UserToken>,
pub creator_set_visibility: Visibility,
pub public_bucket_directory_hash: &'a str,
pub maybe_public_bucket_prefix: Option<&'a str>,
pub maybe_public_bucket_extension: Option<&'a str>,
}
// NB: Do not use from website / jobs. This is meant to be used from the CLI tool,
// and shoudl allow for a more free-form interface.
pub async fn insert_media_file_from_cli_tool(
args: InsertArgs<'_>
) -> AnyhowResult<(MediaFileToken, u64)>
{
let media_file_token = MediaFileToken::generate();
let mut transaction = args.pool.begin().await?;
const ORIGIN_CATEGORY : MediaFileOriginCategory = MediaFileOriginCategory::Upload;
const ORIGIN_PRODUCT_CATEGORY : MediaFileOriginProductCategory = MediaFileOriginProductCategory::Unknown;
// TODO(bt,2023-10-19): UserToken should automatically serialize to the DB. What's going on?
let maybe_user_token = args.maybe_creator_user_token.map(|u| u.to_string());
let record_id = {
let query_result = sqlx::query!(
r#"
INSERT INTO media_files
SET
token = ?,
origin_category = ?,
origin_product_category = ?,
media_type = ?,
maybe_mime_type = ?,
file_size_bytes = ?,
checksum_sha2 = ?,
public_bucket_directory_hash = ?,
maybe_public_bucket_prefix = ?,
maybe_public_bucket_extension = ?,
maybe_creator_user_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?
"#,
media_file_token,
ORIGIN_CATEGORY,
ORIGIN_PRODUCT_CATEGORY,
args.media_file_type,
args.maybe_mime_type,
args.file_size_bytes,
args.sha256_checksum,
args.public_bucket_directory_hash,
args.maybe_public_bucket_prefix,
args.maybe_public_bucket_extension,
maybe_user_token,
"127.0.0.1",
args.creator_set_visibility.to_str(),
)
.execute(&mut transaction)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
// TODO: handle better
//transaction.rollback().await?;
error!("Error executing query: {:?}", err);
return Err(anyhow!("Mysql error: {:?}", err));
}
};
record_id
};
transaction.commit().await?;
Ok((media_file_token, record_id))
}
@@ -94,6 +94,7 @@ SET
maybe_public_bucket_extension = ?,
maybe_creator_user_token = ?,
maybe_creator_anonymous_visitor_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
@@ -123,6 +124,7 @@ SET
args.maybe_public_bucket_extension,
args.job.maybe_creator_user_token,
args.job.maybe_creator_anonymous_visitor_token,
args.job.creator_ip_address,
args.job.creator_set_visibility.to_str(),
@@ -0,0 +1,175 @@
use anyhow::anyhow;
use sqlx;
use sqlx::MySqlPool;
use enums::by_table::generic_synthetic_ids::id_category::IdCategory;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use errors::AnyhowResult;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
use crate::queries::generic_synthetic_ids::transactional_increment_generic_synthetic_id::transactional_increment_generic_synthetic_id;
pub enum VoiceConversionModelType {
SoVitsSvc,
RvcV2,
}
pub struct InsertMediaFileArgs<'a> {
pub pool: &'a MySqlPool,
pub job: &'a AvailableInferenceJob,
pub voice_conversion_type: VoiceConversionModelType,
pub maybe_mime_type: Option<&'a str>,
pub file_size_bytes: u64,
pub duration_millis: u64,
pub sha256_checksum: &'a str,
// TODO: Media duration.
//pub duration_millis: u64,
pub public_bucket_directory_hash: &'a str,
pub maybe_public_bucket_prefix: Option<&'a str>,
pub maybe_public_bucket_extension: Option<&'a str>,
pub is_on_prem: bool,
pub worker_hostname: &'a str,
pub worker_cluster: &'a str,
}
pub async fn insert_media_file_from_voice_conversion(
args: InsertMediaFileArgs<'_>
) -> AnyhowResult<(MediaFileToken, u64)>
{
let result_token = MediaFileToken::generate();
let origin_model_type = match args.voice_conversion_type {
VoiceConversionModelType::SoVitsSvc => MediaFileOriginModelType::SoVitsSvc,
VoiceConversionModelType::RvcV2 => MediaFileOriginModelType::RvcV2,
};
let mut maybe_creator_file_synthetic_id : Option<u64> = None;
let mut maybe_creator_category_synthetic_id : Option<u64> = None;
let mut transaction = args.pool.begin().await?;
if let Some(creator_user_token) = args.job.maybe_creator_user_token.as_deref() {
let user_token = UserToken::new_from_str(creator_user_token);
let next_media_file_id = transactional_increment_generic_synthetic_id(
&user_token,
IdCategory::MediaFile,
&mut transaction
).await?;
let next_voice_conversion_id = transactional_increment_generic_synthetic_id(
&user_token,
IdCategory::VoiceConversion,
&mut transaction
).await?;
maybe_creator_file_synthetic_id = Some(next_media_file_id);
maybe_creator_category_synthetic_id = Some(next_voice_conversion_id);
}
let vc_model_token = args.job.maybe_model_token.as_deref();
let creator_ip_address = args.job.creator_ip_address.as_str();
let creator_set_visibility = args.job.creator_set_visibility.clone();
const ORIGIN_CATEGORY : MediaFileOriginCategory = MediaFileOriginCategory::Inference;
const ORIGIN_PRODUCT_CATEGORY : MediaFileOriginProductCategory = MediaFileOriginProductCategory::VoiceConversion;
const MEDIA_TYPE : MediaFileType = MediaFileType::Audio;
let record_id = {
let query_result = sqlx::query!(
r#"
INSERT INTO media_files
SET
token = ?,
origin_category = ?,
origin_product_category = ?,
maybe_origin_model_type = ?,
maybe_origin_model_token = ?,
media_type = ?,
maybe_mime_type = ?,
file_size_bytes = ?,
checksum_sha2 = ?,
public_bucket_directory_hash = ?,
maybe_public_bucket_prefix = ?,
maybe_public_bucket_extension = ?,
maybe_creator_user_token = ?,
maybe_creator_anonymous_visitor_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
maybe_creator_file_synthetic_id = ?,
maybe_creator_category_synthetic_id = ?,
is_generated_on_prem = ?,
generated_by_worker = ?,
generated_by_cluster = ?
"#,
result_token.as_str(),
ORIGIN_CATEGORY.to_str(),
ORIGIN_PRODUCT_CATEGORY.to_str(),
origin_model_type.to_str(),
args.job.maybe_model_token,
MEDIA_TYPE.to_str(),
args.maybe_mime_type,
args.file_size_bytes,
args.sha256_checksum,
args.public_bucket_directory_hash,
args.maybe_public_bucket_prefix,
args.maybe_public_bucket_extension,
args.job.maybe_creator_user_token,
args.job.maybe_creator_anonymous_visitor_token,
args.job.creator_ip_address,
args.job.creator_set_visibility.to_str(),
maybe_creator_file_synthetic_id,
maybe_creator_category_synthetic_id,
args.is_on_prem,
args.worker_hostname,
args.worker_cluster
)
.execute(&mut transaction)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
// TODO: handle better
//transaction.rollback().await?;
return Err(anyhow!("Mysql error: {:?}", err));
}
};
record_id
};
transaction.commit().await?;
Ok((result_token, record_id))
}
@@ -0,0 +1,157 @@
use anyhow::anyhow;
use sqlx;
use sqlx::MySqlPool;
use enums::by_table::generic_synthetic_ids::id_category::IdCategory;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use errors::AnyhowResult;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
use crate::queries::generic_synthetic_ids::transactional_increment_generic_synthetic_id::transactional_increment_generic_synthetic_id;
// thought about this it seems like this can be a bit more geneneric instead of having this we can ...
pub struct InsertArgs<'a> {
pub pool: &'a MySqlPool,
pub job: &'a AvailableInferenceJob,
pub maybe_mime_type: Option<&'a str>,
pub file_size_bytes: u64,
pub sha256_checksum: &'a str,
// TODO: Media duration.
//pub duration_millis: u64,
pub public_bucket_directory_hash: &'a str,
pub maybe_public_bucket_prefix: Option<&'a str>,
pub maybe_public_bucket_extension: Option<&'a str>,
pub is_on_prem: bool,
pub worker_hostname: &'a str,
pub worker_cluster: &'a str,
}
pub async fn insert_media_file_from_zero_shot(
args: InsertArgs<'_>
) -> AnyhowResult<(MediaFileToken, u64)>
{
let result_token = MediaFileToken::generate();
let mut maybe_creator_file_synthetic_id : Option<u64> = None;
let mut maybe_creator_category_synthetic_id : Option<u64> = None;
let mut transaction = args.pool.begin().await?;
// create a user token
if let Some(creator_user_token) = args.job.maybe_creator_user_token.as_deref() {
let user_token = UserToken::new_from_str(creator_user_token);
let next_media_file_id = transactional_increment_generic_synthetic_id(
&user_token,
IdCategory::MediaFile,
&mut transaction
).await?;
let next_lipsync_id = transactional_increment_generic_synthetic_id(
&user_token,
IdCategory::ZeroShotTTS,
&mut transaction
).await?;
maybe_creator_file_synthetic_id = Some(next_media_file_id);
maybe_creator_category_synthetic_id = Some(next_lipsync_id);
}
// TODO: Ask if it make sense to use ONE function to do the insert
// Since fill this out apart of the funtion this seems like the only thing to change.
const ORIGIN_CATEGORY : MediaFileOriginCategory = MediaFileOriginCategory::Inference;
const ORIGIN_PRODUCT_CATEGORY : MediaFileOriginProductCategory = MediaFileOriginProductCategory::ZeroShotVoice;
const ORIGIN_MODEL_TYPE : MediaFileOriginModelType = MediaFileOriginModelType::VallEX;
const MEDIA_TYPE : MediaFileType = MediaFileType::Audio;
let record_id = {
let query_result = sqlx::query!(
r#"
INSERT INTO media_files
SET
token = ?,
origin_category = ?,
origin_product_category = ?,
maybe_origin_model_type = ?,
media_type = ?,
maybe_mime_type = ?,
file_size_bytes = ?,
checksum_sha2 = ?,
public_bucket_directory_hash = ?,
maybe_public_bucket_prefix = ?,
maybe_public_bucket_extension = ?,
maybe_creator_user_token = ?,
creator_ip_address = ?,
creator_set_visibility = ?,
maybe_creator_file_synthetic_id = ?,
maybe_creator_category_synthetic_id = ?,
is_generated_on_prem = ?,
generated_by_worker = ?,
generated_by_cluster = ?
"#,
result_token.as_str(),
ORIGIN_CATEGORY.to_str(),
ORIGIN_PRODUCT_CATEGORY.to_str(),
ORIGIN_MODEL_TYPE.to_str(),
MEDIA_TYPE.to_str(),
args.maybe_mime_type,
args.file_size_bytes,
args.sha256_checksum,
args.public_bucket_directory_hash,
args.maybe_public_bucket_prefix,
args.maybe_public_bucket_extension,
args.job.maybe_creator_user_token,
args.job.creator_ip_address,
args.job.creator_set_visibility.to_str(),
maybe_creator_file_synthetic_id,
maybe_creator_category_synthetic_id,
args.is_on_prem,
args.worker_hostname,
args.worker_cluster
)
.execute(&mut transaction)
.await;
let record_id = match query_result {
Ok(res) => {
res.last_insert_id()
},
Err(err) => {
// TODO: handle better
//transaction.rollback().await?;
return Err(anyhow!("Mysql error: {:?}", err));
}
};
record_id
};
transaction.commit().await?;
Ok((result_token, record_id))
}
@@ -1,2 +1,5 @@
pub mod get_media_file;
pub mod insert_media_file_from_face_animation;
pub mod insert_media_file_from_face_animation;
pub mod insert_media_file_from_zero_shot_tts;
pub mod insert_media_file_from_cli_tool;
pub mod insert_media_file_from_voice_conversion;
@@ -6,10 +6,10 @@
use chrono::{DateTime, Utc};
use sqlx::MySqlPool;
use enums::common::job_status::JobStatus;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use crate::column_types::job_status::JobStatus;
use crate::helpers::boolean_converters::i8_to_bool;
use crate::queries::tts::tts_inference_jobs::_keys::TtsInferenceJobId;
@@ -83,7 +83,7 @@ SELECT
is_for_twitch,
is_debug_request,
status as `status: crate::column_types::job_status::JobStatus`,
status as `status: enums::common::job_status::JobStatus`,
priority_level,
attempt_count,
failure_reason,
@@ -132,7 +132,7 @@ SELECT
is_for_twitch,
is_debug_request,
status as `status: crate::column_types::job_status::JobStatus`,
status as `status: enums::common::job_status::JobStatus`,
priority_level,
attempt_count,
failure_reason,
@@ -224,7 +224,7 @@ SELECT
is_for_twitch,
is_debug_request,
status as `status: crate::column_types::job_status::JobStatus`,
status as `status: enums::common::job_status::JobStatus`,
priority_level,
attempt_count,
failure_reason,
@@ -7,7 +7,7 @@ use log::warn;
use sqlx::error::Error::Database;
use sqlx::MySqlPool;
use crate::tokens::Tokens;
use tokens::tokens::users::UserToken;
pub struct CreateAccountArgs<'a> {
pub username: &'a str,
@@ -19,7 +19,7 @@ pub struct CreateAccountArgs<'a> {
}
pub struct CreateAccountSuccessResult {
pub user_token: String,
pub user_token: UserToken,
pub user_id: u64,
}
@@ -40,11 +40,7 @@ pub async fn create_account(
const INITIAL_PROFILE_RENDERED_HTML : &str = "";
const INITIAL_USER_ROLE: &str = "user";
let user_token = Tokens::new_user()
.map_err(|_e| {
warn!("problem creating user token");
CreateAccountError::OtherError
})?;
let user_token = UserToken::generate();
let query_result = sqlx::query!(
r#"
@@ -12,7 +12,7 @@ pub struct CreateDatasetArgs<'a> {
pub dataset_title: &'a str,
pub maybe_creator_user_token: Option<&'a str>,
pub creator_ip_address: &'a str,
pub creator_set_visibility: &'a Visibility,
pub creator_set_visibility: Visibility,
pub mysql_pool: &'a MySqlPool
}
@@ -17,11 +17,11 @@ pub struct SyntheticIdRecord {
pub next_id: i64,
}
pub struct Args <'a> {
pub struct InsertDatasetSampleAndMediaFileArgs<'a> {
pub uuid_idempotency_token: &'a str,
// Dataset to which this sample belongs
pub dataset_token: ZsVoiceDatasetToken,
pub dataset_token: &'a ZsVoiceDatasetToken,
pub media_type: MediaUploadType,
@@ -51,7 +51,7 @@ pub struct Args <'a> {
const ORIGIN_PRODUCT : MediaFileOriginProductCategory = MediaFileOriginProductCategory::Unknown; // TODO: Zero shot voice enum
pub async fn insert_dataset_sample_and_media_file(args: Args<'_>) -> AnyhowResult<(ZsVoiceDatasetSampleToken, MediaFileToken, u64)> {
pub async fn insert_dataset_sample_and_media_file(args: InsertDatasetSampleAndMediaFileArgs<'_>) -> AnyhowResult<(ZsVoiceDatasetSampleToken, MediaFileToken, u64)> {
let mut maybe_creator_synthetic_id : Option<u64> = None;
@@ -11,6 +11,8 @@ use tokens::tokens::users::UserToken;
use tokens::tokens::zs_voice_dataset_samples::ZsVoiceDatasetSampleToken;
use tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken;
pub struct DatasetSampleRecordForList {
pub sample_token: ZsVoiceDatasetSampleToken,
@@ -26,7 +26,7 @@ pub struct CreateVoiceArgs<'a> {
pub maybe_creator_user_token: Option<&'a UserToken>,
pub creator_ip_address: &'a str,
pub creator_set_visibility: &'a Visibility,
pub creator_set_visibility: Visibility,
pub mysql_pool: &'a MySqlPool
}
@@ -84,4 +84,4 @@ pub async fn create_voice(args: CreateVoiceArgs<'_>) -> AnyhowResult<ZsVoiceToke
Ok(_) => Ok(voice_token),
Err(err) => Err(anyhow!("zs voice creation error: {:?}", err)),
}
}
}
@@ -12,7 +12,8 @@ pub struct ZsVoice {
pub title: String,
pub ietf_language_tag: String,
pub ietf_primary_language_subtag: String,
pub maybe_creator_user_token: Option<String>,
pub bucket_hash: String,
pub maybe_creator_user_token: Option<String>
}
pub async fn get_voice_by_token(
@@ -68,7 +69,8 @@ pub async fn get_voice_by_token_with_connection(
title: record.title,
ietf_language_tag: record.ietf_language_tag,
ietf_primary_language_subtag: record.ietf_primary_language_subtag,
maybe_creator_user_token: record.maybe_creator_user_token,
bucket_hash: record.bucket_hash,
maybe_creator_user_token: record.maybe_creator_user_token
}))
}
@@ -85,6 +87,7 @@ async fn select_include_deleted(
zv.ietf_language_tag,
zv.ietf_primary_language_subtag,
zv.maybe_creator_user_token,
zv.bucket_hash,
zv.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`
FROM zs_voices as zv
WHERE
@@ -108,6 +111,7 @@ async fn select_without_deleted(
zv.ietf_language_tag,
zv.ietf_primary_language_subtag,
zv.maybe_creator_user_token,
zv.bucket_hash,
zv.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`
FROM zs_voices as zv
WHERE
@@ -126,5 +130,6 @@ pub struct RawVoice {
ietf_language_tag: String,
ietf_primary_language_subtag: String,
maybe_creator_user_token: Option<String>,
bucket_hash: String,
creator_set_visibility: Visibility,
}
-4
View File
@@ -36,10 +36,6 @@ impl Tokens {
// ========== Tokens not typically visible to users ==========
pub fn new_user() -> AnyhowResult<String> {
random_prefix_crockford_token("U:", 15, true)
}
pub fn new_session() -> AnyhowResult<String> {
random_prefix_crockford_token("SESSION:", 32, false)
}
@@ -43,13 +43,11 @@ SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=a79106991ce3
# #
# ================================= #
SO_VITS_SVC_MODEL_CHECK_ROOT_DIRECTORY="/models/voice_conversion/so-vits-svc"
SO_VITS_SVC_MODEL_CHECK_COMMAND="python3 -m so_vits_svc_fork.fakeyou_infer"
SO_VITS_SVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE="8acacf30bb2e"
SO_VITS_SVC_MODEL_CHECK_ROOT_DIRECTORY="/models/voice_conversion/so-vits-svc/src"
SO_VITS_SVC_MODEL_CHECK_COMMAND="/install/python/bin/python3.10 fakeyou_infer.py" # NB: so-vits-svc-paul-2
SO_VITS_SVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE="1136fe8b50d5"
SO_VITS_SVC_MODEL_CHECK_MAYBE_DEFAULT_CONFIG_PATH="/models/voice_conversion/so-vits-svc/example_config.json"
SO_VITS_SVC_MODEL_CHECK_MAYBE_DEFAULT_TEST_WAV_PATH="/models/voice_conversion/so-vits-svc/example.wav"
#SO_VITS_SVC_MODEL_CHECK_COMMAND="/common/so-vits-svc-code/install/python/bin/python3.10 -m so_vits_svc_fork.fakeyou_infer"
#SO_VITS_SVC_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate"
# ================================= #
# #
@@ -18,9 +18,15 @@ REDIS_FOR_KEEPALIVE_URL="redis://localhost/0"
# should rename my folder
# abs path to the folder that contains the inference code need to get that container in the repo need basepath? relative to everyones env
VALL_E_X_INFERENCE_ROOT_DIRECTORY="%storyteller_ml%/tts/VALL-E-X"
VALL_E_X_INFERENCE_COMMAND="python3 main.py"
VALL_E_X_INFERENCE_MAYBE_VENV_COMMAND="source .env/bin/activate"
VALL_E_X_INFERENCE_MAYBE_DOCKER_IMAGE="" # fill this out for dev?
#VALL_E_X_INFERENCE_ROOT_DIRECTORY = "/home/tensor/code/storyteller/storyteller-ml/tts/VALL-E-X"
VALL_E_X_INFERENCE_COMMAND = "python3 main.py"
VALL_E_X_INFERENCE_MAYBE_VENV_COMMAND = "source .venv/bin/activate"
# VALL_E_X_INFERENCE_MAYBE_DOCKER_IMAGE = "" # fill this out for dev?
#VALL_E_X_INFERENCE_ROOT_DIRECTORY="%storyteller_ml%/tts/VALL-E-X"
#VALL_E_X_INFERENCE_COMMAND="python3 main.py"
#VALL_E_X_INFERENCE_MAYBE_VENV_COMMAND="source .env/bin/activate"
#VALL_E_X_INFERENCE_MAYBE_DOCKER_IMAGE="" # fill this out for dev?
# ================================= #
# #
@@ -60,10 +66,9 @@ FFMPEG_LOGO_LOGO_PATH="%storyteller_rust%/includes/container_includes/image_asse
HUBERT_PATH="/tmp/hubert/checkpoint_best_legacy_500.pt"
SO_VITS_SVC_INFERENCE_ROOT_DIRECTORY="/models/voice_conversion/so-vits-svc/src" #NB: so-vits-svc-paul-2
SO_VITS_SVC_INFERENCE_MAYBE_DOCKER_IMAGE="247952d65c82" # so-vits-svc-paul-2: --f0-method
SO_VITS_SVC_INFERENCE_MAYBE_DOCKER_IMAGE="1136fe8b50d5" # so-vits-svc-paul-2: --f0-method
SO_VITS_SVC_INFERENCE_COMMAND="/install/python/bin/python3.10 fakeyou_infer.py" # NB: so-vits-svc-paul-2
SO_VITS_SVC_INFERENCE_MAYBE_DEFAULT_CONFIG_PATH="/models/voice_conversion/so-vits-svc/example_config.json"
#SO_VITS_SVC_INFERENCE_MAYBE_VENV_COMMAND="source python/bin/activate"
# ================================= #
# #
@@ -20,9 +20,10 @@ pub enum ProcessSingleJobError {
/// This is any other kind of error.
/// It might be important, we just haven't special cased it yet.
Other(anyhow::Error),
/// We hit a feature or a path for this job that has not yet been implemented.
/// Permanently fail the job.
NotYetImplemented,
NotYetImplemented
}
impl ProcessSingleJobError {
@@ -207,14 +207,14 @@ pub async fn process_job(args: SadTalkerProcessJobArgs<'_>) -> Result<JobSuccess
let mut error = ProcessSingleJobError::Other(anyhow!("CommandExitStatus: {:?}", command_exit_status));
if let Ok(contents) = read_to_string(&stderr_output_file) {
warn!("Captured stderr output: {}", contents);
match categorize_error(&contents) {
Some(ProcessSingleJobError::FaceDetectionFailure) => {
warn!("Face not detected in source image");
error = ProcessSingleJobError::FaceDetectionFailure;
}
_ => {
warn!("Captured stderr output: {}", contents);
}
_ => {}
}
}
@@ -21,21 +21,21 @@ pub async fn process_single_tts_job(
) -> Result<JobSuccessResult, ProcessSingleJobError> {
// TODO: Move common checks for slurs, etc. here.
let raw_inference_text = job.maybe_raw_inference_text
.as_deref()
.ok_or(ProcessSingleJobError::Other(anyhow!("no inference text")))?;
match job.maybe_model_type {
Some(InferenceModelType::VallEX) => {
// Zero-shot TTS does not need a fine-tuned model token.
dispatch_zero_shot_model(
job_dependencies,
job,
&raw_inference_text
job
).await
},
Some(InferenceModelType::Tacotron2 | InferenceModelType::Vits) => {
// All other TTS types require a fine-tuned TTS database record.
let raw_inference_text = job.maybe_raw_inference_text
.as_deref()
.ok_or(ProcessSingleJobError::Other(anyhow!("no inference text")))?;
dispatch_fine_tuned_weights_model(
job_dependencies,
job,
@@ -54,8 +54,8 @@ pub async fn process_single_tts_job(
async fn dispatch_zero_shot_model(
job_dependencies: &JobDependencies,
job: &AvailableInferenceJob,
raw_inference_text: &str,
) -> Result<JobSuccessResult, ProcessSingleJobError> {
match job.maybe_model_type {
Some(InferenceModelType::VallEX) => {
vall_e_x::process_job::process_job(VALLEXProcessJobArgs {
@@ -1,5 +1,4 @@
pub mod model_downloaders;
pub mod vall_e_x_inference_command;
pub mod process_job;
pub mod validate_job;
pub mod download_embedding_file;
pub mod validate_job;
@@ -4,73 +4,77 @@ pub struct VallEXDownloaders {
pub vall_e_x: VallEXModelMapping,
pub whisper_lg: WhisperLargeModelMapping,
pub whisper_md: WhisperMediumModelMapping,
pub vocos_encodec_24khz: VocosEncodecModelMapping
pub vocos_encodec_24khz: VocosEncodecModelMapping,
pub vocos_config: VocosEncodecConfigMapping
}
crate::impl_model_downloader!(
VallEXModelMapping,
"Vall-e-x Model Mapping",
"VALL_E_X_MAPPING_BUCKET_PATH",
"/dependencies/zero_shot_tts/vall-e-x_1.0",
"/dependencies/zero_shot_tts/vall-e-x_1.0/vallex-checkpoint.pt",
"VALL_E_X_MAPPING_FILESYSTEM_PATH",
"/tmp/downloads/zero_shot_tts/vall-e-x_1.0"
);
crate::impl_model_downloader!(
"/tmp/downloads/zero_shot_tts/vall-e-x_1.0/vallex-checkpoint.pt"
);
crate::impl_model_downloader!(
WhisperLargeModelMapping,
"whisper Large Model Mapping",
"WHISPER_LARGE_MAPPING_BUCKET_PATH",
"dependencies/zero_shot_tts/vall-e-x_1.0/whisper-large",
"/dependencies/zero_shot_tts/vall-e-x_1.0/whisper-large/model.safetensors",
"WHISPER_LARGE_MAPPING_FILESYSTEM_PATH",
"/tmp/downloads/zero_shot_tts/vall-e-x_1.0/whisper-large"
);
crate::impl_model_downloader!(
"/tmp/downloads/zero_shot_tts/vall-e-x_1.0/whisper-large/model.safetensors"
);
crate::impl_model_downloader!(
WhisperMediumModelMapping,
"whisper Medium Model Mapping",
"WHISPER_MEDIUM_TENSOR_BUCKET_PATH",
"dependencies/zero_shot_tts/vall-e-x_1.0/whisper-medium",
"/dependencies/zero_shot_tts/vall-e-x_1.0/whisper/medium.pt",
"WHISPER_MEDIUM_TENSOR_FILESYSTEM_PATH",
"/tmp/downloads/zero_shot_tts/vall-e-x_1.0/whisper-medium"
);
crate::impl_model_downloader!(
"/tmp/downloads/zero_shot_tts/vall-e-x_1.0/whisper/medium.pt"
);
crate::impl_model_downloader!(
VocosEncodecModelMapping,
"Vocos Encodec Model Mapping",
"VOCOS_ENCODEC_TENSOR_BUCKET_PATH",
"dependencies/zero_shot_tts/vall-e-x_1.0/encodec_pytorch_model.bin",
"/dependencies/zero_shot_tts/vall-e-x_1.0/vocos-encodec/encodec_pytorch_model.bin",
"VOCOS_ENCODEC_TENSOR_FILESYSTEM_PATH",
"/tmp/downloads/zero_shot_tts/vall-e-x_1.0/encodec_pytorch_model.bin"
);
"/tmp/downloads/zero_shot_tts/vall-e-x_1.0/vocos-encodec/encodec_pytorch_model.bin"
);
crate::impl_model_downloader!(
VocosEncodecConfigMapping,
"Vocos Config Mapping",
"VOCOS_ENCODEC_CONFIG_BUCKET_PATH",
"/dependencies/zero_shot_tts/vall-e-x_1.0/vocos-encodec/config.yaml",
"VOCOS_ENCODEC_CONFIG_FILESYSTEM_PATH",
"/tmp/downloads/zero_shot_tts/vall-e-x_1.0/vocos-encodec/config.yaml"
);
impl VallEXDownloaders {
pub fn build_all_from_env() -> Self {
Self {
vall_e_x: VallEXModelMapping::from_env(),
whisper_lg: WhisperLargeModelMapping::from_env(),
whisper_md: WhisperMediumModelMapping::from_env(),
vocos_encodec_24khz: VocosEncodecModelMapping::from_env()
}
Self {
vall_e_x: VallEXModelMapping::from_env(),
whisper_lg: WhisperLargeModelMapping::from_env(),
whisper_md: WhisperMediumModelMapping::from_env(),
vocos_encodec_24khz: VocosEncodecModelMapping::from_env(),
vocos_config: VocosEncodecConfigMapping::from_env(),
}
}
pub fn all_downloaders(&self) -> Vec<&dyn ModelDownloader> {
vec![
&self.vall_e_x,
&self.whisper_lg,
&self.whisper_md,
&self.vocos_encodec_24khz,
]
vec![&self.vall_e_x, &self.whisper_lg, &self.whisper_md, &self.vocos_encodec_24khz,&self.vocos_config]
}
}
}
mod test {
mod test {
// use std::path::Path;
// use crate::job::job_types::lipsync::sad_talker::model_downloaders::{SadTalkerModelFirstMapping, SadTalkerModelSecondMapping};
// use crate::util::model_downloader::ModelDownloader;
// #[test]
// fn test_downloader_model_first_mapping() {
// let model = SadTalkerModelFirstMapping::default();
@@ -78,7 +82,7 @@ impl VallEXDownloaders {
// assert_eq!(model.get_cloud_bucket_path(), "/animation_sadtalker/model/mapping_00109-model.pth.tar");
// assert_eq!(model.get_filesystem_path(), Path::new("/tmp/downloads/sadtalker/mapping_00109-model.pth.tar"));
// }
// #[test]
// fn test_downloader_model_second_mapping() {
// let model = SadTalkerModelSecondMapping::default();
@@ -87,5 +91,4 @@ impl VallEXDownloaders {
// assert_eq!(model.get_filesystem_path(), Path::new("/tmp/downloads/sadtalker/mapping_00229-model.pth.tar"));
// }
}
}
@@ -1,70 +1,582 @@
use anyhow::anyhow;
use log::{error, info};
use buckets::public::media_files::original_file::MediaFileBucketPath;
use cloud_storage::bucket_client::BucketClient;
use enums::common::visibility::Visibility;
use hashing::sha256::sha256_hash_file::sha256_hash_file;
use filesys::file_size::file_size;
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
use log::{ error, info, warn };
use mysql_queries::queries::media_files::get_media_file::MediaFile;
use mysql_queries::queries::media_files::insert_media_file_from_zero_shot_tts::InsertArgs;
use mysql_queries::queries::media_files::insert_media_file_from_zero_shot_tts::insert_media_file_from_zero_shot;
use mysql_queries::queries::voice_designer::datasets::get_dataset::ZsDataset;
use mysql_queries::queries::voice_designer::datasets::get_dataset::get_dataset_by_token;
use mysql_queries::queries::voice_designer::datasets::list_datasets::DatasetRecordForList;
use mysql_queries::queries::voice_designer::voice_samples::list_dataset_samples_for_dataset_token::DatasetSampleRecordForList;
use mysql_queries::queries::voice_designer::voice_samples::get_dataset_sample;
use mysql_queries::queries::voice_designer::voice_samples::get_dataset_sample::get_dataset_sample_by_token;
use mysql_queries::queries::voice_designer::voice_samples::list_dataset_samples_for_dataset_token::list_dataset_samples_for_dataset_token;
use mysql_queries::queries::voice_designer::voices::create_voice;
use mysql_queries::queries::voice_designer::voices::create_voice::create_voice;
use mysql_queries::queries::voice_designer::voices::create_voice::CreateVoiceArgs;
use mysql_queries::queries::voice_designer::voices::get_voice::get_voice_by_token;
use serde::de::value;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use std::path::PathBuf;
use std::time::Instant;
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
use crate::job;
use crate::job::job_loop::job_success_result::JobSuccessResult;
use crate::job::job_loop::job_success_result::ResultEntity;
use crate::job::job_loop::process_single_job_error::ProcessSingleJobError;
use crate::job::job_types::tts::vall_e_x::validate_job::validate_job;
use crate::job::job_types::tts::vall_e_x::vall_e_x_inference_command::CreateVoiceInferenceArgs;
use crate::job_dependencies::JobDependencies;
use crate::job::job_types::tts::vall_e_x::vall_e_x_inference_command::InferenceArgs;
use cloud_storage::bucket_path_unifier::BucketPathUnifier;
use super::validate_job::JobType;
// Clearify what this is for ?
const BUCKET_FILE_PREFIX: &str = "fakeyou_";
const BUCKET_FILE_EXTENSION: &str = ".wav";
const MIME_TYPE: &str = "audio/wav";
pub struct VoiceFile {
pub filesystem_path: PathBuf,
}
const BUCKET_FILE_PREFIX_CREATE: &str = "fakeyou_";
const BUCKET_FILE_EXTENSION_CREATE: &str = ".bin";
const MIME_TYPE_CREATE: &str = "application/x-binary";
pub struct AudioFile {
pub filesystem_path: PathBuf,
}
pub async fn download_voice_embedding_from_hash(
bucket_hash: &str,
name: &str,
private_bucket_client: &BucketClient,
path: &PathBuf
) -> Result<VoiceFile, ProcessSingleJobError> {
let unifer = BucketPathUnifier::default_paths();
let object_path = unifer.zero_shot_tts_speaker_encoding(bucket_hash, 0);
let mut path = path.clone();
let file_name = format!("{}", name);
path.push(&file_name);
let result = private_bucket_client.download_file_to_disk(object_path, &path).await;
let voice_file = VoiceFile {
filesystem_path: PathBuf::from(&path.clone()),
};
Ok(voice_file)
}
pub async fn download_audio_from_hash(
bucket_hash: &str,
name: &str,
private_bucket_client: &BucketClient,
path: &PathBuf
) -> Result<AudioFile, ProcessSingleJobError> {
let unifer = BucketPathUnifier::default_paths();
let object_path = unifer.zero_shot_tts_speaker_encoding(bucket_hash, 0);
let mut path = path.clone();
let file_name = format!("{}", name);
path.push(&file_name);
let result = private_bucket_client.download_file_to_disk(object_path, &path).await;
let audio_file = AudioFile {
filesystem_path: PathBuf::from(&path.clone()),
};
Ok(audio_file)
}
// This will download everything get into the root host OS then ... will invoke inference using the pathes from the files invoked
pub struct VALLEXProcessJobArgs<'a> {
pub job_dependencies: &'a JobDependencies,
pub job: &'a AvailableInferenceJob,
pub job_dependencies: &'a JobDependencies,
pub job: &'a AvailableInferenceJob,
}
pub async fn process_job(args: VALLEXProcessJobArgs<'_>) -> Result<JobSuccessResult, ProcessSingleJobError> {
pub async fn process_create_voice(
args: VALLEXProcessJobArgs<'_>,
dataset_token: String
) -> Result<JobSuccessResult, ProcessSingleJobError> {
let deps = args.job_dependencies;
let job = args.job;
let mysql_pool = &deps.mysql_pool;
// get job args and dependencies and execute them with the inputs
let job = args.job;
let deps = args.job_dependencies;
// get some globals
let mut job_progress_reporter = deps.job_progress_reporter
.new_generic_inference(job.inference_job_token.as_str())
.map_err(|e| ProcessSingleJobError::Other(anyhow!(e)))?;
let job_progress_reporter = args.job_dependencies
.job_progress_reporter
.new_generic_inference(job.inference_job_token.as_str())
.map_err(|e| ProcessSingleJobError::Other(anyhow!(e)))?;
info!("token! {}", dataset_token);
let voice_dataset_token = tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken(dataset_token);
// validate the inputs on the job shouldn't this happen at json
//let job_args = validate_job(job)?;
let creator_ip_address = &job.creator_ip_address;
let creator_user_token:UserToken;
// Need to download the models
info!("Download models (if not present)...");
match &job.maybe_creator_user_token {
Some(token) => {
creator_user_token = UserToken::new_from_str(token);
},
None => {
return Err(ProcessSingleJobError::InvalidJob(anyhow!("Missing Creator User Token")));
}
}
for downloader in deps.job_type_details.vall_e_x.downloaders.all_downloaders() {
let result = downloader.download_if_not_on_filesystem(
&args.job_dependencies.private_bucket_client,
&args.job_dependencies.fs.scoped_temp_dir_creator_for_downloads,
// STEP 1. SETUP A TEMP DIRECTORY
let work_temp_dir = format!("/tmp/temp_zeroshot_create_voice_{}", job.id.0);
// NB: TempDir exists until it goes out of scope, at which point it should delete from filesystem.
let work_temp_dir = args.job_dependencies.fs.scoped_temp_dir_creator_for_work
.new_tempdir(&work_temp_dir)
.map_err(|e| ProcessSingleJobError::from_io_error(e))?;
let workdir = work_temp_dir.path().to_path_buf();
// STEP 2. Get dataset for the title for the voice
let voice_dataset = get_dataset_by_token(&voice_dataset_token, false, &mysql_pool).await;
let single_dataset: ZsDataset;
match voice_dataset {
Ok(val) => {
match val {
Some(val) => {
single_dataset = val;
}
None => {
return Err(ProcessSingleJobError::InvalidJob(anyhow!("Missing Dataset")));
}
}
}
Err(e) => {
return Err(ProcessSingleJobError::from_anyhow_error(e));
}
}
info!("Title:{} Token:{}", &single_dataset.title, &single_dataset.token);
// STEP 3. Download dataset each audio file
let result = list_dataset_samples_for_dataset_token(
&voice_dataset_token,
false,
&mysql_pool
).await;
let dataset: Vec<DatasetSampleRecordForList>;
if let Err(e) = result {
error!("could not download: {:?}", e);
return Err(ProcessSingleJobError::from_anyhow_error(e))
match result {
Ok(val) => {
dataset = val;
}
Err(e) => {
return Err(ProcessSingleJobError::from_anyhow_error(e));
}
}
}
// Download embeddings files
// rinr inference
info!("Dataset length info: {}", dataset.len());
// ==================== TEMP DIR ==================== //
// create a temp dir
let mut downloaded_dataset: Vec<PathBuf> = Vec::new();
// run inference
let temp_extension = String::from(".bin");
let temp_prefix:String;
// upload audio to bucket
if deps.container.is_on_prem == false {
temp_prefix = String::from("dev_zs_"); // this is for seed in local dev to download the samples
} else {
temp_prefix = String::from(BUCKET_FILE_PREFIX_CREATE);
}
// deletetemp dir files
for (index, record) in dataset.iter().enumerate() {
//https://storage.googleapis.com/dev-vocodes-public/media/5/3/3/w/8/533w8zs0fy11nv7gkcna7p7vt03h8nda/dev_zs_533w8zs0fy11nv7gkcna7p7vt03h8nda.bin <-- where the file actually is
let prefix: Option<&str> = Some(&temp_prefix); // record.maybe_public_bucket_prefix.as_ref().map(|s| s.as_str());
let extension: Option<&str> = Some(&temp_extension);//record.maybe_public_bucket_extension
// .as_ref()
// .map(|s| s.as_str());
// naming
//[2023-10-23T01:26:46Z INFO inference_job::job::job_types::tts::vall_e_x::process_job] Upload Bucket Path: /media/9/j/6/g/c/9j6gcd3ngb70ybpsq1rv4tw3gk97ds3t/fakeyou_9j6gcd3ngb70ybpsq1rv4tw3gk97ds3t.npz
//[2023-10-23T01:26:46Z INFO inference_job::job::job_types::tts::vall_e_x::process_job] Upload File Path: /tmp/temp_zeroshot_create_voice_11.1BLk16qTwhuo/temp.npz
// save result ?
info!(
"Record=> hash:{} prefix:{:?} extension:{:?}",
record.public_bucket_directory_hash,
prefix,
extension
);
Err(ProcessSingleJobError::InvalidJob(anyhow!("this job flow is not yet implemented")))
let audio_media_file = MediaFileBucketPath::from_object_hash(
&record.public_bucket_directory_hash,
prefix,
extension
);
info!(
"Download using audio_media_file_path:{}",
audio_media_file.to_full_object_pathbuf().to_string_lossy()
);
let file_name_wav = format!("{}.wav", index);
let mut file_path = PathBuf::new();
file_path.push(workdir.clone());
file_path.push(file_path.clone());
file_path.push(file_name_wav);
// TODO: we might want to catch the error and not include the pathes into download dataset?
let _ = deps.public_bucket_client.download_file_to_disk(
audio_media_file.to_full_object_pathbuf(),
&file_path
).await;
info!("FilePath to clone voice: {}", file_path.to_string_lossy());
downloaded_dataset.push(file_path.clone());
}
info!("Dataset Length {}", downloaded_dataset.len());
// STEP 4 Download the models
info!("Download models (if not present)...");
for downloader in deps.job_type_details.vall_e_x.downloaders.all_downloaders() {
let result = downloader.download_if_not_on_filesystem(
&args.job_dependencies.private_bucket_client,
&args.job_dependencies.fs.scoped_temp_dir_creator_for_downloads
).await;
if let Err(e) = result {
error!("could not download: {:?}", e);
return Err(ProcessSingleJobError::from_anyhow_error(e));
}
}
job_progress_reporter
.log_status("running inference")
.map_err(|e| ProcessSingleJobError::Other(e))?;
let inference_start_time = Instant::now();
// Command line arg for a list of pathes to insert the container
let audio_files = join_paths(downloaded_dataset);
// Name of the output file
let output_file_name = String::from("temp"); // don't use the extension... for the inference since the container will add the extension.
// Run Inference
let command_exit_status =
args.job_dependencies.job_type_details.vall_e_x.create_embedding_command.execute_inference(
job::job_types::tts::vall_e_x::vall_e_x_inference_command::CreateVoiceInferenceArgs {
output_embedding_path: &workdir,
output_embedding_name: output_file_name.clone(),
audio_files: audio_files,
stderr_output_file: String::from("zero_shot_create_voice.txt"),
}
);
let inference_duration = Instant::now().duration_since(inference_start_time);
// STEP 4. Download dataset each audio file
info!("Uploading Media ...");
let result_bucket_location: MediaFileBucketPath = MediaFileBucketPath::generate_new(
Some(BUCKET_FILE_PREFIX_CREATE),
Some(BUCKET_FILE_EXTENSION_CREATE)
);
let result_bucket_object_pathbuf = result_bucket_location.to_full_object_pathbuf();
// Get Finished File
let mut finished_file = work_temp_dir.path().to_path_buf();
//let mut finished_file = workdir;
let output_bucket_file_name = String::from("temp.npz"); // use extension for bucket upload.
finished_file.push(&output_bucket_file_name);
info!("Upload Bucket Path: {}",result_bucket_object_pathbuf.to_path_buf().to_string_lossy());
info!("Upload File Path: {}",finished_file.to_path_buf().to_string_lossy());
args.job_dependencies.private_bucket_client
.upload_filename_with_content_type(
&result_bucket_object_pathbuf,
&finished_file,
&MIME_TYPE_CREATE
)
.await // TODO: We should check the mimetype to make sure bad payloads can't get uploaded
.map_err(|e| ProcessSingleJobError::Other(e))?;
// CLEARIFY! these items
// 1.Should this be object hash?
let bucket_hash = result_bucket_location.get_object_hash().clone();
// 2.As well as this what should the voice name be?
let voice_name = single_dataset.title;
// insert record
let voice_token = create_voice(CreateVoiceArgs {
dataset_token: &voice_dataset_token,
model_category: "vc",
model_type: "vall-e-x",
model_version: 0,
model_encoding_type: "encodec",
voice_title: &voice_name,
bucket_hash: bucket_hash,
maybe_creator_user_token: Some(&creator_user_token),
creator_ip_address: &creator_ip_address,
creator_set_visibility: Visibility::Public,
mysql_pool,
}).await;
let media_file_token = MediaFileToken::generate();
match voice_token {
Ok(_value) => {
Ok(JobSuccessResult {
maybe_result_entity: Some(ResultEntity {
entity_type: InferenceResultType::MediaFile,
entity_token: media_file_token.to_string(),
}),
inference_duration,
})
}
Err(e) => { Err(ProcessSingleJobError::Other(e)) }
}
}
pub async fn process_inference_voice(
args: VALLEXProcessJobArgs<'_>,
voice_token: String
) -> Result<JobSuccessResult, ProcessSingleJobError> {
let deps = args.job_dependencies;
let job = args.job;
let mysql_pool = &deps.mysql_pool;
// get some globals
let mut job_progress_reporter = deps.job_progress_reporter
.new_generic_inference(job.inference_job_token.as_str())
.map_err(|e| ProcessSingleJobError::Other(anyhow!(e)))?;
#[cfg(test)]
mod test {
#[test]
fn values() {
print!("{}","Hello!");
// get job args
let text = match job.maybe_raw_inference_text.clone() {
Some(value) => { value }
None => {
return Err(ProcessSingleJobError::InvalidJob(anyhow!("Missing Text for Inference")));
}
};
let voice_token = tokens::tokens::zs_voices::ZsVoiceToken(voice_token);
// Get voice bucket hash - from voice token
let voice_lookup_result = get_voice_by_token(&voice_token, false, &mysql_pool).await;
let voice = match voice_lookup_result {
Ok(Some(voice)) => voice,
Ok(None) => {
warn!("Voice not found: {:?}", voice_token);
return Err(ProcessSingleJobError::Other(anyhow!("Voice not found: {:?}", voice_token)));
}
Err(err) => {
warn!("Error looking up voice: {:?}", err);
return Err(ProcessSingleJobError::Other(anyhow!("Error looking up voice: {:?}", err)));
}
};
// Need to download the models
info!("Download models (if not present)...");
for downloader in deps.job_type_details.vall_e_x.downloaders.all_downloaders() {
let result = downloader.download_if_not_on_filesystem(
&args.job_dependencies.private_bucket_client,
&args.job_dependencies.fs.scoped_temp_dir_creator_for_downloads
).await;
if let Err(e) = result {
error!("could not download: {:?}", e);
return Err(ProcessSingleJobError::from_anyhow_error(e));
}
}
}
// Might not need this for inference.
// let creator_user_token:UserToken;
// match &job.maybe_creator_user_token {
// Some(token) => {
// creator_user_token = UserToken::new_from_str(token);
// },
// None => {
// return Err(ProcessSingleJobError::InvalidJob(anyhow!("Missing Creator User Token")));
// }
// }
// run inference
let work_temp_dir = format!("/tmp/temp_zeroshot_inference_{}", job.id.0);
// NB: TempDir exists until it goes out of scope, at which point it should delete from filesystem.
let work_temp_dir = args.job_dependencies.fs.scoped_temp_dir_creator_for_work
.new_tempdir(&work_temp_dir)
.map_err(|e| ProcessSingleJobError::from_io_error(e))?;
let workdir = work_temp_dir.path().to_path_buf();
let file_name = format!("{}_weights.npz", &voice.title);
// USE THIS LATER SINCE it requires specific typing ...
let voiceFile = download_voice_embedding_from_hash(
&voice.bucket_hash,
&file_name,
&deps.private_bucket_client,
&workdir
).await?;
println!("voicefile path! {}", voiceFile.filesystem_path.to_string_lossy());
// Download embeddings file using embedding token
// Create a temp dir to download things to
job_progress_reporter
.log_status("running inference")
.map_err(|e| ProcessSingleJobError::Other(e))?;
let inference_start_time = Instant::now();
let output_file_name = String::from("output.wav");
// Run Inference
let command_exit_status =
args.job_dependencies.job_type_details.vall_e_x.inference_command.execute_inference(
InferenceArgs {
input_embedding_path: &workdir,
input_embedding_name: file_name,
input_text: String::from(text), // text
output_file_name: output_file_name.clone(), // output file name in the output folder
stderr_output_file: String::from("zero_shot_inference.txt"),
}
);
let inference_duration = Instant::now().duration_since(inference_start_time);
// upload audio to bucket
info!("Uploading media ...");
let result_bucket_location = MediaFileBucketPath::generate_new(
Some(BUCKET_FILE_PREFIX),
Some(BUCKET_FILE_EXTENSION)
);
let result_bucket_object_pathbuf = result_bucket_location.to_full_object_pathbuf();
// Finished file path
let mut finished_file = work_temp_dir.path().to_path_buf();
finished_file.push(&output_file_name);
info!("Upload Bucket Path: {}",result_bucket_object_pathbuf.to_path_buf().to_string_lossy());
info!("Upload File Path: {}",finished_file.to_path_buf().to_string_lossy());
args.job_dependencies.public_bucket_client
.upload_filename_with_content_type(
&result_bucket_object_pathbuf,
&finished_file,
&MIME_TYPE
)
.await
.map_err(|e| ProcessSingleJobError::Other(e))?;
// ==================== UPLOAD AUDIO TO BUCKET ====================
info!("Calculating sha256...");
let file_checksum = sha256_hash_file(&finished_file).map_err(|err| {
ProcessSingleJobError::Other(anyhow!("Error hashing file: {:?}", err))
})?;
let file_size_bytes = file_size(&finished_file).map_err(|err|
ProcessSingleJobError::Other(err)
)?;
job_progress_reporter.log_status("done").map_err(|e| ProcessSingleJobError::Other(e))?;
// insert into db the record
let (media_file_token, id) = insert_media_file_from_zero_shot(InsertArgs {
pool: &args.job_dependencies.mysql_pool,
job: &job,
maybe_mime_type: Some(&MIME_TYPE),
file_size_bytes,
sha256_checksum: &file_checksum,
public_bucket_directory_hash: result_bucket_location.get_object_hash(),
maybe_public_bucket_prefix: Some(BUCKET_FILE_PREFIX),
maybe_public_bucket_extension: Some(BUCKET_FILE_EXTENSION),
is_on_prem: args.job_dependencies.container.is_on_prem,
worker_hostname: &args.job_dependencies.container.hostname,
worker_cluster: &args.job_dependencies.container.cluster_name,
}).await.map_err(|e| ProcessSingleJobError::Other(e))?;
info!(
"Job {:?} complete success! Downloaded, ran inference, and uploaded. Saved model record: {}, Result Token: {}",
job.id,
id,
&media_file_token
);
Ok(JobSuccessResult {
maybe_result_entity: Some(ResultEntity {
entity_type: InferenceResultType::MediaFile,
entity_token: media_file_token.to_string(),
}),
inference_duration,
})
}
// query using the token then grab the bucket hash
pub async fn process_job(
args: VALLEXProcessJobArgs<'_>
) -> Result<JobSuccessResult, ProcessSingleJobError> {
let job = args.job;
// get args token
let jobArgs = validate_job(&job)?; // bubbles error up
match jobArgs.job_type {
JobType::Create => {
if let Some(voice_dataset_token) = jobArgs.voice_dataset_token {
process_create_voice(args, voice_dataset_token).await
} else {
Err(ProcessSingleJobError::Other(anyhow!("Missing Dataset Token?")))
}
}
JobType::Inference => {
if let Some(voice_token) = jobArgs.voice_token {
process_inference_voice(args, voice_token).await
} else {
Err(ProcessSingleJobError::Other(anyhow!("Missing Voice Token?")))
}
}
}
}
fn join_paths(paths: Vec<PathBuf>) -> String {
paths
.into_iter()
.map(|p| format!("\"{}\"", p.display()))
.collect::<Vec<String>>()
.join(" ")
}
mod tests {
use super::*;
#[test]
fn test_path_build() {
let paths = vec![
PathBuf::from("/home/tensor/code/TTSDockerContainer/Vall-E-mount/input/20.wav"),
PathBuf::from("/home/tensor/code/TTSDockerContainer/Vall-E-mount/input/21.wav")
];
let value = join_paths(paths);
let expected =
"\"/home/tensor/code/TTSDockerContainer/Vall-E-mount/input/20.wav\" \"/home/tensor/code/TTSDockerContainer/Vall-E-mount/input/21.wav\"";
assert_eq!(value, expected);
}
}
@@ -1,9 +1,82 @@
use anyhow::anyhow;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{
InferenceCategoryAbbreviated,
PolymorphicInferenceArgs,
};
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
use crate::job::job_loop::process_single_job_error::ProcessSingleJobError;
use crate::job::{job_loop::process_single_job_error::ProcessSingleJobError, job_types::lipsync::sad_talker::validate_job::JobArgs};
pub enum JobType {
Create,
Inference
}
// determines the job type voice inference or voice creation
pub struct JobArgs {
pub voice_token: Option<String>, // infers
pub voice_dataset_token: Option<String>, // creates
pub job_type: JobType
}
pub fn validate_job(job: &AvailableInferenceJob) -> Result<JobArgs, ProcessSingleJobError> {
Err(ProcessSingleJobError::InvalidJob(anyhow!("job not yet written")))
let inference_category = job.maybe_inference_args
.as_ref()
.map(|args| args.inference_category)
.flatten();
match inference_category {
Some(InferenceCategoryAbbreviated::TextToSpeech) => {}
Some(category) => {
return Err(
ProcessSingleJobError::from_anyhow_error(
anyhow!("wrong inference category for job: {:?}", category)
)
);
}
None => {
return Err(
ProcessSingleJobError::from_anyhow_error(anyhow!("no inference category for job!"))
);
}
}
let inference_args = job.maybe_inference_args
.as_ref()
.map(|args| args.args.as_ref())
.flatten();
let inference_args = match inference_args {
Some(args) => args,
None => {
return Err(
ProcessSingleJobError::from_anyhow_error(anyhow!("no inference args for job!"))
);
}
};
let ttsArgs = match inference_args {
PolymorphicInferenceArgs::Tts(inference_args) => inference_args,
_ => {
return Err(
ProcessSingleJobError::from_anyhow_error(anyhow!("wrong inner args for job!"))
);
}
};
if let Some(voice_token) = &ttsArgs.voice_token {
return Ok::<JobArgs, ProcessSingleJobError>(JobArgs {
voice_token: Some(voice_token.clone()),
voice_dataset_token: None,
job_type: JobType::Inference
});
}
if let Some(dataset_token) = &ttsArgs.dataset_token {
return Ok::<JobArgs,ProcessSingleJobError>(JobArgs {
voice_token: None,
voice_dataset_token: Some(dataset_token.clone()),
job_type: JobType::Create
});
}
return Err(ProcessSingleJobError::from_anyhow_error(anyhow!("Missing Voice Token!")));
}
@@ -2,71 +2,74 @@ use std::collections::HashSet;
use std::env;
use std::ffi::OsString;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::path::{ Path, PathBuf };
use std::time::Duration;
use anyhow::anyhow;
use log::info;
use once_cell::sync::Lazy;
use subprocess::{Popen, PopenConfig, Redirection};
use subprocess::{ Popen, PopenConfig, Redirection };
use container_common::anyhow_result::AnyhowResult;
use filesys::path_to_string::path_to_string;
use subprocess_common::docker_options::{DockerFilesystemMount, DockerGpu, DockerOptions};
use subprocess_common::docker_options::{ DockerFilesystemMount, DockerGpu, DockerOptions };
use crate::job::job_loop::command_exit_status::CommandExitStatus;
#[derive(Clone)]
pub enum ExecutableOrCommand {
/// Eg. `inference.py`
Executable(PathBuf),
/// Eg. `python3 inference.py`
Command(String),
/// Eg. `inference.py`
Executable(PathBuf),
/// Eg. `python3 inference.py`
Command(String),
}
// These environment vars are not copied over to the subprocess
// TODO/FIXME(bt, 2023-05-28): This is horrific security!
static IGNORED_ENVIRONMENT_VARS : Lazy<HashSet<String>> = Lazy::new(|| {
let env_var_names= [
"MYSQL_URL",
"ACCESS_KEY",
"SECRET_KEY",
"NEWRELIC_API_KEY",
];
static IGNORED_ENVIRONMENT_VARS: Lazy<HashSet<String>> = Lazy::new(|| {
let env_var_names = ["MYSQL_URL", "ACCESS_KEY", "SECRET_KEY", "NEWRELIC_API_KEY"];
env_var_names.iter()
.map(|value| value.to_string())
.collect::<HashSet<String>>()
env_var_names
.iter()
.map(|value| value.to_string())
.collect::<HashSet<String>>()
});
// The inference command assumes everything has been setup to run inference
// so we have all the files downloaded for the inputs.
pub struct InferenceArgs<P: AsRef<Path>> {
/// --driven_audio: path to the input audio
pub input_embedding: P, // name of the embedding.npz in the prompts folder?
pub input_text: P, // name of the text
/// --result_file: path to final file output
pub output_file_name: P, // output file name in the output folder
pub stderr_output_file: P
}
pub input_embedding_path: P, // name of the embedding.npz in the work dir
pub input_embedding_name: String,
pub input_text: String, // name of the text
pub output_file_name: String, // output file name in the output folder
pub stderr_output_file: String,
}
pub struct CreateVoiceInferenceArgs<P: AsRef<Path>> {
pub output_embedding_path: P,
pub output_embedding_name: String,
pub audio_files: String,
pub stderr_output_file: String
}
#[derive(Clone)]
pub struct VallEXInferenceCommand {
/// 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>,
/// Optional default config file to use
maybe_default_config_path: Option<PathBuf>,
/// 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>,
/// Inference arg.
/// --checkpoint_dir: optional location for checkpoints directory
pub alternate_checkpoint_dir: Option<PathBuf>
pub struct VallEXInferenceCommand {
/// 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>,
/// Optional default config file to use
maybe_default_config_path: Option<PathBuf>,
/// 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>,
/// Inference arg.
/// --checkpoint_dir: optional location for checkpoints directory
pub alternate_checkpoint_dir: Option<PathBuf>,
}
impl VallEXInferenceCommand {
@@ -78,292 +81,469 @@ impl VallEXInferenceCommand {
maybe_docker_options: Option<DockerOptions>,
maybe_execution_timeout: Option<Duration>,
alternate_checkpoint_dir: Option<PathBuf>
) -> Self {
) -> Self {
Self {
root_code_directory: root_code_directory.as_ref().to_path_buf(),
executable_or_command,
maybe_virtual_env_activation_command: maybe_virtual_env_activation_command.map(|s| s.to_string()),
maybe_default_config_path: maybe_default_config_path.map(|p| p.as_ref().to_path_buf()),
maybe_docker_options,
maybe_execution_timeout,
alternate_checkpoint_dir,
root_code_directory: root_code_directory.as_ref().to_path_buf(),
executable_or_command,
maybe_virtual_env_activation_command: maybe_virtual_env_activation_command.map(|s|
s.to_string()
),
maybe_default_config_path: maybe_default_config_path.map(|p| p.as_ref().to_path_buf()),
maybe_docker_options,
maybe_execution_timeout,
alternate_checkpoint_dir,
}
}
}
pub fn from_env() -> AnyhowResult<Self> {
pub fn from_env() -> AnyhowResult<Self> {
let root_code_directory = easyenv::get_env_pathbuf_required(
"VALL_E_X_INFERENCE_ROOT_DIRECTORY")?;
let maybe_inference_command = easyenv::get_env_string_optional(
"VALL_E_X_INFERENCE_COMMAND");
"VALL_E_X_INFERENCE_ROOT_DIRECTORY"
)?;
// EVENTUALLY should remove the check
let maybe_inference_command = easyenv::get_env_string_optional(
"VALL_E_X_INFERENCE_COMMAND"
);
// EVENTUALLY should remove the check
// Optional, eg. `./infer.py`. Typically we'll use the command form instead.
let maybe_inference_executable = easyenv::get_env_pathbuf_optional(
"VALL_E_X_INFERENCE_EXECUTABLE");
"VALL_E_X_INFERENCE_EXECUTABLE"
);
let executable_or_command = match maybe_inference_command {
Some(command) => ExecutableOrCommand::Command(command),
None => match maybe_inference_executable {
Some(executable) => ExecutableOrCommand::Executable(executable),
None => return Err(anyhow!("neither command nor executable passed")),
},
Some(command) => ExecutableOrCommand::Command(command),
None =>
match maybe_inference_executable {
Some(executable) => ExecutableOrCommand::Executable(executable),
None => {
return Err(anyhow!("neither command nor executable passed"));
}
}
};
let maybe_virtual_env_activation_command = easyenv::get_env_string_optional(
"VALL_E_X_INFERENCE_MAYBE_VENV_COMMAND");
"VALL_E_X_INFERENCE_MAYBE_VENV_COMMAND"
);
let maybe_default_config_path = easyenv::get_env_pathbuf_optional(
"VALL_E_X_INFERENCE_MAYBE_DEFAULT_CONFIG_PATH");
let maybe_execution_timeout =
easyenv::get_env_duration_seconds_optional("TIMEOUT_SECONDS");
"VALL_E_X_INFERENCE_MAYBE_DEFAULT_CONFIG_PATH"
);
let maybe_execution_timeout = easyenv::get_env_duration_seconds_optional("TIMEOUT_SECONDS");
// Probably for local
let maybe_docker_options = easyenv::get_env_string_optional(
"VALL_E_X_INFERENCE_MAYBE_DOCKER_IMAGE")
let maybe_docker_options = easyenv
::get_env_string_optional("VALL_E_X_INFERENCE_MAYBE_DOCKER_IMAGE")
.map(|image_name| {
DockerOptions {
image_name,
maybe_bind_mount: Some(DockerFilesystemMount::tmp_to_tmp()),
maybe_environment_variables: None,
maybe_gpu: Some(DockerGpu::All),
}
DockerOptions {
image_name,
maybe_bind_mount: Some(DockerFilesystemMount::tmp_to_tmp()),
maybe_environment_variables: None,
maybe_gpu: Some(DockerGpu::All),
}
});
// Override for --checkpoint_dir at inference time
let alternate_checkpoint_dir = easyenv::get_env_pathbuf_optional(
"SAD_TALKER_ALTERNATE_CHECKPOINT_PATH");
Ok(Self {
root_code_directory,
executable_or_command,
maybe_virtual_env_activation_command,
maybe_default_config_path,
maybe_docker_options,
maybe_execution_timeout,
alternate_checkpoint_dir,
})
}
"VALL_E_X_ALTERNATE_CHECKPOINT_PATH"
);
pub fn execute_inference<P: AsRef<Path>>(
&self,
args: InferenceArgs<P>,
) -> CommandExitStatus {
Ok(Self {
root_code_directory,
executable_or_command,
maybe_virtual_env_activation_command,
maybe_default_config_path,
maybe_docker_options,
maybe_execution_timeout,
alternate_checkpoint_dir,
})
}
pub fn execute_inference<P: AsRef<Path>>(&self, args: InferenceArgs<P>) -> CommandExitStatus {
match self.do_execute_inference(args) {
Ok(exit_status) => exit_status,
Err(error) => CommandExitStatus::FailureWithReason { reason: format!("error: {:?}", error) },
Ok(exit_status) => exit_status,
Err(error) =>
CommandExitStatus::FailureWithReason { reason: format!("error: {:?}", error) },
}
}
}
fn do_execute_inference<P: AsRef<Path>>(
&self,
args: InferenceArgs<P>,
) -> AnyhowResult<CommandExitStatus> {
args: InferenceArgs<P>
) -> AnyhowResult<CommandExitStatus> {
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(" && ");
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(" ");
}
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(" ");
}
}
// ===== Begin Python Args =====
command.push_str(" -text ");
command.push_str(&path_to_string(args.input_text));
command.push_str(" -use_prompt_name ");
command.push_str(&path_to_string(args.input_embedding));
command.push_str(" -output_file_name ");
command.push_str(" --text ");
command.push_str(&format!("\"{}\"", &args.input_text));
command.push_str(" --prompt-name ");
command.push_str(&path_to_string(args.input_embedding_name));
command.push_str(" --prompt-path ");
command.push_str(&path_to_string(&args.input_embedding_path));
command.push_str(" --audio-name ");
command.push_str(&path_to_string(args.output_file_name));
command.push_str(" --audio-path ");
command.push_str(&path_to_string(&args.input_embedding_path));
command.push_str(" --mode ");
command.push_str(&path_to_string("0"));
// TODO improve and use a better model for premium users
command.push_str(" --whisper-model ");
command.push_str(&path_to_string(Path::new("medium")));
command.push_str(" --whisper-folder-path ");
command.push_str(
&path_to_string(Path::new("/tmp/downloads/zero_shot_tts/vall-e-x_1.0/whisper/"))
);
command.push_str(" --vocos-folder-path ");
command.push_str(
&path_to_string(Path::new("/tmp/downloads/zero_shot_tts/vall-e-x_1.0/vocos-encodec/"))
);
command.push_str(" --vallex-path ");
command.push_str(
&path_to_string(
Path::new("/tmp/downloads/zero_shot_tts/vall-e-x_1.0/vallex-checkpoint.pt")
)
);
command.push_str(" --tmp-work-dir ");
command.push_str(&path_to_string(Path::new("/tmp/downloads/zero_shot_tts/")));
// ===== End Python Args =====
if let Some(docker_options) = self.maybe_docker_options.as_ref() {
command = docker_options.to_command_string(&command);
command = docker_options.to_command_string(&command);
}
info!("Command: {:?}", command);
let command_parts = [
"bash",
"-c",
&command
];
let command_parts = ["bash", "-c", &command];
let mut env_vars = Vec::new();
// Copy all environment variables from the parent process.
// This is necessary to send all the kubernetes settings for Nvidia / CUDA.
for (env_key, env_value) in env::vars() {
if IGNORED_ENVIRONMENT_VARS.contains(&env_key) {
continue;
}
env_vars.push((
OsString::from(env_key),
OsString::from(env_value),
));
if IGNORED_ENVIRONMENT_VARS.contains(&env_key) {
continue;
}
env_vars.push((OsString::from(env_key), OsString::from(env_value)));
}
let mut config = PopenConfig::default();
info!("stderr will be written to file: {:?}", args.stderr_output_file.as_ref());
info!("stderr will be written to file: {:?}", args.stderr_output_file);
let stderr_file = File::create(&args.stderr_output_file)?;
config.stderr = Redirection::File(stderr_file);
if !env_vars.is_empty() {
config.env = Some(env_vars);
config.env = Some(env_vars);
}
let mut p = Popen::create(&command_parts, config)?;
info!("Subprocess PID: {:?}", p.pid());
match self.maybe_execution_timeout {
None => {
let exit_status = p.wait()?;
info!("Subprocess exit status: {:?}", exit_status);
Ok(CommandExitStatus::from_exit_status(exit_status))
}
Some(timeout) => {
info!("Executing with timeout: {:?}", &timeout);
let exit_status = p.wait_timeout(timeout.clone())?;
match exit_status {
None => {
// NB: If the program didn't successfully terminate, kill it.
info!("Subprocess didn't end after timeout: {:?}; terminating...", &timeout);
let _r = p.terminate()?;
Ok(CommandExitStatus::Timeout)
}
Some(exit_status) => {
info!("Subprocess timed wait exit status: {:?}", exit_status);
None => {
let exit_status = p.wait()?;
info!("Subprocess exit status: {:?}", exit_status);
Ok(CommandExitStatus::from_exit_status(exit_status))
}
}
}
Some(timeout) => {
info!("Executing with timeout: {:?}", &timeout);
let exit_status = p.wait_timeout(timeout.clone())?;
match exit_status {
None => {
// NB: If the program didn't successfully terminate, kill it.
info!(
"Subprocess didn't end after timeout: {:?}; terminating...",
&timeout
);
let _r = p.terminate()?;
Ok(CommandExitStatus::Timeout)
}
Some(exit_status) => {
info!("Subprocess timed wait exit status: {:?}", exit_status);
Ok(CommandExitStatus::from_exit_status(exit_status))
}
}
}
}
}
}
}
#[derive(Clone)]
pub struct VallEXCreateEmbeddingCommand {
/// 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>,
/// Optional default config file to use
maybe_default_config_path: Option<PathBuf>,
/// 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>,
/// Inference arg.
/// --checkpoint_dir: optional location for checkpoints directory
pub alternate_checkpoint_dir: Option<PathBuf>
pub struct VallEXCreateEmbeddingCommand {
/// 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>,
/// Optional default config file to use
maybe_default_config_path: Option<PathBuf>,
/// 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>,
/// Inference arg.
/// --checkpoint_dir: optional location for checkpoints directory
pub alternate_checkpoint_dir: Option<PathBuf>,
}
impl VallEXCreateEmbeddingCommand {
pub fn new<P: AsRef<Path>>(
root_code_directory: P,
executable_or_command: ExecutableOrCommand,
maybe_virtual_env_activation_command: Option<&str>,
maybe_default_config_path: Option<P>,
maybe_docker_options: Option<DockerOptions>,
maybe_execution_timeout: Option<Duration>,
alternate_checkpoint_dir: Option<PathBuf>
) -> Self {
Self {
root_code_directory: root_code_directory.as_ref().to_path_buf(),
executable_or_command,
maybe_virtual_env_activation_command: maybe_virtual_env_activation_command.map(|s| s.to_string()),
maybe_default_config_path: maybe_default_config_path.map(|p| p.as_ref().to_path_buf()),
maybe_docker_options,
maybe_execution_timeout,
alternate_checkpoint_dir,
impl VallEXCreateEmbeddingCommand {
pub fn new<P: AsRef<Path>>(
root_code_directory: P,
executable_or_command: ExecutableOrCommand,
maybe_virtual_env_activation_command: Option<&str>,
maybe_default_config_path: Option<P>,
maybe_docker_options: Option<DockerOptions>,
maybe_execution_timeout: Option<Duration>,
alternate_checkpoint_dir: Option<PathBuf>
) -> Self {
Self {
root_code_directory: root_code_directory.as_ref().to_path_buf(),
executable_or_command,
maybe_virtual_env_activation_command: maybe_virtual_env_activation_command.map(|s|
s.to_string()
),
maybe_default_config_path: maybe_default_config_path.map(|p| p.as_ref().to_path_buf()),
maybe_docker_options,
maybe_execution_timeout,
alternate_checkpoint_dir,
}
}
}
// helper funciton to set up env
pub fn from_env() -> AnyhowResult<Self> {
// TODO: fix for now just for compilation
let root_code_directory = easyenv::get_env_pathbuf_required(
"VALL_E_X_INFERENCE_ROOT_DIRECTORY")?;
let maybe_inference_command = easyenv::get_env_string_optional(
"VALL_E_X_INFERENCE_COMMAND");
// helper funciton to set up env
pub fn from_env() -> AnyhowResult<Self> {
// TODO: fix for now just for compilation
let root_code_directory = easyenv::get_env_pathbuf_required(
"VALL_E_X_INFERENCE_ROOT_DIRECTORY"
)?;
let maybe_inference_command = easyenv::get_env_string_optional(
"VALL_E_X_INFERENCE_COMMAND"
);
// EVENTUALLY should remove the check
// Optional, eg. `./infer.py`. Typically we'll use the command form instead.
let maybe_inference_executable = easyenv::get_env_pathbuf_optional(
"VALL_E_X_INFERENCE_EXECUTABLE");
let executable_or_command = match maybe_inference_command {
Some(command) => ExecutableOrCommand::Command(command),
None => match maybe_inference_executable {
Some(executable) => ExecutableOrCommand::Executable(executable),
None => return Err(anyhow!("neither command nor executable passed")),
},
};
let maybe_virtual_env_activation_command = easyenv::get_env_string_optional(
"VALL_E_X_INFERENCE_MAYBE_VENV_COMMAND");
let maybe_default_config_path = easyenv::get_env_pathbuf_optional(
"VALL_E_X_INFERENCE_MAYBE_DEFAULT_CONFIG_PATH");
let maybe_execution_timeout =
easyenv::get_env_duration_seconds_optional("TIMEOUT_SECONDS");
// Optional, eg. `./infer.py`. Typically we'll use the command form instead.
let maybe_inference_executable = easyenv::get_env_pathbuf_optional(
"VALL_E_X_INFERENCE_EXECUTABLE"
);
// Probably for local
let maybe_docker_options = easyenv::get_env_string_optional(
"VALL_E_X_INFERENCE_MAYBE_DOCKER_IMAGE")
.map(|image_name| {
DockerOptions {
image_name,
maybe_bind_mount: Some(DockerFilesystemMount::tmp_to_tmp()),
maybe_environment_variables: None,
maybe_gpu: Some(DockerGpu::All),
let executable_or_command = match maybe_inference_command {
Some(command) => ExecutableOrCommand::Command(command),
None =>
match maybe_inference_executable {
Some(executable) => ExecutableOrCommand::Executable(executable),
None => {
return Err(anyhow!("neither command nor executable passed"));
}
}
};
let maybe_virtual_env_activation_command = easyenv::get_env_string_optional(
"VALL_E_X_INFERENCE_MAYBE_VENV_COMMAND"
);
let maybe_default_config_path = easyenv::get_env_pathbuf_optional(
"VALL_E_X_INFERENCE_MAYBE_DEFAULT_CONFIG_PATH"
);
let maybe_execution_timeout = easyenv::get_env_duration_seconds_optional("TIMEOUT_SECONDS");
// Probably for local
let maybe_docker_options = easyenv
::get_env_string_optional("VALL_E_X_INFERENCE_MAYBE_DOCKER_IMAGE")
.map(|image_name| {
DockerOptions {
image_name,
maybe_bind_mount: Some(DockerFilesystemMount::tmp_to_tmp()),
maybe_environment_variables: None,
maybe_gpu: Some(DockerGpu::All),
}
});
// Override for --checkpoint_dir at inference time
let alternate_checkpoint_dir = easyenv::get_env_pathbuf_optional(
"VALL_E_X_CHECKPOINT_PATH"
);
Ok(Self {
root_code_directory,
executable_or_command,
maybe_virtual_env_activation_command,
maybe_default_config_path,
maybe_docker_options,
maybe_execution_timeout,
alternate_checkpoint_dir,
})
}
pub fn execute_inference<P: AsRef<Path>>(&self, args: CreateVoiceInferenceArgs<P>) -> CommandExitStatus {
match self.do_execute_inference(args) {
Ok(exit_status) => exit_status,
Err(error) =>
CommandExitStatus::FailureWithReason { reason: format!("error: {:?}", error) },
}
}
fn do_execute_inference<P: AsRef<Path>>(
&self,
args: CreateVoiceInferenceArgs<P>
) -> AnyhowResult<CommandExitStatus> {
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(" ");
}
});
// Override for --checkpoint_dir at inference time
let alternate_checkpoint_dir = easyenv::get_env_pathbuf_optional(
"SAD_TALKER_ALTERNATE_CHECKPOINT_PATH");
Ok(Self {
root_code_directory,
executable_or_command,
maybe_virtual_env_activation_command,
maybe_default_config_path,
maybe_docker_options,
maybe_execution_timeout,
alternate_checkpoint_dir,
})
ExecutableOrCommand::Command(ref cmd) => {
command.push_str(cmd);
command.push_str(" ");
}
}
// ===== Begin Python Args =====
command.push_str(" --prompt-name ");
command.push_str(&path_to_string(args.output_embedding_name));
command.push_str(" --prompt-path ");
command.push_str(&path_to_string(&args.output_embedding_path));
command.push_str(" --audio-wav-files ");
command.push_str(&args.audio_files);
command.push_str(" --mode ");
command.push_str(&path_to_string("1"));
// TODO improve and use a better model for premium users
command.push_str(" --whisper-model ");
command.push_str(&path_to_string(Path::new("medium")));
command.push_str(" --whisper-folder-path ");
command.push_str(
&path_to_string(Path::new("/tmp/downloads/zero_shot_tts/vall-e-x_1.0/whisper/"))
);
command.push_str(" --vocos-folder-path ");
command.push_str(
&path_to_string(Path::new("/tmp/downloads/zero_shot_tts/vall-e-x_1.0/vocos-encodec/"))
);
command.push_str(" --vallex-path ");
command.push_str(
&path_to_string(
Path::new("/tmp/downloads/zero_shot_tts/vall-e-x_1.0/vallex-checkpoint.pt")
)
);
command.push_str(" --tmp-work-dir ");
command.push_str(&path_to_string(Path::new("/tmp/downloads/zero_shot_tts/")));
// ===== End Python Args =====
if let Some(docker_options) = self.maybe_docker_options.as_ref() {
command = docker_options.to_command_string(&command);
}
info!("Command: {:?}", command);
let command_parts = ["bash", "-c", &command];
let mut env_vars = Vec::new();
// Copy all environment variables from the parent process.
// This is necessary to send all the kubernetes settings for Nvidia / CUDA.
for (env_key, env_value) in env::vars() {
if IGNORED_ENVIRONMENT_VARS.contains(&env_key) {
continue;
}
env_vars.push((OsString::from(env_key), OsString::from(env_value)));
}
let mut config = PopenConfig::default();
info!("stderr will be written to file: {:?}", args.stderr_output_file);
let stderr_file = File::create(&args.stderr_output_file)?;
config.stderr = Redirection::File(stderr_file);
if !env_vars.is_empty() {
config.env = Some(env_vars);
}
let mut p = Popen::create(&command_parts, config)?;
info!("Subprocess PID: {:?}", p.pid());
match self.maybe_execution_timeout {
None => {
let exit_status = p.wait()?;
info!("Subprocess exit status: {:?}", exit_status);
Ok(CommandExitStatus::from_exit_status(exit_status))
}
Some(timeout) => {
info!("Executing with timeout: {:?}", &timeout);
let exit_status = p.wait_timeout(timeout.clone())?;
match exit_status {
None => {
// NB: If the program didn't successfully terminate, kill it.
info!(
"Subprocess didn't end after timeout: {:?}; terminating...",
&timeout
);
let _r = p.terminate()?;
Ok(CommandExitStatus::Timeout)
}
Some(exit_status) => {
info!("Subprocess timed wait exit status: {:?}", exit_status);
Ok(CommandExitStatus::from_exit_status(exit_status))
}
}
}
}
}
}
@@ -4,21 +4,22 @@ use std::time::Instant;
use anyhow::anyhow;
use log::{error, info, warn};
use buckets::public::media_files::original_file::MediaFileBucketPath;
use buckets::public::media_uploads::original_file::MediaUploadOriginalFilePath;
use buckets::public::voice_conversion_results::original_file::VoiceConversionResultOriginalFilePath;
use container_common::filesystem::check_file_exists::check_file_exists;
use container_common::filesystem::safe_delete_temp_directory::safe_delete_temp_directory;
use container_common::filesystem::safe_delete_temp_file::safe_delete_temp_file;
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
use filesys::create_dir_all_if_missing::create_dir_all_if_missing;
use filesys::file_size::file_size;
use hashing::sha256::sha256_hash_file::sha256_hash_file;
use media::decode_basic_audio_info::decode_basic_audio_file_info;
use mimetypes::mimetype_for_file::get_mimetype_for_file;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::PolymorphicInferenceArgs;
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
use mysql_queries::queries::media_files::insert_media_file_from_voice_conversion::{insert_media_file_from_voice_conversion, InsertMediaFileArgs, VoiceConversionModelType};
use mysql_queries::queries::media_uploads::get_media_upload_for_inference::MediaUploadRecordForInference;
use mysql_queries::queries::voice_conversion::inference::get_voice_conversion_model_for_inference::VoiceConversionModelForInference;
use mysql_queries::queries::voice_conversion::results::insert_voice_conversion_result::{insert_voice_conversion_result, InsertArgs};
use tokens::tokens::media_uploads::MediaUploadToken;
use tokens::tokens::users::UserToken;
@@ -28,6 +29,9 @@ use crate::job::job_types::vc::rvc_v2::rvc_v2_inference_command::InferenceArgs;
use crate::job_dependencies::JobDependencies;
use crate::util::maybe_download_file_from_bucket::maybe_download_file_from_bucket;
const BUCKET_FILE_PREFIX : &str = "fakeyou_rvc_";
const BUCKET_FILE_EXTENSION : &str = ".wav";
pub struct RvcV2ProcessJobArgs<'a> {
pub job_dependencies: &'a JobDependencies,
pub job: &'a AvailableInferenceJob,
@@ -267,8 +271,6 @@ pub async fn process_job(args: RvcV2ProcessJobArgs<'_>) -> Result<JobSuccessResu
info!("Checking that output files exist...");
check_file_exists(&output_audio_fs_path).map_err(|e| ProcessSingleJobError::Other(e))?;
//check_file_exists(&output_metadata_fs_path).map_err(|e| ProcessSingleJobError::Other(e))?;
//check_file_exists(&output_spectrogram_fs_path).map_err(|e| ProcessSingleJobError::Other(e))?;
info!("Interrogating result file properties...");
@@ -286,11 +288,18 @@ pub async fn process_job(args: RvcV2ProcessJobArgs<'_>) -> Result<JobSuccessResu
warn!("Required a full decode of the output data to get duration! That's inefficient!");
}
info!("Calculating sha256...");
let file_checksum = sha256_hash_file(&output_audio_fs_path)
.map_err(|err| {
ProcessSingleJobError::Other(anyhow!("Error hashing file: {:?}", err))
})?;
// TODO: Make a new python image that generates spectrograms from any audio file.
let file_metadata = FileMetadata {
duration_millis: audio_info.duration_millis,
mimetype: maybe_mimetype,
mimetype: maybe_mimetype.clone(),
file_size_bytes,
};
@@ -301,7 +310,15 @@ pub async fn process_job(args: RvcV2ProcessJobArgs<'_>) -> Result<JobSuccessResu
job_progress_reporter.log_status("uploading result")
.map_err(|e| ProcessSingleJobError::Other(e))?;
let result_bucket_location = VoiceConversionResultOriginalFilePath::generate_new();
// let result_bucket_location = VoiceConversionResultOriginalFilePath::generate_new();
//
// let result_bucket_object_pathbuf = result_bucket_location.to_full_object_pathbuf();
//
// info!("Audio destination bucket path: {:?}", &result_bucket_object_pathbuf);
let result_bucket_location = MediaFileBucketPath::generate_new(
Some(BUCKET_FILE_PREFIX),
Some(BUCKET_FILE_EXTENSION));
let result_bucket_object_pathbuf = result_bucket_location.to_full_object_pathbuf();
@@ -318,24 +335,6 @@ pub async fn process_job(args: RvcV2ProcessJobArgs<'_>) -> Result<JobSuccessResu
safe_delete_temp_file(&output_audio_fs_path);
// // ==================== UPLOAD SPECTROGRAM TO BUCKETS ==================== //
//
// let spectrogram_result_object_path = args.job_dependencies.bucket_path_unifier.tts_inference_spectrogram_output_path(
// &job.uuid_idempotency_token); // TODO: Don't use this!
//
// info!("Spectrogram destination bucket path: {:?}", &spectrogram_result_object_path);
//
// info!("Uploading spectrogram...");
//
// args.job_dependencies.public_bucket_client.upload_filename_with_content_type(
// &spectrogram_result_object_path,
// &output_spectrogram_fs_path,
// "application/json")
// .await
// .map_err(|e| ProcessSingleJobError::Other(e))?;
//
// safe_delete_temp_file(&output_spectrogram_fs_path);
// ==================== DELETE DOWNLOADED FILE ==================== //
// NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk...
@@ -345,16 +344,34 @@ pub async fn process_job(args: RvcV2ProcessJobArgs<'_>) -> Result<JobSuccessResu
info!("Saving vc inference record...");
let (inference_result_token, id) = insert_voice_conversion_result(InsertArgs {
// let (inference_result_token, id) = insert_voice_conversion_result(InsertArgs {
// pool: &args.job_dependencies.mysql_pool,
// job: &job,
// public_bucket_hash: result_bucket_location.get_object_hash(),
// file_size_bytes: file_metadata.file_size_bytes,
// duration_millis: file_metadata.duration_millis.unwrap_or(0),
// is_on_prem: args.job_dependencies.container.is_on_prem,
// worker_hostname: &args.job_dependencies.container.hostname,
// worker_cluster: &args.job_dependencies.container.cluster_name,
// is_debug_worker: args.job_dependencies.worker_details.is_debug_worker
// })
// .await
// .map_err(|e| ProcessSingleJobError::Other(e))?;
let (inference_result_token, id) = insert_media_file_from_voice_conversion(InsertMediaFileArgs {
pool: &args.job_dependencies.mysql_pool,
job: &job,
public_bucket_hash: result_bucket_location.get_object_hash(),
voice_conversion_type: VoiceConversionModelType::RvcV2,
maybe_mime_type: maybe_mimetype.as_deref(),
file_size_bytes: file_metadata.file_size_bytes,
duration_millis: file_metadata.duration_millis.unwrap_or(0),
sha256_checksum: &file_checksum,
public_bucket_directory_hash: result_bucket_location.get_object_hash(),
maybe_public_bucket_prefix: Some(BUCKET_FILE_PREFIX),
maybe_public_bucket_extension: Some(BUCKET_FILE_EXTENSION),
is_on_prem: args.job_dependencies.container.is_on_prem,
worker_hostname: &args.job_dependencies.container.hostname,
worker_cluster: &args.job_dependencies.container.cluster_name,
is_debug_worker: args.job_dependencies.worker_details.is_debug_worker
})
.await
.map_err(|e| ProcessSingleJobError::Other(e))?;
@@ -383,7 +400,7 @@ pub async fn process_job(args: RvcV2ProcessJobArgs<'_>) -> Result<JobSuccessResu
Ok(JobSuccessResult {
maybe_result_entity: Some(ResultEntity {
entity_type: InferenceResultType::VoiceConversion,
entity_type: InferenceResultType::MediaFile,
entity_token: inference_result_token.to_string(),
}),
inference_duration,
@@ -3,21 +3,22 @@ use std::time::Instant;
use anyhow::anyhow;
use log::{error, info, warn};
use buckets::public::media_files::original_file::MediaFileBucketPath;
use buckets::public::media_uploads::original_file::MediaUploadOriginalFilePath;
use buckets::public::voice_conversion_results::original_file::VoiceConversionResultOriginalFilePath;
use container_common::filesystem::check_file_exists::check_file_exists;
use container_common::filesystem::safe_delete_temp_directory::safe_delete_temp_directory;
use container_common::filesystem::safe_delete_temp_file::safe_delete_temp_file;
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
use filesys::create_dir_all_if_missing::create_dir_all_if_missing;
use filesys::file_size::file_size;
use hashing::sha256::sha256_hash_file::sha256_hash_file;
use media::decode_basic_audio_info::decode_basic_audio_file_info;
use mimetypes::mimetype_for_file::get_mimetype_for_file;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::PolymorphicInferenceArgs;
use mysql_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
use mysql_queries::queries::media_files::insert_media_file_from_voice_conversion::{insert_media_file_from_voice_conversion, InsertMediaFileArgs, VoiceConversionModelType};
use mysql_queries::queries::media_uploads::get_media_upload_for_inference::MediaUploadRecordForInference;
use mysql_queries::queries::voice_conversion::inference::get_voice_conversion_model_for_inference::VoiceConversionModelForInference;
use mysql_queries::queries::voice_conversion::results::insert_voice_conversion_result::{insert_voice_conversion_result, InsertArgs};
use tokens::tokens::media_uploads::MediaUploadToken;
use tokens::tokens::users::UserToken;
@@ -27,6 +28,9 @@ use crate::job::job_types::vc::so_vits_svc::so_vits_svc_inference_command::{Devi
use crate::job_dependencies::JobDependencies;
use crate::util::maybe_download_file_from_bucket::maybe_download_file_from_bucket;
const BUCKET_FILE_PREFIX : &str = "fakeyou_svc_";
const BUCKET_FILE_EXTENSION : &str = ".wav";
pub struct SoVitsSvcProcessJobArgs<'a> {
pub job_dependencies: &'a JobDependencies,
pub job: &'a AvailableInferenceJob,
@@ -131,14 +135,10 @@ pub async fn process_job(args: SoVitsSvcProcessJobArgs<'_>) -> Result<JobSuccess
let input_wav_path = original_media_upload_fs_path;
let output_audio_fs_path = work_temp_dir.path().join("output.wav");
//let output_metadata_fs_path = temp_dir.path().join("metadata.json");
//let output_spectrogram_fs_path = temp_dir.path().join("spectrogram.json");
info!("Running VC inference...");
info!("Expected output audio filename: {:?}", &output_audio_fs_path);
//info!("Expected output metadata filename: {:?}", &output_metadata_fs_path);
//info!("Expected output spectrogram filename: {:?}", &output_spectrogram_fs_path);
// TODO: Limit output length for premium.
@@ -200,8 +200,6 @@ pub async fn process_job(args: SoVitsSvcProcessJobArgs<'_>) -> Result<JobSuccess
info!("Checking that output files exist...");
check_file_exists(&output_audio_fs_path).map_err(|e| ProcessSingleJobError::Other(e))?;
//check_file_exists(&output_metadata_fs_path).map_err(|e| ProcessSingleJobError::Other(e))?;
//check_file_exists(&output_spectrogram_fs_path).map_err(|e| ProcessSingleJobError::Other(e))?;
info!("Interrogating result file properties...");
@@ -219,28 +217,41 @@ pub async fn process_job(args: SoVitsSvcProcessJobArgs<'_>) -> Result<JobSuccess
warn!("Required a full decode of the output data to get duration! That's inefficient!");
}
info!("Calculating sha256...");
let file_checksum = sha256_hash_file(&output_audio_fs_path)
.map_err(|err| {
ProcessSingleJobError::Other(anyhow!("Error hashing file: {:?}", err))
})?;
// TODO: Make a new python image that generates spectrograms from any audio file.
let file_metadata = FileMetadata {
duration_millis: audio_info.duration_millis,
mimetype: maybe_mimetype,
mimetype: maybe_mimetype.clone(),
file_size_bytes,
};
//safe_delete_temp_file(&output_metadata_fs_path);
// ==================== UPLOAD AUDIO TO BUCKET ==================== //
job_progress_reporter.log_status("uploading result")
.map_err(|e| ProcessSingleJobError::Other(e))?;
let result_bucket_location = VoiceConversionResultOriginalFilePath::generate_new();
// let result_bucket_location = VoiceConversionResultOriginalFilePath::generate_new();
//
// let result_bucket_object_pathbuf = result_bucket_location.to_full_object_pathbuf();
//
// info!("Audio destination bucket path: {:?}", &result_bucket_object_pathbuf);
let result_bucket_location = MediaFileBucketPath::generate_new(
Some(BUCKET_FILE_PREFIX),
Some(BUCKET_FILE_EXTENSION));
let result_bucket_object_pathbuf = result_bucket_location.to_full_object_pathbuf();
info!("Audio destination bucket path: {:?}", &result_bucket_object_pathbuf);
info!("Uploading audio...");
info!("Uploading audio media file...");
args.job_dependencies.public_bucket_client.upload_filename_with_content_type(
&result_bucket_object_pathbuf,
@@ -251,24 +262,6 @@ pub async fn process_job(args: SoVitsSvcProcessJobArgs<'_>) -> Result<JobSuccess
safe_delete_temp_file(&output_audio_fs_path);
// // ==================== UPLOAD SPECTROGRAM TO BUCKETS ==================== //
//
// let spectrogram_result_object_path = args.job_dependencies.bucket_path_unifier.tts_inference_spectrogram_output_path(
// &job.uuid_idempotency_token); // TODO: Don't use this!
//
// info!("Spectrogram destination bucket path: {:?}", &spectrogram_result_object_path);
//
// info!("Uploading spectrogram...");
//
// args.job_dependencies.public_bucket_client.upload_filename_with_content_type(
// &spectrogram_result_object_path,
// &output_spectrogram_fs_path,
// "application/json")
// .await
// .map_err(|e| ProcessSingleJobError::Other(e))?;
//
// safe_delete_temp_file(&output_spectrogram_fs_path);
// ==================== DELETE DOWNLOADED FILE ==================== //
// NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk...
@@ -278,16 +271,34 @@ pub async fn process_job(args: SoVitsSvcProcessJobArgs<'_>) -> Result<JobSuccess
info!("Saving vc inference record...");
let (inference_result_token, id) = insert_voice_conversion_result(InsertArgs {
// let (inference_result_token, id) = insert_voice_conversion_result(InsertArgs {
// pool: &args.job_dependencies.mysql_pool,
// job: &job,
// public_bucket_hash: result_bucket_location.get_object_hash(),
// file_size_bytes: file_metadata.file_size_bytes,
// duration_millis: file_metadata.duration_millis.unwrap_or(0),
// is_on_prem: args.job_dependencies.container.is_on_prem,
// worker_hostname: &args.job_dependencies.container.hostname,
// worker_cluster: &args.job_dependencies.container.cluster_name,
// is_debug_worker: args.job_dependencies.worker_details.is_debug_worker
// })
// .await
// .map_err(|e| ProcessSingleJobError::Other(e))?;
let (inference_result_token, id) = insert_media_file_from_voice_conversion(InsertMediaFileArgs {
pool: &args.job_dependencies.mysql_pool,
job: &job,
public_bucket_hash: result_bucket_location.get_object_hash(),
voice_conversion_type: VoiceConversionModelType::SoVitsSvc,
maybe_mime_type: maybe_mimetype.as_deref(),
file_size_bytes: file_metadata.file_size_bytes,
duration_millis: file_metadata.duration_millis.unwrap_or(0),
sha256_checksum: &file_checksum,
public_bucket_directory_hash: result_bucket_location.get_object_hash(),
maybe_public_bucket_prefix: Some(BUCKET_FILE_PREFIX),
maybe_public_bucket_extension: Some(BUCKET_FILE_EXTENSION),
is_on_prem: args.job_dependencies.container.is_on_prem,
worker_hostname: &args.job_dependencies.container.hostname,
worker_cluster: &args.job_dependencies.container.cluster_name,
is_debug_worker: args.job_dependencies.worker_details.is_debug_worker
})
.await
.map_err(|e| ProcessSingleJobError::Other(e))?;
@@ -316,7 +327,7 @@ pub async fn process_job(args: SoVitsSvcProcessJobArgs<'_>) -> Result<JobSuccess
Ok(JobSuccessResult {
maybe_result_entity: Some(ResultEntity {
entity_type: InferenceResultType::VoiceConversion,
entity_type: InferenceResultType::MediaFile,
entity_token: inference_result_token.to_string(),
}),
inference_duration,
@@ -5,6 +5,7 @@ use log::warn;
use cookies::jwt_signer::JwtSigner;
use errors::AnyhowResult;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use crate::cookies::anonymous_visitor_tracking::avt_cookie_payload::AvtCookiePayload;
@@ -76,4 +77,11 @@ impl AvtCookieManager {
Ok(payload) => Ok(Some(payload)),
}
}
pub fn get_avt_token_from_request(&self, request: &HttpRequest) -> Option<AnonymousVisitorTrackingToken> {
self.decode_cookie_payload_from_request(request)
.ok()
.flatten()
.map(|payload| payload.avt_token)
}
}
@@ -31,17 +31,15 @@ const SESSION_COOKIE_NAME : &str = "session";
#[derive(Clone)]
pub struct SessionCookieManager {
cookie_domain: String,
hmac_secret: String,
jwt_signer: JwtSigner,
}
impl SessionCookieManager {
pub fn new(cookie_domain: &str, hmac_secret: &str) -> Self {
Self {
pub fn new(cookie_domain: &str, hmac_secret: &str) -> AnyhowResult<Self> {
Ok(Self {
cookie_domain: cookie_domain.to_string(),
hmac_secret: hmac_secret.to_string(),
jwt_signer: JwtSigner::new(hmac_secret).unwrap(),//TODO FIXME
}
jwt_signer: JwtSigner::new(hmac_secret)?
})
}
pub fn create_cookie(&self, session_token: &str, user_token: &str) -> AnyhowResult<Cookie> {
@@ -143,7 +141,7 @@ mod tests {
fn test_cookie_payload() {
// NB: Let's make extra sure this always works when migrating cookies, else we'll accidentally log out logged-in users.
// (These are version 2 cookies.)
let manager = SessionCookieManager::new("fakeyou.com", "secret");
let manager = SessionCookieManager::new("fakeyou.com", "secret").unwrap();
let cookie = manager.create_cookie("ex_session_token", "ex_user_token").unwrap();
assert_eq!(cookie.value(), "eyJhbGciOiJIUzI1NiJ9.eyJjb29raWVfdmVyc2lvbiI6IjIiLCJzZXNzaW9uX3Rva2VuIjoiZXhfc2Vzc2lvbl90b2tlbiIsInVzZXJfdG9rZW4iOiJleF91c2VyX3Rva2VuIn0.94ly2gHhlPVtnANsNy6cJozFVmId4imwW5v-mei7jD8");
@@ -153,7 +151,7 @@ mod tests {
fn test_cookie_round_trip() {
// NB: Let's make extra sure this always works when migrating cookies, else we'll accidentally log out logged-in users.
// (These are version 2 cookies.)
let manager = SessionCookieManager::new("fakeyou.com", "secret");
let manager = SessionCookieManager::new("fakeyou.com", "secret").unwrap();
let cookie = manager.create_cookie("ex_session_token", "ex_user_token").unwrap();
let decoded = manager.decode_session_cookie_payload(&cookie).unwrap();
@@ -18,7 +18,6 @@ use http_server_common::response::serialize_as_json_error::serialize_as_json_err
use mysql_queries::mediators::firehose_publisher::FirehosePublisher;
use mysql_queries::queries::users::user::create_account::{create_account, CreateAccountArgs, CreateAccountError};
use mysql_queries::queries::users::user_sessions::create_user_session::create_user_session;
use mysql_queries::tokens::Tokens;
use user_input_common::check_for_slurs::contains_slurs;
use crate::cookies::session::session_cookie_manager::SessionCookieManager;
@@ -133,12 +132,6 @@ pub async fn create_account_handler(
});
}
let user_token = Tokens::new_user()
.map_err(|e| {
warn!("Bad crockford token: {:?}", e);
CreateAccountErrorResponse::server_error()
})?;
let password_hash = match bcrypt::hash(&request.password, bcrypt::DEFAULT_COST) {
Ok(hash) => hash,
Err(err) => {
@@ -200,7 +193,7 @@ pub async fn create_account_handler(
info!("new user id: {}", new_user_data.user_id);
let create_session_result = create_user_session(
&new_user_data.user_token,
new_user_data.user_token.as_str(),
&ip_address,
&mysql_pool
).await;
@@ -215,14 +208,14 @@ pub async fn create_account_handler(
info!("new user session created");
firehose_publisher.publish_user_sign_up(&user_token)
firehose_publisher.publish_user_sign_up(new_user_data.user_token.as_str())
.await
.map_err(|e| {
warn!("error publishing event: {:?}", e);
CreateAccountErrorResponse::server_error()
})?;
let session_cookie = match session_cookie_manager.create_cookie(&session_token, &user_token) {
let session_cookie = match session_cookie_manager.create_cookie(&session_token, new_user_data.user_token.as_str()) {
Ok(cookie) => cookie,
Err(_) => return Err(CreateAccountErrorResponse::server_error()),
};
@@ -18,11 +18,11 @@ 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};
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
use tokens::tokens::media_uploads::MediaUploadToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::voice_conversion_results::VoiceConversionResultToken;
use tokens::tokens::media_uploads::MediaUploadToken;
use tokens::tokens::users::UserToken;
use tokens::tokens::voice_conversion_results::VoiceConversionResultToken;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
@@ -174,6 +174,8 @@ pub async fn enqueue_lipsync_animation_handler(
EnqueueLipsyncAnimationError::ServerError
})?;
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
// ==================== USER SESSION ==================== //
let maybe_user_session = server_state
@@ -312,6 +314,7 @@ pub async fn enqueue_lipsync_animation_handler(
args: Some(PolymorphicInferenceArgs::La(inference_args)),
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: set_visibility,
priority_level,
@@ -6,13 +6,14 @@ use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Path;
use chrono::{DateTime, Utc};
use log::{error, log};
use r2d2_redis::redis::Commands;
use log::error;
use r2d2_redis::redis::{Commands, RedisResult};
use buckets::public::media_files::original_file::MediaFileBucketPath;
use buckets::public::voice_conversion_results::original_file::VoiceConversionResultOriginalFilePath;
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use enums::common::job_status_plus::JobStatusPlus;
use mysql_queries::queries::generic_inference::web::get_inference_job_status::get_inference_job_status;
use redis_common::redis_keys::RedisKeys;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
@@ -65,7 +66,7 @@ pub struct RequestDetailsResponse {
#[derive(Serialize)]
pub struct StatusDetailsResponse {
/// Primary status from the database (a state machine).
pub status: String,
pub status: JobStatusPlus,
/// Extra, temporary status from Redis.
/// This can denote inference progress, and the Python code can write to it.
@@ -163,15 +164,22 @@ pub async fn get_inference_job_status_handler(
// TODO(bt,2023-05-21): Make async.
let extra_status_key = RedisKeys::generic_inference_extra_status_info(path.token.as_str());
let maybe_extra_status_description : Option<String> = match redis.get(&extra_status_key) {
Ok(Some(status)) => {
Some(status)
},
Ok(None) => None,
let maybe_extra_status_value : RedisResult<Option<String>> = redis.get(&extra_status_key);
let maybe_extra_status_description = match maybe_extra_status_value {
Err(e) => {
error!("redis error: {:?}", e);
None // Fail open
},
Ok(maybe_value) => match maybe_value.as_deref() {
Some("1") => {
// TODO(bt,2023-10-20): Redis is reporting "1" and it's been surfacing this as a weird
// message to the frontend for months. This needs proper fixing.
None
},
Some(value) => Some(value.to_string()),
None => None,
}
};
if record.is_keepalive_required {
@@ -226,9 +234,23 @@ pub async fn get_inference_job_status_handler(
result_details.public_bucket_location_or_hash
}
InferenceCategory::VoiceConversion => {
VoiceConversionResultOriginalFilePath::from_object_hash(&result_details.public_bucket_location_or_hash)
.get_full_object_path_str()
.to_string()
match result_details.entity_type.as_str() {
"media_file" => {
// NB: We're migrating voice conversion to media_files.
MediaFileBucketPath::from_object_hash(
&result_details.public_bucket_location_or_hash,
result_details.maybe_media_file_public_bucket_prefix.as_deref(),
result_details.maybe_media_file_public_bucket_extension.as_deref())
.get_full_object_path_str()
.to_string()
}
_ => {
// NB: This is the old voice conversion result pathing.
VoiceConversionResultOriginalFilePath::from_object_hash(&result_details.public_bucket_location_or_hash)
.get_full_object_path_str()
.to_string()
}
}
}
};
@@ -1,2 +1,3 @@
pub mod get_inference_job_status;
pub mod get_pending_inference_job_count;
pub mod get_pending_inference_job_count;
pub mod terminate_inference_job_handler;
@@ -0,0 +1,134 @@
use std::fmt;
use std::sync::Arc;
use actix_web::{HttpMessage, HttpRequest, HttpResponse, web};
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use actix_web::web::Path;
use log::{error, warn};
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::generic_inference::job::mark_generic_inference_job_cancelled_by_user::mark_generic_inference_job_cancelled_by_user;
use mysql_queries::queries::generic_inference::web::get_inference_job_status::{get_inference_job_status, get_inference_job_status_from_connection};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::server_state::ServerState;
/// For the URL PathInfo
#[derive(Deserialize)]
pub struct TerminateInferenceJobPathInfo {
token: InferenceJobToken,
}
#[derive(Serialize)]
pub struct TerminateInferenceJobSuccessResponse {
pub success: bool,
}
#[derive(Debug)]
pub enum TerminateInferenceJobError {
ServerError,
NotFound,
NotAuthorized,
}
impl ResponseError for TerminateInferenceJobError {
fn status_code(&self) -> StatusCode {
match *self {
TerminateInferenceJobError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
TerminateInferenceJobError::NotFound => StatusCode::NOT_FOUND,
TerminateInferenceJobError::NotAuthorized => StatusCode::UNAUTHORIZED,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
Self::ServerError => "server error".to_string(),
Self::NotFound => "not found".to_string(),
Self::NotAuthorized => "unauthorized".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
// NB: Not using derive_more::Display since Clion doesn't understand it.
impl fmt::Display for TerminateInferenceJobError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
pub async fn terminate_inference_job_handler(
http_request: HttpRequest,
path: Path<TerminateInferenceJobPathInfo>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse, TerminateInferenceJobError>
{
let mut mysql_connection = server_state.mysql_pool.acquire()
.await
.map_err(|e| {
warn!("Could not acquire DB pool: {:?}", e);
TerminateInferenceJobError::ServerError
})?;
let maybe_user_session = server_state
.session_checker
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
.await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
TerminateInferenceJobError::ServerError
})?;
let maybe_status = get_inference_job_status_from_connection(
&path.token, &mut mysql_connection).await;
let job_status = match maybe_status {
Ok(Some(record)) => record,
Ok(None) => return Err(TerminateInferenceJobError::NotFound),
Err(err) => {
error!("tts job query error: {:?}", err);
return Err(TerminateInferenceJobError::ServerError);
}
};
let maybe_user_tokens = maybe_user_session
.as_ref()
.and_then(|session| Some(session.user_token_typed.clone()))
.zip(job_status.user_details.maybe_creator_user_token.clone());
if let Some((session_user_token, job_user_token)) = maybe_user_tokens {
if session_user_token != job_user_token {
return Err(TerminateInferenceJobError::NotAuthorized);
}
} else {
let ip_address = get_request_ip(&http_request);
if ip_address != job_status.user_details.creator_ip_address {
// TODO(bt,2023-10-24): Allow if anonymous visitor token cookie matches
return Err(TerminateInferenceJobError::NotAuthorized);
}
}
mark_generic_inference_job_cancelled_by_user(&mut mysql_connection, &path.token)
.await
.map_err(|err| {
error!("tts job query error: {:?}", err);
TerminateInferenceJobError::ServerError
})?;
let response = TerminateInferenceJobSuccessResponse {
success: true,
};
let body = serde_json::to_string(&response)
.map_err(|e| {
error!("error returning response: {:?}", e);
TerminateInferenceJobError::ServerError
})?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
}
@@ -383,6 +383,8 @@ pub async fn enqueue_infer_tts_handler(
.as_deref()
.map(|token| UserToken::new_from_str(token));
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
inference_category: InferenceCategory::TextToSpeech,
@@ -396,6 +398,7 @@ pub async fn enqueue_infer_tts_handler(
args: None, // NB: We don't need to encode any args yet.
}),
maybe_creator_user_token: maybe_creator_user_token_typed.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: set_visibility,
priority_level,
@@ -25,8 +25,8 @@ use mysql_queries::payloads::generic_inference_args::generic_inference_args::{Fu
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
use mysql_queries::queries::voice_conversion::model_info_lite::get_voice_conversion_model_info_lite::get_voice_conversion_model_info_lite_with_connection;
use redis_common::redis_keys::RedisKeys;
use tokens::tokens::media_uploads::MediaUploadToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_uploads::MediaUploadToken;
use tokens::tokens::users::UserToken;
use tokens::tokens::voice_conversion_models::VoiceConversionModelToken;
use tts_common::priority::FAKEYOU_INVESTOR_PRIORITY_LEVEL;
@@ -143,6 +143,10 @@ pub async fn enqueue_voice_conversion_inference_handler(
EnqueueVoiceConversionInferenceError::ServerError
})?;
// ==================== AVT TOKEN ==================== //
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
// ==================== USER SESSION ==================== //
let maybe_user_session = server_state
@@ -298,6 +302,7 @@ pub async fn enqueue_voice_conversion_inference_handler(
args: maybe_args,
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: set_visibility,
priority_level,
@@ -3,179 +3,215 @@
#![forbid(unused_variables)]
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{GenericInferenceArgs, InferenceCategoryAbbreviated, PolymorphicInferenceArgs};
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{
GenericInferenceArgs,
InferenceCategoryAbbreviated,
PolymorphicInferenceArgs,
};
use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::{ HttpRequest, HttpResponse, web };
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use log::{warn};
use http_server_common::request::get_request_ip::get_request_ip;
use tokens::tokens::users::UserToken;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use mysql_queries::payloads::generic_inference_args::tts_payload::TTSArgs;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use crate::server_state::ServerState;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{insert_generic_inference_job, InsertGenericInferenceArgs};
// TODO MOVE into own file.
use std::fmt::Debug;
use log::warn;
use serde::Deserialize;
use serde::Serialize;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
/// Debug requests can get routed to special "debug-only" workers, which can
/// be used to trial new code, run debugging, etc.
const DEBUG_HEADER_NAME: &str = "enable-debug-mode";
/// The routing tag header can send workloads to particular k8s hosts.
/// This is useful for catching the live logs or intercepting the job.
const ROUTING_TAG_HEADER_NAME: &str = "routing-tag";
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::payloads::generic_inference_args::tts_payload::TTSArgs;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{
insert_generic_inference_job,
InsertGenericInferenceArgs,
};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::users::UserToken;
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use crate::server_state::ServerState;
// TODO MOVE into own file.
use std::fmt::Debug;
#[derive(Deserialize)]
pub struct EnqueueTTSRequest {
uuid_idempotency_token: String,
text: String,
embedding_token: String
uuid_idempotency_token: String,
text: String,
voice_token: String,
}
#[derive(Serialize)]
pub struct EnqueueTTSRequestSuccessResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
#[derive(Debug)]
pub enum EnqueueTTSRequestError {
BadInput(String),
NotAuthorized,
ServerError,
RateLimited,
BadInput(String),
NotAuthorized,
ServerError,
RateLimited,
}
impl ResponseError for EnqueueTTSRequestError {
fn status_code(&self) -> StatusCode {
match *self {
EnqueueTTSRequestError::BadInput(_) => StatusCode::BAD_REQUEST,
EnqueueTTSRequestError::NotAuthorized => StatusCode::UNAUTHORIZED,
EnqueueTTSRequestError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
EnqueueTTSRequestError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
fn status_code(&self) -> StatusCode {
match *self {
EnqueueTTSRequestError::BadInput(_) => StatusCode::BAD_REQUEST,
EnqueueTTSRequestError::NotAuthorized => StatusCode::UNAUTHORIZED,
EnqueueTTSRequestError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
EnqueueTTSRequestError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
}
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
EnqueueTTSRequestError::BadInput(reason) => reason.to_string(),
EnqueueTTSRequestError::NotAuthorized => "unauthorized".to_string(),
EnqueueTTSRequestError::ServerError => "server error".to_string(),
EnqueueTTSRequestError::RateLimited => "rate limited".to_string(),
};
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
EnqueueTTSRequestError::BadInput(reason) => reason.to_string(),
EnqueueTTSRequestError::NotAuthorized => "unauthorized".to_string(),
EnqueueTTSRequestError::ServerError => "server error".to_string(),
EnqueueTTSRequestError::RateLimited => "rate limited".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
to_simple_json_error(&error_reason, self.status_code())
}
}
impl std::fmt::Display for EnqueueTTSRequestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
// Implementation for enqueuing a TTS request
// Reference enqueue_infer_tts_handler.rs for checks: rate limiting / user sessions
// insert generic inference job.rs
// Need to convert it to generic inference job.
pub async fn enqueue_tts_request(
http_request: HttpRequest,
request: web::Json<EnqueueTTSRequest>,
server_state: web::Data<Arc<ServerState>>) -> Result<HttpResponse,EnqueueTTSRequestError> {
http_request: HttpRequest,
request: web::Json<EnqueueTTSRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, EnqueueTTSRequestError> {
println!("Received payload");
println!("Recieved payload");
let is_debug_request = true;
let maybe_user_token : Option<UserToken> = Some(UserToken::new_from_str(&"place holder")); // TODO fix this
let priority_level = 0;
//let disable_rate_limiter = false; // NB: Careful!
let mut maybe_user_token: Option<UserToken> = None;
// do something with user session check if the user should even be able to access the end point
// CAN address this soon just getting everything out of the way to just enqueue the job.
// GET MY SQL
// let mut mysql_connection = server_state.mysql_pool
// .acquire()
// .await
// .map_err(|err| {
// warn!("MySql pool error: {:?}", err);
// EnqueueTTSRequestError::ServerError
// })?;
let mut mysql_connection = server_state.mysql_pool.acquire().await.map_err(|err| {
warn!("MySql pool error: {:?}", err);
EnqueueTTSRequestError::ServerError
})?;
// return errors on payload not being correct
//Err(EnqueueTTSRequestError::ServerError) # missing voice token
// ==================== USER SESSION ==================== //
// TODO: check for session
let maybe_user_session = server_state.session_checker
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection).await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
EnqueueTTSRequestError::ServerError
})?;
// TODO: Give investors priority
if let Some(user_session) = maybe_user_session.as_ref() {
maybe_user_token = Some(UserToken::new_from_str(&user_session.user_token));
}
// TODO: Add Rate Limiter
// 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()
);
// TODO: Look up model info?
// Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
// TODO(bt): CHECK DATABASE FOR TOKENS? I am guessing we need to ensure those tokens exist because the files may not uploaded or are availible?
// get input from the container spec and create a object that similar to llipsync_payload.rs
// ==================== DEBUG MODE + ROUTING TAG ==================== //
// PACKAGE JSON into RUST Struct, any smaller components
// remap from the request
// pass tokens from the request and create a payload that will have the information.
// check malformed json
let is_debug_request = get_request_header_optional(&http_request, DEBUG_HEADER_NAME).is_some();
// Get up IP address
let ip_address = get_request_ip(&http_request);
let maybe_routing_tag = get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME).map(
|routing_tag| routing_tag.trim().to_string()
);
// package as larger component args should always have an embedding token ..
let inference_args = TTSArgs {
text: request.text.clone(),
voice_token: request.embedding_token.clone()
};
// ==================== RATE LIMIT ==================== //
// create the inference args here
// enqueue a zero shot tts request here...
// create the job record here! explore the table insert the inference args in here as json! keep it short
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
inference_category: InferenceCategory::TextToSpeech,
maybe_model_type: Some(InferenceModelType::VallEX), // NB: Model is static during inference
maybe_model_token: None, // NB: Model is static during inference
maybe_input_source_token: None, // TODO: Introduce a second foreign key ?
maybe_input_source_token_type: None, // TODO: Introduce a second foreign key ?
maybe_raw_inference_text: None, // No text
maybe_inference_args: Some(GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::TextToSpeech),
args: Some(PolymorphicInferenceArgs::Tts(inference_args)),
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: enums::common::visibility::Visibility::Public,
priority_level,
requires_keepalive: true, // do we need this? I think so
is_debug_request,
maybe_routing_tag: Some("Tag"), // TODO fix later what is this tag for?
mysql_pool: &server_state.mysql_pool,
}).await;
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(EnqueueTTSRequestError::NotAuthorized);
}
&server_state.redis_rate_limiters.logged_in
}
};
let job_token = match query_result {
Ok((job_token, _id)) => job_token,
Err(err) => {
warn!("New generic inference job creation DB error: {:?}", err);
return Err(EnqueueTTSRequestError::ServerError);
}
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request) {
return Err(EnqueueTTSRequestError::RateLimited);
}
let response: EnqueueTTSRequestSuccessResponse = EnqueueTTSRequestSuccessResponse {
success: true,
inference_job_token: job_token,
};
// Get up IP address
let ip_address = get_request_ip(&http_request);
let body = serde_json::to_string(&response)
.map_err(|_e| EnqueueTTSRequestError::ServerError)?;
// package as larger component args should always have an embedding token ..
let inference_args = TTSArgs {
voice_token: Some(request.voice_token.clone()),
dataset_token: None,
};
// Error handling 101 rust result type returned like so.
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(body))
// create the inference args here
// enqueue a zero shot tts request here...
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
// create the job record here!
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
inference_category: InferenceCategory::TextToSpeech,
maybe_model_type: Some(InferenceModelType::VallEX), // NB: Model is static during inference
maybe_model_token: None, // NB: Model is static during inference
maybe_input_source_token: None,
maybe_input_source_token_type: None,
maybe_raw_inference_text: Some(&request.text),
maybe_inference_args: Some(GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::TextToSpeech),
args: Some(PolymorphicInferenceArgs::Tts(inference_args)),
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
maybe_avt_token: maybe_avt_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: enums::common::visibility::Visibility::Public,
priority_level,
requires_keepalive: true, // do we need this? I think so
is_debug_request: is_debug_request,
maybe_routing_tag: maybe_routing_tag.as_deref(),
mysql_pool: &server_state.mysql_pool,
}).await;
}
let job_token = match query_result {
Ok((job_token, _id)) => job_token,
Err(err) => {
warn!("New generic inference job creation DB error: {:?}", err);
return Err(EnqueueTTSRequestError::ServerError);
}
};
let response: EnqueueTTSRequestSuccessResponse = EnqueueTTSRequestSuccessResponse {
success: true,
inference_job_token: job_token,
};
let body = serde_json::to_string(&response).map_err(|_e| EnqueueTTSRequestError::ServerError)?;
// Error handling 101 rust result type returned like so.
Ok(HttpResponse::Ok().content_type("application/json").body(body))
}
@@ -19,7 +19,7 @@ use media::decode_basic_audio_info::decode_basic_audio_bytes_info;
use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes;
use mysql_queries::queries::media_uploads::insert_media_upload::insert_media_upload;
use mysql_queries::queries::voice_designer::voice_samples::get_dataset_sample_by_uuid::get_dataset_sample_by_uuid_with_connection;
use mysql_queries::queries::voice_designer::voice_samples::insert_dataset_sample_and_media_file::{Args, insert_dataset_sample_and_media_file};
use mysql_queries::queries::voice_designer::voice_samples::insert_dataset_sample_and_media_file::{InsertDatasetSampleAndMediaFileArgs, insert_dataset_sample_and_media_file};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::zs_voice_dataset_samples::ZsVoiceDatasetSampleToken;
use tokens::tokens::zs_voice_datasets::ZsVoiceDatasetToken;
@@ -325,11 +325,11 @@ pub async fn upload_sample_handler(
UploadSampleError::ServerError
})?;
let (dataset_sample_token, media_file_token, _record_id) = insert_dataset_sample_and_media_file(Args {
let (dataset_sample_token, media_file_token, _record_id) = insert_dataset_sample_and_media_file(InsertDatasetSampleAndMediaFileArgs {
uuid_idempotency_token: &uuid_idempotency_token,
media_type: media_upload_type,
origin_category: media_file_origin,
dataset_token,
dataset_token: &dataset_token,
//maybe_original_filename: upload_media_request.file_name.as_deref(),
//original_file_size_bytes: file_size_bytes as u64,
//maybe_original_duration_millis: maybe_duration_millis,
@@ -88,7 +88,7 @@ pub async fn create_dataset_handler(http_request: HttpRequest, request: web::Jso
dataset_title: &title,
maybe_creator_user_token: Some(user_session.user_token.clone().as_ref()),
creator_ip_address: &creator_ip_address,
creator_set_visibility: &creator_set_visibility,
creator_set_visibility,
mysql_pool: &server_state.mysql_pool
}).await;
@@ -1,13 +1,217 @@
use std::sync::Arc;
// #![forbid(unused_imports)]
// #![forbid(unused_mut)]
// #![forbid(unused_variables)]
use actix_web::{HttpRequest, HttpResponse, web};
use std::sync::Arc;
use http_server_common::request::get_request_header_optional::get_request_header_optional;
use log::warn;
use actix_web::{ HttpRequest, HttpResponse, web };
use actix_web::error::ResponseError;
use actix_web::http::StatusCode;
use log::{info, warn};
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
use mysql_queries::payloads::generic_inference_args::generic_inference_args::{
GenericInferenceArgs,
InferenceCategoryAbbreviated,
PolymorphicInferenceArgs,
};
use http_server_common::request::get_request_ip::get_request_ip;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job;
use tokens::tokens::users::UserToken;
use enums::by_table::generic_inference_jobs::inference_model_type::InferenceModelType;
use mysql_queries::payloads::generic_inference_args::tts_payload::TTSArgs;
pub async fn create_voice_handler() -> HttpResponse {
// Implementation for creating a voice
HttpResponse::Ok().json("Voice created successfully")
}
use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session;
use crate::http_server::endpoints::voice_designer::inference::enqueue_tts_request::EnqueueTTSRequest;
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use crate::server_state::ServerState;
use mysql_queries::queries::generic_inference::web::insert_generic_inference_job::{
insert_generic_inference_job,
InsertGenericInferenceArgs,
};
use std::fmt::Debug;
use serde::Deserialize;
use serde::Serialize;
#[derive(Deserialize)]
pub struct EnqueueCreateVoiceRequest {
uuid_idempotency_token: String,
voice_dataset_token: String,
}
#[derive(Serialize)]
pub struct EnqueueCreateVoiceRequestSuccessResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
#[derive(Debug)]
pub enum EnqueueCreateVoiceRequestError {
BadInput(String),
NotAuthorized,
ServerError,
RateLimited,
}
impl ResponseError for EnqueueCreateVoiceRequestError {
fn status_code(&self) -> StatusCode {
match *self {
EnqueueCreateVoiceRequestError::BadInput(_) => StatusCode::BAD_REQUEST,
EnqueueCreateVoiceRequestError::NotAuthorized => StatusCode::UNAUTHORIZED,
EnqueueCreateVoiceRequestError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
EnqueueCreateVoiceRequestError::RateLimited => StatusCode::TOO_MANY_REQUESTS,
}
}
fn error_response(&self) -> HttpResponse {
let error_reason = match self {
EnqueueCreateVoiceRequestError::BadInput(reason) => reason.to_string(),
EnqueueCreateVoiceRequestError::NotAuthorized => "unauthorized".to_string(),
EnqueueCreateVoiceRequestError::ServerError => "server error".to_string(),
EnqueueCreateVoiceRequestError::RateLimited => "rate limited".to_string(),
};
to_simple_json_error(&error_reason, self.status_code())
}
}
impl std::fmt::Display for EnqueueCreateVoiceRequestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
/// Debug requests can get routed to special "debug-only" workers, which can
/// be used to trial new code, run debugging, etc.
const DEBUG_HEADER_NAME: &str = "enable-debug-mode";
/// The routing tag header can send workloads to particular k8s hosts.
/// This is useful for catching the live logs or intercepting the job.
const ROUTING_TAG_HEADER_NAME: &str = "routing-tag";
// pub async fn get_dataset_by_token
pub async fn create_voice_handler(
http_request: HttpRequest,
request: web::Json<EnqueueCreateVoiceRequest>,
server_state: web::Data<Arc<ServerState>>
) -> Result<HttpResponse, EnqueueCreateVoiceRequestError> {
// voice data set token ... zsd_wb08asm0m4a4cz42wwqked6tfg6e e.g
// need this for the job
// -> create_voice_records
// Implementation for creating a voice
let mut maybe_user_token: Option<UserToken> = None;
let mut mysql_connection = server_state.mysql_pool.acquire().await.map_err(|err| {
warn!("MySql pool error: {:?}", err);
EnqueueCreateVoiceRequestError::ServerError
})?;
// ==================== USER SESSION ==================== //
let maybe_user_session = server_state.session_checker
.maybe_get_user_session_extended_from_connection(&http_request, &mut mysql_connection).await
.map_err(|e| {
warn!("Session checker error: {:?}", e);
EnqueueCreateVoiceRequestError::ServerError
})?;
if let Some(user_session) = maybe_user_session.as_ref() {
maybe_user_token = Some(UserToken::new_from_str(&user_session.user_token));
}
// 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()
);
// TODO: Separate priority for animation.
let priority_level = plan.web_vc_base_priority_level();
// ==================== DEBUG MODE + ROUTING TAG ==================== //
let is_debug_request = get_request_header_optional(&http_request, DEBUG_HEADER_NAME).is_some();
let maybe_routing_tag = get_request_header_optional(&http_request, ROUTING_TAG_HEADER_NAME).map(
|routing_tag| routing_tag.trim().to_string()
);
// ==================== RATE LIMIT ==================== //
let rate_limiter = match maybe_user_session {
None => &server_state.redis_rate_limiters.logged_out,
Some(ref user) => {
if user.role.is_banned {
return Err(EnqueueCreateVoiceRequestError::NotAuthorized);
}
&server_state.redis_rate_limiters.logged_in
}
};
if let Err(_err) = rate_limiter.rate_limit_request(&http_request) {
return Err(EnqueueCreateVoiceRequestError::RateLimited);
}
println!("Recieved payload for voice creation.");
// Get up IP address
let ip_address = get_request_ip(&http_request);
// package as larger component args should always have an embedding token ..
let inference_args = TTSArgs {
voice_token: None,
dataset_token: Some(request.voice_dataset_token.clone()),
};
let maybe_avt_token = server_state.avt_cookie_manager.get_avt_token_from_request(&http_request);
// create the inference args here
// enqueue a zero shot tts request here...
// create the job record here! explore the table insert the inference args in here as json! keep it short
let query_result = insert_generic_inference_job(InsertGenericInferenceArgs {
uuid_idempotency_token: &request.uuid_idempotency_token,
inference_category: InferenceCategory::TextToSpeech,
maybe_model_type: Some(InferenceModelType::VallEX), // NB: Model is static during inference
maybe_model_token: None, // NB: Model is static during inference
maybe_input_source_token: None,
maybe_input_source_token_type: None,
maybe_raw_inference_text: None,
maybe_inference_args: Some(GenericInferenceArgs {
inference_category: Some(InferenceCategoryAbbreviated::TextToSpeech),
args: Some(PolymorphicInferenceArgs::Tts(inference_args)),
}),
maybe_creator_user_token: maybe_user_token.as_ref(),
creator_ip_address: &ip_address,
creator_set_visibility: enums::common::visibility::Visibility::Public,
priority_level: priority_level,
requires_keepalive: true,
maybe_avt_token: maybe_avt_token.as_ref(),
is_debug_request: is_debug_request,
maybe_routing_tag: maybe_routing_tag.as_deref(),
mysql_pool: &server_state.mysql_pool,
}).await;
let job_token = match query_result {
Ok((job_token, _id)) => job_token,
Err(err) => {
warn!("New generic inference job creation DB error: {:?}", err);
return Err(EnqueueCreateVoiceRequestError::ServerError);
}
};
let response: EnqueueCreateVoiceRequestSuccessResponse =
EnqueueCreateVoiceRequestSuccessResponse {
success: true,
inference_job_token: job_token,
};
let body = serde_json
::to_string(&response)
.map_err(|_e| EnqueueCreateVoiceRequestError::ServerError)?;
// Error handling 101 rust result type returned like so.
Ok(HttpResponse::Ok().content_type("application/json").body(body))
}
@@ -241,7 +241,7 @@ async fn main() -> AnyhowResult<()> {
let website_homepage_redirect =
easyenv::get_env_string_or_default("WEBSITE_HOMEPAGE_REDIRECT", "https://vo.codes/");
let session_cookie_manager = SessionCookieManager::new(&cookie_domain, &hmac_secret);
let session_cookie_manager = SessionCookieManager::new(&cookie_domain, &hmac_secret)?;
let avt_cookie_manager = AvtCookieManager::new(&cookie_domain, &hmac_secret)?;
let session_checker = SessionChecker::new_with_cache(
@@ -31,6 +31,7 @@ use crate::http_server::endpoints::flags::design_refresh_flag::disable_design_re
use crate::http_server::endpoints::flags::design_refresh_flag::enable_design_refresh_flag_handler::enable_design_refresh_flag_handler;
use crate::http_server::endpoints::inference_job::get_inference_job_status::get_inference_job_status_handler;
use crate::http_server::endpoints::inference_job::get_pending_inference_job_count::get_pending_inference_job_count_handler;
use crate::http_server::endpoints::inference_job::terminate_inference_job_handler::terminate_inference_job_handler;
use crate::http_server::endpoints::investor_demo::disable_demo_mode_handler::disable_demo_mode_handler;
use crate::http_server::endpoints::investor_demo::enable_demo_mode_handler::enable_demo_mode_handler;
use crate::http_server::endpoints::leaderboard::get_leaderboard::leaderboard_handler;
@@ -171,7 +172,7 @@ pub fn add_routes<T, B> (app: App<T>) -> App<T>
app = add_subscription_routes(app); /* /v1/subscriptions/... */
// TODO find a long term feature flag solution, since this code is likely deployed into production we don't want the route found.
let enable_voice_designer_route = false;
let enable_voice_designer_route = true;
if enable_voice_designer_route {
app = add_voice_designer_routes(app); /* /v1/voice_designer */
}
@@ -193,6 +194,7 @@ pub fn add_routes<T, B> (app: App<T>) -> App<T>
let mut app = RouteBuilder::from_app(app)
.add_get("/v1/model_inference/job_status/{token}", get_inference_job_status_handler)
.add_delete("/v1/model_inference/job/{token}", terminate_inference_job_handler, true)
.add_get("/v1/model_inference/queue_length", get_pending_inference_job_count_handler)
.into_app();