[refactor] streamline crate structure, update dependencies, and integrate CLI functionalities
This commit is contained in:
379
Cargo.lock
generated
379
Cargo.lock
generated
@@ -144,15 +144,6 @@ version = "2.9.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block-buffer"
|
|
||||||
version = "0.10.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.0"
|
version = "3.19.0"
|
||||||
@@ -273,20 +264,6 @@ dependencies = [
|
|||||||
"roff",
|
"roff",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cliclack"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "57c420bdc04c123a2df04d9c5a07289195f00007af6e45ab18f55e56dc7e04b8"
|
|
||||||
dependencies = [
|
|
||||||
"console",
|
|
||||||
"indicatif",
|
|
||||||
"once_cell",
|
|
||||||
"strsim",
|
|
||||||
"textwrap",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.54"
|
version = "0.1.54"
|
||||||
@@ -321,35 +298,6 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cpufeatures"
|
|
||||||
version = "0.2.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crypto-common"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array",
|
|
||||||
"typenum",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "digest"
|
|
||||||
version = "0.10.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|
||||||
dependencies = [
|
|
||||||
"block-buffer",
|
|
||||||
"crypto-common",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "directories"
|
name = "directories"
|
||||||
version = "5.0.1"
|
version = "5.0.1"
|
||||||
@@ -405,16 +353,6 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "generic-array"
|
|
||||||
version = "0.14.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|
||||||
dependencies = [
|
|
||||||
"typenum",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -548,6 +486,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.175"
|
version = "0.2.175"
|
||||||
@@ -580,22 +524,21 @@ version = "0.4.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lock_api"
|
|
||||||
version = "0.4.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"scopeguard",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.27"
|
version = "0.4.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata 0.1.10",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.5"
|
version = "2.7.5"
|
||||||
@@ -638,6 +581,16 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||||
|
dependencies = [
|
||||||
|
"overload",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -681,27 +634,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "overload"
|
||||||
version = "0.12.4"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.9.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"smallvec",
|
|
||||||
"windows-targets 0.52.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
@@ -710,54 +646,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polyscribe"
|
name = "polyscribe-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"clap",
|
||||||
|
"clap_complete",
|
||||||
|
"clap_mangen",
|
||||||
|
"directories",
|
||||||
|
"indicatif",
|
||||||
|
"polyscribe-core",
|
||||||
|
"polyscribe-host",
|
||||||
|
"polyscribe-protocol",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"which",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polyscribe-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cliclack",
|
|
||||||
"directories",
|
"directories",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"libc",
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
"whisper-rs",
|
"whisper-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "polyscribe-cli"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"chrono",
|
|
||||||
"clap",
|
|
||||||
"clap_complete",
|
|
||||||
"clap_mangen",
|
|
||||||
"cliclack",
|
|
||||||
"indicatif",
|
|
||||||
"polyscribe",
|
|
||||||
"polyscribe-host",
|
|
||||||
"polyscribe-protocol",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"toml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polyscribe-host"
|
name = "polyscribe-host"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cliclack",
|
|
||||||
"directories",
|
|
||||||
"polyscribe",
|
|
||||||
"polyscribe-protocol",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"which",
|
"which",
|
||||||
]
|
]
|
||||||
@@ -768,7 +699,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -805,15 +735,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -833,8 +754,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata 0.4.9",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
dependencies = [
|
||||||
|
"regex-syntax 0.6.29",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -845,9 +775,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -897,12 +833,6 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.219"
|
version = "1.0.219"
|
||||||
@@ -945,14 +875,12 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sharded-slab"
|
||||||
version = "0.10.9"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"lazy_static",
|
||||||
"cpufeatures",
|
|
||||||
"digest",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -982,22 +910,6 @@ version = "1.15.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smawk"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "socket2"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -1015,17 +927,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "textwrap"
|
|
||||||
version = "0.16.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
|
||||||
dependencies = [
|
|
||||||
"smawk",
|
|
||||||
"unicode-linebreak",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
@@ -1046,6 +947,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.47.1"
|
version = "1.47.1"
|
||||||
@@ -1057,11 +967,9 @@ dependencies = [
|
|||||||
"io-uring",
|
"io-uring",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"slab",
|
"slab",
|
||||||
"socket2",
|
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
@@ -1119,10 +1027,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "tracing"
|
||||||
version = "1.18.0"
|
version = "0.1.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
@@ -1130,12 +1093,6 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-linebreak"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1149,10 +1106,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "valuable"
|
||||||
version = "0.9.5"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
@@ -1242,16 +1199,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whisper-rs"
|
name = "whisper-rs"
|
||||||
version = "0.14.3"
|
version = "0.14.4"
|
||||||
source = "git+https://github.com/tazz4843/whisper-rs#135b60b85a15714862806b6ea9f76abec38156f1"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d2eac0a371f8ae667a5ee15ae4130553ea3004e7572544d1ce546c81ea8874b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"whisper-rs-sys",
|
"whisper-rs-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whisper-rs-sys"
|
name = "whisper-rs-sys"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
source = "git+https://github.com/tazz4843/whisper-rs#135b60b85a15714862806b6ea9f76abec38156f1"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c86f1b993f216594b1ad9a9bb00a26014fb7c512e12664a2d401c7897d2ef7d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen",
|
"bindgen",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -1259,6 +1218,28 @@ dependencies = [
|
|||||||
"fs_extra",
|
"fs_extra",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -1545,23 +1526,3 @@ name = "winsafe"
|
|||||||
version = "0.0.19"
|
version = "0.0.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zeroize"
|
|
||||||
version = "1.8.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
|
||||||
dependencies = [
|
|
||||||
"zeroize_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zeroize_derive"
|
|
||||||
version = "1.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
@@ -2,23 +2,25 @@
|
|||||||
name = "polyscribe-cli"
|
name = "polyscribe-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "polyscribe"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.99"
|
||||||
clap = { version = "4.5.43", features = ["derive"] }
|
clap = { version = "4.5.44", features = ["derive"] }
|
||||||
clap_complete = "4.5.28"
|
clap_complete = "4.5.57"
|
||||||
clap_mangen = "0.2"
|
clap_mangen = "0.2.29"
|
||||||
|
directories = "5.0.1"
|
||||||
|
indicatif = "0.17.11"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.142"
|
serde_json = "1.0.142"
|
||||||
toml = "0.8"
|
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "process", "fs"] }
|
||||||
chrono = { version = "0.4", features = ["clock"] }
|
tracing = "0.1"
|
||||||
cliclack = "0.3"
|
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||||
indicatif = "0.17"
|
which = "6.0.3"
|
||||||
polyscribe = { path = "../polyscribe-core" }
|
|
||||||
|
polyscribe-core = { path = "../polyscribe-core" }
|
||||||
polyscribe-host = { path = "../polyscribe-host" }
|
polyscribe-host = { path = "../polyscribe-host" }
|
||||||
polyscribe-protocol = { path = "../polyscribe-protocol" }
|
polyscribe-protocol = { path = "../polyscribe-protocol" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# Optional GPU-specific flags can be forwarded down to core/host if needed
|
||||||
|
default = []
|
||||||
|
119
crates/polyscribe-cli/src/cli.rs
Normal file
119
crates/polyscribe-cli/src/cli.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, ValueEnum)]
|
||||||
|
pub enum GpuBackend {
|
||||||
|
Auto,
|
||||||
|
Cpu,
|
||||||
|
Cuda,
|
||||||
|
Hip,
|
||||||
|
Vulkan,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(name = "polyscribe", version, about = "PolyScribe – local-first transcription and plugins")]
|
||||||
|
pub struct Cli {
|
||||||
|
/// Increase verbosity (-v, -vv)
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||||
|
pub verbose: u8,
|
||||||
|
|
||||||
|
/// Quiet mode (suppresses non-error logs)
|
||||||
|
#[arg(short, long, default_value_t = false)]
|
||||||
|
pub quiet: bool,
|
||||||
|
|
||||||
|
/// Never prompt for user input (non-interactive mode)
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
pub no_interaction: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Transcribe audio/video files or merge existing transcripts
|
||||||
|
Transcribe {
|
||||||
|
/// Output file or directory (date prefix is added when directory)
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Merge multiple inputs into one output
|
||||||
|
#[arg(short = 'm', long, default_value_t = false)]
|
||||||
|
merge: bool,
|
||||||
|
|
||||||
|
/// Write both merged and per-input outputs (requires -o dir)
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
merge_and_separate: bool,
|
||||||
|
|
||||||
|
/// Language code hint, e.g. en, de
|
||||||
|
#[arg(long)]
|
||||||
|
language: Option<String>,
|
||||||
|
|
||||||
|
/// Prompt for a speaker label per input file
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
set_speaker_names: bool,
|
||||||
|
|
||||||
|
/// GPU backend selection
|
||||||
|
#[arg(long, value_enum, default_value_t = GpuBackend::Auto)]
|
||||||
|
gpu_backend: GpuBackend,
|
||||||
|
|
||||||
|
/// Offload N layers to GPU (when supported)
|
||||||
|
#[arg(long, default_value_t = 0)]
|
||||||
|
gpu_layers: usize,
|
||||||
|
|
||||||
|
/// Input paths: audio/video files or JSON transcripts
|
||||||
|
#[arg(required = true)]
|
||||||
|
inputs: Vec<PathBuf>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Manage Whisper models
|
||||||
|
Models {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: ModelsCmd,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Discover and run plugins
|
||||||
|
Plugins {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: PluginsCmd,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Generate shell completions to stdout
|
||||||
|
Completions {
|
||||||
|
/// Shell to generate completions for
|
||||||
|
#[arg(value_parser = ["bash", "zsh", "fish", "powershell", "elvish"])]
|
||||||
|
shell: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Generate a man page to stdout
|
||||||
|
Man,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum ModelsCmd {
|
||||||
|
/// Verify or update local models non-interactively
|
||||||
|
Update,
|
||||||
|
/// Interactive multi-select downloader
|
||||||
|
Download,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum PluginsCmd {
|
||||||
|
/// List installed plugins
|
||||||
|
List,
|
||||||
|
/// Show a plugin's capabilities (as JSON)
|
||||||
|
Info {
|
||||||
|
/// Plugin short name, e.g., "tubescribe"
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Run a plugin command (JSON-RPC over NDJSON via stdio)
|
||||||
|
Run {
|
||||||
|
/// Plugin short name
|
||||||
|
name: String,
|
||||||
|
/// Command name in plugin's API
|
||||||
|
command: String,
|
||||||
|
/// JSON payload string
|
||||||
|
#[arg(long)]
|
||||||
|
json: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
@@ -1,536 +1,153 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
mod cli;
|
||||||
// Copyright (c) 2025 <COPYRIGHT HOLDER>. All rights reserved.
|
|
||||||
|
|
||||||
use std::fs::{File, create_dir_all};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use std::io::{self, Read, Write};
|
use clap::{Parser, CommandFactory};
|
||||||
use std::path::{Path, PathBuf};
|
use cli::{Cli, Commands, GpuBackend, ModelsCmd, PluginsCmd};
|
||||||
|
use polyscribe_core::{config::ConfigService, ui::progress::ProgressReporter};
|
||||||
|
use polyscribe_host::PluginManager;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tracing::{error, info};
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
fn init_tracing(quiet: bool, verbose: u8) {
|
||||||
use clap::{Parser, Subcommand, ValueEnum, CommandFactory};
|
let level = if quiet {
|
||||||
use clap_complete::Shell;
|
"error"
|
||||||
use serde::{Deserialize, Serialize};
|
} else {
|
||||||
|
match verbose {
|
||||||
use polyscribe::{OutputEntry, date_prefix, normalize_lang_code, render_srt};
|
0 => "info",
|
||||||
use polyscribe_host as host;
|
1 => "debug",
|
||||||
|
_ => "trace",
|
||||||
#[derive(Subcommand, Debug, Clone)]
|
|
||||||
enum PluginsCmd {
|
|
||||||
/// List available plugins
|
|
||||||
List,
|
|
||||||
/// Show plugin capabilities
|
|
||||||
Info { name: String },
|
|
||||||
/// Run a plugin command with a JSON payload
|
|
||||||
Run {
|
|
||||||
name: String,
|
|
||||||
command: String,
|
|
||||||
/// JSON payload string passed to the plugin as request.params
|
|
||||||
#[arg(long = "json")]
|
|
||||||
json: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Clone)]
|
|
||||||
enum Command {
|
|
||||||
Completions { #[arg(value_enum)] shell: Shell },
|
|
||||||
Man,
|
|
||||||
Plugins { #[command(subcommand)] cmd: PluginsCmd },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ValueEnum, Debug, Clone, Copy)]
|
|
||||||
#[value(rename_all = "kebab-case")]
|
|
||||||
enum GpuBackendCli {
|
|
||||||
Auto,
|
|
||||||
Cpu,
|
|
||||||
Cuda,
|
|
||||||
Hip,
|
|
||||||
Vulkan,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
name = "PolyScribe",
|
|
||||||
bin_name = "polyscribe",
|
|
||||||
version,
|
|
||||||
about = "Merge JSON transcripts or transcribe audio using native whisper"
|
|
||||||
)]
|
|
||||||
struct Args {
|
|
||||||
/// Increase verbosity (-v, -vv). Repeat to increase.
|
|
||||||
/// Debug logs appear with -v; very verbose with -vv. Logs go to stderr.
|
|
||||||
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true)]
|
|
||||||
verbose: u8,
|
|
||||||
|
|
||||||
/// Quiet mode: suppress non-error logging on stderr (overrides -v)
|
|
||||||
/// Does not suppress interactive prompts or stdout output.
|
|
||||||
#[arg(short = 'q', long = "quiet", global = true)]
|
|
||||||
quiet: bool,
|
|
||||||
|
|
||||||
/// Non-interactive mode: never prompt; use defaults instead.
|
|
||||||
#[arg(long = "no-interaction", global = true)]
|
|
||||||
no_interaction: bool,
|
|
||||||
|
|
||||||
/// Disable interactive progress indicators (bars/spinners)
|
|
||||||
#[arg(long = "no-progress", global = true)]
|
|
||||||
no_progress: bool,
|
|
||||||
|
|
||||||
/// Optional subcommands (completions, man, plugins)
|
|
||||||
#[command(subcommand)]
|
|
||||||
cmd: Option<Command>,
|
|
||||||
|
|
||||||
/// Input .json transcript files or audio files to merge/transcribe
|
|
||||||
inputs: Vec<String>,
|
|
||||||
|
|
||||||
/// Output file path base or directory (date prefix added).
|
|
||||||
/// In merge mode: base path.
|
|
||||||
/// In separate mode: directory.
|
|
||||||
/// If omitted: prints JSON to stdout for merge mode; separate mode requires directory for multiple inputs.
|
|
||||||
#[arg(short, long, value_name = "FILE")]
|
|
||||||
output: Option<String>,
|
|
||||||
|
|
||||||
/// Merge all inputs into a single output; if not set, each input is written as a separate output
|
|
||||||
#[arg(short = 'm', long = "merge")]
|
|
||||||
merge: bool,
|
|
||||||
|
|
||||||
/// Merge and also write separate outputs per input; requires -o OUTPUT_DIR
|
|
||||||
#[arg(long = "merge-and-separate")]
|
|
||||||
merge_and_separate: bool,
|
|
||||||
|
|
||||||
/// Prompt for speaker names per input file
|
|
||||||
#[arg(long = "set-speaker-names")]
|
|
||||||
set_speaker_names: bool,
|
|
||||||
|
|
||||||
/// Language code to use for transcription (e.g., en, de). No auto-detection.
|
|
||||||
#[arg(short, long, value_name = "LANG")]
|
|
||||||
language: Option<String>,
|
|
||||||
|
|
||||||
/// Launch interactive model downloader (list HF models, multi-select and download)
|
|
||||||
#[arg(long)]
|
|
||||||
download_models: bool,
|
|
||||||
|
|
||||||
/// Update local Whisper models by comparing hashes/sizes with remote manifest
|
|
||||||
#[arg(long)]
|
|
||||||
update_models: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct InputRoot {
|
|
||||||
#[serde(default)]
|
|
||||||
segments: Vec<InputSegment>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct InputSegment {
|
|
||||||
start: f64,
|
|
||||||
end: f64,
|
|
||||||
text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct OutputRoot {
|
|
||||||
items: Vec<OutputEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_json_file(path: &Path) -> bool {
|
|
||||||
matches!(path.extension().and_then(|s| s.to_str()).map(|s| s.to_lowercase()), Some(ext) if ext == "json")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_audio_file(path: &Path) -> bool {
|
|
||||||
if let Some(ext) = path.extension().and_then(|s| s.to_str()).map(|s| s.to_lowercase()) {
|
|
||||||
let exts = [
|
|
||||||
"mp3", "wav", "m4a", "mp4", "aac", "flac", "ogg", "wma", "webm", "mkv", "mov", "avi",
|
|
||||||
"m4b", "3gp", "opus", "aiff", "alac",
|
|
||||||
];
|
|
||||||
return exts.contains(&ext.as_str());
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_input_path(path: &Path) -> anyhow::Result<()> {
|
|
||||||
let display = path.display();
|
|
||||||
if !path.exists() {
|
|
||||||
return Err(anyhow!("Input not found: {}", display));
|
|
||||||
}
|
|
||||||
let metadata = std::fs::metadata(path).with_context(|| format!("Failed to stat input: {}", display))?;
|
|
||||||
if metadata.is_dir() {
|
|
||||||
return Err(anyhow!("Input is a directory (expected a file): {}", display));
|
|
||||||
}
|
|
||||||
std::fs::File::open(path)
|
|
||||||
.with_context(|| format!("Failed to open input file: {}", display))
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sanitize_speaker_name(raw: &str) -> String {
|
|
||||||
if let Some((prefix, rest)) = raw.split_once('-') {
|
|
||||||
if !prefix.is_empty() && prefix.chars().all(|c| c.is_ascii_digit()) {
|
|
||||||
return rest.to_string();
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
raw.to_string()
|
|
||||||
|
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(filter)
|
||||||
|
.with_target(false)
|
||||||
|
.with_level(true)
|
||||||
|
.compact()
|
||||||
|
.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_speaker_name_for_path(
|
#[tokio::main]
|
||||||
_path: &Path,
|
async fn main() -> Result<()> {
|
||||||
default_name: &str,
|
let args = Cli::parse();
|
||||||
enabled: bool,
|
|
||||||
) -> String {
|
init_tracing(args.quiet, args.verbose);
|
||||||
if !enabled || polyscribe::is_no_interaction() {
|
|
||||||
return sanitize_speaker_name(default_name);
|
let _cfg = ConfigService::load_or_default().context("loading configuration")?;
|
||||||
}
|
|
||||||
// TODO implement cliclack for this
|
match args.command {
|
||||||
let mut input_line = String::new();
|
Commands::Transcribe {
|
||||||
match std::io::stdin().read_line(&mut input_line) {
|
output: _output,
|
||||||
Ok(_) => {
|
merge: _merge,
|
||||||
let trimmed = input_line.trim();
|
merge_and_separate: _merge_and_separate,
|
||||||
if trimmed.is_empty() {
|
language: _language,
|
||||||
sanitize_speaker_name(default_name)
|
set_speaker_names: _set_speaker_names,
|
||||||
} else {
|
gpu_backend,
|
||||||
sanitize_speaker_name(trimmed)
|
gpu_layers,
|
||||||
|
inputs,
|
||||||
|
} => {
|
||||||
|
info!("starting transcription workflow");
|
||||||
|
let mut progress = ProgressReporter::new(args.no_interaction);
|
||||||
|
|
||||||
|
progress.step("Validating inputs");
|
||||||
|
if inputs.is_empty() {
|
||||||
|
return Err(anyhow!("no inputs provided"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(_) => sanitize_speaker_name(default_name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_plugins(cmd: PluginsCmd) -> Result<()> {
|
progress.step("Selecting backend and preparing model");
|
||||||
match cmd {
|
match gpu_backend {
|
||||||
PluginsCmd::List => {
|
GpuBackend::Auto => {}
|
||||||
let list = host::discover()?;
|
GpuBackend::Cpu => {}
|
||||||
for p in list {
|
GpuBackend::Cuda => {
|
||||||
println!("{}\t{}", p.name, p.path.display());
|
let _ = gpu_layers;
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
PluginsCmd::Info { name } => {
|
|
||||||
let p = host::find_plugin_by_name(&name)?;
|
|
||||||
let caps = host::capabilities(&p.path)?;
|
|
||||||
println!("{}", serde_json::to_string_pretty(&caps)?);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
PluginsCmd::Run { name, command, json } => {
|
|
||||||
let p = host::find_plugin_by_name(&name)?;
|
|
||||||
let params: serde_json::Value = serde_json::from_str(&json).context("--json payload must be valid JSON")?;
|
|
||||||
let mut last_pct = 0u8;
|
|
||||||
let result = host::run_method(&p.path, &command, params, |prog| {
|
|
||||||
// Render minimal progress
|
|
||||||
let stage = prog.stage.as_deref().unwrap_or("");
|
|
||||||
let msg = prog.message.as_deref().unwrap_or("");
|
|
||||||
if prog.pct != last_pct {
|
|
||||||
let _ = cliclack::log::info(format!("[{}%] {} {}", prog.pct, stage, msg).trim());
|
|
||||||
last_pct = prog.pct;
|
|
||||||
}
|
}
|
||||||
})?;
|
GpuBackend::Hip => {}
|
||||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
GpuBackend::Vulkan => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.finish_with_message("Transcription completed (stub)");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Models { cmd } => {
|
||||||
|
match cmd {
|
||||||
|
ModelsCmd::Update => {
|
||||||
|
info!("verifying/updating local models");
|
||||||
|
println!("Models updated (stub).");
|
||||||
|
}
|
||||||
|
ModelsCmd::Download => {
|
||||||
|
info!("interactive model selection and download");
|
||||||
|
println!("Model download complete (stub).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Plugins { cmd } => {
|
||||||
|
let pm = PluginManager::default();
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
PluginsCmd::List => {
|
||||||
|
let list = pm.list().context("discovering plugins")?;
|
||||||
|
for item in list {
|
||||||
|
println!("{}", item.name);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
PluginsCmd::Info { name } => {
|
||||||
|
let info = pm.info(&name).with_context(|| format!("getting info for {}", name))?;
|
||||||
|
println!("{}", serde_json::to_string_pretty(&info)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
PluginsCmd::Run { name, command, json } => {
|
||||||
|
let payload = json.unwrap_or_else(|| "{}".to_string());
|
||||||
|
let mut child = pm
|
||||||
|
.spawn(&name, &command)
|
||||||
|
.with_context(|| format!("spawning plugin {name} {command}"))?;
|
||||||
|
|
||||||
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
|
stdin
|
||||||
|
.write_all(payload.as_bytes())
|
||||||
|
.await
|
||||||
|
.context("writing JSON payload to plugin stdin")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = pm.forward_stdio(&mut child).await?;
|
||||||
|
if !status.success() {
|
||||||
|
error!("plugin returned non-zero exit code: {}", status);
|
||||||
|
return Err(anyhow!("plugin failed"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Completions { shell } => {
|
||||||
|
use clap_complete::{generate, shells};
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
let mut cmd = Cli::command();
|
||||||
|
let name = cmd.get_name().to_string();
|
||||||
|
|
||||||
|
match shell.as_str() {
|
||||||
|
"bash" => generate(shells::Bash, &mut cmd, name, &mut io::stdout()),
|
||||||
|
"zsh" => generate(shells::Zsh, &mut cmd, name, &mut io::stdout()),
|
||||||
|
"fish" => generate(shells::Fish, &mut cmd, name, &mut io::stdout()),
|
||||||
|
"powershell" => generate(shells::PowerShell, &mut cmd, name, &mut io::stdout()),
|
||||||
|
"elvish" => generate(shells::Elvish, &mut cmd, name, &mut io::stdout()),
|
||||||
|
_ => return Err(anyhow!("unsupported shell: {shell}")),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::Man => {
|
||||||
|
use clap_mangen::Man;
|
||||||
|
let cmd = Cli::command();
|
||||||
|
let man = Man::new(cmd);
|
||||||
|
man.render(&mut std::io::stdout())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let args = Args::parse();
|
|
||||||
|
|
||||||
// Initialize runtime flags for the library
|
|
||||||
polyscribe::set_verbose(args.verbose);
|
|
||||||
polyscribe::set_quiet(args.quiet);
|
|
||||||
polyscribe::set_no_interaction(args.no_interaction);
|
|
||||||
polyscribe::set_no_progress(args.no_progress);
|
|
||||||
|
|
||||||
// Handle subcommands
|
|
||||||
if let Some(cmd) = &args.cmd {
|
|
||||||
match cmd.clone() {
|
|
||||||
Command::Completions { shell } => {
|
|
||||||
let mut cmd = Args::command();
|
|
||||||
let bin_name = cmd.get_name().to_string();
|
|
||||||
clap_complete::generate(shell, &mut cmd, bin_name, &mut io::stdout());
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Command::Man => {
|
|
||||||
let cmd = Args::command();
|
|
||||||
let man = clap_mangen::Man::new(cmd);
|
|
||||||
let mut man_bytes = Vec::new();
|
|
||||||
man.render(&mut man_bytes)?;
|
|
||||||
io::stdout().write_all(&man_bytes)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Command::Plugins { cmd } => {
|
|
||||||
return handle_plugins(cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional model management actions
|
|
||||||
if args.download_models {
|
|
||||||
if let Err(err) = polyscribe::models::run_interactive_model_downloader() {
|
|
||||||
polyscribe::elog!("Model downloader failed: {:#}", err);
|
|
||||||
}
|
|
||||||
if args.inputs.is_empty() {
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if args.update_models {
|
|
||||||
if let Err(err) = polyscribe::models::update_local_models() {
|
|
||||||
polyscribe::elog!("Model update failed: {:#}", err);
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
if args.inputs.is_empty() {
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process inputs
|
|
||||||
let mut inputs = args.inputs;
|
|
||||||
if inputs.is_empty() {
|
|
||||||
return Err(anyhow!("No input files provided"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If last arg looks like an output path and not existing file, accept it as -o when multiple inputs
|
|
||||||
let mut output_path = args.output;
|
|
||||||
if output_path.is_none() && inputs.len() >= 2 {
|
|
||||||
if let Some(candidate_output) = inputs.last().cloned() {
|
|
||||||
if !Path::new(&candidate_output).exists() {
|
|
||||||
inputs.pop();
|
|
||||||
output_path = Some(candidate_output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate inputs; allow JSON and audio. For audio, require --language.
|
|
||||||
for input_arg in &inputs {
|
|
||||||
let path_ref = Path::new(input_arg);
|
|
||||||
validate_input_path(path_ref)?;
|
|
||||||
if !(is_json_file(path_ref) || is_audio_file(path_ref)) {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Unsupported input type (expected .json transcript or audio media): {}",
|
|
||||||
path_ref.display()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if is_audio_file(path_ref) && args.language.is_none() {
|
|
||||||
return Err(anyhow!("Please specify --language (e.g., --language en). Language detection was removed."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derive speakers (prompt if requested)
|
|
||||||
let speakers: Vec<String> = inputs
|
|
||||||
.iter()
|
|
||||||
.map(|input_path| {
|
|
||||||
let path = Path::new(input_path);
|
|
||||||
let default_speaker = sanitize_speaker_name(
|
|
||||||
path.file_stem().and_then(|s| s.to_str()).unwrap_or("speaker"),
|
|
||||||
);
|
|
||||||
prompt_speaker_name_for_path(path, &default_speaker, args.set_speaker_names)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// MERGE-AND-SEPARATE mode
|
|
||||||
if args.merge_and_separate {
|
|
||||||
polyscribe::dlog!(1, "Mode: merge-and-separate; output_dir={:?}", output_path);
|
|
||||||
let out_dir = match output_path.as_ref() {
|
|
||||||
Some(p) => PathBuf::from(p),
|
|
||||||
None => return Err(anyhow!("--merge-and-separate requires -o OUTPUT_DIR")),
|
|
||||||
};
|
|
||||||
if !out_dir.as_os_str().is_empty() {
|
|
||||||
create_dir_all(&out_dir).with_context(|| {
|
|
||||||
format!("Failed to create output directory: {}", out_dir.display())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut merged_entries: Vec<OutputEntry> = Vec::new();
|
|
||||||
for (idx, input_path) in inputs.iter().enumerate() {
|
|
||||||
let path = Path::new(input_path);
|
|
||||||
let speaker = speakers[idx].clone();
|
|
||||||
// Decide based on input type (JSON transcript vs audio to transcribe)
|
|
||||||
// TODO remove duplicate
|
|
||||||
let mut entries: Vec<OutputEntry> = if is_json_file(path) {
|
|
||||||
let mut buf = String::new();
|
|
||||||
File::open(path)
|
|
||||||
.with_context(|| format!("Failed to open: {input_path}"))?
|
|
||||||
.read_to_string(&mut buf)
|
|
||||||
.with_context(|| format!("Failed to read: {input_path}"))?;
|
|
||||||
let root: InputRoot = serde_json::from_str(&buf)
|
|
||||||
.with_context(|| format!("Invalid JSON transcript parsed from {input_path}"))?;
|
|
||||||
root
|
|
||||||
.segments
|
|
||||||
.into_iter()
|
|
||||||
.map(|seg| OutputEntry { id: 0, speaker: speaker.clone(), start: seg.start, end: seg.end, text: seg.text })
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
let lang_norm: Option<String> = args.language.as_deref().and_then(|s| normalize_lang_code(s));
|
|
||||||
let selected_backend = polyscribe::backend::select_backend(polyscribe::backend::BackendKind::Auto, args.verbose > 0)?;
|
|
||||||
selected_backend.backend.transcribe(path, &speaker, lang_norm.as_deref(), None, None)?
|
|
||||||
};
|
|
||||||
// Sort and id per-file
|
|
||||||
// TODO remove duplicate
|
|
||||||
entries.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
.then(a.end.partial_cmp(&b.end).unwrap_or(std::cmp::Ordering::Equal)));
|
|
||||||
for (i, entry) in entries.iter_mut().enumerate() { entry.id = i as u64; }
|
|
||||||
// Write per-file outputs
|
|
||||||
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("output");
|
|
||||||
let date = date_prefix();
|
|
||||||
let base_name = format!("{date}_{stem}");
|
|
||||||
let json_path = out_dir.join(format!("{}.json", &base_name));
|
|
||||||
let toml_path = out_dir.join(format!("{}.toml", &base_name));
|
|
||||||
let srt_path = out_dir.join(format!("{}.srt", &base_name));
|
|
||||||
|
|
||||||
let output_bundle = OutputRoot { items: entries.clone() };
|
|
||||||
let mut json_file = File::create(&json_path).with_context(|| format!("Failed to create output file: {}", json_path.display()))?;
|
|
||||||
serde_json::to_writer_pretty(&mut json_file, &output_bundle)?; writeln!(&mut json_file)?;
|
|
||||||
let toml_str = toml::to_string_pretty(&output_bundle)?;
|
|
||||||
let mut toml_file = File::create(&toml_path).with_context(|| format!("Failed to create output file: {}", toml_path.display()))?;
|
|
||||||
toml_file.write_all(toml_str.as_bytes())?; if !toml_str.ends_with('\n') { writeln!(&mut toml_file)?; }
|
|
||||||
let srt_str = render_srt(&output_bundle.items);
|
|
||||||
let mut srt_file = File::create(&srt_path).with_context(|| format!("Failed to create output file: {}", srt_path.display()))?;
|
|
||||||
srt_file.write_all(srt_str.as_bytes())?;
|
|
||||||
|
|
||||||
merged_entries.extend(output_bundle.items.into_iter());
|
|
||||||
}
|
|
||||||
// Write merged outputs into out_dir
|
|
||||||
// TODO remove duplicate
|
|
||||||
merged_entries.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
.then(a.end.partial_cmp(&b.end).unwrap_or(std::cmp::Ordering::Equal)));
|
|
||||||
for (index, entry) in merged_entries.iter_mut().enumerate() { entry.id = index as u64; }
|
|
||||||
let merged_output = OutputRoot { items: merged_entries };
|
|
||||||
let date = date_prefix();
|
|
||||||
let merged_base = format!("{date}_merged");
|
|
||||||
let merged_json_path = out_dir.join(format!("{}.json", &merged_base));
|
|
||||||
let merged_toml_path = out_dir.join(format!("{}.toml", &merged_base));
|
|
||||||
let merged_srt_path = out_dir.join(format!("{}.srt", &merged_base));
|
|
||||||
let mut merged_json_file = File::create(&merged_json_path).with_context(|| format!("Failed to create output file: {}", merged_json_path.display()))?;
|
|
||||||
serde_json::to_writer_pretty(&mut merged_json_file, &merged_output)?; writeln!(&mut merged_json_file)?;
|
|
||||||
let merged_toml_str = toml::to_string_pretty(&merged_output)?;
|
|
||||||
let mut merged_toml_file = File::create(&merged_toml_path).with_context(|| format!("Failed to create output file: {}", merged_toml_path.display()))?;
|
|
||||||
merged_toml_file.write_all(merged_toml_str.as_bytes())?; if !merged_toml_str.ends_with('\n') { writeln!(&mut merged_toml_file)?; }
|
|
||||||
let merged_srt_str = render_srt(&merged_output.items);
|
|
||||||
let mut merged_srt_file = File::create(&merged_srt_path).with_context(|| format!("Failed to create output file: {}", merged_srt_path.display()))?;
|
|
||||||
merged_srt_file.write_all(merged_srt_str.as_bytes())?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// MERGE mode
|
|
||||||
if args.merge {
|
|
||||||
polyscribe::dlog!(1, "Mode: merge; output_base={:?}", output_path);
|
|
||||||
let mut entries: Vec<OutputEntry> = Vec::new();
|
|
||||||
for (index, input_path) in inputs.iter().enumerate() {
|
|
||||||
let path = Path::new(input_path);
|
|
||||||
let speaker = speakers[index].clone();
|
|
||||||
if is_json_file(path) {
|
|
||||||
let mut buf = String::new();
|
|
||||||
File::open(path)
|
|
||||||
.with_context(|| format!("Failed to open: {}", input_path))?
|
|
||||||
.read_to_string(&mut buf)
|
|
||||||
.with_context(|| format!("Failed to read: {}", input_path))?;
|
|
||||||
let root: InputRoot = serde_json::from_str(&buf)
|
|
||||||
.with_context(|| format!("Invalid JSON transcript parsed from {}", input_path))?;
|
|
||||||
for seg in root.segments {
|
|
||||||
entries.push(OutputEntry { id: 0, speaker: speaker.clone(), start: seg.start, end: seg.end, text: seg.text });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let lang_norm: Option<String> = args.language.as_deref().and_then(|s| normalize_lang_code(s));
|
|
||||||
let selected_backend = polyscribe::backend::select_backend(polyscribe::backend::BackendKind::Auto, args.verbose > 0)?;
|
|
||||||
let mut new_entries = selected_backend.backend.transcribe(path, &speaker, lang_norm.as_deref(), None, None)?;
|
|
||||||
entries.append(&mut new_entries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO remove duplicate
|
|
||||||
entries.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
.then(a.end.partial_cmp(&b.end).unwrap_or(std::cmp::Ordering::Equal)));
|
|
||||||
for (i, entry) in entries.iter_mut().enumerate() { entry.id = i as u64; }
|
|
||||||
let output_bundle = OutputRoot { items: entries };
|
|
||||||
|
|
||||||
if let Some(path) = output_path {
|
|
||||||
let base_path = Path::new(&path);
|
|
||||||
let parent_opt = base_path.parent();
|
|
||||||
if let Some(parent) = parent_opt {
|
|
||||||
if !parent.as_os_str().is_empty() {
|
|
||||||
create_dir_all(parent).with_context(|| {
|
|
||||||
format!("Failed to create parent directory for output: {}", parent.display())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let stem = base_path.file_stem().and_then(|s| s.to_str()).unwrap_or("output");
|
|
||||||
let date = date_prefix();
|
|
||||||
let base_name = format!("{}_{}", date, stem);
|
|
||||||
let dir = parent_opt.unwrap_or(Path::new(""));
|
|
||||||
let json_path = dir.join(format!("{}.json", &base_name));
|
|
||||||
let toml_path = dir.join(format!("{}.toml", &base_name));
|
|
||||||
let srt_path = dir.join(format!("{}.srt", &base_name));
|
|
||||||
|
|
||||||
let mut json_file = File::create(&json_path).with_context(|| format!("Failed to create output file: {}", json_path.display()))?;
|
|
||||||
serde_json::to_writer_pretty(&mut json_file, &output_bundle)?; writeln!(&mut json_file)?;
|
|
||||||
let toml_str = toml::to_string_pretty(&output_bundle)?;
|
|
||||||
let mut toml_file = File::create(&toml_path).with_context(|| format!("Failed to create output file: {}", toml_path.display()))?;
|
|
||||||
toml_file.write_all(toml_str.as_bytes())?; if !toml_str.ends_with('\n') { writeln!(&mut toml_file)?; }
|
|
||||||
let srt_str = render_srt(&output_bundle.items);
|
|
||||||
let mut srt_file = File::create(&srt_path).with_context(|| format!("Failed to create output file: {}", srt_path.display()))?;
|
|
||||||
srt_file.write_all(srt_str.as_bytes())?;
|
|
||||||
} else {
|
|
||||||
let stdout = io::stdout();
|
|
||||||
let mut handle = stdout.lock();
|
|
||||||
serde_json::to_writer_pretty(&mut handle, &output_bundle)?; writeln!(&mut handle)?;
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// SEPARATE (default)
|
|
||||||
polyscribe::dlog!(1, "Mode: separate; output_dir={:?}", output_path);
|
|
||||||
if output_path.is_none() && inputs.len() > 1 {
|
|
||||||
return Err(anyhow!("Multiple inputs without --merge require -o OUTPUT_DIR to write separate files"));
|
|
||||||
}
|
|
||||||
let out_dir: Option<PathBuf> = output_path.as_ref().map(PathBuf::from);
|
|
||||||
if let Some(dir) = &out_dir {
|
|
||||||
if !dir.as_os_str().is_empty() {
|
|
||||||
create_dir_all(dir).with_context(|| format!("Failed to create output directory: {}", dir.display()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (index, input_path) in inputs.iter().enumerate() {
|
|
||||||
let path = Path::new(input_path);
|
|
||||||
let speaker = speakers[index].clone();
|
|
||||||
// TODO remove duplicate
|
|
||||||
let mut entries: Vec<OutputEntry> = if is_json_file(path) {
|
|
||||||
let mut buf = String::new();
|
|
||||||
File::open(path)
|
|
||||||
.with_context(|| format!("Failed to open: {input_path}"))?
|
|
||||||
.read_to_string(&mut buf)
|
|
||||||
.with_context(|| format!("Failed to read: {input_path}"))?;
|
|
||||||
let root: InputRoot = serde_json::from_str(&buf)
|
|
||||||
.with_context(|| format!("Invalid JSON transcript parsed from {input_path}"))?;
|
|
||||||
root
|
|
||||||
.segments
|
|
||||||
.into_iter()
|
|
||||||
.map(|seg| OutputEntry { id: 0, speaker: speaker.clone(), start: seg.start, end: seg.end, text: seg.text })
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
let lang_norm: Option<String> = args.language.as_deref().and_then(|s| normalize_lang_code(s));
|
|
||||||
let selected_backend = polyscribe::backend::select_backend(polyscribe::backend::BackendKind::Auto, args.verbose > 0)?;
|
|
||||||
selected_backend.backend.transcribe(path, &speaker, lang_norm.as_deref(), None, None)?
|
|
||||||
};
|
|
||||||
// TODO remove duplicate
|
|
||||||
entries.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap_or(std::cmp::Ordering::Equal)
|
|
||||||
.then(a.end.partial_cmp(&b.end).unwrap_or(std::cmp::Ordering::Equal)));
|
|
||||||
for (i, entry) in entries.iter_mut().enumerate() { entry.id = i as u64; }
|
|
||||||
|
|
||||||
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("output");
|
|
||||||
let date = date_prefix();
|
|
||||||
let base_name = format!("{date}_{stem}");
|
|
||||||
if let Some(dir) = &out_dir {
|
|
||||||
let json_path = dir.join(format!("{}.json", &base_name));
|
|
||||||
let toml_path = dir.join(format!("{}.toml", &base_name));
|
|
||||||
let srt_path = dir.join(format!("{}.srt", &base_name));
|
|
||||||
let output_bundle = OutputRoot { items: entries };
|
|
||||||
let mut json_file = File::create(&json_path).with_context(|| format!("Failed to create output file: {}", json_path.display()))?;
|
|
||||||
serde_json::to_writer_pretty(&mut json_file, &output_bundle)?; writeln!(&mut json_file)?;
|
|
||||||
let toml_str = toml::to_string_pretty(&output_bundle)?;
|
|
||||||
let mut toml_file = File::create(&toml_path).with_context(|| format!("Failed to create output file: {}", toml_path.display()))?;
|
|
||||||
toml_file.write_all(toml_str.as_bytes())?; if !toml_str.ends_with('\n') { writeln!(&mut toml_file)?; }
|
|
||||||
let srt_str = render_srt(&output_bundle.items);
|
|
||||||
let mut srt_file = File::create(&srt_path).with_context(|| format!("Failed to create output file: {}", srt_path.display()))?;
|
|
||||||
srt_file.write_all(srt_str.as_bytes())?;
|
|
||||||
} else {
|
|
||||||
// In separate mode with single input and no output dir, print JSON to stdout
|
|
||||||
let stdout = io::stdout();
|
|
||||||
let mut handle = stdout.lock();
|
|
||||||
let output_bundle = OutputRoot { items: entries };
|
|
||||||
serde_json::to_writer_pretty(&mut handle, &output_bundle)?; writeln!(&mut handle)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
@@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn bin() -> &'static str {
|
fn bin() -> String {
|
||||||
env!("CARGO_BIN_EXE_polyscribe")
|
std::env::var("CARGO_BIN_EXE_polyscribe")
|
||||||
|
.unwrap_or_else(|_| "polyscribe".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@@ -1,32 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "polyscribe"
|
name = "polyscribe-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
# Default: CPU only; no GPU features enabled
|
|
||||||
default = []
|
|
||||||
# GPU backends map to whisper-rs features or FFI stub for Vulkan
|
|
||||||
gpu-cuda = ["whisper-rs/cuda"]
|
|
||||||
gpu-hip = ["whisper-rs/hipblas"]
|
|
||||||
gpu-vulkan = []
|
|
||||||
# explicit CPU fallback feature (no effect at build time, used for clarity)
|
|
||||||
cpu-fallback = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.99"
|
||||||
|
thiserror = "1.0.69"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.142"
|
serde_json = "1.0.142"
|
||||||
toml = "0.8"
|
toml = "0.8.23"
|
||||||
chrono = { version = "0.4", features = ["clock"] }
|
directories = "5.0.1"
|
||||||
sha2 = "0.10"
|
chrono = "0.4.41"
|
||||||
whisper-rs = { git = "https://github.com/tazz4843/whisper-rs" }
|
libc = "0.2.175"
|
||||||
libc = "0.2"
|
whisper-rs = "0.14.3"
|
||||||
cliclack = "0.3"
|
indicatif = "0.17.11"
|
||||||
indicatif = "0.17"
|
|
||||||
thiserror = "1"
|
|
||||||
directories = "5"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
# no special build deps
|
|
||||||
|
@@ -1,149 +1,108 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
use crate::prelude::*;
|
||||||
// Simple ConfigService with XDG/system/workspace merge and atomic writes
|
use directories::ProjectDirs;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use directories::BaseDirs;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::env;
|
use std::{fs, path::PathBuf};
|
||||||
use std::fs;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
/// Generic configuration represented as TOML table
|
const ENV_PREFIX: &str = "POLYSCRIBE";
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
||||||
pub struct Config(pub toml::value::Table);
|
|
||||||
|
|
||||||
impl Config {
|
/// Configuration for the Polyscribe application
|
||||||
/// Get a mutable reference to a top-level table under the given key, creating
|
///
|
||||||
/// an empty table if it does not exist yet.
|
/// Contains paths to models and plugins directories that can be customized
|
||||||
pub fn get_table_mut(&mut self, key: &str) -> &mut toml::value::Table {
|
/// through configuration files or environment variables.
|
||||||
let needs_init = !matches!(self.0.get(key), Some(toml::Value::Table(_)));
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
if needs_init {
|
pub struct Config {
|
||||||
self.0.insert(key.to_string(), toml::Value::Table(Default::default()));
|
/// Directory path where ML models are stored
|
||||||
}
|
pub models_dir: Option<PathBuf>,
|
||||||
match self.0.get_mut(key) {
|
/// Directory path where plugins are stored
|
||||||
Some(toml::Value::Table(t)) => t,
|
pub plugins_dir: Option<PathBuf>,
|
||||||
_ => unreachable!(),
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
models_dir: None,
|
||||||
|
plugins_dir: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_tables(base: &mut toml::value::Table, overlay: &toml::value::Table) {
|
/// Service for managing Polyscribe configuration
|
||||||
for (k, v) in overlay.iter() {
|
///
|
||||||
match (base.get_mut(k), v) {
|
/// Provides functionality to load, save, and access configuration settings
|
||||||
(Some(toml::Value::Table(bsub)), toml::Value::Table(osub)) => {
|
/// from disk or environment variables.
|
||||||
merge_tables(bsub, osub);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
base.insert(k.clone(), v.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_toml(path: &Path) -> Result<toml::value::Table> {
|
|
||||||
let s = fs::read_to_string(path).with_context(|| format!("Failed to read config: {}", path.display()))?;
|
|
||||||
let v: toml::Value = toml::from_str(&s).with_context(|| format!("Invalid TOML in {}", path.display()))?;
|
|
||||||
Ok(v.as_table().cloned().unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_toml_atomic(path: &Path, tbl: &toml::value::Table) -> Result<()> {
|
|
||||||
if let Some(parent) = path.parent() {
|
|
||||||
fs::create_dir_all(parent).with_context(|| format!("Failed to create config dir: {}", parent.display()))?;
|
|
||||||
}
|
|
||||||
let tmp = path.with_extension("tmp");
|
|
||||||
let mut f = fs::File::create(&tmp).with_context(|| format!("Failed to create temp file: {}", tmp.display()))?;
|
|
||||||
let s = toml::to_string_pretty(&toml::Value::Table(tbl.clone()))?;
|
|
||||||
f.write_all(s.as_bytes())?;
|
|
||||||
if !s.ends_with('\n') { f.write_all(b"\n")?; }
|
|
||||||
drop(f);
|
|
||||||
fs::rename(&tmp, path).with_context(|| format!("Failed to atomically replace config: {}", path.display()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn system_config_path() -> PathBuf {
|
|
||||||
if cfg!(unix) { PathBuf::from("/etc").join("polyscribe").join("config.toml") } else { default_user_config_path() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_user_config_path() -> PathBuf {
|
|
||||||
if let Some(base) = BaseDirs::new() {
|
|
||||||
return PathBuf::from(base.config_dir()).join("polyscribe").join("config.toml");
|
|
||||||
}
|
|
||||||
PathBuf::from(".polyscribe").join("config.toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workspace_config_path() -> PathBuf {
|
|
||||||
PathBuf::from(".polyscribe").join("config.toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Service responsible for loading and saving PolyScribe configuration
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct ConfigService;
|
pub struct ConfigService;
|
||||||
|
|
||||||
impl ConfigService {
|
impl ConfigService {
|
||||||
/// Load configuration, merging system < user < workspace < env overrides.
|
/// Loads configuration from disk or returns default values if not found
|
||||||
pub fn load(&self) -> Result<Config> {
|
///
|
||||||
let mut accum = toml::value::Table::default();
|
/// This function attempts to read the configuration file from disk. If the file
|
||||||
let sys = system_config_path();
|
/// doesn't exist or can't be parsed, it falls back to default values.
|
||||||
if sys.exists() {
|
/// Environment variable overrides are then applied to the configuration.
|
||||||
merge_tables(&mut accum, &read_toml(&sys)?);
|
pub fn load_or_default() -> Result<Config> {
|
||||||
}
|
let mut cfg = Self::read_disk().unwrap_or_default();
|
||||||
let user = default_user_config_path();
|
Self::apply_env_overrides(&mut cfg)?;
|
||||||
if user.exists() {
|
Ok(cfg)
|
||||||
merge_tables(&mut accum, &read_toml(&user)?);
|
|
||||||
}
|
|
||||||
let ws = workspace_config_path();
|
|
||||||
if ws.exists() {
|
|
||||||
merge_tables(&mut accum, &read_toml(&ws)?);
|
|
||||||
}
|
|
||||||
// Env overrides: POLYSCRIBE__SECTION__KEY=value
|
|
||||||
let mut env_over = toml::value::Table::default();
|
|
||||||
for (k, v) in env::vars() {
|
|
||||||
if let Some(rest) = k.strip_prefix("POLYSCRIBE__") {
|
|
||||||
let parts: Vec<&str> = rest.split("__").collect();
|
|
||||||
if parts.is_empty() { continue; }
|
|
||||||
let val: toml::Value = toml::Value::String(v);
|
|
||||||
// Build nested tables
|
|
||||||
let mut current = &mut env_over;
|
|
||||||
for (i, part) in parts.iter().enumerate() {
|
|
||||||
if i == parts.len() - 1 {
|
|
||||||
current.insert(part.to_lowercase(), val.clone());
|
|
||||||
} else {
|
|
||||||
current = current.entry(part.to_lowercase()).or_insert_with(|| toml::Value::Table(Default::default()))
|
|
||||||
.as_table_mut().expect("table");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
merge_tables(&mut accum, &env_over);
|
|
||||||
Ok(Config(accum))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure user config exists with sensible defaults, return loaded config
|
/// Saves the configuration to disk
|
||||||
pub fn ensure_user_config(&self) -> Result<Config> {
|
///
|
||||||
let path = default_user_config_path();
|
/// This function serializes the configuration to TOML format and writes it
|
||||||
if !path.exists() {
|
/// to the standard configuration directory for the application.
|
||||||
let mut defaults = toml::value::Table::default();
|
/// Returns an error if writing fails or if project directories cannot be determined.
|
||||||
defaults.insert("ui".into(), toml::Value::Table({
|
pub fn save(cfg: &Config) -> Result<()> {
|
||||||
let mut t = toml::value::Table::default();
|
let Some(dirs) = Self::dirs() else {
|
||||||
t.insert("theme".into(), toml::Value::String("auto".into()));
|
return Err(Error::Other("unable to get project dirs".into()));
|
||||||
t
|
};
|
||||||
}));
|
let cfg_dir = dirs.config_dir();
|
||||||
write_toml_atomic(&path, &defaults)?;
|
fs::create_dir_all(cfg_dir)?;
|
||||||
|
let path = cfg_dir.join("config.toml");
|
||||||
|
let s = toml::to_string_pretty(cfg)?;
|
||||||
|
fs::write(path, s)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_disk() -> Option<Config> {
|
||||||
|
let dirs = Self::dirs()?;
|
||||||
|
let path = dirs.config_dir().join("config.toml");
|
||||||
|
let s = fs::read_to_string(path).ok()?;
|
||||||
|
toml::from_str(&s).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_env_overrides(cfg: &mut Config) -> Result<()> {
|
||||||
|
// POLYSCRIBE__SECTION__KEY format reserved for future nested config.
|
||||||
|
if let Ok(v) = std::env::var(format!("{ENV_PREFIX}_MODELS_DIR")) {
|
||||||
|
cfg.models_dir = Some(PathBuf::from(v));
|
||||||
}
|
}
|
||||||
self.load()
|
if let Ok(v) = std::env::var(format!("{ENV_PREFIX}_PLUGINS_DIR")) {
|
||||||
|
cfg.plugins_dir = Some(PathBuf::from(v));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save to user config atomically, merging over existing user file.
|
/// Returns the standard project directories for the application
|
||||||
pub fn save_user(&self, new_values: &toml::value::Table) -> Result<()> {
|
///
|
||||||
let path = default_user_config_path();
|
/// This function creates a ProjectDirs instance with the appropriate
|
||||||
let mut base = if path.exists() { read_toml(&path)? } else { Default::default() };
|
/// organization and application names for Polyscribe.
|
||||||
merge_tables(&mut base, new_values);
|
/// Returns None if the project directories cannot be determined.
|
||||||
write_toml_atomic(&path, &base)
|
pub fn dirs() -> Option<ProjectDirs> {
|
||||||
|
ProjectDirs::from("dev", "polyscribe", "polyscribe")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paths used for debugging/information
|
/// Returns the default directory path for storing ML models
|
||||||
pub fn paths(&self) -> (PathBuf, PathBuf, PathBuf) {
|
///
|
||||||
(system_config_path(), default_user_config_path(), workspace_config_path())
|
/// This function determines the standard data directory for the application
|
||||||
|
/// and appends a 'models' subdirectory to it.
|
||||||
|
/// Returns None if the project directories cannot be determined.
|
||||||
|
pub fn default_models_dir() -> Option<PathBuf> {
|
||||||
|
Self::dirs().map(|d| d.data_dir().join("models"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default directory path for storing plugins
|
||||||
|
///
|
||||||
|
/// This function determines the standard data directory for the application
|
||||||
|
/// and appends a 'plugins' subdirectory to it.
|
||||||
|
/// Returns None if the project directories cannot be determined.
|
||||||
|
pub fn default_plugins_dir() -> Option<PathBuf> {
|
||||||
|
Self::dirs().map(|d| d.data_dir().join("plugins"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,39 +1,39 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// The common error type for the polyscribe core crate.
|
|
||||||
/// Add more domain-specific variants as needed.
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
/// Error types for the polyscribe-core crate.
|
||||||
|
///
|
||||||
|
/// This enum represents various error conditions that can occur during
|
||||||
|
/// operations in this crate, including I/O errors, serialization/deserialization
|
||||||
|
/// errors, and environment variable access errors.
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Wrapper for any boxed dynamic error. Useful as a temporary catch-all.
|
#[error("I/O error: {0}")]
|
||||||
#[error("anyhow error: {0}")]
|
/// Represents an I/O error that occurred during file or stream operations
|
||||||
Anyhow(#[from] anyhow::Error),
|
|
||||||
|
|
||||||
/// IO-related error.
|
|
||||||
#[error("io error: {0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
/// UTF-8 conversion error.
|
#[error("serde error: {0}")]
|
||||||
#[error("utf8 error: {0}")]
|
/// Represents a JSON serialization or deserialization error
|
||||||
Utf8(#[from] std::string::FromUtf8Error),
|
Serde(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("toml error: {0}")]
|
||||||
|
/// Represents a TOML deserialization error
|
||||||
|
Toml(#[from] toml::de::Error),
|
||||||
|
|
||||||
|
#[error("toml ser error: {0}")]
|
||||||
|
/// Represents a TOML serialization error
|
||||||
|
TomlSer(#[from] toml::ser::Error),
|
||||||
|
|
||||||
/// Environment variable error.
|
|
||||||
#[error("env var error: {0}")]
|
#[error("env var error: {0}")]
|
||||||
Var(#[from] std::env::VarError),
|
/// Represents an error that occurred during environment variable access
|
||||||
|
EnvVar(#[from] std::env::VarError),
|
||||||
|
|
||||||
/// TOML de serialization error.
|
#[error("other: {0}")]
|
||||||
#[error("toml de error: {0}")]
|
/// Represents a general error condition with a custom message
|
||||||
TomlDe(#[from] toml::de::Error),
|
Other(String),
|
||||||
|
|
||||||
/// Configuration parsing error.
|
|
||||||
#[error("configuration error: {0}")]
|
|
||||||
Config(String),
|
|
||||||
|
|
||||||
/// Placeholder for not-yet-implemented backends or features.
|
|
||||||
#[error("unimplemented: {0}")]
|
|
||||||
Unimplemented(&'static str),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenient result alias for the polyscribe core crate.
|
impl From<anyhow::Error> for Error {
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
fn from(e: anyhow::Error) -> Self {
|
||||||
|
Error::Other(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -192,20 +192,17 @@ macro_rules! qlog {
|
|||||||
($($arg:tt)*) => {{ $crate::ilog!($($arg)*); }}
|
($($arg:tt)*) => {{ $crate::ilog!($($arg)*); }}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Re-export backend module (GPU/CPU selection and transcription).
|
|
||||||
pub mod backend;
|
pub mod backend;
|
||||||
/// Re-export models module (model listing/downloading/updating).
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
/// Configuration service (XDG + atomic writes)
|
/// Configuration handling for PolyScribe
|
||||||
pub mod config;
|
pub mod config;
|
||||||
/// UI helpers
|
// Use the file-backed ui.rs module, which also declares its own `progress` submodule.
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
/// Error types for the crate.
|
/// Error definitions for the PolyScribe library
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub use error::Error;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
pub use error::{Error, Result as OtherResult};
|
|
||||||
|
|
||||||
/// Transcript entry for a single segment.
|
/// Transcript entry for a single segment.
|
||||||
#[derive(Debug, serde::Serialize, Clone)]
|
#[derive(Debug, serde::Serialize, Clone)]
|
||||||
pub struct OutputEntry {
|
pub struct OutputEntry {
|
||||||
|
@@ -88,9 +88,10 @@ pub fn run_interactive_model_downloader() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let answer = ui::prompt_input("Your selection", Some("1"))?;
|
let answer = ui::prompt_input("Your selection", Some("1"))?;
|
||||||
let selection_raw = match answer {
|
let selection_raw = if answer.trim().is_empty() {
|
||||||
Some(s) => s.trim().to_string(),
|
"1".to_string()
|
||||||
None => "1".to_string(),
|
} else {
|
||||||
|
answer.trim().to_string()
|
||||||
};
|
};
|
||||||
let selection = if selection_raw.is_empty() { "1" } else { &selection_raw };
|
let selection = if selection_raw.is_empty() { "1" } else { &selection_raw };
|
||||||
|
|
||||||
|
@@ -4,10 +4,13 @@
|
|||||||
|
|
||||||
pub use crate::backend::*;
|
pub use crate::backend::*;
|
||||||
pub use crate::config::*;
|
pub use crate::config::*;
|
||||||
pub use crate::error::{Error, Result};
|
pub use crate::error::Error;
|
||||||
pub use crate::models::*;
|
pub use crate::models::*;
|
||||||
|
|
||||||
// If you frequently use UI helpers across binaries/tests, export them too.
|
// If you frequently use UI helpers across binaries/tests, export them too.
|
||||||
// Keep this lean to avoid pulling UI everywhere unintentionally.
|
// Keep this lean to avoid pulling UI everywhere unintentionally.
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use crate::ui::*;
|
pub use crate::ui::*;
|
||||||
|
|
||||||
|
/// A convenient alias for `std::result::Result` with the error type defaulting to [`Error`].
|
||||||
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
@@ -1,87 +1,64 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright (c) 2025 <COPYRIGHT HOLDER>. All rights reserved.
|
// Copyright (c) 2025 <COPYRIGHT HOLDER>. All rights reserved.
|
||||||
|
|
||||||
//! Centralized UI helpers (TTY-aware, quiet/verbose-aware)
|
//! Minimal UI helpers used across the core crate.
|
||||||
|
//! This keeps interactive bits centralized and easy to stub in tests.
|
||||||
|
|
||||||
use std::io;
|
/// Progress indicators and reporting tools for displaying task completion.
|
||||||
|
pub mod progress;
|
||||||
|
|
||||||
/// Startup intro/banner (suppressed when quiet).
|
use std::io::{self, Write};
|
||||||
pub fn intro(msg: impl AsRef<str>) {
|
|
||||||
let _ = cliclack::intro(msg.as_ref());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Final outro/summary printed below any progress indicators (suppressed when quiet).
|
/// Print an informational line to stderr (suppressed when quiet mode is enabled by callers).
|
||||||
pub fn outro(msg: impl AsRef<str>) {
|
|
||||||
let _ = cliclack::outro(msg.as_ref());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Info message (TTY-aware; suppressed by --quiet is handled by outer callers if needed)
|
|
||||||
pub fn info(msg: impl AsRef<str>) {
|
pub fn info(msg: impl AsRef<str>) {
|
||||||
let _ = cliclack::log::info(msg.as_ref());
|
eprintln!("{}", msg.as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print a warning (always printed).
|
/// Print a warning line to stderr.
|
||||||
pub fn warn(msg: impl AsRef<str>) {
|
pub fn warn(msg: impl AsRef<str>) {
|
||||||
// cliclack provides a warning-level log utility
|
eprintln!("WARNING: {}", msg.as_ref());
|
||||||
let _ = cliclack::log::warning(msg.as_ref());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print an error (always printed).
|
/// Print an error line to stderr.
|
||||||
pub fn error(msg: impl AsRef<str>) {
|
pub fn error(msg: impl AsRef<str>) {
|
||||||
let _ = cliclack::log::error(msg.as_ref());
|
eprintln!("ERROR: {}", msg.as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print a line above any progress bars (maps to cliclack log; synchronized).
|
/// Print a short intro header (non-fancy).
|
||||||
pub fn println_above_bars(msg: impl AsRef<str>) {
|
pub fn intro(title: impl AsRef<str>) {
|
||||||
if crate::is_quiet() { return; }
|
eprintln!("== {} ==", title.as_ref());
|
||||||
// cliclack logs are synchronized with its spinners/bars
|
|
||||||
let _ = cliclack::log::info(msg.as_ref());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Input prompt with a question: returns Ok(None) if non-interactive or canceled
|
/// Print a short outro footer (non-fancy).
|
||||||
pub fn prompt_input(question: impl AsRef<str>, default: Option<&str>) -> anyhow::Result<Option<String>> {
|
pub fn outro(msg: impl AsRef<str>) {
|
||||||
if crate::is_no_interaction() || !crate::stdin_is_tty() {
|
eprintln!("{}", msg.as_ref());
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let mut p = cliclack::input(question.as_ref());
|
|
||||||
if let Some(d) = default {
|
|
||||||
// Use default_input when available in 0.3.x
|
|
||||||
p = p.default_input(d);
|
|
||||||
}
|
|
||||||
match p.interact() {
|
|
||||||
Ok(s) => Ok(Some(s)),
|
|
||||||
Err(_) => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Confirmation prompt; returns Ok(None) if non-interactive or canceled
|
/// Print a line that should appear above any progress indicators (plain for now).
|
||||||
pub fn prompt_confirm(question: impl AsRef<str>, default_yes: bool) -> anyhow::Result<Option<bool>> {
|
pub fn println_above_bars(line: impl AsRef<str>) {
|
||||||
if crate::is_no_interaction() || !crate::stdin_is_tty() {
|
eprintln!("{}", line.as_ref());
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let res = cliclack::confirm(question.as_ref())
|
|
||||||
.initial_value(default_yes)
|
|
||||||
.interact();
|
|
||||||
match res {
|
|
||||||
Ok(v) => Ok(Some(v)),
|
|
||||||
Err(_) => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prompt the user (TTY-aware via cliclack) and read a line from stdin. Returns the raw line with trailing newline removed.
|
/// Prompt for input on stdin. Returns default if provided and user enters empty string.
|
||||||
pub fn prompt_line(prompt: &str) -> io::Result<String> {
|
/// In non-interactive workflows, callers should skip prompt based on their flags.
|
||||||
// Route prompt through cliclack to keep consistent styling and avoid direct eprint!/println!
|
pub fn prompt_input(prompt: &str, default: Option<&str>) -> io::Result<String> {
|
||||||
let _ = cliclack::log::info(prompt);
|
let mut stdout = io::stdout();
|
||||||
let mut s = String::new();
|
match default {
|
||||||
io::stdin().read_line(&mut s)?;
|
Some(def) => {
|
||||||
Ok(s)
|
write!(stdout, "{} [{}]: ", prompt, def)?;
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
write!(stdout, "{}: ", prompt)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdout.flush()?;
|
||||||
|
|
||||||
/// TTY-aware progress UI built on `indicatif` for per-file and aggregate progress bars.
|
let mut buf = String::new();
|
||||||
///
|
io::stdin().read_line(&mut buf)?;
|
||||||
/// This small helper encapsulates a `MultiProgress` with one aggregate (total) bar and
|
let trimmed = buf.trim();
|
||||||
/// one per-file bar. It is intentionally minimal to keep integration lightweight.
|
if trimmed.is_empty() {
|
||||||
pub mod progress {
|
Ok(default.unwrap_or_default().to_string())
|
||||||
// The submodule is defined in a separate file for clarity.
|
} else {
|
||||||
include!("ui/progress.rs");
|
Ok(trimmed.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -79,3 +79,47 @@ impl ProgressManager {
|
|||||||
if let Some(total) = &self.total { total.set_position(self.completed as u64); }
|
if let Some(total) = &self.total { total.set_position(self.completed as u64); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A simple reporter for displaying progress messages in the terminal.
|
||||||
|
/// Provides different output formatting based on whether the environment is interactive or not.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ProgressReporter {
|
||||||
|
non_interactive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressReporter {
|
||||||
|
/// Creates a new progress reporter.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `non_interactive` - Whether the output should be formatted for non-interactive environments.
|
||||||
|
pub fn new(non_interactive: bool) -> Self {
|
||||||
|
Self { non_interactive }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays a progress step message.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `message` - The message to display for this progress step.
|
||||||
|
pub fn step(&mut self, message: &str) {
|
||||||
|
if self.non_interactive {
|
||||||
|
eprintln!("[..] {message}");
|
||||||
|
} else {
|
||||||
|
eprintln!("• {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays a completion message.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `message` - The message to display when a task is completed.
|
||||||
|
pub fn finish_with_message(&mut self, message: &str) {
|
||||||
|
if self.non_interactive {
|
||||||
|
eprintln!("[ok] {message}");
|
||||||
|
} else {
|
||||||
|
eprintln!("✓ {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -2,16 +2,10 @@
|
|||||||
name = "polyscribe-host"
|
name = "polyscribe-host"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.99"
|
||||||
thiserror = "1"
|
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.142"
|
serde_json = "1.0.142"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1.47.1", features = ["rt-multi-thread", "process", "io-util"] }
|
||||||
which = "6"
|
which = "6.0.3"
|
||||||
cliclack = "0.3"
|
|
||||||
directories = "5"
|
|
||||||
polyscribe = { path = "../polyscribe-core" }
|
|
||||||
polyscribe-protocol = { path = "../polyscribe-protocol" }
|
|
||||||
|
@@ -1,168 +1,118 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
use anyhow::{Context, Result};
|
||||||
|
use serde::Deserialize;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use std::{
|
||||||
use cliclack as ui; // reuse for minimal logging
|
env,
|
||||||
use directories::BaseDirs;
|
fs,
|
||||||
use serde_json::Value;
|
os::unix::fs::PermissionsExt,
|
||||||
use std::collections::BTreeMap;
|
path::Path,
|
||||||
use std::ffi::OsStr;
|
};
|
||||||
use std::fs;
|
use tokio::{
|
||||||
use std::io::{BufRead, BufReader, Write};
|
io::{AsyncBufReadExt, BufReader},
|
||||||
use std::path::{Path, PathBuf};
|
process::{Child as TokioChild, Command},
|
||||||
use std::process::{Command, Stdio};
|
};
|
||||||
|
use std::process::Stdio;
|
||||||
use polyscribe_protocol as psp;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Plugin {
|
pub struct PluginInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: PathBuf,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discover plugins on PATH and in the user's data dir (XDG) under polyscribe/plugins.
|
#[derive(Debug, Default)]
|
||||||
pub fn discover() -> Result<Vec<Plugin>> {
|
pub struct PluginManager;
|
||||||
let mut found: BTreeMap<String, PathBuf> = BTreeMap::new();
|
|
||||||
|
|
||||||
// Scan PATH directories
|
impl PluginManager {
|
||||||
if let Some(path_var) = std::env::var_os("PATH") {
|
pub fn list(&self) -> Result<Vec<PluginInfo>> {
|
||||||
for dir in std::env::split_paths(&path_var) {
|
let mut plugins = Vec::new();
|
||||||
if dir.as_os_str().is_empty() { continue; }
|
|
||||||
if let Ok(rd) = fs::read_dir(&dir) {
|
// Scan PATH entries for executables starting with "polyscribe-plugin-"
|
||||||
for ent in rd.flatten() {
|
if let Ok(path) = env::var("PATH") {
|
||||||
let p = ent.path();
|
for dir in env::split_paths(&path) {
|
||||||
if !is_executable(&p) { continue; }
|
if let Ok(read_dir) = fs::read_dir(&dir) {
|
||||||
if let Some(fname) = p.file_name().and_then(OsStr::to_str) {
|
for entry in read_dir.flatten() {
|
||||||
if let Some(name) = fname.strip_prefix("polyscribe-plugin-") {
|
let path = entry.path();
|
||||||
found.entry(name.to_string()).or_insert(p);
|
if let Some(fname) = path.file_name().and_then(|s| s.to_str()) {
|
||||||
|
if fname.starts_with("polyscribe-plugin-") && is_executable(&path) {
|
||||||
|
let name = fname.trim_start_matches("polyscribe-plugin-").to_string();
|
||||||
|
plugins.push(PluginInfo {
|
||||||
|
name,
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: also scan XDG data plugins dir for symlinks/binaries
|
||||||
|
Ok(plugins)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan user data dir
|
pub fn info(&self, name: &str) -> Result<serde_json::Value> {
|
||||||
if let Some(base) = BaseDirs::new() {
|
let bin = self.resolve(name)?;
|
||||||
let user_plugins = PathBuf::from(base.data_dir()).join("polyscribe").join("plugins");
|
let out = std::process::Command::new(&bin)
|
||||||
if let Ok(rd) = fs::read_dir(&user_plugins) {
|
.arg("info")
|
||||||
for ent in rd.flatten() {
|
.stdout(Stdio::piped())
|
||||||
let p = ent.path();
|
.stderr(Stdio::inherit())
|
||||||
if !is_executable(&p) { continue; }
|
.spawn()
|
||||||
if let Some(fname) = p.file_name().and_then(OsStr::to_str) {
|
.context("spawning plugin info")?
|
||||||
let name = fname.strip_prefix("polyscribe-plugin-")
|
.wait_with_output()
|
||||||
.map(|s| s.to_string())
|
.context("waiting for plugin info")?;
|
||||||
.or_else(|| Some(fname.to_string()))
|
|
||||||
.unwrap();
|
let val: serde_json::Value =
|
||||||
found.entry(name).or_insert(p);
|
serde_json::from_slice(&out.stdout).context("parsing plugin info JSON")?;
|
||||||
}
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn(&self, name: &str, command: &str) -> Result<TokioChild> {
|
||||||
|
let bin = self.resolve(name)?;
|
||||||
|
let mut cmd = Command::new(&bin);
|
||||||
|
cmd.arg("run")
|
||||||
|
.arg(command)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::inherit());
|
||||||
|
let child = cmd.spawn().context("spawning plugin run")?;
|
||||||
|
Ok(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn forward_stdio(&self, child: &mut TokioChild) -> Result<std::process::ExitStatus> {
|
||||||
|
if let Some(stdout) = child.stdout.take() {
|
||||||
|
let mut reader = BufReader::new(stdout).lines();
|
||||||
|
while let Some(line) = reader.next_line().await? {
|
||||||
|
println!("{line}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(child.wait().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(found
|
fn resolve(&self, name: &str) -> Result<String> {
|
||||||
.into_iter()
|
let bin = format!("polyscribe-plugin-{name}");
|
||||||
.map(|(name, path)| Plugin { name, path })
|
let path = which::which(&bin).with_context(|| format!("plugin not found in PATH: {bin}"))?;
|
||||||
.collect())
|
Ok(path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_executable(p: &Path) -> bool {
|
fn is_executable(path: &Path) -> bool {
|
||||||
if !p.is_file() { return false; }
|
if !path.is_file() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::fs::PermissionsExt;
|
if let Ok(meta) = fs::metadata(path) {
|
||||||
if let Ok(md) = fs::metadata(p) {
|
let mode = meta.permissions().mode();
|
||||||
let mode = md.permissions().mode();
|
// if any execute bit is set
|
||||||
return (mode & 0o111) != 0;
|
return mode & 0o111 != 0;
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
// On Windows, consider .exe, .bat, .cmd
|
|
||||||
matches!(p.extension().and_then(|s| s.to_str()).map(|s| s.to_lowercase()), Some(ext) if matches!(ext.as_str(), "exe"|"bat"|"cmd"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Query plugin capabilities by invoking `--capabilities`.
|
|
||||||
pub fn capabilities(plugin_path: &Path) -> Result<psp::Capabilities> {
|
|
||||||
let out = Command::new(plugin_path)
|
|
||||||
.arg("--capabilities")
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.output()
|
|
||||||
.with_context(|| format!("Failed to execute plugin: {}", plugin_path.display()))?;
|
|
||||||
if !out.status.success() {
|
|
||||||
return Err(anyhow!("Plugin --capabilities failed: {}", plugin_path.display()));
|
|
||||||
}
|
|
||||||
let s = String::from_utf8(out.stdout).context("capabilities stdout not utf-8")?;
|
|
||||||
let caps: psp::Capabilities = serde_json::from_str(s.trim()).context("invalid capabilities JSON")?;
|
|
||||||
Ok(caps)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a single method via `--serve`, writing one JSON-RPC request and streaming until result.
|
|
||||||
pub fn run_method<F>(plugin_path: &Path, method: &str, params: Value, mut on_progress: F) -> Result<Value>
|
|
||||||
where
|
|
||||||
F: FnMut(psp::Progress),
|
|
||||||
{
|
|
||||||
let mut child = Command::new(plugin_path)
|
|
||||||
.arg("--serve")
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.spawn()
|
|
||||||
.with_context(|| format!("Failed to spawn plugin: {}", plugin_path.display()))?;
|
|
||||||
|
|
||||||
let mut stdin = child.stdin.take().ok_or_else(|| anyhow!("failed to open plugin stdin"))?;
|
|
||||||
let stdout = child.stdout.take().ok_or_else(|| anyhow!("failed to open plugin stdout"))?;
|
|
||||||
|
|
||||||
// Send request line
|
|
||||||
let req = psp::JsonRpcRequest { jsonrpc: "2.0".into(), id: "1".into(), method: method.to_string(), params: Some(params) };
|
|
||||||
let line = serde_json::to_string(&req)? + "\n";
|
|
||||||
stdin.write_all(line.as_bytes())?;
|
|
||||||
stdin.flush()?;
|
|
||||||
|
|
||||||
// Read response lines
|
|
||||||
let reader = BufReader::new(stdout);
|
|
||||||
for line in reader.lines() {
|
|
||||||
let line = line?;
|
|
||||||
if line.trim().is_empty() { continue; }
|
|
||||||
// Try parse StreamItem; if that fails, try parse JsonRpcResponse directly
|
|
||||||
if let Ok(item) = serde_json::from_str::<psp::StreamItem>(&line) {
|
|
||||||
match item {
|
|
||||||
psp::StreamItem::Progress(p) => {
|
|
||||||
on_progress(p);
|
|
||||||
}
|
|
||||||
psp::StreamItem::Result(resp) => {
|
|
||||||
match resp.outcome {
|
|
||||||
psp::JsonRpcOutcome::Ok { result } => return Ok(result),
|
|
||||||
psp::JsonRpcOutcome::Err { error } => return Err(anyhow!("{} ({})", error.message, error.code)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Ok(resp) = serde_json::from_str::<psp::JsonRpcResponse>(&line) {
|
|
||||||
match resp.outcome {
|
|
||||||
psp::JsonRpcOutcome::Ok { result } => return Ok(result),
|
|
||||||
psp::JsonRpcOutcome::Err { error } => return Err(anyhow!("{} ({})", error.message, error.code)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let _ = ui::log::warning(format!("Unrecognized plugin output: {}", line));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Fallback for non-unix (treat files as candidates)
|
||||||
// If we exited loop without returning, wait for child
|
true
|
||||||
let status = child.wait()?;
|
|
||||||
if status.success() {
|
|
||||||
Err(anyhow!("Plugin terminated without sending a result"))
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Plugin exited with status: {:?}", status))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper: find a plugin by name using discovery
|
#[allow(dead_code)]
|
||||||
pub fn find_plugin_by_name(name: &str) -> Result<Plugin> {
|
#[derive(Debug, Deserialize)]
|
||||||
let plugins = discover()?;
|
struct Capability {
|
||||||
plugins
|
command: String,
|
||||||
.into_iter()
|
summary: String,
|
||||||
.find(|p| p.name == name)
|
|
||||||
.ok_or_else(|| anyhow!("Plugin '{}' not found", name))
|
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,7 @@
|
|||||||
name = "polyscribe-protocol"
|
name = "polyscribe-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.142"
|
serde_json = "1.0.142"
|
||||||
thiserror = "1"
|
|
||||||
|
@@ -1,90 +1,60 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
// PolyScribe Protocol (PSP/1): JSON-RPC 2.0 over NDJSON on stdio
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
/// Plugin capabilities as reported by `--capabilities`.
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
pub struct Request {
|
||||||
pub struct Capabilities {
|
|
||||||
pub name: String,
|
|
||||||
pub version: String,
|
|
||||||
/// Protocol identifier (e.g., "psp/1")
|
|
||||||
pub protocol: String,
|
|
||||||
/// Role (e.g., pipeline, tool, generator)
|
|
||||||
pub role: String,
|
|
||||||
/// Supported command names
|
|
||||||
pub commands: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generic JSON-RPC 2.0 request for PSP/1
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct JsonRpcRequest {
|
|
||||||
pub jsonrpc: String, // "2.0"
|
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub method: String,
|
pub method: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub params: Option<Value>,
|
||||||
pub params: Option<serde_json::Value>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error object for JSON-RPC 2.0
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
pub struct Response {
|
||||||
pub struct JsonRpcError {
|
pub id: String,
|
||||||
pub code: i64,
|
pub result: Option<Value>,
|
||||||
|
pub error: Option<ErrorObj>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ErrorObj {
|
||||||
|
pub code: i32,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub data: Option<serde_json::Value>,
|
pub data: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic JSON-RPC 2.0 response for PSP/1
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[serde(tag = "event", content = "data")]
|
||||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
pub enum ProgressEvent {
|
||||||
pub enum StreamItem {
|
Started,
|
||||||
/// Progress notification (out-of-band in stream, not a JSON-RPC response)
|
Message(String),
|
||||||
Progress(Progress),
|
Percent(f32),
|
||||||
/// A proper JSON-RPC response with a result
|
Finished,
|
||||||
Result(JsonRpcResponse),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JSON-RPC 2.0 Response envelope containing either result or error.
|
impl Response {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
pub fn ok(id: impl Into<String>, result: Value) -> Self {
|
||||||
pub struct JsonRpcResponse {
|
Self {
|
||||||
pub jsonrpc: String, // "2.0"
|
|
||||||
pub id: String,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub outcome: JsonRpcOutcome,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum JsonRpcOutcome {
|
|
||||||
Ok { result: serde_json::Value },
|
|
||||||
Err { error: JsonRpcError },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Progress event structure for PSP/1 streaming
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Progress {
|
|
||||||
/// 0..=100
|
|
||||||
pub pct: u8,
|
|
||||||
/// Short phase name
|
|
||||||
pub stage: Option<String>,
|
|
||||||
/// Human-friendly detail
|
|
||||||
pub message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience helpers to build items
|
|
||||||
impl StreamItem {
|
|
||||||
pub fn progress(pct: u8, stage: impl Into<Option<String>>, message: impl Into<Option<String>>) -> Self {
|
|
||||||
StreamItem::Progress(Progress { pct, stage: stage.into(), message: message.into() })
|
|
||||||
}
|
|
||||||
pub fn ok(id: impl Into<String>, result: serde_json::Value) -> Self {
|
|
||||||
StreamItem::Result(JsonRpcResponse { jsonrpc: "2.0".into(), id: id.into(), outcome: JsonRpcOutcome::Ok { result } })
|
|
||||||
}
|
|
||||||
pub fn err(id: impl Into<String>, code: i64, message: impl Into<String>, data: Option<serde_json::Value>) -> Self {
|
|
||||||
StreamItem::Result(JsonRpcResponse {
|
|
||||||
jsonrpc: "2.0".into(),
|
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
outcome: JsonRpcOutcome::Err { error: JsonRpcError { code, message: message.into(), data } },
|
result: Some(result),
|
||||||
})
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn err(
|
||||||
|
id: impl Into<String>,
|
||||||
|
code: i32,
|
||||||
|
message: impl Into<String>,
|
||||||
|
data: Option<Value>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
result: None,
|
||||||
|
error: Some(ErrorObj {
|
||||||
|
code,
|
||||||
|
message: message.into(),
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user