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