diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2402448 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [ dev, main ] + pull_request: + branches: [ dev, main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and target + uses: Swatinem/rust-cache@v2 + + - name: Install components + run: rustup component add clippy rustfmt + + - name: Cargo fmt + run: cargo fmt --all -- --check + + - name: Clippy + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Test + run: cargo test --workspace --all --locked + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d1b581f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. + +## Unreleased + +### Changed +- Docs: Replace `--download-models`/`--update-models` flags with `models download`/`models update` subcommands in `README.md`, `docs/usage.md`, and `docs/development.md`. +- Host: Plugin discovery now scans `$XDG_DATA_HOME/polyscribe/plugins` (platform equivalent via `directories`) in addition to `PATH`. +- CI: Add GitHub Actions workflow to run fmt, clippy (warnings as errors), and tests for pushes and PRs. + + diff --git a/Cargo.lock b/Cargo.lock index ca9e6df..3536986 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,6 +1193,7 @@ name = "polyscribe-host" version = "0.1.0" dependencies = [ "anyhow", + "directories", "serde", "serde_json", "tokio", diff --git a/README.md b/README.md index 832ca29..30fc5f3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Installation Quickstart 1) Download a model (first run can prompt you): -- ./target/release/polyscribe --download-models +- ./target/release/polyscribe models download - In the interactive picker, use Up/Down to navigate, Space to toggle selections, and Enter to confirm. Models are grouped by base (e.g., tiny, base, small). 2) Transcribe a file: @@ -45,13 +45,14 @@ Model locations - Override via env var: POLYSCRIBE_MODELS_DIR=/path/to/models. - Force a specific model file via env var: WHISPER_MODEL=/path/to/model.bin. -Most-used CLI flags +Most-used CLI flags and subcommands - -o, --output FILE_OR_DIR: Output path base (date prefix added). If omitted, JSON prints to stdout. - -m, --merge: Merge all inputs into one output; otherwise one output per input. - --merge-and-separate: Write both merged output and separate per-input outputs (requires -o dir). - --set-speaker-names: Prompt for a speaker label per input file. -- --update-models: Verify/update local models by size/hash against the upstream manifest. -- --download-models: Interactive model list + multi-select download. +- Subcommands: + - models update: Verify/update local models by size/hash against the upstream manifest. + - models download: Interactive model list + multi-select download. - --language LANG: Language code hint (e.g., en, de). English-only models reject non-en hints. - --gpu-backend [auto|cpu|cuda|hip|vulkan]: Select backend (auto by default). - --gpu-layers N: Offload N layers to GPU when supported. @@ -65,9 +66,9 @@ Minimal usage examples - Merge multiple transcripts into one: - ./target/release/polyscribe -m -o output merged input/a.json input/b.json - Update local models non-interactively (good for CI): - - ./target/release/polyscribe --update-models --no-interaction -q + - ./target/release/polyscribe models update --no-interaction -q - Download models interactively: - - ./target/release/polyscribe --download-models + - ./target/release/polyscribe models download Troubleshooting & docs - docs/faq.md – common issues and solutions (missing ffmpeg, GPU selection, model paths) @@ -77,7 +78,7 @@ Troubleshooting & docs - docs/release-packaging.md – packaging notes for distributions - CONTRIBUTING.md – PR checklist and CI workflow -CI status: [CI badge placeholder] +CI status: ![CI](https://github.com/yourusername/yourrepo/actions/workflows/ci.yml/badge.svg) License ------- @@ -86,7 +87,7 @@ This project is licensed under the MIT License — see the LICENSE file for deta --- Workspace layout -- This repo is a Cargo workspace using resolver = "2". +- This repo is a Cargo workspace using resolver = "3". - Members: - crates/polyscribe-core — types, errors, config service, core helpers. - crates/polyscribe-protocol — PSP/1 serde types for NDJSON over stdio. diff --git a/crates/polyscribe-host/Cargo.toml b/crates/polyscribe-host/Cargo.toml index f0be386..7aae6bd 100644 --- a/crates/polyscribe-host/Cargo.toml +++ b/crates/polyscribe-host/Cargo.toml @@ -9,3 +9,4 @@ serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.142" tokio = { version = "1.47.1", features = ["rt-multi-thread", "process", "io-util"] } which = "6.0.3" +directories = { workspace = true } diff --git a/crates/polyscribe-host/src/lib.rs b/crates/polyscribe-host/src/lib.rs index 466e0ff..95db398 100644 --- a/crates/polyscribe-host/src/lib.rs +++ b/crates/polyscribe-host/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use serde::Deserialize; use std::process::Stdio; -use std::{env, fs, os::unix::fs::PermissionsExt, path::Path}; +use std::{env, fs, os::unix::fs::PermissionsExt, path::{Path, PathBuf}}; use tokio::{ io::{AsyncBufReadExt, BufReader}, process::{Child as TokioChild, Command}, @@ -20,28 +20,22 @@ impl PluginManager { pub fn list(&self) -> Result> { let mut plugins = Vec::new(); - // Scan PATH entries for executables starting with "polyscribe-plugin-" + // 1) Scan PATH entries for executables starting with "polyscribe-plugin-" if let Ok(path) = env::var("PATH") { for dir in env::split_paths(&path) { - if let Ok(read_dir) = fs::read_dir(&dir) { - for entry in read_dir.flatten() { - let path = entry.path(); - if let Some(fname) = path.file_name().and_then(|s| s.to_str()) - && 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(), - }); - } - } - } + scan_dir_for_plugins(&dir, &mut plugins); } } - // TODO: also scan XDG data plugins dir for symlinks/binaries + // 2) Scan XDG data dir: $XDG_DATA_HOME/polyscribe/plugins or platform equiv + if let Some(dirs) = directories::ProjectDirs::from("dev", "polyscribe", "polyscribe") { + let plugin_dir = dirs.data_dir().join("plugins"); + scan_dir_for_plugins(&plugin_dir, &mut plugins); + } + + // 3) De-duplicate by binary path + plugins.sort_by(|a, b| a.path.cmp(&b.path)); + plugins.dedup_by(|a, b| a.path == b.path); Ok(plugins) } @@ -107,6 +101,24 @@ fn is_executable(path: &Path) -> bool { true } +fn scan_dir_for_plugins(dir: &Path, out: &mut Vec) { + if let Ok(read_dir) = fs::read_dir(dir) { + for entry in read_dir.flatten() { + let path = entry.path(); + if let Some(fname) = path.file_name().and_then(|s| s.to_str()) + && fname.starts_with("polyscribe-plugin-") + && is_executable(&path) + { + let name = fname.trim_start_matches("polyscribe-plugin-").to_string(); + out.push(PluginInfo { + name, + path: path.to_string_lossy().to_string(), + }); + } + } + } +} + #[allow(dead_code)] #[derive(Debug, Deserialize)] struct Capability { diff --git a/docs/development.md b/docs/development.md index 757e295..35e41c4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -32,18 +32,20 @@ Run locally Models during development - Interactive downloader: - - cargo run -- --download-models + - cargo run -- models download - Non-interactive update (checks sizes/hashes, downloads if missing): - - cargo run -- --update-models --no-interaction -q + - cargo run -- models update --no-interaction -q Tests - Run all tests: - cargo test - The test suite includes CLI-oriented integration tests and unit tests. Some tests simulate GPU detection using env vars (POLYSCRIBE_TEST_FORCE_*). Do not rely on these flags in production code. -Clippy +Clippy & formatting - Run lint checks and treat warnings as errors: - cargo clippy --all-targets -- -D warnings +- Check formatting: + - cargo fmt --all -- --check - Common warnings can often be fixed by simplifying code, removing unused imports, and following idiomatic patterns. Code layout @@ -61,10 +63,10 @@ Adding a feature Running the model downloader - Interactive: - - cargo run -- --download-models + - cargo run -- models download - Non-interactive suggestions for CI: - POLYSCRIBE_MODELS_DIR=$PWD/models \ - cargo run -- --update-models --no-interaction -q + cargo run -- models update --no-interaction -q Env var examples for local testing - Use a local copy of models and a specific model file: diff --git a/docs/usage.md b/docs/usage.md index a0c5c2e..0f0d368 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -30,10 +30,10 @@ CLI reference - Choose runtime backend. Default is auto (prefers CUDA → HIP → Vulkan → CPU), depending on detection. - --gpu-layers N - Number of layers to offload to the GPU when supported. - - --download-models + - models download - Launch interactive model downloader (lists Hugging Face models; multi-select to download). - Controls: Use Up/Down to navigate, Space to toggle selections, and Enter to confirm. Models are grouped by base (e.g., tiny, base, small). - - --update-models + - models update - Verify/update local models by comparing sizes and hashes with the upstream manifest. - -v, --verbose (repeatable) - Increase log verbosity; use -vv for very detailed logs. @@ -42,6 +42,9 @@ CLI reference - --no-interaction - Disable all interactive prompts (for CI). Combine with env vars to control behavior. - Subcommands: + - models download: Launch interactive model downloader. + - models update: Verify/update local models (non-interactive). + - plugins list|info|run: Discover and run plugins. - completions : Write shell completion script to stdout. - man: Write a man page to stdout.