mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-08 15:14:16 -05:00
Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a5e4538a0 | ||
|
|
3f70ca72dd | ||
|
|
d8a4b6c735 | ||
|
|
48ed6d118c | ||
|
|
4c3ac3edda | ||
|
|
dae5764658 | ||
|
|
98547df063 | ||
|
|
9c856235fc | ||
|
|
205f6de1b2 | ||
|
|
a9fa45575e | ||
|
|
1b77974a0e | ||
|
|
724ae2570e | ||
|
|
dc7b5b7e60 | ||
|
|
3259433717 | ||
|
|
bc5d84a306 | ||
|
|
2357f00944 | ||
|
|
24127f20e4 | ||
|
|
4c8f14d4d5 | ||
|
|
e14440ba02 | ||
|
|
7295a3295f | ||
|
|
10176079c7 | ||
|
|
8999a80e92 | ||
|
|
41b07049ff | ||
|
|
b45f6ed691 | ||
|
|
9d16eb05c1 | ||
|
|
b414540251 | ||
|
|
2949456adc | ||
|
|
3a72292898 | ||
|
|
07c640ce84 | ||
|
|
49a15fd1f1 | ||
|
|
3267f9949d | ||
|
|
47df6930d2 | ||
|
|
913d282d60 | ||
|
|
743934ff50 | ||
|
|
bc209fe939 | ||
|
|
302975ddfd | ||
|
|
d7664e8e1c | ||
|
|
9f7fc09e9f | ||
|
|
2686cb94e0 | ||
|
|
fd7f1e1893 | ||
|
|
eb3499596e | ||
|
|
00bfdad3d0 | ||
|
|
ff3821956a | ||
|
|
a5a5f5473b | ||
|
|
7c553ffcee | ||
|
|
52f0b44c26 | ||
|
|
fbed7cd530 | ||
|
|
4ce8752a6a | ||
|
|
3239e318b1 | ||
|
|
471dcff697 | ||
|
|
2af57fcf3e | ||
|
|
f2b0e77210 | ||
|
|
10c8bc1ef3 | ||
|
|
35dfe6a7f2 | ||
|
|
48ecc44978 | ||
|
|
4f0e2d5f72 | ||
|
|
96c99fd102 | ||
|
|
26794b3d2e | ||
|
|
c3a60ed8ab | ||
|
|
13964901f0 | ||
|
|
593fb50fcb | ||
|
|
57eecb3757 | ||
|
|
25cdbf2f1d | ||
|
|
0f4cab9ca3 | ||
|
|
6de4d451ac | ||
|
|
9d4e426ace | ||
|
|
bfb5103f46 | ||
|
|
3308c05836 | ||
|
|
be19eefa1a | ||
|
|
34e2b1e544 | ||
|
|
cbe8aaa1ba | ||
|
|
11185584a7 | ||
|
|
76559cf783 | ||
|
|
2e7bd622f3 | ||
|
|
68048659d4 | ||
|
|
17d51f8d87 | ||
|
|
5f7a10a54c | ||
|
|
7eee225296 | ||
|
|
1319471c04 | ||
|
|
3e8b6f39fe | ||
|
|
a0b8849806 | ||
|
|
bb0a4958ef | ||
|
|
a256681f6d | ||
|
|
ec03420efc | ||
|
|
264058ea87 | ||
|
|
3845c632b2 | ||
|
|
22518d9164 | ||
|
|
5dbed7afbb | ||
|
|
a485666b1c | ||
|
|
54e9b8af70 |
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE password_resets
|
||||
MODIFY ip_address_redemption VARCHAR(40) NOT NULL;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE password_resets
|
||||
MODIFY ip_address_redemption VARCHAR(40) DEFAULT NULL;
|
||||
+13
@@ -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';
|
||||
+16
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod job_status;
|
||||
pub mod job_status_plus;
|
||||
pub mod sqlite;
|
||||
pub mod visibility;
|
||||
pub mod vocoder_type;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
®ion_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,
|
||||
®ion_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.
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+1340
-1167
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,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>
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+9
-3
@@ -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,
|
||||
|
||||
+33
@@ -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(()),
|
||||
}
|
||||
}
|
||||
+2
@@ -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;
|
||||
|
||||
+57
-6
@@ -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 +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))
|
||||
}
|
||||
+2
@@ -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(),
|
||||
|
||||
+175
@@ -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))
|
||||
}
|
||||
+157
@@ -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;
|
||||
|
||||
+4
-4
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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;
|
||||
|
||||
|
||||
+2
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-3
@@ -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;
|
||||
+44
-41
@@ -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!")));
|
||||
}
|
||||
|
||||
+443
-263
@@ -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,
|
||||
|
||||
+8
@@ -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()),
|
||||
};
|
||||
|
||||
+5
-2
@@ -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,
|
||||
|
||||
+33
-11
@@ -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;
|
||||
+134
@@ -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))
|
||||
}
|
||||
+3
@@ -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,
|
||||
|
||||
+6
-1
@@ -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,
|
||||
|
||||
+161
-125
@@ -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))
|
||||
}
|
||||
|
||||
+3
-3
@@ -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,
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
|
||||
+211
-7
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user