diff --git a/.env-download-job b/.env-download-job index 032eac7e27..a2d17913b0 100644 --- a/.env-download-job +++ b/.env-download-job @@ -6,11 +6,19 @@ WEB_DOWNLOADER_COMMAND=./download_internet_file.py WEB_DOWNLOADER_MAYBE_VENV_SCRIPT=./python/bin/activate WEB_DOWNLOADER_MAYBE_DOCKER_IMAGE=d73f28ce3ff6 +SOFTVC_MODEL_CHECK_ROOT_DIRECTORY=/ +SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" +SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=a79106991ce3 + TACOTRON_MODEL_CHECK_ROOT_DIRECTORY=/models/tts TACOTRON_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" TACOTRON_MODEL_CHECK_COMMAND=./vocodes_model_check_tacotron.py TACOTRON_MODEL_CHECK_MAYBE_DOCKER_IMAGE=5642d0fd7fc1 +HIFIGAN_SOFTVC_MODEL_CHECK_ROOT_DIRECTORY=/ +HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" +HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE=a79106991ce3 + HIFIGAN_MODEL_CHECK_ROOT_DIRECTORY=/models/tts HIFIGAN_MODEL_CHECK_MAYBE_VENV_COMMAND="source python/bin/activate" HIFIGAN_MODEL_CHECK_COMMAND=./vocodes_model_check_hifigan.py diff --git a/.idea/dictionaries/bt.xml b/.idea/dictionaries/bt.xml index 50b698c5ca..be0599beae 100644 --- a/.idea/dictionaries/bt.xml +++ b/.idea/dictionaries/bt.xml @@ -7,9 +7,14 @@ cheermote crockford fakeyou + ffprobe hifigan scrollback + softvc + symphonia tacotron + transcoded + transcodings upserting vocoder vocoders diff --git a/Cargo.lock b/Cargo.lock index 438c6a6ddd..ad01d3f8d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -667,6 +667,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "ascii" version = "0.9.3" @@ -711,7 +717,7 @@ dependencies = [ "smol_str", "thiserror", "tokio 1.16.1", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -975,12 +981,26 @@ dependencies = [ "libc", ] +[[package]] +name = "buckets" +version = "0.0.1" +dependencies = [ + "crockford", + "errors", +] + [[package]] name = "bumpalo" version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" +[[package]] +name = "bytemuck" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" + [[package]] name = "byteorder" version = "1.4.3" @@ -1040,7 +1060,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca453e8624711b2f0f4eb47076a318feda166252a827ee25d067b43de83dcba0" dependencies = [ "byteorder", - "uuid", + "uuid 0.8.2", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid 1.2.2", ] [[package]] @@ -1115,6 +1146,18 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "cloud_storage" +version = "0.0.1" +dependencies = [ + "anyhow", + "buckets", + "container_common", + "log", + "rust-s3", + "tokio 1.16.1", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -1205,12 +1248,12 @@ dependencies = [ "log", "once_cell", "rand 0.8.4", - "ring 0.17.0-alpha.10", + "ring 0.17.0-alpha.11", "serde", "serde_json", "tempfile", "toml", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -1314,6 +1357,13 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crockford" +version = "0.0.1" +dependencies = [ + "rand 0.8.4", +] + [[package]] name = "crossbeam" version = "0.8.1" @@ -1415,9 +1465,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" [[package]] name = "database-backup" @@ -1462,10 +1512,13 @@ version = "0.0.1" dependencies = [ "anyhow", "base64 0.13.0", + "buckets", "chrono", "config", "container_common", "dotenv", + "enums", + "errors", "log", "once_cell", "regex", @@ -1618,11 +1671,13 @@ dependencies = [ "anyhow", "chrono", "clap 2.34.0", + "cloud_storage", "config", "container_common", "database_queries", "dotenv", "easyenv", + "enums", "google_drive_common", "hostname", "hyper 0.14.14", @@ -1636,7 +1691,6 @@ dependencies = [ "serde_derive", "serde_json", "sqlx", - "storage_buckets_common", "subprocess", "subprocess_common", "tempdir", @@ -1691,6 +1745,19 @@ dependencies = [ "syn", ] +[[package]] +name = "enums" +version = "0.0.1" +dependencies = [ + "serde", + "serde_derive", + "serde_json", + "sqlx", + "sqlx-core", + "strum", + "toml", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -1704,6 +1771,13 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errors" +version = "0.0.1" +dependencies = [ + "anyhow", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -1719,6 +1793,15 @@ dependencies = [ "instant", ] +[[package]] +name = "files" +version = "0.0.1" +dependencies = [ + "data-encoding", + "infer 0.11.0", + "ring 0.17.0-alpha.11", +] + [[package]] name = "firestorm" version = "0.4.6" @@ -2353,6 +2436,7 @@ dependencies = [ "base64 0.13.0", "bcrypt", "chrono", + "cloud_storage", "config", "container_common", "data-encoding", @@ -2392,14 +2476,13 @@ dependencies = [ "regex", "reqwest", "reusable_types", - "ring 0.17.0-alpha.10", + "ring 0.17.0-alpha.11", "rust-s3", "serde", "serde_derive", "serde_json", "sha2 0.9.5", "sqlx", - "storage_buckets_common", "subprocess", "tempdir", "time 0.2.26", @@ -2408,7 +2491,7 @@ dependencies = [ "tts_common", "twitch_common", "user_input_common", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -2433,7 +2516,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea70330449622910e0edebab230734569516269fb32342fb0a8956340fa48c6c" dependencies = [ - "cfb", + "cfb 0.4.0", +] + +[[package]] +name = "infer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6c16b11a665b26aeeb9b1d7f954cdeb034be38dd00adab4f2ae921a8fee804" +dependencies = [ + "cfb 0.7.3", ] [[package]] @@ -2443,11 +2535,14 @@ dependencies = [ "anyhow", "chrono", "clap 2.34.0", + "cloud_storage", "config", "container_common", "database_queries", "dotenv", "easyenv", + "enums", + "errors", "hostname", "hyper 0.14.14", "jobs_common", @@ -2460,9 +2555,9 @@ dependencies = [ "serde_derive", "serde_json", "sqlx", - "storage_buckets_common", "subprocess", "tempdir", + "tokens", "tokio 1.16.1", "tts_common", ] @@ -2607,7 +2702,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", "bitflags", "cfg-if 1.0.0", "ryu", @@ -2800,6 +2895,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "media" +version = "0.0.1" +dependencies = [ + "errors", + "log", + "symphonia", +] + [[package]] name = "memchr" version = "2.4.0" @@ -3007,7 +3111,7 @@ dependencies = [ "serde", "serde_json", "tokio 1.16.1", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -3827,9 +3931,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.0-alpha.10" +version = "0.17.0-alpha.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ae9a4d2975bdd8254d7bcdd2261be62b42b8920f903ec682b08cdc11b87af2" +checksum = "4575a179070909595bea5f999d67934737c2e0757a1eb9839af555917817b257" dependencies = [ "cc", "libc", @@ -4475,17 +4579,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" -[[package]] -name = "storage_buckets_common" -version = "0.0.1" -dependencies = [ - "anyhow", - "container_common", - "log", - "rust-s3", - "tokio 1.16.1", -] - [[package]] name = "storyteller-web" version = "0.0.1" @@ -4505,7 +4598,9 @@ dependencies = [ "base64 0.13.0", "bcrypt", "billing_component", + "buckets", "chrono", + "cloud_storage", "config", "container_common", "data-encoding", @@ -4514,6 +4609,8 @@ dependencies = [ "diesel", "dotenv", "easyenv", + "enums", + "files", "futures 0.1.31", "futures 0.3.17", "futures-util", @@ -4535,6 +4632,7 @@ dependencies = [ "lru_time_cache", "magic-crypt", "md-5", + "media", "memory_caching", "native-tls", "newrelic-telemetry", @@ -4546,7 +4644,7 @@ dependencies = [ "regex", "reqwest", "reusable_types", - "ring 0.17.0-alpha.10", + "ring 0.17.0-alpha.11", "rust-s3", "serde", "serde_derive", @@ -4554,8 +4652,8 @@ dependencies = [ "sha2 0.9.5", "speculoos", "sqlx", - "storage_buckets_common", "subprocess", + "symphonia", "tempdir", "time 0.2.26", "tokens", @@ -4568,7 +4666,7 @@ dependencies = [ "url_config", "user_input_common", "users_component", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -4639,6 +4737,178 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +[[package]] +name = "symphonia" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17033fe05e4f7f10a6ad602c272bafd2520b2e5cdd9feb61494d9cdce08e002f" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-alac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-wav", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044f655337892b217d6df1d8336ee119414c3886c89c72e0156989cd2ad7934a" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5d3d53535ae2b7d0e39e82f683cac5398a6c8baca25ff1183e107d13959d3e" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9431b89428c31b01428563df18e52b1aff7c49e71fb80b2fa8e85632094776" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452438d6f32bf07f2f55f35371c3c8e9cce4a028a08b47fb301001f85a950f02" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cca412c954abda6ab62b5e51223568eb604ed8266ec777d99e1d63c608443c" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323b94435a1a807e1001e29490aeaef2660fb72b145d47497e8429a6cb1d67c3" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199a6417cd4115bac79289b64b859358ea050b7add0ceb364dc991f628c5b347" +dependencies = [ + "arrayvec 0.7.2", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dce84cea63247dfcc43e12edf2d19406fbb62ebc24bac1b8e6c49f1ba67b890" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f405400330b2cc0c70e19515198628ae9a6a99a59c77800541127d4cff113b96" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2f741469a0f103607ed1f2605f7f00b13ba044ea9ddc616764558c6d3d9b7d" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-wav" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9d771aa6889f05b771e629110f6a60b5e1c0ad580fc41da574bc490fbe2822" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed71acf6b5e6e8bee1509597b86365a06b78c1d73218df47357620a6fe5997b" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbb0766ce77a8aef535f9438db645e7b6f1b2c4cf3be9bf246b4e11a7d5531" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.103" @@ -4843,7 +5113,6 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" name = "tokens" version = "0.0.1" dependencies = [ - "anyhow", "rand 0.8.4", "serde", "serde_derive", @@ -5173,6 +5442,7 @@ dependencies = [ "anyhow", "chrono", "clap 2.34.0", + "cloud_storage", "config", "container_common", "database_queries", @@ -5190,7 +5460,6 @@ dependencies = [ "serde_derive", "serde_json", "sqlx", - "storage_buckets_common", "subprocess", "tempdir", "tokio 1.16.1", @@ -5204,6 +5473,7 @@ dependencies = [ "anyhow", "chrono", "clap 2.34.0", + "cloud_storage", "config", "container_common", "database_queries", @@ -5221,7 +5491,6 @@ dependencies = [ "serde_derive", "serde_json", "sqlx", - "storage_buckets_common", "subprocess", "tempdir", "tokio 1.16.1", @@ -5329,7 +5598,7 @@ dependencies = [ "twitch_api2", "twitch_common", "twitch_oauth2", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -5555,6 +5824,7 @@ dependencies = [ "container_common", "database_queries", "dotenv", + "enums", "hmac 0.10.1", "http_server_common", "jwt", @@ -5590,6 +5860,12 @@ dependencies = [ "getrandom 0.2.2", ] +[[package]] +name = "uuid" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" + [[package]] name = "vcpkg" version = "0.2.12" @@ -5619,8 +5895,10 @@ name = "w2l-download-job" version = "0.0.1" dependencies = [ "anyhow", + "buckets", "chrono", "clap 2.34.0", + "cloud_storage", "config", "container_common", "database_queries", @@ -5638,7 +5916,6 @@ dependencies = [ "serde_derive", "serde_json", "sqlx", - "storage_buckets_common", "subprocess", "tempdir", "tokio 1.16.1", @@ -5652,6 +5929,7 @@ dependencies = [ "anyhow", "chrono", "clap 2.34.0", + "cloud_storage", "config", "container_common", "database_queries", @@ -5669,7 +5947,6 @@ dependencies = [ "serde_derive", "serde_json", "sqlx", - "storage_buckets_common", "subprocess", "tempdir", "tokio 1.16.1", @@ -5862,7 +6139,7 @@ dependencies = [ "twitch_api2", "twitch_common", "twitch_oauth2", - "uuid", + "uuid 0.8.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index af9d86a146..cd024f28fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,13 @@ [workspace] members = [ + # Types: common strongly-typed tokens, ids, payloads, etc. + # Also common cloud buckets and other "strongly typed" configs. + "crates/_types/buckets", + "crates/_types/enums", + "crates/_types/reusable_types", # TODO(bt, 2022-12-21): Deprecate + "crates/_types/tokens", + # CLI tools for operations, etc. "crates/cli/database_backup", "crates/cli/database_cleanup", @@ -11,32 +18,34 @@ members = [ "crates/plugins/users", # Shared libraries + "crates/lib/cloud_storage", "crates/lib/config", "crates/lib/container_common", + "crates/lib/crockford", "crates/lib/database_queries", + "crates/lib/errors", + "crates/lib/files", "crates/lib/google_drive_common", "crates/lib/http_server_common", "crates/lib/jobs_common", + "crates/lib/media", "crates/lib/memory_caching", "crates/lib/redis_common", - "crates/lib/reusable_types", - "crates/lib/storage_buckets_common", "crates/lib/subprocess_common", - "crates/lib/tokens", "crates/lib/tts_common", "crates/lib/twitch_common", "crates/lib/url_config", "crates/lib/user_input_common", # Services: API servers, jobs, workers, etc. - "crates/service/download_job", - "crates/service/image_generation_service", - "crates/service/inference_job", - "crates/service/storyteller_web", - "crates/service/tts_download_job", - "crates/service/tts_inference_job", - "crates/service/twitch_pubsub_subscriber", - "crates/service/w2l_download_job", - "crates/service/w2l_inference_job", - "crates/service/websocket_gateway", + "crates/service/job/download_job", + "crates/service/job/inference_job", + "crates/service/job/tts_download_job", + "crates/service/job/tts_inference_job", + "crates/service/job/w2l_download_job", + "crates/service/job/w2l_inference_job", + "crates/service/other/twitch_pubsub_subscriber", + "crates/service/web/image_generation_service", + "crates/service/web/storyteller_web", + "crates/service/web/websocket_gateway", ] diff --git a/Dockerfile b/Dockerfile index dd52eb70d6..709e9f751f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ FROM ubuntu:jammy as rust-base # NB: This can be "stable" or another version. -ARG RUST_TOOLCHAIN="1.65.0" +ARG RUST_TOOLCHAIN="1.66.0" WORKDIR /tmp @@ -64,6 +64,7 @@ COPY Cargo.toml . COPY sqlx-data.json . COPY crates/ ./crates COPY db/ ./db +COPY test_data/ ./test_data # Run all of the tests RUN SQLX_OFFLINE=true \ diff --git a/_migrations/2022-08-05-202017_generic_download_jobs/up.sql b/_migrations/2022-08-05-202017_generic_download_jobs/up.sql index 17a883e0a6..2b84c5bde7 100644 --- a/_migrations/2022-08-05-202017_generic_download_jobs/up.sql +++ b/_migrations/2022-08-05-202017_generic_download_jobs/up.sql @@ -22,12 +22,13 @@ CREATE TABLE generic_download_jobs ( -- The type of the object will vary based on the type of the upload, -- and we may include heuristics that auto-detect types in the future + -- Example types: 'hifigan', 'hifigan_rocket_vc' (codename for softvc), 'tacotron2', ... on_success_downloaded_entity_type VARCHAR(32) DEFAULT NULL, -- ========== UPLOAD DETAILS ========== -- A "well defined" type of upload - -- The first upload type will be "hifigan_model" + -- Examples: "hifigan", "tacotron2", etc. download_type VARCHAR(32) NOT NULL, -- Assume the same URL can be uploaded more than once (in case of errors or diff --git a/_migrations/2022-08-05-202110_vocoder_models/up.sql b/_migrations/2022-08-05-202110_vocoder_models/up.sql index 1ee7c74c58..d11793c780 100644 --- a/_migrations/2022-08-05-202110_vocoder_models/up.sql +++ b/_migrations/2022-08-05-202110_vocoder_models/up.sql @@ -10,7 +10,9 @@ CREATE TABLE vocoder_models ( token VARCHAR(32) NOT NULL, -- The architecture of the vocoder. - -- Currently supported values: 'hifigan' + -- Currently supported values: + -- * 'hifigan' - hifigan for Tacotron2 + -- * 'hifigan_rocket_vc' - hifigan for SoftVC ("rocket" is a codename) vocoder_type VARCHAR(32) NOT NULL, -- ========== MUTABLE METADATA ========== diff --git a/_migrations/2022-12-14-050849_generic_inference_jobs/down.sql b/_migrations/2022-12-14-050849_generic_inference_jobs/down.sql new file mode 100644 index 0000000000..78a9716f01 --- /dev/null +++ b/_migrations/2022-12-14-050849_generic_inference_jobs/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE generic_inference_jobs; \ No newline at end of file diff --git a/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql b/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql new file mode 100644 index 0000000000..b547482249 --- /dev/null +++ b/_migrations/2022-12-14-050849_generic_inference_jobs/up.sql @@ -0,0 +1,150 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE generic_inference_jobs ( + + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + -- This is so the in-progress results can be looked up by the UI. + token VARCHAR(32) NOT NULL, + + -- Idempotency token from client + -- This is so the frontend client doesn't submit duplicate jobs. + uuid_idempotency_token VARCHAR(36) NOT NULL, + + -- ========== INFERENCE DETAILS ========== + + -- Type of inference + inference_type VARCHAR(32) NOT NULL, + + -- Polymorphic arguments payload that depends on the type of inference job. + -- TEXT = 65,535 bytes (64 KiB), ~= 4 bytes per UTF-8 character, ~ 16383 characters. + maybe_inference_args TEXT DEFAULT NULL, + + -- For text-based workloads, the raw, unprocessed user input. + -- TEXT = 65,535 bytes (64 KiB), ~= 4 bytes per UTF-8 character, ~ 16383 characters. + maybe_raw_inference_text TEXT DEFAULT NULL, + + -- Potential foreign key to the primary model being used, if any. + -- This will normally live in `maybe_inference_args`, but in this case, it's useful for + -- running easy database analytical queries. + maybe_model_token VARCHAR(32) DEFAULT NULL, + + -- ========== SUCCESS CASE ========== + + -- The type of the object will vary based on the type of the upload, + -- and we may include heuristics that auto-detect types in the future + on_success_result_entity_type VARCHAR(32) DEFAULT NULL, + + -- If the job completes successfully, this is the result token. + -- This is only populated on a successful result. + on_success_result_entity_token VARCHAR(32) DEFAULT NULL, + + -- ========== CREATOR DETAILS AND PREFERENCES ========== + + -- Foreign key to user + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + -- The creator can set a desired visibility for their data. + -- This does not always apply to every upload type. + -- Additionally, some upload types may require moderator approval prior + -- to being publicly listed, and this field has no bearing on that. + -- NB: DO NOT CHANGE THE ORDER OF THE ENUM VALUES DURING SCHEMA MIGRATIONS. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + -- ========== PREMIUM FEATURES METADATA ========== + + -- The maximum duration for generated audio in seconds. + -- Zero is implied to be the default value, which is typically 12 seconds. + -- A negative value implies "unlimited". + max_duration_seconds INTEGER NOT NULL DEFAULT 0, + + -- ========== WORKLOAD SOURCE AND PRIORITY ========== + + -- Priority *increases*, so a level of 2 will be higher than 1. + -- These are the level semantics currently: + -- - All jobs from anonymous FakeYou users have level 0. + -- - All jobs from logged in FakeYou users have level 1. + -- - All jobs from Twitch TTS (unpaid) have level 10 (ten). + -- - (There will be future levels for paid Twitch and social FakeYou rewards.) + priority_level SMALLINT UNSIGNED NOT NULL DEFAULT 0, + + is_from_premium_user BOOLEAN NOT NULL DEFAULT FALSE, + is_from_api_user BOOLEAN NOT NULL DEFAULT FALSE, + is_for_twitch BOOLEAN NOT NULL DEFAULT FALSE, + + -- If true, the request gets routed to a special "debug" worker. + is_debug_request BOOLEAN NOT NULL DEFAULT FALSE, + + -- ========== JOB SYSTEM DETAILS ========== + + -- Jobs begin as "pending", then transition to other states. + -- + -- * Pending = job is ready to go + -- * Started = job is running + -- * Complete_Success = job is done (success) + -- * Complete_Failure = job is done (failure) + -- * Attempt_Failed = job failed but may retry. + -- * Dead = job failed permanently. + -- + -- Pending -> Started -> Complete_Success + -- |-> Complete_Failure + -- \-> Attempt_Failed -> Started -> { Complete, Failed, Dead } + status ENUM( + 'pending', + 'started', + 'complete_success', + 'complete_failure', + 'attempt_failed', + 'dead') NOT NULL DEFAULT 'pending', + + -- We can track this against a "max_attempt_count" + attempt_count SMALLINT UNSIGNED NOT NULL DEFAULT 0, + + -- If there is a failure, tell the user why. + failure_reason VARCHAR(512) DEFAULT NULL, + + -- Optional internal-only debugging information in the case of failure. + internal_debugging_failure_reason VARCHAR(512) DEFAULT NULL, + + -- The last worker (hostname or pod name) to touch the job, either in the case of success or failure. + last_assigned_worker VARCHAR(255) DEFAULT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Failed jobs will set a next attempt time. + -- Subsequent tries can increase the timeout. + -- Failures because of permissions require human intervention => [retry_at=null]. + -- Failures because of invalid files are dead => [status=dead]. + retry_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + UNIQUE KEY (uuid_idempotency_token), + KEY index_inference_type (inference_type), + KEY fk_maybe_model_token (maybe_model_token), + KEY fk_on_success_result_entity_token (on_success_result_entity_token), + KEY fk_maybe_creator_user_token (maybe_creator_user_token), + KEY index_creator_ip_address (creator_ip_address), + KEY index_priority_level (priority_level), + KEY index_is_debug_request (is_debug_request), + KEY index_status (status) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations/2022-12-15-060826_media_uploads/down.sql b/_migrations/2022-12-15-060826_media_uploads/down.sql new file mode 100644 index 0000000000..4f7f40fe13 --- /dev/null +++ b/_migrations/2022-12-15-060826_media_uploads/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +drop table media_uploads; diff --git a/_migrations/2022-12-15-060826_media_uploads/up.sql b/_migrations/2022-12-15-060826_media_uploads/up.sql new file mode 100644 index 0000000000..c5c8b50d61 --- /dev/null +++ b/_migrations/2022-12-15-060826_media_uploads/up.sql @@ -0,0 +1,135 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE media_uploads ( + + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + -- This is so the in-progress results can be looked up by the UI. + token VARCHAR(32) NOT NULL, + + -- Idempotency token from client + -- This is so the frontend client doesn't submit duplicate jobs. + uuid_idempotency_token VARCHAR(36) NOT NULL, + + -- ========== MEDIA DETAILS ========== + + -- Type of media: + -- * 'audio' for wav, mp3, etc. + -- * 'video' for a variety of video types. + media_type VARCHAR(16) NOT NULL, + + -- The original filename of the media + maybe_original_filename VARCHAR(255) DEFAULT NULL, + + -- The original (non-transcoded or truncated) file size + original_file_size_bytes INT(10) NOT NULL DEFAULT 0, + + -- The original (non-transcoded or truncated) duration in millis. + original_duration_millis INT(10) NOT NULL DEFAULT 0, + + -- The original file's mime type. + maybe_original_mime_type VARCHAR(32) DEFAULT NULL, + + -- Audio encoder details + -- Only present if the file was audio or a video that had audio. + maybe_original_audio_encoding VARCHAR(32) DEFAULT NULL, + + -- Video encoder details + -- Only present if the file was a video. + maybe_original_video_encoding VARCHAR(32) DEFAULT NULL, + + -- For videos, the original frame width. + maybe_original_frame_width INT(5) DEFAULT NULL, + + -- For videos, the original frame height. + maybe_original_frame_height INT(5) DEFAULT NULL, + + -- Checksum of the original media + -- SHA1 hash [SHA2 = CHAR(64), SHA1 = CHAR(40), MD5 = CHAR(32)] + -- checksum_sha1 CHAR(40) NOT NULL, + + -- Checksum of the original media + -- SHA2 hash [SHA2 = CHAR(64), SHA1 = CHAR(40), MD5 = CHAR(32)] + -- Note that SHA2 is a hash family (SHA-228, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256, ...) + -- and produces different lengths output depending on the choice of algorithm. + checksum_sha2 CHAR(64) NOT NULL, + + -- ========== UPLOAD, TRANSCODING, AND TRUNCATION DETAILS ========== + + -- The hash for the bucket directory that contains the original upload + -- as well as any transcodings, downsamplings, etc. + public_bucket_directory_hash VARCHAR(32) NOT NULL, + + -- TODO(bt, 2022-12-20): Before landing, rename this field be better and indicate this + -- is a *directory*. Add a second field to contain the default file name. + -- The directory this media is uploaded to will be exclusive for this file. + -- Only this given record will live in this bucket, but the directory may include + -- other transcodings or truncations. + public_bucket_directory_full_path VARCHAR(255) NOT NULL, + + -- We'll likely transcode (and potentially truncate) most media given to us. + -- This will store a json-encoded struct that details the changes. + -- TEXT = 65,535 bytes (64 KiB), ~= 4 bytes per UTF-8 character, ~ 16383 characters. + extra_file_modification_info TEXT DEFAULT NULL, + + -- ========== CREATOR DETAILS AND PREFERENCES ========== + + -- Foreign key to user + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + -- The creator can set a desired visibility for their data. + -- This does not always apply to every upload type. + -- Additionally, some upload types may require moderator approval prior + -- to being publicly listed, and this field has no bearing on that. + -- NB: DO NOT CHANGE THE ORDER OF THE ENUM VALUES DURING SCHEMA MIGRATIONS. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + -- The synthetic id associated with this result. + -- These ids are incremented on a per-user basis to help users + -- sequence their own work. They serve no other purpose. + maybe_creator_synthetic_id BIGINT(20) DEFAULT NULL, + + -- ========== MODERATION DETAILS ========== + + -- The last moderator that made changes last. + -- We should also build an audit table to supersede this. + maybe_mod_user_token VARCHAR(32) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- If this is removed by a mod or the creator. + user_deleted_at TIMESTAMP NULL, + mod_deleted_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + UNIQUE KEY (uuid_idempotency_token), + KEY index_media_type (media_type), + KEY index_checksum_sha2 (checksum_sha2), + KEY fk_maybe_creator_user_token (maybe_creator_user_token), + KEY fk_maybe_mod_user_token (maybe_mod_user_token), + KEY index_creator_ip_address (creator_ip_address), + KEY index_user_deleted_at (user_deleted_at), + KEY index_mod_deleted_at (mod_deleted_at) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations/2022-12-15-060830_audit_logs/down.sql b/_migrations/2022-12-15-060830_audit_logs/down.sql new file mode 100644 index 0000000000..10aae5c1a0 --- /dev/null +++ b/_migrations/2022-12-15-060830_audit_logs/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +drop table audit_logs; diff --git a/_migrations/2022-12-15-060830_audit_logs/up.sql b/_migrations/2022-12-15-060830_audit_logs/up.sql new file mode 100644 index 0000000000..bf902a8847 --- /dev/null +++ b/_migrations/2022-12-15-060830_audit_logs/up.sql @@ -0,0 +1,48 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE audit_logs ( + + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" of the audit log item (in case we add a UI or notes) + token VARCHAR(32) NOT NULL, + + -- The type of entity mutated + entity_type VARCHAR(32) NOT NULL, + + -- The token of the entity mutated + entity_token VARCHAR(32) NOT NULL, + + -- The type of action taken on the entity + entity_action VARCHAR(32) NOT NULL, + + -- Typically only signed in users will be performing actions on known entities, + -- but we're leaving this null just in case. This is the corresponding user that + -- made the change. + maybe_actor_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_actor_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- Whether the actor has some sort of moderator privilege. + is_actor_moderator BOOLEAN NOT NULL DEFAULT FALSE, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + actor_ip_address VARCHAR(40) NOT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY fk_entity_type (entity_type), + KEY fk_entity_token (entity_token), + KEY fk_maybe_actor_user_token (maybe_actor_user_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations/2022-12-15-223359_voice_conversion_models/down.sql b/_migrations/2022-12-15-223359_voice_conversion_models/down.sql new file mode 100644 index 0000000000..211a6a85e0 --- /dev/null +++ b/_migrations/2022-12-15-223359_voice_conversion_models/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE voice_conversion_models; diff --git a/_migrations/2022-12-15-223359_voice_conversion_models/up.sql b/_migrations/2022-12-15-223359_voice_conversion_models/up.sql new file mode 100644 index 0000000000..39d773ed28 --- /dev/null +++ b/_migrations/2022-12-15-223359_voice_conversion_models/up.sql @@ -0,0 +1,144 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE voice_conversion_models ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + token VARCHAR(32) NOT NULL, + + -- ========== MODEL FUNCTIONALITY ========== + + -- The type of voice conversion model + -- eg. 'rocket_vc' (codename for 'softvc'), 'diffsvc' + model_type VARCHAR(32) NOT NULL, + + -- If set, we'll use the vocoder defined. + -- If not set, some global default will be used. + maybe_vocoder_token VARCHAR(32) DEFAULT NULL, + + -- ========== MODEL DESCRIPTION ========== + + -- The "name" of the voice model, which might be complicated. + -- If maybe_subject_token (etc.) is set, then it's authoritative instead. + title VARCHAR(255) NOT NULL, + + -- The description of the model in markdown. + description_markdown TEXT NOT NULL, + + -- Generated HTML (not user-editable). + description_rendered_html TEXT NOT NULL, + + -- The full IETF BCP47 language tag (eg. en, en-US, es-419, ja-JP, pt, etc.) + -- (Not that it matters apart from categorization, since voice conversion is universal.) + ietf_language_tag VARCHAR(64) NOT NULL DEFAULT 'en', + + -- The IETF BCP47 language tag's primary language subtag (eg. "es-419" becomes "es") + -- (Not that it matters apart from categorization, since voice conversion is universal.) + ietf_primary_language_subtag VARCHAR(12) NOT NULL DEFAULT 'en', + + -- ========== MODEL PLACEMENT ========== + + -- Whether the voice features on the front of FakeYou + is_front_page_featured BOOLEAN NOT NULL DEFAULT FALSE, + + -- ========== IMMUTABLE PROVENANCE AND METADATA ========== + + -- Where the file was originally downloaded (if it was downloaded) + original_download_url VARCHAR(512) DEFAULT NULL, + + -- The filename that was used at upload time (if available) + original_filename VARCHAR(255) DEFAULT NULL, + + -- File characteristics + file_size_bytes INT(10) NOT NULL DEFAULT 0, + + -- ========== CREATOR DETAILS ========== + + -- The person that created the template. + creator_user_token VARCHAR(32) NOT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6. + -- This will pick up moderator IP addresses, but a future audit logs table + -- will capture all edits. + ip_address_creation VARCHAR(40) NOT NULL, + ip_address_last_update VARCHAR(40) NOT NULL, + + -- The last user to edit the model. This could be the creator or a moderator. + -- Check the future audit logs table for more info. + maybe_last_update_user_token VARCHAR(32) DEFAULT NULL, + + -- (THIS MIGHT NOT BE USED) + -- NB: DO NOT SORT! + -- THIS MUST MATCH THE RESPECTIVE JOBS TABLE. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + -- ========== BUCKET STORAGE ========== + + -- The pytorch model + -- For now, this will be a hash of the file contents. + -- NB: NOT UNIQUE! We can allow duplicate uploads. + private_bucket_hash CHAR(64) NOT NULL, + + -- The "full url" version of the path + private_bucket_object_name VARCHAR(255) NOT NULL, + + -- ========== RATING AND RANKING ========== + + -- Calculated average, on a scale of 0-100 + -- Null with zero ratings. + -- calculated_average_score INT(3) DEFAULT NULL, + + -- Other metrics + -- calculated_total_ratings_submitted_count INT(10) NOT NULL DEFAULT 0, + -- calculated_total_uses_count BIGINT(10) NOT NULL DEFAULT 0, + + -- ========== MODERATION DETAILS ========== + + -- Mods may have to approve of vc models for them to show up in a public index. + -- Models can be used by the author (or other parties that know the URL) before + -- they're approved, but unapproved models won't show up in public indices. + -- This is independent of other visibility controls (is_mod_disabled, deleted_at, + -- and creator_set_visibility) + is_public_listing_approved BOOLEAN DEFAULT NULL, + + -- Extremely popular models may be locked from deletion or modification by users. + is_locked_from_user_modification BOOLEAN NOT NULL DEFAULT FALSE, + + -- If a moderator has comments. + maybe_mod_comments VARCHAR(255) DEFAULT NULL, + + -- ========== VECTOR CLOCK ========== + + -- 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, + + -- If this is removed by a mod. + -- It completely disappears from the system. + user_deleted_at TIMESTAMP NULL, + mod_deleted_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY index_model_type (model_type), + KEY fk_maybe_vocoder_token (maybe_vocoder_token), + KEY fk_creator_user_token (creator_user_token), + KEY index_creator_set_visibility (creator_set_visibility), + KEY index_private_bucket_hash (private_bucket_hash), + KEY index_is_public_listing_approved (is_public_listing_approved), + KEY index_is_locked_from_user_modification (is_locked_from_user_modification) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations/2022-12-15-223422_voice_conversion_results/down.sql b/_migrations/2022-12-15-223422_voice_conversion_results/down.sql new file mode 100644 index 0000000000..4ecacb5a65 --- /dev/null +++ b/_migrations/2022-12-15-223422_voice_conversion_results/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE voice_conversion_results; diff --git a/_migrations/2022-12-15-223422_voice_conversion_results/up.sql b/_migrations/2022-12-15-223422_voice_conversion_results/up.sql new file mode 100644 index 0000000000..d778adf413 --- /dev/null +++ b/_migrations/2022-12-15-223422_voice_conversion_results/up.sql @@ -0,0 +1,106 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +CREATE TABLE voice_conversion_results ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + token VARCHAR(32) NOT NULL, + + -- ========== INFERENCE DETAILS ========== + + -- The TTS model that was used + model_token VARCHAR(32) NOT NULL, + + -- If set, specifies which vocoder was used. + maybe_vocoder_token VARCHAR(64) DEFAULT NULL, + + -- Which media token was used (can be audio or video) + media_token VARCHAR(32) NOT NULL, + + -- ========== CREATOR DETAILS ========== + + -- The person that created the result + -- If the user wasn't logged in, this is null + maybe_creator_user_token VARCHAR(32) DEFAULT NULL, + + -- Based on a cookie sent by the frontend. + -- We'll save this even if the user is logged in. + maybe_creator_anonymous_visitor_token VARCHAR(32) DEFAULT NULL, + + -- For abuse tracking. + -- Wide enough for IPv4/6 + creator_ip_address VARCHAR(40) NOT NULL, + + -- (THIS MIGHT NOT BE USED) + -- NB: DO NOT SORT! + -- THIS MUST MATCH THE RESPECTIVE JOBS TABLE. + creator_set_visibility ENUM( + 'public', + 'hidden', + 'private' + ) NOT NULL DEFAULT 'public', + + -- The synthetic id associated with this result. + -- These ids are incremented on a per-user basis to help users + -- sequence their own work. They serve no other purpose. + maybe_creator_synthetic_id BIGINT(20) DEFAULT NULL, + + -- ========== BUCKET STORAGE ========== + + -- Full path where the wav, spectrogram, video, etc. are located. + public_bucket_directory_full_path VARCHAR(255) NOT NULL, + + -- Name of the wav audio file (if it exists) + public_bucket_wav_audio_object_name VARCHAR(255) NOT NULL, + + -- Name of the spectrogram (if it exists) + public_bucket_spectrogram_object_name VARCHAR(255) NOT NULL, + + -- Name of the video (if it exists) + public_bucket_video_object_name VARCHAR(255) NOT NULL, + + -- ========== METADATA ========== + + file_size_bytes INT(10) NOT NULL DEFAULT 0, + duration_millis INT(10) NOT NULL DEFAULT 0, + + -- ========== INFERENCE METADATA, DC, ROUTING, DEBUGGING ========== + + -- If true, the request was routed to a special "debug" worker. + is_debug_request BOOLEAN NOT NULL DEFAULT FALSE, + + is_generated_on_prem BOOLEAN NOT NULL DEFAULT FALSE, + generated_by_worker VARCHAR(255) DEFAULT NULL, + + -- ========== MODERATION DETAILS ========== + + -- The last user (possibily moderator) that made changes. + -- See the future audit logs table for better info and accounting. + maybe_last_update_user_token VARCHAR(32) DEFAULT NULL, + + -- ========== TIMESTAMPS ========== + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- If this is removed by a mod or the creator. + user_deleted_at TIMESTAMP NULL, + mod_deleted_at TIMESTAMP NULL, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (token), + KEY fk_model_token (model_token), + KEY fk_maybe_vocoder_token (maybe_vocoder_token), + KEY fk_media_token (media_token), + KEY fk_maybe_creator_user_token (maybe_creator_user_token), + KEY fk_maybe_creator_anonymous_visitor_token (maybe_creator_anonymous_visitor_token), + KEY index_creator_ip_address (creator_ip_address), + KEY index_user_deleted_at(user_deleted_at), + KEY index_mod_deleted_at(mod_deleted_at), + KEY index_created_at(created_at) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/down.sql b/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/down.sql new file mode 100644 index 0000000000..ea17951df9 --- /dev/null +++ b/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE voice_conversion_result_synthetic_ids; diff --git a/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/up.sql b/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/up.sql new file mode 100644 index 0000000000..a950643840 --- /dev/null +++ b/_migrations/2022-12-15-223447_voice_conversion_result_synthetic_ids/up.sql @@ -0,0 +1,25 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- It gets unwieldy to show results without a numeric sequence. +-- We create a synthetic sequence for each user to make revisiting results easier. +CREATE TABLE voice_conversion_result_synthetic_ids ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + user_token VARCHAR(32) NOT NULL, + + -- The next result id. + -- After we use an id, we increment this counter. + next_id BIGINT(20) NOT NULL DEFAULT 0, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (user_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_migrations/2022-12-29-063005_media_upload_synthetic_ids/down.sql b/_migrations/2022-12-29-063005_media_upload_synthetic_ids/down.sql new file mode 100644 index 0000000000..f33aa6db4e --- /dev/null +++ b/_migrations/2022-12-29-063005_media_upload_synthetic_ids/down.sql @@ -0,0 +1,5 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +DROP TABLE media_upload_synthetic_ids; diff --git a/_migrations/2022-12-29-063005_media_upload_synthetic_ids/up.sql b/_migrations/2022-12-29-063005_media_upload_synthetic_ids/up.sql new file mode 100644 index 0000000000..2d9916f56f --- /dev/null +++ b/_migrations/2022-12-29-063005_media_upload_synthetic_ids/up.sql @@ -0,0 +1,25 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- It gets unwieldy to show results without a numeric sequence. +-- We create a synthetic sequence for each user to make revisiting results easier. +CREATE TABLE media_upload_synthetic_ids ( + -- Not used for anything except replication. + id BIGINT(20) NOT NULL AUTO_INCREMENT, + + -- Effective "primary key" (PUBLIC) + user_token VARCHAR(32) NOT NULL, + + -- The next result id. + -- After we use an id, we increment this counter. + next_id BIGINT(20) NOT NULL DEFAULT 0, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- INDICES -- + PRIMARY KEY (id), + UNIQUE KEY (user_token) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/analytics/most_popular_tts_models.sql b/analytics/tts_models.sql similarity index 60% rename from analytics/most_popular_tts_models.sql rename to analytics/tts_models.sql index 81e752cdeb..ae77c0f035 100644 --- a/analytics/most_popular_tts_models.sql +++ b/analytics/tts_models.sql @@ -3,7 +3,7 @@ -- noinspection SqlResolveForFile -- --- Top 100 models all time by use count +-- Top 500 models all time by use count -- select m.token, @@ -24,7 +24,7 @@ from ( join users as u on u.token = m.creator_user_token order by r.use_count desc - limit 100; + limit 500; -- -- Same, but simpler... @@ -32,7 +32,7 @@ order by r.use_count desc select m.token, m.title, r.use_count from ( select model_token, count(*) as use_count from tts_results group by model_token - order by use_count desc limit 100 + order by use_count desc limit 500 ) as r join tts_models as m on m.token = r.model_token; @@ -46,7 +46,7 @@ select m.token, m.title, m.ietf_language_tag, r.use_count from ( where created_at > ( CURDATE() - INTERVAL 30 DAY ) group by model_token order by use_count desc - limit 100 + limit 500 ) as r join tts_models as m on m.token = r.model_token; @@ -69,7 +69,7 @@ order by r.use_count desc limit 500; -- --- Top 100 models by use count, last 5 days, not spanish, etc. +-- Top 500 models by use count, last 5 days, not spanish, etc. -- Limited due to tmux scrollback. -- select @@ -93,7 +93,34 @@ from ( on u.token = m.creator_user_token where m.ietf_language_tag NOT IN ('es', 'es-419', 'es-ES', 'es-MX', 'pt-BR') order by r.use_count desc - limit 100; + limit 500; + +-- +-- Top 500 models by use count, last 5 days, in specific language. +-- Limited due to tmux scrollback. +-- +select + m.token, + m.title, + m.ietf_language_tag, + u.username, + r.use_count, + m.user_deleted_at, + m.mod_deleted_at, + m.created_at +from ( + select model_token, count(*) as use_count + from tts_results + where created_at > ( CURDATE() - INTERVAL 5 DAY ) + group by model_token + ) as r + join tts_models as m + on m.token = r.model_token + join users as u + on u.token = m.creator_user_token +where m.ietf_language_tag IN ('en', 'en-US', 'en-AU', 'en-CA', 'en-GB') +order by r.use_count desc + limit 500; -- -- Most popular voices by use count over 5-day window, single language, single user. @@ -121,7 +148,7 @@ from ( m.ietf_language_tag NOT IN ('es', 'es-419', 'es-ES', 'es-MX', 'pt-BR') AND u.username = 'vegito1089' order by r.use_count desc - limit 100; + limit 500; -- -- Most popular deleted models @@ -152,4 +179,63 @@ from ( join users as u on u.token = m.creator_user_token order by r.use_count desc - limit 100; + limit 500; + +-- +-- Most popular deleted models (only by users, not mods) +-- Mod-deleted models were probably on purpose +-- +select + m.token, + m.title, + m.ietf_language_tag, + u.username, + r.use_count, + m.user_deleted_at, + m.created_at +from ( + select model_token, count(*) as use_count + from tts_results + where model_token IN ( + select token + from tts_models + where + user_deleted_at IS NOT NULL + ) + group by model_token + ) as r + join tts_models as m + on m.token = r.model_token + join users as u + on u.token = m.creator_user_token +order by r.use_count desc + limit 500; + + +-- +-- Models uploaded recently (to check if they use the right text_pipeline_type, +-- vocoder -todo-, etc.) +-- +select + m.token, + m.title, + m.text_pipeline_type, + m.ietf_language_tag, + u.username, + r.use_count, + m.user_deleted_at, + m.mod_deleted_at, + m.created_at +from ( + select model_token, count(*) as use_count + from tts_results + where created_at > ( CURDATE() - INTERVAL 5 DAY ) + group by model_token + ) as r + join tts_models as m + on m.token = r.model_token + join users as u + on u.token = m.creator_user_token +order by m.created_at desc + limit 500; + diff --git a/analytics/tts_results.sql b/analytics/tts_results.sql index 7684820a6f..4b0db1a83d 100644 --- a/analytics/tts_results.sql +++ b/analytics/tts_results.sql @@ -31,3 +31,20 @@ from ( ) as sample where sample.is_generated_on_prem IS TRUE; + +-- Find TTS results for a single model +select count(*) from tts_results +where model_token = 'TM:ztt5s1be5tq6'; + +-- Find TTS results for a single model that are not deleted +select count(*) from tts_results +where model_token = 'TM:ztt5s1be5tq6' +and mod_deleted_at is NULL; + +-- Delete TTS results for a single model +-- Do this in short batches so a lock isn't held for prohibitively long. +update tts_results +set mod_deleted_at = NOW() +where model_token = 'TM:ztt5s1be5tq6' +and mod_deleted_at IS NULL +limit 5000; diff --git a/analytics/w2l_inference_jobs.sql b/analytics/w2l_inference_jobs.sql new file mode 100644 index 0000000000..40ccdb6cca --- /dev/null +++ b/analytics/w2l_inference_jobs.sql @@ -0,0 +1,7 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Pending job count +select count(*) from w2l_inference_jobs where status='pending'; + diff --git a/analytics/w2l_template_upload_jobs.sql b/analytics/w2l_template_upload_jobs.sql new file mode 100644 index 0000000000..ceafb1a5e0 --- /dev/null +++ b/analytics/w2l_template_upload_jobs.sql @@ -0,0 +1,28 @@ +-- noinspection SqlDialectInspectionForFile +-- noinspection SqlNoDataSourceInspectionForFile +-- noinspection SqlResolveForFile + +-- Get a count of pending jobs +select count(*) from w2l_template_upload_jobs where status='pending'; + +-- See pending download URLs: +select download_url from w2l_template_upload_jobs where status IN ('pending', 'started', 'attempt_failed'); + +-- See old jobs that might be stuck as "started" +select count(*) +from w2l_template_upload_jobs +where status='started' +and created_at < ( CURDATE() - INTERVAL 1 DAY ); + +-- Unstick old jobs that might be stuck as "started" +update w2l_template_upload_jobs +set status='dead' +where status='started' + and created_at < ( CURDATE() - INTERVAL 1 DAY ); + +-- Kill pending jobs with tiktok URLs: +-- These need to be blocked in the server and job directly. +update w2l_template_upload_jobs +set status='dead' +where status='pending' + and download_url LIKE '%tiktok%'; diff --git a/crates/_types/buckets/Cargo.toml b/crates/_types/buckets/Cargo.toml new file mode 100644 index 0000000000..7e78465536 --- /dev/null +++ b/crates/_types/buckets/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "buckets" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +name = "buckets" +path = "src/lib.rs" + +[dependencies] + +# Internal +crockford = { path = "../../lib/crockford" } +errors = { path = "../../lib/errors" } + +# External +# (none yet) + +[dev-dependencies] +# (none yet) diff --git a/crates/_types/buckets/src/lib.rs b/crates/_types/buckets/src/lib.rs new file mode 100644 index 0000000000..3929dc74b2 --- /dev/null +++ b/crates/_types/buckets/src/lib.rs @@ -0,0 +1,24 @@ +//! buckets +//! +//! The purpose of this library is to maintain strongly typed cloud storage buckets +//! that can be shared between projects. +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +pub mod util; + +pub mod private; +pub mod public; diff --git a/crates/_types/buckets/src/private/mod.rs b/crates/_types/buckets/src/private/mod.rs new file mode 100644 index 0000000000..27089e4df1 --- /dev/null +++ b/crates/_types/buckets/src/private/mod.rs @@ -0,0 +1 @@ +pub mod private_path; \ No newline at end of file diff --git a/crates/_types/buckets/src/private/private_path.rs b/crates/_types/buckets/src/private/private_path.rs new file mode 100644 index 0000000000..d16290a2ca --- /dev/null +++ b/crates/_types/buckets/src/private/private_path.rs @@ -0,0 +1,7 @@ + +/// A trait implemented by all private paths. +pub trait PrivatePath { + fn is_public(&self) -> bool { + false + } +} diff --git a/crates/_types/buckets/src/public/media_uploads/directory.rs b/crates/_types/buckets/src/public/media_uploads/directory.rs new file mode 100644 index 0000000000..0b713b5433 --- /dev/null +++ b/crates/_types/buckets/src/public/media_uploads/directory.rs @@ -0,0 +1,75 @@ +use crate::public::public_path::PublicPath; +use crate::util::hashed_directory_path_long_string::hashed_directory_path_long_string; +use crockford::crockford_entropy_lower; + +// TODO: Generate these from a macro. + +// TODO: Use a central path registry for quick reference +const MEDIA_UPLOAD_DIRECTORY : &'static str = "/media_upload"; + +/// Directory for user media uploads. +/// Each uploaded file gets its own directory so that we can store the original +/// file alongside re-processed derivatives. +#[derive(Clone)] +pub struct MediaUploadDirectory { + object_hash: String, + directory: String, +} + +impl PublicPath for MediaUploadDirectory {} + +impl MediaUploadDirectory { + + pub fn generate_new() -> Self { + let entropy = crockford_entropy_lower(32); + Self::from_object_hash(&entropy) + } + + pub fn from_object_hash(object_hash: &str) -> Self { + // TODO: Path construction could be cleaner. + let middle = hashed_directory_path_long_string(object_hash); + let directory = format!("{}/{}{}", MEDIA_UPLOAD_DIRECTORY, middle, object_hash); + Self { + object_hash: object_hash.to_string(), + directory, + } + } + + pub fn get_directory_path_str(&self) -> &str { + &self.directory + } + + pub fn get_object_hash(&self) -> &str { + &self.object_hash + } +} + +#[cfg(test)] +mod tests { + use crate::public::media_uploads::directory::MediaUploadDirectory; + + #[test] + pub fn generate_new_entropy() { + let directory = MediaUploadDirectory::generate_new(); + assert_eq!(directory.get_object_hash().len(), 32); + } + + #[test] + pub fn get_directory_path_str() { + let directory = MediaUploadDirectory::from_object_hash("abcdefghijk"); + assert_eq!(directory.get_directory_path_str(), "/media_upload/a/b/c/d/e/abcdefghijk"); + } + + #[test] + pub fn get_directory_path_str_short_name() { + let directory = MediaUploadDirectory::from_object_hash("foo"); + assert_eq!(directory.get_directory_path_str(), "/media_upload/f/o/foo"); + } + + #[test] + pub fn get_object_hash() { + let hash = "abcdefghijk"; + let directory = MediaUploadDirectory::from_object_hash(hash); + assert_eq!(directory.get_object_hash(), hash); + } +} diff --git a/crates/_types/buckets/src/public/media_uploads/mod.rs b/crates/_types/buckets/src/public/media_uploads/mod.rs new file mode 100644 index 0000000000..42ede393cf --- /dev/null +++ b/crates/_types/buckets/src/public/media_uploads/mod.rs @@ -0,0 +1,2 @@ +pub mod directory; +pub mod original_file; \ No newline at end of file diff --git a/crates/_types/buckets/src/public/media_uploads/original_file.rs b/crates/_types/buckets/src/public/media_uploads/original_file.rs new file mode 100644 index 0000000000..56cd34f963 --- /dev/null +++ b/crates/_types/buckets/src/public/media_uploads/original_file.rs @@ -0,0 +1,89 @@ +use crate::public::media_uploads::directory::MediaUploadDirectory; +use crate::public::public_path::PublicPath; +use crockford::crockford_entropy_lower; + +const ORIGINAL_FILE_BASENAME : &'static str = "original_upload.bin"; + +// TODO: Generate these from a macro. + +/// The original user upload file. +/// It may have derivative files (down samples, crops, etc.) that live alongside it. +#[derive(Clone)] +pub struct MediaUploadOriginalFilePath { + directory: MediaUploadDirectory, + full_object_path: String, +} + +impl PublicPath for MediaUploadOriginalFilePath {} + +impl MediaUploadOriginalFilePath { + + pub fn generate_new() -> Self { + let entropy = crockford_entropy_lower(32); + Self::from_object_hash(&entropy) + } + + pub fn from_object_hash(hash: &str) -> Self { + // TODO: Path construction could be cleaner. + let directory = MediaUploadDirectory::from_object_hash(hash); + let full_object_path = format!("{}/{}", directory.get_directory_path_str(), ORIGINAL_FILE_BASENAME); + Self { + directory, + full_object_path, + } + } + + pub fn get_full_object_path_str(&self) -> &str { + &self.full_object_path + } + + pub fn get_directory(&self) -> &MediaUploadDirectory { + &self.directory + } + + pub fn get_object_hash(&self) -> &str { + &self.directory.get_object_hash() + } + + pub fn get_basename() -> &'static str { + ORIGINAL_FILE_BASENAME + } +} + +#[cfg(test)] +mod tests { + use crate::public::media_uploads::original_file::MediaUploadOriginalFilePath; + + #[test] + pub fn generate_new_entropy() { + let file = MediaUploadOriginalFilePath::generate_new(); + assert_eq!(file.get_object_hash().len(), 32); + assert_eq!(file.get_directory().get_object_hash().len(), 32); + } + + #[test] + pub fn get_full_object_path_str() { + let file = MediaUploadOriginalFilePath::from_object_hash("abcdefghijk"); + assert_eq!(file.get_full_object_path_str(), "/media_upload/a/b/c/d/e/abcdefghijk/original_upload.bin"); + } + + #[test] + pub fn get_full_object_path_str_short_name() { + let file = MediaUploadOriginalFilePath::from_object_hash("foo"); + assert_eq!(file.get_full_object_path_str(), "/media_upload/f/o/foo/original_upload.bin"); + } + + #[test] + pub fn get_full_object_path_str_starts_with_directory() { + let file = MediaUploadOriginalFilePath::from_object_hash("abcdefghijk"); + let full_path = file.get_full_object_path_str(); + assert!(full_path.starts_with(file.get_directory().get_directory_path_str())) + } + + #[test] + pub fn get_object_hash() { + let hash = "abcdefghijk"; + let file = MediaUploadOriginalFilePath::from_object_hash(hash); + assert_eq!(file.get_object_hash(), hash); + } +} diff --git a/crates/_types/buckets/src/public/mod.rs b/crates/_types/buckets/src/public/mod.rs new file mode 100644 index 0000000000..38f2952846 --- /dev/null +++ b/crates/_types/buckets/src/public/mod.rs @@ -0,0 +1,2 @@ +pub mod media_uploads; +pub mod public_path; \ No newline at end of file diff --git a/crates/_types/buckets/src/public/public_path.rs b/crates/_types/buckets/src/public/public_path.rs new file mode 100644 index 0000000000..9c6d74d1c4 --- /dev/null +++ b/crates/_types/buckets/src/public/public_path.rs @@ -0,0 +1,7 @@ + +/// A trait implemented by all public paths. +pub trait PublicPath { + fn is_public(&self) -> bool { + true + } +} diff --git a/crates/lib/storage_buckets_common/src/bucket_paths.rs b/crates/_types/buckets/src/util/hash_to_bucket_path.rs similarity index 61% rename from crates/lib/storage_buckets_common/src/bucket_paths.rs rename to crates/_types/buckets/src/util/hash_to_bucket_path.rs index 81347b4d3a..770deede67 100644 --- a/crates/lib/storage_buckets_common/src/bucket_paths.rs +++ b/crates/_types/buckets/src/util/hash_to_bucket_path.rs @@ -1,11 +1,11 @@ -use anyhow::anyhow; -use anyhow::bail; -use container_common::anyhow_result::AnyhowResult; +use errors::AnyhowResult; +use errors::bail; use std::path::PathBuf; -pub fn hash_to_bucket_path(file_hash: &str, +#[deprecated(note = "this is a messy function and should be avoided")] +pub (crate) fn hash_to_bucket_path(file_hash: &str, optional_root_directory: Option<&str>) - -> AnyhowResult + -> AnyhowResult { if file_hash.len() < 4 { bail!("File length is too short"); @@ -23,29 +23,25 @@ pub fn hash_to_bucket_path(file_hash: &str, &file_hash, ); - let maybe_path = root_directory - .join(file_path) - .to_str() - .map(|s| s.to_string()); - - maybe_path.ok_or(anyhow!("Path could not be converted")) + Ok(root_directory.join(file_path)) } #[cfg(test)] mod tests { - use crate::bucket_paths::hash_to_bucket_path; + use crate::util::hash_to_bucket_path::hash_to_bucket_path; + use std::path::PathBuf; #[test] fn test_bucket_without_root() { assert_eq!( hash_to_bucket_path("abcdefg", None).unwrap(), - "/a/b/c/abcdefg".to_string()); + PathBuf::from("/a/b/c/abcdefg")); } #[test] fn test_bucket_with_root() { assert_eq!( hash_to_bucket_path("abcdefg", Some("/root")).unwrap(), - "/root/a/b/c/abcdefg".to_string()); + PathBuf::from("/root/a/b/c/abcdefg")); } } diff --git a/crates/_types/buckets/src/util/hash_to_bucket_path_string.rs b/crates/_types/buckets/src/util/hash_to_bucket_path_string.rs new file mode 100644 index 0000000000..af2e3cbb0b --- /dev/null +++ b/crates/_types/buckets/src/util/hash_to_bucket_path_string.rs @@ -0,0 +1,34 @@ +use crate::util::hash_to_bucket_path::hash_to_bucket_path; +use errors::AnyhowResult; +use errors::anyhow; + +#[deprecated(note = "this is a messy function and should be avoided")] +pub fn hash_to_bucket_path_string(file_hash: &str, + optional_root_directory: Option<&str>) + -> AnyhowResult +{ + let path = hash_to_bucket_path(file_hash, optional_root_directory)?; + + let maybe_path = path.to_str().map(|s| s.to_string()); + + maybe_path.ok_or(anyhow!("Path could not be converted")) +} + +#[cfg(test)] +mod tests { + use crate::util::hash_to_bucket_path_string::hash_to_bucket_path_string; + + #[test] + fn test_bucket_without_root() { + assert_eq!( + hash_to_bucket_path_string("abcdefg", None).unwrap(), + "/a/b/c/abcdefg".to_string()); + } + + #[test] + fn test_bucket_with_root() { + assert_eq!( + hash_to_bucket_path_string("abcdefg", Some("/root")).unwrap(), + "/root/a/b/c/abcdefg".to_string()); + } +} diff --git a/crates/_types/buckets/src/util/hashed_directory_path_long_string.rs b/crates/_types/buckets/src/util/hashed_directory_path_long_string.rs new file mode 100644 index 0000000000..6d79980d17 --- /dev/null +++ b/crates/_types/buckets/src/util/hashed_directory_path_long_string.rs @@ -0,0 +1,67 @@ + +/// Create a cloud object store directory structure that can be easily traversed without +/// too many items living in a single directory. +/// +/// With a million items, each directory will have ~1 item. +/// With a billion items, each directory will have ~953 items (totally tractable). +/// +pub fn hashed_directory_path_long_string(file_hash: &str) -> String { + match file_hash.len() { + 0 | 1 => "".to_string(), + 2 => format!("{}/", &file_hash[0..1]), + 3 => format!("{}/{}/", &file_hash[0..1], &file_hash[1..2]), + 4 => format!("{}/{}/{}/", &file_hash[0..1], &file_hash[1..2], &file_hash[2..3]), + 5 => format!("{}/{}/{}/{}/", &file_hash[0..1], &file_hash[1..2], &file_hash[2..3], &file_hash[3..4]), + _ => format!("{}/{}/{}/{}/{}/", &file_hash[0..1], &file_hash[1..2], &file_hash[2..3], &file_hash[3..4], &file_hash[4..5]), + } +} + +#[cfg(test)] +mod tests { + use crate::util::hashed_directory_path_long_string::hashed_directory_path_long_string; + + #[test] + fn test_length_zero() { + assert_eq!(hashed_directory_path_long_string(""), "".to_string()); + } + + #[test] + fn test_length_one() { + assert_eq!(hashed_directory_path_long_string("a"), "".to_string()); + } + + #[test] + fn test_length_two() { + assert_eq!(hashed_directory_path_long_string("ab"), "a/".to_string()); + } + + #[test] + fn test_length_three() { + assert_eq!(hashed_directory_path_long_string("abc"), "a/b/".to_string()); + } + + #[test] + fn test_length_four() { + assert_eq!(hashed_directory_path_long_string("abcd"), "a/b/c/".to_string()); + } + + #[test] + fn test_length_five() { + assert_eq!(hashed_directory_path_long_string("abcde"), "a/b/c/d/".to_string()); + } + + #[test] + fn test_length_six() { + assert_eq!(hashed_directory_path_long_string("abcdef"), "a/b/c/d/e/".to_string()); + } + + #[test] + fn test_length_seven() { + assert_eq!(hashed_directory_path_long_string("abcdefg"), "a/b/c/d/e/".to_string()); + } + + #[test] + fn test_length_ten() { + assert_eq!(hashed_directory_path_long_string("abcdefghij"), "a/b/c/d/e/".to_string()); + } +} diff --git a/crates/_types/buckets/src/util/hashed_directory_path_short_string.rs b/crates/_types/buckets/src/util/hashed_directory_path_short_string.rs new file mode 100644 index 0000000000..c4fe2ca05a --- /dev/null +++ b/crates/_types/buckets/src/util/hashed_directory_path_short_string.rs @@ -0,0 +1,55 @@ + +/// Create a cloud object store directory structure that can be easily traversed without +/// too many items living in a single directory. +/// +/// With a million items, each directory will have ~244 items. +/// With a billion items, each directory will have ~244,140 items (impractical at this scale). +/// +pub fn hashed_directory_path_short_string(file_hash: &str) -> String { + match file_hash.len() { + 0 | 1 => "".to_string(), + 2 => format!("{}/", &file_hash[0..1]), + 3 => format!("{}/{}/", &file_hash[0..1], &file_hash[1..2]), + _ => format!("{}/{}/{}/", &file_hash[0..1], &file_hash[1..2], &file_hash[2..3]), + } +} + +#[cfg(test)] +mod tests { + use crate::util::hashed_directory_path_short_string::hashed_directory_path_short_string; + + #[test] + fn test_length_zero() { + assert_eq!(hashed_directory_path_short_string(""), "".to_string()); + } + + #[test] + fn test_length_one() { + assert_eq!(hashed_directory_path_short_string("a"), "".to_string()); + } + + #[test] + fn test_length_two() { + assert_eq!(hashed_directory_path_short_string("ab"), "a/".to_string()); + } + + #[test] + fn test_length_three() { + assert_eq!(hashed_directory_path_short_string("abc"), "a/b/".to_string()); + } + + #[test] + fn test_length_four() { + assert_eq!(hashed_directory_path_short_string("abcd"), "a/b/c/".to_string()); + } + + #[test] + fn test_length_five() { + assert_eq!(hashed_directory_path_short_string("abcde"), "a/b/c/".to_string()); + } + + #[test] + fn test_length_ten() { + assert_eq!(hashed_directory_path_short_string("abcdefghij"), "a/b/c/".to_string()); + } +} diff --git a/crates/_types/buckets/src/util/mod.rs b/crates/_types/buckets/src/util/mod.rs new file mode 100644 index 0000000000..ed783efb2c --- /dev/null +++ b/crates/_types/buckets/src/util/mod.rs @@ -0,0 +1,4 @@ +pub (crate) mod hash_to_bucket_path; +pub mod hash_to_bucket_path_string; +pub mod hashed_directory_path_long_string; +pub mod hashed_directory_path_short_string; diff --git a/crates/_types/enums/Cargo.toml b/crates/_types/enums/Cargo.toml new file mode 100644 index 0000000000..c325f41cce --- /dev/null +++ b/crates/_types/enums/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "enums" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +name = "enums" +path = "src/lib.rs" + +[dependencies] +# Serialization +serde = "1.0.125" +serde_derive = "1.0.125" +serde_json = "1.0.64" +toml = "0.5.8" + +# 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.4", features = [ "mysql", "runtime-tokio-native-tls", "offline", "chrono" ] } +sqlx = { version = "0.5", features = [ "mysql", "runtime-actix-native-tls", "offline", "chrono" ] } +sqlx-core = { version = "0.5.7" } + +[dev-dependencies] + +# Enum utilities +strum = { version = "0.24", features = ["derive"] } diff --git a/crates/_types/enums/src/core/mod.rs b/crates/_types/enums/src/core/mod.rs new file mode 100644 index 0000000000..21b9312b2f --- /dev/null +++ b/crates/_types/enums/src/core/mod.rs @@ -0,0 +1 @@ +pub mod visibility; \ No newline at end of file diff --git a/crates/_types/enums/src/core/visibility.rs b/crates/_types/enums/src/core/visibility.rs new file mode 100644 index 0000000000..562b9d975b --- /dev/null +++ b/crates/_types/enums/src/core/visibility.rs @@ -0,0 +1,189 @@ +#[cfg(test)] +use strum::EnumCount; +#[cfg(test)] +use strum::EnumIter; + +/// Visibility +/// +/// Used in various database tables (as enums! careful!) and the HTTP API to convey +/// how the associated entity should be made visible to the public. +/// +/// To use this in a query, the query must have type annotations. +/// See: https://www.gitmemory.com/issue/launchbadge/sqlx/1241/847154375 +/// eg. preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::core::visibility::Visibility` +/// +/// See also: https://docs.rs/sqlx/0.4.0-beta.1/sqlx/trait.Type.html +/// +/// *DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY!* +/// +#[cfg_attr(test, derive(EnumIter, EnumCount))] +#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize, sqlx::Type)] +#[serde(rename_all = "lowercase")] +#[sqlx(rename_all = "lowercase")] +pub enum Visibility { + /// Public entities are able to be listed in public lists. + /// It does not mean that they necessarily will be (eg. they could be "mod unapproved" or deleted). + Public, + /// Hidden entities are not shown in public lists, but the URL to them may be given out freely. + /// They are available to non-logged-in users as long as they have the URL. + Hidden, + /// Private entities should only be available to the creator, a list of approved users, and + /// website moderation staff. + Private, + + // TODO(bt, 2022-12-20): We need a "Shared" option where users can share it with a specified group. + // This should perhaps be its own type, eg. VisibilityV2., so that we don't use it in tables that + // have not yet been migrated to this scheme. +} + + +impl_enum_display_and_debug_using_to_str!(Visibility); + +// For reference, here's what the serde implementation might be if manually written. +// This may be useful for designing composite types in the future: +// +// use serde::{Deserializer, Serializer}; +// +// impl serde::Serialize for UserToken { +// fn serialize(&self, serializer: S) -> Result { +// serializer.serialize_str(&self.0) +// } +// } +// +// impl<'de> serde::Deserialize<'de> for UserToken { +// fn deserialize>(d: D) -> Result { +// let s = String::deserialize(d)?; +// Ok(UserToken(s)) +// } +// } + +//impl sqlx::Type for Visibility { +// fn type_info() -> sqlx_core::database::TypeInfo { +// todo!() +// } +//} + +impl Default for Visibility { + fn default() -> Self { Self::Public } +} + +/// NB: Legacy API for older code. +impl Visibility { + pub fn to_str(&self) -> &'static str { + match self { + Self::Public => "public", + Self::Hidden => "hidden", + Self::Private => "private", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "public" => Ok(Self::Public), + "hidden" => Ok(Self::Hidden), + "private" => Ok(Self::Private), + _ => Err(format!("invalid value: {:?}", value)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::core::visibility::Visibility; + use crate::test_helpers::assert_serialization; + + #[test] + fn test_default() { + assert_eq!(Visibility::default(), Visibility::Public); + + } + + #[test] + fn test_serialization() { + assert_serialization(Visibility::Public, "public"); + assert_serialization(Visibility::Hidden, "hidden"); + assert_serialization(Visibility::Private, "private"); + } + + #[test] + fn test_to_str() { + assert_eq!(Visibility::Public.to_str(), "public"); + assert_eq!(Visibility::Hidden.to_str(), "hidden"); + assert_eq!(Visibility::Private.to_str(), "private"); + } + + #[test] + fn test_from_str() { + assert_eq!(Visibility::from_str("public").unwrap(), Visibility::Public); + assert_eq!(Visibility::from_str("hidden").unwrap(), Visibility::Hidden); + assert_eq!(Visibility::from_str("private").unwrap(), Visibility::Private); + assert!(Visibility::from_str("foo").is_err()); + } + + mod traits { + use crate::core::visibility::Visibility; + + #[test] + fn display() { + let visibility = Visibility::Hidden; + assert_eq!(format!("{}", visibility), "hidden".to_string()); + } + + #[test] + fn debug() { + let visibility = Visibility::Private; + assert_eq!(format!("{:?}", visibility), "private".to_string()); + } + } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct CompositeType { + visibility: Visibility, + string: String, + } + + mod serde_serialization { + use crate::core::visibility::Visibility; + use super::CompositeType; + + #[test] + fn serialize() { + let expected = "\"public\"".to_string(); // NB: Quoted + + assert_eq!(expected, toml::to_string(&Visibility::Public).unwrap()); + + // Just to show this serializes the same as a string + assert_eq!(expected, toml::to_string("public").unwrap()); + } + + #[test] + fn nested_serialize() { + let value = CompositeType { visibility: Visibility::Hidden, string: "bar".to_string() }; + let expected = r#"{"visibility":"hidden","string":"bar"}"#.to_string(); + assert_eq!(expected, serde_json::to_string(&value).unwrap()); + } + } + + mod serde_deserialization { + use crate::core::visibility::Visibility; + use super::CompositeType; + + #[test] + fn deserialize() { + let payload = "\"private\""; // NB: Quoted + let value: Visibility = serde_json::from_str(payload).unwrap(); + assert_eq!(value, Visibility::Private); + } + + #[test] + fn nested_deserialize() { + let payload = r#"{"visibility":"hidden","string":"bar"}"#.to_string(); + let expected = CompositeType { + visibility: Visibility::Hidden, + string: "bar".to_string(), + }; + + assert_eq!(expected, serde_json::from_str::(&payload).unwrap()); + } + } +} diff --git a/crates/_types/enums/src/files/media_upload_type.rs b/crates/_types/enums/src/files/media_upload_type.rs new file mode 100644 index 0000000000..4e5d9d169d --- /dev/null +++ b/crates/_types/enums/src/files/media_upload_type.rs @@ -0,0 +1,67 @@ +use serde::Deserialize; +use serde::Serialize; + +#[cfg(test)] +use strum::EnumCount; +#[cfg(test)] +use strum::EnumIter; + +/// Used in the `media_uploads` table in a `VARCHAR` field. +/// +/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. +#[cfg_attr(test, derive(EnumIter, EnumCount))] +#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum MediaUploadType { + /// Audio files: wav, mp3, etc. + Audio, + /// Video files: mp4, etc. + Video, +} + +// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests! +impl_enum_display_and_debug_using_to_str!(MediaUploadType); +impl_enum_sqlx_coders!(MediaUploadType); + +/// NB: Legacy API for older code. +impl MediaUploadType { + pub fn to_str(&self) -> &'static str { + match self { + Self::Audio => "audio", + Self::Video => "video", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "audio" => Ok(Self::Audio), + "video" => Ok(Self::Video), + _ => Err(format!("invalid value: {:?}", value)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::files::media_upload_type::MediaUploadType; + use crate::test_helpers::assert_serialization; + + #[test] + fn test_serialization() { + assert_serialization(MediaUploadType::Audio, "audio"); + assert_serialization(MediaUploadType::Video, "video"); + } + + #[test] + fn test_to_str() { + assert_eq!(MediaUploadType::Audio.to_str(), "audio"); + assert_eq!(MediaUploadType::Video.to_str(), "video"); + } + + #[test] + fn test_from_str() { + assert_eq!(MediaUploadType::from_str("audio").unwrap(), MediaUploadType::Audio); + assert_eq!(MediaUploadType::from_str("video").unwrap(), MediaUploadType::Video); + assert!(MediaUploadType::from_str("foo").is_err()); + } +} diff --git a/crates/_types/enums/src/files/mod.rs b/crates/_types/enums/src/files/mod.rs new file mode 100644 index 0000000000..8c653cf6ad --- /dev/null +++ b/crates/_types/enums/src/files/mod.rs @@ -0,0 +1 @@ +pub mod media_upload_type; diff --git a/crates/_types/enums/src/lib.rs b/crates/_types/enums/src/lib.rs new file mode 100644 index 0000000000..f59f68c2fe --- /dev/null +++ b/crates/_types/enums/src/lib.rs @@ -0,0 +1,32 @@ +//! enums +//! +//! The purpose of this library is to have a strongly-typed MySQL enum-type wrapper. +//! This should also work for CHAR/VARCHAR fields that work similarly to enums (typically +//! as part of a composite key) +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +#[macro_use] extern crate serde_derive; + +#[macro_use] +mod macros; + +#[cfg(test)] pub mod test_helpers; + +pub mod core; +pub mod files; +pub mod ml; +pub mod workers; diff --git a/crates/_types/enums/src/macros.rs b/crates/_types/enums/src/macros.rs new file mode 100644 index 0000000000..ede7c6970a --- /dev/null +++ b/crates/_types/enums/src/macros.rs @@ -0,0 +1,86 @@ + +/// Implement `Display` and `Debug` for enums that have a `.to_str()` method. +/// This ensures that the casing follows whatever `.to_str()` specifies. +macro_rules! impl_enum_display_and_debug_using_to_str { + ($t:ident) => { + + // Debug trait. + // Requires that the type has `.to_str()`. + impl std::fmt::Debug for $t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_str()) + } + } + + // Display trait. + // Requires that the type has `.to_str()`. + impl std::fmt::Display for $t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_str()) + } + } + + // NB: This test requires `strum::{EnumIter}` on the enum + #[cfg(test)] + #[test] + fn test_display_trait_matches_to_str() { + use strum::IntoEnumIterator; + for variant in $t::iter() { + assert_eq!(format!("{}", variant), variant.to_str()); + } + } + + // NB: This test requires `strum::{EnumIter}` on the enum + #[cfg(test)] + #[test] + fn test_debug_trait_matches_to_str() { + use strum::IntoEnumIterator; + for variant in $t::iter() { + assert_eq!(format!("{:?}", variant), variant.to_str()); + } + } + } +} + +/// This overt approach is being taken because of the following error: +/// +/// `MySqlDatabaseError { code: Some("HY000"), number: 1210, message: "Incorrect arguments to mysqld_stmt_execute" }` +/// +/// Basically, sqlx can't turn our enum into a VARCHAR when using #[derive(sqlx::Type)]. +/// Unfortunately, by not using this, we also lose the ability to `#[sqlx(rename_all="lowercase")]`, etc., +/// so our encoder/decoder need to set the rules. +/// +/// Solution adapted from https://github.com/launchbadge/sqlx/discussions/1502 +macro_rules! impl_enum_sqlx_coders { + ($t:ident) => { + + impl sqlx::Type for $t { + fn type_info() -> sqlx_core::mysql::MySqlTypeInfo { + String::type_info() + } + } + + impl<'q> sqlx::Encode<'q, sqlx_core::mysql::MySql> for $t { + fn encode_by_ref( + &self, + buf: &mut >::ArgumentBuffer + ) -> sqlx_core::encode::IsNull { + // NB: In the absence of `#[derive(sqlx::Type)]` and `#sqlx(rename_all="lowercase")]`, + // this controls the casing of the variants when sent to MySQL. + self.to_str().encode_by_ref(buf) + } + } + + impl<'r> sqlx::Decode<'r, sqlx_core::mysql::MySql> for $t { + fn decode( + value: sqlx_core::mysql::MySqlValueRef<'r>, + ) -> Result { + let string = String::decode(value)?; + let value = $t::from_str(&string)?; + Ok(value) + } + } + + } +} + diff --git a/crates/_types/enums/src/ml/mod.rs b/crates/_types/enums/src/ml/mod.rs new file mode 100644 index 0000000000..19ebf80f4f --- /dev/null +++ b/crates/_types/enums/src/ml/mod.rs @@ -0,0 +1 @@ +pub mod vocoder_type; diff --git a/crates/_types/enums/src/ml/vocoder_type.rs b/crates/_types/enums/src/ml/vocoder_type.rs new file mode 100644 index 0000000000..6443244394 --- /dev/null +++ b/crates/_types/enums/src/ml/vocoder_type.rs @@ -0,0 +1,56 @@ +#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] +pub enum VocoderType { + /// NB: Note - this is hifigan for Tacotron2. + /// Some work will be needed to unify this with other hifigan types. + #[serde(rename = "hifigan")] + #[sqlx(rename = "hifigan")] + HifiGan, + + /// NB: Note - this is hifigan for SoftVC (our internal codename is "rocketvc"). + /// Some work will need to be done to unify this with other hifigan types. + #[serde(rename = "hifigan_rocket_vc")] + #[sqlx(rename = "hifigan_rocket_vc")] + HifiGanRocketVc, +} + +/// NB: Legacy API for older code. +impl VocoderType { + pub fn to_str(&self) -> &'static str { + match self { + Self::HifiGan=> "hifigan", + Self::HifiGanRocketVc => "hifigan_rocket_vc", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "hifigan" => Ok(Self::HifiGan), + "hifigan_rocket_vc" => Ok(Self::HifiGanRocketVc), + _ => Err(format!("invalid value: {:?}", value)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::ml::vocoder_type::VocoderType; + use crate::test_helpers::assert_serialization; + + #[test] + fn test_serialization() { + assert_serialization(VocoderType::HifiGan, "hifigan"); + assert_serialization(VocoderType::HifiGanRocketVc, "hifigan_rocket_vc"); + } + + #[test] + fn to_str() { + assert_eq!(VocoderType::HifiGan.to_str(), "hifigan"); + assert_eq!(VocoderType::HifiGanRocketVc.to_str(), "hifigan_rocket_vc"); + } + + #[test] + fn from_str() { + assert_eq!(VocoderType::from_str("hifigan").unwrap(), VocoderType::HifiGan); + assert_eq!(VocoderType::from_str("hifigan_rocket_vc").unwrap(), VocoderType::HifiGanRocketVc); + } +} diff --git a/crates/lib/reusable_types/src/test_helpers.rs b/crates/_types/enums/src/test_helpers.rs similarity index 100% rename from crates/lib/reusable_types/src/test_helpers.rs rename to crates/_types/enums/src/test_helpers.rs diff --git a/crates/lib/reusable_types/src/generic_download_type.rs b/crates/_types/enums/src/workers/generic_download_type.rs similarity index 50% rename from crates/lib/reusable_types/src/generic_download_type.rs rename to crates/_types/enums/src/workers/generic_download_type.rs index 6a5d1fb289..87810a635a 100644 --- a/crates/lib/reusable_types/src/generic_download_type.rs +++ b/crates/_types/enums/src/workers/generic_download_type.rs @@ -1,18 +1,38 @@ + +// TODO: Use macro-derived impls + /// Our "generic downloads" pipeline supports a wide variety of ML models and other media. +/// They are serialized in the database table `generic_download_jobs` as a VARCHAR(32). +/// /// Each type of download is identified by the following enum variants. /// These types are present in the HTTP API and database columns as serialized here. /// /// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. #[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] pub enum GenericDownloadType { + /// NB: Note - this is hifigan for Tacotron2. + /// Some work will be needed to unify this with other hifigan types. #[serde(rename = "hifigan")] #[sqlx(rename = "hifigan")] HifiGan, - #[serde(rename = "melgan_vocodes")] - #[sqlx(rename = "melgan_vocodes")] - MelGanVocodes, + /// NB: Note - this is hifigan for SoftVC (our internal codename is "rocketvc"). + /// Some work will need to be done to unify this with other hifigan types. + #[serde(rename = "hifigan_rocket_vc")] + #[sqlx(rename = "hifigan_rocket_vc")] + HifiGanRocketVc, + //#[serde(rename = "melgan_vocodes")] + //#[sqlx(rename = "melgan_vocodes")] + //MelGanVocodes, + + /// NB: Our external-facing name for "softvc" is rocketvc. + /// I wish we could stop being stupid about this. + #[serde(rename = "rocket_vc")] + #[sqlx(rename = "rocket_vc")] + RocketVc, + + /// Tacotron TTS models. #[serde(rename = "tacotron2")] #[sqlx(rename = "tacotron2")] Tacotron2, @@ -23,7 +43,8 @@ impl GenericDownloadType { pub fn to_str(&self) -> &'static str { match self { Self::HifiGan => "hifigan", - Self::MelGanVocodes => "melgan_vocodes", + Self::HifiGanRocketVc => "hifigan_rocket_vc", + Self::RocketVc => "rocket_vc", Self::Tacotron2 => "tacotron2", } } @@ -31,7 +52,8 @@ impl GenericDownloadType { pub fn from_str(value: &str) -> Result { match value { "hifigan" => Ok(Self::HifiGan), - "melgan_vocodes" => Ok(Self::MelGanVocodes), + "hifigan_rocket_vc" => Ok(Self::HifiGanRocketVc), + "rocket_vc" => Ok(Self::RocketVc), "tacotron2" => Ok(Self::Tacotron2), _ => Err(format!("invalid value: {:?}", value)), } @@ -40,27 +62,30 @@ impl GenericDownloadType { #[cfg(test)] mod tests { - use crate::generic_download_type::GenericDownloadType; + use crate::workers::generic_download_type::GenericDownloadType; use crate::test_helpers::assert_serialization; #[test] fn test_serialization() { assert_serialization(GenericDownloadType::HifiGan, "hifigan"); - assert_serialization(GenericDownloadType::MelGanVocodes, "melgan_vocodes"); + assert_serialization(GenericDownloadType::HifiGanRocketVc, "hifigan_rocket_vc"); + assert_serialization(GenericDownloadType::RocketVc, "rocket_vc"); assert_serialization(GenericDownloadType::Tacotron2, "tacotron2"); } #[test] fn to_str() { assert_eq!(GenericDownloadType::HifiGan.to_str(), "hifigan"); - assert_eq!(GenericDownloadType::MelGanVocodes.to_str(), "melgan_vocodes"); + assert_eq!(GenericDownloadType::HifiGanRocketVc.to_str(), "hifigan_rocket_vc"); + assert_eq!(GenericDownloadType::RocketVc.to_str(), "rocket_vc"); assert_eq!(GenericDownloadType::Tacotron2.to_str(), "tacotron2"); } #[test] fn from_str() { assert_eq!(GenericDownloadType::from_str("hifigan").unwrap(), GenericDownloadType::HifiGan); - assert_eq!(GenericDownloadType::from_str("melgan_vocodes").unwrap(), GenericDownloadType::MelGanVocodes); + assert_eq!(GenericDownloadType::from_str("hifigan_rocket_vc").unwrap(), GenericDownloadType::HifiGanRocketVc); + assert_eq!(GenericDownloadType::from_str("rocket_vc").unwrap(), GenericDownloadType::RocketVc); assert_eq!(GenericDownloadType::from_str("tacotron2").unwrap(), GenericDownloadType::Tacotron2); } } diff --git a/crates/_types/enums/src/workers/generic_inference_type.rs b/crates/_types/enums/src/workers/generic_inference_type.rs new file mode 100644 index 0000000000..13920ad7d1 --- /dev/null +++ b/crates/_types/enums/src/workers/generic_inference_type.rs @@ -0,0 +1,80 @@ + +// TODO: Use macro-derived impls + +use std::collections::BTreeSet; + +/// Our "generic inference" pipeline supports a wide variety of ML models and other media. +/// Each type of inference is identified by the following enum variants. +/// These types are present in the HTTP API and database columns as serialized here. +/// +/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Ord, PartialOrd, Deserialize, Serialize, sqlx::Type)] +pub enum GenericInferenceType { + #[serde(rename = "text_to_speech")] + #[sqlx(rename = "text_to_speech")] + TextToSpeech, + + #[serde(rename = "voice_conversion")] + #[sqlx(rename = "voice_conversion")] + VoiceConversion, +} + +/// NB: Legacy API for older code. +impl GenericInferenceType { + pub fn to_str(&self) -> &'static str { + match self { + Self::TextToSpeech => "text_to_speech", + Self::VoiceConversion => "voice_conversion", + } + } + + pub fn from_str(value: &str) -> Result { + match value { + "text_to_speech" => Ok(Self::TextToSpeech), + "voice_conversion" => Ok(Self::VoiceConversion), + _ => Err(format!("invalid value: {:?}", value)), + } + } + + pub fn all_variants() -> BTreeSet { + // NB: BTreeSet is sorted + // NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc. + BTreeSet::from([ + GenericInferenceType::TextToSpeech, + GenericInferenceType::VoiceConversion, + ]) + } +} + +#[cfg(test)] +mod tests { + use crate::workers::generic_inference_type::GenericInferenceType; + use crate::test_helpers::assert_serialization; + + #[test] + fn test_serialization() { + assert_serialization(GenericInferenceType::TextToSpeech, "text_to_speech"); + assert_serialization(GenericInferenceType::VoiceConversion, "voice_conversion"); + } + + #[test] + fn to_str() { + assert_eq!(GenericInferenceType::TextToSpeech.to_str(), "text_to_speech"); + assert_eq!(GenericInferenceType::VoiceConversion.to_str(), "voice_conversion"); + } + + #[test] + fn from_str() { + assert_eq!(GenericInferenceType::from_str("text_to_speech").unwrap(), GenericInferenceType::TextToSpeech); + assert_eq!(GenericInferenceType::from_str("voice_conversion").unwrap(), GenericInferenceType::VoiceConversion); + } + + #[test] + fn all_variants() { + let mut variants = GenericInferenceType::all_variants(); + assert_eq!(variants.len(), 2); + assert_eq!(variants.pop_first(), Some(GenericInferenceType::TextToSpeech)); + assert_eq!(variants.pop_first(), Some(GenericInferenceType::VoiceConversion)); + assert_eq!(variants.pop_first(), None); + } +} diff --git a/crates/_types/enums/src/workers/mod.rs b/crates/_types/enums/src/workers/mod.rs new file mode 100644 index 0000000000..f6716b69e8 --- /dev/null +++ b/crates/_types/enums/src/workers/mod.rs @@ -0,0 +1,2 @@ +pub mod generic_inference_type; +pub mod generic_download_type; diff --git a/crates/lib/reusable_types/Cargo.toml b/crates/_types/reusable_types/Cargo.toml similarity index 100% rename from crates/lib/reusable_types/Cargo.toml rename to crates/_types/reusable_types/Cargo.toml diff --git a/crates/lib/reusable_types/src/achievements/mod.rs b/crates/_types/reusable_types/src/achievements/mod.rs similarity index 100% rename from crates/lib/reusable_types/src/achievements/mod.rs rename to crates/_types/reusable_types/src/achievements/mod.rs diff --git a/crates/lib/reusable_types/src/achievements/user_badge.rs b/crates/_types/reusable_types/src/achievements/user_badge.rs similarity index 65% rename from crates/lib/reusable_types/src/achievements/user_badge.rs rename to crates/_types/reusable_types/src/achievements/user_badge.rs index e45f42732a..d204af398c 100644 --- a/crates/lib/reusable_types/src/achievements/user_badge.rs +++ b/crates/_types/reusable_types/src/achievements/user_badge.rs @@ -17,6 +17,8 @@ pub enum UserBadge { TtsModelUploader150, TtsModelUploader200, TtsModelUploader250, + TtsModelUploader500, + TtsModelUploader1000, // Granted for vocoder models VocoderModelUploader1, @@ -28,6 +30,21 @@ pub enum UserBadge { VocoderModelUploader150, VocoderModelUploader200, VocoderModelUploader250, + VocoderModelUploader500, + VocoderModelUploader1000, + + // Granted for vocoder (softvc aka "rocket_vc") models + VocoderRocketVcModelUploader1, + VocoderRocketVcModelUploader5, + VocoderRocketVcModelUploader10, + VocoderRocketVcModelUploader20, + VocoderRocketVcModelUploader50, + VocoderRocketVcModelUploader100, + VocoderRocketVcModelUploader150, + VocoderRocketVcModelUploader200, + VocoderRocketVcModelUploader250, + VocoderRocketVcModelUploader500, + VocoderRocketVcModelUploader1000, // Granted for uploading templates W2lTemplateUploader1, @@ -57,6 +74,7 @@ pub enum UserBadge { } impl UserBadge { + /// This is the value that lives in the database as a VARCHAR(32) pub fn to_db_value(&self) -> &'static str { match self { UserBadge::EarlyUser => "early_user", @@ -69,6 +87,9 @@ impl UserBadge { UserBadge::TtsModelUploader150 => "tts_model_uploader_150", UserBadge::TtsModelUploader200 => "tts_model_uploader_200", UserBadge::TtsModelUploader250 => "tts_model_uploader_250", + UserBadge::TtsModelUploader500 => "tts_model_uploader_500", + UserBadge::TtsModelUploader1000 => "tts_model_uploader_1000", + UserBadge::VocoderModelUploader1 => "vocoder_model_uploader_1", UserBadge::VocoderModelUploader5 => "vocoder_model_uploader_5", UserBadge::VocoderModelUploader10 => "vocoder_model_uploader_10", @@ -78,6 +99,21 @@ impl UserBadge { UserBadge::VocoderModelUploader150 => "vocoder_model_uploader_150", UserBadge::VocoderModelUploader200 => "vocoder_model_uploader_200", UserBadge::VocoderModelUploader250 => "vocoder_model_uploader_250", + UserBadge::VocoderModelUploader500 => "vocoder_model_uploader_500", + UserBadge::VocoderModelUploader1000 => "vocoder_model_uploader_1000", + + UserBadge::VocoderRocketVcModelUploader1 => "vocoder_rocket_vc_uploader_1", + UserBadge::VocoderRocketVcModelUploader5 => "vocoder_rocket_vc_uploader_5", + UserBadge::VocoderRocketVcModelUploader10 => "vocoder_rocket_vc_uploader_10", + UserBadge::VocoderRocketVcModelUploader20 => "vocoder_rocket_vc_uploader_20", + UserBadge::VocoderRocketVcModelUploader50 => "vocoder_rocket_vc_uploader_50", + UserBadge::VocoderRocketVcModelUploader100 => "vocoder_rocket_vc_uploader_100", + UserBadge::VocoderRocketVcModelUploader150 => "vocoder_rocket_vc_uploader_150", + UserBadge::VocoderRocketVcModelUploader200 => "vocoder_rocket_vc_uploader_200", + UserBadge::VocoderRocketVcModelUploader250 => "vocoder_rocket_vc_uploader_250", + UserBadge::VocoderRocketVcModelUploader500 => "vocoder_rocket_vc_uploader_500", + UserBadge::VocoderRocketVcModelUploader1000 => "vocoder_rocket_vc_uploader_1000", // NB: 31 characters, table max is 32 + UserBadge::W2lTemplateUploader1 => "w2l_template_uploader_1", UserBadge::W2lTemplateUploader10 => "w2l_template_uploader_10", UserBadge::W2lTemplateUploader50 => "w2l_template_uploader_50", @@ -88,6 +124,7 @@ impl UserBadge { UserBadge::W2lTemplateUploader2000 => "w2l_template_uploader_2000", UserBadge::W2lTemplateUploader5000 => "w2l_template_uploader_5000", UserBadge::W2lTemplateUploader10000 => "w2l_template_uploader_10000", + //UserBadge::TtsInferenceUser100 => "tts_inference_100", //UserBadge::TtsInferenceUser500 => "tts_inference_500", //UserBadge::TtsInferenceUser1000 => "tts_inference_1000", @@ -113,7 +150,10 @@ impl UserBadge { | UserBadge::TtsModelUploader100 | UserBadge::TtsModelUploader150 | UserBadge::TtsModelUploader200 - | UserBadge::TtsModelUploader250 => UserBadgeType::TtsModelUploader, + | UserBadge::TtsModelUploader250 + | UserBadge::TtsModelUploader500 + | UserBadge::TtsModelUploader1000 + => UserBadgeType::TtsModelUploader, UserBadge::VocoderModelUploader1 | UserBadge::VocoderModelUploader5 @@ -123,7 +163,23 @@ impl UserBadge { | UserBadge::VocoderModelUploader100 | UserBadge::VocoderModelUploader150 | UserBadge::VocoderModelUploader200 - | UserBadge::VocoderModelUploader250 => UserBadgeType::VocoderModelUploader, + | UserBadge::VocoderModelUploader250 + | UserBadge::VocoderModelUploader500 + | UserBadge::VocoderModelUploader1000 + => UserBadgeType::VocoderModelUploader, + + UserBadge::VocoderRocketVcModelUploader1 + | UserBadge::VocoderRocketVcModelUploader5 + | UserBadge::VocoderRocketVcModelUploader10 + | UserBadge::VocoderRocketVcModelUploader20 + | UserBadge::VocoderRocketVcModelUploader50 + | UserBadge::VocoderRocketVcModelUploader100 + | UserBadge::VocoderRocketVcModelUploader150 + | UserBadge::VocoderRocketVcModelUploader200 + | UserBadge::VocoderRocketVcModelUploader250 + | UserBadge::VocoderRocketVcModelUploader500 + | UserBadge::VocoderRocketVcModelUploader1000 + => UserBadgeType::VocoderRocketVcModelUploader, UserBadge::W2lTemplateUploader1 | UserBadge::W2lTemplateUploader10 diff --git a/crates/lib/reusable_types/src/achievements/user_badge_type.rs b/crates/_types/reusable_types/src/achievements/user_badge_type.rs similarity index 78% rename from crates/lib/reusable_types/src/achievements/user_badge_type.rs rename to crates/_types/reusable_types/src/achievements/user_badge_type.rs index 41426374ad..f5c3be7ecf 100644 --- a/crates/lib/reusable_types/src/achievements/user_badge_type.rs +++ b/crates/_types/reusable_types/src/achievements/user_badge_type.rs @@ -11,6 +11,9 @@ pub enum UserBadgeType { /// Granted for uploading vocoder models VocoderModelUploader, + /// Granted for uploading vocoder (softvc aka "rocket_vc") models + VocoderRocketVcModelUploader, + /// Granted for uploading W2L templates W2lTemplateUploader, diff --git a/crates/lib/reusable_types/src/lib.rs b/crates/_types/reusable_types/src/lib.rs similarity index 85% rename from crates/lib/reusable_types/src/lib.rs rename to crates/_types/reusable_types/src/lib.rs index 6d0d911027..f32a10522f 100644 --- a/crates/lib/reusable_types/src/lib.rs +++ b/crates/_types/reusable_types/src/lib.rs @@ -17,8 +17,5 @@ #[cfg(test)] pub mod test_helpers; pub mod achievements; -pub mod entity_visibility; -pub mod generic_download_type; pub mod server_environment; pub mod stripe; -pub mod vocoder_type; diff --git a/crates/lib/reusable_types/src/server_environment.rs b/crates/_types/reusable_types/src/server_environment.rs similarity index 100% rename from crates/lib/reusable_types/src/server_environment.rs rename to crates/_types/reusable_types/src/server_environment.rs diff --git a/crates/lib/reusable_types/src/stripe/mod.rs b/crates/_types/reusable_types/src/stripe/mod.rs similarity index 100% rename from crates/lib/reusable_types/src/stripe/mod.rs rename to crates/_types/reusable_types/src/stripe/mod.rs diff --git a/crates/lib/reusable_types/src/stripe/stripe_recurring_interval.rs b/crates/_types/reusable_types/src/stripe/stripe_recurring_interval.rs similarity index 100% rename from crates/lib/reusable_types/src/stripe/stripe_recurring_interval.rs rename to crates/_types/reusable_types/src/stripe/stripe_recurring_interval.rs diff --git a/crates/lib/reusable_types/src/stripe/stripe_subscription_status.rs b/crates/_types/reusable_types/src/stripe/stripe_subscription_status.rs similarity index 100% rename from crates/lib/reusable_types/src/stripe/stripe_subscription_status.rs rename to crates/_types/reusable_types/src/stripe/stripe_subscription_status.rs diff --git a/crates/_types/reusable_types/src/test_helpers.rs b/crates/_types/reusable_types/src/test_helpers.rs new file mode 100644 index 0000000000..6d16bfddd5 --- /dev/null +++ b/crates/_types/reusable_types/src/test_helpers.rs @@ -0,0 +1,16 @@ +//! Only imported in tests + +use serde::Serialize; + +pub fn to_json(t: T) -> String { + toml::to_string(&t) + .expect("serialization error") + .replace("\"", "") // JSON values are quoted, so we remove quotes +} + +/// Assert that the Serialize is represented by the expected string value. +/// This is useful for ensuring stability of serialization. +pub fn assert_serialization(t: T, expected: &str) { + // TODO: See if there's a way to check the sqlx serialization + assert_eq!(&to_json(t), expected); +} diff --git a/crates/lib/tokens/Cargo.toml b/crates/_types/tokens/Cargo.toml similarity index 96% rename from crates/lib/tokens/Cargo.toml rename to crates/_types/tokens/Cargo.toml index c6626ba4e2..5ca9d9b935 100644 --- a/crates/lib/tokens/Cargo.toml +++ b/crates/_types/tokens/Cargo.toml @@ -13,9 +13,6 @@ name = "tokens" path = "src/lib.rs" [dependencies] -# Errors -anyhow = "1.0.40" - # Serialization serde = "1.0.125" serde_derive = "1.0.125" diff --git a/crates/_types/tokens/src/files/media_upload.rs b/crates/_types/tokens/src/files/media_upload.rs new file mode 100644 index 0000000000..5f8532b89e --- /dev/null +++ b/crates/_types/tokens/src/files/media_upload.rs @@ -0,0 +1,12 @@ +use crate::prefixes::EntityType; +use serde::Deserialize; +use serde::Serialize; +use std::fmt::Debug; + +/// The primary key for user media uploads (images, video, etc.) +#[derive(Clone, PartialEq, Eq, sqlx::Type, Debug, Serialize, Deserialize)] +#[sqlx(transparent)] +pub struct MediaUploadToken(pub String); + +impl_string_token!(MediaUploadToken); +impl_crockford_generator!(MediaUploadToken, 32usize, EntityType::MediaUpload, CrockfordLower); diff --git a/crates/_types/tokens/src/files/mod.rs b/crates/_types/tokens/src/files/mod.rs new file mode 100644 index 0000000000..740382ea60 --- /dev/null +++ b/crates/_types/tokens/src/files/mod.rs @@ -0,0 +1 @@ +pub mod media_upload; \ No newline at end of file diff --git a/crates/lib/tokens/src/jobs/download.rs b/crates/_types/tokens/src/jobs/download.rs similarity index 100% rename from crates/lib/tokens/src/jobs/download.rs rename to crates/_types/tokens/src/jobs/download.rs diff --git a/crates/lib/tokens/src/jobs/inference.rs b/crates/_types/tokens/src/jobs/inference.rs similarity index 100% rename from crates/lib/tokens/src/jobs/inference.rs rename to crates/_types/tokens/src/jobs/inference.rs diff --git a/crates/lib/tokens/src/jobs/mod.rs b/crates/_types/tokens/src/jobs/mod.rs similarity index 100% rename from crates/lib/tokens/src/jobs/mod.rs rename to crates/_types/tokens/src/jobs/mod.rs diff --git a/crates/lib/tokens/src/lib.rs b/crates/_types/tokens/src/lib.rs similarity index 96% rename from crates/lib/tokens/src/lib.rs rename to crates/_types/tokens/src/lib.rs index 2917558208..7453a53f88 100644 --- a/crates/lib/tokens/src/lib.rs +++ b/crates/_types/tokens/src/lib.rs @@ -1,7 +1,9 @@ //! tokens +//! //! The purpose of this library is to have a strongly-typed primary/foreign key system. //! Every Database or Redis key will have a type here. //! Well known keys will have short identifiers (eg. "user" is prefixed with "U:") +//! // Never allow these #![forbid(private_in_public)] @@ -37,5 +39,7 @@ mod macros; pub (crate) mod prefixes; +pub mod files; pub mod jobs; pub mod users; +pub mod voice_conversion; diff --git a/crates/lib/tokens/src/macros.rs b/crates/_types/tokens/src/macros.rs similarity index 100% rename from crates/lib/tokens/src/macros.rs rename to crates/_types/tokens/src/macros.rs diff --git a/crates/lib/tokens/src/prefixes.rs b/crates/_types/tokens/src/prefixes.rs similarity index 74% rename from crates/lib/tokens/src/prefixes.rs rename to crates/_types/tokens/src/prefixes.rs index 69b25d7dc0..6508b68a82 100644 --- a/crates/lib/tokens/src/prefixes.rs +++ b/crates/_types/tokens/src/prefixes.rs @@ -15,16 +15,20 @@ use strum::EnumIter; pub(crate) enum EntityType { DownloadJob, InferenceJob, + MediaUpload, User, + VoiceConversionModel, } impl EntityType { pub fn prefix(self) -> &'static str { match self { - Self::DownloadJob => "jdown_:", // NB: Was "JGUP:" + Self::DownloadJob => "jdown_", // NB: Was "JGUP:" Self::InferenceJob => "jinf_", + Self::MediaUpload => "mu_", Self::User => "U:", // NB: Old-style prefix. + Self::VoiceConversionModel => "voco_", } } } @@ -59,4 +63,18 @@ mod tests { assert!(entities.len() > 0); assert_eq!(entities.len(), EntityType::COUNT); } + + #[test] + fn test_all_prefixes_end_with_separator() { + assert!(EntityType::iter() + .map(|entity| entity.prefix()) + .all(|prefix| prefix.ends_with(":") || prefix.ends_with("_"))); + } + + #[test] + fn test_all_prefixes_end_with_separator_length_one() { + for prefix in EntityType::iter().map(|entity| entity.prefix()) { + assert_eq!(prefix.len() - 1, prefix.replace(":", "").replace("_", "").len()); + } + } } diff --git a/crates/lib/tokens/src/users/mod.rs b/crates/_types/tokens/src/users/mod.rs similarity index 100% rename from crates/lib/tokens/src/users/mod.rs rename to crates/_types/tokens/src/users/mod.rs diff --git a/crates/lib/tokens/src/users/user.rs b/crates/_types/tokens/src/users/user.rs similarity index 87% rename from crates/lib/tokens/src/users/user.rs rename to crates/_types/tokens/src/users/user.rs index ed75f0b9bb..db1191e658 100644 --- a/crates/lib/tokens/src/users/user.rs +++ b/crates/_types/tokens/src/users/user.rs @@ -4,7 +4,6 @@ use serde::Serialize; use std::fmt::Debug; /// The primary key for users. -// https://docs.rs/sqlx/latest/sqlx/trait.Type.html #[derive(Clone, PartialEq, Eq, sqlx::Type, Debug, Serialize, Deserialize)] #[sqlx(transparent)] pub struct UserToken(pub String); diff --git a/crates/_types/tokens/src/voice_conversion/mod.rs b/crates/_types/tokens/src/voice_conversion/mod.rs new file mode 100644 index 0000000000..99bbd124c1 --- /dev/null +++ b/crates/_types/tokens/src/voice_conversion/mod.rs @@ -0,0 +1 @@ +pub mod model; \ No newline at end of file diff --git a/crates/_types/tokens/src/voice_conversion/model.rs b/crates/_types/tokens/src/voice_conversion/model.rs new file mode 100644 index 0000000000..031f88974f --- /dev/null +++ b/crates/_types/tokens/src/voice_conversion/model.rs @@ -0,0 +1,12 @@ +use crate::prefixes::EntityType; +use serde::Deserialize; +use serde::Serialize; +use std::fmt::Debug; + +/// The primary key for "generic" inference jobs. +#[derive(Clone, PartialEq, Eq, sqlx::Type, Debug, Serialize, Deserialize)] +#[sqlx(transparent)] +pub struct VoiceConversionModelToken(pub String); + +impl_string_token!(VoiceConversionModelToken); +impl_crockford_generator!(VoiceConversionModelToken, 16usize, EntityType::VoiceConversionModel, CrockfordLower); diff --git a/crates/_types/tokens/tests/media_upload_token_tests.rs b/crates/_types/tokens/tests/media_upload_token_tests.rs new file mode 100644 index 0000000000..1ad8323a0b --- /dev/null +++ b/crates/_types/tokens/tests/media_upload_token_tests.rs @@ -0,0 +1,166 @@ +/// Test MediaUploadToken in isolation, just in case our macro-derived tests break. + +use serde::Deserialize; +use serde::Serialize; +use tokens::files::media_upload::MediaUploadToken; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct CompositeType { + media_upload_token: MediaUploadToken, + string: String, +} + +mod interface { + use tokens::files::media_upload::MediaUploadToken; + + #[test] + fn generate() { + let token = MediaUploadToken::generate(); + assert!(!token.to_string().is_empty()); + assert!(token.to_string().starts_with("mu_")); + } + + #[test] + fn new() { + let token = MediaUploadToken::new("mu_foo".to_string()); + assert_eq!(token, MediaUploadToken("mu_foo".to_string())); + } + + #[test] + fn new_from_str() { + let token = MediaUploadToken::new_from_str("mu_foo"); + assert_eq!(token, MediaUploadToken("mu_foo".to_string())); + } + + #[test] + fn as_str() { + let token = MediaUploadToken("mu_foo".to_string()); + assert_eq!(token.as_str(), "mu_foo"); + } + + #[test] + fn to_string() { + let token = MediaUploadToken("mu_foo".to_string()); + assert_eq!(token.to_string(), "mu_foo".to_string()); + } +} + +mod traits { + use tokens::files::media_upload::MediaUploadToken; + + #[test] + fn display() { + let token = MediaUploadToken("mu_foo".to_string()); + assert_eq!(format!("{}", token), "mu_foo".to_string()); + } + + #[test] + fn debug() { + let token = MediaUploadToken("mu_foo".to_string()); + assert_eq!(format!("{:?}", token), "MediaUploadToken(\"mu_foo\")".to_string()); + } +} + +mod serialization { + use crate::CompositeType; + use tokens::files::media_upload::MediaUploadToken; + + #[test] + fn serialize() { + let expected = "\"mu_foo\"".to_string(); // NB: Quoted + + let token = MediaUploadToken("mu_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("mu_foo").unwrap()); + } + + #[test] + fn nested_serialize() { + let value = CompositeType { media_upload_token: MediaUploadToken("mu_foo".to_string()), string: "bar".to_string() }; + let expected = r#"{"media_upload_token":"mu_foo","string":"bar"}"#.to_string(); + assert_eq!(expected, serde_json::to_string(&value).unwrap()); + } +} + +mod deserialization { + use crate::CompositeType; + use tokens::files::media_upload::MediaUploadToken; + + #[test] + fn deserialize() { + let payload = "\"mu_foo\""; // NB: Quoted + let expected = "mu_foo".to_string(); + + let value: MediaUploadToken = serde_json::from_str(payload).unwrap(); + assert_eq!(value, MediaUploadToken(expected.clone())); + + // Just to show this deserializes the same way as a string + let value: String = serde_json::from_str(payload).unwrap(); + assert_eq!(value, expected.clone()); + } + + #[test] + fn nested_deserialize() { + let payload = r#"{"media_upload_token":"mu_foo","string":"bar"}"#.to_string(); + let expected = CompositeType { + media_upload_token: MediaUploadToken("mu_foo".to_string()), + string: "bar".to_string(), + }; + + assert_eq!(expected, serde_json::from_str::(&payload).unwrap()); + } +} + +// These traits should be tested by the macro, but we duplicate them in case that breaks +mod crockford_traits { + use tokens::files::media_upload::MediaUploadToken; + + const ENTROPIC_CHARACTERS_MINIMUM : usize = 8; + + #[test] + fn entropy_is_sufficient() { + assert!(MediaUploadToken::entropic_character_len() > ENTROPIC_CHARACTERS_MINIMUM); + } + + #[test] + fn token_length() { + assert_eq!(MediaUploadToken::generate().as_str().len(), 32); + } + + #[test] + fn tokens_are_random() { + let mut tokens = std::collections::HashSet::new(); + for _ in 0..100 { + tokens.insert(MediaUploadToken::generate().to_string()); + } + assert_eq!(tokens.len(), 100); + } + + #[test] + fn character_set() { + let token_string = MediaUploadToken::generate().to_string(); + let prefix = MediaUploadToken::token_prefix(); + 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_lowercase())); + } + + #[test] + fn prefix_ends_with_separator() { + let prefix = MediaUploadToken::token_prefix(); + assert!(prefix.ends_with(":") || prefix.ends_with("_")); + + let token_string = MediaUploadToken::generate().to_string(); + assert!(token_string.contains(":") || token_string.contains("_")); + } + + #[test] + fn token_begins_with_prefix() { + let prefix = MediaUploadToken::token_prefix(); + let token_string = MediaUploadToken::generate().to_string(); + assert!(token_string.starts_with(prefix)); + } +} diff --git a/crates/lib/tokens/tests/user_token_tests.rs b/crates/_types/tokens/tests/user_token_tests.rs similarity index 97% rename from crates/lib/tokens/tests/user_token_tests.rs rename to crates/_types/tokens/tests/user_token_tests.rs index 8fb10e61a3..c14b3edc6e 100644 --- a/crates/lib/tokens/tests/user_token_tests.rs +++ b/crates/_types/tokens/tests/user_token_tests.rs @@ -17,6 +17,7 @@ mod interface { fn generate() { let token = UserToken::generate(); assert!(!token.to_string().is_empty()); + assert!(token.to_string().starts_with("U:")); } #[test] @@ -88,7 +89,7 @@ mod deserialization { use tokens::users::user::UserToken; #[test] - fn test_deserialize_trait() { + fn deserialize() { let payload = "\"U:foo\""; // NB: Quoted let expected = "U:foo".to_string(); @@ -101,7 +102,7 @@ mod deserialization { } #[test] - fn test_nested_deserialize_trait() { + fn nested_deserialize() { let payload = r#"{"user_token":"U:foo","string":"bar"}"#.to_string(); let expected = CompositeType { user_token: UserToken("U:foo".to_string()), diff --git a/crates/lib/storage_buckets_common/Cargo.toml b/crates/lib/cloud_storage/Cargo.toml similarity index 86% rename from crates/lib/storage_buckets_common/Cargo.toml rename to crates/lib/cloud_storage/Cargo.toml index 76e2a58bcd..bb85202245 100644 --- a/crates/lib/storage_buckets_common/Cargo.toml +++ b/crates/lib/cloud_storage/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "storage_buckets_common" +name = "cloud_storage" edition = "2018" version = "0.0.1" authors = [ @@ -9,12 +9,13 @@ authors = [ publish = false [lib] -name = "storage_buckets_common" +name = "cloud_storage" path = "src/lib.rs" [dependencies] # Internal +buckets = { path = "../../_types/buckets" } container_common = { path = "../container_common" } # External diff --git a/crates/lib/storage_buckets_common/src/bucket_client.rs b/crates/lib/cloud_storage/src/bucket_client.rs similarity index 100% rename from crates/lib/storage_buckets_common/src/bucket_client.rs rename to crates/lib/cloud_storage/src/bucket_client.rs diff --git a/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs b/crates/lib/cloud_storage/src/bucket_path_unifier.rs similarity index 89% rename from crates/lib/storage_buckets_common/src/bucket_path_unifier.rs rename to crates/lib/cloud_storage/src/bucket_path_unifier.rs index a9611564d6..a1741fec78 100644 --- a/crates/lib/storage_buckets_common/src/bucket_path_unifier.rs +++ b/crates/lib/cloud_storage/src/bucket_path_unifier.rs @@ -1,9 +1,10 @@ +use buckets::util::hashed_directory_path_short_string::hashed_directory_path_short_string; use container_common::anyhow_result::AnyhowResult; -use crate::bucket_paths::hash_to_bucket_path; use std::path::PathBuf; /// This is designed to make it centrally configurable where /// different types of objects are stored. +#[deprecated(note = "see the 'buckets' crate for a better approach")] pub struct BucketPathUnifier { // TTS pub tts_synthesizer_model_root: PathBuf, @@ -16,6 +17,8 @@ pub struct BucketPathUnifier { pub w2l_inference_output_root: PathBuf, pub w2l_model_root: PathBuf, pub w2l_end_bump_root: PathBuf, + // VC + pub softvc_model_root: PathBuf, } impl BucketPathUnifier { @@ -33,11 +36,14 @@ impl BucketPathUnifier { w2l_inference_output_root: PathBuf::from("/w2l_inference_output"), w2l_model_root: PathBuf::from("/w2l_pretrained_models"), w2l_end_bump_root: PathBuf::from("/w2l_end_bumps"), + // VC + softvc_model_root: PathBuf::from("/user_uploaded_softvc_models"), } } // ==================== TTS MODELS (SYNTHESIZER + VOCODER) ==================== // + // NB: Callers use "hash_file_sha2" as the file hash. pub fn tts_synthesizer_path(&self, tts_synthesizer_file_hash: &str) -> PathBuf { let hashed_path = Self::hashed_directory_path(tts_synthesizer_file_hash); let model_filename = format!("{}.pt", &tts_synthesizer_file_hash); @@ -65,7 +71,9 @@ impl BucketPathUnifier { self.tts_pretrained_vocoder_model_root.join(tts_vocoder_model_name) } + // NB: Callers use "hash_file_sha2" as the file hash. /// User-uploaded vocoders. + /// These can be HifiGan and HifiGanSoftVc pub fn vocoder_path(&self, vocoder_file_hash: &str) -> PathBuf { let hashed_path = Self::hashed_directory_path(vocoder_file_hash); let model_filename = format!("{}.pt", &vocoder_file_hash); @@ -77,6 +85,7 @@ impl BucketPathUnifier { // ==================== TTS INFERENCE OUTPUT ==================== // + // NB: Callers use job uuid_idempotency_token, which seems *bad* /// This should include the string "vocodes" for downloaders. pub fn tts_inference_wav_audio_output_path(&self, tts_inference_output_uuid: &str) -> PathBuf { let hashed_path = Self::hashed_directory_path(tts_inference_output_uuid); @@ -141,6 +150,7 @@ impl BucketPathUnifier { // ==================== W2L INFERENCE OUTPUT ==================== // + // NB: caller uses inference job token, which seems ideal // W2L inference output videos pub fn w2l_inference_video_output_path(&self, w2l_inference_job_token: &str) -> PathBuf { // NB: We don't want colons from the token in the filename. @@ -168,13 +178,22 @@ impl BucketPathUnifier { .join(video_filename) } + // ==================== VOICE CONVERSION MODELS ==================== // + + // NB: Callers use "hash_file_sha2" as the file hash. + pub fn softvc_model_path(&self, softvc_model_file_hash: &str) -> PathBuf { + let hashed_path = Self::hashed_directory_path(softvc_model_file_hash); + let model_filename = format!("{}.pt", &softvc_model_file_hash); + + self.softvc_model_root + .join(hashed_path) + .join(model_filename) + } + + // ==================== UTILITY ==================== // + pub fn hashed_directory_path(file_hash: &str) -> String { - match file_hash.len() { - 0 | 1=> "".to_string(), - 2 => format!("{}/", &file_hash[0..1]), - 3 => format!("{}/{}/", &file_hash[0..1], &file_hash[1..2]), - _ => format!("{}/{}/{}/", &file_hash[0..1], &file_hash[1..2], &file_hash[2..3]), - } + hashed_directory_path_short_string(file_hash) } } @@ -196,6 +215,8 @@ mod tests { w2l_inference_output_root: PathBuf::from("/test_path_w2l_output"), w2l_model_root: PathBuf::from("/test_path_w2l_pretrained_models"), w2l_end_bump_root: PathBuf::from("/test_path_w2l_end_bumps"), + // VOICE CONVERSION + softvc_model_root: PathBuf::from("/test_path_softvc_models"), } } @@ -296,6 +317,13 @@ mod tests { "/test_path_w2l_output/a/b/c/vocodes_video_TYPEABCDEF.mp4"); } + #[test] + fn test_softvc_model_path() { + let paths = get_instance(); + assert_eq!(paths.softvc_model_path("foobar").to_str().unwrap(), + "/test_path_softvc_models/f/o/o/foobar.pt"); + } + #[test] fn hashed_directory_path_length_zero() { assert_eq!(&BucketPathUnifier::hashed_directory_path(""), ""); diff --git a/crates/lib/storage_buckets_common/src/lib.rs b/crates/lib/cloud_storage/src/lib.rs similarity index 71% rename from crates/lib/storage_buckets_common/src/lib.rs rename to crates/lib/cloud_storage/src/lib.rs index 31a7cd49b6..6c74cb023f 100644 --- a/crates/lib/storage_buckets_common/src/lib.rs +++ b/crates/lib/cloud_storage/src/lib.rs @@ -1,3 +1,2 @@ pub mod bucket_client; pub mod bucket_path_unifier; -pub mod bucket_paths; \ No newline at end of file diff --git a/crates/lib/container_common/src/token/random_crockford_token.rs b/crates/lib/container_common/src/token/random_crockford_token.rs index a7cdd5346c..8d6762eb32 100644 --- a/crates/lib/container_common/src/token/random_crockford_token.rs +++ b/crates/lib/container_common/src/token/random_crockford_token.rs @@ -3,6 +3,7 @@ use rand::Rng; // Crockford characters const CROCKFORD_CHARSET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; +#[deprecated(note = "see internal 'crockford' and 'token' crates")] pub fn random_crockford_token(length: usize) -> String { let mut rng = rand::thread_rng(); diff --git a/crates/lib/container_common/src/token/random_prefix_crockford_token.rs b/crates/lib/container_common/src/token/random_prefix_crockford_token.rs index 0d9508ce65..0a20607e6d 100644 --- a/crates/lib/container_common/src/token/random_prefix_crockford_token.rs +++ b/crates/lib/container_common/src/token/random_prefix_crockford_token.rs @@ -10,6 +10,7 @@ const CROCKFORD_LOWERCASE_CHARSET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz"; // Random part can't be any less than this. const MIN_ENTROPY_LENGTH : usize = 8; +#[deprecated(note = "see internal 'crockford' and 'token' crates")] pub fn random_prefix_crockford_token(prefix: &str, length: usize, uppercase: bool) -> AnyhowResult { let mut prefix = prefix.trim().to_string(); diff --git a/crates/lib/crockford/Cargo.toml b/crates/lib/crockford/Cargo.toml new file mode 100644 index 0000000000..89cea5e711 --- /dev/null +++ b/crates/lib/crockford/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "crockford" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +# NB: Public "crockford" crate is non-sensically only for encoding and decoding u64s (wtf?) +# Our crate is for generating entropy and crockford encoding it. +name = "crockford" +path = "src/lib.rs" + +[dependencies] +rand = "0.8.3" + +[dev-dependencies] +# None yet diff --git a/crates/lib/crockford/src/crockford_entropy.rs b/crates/lib/crockford/src/crockford_entropy.rs new file mode 100644 index 0000000000..d78fa1d28a --- /dev/null +++ b/crates/lib/crockford/src/crockford_entropy.rs @@ -0,0 +1,86 @@ +use crate::{CROCKFORD_LOWERCASE_CHARSET, CROCKFORD_UPPERCASE_CHARSET}; +use rand::Rng; + +// TODO: Modify these routines to filter out intra-string swear words. + +/// Generate a crockford-encoded entropy string (uppercase) +pub fn crockford_entropy_upper(length: usize) -> String { + crockford_entropy(length, CROCKFORD_UPPERCASE_CHARSET) +} + +/// Generate a crockford-encoded entropy string (lowercase) +pub fn crockford_entropy_lower(length: usize) -> String { + crockford_entropy(length, CROCKFORD_LOWERCASE_CHARSET) +} + +#[inline] +fn crockford_entropy(length: usize, character_set: &[u8]) -> String { + let mut rng = rand::thread_rng(); + + (0..length).map(|_| { + let idx = rng.gen_range(0..character_set.len()); + character_set[idx] as char + }).collect() +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + use crate::crockford_entropy::{crockford_entropy_lower, crockford_entropy_upper}; + + #[test] + fn crockford_entropy_upper_length() { + for len in 0..100 { + assert_eq!(crockford_entropy_upper(len).len(), len); + } + } + + #[test] + fn crockford_entropy_lower_length() { + for len in 0..100 { + assert_eq!(crockford_entropy_lower(len).len(), len); + } + } + + #[test] + fn crockford_entropy_upper_randomness() { + // NB: This *could* fail if the random pool or seed is bad and we generate duplicates. + let mut set = HashSet::new(); + for _i in 0..100 { + let entropy = crockford_entropy_upper(16); + set.insert(entropy); + } + assert_eq!(set.len(), 100); + } + + #[test] + fn crockford_entropy_lower_randomness() { + // NB: This *could* fail if the random pool or seed is bad and we generate duplicates. + let mut set = HashSet::new(); + for _i in 0..100 { + let entropy = crockford_entropy_lower(16); + set.insert(entropy); + } + assert_eq!(set.len(), 100); + } + + #[test] + fn crockford_entropy_upper_uses_uppercase() { + // NB: This *could* fail if the random pool or seed is bad and we get all numbers. + for _i in 0..100 { + let entropy = crockford_entropy_upper(16); + assert_ne!(entropy, entropy.to_lowercase()); + assert_eq!(entropy, entropy.to_uppercase()); + } + } + + #[test] + fn crockford_entropy_upper_uses_lowercase() { + // NB: This *could* fail if the random pool or seed is bad and we get all numbers. + for _i in 0..100 { + let entropy = crockford_entropy_lower(16); + assert_ne!(entropy, entropy.to_uppercase()); + assert_eq!(entropy, entropy.to_lowercase()); + } + } +} diff --git a/crates/lib/crockford/src/lib.rs b/crates/lib/crockford/src/lib.rs new file mode 100644 index 0000000000..c08fefaba0 --- /dev/null +++ b/crates/lib/crockford/src/lib.rs @@ -0,0 +1,31 @@ +//! crockford +//! +//! The purpose of this library is to generate entropy and crockford encode it. +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +/// Crockford characters (uppercase) +const CROCKFORD_UPPERCASE_CHARSET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + +/// Crockford characters (lowercase) +const CROCKFORD_LOWERCASE_CHARSET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz"; + +mod crockford_entropy; + +// Re-export +pub use crockford_entropy::crockford_entropy_upper; +pub use crockford_entropy::crockford_entropy_lower; + diff --git a/crates/lib/database_queries/Cargo.toml b/crates/lib/database_queries/Cargo.toml index 9ce708b265..8e590a1319 100644 --- a/crates/lib/database_queries/Cargo.toml +++ b/crates/lib/database_queries/Cargo.toml @@ -15,10 +15,13 @@ path = "src/lib.rs" [dependencies] # Internal +buckets = { path = "../../_types/buckets" } config = { path = "../config" } container_common = { path = "../container_common" } -reusable_types = { path = "../reusable_types" } -tokens = { path = "../tokens" } +enums = { path = "../../_types/enums" } +errors = { path = "../errors" } +reusable_types = { path = "../../_types/reusable_types" } +tokens = { path = "../../_types/tokens" } # External anyhow = "1.0.40" diff --git a/crates/lib/database_queries/src/column_types/mod.rs b/crates/lib/database_queries/src/column_types/mod.rs index 9b54f0c4a4..a0ee4f37e6 100644 --- a/crates/lib/database_queries/src/column_types/mod.rs +++ b/crates/lib/database_queries/src/column_types/mod.rs @@ -1,5 +1,4 @@ pub mod job_status; -pub mod record_visibility; pub mod tts_model_type; pub mod twitch_event_category; pub mod vocoder_type; diff --git a/crates/lib/database_queries/src/column_types/record_visibility.rs b/crates/lib/database_queries/src/column_types/record_visibility.rs deleted file mode 100644 index 2b647db433..0000000000 --- a/crates/lib/database_queries/src/column_types/record_visibility.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! These are columns where users can control the visibility of their data. - -use anyhow::anyhow; -use container_common::anyhow_result::AnyhowResult; - -/// To use this in a query, the query must have type annotations. -/// See: https://www.gitmemory.com/issue/launchbadge/sqlx/1241/847154375 -/// eg. preferred_tts_result_visibility as `preferred_tts_result_visibility: crate::column_types::record_visibility::RecordVisibility` -#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, sqlx::Type)] -#[serde(rename_all = "lowercase")] -#[sqlx(rename_all = "lowercase")] -pub enum RecordVisibility { - Public, - Hidden, - Private, -} - -impl Default for RecordVisibility { - fn default() -> Self { Self::Public } -} - -impl RecordVisibility { - pub fn to_str(&self) -> &'static str { - match self { - RecordVisibility::Public => "public", - RecordVisibility::Hidden => "hidden", - RecordVisibility::Private => "private", - } - } - - pub fn from_str(record_visibility: &str) -> AnyhowResult { - match record_visibility { - "public" => Ok(RecordVisibility::Public), - "hidden" => Ok(RecordVisibility::Hidden), - "private" => Ok(RecordVisibility::Private), - _ => Err(anyhow!("invalid value: {:?}", record_visibility)), - } - } -} diff --git a/crates/lib/database_queries/src/lib.rs b/crates/lib/database_queries/src/lib.rs index db1243c4eb..f645ae6bca 100644 --- a/crates/lib/database_queries/src/lib.rs +++ b/crates/lib/database_queries/src/lib.rs @@ -6,5 +6,6 @@ pub mod column_types; pub mod complex_models; pub mod helpers; pub mod mediators; +pub mod payloads; pub mod queries; pub mod tokens; diff --git a/crates/lib/database_queries/src/mediators/badge_granter.rs b/crates/lib/database_queries/src/mediators/badge_granter.rs index 34dcee3975..9adb4a85d2 100644 --- a/crates/lib/database_queries/src/mediators/badge_granter.rs +++ b/crates/lib/database_queries/src/mediators/badge_granter.rs @@ -38,7 +38,11 @@ impl BadgeGranter { let mut maybe_badge = None; - if count >= 250 { + if count >= 1000 { + maybe_badge = Some(UserBadge::TtsModelUploader1000); + } else if count >= 500 { + maybe_badge = Some(UserBadge::TtsModelUploader500); + } else if count >= 250 { maybe_badge = Some(UserBadge::TtsModelUploader250); } else if count >= 200 { maybe_badge = Some(UserBadge::TtsModelUploader200); @@ -133,7 +137,11 @@ impl BadgeGranter { let mut maybe_badge = None; - if count >= 250 { + if count >= 1000 { + maybe_badge = Some(UserBadge::VocoderModelUploader1000); + } else if count >= 500 { + maybe_badge = Some(UserBadge::VocoderModelUploader500); + } else if count >= 250 { maybe_badge = Some(UserBadge::VocoderModelUploader250); } else if count >= 200 { maybe_badge = Some(UserBadge::VocoderModelUploader200); @@ -173,6 +181,56 @@ impl BadgeGranter { Ok(()) } + /// This needs to be called *after* successful upload. + pub async fn maybe_grant_softvc_vocoder_model_uploads_badge(&self, user_token: &str) -> AnyhowResult<()> { + let count = self.count_softvc_vocoder_models_uploaded(user_token).await?; + + let mut maybe_badge = None; + + if count >= 1000 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader1000); + } else if count >= 500 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader500); + } else if count >= 250 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader250); + } else if count >= 200 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader200); + } else if count >= 150 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader150); + } else if count >= 100 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader100); + } else if count >= 50 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader50); + } else if count >= 20 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader20); + } else if count >= 10 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader10); + } else if count >= 5 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader5); + } else if count >= 1 { + maybe_badge = Some(UserBadge::VocoderRocketVcModelUploader1); + } + + let badge = match maybe_badge { + Some(badge) => badge, + None => return Ok(()), + }; + + if self.has_badge(user_token, badge).await? { + return Ok(()) + } + + let _record_id = self.insert( + badge, + user_token, + ).await?; + + self.firehose_publisher.publish_user_badge_granted(user_token, badge.to_db_value()) + .await?; + + Ok(()) + } + // ======================================================================= pub async fn has_badge(&self, user_token: &str, user_badge: UserBadge) -> AnyhowResult { @@ -245,6 +303,30 @@ SELECT count(*) as count FROM vocoder_models WHERE creator_user_token = ? + AND vocoder_type = 'hifigan' +LIMIT 1 + "#, + user_token + ) + .fetch_one(&self.mysql_pool) + .await; + + self.handle_count_query(maybe_result) + } + + async fn count_softvc_vocoder_models_uploaded( + &self, + user_token: &str, + ) -> AnyhowResult { + // NB: This could get expensive! + let maybe_result = sqlx::query_as!( + CountRecord, + r#" +SELECT count(*) as count +FROM vocoder_models +WHERE + creator_user_token = ? + AND vocoder_type = 'hifigan_rocket_vc' LIMIT 1 "#, user_token diff --git a/crates/lib/database_queries/src/mediators/firehose_publisher.rs b/crates/lib/database_queries/src/mediators/firehose_publisher.rs index 203554fafa..74825ce83c 100644 --- a/crates/lib/database_queries/src/mediators/firehose_publisher.rs +++ b/crates/lib/database_queries/src/mediators/firehose_publisher.rs @@ -6,12 +6,16 @@ use sqlx::error::Error::Database; use sqlx::mysql::MySqlQueryResult; use sqlx::{MySqlPool}; use std::sync::Arc; +use tokens::files::media_upload::MediaUploadToken; +use tokens::jobs::inference::InferenceJobToken; +use tokens::users::user::UserToken; +// TODO(bt, 2022-12-19): Convert this to a database 'enum'. Also, create an 'enums' package similar to 'tokens'. #[derive(Debug, Clone, Copy)] enum FirehoseEvent { UserSignUp, - // We don't publish for all badges (eg. early user signup) + // NB: We don't publish for all badges (eg. early user signup) UserBadgeGranted, TtsModelUploadStarted, @@ -24,9 +28,15 @@ enum FirehoseEvent { W2lInferenceStarted, W2lInferenceCompleted, + VcInferenceStarted, + VcInferenceCompleted, + GenericDownloadStarted, GenericDownloadCompleted, + MediaUploaded, + + // TODO(bt, 2022-12-19): Are the following unused, merely planned (?) TwitterMention, TwitterRetweet, DiscordJoin, @@ -48,14 +58,19 @@ impl FirehoseEvent { FirehoseEvent::W2lTemplateUploadCompleted => "w2l_template_upload_completed", FirehoseEvent::W2lInferenceStarted => "w2l_inference_started", FirehoseEvent::W2lInferenceCompleted => "w2l_inference_completed", + FirehoseEvent::VcInferenceStarted => "vc_inference_started", + FirehoseEvent::VcInferenceCompleted => "vc_inference_completed", + FirehoseEvent::GenericDownloadStarted => "generic_download_started", + FirehoseEvent::GenericDownloadCompleted => "generic_download_completed", + FirehoseEvent::MediaUploaded => "media_uploaded", + + // Are the following unused (?) FirehoseEvent::TwitterMention => "twitter_mention", FirehoseEvent::TwitterRetweet => "twitter_retweet", FirehoseEvent::DiscordJoin => "discord_join", FirehoseEvent::DiscordMessage => "discord_message", FirehoseEvent::TwitchSubscribe => "twitch_subscribe", FirehoseEvent::TwitchFollow => "twitch_follow", - FirehoseEvent::GenericDownloadStarted => "generic_download_started", - FirehoseEvent::GenericDownloadCompleted => "generic_download_completed", } } } @@ -177,6 +192,27 @@ impl FirehosePublisher { Ok(()) } + pub async fn enqueue_vc_inference(&self, maybe_user_token: Option<&UserToken>, inference_job_token: &InferenceJobToken) -> AnyhowResult<()> { + let _record_id = self.insert( + FirehoseEvent::VcInferenceStarted, + maybe_user_token.map(|u| u.as_str()), + Some(inference_job_token.as_str()), + Some(inference_job_token.as_str()), + ).await?; + Ok(()) + } + + // TODO: Change result token type. + pub async fn vc_inference_finished(&self, maybe_user_token: Option<&UserToken>, inference_job_token: &InferenceJobToken, result_token: &str) -> AnyhowResult<()> { + let _record_id = self.insert( + FirehoseEvent::VcInferenceCompleted, + maybe_user_token.map(|u| u.as_str()), + Some(inference_job_token.as_str()), // TODO: This could be vc model token + Some(result_token) + ).await?; + Ok(()) + } + pub async fn enqueue_generic_download(&self, user_token: &str, job_token: &str) -> AnyhowResult<()> { let _record_id = self.insert( FirehoseEvent::GenericDownloadStarted, @@ -198,6 +234,16 @@ impl FirehosePublisher { Ok(()) } + pub async fn publish_media_uploaded(&self, maybe_user_token: Option<&UserToken>, upload_token: &MediaUploadToken) -> AnyhowResult<()> { + let _record_id = self.insert( + FirehoseEvent::MediaUploaded, + maybe_user_token.map(|u| u.as_str()), + Some(upload_token.as_str()), + Some(upload_token.as_str()), + ).await?; + Ok(()) + } + // ======================================================================= async fn insert( @@ -237,6 +283,8 @@ SET Ok(res) => { res.last_insert_id() }, + // TODO(bt, 2022-12-20): I've never richly handled database errors/error codes. + // I should revisit these in the future and consider some kind of middleware. Err(err) => { warn!("Insert record DB error: {:?}", err); diff --git a/crates/lib/database_queries/src/payloads/generic_inference_args.rs b/crates/lib/database_queries/src/payloads/generic_inference_args.rs new file mode 100644 index 0000000000..0f3113d87c --- /dev/null +++ b/crates/lib/database_queries/src/payloads/generic_inference_args.rs @@ -0,0 +1,86 @@ +use enums::workers::generic_inference_type::GenericInferenceType; +use tokens::files::media_upload::MediaUploadToken; +use tokens::voice_conversion::model::VoiceConversionModelToken; + +/// 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, Serialize, Deserialize)] +pub struct GenericInferenceArgs { + /// The type of inference (probably also present in a top-level field) + pub inference_type: Option, + + /// REQUIRED. + /// Actual type-specific arguments. + pub args: Option, +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum PolymorphicInferenceArgs { + TextToSpeechInferenceArgs { + // TODO + }, + VoiceConversionInferenceArgs { + /// REQUIRED. + /// The voice conversion model to use. + model_token: Option, + + /// OPTIONAL. + /// If set, the media file to use as the source in the conversion. + /// It's "optional" in case we use other types of records in the future. + maybe_media_token: Option, + }, +} + +#[cfg(test)] +mod tests { + use crate::payloads::generic_inference_args::{GenericInferenceArgs, PolymorphicInferenceArgs}; + use enums::workers::generic_inference_type::GenericInferenceType; + use tokens::files::media_upload::MediaUploadToken; + use tokens::voice_conversion::model::VoiceConversionModelToken; + + #[test] + fn typical_voice_conversion_args_serialize() { + let args = GenericInferenceArgs { + inference_type: Some(GenericInferenceType::VoiceConversion), + args: Some(PolymorphicInferenceArgs::VoiceConversionInferenceArgs { + model_token: Some(VoiceConversionModelToken::new_from_str("vc_model_token")), + maybe_media_token: Some(MediaUploadToken::new_from_str("media_token")), + }), + }; + + 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#"{"inference_type":"voice_conversion","args":{"VoiceConversionInferenceArgs":{"model_token":"vc_model_token","maybe_media_token":"media_token"}}}"#.to_string()); + + // NB: Make sure we don't overflow the DB field capacity (TEXT column). + assert!(json.len() < 1000); + } + + #[test] + fn serialize_nullable_form() { + let mut args : Option = None; + 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, "null"); + + args = Some(GenericInferenceArgs { + inference_type: Some(GenericInferenceType::VoiceConversion), + args: Some(PolymorphicInferenceArgs::VoiceConversionInferenceArgs { + model_token: Some(VoiceConversionModelToken::new_from_str("vc_model_token")), + maybe_media_token: Some(MediaUploadToken::new_from_str("media_token")), + }), + }); + + 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#"{"inference_type":"voice_conversion","args":{"VoiceConversionInferenceArgs":{"model_token":"vc_model_token","maybe_media_token":"media_token"}}}"#.to_string()); + + // NB: Make sure we don't overflow the DB field capacity (TEXT column). + assert!(json.len() < 1000); + } +} diff --git a/crates/lib/database_queries/src/payloads/media_upload_details.rs b/crates/lib/database_queries/src/payloads/media_upload_details.rs new file mode 100644 index 0000000000..f83770ddbc --- /dev/null +++ b/crates/lib/database_queries/src/payloads/media_upload_details.rs @@ -0,0 +1,4 @@ + +pub struct MediaUploadDetails { + // TODO +} diff --git a/crates/lib/database_queries/src/payloads/mod.rs b/crates/lib/database_queries/src/payloads/mod.rs new file mode 100644 index 0000000000..6d4ebc332f --- /dev/null +++ b/crates/lib/database_queries/src/payloads/mod.rs @@ -0,0 +1,4 @@ +//! This module is used to serialize values to database 'TEXT' fields. + +pub mod generic_inference_args; +pub mod media_upload_details; diff --git a/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs b/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs index 973c6db434..5419080336 100644 --- a/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs +++ b/crates/lib/database_queries/src/queries/generic_download/job/list_available_generic_download_jobs.rs @@ -4,8 +4,8 @@ use container_common::anyhow_result::AnyhowResult; use crate::column_types::job_status::JobStatus; use crate::queries::generic_download::job::_keys::GenericDownloadJobId; use crate::tokens::Tokens; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_download_type::GenericDownloadType; +use enums::core::visibility::Visibility; +use enums::workers::generic_download_type::GenericDownloadType; use sqlx::MySqlPool; use std::path::Path; use tokens::jobs::download::DownloadJobToken; @@ -18,7 +18,7 @@ pub struct AvailableDownloadJob { pub creator_user_token: String, pub creator_ip_address: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub download_type: GenericDownloadType, pub download_url: String, @@ -45,9 +45,9 @@ SELECT creator_user_token, creator_ip_address, - creator_set_visibility as `creator_set_visibility: reusable_types::entity_visibility::EntityVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, - download_type as `download_type: reusable_types::generic_download_type::GenericDownloadType`, + download_type as `download_type: enums::workers::generic_download_type::GenericDownloadType`, download_url, title, @@ -108,7 +108,7 @@ struct AvailableDownloadJobRawInternal { pub creator_user_token: String, pub creator_ip_address: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub download_type: GenericDownloadType, pub download_url: String, diff --git a/crates/lib/database_queries/src/queries/generic_download/mod.rs b/crates/lib/database_queries/src/queries/generic_download/mod.rs index 4feea997bc..d16f215da7 100644 --- a/crates/lib/database_queries/src/queries/generic_download/mod.rs +++ b/crates/lib/database_queries/src/queries/generic_download/mod.rs @@ -1,3 +1,2 @@ -pub mod get_generic_download_job_status; -pub mod insert_generic_download_job; pub mod job; +pub mod web; diff --git a/crates/lib/database_queries/src/queries/generic_download/get_generic_download_job_status.rs b/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs similarity index 92% rename from crates/lib/database_queries/src/queries/generic_download/get_generic_download_job_status.rs rename to crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs index 0d0b4b5530..ef1d035dbe 100644 --- a/crates/lib/database_queries/src/queries/generic_download/get_generic_download_job_status.rs +++ b/crates/lib/database_queries/src/queries/generic_download/web/get_generic_download_job_status.rs @@ -2,8 +2,6 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use log::warn; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_download_type::GenericDownloadType; use sqlx::MySqlPool; use tokens::jobs::download::DownloadJobToken; diff --git a/crates/lib/database_queries/src/queries/generic_download/insert_generic_download_job.rs b/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs similarity index 88% rename from crates/lib/database_queries/src/queries/generic_download/insert_generic_download_job.rs rename to crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs index 8a0cc1057a..356a5ad5a6 100644 --- a/crates/lib/database_queries/src/queries/generic_download/insert_generic_download_job.rs +++ b/crates/lib/database_queries/src/queries/generic_download/web/insert_generic_download_job.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_download_type::GenericDownloadType; +use enums::core::visibility::Visibility; +use enums::workers::generic_download_type::GenericDownloadType; use sqlx::MySqlPool; use tokens::jobs::download::DownloadJobToken; @@ -13,7 +13,7 @@ pub struct Args <'a> { pub title: &'a str, pub creator_user_token: &'a str, pub creator_ip_address: &'a str, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub mysql_pool: &'a MySqlPool, } diff --git a/crates/lib/database_queries/src/queries/generic_download/web/mod.rs b/crates/lib/database_queries/src/queries/generic_download/web/mod.rs new file mode 100644 index 0000000000..4def6e37b1 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_download/web/mod.rs @@ -0,0 +1,2 @@ +pub mod get_generic_download_job_status; +pub mod insert_generic_download_job; diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/_keys.rs b/crates/lib/database_queries/src/queries/generic_inference/job/_keys.rs new file mode 100644 index 0000000000..93ba2f37e5 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/_keys.rs @@ -0,0 +1,6 @@ +//! Primary and foreign keys + +/// Since jobs are internal, we'll use IDs as primary keys. +#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug, Serialize, Deserialize, sqlx::Type)] +#[sqlx(transparent)] +pub struct GenericInferenceJobId(pub i64); diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs b/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs new file mode 100644 index 0000000000..3eddb4876c --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/list_available_generic_inference_jobs.rs @@ -0,0 +1,287 @@ +use anyhow::anyhow; +use chrono::Utc; +use container_common::anyhow_result::AnyhowResult; +use crate::column_types::job_status::JobStatus; +use crate::helpers::boolean_converters::i8_to_bool; +use crate::queries::generic_inference::job::_keys::GenericInferenceJobId; +use enums::core::visibility::Visibility; +use enums::workers::generic_inference_type::GenericInferenceType; +use sqlx::mysql::MySqlArguments; +use sqlx::{MySql, MySqlPool}; +use std::collections::BTreeSet; +use std::future::Future; +use std::path::Path; +use tokens::jobs::inference::InferenceJobToken; + +/// table: generic_inference_jobs +#[derive(Debug)] +pub struct AvailableInferenceJob { + pub id: GenericInferenceJobId, + pub inference_job_token: InferenceJobToken, + + // Inference information + pub inference_type: GenericInferenceType, + pub maybe_inference_args: Option, + pub maybe_raw_inference_text: Option, + pub maybe_model_token: Option, + + // User information to propagate downstream + pub maybe_creator_user_token: Option, + pub creator_ip_address: String, + pub creator_set_visibility: Visibility, + + // Job information + pub status: JobStatus, + pub attempt_count: u16, + pub priority_level: u16, + pub is_from_premium_user: bool, + pub is_from_api_user: bool, + pub is_for_twitch: bool, + pub is_debug_request: bool, + + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub retry_at: Option>, +} + +pub struct ListAvailableGenericInferenceJobArgs<'a> { + pub num_records: u32, + pub is_debug_worker: bool, + pub sort_by_priority: bool, + pub maybe_scope_by_job_type: Option>, + pub mysql_pool: &'a MySqlPool, +} + +pub async fn list_available_generic_inference_jobs( + args: ListAvailableGenericInferenceJobArgs<'_>, +) + -> AnyhowResult> +{ + let ALL_TYPES = GenericInferenceType::all_variants(); + + let inference_types = args.maybe_scope_by_job_type + .as_ref() + .map(|types| types.clone()) + .unwrap_or(ALL_TYPES); + + let query = if args.sort_by_priority { + list_sorted_by_priority(args, inference_types).await + } else { + list_sorted_by_id(args, inference_types).await + }; + + let job_records = query?; + + let job_records = job_records.into_iter() + .map(|record : AvailableInferenceJobRawInternal| { + AvailableInferenceJob { + id: record.id, + inference_job_token: record.inference_job_token, + creator_ip_address: record.creator_ip_address, + maybe_creator_user_token: record.maybe_creator_user_token, + creator_set_visibility: record.creator_set_visibility, + inference_type: record.inference_type, + maybe_inference_args: record.maybe_inference_args, + maybe_raw_inference_text: record.maybe_raw_inference_text, + maybe_model_token: record.maybe_model_token, + status: record.status, + attempt_count: record.attempt_count, + priority_level: record.priority_level, + is_from_premium_user: i8_to_bool(record.is_from_premium_user), + is_from_api_user: i8_to_bool(record.is_from_api_user), + is_for_twitch: i8_to_bool(record.is_for_twitch), + is_debug_request: i8_to_bool(record.is_debug_request), + created_at: record.created_at, + updated_at: record.updated_at, + retry_at: record.retry_at, + } + }) + .collect::>(); + + Ok(job_records) +} + +async fn list_sorted_by_id(args: ListAvailableGenericInferenceJobArgs<'_>, inference_types: BTreeSet) -> Result, sqlx::Error> { + sqlx::query_as!( + AvailableInferenceJobRawInternal, + r#" +SELECT + id as `id: crate::queries::generic_inference::job::_keys::GenericInferenceJobId`, + token AS `inference_job_token: tokens::jobs::inference::InferenceJobToken`, + + inference_type as `inference_type: enums::workers::generic_inference_type::GenericInferenceType`, + maybe_inference_args, + maybe_raw_inference_text, + maybe_model_token, + + maybe_creator_user_token, + creator_ip_address, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, + + status as `status: crate::column_types::job_status::JobStatus`, + + attempt_count, + priority_level, + is_from_premium_user, + is_from_api_user, + is_for_twitch, + is_debug_request, + + created_at, + updated_at, + retry_at +FROM generic_inference_jobs +WHERE + ( + inference_type IN (?) + ) + AND + ( + status IN ("pending", "attempt_failed") + ) + AND + ( + retry_at IS NULL + OR + retry_at < CURRENT_TIMESTAMP + ) + AND + ( + is_debug_request = ? + ) + ORDER BY id ASC + LIMIT ? + "#, + to_where_in_predicate(&inference_types), + args.is_debug_worker, + args.num_records, + ) + .fetch_all(args.mysql_pool) + .await +} + +async fn list_sorted_by_priority(args: ListAvailableGenericInferenceJobArgs<'_>, inference_types: BTreeSet) -> Result, sqlx::Error> { + sqlx::query_as!( + AvailableInferenceJobRawInternal, + r#" +SELECT + id as `id: crate::queries::generic_inference::job::_keys::GenericInferenceJobId`, + token AS `inference_job_token: tokens::jobs::inference::InferenceJobToken`, + + inference_type as `inference_type: enums::workers::generic_inference_type::GenericInferenceType`, + maybe_inference_args, + maybe_raw_inference_text, + maybe_model_token, + + maybe_creator_user_token, + creator_ip_address, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, + + status as `status: crate::column_types::job_status::JobStatus`, + + attempt_count, + priority_level, + is_from_premium_user, + is_from_api_user, + is_for_twitch, + is_debug_request, + + created_at, + updated_at, + retry_at +FROM generic_inference_jobs +WHERE + ( + inference_type IN (?) + ) + AND + ( + status IN ("pending", "attempt_failed") + ) + AND + ( + retry_at IS NULL + OR + retry_at < CURRENT_TIMESTAMP + ) + AND + ( + is_debug_request = ? + ) + ORDER BY priority_level DESC, id ASC + LIMIT ? + "#, + to_where_in_predicate(&inference_types), + args.is_debug_worker, + args.num_records, + ) + .fetch_all(args.mysql_pool) + .await +} + +#[derive(Debug)] +struct AvailableInferenceJobRawInternal { + pub id: GenericInferenceJobId, + pub inference_job_token: InferenceJobToken, + + // Inference information + pub inference_type: GenericInferenceType, + pub maybe_inference_args: Option, + pub maybe_raw_inference_text: Option, + pub maybe_model_token: Option, + + // User information to propagate downstream + pub maybe_creator_user_token: Option, + pub creator_ip_address: String, + pub creator_set_visibility: Visibility, + + // Job information + pub status: JobStatus, + pub attempt_count: u16, + pub priority_level: u16, + pub is_from_premium_user: i8, + pub is_from_api_user: i8, + pub is_for_twitch: i8, + pub is_debug_request: i8, + + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub retry_at: Option>, +} + +/// Return a comma-separated predicate, since SQLx does not yet support WHERE IN(?) for Vec, etc. +/// Issue: https://github.com/launchbadge/sqlx/issues/875 +fn to_where_in_predicate(types: &BTreeSet) -> String { + let mut vec = types.iter() + .map(|ty| ty.to_str()) // TODO: These strings might need to be quoted. + .collect::>(); + vec.sort(); // NB: For the benefit of tests. + vec.join(", ") +} + +#[cfg(test)] +mod tests { + use crate::queries::generic_inference::job::list_available_generic_inference_jobs::to_where_in_predicate; + use enums::workers::generic_inference_type::GenericInferenceType; + use std::collections::BTreeSet; + + #[test] + fn test_where_in_clause() { + // None + let types = BTreeSet::from([]); + assert_eq!(to_where_in_predicate(&types), "".to_string()); + + // Some + let types = BTreeSet::from([ + GenericInferenceType::VoiceConversion, + ]); + + assert_eq!(to_where_in_predicate(&types), "voice_conversion".to_string()); + // All + let types = BTreeSet::from([ + GenericInferenceType::TextToSpeech, + GenericInferenceType::VoiceConversion, + ]); + assert_eq!(to_where_in_predicate(&types), "text_to_speech, voice_conversion".to_string()); + } +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_completely_failed.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_completely_failed.rs new file mode 100644 index 0000000000..8ee46fd3de --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_completely_failed.rs @@ -0,0 +1,34 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob; +use sqlx::MySqlPool; + +pub async fn mark_generic_inference_job_completely_failed( + pool: &MySqlPool, + job: &AvailableInferenceJob, + maybe_public_failure_reason: Option<&str>, + maybe_internal_debugging_failure_reason: Option<&str>, +) -> AnyhowResult<()> +{ + let query_result = sqlx::query!( + r#" +UPDATE generic_inference_jobs +SET + status = "complete_failure", + failure_reason = ?, + internal_debugging_failure_reason = ?, + retry_at = NULL +WHERE id = ? + "#, + maybe_public_failure_reason, + maybe_internal_debugging_failure_reason, + job.id.0, + ) + .execute(pool) + .await; + + match query_result { + Err(err) => Err(anyhow!("error with query: {:?}", err)), + Ok(_r) => Ok(()), + } +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_failure.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_failure.rs new file mode 100644 index 0000000000..aa33c88462 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_failure.rs @@ -0,0 +1,46 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob; +use sqlx::MySqlPool; +use sqlx; + +/// Mark a single inference job failure. The job may be re-run. +pub async fn mark_generic_inference_job_failure( + pool: &MySqlPool, + job: &AvailableInferenceJob, + failure_reason: &str, + internal_debugging_failure_reason: &str, + max_attempts: u16 +) -> AnyhowResult<()> { + + // statuses: "attempt_failed", "complete_failure", "dead" + let mut next_status = "attempt_failed"; + + if job.attempt_count >= max_attempts { + // NB: Job attempt count is incremented at start + next_status = "dead"; + } + + let query_result = sqlx::query!( + r#" +UPDATE generic_inference_jobs +SET + status = ?, + failure_reason = ?, + internal_debugging_failure_reason = ?, + retry_at = NOW() + interval 2 minute +WHERE id = ? + "#, + next_status, + failure_reason, + internal_debugging_failure_reason, + job.id.0, + ) + .execute(pool) + .await; + + match query_result { + Err(err) => Err(anyhow!("error with query: {:?}", err)), + Ok(_r) => Ok(()), + } +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_pending_and_grab_lock.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_pending_and_grab_lock.rs new file mode 100644 index 0000000000..ace9494698 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_pending_and_grab_lock.rs @@ -0,0 +1,84 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use crate::queries::generic_inference::job::_keys::GenericInferenceJobId; +use sqlx::MySqlPool; +use sqlx; + +// TODO(2022-02-09): It's unclear how stuck "pending" jobs don't become perma-dead +// I think this logic is flawed and the job system needs to pick up stuck "pending" jobs. + +pub struct GenericInferenceJobLockRecord { + id: i64, + status: String, +} + +pub async fn mark_generic_inference_job_pending_and_grab_lock( + pool: &MySqlPool, + job_id: GenericInferenceJobId +) -> AnyhowResult { + + // NB: We use transactions and "SELECT ... FOR UPDATE" to simulate mutexes. + let mut transaction = pool.begin().await?; + + let maybe_record = sqlx::query_as!( + GenericInferenceJobLockRecord, + r#" +SELECT + id, + status +FROM generic_inference_jobs +WHERE id = ? +FOR UPDATE + "#, + job_id.0, + ) + .fetch_one(&mut transaction) + .await; + + let record : GenericInferenceJobLockRecord = match maybe_record { + Ok(record) => record, + Err(err) => { + match err { + sqlx::Error::RowNotFound => { + return Err(anyhow!("could not job")); + }, + _ => { + return Err(anyhow!("query error")); + } + } + } + }; + + let can_transact = match record.status.as_ref() { + "pending" => true, // It's okay for us to take the lock. + "attempt_failed" => true, // We can retry. + "started" => false, // Job in progress (another job beat us, and we can't take the lock) + "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) + _ => false, // Future-proof + }; + + if !can_transact { + transaction.rollback().await?; + return Ok(false); + } + + let _acquire_lock = sqlx::query!( + r#" +UPDATE generic_inference_jobs +SET + status = 'started', + attempt_count = attempt_count + 1, + retry_at = NOW() + interval 2 minute +WHERE id = ? + "#, + job_id.0, + ) + .execute(&mut transaction) + .await?; + + transaction.commit().await?; + + Ok(true) +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_successfully_done.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_successfully_done.rs new file mode 100644 index 0000000000..1fbeec22cf --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mark_generic_inference_job_successfully_done.rs @@ -0,0 +1,36 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob; +use sqlx::MySqlPool; + +pub async fn mark_generic_inference_job_successfully_done( + pool: &MySqlPool, + job: &AvailableInferenceJob, + maybe_entity_type: Option<&str>, + maybe_entity_token: Option<&str>, +) -> AnyhowResult<()> +{ + let query_result = sqlx::query!( + r#" +UPDATE generic_inference_jobs +SET + status = "complete_success", + on_success_result_entity_type = ?, + on_success_result_entity_token = ?, + failure_reason = NULL, + internal_debugging_failure_reason = NULL, + retry_at = NULL +WHERE id = ? + "#, + maybe_entity_type, + maybe_entity_token, + job.id.0, + ) + .execute(pool) + .await; + + match query_result { + Err(err) => Err(anyhow!("error with query: {:?}", err)), + Ok(_r) => Ok(()), + } +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/job/mod.rs b/crates/lib/database_queries/src/queries/generic_inference/job/mod.rs new file mode 100644 index 0000000000..94cff4d755 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/job/mod.rs @@ -0,0 +1,6 @@ +pub mod _keys; +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; diff --git a/crates/lib/database_queries/src/queries/generic_inference/mod.rs b/crates/lib/database_queries/src/queries/generic_inference/mod.rs new file mode 100644 index 0000000000..d16f215da7 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/mod.rs @@ -0,0 +1,2 @@ +pub mod job; +pub mod web; diff --git a/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs b/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs new file mode 100644 index 0000000000..f9cffd230c --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/web/get_generic_inference_job_status.rs @@ -0,0 +1,59 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use container_common::anyhow_result::AnyhowResult; +use log::warn; +use sqlx::MySqlPool; +use tokens::jobs::inference::InferenceJobToken; + +pub struct GenericInferenceJobStatus { + pub job_token: InferenceJobToken, + + pub status: String, + pub attempt_count: u16, + + pub maybe_result_entity_type: Option, + pub maybe_result_entity_token: Option, + + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// Look up job status. +/// Returns Ok(None) when the record cannot be found. +pub async fn get_generic_inference_job_status(job_token: &InferenceJobToken, mysql_pool: &MySqlPool) + -> AnyhowResult> +{ + let maybe_status = sqlx::query_as!( + GenericInferenceJobStatus, + r#" +SELECT + jobs.token as `job_token: tokens::jobs::inference::InferenceJobToken`, + + jobs.status, + jobs.attempt_count, + jobs.on_success_result_entity_type as maybe_result_entity_type, + jobs.on_success_result_entity_token as maybe_result_entity_token, + + jobs.created_at, + jobs.updated_at + +FROM generic_inference_jobs as jobs + +WHERE jobs.token = ? + "#, + job_token + ) + .fetch_one(mysql_pool) + .await; + + match maybe_status { + Ok(record) => Ok(Some(record)), + Err(err) => match err { + sqlx::Error::RowNotFound => Ok(None), + _ => { + warn!("error querying job record: {:?}", err); + Err(anyhow!("error querying job record: {:?}", err)) + } + } + } +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs b/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs new file mode 100644 index 0000000000..d66bc7efeb --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/web/insert_generic_inference_job.rs @@ -0,0 +1,82 @@ +use anyhow::anyhow; +use crate::payloads::generic_inference_args::GenericInferenceArgs; +use enums::core::visibility::Visibility; +use enums::workers::generic_inference_type::GenericInferenceType; +use errors::AnyhowResult; +use sqlx::MySqlPool; +use tokens::jobs::inference::InferenceJobToken; +use tokens::users::user::UserToken; + +pub struct Args <'a> { + pub job_token: &'a InferenceJobToken, + pub uuid_idempotency_token: &'a str, + + pub inference_type: GenericInferenceType, + pub maybe_inference_args: Option, + pub maybe_raw_inference_text: Option, + pub maybe_model_token: Option, + + pub maybe_creator_user_token: Option<&'a UserToken>, + pub creator_ip_address: &'a str, + pub creator_set_visibility: Visibility, + + pub priority_level: u8, + pub is_debug_request: bool, + + pub mysql_pool: &'a MySqlPool, +} + +pub async fn insert_generic_inference_job(args: Args<'_>) -> AnyhowResult { + let serialized_args_payload = serde_json::ser::to_string(&args.maybe_inference_args) + .map_err(|_e| anyhow!("could not encode inference args"))?; + + let query = sqlx::query!( + r#" +INSERT INTO generic_inference_jobs +SET + token = ?, + uuid_idempotency_token = ?, + + inference_type = ?, + maybe_inference_args = ?, + maybe_raw_inference_text = ?, + maybe_model_token = ?, + + maybe_creator_user_token = ?, + creator_ip_address = ?, + creator_set_visibility = ?, + + priority_level = ?, + is_debug_request = ?, + + status = "pending" + "#, + args.job_token.as_str(), + args.uuid_idempotency_token, + + args.inference_type.to_str(), + serialized_args_payload, + args.maybe_raw_inference_text, + args.maybe_model_token.map(|t| t.to_string()), + + args.maybe_creator_user_token.map(|t| t.to_string()), + args.creator_ip_address, + args.creator_set_visibility.to_str(), + args.priority_level, + args.is_debug_request, + ); + + let query_result = query.execute(args.mysql_pool) + .await; + + let record_id = match query_result { + Ok(res) => { + res.last_insert_id() + }, + Err(err) => { + return Err(anyhow!("error inserting new generic inference job: {:?}", err)); + } + }; + + Ok(record_id) +} diff --git a/crates/lib/database_queries/src/queries/generic_inference/web/mod.rs b/crates/lib/database_queries/src/queries/generic_inference/web/mod.rs new file mode 100644 index 0000000000..3e4d73ced2 --- /dev/null +++ b/crates/lib/database_queries/src/queries/generic_inference/web/mod.rs @@ -0,0 +1,2 @@ +pub mod get_generic_inference_job_status; +pub mod insert_generic_inference_job; diff --git a/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs new file mode 100644 index 0000000000..1cf026d39b --- /dev/null +++ b/crates/lib/database_queries/src/queries/media_uploads/insert_media_upload.rs @@ -0,0 +1,102 @@ +use anyhow::anyhow; +use buckets::public::media_uploads::original_file::MediaUploadOriginalFilePath; +use container_common::anyhow_result::AnyhowResult; +use crate::payloads::media_upload_details::MediaUploadDetails; +use enums::core::visibility::Visibility; +use enums::files::media_upload_type::MediaUploadType; +use sqlx::MySqlPool; +use tokens::files::media_upload::MediaUploadToken; +use tokens::users::user::UserToken; + +pub struct Args <'a> { + pub token: &'a MediaUploadToken, + pub uuid_idempotency_token: &'a str, + + pub media_type: MediaUploadType, + pub maybe_original_filename: Option<&'a str>, + pub original_file_size_bytes: u64, + pub maybe_original_duration_millis: Option, + pub maybe_original_mime_type: Option<&'a str>, + pub maybe_original_audio_encoding: Option<&'a str>, + pub maybe_original_video_encoding: Option<&'a str>, + pub maybe_original_frame_width: Option, + pub maybe_original_frame_height: Option, + pub checksum_sha2: &'a str, + + pub public_upload_path: &'a MediaUploadOriginalFilePath, + pub extra_file_modification_info: MediaUploadDetails, + + pub maybe_creator_user_token: Option<&'a UserToken>, + pub maybe_creator_anonymous_visitor_token: Option<&'a str>, + pub creator_ip_address: &'a str, + pub creator_set_visibility: Visibility, + + pub maybe_creator_synthetic_id: Option, + + pub mysql_pool: &'a MySqlPool, +} + +pub async fn insert_media_upload(args: Args<'_>) -> AnyhowResult { + let query = sqlx::query!( + r#" +INSERT INTO media_uploads +SET + token = ?, + uuid_idempotency_token = ?, + + media_type = ?, + maybe_original_filename = ?, + original_file_size_bytes = ?, + original_duration_millis = ?, + maybe_original_mime_type = ?, + maybe_original_audio_encoding = ?, + maybe_original_video_encoding = ?, + maybe_original_frame_width = ?, + maybe_original_frame_height = ?, + checksum_sha2 = ?, + + public_bucket_directory_hash = ?, + public_bucket_directory_full_path = ?, + extra_file_modification_info = ?, + + maybe_creator_user_token = ?, + maybe_creator_anonymous_visitor_token = ?, + creator_ip_address = ?, + creator_set_visibility = ?, + + maybe_creator_synthetic_id = ? + "#, + args.token, + args.uuid_idempotency_token, + + args.media_type, + args.maybe_original_filename, + args.original_file_size_bytes, + args.maybe_original_duration_millis.unwrap_or(0), + args.maybe_original_mime_type, + args.maybe_original_audio_encoding, + args.maybe_original_video_encoding, + args.maybe_original_frame_width, + args.maybe_original_frame_height, + args.checksum_sha2, + + args.public_upload_path.get_object_hash(), + args.public_upload_path.get_full_object_path_str(), + "", // TODO: args.extra_file_modification_info, + + args.maybe_creator_user_token, + args.maybe_creator_anonymous_visitor_token, + args.creator_ip_address, + args.creator_set_visibility.to_str(), + + args.maybe_creator_synthetic_id, + ); + + let query_result = query.execute(args.mysql_pool) + .await; + + match query_result { + Ok(res) => Ok(res.last_insert_id()), + Err(err) => Err(anyhow!("error inserting new media upload: {:?}", err)), + } +} diff --git a/crates/lib/database_queries/src/queries/media_uploads/mod.rs b/crates/lib/database_queries/src/queries/media_uploads/mod.rs new file mode 100644 index 0000000000..d8571ca39b --- /dev/null +++ b/crates/lib/database_queries/src/queries/media_uploads/mod.rs @@ -0,0 +1 @@ +pub mod insert_media_upload; \ No newline at end of file diff --git a/crates/lib/database_queries/src/queries/mod.rs b/crates/lib/database_queries/src/queries/mod.rs index 57ffb02a8e..b15d0be795 100644 --- a/crates/lib/database_queries/src/queries/mod.rs +++ b/crates/lib/database_queries/src/queries/mod.rs @@ -1,8 +1,10 @@ pub mod api_tokens; pub mod billing; pub mod generic_download; +pub mod generic_inference; pub mod health_check; pub mod ip_bans; +pub mod media_uploads; pub mod model_categories; pub mod stats; pub mod tts; @@ -10,4 +12,5 @@ pub mod twitch; pub mod users; pub mod vocoder; pub mod voice_clone_requests; +pub mod voice_conversion; pub mod w2l; diff --git a/crates/lib/database_queries/src/queries/tts/tts_inference_jobs/list_available_tts_inference_jobs.rs b/crates/lib/database_queries/src/queries/tts/tts_inference_jobs/list_available_tts_inference_jobs.rs index 449f539301..d05ed468cc 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_inference_jobs/list_available_tts_inference_jobs.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_inference_jobs/list_available_tts_inference_jobs.rs @@ -6,9 +6,9 @@ use chrono::{Utc, DateTime}; use container_common::anyhow_result::AnyhowResult; use crate::column_types::job_status::JobStatus; -use crate::column_types::record_visibility::RecordVisibility; use crate::helpers::boolean_converters::i8_to_bool; use crate::queries::tts::tts_inference_jobs::_keys::TtsInferenceJobId; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; /// table: tts_inference_jobs @@ -29,7 +29,7 @@ pub struct AvailableTtsInferenceJob { pub creator_ip_address: String, pub maybe_creator_user_token: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_from_api: bool, pub is_for_twitch: bool, @@ -75,7 +75,7 @@ SELECT creator_ip_address, maybe_creator_user_token, - creator_set_visibility as `creator_set_visibility: crate::column_types::record_visibility::RecordVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, is_from_api, is_for_twitch, @@ -124,7 +124,7 @@ SELECT creator_ip_address, maybe_creator_user_token, - creator_set_visibility as `creator_set_visibility: crate::column_types::record_visibility::RecordVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, is_from_api, is_for_twitch, @@ -216,7 +216,7 @@ SELECT creator_ip_address, maybe_creator_user_token, - creator_set_visibility as `creator_set_visibility: crate::column_types::record_visibility::RecordVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, is_from_api, is_for_twitch, @@ -294,7 +294,7 @@ struct AvailableTtsInferenceJobRawInternal { pub creator_ip_address: String, pub maybe_creator_user_token: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_from_api: i8, pub is_for_twitch: i8, diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_details.rs b/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_details.rs index 3d850e2720..e11bf06396 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_details.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_details.rs @@ -1,5 +1,5 @@ -use crate::column_types::record_visibility::RecordVisibility; use crate::column_types::vocoder_type::VocoderType; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; use sqlx::mysql::MySqlQueryResult; use sqlx; @@ -12,7 +12,7 @@ pub async fn edit_tts_model_details_as_author( description_html: Option<&str>, ietf_language_tag: &str, ietf_primary_language_subtag: &str, - creator_set_visibility: RecordVisibility, + creator_set_visibility: Visibility, maybe_default_pretrained_vocoder: Option, maybe_custom_vocoder_token: Option<&str>, text_pipeline_type: Option<&str>, @@ -62,7 +62,7 @@ pub async fn edit_tts_model_details_as_mod( description_html: Option<&str>, ietf_language_tag: &str, ietf_primary_language_subtag: &str, - creator_set_visibility: RecordVisibility, + creator_set_visibility: Visibility, maybe_default_pretrained_vocoder: Option, maybe_custom_vocoder_token: Option<&str>, text_pipeline_type: Option<&str>, diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_moderator_details.rs b/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_moderator_details.rs index 484995077c..f19c37df94 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_moderator_details.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/edit_tts_model_moderator_details.rs @@ -1,6 +1,8 @@ -use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; -use crate::column_types::vocoder_type::VocoderType; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use sqlx::MySqlPool; use sqlx; use sqlx::mysql::MySqlQueryResult; diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model.rs b/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model.rs index cae563b19c..fae6592cd6 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model.rs @@ -6,9 +6,9 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::column_types::vocoder_type::VocoderType; use crate::helpers::boolean_converters::i8_to_bool; +use enums::core::visibility::Visibility; use log::warn; use sqlx::pool::PoolConnection; use sqlx::{MySql, MySqlPool}; @@ -44,7 +44,7 @@ pub struct TtsModelRecord { pub maybe_suggested_unique_bot_command: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_locked_from_use: bool, pub is_locked_from_user_modification: bool, @@ -152,8 +152,8 @@ pub async fn get_tts_model_by_token_using_connection( is_front_page_featured: i8_to_bool(model.is_front_page_featured), is_twitch_featured: i8_to_bool(model.is_twitch_featured), maybe_suggested_unique_bot_command: model.maybe_suggested_unique_bot_command, - creator_set_visibility: RecordVisibility::from_str(&model.creator_set_visibility) - .unwrap_or(RecordVisibility::Public), + creator_set_visibility: Visibility::from_str(&model.creator_set_visibility) + .unwrap_or(Visibility::Public), is_locked_from_use: i8_to_bool(model.is_locked_from_use), is_locked_from_user_modification: i8_to_bool(model.is_locked_from_user_modification), created_at: model.created_at, diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model_for_inference.rs b/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model_for_inference.rs index f3e840e224..1d35e98250 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model_for_inference.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/get_tts_model_for_inference.rs @@ -4,10 +4,10 @@ #![forbid(unused_variables)] use chrono::{Utc, DateTime}; +use enums::ml::vocoder_type::VocoderType; use log::warn; use sqlx::MySqlPool; use sqlx; -use reusable_types::vocoder_type::VocoderType; // TODO: Can probably just reuse another query. @@ -91,7 +91,7 @@ SELECT tts.maybe_default_pretrained_vocoder, tts.maybe_custom_vocoder_token, - vocoder.vocoder_type as `maybe_custom_vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `maybe_custom_vocoder_type: enums::ml::vocoder_type::VocoderType`, vocoder.title as maybe_custom_vocoder_title, vocoder.private_bucket_hash as maybe_custom_vocoder_private_bucket_hash, diff --git a/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs b/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs index 120a589edc..00daa6d73e 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_models/insert_tts_model_from_download_job.rs @@ -3,7 +3,7 @@ use chrono::Utc; use container_common::anyhow_result::AnyhowResult; use crate::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use crate::tokens::Tokens; -use reusable_types::entity_visibility::EntityVisibility; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; use std::path::Path; @@ -16,7 +16,7 @@ pub struct Args<'a, P: AsRef> { pub creator_user_token: &'a str, pub creator_ip_address: &'a str, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub private_bucket_hash: &'a str, pub private_bucket_object_name: P, diff --git a/crates/lib/database_queries/src/queries/tts/tts_results/list_tts_results_query_builder.rs b/crates/lib/database_queries/src/queries/tts/tts_results/list_tts_results_query_builder.rs index a60b5bc44e..87762c8c6f 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_results/list_tts_results_query_builder.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_results/list_tts_results_query_builder.rs @@ -1,9 +1,12 @@ -use anyhow::anyhow; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +//#![forbid(unused_variables)] + use chrono::{DateTime, Utc}; use config::shared_constants::DEFAULT_MYSQL_QUERY_RESULT_PAGE_SIZE; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; -use log::{warn, info}; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; #[derive(Serialize)] @@ -35,7 +38,7 @@ pub struct TtsInferenceRecordForList { pub file_size_bytes: u32, pub duration_millis: u32, - pub visibility: RecordVisibility, + pub visibility: Visibility, //pub model_is_mod_approved: bool, // converted //pub maybe_mod_user_token: Option, @@ -115,10 +118,10 @@ impl ListTtsResultsQueryBuilder { let internal_results = self.perform_internal_query(mysql_pool).await?; - let mut first_id = internal_results.first() + let first_id = internal_results.first() .map(|raw_result| raw_result.tts_result_id); - let mut last_id = internal_results.last() + let last_id = internal_results.last() .map(|raw_result| raw_result.tts_result_id); let inference_results = internal_results @@ -135,7 +138,7 @@ impl ListTtsResultsQueryBuilder { maybe_creator_result_id: r.maybe_creator_result_id.map(|v| v as u64).clone(), file_size_bytes: if r.file_size_bytes > 0 { r.file_size_bytes as u32 } else { 0 }, duration_millis: if r.duration_millis > 0 { r.duration_millis as u32 } else { 0 }, - visibility: RecordVisibility::from_str(&r.creator_set_visibility).unwrap_or(RecordVisibility::Public), + visibility: Visibility::from_str(&r.creator_set_visibility).unwrap_or(Visibility::Public), created_at: r.created_at, updated_at: r.updated_at, } diff --git a/crates/lib/database_queries/src/queries/tts/tts_results/query_tts_result.rs b/crates/lib/database_queries/src/queries/tts/tts_results/query_tts_result.rs index c9a6a84653..cf72c53577 100644 --- a/crates/lib/database_queries/src/queries/tts/tts_results/query_tts_result.rs +++ b/crates/lib/database_queries/src/queries/tts/tts_results/query_tts_result.rs @@ -1,15 +1,16 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::column_types::vocoder_type::VocoderType; use crate::helpers::boolean_converters::{nullable_i8_to_bool, i8_to_bool}; -use log::{info, warn, log}; +use enums::core::visibility::Visibility; +use log::warn; use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; -use std::sync::Arc; #[derive(Serialize)] pub struct TtsResultRecordForResponse { @@ -34,7 +35,7 @@ pub struct TtsResultRecordForResponse { pub public_bucket_wav_audio_path: String, pub public_bucket_spectrogram_path: String, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, // Worker hostname that generated the audio. Has the value "unknown" if null. pub generated_by_worker: String, @@ -170,8 +171,8 @@ pub async fn select_tts_result_by_token( public_bucket_spectrogram_path: ir.public_bucket_spectrogram_path, // NB: Fail open/public since we're already looking at it - creator_set_visibility: RecordVisibility::from_str(&ir.creator_set_visibility) - .unwrap_or(RecordVisibility::Public), + creator_set_visibility: Visibility::from_str(&ir.creator_set_visibility) + .unwrap_or(Visibility::Public), generated_by_worker: ir.generated_by_worker.unwrap_or("unknown".to_string()), diff --git a/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs b/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs index 310cdb4345..f740834ae8 100644 --- a/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs +++ b/crates/lib/database_queries/src/queries/users/user_profiles/get_user_profile_by_username.rs @@ -2,8 +2,8 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use crate::helpers::boolean_converters::i8_to_bool; +use enums::core::visibility::Visibility; use log::{info, warn, log}; -use reusable_types::entity_visibility::EntityVisibility; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; @@ -38,8 +38,8 @@ pub struct UserProfileResult { pub website_url: Option, // Preferences; NB: included for get_profile_handler legacy reasons - pub preferred_tts_result_visibility: EntityVisibility, - pub preferred_w2l_result_visibility: EntityVisibility, + pub preferred_tts_result_visibility: Visibility, + pub preferred_w2l_result_visibility: Visibility, pub created_at: DateTime, @@ -75,8 +75,8 @@ struct RawUserProfileRecord { created_at: DateTime, // Preferences; NB: included for get_profile_handler legacy reasons - preferred_tts_result_visibility: EntityVisibility, - preferred_w2l_result_visibility: EntityVisibility, + preferred_tts_result_visibility: Visibility, + preferred_w2l_result_visibility: Visibility, // Mod fields is_banned: i8, @@ -109,8 +109,8 @@ SELECT profile_rendered_html, user_role_slug, disable_gravatar, - preferred_tts_result_visibility as `preferred_tts_result_visibility: reusable_types::entity_visibility::EntityVisibility`, - preferred_w2l_result_visibility as `preferred_w2l_result_visibility: reusable_types::entity_visibility::EntityVisibility`, + preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::core::visibility::Visibility`, + preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::core::visibility::Visibility`, discord_username, twitch_username, twitter_username, diff --git a/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs b/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs index 9d848bd0e3..6079245b6a 100644 --- a/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs +++ b/crates/lib/database_queries/src/queries/users/user_sessions/get_user_session_by_token.rs @@ -4,14 +4,15 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::helpers::boolean_converters::{nullable_i8_to_optional_bool, i8_to_bool, nullable_i8_to_bool_default_false}; +use enums::core::visibility::Visibility; use log::warn; use sqlx::MySql; use sqlx::pool::PoolConnection; +use tokens::users::user::UserToken; pub struct SessionUserRecord { - pub user_token: String, + pub user_token: String, // TODO(bt, 2022-12-20): Convert to strongly-typed `UserToken` pub username: String, pub display_name: String, @@ -28,8 +29,8 @@ pub struct SessionUserRecord { pub disable_gravatar: bool, pub auto_play_audio_preference: Option, - pub preferred_tts_result_visibility: RecordVisibility, - pub preferred_w2l_result_visibility: RecordVisibility, + pub preferred_tts_result_visibility: Visibility, + pub preferred_w2l_result_visibility: Visibility, pub auto_play_video_preference: Option, // ===== ROLE ===== // @@ -65,6 +66,13 @@ pub struct SessionUserRecord { pub can_delete_users: bool, } +impl SessionUserRecord { + // TODO(bt, 2022-12-20): Convert all users of the bare record to using `UserToken`, then get rid of this method. + pub fn get_strongly_typed_user_token(&self) -> UserToken { + UserToken::new_from_str(&self.user_token) + } +} + pub async fn get_user_session_by_token( mysql_connection: &mut PoolConnection, session_token: &str, @@ -89,8 +97,8 @@ SELECT users.disable_gravatar, users.auto_play_audio_preference, users.auto_play_video_preference, - users.preferred_tts_result_visibility as `preferred_tts_result_visibility: crate::column_types::record_visibility::RecordVisibility`, - users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: crate::column_types::record_visibility::RecordVisibility`, + users.preferred_tts_result_visibility as `preferred_tts_result_visibility: enums::core::visibility::Visibility`, + users.preferred_w2l_result_visibility as `preferred_w2l_result_visibility: enums::core::visibility::Visibility`, users.user_role_slug, users.is_banned, @@ -211,8 +219,8 @@ struct SessionUserRawDbRecord { disable_gravatar: i8, auto_play_audio_preference: Option, auto_play_video_preference: Option, - preferred_tts_result_visibility: RecordVisibility, - preferred_w2l_result_visibility: RecordVisibility, + preferred_tts_result_visibility: Visibility, + preferred_w2l_result_visibility: Visibility, user_role_slug: String, is_banned: i8, diff --git a/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs b/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs index 9ad4683a8b..e2ec1fc6a9 100644 --- a/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs +++ b/crates/lib/database_queries/src/queries/vocoder/get_vocoder_model.rs @@ -7,9 +7,9 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use crate::helpers::boolean_converters::i8_to_bool; +use enums::core::visibility::Visibility; +use enums::ml::vocoder_type::VocoderType; use log::warn; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::vocoder_type::VocoderType; use sqlx::MySqlPool; // FIXME: This is the old style of query scoping and shouldn't be copied. @@ -31,7 +31,7 @@ pub struct VocoderModelRecord { pub creator_display_name: String, pub creator_gravatar_hash: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub created_at: DateTime, pub updated_at: DateTime, @@ -120,7 +120,7 @@ async fn select_including_deleted( r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: enums::ml::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, @@ -137,7 +137,7 @@ SELECT vocoder.is_mod_disabled_from_author_use, vocoder.is_mod_author_editing_locked, - vocoder.creator_set_visibility as `creator_set_visibility: reusable_types::entity_visibility::EntityVisibility`, + vocoder.creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, vocoder.created_at, vocoder.updated_at, @@ -168,7 +168,7 @@ async fn select_without_deleted( r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: enums::ml::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, @@ -185,7 +185,7 @@ SELECT vocoder.is_mod_disabled_from_author_use, vocoder.is_mod_author_editing_locked, - vocoder.creator_set_visibility as `creator_set_visibility: reusable_types::entity_visibility::EntityVisibility`, + vocoder.creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, vocoder.created_at, vocoder.updated_at, @@ -224,7 +224,7 @@ struct InternalVocoderModelRecordRaw { pub creator_display_name: String, pub creator_gravatar_hash: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub created_at: DateTime, pub updated_at: DateTime, diff --git a/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs b/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs index f6152981a5..78f1db58cb 100644 --- a/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs +++ b/crates/lib/database_queries/src/queries/vocoder/insert_vocoder_model.rs @@ -6,8 +6,8 @@ use anyhow::anyhow; use container_common::anyhow_result::AnyhowResult; use crate::tokens::Tokens; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::vocoder_type::VocoderType; +use enums::core::visibility::Visibility; +use enums::ml::vocoder_type::VocoderType; use sqlx::MySqlPool; use std::path::Path; @@ -22,7 +22,7 @@ pub struct Args<'a, P: AsRef> { pub creator_user_token: &'a str, pub creator_ip_address: &'a str, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub private_bucket_hash: &'a str, pub private_bucket_object_name: P, diff --git a/crates/lib/database_queries/src/queries/vocoder/list_vocoder_models.rs b/crates/lib/database_queries/src/queries/vocoder/list_vocoder_models.rs index 728eab0b95..46e2de7f8f 100644 --- a/crates/lib/database_queries/src/queries/vocoder/list_vocoder_models.rs +++ b/crates/lib/database_queries/src/queries/vocoder/list_vocoder_models.rs @@ -7,8 +7,8 @@ use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; use crate::helpers::boolean_converters::i8_to_bool; +use enums::ml::vocoder_type::VocoderType; use log::{warn, info}; -use reusable_types::vocoder_type::VocoderType; use sqlx::MySqlPool; // FIXME: This is the old style of query scoping and shouldn't be copied. @@ -115,7 +115,7 @@ async fn list_vocoder_models_for_all_creators( r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: enums::ml::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, users.display_name as creator_display_name, @@ -147,7 +147,7 @@ WHERE r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: enums::ml::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, users.display_name as creator_display_name, @@ -187,7 +187,7 @@ async fn list_vocoder_models_creator_scoped( r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: enums::ml::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, users.display_name as creator_display_name, @@ -221,7 +221,7 @@ WHERE r#" SELECT vocoder.token as vocoder_token, - vocoder.vocoder_type as `vocoder_type: reusable_types::vocoder_type::VocoderType`, + vocoder.vocoder_type as `vocoder_type: enums::ml::vocoder_type::VocoderType`, vocoder.creator_user_token, users.username as creator_username, users.display_name as creator_display_name, diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/delete_vocoder.rs b/crates/lib/database_queries/src/queries/voice_conversion/inference/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/delete_vocoder.rs rename to crates/lib/database_queries/src/queries/voice_conversion/inference/mod.rs diff --git a/crates/lib/database_queries/src/queries/voice_conversion/mod.rs b/crates/lib/database_queries/src/queries/voice_conversion/mod.rs new file mode 100644 index 0000000000..1cd77f670e --- /dev/null +++ b/crates/lib/database_queries/src/queries/voice_conversion/mod.rs @@ -0,0 +1,2 @@ +pub mod inference; +pub mod models; \ No newline at end of file diff --git a/crates/lib/database_queries/src/queries/voice_conversion/models/insert_voice_conversion_model_from_download_job.rs b/crates/lib/database_queries/src/queries/voice_conversion/models/insert_voice_conversion_model_from_download_job.rs new file mode 100644 index 0000000000..d6bf18996b --- /dev/null +++ b/crates/lib/database_queries/src/queries/voice_conversion/models/insert_voice_conversion_model_from_download_job.rs @@ -0,0 +1,75 @@ +use anyhow::anyhow; +use chrono::Utc; +use container_common::anyhow_result::AnyhowResult; +use enums::core::visibility::Visibility; +use sqlx::MySqlPool; +use std::path::Path; +use tokens::voice_conversion::model::VoiceConversionModelToken; + +pub struct Args<'a, P: AsRef> { + pub title: &'a str, + + pub original_download_url: &'a str, + pub original_filename: &'a str, + pub file_size_bytes: u64, + + pub creator_user_token: &'a str, + pub creator_ip_address: &'a str, + pub creator_set_visibility: Visibility, + + pub private_bucket_hash: &'a str, + pub private_bucket_object_name: P, + + pub mysql_pool: &'a MySqlPool, +} + + +pub async fn insert_voice_conversion_model_from_download_job>( + args: Args<'_, P>, +) -> AnyhowResult<(u64, VoiceConversionModelToken)> { + + let model_token = VoiceConversionModelToken::generate(); + + let private_bucket_object_name = &args.private_bucket_object_name + .as_ref() + .display() + .to_string(); + + // NB: 'rocket_vc' is codename for 'softvc' + let query_result = sqlx::query!( + r#" +INSERT INTO voice_conversion_models +SET + token = ?, + model_type = "rocket_vc", + title = ?, + description_markdown = '', + description_rendered_html = '', + creator_user_token = ?, + ip_address_creation = ?, + ip_address_last_update = ?, + original_download_url = ?, + private_bucket_hash = ?, + private_bucket_object_name = ?, + file_size_bytes = ? + "#, + &model_token, + args.title, + args.creator_user_token, + args.creator_ip_address, + args.creator_ip_address, + args.original_download_url, + args.private_bucket_hash, + private_bucket_object_name, + args.file_size_bytes + ) + .execute(args.mysql_pool) + .await; + + let record_id = match query_result { + Ok(res) => res.last_insert_id(), + Err(err) => return Err(anyhow!("Mysql error: {:?}", err)), + }; + + Ok((record_id, model_token)) +} diff --git a/crates/lib/database_queries/src/queries/voice_conversion/models/mod.rs b/crates/lib/database_queries/src/queries/voice_conversion/models/mod.rs new file mode 100644 index 0000000000..63c6cdf401 --- /dev/null +++ b/crates/lib/database_queries/src/queries/voice_conversion/models/mod.rs @@ -0,0 +1 @@ +pub mod insert_voice_conversion_model_from_download_job; diff --git a/crates/lib/database_queries/src/queries/w2l/w2l_inference_jobs/w2l_inference_job_queries.rs b/crates/lib/database_queries/src/queries/w2l/w2l_inference_jobs/w2l_inference_job_queries.rs index 61ac44ba89..9a96aed0a2 100644 --- a/crates/lib/database_queries/src/queries/w2l/w2l_inference_jobs/w2l_inference_job_queries.rs +++ b/crates/lib/database_queries/src/queries/w2l/w2l_inference_jobs/w2l_inference_job_queries.rs @@ -5,8 +5,8 @@ use anyhow::anyhow; use chrono::{Utc, DateTime}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::tokens::Tokens; +use enums::core::visibility::Visibility; use log::{warn, info}; use sqlx::MySqlPool; use std::path::Path; @@ -38,7 +38,7 @@ pub struct W2lInferenceJobRecord { pub creator_ip_address: String, pub maybe_creator_user_token: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub disable_end_bump: i8, // bool pub disable_watermark: i8, // bool @@ -76,7 +76,7 @@ SELECT creator_ip_address, maybe_creator_user_token, - creator_set_visibility as `creator_set_visibility: crate::column_types::record_visibility::RecordVisibility`, + creator_set_visibility as `creator_set_visibility: enums::core::visibility::Visibility`, disable_end_bump, disable_watermark, diff --git a/crates/lib/database_queries/src/queries/w2l/w2l_results/list_w2l_inference_results_query_builder.rs b/crates/lib/database_queries/src/queries/w2l/w2l_results/list_w2l_inference_results_query_builder.rs index 6c4a6f1760..aa3d299a28 100644 --- a/crates/lib/database_queries/src/queries/w2l/w2l_results/list_w2l_inference_results_query_builder.rs +++ b/crates/lib/database_queries/src/queries/w2l/w2l_results/list_w2l_inference_results_query_builder.rs @@ -1,9 +1,12 @@ -use anyhow::anyhow; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use chrono::{DateTime, Utc}; use config::shared_constants::DEFAULT_MYSQL_QUERY_RESULT_PAGE_SIZE; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; -use log::{warn, info}; +use enums::core::visibility::Visibility; use sqlx::MySqlPool; #[derive(Serialize)] @@ -38,7 +41,7 @@ pub struct W2lInferenceRecordForList { pub frame_height: u32, pub duration_millis: u32, - pub visibility: RecordVisibility, + pub visibility: Visibility, //pub template_is_mod_approved: bool, // converted //pub maybe_mod_user_token: Option, @@ -132,10 +135,10 @@ impl ListW2lResultsQueryBuilder { let internal_results = self.perform_internal_query(mysql_pool).await?; - let mut first_id = internal_results.first() + let first_id = internal_results.first() .map(|raw_result| raw_result.w2l_result_id); - let mut last_id = internal_results.last() + let last_id = internal_results.last() .map(|raw_result| raw_result.w2l_result_id); let inference_results = internal_results @@ -155,7 +158,7 @@ impl ListW2lResultsQueryBuilder { frame_width: if r.frame_width > 0 { r.frame_width as u32 } else { 0 }, frame_height: if r.frame_height > 0 { r.frame_height as u32 } else { 0 }, duration_millis: if r.duration_millis > 0 { r.duration_millis as u32 } else { 0 }, - visibility: RecordVisibility::from_str(&r.creator_set_visibility).unwrap_or(RecordVisibility::Public), + visibility: Visibility::from_str(&r.creator_set_visibility).unwrap_or(Visibility::Public), created_at: r.created_at, updated_at: r.updated_at, } @@ -255,7 +258,7 @@ LEFT OUTER JOIN users let mut query = "".to_string(); - if let Some(offset) = self.offset { + if let Some(_offset) = self.offset { if !first_predicate_added { query.push_str(" WHERE"); first_predicate_added = true; @@ -282,7 +285,7 @@ LEFT OUTER JOIN users } } - if let Some(username) = self.scope_creator_username.as_deref() { + if let Some(_username) = self.scope_creator_username.as_deref() { if !first_predicate_added { query.push_str(" WHERE users.username = ?"); first_predicate_added = true; diff --git a/crates/lib/database_queries/src/queries/w2l/w2l_results/query_w2l_result.rs b/crates/lib/database_queries/src/queries/w2l/w2l_results/query_w2l_result.rs index 2d8339d6ce..a0302b11c0 100644 --- a/crates/lib/database_queries/src/queries/w2l/w2l_results/query_w2l_result.rs +++ b/crates/lib/database_queries/src/queries/w2l/w2l_results/query_w2l_result.rs @@ -1,13 +1,15 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::helpers::boolean_converters::nullable_i8_to_bool; -use log::{info, warn, log}; +use enums::core::visibility::Visibility; +use log::warn; use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; #[derive(Serialize)] pub struct W2lResultRecordForResponse { @@ -30,7 +32,7 @@ pub struct W2lResultRecordForResponse { pub maybe_template_creator_display_name: Option, pub maybe_template_creator_gravatar_hash: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub file_size_bytes: u32, pub frame_width: u32, @@ -148,8 +150,8 @@ pub async fn select_w2l_result_by_token( maybe_template_creator_gravatar_hash: ir.maybe_template_creator_gravatar_hash, // NB: Fail open/public since we're already looking at it - creator_set_visibility: RecordVisibility::from_str(&ir.creator_set_visibility) - .unwrap_or(RecordVisibility::Public), + creator_set_visibility: Visibility::from_str(&ir.creator_set_visibility) + .unwrap_or(Visibility::Public), //template_is_mod_approved: if ir.template_is_mod_approved == 0 { false } else { true }, diff --git a/crates/lib/database_queries/src/queries/w2l/w2l_templates/get_w2l_template.rs b/crates/lib/database_queries/src/queries/w2l/w2l_templates/get_w2l_template.rs index 3c9df4abb3..f50f3f97f7 100644 --- a/crates/lib/database_queries/src/queries/w2l/w2l_templates/get_w2l_template.rs +++ b/crates/lib/database_queries/src/queries/w2l/w2l_templates/get_w2l_template.rs @@ -1,14 +1,15 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use anyhow::anyhow; use chrono::{DateTime, Utc}; use container_common::anyhow_result::AnyhowResult; -use crate::column_types::record_visibility::RecordVisibility; use crate::helpers::boolean_converters::{nullable_i8_to_optional_bool, i8_to_bool}; -use log::{info, warn, log}; +use enums::core::visibility::Visibility; +use log::warn; use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; -use std::sync::Arc; // FIXME: This is the old style of query scoping and shouldn't be copied. // The moderator-only fields are good practice, though. @@ -29,7 +30,7 @@ pub struct W2lTemplateRecordForResponse { pub duration_millis: u32, pub maybe_image_object_name: Option, pub maybe_video_object_name: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_public_listing_approved: Option, pub is_locked_from_use: bool, pub is_locked_from_user_modification: bool, @@ -122,8 +123,8 @@ pub async fn select_w2l_template_by_token( maybe_image_object_name: template.maybe_public_bucket_preview_image_object_name, maybe_video_object_name: template.maybe_public_bucket_preview_video_object_name, // NB: Fail open/public with creator_set_visibility since we're already looking at it - creator_set_visibility: RecordVisibility::from_str(&template.creator_set_visibility) - .unwrap_or(RecordVisibility::Public), + creator_set_visibility: Visibility::from_str(&template.creator_set_visibility) + .unwrap_or(Visibility::Public), is_public_listing_approved: nullable_i8_to_optional_bool(template.is_public_listing_approved), is_locked_from_use: i8_to_bool(template.is_locked_from_use), is_locked_from_user_modification: i8_to_bool(template.is_locked_from_user_modification), diff --git a/crates/lib/errors/Cargo.toml b/crates/lib/errors/Cargo.toml new file mode 100644 index 0000000000..5993c618d8 --- /dev/null +++ b/crates/lib/errors/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "errors" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +# NB: Public "errors" crate is name-squatted, so we're fine to use this internally. +name = "errors" +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0.40" + +[dev-dependencies] +# None yet diff --git a/crates/lib/errors/src/lib.rs b/crates/lib/errors/src/lib.rs new file mode 100644 index 0000000000..dcaa0af2fe --- /dev/null +++ b/crates/lib/errors/src/lib.rs @@ -0,0 +1,27 @@ +//! errors +//! +//! The purpose of this library is to pin to a single 'anyhow' and also develop +//! common error utilities. +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +/// Easier to import than anyhow::Result. +/// (Naming things "Result" pollutes the import scope or requires nasty renames. Gross.) +pub type AnyhowResult = anyhow::Result; + +/// Useful re-export. +pub use anyhow::anyhow; +pub use anyhow::bail; diff --git a/crates/lib/files/Cargo.toml b/crates/lib/files/Cargo.toml new file mode 100644 index 0000000000..3fa52dadda --- /dev/null +++ b/crates/lib/files/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "files" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +# NB: Public "files" crate is some CDN nonsense we'll never use, so we're good to +# use this crate name internally. +name = "files" +path = "src/lib.rs" + +[dependencies] +data-encoding = "2.3.3" # base64, hex, etc. +infer = "0.11" # Mimetype detection +ring = "0.17.0-alpha.11" # Crypto + +[dev-dependencies] +# None yet diff --git a/crates/lib/files/src/hash.rs b/crates/lib/files/src/hash.rs new file mode 100644 index 0000000000..450f8374dd --- /dev/null +++ b/crates/lib/files/src/hash.rs @@ -0,0 +1,39 @@ +use data_encoding::HEXLOWER_PERMISSIVE; +use ring::digest::{Context, Digest, SHA256}; +use std::io::{BufReader, Read}; + +pub fn hash_bytes_sha2(bytes: &[u8]) -> std::io::Result { + let reader = BufReader::new(bytes); + let digest = sha256_digest(reader)?; + let hash = HEXLOWER_PERMISSIVE.encode(digest.as_ref()); + Ok(hash) +} + +fn sha256_digest(mut reader: R) -> std::io::Result { + let mut context = Context::new(&SHA256); + let mut buffer = [0; 1024]; + + loop { + let count = reader.read(&mut buffer)?; + if count == 0 { + break; + } + context.update(&buffer[..count]); + } + + Ok(context.finish()) +} + +#[cfg(test)] +mod tests { + use crate::hash::hash_bytes_sha2; + + #[test] + fn test_hash_bytes_sha2() { + assert_eq!(hash_bytes_sha2(&[]).unwrap(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + assert_eq!(hash_bytes_sha2(&[0, 0, 0, 0]).unwrap(), "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"); + assert_eq!(hash_bytes_sha2(&[0, 255, 0, 255]).unwrap(), "7a7bf454c5f3cb1b9d9a20f81417f98d976fe3b3dd52c1b9968f02e89e7e8a2f"); + assert_eq!(hash_bytes_sha2("testing".as_bytes()).unwrap(), "cf80cd8aed482d5d1527d7dc72fceff84e6326592848447d2dc0b0e87dfc9a90"); + assert_eq!(hash_bytes_sha2("hash this".as_bytes()).unwrap(), "19467788bc0cf11790a075ea718452cecf0e79db59d1964670475e5fe2e4a611"); + } +} diff --git a/crates/lib/files/src/lib.rs b/crates/lib/files/src/lib.rs new file mode 100644 index 0000000000..63673830ad --- /dev/null +++ b/crates/lib/files/src/lib.rs @@ -0,0 +1,21 @@ +//! files +//! +//! The purpose of this library is to group common file (or in-memory file) operations. +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +pub mod hash; +pub mod mimetype; diff --git a/crates/lib/files/src/mimetype.rs b/crates/lib/files/src/mimetype.rs new file mode 100644 index 0000000000..444515c9ba --- /dev/null +++ b/crates/lib/files/src/mimetype.rs @@ -0,0 +1,41 @@ + +const DEFAULT_BINARY_MIMETYPE : &'static str = "application/octet-stream"; + +pub fn get_mimetype_for_bytes(bytes: &[u8]) -> Option<&'static str> { + infer::get(bytes) + .map(|typ| typ.mime_type()) +} + +pub fn get_mimetype_for_bytes_or_default(bytes: &[u8]) -> &'static str { + infer::get(bytes) + .map(|typ| typ.mime_type()) + .unwrap_or(DEFAULT_BINARY_MIMETYPE) +} + +#[cfg(test)] +mod tests { + use crate::mimetype::{get_mimetype_for_bytes, get_mimetype_for_bytes_or_default}; + + #[test] + fn test_mimetype_png() { + // NB: From "Hanashi Mask.png" + let bytes : Vec = vec![ + 137, 80, 78, 71, 13, 10, 26, 10, + 0, 0, 0, 13, 73, 72, 68, 82, + 0, 0, 20, 193, 0, 0, 15, 239, + 8, 6, 0, 0, 0, 163, 131, 119, + 122, 0, 0, 0, + ]; + + assert_eq!(get_mimetype_for_bytes(&bytes), Some("image/png")); + assert_eq!(get_mimetype_for_bytes_or_default(&bytes), "image/png"); + } + + #[test] + fn test_mimetype_unknown() { + let bytes : Vec = vec![]; + + assert_eq!(get_mimetype_for_bytes(&bytes), None); + assert_eq!(get_mimetype_for_bytes_or_default(&bytes), "application/octet-stream"); + } +} diff --git a/crates/lib/http_server_common/Cargo.toml b/crates/lib/http_server_common/Cargo.toml index ae6e251573..10554fd2a9 100644 --- a/crates/lib/http_server_common/Cargo.toml +++ b/crates/lib/http_server_common/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" # Internal container_common = { path = "../container_common" } -reusable_types = { path = "../reusable_types" } +reusable_types = { path = "../../_types/reusable_types" } # External anyhow = "1.0.40" diff --git a/crates/lib/media/Cargo.toml b/crates/lib/media/Cargo.toml new file mode 100644 index 0000000000..e6c8a945c2 --- /dev/null +++ b/crates/lib/media/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "media" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +name = "media" # NB: Public "media" crate was squatted, so we're good to use this name internally. +path = "src/lib.rs" + +[dependencies] + +# Internal +errors = { path = "../errors" } + +# External +log = "0.4.14" + +# Powerful audio decoder and demuxer +symphonia = { version = "0.5.1", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "vorbis", "wav"] } + +[dev-dependencies] +# None yet diff --git a/crates/lib/media/src/decode_basic_audio_info.rs b/crates/lib/media/src/decode_basic_audio_info.rs new file mode 100644 index 0000000000..06ccd1f048 --- /dev/null +++ b/crates/lib/media/src/decode_basic_audio_info.rs @@ -0,0 +1,388 @@ +use errors::AnyhowResult; +use std::io::Cursor; +use symphonia::core::audio::SampleBuffer; +use symphonia::core::formats::{FormatOptions, FormatReader, Track}; +use symphonia::core::io::{MediaSourceStream, ReadOnlySource}; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; + +// Returned if nothing could be decoded +const NO_AUDIO_INFO : BasicAudioInfo = BasicAudioInfo { duration_millis: None, codec_name: None, required_full_decode: false }; + +#[derive(Clone)] +pub struct BasicAudioInfo { + pub duration_millis: Option, + pub codec_name: Option, + pub required_full_decode: bool, +} + +/// Decode audio info from an audio or video file containing audio streams. +/// This handles multiple formats and codecs. +pub fn decode_basic_audio_info( + audio_bytes: &[u8], + maybe_mimetype: Option<&str>, + maybe_extension: Option<&str>, +) -> AnyhowResult { + + // FIXME(bt, 2022-12-21): This is horribly inefficient. + let bytes = audio_bytes.to_vec(); + let reader = Cursor::new(bytes); + let source = ReadOnlySource::new(reader); + let mss = MediaSourceStream::new(Box::new(source), Default::default()); + + let mut hint = Hint::new(); + if let Some(extension) = maybe_extension { + hint.with_extension(extension); + } + if let Some(mimetype) = maybe_mimetype { + hint.mime_type(mimetype); + } + + // Use the default options for metadata and format readers. + let meta_opts: MetadataOptions = Default::default(); + let fmt_opts: FormatOptions = Default::default(); + + // Probe the media source. + let probed = symphonia::default::get_probe() + .format(&hint, mss, &fmt_opts, &meta_opts)?; + + let mut format = probed.format; + + let audio_track = match find_audio_track(&format) { + None => return Ok(NO_AUDIO_INFO.clone()), + Some(default_track) => default_track, + }; + + let mut maybe_track_duration = audio_track.codec_params.time_base + .zip(audio_track.codec_params.n_frames) + .map(|(time_base, n_frames)| { + // NB: This yields the duration of the track + time_base.calc_time(n_frames) + }) + .map(|time| { + let duration_millis = time.seconds * 1000; + let frac_millis = (time.frac * 1000.0).trunc() as u64; + duration_millis + frac_millis + }); + + let maybe_codec_name = get_codec_short_name(&audio_track); + + let mut required_full_decode = false; + + if maybe_track_duration.is_none() { + // NB: The number of samples could not be determined, so we need to decode the file. + // See https://github.com/pdeljanov/Symphonia/issues/18#issuecomment-770157948 + + maybe_track_duration = read_duration(&mut format)?; + required_full_decode = true; + } + + Ok(BasicAudioInfo { + duration_millis: maybe_track_duration, + codec_name: maybe_codec_name, + required_full_decode, + }) +} + +fn find_audio_track(format: &Box) -> Option<&Track> { + // For audio files, the default track is sufficient. + // Video files include multiple streams, and they will need to be searched. + + for track in format.tracks() { + match track.codec_params.codec { + symphonia::core::codecs::CODEC_TYPE_NULL => continue, // NB: Eg. unsupported, video, etc. + _ => return Some(track), + } + } + + format.default_track() +} + +fn get_codec_short_name(track: &Track) -> Option { + let codec_registry = symphonia::default::get_codecs(); + + if let Some(codec_descriptor) = codec_registry.get_codec(track.codec_params.codec) { + return Some(codec_descriptor.short_name.to_string()); + } + + match track.codec_params.codec { + // NB: Opus (which is newer than Vorbis and for streaming) support has not landed yet, but the + // Symphonia library knows which tracks are Opus-encoded. + // https://github.com/pdeljanov/Symphonia/issues/8 + symphonia::core::codecs::CODEC_TYPE_OPUS => Some("opus".to_string()), + _ => None, + } +} + +fn read_duration(format: &mut Box) -> AnyhowResult> { + // NB: Code adapted from symphonia repo example code. + let audio_track = match find_audio_track(format) { + None => return Ok(None), + Some(track) => track, + }; + + let decoder_opts = Default::default(); + + // Create a decoder for the track. + let mut decoder = symphonia::default::get_codecs() + .make(&audio_track.codec_params, &decoder_opts)?; + + let channel_count = match audio_track.codec_params.channels { + None => return Ok(None), + Some(channels) => channels.count(), + }; + + let sample_rate = match audio_track.codec_params.sample_rate { + None => return Ok(None), + Some(sample_rate) => sample_rate, + }; + + // Store the track identifier, we'll use it to filter packets. + let track_id = audio_track.id; + + let mut sample_count = 0; + let mut sample_buf = None; + + loop { + // Get the next packet from the format reader. + let packet = match format.next_packet() { + Ok(packet) => packet, + Err(_e) => break, + }; + + // If the packet does not belong to the selected track, skip it. + if packet.track_id() != track_id { + continue; + } + + // Decode the packet into audio samples, ignoring any decode errors. + match decoder.decode(&packet) { + Ok(audio_buf) => { + // The decoded audio samples may now be accessed via the audio buffer if per-channel + // slices of samples in their native decoded format is desired. Use-cases where + // the samples need to be accessed in an interleaved order or converted into + // another sample format, or a byte buffer is required, are covered by copying the + // audio buffer into a sample buffer or raw sample buffer, respectively. In the + // example below, we will copy the audio buffer into a sample buffer in an + // interleaved order while also converting to a f32 sample format. + + // If this is the *first* decoded packet, create a sample buffer matching the + // decoded audio buffer format. + if sample_buf.is_none() { + // Get the audio buffer specification. + let spec = *audio_buf.spec(); + + // Get the capacity of the decoded buffer. Note: This is capacity, not length! + let duration = audio_buf.capacity() as u64; + + // Create the f32 sample buffer. + sample_buf = Some(SampleBuffer::::new(duration, spec)); + } + + // Copy the decoded audio buffer into the sample buffer in an interleaved format. + if let Some(buf) = &mut sample_buf { + buf.copy_interleaved_ref(audio_buf); + + // The samples may now be access via the `samples()` function. + sample_count += buf.samples().len(); + } + } + Err(symphonia::core::errors::Error::DecodeError(_)) => (), + Err(_) => break, + } + } + + let channel_samples = sample_count / channel_count; + let duration_millis = ((channel_samples as f32) / (sample_rate as f32) * 1000.0) as u64; + + Ok(Some(duration_millis)) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + use errors::AnyhowResult; + use crate::decode_basic_audio_info::decode_basic_audio_info; + + fn test_file(path_from_repo_root: &str) -> PathBuf { + // https://doc.rust-lang.org/cargo/reference/environment-variables.html + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push(format!("../../../{}", path_from_repo_root)); + path + } + + mod audio { + use super::*; + + #[test] + fn aac() -> AnyhowResult<()> { + // NB: ffprobe -i output: + // [aac @ 0x55d4e03a4ec0] Estimating duration from bitrate, this may be inaccurate + // Input #0, aac, from 'audio/aac/golden_sun_elemental_stars_cyanne.aac': + // Duration: 00:00:40.05, bitrate: 128 kb/s + // Stream #0:0: Audio: aac (LC), 16000 Hz, stereo, fltp, 128 kb/s + // It might be faster to use ffmpeg's estimation method. + let path = test_file("test_data/audio/aac/golden_sun_elemental_stars_cyanne.aac"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("aac".to_string())); + assert_eq!(info.duration_millis, Some(40128)); + assert_eq!(info.required_full_decode, true); + Ok(()) + } + + #[test] + fn flac() -> AnyhowResult<()> { + let path = test_file("test_data/audio/flac/zelda_ocarina_small_item.flac"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("flac".to_string())); + assert_eq!(info.duration_millis, Some(5120)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + #[test] + fn flac_wrong_extension() -> AnyhowResult<()> { + let path = test_file("test_data/audio/flac/zelda_ocarina_small_item.flac"); + let bytes = std::fs::read(path)?; + let incorrect_mimetype = Some("audio/wav"); + let incorrect_extension = Some("wav"); + let info = decode_basic_audio_info(&bytes, incorrect_mimetype, incorrect_extension)?; + assert_eq!(info.codec_name, Some("flac".to_string())); + assert_eq!(info.duration_millis, Some(5120)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + // NB: Requires symphonia 'aac' and 'isomp4' feature flags + #[test] + fn m4a() -> AnyhowResult<()> { + let path = test_file("test_data/audio/m4a/super_mario_bros_lost_life.m4a"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("alac".to_string())); + assert_eq!(info.duration_millis, Some(5493)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + // NB: Requires symphonia 'mp3' feature flag + #[test] + fn mp3() -> AnyhowResult<()> { + let path = test_file("test_data/audio/mp3/super_mario_rpg_beware_the_forests_mushrooms.mp3"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("mp3".to_string())); + assert_eq!(info.duration_millis, Some(15023)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + #[test] + fn ogg() -> AnyhowResult<()> { + // According to ffprobe (which ever so slightly disagrees with our calculation), + // Duration: 00:00:04.90, start: 0.000000, bitrate: 83 kb/s + // length : 4.94 + let path = test_file("test_data/audio/ogg/banjo-kazooie_jiggy_appearance.ogg"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("vorbis".to_string())); + assert_eq!(info.duration_millis, Some(4903)); // NB: This disagrees with ffprobe, but it's pretty close. + assert_eq!(info.required_full_decode, true); + Ok(()) + } + + #[test] + fn wav_pcm_s16le_16khz() -> AnyhowResult<()> { + let path = test_file("test_data/audio/wav/sm64_mario_its_me.wav"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("pcm_s16le".to_string())); + assert_eq!(info.duration_millis, Some(1891)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + #[test] + fn wav_pcm_s16le_44khz() -> AnyhowResult<()> { + let path = test_file("test_data/audio/wav/smrpg_correct.wav"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("pcm_s16le".to_string())); + assert_eq!(info.duration_millis, Some(847)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + #[test] + fn wav_pcm_f32() -> AnyhowResult<()> { + let path = test_file("test_data/audio/wav/smrpg_battlestart_f32.wav"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("pcm_f32le".to_string())); + assert_eq!(info.duration_millis, Some(708)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + #[test] + fn wav_unsigned() -> AnyhowResult<()> { + let path = test_file("test_data/audio/wav/smrpg_item_mushroom_8u.wav"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("pcm_u8".to_string())); + assert_eq!(info.duration_millis, Some(1741)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + } + + mod video { + use super::*; + + #[test] + fn mkv_h264_video_opus_audio() -> AnyhowResult<()> { + let path = test_file("test_data/video/mkv/fake_you.mkv"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("opus".to_string())); + assert_eq!(info.duration_millis, Some(15007)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + #[test] + fn mov_h264_video_aac_audio() -> AnyhowResult<()> { + let path = test_file("test_data/video/mov/majoras_mask_intro.mov"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("aac".to_string())); + assert_eq!(info.duration_millis, Some(30128)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + #[test] + fn mp4_h264_video_aac_audio() -> AnyhowResult<()> { + let path = test_file("test_data/video/mp4/golden_sun_garoh.mp4"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("aac".to_string())); + assert_eq!(info.duration_millis, Some(15295)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + + #[test] + fn webm_vp9_video_opus_audio() -> AnyhowResult<()> { + let path = test_file("test_data/video/webm/laser_pong.webm"); + let bytes = std::fs::read(path)?; + let info = decode_basic_audio_info(&bytes, None, None)?; + assert_eq!(info.codec_name, Some("opus".to_string())); + assert_eq!(info.duration_millis, Some(10016)); + assert_eq!(info.required_full_decode, false); + Ok(()) + } + } +} diff --git a/crates/lib/media/src/lib.rs b/crates/lib/media/src/lib.rs new file mode 100644 index 0000000000..2e60c385ff --- /dev/null +++ b/crates/lib/media/src/lib.rs @@ -0,0 +1,21 @@ +//! media +//! +//! The purpose of this library is to package audio/video related functions together until they need +//! to be split apart. +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +pub mod decode_basic_audio_info; \ No newline at end of file diff --git a/crates/lib/redis_common/src/redis_keys.rs b/crates/lib/redis_common/src/redis_keys.rs index c151d73c91..b0afb2e4ad 100644 --- a/crates/lib/redis_common/src/redis_keys.rs +++ b/crates/lib/redis_common/src/redis_keys.rs @@ -12,6 +12,13 @@ impl RedisKeys { format!("ttsUseCount:{}", model_token) } + /// This is a counter incremented with web VC inference. + /// We can use a job to read MySql and fix counts if they get out of sync. + /// These should be long-lived keys. + pub fn web_vc_model_usage_count(model_token: &str) -> String { + format!("vcUseCount:{}", model_token) + } + /// This is a counter incremented with TTS inference. /// We can use a job to read MySql and fix counts if they get out of sync. /// These should be long-lived keys. diff --git a/crates/lib/reusable_types/src/entity_visibility.rs b/crates/lib/reusable_types/src/entity_visibility.rs deleted file mode 100644 index 55fea828f7..0000000000 --- a/crates/lib/reusable_types/src/entity_visibility.rs +++ /dev/null @@ -1,66 +0,0 @@ -/// Used in various database tables (as enums! careful!) and the HTTP API to convey -/// how the associated entity should be made visible to the public. -/// -/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY. -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] -#[serde(rename_all = "lowercase")] -#[sqlx(rename_all = "lowercase")] -pub enum EntityVisibility { - /// Public entities are able to be listed in public lists. - /// It does not mean that they necessarily will be (eg. they could be "mod unapproved" or deleted). - Public, - /// Hidden entities are not shown in public lists, but the URL to them may be given out freely. - /// They are available to non-logged-in users as long as they have the URL. - Hidden, - /// Private entities should only be available to the creator, a list of approved users, and - /// website moderation staff. - Private, -} - -/// NB: Legacy API for older code. -impl EntityVisibility { - pub fn to_str(&self) -> &'static str { - match self { - Self::Public => "public", - Self::Hidden => "hidden", - Self::Private => "private", - } - } - - pub fn from_str(value: &str) -> Result { - match value { - "public" => Ok(Self::Public), - "hidden" => Ok(Self::Hidden), - "private" => Ok(Self::Private), - _ => Err(format!("invalid value: {:?}", value)), - } - } -} - -#[cfg(test)] -mod tests { - use crate::entity_visibility::EntityVisibility; - use crate::test_helpers::assert_serialization; - - #[test] - fn test_serialization() { - assert_serialization(EntityVisibility::Public, "public"); - assert_serialization(EntityVisibility::Hidden, "hidden"); - assert_serialization(EntityVisibility::Private, "private"); - } - - #[test] - fn test_to_str() { - assert_eq!(EntityVisibility::Public.to_str(), "public"); - assert_eq!(EntityVisibility::Hidden.to_str(), "hidden"); - assert_eq!(EntityVisibility::Private.to_str(), "private"); - } - - #[test] - fn test_from_str() { - assert_eq!(EntityVisibility::from_str("public").unwrap(), EntityVisibility::Public); - assert_eq!(EntityVisibility::from_str("hidden").unwrap(), EntityVisibility::Hidden); - assert_eq!(EntityVisibility::from_str("private").unwrap(), EntityVisibility::Private); - assert!(EntityVisibility::from_str("foo").is_err()); - } -} diff --git a/crates/lib/reusable_types/src/vocoder_type.rs b/crates/lib/reusable_types/src/vocoder_type.rs deleted file mode 100644 index 1581c99903..0000000000 --- a/crates/lib/reusable_types/src/vocoder_type.rs +++ /dev/null @@ -1,33 +0,0 @@ -#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, sqlx::Type)] -pub enum VocoderType { - #[serde(rename = "hifigan")] - #[sqlx(rename = "hifigan")] - HifiGan, -} - -/// NB: Legacy API for older code. -impl VocoderType { - pub fn to_str(&self) -> &'static str { - match self { - Self::HifiGan=> "hifigan", - } - } - - pub fn from_str(value: &str) -> Result { - match value { - "hifigan" => Ok(Self::HifiGan), - _ => Err(format!("invalid value: {:?}", value)), - } - } -} - -#[cfg(test)] -mod tests { - use crate::test_helpers::assert_serialization; - use crate::vocoder_type::VocoderType; - - #[test] - fn test_serialization() { - assert_serialization(VocoderType::HifiGan, "hifigan"); - } -} diff --git a/crates/lib/url_config/Cargo.toml b/crates/lib/url_config/Cargo.toml index 534cbb5ff3..11f643a027 100644 --- a/crates/lib/url_config/Cargo.toml +++ b/crates/lib/url_config/Cargo.toml @@ -16,7 +16,7 @@ path = "src/lib.rs" # Internal http_server_common = { path = "../../lib/http_server_common" } -reusable_types = { path = "../../lib/reusable_types" } +reusable_types = { path = "../../_types/reusable_types" } # External actix-web = "4.0.0-beta.10" diff --git a/crates/plugins/billing/Cargo.toml b/crates/plugins/billing/Cargo.toml index 6b090e3079..dae42be476 100644 --- a/crates/plugins/billing/Cargo.toml +++ b/crates/plugins/billing/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" container_common = { path = "../../lib/container_common" } database_queries = { path = "../../lib/database_queries" } http_server_common = { path = "../../lib/http_server_common" } -reusable_types = { path = "../../lib/reusable_types" } +reusable_types = { path = "../../_types/reusable_types" } url_config = { path = "../../lib/url_config" } # External diff --git a/crates/plugins/users/Cargo.toml b/crates/plugins/users/Cargo.toml index 215efd96c1..d798745426 100644 --- a/crates/plugins/users/Cargo.toml +++ b/crates/plugins/users/Cargo.toml @@ -17,9 +17,10 @@ path = "src/lib.rs" # Internal container_common = { path = "../../lib/container_common" } database_queries = { path = "../../lib/database_queries" } +enums = { path = "../../_types/enums" } http_server_common = { path = "../../lib/http_server_common" } -reusable_types = { path = "../../lib/reusable_types" } -tokens = { path = "../../lib/tokens" } +reusable_types = { path = "../../_types/reusable_types" } +tokens = { path = "../../_types/tokens" } user_input_common = { path = "../../lib/user_input_common" } # External diff --git a/crates/plugins/users/src/endpoints/edit_profile_handler.rs b/crates/plugins/users/src/endpoints/edit_profile_handler.rs index 1fdf818383..9850dfefd4 100644 --- a/crates/plugins/users/src/endpoints/edit_profile_handler.rs +++ b/crates/plugins/users/src/endpoints/edit_profile_handler.rs @@ -14,11 +14,11 @@ use crate::validations::validate_profile_github_username::validate_profile_githu use crate::validations::validate_profile_twitch_username::validate_profile_twitch_username; use crate::validations::validate_profile_twitter_username::{normalize_twitter_username_for_storage, validate_profile_twitter_username}; use crate::validations::validate_profile_website_url::validate_profile_website_url; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::users::user_profiles::edit_user_profile_as_account_holder::edit_user_profile_as_account_holder; use database_queries::queries::users::user_profiles::edit_user_profile_as_mod::edit_user_profile_as_mod; use database_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username; use database_queries::queries::users::user_profiles::{edit_user_profile_as_account_holder, edit_user_profile_as_mod}; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::warn; @@ -47,8 +47,8 @@ pub struct EditProfileRequest { pub cashapp_username: Option, pub website_url: Option, - pub preferred_tts_result_visibility: Option, - pub preferred_w2l_result_visibility: Option, + pub preferred_tts_result_visibility: Option, + pub preferred_w2l_result_visibility: Option, } #[derive(Serialize)] @@ -242,10 +242,10 @@ pub async fn edit_profile_handler( let ip_address = get_request_ip(&http_request); let preferred_tts_result_visibility = request.preferred_tts_result_visibility - .unwrap_or(RecordVisibility::Hidden); + .unwrap_or(Visibility::Hidden); let preferred_w2l_result_visibility = request.preferred_w2l_result_visibility - .unwrap_or(RecordVisibility::Hidden); + .unwrap_or(Visibility::Hidden); let query_result = if editor_is_original_user { edit_user_profile_as_account_holder( diff --git a/crates/plugins/users/src/endpoints/get_profile_handler.rs b/crates/plugins/users/src/endpoints/get_profile_handler.rs index be46c3011c..7120d372ed 100644 --- a/crates/plugins/users/src/endpoints/get_profile_handler.rs +++ b/crates/plugins/users/src/endpoints/get_profile_handler.rs @@ -12,11 +12,11 @@ use crate::utils::session_checker::SessionChecker; use database_queries::queries::users::user_badges::list_user_badges::UserBadgeForList; use database_queries::queries::users::user_badges::list_user_badges::list_user_badges; use database_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username_from_connection; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_header_optional::get_request_header_optional; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use http_server_common::util::timer::MultiBenchmarkingTimer; use log::warn; -use reusable_types::entity_visibility::EntityVisibility; use sqlx::MySqlPool; use std::fmt; use tokens::users::user::UserToken; @@ -34,8 +34,8 @@ pub struct UserProfileRecordForResponse { pub profile_rendered_html: String, pub user_role_slug: String, pub disable_gravatar: bool, - pub preferred_tts_result_visibility: EntityVisibility, - pub preferred_w2l_result_visibility: EntityVisibility, + pub preferred_tts_result_visibility: Visibility, + pub preferred_w2l_result_visibility: Visibility, pub discord_username: Option, pub twitch_username: Option, pub twitter_username: Option, diff --git a/crates/plugins/users/src/utils/user_session_extended.rs b/crates/plugins/users/src/utils/user_session_extended.rs index 821d416d96..c784484e6b 100644 --- a/crates/plugins/users/src/utils/user_session_extended.rs +++ b/crates/plugins/users/src/utils/user_session_extended.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use database_queries::column_types::record_visibility::RecordVisibility; +use enums::core::visibility::Visibility; #[derive(Clone, Default)] pub struct UserSessionExtended { @@ -54,8 +54,8 @@ impl Default for UserSessionSubscriptionPlan { pub struct UserSessionPreferences { pub disable_gravatar: bool, pub auto_play_audio_preference: Option, - pub preferred_tts_result_visibility: RecordVisibility, - pub preferred_w2l_result_visibility: RecordVisibility, + pub preferred_tts_result_visibility: Visibility, + pub preferred_w2l_result_visibility: Visibility, pub auto_play_video_preference: Option, } diff --git a/crates/plugins/users/src/validations/is_reserved_username.rs b/crates/plugins/users/src/validations/is_reserved_username.rs index 62d1b0d4f8..7ada67b048 100644 --- a/crates/plugins/users/src/validations/is_reserved_username.rs +++ b/crates/plugins/users/src/validations/is_reserved_username.rs @@ -47,6 +47,7 @@ mod tests { assert_eq!(is_reserved_username("vocodes"), true); assert_eq!(is_reserved_username("user"), true); assert_eq!(is_reserved_username("username"), true); + assert_eq!(is_reserved_username("thread"), true); } #[test] diff --git a/crates/service/download_job/Cargo.toml b/crates/service/job/download_job/Cargo.toml similarity index 67% rename from crates/service/download_job/Cargo.toml rename to crates/service/job/download_job/Cargo.toml index 7de6324f9c..24ddd1c559 100644 --- a/crates/service/download_job/Cargo.toml +++ b/crates/service/job/download_job/Cargo.toml @@ -15,18 +15,19 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -jobs_common = { path = "../../lib/jobs_common" } -reusable_types = { path = "../../lib/reusable_types" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -subprocess_common = { path = "../../lib/subprocess_common" } -tts_common = { path = "../../lib/tts_common" } +cloud_storage = { path = "../../../lib/cloud_storage" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +enums = { path = "../../../_types/enums" } +google_drive_common = { path = "../../../lib/google_drive_common" } +jobs_common = { path = "../../../lib/jobs_common" } +reusable_types = { path = "../../../_types/reusable_types" } +subprocess_common = { path = "../../../lib/subprocess_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/download_job/src/job_state.rs b/crates/service/job/download_job/src/job_state.rs similarity index 77% rename from crates/service/download_job/src/job_state.rs rename to crates/service/job/download_job/src/job_state.rs index 18169265e3..b502601a9f 100644 --- a/crates/service/download_job/src/job_state.rs +++ b/crates/service/job/download_job/src/job_state.rs @@ -1,4 +1,8 @@ +use cloud_storage::bucket_client::BucketClient; +use cloud_storage::bucket_path_unifier::BucketPathUnifier; use crate::job_types::hifigan::hifigan_model_check_command::HifiGanModelCheckCommand; +use crate::job_types::hifigan_softvc::hifigan_softvc_model_check_command::HifiGanSoftVcModelCheckCommand; +use crate::job_types::softvc::softvc_model_check_command::SoftVcModelCheckCommand; use crate::job_types::tacotron::tacotron_model_check_command::TacotronModelCheckCommand; use database_queries::mediators::badge_granter::BadgeGranter; use database_queries::mediators::firehose_publisher::FirehosePublisher; @@ -7,8 +11,6 @@ use r2d2_redis::RedisConnectionManager; use r2d2_redis::r2d2; use sqlx::MySqlPool; use std::path::PathBuf; -use storage_buckets_common::bucket_client::BucketClient; -use storage_buckets_common::bucket_path_unifier::BucketPathUnifier; pub struct JobState { pub download_temp_directory: PathBuf, @@ -41,6 +43,8 @@ pub struct JobState { /// Configurations and interfaces to code deployed as sidecars or container mounts. pub struct SidecarConfigs { pub google_drive_downloader: GoogleDriveDownloadCommand, + pub softvc_model_check_command: SoftVcModelCheckCommand, pub tacotron_model_check_command: TacotronModelCheckCommand, pub hifigan_model_check_command: HifiGanModelCheckCommand, + pub hifigan_softvc_model_check_command: HifiGanSoftVcModelCheckCommand, } \ No newline at end of file diff --git a/crates/service/download_job/src/job_steps/job_results.rs b/crates/service/job/download_job/src/job_steps/job_results.rs similarity index 100% rename from crates/service/download_job/src/job_steps/job_results.rs rename to crates/service/job/download_job/src/job_steps/job_results.rs diff --git a/crates/service/download_job/src/job_steps/main_loop.rs b/crates/service/job/download_job/src/job_steps/main_loop.rs similarity index 96% rename from crates/service/download_job/src/job_steps/main_loop.rs rename to crates/service/job/download_job/src/job_steps/main_loop.rs index 36d29753d8..e70395224e 100644 --- a/crates/service/download_job/src/job_steps/main_loop.rs +++ b/crates/service/job/download_job/src/job_steps/main_loop.rs @@ -7,6 +7,7 @@ use jobs_common::noop_logger::NoOpLogger; use log::warn; use std::time::Duration; +// Job runner timeouts (guards MySQL) const START_TIMEOUT_MILLIS : u64 = 500; const INCREASE_TIMEOUT_MILLIS : u64 = 1000; @@ -41,7 +42,7 @@ pub async fn main_loop(job_state: JobState) { match result { Ok(_) => {}, Err(e) => { - warn!("Error querying jobs: {:?}", e); + warn!("Error processing jobs: {:?}", e); std::thread::sleep(Duration::from_millis(error_timeout_millis)); error_timeout_millis += INCREASE_TIMEOUT_MILLIS; continue; diff --git a/crates/service/download_job/src/job_steps/mod.rs b/crates/service/job/download_job/src/job_steps/mod.rs similarity index 100% rename from crates/service/download_job/src/job_steps/mod.rs rename to crates/service/job/download_job/src/job_steps/mod.rs diff --git a/crates/service/download_job/src/job_steps/process_single_job.rs b/crates/service/job/download_job/src/job_steps/process_single_job.rs similarity index 79% rename from crates/service/download_job/src/job_steps/process_single_job.rs rename to crates/service/job/download_job/src/job_steps/process_single_job.rs index 4c2a102337..6d1c3fe59a 100644 --- a/crates/service/download_job/src/job_steps/process_single_job.rs +++ b/crates/service/job/download_job/src/job_steps/process_single_job.rs @@ -4,13 +4,15 @@ use container_common::anyhow_result::AnyhowResult; use container_common::filesystem::safe_delete_temp_directory::safe_delete_temp_directory; use crate::JobState; use crate::job_types::hifigan::process_hifigan_vocoder::process_hifigan_vocoder; +use crate::job_types::hifigan_softvc::process_hifigan_softvc_vocoder::process_hifigan_softvc_vocoder; +use crate::job_types::softvc::process_softvc_model::process_softvc_model; use crate::job_types::tacotron::process_tacotron_model::process_tacotron_model; use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use database_queries::queries::generic_download::job::mark_generic_download_job_done::mark_generic_download_job_done; use database_queries::queries::generic_download::job::mark_generic_download_job_pending_and_grab_lock::mark_generic_download_job_pending_and_grab_lock; +use enums::workers::generic_download_type::GenericDownloadType; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; -use reusable_types::generic_download_type::GenericDownloadType; use tempdir::TempDir; pub async fn process_single_job(job_state: &JobState, job: &AvailableDownloadJob) -> AnyhowResult<()> { @@ -26,6 +28,8 @@ pub async fn process_single_job(job_state: &JobState, job: &AvailableDownloadJob return Ok(()) } + info!("Beginning work on {:?} = {} | {}", job.download_type, job.download_job_token, job.download_url); + // ==================== SETUP TEMP DIRS ==================== // let temp_dir = format!("temp_{}", job.id.0); @@ -69,8 +73,27 @@ pub async fn process_single_job(job_state: &JobState, job: &AvailableDownloadJob entity_token = results.entity_token.clone(); entity_type = results.entity_type.clone(); } - GenericDownloadType::MelGanVocodes => { - return Err(anyhow!("MelGan not yet supported!")); + GenericDownloadType::HifiGanRocketVc => { + let results = process_hifigan_softvc_vocoder( + job_state, + job, + &temp_dir, + &download_filename, + &mut redis_logger, + ).await?; + entity_token = results.entity_token.clone(); + entity_type = results.entity_type.clone(); + } + GenericDownloadType::RocketVc => { + let results = process_softvc_model( + job_state, + job, + &temp_dir, + &download_filename, + &mut redis_logger, + ).await?; + entity_token = results.entity_token.clone(); + entity_type = results.entity_type.clone(); } GenericDownloadType::Tacotron2 => { let results = process_tacotron_model( diff --git a/crates/service/download_job/src/job_types/hifigan/hifigan_model_check_command.rs b/crates/service/job/download_job/src/job_types/hifigan/hifigan_model_check_command.rs similarity index 100% rename from crates/service/download_job/src/job_types/hifigan/hifigan_model_check_command.rs rename to crates/service/job/download_job/src/job_types/hifigan/hifigan_model_check_command.rs index 37a8cc5d62..9bf645a400 100644 --- a/crates/service/download_job/src/job_types/hifigan/hifigan_model_check_command.rs +++ b/crates/service/job/download_job/src/job_types/hifigan/hifigan_model_check_command.rs @@ -1,7 +1,7 @@ use crate::AnyhowResult; use log::info; -use subprocess::{Popen, PopenConfig}; use std::path::Path; +use subprocess::{Popen, PopenConfig}; use subprocess_common::docker_options::DockerOptions; /// This command is used to check hifigan for being a real model diff --git a/crates/service/download_job/src/job_types/hifigan/mod.rs b/crates/service/job/download_job/src/job_types/hifigan/mod.rs similarity index 100% rename from crates/service/download_job/src/job_types/hifigan/mod.rs rename to crates/service/job/download_job/src/job_types/hifigan/mod.rs diff --git a/crates/service/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs b/crates/service/job/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs similarity index 98% rename from crates/service/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs rename to crates/service/job/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs index 12665dafbb..95c4324afe 100644 --- a/crates/service/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs +++ b/crates/service/job/download_job/src/job_types/hifigan/process_hifigan_vocoder.rs @@ -8,9 +8,9 @@ use crate::JobState; use crate::job_steps::job_results::JobResults; use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use database_queries::queries::vocoder::insert_vocoder_model::{Args, insert_vocoder_model}; +use enums::ml::vocoder_type::VocoderType; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; -use reusable_types::vocoder_type::VocoderType; use std::fs::File; use std::io::Read; use std::path::PathBuf; diff --git a/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs b/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs new file mode 100644 index 0000000000..054b98f897 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/hifigan_softvc/hifigan_softvc_model_check_command.rs @@ -0,0 +1,84 @@ +use crate::AnyhowResult; +use log::info; +use std::path::Path; +use subprocess::{Popen, PopenConfig}; +use subprocess_common::docker_options::DockerOptions; + +/// This command is used to check hifigan (softvc) for being a real model +#[derive(Clone)] +pub struct HifiGanSoftVcModelCheckCommand { + /// Where the HifiGan (softvc version) code lives + hifigan_softvc_root_code_directory: String, + + /// eg. `source python/bin/activate` + maybe_virtual_env_activation_command: Option, + + hifigan_softvc_model_check_script_name: String, + + /// If this is run under Docker (eg. in development), these are the options. + maybe_docker_options: Option, +} + +impl HifiGanSoftVcModelCheckCommand { + pub fn new( + hifigan_softvc_root_code_directory: &str, + maybe_virtual_env_activation_command: Option<&str>, + hifigan_softvc_model_check_script_name: &str, + maybe_docker_options: Option, + ) -> Self { + Self { + hifigan_softvc_root_code_directory: hifigan_softvc_root_code_directory.to_string(), + maybe_virtual_env_activation_command: maybe_virtual_env_activation_command.map(|s| s.to_string()), + hifigan_softvc_model_check_script_name: hifigan_softvc_model_check_script_name.to_string(), + maybe_docker_options, + } + } + + pub fn execute>( + &self, + checkpoint_path: P, + output_metadata_filename: P, + ) -> AnyhowResult<()> { + + let mut command = String::new(); + command.push_str(&format!("cd {}", self.hifigan_softvc_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("python "); + command.push_str(&self.hifigan_softvc_model_check_script_name); + command.push_str(" --checkpoint_path "); + command.push_str(&checkpoint_path.as_ref().display().to_string()); + command.push_str(" --output_metadata_filename "); + command.push_str(&output_metadata_filename.as_ref().display().to_string()); + + 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 p = Popen::create(&command_parts, PopenConfig { + ..Default::default() + })?; + + info!("Pid : {:?}", p.pid()); + + let exit_status = p.wait()?; + + info!("Exit status: {:?}", exit_status); + + Ok(()) + } +} diff --git a/crates/service/job/download_job/src/job_types/hifigan_softvc/mod.rs b/crates/service/job/download_job/src/job_types/hifigan_softvc/mod.rs new file mode 100644 index 0000000000..144e48ea3a --- /dev/null +++ b/crates/service/job/download_job/src/job_types/hifigan_softvc/mod.rs @@ -0,0 +1,2 @@ +pub mod hifigan_softvc_model_check_command; +pub mod process_hifigan_softvc_vocoder; diff --git a/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs b/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs new file mode 100644 index 0000000000..a8aa552d41 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/hifigan_softvc/process_hifigan_softvc_vocoder.rs @@ -0,0 +1,134 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +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 container_common::hashing::hash_file_sha2::hash_file_sha2; +use crate::JobState; +use crate::job_steps::job_results::JobResults; +use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; +use database_queries::queries::vocoder::insert_vocoder_model::{Args, insert_vocoder_model}; +use enums::ml::vocoder_type::VocoderType; +use jobs_common::redis_job_status_logger::RedisJobStatusLogger; +use log::{info, warn}; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use tempdir::TempDir; + +/// Returns the token of the entity. +pub async fn process_hifigan_softvc_vocoder<'a, 'b>( + job_state: &JobState, + job: &AvailableDownloadJob, + temp_dir: &TempDir, + download_filename: &str, + redis_logger: &'a mut RedisJobStatusLogger<'b>, +) -> AnyhowResult { + + // ==================== RUN MODEL CHECK ==================== // + + info!("Checking that model is valid..."); + + redis_logger.log_status("checking hifigan (softvc) model")?; + + let file_path = PathBuf::from(download_filename.clone()); + + let output_metadata_fs_path = temp_dir.path().join("metadata.json"); + + let model_check_result = job_state.sidecar_configs.hifigan_softvc_model_check_command.execute( + &file_path, + &output_metadata_fs_path + ); + + if let Err(e) = model_check_result { + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + return Err(anyhow!("model check error: {:?}", e)); + } + + // ==================== CHECK ALL FILES EXIST AND GET METADATA ==================== // + + info!("Checking that metadata output file exists..."); + + check_file_exists(&output_metadata_fs_path)?; + + let file_metadata = match read_metadata_file(&output_metadata_fs_path) { + Ok(metadata) => metadata, + Err(e) => { + safe_delete_temp_file(&file_path); + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_directory(&temp_dir); + return Err(e); + } + }; + + // ==================== UPLOAD MODEL FILE ==================== // + + info!("Uploading HifiGan (softvc) vocoder to GCS..."); + + let private_bucket_hash = hash_file_sha2(&download_filename)?; + + info!("File hash: {}", private_bucket_hash); + + let model_bucket_path = job_state.bucket_path_unifier.vocoder_path(&private_bucket_hash); + + info!("Destination bucket path: {:?}", &model_bucket_path); + + redis_logger.log_status("uploading hifigan model")?; + + if let Err(e) = job_state.bucket_client.upload_filename(&model_bucket_path, &file_path).await { + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + return Err(e); + } + + // ==================== DELETE DOWNLOADED FILE ==================== // + + // NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk... + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + + // ==================== SAVE RECORDS ==================== // + + info!("Saving model record..."); + let (_id, model_token) = insert_vocoder_model(Args { + vocoder_type: VocoderType::HifiGanRocketVc, // NB: "rocket_vc" is an internal codename for softvc. + title: &job.title, + original_download_url: &job.download_url, + original_filename: &download_filename, + file_size_bytes: file_metadata.file_size_bytes, + creator_user_token: &job.creator_user_token, + creator_ip_address: &job.creator_ip_address, + creator_set_visibility: job.creator_set_visibility, + private_bucket_hash: &private_bucket_hash, + private_bucket_object_name: &model_bucket_path, + mysql_pool: &job_state.mysql_pool + }).await?; + + job_state.badge_granter.maybe_grant_softvc_vocoder_model_uploads_badge(&job.creator_user_token) + .await + .map_err(|e| { + warn!("error maybe awarding badge: {:?}", e); + anyhow!("error maybe awarding badge") + })?; + + Ok(JobResults { + entity_token: Some(model_token), + entity_type: Some("hifigan_rocket_vc".to_string()), // NB: This may be different from `GenericDownloadType` in the future! + }) +} + +#[derive(Deserialize)] +struct FileMetadata { + pub file_size_bytes: u64, +} + +fn read_metadata_file(filename: &PathBuf) -> AnyhowResult { + let mut file = File::open(filename)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + Ok(serde_json::from_str(&buffer)?) +} + diff --git a/crates/service/download_job/src/job_types/mod.rs b/crates/service/job/download_job/src/job_types/mod.rs similarity index 56% rename from crates/service/download_job/src/job_types/mod.rs rename to crates/service/job/download_job/src/job_types/mod.rs index 9f7e3cc1f8..8342328df3 100644 --- a/crates/service/download_job/src/job_types/mod.rs +++ b/crates/service/job/download_job/src/job_types/mod.rs @@ -1,3 +1,5 @@ pub mod hifigan; +pub mod hifigan_softvc; +pub mod softvc; pub mod tacotron; pub mod talknet; \ No newline at end of file diff --git a/crates/service/job/download_job/src/job_types/softvc/mod.rs b/crates/service/job/download_job/src/job_types/softvc/mod.rs new file mode 100644 index 0000000000..87ade21b43 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/softvc/mod.rs @@ -0,0 +1,2 @@ +pub mod softvc_model_check_command; +pub mod process_softvc_model; diff --git a/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs new file mode 100644 index 0000000000..c7edd08b46 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/softvc/process_softvc_model.rs @@ -0,0 +1,134 @@ +use anyhow::anyhow; +use container_common::anyhow_result::AnyhowResult; +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 container_common::hashing::hash_file_sha2::hash_file_sha2; +use crate::JobState; +use crate::job_steps::job_results::JobResults; +use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; +use database_queries::queries::voice_conversion::models::insert_voice_conversion_model_from_download_job::{Args, insert_voice_conversion_model_from_download_job}; +use enums::core::visibility::Visibility; +use jobs_common::redis_job_status_logger::RedisJobStatusLogger; +use log::{info, warn}; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use tempdir::TempDir; + +/// Returns the token of the entity. +pub async fn process_softvc_model<'a, 'b>( + job_state: &JobState, + job: &AvailableDownloadJob, + temp_dir: &TempDir, + download_filename: &str, + redis_logger: &'a mut RedisJobStatusLogger<'b>, +) -> AnyhowResult { + + // ==================== RUN MODEL CHECK ==================== // + + info!("Checking that softvc model is valid..."); + + redis_logger.log_status("checking rocket vc model")?; // NB: "rocket vc" is a codename + + let file_path = PathBuf::from(download_filename.clone()); + + let output_metadata_fs_path = temp_dir.path().join("metadata.json"); + + let model_check_result = job_state.sidecar_configs.softvc_model_check_command.execute( + &file_path, + &output_metadata_fs_path, + ); + + if let Err(e) = model_check_result { + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + return Err(anyhow!("model check error: {:?}", e)); + } + + // ==================== CHECK ALL FILES EXIST AND GET METADATA ==================== // + + info!("Checking that metadata output file exists..."); + + check_file_exists(&output_metadata_fs_path)?; + + let file_metadata = match read_metadata_file(&output_metadata_fs_path) { + Ok(metadata) => metadata, + Err(e) => { + safe_delete_temp_file(&file_path); + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_directory(&temp_dir); + return Err(e); + } + }; + + // ==================== UPLOAD MODEL FILE ==================== // + + info!("Uploading SoftVC model to GCS..."); + + let private_bucket_hash = hash_file_sha2(&download_filename)?; + + info!("File hash: {}", private_bucket_hash); + + let model_bucket_path = job_state.bucket_path_unifier.softvc_model_path(&private_bucket_hash); + + info!("Destination bucket path: {:?}", &model_bucket_path); + + redis_logger.log_status("uploading rocket vc model")?; // NB: "rocket vc" is a codename + + if let Err(e) = job_state.bucket_client.upload_filename(&model_bucket_path, &file_path).await { + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + return Err(e); + } + + // ==================== DELETE DOWNLOADED FILE ==================== // + + // NB: We should be using a tempdir, but to make absolutely certain we don't overflow the disk... + safe_delete_temp_file(&output_metadata_fs_path); + safe_delete_temp_file(&file_path); + safe_delete_temp_directory(&temp_dir); + + // ==================== SAVE RECORDS ==================== // + + info!("Saving Soft VC record..."); + + let (_id, model_token) = insert_voice_conversion_model_from_download_job(Args { + title: &job.title, + original_download_url: &job.download_url, + original_filename: &download_filename, + file_size_bytes: file_metadata.file_size_bytes, + creator_user_token: &job.creator_user_token, + creator_ip_address: &job.creator_ip_address, + creator_set_visibility: Visibility::Public, // TODO: All models default to public at start + private_bucket_hash: &private_bucket_hash, + private_bucket_object_name: &model_bucket_path, + mysql_pool: &job_state.mysql_pool, + }).await?; + + job_state.badge_granter.maybe_grant_softvc_vocoder_model_uploads_badge(&job.creator_user_token) + .await + .map_err(|e| { + warn!("error maybe awarding badge: {:?}", e); + anyhow!("error maybe awarding badge") + })?; + + Ok(JobResults { + entity_token: Some(model_token.to_string()), + // NB(1): "rocket_vc" is codename for softvc + entity_type: Some("rocket_vc".to_string()), // NB(2): This may be different from `GenericDownloadType` in the future! + }) +} + +#[derive(Deserialize)] +struct FileMetadata { + pub file_size_bytes: u64, +} + +fn read_metadata_file(filename: &PathBuf) -> AnyhowResult { + let mut file = File::open(filename)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + Ok(serde_json::from_str(&buffer)?) +} diff --git a/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs b/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs new file mode 100644 index 0000000000..18e0b9c717 --- /dev/null +++ b/crates/service/job/download_job/src/job_types/softvc/softvc_model_check_command.rs @@ -0,0 +1,86 @@ +use container_common::anyhow_result::AnyhowResult; +use log::info; +use std::path::Path; +use subprocess::{Popen, PopenConfig}; +use subprocess_common::docker_options::DockerOptions; + +/// This command is used to check softvc for being a real model +#[derive(Clone)] +pub struct SoftVcModelCheckCommand { + /// Where the Tacotron code lives + softvc_root_code_directory: String, + + /// eg. `source python/bin/activate` + maybe_virtual_env_activation_command: Option, + + softvc_model_check_script_name: String, + + /// If this is run under Docker (eg. in development), these are the options. + maybe_docker_options: Option, +} + +impl SoftVcModelCheckCommand { + pub fn new( + softvc_root_code_directory: &str, + maybe_virtual_env_activation_command: Option<&str>, + softvc_model_check_script_name: &str, + maybe_docker_options: Option, + ) -> Self { + Self { + softvc_root_code_directory: softvc_root_code_directory.to_string(), + maybe_virtual_env_activation_command: maybe_virtual_env_activation_command.map(|s| s.to_string()), + softvc_model_check_script_name: softvc_model_check_script_name.to_string(), + maybe_docker_options, + } + } + + pub fn execute>( + &self, + synthesizer_checkpoint_path: P, + output_metadata_filename: P, + ) -> AnyhowResult<()> { + let mut command = String::new(); + + command.push_str("echo 'test'"); + command.push_str(" && "); + command.push_str(&format!("cd {}", self.softvc_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("python "); + command.push_str(&self.softvc_model_check_script_name); + command.push_str(" --synthesizer_checkpoint_path "); + command.push_str(&synthesizer_checkpoint_path.as_ref().display().to_string()); + command.push_str(" --output_metadata_filename "); + command.push_str(&output_metadata_filename.as_ref().display().to_string()); + + 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 p = Popen::create(&command_parts, PopenConfig { + ..Default::default() + })?; + + info!("Pid : {:?}", p.pid()); + + let exit_status = p.wait()?; + + info!("Exit status: {:?}", exit_status); + + Ok(()) + } +} diff --git a/crates/service/download_job/src/job_types/tacotron/mod.rs b/crates/service/job/download_job/src/job_types/tacotron/mod.rs similarity index 100% rename from crates/service/download_job/src/job_types/tacotron/mod.rs rename to crates/service/job/download_job/src/job_types/tacotron/mod.rs diff --git a/crates/service/download_job/src/job_types/tacotron/process_tacotron_model.rs b/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs similarity index 96% rename from crates/service/download_job/src/job_types/tacotron/process_tacotron_model.rs rename to crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs index 95c2c9516e..0306733d08 100644 --- a/crates/service/download_job/src/job_types/tacotron/process_tacotron_model.rs +++ b/crates/service/job/download_job/src/job_types/tacotron/process_tacotron_model.rs @@ -9,9 +9,9 @@ use crate::job_steps::job_results::JobResults; use database_queries::queries::generic_download::job::list_available_generic_download_jobs::AvailableDownloadJob; use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job::insert_tts_model_from_download_job; use database_queries::queries::tts::tts_models::insert_tts_model_from_download_job; +use enums::core::visibility::Visibility; use jobs_common::redis_job_status_logger::RedisJobStatusLogger; use log::{info, warn}; -use reusable_types::entity_visibility::EntityVisibility; use std::fs::File; use std::io::Read; use std::path::PathBuf; @@ -103,7 +103,7 @@ pub async fn process_tacotron_model<'a, 'b>( file_size_bytes: file_metadata.file_size_bytes, creator_user_token: &job.creator_user_token, creator_ip_address: &job.creator_ip_address, - creator_set_visibility: EntityVisibility::Public, // TODO: All models default to public at start + creator_set_visibility: Visibility::Public, // TODO: All models default to public at start private_bucket_hash: &private_bucket_hash, private_bucket_object_name: &model_bucket_path, mysql_pool: &job_state.mysql_pool, diff --git a/crates/service/download_job/src/job_types/tacotron/tacotron_model_check_command.rs b/crates/service/job/download_job/src/job_types/tacotron/tacotron_model_check_command.rs similarity index 100% rename from crates/service/download_job/src/job_types/tacotron/tacotron_model_check_command.rs rename to crates/service/job/download_job/src/job_types/tacotron/tacotron_model_check_command.rs diff --git a/crates/service/download_job/src/job_types/talknet/mod.rs b/crates/service/job/download_job/src/job_types/talknet/mod.rs similarity index 100% rename from crates/service/download_job/src/job_types/talknet/mod.rs rename to crates/service/job/download_job/src/job_types/talknet/mod.rs diff --git a/crates/service/download_job/src/job_types/talknet/talknet_model_check_command.rs b/crates/service/job/download_job/src/job_types/talknet/talknet_model_check_command.rs similarity index 100% rename from crates/service/download_job/src/job_types/talknet/talknet_model_check_command.rs rename to crates/service/job/download_job/src/job_types/talknet/talknet_model_check_command.rs diff --git a/crates/service/download_job/src/main.rs b/crates/service/job/download_job/src/main.rs similarity index 77% rename from crates/service/download_job/src/main.rs rename to crates/service/job/download_job/src/main.rs index fe9117413d..a427f52623 100644 --- a/crates/service/download_job/src/main.rs +++ b/crates/service/job/download_job/src/main.rs @@ -21,6 +21,8 @@ pub mod job_state; pub mod job_steps; pub mod job_types; +use cloud_storage::bucket_client::BucketClient; +use cloud_storage::bucket_path_unifier::BucketPathUnifier; use config::common_env::CommonEnv; use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING; use config::shared_constants::DEFAULT_RUST_LOG; @@ -29,6 +31,8 @@ use container_common::filesystem::check_directory_exists::check_directory_exists use crate::job_state::{JobState, SidecarConfigs}; use crate::job_steps::main_loop::main_loop; use crate::job_types::hifigan::hifigan_model_check_command::HifiGanModelCheckCommand; +use crate::job_types::hifigan_softvc::hifigan_softvc_model_check_command::HifiGanSoftVcModelCheckCommand; +use crate::job_types::softvc::softvc_model_check_command::SoftVcModelCheckCommand; use crate::job_types::tacotron::tacotron_model_check_command::TacotronModelCheckCommand; use database_queries::mediators::badge_granter::BadgeGranter; use database_queries::mediators::firehose_publisher::FirehosePublisher; @@ -40,8 +44,6 @@ use sqlx::mysql::MySqlPoolOptions; use std::ops::Deref; use std::path::PathBuf; use std::time::Duration; -use storage_buckets_common::bucket_client::BucketClient; -use storage_buckets_common::bucket_path_unifier::BucketPathUnifier; use subprocess_common::docker_options::{DockerFilesystemMount, DockerGpu, DockerOptions}; // Buckets @@ -123,6 +125,35 @@ async fn main() -> AnyhowResult<()> { ) }; + let softvc_model_check_command = { + let root_directory = easyenv::get_env_string_required( + "SOFTVC_MODEL_CHECK_ROOT_DIRECTORY")?; + + let python_command = easyenv::get_env_string_or_default( + "SOFTVC_MODEL_CHECK_COMMAND", + "./model_check_softvc_acoustic.py"); + + let maybe_venv_command = easyenv::get_env_string_optional( + "SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND"); + + let maybe_docker_options = easyenv::get_env_string_optional( + "SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE") + .map(|image_name| { + DockerOptions { + image_name, + maybe_bind_mount: Some(DockerFilesystemMount::tmp_to_tmp()), + maybe_gpu: Some(DockerGpu::All), + } + }); + + SoftVcModelCheckCommand::new( + &root_directory, + maybe_venv_command.as_deref(), + &python_command, + maybe_docker_options, + ) + }; + let tacotron_model_check_command = { let root_directory = easyenv::get_env_string_required( "TACOTRON_MODEL_CHECK_ROOT_DIRECTORY")?; @@ -181,6 +212,35 @@ async fn main() -> AnyhowResult<()> { ) }; + let hifigan_softvc_model_check_command= { + let root_directory = easyenv::get_env_string_required( + "HIFIGAN_SOFTVC_MODEL_CHECK_ROOT_DIRECTORY")?; + + let python_command = easyenv::get_env_string_or_default( + "HIFIGAN_SOFTVC_MODEL_CHECK_COMMAND", + "./model_check_hifigan.py"); + + let maybe_venv_command = easyenv::get_env_string_optional( + "HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_VENV_COMMAND"); + + let maybe_docker_options = easyenv::get_env_string_optional( + "HIFIGAN_SOFTVC_MODEL_CHECK_MAYBE_DOCKER_IMAGE") + .map(|image_name| { + DockerOptions { + image_name, + maybe_bind_mount: Some(DockerFilesystemMount::tmp_to_tmp()), + maybe_gpu: Some(DockerGpu::All), + } + }); + + HifiGanSoftVcModelCheckCommand::new( + &root_directory, + maybe_venv_command.as_deref(), + &python_command, + maybe_docker_options, + ) + }; + // =============== End Configure Python "Sidecars" =============== let temp_directory = PathBuf::from(temp_directory); @@ -230,8 +290,10 @@ async fn main() -> AnyhowResult<()> { badge_granter, sidecar_configs: SidecarConfigs { google_drive_downloader, + softvc_model_check_command, tacotron_model_check_command, hifigan_model_check_command, + hifigan_softvc_model_check_command, }, job_batch_wait_millis: common_env.job_batch_wait_millis, job_max_attempts: common_env.job_max_attempts as i32, diff --git a/crates/service/inference_job/Cargo.toml b/crates/service/job/inference_job/Cargo.toml similarity index 69% rename from crates/service/inference_job/Cargo.toml rename to crates/service/job/inference_job/Cargo.toml index 5919936220..3c54b2fb33 100644 --- a/crates/service/inference_job/Cargo.toml +++ b/crates/service/job/inference_job/Cargo.toml @@ -15,16 +15,19 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -jobs_common = { path = "../../lib/jobs_common" } -memory_caching = { path = "../../lib/memory_caching" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +cloud_storage = { path = "../../../lib/cloud_storage" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +enums = { path = "../../../_types/enums" } +errors = { path = "../../../lib/errors" } +jobs_common = { path = "../../../lib/jobs_common" } +memory_caching = { path = "../../../lib/memory_caching" } +tokens = { path = "../../../_types/tokens" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/tts_inference_job/src/caching/cache_miss_strategizer.rs b/crates/service/job/inference_job/src/caching/cache_miss_strategizer.rs similarity index 98% rename from crates/service/tts_inference_job/src/caching/cache_miss_strategizer.rs rename to crates/service/job/inference_job/src/caching/cache_miss_strategizer.rs index 3d99c4fb72..88ee4e9ab9 100644 --- a/crates/service/tts_inference_job/src/caching/cache_miss_strategizer.rs +++ b/crates/service/job/inference_job/src/caching/cache_miss_strategizer.rs @@ -122,7 +122,7 @@ impl CacheMissStrategizer { #[cfg(test)] mod tests { use chrono::{Duration, Utc, DateTime}; - use crate::{CacheMissStrategizer, CacheMissStrategy}; + use crate::caching::cache_miss_strategizer::{CacheMissStrategizer, CacheMissStrategy}; fn get_date(datetime: &str) -> DateTime { let datetime = DateTime::parse_from_rfc3339(datetime).unwrap(); diff --git a/crates/service/inference_job/src/caching/cache_miss_strategizer_multi.rs b/crates/service/job/inference_job/src/caching/cache_miss_strategizer_multi.rs similarity index 100% rename from crates/service/inference_job/src/caching/cache_miss_strategizer_multi.rs rename to crates/service/job/inference_job/src/caching/cache_miss_strategizer_multi.rs diff --git a/crates/service/inference_job/src/caching/mod.rs b/crates/service/job/inference_job/src/caching/mod.rs similarity index 100% rename from crates/service/inference_job/src/caching/mod.rs rename to crates/service/job/inference_job/src/caching/mod.rs diff --git a/crates/service/inference_job/src/caching/virtual_lfu_cache.rs b/crates/service/job/inference_job/src/caching/virtual_lfu_cache.rs similarity index 100% rename from crates/service/inference_job/src/caching/virtual_lfu_cache.rs rename to crates/service/job/inference_job/src/caching/virtual_lfu_cache.rs diff --git a/crates/service/inference_job/src/http_clients/mod.rs b/crates/service/job/inference_job/src/http_clients/mod.rs similarity index 100% rename from crates/service/inference_job/src/http_clients/mod.rs rename to crates/service/job/inference_job/src/http_clients/mod.rs diff --git a/crates/service/inference_job/src/http_clients/tts_inference_sidecar_client.rs b/crates/service/job/inference_job/src/http_clients/tts_inference_sidecar_client.rs similarity index 100% rename from crates/service/inference_job/src/http_clients/tts_inference_sidecar_client.rs rename to crates/service/job/inference_job/src/http_clients/tts_inference_sidecar_client.rs diff --git a/crates/service/inference_job/src/http_clients/tts_sidecar_health_check_client.rs b/crates/service/job/inference_job/src/http_clients/tts_sidecar_health_check_client.rs similarity index 100% rename from crates/service/inference_job/src/http_clients/tts_sidecar_health_check_client.rs rename to crates/service/job/inference_job/src/http_clients/tts_sidecar_health_check_client.rs diff --git a/crates/service/inference_job/src/job_steps/job_args.rs b/crates/service/job/inference_job/src/job_steps/job_dependencies.rs similarity index 84% rename from crates/service/inference_job/src/job_steps/job_args.rs rename to crates/service/job/inference_job/src/job_steps/job_dependencies.rs index be4e37ea6b..1c64cd21c0 100644 --- a/crates/service/inference_job/src/job_steps/job_args.rs +++ b/crates/service/job/inference_job/src/job_steps/job_dependencies.rs @@ -1,3 +1,5 @@ +use cloud_storage::bucket_client::BucketClient; +use cloud_storage::bucket_path_unifier::BucketPathUnifier; use crate::caching::cache_miss_strategizer_multi::SyncMultiCacheMissStrategizer; use crate::caching::virtual_lfu_cache::SyncVirtualLfuCache; use crate::http_clients::tts_inference_sidecar_client::TtsInferenceSidecarClient; @@ -11,17 +13,24 @@ use jobs_common::job_progress_reporter::job_progress_reporter::JobProgressReport use jobs_common::semi_persistent_cache_dir::SemiPersistentCacheDir; use memory_caching::multi_item_ttl_cache::MultiItemTtlCache; use newrelic_telemetry::Client as NewRelicClient; +use r2d2_redis::RedisConnectionManager; +use r2d2_redis::r2d2; use sqlx::MySqlPool; use std::path::PathBuf; -use storage_buckets_common::bucket_client::BucketClient; -use storage_buckets_common::bucket_path_unifier::BucketPathUnifier; -pub struct JobArgs { +pub struct JobDependencies { pub download_temp_directory: PathBuf, pub scoped_temp_dir_creator: ScopedTempDirCreator, pub mysql_pool: MySqlPool, + // TODO(2023-01-11): We don't always connect to a Redis + // Typically this is for job status reporting, but we might also report on when users leave the + // site to proactively kill their inference jobs and save on worker quota. + // On local dev we probably don't care about Redis at all, and on on-prem workers, we cannot + // connect to production Redis easily (requires lots of setup - ghosttunnel or something + IP rules) + pub maybe_redis_pool: Option>, + pub job_progress_reporter: Box, pub private_bucket_client: BucketClient, @@ -64,8 +73,7 @@ pub struct JobArgs { pub job_batch_wait_millis: u64, // Max job attempts before failure. - // NB: This is an i32 so we don't need to convert to db column type. - pub job_max_attempts: i32, + pub job_max_attempts: u16, // Number of jobs to dequeue at once. pub job_batch_size: u32, @@ -111,7 +119,7 @@ pub struct JobHttpClients { pub tts_sidecar_health_check_client: TtsSidecarHealthCheckClient, } -impl JobArgs { +impl JobDependencies { /// Get the best name for the worker. pub fn get_worker_name(&self) -> String { diff --git a/crates/service/inference_job/src/job_steps/job_stats.rs b/crates/service/job/inference_job/src/job_steps/job_stats.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/job_stats.rs rename to crates/service/job/inference_job/src/job_steps/job_stats.rs diff --git a/crates/service/job/inference_job/src/job_steps/main_loop.rs b/crates/service/job/inference_job/src/job_steps/main_loop.rs new file mode 100644 index 0000000000..434e6b202d --- /dev/null +++ b/crates/service/job/inference_job/src/job_steps/main_loop.rs @@ -0,0 +1,99 @@ +use crate::job_steps::job_dependencies::JobDependencies; +use database_queries::queries::generic_inference::job::list_available_generic_inference_jobs::{AvailableInferenceJob, list_available_generic_inference_jobs, ListAvailableGenericInferenceJobArgs}; +use database_queries::queries::generic_inference::job::mark_generic_inference_job_failure::mark_generic_inference_job_failure; +use errors::AnyhowResult; +use jobs_common::noop_logger::NoOpLogger; +use log::{error, info, warn}; +use std::time::Duration; +use crate::job_steps::process_single_job::process_single_job; + +// Job runner timeouts (guards MySQL) +const START_TIMEOUT_MILLIS : u64 = 500; +const INCREASE_TIMEOUT_MILLIS : u64 = 1000; + +pub async fn main_loop(job_dependencies: JobDependencies) { + let mut noop_logger = NoOpLogger::new(job_dependencies.no_op_logger_millis as i64); + + let mut error_timeout_millis = START_TIMEOUT_MILLIS; + let mut sort_by_priority = true; + let mut sort_by_priority_count = 0; + + loop { + // Don't completely starve low-priority jobs + if sort_by_priority_count >= job_dependencies.low_priority_starvation_prevention_every_nth { + sort_by_priority_count = 0; + sort_by_priority = false; + } + + let maybe_available_jobs = list_available_generic_inference_jobs(ListAvailableGenericInferenceJobArgs { + num_records: job_dependencies.job_batch_size, + is_debug_worker: false, // TODO + sort_by_priority, + maybe_scope_by_job_type: None, // TODO + mysql_pool: &job_dependencies.mysql_pool, + }).await; + + sort_by_priority = true; + sort_by_priority_count += 1; + + let jobs = match maybe_available_jobs { + Ok(jobs) => jobs, + Err(e) => { + error!("Error querying jobs: {:?}", e); + std::thread::sleep(Duration::from_millis(error_timeout_millis)); + error_timeout_millis += INCREASE_TIMEOUT_MILLIS; + continue; + } + }; + + if jobs.is_empty() { + noop_logger.log_message_after_awhile("No jobs picked up from database!"); + std::thread::sleep(Duration::from_millis(job_dependencies.job_batch_wait_millis)); + error_timeout_millis = START_TIMEOUT_MILLIS; // reset + continue; + } + + info!("Queried {} jobs from database", jobs.len()); + + let batch_result = process_job_batch(&job_dependencies, jobs).await; + + match batch_result { + Ok(_) => {}, + Err(e) => { + error!("Error processing jobs: {:?}", e); + std::thread::sleep(Duration::from_millis(error_timeout_millis)); + error_timeout_millis += INCREASE_TIMEOUT_MILLIS; + continue; + } + } + + error_timeout_millis = START_TIMEOUT_MILLIS; // reset + + std::thread::sleep(Duration::from_millis(job_dependencies.job_batch_wait_millis)); + } +} + +// TODO: A common interface/trait for each submodule (tts, webvc) to declare how to determine if the job is "ready". +// This probably returns a struct or enum with some measure of how many GB need to be downloaded. + +async fn process_job_batch(job_dependencies: &JobDependencies, jobs: Vec) -> AnyhowResult<()> { + for job in jobs.into_iter() { + let result = process_single_job(job_dependencies, &job).await; + match result { + Ok(_) => {}, + Err(e) => { + warn!("Failure to process job: {:?}", e); + let failure_reason = ""; + let _r = mark_generic_inference_job_failure( + &job_dependencies.mysql_pool, + &job, + failure_reason, + failure_reason, + job_dependencies.job_max_attempts + ).await; + } + } + } + + Ok(()) +} diff --git a/crates/service/inference_job/src/main.rs b/crates/service/job/inference_job/src/job_steps/main_loop_old.rs.txt similarity index 54% rename from crates/service/inference_job/src/main.rs rename to crates/service/job/inference_job/src/job_steps/main_loop_old.rs.txt index a171ffa07d..c13727d237 100644 --- a/crates/service/inference_job/src/main.rs +++ b/crates/service/job/inference_job/src/job_steps/main_loop_old.rs.txt @@ -1,353 +1,29 @@ -// Never allow these -#![forbid(private_in_public)] -#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things - -// Okay to toggle -//#![forbid(warnings)] -//#![forbid(unreachable_patterns)] -#![forbid(unused_imports)] -#![forbid(unused_mut)] -#![forbid(unused_variables)] - -// Always allow -#![allow(dead_code)] -#![allow(non_snake_case)] - -#[macro_use] extern crate serde_derive; - -pub mod caching; -pub mod http_clients; -pub mod job_steps; -pub mod script_execution; -pub mod util; - -use clap::{App, Arg}; -use config::common_env::CommonEnv; -use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING; -use config::shared_constants::DEFAULT_RUST_LOG; use container_common::anyhow_result::AnyhowResult; use container_common::collections::multiple_random_from_vec::multiple_random_from_vec; -use container_common::filesystem::check_directory_exists::check_directory_exists; -use crate::caching::cache_miss_strategizer::CacheMissStrategizer; use crate::caching::cache_miss_strategizer::CacheMissStrategy; -use crate::caching::cache_miss_strategizer_multi::SyncMultiCacheMissStrategizer; use crate::caching::virtual_lfu_cache::SyncVirtualLfuCache; -use crate::http_clients::tts_inference_sidecar_client::TtsInferenceSidecarClient; -use crate::http_clients::tts_sidecar_health_check_client::TtsSidecarHealthCheckClient; -use crate::job_steps::health_check_trap::maybe_block_on_sidecar_health_check; -use crate::job_steps::job_args::JobWorkerDetails; -use crate::job_steps::job_args::{JobArgs, JobCaches, JobHttpClients}; +use crate::job_steps::job_dependencies::JobDependencies; use crate::job_steps::job_stats::JobStats; -use crate::job_steps::process_single_job::process_single_job; +use crate::job_steps::process_single_job_old::process_single_job_old; use crate::job_steps::process_single_job_error::ProcessSingleJobError; -use crate::script_execution::tacotron_inference_command::TacotronInferenceCommand; -use crate::util::scoped_temp_dir_creator::ScopedTempDirCreator; -use database_queries::mediators::firehose_publisher::FirehosePublisher; +use crate::job_steps::sidecar_health_check_trap::maybe_block_on_sidecar_health_check; use database_queries::queries::tts::tts_inference_jobs::list_available_tts_inference_jobs::{AvailableTtsInferenceJob, list_available_tts_inference_jobs, list_available_tts_inference_jobs_with_minimum_priority}; use database_queries::queries::tts::tts_inference_jobs::mark_tts_inference_job_failure::mark_tts_inference_job_failure; use database_queries::queries::tts::tts_inference_jobs::mark_tts_inference_job_permanently_dead::mark_tts_inference_job_permanently_dead; use database_queries::queries::tts::tts_models::get_tts_model_for_inference::{get_tts_model_for_inference, TtsModelForInferenceError, TtsModelForInferenceRecord}; -use jobs_common::job_progress_reporter::job_progress_reporter::JobProgressReporterBuilder; -use jobs_common::job_progress_reporter::noop_job_progress_reporter::NoOpJobProgressReporterBuilder; -use jobs_common::job_progress_reporter::redis_job_progress_reporter::RedisJobProgressReporterBuilder; use jobs_common::noop_logger::NoOpLogger; use jobs_common::semi_persistent_cache_dir::SemiPersistentCacheDir; -use log::{warn, info, error}; +use log::{error, info, warn}; use memory_caching::multi_item_ttl_cache::MultiItemTtlCache; -use newrelic_telemetry::ClientBuilder; use newrelic_telemetry::Span; -use r2d2_redis::RedisConnectionManager; -use r2d2_redis::r2d2; use sqlx::MySqlPool; -use sqlx::mysql::MySqlPoolOptions; -use std::path::PathBuf; use std::time::Duration; -use storage_buckets_common::bucket_client::BucketClient; -use storage_buckets_common::bucket_path_unifier::BucketPathUnifier; - -// Buckets (shared config) -const ENV_ACCESS_KEY : &'static str = "ACCESS_KEY"; -const ENV_SECRET_KEY : &'static str = "SECRET_KEY"; -const ENV_REGION_NAME : &'static str = "REGION_NAME"; - -// Bucket names -const ENV_PRIVATE_BUCKET_NAME : &'static str = "PRIVATE_BUCKET_NAME"; -const ENV_PUBLIC_BUCKET_NAME : &'static str = "PUBLIC_BUCKET_NAME"; - -// Where models and other assets get downloaded to. -const ENV_SEMIPERSISTENT_CACHE_DIR : &'static str = "SEMIPERSISTENT_CACHE_DIR"; - -// Python code -const ENV_CODE_DIRECTORY : &'static str = "TTS_CODE_DIRECTORY"; -const ENV_INFERENCE_SCRIPT_NAME : &'static str = "TTS_INFERENCE_SCRIPT_NAME"; - -// HTTP sidecar -const ENV_TTS_INFERENCE_SIDECAR_HOSTNAME: &'static str = "TTS_INFERENCE_SIDECAR_HOSTNAME"; - -const DEFAULT_TEMP_DIR: &'static str = "/tmp"; - -#[tokio::main] -async fn main() -> AnyhowResult<()> { - easyenv::init_all_with_default_logging(Some(DEFAULT_RUST_LOG)); - - // NB: Do not check this secrets-containing dotenv file into VCS. - // This file should only contain *development* secrets, never production. - let _ = dotenv::from_filename(".env-secrets").ok(); - - let matches = App::new("tts-inference-job") - .arg(Arg::with_name("sidecar_hostname") - .long("sidecar_hostname") - .value_name("HOSTNAME") - .help("Hostname for the TTS inference sidecar") - .takes_value(true) - .required(false)) - .get_matches(); - - info!("Obtaining worker hostname..."); - - let server_hostname = hostname::get() - .ok() - .and_then(|h| h.into_string().ok()) - .unwrap_or("tts-inference-job".to_string()); - - // NB: These are non-standard env vars we're injecting ourselves. - let k8s_node_name = easyenv::get_env_string_optional("K8S_NODE_NAME"); - let k8s_pod_name = easyenv::get_env_string_optional("K8S_POD_NAME"); - - // NB: It'll be worthwhile to see how much compute is happening at our local on-premises cluster - // Only our local workers will set this to true. - let is_on_prem = easyenv::get_env_bool_or_default("IS_ON_PREM", false); - - info!("Hostname: {}", &server_hostname); - info!("Is on-premises: {}", is_on_prem); - - // Bucket stuff (shared) - let access_key = easyenv::get_env_string_required(ENV_ACCESS_KEY)?; - let secret_key = easyenv::get_env_string_required(ENV_SECRET_KEY)?; - let region_name = easyenv::get_env_string_required(ENV_REGION_NAME)?; - - // Private and Public Buckets - let private_bucket_name = easyenv::get_env_string_required(ENV_PRIVATE_BUCKET_NAME)?; - let public_bucket_name = easyenv::get_env_string_required(ENV_PUBLIC_BUCKET_NAME)?; - - let bucket_timeout = easyenv::get_env_duration_seconds_or_default("BUCKET_TIMEOUT_SECONDS", - Duration::from_secs(60 * 5)); - - let private_bucket_client = BucketClient::create( - &access_key, - &secret_key, - ®ion_name, - &private_bucket_name, - None, - Some(bucket_timeout), - )?; - - let public_bucket_client = BucketClient::create( - &access_key, - &secret_key, - ®ion_name, - &public_bucket_name, - None, - Some(bucket_timeout), - )?; - - let py_code_directory = easyenv::get_env_string_required(ENV_CODE_DIRECTORY)?; - let py_script_name = easyenv::get_env_string_required(ENV_INFERENCE_SCRIPT_NAME)?; - - let tts_inference_command = TacotronInferenceCommand::new( - &py_code_directory, - &py_script_name, - ); - - let mut sidecar_hostname = - easyenv::get_env_string_required(ENV_TTS_INFERENCE_SIDECAR_HOSTNAME)?; - - if let Some(hostname) = matches.value_of("sidecar_hostname") { - sidecar_hostname = hostname.to_string(); - } - - info!("Sidecar hostname: {:?}", sidecar_hostname); - - let tts_inference_sidecar_client = - TtsInferenceSidecarClient::new(&sidecar_hostname); - - let tts_sidecar_health_check_client= - TtsSidecarHealthCheckClient::new(&sidecar_hostname)?; - - let temp_directory = easyenv::get_env_string_or_default( - "DOWNLOAD_TEMP_DIR", - DEFAULT_TEMP_DIR); - - let temp_directory = PathBuf::from(temp_directory); - - check_directory_exists(&temp_directory)?; - - let db_connection_string = - easyenv::get_env_string_or_default( - "MYSQL_URL", - DEFAULT_MYSQL_CONNECTION_STRING); - - info!("Connecting to database..."); - - let mysql_pool = MySqlPoolOptions::new() - .max_connections(2) - .connect(&db_connection_string) - .await?; - - let common_env = CommonEnv::read_from_env()?; - - - let persistent_cache_path = easyenv::get_env_string_or_default( - ENV_SEMIPERSISTENT_CACHE_DIR, - "/tmp"); - - let semi_persistent_cache = - SemiPersistentCacheDir::configured_root(&persistent_cache_path); - - info!("Creating pod semi-persistent cache dirs..."); - semi_persistent_cache.create_tts_synthesizer_model_path()?; - semi_persistent_cache.create_tts_pretrained_vocoder_model_path()?; - semi_persistent_cache.create_custom_vocoder_model_path()?; - - let waveglow_vocoder_model_filename = easyenv::get_env_string_or_default( - "TTS_WAVEGLOW_VOCODER_MODEL_FILENAME", "waveglow.pth"); - - let hifigan_vocoder_model_filename = easyenv::get_env_string_or_default( - "TTS_HIFIGAN_VOCODER_MODEL_FILENAME", "hifigan.pth"); - - let hifigan_superres_vocoder_model_filename = easyenv::get_env_string_or_default( - "TTS_HIFIGAN_SUPERRES_VOCODER_MODEL_FILENAME", "hifigan_superres.pth"); - - let sidecar_max_synthesizer_models = easyenv::get_env_num( - "SIDECAR_MAX_SYNTHESIZER_MODELS", 3)?; - - // Set to "0" to always treat low priority the same as high priority - let low_priority_starvation_prevention_every_nth= easyenv::get_env_num( - "LOW_PRIORITY_STARVATION_PREVENTION_EVERY_NTH", 3)?; - - let firehose_publisher = FirehosePublisher { - mysql_pool: mysql_pool.clone(), // NB: MySqlPool is clone/send/sync safe - }; - - let virtual_lfu_cache = SyncVirtualLfuCache::new(sidecar_max_synthesizer_models)?; - - let cache_miss_strategizers = { - let in_memory_strategizer = CacheMissStrategizer::new( - chrono::Duration::milliseconds( - easyenv::get_env_num("MEMORY_MAX_COLD_DURATION_MILLIS", 5_000)?, - ), - chrono::Duration::milliseconds( - easyenv::get_env_num("MEMORY_CACHE_FORGET_DURATION_MILLIS", 60_000)?, - ), - ); - - let on_disk_strategizer = CacheMissStrategizer::new( - chrono::Duration::milliseconds( - easyenv::get_env_num("DISK_MAX_COLD_DURATION_MILLIS", 20_000)?, - ), - chrono::Duration::milliseconds( - easyenv::get_env_num("DISK_CACHE_FORGET_DURATION_MILLIS", 120_000)?, - ), - ); - - SyncMultiCacheMissStrategizer::new( - in_memory_strategizer, - on_disk_strategizer, - ) - }; - - let model_cache_duration = std::time::Duration::from_millis( - easyenv::get_env_num("TTS_MODEL_RECORD_CACHE_MILLIS", 300_000)?, // Five minutes - ); - - let license_key = easyenv::get_env_string_required("NEWRELIC_API_KEY")?; - - let newrelic_disabled = easyenv::get_env_bool_or_default("IS_NEWRELIC_DISABLED", false); - - let newrelic_client = ClientBuilder::new(&license_key).build().unwrap(); - - let maybe_minimum_priority = easyenv::get_env_string_optional("MAYBE_MINIMUM_PRIORITY") - .map(|priority_string| { - priority_string.parse::() - }) - .transpose()?; - - info!("Using 'MAYBE_MINIMUM_PRIORITY' of {:?}", maybe_minimum_priority); - - let is_debug_worker = easyenv::get_env_bool_or_default("IS_DEBUG_WORKER", false); - - info!("Is debug worker? {}", is_debug_worker); - - // Optionally report job progress to the user via Redis (for now) - // We want to turn this off in the on-premises workers since we're not tunneling to the production Redis. - let job_progress_reporter : Box; - - job_progress_reporter = match easyenv::get_env_string_optional("REDIS_FOR_JOB_PROGRESS") { - None => { - warn!("Redis for job progress status reports is DISABLED! Users will not see in-flight details of inference progress."); - Box::new(NoOpJobProgressReporterBuilder {}) - }, - Some(redis_connection_string) => { - info!("Connecting to Redis to use for reporting job progress... {}", redis_connection_string); - let redis_manager = RedisConnectionManager::new(redis_connection_string)?; - let redis_pool = r2d2::Pool::builder().build(redis_manager)?; - - Box::new(RedisJobProgressReporterBuilder::from_redis_pool(redis_pool)) - } - }; - - let inferencer = JobArgs { - scoped_temp_dir_creator: ScopedTempDirCreator::for_directory(&temp_directory), - download_temp_directory: temp_directory, - mysql_pool, - job_progress_reporter, - public_bucket_client, - private_bucket_client, - tts_inference_command, - http_clients: JobHttpClients { - tts_inference_sidecar_client, - tts_sidecar_health_check_client, - }, - job_stats: JobStats::new(), - newrelic_client, - newrelic_disabled, - worker_details: JobWorkerDetails { - is_on_prem, - worker_hostname: server_hostname.clone(), - k8s_node_name, - k8s_pod_name, - is_debug_worker, - }, - caches: JobCaches { - tts_model_record_cache: MultiItemTtlCache::create_with_duration(model_cache_duration), - }, - virtual_model_lfu: virtual_lfu_cache, - cache_miss_strategizers, - bucket_path_unifier: BucketPathUnifier::default_paths(), - semi_persistent_cache, - firehose_publisher, - waveglow_vocoder_model_filename, - hifigan_vocoder_model_filename, - hifigan_superres_vocoder_model_filename, - job_batch_wait_millis: common_env.job_batch_wait_millis, - job_max_attempts: common_env.job_max_attempts as i32, - job_batch_size: common_env.job_batch_size, - no_op_logger_millis: common_env.no_op_logger_millis, - sidecar_max_synthesizer_models, - low_priority_starvation_prevention_every_nth, - maybe_minimum_priority, - }; - - main_loop(inferencer).await; - - Ok(()) -} // Job runner timeouts (guards MySQL) const START_TIMEOUT_MILLIS : u64 = 500; const INCREASE_TIMEOUT_MILLIS : u64 = 1000; -async fn main_loop(job_args: JobArgs) { +pub async fn main_loop_old(job_args: JobDependencies) { let mut error_timeout_millis = START_TIMEOUT_MILLIS; let mut noop_logger = NoOpLogger::new(job_args.no_op_logger_millis as i64); @@ -401,7 +77,7 @@ async fn main_loop(job_args: JobArgs) { }; if jobs.is_empty() { - noop_logger.log_message_after_awhile("No TTS jobs picked up from database!"); + noop_logger.log_message_after_awhile("No inference jobs picked up from database!"); std::thread::sleep(Duration::from_millis(job_args.job_batch_wait_millis)); continue; @@ -446,7 +122,7 @@ async fn main_loop(job_args: JobArgs) { /// Process a batch of jobs, returning the count of cold-cache skipped jobs. async fn process_jobs( - inferencer: &JobArgs, + inferencer: &JobDependencies, jobs: Vec, needs_health_check_at_start: bool, ) -> AnyhowResult> { @@ -562,7 +238,7 @@ async fn process_jobs( maybe_sidecar_health_issue = false; } - let result = process_single_job(inferencer, &job, &model_state.model_record).await; + let result = process_single_job_old(inferencer, &job, &model_state.model_record).await; match result { Ok((span1, span2)) => { batch_spans.push(span1); @@ -744,4 +420,3 @@ impl ModelState { }) } } - diff --git a/crates/service/job/inference_job/src/job_steps/mod.rs b/crates/service/job/inference_job/src/job_steps/mod.rs new file mode 100644 index 0000000000..9cc6e3c9fb --- /dev/null +++ b/crates/service/job/inference_job/src/job_steps/mod.rs @@ -0,0 +1,7 @@ +pub mod job_dependencies; +pub mod job_stats; +pub mod main_loop; +pub mod process_single_job; +pub mod process_single_job_error; +pub mod select_job_batch; +pub mod sidecar_health_check_trap; diff --git a/crates/service/job/inference_job/src/job_steps/process_single_job.rs b/crates/service/job/inference_job/src/job_steps/process_single_job.rs new file mode 100644 index 0000000000..210eaa022d --- /dev/null +++ b/crates/service/job/inference_job/src/job_steps/process_single_job.rs @@ -0,0 +1,121 @@ +use crate::job_steps::job_dependencies::JobDependencies; +use database_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob; +use database_queries::queries::generic_inference::job::mark_generic_inference_job_pending_and_grab_lock::mark_generic_inference_job_pending_and_grab_lock; +use database_queries::queries::generic_inference::job::mark_generic_inference_job_successfully_done::mark_generic_inference_job_successfully_done; +use enums::workers::generic_inference_type::GenericInferenceType; +use errors::AnyhowResult; +use log::{info, warn}; + +pub async fn process_single_job(job_dependencies: &JobDependencies, job: &AvailableInferenceJob) -> AnyhowResult<()> { + // TODO(bt, 2023-01-11): Restore an optional status logger + //let mut redis = job_dependencies.redis_pool.get()?; + //let mut redis_logger = RedisJobStatusLogger::new_generic_download(&mut redis, job.download_job_token.as_str()); + + // ==================== ATTEMPT TO GRAB JOB LOCK ==================== // + + let lock_acquired = mark_generic_inference_job_pending_and_grab_lock(&job_dependencies.mysql_pool, job.id).await?; + + if !lock_acquired { + warn!("Could not acquire job lock for: {}", &job.id.0); + return Ok(()) + } + + info!("Beginning work on {:?} = {}", job.inference_type, job.inference_job_token); + + // ==================== SETUP TEMP DIRS ==================== // + + let temp_dir = format!("temp_{}", job.id.0); + let temp_dir = job_dependencies.scoped_temp_dir_creator.new_tempdir(&temp_dir)?; + + let _p = temp_dir.path(); // TODO: Just so the build doesn't complain about unused. Remove. + + // ==================== HANDLE DIFFERENT INFERENCE TYPES ==================== // + + let mut entity_token : Option = None; + let mut entity_type : Option = None; + + match job.inference_type { + GenericInferenceType::TextToSpeech => { + // TODO + entity_type = Some("todo".to_string()); // TODO + entity_token = Some("todo".to_string()); // TODO + } + GenericInferenceType::VoiceConversion => { + // TODO + } +// GenericDownloadType::HifiGan => { +// let results = process_hifigan_vocoder( +// job_state, +// job, +// &temp_dir, +// &download_filename, +// &mut redis_logger, +// ).await?; +// entity_token = results.entity_token.clone(); +// entity_type = results.entity_type.clone(); +// } +// GenericDownloadType::HifiGanRocketVc => { +// let results = process_hifigan_softvc_vocoder( +// job_state, +// job, +// &temp_dir, +// &download_filename, +// &mut redis_logger, +// ).await?; +// entity_token = results.entity_token.clone(); +// entity_type = results.entity_type.clone(); +// } +// GenericDownloadType::RocketVc => { +// let results = process_softvc_model( +// job_state, +// job, +// &temp_dir, +// &download_filename, +// &mut redis_logger, +// ).await?; +// entity_token = results.entity_token.clone(); +// entity_type = results.entity_type.clone(); +// } +// GenericDownloadType::Tacotron2 => { +// let results = process_tacotron_model( +// job_state, +// job, +// &temp_dir, +// &download_filename, +// &mut redis_logger, +// ).await?; +// entity_token = results.entity_token.clone(); +// entity_type = results.entity_type.clone(); +// } + } + + // ===================================================== + + info!("Marking job complete..."); + mark_generic_inference_job_successfully_done( + &job_dependencies.mysql_pool, + job, + entity_type.as_deref(), + entity_token.as_deref(), + ).await?; + + info!("Saved model record: {} - {}", job.id.0, &job.inference_job_token); + + // TODO(bt, 2023-01-11): Need to publish that the job finished. + // Publish the *correct type* of event. + //job_dependencies.firehose_publisher.publish_generic_download_finished( + // &job.maybe_creator_user_token, + // entity_token.as_deref()) + // .await + // .map_err(|e| { + // warn!("error publishing event: {:?}", e); + // anyhow!("error publishing event") + // })?; + + // TODO(bt, 2023-01-11): Restore optional Redis logging + //redis_logger.log_status("done")?; + + info!("Job done: {}", job.id.0); + + Ok(()) +} diff --git a/crates/service/inference_job/src/job_steps/process_single_job_error.rs b/crates/service/job/inference_job/src/job_steps/process_single_job_error.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/process_single_job_error.rs rename to crates/service/job/inference_job/src/job_steps/process_single_job_error.rs diff --git a/crates/service/tts_inference_job/src/job_steps/process_single_job.rs b/crates/service/job/inference_job/src/job_steps/process_single_job_old.rs.txt similarity index 91% rename from crates/service/tts_inference_job/src/job_steps/process_single_job.rs rename to crates/service/job/inference_job/src/job_steps/process_single_job_old.rs.txt index 6df21d6118..7b9bde648b 100644 --- a/crates/service/tts_inference_job/src/job_steps/process_single_job.rs +++ b/crates/service/job/inference_job/src/job_steps/process_single_job_old.rs.txt @@ -4,21 +4,22 @@ #![forbid(unused_variables)] use anyhow::anyhow; -use container_common::anyhow_result::AnyhowResult; 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 container_common::hashing::hash_string_sha2::hash_string_sha2; use container_common::token::random_uuid::generate_random_uuid; -use crate::job_steps::download_file_from_bucket::maybe_download_file_from_bucket; -use crate::job_steps::job_args::JobArgs; +use crate::job_steps::job_dependencies::JobDependencies; use crate::job_steps::process_single_job_error::ProcessSingleJobError; +use crate::job_types::tts::seconds_to_decoder_steps::seconds_to_decoder_steps; +use crate::util::download_file_from_bucket::maybe_download_file_from_bucket; use database_queries::column_types::vocoder_type::VocoderType; use database_queries::queries::tts::tts_inference_jobs::list_available_tts_inference_jobs::AvailableTtsInferenceJob; use database_queries::queries::tts::tts_inference_jobs::mark_tts_inference_job_done::mark_tts_inference_job_done; use database_queries::queries::tts::tts_inference_jobs::mark_tts_inference_job_pending_and_grab_lock::mark_tts_inference_job_pending_and_grab_lock; use database_queries::queries::tts::tts_models::get_tts_model_for_inference::TtsModelForInferenceRecord; use database_queries::queries::tts::tts_results::insert_tts_result::insert_tts_result; +use errors::AnyhowResult; use log::{warn, info, error}; use newrelic_telemetry::Span; use std::fs::File; @@ -29,48 +30,33 @@ use tempdir::TempDir; use tts_common::clean_symbols::clean_symbols; use tts_common::text_pipelines::guess_pipeline::guess_text_pipeline_heuristic; use tts_common::text_pipelines::text_pipeline_type::TextPipelineType; -use crate::job_steps::seconds_to_decoder_steps::seconds_to_decoder_steps; -#[derive(Deserialize, Default)] -struct FileMetadata { - pub duration_millis: Option, - pub mimetype: Option, - pub file_size_bytes: u64, -} - -fn read_metadata_file(filename: &PathBuf) -> AnyhowResult { - let mut file = File::open(filename)?; - let mut buffer = String::new(); - file.read_to_string(&mut buffer)?; - Ok(serde_json::from_str(&buffer)?) -} - -pub async fn process_single_job( - job_args: &JobArgs, +pub async fn process_single_job_old( + job_args: &JobDependencies, job: &AvailableTtsInferenceJob, model_record: &TtsModelForInferenceRecord, ) -> Result<(Span, Span), ProcessSingleJobError> { let start = Instant::now(); - let span_id = generate_random_uuid(); - let trace_id = generate_random_uuid(); - let maybe_user_token = job.maybe_creator_user_token.as_deref().unwrap_or(""); + //let span_id = generate_random_uuid(); + //let trace_id = generate_random_uuid(); + //let maybe_user_token = job.maybe_creator_user_token.as_deref().unwrap_or(""); - let mut job_iteration_span = Span::new(&span_id, &trace_id, get_timestamp_millis()) - .name("single job execution") - .attribute("user_token", maybe_user_token) - .service_name("tts-inference-job"); + //let mut job_iteration_span = Span::new(&span_id, &trace_id, get_timestamp_millis()) + // .name("single job execution") + // .attribute("user_token", maybe_user_token) + // .service_name("tts-inference-job"); - let span_id = generate_random_uuid(); - let trace_id = generate_random_uuid(); + //let span_id = generate_random_uuid(); + //let trace_id = generate_random_uuid(); - let created_at_timestamp = (job.created_at.timestamp() as u64) * 1000; + //let created_at_timestamp = (job.created_at.timestamp() as u64) * 1000; - let mut since_creation_span = Span::new(&span_id, &trace_id, created_at_timestamp) - .name("job since creation") - .attribute("user_token", maybe_user_token) - .service_name("tts-inference-job"); + //let mut since_creation_span = Span::new(&span_id, &trace_id, created_at_timestamp) + // .name("job since creation") + // .attribute("user_token", maybe_user_token) + // .service_name("tts-inference-job"); let mut job_progress_reporter = job_args .job_progress_reporter diff --git a/crates/service/job/inference_job/src/job_steps/select_job_batch.rs b/crates/service/job/inference_job/src/job_steps/select_job_batch.rs new file mode 100644 index 0000000000..5bb0fcdb1d --- /dev/null +++ b/crates/service/job/inference_job/src/job_steps/select_job_batch.rs @@ -0,0 +1,23 @@ +use database_queries::queries::generic_inference::job::list_available_generic_inference_jobs::{list_available_generic_inference_jobs, ListAvailableGenericInferenceJobArgs}; +use enums::workers::generic_inference_type::GenericInferenceType; +use sqlx::MySqlPool; +use std::collections::BTreeSet; + +pub struct SelectJobBatchArgs <'a> { + pub num_records: u32, + pub is_debug_worker: bool, + pub sort_by_priority: bool, + pub maybe_scope_by_job_type: Option>, + pub mysql_pool: &'a MySqlPool, +} + +pub async fn select_job_batch(args: SelectJobBatchArgs<'_>) { + let _results = list_available_generic_inference_jobs(ListAvailableGenericInferenceJobArgs { + num_records: args.num_records, + is_debug_worker: args.is_debug_worker, + sort_by_priority: args.sort_by_priority, + maybe_scope_by_job_type: args.maybe_scope_by_job_type, + mysql_pool: args.mysql_pool, + }).await; + +} diff --git a/crates/service/inference_job/src/job_steps/health_check_trap.rs b/crates/service/job/inference_job/src/job_steps/sidecar_health_check_trap.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/health_check_trap.rs rename to crates/service/job/inference_job/src/job_steps/sidecar_health_check_trap.rs diff --git a/crates/service/job/inference_job/src/job_types/mod.rs b/crates/service/job/inference_job/src/job_types/mod.rs new file mode 100644 index 0000000000..2a49a1b0f9 --- /dev/null +++ b/crates/service/job/inference_job/src/job_types/mod.rs @@ -0,0 +1,2 @@ +pub mod tts; +pub mod vc; \ No newline at end of file diff --git a/crates/service/job/inference_job/src/job_types/tts/handle_tts_inference.rs b/crates/service/job/inference_job/src/job_types/tts/handle_tts_inference.rs new file mode 100644 index 0000000000..b4371278e3 --- /dev/null +++ b/crates/service/job/inference_job/src/job_types/tts/handle_tts_inference.rs @@ -0,0 +1,342 @@ +use crate::job_steps::job_dependencies::JobDependencies; +use database_queries::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob; +use errors::AnyhowResult; + +pub async fn handle_tts_inference( + _job_dependencies: &JobDependencies, + _job: &AvailableInferenceJob, +) -> AnyhowResult<()> { + + /* + TODO(bt, 2023-01-11): This was just copied verbatim from tts-inference-job. + It's not ready to run from inference-job yet as it needs adjustments to the new schema. + + let mut job_progress_reporter = job_dependencies + .job_progress_reporter + .new_tts_inference(job.inference_job_token.as_str()) + .map_err(|e| ProcessSingleJobError::Other(anyhow!(e)))?; + + // ==================== CONFIRM OR DOWNLOAD WAVEGLOW VOCODER MODEL ==================== // + + let waveglow_vocoder_model_fs_path = { + let waveglow_vocoder_model_filename = job_dependencies.waveglow_vocoder_model_filename.clone(); + let waveglow_vocoder_model_fs_path = job_dependencies.semi_persistent_cache.tts_pretrained_vocoder_model_path(&waveglow_vocoder_model_filename); + let waveglow_vocoder_model_object_path = job_dependencies.bucket_path_unifier.tts_pretrained_vocoders_path(&waveglow_vocoder_model_filename); + + maybe_download_file_from_bucket( + "waveglow vocoder model", + &waveglow_vocoder_model_fs_path, + &waveglow_vocoder_model_object_path, + &job_dependencies.private_bucket_client, + &mut job_progress_reporter, + "downloading vocoder (1 of 3)", + job.id.0, + &job_dependencies.scoped_temp_dir_creator, + ).await?; + + waveglow_vocoder_model_fs_path + }; + + // ==================== CONFIRM OR DOWNLOAD HIFIGAN (NORMAL) VOCODER MODEL ==================== // + + let pretrained_hifigan_vocoder_model_fs_path = { + let hifigan_vocoder_model_filename = job_dependencies.hifigan_vocoder_model_filename.clone(); + let hifigan_vocoder_model_fs_path = job_dependencies.semi_persistent_cache.tts_pretrained_vocoder_model_path(&hifigan_vocoder_model_filename); + let hifigan_vocoder_model_object_path = job_dependencies.bucket_path_unifier.tts_pretrained_vocoders_path(&hifigan_vocoder_model_filename); + + maybe_download_file_from_bucket( + "hifigan vocoder model", + &hifigan_vocoder_model_fs_path, + &hifigan_vocoder_model_object_path, + &job_dependencies.private_bucket_client, + &mut job_progress_reporter, + "downloading vocoder (2 of 3)", + job.id.0, + &job_dependencies.scoped_temp_dir_creator, + ).await?; + + hifigan_vocoder_model_fs_path + }; + + // ==================== CONFIRM OR DOWNLOAD HIFIGAN (SUPERRES) VOCODER MODEL ==================== // + + let hifigan_superres_vocoder_model_fs_path = { + let hifigan_superres_vocoder_model_filename = job_dependencies.hifigan_superres_vocoder_model_filename.clone(); + let hifigan_superres_vocoder_model_fs_path = job_dependencies.semi_persistent_cache.tts_pretrained_vocoder_model_path(&hifigan_superres_vocoder_model_filename); + let hifigan_superres_vocoder_model_object_path = job_dependencies.bucket_path_unifier.tts_pretrained_vocoders_path(&hifigan_superres_vocoder_model_filename); + + maybe_download_file_from_bucket( + "hifigan superres vocoder model", + &hifigan_superres_vocoder_model_fs_path, + &hifigan_superres_vocoder_model_object_path, + &job_dependencies.private_bucket_client, + &mut job_progress_reporter, + "downloading vocoder (3 of 3)", + job.id.0, + &job_dependencies.scoped_temp_dir_creator, + ).await?; + + hifigan_superres_vocoder_model_fs_path + }; + +// // ==================== CONFIRM OR DOWNLOAD OPTIONAL CUSTOM VOCODER MODEL ==================== // + + let custom_vocoder_fs_path = match &model_record.maybe_custom_vocoder { + None => None, + Some(vocoder) => { + let custom_vocoder_fs_path = job_dependencies.semi_persistent_cache.custom_vocoder_model_path(&vocoder.vocoder_token); + let custom_vocoder_object_path = job_dependencies.bucket_path_unifier.vocoder_path(&vocoder.vocoder_private_bucket_hash); + + maybe_download_file_from_bucket( + "custom vocoder", + &custom_vocoder_fs_path, + &custom_vocoder_object_path, + &job_dependencies.private_bucket_client, + &mut job_progress_reporter, + "downloading user vocoder", + job.id.0, + &job_dependencies.scoped_temp_dir_creator, + ).await?; + + Some(custom_vocoder_fs_path) + } + }; + + // ==================== CONFIRM OR DOWNLOAD TTS SYNTHESIZER MODEL ==================== // + + let tts_synthesizer_fs_path = { + let tts_synthesizer_fs_path = job_dependencies.semi_persistent_cache.tts_synthesizer_model_path(&model_record.model_token); + let tts_synthesizer_object_path = job_dependencies.bucket_path_unifier.tts_synthesizer_path(&model_record.private_bucket_hash); + + maybe_download_file_from_bucket( + "synthesizer", + &tts_synthesizer_fs_path, + &tts_synthesizer_object_path, + &job_dependencies.private_bucket_client, + &mut job_progress_reporter, + "downloading synthesizer", + job.id.0, + &job_dependencies.scoped_temp_dir_creator, + ).await?; + + tts_synthesizer_fs_path + }; + + // ==================== Preprocess text ==================== // + + let cleaned_inference_text = clean_symbols(&job.raw_inference_text); + + // ==================== WRITE TEXT TO FILE ==================== // + + info!("Creating tempdir for inference results."); + + let temp_dir = format!("temp_tts_inference_{}", job.id.0); + + // NB: TempDir exists until it goes out of scope, at which point it should delete from filesystem. + let temp_dir = TempDir::new(&temp_dir) + .map_err(|e| ProcessSingleJobError::from_io_error(e))?; + + let text_input_fs_path = temp_dir.path().join("inference_input.txt"); + + std::fs::write(&text_input_fs_path, &cleaned_inference_text) + .map_err(|e| ProcessSingleJobError::from_io_error(e))?; + + // ==================== RUN INFERENCE ==================== // + + job_progress_reporter.log_status("running inference") + .map_err(|e| ProcessSingleJobError::Other(e))?; + + // TODO: Fix this. + let maybe_unload_model_path = job_dependencies + .virtual_model_lfu + .insert_returning_replaced(tts_synthesizer_fs_path.to_str().unwrap_or("")) + .map_err(|e| ProcessSingleJobError::Other(e))?; + + if let Some(model_path) = maybe_unload_model_path.as_deref() { + warn!("Remove model from LFU cache: {:?}", model_path); + } + + let output_audio_fs_path = 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 TTS inference..."); + + info!("Expected output audio filename: {:?}", &output_audio_fs_path); + info!("Expected output spectrogram filename: {:?}", &output_spectrogram_fs_path); + info!("Expected output metadata filename: {:?}", &output_metadata_fs_path); + + if let Some(model_path) = maybe_unload_model_path.as_deref() { + warn!("Unload model from sidecar: {:?}", &model_path); + } + + let mut pretrained_vocoder = VocoderType::HifiGanSuperResolution; + if let Some(default_vocoder) = model_record.maybe_default_pretrained_vocoder.as_deref() { + pretrained_vocoder = VocoderType::from_str(default_vocoder) + .map_err(|e| ProcessSingleJobError::Other(e))?; + } + + info!("With pretrained vocoder: {:?}", pretrained_vocoder); + + let text_pipeline_type_or_guess = model_record.text_pipeline_type + .as_deref() + .and_then(|pipeline_type| + TextPipelineType::from_str(pipeline_type).ok())// NB: If there's an error deserializing, turn it to None. + .unwrap_or_else(|| + guess_text_pipeline_heuristic(Some(model_record.created_at))); + + info!("With text pipeline type `{:?} ` (or guess: {:?})", + &model_record.text_pipeline_type, + &text_pipeline_type_or_guess); + + let hifigan_vocoder_model_fs_path_to_use = match custom_vocoder_fs_path { + None => { + info!("using pretrained HiFi-GAN vocoder"); + pretrained_hifigan_vocoder_model_fs_path + }, + Some(custom_vocoder_fs_path) => { + info!("using custom user-trained HiFi-GAN vocoder: {:?}", custom_vocoder_fs_path); + custom_vocoder_fs_path + }, + }; + + // NB: Tacotron operates on decoder steps. 1000 steps is the default and correlates to + // roughly 12 seconds max. Here we map seconds to decoder steps. + let max_decoder_steps = seconds_to_decoder_steps(job.max_duration_seconds); + + job_dependencies.http_clients.tts_inference_sidecar_client.request_inference( + &cleaned_inference_text, + max_decoder_steps, + &tts_synthesizer_fs_path, + pretrained_vocoder, + &text_pipeline_type_or_guess.to_str(), + &hifigan_vocoder_model_fs_path_to_use, + &hifigan_superres_vocoder_model_fs_path, + &waveglow_vocoder_model_fs_path, + &output_audio_fs_path, + &output_spectrogram_fs_path, + &output_metadata_fs_path, + maybe_unload_model_path) + .await + .map_err(|e| ProcessSingleJobError::Other(e))?; + + // ==================== CHECK ALL FILES EXIST AND GET METADATA ==================== // + + info!("Checking that output files exist..."); + + check_file_exists(&output_audio_fs_path).map_err(|e| ProcessSingleJobError::Other(e))?; + check_file_exists(&output_spectrogram_fs_path).map_err(|e| ProcessSingleJobError::Other(e))?; + check_file_exists(&output_metadata_fs_path).map_err(|e| ProcessSingleJobError::Other(e))?; + + let file_metadata = read_metadata_file(&output_metadata_fs_path) + .map_err(|e| ProcessSingleJobError::Other(e))?; + + 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 audio_result_object_path = job_dependencies.bucket_path_unifier.tts_inference_wav_audio_output_path( + &job.uuid_idempotency_token); // TODO: Don't use this! + + info!("Audio destination bucket path: {:?}", &audio_result_object_path); + + info!("Uploading audio..."); + + job_dependencies.public_bucket_client.upload_filename_with_content_type( + &audio_result_object_path, + &output_audio_fs_path, + "audio/wav") + .await + .map_err(|e| ProcessSingleJobError::Other(e))?; + + safe_delete_temp_file(&output_audio_fs_path); + + // ==================== UPLOAD SPECTROGRAM TO BUCKETS ==================== // + + let spectrogram_result_object_path = 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..."); + + 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... + safe_delete_temp_directory(&temp_dir); + + // ==================== SAVE RECORDS ==================== // + + let text_hash = hash_string_sha2(&cleaned_inference_text) + .map_err(|e| ProcessSingleJobError::Other(e))?; + + let worker_name = job_dependencies.get_worker_name(); + + info!("Saving tts inference record..."); + + let (id, inference_result_token) = insert_tts_result( + &job_dependencies.mysql_pool, + job, + &text_hash, + pretrained_vocoder, + &audio_result_object_path, + &spectrogram_result_object_path, + file_metadata.file_size_bytes, + file_metadata.duration_millis.unwrap_or(0), + job_dependencies.worker_details.is_on_prem, + &worker_name, + job_dependencies.worker_details.is_debug_worker) + .await + .map_err(|e| ProcessSingleJobError::Other(e))?; + + info!("Marking job complete..."); + mark_tts_inference_job_done( + &job_dependencies.mysql_pool, + job.id, + true, + Some(&inference_result_token), + &worker_name) + .await + .map_err(|e| ProcessSingleJobError::Other(e))?; + + info!("TTS Done. Original text was: {}", &job.raw_inference_text); + + job_dependencies.firehose_publisher.tts_inference_finished( + job.maybe_creator_user_token.as_deref(), + &model_record.model_token, + &inference_result_token) + .await + .map_err(|e| { + error!("error publishing event: {:?}", e); + ProcessSingleJobError::Other(anyhow!("error publishing event")) + })?; + + job_progress_reporter.log_status("done") + .map_err(|e| ProcessSingleJobError::Other(e))?; + + info!("Job {:?} complete success! Downloaded, ran inference, and uploaded. Saved model record: {}, Result Token: {}", + job.id, id, &inference_result_token); + + let duration = start.elapsed(); + + since_creation_span.set_attribute("status", "success"); + since_creation_span.set_duration(duration); + + job_iteration_span.set_attribute("status", "success"); + job_iteration_span.set_duration(duration); + + */ + Ok(()) +} \ No newline at end of file diff --git a/crates/service/job/inference_job/src/job_types/tts/mod.rs b/crates/service/job/inference_job/src/job_types/tts/mod.rs new file mode 100644 index 0000000000..673e15b371 --- /dev/null +++ b/crates/service/job/inference_job/src/job_types/tts/mod.rs @@ -0,0 +1,3 @@ +pub mod handle_tts_inference; +pub mod read_metadata_file; +pub mod seconds_to_decoder_steps; diff --git a/crates/service/job/inference_job/src/job_types/tts/read_metadata_file.rs b/crates/service/job/inference_job/src/job_types/tts/read_metadata_file.rs new file mode 100644 index 0000000000..514813d9c3 --- /dev/null +++ b/crates/service/job/inference_job/src/job_types/tts/read_metadata_file.rs @@ -0,0 +1,18 @@ +use errors::AnyhowResult; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; + +#[derive(Deserialize, Default)] +pub struct FileMetadata { + pub duration_millis: Option, + pub mimetype: Option, + pub file_size_bytes: u64, +} + +pub fn read_metadata_file(filename: &PathBuf) -> AnyhowResult { + let mut file = File::open(filename)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + Ok(serde_json::from_str(&buffer)?) +} diff --git a/crates/service/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs b/crates/service/job/inference_job/src/job_types/tts/seconds_to_decoder_steps.rs similarity index 95% rename from crates/service/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs rename to crates/service/job/inference_job/src/job_types/tts/seconds_to_decoder_steps.rs index 6488c67e04..85d12d18d2 100644 --- a/crates/service/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs +++ b/crates/service/job/inference_job/src/job_types/tts/seconds_to_decoder_steps.rs @@ -24,7 +24,7 @@ pub fn seconds_to_decoder_steps(max_duration_seconds: i32) -> u32 { #[cfg(test)] mod tests { - use crate::job_steps::seconds_to_decoder_steps::seconds_to_decoder_steps; + use crate::job_types::tts::seconds_to_decoder_steps::seconds_to_decoder_steps; #[test] fn test_max_decoder_steps_zero() { diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/edit_vocoder.rs b/crates/service/job/inference_job/src/job_types/vc/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/edit_vocoder.rs rename to crates/service/job/inference_job/src/job_types/vc/mod.rs diff --git a/crates/service/job/inference_job/src/main.rs b/crates/service/job/inference_job/src/main.rs new file mode 100644 index 0000000000..dae31d27cf --- /dev/null +++ b/crates/service/job/inference_job/src/main.rs @@ -0,0 +1,335 @@ +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +//#![forbid(warnings)] +//#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +#[macro_use] extern crate serde_derive; + +pub mod caching; +pub mod http_clients; +pub mod job_steps; +pub mod job_types; +pub mod script_execution; +pub mod util; + +use clap::{App, Arg}; +use cloud_storage::bucket_client::BucketClient; +use cloud_storage::bucket_path_unifier::BucketPathUnifier; +use config::common_env::CommonEnv; +use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING; +use config::shared_constants::DEFAULT_RUST_LOG; +use container_common::anyhow_result::AnyhowResult; +use container_common::filesystem::check_directory_exists::check_directory_exists; +use crate::caching::cache_miss_strategizer::CacheMissStrategizer; +use crate::caching::cache_miss_strategizer_multi::SyncMultiCacheMissStrategizer; +use crate::caching::virtual_lfu_cache::SyncVirtualLfuCache; +use crate::http_clients::tts_inference_sidecar_client::TtsInferenceSidecarClient; +use crate::http_clients::tts_sidecar_health_check_client::TtsSidecarHealthCheckClient; +use crate::job_steps::job_dependencies::JobWorkerDetails; +use crate::job_steps::job_dependencies::{JobDependencies, JobCaches, JobHttpClients}; +use crate::job_steps::job_stats::JobStats; +use crate::script_execution::tacotron_inference_command::TacotronInferenceCommand; +use crate::util::scoped_temp_dir_creator::ScopedTempDirCreator; +use database_queries::mediators::firehose_publisher::FirehosePublisher; +use jobs_common::job_progress_reporter::job_progress_reporter::JobProgressReporterBuilder; +use jobs_common::job_progress_reporter::noop_job_progress_reporter::NoOpJobProgressReporterBuilder; +use jobs_common::job_progress_reporter::redis_job_progress_reporter::RedisJobProgressReporterBuilder; +use jobs_common::semi_persistent_cache_dir::SemiPersistentCacheDir; +use log::{warn, info}; +use memory_caching::multi_item_ttl_cache::MultiItemTtlCache; +use newrelic_telemetry::ClientBuilder; +use r2d2_redis::RedisConnectionManager; +use r2d2_redis::r2d2; +use sqlx::mysql::MySqlPoolOptions; +use std::path::PathBuf; +use std::time::Duration; + +// Buckets (shared config) +const ENV_ACCESS_KEY : &'static str = "ACCESS_KEY"; +const ENV_SECRET_KEY : &'static str = "SECRET_KEY"; +const ENV_REGION_NAME : &'static str = "REGION_NAME"; + +// Bucket names +const ENV_PRIVATE_BUCKET_NAME : &'static str = "PRIVATE_BUCKET_NAME"; +const ENV_PUBLIC_BUCKET_NAME : &'static str = "PUBLIC_BUCKET_NAME"; + +// Where models and other assets get downloaded to. +const ENV_SEMIPERSISTENT_CACHE_DIR : &'static str = "SEMIPERSISTENT_CACHE_DIR"; + +// Python code +const ENV_CODE_DIRECTORY : &'static str = "TTS_CODE_DIRECTORY"; +const ENV_INFERENCE_SCRIPT_NAME : &'static str = "TTS_INFERENCE_SCRIPT_NAME"; + +// HTTP sidecar +const ENV_TTS_INFERENCE_SIDECAR_HOSTNAME: &'static str = "TTS_INFERENCE_SIDECAR_HOSTNAME"; + +const DEFAULT_TEMP_DIR: &'static str = "/tmp"; + +#[tokio::main] +async fn main() -> AnyhowResult<()> { + easyenv::init_all_with_default_logging(Some(DEFAULT_RUST_LOG)); + + // NB: Do not check this secrets-containing dotenv file into VCS. + // This file should only contain *development* secrets, never production. + let _ = dotenv::from_filename(".env-secrets").ok(); + + let matches = App::new("tts-inference-job") + .arg(Arg::with_name("sidecar_hostname") + .long("sidecar_hostname") + .value_name("HOSTNAME") + .help("Hostname for the TTS inference sidecar") + .takes_value(true) + .required(false)) + .get_matches(); + + info!("Obtaining worker hostname..."); + + let server_hostname = hostname::get() + .ok() + .and_then(|h| h.into_string().ok()) + .unwrap_or("tts-inference-job".to_string()); + + // NB: These are non-standard env vars we're injecting ourselves. + let k8s_node_name = easyenv::get_env_string_optional("K8S_NODE_NAME"); + let k8s_pod_name = easyenv::get_env_string_optional("K8S_POD_NAME"); + + // NB: It'll be worthwhile to see how much compute is happening at our local on-premises cluster + // Only our local workers will set this to true. + let is_on_prem = easyenv::get_env_bool_or_default("IS_ON_PREM", false); + + info!("Hostname: {}", &server_hostname); + info!("Is on-premises: {}", is_on_prem); + + // Bucket stuff (shared) + let access_key = easyenv::get_env_string_required(ENV_ACCESS_KEY)?; + let secret_key = easyenv::get_env_string_required(ENV_SECRET_KEY)?; + let region_name = easyenv::get_env_string_required(ENV_REGION_NAME)?; + + // Private and Public Buckets + let private_bucket_name = easyenv::get_env_string_required(ENV_PRIVATE_BUCKET_NAME)?; + let public_bucket_name = easyenv::get_env_string_required(ENV_PUBLIC_BUCKET_NAME)?; + + let bucket_timeout = easyenv::get_env_duration_seconds_or_default("BUCKET_TIMEOUT_SECONDS", + Duration::from_secs(60 * 5)); + + let private_bucket_client = BucketClient::create( + &access_key, + &secret_key, + ®ion_name, + &private_bucket_name, + None, + Some(bucket_timeout), + )?; + + let public_bucket_client = BucketClient::create( + &access_key, + &secret_key, + ®ion_name, + &public_bucket_name, + None, + Some(bucket_timeout), + )?; + + let py_code_directory = easyenv::get_env_string_required(ENV_CODE_DIRECTORY)?; + let py_script_name = easyenv::get_env_string_required(ENV_INFERENCE_SCRIPT_NAME)?; + + let tts_inference_command = TacotronInferenceCommand::new( + &py_code_directory, + &py_script_name, + ); + + let mut sidecar_hostname = + easyenv::get_env_string_required(ENV_TTS_INFERENCE_SIDECAR_HOSTNAME)?; + + if let Some(hostname) = matches.value_of("sidecar_hostname") { + sidecar_hostname = hostname.to_string(); + } + + info!("Sidecar hostname: {:?}", sidecar_hostname); + + let tts_inference_sidecar_client = + TtsInferenceSidecarClient::new(&sidecar_hostname); + + let tts_sidecar_health_check_client= + TtsSidecarHealthCheckClient::new(&sidecar_hostname)?; + + let temp_directory = easyenv::get_env_string_or_default( + "DOWNLOAD_TEMP_DIR", + DEFAULT_TEMP_DIR); + + let temp_directory = PathBuf::from(temp_directory); + + check_directory_exists(&temp_directory)?; + + let db_connection_string = + easyenv::get_env_string_or_default( + "MYSQL_URL", + DEFAULT_MYSQL_CONNECTION_STRING); + + info!("Connecting to database..."); + + let mysql_pool = MySqlPoolOptions::new() + .max_connections(2) + .connect(&db_connection_string) + .await?; + + let common_env = CommonEnv::read_from_env()?; + + + let persistent_cache_path = easyenv::get_env_string_or_default( + ENV_SEMIPERSISTENT_CACHE_DIR, + "/tmp"); + + let semi_persistent_cache = + SemiPersistentCacheDir::configured_root(&persistent_cache_path); + + info!("Creating pod semi-persistent cache dirs..."); + semi_persistent_cache.create_tts_synthesizer_model_path()?; + semi_persistent_cache.create_tts_pretrained_vocoder_model_path()?; + semi_persistent_cache.create_custom_vocoder_model_path()?; + + let waveglow_vocoder_model_filename = easyenv::get_env_string_or_default( + "TTS_WAVEGLOW_VOCODER_MODEL_FILENAME", "waveglow.pth"); + + let hifigan_vocoder_model_filename = easyenv::get_env_string_or_default( + "TTS_HIFIGAN_VOCODER_MODEL_FILENAME", "hifigan.pth"); + + let hifigan_superres_vocoder_model_filename = easyenv::get_env_string_or_default( + "TTS_HIFIGAN_SUPERRES_VOCODER_MODEL_FILENAME", "hifigan_superres.pth"); + + let sidecar_max_synthesizer_models = easyenv::get_env_num( + "SIDECAR_MAX_SYNTHESIZER_MODELS", 3)?; + + // Set to "0" to always treat low priority the same as high priority + let low_priority_starvation_prevention_every_nth= easyenv::get_env_num( + "LOW_PRIORITY_STARVATION_PREVENTION_EVERY_NTH", 3)?; + + let firehose_publisher = FirehosePublisher { + mysql_pool: mysql_pool.clone(), // NB: MySqlPool is clone/send/sync safe + }; + + let virtual_lfu_cache = SyncVirtualLfuCache::new(sidecar_max_synthesizer_models)?; + + let cache_miss_strategizers = { + let in_memory_strategizer = CacheMissStrategizer::new( + chrono::Duration::milliseconds( + easyenv::get_env_num("MEMORY_MAX_COLD_DURATION_MILLIS", 5_000)?, + ), + chrono::Duration::milliseconds( + easyenv::get_env_num("MEMORY_CACHE_FORGET_DURATION_MILLIS", 60_000)?, + ), + ); + + let on_disk_strategizer = CacheMissStrategizer::new( + chrono::Duration::milliseconds( + easyenv::get_env_num("DISK_MAX_COLD_DURATION_MILLIS", 20_000)?, + ), + chrono::Duration::milliseconds( + easyenv::get_env_num("DISK_CACHE_FORGET_DURATION_MILLIS", 120_000)?, + ), + ); + + SyncMultiCacheMissStrategizer::new( + in_memory_strategizer, + on_disk_strategizer, + ) + }; + + let model_cache_duration = std::time::Duration::from_millis( + easyenv::get_env_num("TTS_MODEL_RECORD_CACHE_MILLIS", 300_000)?, // Five minutes + ); + + let license_key = easyenv::get_env_string_required("NEWRELIC_API_KEY")?; + + let newrelic_disabled = easyenv::get_env_bool_or_default("IS_NEWRELIC_DISABLED", false); + + let newrelic_client = ClientBuilder::new(&license_key).build().unwrap(); + + let maybe_minimum_priority = easyenv::get_env_string_optional("MAYBE_MINIMUM_PRIORITY") + .map(|priority_string| { + priority_string.parse::() + }) + .transpose()?; + + info!("Using 'MAYBE_MINIMUM_PRIORITY' of {:?}", maybe_minimum_priority); + + let is_debug_worker = easyenv::get_env_bool_or_default("IS_DEBUG_WORKER", false); + + info!("Is debug worker? {}", is_debug_worker); + + // Optionally report job progress to the user via Redis (for now) + // We want to turn this off in the on-premises workers since we're not tunneling to the production Redis. + let job_progress_reporter : Box; + + job_progress_reporter = match easyenv::get_env_string_optional("REDIS_FOR_JOB_PROGRESS") { + None => { + warn!("Redis for job progress status reports is DISABLED! Users will not see in-flight details of inference progress."); + Box::new(NoOpJobProgressReporterBuilder {}) + }, + Some(redis_connection_string) => { + info!("Connecting to Redis to use for reporting job progress... {}", redis_connection_string); + let redis_manager = RedisConnectionManager::new(redis_connection_string)?; + let redis_pool = r2d2::Pool::builder().build(redis_manager)?; + + Box::new(RedisJobProgressReporterBuilder::from_redis_pool(redis_pool)) + } + }; + + let _job_dependencies = JobDependencies { + scoped_temp_dir_creator: ScopedTempDirCreator::for_directory(&temp_directory), + download_temp_directory: temp_directory, + mysql_pool, + maybe_redis_pool: None, // TODO(bt, 2023-01-11): See note in JobDependencies + job_progress_reporter, + public_bucket_client, + private_bucket_client, + tts_inference_command, + http_clients: JobHttpClients { + tts_inference_sidecar_client, + tts_sidecar_health_check_client, + }, + job_stats: JobStats::new(), + newrelic_client, + newrelic_disabled, + worker_details: JobWorkerDetails { + is_on_prem, + worker_hostname: server_hostname.clone(), + k8s_node_name, + k8s_pod_name, + is_debug_worker, + }, + caches: JobCaches { + tts_model_record_cache: MultiItemTtlCache::create_with_duration(model_cache_duration), + }, + virtual_model_lfu: virtual_lfu_cache, + cache_miss_strategizers, + bucket_path_unifier: BucketPathUnifier::default_paths(), + semi_persistent_cache, + firehose_publisher, + waveglow_vocoder_model_filename, + hifigan_vocoder_model_filename, + hifigan_superres_vocoder_model_filename, + job_batch_wait_millis: common_env.job_batch_wait_millis, + job_max_attempts: common_env.job_max_attempts as u16, + job_batch_size: common_env.job_batch_size, + no_op_logger_millis: common_env.no_op_logger_millis, + sidecar_max_synthesizer_models, + low_priority_starvation_prevention_every_nth, + maybe_minimum_priority, + }; + + // TODO: Main loop start + //main_loop_old(inferencer).await; + + Ok(()) +} diff --git a/crates/service/inference_job/src/script_execution/mod.rs b/crates/service/job/inference_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/inference_job/src/script_execution/mod.rs rename to crates/service/job/inference_job/src/script_execution/mod.rs diff --git a/crates/service/inference_job/src/script_execution/tacotron_inference_command.rs b/crates/service/job/inference_job/src/script_execution/tacotron_inference_command.rs similarity index 100% rename from crates/service/inference_job/src/script_execution/tacotron_inference_command.rs rename to crates/service/job/inference_job/src/script_execution/tacotron_inference_command.rs diff --git a/crates/service/tts_inference_job/src/job_steps/download_file_from_bucket.rs b/crates/service/job/inference_job/src/util/download_file_from_bucket.rs similarity index 91% rename from crates/service/tts_inference_job/src/job_steps/download_file_from_bucket.rs rename to crates/service/job/inference_job/src/util/download_file_from_bucket.rs index f11fe2d748..a81a0ea04a 100644 --- a/crates/service/tts_inference_job/src/job_steps/download_file_from_bucket.rs +++ b/crates/service/job/inference_job/src/util/download_file_from_bucket.rs @@ -1,8 +1,9 @@ -use std::path::Path; -use log::{info, warn}; +use cloud_storage::bucket_client::BucketClient; +use crate::job_steps::process_single_job_error::ProcessSingleJobError; +use crate::util::scoped_temp_dir_creator::ScopedTempDirCreator; use jobs_common::job_progress_reporter::job_progress_reporter::JobProgressReporter; -use storage_buckets_common::bucket_client::BucketClient; -use crate::{ProcessSingleJobError, ScopedTempDirCreator}; +use log::{info, warn}; +use std::path::Path; // TODO(bt, 2022-07-15): Make a concrete type for bucket paths diff --git a/crates/service/job/inference_job/src/util/mod.rs b/crates/service/job/inference_job/src/util/mod.rs new file mode 100644 index 0000000000..87a33eb3f8 --- /dev/null +++ b/crates/service/job/inference_job/src/util/mod.rs @@ -0,0 +1,2 @@ +pub mod scoped_temp_dir_creator; +pub mod download_file_from_bucket; diff --git a/crates/service/inference_job/src/util/scoped_temp_dir_creator.rs b/crates/service/job/inference_job/src/util/scoped_temp_dir_creator.rs similarity index 100% rename from crates/service/inference_job/src/util/scoped_temp_dir_creator.rs rename to crates/service/job/inference_job/src/util/scoped_temp_dir_creator.rs diff --git a/crates/service/tts_download_job/Cargo.toml b/crates/service/job/tts_download_job/Cargo.toml similarity index 74% rename from crates/service/tts_download_job/Cargo.toml rename to crates/service/job/tts_download_job/Cargo.toml index c0acba04e4..e98c0bd013 100644 --- a/crates/service/tts_download_job/Cargo.toml +++ b/crates/service/job/tts_download_job/Cargo.toml @@ -15,16 +15,16 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -jobs_common = { path = "../../lib/jobs_common" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +cloud_storage = { path = "../../../lib/cloud_storage" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +jobs_common = { path = "../../../lib/jobs_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/tts_download_job/src/main.rs b/crates/service/job/tts_download_job/src/main.rs similarity index 99% rename from crates/service/tts_download_job/src/main.rs rename to crates/service/job/tts_download_job/src/main.rs index 53855e8ec1..73d4152cba 100644 --- a/crates/service/tts_download_job/src/main.rs +++ b/crates/service/job/tts_download_job/src/main.rs @@ -15,6 +15,8 @@ pub mod script_execution; use anyhow::anyhow; use chrono::Utc; +use cloud_storage::bucket_client::BucketClient; +use cloud_storage::bucket_path_unifier::BucketPathUnifier; use config::bad_urls::is_bad_tts_model_download_url; use config::common_env::CommonEnv; use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING; @@ -51,8 +53,6 @@ use std::ops::Deref; use std::path::{PathBuf, Path}; use std::process::Command; use std::time::Duration; -use storage_buckets_common::bucket_client::BucketClient; -use storage_buckets_common::bucket_path_unifier::BucketPathUnifier; use tempdir::TempDir; // Buckets diff --git a/crates/service/tts_download_job/src/script_execution/mod.rs b/crates/service/job/tts_download_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/tts_download_job/src/script_execution/mod.rs rename to crates/service/job/tts_download_job/src/script_execution/mod.rs diff --git a/crates/service/tts_download_job/src/script_execution/tacotron_model_check_command.rs b/crates/service/job/tts_download_job/src/script_execution/tacotron_model_check_command.rs similarity index 100% rename from crates/service/tts_download_job/src/script_execution/tacotron_model_check_command.rs rename to crates/service/job/tts_download_job/src/script_execution/tacotron_model_check_command.rs diff --git a/crates/service/tts_download_job/src/script_execution/talknet_model_check_command.rs b/crates/service/job/tts_download_job/src/script_execution/talknet_model_check_command.rs similarity index 100% rename from crates/service/tts_download_job/src/script_execution/talknet_model_check_command.rs rename to crates/service/job/tts_download_job/src/script_execution/talknet_model_check_command.rs diff --git a/crates/service/tts_inference_job/Cargo.toml b/crates/service/job/tts_inference_job/Cargo.toml similarity index 74% rename from crates/service/tts_inference_job/Cargo.toml rename to crates/service/job/tts_inference_job/Cargo.toml index 62d22f4060..958ddd0b65 100644 --- a/crates/service/tts_inference_job/Cargo.toml +++ b/crates/service/job/tts_inference_job/Cargo.toml @@ -15,16 +15,16 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -jobs_common = { path = "../../lib/jobs_common" } -memory_caching = { path = "../../lib/memory_caching" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +cloud_storage = { path = "../../../lib/cloud_storage" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +jobs_common = { path = "../../../lib/jobs_common" } +memory_caching = { path = "../../../lib/memory_caching" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/inference_job/src/caching/cache_miss_strategizer.rs b/crates/service/job/tts_inference_job/src/caching/cache_miss_strategizer.rs similarity index 100% rename from crates/service/inference_job/src/caching/cache_miss_strategizer.rs rename to crates/service/job/tts_inference_job/src/caching/cache_miss_strategizer.rs diff --git a/crates/service/tts_inference_job/src/caching/cache_miss_strategizer_multi.rs b/crates/service/job/tts_inference_job/src/caching/cache_miss_strategizer_multi.rs similarity index 100% rename from crates/service/tts_inference_job/src/caching/cache_miss_strategizer_multi.rs rename to crates/service/job/tts_inference_job/src/caching/cache_miss_strategizer_multi.rs diff --git a/crates/service/tts_inference_job/src/caching/mod.rs b/crates/service/job/tts_inference_job/src/caching/mod.rs similarity index 100% rename from crates/service/tts_inference_job/src/caching/mod.rs rename to crates/service/job/tts_inference_job/src/caching/mod.rs diff --git a/crates/service/tts_inference_job/src/caching/virtual_lfu_cache.rs b/crates/service/job/tts_inference_job/src/caching/virtual_lfu_cache.rs similarity index 100% rename from crates/service/tts_inference_job/src/caching/virtual_lfu_cache.rs rename to crates/service/job/tts_inference_job/src/caching/virtual_lfu_cache.rs diff --git a/crates/service/tts_inference_job/src/http_clients/mod.rs b/crates/service/job/tts_inference_job/src/http_clients/mod.rs similarity index 100% rename from crates/service/tts_inference_job/src/http_clients/mod.rs rename to crates/service/job/tts_inference_job/src/http_clients/mod.rs diff --git a/crates/service/tts_inference_job/src/http_clients/tts_inference_sidecar_client.rs b/crates/service/job/tts_inference_job/src/http_clients/tts_inference_sidecar_client.rs similarity index 100% rename from crates/service/tts_inference_job/src/http_clients/tts_inference_sidecar_client.rs rename to crates/service/job/tts_inference_job/src/http_clients/tts_inference_sidecar_client.rs diff --git a/crates/service/tts_inference_job/src/http_clients/tts_sidecar_health_check_client.rs b/crates/service/job/tts_inference_job/src/http_clients/tts_sidecar_health_check_client.rs similarity index 100% rename from crates/service/tts_inference_job/src/http_clients/tts_sidecar_health_check_client.rs rename to crates/service/job/tts_inference_job/src/http_clients/tts_sidecar_health_check_client.rs diff --git a/crates/service/inference_job/src/job_steps/download_file_from_bucket.rs b/crates/service/job/tts_inference_job/src/job_steps/download_file_from_bucket.rs similarity index 97% rename from crates/service/inference_job/src/job_steps/download_file_from_bucket.rs rename to crates/service/job/tts_inference_job/src/job_steps/download_file_from_bucket.rs index f11fe2d748..2df4d1afd3 100644 --- a/crates/service/inference_job/src/job_steps/download_file_from_bucket.rs +++ b/crates/service/job/tts_inference_job/src/job_steps/download_file_from_bucket.rs @@ -1,8 +1,8 @@ -use std::path::Path; -use log::{info, warn}; -use jobs_common::job_progress_reporter::job_progress_reporter::JobProgressReporter; -use storage_buckets_common::bucket_client::BucketClient; +use cloud_storage::bucket_client::BucketClient; use crate::{ProcessSingleJobError, ScopedTempDirCreator}; +use jobs_common::job_progress_reporter::job_progress_reporter::JobProgressReporter; +use log::{info, warn}; +use std::path::Path; // TODO(bt, 2022-07-15): Make a concrete type for bucket paths diff --git a/crates/service/tts_inference_job/src/job_steps/health_check_trap.rs b/crates/service/job/tts_inference_job/src/job_steps/health_check_trap.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/health_check_trap.rs rename to crates/service/job/tts_inference_job/src/job_steps/health_check_trap.rs diff --git a/crates/service/tts_inference_job/src/job_steps/job_args.rs b/crates/service/job/tts_inference_job/src/job_steps/job_args.rs similarity index 97% rename from crates/service/tts_inference_job/src/job_steps/job_args.rs rename to crates/service/job/tts_inference_job/src/job_steps/job_args.rs index be4e37ea6b..6aac938bd2 100644 --- a/crates/service/tts_inference_job/src/job_steps/job_args.rs +++ b/crates/service/job/tts_inference_job/src/job_steps/job_args.rs @@ -1,3 +1,5 @@ +use cloud_storage::bucket_client::BucketClient; +use cloud_storage::bucket_path_unifier::BucketPathUnifier; use crate::caching::cache_miss_strategizer_multi::SyncMultiCacheMissStrategizer; use crate::caching::virtual_lfu_cache::SyncVirtualLfuCache; use crate::http_clients::tts_inference_sidecar_client::TtsInferenceSidecarClient; @@ -13,8 +15,6 @@ use memory_caching::multi_item_ttl_cache::MultiItemTtlCache; use newrelic_telemetry::Client as NewRelicClient; use sqlx::MySqlPool; use std::path::PathBuf; -use storage_buckets_common::bucket_client::BucketClient; -use storage_buckets_common::bucket_path_unifier::BucketPathUnifier; pub struct JobArgs { pub download_temp_directory: PathBuf, diff --git a/crates/service/tts_inference_job/src/job_steps/job_stats.rs b/crates/service/job/tts_inference_job/src/job_steps/job_stats.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/job_stats.rs rename to crates/service/job/tts_inference_job/src/job_steps/job_stats.rs diff --git a/crates/service/inference_job/src/job_steps/mod.rs b/crates/service/job/tts_inference_job/src/job_steps/mod.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/mod.rs rename to crates/service/job/tts_inference_job/src/job_steps/mod.rs diff --git a/crates/service/inference_job/src/job_steps/process_single_job.rs b/crates/service/job/tts_inference_job/src/job_steps/process_single_job.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/process_single_job.rs rename to crates/service/job/tts_inference_job/src/job_steps/process_single_job.rs diff --git a/crates/service/tts_inference_job/src/job_steps/process_single_job_error.rs b/crates/service/job/tts_inference_job/src/job_steps/process_single_job_error.rs similarity index 100% rename from crates/service/tts_inference_job/src/job_steps/process_single_job_error.rs rename to crates/service/job/tts_inference_job/src/job_steps/process_single_job_error.rs diff --git a/crates/service/inference_job/src/job_steps/seconds_to_decoder_steps.rs b/crates/service/job/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs similarity index 100% rename from crates/service/inference_job/src/job_steps/seconds_to_decoder_steps.rs rename to crates/service/job/tts_inference_job/src/job_steps/seconds_to_decoder_steps.rs diff --git a/crates/service/tts_inference_job/src/main.rs b/crates/service/job/tts_inference_job/src/main.rs similarity index 99% rename from crates/service/tts_inference_job/src/main.rs rename to crates/service/job/tts_inference_job/src/main.rs index a171ffa07d..e112d5d1de 100644 --- a/crates/service/tts_inference_job/src/main.rs +++ b/crates/service/job/tts_inference_job/src/main.rs @@ -22,6 +22,8 @@ pub mod script_execution; pub mod util; use clap::{App, Arg}; +use cloud_storage::bucket_client::BucketClient; +use cloud_storage::bucket_path_unifier::BucketPathUnifier; use config::common_env::CommonEnv; use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING; use config::shared_constants::DEFAULT_RUST_LOG; @@ -62,8 +64,6 @@ use sqlx::MySqlPool; use sqlx::mysql::MySqlPoolOptions; use std::path::PathBuf; use std::time::Duration; -use storage_buckets_common::bucket_client::BucketClient; -use storage_buckets_common::bucket_path_unifier::BucketPathUnifier; // Buckets (shared config) const ENV_ACCESS_KEY : &'static str = "ACCESS_KEY"; diff --git a/crates/service/tts_inference_job/src/script_execution/mod.rs b/crates/service/job/tts_inference_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/tts_inference_job/src/script_execution/mod.rs rename to crates/service/job/tts_inference_job/src/script_execution/mod.rs diff --git a/crates/service/tts_inference_job/src/script_execution/tacotron_inference_command.rs b/crates/service/job/tts_inference_job/src/script_execution/tacotron_inference_command.rs similarity index 100% rename from crates/service/tts_inference_job/src/script_execution/tacotron_inference_command.rs rename to crates/service/job/tts_inference_job/src/script_execution/tacotron_inference_command.rs diff --git a/crates/service/inference_job/src/util/mod.rs b/crates/service/job/tts_inference_job/src/util/mod.rs similarity index 100% rename from crates/service/inference_job/src/util/mod.rs rename to crates/service/job/tts_inference_job/src/util/mod.rs diff --git a/crates/service/tts_inference_job/src/util/scoped_temp_dir_creator.rs b/crates/service/job/tts_inference_job/src/util/scoped_temp_dir_creator.rs similarity index 100% rename from crates/service/tts_inference_job/src/util/scoped_temp_dir_creator.rs rename to crates/service/job/tts_inference_job/src/util/scoped_temp_dir_creator.rs diff --git a/crates/service/w2l_download_job/Cargo.toml b/crates/service/job/w2l_download_job/Cargo.toml similarity index 72% rename from crates/service/w2l_download_job/Cargo.toml rename to crates/service/job/w2l_download_job/Cargo.toml index 2b0dd5df88..3d212d5ea4 100644 --- a/crates/service/w2l_download_job/Cargo.toml +++ b/crates/service/job/w2l_download_job/Cargo.toml @@ -15,16 +15,17 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -jobs_common = { path = "../../lib/jobs_common" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +buckets = { path = "../../../_types/buckets" } +cloud_storage = { path = "../../../lib/cloud_storage" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +jobs_common = { path = "../../../lib/jobs_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/w2l_download_job/src/main.rs b/crates/service/job/w2l_download_job/src/main.rs similarity index 99% rename from crates/service/w2l_download_job/src/main.rs rename to crates/service/job/w2l_download_job/src/main.rs index 45c3a594c9..c2d5da4e45 100644 --- a/crates/service/w2l_download_job/src/main.rs +++ b/crates/service/job/w2l_download_job/src/main.rs @@ -14,7 +14,9 @@ pub mod script_execution; use anyhow::{anyhow, Error}; +use buckets::util::hash_to_bucket_path_string::hash_to_bucket_path_string; use chrono::Utc; +use cloud_storage::bucket_client::BucketClient; use config::common_env::CommonEnv; use config::is_bad_video_download_url::is_bad_video_download_url; use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING; @@ -53,8 +55,6 @@ use std::path::{PathBuf, Path}; use std::process::Command; use std::thread; use std::time::Duration; -use storage_buckets_common::bucket_client::BucketClient; -use storage_buckets_common::bucket_paths::hash_to_bucket_path; use tempdir::TempDir; // Buckets (shared config) @@ -471,7 +471,7 @@ async fn process_job(downloader: &Downloader, job: &W2lTemplateUploadJobRecord) info!("File hash: {}", private_bucket_hash); // Full path to video/image - let full_object_path = hash_to_bucket_path( + let full_object_path = hash_to_bucket_path_string( &private_bucket_hash, Some(&downloader.bucket_root_w2l_template_uploads))?; diff --git a/crates/service/w2l_download_job/src/script_execution/ffmpeg_generate_preview_image_command.rs b/crates/service/job/w2l_download_job/src/script_execution/ffmpeg_generate_preview_image_command.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/ffmpeg_generate_preview_image_command.rs rename to crates/service/job/w2l_download_job/src/script_execution/ffmpeg_generate_preview_image_command.rs diff --git a/crates/service/w2l_download_job/src/script_execution/ffmpeg_generate_preview_video_command.rs b/crates/service/job/w2l_download_job/src/script_execution/ffmpeg_generate_preview_video_command.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/ffmpeg_generate_preview_video_command.rs rename to crates/service/job/w2l_download_job/src/script_execution/ffmpeg_generate_preview_video_command.rs diff --git a/crates/service/w2l_download_job/src/script_execution/imagemagick_generate_preview_image_command.rs b/crates/service/job/w2l_download_job/src/script_execution/imagemagick_generate_preview_image_command.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/imagemagick_generate_preview_image_command.rs rename to crates/service/job/w2l_download_job/src/script_execution/imagemagick_generate_preview_image_command.rs diff --git a/crates/service/w2l_download_job/src/script_execution/mod.rs b/crates/service/job/w2l_download_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/mod.rs rename to crates/service/job/w2l_download_job/src/script_execution/mod.rs diff --git a/crates/service/w2l_download_job/src/script_execution/wav2lip_process_upload_command.rs b/crates/service/job/w2l_download_job/src/script_execution/wav2lip_process_upload_command.rs similarity index 100% rename from crates/service/w2l_download_job/src/script_execution/wav2lip_process_upload_command.rs rename to crates/service/job/w2l_download_job/src/script_execution/wav2lip_process_upload_command.rs diff --git a/crates/service/w2l_inference_job/Cargo.toml b/crates/service/job/w2l_inference_job/Cargo.toml similarity index 74% rename from crates/service/w2l_inference_job/Cargo.toml rename to crates/service/job/w2l_inference_job/Cargo.toml index 6b8f26ab50..82739ee5d5 100644 --- a/crates/service/w2l_inference_job/Cargo.toml +++ b/crates/service/job/w2l_inference_job/Cargo.toml @@ -15,16 +15,16 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -jobs_common = { path = "../../lib/jobs_common" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } +cloud_storage = { path = "../../../lib/cloud_storage" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +jobs_common = { path = "../../../lib/jobs_common" } +tts_common = { path = "../../../lib/tts_common" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" diff --git a/crates/service/w2l_inference_job/src/main.rs b/crates/service/job/w2l_inference_job/src/main.rs similarity index 99% rename from crates/service/w2l_inference_job/src/main.rs rename to crates/service/job/w2l_inference_job/src/main.rs index 0bb6f95ef5..4c6e264f4e 100644 --- a/crates/service/w2l_inference_job/src/main.rs +++ b/crates/service/job/w2l_inference_job/src/main.rs @@ -15,6 +15,8 @@ mod script_execution; use anyhow::anyhow; use chrono::Utc; +use cloud_storage::bucket_client::BucketClient; +use cloud_storage::bucket_path_unifier::BucketPathUnifier; use config::common_env::CommonEnv; use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING; use config::shared_constants::DEFAULT_RUST_LOG; @@ -50,8 +52,6 @@ use std::path::{PathBuf, Path}; use std::process::Command; use std::thread; use std::time::Duration; -use storage_buckets_common::bucket_client::BucketClient; -use storage_buckets_common::bucket_path_unifier::BucketPathUnifier; use tempdir::TempDir; // Buckets (shared config) diff --git a/crates/service/w2l_inference_job/src/script_execution/mod.rs b/crates/service/job/w2l_inference_job/src/script_execution/mod.rs similarity index 100% rename from crates/service/w2l_inference_job/src/script_execution/mod.rs rename to crates/service/job/w2l_inference_job/src/script_execution/mod.rs diff --git a/crates/service/w2l_inference_job/src/script_execution/wav2lip_inference_command.rs b/crates/service/job/w2l_inference_job/src/script_execution/wav2lip_inference_command.rs similarity index 100% rename from crates/service/w2l_inference_job/src/script_execution/wav2lip_inference_command.rs rename to crates/service/job/w2l_inference_job/src/script_execution/wav2lip_inference_command.rs diff --git a/crates/service/twitch_pubsub_subscriber/Cargo.toml b/crates/service/other/twitch_pubsub_subscriber/Cargo.toml similarity index 89% rename from crates/service/twitch_pubsub_subscriber/Cargo.toml rename to crates/service/other/twitch_pubsub_subscriber/Cargo.toml index 8e154cf384..03c1ca872c 100644 --- a/crates/service/twitch_pubsub_subscriber/Cargo.toml +++ b/crates/service/other/twitch_pubsub_subscriber/Cargo.toml @@ -15,13 +15,13 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -http_server_common = { path = "../../lib/http_server_common" } -redis_common = { path = "../../lib/redis_common" } -tts_common = { path = "../../lib/tts_common" } -twitch_common = { path = "../../lib/twitch_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +http_server_common = { path = "../../../lib/http_server_common" } +redis_common = { path = "../../../lib/redis_common" } +tts_common = { path = "../../../lib/tts_common" } +twitch_common = { path = "../../../lib/twitch_common" } # External anyhow = "1.0.40" diff --git a/crates/service/twitch_pubsub_subscriber/src/main.rs b/crates/service/other/twitch_pubsub_subscriber/src/main.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/main.rs rename to crates/service/other/twitch_pubsub_subscriber/src/main.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/listen_for_active_obs_sessions_thread.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/listen_for_active_obs_sessions_thread.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/listen_for_active_obs_sessions_thread.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/listen_for_active_obs_sessions_thread.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/listen_for_active_obs_sessions/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/bits_event_handler.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/bits_event_handler.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/bits_event_handler.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/bits_event_handler.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/channel_points_event_handler.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/channel_points_event_handler.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/channel_points_event_handler.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/channel_points_event_handler.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/event_handlers/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/subscriber_preferences_cache.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/subscriber_preferences_cache.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/subscriber_preferences_cache.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/subscriber_preferences_cache.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/tts_writer.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/tts_writer.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/tts_writer.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/tts_writer.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/twitch_pubsub_user_subscriber_thread.rs b/crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/twitch_pubsub_user_subscriber_thread.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/twitch_pubsub_user_subscriber_thread.rs rename to crates/service/other/twitch_pubsub_subscriber/src/threads/twitch_pubsub_user_subscriber/twitch_pubsub_user_subscriber_thread.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/constants.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/constants.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/constants.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/constants.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/oauth/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/oauth/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/oauth/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/oauth/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/oauth/oauth_token_refresher.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/oauth/oauth_token_refresher.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/oauth/oauth_token_refresher.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/oauth/oauth_token_refresher.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/pubsub/build_pubsub_topics_for_user.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/pubsub/build_pubsub_topics_for_user.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/pubsub/build_pubsub_topics_for_user.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/pubsub/build_pubsub_topics_for_user.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/pubsub/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/pubsub/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/pubsub/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/pubsub/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/twitch/websocket_client.rs b/crates/service/other/twitch_pubsub_subscriber/src/twitch/websocket_client.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/twitch/websocket_client.rs rename to crates/service/other/twitch_pubsub_subscriber/src/twitch/websocket_client.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/util/mod.rs b/crates/service/other/twitch_pubsub_subscriber/src/util/mod.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/util/mod.rs rename to crates/service/other/twitch_pubsub_subscriber/src/util/mod.rs diff --git a/crates/service/twitch_pubsub_subscriber/src/util/random_nonce.rs b/crates/service/other/twitch_pubsub_subscriber/src/util/random_nonce.rs similarity index 100% rename from crates/service/twitch_pubsub_subscriber/src/util/random_nonce.rs rename to crates/service/other/twitch_pubsub_subscriber/src/util/random_nonce.rs diff --git a/crates/service/tts_inference_job/src/job_steps/mod.rs b/crates/service/tts_inference_job/src/job_steps/mod.rs deleted file mode 100644 index 28cd17ed96..0000000000 --- a/crates/service/tts_inference_job/src/job_steps/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod download_file_from_bucket; -pub mod health_check_trap; -pub mod job_args; -pub mod job_stats; -pub mod process_single_job; -pub mod process_single_job_error; -pub mod seconds_to_decoder_steps; \ No newline at end of file diff --git a/crates/service/tts_inference_job/src/util/mod.rs b/crates/service/tts_inference_job/src/util/mod.rs deleted file mode 100644 index db617bc657..0000000000 --- a/crates/service/tts_inference_job/src/util/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod scoped_temp_dir_creator; \ No newline at end of file diff --git a/crates/service/image_generation_service/Cargo.toml b/crates/service/web/image_generation_service/Cargo.toml similarity index 79% rename from crates/service/image_generation_service/Cargo.toml rename to crates/service/web/image_generation_service/Cargo.toml index 31c08b374b..a209fb30f4 100644 --- a/crates/service/image_generation_service/Cargo.toml +++ b/crates/service/web/image_generation_service/Cargo.toml @@ -16,19 +16,19 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -http_server_common = { path = "../../lib/http_server_common" } -jobs_common = { path = "../../lib/jobs_common" } -memory_caching = { path = "../../lib/memory_caching" } -redis_common = { path = "../../lib/redis_common" } -reusable_types = { path = "../../lib/reusable_types" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tts_common = { path = "../../lib/tts_common" } -twitch_common = { path = "../../lib/twitch_common" } -user_input_common = { path = "../../lib/user_input_common" } +cloud_storage = { path = "../../../lib/cloud_storage" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +google_drive_common = { path = "../../../lib/google_drive_common" } +http_server_common = { path = "../../../lib/http_server_common" } +jobs_common = { path = "../../../lib/jobs_common" } +memory_caching = { path = "../../../lib/memory_caching" } +redis_common = { path = "../../../lib/redis_common" } +reusable_types = { path = "../../../_types/reusable_types" } +tts_common = { path = "../../../lib/tts_common" } +twitch_common = { path = "../../../lib/twitch_common" } +user_input_common = { path = "../../../lib/user_input_common" } # External anyhow = "1.0.40" diff --git a/crates/service/image_generation_service/src/main.rs b/crates/service/web/image_generation_service/src/main.rs similarity index 99% rename from crates/service/image_generation_service/src/main.rs rename to crates/service/web/image_generation_service/src/main.rs index 16748931a7..8f9df7794f 100644 --- a/crates/service/image_generation_service/src/main.rs +++ b/crates/service/web/image_generation_service/src/main.rs @@ -22,6 +22,7 @@ mod server_state; use actix_web::middleware::{DefaultHeaders, Logger}; use actix_web::{App, HttpServer, web}; +use cloud_storage::bucket_client::BucketClient; use config::common_env::CommonEnv; use config::shared_constants::{DEFAULT_MYSQL_CONNECTION_STRING, DEFAULT_RUST_LOG}; use container_common::anyhow_result::AnyhowResult; @@ -35,7 +36,6 @@ use r2d2_redis::{r2d2, RedisConnectionManager}; use sqlx::mysql::MySqlPoolOptions; use std::sync::Arc; use std::time::Duration; -use storage_buckets_common::bucket_client::BucketClient; use tokio::runtime::Runtime; const DEFAULT_BIND_ADDRESS : &'static str = "0.0.0.0:12345"; diff --git a/crates/service/image_generation_service/src/routes.rs b/crates/service/web/image_generation_service/src/routes.rs similarity index 100% rename from crates/service/image_generation_service/src/routes.rs rename to crates/service/web/image_generation_service/src/routes.rs diff --git a/crates/service/image_generation_service/src/server_state.rs b/crates/service/web/image_generation_service/src/server_state.rs similarity index 100% rename from crates/service/image_generation_service/src/server_state.rs rename to crates/service/web/image_generation_service/src/server_state.rs diff --git a/crates/service/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml similarity index 74% rename from crates/service/storyteller_web/Cargo.toml rename to crates/service/web/storyteller_web/Cargo.toml index 5cc9db3bf3..4142564277 100644 --- a/crates/service/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -16,28 +16,32 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -google_drive_common = { path = "../../lib/google_drive_common" } -http_server_common = { path = "../../lib/http_server_common" } -jobs_common = { path = "../../lib/jobs_common" } -memory_caching = { path = "../../lib/memory_caching" } -redis_common = { path = "../../lib/redis_common" } -reusable_types = { path = "../../lib/reusable_types" } -storage_buckets_common = { path = "../../lib/storage_buckets_common" } -tokens = { path = "../../lib/tokens" } -tts_common = { path = "../../lib/tts_common" } -twitch_common = { path = "../../lib/twitch_common" } -url_config = { path = "../../lib/url_config" } -user_input_common = { path = "../../lib/user_input_common" } +buckets = { path = "../../../_types/buckets" } +cloud_storage = { path = "../../../lib/cloud_storage" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +enums = { path = "../../../_types/enums" } +files = { path = "../../../lib/files" } +google_drive_common = { path = "../../../lib/google_drive_common" } +http_server_common = { path = "../../../lib/http_server_common" } +jobs_common = { path = "../../../lib/jobs_common" } +media = { path = "../../../lib/media" } +memory_caching = { path = "../../../lib/memory_caching" } +redis_common = { path = "../../../lib/redis_common" } +reusable_types = { path = "../../../_types/reusable_types" } +tokens = { path = "../../../_types/tokens" } +tts_common = { path = "../../../lib/tts_common" } +twitch_common = { path = "../../../lib/twitch_common" } +url_config = { path = "../../../lib/url_config" } +user_input_common = { path = "../../../lib/user_input_common" } # Internal Components -billing_component = { path = "../../plugins/billing" } -users_component = { path = "../../plugins/users" } +billing_component = { path = "../../../plugins/billing" } +users_component = { path = "../../../plugins/users" } # Vendored code -newrelic-telemetry = { path = "../../vendor/newrelic-telemetry-sdk-rust" } +newrelic-telemetry = { path = "../../../vendor/newrelic-telemetry-sdk-rust" } # External anyhow = "1.0.40" @@ -55,6 +59,7 @@ log = "0.4.14" once_cell = "1.9.0" rand = "0.8.3" regex = "1.5.4" +symphonia = "0.5.1" # Rust audio demuxer time = "0.2.26" uuid = { version = "0.8.2", features = ["v4"] } diff --git a/crates/service/storyteller_web/src/billing/internal_product_to_stripe_lookup_impl.rs b/crates/service/web/storyteller_web/src/billing/internal_product_to_stripe_lookup_impl.rs similarity index 100% rename from crates/service/storyteller_web/src/billing/internal_product_to_stripe_lookup_impl.rs rename to crates/service/web/storyteller_web/src/billing/internal_product_to_stripe_lookup_impl.rs diff --git a/crates/service/storyteller_web/src/billing/mod.rs b/crates/service/web/storyteller_web/src/billing/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/billing/mod.rs rename to crates/service/web/storyteller_web/src/billing/mod.rs diff --git a/crates/service/storyteller_web/src/billing/stripe_internal_subscription_product_lookup_impl.rs b/crates/service/web/storyteller_web/src/billing/stripe_internal_subscription_product_lookup_impl.rs similarity index 100% rename from crates/service/storyteller_web/src/billing/stripe_internal_subscription_product_lookup_impl.rs rename to crates/service/web/storyteller_web/src/billing/stripe_internal_subscription_product_lookup_impl.rs diff --git a/crates/service/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs b/crates/service/web/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs similarity index 100% rename from crates/service/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs rename to crates/service/web/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs diff --git a/crates/service/storyteller_web/src/configs/mod.rs b/crates/service/web/storyteller_web/src/configs/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/mod.rs rename to crates/service/web/storyteller_web/src/configs/mod.rs diff --git a/crates/service/storyteller_web/src/configs/plans/get_correct_plan_for_session.rs b/crates/service/web/storyteller_web/src/configs/plans/get_correct_plan_for_session.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/plans/get_correct_plan_for_session.rs rename to crates/service/web/storyteller_web/src/configs/plans/get_correct_plan_for_session.rs diff --git a/crates/service/storyteller_web/src/configs/plans/mod.rs b/crates/service/web/storyteller_web/src/configs/plans/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/plans/mod.rs rename to crates/service/web/storyteller_web/src/configs/plans/mod.rs diff --git a/crates/service/storyteller_web/src/configs/plans/plan.rs b/crates/service/web/storyteller_web/src/configs/plans/plan.rs similarity index 92% rename from crates/service/storyteller_web/src/configs/plans/plan.rs rename to crates/service/web/storyteller_web/src/configs/plans/plan.rs index 2eb80e3d78..1588733a5e 100644 --- a/crates/service/storyteller_web/src/configs/plans/plan.rs +++ b/crates/service/web/storyteller_web/src/configs/plans/plan.rs @@ -4,6 +4,8 @@ use crate::configs::plans::plan_category::PlanCategory; const TTS_DEFAULT_PRIORITY_LEVEL : u8 = 0; const TTS_DEFAULT_DURATION_SECONDS : i64 = 12; +const VC_DEFAULT_PRIORITY_LEVEL : u8 = 0; + const VC_DEFAULT_MAX_CONCURRENT_MODELS : u32 = 5; const VC_DEFAULT_PRE_RECORDED_TIME_LIMIT_SECONDS : i64 = 20; @@ -48,7 +50,11 @@ pub struct Plan { tts_can_upload_private_models: bool, tts_can_share_private_models: bool, - // ========== Features for Voice Conversion ========== + // ========== Features for Web Voice Conversion ========== + + web_vc_base_priority_level: u8, + + // ========== Features for Real Time Voice Conversion ========== vc_max_concurrent_models: u32, @@ -79,6 +85,7 @@ impl Plan { tts_can_generate_mp3: builder.tts_can_generate_mp3, tts_can_upload_private_models: builder.tts_can_upload_private_models, tts_can_share_private_models: builder.tts_can_share_private_models, + web_vc_base_priority_level: builder.web_vc_base_priority_level, vc_max_concurrent_models: builder.vc_max_concurrent_models, vc_pre_recorded_time_limit: builder.vc_pre_recorded_time_limit, vc_pre_recorded_time_is_unlimited: builder.vc_pre_recorded_time_is_unlimited, @@ -132,6 +139,10 @@ impl Plan { pub fn tts_max_duration_seconds(&self) -> i32 { self.tts_max_duration.num_seconds() as i32 } + + pub fn web_vc_base_priority_level(&self) -> u8 { + self.web_vc_base_priority_level + } } /// Builder pattern for Plans. @@ -153,7 +164,11 @@ pub struct PlanBuilder { tts_can_upload_private_models: bool, tts_can_share_private_models: bool, - // ========== Features for Voice Conversion ========== + // ========== Features for Web Voice Conversion ========== + + web_vc_base_priority_level: u8, + + // ========== Features for Real Time Voice Conversion ========== vc_max_concurrent_models: u32, @@ -188,7 +203,10 @@ impl PlanBuilder { tts_can_upload_private_models: false, tts_can_share_private_models : false, - // VC + // VC (WEB) + web_vc_base_priority_level: VC_DEFAULT_PRIORITY_LEVEL, + + // VC (REALTIME) vc_max_concurrent_models: VC_DEFAULT_MAX_CONCURRENT_MODELS, vc_pre_recorded_time_limit: Duration::seconds(VC_DEFAULT_PRE_RECORDED_TIME_LIMIT_SECONDS), vc_pre_recorded_time_is_unlimited: false, @@ -260,6 +278,11 @@ impl PlanBuilder { self } + pub fn web_vc_base_priority_level(mut self, value: u8) -> Self { + self.web_vc_base_priority_level = value; + self + } + pub fn vc_max_concurrent_models(mut self, value: u32) -> Self { self.vc_max_concurrent_models = value; self diff --git a/crates/service/storyteller_web/src/configs/plans/plan_category.rs b/crates/service/web/storyteller_web/src/configs/plans/plan_category.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/plans/plan_category.rs rename to crates/service/web/storyteller_web/src/configs/plans/plan_category.rs diff --git a/crates/service/storyteller_web/src/configs/plans/plan_list.rs b/crates/service/web/storyteller_web/src/configs/plans/plan_list.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/plans/plan_list.rs rename to crates/service/web/storyteller_web/src/configs/plans/plan_list.rs diff --git a/crates/service/storyteller_web/src/configs/static_api_tokens.rs b/crates/service/web/storyteller_web/src/configs/static_api_tokens.rs similarity index 100% rename from crates/service/storyteller_web/src/configs/static_api_tokens.rs rename to crates/service/web/storyteller_web/src/configs/static_api_tokens.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/create_api_token.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/delete_api_token.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/delete_api_token.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/delete_api_token.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/delete_api_token.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/edit_api_token.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/edit_api_token.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/edit_api_token.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/edit_api_token.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/list_api_tokens.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/api_tokens/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/api_tokens/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/api_tokens/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/assign_tts_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/create_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/create_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/create_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/create_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/get_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/get_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/get_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/get_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/list_tts_categories.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/list_tts_model_assigned_categories.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/list_tts_model_assigned_categories.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/list_tts_model_assigned_categories.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/list_tts_model_assigned_categories.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/categories/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/categories/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/categories/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/categories/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs similarity index 94% rename from crates/service/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs index 9fe9c3f31e..c4d2ced1ed 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/enqueue_generic_download.rs @@ -10,14 +10,14 @@ use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; -use database_queries::queries::generic_download::insert_generic_download_job::{Args, insert_generic_download_job}; +use database_queries::queries::generic_download::web::insert_generic_download_job::{Args, insert_generic_download_job}; use database_queries::tokens::Tokens; +use enums::core::visibility::Visibility; +use enums::workers::generic_download_type::GenericDownloadType; use http_server_common::request::get_request_ip::get_request_ip; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; use regex::Regex; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::generic_download_type::GenericDownloadType; use sqlx::error::DatabaseError; use sqlx::error::Error::Database; use sqlx::mysql::MySqlDatabaseError; @@ -35,7 +35,7 @@ pub struct EnqueueGenericDownloadRequest { /// Not all upload types will have an associated visibility, so supplying this is optional. /// If an upload type supports it and it is not supplied, assume a reasonable default or user /// setting. - creator_set_visibility: Option, + creator_set_visibility: Option, } #[derive(Serialize)] @@ -107,7 +107,7 @@ pub async fn enqueue_generic_download_handler( let download_url = request.download_url.trim().to_string(); let creator_set_visibility = request.creator_set_visibility - .unwrap_or(EntityVisibility::Public); + .unwrap_or(Visibility::Public); if let Err(reason) = validate_idempotency_token_format(&uuid) { return Err(EnqueueGenericDownloadError::BadInput(reason)); diff --git a/crates/service/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs similarity index 96% rename from crates/service/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs index 6f870f78f2..cddda3a3c3 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/get_generic_upload_job_status.rs @@ -11,7 +11,7 @@ use chrono::{DateTime, Utc}; use crate::AnyhowResult; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use database_queries::queries::generic_download::get_generic_download_job_status::{GenericDownloadJobStatus, get_generic_download_job_status}; +use database_queries::queries::generic_download::web::get_generic_download_job_status::{GenericDownloadJobStatus, get_generic_download_job_status}; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::{info, warn, log}; use r2d2_redis::redis::Commands; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/download_job/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/download_job/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/download_job/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/download_job/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/events/list_events.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/events/list_events.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/events/list_events.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/events/list_events.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/events/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/events/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/events/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/events/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/build_design_refresh_cookie.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/build_design_refresh_cookie.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/build_design_refresh_cookie.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/build_design_refresh_cookie.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/disable_design_refresh_flag_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/disable_design_refresh_flag_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/disable_design_refresh_flag_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/disable_design_refresh_flag_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/enable_design_refresh_flag_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/enable_design_refresh_flag_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/enable_design_refresh_flag_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/enable_design_refresh_flag_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/design_refresh_flag/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/get_cookie_domain.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/get_cookie_domain.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/get_cookie_domain.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/get_cookie_domain.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/flags/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/flags/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/flags/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/flags/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/default_redirect.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/default_redirect.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/default_redirect.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/default_redirect.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/demo_cookie.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/demo_cookie.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/demo_cookie.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/demo_cookie.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/disable_demo_mode_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/disable_demo_mode_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/disable_demo_mode_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/disable_demo_mode_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/enable_demo_mode_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/enable_demo_mode_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/enable_demo_mode_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/enable_demo_mode_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/investor_demo/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/investor_demo/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/investor_demo/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/leaderboard/get_leaderboard.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/leaderboard/get_leaderboard.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/leaderboard/get_leaderboard.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/leaderboard/get_leaderboard.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/leaderboard/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/leaderboard/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/leaderboard/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/leaderboard/mod.rs diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs new file mode 100644 index 0000000000..b6a30f44cd --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/drain_multipart_request.rs @@ -0,0 +1,57 @@ +use actix_multipart::Field; +use actix_multipart::Multipart; +use actix_web::web::BytesMut; +use container_common::anyhow_result::AnyhowResult; +use crate::http_server::web_utils::read_multipart_field_bytes::{checked_read_multipart_bytes, read_multipart_field_as_text}; +use futures::TryStreamExt; +use log::warn; + +pub struct UploadMediaRequest { + pub uuid_idempotency_token: Option, + pub file_name: Option, + pub file_bytes: Option, +} + +/// Pull common parts out of multipart media HTTP requests, typically for handling file uploads. +pub async fn drain_multipart_request(mut multipart_payload: Multipart) -> AnyhowResult { + let mut uuid_idempotency_token = None; + let mut file_bytes = None; + let mut file_name = None; + + while let Ok(Some(mut field)) = multipart_payload.try_next().await { + let mut field_name = None; + let mut field_filename = None; + + if let Some(content_disposition) = field.content_disposition() { + field_name = content_disposition.get_name() + .map(|s| s.to_string()); + field_filename = content_disposition.get_filename() // NB: Only used for the file bytes. + .map(|s| s.to_string()); + } + + match field_name.as_deref() { + Some("uuid_idempotency_token") => { + uuid_idempotency_token = read_multipart_field_as_text(&mut field).await + .map_err(|e| { + warn!("Error reading idempotency token: {:}", e); + e + })?; + }, + Some("file") => { + file_name = field_filename.clone(); + file_bytes = checked_read_multipart_bytes(&mut field).await + .map_err(|e| { + warn!("Error reading audio upload: {:}", e); + e + })?; + }, + _ => continue, + } + } + + Ok(UploadMediaRequest { + uuid_idempotency_token, + file_name, + file_bytes, + }) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/mod.rs new file mode 100644 index 0000000000..1c753941e4 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/mod.rs @@ -0,0 +1,2 @@ +pub mod drain_multipart_request; +pub mod upload_media; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_media.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_media.rs new file mode 100644 index 0000000000..130913e9bc --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_uploads/upload_media.rs @@ -0,0 +1,293 @@ +use actix_http::Error; +use actix_http::http::header; +use actix_multipart::Multipart; +use actix_web::HttpResponseBuilder; +use actix_web::cookie::Cookie; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::BytesMut; +use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use buckets::public::media_uploads::original_file::MediaUploadOriginalFilePath; +use config::bad_urls::is_bad_tts_model_download_url; +use crate::http_server::endpoints::media_uploads::drain_multipart_request::drain_multipart_request; +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::server_state::ServerState; +use crate::validations::model_uploads::validate_model_title; +use crate::validations::validate_idempotency_token_format::validate_idempotency_token_format; +use database_queries::payloads::media_upload_details::MediaUploadDetails; +use database_queries::queries::media_uploads::insert_media_upload::{Args, insert_media_upload}; +use enums::core::visibility::Visibility; +use enums::files::media_upload_type::MediaUploadType; +use files::hash::hash_bytes_sha2; +use files::mimetype::{get_mimetype_for_bytes, get_mimetype_for_bytes_or_default}; +use http_server_common::request::get_request_ip::get_request_ip; +use http_server_common::response::serialize_as_json_error::serialize_as_json_error; +use log::{info, warn, log}; +use media::decode_basic_audio_info::decode_basic_audio_info; +use regex::Regex; +use sqlx::error::DatabaseError; +use sqlx::error::Error::Database; +use sqlx::mysql::MySqlDatabaseError; +use std::fmt; +use std::io::{BufReader, Cursor}; +use std::sync::Arc; +use symphonia::core::formats::FormatOptions; +use symphonia::core::io::{MediaSourceStream, ReadOnlySource}; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; +use tokens::files::media_upload::MediaUploadToken; + +#[derive(Serialize)] +pub struct UploadMediaSuccessResponse { + pub success: bool, + pub upload_token: MediaUploadToken, +} + +#[derive(Debug, Serialize)] +pub enum UploadMediaError { + BadInput(String), + NotAuthorized, + MustBeLoggedIn, + ServerError, + RateLimited, +} + +impl ResponseError for UploadMediaError { + fn status_code(&self) -> StatusCode { + match *self { + UploadMediaError::BadInput(_) => StatusCode::BAD_REQUEST, + UploadMediaError::NotAuthorized => StatusCode::UNAUTHORIZED, + UploadMediaError::MustBeLoggedIn => StatusCode::UNAUTHORIZED, + UploadMediaError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + UploadMediaError::RateLimited => StatusCode::TOO_MANY_REQUESTS, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for UploadMediaError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn upload_media_handler( + http_request: HttpRequest, + server_state: web::Data>, + mut multipart_payload: Multipart, +) -> Result { + + // ==================== READ SESSION ==================== // + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session(&http_request, &server_state.mysql_pool) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + UploadMediaError::ServerError + })?; + + // ==================== RATE LIMIT ==================== // + + let rate_limiter = match maybe_user_session { + None => &server_state.redis_rate_limiters.logged_out, + Some(ref user) => { + if user.is_banned { + return Err(UploadMediaError::NotAuthorized); + } + &server_state.redis_rate_limiters.logged_in + }, + }; + + if let Err(_err) = rate_limiter.rate_limit_request(&http_request) { + return Err(UploadMediaError::RateLimited); + } + + if let Err(_err) = server_state.redis_rate_limiters.model_upload.rate_limit_request(&http_request) { + return Err(UploadMediaError::RateLimited); + } + + // ==================== READ MULTIPART REQUEST ==================== // + + let upload_media_request = drain_multipart_request(multipart_payload) + .await + .map_err(|e| { + // TODO: Error handling could be nicer. + UploadMediaError::BadInput("bad request".to_string()) + })?; + + + let uuid_idempotency_token = upload_media_request.uuid_idempotency_token + .ok_or(UploadMediaError::BadInput("no uuid".to_string()))?; + + if let Err(reason) = validate_idempotency_token_format(&uuid_idempotency_token) { + return Err(UploadMediaError::BadInput(reason)); + } + + let creator_set_visibility = maybe_user_session + .as_ref() + .map(|user_session| user_session.preferred_tts_result_visibility) // TODO: We need a new type of visibility control. + .unwrap_or(Visibility::default()); + + let ip_address = get_request_ip(&http_request); + + let maybe_user_token = maybe_user_session + .map(|session| session.get_strongly_typed_user_token()); + + let token = MediaUploadToken::generate(); + + let maybe_mimetype = upload_media_request.file_bytes + .as_ref() + .map(|bytes| get_mimetype_for_bytes(bytes.as_ref())) + .flatten(); + + let maybe_hash = upload_media_request.file_bytes + .as_ref() + .map(|bytes| hash_bytes_sha2(bytes.as_ref())) + .transpose() + .map_err(|io_error| { + warn!("Problem hashing bytes: {:?}", io_error); + return UploadMediaError::ServerError; + })?; + + let hash = match maybe_hash { + None => return Err(UploadMediaError::BadInput("invalid file".to_string())), + Some(hash) => hash, + }; + + let bytes = match upload_media_request.file_bytes { + None => return Err(UploadMediaError::BadInput("invalid file".to_string())), + Some(bytes) => bytes, + }; + + let file_size_bytes = bytes.len(); + + let mut maybe_duration_millis = None; + let mut maybe_codec_name = None; + let mut media_upload_type = None; + + if let Some(mimetype) = maybe_mimetype.as_deref() { + // NB: .aiff (audio/aiff) isn't supported by Symphonia: + // It contains uncompressed PCM-encoded audio similar to wav. + // See: https://github.com/pdeljanov/Symphonia/issues/75 + // NB: The following formats are not supported by Symphonia and + // do not have any open issues filed. They may simply be too old: + // - .wma (audio/x-ms-wma) + // - .avi (video/x-msvideo) + media_upload_type = match mimetype { + // Audio + "audio/aac" /* .aac */ => Some(MediaUploadType::Audio), + "audio/m4a" /* .m4a */ => Some(MediaUploadType::Audio), + "audio/mpeg" /* .mp3 */ => Some(MediaUploadType::Audio), + "audio/ogg" /* .ogg */ => Some(MediaUploadType::Audio), + "audio/x-flac" /* .flac */ => Some(MediaUploadType::Audio), + "audio/x-wav" /* .wav */ => Some(MediaUploadType::Audio), + // Video + "video/mp4" /* .mp4 */ => Some(MediaUploadType::Video), + "video/quicktime" /* .mov */ => Some(MediaUploadType::Video), + "video/webm" /* .webm */ => Some(MediaUploadType::Video), + _ => None, + }; + + if media_upload_type.is_some() { + let basic_info = decode_basic_audio_info( + bytes.as_ref(), + Some(mimetype), + None + ).map_err(|e| { + warn!("file decoding error: {:?}", e); + UploadMediaError::BadInput("could not decode file".to_string()) + })?; + + maybe_duration_millis = basic_info.duration_millis; + maybe_codec_name = basic_info.codec_name; + } + } + + let media_upload_type = match media_upload_type { + Some(m) => m, + None => { + warn!("Invalid mimetype: {:?}", maybe_mimetype); + return Err(UploadMediaError::BadInput(format!("Bad mimetype: {:?}", maybe_mimetype))) + }, + }; + + // TODO: Clean up code + let mime_type = match maybe_mimetype { + Some(m) => m, + None => { + warn!("Missing mimetype!"); + return Err(UploadMediaError::BadInput("Missing mimetype".to_string())); + }, + }; + + let public_upload_path = MediaUploadOriginalFilePath::generate_new(); + + info!("Uploading media to bucket path: {}", public_upload_path.get_full_object_path_str()); + + server_state.public_bucket_client.upload_file_with_content_type( + public_upload_path.get_full_object_path_str(), + bytes.as_ref(), + &mime_type) + .await + .map_err(|e| { + warn!("Upload media bytes to bucket error: {:?}", e); + UploadMediaError::ServerError + })?; + + let record_id = insert_media_upload(Args { + token: &token, + uuid_idempotency_token: &uuid_idempotency_token, + media_type: media_upload_type, + 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, + maybe_original_mime_type: maybe_mimetype, + maybe_original_audio_encoding: maybe_codec_name.as_deref(), + maybe_original_video_encoding: None, + maybe_original_frame_width: None, + maybe_original_frame_height: None, + checksum_sha2: &hash, + public_upload_path: &public_upload_path, + extra_file_modification_info: MediaUploadDetails {}, // TODO + maybe_creator_user_token: maybe_user_token.as_ref(), + maybe_creator_anonymous_visitor_token: None, + creator_ip_address: &ip_address, + creator_set_visibility, + maybe_creator_synthetic_id: None, // TODO: Don't forget about this. + mysql_pool: &server_state.mysql_pool, + }) + .await + .map_err(|err| { + warn!("New generic download creation DB error: {:?}", err); + UploadMediaError::ServerError + })?; + + info!("new media upload id: {}", record_id); + + server_state.firehose_publisher.publish_media_uploaded( + maybe_user_token.as_ref(), + &token) + .await + .map_err(|e| { + warn!("error publishing event: {:?}", e); + UploadMediaError::ServerError + })?; + + let response = UploadMediaSuccessResponse { + success: true, + upload_token: token, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| UploadMediaError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/alpha_cookie.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/alpha_cookie.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/alpha_cookie.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/alpha_cookie.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/default_route_404.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/default_route_404.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/default_route_404.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/default_route_404.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/detect_locale_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/enable_alpha_easy_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/enable_alpha_easy_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/enable_alpha_easy_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/enable_alpha_easy_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/enable_alpha_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/enable_alpha_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/enable_alpha_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/enable_alpha_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/misc/root_index.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/misc/root_index.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/misc/root_index.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/misc/root_index.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs similarity index 85% rename from crates/service/storyteller_web/src/http_server/endpoints/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs index 40244a9277..ed3ae9a1b4 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/mod.rs @@ -5,6 +5,7 @@ pub mod events; pub mod flags; pub mod investor_demo; pub mod leaderboard; +pub mod media_uploads; pub mod misc; pub mod moderation; pub mod service; @@ -13,4 +14,5 @@ pub mod tts; pub mod twitch; pub mod vocoders; pub mod voice_clone_requests; +pub mod voice_conversion; pub mod w2l; \ No newline at end of file diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/approval/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/approval/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/approval/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/approval/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/approval/pending_w2l_templates.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/approval/pending_w2l_templates.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/approval/pending_w2l_templates.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/approval/pending_w2l_templates.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/delete_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/edit_category.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/list_tts_categories_for_moderation.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/categories/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/categories/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/add_ip_ban.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/delete_ip_ban.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/get_ip_ban.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/list_ip_bans.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/ip_bans/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/ip_bans/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/get_tts_inference_queue_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/get_tts_inference_queue_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/get_tts_inference_queue_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/get_tts_inference_queue_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/get_w2l_inference_queue_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/get_w2l_inference_queue_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/get_w2l_inference_queue_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/get_w2l_inference_queue_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/kill_tts_inference_jobs.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/kill_tts_inference_jobs.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/kill_tts_inference_jobs.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/kill_tts_inference_jobs.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/jobs/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/jobs/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/get_on_prem_worker_stats.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/get_on_prem_worker_stats.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/get_on_prem_worker_stats.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/get_on_prem_worker_stats.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/get_voice_count_stats.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/get_voice_count_stats.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/get_voice_count_stats.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/get_voice_count_stats.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/stats/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/stats/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/ban_user.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/list_banned_users.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/list_banned_users.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/list_banned_users.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/list_banned_users.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_bans/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_bans/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_roles.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/list_staff.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_roles/set_user_role.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/users/list_users.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/users/list_users.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/users/list_users.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/users/list_users.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/moderation/users/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/users/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/moderation/users/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/moderation/users/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/service/health_check_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/service/health_check_handler.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/service/health_check_handler.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/service/health_check_handler.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/service/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/service/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/service/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/service/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/app_model_downloads.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_model_downloads.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/app_model_downloads.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_model_downloads.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/app_news.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_news.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/app_news.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_news.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/app_plans.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_plans.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/app_plans.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/app_plans.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/stubs/post_app_analytics.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/stubs/post_app_analytics.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/stubs/post_app_analytics.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/stubs/post_app_analytics.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/delete_tts_model.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_model.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/delete_tts_model.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/delete_tts_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs similarity index 98% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs index 98118e7c4d..80b3b4342e 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_model.rs @@ -11,11 +11,11 @@ use container_common::i18n::supported_languages_for_models::get_canonicalized_la use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::column_types::vocoder_type::VocoderType; use database_queries::queries::tts::tts_models::edit_tts_model_details::{edit_tts_model_details_as_author, edit_tts_model_details_as_mod}; use database_queries::queries::tts::tts_models::edit_tts_model_moderator_details::edit_tts_model_moderator_details; use database_queries::queries::tts::tts_models::get_tts_model::get_tts_model_by_token; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use language_tags::LanguageTag; use log::{info, warn, error}; @@ -193,7 +193,7 @@ pub async fn edit_tts_model_handler( let mut ietf_language_tag = None; let mut ietf_primary_language_subtag = None; - let mut creator_set_visibility = RecordVisibility::Public; + let mut creator_set_visibility = Visibility::Public; let mut maybe_default_pretrained_vocoder = model_record.maybe_default_pretrained_vocoder .clone(); @@ -248,7 +248,7 @@ pub async fn edit_tts_model_handler( .unwrap_or(DEFAULT_IETF_PRIMARY_LANGUAGE_SUBTAG.to_string()); if let Some(visibility) = request.creator_set_visibility.as_deref() { - creator_set_visibility = RecordVisibility::from_str(visibility) + creator_set_visibility = Visibility::from_str(visibility) .map_err(|_| EditTtsModelError::BadInput("bad record visibility".to_string()))?; } diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs similarity index 88% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs index 9a64bc755c..2375c4f890 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/edit_tts_result.rs @@ -1,23 +1,19 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::web::{Path, Json}; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::web::Path; +use actix_web::{web, HttpResponse, HttpRequest}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::tts::tts_results::query_tts_result::select_tts_result_by_token; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; +use log::warn; use std::fmt; use std::sync::Arc; @@ -107,7 +103,7 @@ pub async fn edit_tts_inference_result_handler( &server_state.mysql_pool ).await; - let mut inference_result = match inference_result_query_result { + let inference_result = match inference_result_query_result { Err(e) => { warn!("query error: {:?}", e); return Err(EditTtsResultError::ServerError); @@ -129,10 +125,10 @@ pub async fn edit_tts_inference_result_handler( // Author + Mod fields. // These fields must be present on all requests. - let mut creator_set_visibility = RecordVisibility::Public; + let mut creator_set_visibility = Visibility::Public; if let Some(visibility) = request.creator_set_visibility.as_deref() { - creator_set_visibility = RecordVisibility::from_str(visibility) + creator_set_visibility = Visibility::from_str(visibility) .map_err(|_| EditTtsResultError::BadInput("bad record visibility".to_string()))?; } diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs similarity index 92% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs index 53a4523622..3eb125ee8e 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_infer_tts.rs @@ -1,29 +1,28 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::{web, HttpResponse, HttpRequest}; +use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session; use crate::http_server::endpoints::investor_demo::demo_cookie::request_has_demo_cookie; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use crate::validations::model_uploads::validate_model_title; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::tts::tts_inference_jobs::insert_tts_inference_job::TtsInferenceJobInsertBuilder; use database_queries::tokens::Tokens; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_api_token::get_request_api_token; use http_server_common::request::get_request_header_optional::get_request_header_optional; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; +use log::{info, warn}; use r2d2_redis::redis::Commands; use redis_common::redis_keys::RedisKeys; -use regex::Regex; use std::fmt; use std::sync::Arc; -use tts_common::priority::{FAKEYOU_LOGGED_IN_PRIORITY_LEVEL, FAKEYOU_ANONYMOUS_PRIORITY_LEVEL, FAKEYOU_INVESTOR_PRIORITY_LEVEL, FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL}; +use tts_common::priority::{FAKEYOU_INVESTOR_PRIORITY_LEVEL, FAKEYOU_DEFAULT_VALID_API_TOKEN_PRIORITY_LEVEL}; use user_input_common::check_for_slurs::contains_slurs; -use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session; // TODO: Temporary for investor demo const STORYTELLER_DEMO_COOKIE_NAME : &'static str = "storyteller_demo"; @@ -37,7 +36,7 @@ pub struct InferTtsRequest { uuid_idempotency_token: String, tts_model_token: String, inference_text: String, - creator_set_visibility: Option, + creator_set_visibility: Option, is_storyteller_demo: Option, } @@ -238,17 +237,17 @@ pub async fn infer_tts_handler( let ip_address = get_request_ip(&http_request); - let maybe_user_preferred_visibility : Option = maybe_user_session + let maybe_user_preferred_visibility : Option = maybe_user_session .as_ref() .map(|user_session| user_session.preferences.preferred_tts_result_visibility); let set_visibility = request.creator_set_visibility .or(maybe_user_preferred_visibility) - .unwrap_or(RecordVisibility::Public); + .unwrap_or(Visibility::Public); // This token is returned to the client. let job_token = Tokens::new_tts_inference_job() - .map_err(|e| { + .map_err(|_e| { warn!("Error creating token"); InferTtsError::ServerError })?; @@ -291,7 +290,7 @@ pub async fn infer_tts_handler( }; let body = serde_json::to_string(&response) - .map_err(|e| InferTtsError::ServerError)?; + .map_err(|_e| InferTtsError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs similarity index 90% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs index ff8fcf6ba0..400e324316 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/enqueue_upload_tts_model.rs @@ -1,21 +1,20 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::{web, HttpResponse, HttpRequest}; use config::bad_urls::is_bad_tts_model_download_url; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; use database_queries::tokens::Tokens; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::error::DatabaseError; +use log::{info, warn}; use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; use std::fmt; use std::sync::Arc; @@ -31,24 +30,13 @@ pub enum TtsModelType { Talknet, } -#[deprecated(note = "Use `RecordVisibility` instead!")] -#[derive(Deserialize)] -pub enum CreatorSetVisibility { - /// public - Public, - /// hidden - Hidden, - /// private - Private, -} - #[derive(Deserialize)] pub struct UploadTtsModelRequest { idempotency_token: String, title: String, download_url: String, tts_model_type: Option, - creator_set_visibility: Option, + creator_set_visibility: Option, } #[derive(Serialize)] @@ -151,7 +139,7 @@ pub async fn upload_tts_model_handler( // This token is returned to the client. let job_token = Tokens::new_tts_model_upload_job() - .map_err(|e| { + .map_err(|_e| { warn!("Error creating token"); UploadTtsModelError::ServerError })?; @@ -193,7 +181,7 @@ SET // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) match err { Database(err) => { - let maybe_code = err.code().map(|c| c.into_owned()); + let _maybe_code = err.code().map(|c| c.into_owned()); /*match maybe_code.as_deref() { Some("23000") => { if err.message().contains("username") { @@ -226,7 +214,7 @@ SET }; let body = serde_json::to_string(&response) - .map_err(|e| UploadTtsModelError::ServerError)?; + .map_err(|_e| UploadTtsModelError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_pending_tts_inference_job_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_pending_tts_inference_job_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_pending_tts_inference_job_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_pending_tts_inference_job_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_inference_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_inference_job_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_inference_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_inference_job_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs similarity index 98% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs index 89019199df..84d491bd81 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model.rs @@ -9,9 +9,9 @@ use actix_web::web::Path; use actix_web::{web, HttpResponse, HttpRequest}; use chrono::{DateTime, Utc}; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::column_types::vocoder_type::VocoderType; use database_queries::queries::tts::tts_models::get_tts_model::get_tts_model_by_token_using_connection; +use enums::core::visibility::Visibility; use http_server_common::response::serialize_as_json_error::serialize_as_json_error; use log::warn; use std::fmt; @@ -80,7 +80,7 @@ pub struct TtsModelInfo { pub maybe_suggested_unique_bot_command: Option, - pub creator_set_visibility: RecordVisibility, + pub creator_set_visibility: Visibility, pub is_locked_from_use: bool, pub is_locked_from_user_modification: bool, diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_model_use_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model_use_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_model_use_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_model_use_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_upload_model_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_upload_model_job_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/get_tts_upload_model_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/get_tts_upload_model_job_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/list_tts_models.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_tts_models.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/list_tts_models.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_tts_models.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_inference_results.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/list_user_tts_models.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/tts/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/tts/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/tts/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/tts/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/create_event_rule.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/create_event_rule.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/create_event_rule.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/create_event_rule.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/delete_event_rule.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/delete_event_rule.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/delete_event_rule.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/delete_event_rule.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/edit_event_rule.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/edit_event_rule.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/edit_event_rule.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/edit_event_rule.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/get_event_rule.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/get_event_rule.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/get_event_rule.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/get_event_rule.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/list_event_rules_for_user.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/list_event_rules_for_user.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/list_event_rules_for_user.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/list_event_rules_for_user.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/reorder_twitch_event_rules.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/reorder_twitch_event_rules.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/reorder_twitch_event_rules.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/reorder_twitch_event_rules.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_match_predicate.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_match_predicate.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_match_predicate.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_match_predicate.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_response.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_response.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_response.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/event_rules/validations/validate_event_response.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/check_oauth_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/check_oauth_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/check_oauth_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/check_oauth_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_json.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_json.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_json.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_json.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_redirect.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_redirect.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_redirect.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_begin_redirect.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_end.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_end.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_end.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/twitch/oauth/oauth_end.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/delete_vocoder.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/delete_vocoder.rs diff --git a/crates/service/storyteller_web/src/subscriptions/subscription_plan_list.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/edit_vocoder.rs similarity index 100% rename from crates/service/storyteller_web/src/subscriptions/subscription_plan_list.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/edit_vocoder.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs similarity index 97% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs index d71057def2..1a4de7fd3e 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/get_vocoder.rs @@ -11,9 +11,9 @@ use chrono::{DateTime, Utc}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use database_queries::queries::vocoder::get_vocoder_model::get_vocoder_model_by_token; +use enums::core::visibility::Visibility; +use enums::ml::vocoder_type::VocoderType; use log::warn; -use reusable_types::entity_visibility::EntityVisibility; -use reusable_types::vocoder_type::VocoderType; use std::fmt; use std::sync::Arc; @@ -49,7 +49,7 @@ pub struct Vocoder { pub creator_display_name: String, pub creator_gravatar_hash: String, - pub creator_set_visibility: EntityVisibility, + pub creator_set_visibility: Visibility, pub created_at: DateTime, pub updated_at: DateTime, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_user_vocoders.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs similarity index 99% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs index 6cebb229c1..933b0d22f6 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/list_vocoders.rs @@ -10,8 +10,8 @@ use chrono::{DateTime, Utc}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use database_queries::queries::vocoder::list_vocoder_models::{list_vocoder_models, VocoderModelListItem}; +use enums::ml::vocoder_type::VocoderType; use log::warn; -use reusable_types::vocoder_type::VocoderType; use std::fmt; use std::sync::Arc; diff --git a/crates/service/storyteller_web/src/http_server/endpoints/vocoders/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/vocoders/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/vocoders/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/check_if_voice_clone_request_submitted.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/create_voice_clone_request.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/voice_clone_requests/mod.rs diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/enqueue_voice_conversion_inference.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/enqueue_voice_conversion_inference.rs new file mode 100644 index 0000000000..37aa3a449f --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/enqueue_voice_conversion_inference.rs @@ -0,0 +1,271 @@ +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::{web, HttpResponse, HttpRequest}; +use crate::configs::plans::get_correct_plan_for_session::get_correct_plan_for_session; +use crate::http_server::endpoints::investor_demo::demo_cookie::request_has_demo_cookie; +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::server_state::ServerState; +use database_queries::queries::generic_inference::web::insert_generic_inference_job::{Args, insert_generic_inference_job}; +use enums::core::visibility::Visibility; +use enums::workers::generic_inference_type::GenericInferenceType; +use http_server_common::request::get_request_header_optional::get_request_header_optional; +use http_server_common::request::get_request_ip::get_request_ip; +use log::{info, warn}; +use r2d2_redis::redis::Commands; +use redis_common::redis_keys::RedisKeys; +use std::fmt; +use std::sync::Arc; +use database_queries::payloads::generic_inference_args::{GenericInferenceArgs, PolymorphicInferenceArgs}; +use tokens::files::media_upload::MediaUploadToken; +use tokens::jobs::inference::InferenceJobToken; +use tokens::users::user::UserToken; +use tokens::voice_conversion::model::VoiceConversionModelToken; +use tts_common::priority::FAKEYOU_INVESTOR_PRIORITY_LEVEL; + +/// 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 : &'static str = "enable_debug_mode"; + +#[derive(Deserialize)] +pub struct EnqueueVoiceConversionInferenceRequest { + uuid_idempotency_token: String, + voice_conversion_model_token: String, + source_media_token: String, + creator_set_visibility: Option, + is_storyteller_demo: Option, +} + +#[derive(Serialize)] +pub struct EnqueueVoiceConversionInferenceSuccessResponse { + pub success: bool, + pub inference_job_token: InferenceJobToken, +} + +#[derive(Debug)] +pub enum EnqueueVoiceConversionInferenceError { + BadInput(String), + NotAuthorized, + ServerError, + RateLimited, +} + +impl ResponseError for EnqueueVoiceConversionInferenceError { + fn status_code(&self) -> StatusCode { + match *self { + EnqueueVoiceConversionInferenceError::BadInput(_) => StatusCode::BAD_REQUEST, + EnqueueVoiceConversionInferenceError::NotAuthorized => StatusCode::UNAUTHORIZED, + EnqueueVoiceConversionInferenceError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + EnqueueVoiceConversionInferenceError::RateLimited => StatusCode::TOO_MANY_REQUESTS, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + EnqueueVoiceConversionInferenceError::BadInput(reason) => reason.to_string(), + EnqueueVoiceConversionInferenceError::NotAuthorized => "unauthorized".to_string(), + EnqueueVoiceConversionInferenceError::ServerError => "server error".to_string(), + EnqueueVoiceConversionInferenceError::RateLimited => "rate limited".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for EnqueueVoiceConversionInferenceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub async fn enqueue_voice_conversion_inference_handler( + http_request: HttpRequest, + request: web::Json, + server_state: web::Data>) -> Result +{ + let mut maybe_user_token : Option = None; + let mut priority_level ; + let disable_rate_limiter = false; // NB: Careful! + + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await + .map_err(|err| { + warn!("MySql pool error: {:?}", err); + EnqueueVoiceConversionInferenceError::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); + EnqueueVoiceConversionInferenceError::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()); + + priority_level = plan.web_vc_base_priority_level(); + + // ==================== INVESTOR PRIORITY ==================== // + + // TODO/TEMP: Give investors even more priority + let mut is_investor = false; + + { + // TODO/TEMP: The storyteller.io website's AJAX calls will set this. + // This is just for the YCombinator demo. + match request.is_storyteller_demo { + Some(true) => { + is_investor = true; + }, + _ => {}, + }; + + // TODO/TEMP: The storyteller.io website will redirect and establish this cookie. + // This is just for the YCombinator demo. + if request_has_demo_cookie(&http_request) { + is_investor = true; + } + + if is_investor { + priority_level = FAKEYOU_INVESTOR_PRIORITY_LEVEL; + } + } + + // ==================== DEBUG MODE ==================== // + + let is_debug_request = get_request_header_optional(&http_request, DEBUG_HEADER_NAME) + .is_some(); + + // ==================== RATE LIMIT ==================== // + + if !disable_rate_limiter { + let mut rate_limiter = match maybe_user_session { + None => &server_state.redis_rate_limiters.logged_out, + Some(ref user) => { + if user.role.is_banned { + return Err(EnqueueVoiceConversionInferenceError::NotAuthorized); + } + &server_state.redis_rate_limiters.logged_in + }, + }; + + // TODO/TEMP + if is_investor { + rate_limiter = &server_state.redis_rate_limiters.logged_in; + } + + if let Err(_err) = rate_limiter.rate_limit_request(&http_request) { + return Err(EnqueueVoiceConversionInferenceError::RateLimited); + } + } + + // ==================== CHECK AND ENQUEUE VOICE CONVERSION ==================== // + + // TODO(bt): CHECK DATABASE! + let model_token = request.voice_conversion_model_token.to_string(); + let media_token = request.source_media_token.to_string(); + + let mut redis = server_state.redis_pool + .get() + .map_err(|e| { + warn!("redis error: {:?}", e); + EnqueueVoiceConversionInferenceError::ServerError + })?; + + let redis_count_key = RedisKeys::web_vc_model_usage_count(&model_token); + + redis.incr(&redis_count_key, 1) + .map_err(|e| { + warn!("redis error: {:?}", e); + EnqueueVoiceConversionInferenceError::ServerError + })?; + + let ip_address = get_request_ip(&http_request); + + let maybe_user_preferred_visibility : Option = maybe_user_session + .as_ref() + .map(|user_session| user_session.preferences.preferred_tts_result_visibility); // TODO: New setting for web-vc + + let set_visibility = request.creator_set_visibility + .or(maybe_user_preferred_visibility) + .unwrap_or(Visibility::Public); + + let job_token = InferenceJobToken::generate(); + + info!("Creating voice conversion inference job record..."); + + + // TODO TODO TODO TODO --- InferenceArgs. ! This is where the media token will live. + // - Also maybe move reusable_types stuff to `_types` as a manner of cleanup + // - Also figure out which serializable types are giving the DB trouble. Wasn't this solved? + // I don't want to call to_str, as_str, etc. everywhere. + // - INFERENCE JOB(1) -> Voice Conversion (w/ Docker sidecar) + // - INFERENCE JOB(2) -> Get it working with TTS too, and update the frontend + + let query_result = insert_generic_inference_job(Args { + job_token: &job_token, + uuid_idempotency_token: &request.uuid_idempotency_token, + inference_type: GenericInferenceType::VoiceConversion, + maybe_inference_args: Some(GenericInferenceArgs { + inference_type: Some(GenericInferenceType::VoiceConversion), + args: Some(PolymorphicInferenceArgs::VoiceConversionInferenceArgs { + model_token: Some(VoiceConversionModelToken::new_from_str(&model_token)), + maybe_media_token: Some(MediaUploadToken::new_from_str(&media_token)), + }), + }), + maybe_raw_inference_text: None, + maybe_model_token: Some(model_token), + maybe_creator_user_token: maybe_user_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: set_visibility, + priority_level, + is_debug_request, + mysql_pool: &server_state.mysql_pool, + }).await; + + match query_result { + Ok(_) => {}, + Err(err) => { + warn!("New generic inference job creation DB error: {:?}", err); + return Err(EnqueueVoiceConversionInferenceError::ServerError); + } + } + + server_state.firehose_publisher.enqueue_vc_inference( + maybe_user_token.as_ref(), + &job_token) + .await + .map_err(|e| { + warn!("error publishing event: {:?}", e); + EnqueueVoiceConversionInferenceError::ServerError + })?; + + let response = EnqueueVoiceConversionInferenceSuccessResponse { + success: true, + inference_job_token: job_token, + }; + + let body = serde_json::to_string(&response) + .map_err(|_e| EnqueueVoiceConversionInferenceError::ServerError)?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/get_voice_conversion_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/get_voice_conversion_result.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/list_user_voice_conversion_results.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/list_user_voice_conversion_results.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/mod.rs new file mode 100644 index 0000000000..9e4478016b --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/inference/mod.rs @@ -0,0 +1,3 @@ +pub mod enqueue_voice_conversion_inference; +pub mod get_voice_conversion_result; +pub mod list_user_voice_conversion_results; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/mod.rs new file mode 100644 index 0000000000..1cd77f670e --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/mod.rs @@ -0,0 +1,2 @@ +pub mod inference; +pub mod models; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/models/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/voice_conversion/models/mod.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/delete_w2l_template.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs similarity index 88% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs index fe63144ee4..78f1ccb5b6 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_result.rs @@ -1,23 +1,19 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::web::{Path, Json}; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::web::Path; +use actix_web::{web, HttpResponse, HttpRequest}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::w2l::w2l_results::query_w2l_result::select_w2l_result_by_token; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::MySqlPool; -use sqlx::error::DatabaseError; -use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; +use log::warn; use std::fmt; use std::sync::Arc; @@ -107,7 +103,7 @@ pub async fn edit_w2l_inference_result_handler( &server_state.mysql_pool ).await; - let mut inference_result = match inference_result_query_result { + let inference_result = match inference_result_query_result { Err(e) => { warn!("query error: {:?}", e); return Err(EditW2lResultError::ServerError); @@ -129,10 +125,10 @@ pub async fn edit_w2l_inference_result_handler( // Author + Mod fields. // These fields must be present on all requests. - let mut creator_set_visibility = RecordVisibility::Public; + let mut creator_set_visibility = Visibility::Public; if let Some(visibility) = request.creator_set_visibility.as_deref() { - creator_set_visibility = RecordVisibility::from_str(visibility) + creator_set_visibility = Visibility::from_str(visibility) .map_err(|_| EditW2lResultError::BadInput("bad record visibility".to_string()))?; } diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs similarity index 97% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs index e60c7172ba..714b8c80a9 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/edit_w2l_template.rs @@ -10,8 +10,8 @@ use actix_web::{web, HttpResponse, HttpRequest}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::http_server::web_utils::response_success_helpers::simple_json_success; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::queries::w2l::w2l_templates::get_w2l_template::select_w2l_template_by_token; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; use log::{info, warn}; use sqlx::MySqlPool; @@ -145,7 +145,7 @@ pub async fn edit_w2l_template_handler( let mut title = None; let mut description_markdown = None; let mut description_html = None; - let mut creator_set_visibility = RecordVisibility::Public; + let mut creator_set_visibility = Visibility::Public; if let Some(payload) = request.title.as_deref() { if contains_slurs(payload) { @@ -168,7 +168,7 @@ pub async fn edit_w2l_template_handler( } if let Some(visibility) = request.creator_set_visibility.as_deref() { - creator_set_visibility = RecordVisibility::from_str(visibility) + creator_set_visibility = Visibility::from_str(visibility) .map_err(|_| EditW2lTemplateError::BadInput("bad record visibility".to_string()))?; } diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs similarity index 88% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs index d0671c948b..3d58c2bd0f 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l.rs @@ -1,22 +1,19 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::{web, HttpResponse, HttpRequest}; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use crate::validations::model_uploads::validate_model_title; -use database_queries::column_types::record_visibility::RecordVisibility; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; +use log::{info, warn}; use r2d2_redis::redis::Commands; use redis_common::redis_keys::RedisKeys; -use regex::Regex; -use sqlx::error::DatabaseError; use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; use std::fmt; use std::sync::Arc; @@ -24,7 +21,7 @@ use std::sync::Arc; pub struct InferW2lRequest { w2l_template_token: Option, tts_inference_result_token: Option, - creator_set_visibility: Option, + creator_set_visibility: Option, } #[derive(Serialize)] @@ -93,7 +90,7 @@ pub async fn infer_w2l_handler( // return Err(InferW2lError::RateLimited); //} - let mut maybe_user_token : Option = maybe_session + let maybe_user_token : Option = maybe_session .as_ref() .map(|user_session| user_session.user_token.to_string()); @@ -176,7 +173,7 @@ SET // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) match err { Database(err) => { - let maybe_code = err.code().map(|c| c.into_owned()); + let _maybe_code = err.code().map(|c| c.into_owned()); /*match maybe_code.as_deref() { Some("23000") => { if err.message().contains("username") { @@ -201,7 +198,7 @@ SET }; let body = serde_json::to_string(&response) - .map_err(|e| InferW2lError::ServerError)?; + .map_err(|_e| InferW2lError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs similarity index 90% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs index dc4f8eb91a..489fe50ac0 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_infer_w2l_with_uploads.rs @@ -1,23 +1,23 @@ -use actix_http::http::header; -use actix_multipart::{Multipart, Field}; -use actix_web::HttpResponseBuilder; -use actix_web::body::ResponseBody::Body; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +use actix_multipart::Multipart; use actix_web::http::StatusCode; -use actix_web::http::header::ContentDisposition; -use actix_web::web::{Data, Json, BytesMut}; -use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer, HttpRequest, Either, ResponseError}; +use actix_web::web::BytesMut; +use actix_web::{web, HttpResponse, HttpRequest, ResponseError}; use anyhow::anyhow; +use buckets::util::hash_to_bucket_path_string::hash_to_bucket_path_string; use container_common::anyhow_result::AnyhowResult; use container_common::token::random_uuid::generate_random_uuid; use crate::http_server::web_utils::read_multipart_field_bytes::checked_read_multipart_bytes; -use crate::http_server::web_utils::read_multipart_field_bytes::read_multipart_field_as_boolean; use crate::http_server::web_utils::read_multipart_field_bytes::read_multipart_field_as_text; -use crate::http_server::web_utils::read_multipart_field_bytes::read_multipart_field_bytes; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; -use database_queries::column_types::record_visibility::RecordVisibility; use database_queries::tokens::Tokens; -use futures::{StreamExt, TryStreamExt}; +use enums::core::visibility::Visibility; +use futures::TryStreamExt; use http_server_common::request::get_request_ip::get_request_ip; use log::{warn, info}; use r2d2_redis::redis::Commands; @@ -25,9 +25,7 @@ use redis_common::redis_keys::RedisKeys; use sqlx::MySqlPool; use sqlx::error::Error::Database; use std::fmt; -use std::io::Write; use std::sync::Arc; -use storage_buckets_common::bucket_paths::hash_to_bucket_path; const BUCKET_AUDIO_FILE_NAME : &'static str = "input_audio_file"; const BUCKET_IMAGE_FILE_NAME: &'static str = "input_image_file"; @@ -127,16 +125,16 @@ pub async fn enqueue_infer_w2l_with_uploads( // ==================== SESSION DETAILS ==================== // - let mut maybe_user_token : Option = maybe_session + let maybe_user_token : Option = maybe_session .as_ref() .map(|user_session| user_session.user_token.to_string()); - let maybe_user_preferred_visibility : Option = maybe_session + let maybe_user_preferred_visibility : Option = maybe_session .as_ref() .map(|user_session| user_session.preferred_tts_result_visibility); let set_visibility = maybe_user_preferred_visibility - .unwrap_or(RecordVisibility::Public); + .unwrap_or(Visibility::Public); info!("Enqueue infer w2l by user token: {:?}", maybe_user_token); @@ -256,7 +254,7 @@ pub async fn enqueue_infer_w2l_with_uploads( let audio_upload_bucket_hash = upload_uuid.clone(); - let audio_upload_bucket_path = hash_to_bucket_path( + let audio_upload_bucket_path = hash_to_bucket_path_string( &upload_uuid, Some(&server_state.audio_uploads_bucket_root) ).map_err(|e| { @@ -279,7 +277,7 @@ pub async fn enqueue_infer_w2l_with_uploads( // This token is returned to the client. let job_token = Tokens::new_w2l_inference_job() - .map_err(|e| { + .map_err(|_e| { warn!("Error creating token"); InferW2lWithUploadError::ServerError })?; @@ -320,7 +318,7 @@ SET .execute(&server_state.mysql_pool) .await; - let record_id = match query_result { + let _record_id = match query_result { Ok(res) => { res.last_insert_id() }, @@ -331,7 +329,7 @@ SET // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) match err { Database(err) => { - let maybe_code = err.code().map(|c| c.into_owned()); + let _maybe_code = err.code().map(|c| c.into_owned()); /*match maybe_code.as_deref() { Some("23000") => { if err.message().contains("username") { @@ -363,7 +361,7 @@ SET }; let body = serde_json::to_string(&response) - .map_err(|e| InferW2lWithUploadError::ServerError)?; + .map_err(|_e| InferW2lWithUploadError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") @@ -391,7 +389,7 @@ WHERE .await; // TODO: This will return error if it doesn't exist let record_exists = match maybe_template { - Ok(record) => { + Ok(_record) => { true }, Err(err) => { diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs similarity index 90% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs index a516a857f0..4528ae381e 100644 --- a/crates/service/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/enqueue_upload_w2l_template.rs @@ -1,21 +1,20 @@ -use actix_http::Error; -use actix_http::http::header; -use actix_web::HttpResponseBuilder; -use actix_web::cookie::Cookie; +// NB: Incrementally getting rid of build warnings... +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + use actix_web::error::ResponseError; use actix_web::http::StatusCode; -use actix_web::{Responder, web, HttpResponse, error, HttpRequest}; +use actix_web::{web, HttpResponse, HttpRequest}; use config::is_bad_video_download_url::is_bad_video_download_url; use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; use crate::server_state::ServerState; use crate::validations::model_uploads::validate_model_title; use database_queries::tokens::Tokens; +use enums::core::visibility::Visibility; use http_server_common::request::get_request_ip::get_request_ip; -use log::{info, warn, log}; -use regex::Regex; -use sqlx::error::DatabaseError; +use log::{info, warn}; use sqlx::error::Error::Database; -use sqlx::mysql::MySqlDatabaseError; use std::fmt; use std::sync::Arc; use user_input_common::check_for_slurs::contains_slurs; @@ -30,24 +29,13 @@ pub enum W2lTemplateType { Image, } -#[deprecated(note = "Use `RecordVisibility` instead!")] -#[derive(Deserialize)] -pub enum CreatorSetVisibility { - /// public - Public, - /// hidden - Hidden, - /// private - Private, -} - #[derive(Deserialize)] pub struct UploadW2lTemplateRequest { idempotency_token: String, title: String, download_url: String, template_type: Option, - creator_set_visibility: Option, + creator_set_visibility: Option, } #[derive(Serialize)] @@ -147,7 +135,7 @@ pub async fn upload_w2l_template_handler( // This token is returned to the client. let job_token = Tokens::new_w2l_template_upload_job() - .map_err(|e| { + .map_err(|_e| { warn!("Error creating token"); UploadW2lTemplateError::ServerError })?; @@ -190,7 +178,7 @@ SET // NB: MySQL Error Code 1062: Duplicate key insertion (this is harder to access) match err { Database(err) => { - let maybe_code = err.code().map(|c| c.into_owned()); + let _maybe_code = err.code().map(|c| c.into_owned()); /*match maybe_code.as_deref() { Some("23000") => { if err.message().contains("username") { @@ -223,7 +211,7 @@ SET }; let body = serde_json::to_string(&response) - .map_err(|e| UploadW2lTemplateError::ServerError)?; + .map_err(|_e| UploadW2lTemplateError::ServerError)?; Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_inference_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_inference_job_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_inference_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_inference_job_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_result.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_result.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_result.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_result.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template_use_count.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template_use_count.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template_use_count.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_template_use_count.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_upload_template_job_status.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_upload_template_job_status.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/get_w2l_upload_template_job_status.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/get_w2l_upload_template_job_status.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_inference_results.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_user_w2l_templates.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/list_w2l_templates.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_w2l_templates.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/list_w2l_templates.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/list_w2l_templates.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/mod.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs rename to crates/service/web/storyteller_web/src/http_server/endpoints/w2l/set_w2l_template_mod_approval.rs diff --git a/crates/service/storyteller_web/src/http_server/middleware/ip_filter_middleware.rs b/crates/service/web/storyteller_web/src/http_server/middleware/ip_filter_middleware.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/middleware/ip_filter_middleware.rs rename to crates/service/web/storyteller_web/src/http_server/middleware/ip_filter_middleware.rs diff --git a/crates/service/storyteller_web/src/http_server/middleware/mod.rs b/crates/service/web/storyteller_web/src/http_server/middleware/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/middleware/mod.rs rename to crates/service/web/storyteller_web/src/http_server/middleware/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/mod.rs b/crates/service/web/storyteller_web/src/http_server/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/mod.rs rename to crates/service/web/storyteller_web/src/http_server/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/get_host_header.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/get_host_header.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/get_host_header.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/get_host_header.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/mod.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/mod.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/mod.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/read_multipart_field_bytes.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/read_multipart_field_bytes.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/read_multipart_field_bytes.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/read_multipart_field_bytes.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/redis_rate_limiter.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/redis_rate_limiter.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/redis_rate_limiter.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/redis_rate_limiter.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/response_error_helpers.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/response_error_helpers.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/response_error_helpers.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/response_error_helpers.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/response_success_helpers.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/response_success_helpers.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/response_success_helpers.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/response_success_helpers.rs diff --git a/crates/service/storyteller_web/src/http_server/web_utils/serialize_as_json_error.rs b/crates/service/web/storyteller_web/src/http_server/web_utils/serialize_as_json_error.rs similarity index 100% rename from crates/service/storyteller_web/src/http_server/web_utils/serialize_as_json_error.rs rename to crates/service/web/storyteller_web/src/http_server/web_utils/serialize_as_json_error.rs diff --git a/crates/service/storyteller_web/src/main.rs b/crates/service/web/storyteller_web/src/main.rs similarity index 99% rename from crates/service/storyteller_web/src/main.rs rename to crates/service/web/storyteller_web/src/main.rs index 8398c8bd22..c7f5a7ee15 100644 --- a/crates/service/storyteller_web/src/main.rs +++ b/crates/service/web/storyteller_web/src/main.rs @@ -17,8 +17,8 @@ #[macro_use] extern crate magic_crypt; #[macro_use] extern crate serde_derive; -pub const RESERVED_USERNAMES : &'static str = include_str!("../../../../db/reserved_usernames.txt"); -pub const RESERVED_SUBSTRINGS : &'static str = include_str!("../../../../db/reserved_usernames_including.txt"); +pub const RESERVED_USERNAMES : &'static str = include_str!("../../../../../db/reserved_usernames.txt"); +pub const RESERVED_SUBSTRINGS : &'static str = include_str!("../../../../../db/reserved_usernames_including.txt"); pub mod billing; pub mod configs; @@ -47,6 +47,7 @@ use billing_component::stripe::stripe_config::{FullUrlOrPath, StripeCheckoutConf use billing_component::stripe::traits::internal_product_to_stripe_lookup::InternalProductToStripeLookup; use billing_component::stripe::traits::internal_subscription_product_lookup::InternalSubscriptionProductLookup; use billing_component::stripe::traits::internal_user_lookup::InternalUserLookup; +use cloud_storage::bucket_client::BucketClient; use config::common_env::CommonEnv; use config::shared_constants::DEFAULT_MYSQL_CONNECTION_STRING; use config::shared_constants::DEFAULT_RUST_LOG; @@ -80,7 +81,6 @@ use sqlx::MySqlPool; use sqlx::mysql::MySqlPoolOptions; use std::sync::Arc; use std::time::Duration; -use storage_buckets_common::bucket_client::BucketClient; use tokio::runtime::Runtime; use twitch_common::twitch_secrets::TwitchSecrets; use url_config::third_party_url_redirector::ThirdPartyUrlRedirector; diff --git a/crates/service/storyteller_web/src/routes.rs b/crates/service/web/storyteller_web/src/routes.rs similarity index 89% rename from crates/service/storyteller_web/src/routes.rs rename to crates/service/web/storyteller_web/src/routes.rs index 80698ac7fb..b44074cad4 100644 --- a/crates/service/storyteller_web/src/routes.rs +++ b/crates/service/web/storyteller_web/src/routes.rs @@ -21,6 +21,7 @@ use crate::http_server::endpoints::flags::design_refresh_flag::enable_design_ref 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; +use crate::http_server::endpoints::media_uploads::upload_media::upload_media_handler; use crate::http_server::endpoints::misc::default_route_404::default_route_404; use crate::http_server::endpoints::misc::detect_locale_handler::detect_locale_handler; use crate::http_server::endpoints::misc::enable_alpha_easy_handler::enable_alpha_easy_handler; @@ -37,6 +38,7 @@ use crate::http_server::endpoints::moderation::ip_bans::list_ip_bans::list_ip_ba use crate::http_server::endpoints::moderation::jobs::get_tts_inference_queue_count::get_tts_inference_queue_count_handler; use crate::http_server::endpoints::moderation::jobs::get_w2l_inference_queue_count::get_w2l_inference_queue_count_handler; use crate::http_server::endpoints::moderation::jobs::kill_tts_inference_jobs::kill_tts_inference_jobs_handler; +use crate::http_server::endpoints::moderation::stats::get_on_prem_worker_stats::get_on_prem_worker_stats_handler; use crate::http_server::endpoints::moderation::stats::get_voice_count_stats::get_voice_count_stats_handler; use crate::http_server::endpoints::moderation::user_bans::ban_user::ban_user_handler; use crate::http_server::endpoints::moderation::user_bans::list_banned_users::list_banned_users_handler; @@ -78,6 +80,7 @@ use crate::http_server::endpoints::vocoders::get_vocoder::get_vocoder_handler; use crate::http_server::endpoints::vocoders::list_vocoders::list_vocoders_handler; use crate::http_server::endpoints::voice_clone_requests::check_if_voice_clone_request_submitted::check_if_voice_clone_request_submitted_handler; use crate::http_server::endpoints::voice_clone_requests::create_voice_clone_request::create_voice_clone_request_handler; +use crate::http_server::endpoints::voice_conversion::inference::enqueue_voice_conversion_inference::enqueue_voice_conversion_inference_handler; use crate::http_server::endpoints::w2l::delete_w2l_result::delete_w2l_inference_result_handler; use crate::http_server::endpoints::w2l::delete_w2l_template::delete_w2l_template_handler; use crate::http_server::endpoints::w2l::edit_w2l_result::edit_w2l_inference_result_handler; @@ -97,7 +100,6 @@ use crate::http_server::endpoints::w2l::set_w2l_template_mod_approval::set_w2l_t use users_component::default_routes::add_suggested_api_v1_account_creation_and_session_routes; use users_component::endpoints::edit_profile_handler::edit_profile_handler; use users_component::endpoints::get_profile_handler::get_profile_handler; -use crate::http_server::endpoints::moderation::stats::get_on_prem_worker_stats::get_on_prem_worker_stats_handler; pub fn add_routes (app: App) -> App where @@ -113,6 +115,7 @@ pub fn add_routes (app: App) -> App let mut app = add_moderator_routes(app); /* /moderation */ app = add_tts_routes(app); /* /tts */ app = add_w2l_routes(app); /* /w2l */ + app = add_web_vc_routes(app); /* /v1/voice_conversion */ app = add_vocoder_routes(app); /* /vocoder */ app = add_retrieval_routes(app); /* /retrieval (aka. "generic_download_jobs") */ app = add_category_routes(app); /* /category */ @@ -122,7 +125,8 @@ pub fn add_routes (app: App) -> App app = add_twitch_routes(app); /* /twitch */ // TODO: MAYBE TEMPORARY app = add_investor_demo_routes(app); /* /demo_mode */ // TODO: DEFINITELY TEMPORARY app = add_flag_routes(app); /* /flag */ - app = add_desktop_app_routes(app); /* /api/v1/vc/... */ + app = add_desktop_app_routes(app); /* /v1/vc/... */ + app = add_media_upload_routes(app); /* /v1/media_upload/... */ // From components app = add_suggested_api_v1_account_creation_and_session_routes(app); // /create_account, /session, /login, /logout @@ -467,6 +471,90 @@ fn add_w2l_routes (app: App) -> App ) } +// ==================== WEB VOICE CONVERSION ROUTES ==================== + +fn add_web_vc_routes (app: App) -> App + where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app.service( + web::scope("/v1/voice_conversion") + .service( + web::resource("/inference") + .route(web::post().to(enqueue_voice_conversion_inference_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + //.service( + // web::resource("/upload") + // .route(web::post().to(upload_w2l_template_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/list") + // .route(web::get().to(list_w2l_templates_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/template/{token}") + // .route(web::get().to(get_w2l_template_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/template/{template_token}/count") + // .route(web::get().to(get_w2l_template_use_count_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/template/{template_token}/edit") + // .route(web::post().to(edit_w2l_template_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/template/{token}/moderate") + // .route(web::post().to(set_w2l_template_mod_approval_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/template/{token}/delete") + // .route(web::post().to(delete_w2l_template_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/result/{token}") + // .route(web::get().to(get_w2l_inference_result_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/result/{token}/edit") + // .route(web::post().to(edit_w2l_inference_result_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/result/{token}/delete") + // .route(web::post().to(delete_w2l_inference_result_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/job/{token}") + // .route(web::get().to(get_w2l_inference_job_status_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + //.service( + // web::resource("/upload_template_job/{token}") + // .route(web::get().to(get_w2l_upload_template_job_status_handler)) + // .route(web::head().to(|| HttpResponse::Ok())) + //) + ) +} + + // ==================== VOCODER ROUTES ==================== fn add_vocoder_routes (app: App) -> App @@ -776,6 +864,27 @@ fn add_desktop_app_routes (app: App) -> App ) } +// ==================== MEDIA UPLOAD ROUTES ==================== + +fn add_media_upload_routes (app: App) -> App + where + B: MessageBody, + T: ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + >, +{ + app.service(web::scope("/v1/media_uploads") + .service(web::resource("/upload") + .route(web::post().to(upload_media_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) + ) +} + // ==================== TWITCH ROUTES ==================== // TODO: Maybe move these into an "oauth-gateway" type http service. diff --git a/crates/service/storyteller_web/src/server_state.rs b/crates/service/web/storyteller_web/src/server_state.rs similarity index 98% rename from crates/service/storyteller_web/src/server_state.rs rename to crates/service/web/storyteller_web/src/server_state.rs index 11ff27bf89..831a2ea91a 100644 --- a/crates/service/storyteller_web/src/server_state.rs +++ b/crates/service/web/storyteller_web/src/server_state.rs @@ -1,4 +1,5 @@ use billing_component::stripe::stripe_config::StripeConfig; +use cloud_storage::bucket_client::BucketClient; use crate::StaticApiTokenSet; use crate::http_server::endpoints::categories::list_tts_categories::DisplayCategory; use crate::http_server::endpoints::tts::list_tts_models::TtsModelRecordForResponse; @@ -14,7 +15,6 @@ use memory_caching::single_item_ttl_cache::SingleItemTtlCache; use r2d2_redis::{r2d2, RedisConnectionManager}; use reusable_types::server_environment::ServerEnvironment; use sqlx::MySqlPool; -use storage_buckets_common::bucket_client::BucketClient; use url_config::third_party_url_redirector::ThirdPartyUrlRedirector; use users_component::utils::session_checker::SessionChecker; use users_component::utils::session_cookie_manager::SessionCookieManager; diff --git a/crates/service/storyteller_web/src/subscriptions/mod.rs b/crates/service/web/storyteller_web/src/subscriptions/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/subscriptions/mod.rs rename to crates/service/web/storyteller_web/src/subscriptions/mod.rs diff --git a/crates/service/storyteller_web/src/subscriptions/subscription_plan.rs b/crates/service/web/storyteller_web/src/subscriptions/subscription_plan.rs similarity index 100% rename from crates/service/storyteller_web/src/subscriptions/subscription_plan.rs rename to crates/service/web/storyteller_web/src/subscriptions/subscription_plan.rs diff --git a/crates/service/web/storyteller_web/src/subscriptions/subscription_plan_list.rs b/crates/service/web/storyteller_web/src/subscriptions/subscription_plan_list.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/service/storyteller_web/src/threads/db_health_checker_thread/db_health_check_status.rs b/crates/service/web/storyteller_web/src/threads/db_health_checker_thread/db_health_check_status.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/db_health_checker_thread/db_health_check_status.rs rename to crates/service/web/storyteller_web/src/threads/db_health_checker_thread/db_health_check_status.rs diff --git a/crates/service/storyteller_web/src/threads/db_health_checker_thread/db_health_checker_thread.rs b/crates/service/web/storyteller_web/src/threads/db_health_checker_thread/db_health_checker_thread.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/db_health_checker_thread/db_health_checker_thread.rs rename to crates/service/web/storyteller_web/src/threads/db_health_checker_thread/db_health_checker_thread.rs diff --git a/crates/service/storyteller_web/src/threads/db_health_checker_thread/mod.rs b/crates/service/web/storyteller_web/src/threads/db_health_checker_thread/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/db_health_checker_thread/mod.rs rename to crates/service/web/storyteller_web/src/threads/db_health_checker_thread/mod.rs diff --git a/crates/service/storyteller_web/src/threads/ip_banlist_set.rs b/crates/service/web/storyteller_web/src/threads/ip_banlist_set.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/ip_banlist_set.rs rename to crates/service/web/storyteller_web/src/threads/ip_banlist_set.rs diff --git a/crates/service/storyteller_web/src/threads/mod.rs b/crates/service/web/storyteller_web/src/threads/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/mod.rs rename to crates/service/web/storyteller_web/src/threads/mod.rs diff --git a/crates/service/storyteller_web/src/threads/poll_ip_banlist_thread.rs b/crates/service/web/storyteller_web/src/threads/poll_ip_banlist_thread.rs similarity index 100% rename from crates/service/storyteller_web/src/threads/poll_ip_banlist_thread.rs rename to crates/service/web/storyteller_web/src/threads/poll_ip_banlist_thread.rs diff --git a/crates/service/storyteller_web/src/util/delete_role_disambiguation.rs b/crates/service/web/storyteller_web/src/util/delete_role_disambiguation.rs similarity index 100% rename from crates/service/storyteller_web/src/util/delete_role_disambiguation.rs rename to crates/service/web/storyteller_web/src/util/delete_role_disambiguation.rs diff --git a/crates/service/storyteller_web/src/util/encrypted_sort_id.rs b/crates/service/web/storyteller_web/src/util/encrypted_sort_id.rs similarity index 100% rename from crates/service/storyteller_web/src/util/encrypted_sort_id.rs rename to crates/service/web/storyteller_web/src/util/encrypted_sort_id.rs diff --git a/crates/service/storyteller_web/src/util/mod.rs b/crates/service/web/storyteller_web/src/util/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/util/mod.rs rename to crates/service/web/storyteller_web/src/util/mod.rs diff --git a/crates/service/storyteller_web/src/validations/mod.rs b/crates/service/web/storyteller_web/src/validations/mod.rs similarity index 100% rename from crates/service/storyteller_web/src/validations/mod.rs rename to crates/service/web/storyteller_web/src/validations/mod.rs diff --git a/crates/service/storyteller_web/src/validations/model_uploads.rs b/crates/service/web/storyteller_web/src/validations/model_uploads.rs similarity index 100% rename from crates/service/storyteller_web/src/validations/model_uploads.rs rename to crates/service/web/storyteller_web/src/validations/model_uploads.rs diff --git a/crates/service/storyteller_web/src/validations/validate_idempotency_token_format.rs b/crates/service/web/storyteller_web/src/validations/validate_idempotency_token_format.rs similarity index 100% rename from crates/service/storyteller_web/src/validations/validate_idempotency_token_format.rs rename to crates/service/web/storyteller_web/src/validations/validate_idempotency_token_format.rs diff --git a/crates/service/websocket_gateway/Cargo.toml b/crates/service/web/websocket_gateway/Cargo.toml similarity index 90% rename from crates/service/websocket_gateway/Cargo.toml rename to crates/service/web/websocket_gateway/Cargo.toml index f3debcbe34..c71157c28c 100644 --- a/crates/service/websocket_gateway/Cargo.toml +++ b/crates/service/web/websocket_gateway/Cargo.toml @@ -15,12 +15,12 @@ path = "src/main.rs" [dependencies] # Internal -config = { path = "../../lib/config" } -container_common = { path = "../../lib/container_common" } -database_queries = { path = "../../lib/database_queries" } -http_server_common = { path = "../../lib/http_server_common" } -redis_common = { path = "../../lib/redis_common" } -twitch_common = { path = "../../lib/twitch_common" } +config = { path = "../../../lib/config" } +container_common = { path = "../../../lib/container_common" } +database_queries = { path = "../../../lib/database_queries" } +http_server_common = { path = "../../../lib/http_server_common" } +redis_common = { path = "../../../lib/redis_common" } +twitch_common = { path = "../../../lib/twitch_common" } # External anyhow = "1.0.40" diff --git a/crates/service/websocket_gateway/src/endpoints_ws/helpers/mod.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/helpers/mod.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/helpers/mod.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/helpers/mod.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/helpers/publish_active_browser_info.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/helpers/publish_active_browser_info.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/helpers/publish_active_browser_info.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/helpers/publish_active_browser_info.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/mod.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/mod.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/mod.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/mod.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/obs_gateway_websocket_handler.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/obs_gateway_websocket_handler.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/obs_gateway_websocket_handler.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/obs_gateway_websocket_handler.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/threads/mod.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/threads/mod.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/threads/mod.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/threads/mod.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/threads/redis_pubsub_event_listener_thread.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/threads/redis_pubsub_event_listener_thread.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/threads/redis_pubsub_event_listener_thread.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/threads/redis_pubsub_event_listener_thread.rs diff --git a/crates/service/websocket_gateway/src/endpoints_ws/threads/tts_inference_job_token_queue.rs b/crates/service/web/websocket_gateway/src/endpoints_ws/threads/tts_inference_job_token_queue.rs similarity index 100% rename from crates/service/websocket_gateway/src/endpoints_ws/threads/tts_inference_job_token_queue.rs rename to crates/service/web/websocket_gateway/src/endpoints_ws/threads/tts_inference_job_token_queue.rs diff --git a/crates/service/websocket_gateway/src/main.rs b/crates/service/web/websocket_gateway/src/main.rs similarity index 100% rename from crates/service/websocket_gateway/src/main.rs rename to crates/service/web/websocket_gateway/src/main.rs diff --git a/crates/service/websocket_gateway/src/server_state.rs b/crates/service/web/websocket_gateway/src/server_state.rs similarity index 100% rename from crates/service/websocket_gateway/src/server_state.rs rename to crates/service/web/websocket_gateway/src/server_state.rs diff --git a/db/reserved_usernames.txt b/db/reserved_usernames.txt index 3c86313eeb..1fa1e668cd 100644 --- a/db/reserved_usernames.txt +++ b/db/reserved_usernames.txt @@ -43,8 +43,318 @@ w2l 15ai fifteen_ai fifteenai +playground +playground_ai +playgroundai uber_duck uberduck +voice_ai +voice_mod +voiceai +voicemod + +# Existing frontend pages (frontend repo) +about +category +checkout_cancel +checkout_success +clone +coming-soon +coming_soon +contribute +demo +firehose +guide +jobs +leaderboard +login +moderation +obs_configs +old +patrons +pending +portal_success +pricing +privacy +profile +signup +stream +terms +tts +tts_configs +twitch +upload +video +w2l + +# Potential pages (our list) +1d +2d +3d +4d +5d +6d +admin +administrate +administrator +administrators +admins +alpha +article +article_list +articles +ask +assign +assigned +bad +best +beta +blog +blogs +bookmark +bookmarks +bot +build +building +builds +career +careers +collection +collections +comment +comments +config +configs +contact +control_panel +delta +doc +docs +download +downloads +email +employee +employees +events +explore +favorite +favorites +favourite +favourites +feedback +find +forum +forums +front +gamma +generate +generating +good +help +hide +highlight +highlighted +hiring +home +issues +item +items +job +jobs +least +legal +less +list +lists +lists +log_in +log_out +login +logout +mail +manage +management +map +marketplace +mention +mentions +message +messages +mod +mod_mail +model +model_list +models +moderator +moderators +modmail +mods +more +most +new +newer +newest +news +notification +notifications +old +older +oldest +panel +policies +policy +post +posts +preference +preferences +profile +profiles +rank +ranking +rankings +recent +release +releases +rename +render +rendering +renders +result +results +robot +robots +search +setting +settings +show +sign_in +sign_out +signin +signout +sitemap +sponsor +sponsors +staff +staging +star +stared +starred +stars +submissions +submit +support +team +teams +terms +thread +thread_list +threads +top +topic +topics +trending +trends +user +users +view +viewer +worst + +# Potential API surface area +api +create +delete +download +downloads +feed +http +https +id +ids +json +list +payload +post +proto +publish +pubsub +put +rss +show +socket +sort +sort_by +subscribe +token +tokens +update +upload +uploads +webhook +webhooks +ws +wss +v1 +v2 +v3 +v4 +v5 +v6 +v7 +v8 +v9 +v10 +v11 +v12 +v13 +v14 +v15 +v16 +v17 +v18 +v19 +v20 + +# Potential actions (our list) +bottom +change +changed +create +delete +demote +downvote +edit +filter +filtered_by +find +first +generate +last +least +most +origin +post +promote +rank +ranking +rankings +redirect +regenerate +remove +reset +search +send +send +since +sort +sorted +sorted_by +top +until +update +upload +uploads +upvote # Companies we'd love to have use our service # so we'll reserve them to be safe @@ -60,6 +370,7 @@ google microsoft netflix nintendo +openai paypal reddit snap @@ -75,6 +386,13 @@ unreal venmo youtube +# Production companies, too +a24 +disney +ghibli +hbo +marvel + # Top 50 languages by speaking population chinese mandarin diff --git a/db/reserved_usernames_including.txt b/db/reserved_usernames_including.txt index 82c005e9ad..1878d7c29e 100644 --- a/db/reserved_usernames_including.txt +++ b/db/reserved_usernames_including.txt @@ -1,7 +1,9 @@ # This is a more restrictive list of reserved usernames. # Any of the substrings may not be used. # You should check string.remove('_') as well. +admin fake_you +fakeu fakeyou hyperjail power_stream diff --git a/test_data/audio/aac/golden_sun_elemental_stars_cyanne.aac b/test_data/audio/aac/golden_sun_elemental_stars_cyanne.aac new file mode 100644 index 0000000000..f8ea4e26fc Binary files /dev/null and b/test_data/audio/aac/golden_sun_elemental_stars_cyanne.aac differ diff --git a/test_data/audio/aiff/jet_force_gemini_anubis.aiff b/test_data/audio/aiff/jet_force_gemini_anubis.aiff new file mode 100644 index 0000000000..3b0d909ca1 Binary files /dev/null and b/test_data/audio/aiff/jet_force_gemini_anubis.aiff differ diff --git a/test_data/audio/flac/zelda_ocarina_small_item.flac b/test_data/audio/flac/zelda_ocarina_small_item.flac new file mode 100644 index 0000000000..87885640cd Binary files /dev/null and b/test_data/audio/flac/zelda_ocarina_small_item.flac differ diff --git a/test_data/audio/m4a/super_mario_bros_lost_life.m4a b/test_data/audio/m4a/super_mario_bros_lost_life.m4a new file mode 100644 index 0000000000..7e736c7eac Binary files /dev/null and b/test_data/audio/m4a/super_mario_bros_lost_life.m4a differ diff --git a/test_data/audio/mp3/super_mario_rpg_beware_the_forests_mushrooms.mp3 b/test_data/audio/mp3/super_mario_rpg_beware_the_forests_mushrooms.mp3 new file mode 100644 index 0000000000..41b9209122 Binary files /dev/null and b/test_data/audio/mp3/super_mario_rpg_beware_the_forests_mushrooms.mp3 differ diff --git a/test_data/audio/ogg/banjo-kazooie_jiggy_appearance.ogg b/test_data/audio/ogg/banjo-kazooie_jiggy_appearance.ogg new file mode 100644 index 0000000000..50a3b5b724 Binary files /dev/null and b/test_data/audio/ogg/banjo-kazooie_jiggy_appearance.ogg differ diff --git a/test_data/audio/wav/sm64_mario_its_me.wav b/test_data/audio/wav/sm64_mario_its_me.wav new file mode 100644 index 0000000000..d5385f85cf Binary files /dev/null and b/test_data/audio/wav/sm64_mario_its_me.wav differ diff --git a/test_data/audio/wav/smrpg_battlestart_f32.wav b/test_data/audio/wav/smrpg_battlestart_f32.wav new file mode 100644 index 0000000000..3c38b85ed7 Binary files /dev/null and b/test_data/audio/wav/smrpg_battlestart_f32.wav differ diff --git a/test_data/audio/wav/smrpg_correct.wav b/test_data/audio/wav/smrpg_correct.wav new file mode 100644 index 0000000000..ccb8c51776 Binary files /dev/null and b/test_data/audio/wav/smrpg_correct.wav differ diff --git a/test_data/audio/wav/smrpg_item_mushroom_8u.wav b/test_data/audio/wav/smrpg_item_mushroom_8u.wav new file mode 100644 index 0000000000..398a936b76 Binary files /dev/null and b/test_data/audio/wav/smrpg_item_mushroom_8u.wav differ diff --git a/test_data/video/mkv/fake_you.mkv b/test_data/video/mkv/fake_you.mkv new file mode 100644 index 0000000000..c59840b9a8 Binary files /dev/null and b/test_data/video/mkv/fake_you.mkv differ diff --git a/test_data/video/mov/majoras_mask_intro.mov b/test_data/video/mov/majoras_mask_intro.mov new file mode 100644 index 0000000000..b0b1a8ae59 Binary files /dev/null and b/test_data/video/mov/majoras_mask_intro.mov differ diff --git a/test_data/video/mp4/golden_sun_garoh.mp4 b/test_data/video/mp4/golden_sun_garoh.mp4 new file mode 100644 index 0000000000..c2047dcfb6 Binary files /dev/null and b/test_data/video/mp4/golden_sun_garoh.mp4 differ diff --git a/test_data/video/webm/laser_pong.webm b/test_data/video/webm/laser_pong.webm new file mode 100644 index 0000000000..157938c6a3 Binary files /dev/null and b/test_data/video/webm/laser_pong.webm differ