refactor(voice): replace HTTP with stdin/stdout IPC for sr-voice workers
Gemma 2 T&C compliance: exposed HTTP ports allow mods or external code to reach the model, complicating license enforcement. Switch to piped stdin/stdout (JSONL protocol) so the model is only reachable through the game server's internal queue. - worker.rs: VoicePipe owns Child + piped stdin/stdout, VoiceProcessConfig replaces port-based config, workers spawn their own sr-voice child - hardware.rs: remove VoiceInstanceManager (port/process lifecycle), replace with evaluate_scaling() free function + HardwareProbe::voice_config() - sr-voice: add --stdio flag to serve command, new stdio.rs JSONL mode - Remove ureq dependency from server crate (no longer needed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
+7
-535
@@ -2,12 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -53,7 +47,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -64,7 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -140,12 +134,6 @@ version = "0.21.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bevy_app"
|
||||
version = "0.18.0"
|
||||
@@ -383,16 +371,6 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -470,15 +448,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
@@ -517,7 +486,7 @@ checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"nix",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -567,17 +536,6 @@ dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "disqualified"
|
||||
version = "1.0.0"
|
||||
@@ -633,43 +591,18 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -723,17 +656,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
@@ -792,108 +714,6 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
@@ -947,12 +767,6 @@ version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
@@ -974,16 +788,6 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.1"
|
||||
@@ -1017,7 +821,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1095,12 +899,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
@@ -1148,15 +946,6 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -1216,7 +1005,7 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1236,20 +1025,6 @@ version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.15"
|
||||
@@ -1275,7 +1050,7 @@ version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"base64",
|
||||
"bitflags",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -1296,41 +1071,6 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
@@ -1426,7 +1166,6 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1438,18 +1177,6 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -1501,12 +1228,6 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
@@ -1518,17 +1239,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.35.2"
|
||||
@@ -1572,16 +1282,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.5+spec-1.1.0"
|
||||
@@ -1716,48 +1416,6 @@ version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"flate2",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
@@ -1770,7 +1428,7 @@ version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
@@ -1799,12 +1457,6 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
@@ -1883,24 +1535,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-types"
|
||||
version = "27.0.1"
|
||||
@@ -2046,15 +1680,6 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -2064,22 +1689,6 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
@@ -2089,54 +1698,6 @@ dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
@@ -2152,35 +1713,6 @@ version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.39"
|
||||
@@ -2201,66 +1733,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
@@ -19,7 +19,6 @@ tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
crossbeam-channel = "0.5"
|
||||
ureq = { version = "2", features = ["json"] }
|
||||
sysinfo = "0.35"
|
||||
serde_json = "1"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod inference;
|
||||
mod prompt;
|
||||
mod server;
|
||||
mod stdio;
|
||||
|
||||
use std::io::Read;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -34,7 +35,7 @@ enum Command {
|
||||
/// Path to GGUF model file
|
||||
#[arg(long)]
|
||||
model: String,
|
||||
/// Listen port
|
||||
/// Listen port (ignored when --stdio is set)
|
||||
#[arg(long, default_value = "8321")]
|
||||
port: u16,
|
||||
/// CPU threads for inference
|
||||
@@ -43,6 +44,11 @@ enum Command {
|
||||
/// Context window size in tokens
|
||||
#[arg(long, default_value = "512")]
|
||||
ctx_size: u32,
|
||||
/// Run in stdio mode: read JSONL from stdin, write JSONL to stdout.
|
||||
/// No network port is opened. Used by the game server's worker pool
|
||||
/// for Gemma 2 T&C compliance (no exposed inference endpoint).
|
||||
#[arg(long)]
|
||||
stdio: bool,
|
||||
},
|
||||
/// Generate text from a single prompt (requires running server)
|
||||
Generate {
|
||||
@@ -83,7 +89,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Command::Serve { model, port, threads, ctx_size } => {
|
||||
Command::Serve { model, port, threads, ctx_size, stdio } => {
|
||||
let threads = threads.unwrap_or_else(default_threads);
|
||||
let config = InferenceConfig {
|
||||
model_path: model.clone(),
|
||||
@@ -96,12 +102,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let engine = InferenceEngine::load(&config)?;
|
||||
eprintln!("Model loaded ({} threads, {} ctx)", threads, ctx_size);
|
||||
|
||||
let model_name = std::path::Path::new(&model)
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy().to_string())
|
||||
.unwrap_or(model);
|
||||
if stdio {
|
||||
eprintln!("sr-voice stdio mode — reading JSONL from stdin");
|
||||
stdio::run_stdio(engine)?;
|
||||
} else {
|
||||
let model_name = std::path::Path::new(&model)
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy().to_string())
|
||||
.unwrap_or(model);
|
||||
|
||||
server::run_server(engine, port, &model_name)?;
|
||||
server::run_server(engine, port, &model_name)?;
|
||||
}
|
||||
}
|
||||
Command::Generate { port, seed, prompt_file } => {
|
||||
let prompt = read_prompt(prompt_file)?;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Stdio JSONL mode for sr-voice (D-138, Spike 2).
|
||||
//!
|
||||
//! Reads one JSON object per line from stdin, runs inference, writes one JSON
|
||||
//! object per line to stdout. No network port is opened — the model is only
|
||||
//! reachable through the parent process's pipe (Gemma 2 T&C compliance).
|
||||
//!
|
||||
//! Request format: {"prompt": "...", "seed": 42}
|
||||
//! Response format: {"text": "...", "tokens_generated": N, ...}
|
||||
//! or {"error": "..."}
|
||||
|
||||
use std::io::{self, BufRead, Write};
|
||||
|
||||
use crate::inference::InferenceEngine;
|
||||
|
||||
const MAX_TOKENS: u32 = 64;
|
||||
const TEMPERATURE: f32 = 0.7;
|
||||
const TOP_P: f32 = 0.9;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct StdioRequest {
|
||||
prompt: String,
|
||||
seed: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn run_stdio(engine: InferenceEngine) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let stdin = io::stdin().lock();
|
||||
let mut stdout = io::stdout().lock();
|
||||
|
||||
for line in stdin.lines() {
|
||||
let line = match line {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("stdin read error: {}", e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let response = match serde_json::from_str::<StdioRequest>(&line) {
|
||||
Ok(req) => {
|
||||
eprintln!(" stdio: {} chars", req.prompt.len());
|
||||
match engine.generate(&req.prompt, MAX_TOKENS, TEMPERATURE, TOP_P, req.seed) {
|
||||
Ok(result) => {
|
||||
eprintln!(
|
||||
" -> {} tokens, {:.1} t/s",
|
||||
result.tokens_generated, result.tokens_per_sec
|
||||
);
|
||||
serde_json::to_string(&result).unwrap()
|
||||
}
|
||||
Err(e) => {
|
||||
serde_json::json!({"error": e.to_string()}).to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
serde_json::json!({"error": format!("invalid JSON: {}", e)}).to_string()
|
||||
}
|
||||
};
|
||||
|
||||
writeln!(stdout, "{}", response)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
|
||||
eprintln!("sr-voice stdio mode — stdin closed, exiting");
|
||||
Ok(())
|
||||
}
|
||||
+115
-215
@@ -1,54 +1,40 @@
|
||||
//! Hardware detection + dynamic sr-voice instance management (D-138, Spike 2).
|
||||
//! Hardware detection + dynamic scaling decisions (D-138, Spike 2).
|
||||
//!
|
||||
//! Determines how many parallel LLM workers the system can sustain and manages
|
||||
//! sr-voice process lifecycle. The scaling ceiling is conservative:
|
||||
//! Determines how many parallel LLM workers the system can sustain.
|
||||
//! The scaling ceiling is conservative:
|
||||
//!
|
||||
//! max_new = (free_resource - existing_llm_usage) / 2 / PER_INSTANCE_COST
|
||||
//!
|
||||
//! "free_resource" is VRAM when a GPU is detected (nvidia-smi / AMD sysfs),
|
||||
//! or system RAM otherwise. This ensures the voice pipeline never takes more
|
||||
//! than half the available headroom after accounting for its own instances.
|
||||
//!
|
||||
//! Process lifecycle is handled by `WorkerPool` / `VoicePipe` in `worker.rs`.
|
||||
//! This module only probes hardware and advises on scaling — it does not
|
||||
//! spawn or stop sr-voice processes.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
use sysinfo::System;
|
||||
|
||||
/// RAM budget per sr-voice instance (Gemma 2B Q4_K_M ≈ 1.5 GB resident).
|
||||
use crate::voice::worker::VoiceProcessConfig;
|
||||
|
||||
/// RAM/VRAM budget per sr-voice instance (Gemma 2B Q4_K_M ≈ 1.5 GB resident).
|
||||
const PER_INSTANCE_RAM_MB: u64 = 1536;
|
||||
|
||||
/// Minimum free RAM to allow any voice instance at all.
|
||||
/// Minimum free resource to allow any voice instance at all.
|
||||
const MIN_FREE_RAM_MB: u64 = 1536;
|
||||
|
||||
/// Base port for sr-voice instances. Worker N listens on BASE_PORT + N.
|
||||
const BASE_PORT: u16 = 8321;
|
||||
|
||||
/// Context window size for sr-voice instances.
|
||||
const CTX_SIZE: u32 = 512;
|
||||
|
||||
/// How often the scaler thread checks for scale-up/down opportunities.
|
||||
/// How often the scaler thread checks for scale-up/down opportunities.
|
||||
/// Used by the runtime scaler loop (not yet implemented).
|
||||
const _SCALE_CHECK_INTERVAL: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Queue depth threshold — sustained above this triggers scale-up consideration.
|
||||
const QUEUE_DEPTH_SCALE_UP: usize = 32;
|
||||
|
||||
/// Worker idle duration before scale-down.
|
||||
const IDLE_BEFORE_SCALE_DOWN: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Managed sr-voice process instances.
|
||||
#[derive(Debug)]
|
||||
struct VoiceInstance {
|
||||
process: Child,
|
||||
port: u16,
|
||||
spawned_at: Instant,
|
||||
}
|
||||
|
||||
/// Whether the scaling resource is GPU VRAM or system RAM.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ResourceMode {
|
||||
@@ -75,18 +61,16 @@ pub struct HardwareProbe {
|
||||
pub threads_per_instance: u32,
|
||||
}
|
||||
|
||||
/// Manages sr-voice process lifecycle and dynamic scaling.
|
||||
pub struct VoiceInstanceManager {
|
||||
/// Path to sr-voice binary.
|
||||
binary_path: PathBuf,
|
||||
/// Path to model file.
|
||||
model_path: PathBuf,
|
||||
/// Running instances keyed by worker ID.
|
||||
instances: HashMap<usize, VoiceInstance>,
|
||||
/// Hardware probe from startup.
|
||||
probe: HardwareProbe,
|
||||
/// Shutdown signal.
|
||||
shutdown: Arc<AtomicBool>,
|
||||
impl HardwareProbe {
|
||||
/// Build a `VoiceProcessConfig` from probe results and user paths.
|
||||
pub fn voice_config(&self, binary_path: String, model_path: String) -> VoiceProcessConfig {
|
||||
VoiceProcessConfig {
|
||||
binary_path,
|
||||
model_path,
|
||||
threads: self.threads_per_instance,
|
||||
ctx_size: CTX_SIZE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect GPU type. Run once at install/first-run, persist to settings.
|
||||
@@ -176,6 +160,70 @@ pub fn probe_hardware(mode: Option<ResourceMode>) -> HardwareProbe {
|
||||
probe
|
||||
}
|
||||
|
||||
/// Evaluate whether the worker pool should scale up or down.
|
||||
///
|
||||
/// Called periodically by the voice pipeline coordinator. Does not take
|
||||
/// action itself — returns a `ScalingDecision` for the caller to act on.
|
||||
pub fn evaluate_scaling(
|
||||
mode: ResourceMode,
|
||||
running_workers: usize,
|
||||
queue_depth: usize,
|
||||
active_workers: usize,
|
||||
max_idle_duration: Option<Duration>,
|
||||
) -> ScalingDecision {
|
||||
// Battery → scale to 1
|
||||
if on_battery() && running_workers > 1 {
|
||||
return ScalingDecision::ScaleDown {
|
||||
reason: "battery power detected".into(),
|
||||
};
|
||||
}
|
||||
|
||||
// Re-probe free resource for current conditions
|
||||
let current_free_mb = probe_current_free(mode);
|
||||
let max_slots = compute_max_slots(current_free_mb, running_workers);
|
||||
|
||||
// Scale up: queue pressure + capacity available
|
||||
if queue_depth >= QUEUE_DEPTH_SCALE_UP
|
||||
&& running_workers < max_slots
|
||||
&& current_free_mb >= MIN_FREE_RAM_MB
|
||||
{
|
||||
return ScalingDecision::ScaleUp {
|
||||
reason: format!(
|
||||
"queue depth {} >= {}, {} slots available",
|
||||
queue_depth, QUEUE_DEPTH_SCALE_UP, max_slots
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Scale down: worker idle too long + more than 1 worker
|
||||
if running_workers > 1 {
|
||||
if let Some(idle) = max_idle_duration {
|
||||
if idle >= IDLE_BEFORE_SCALE_DOWN && active_workers < running_workers {
|
||||
return ScalingDecision::ScaleDown {
|
||||
reason: format!("worker idle for {}s", idle.as_secs()),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScalingDecision::Hold
|
||||
}
|
||||
|
||||
/// Result of a scaling evaluation.
|
||||
#[derive(Debug)]
|
||||
pub enum ScalingDecision {
|
||||
/// No change needed.
|
||||
Hold,
|
||||
/// Spawn an additional worker (caller decides which).
|
||||
ScaleUp { reason: String },
|
||||
/// Stop an idle worker (caller picks the most idle).
|
||||
ScaleDown { reason: String },
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GPU / resource probing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Probe NVIDIA GPU VRAM via nvidia-smi.
|
||||
/// Returns (total_mb, free_mb) for the first GPU, or None.
|
||||
fn probe_nvidia_vram() -> Option<(u64, u64)> {
|
||||
@@ -323,180 +371,6 @@ fn on_battery() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
impl VoiceInstanceManager {
|
||||
/// Create a new manager. Does not spawn any instances yet.
|
||||
pub fn new(
|
||||
binary_path: PathBuf,
|
||||
model_path: PathBuf,
|
||||
probe: HardwareProbe,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
binary_path,
|
||||
model_path,
|
||||
instances: HashMap::new(),
|
||||
probe,
|
||||
shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of currently running instances.
|
||||
pub fn instance_count(&self) -> usize {
|
||||
self.instances.len()
|
||||
}
|
||||
|
||||
/// Base port for worker connections.
|
||||
pub fn base_port(&self) -> u16 {
|
||||
BASE_PORT
|
||||
}
|
||||
|
||||
/// The hardware probe from startup.
|
||||
pub fn probe(&self) -> &HardwareProbe {
|
||||
&self.probe
|
||||
}
|
||||
|
||||
/// Spawn a sr-voice instance for the given worker ID.
|
||||
/// Returns the port it's listening on, or an error.
|
||||
pub fn spawn_instance(&mut self, worker_id: usize) -> Result<u16, String> {
|
||||
let port = BASE_PORT + worker_id as u16;
|
||||
|
||||
if self.instances.contains_key(&worker_id) {
|
||||
return Ok(port); // already running
|
||||
}
|
||||
|
||||
let child = Command::new(&self.binary_path)
|
||||
.arg("serve")
|
||||
.arg("--model")
|
||||
.arg(&self.model_path)
|
||||
.arg("--port")
|
||||
.arg(port.to_string())
|
||||
.arg("--threads")
|
||||
.arg(self.probe.threads_per_instance.to_string())
|
||||
.arg("--ctx-size")
|
||||
.arg(CTX_SIZE.to_string())
|
||||
.spawn()
|
||||
.map_err(|e| format!("failed to spawn sr-voice on port {}: {}", port, e))?;
|
||||
|
||||
tracing::info!(worker_id, port, "spawned sr-voice instance");
|
||||
|
||||
self.instances.insert(worker_id, VoiceInstance {
|
||||
process: child,
|
||||
port,
|
||||
spawned_at: Instant::now(),
|
||||
});
|
||||
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
/// Stop a sr-voice instance for the given worker ID.
|
||||
pub fn stop_instance(&mut self, worker_id: usize) {
|
||||
if let Some(mut instance) = self.instances.remove(&worker_id) {
|
||||
let _ = instance.process.kill();
|
||||
let _ = instance.process.wait();
|
||||
tracing::info!(worker_id, port = instance.port, "stopped sr-voice instance");
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate whether to scale up or down based on current conditions.
|
||||
///
|
||||
/// Returns (should_scale_up, should_scale_down_worker_id).
|
||||
pub fn evaluate_scaling(
|
||||
&self,
|
||||
queue_depth: usize,
|
||||
active_workers: usize,
|
||||
worker_idle_durations: &HashMap<usize, Duration>,
|
||||
) -> ScalingDecision {
|
||||
// Battery → scale to 1
|
||||
if on_battery() && self.instances.len() > 1 {
|
||||
return ScalingDecision::ScaleDown {
|
||||
reason: "battery power detected".into(),
|
||||
};
|
||||
}
|
||||
|
||||
// Re-probe free resource (VRAM or RAM) for current conditions
|
||||
let current_free_mb = probe_current_free(self.probe.mode);
|
||||
let max_slots = compute_max_slots(current_free_mb, self.instances.len());
|
||||
|
||||
// Scale up: queue pressure + capacity available
|
||||
if queue_depth >= QUEUE_DEPTH_SCALE_UP
|
||||
&& self.instances.len() < max_slots
|
||||
&& current_free_mb >= MIN_FREE_RAM_MB
|
||||
{
|
||||
return ScalingDecision::ScaleUp {
|
||||
reason: format!(
|
||||
"queue depth {} >= {}, {} slots available",
|
||||
queue_depth, QUEUE_DEPTH_SCALE_UP, max_slots
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Scale down: worker idle too long + more than 1 instance
|
||||
if self.instances.len() > 1 {
|
||||
for (&worker_id, &idle_time) in worker_idle_durations {
|
||||
if idle_time >= IDLE_BEFORE_SCALE_DOWN && active_workers < self.instances.len() {
|
||||
return ScalingDecision::ScaleDown {
|
||||
reason: format!(
|
||||
"worker {} idle for {}s",
|
||||
worker_id,
|
||||
idle_time.as_secs()
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScalingDecision::Hold
|
||||
}
|
||||
|
||||
/// Shut down all sr-voice instances.
|
||||
pub fn shutdown_all(&mut self) {
|
||||
let ids: Vec<usize> = self.instances.keys().copied().collect();
|
||||
for id in ids {
|
||||
self.stop_instance(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for a sr-voice instance to become healthy (responds to /health).
|
||||
/// Returns true if healthy within timeout, false otherwise.
|
||||
pub fn wait_for_healthy(&self, port: u16, timeout: Duration) -> bool {
|
||||
let url = format!("http://127.0.0.1:{}/health", port);
|
||||
let deadline = Instant::now() + timeout;
|
||||
|
||||
while Instant::now() < deadline {
|
||||
let result = ureq::AgentBuilder::new()
|
||||
.timeout(Duration::from_secs(1))
|
||||
.build()
|
||||
.get(&url)
|
||||
.call();
|
||||
|
||||
if result.is_ok() {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VoiceInstanceManager {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown_all();
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a scaling evaluation.
|
||||
#[derive(Debug)]
|
||||
pub enum ScalingDecision {
|
||||
/// No change needed.
|
||||
Hold,
|
||||
/// Spawn an additional instance.
|
||||
ScaleUp { reason: String },
|
||||
/// Stop an instance (pick the most idle worker).
|
||||
ScaleDown { reason: String },
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -544,8 +418,7 @@ mod tests {
|
||||
let slots_0 = compute_max_slots(8192, 0); // 8GB free, 0 running
|
||||
let slots_2 = compute_max_slots(8192, 2); // 8GB free, 2 running
|
||||
|
||||
// With 2 running, effective headroom is smaller so ceiling is lower per-new-instance
|
||||
// but total (running + new) can still be higher
|
||||
// Both should be non-zero with 8GB
|
||||
assert!(slots_0 > 0);
|
||||
assert!(slots_2 > 0);
|
||||
// The key property: free RAM measured at probe time is the same,
|
||||
@@ -561,9 +434,36 @@ mod tests {
|
||||
assert!(probe.threads_per_instance >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn voice_config_from_probe() {
|
||||
let probe = HardwareProbe {
|
||||
mode: ResourceMode::Cpu,
|
||||
total_mb: 16384,
|
||||
free_mb: 8192,
|
||||
max_slots: 2,
|
||||
cpu_threads: 8,
|
||||
threads_per_instance: 2,
|
||||
};
|
||||
let config = probe.voice_config("/usr/bin/sr-voice".into(), "/models/gemma.gguf".into());
|
||||
assert_eq!(config.threads, 2);
|
||||
assert_eq!(config.ctx_size, CTX_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_detection_does_not_crash() {
|
||||
// Just verify it doesn't panic — result depends on hardware
|
||||
let _ = on_battery();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_scaling_hold_when_calm() {
|
||||
let decision = evaluate_scaling(
|
||||
ResourceMode::Cpu,
|
||||
2, // running
|
||||
5, // queue depth (low)
|
||||
1, // active
|
||||
None,
|
||||
);
|
||||
assert!(matches!(decision, ScalingDecision::Hold));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
//! - `prompt_builder` — composition engine: NPC data + culture + tell state → prompt string
|
||||
//! - `cache` — MessagePack voice cache (store/retrieve, length-gated variants)
|
||||
//! - `queue` — crossbeam work queue with priority + backpressure
|
||||
//! - `worker` — inference worker pool (dynamic scaling, owns sr-voice HTTP clients)
|
||||
//! - `hardware` — hardware detection + dynamic sr-voice instance management
|
||||
//! - `worker` — inference worker pool (each worker owns a piped sr-voice child process)
|
||||
//! - `hardware` — hardware detection + dynamic scaling decisions
|
||||
|
||||
pub mod cache;
|
||||
pub mod hardware;
|
||||
|
||||
+123
-57
@@ -1,8 +1,12 @@
|
||||
//! Inference worker pool (D-138, Spike 2).
|
||||
//!
|
||||
//! Dynamic pool of worker threads, each owning an HTTP client to its own
|
||||
//! sr-voice instance. Pool size controlled by hardware detection.
|
||||
//! Dynamic pool of worker threads, each owning a piped stdin/stdout connection
|
||||
//! to its own sr-voice child process. No network ports — the model is only
|
||||
//! reachable through the game server's queue (Gemma 2 T&C compliance).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufRead, BufReader, Write as IoWrite};
|
||||
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::{self, JoinHandle};
|
||||
@@ -18,12 +22,6 @@ use crate::voice::queue::VoiceRequest;
|
||||
/// Minimum token count for a valid response. Below this, retry once.
|
||||
const MIN_TOKENS: usize = 4;
|
||||
|
||||
/// How long to wait before retrying connection to sr-voice.
|
||||
const _RECONNECT_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
/// HTTP request timeout for inference calls.
|
||||
const INFERENCE_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Worker pool manages inference worker threads.
|
||||
pub struct WorkerPool {
|
||||
workers: Vec<WorkerHandle>,
|
||||
@@ -46,14 +44,20 @@ pub struct WorkerContext {
|
||||
pub paused: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
use std::collections::HashMap;
|
||||
/// Configuration for spawning sr-voice child processes.
|
||||
#[derive(Clone)]
|
||||
pub struct VoiceProcessConfig {
|
||||
pub binary_path: String,
|
||||
pub model_path: String,
|
||||
pub threads: u32,
|
||||
pub ctx_size: u32,
|
||||
}
|
||||
|
||||
impl WorkerPool {
|
||||
/// Spawn `count` worker threads, each connecting to sr-voice on
|
||||
/// `base_port + worker_id`.
|
||||
/// Spawn `count` worker threads, each owning a piped sr-voice child process.
|
||||
pub fn spawn(
|
||||
count: usize,
|
||||
base_port: u16,
|
||||
config: &VoiceProcessConfig,
|
||||
receiver: Receiver<VoiceRequest>,
|
||||
cache: Arc<Mutex<VoiceCacheStore>>,
|
||||
cultures: Arc<HashMap<String, CultureProfile>>,
|
||||
@@ -73,11 +77,11 @@ impl WorkerPool {
|
||||
active_count: Arc::clone(&active_count),
|
||||
paused: Arc::clone(&paused),
|
||||
};
|
||||
let port = base_port + id as u16;
|
||||
let cfg = config.clone();
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name(format!("voice-worker-{}", id))
|
||||
.spawn(move || worker_loop(id, port, ctx))
|
||||
.spawn(move || worker_loop(id, cfg, ctx))
|
||||
.expect("failed to spawn voice worker thread");
|
||||
|
||||
workers.push(WorkerHandle {
|
||||
@@ -86,7 +90,7 @@ impl WorkerPool {
|
||||
});
|
||||
}
|
||||
|
||||
tracing::info!(count, base_port, "voice worker pool started");
|
||||
tracing::info!(count, "voice worker pool started");
|
||||
|
||||
Self {
|
||||
workers,
|
||||
@@ -123,13 +127,105 @@ impl Drop for WorkerPool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Main worker loop: receive requests, build prompts, call sr-voice, cache results.
|
||||
fn worker_loop(id: usize, port: u16, ctx: WorkerContext) {
|
||||
let base_url = format!("http://127.0.0.1:{}", port);
|
||||
tracing::debug!(id, port, "voice worker started");
|
||||
/// A piped connection to a sr-voice child process.
|
||||
struct VoicePipe {
|
||||
child: Child,
|
||||
stdin: ChildStdin,
|
||||
reader: BufReader<ChildStdout>,
|
||||
}
|
||||
|
||||
// Below-normal thread priority is handled at the OS level by the
|
||||
// sr-voice process itself (nice value). Worker threads inherit it.
|
||||
impl VoicePipe {
|
||||
/// Spawn a sr-voice child process with piped stdin/stdout.
|
||||
fn spawn(config: &VoiceProcessConfig) -> Result<Self, String> {
|
||||
let mut child = Command::new(&config.binary_path)
|
||||
.arg("serve")
|
||||
.arg("--model")
|
||||
.arg(&config.model_path)
|
||||
.arg("--threads")
|
||||
.arg(config.threads.to_string())
|
||||
.arg("--ctx-size")
|
||||
.arg(config.ctx_size.to_string())
|
||||
.arg("--stdio")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.map_err(|e| format!("failed to spawn sr-voice: {}", e))?;
|
||||
|
||||
let stdin = child.stdin.take()
|
||||
.ok_or_else(|| "failed to capture sr-voice stdin".to_string())?;
|
||||
let stdout = child.stdout.take()
|
||||
.ok_or_else(|| "failed to capture sr-voice stdout".to_string())?;
|
||||
|
||||
Ok(Self {
|
||||
child,
|
||||
stdin,
|
||||
reader: BufReader::new(stdout),
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a prompt and read the response (JSONL: one JSON object per line).
|
||||
fn generate(&mut self, prompt: &str, seed: Option<u64>) -> Result<String, String> {
|
||||
let request = serde_json::json!({
|
||||
"prompt": prompt,
|
||||
"seed": seed,
|
||||
});
|
||||
|
||||
let mut line = serde_json::to_string(&request)
|
||||
.map_err(|e| format!("failed to serialize request: {}", e))?;
|
||||
line.push('\n');
|
||||
|
||||
self.stdin
|
||||
.write_all(line.as_bytes())
|
||||
.map_err(|e| format!("failed to write to sr-voice stdin: {}", e))?;
|
||||
self.stdin
|
||||
.flush()
|
||||
.map_err(|e| format!("failed to flush sr-voice stdin: {}", e))?;
|
||||
|
||||
let mut response_line = String::new();
|
||||
self.reader
|
||||
.read_line(&mut response_line)
|
||||
.map_err(|e| format!("failed to read from sr-voice stdout: {}", e))?;
|
||||
|
||||
if response_line.is_empty() {
|
||||
return Err("sr-voice process closed stdout".to_string());
|
||||
}
|
||||
|
||||
let body: serde_json::Value = serde_json::from_str(&response_line)
|
||||
.map_err(|e| format!("failed to parse response JSON: {}", e))?;
|
||||
|
||||
if let Some(err) = body.get("error") {
|
||||
return Err(format!("sr-voice error: {}", err));
|
||||
}
|
||||
|
||||
body["text"]
|
||||
.as_str()
|
||||
.map(|s| s.trim().to_string())
|
||||
.ok_or_else(|| "response missing 'text' field".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VoicePipe {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.child.kill();
|
||||
let _ = self.child.wait();
|
||||
}
|
||||
}
|
||||
|
||||
/// Main worker loop: spawn sr-voice child, receive requests, process them.
|
||||
fn worker_loop(id: usize, config: VoiceProcessConfig, ctx: WorkerContext) {
|
||||
tracing::debug!(id, "voice worker starting sr-voice child process");
|
||||
|
||||
let mut pipe = match VoicePipe::spawn(&config) {
|
||||
Ok(p) => {
|
||||
tracing::info!(id, "voice worker connected to sr-voice via stdio");
|
||||
p
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(id, error = %e, "voice worker failed to spawn sr-voice — exiting");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
if ctx.shutdown.load(Ordering::SeqCst) {
|
||||
@@ -145,23 +241,22 @@ fn worker_loop(id: usize, port: u16, ctx: WorkerContext) {
|
||||
|
||||
// Skip while paused (zone transition)
|
||||
if ctx.paused.load(Ordering::Relaxed) {
|
||||
// Re-queue the request — it wasn't consumed
|
||||
let _ = ctx.receiver.clone(); // can't re-send, just drop during pause
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx.active_count.fetch_add(1, Ordering::Relaxed);
|
||||
process_request(id, &base_url, &request, &ctx);
|
||||
process_request(id, &mut pipe, &request, &ctx);
|
||||
ctx.active_count.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
tracing::debug!(id, "voice worker stopped");
|
||||
// VoicePipe::drop kills the child process
|
||||
}
|
||||
|
||||
/// Process a single voice request: build prompt → infer → validate → cache.
|
||||
fn process_request(
|
||||
worker_id: usize,
|
||||
base_url: &str,
|
||||
pipe: &mut VoicePipe,
|
||||
request: &VoiceRequest,
|
||||
ctx: &WorkerContext,
|
||||
) {
|
||||
@@ -186,8 +281,8 @@ fn process_request(
|
||||
request.seed,
|
||||
);
|
||||
|
||||
// Call sr-voice
|
||||
let result = call_sr_voice(base_url, &built.prompt);
|
||||
// Call sr-voice via stdio pipe
|
||||
let result = pipe.generate(&built.prompt, Some(request.seed));
|
||||
|
||||
match result {
|
||||
Ok(text) if text.split_whitespace().count() >= MIN_TOKENS => {
|
||||
@@ -207,12 +302,11 @@ fn process_request(
|
||||
request.tell_state,
|
||||
request.seed.wrapping_add(1),
|
||||
);
|
||||
match call_sr_voice(base_url, &retry_built.prompt) {
|
||||
match pipe.generate(&retry_built.prompt, Some(request.seed.wrapping_add(1))) {
|
||||
Ok(text) if text.split_whitespace().count() >= MIN_TOKENS => {
|
||||
cache_result(request, &text, ctx);
|
||||
}
|
||||
_ => {
|
||||
// Graceful degradation: cache base text
|
||||
tracing::debug!(
|
||||
worker_id,
|
||||
npc = request.npc_stable_id,
|
||||
@@ -233,34 +327,6 @@ fn process_request(
|
||||
}
|
||||
}
|
||||
|
||||
/// POST to sr-voice /generate endpoint and return the generated text.
|
||||
fn call_sr_voice(base_url: &str, prompt: &str) -> Result<String, String> {
|
||||
let url = format!("{}/generate", base_url);
|
||||
|
||||
let payload = serde_json::json!({ "prompt": prompt });
|
||||
|
||||
let response = ureq::AgentBuilder::new()
|
||||
.timeout(INFERENCE_TIMEOUT)
|
||||
.build()
|
||||
.post(&url)
|
||||
.send_json(payload);
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
let body_str = resp
|
||||
.into_string()
|
||||
.map_err(|e| format!("failed to read response: {}", e))?;
|
||||
let body: serde_json::Value = serde_json::from_str(&body_str)
|
||||
.map_err(|e| format!("failed to parse JSON: {}", e))?;
|
||||
body["text"]
|
||||
.as_str()
|
||||
.map(|s| s.trim().to_string())
|
||||
.ok_or_else(|| "response missing 'text' field".to_string())
|
||||
}
|
||||
Err(e) => Err(format!("HTTP error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache the inference result.
|
||||
fn cache_result(request: &VoiceRequest, text: &str, ctx: &WorkerContext) {
|
||||
let key = cache_key_from_request(request);
|
||||
|
||||
Reference in New Issue
Block a user