Skip to main content

Corpus-to-character pipeline: synthesize a fine-tuning dataset in a target voice, dedup it, triage it, assemble it, train a QLoRA adapter, and deploy via Ollama.

Project description

voicepipe — a corpus-to-character pipeline

Models built with voicepipe

All three are public ollama tags — ollama run <name> to talk to any of them:

  • execxd/mistral-nemo-12b-marcus-aurelius — Mistral Nemo 12B QLoRA fine-tune, in the voice of Marcus Aurelius (121–180 CE), in the register of George Long's 1862 Meditations translation. Aphoristic Stoic self-address; cosmic perspective; second-person directives.
  • execxd/mistral-nemo-12b-oscar-wilde — Mistral Nemo 12B QLoRA fine-tune, in the voice of Oscar Wilde (1854–1900). End-to-end validation of the pipeline on a fresh character; the full project ships in examples/oscar-wilde/.
  • execxd/mistral-nemo-12b-francis-e-dec — Mistral Nemo 12B QLoRA fine-tune, in the voice of Francis E. Dec, Esq. (1926–1996). The original proof-of-concept that drove the engine's design.

Get it

Desktop app (recommended): grab the build for your OS (macOS .dmg, Windows .msi, Linux .AppImage/.deb) from the releases page, open it, and configure + run the pipeline in the window. It's self-contained — no Python or other setup needed. (macOS: the build is currently unsigned; notarization is on the roadmap. As a workaround you can either (a) run xattr -dr com.apple.quarantine /Applications/voicepipe.app after first launch — note that path translocation of an unsigned .app with a nested Mach-O sidecar can still break bundled-Python path resolution, and the quarantine attribute may return after macOS updates — or (b) build from source, see below.) On first run, open Settings (bottom-left of the sidebar) and paste your Ollama Cloud API key — synthesis, triage, and the category proposer need it.

Or from source (also gives you the CLI):

pip install -e ".[gui]"               # the engine + the web GUI/control server
voicepipe new my-character            # scaffold a project from a template (also: --template blank, --list-templates)
#   ...drop source texts in my-character/corpus/ , a few example pairs in my-character/seeds/seed_pairs.jsonl
export OLLAMA_API_KEY=ollama_...      # (or set it in the GUI's Settings)
voicepipe serve                       # http://127.0.0.1:8765 — configure + run the pipeline in the browser
# or on the CLI:
voicepipe synthesize --project my-character   # → dedup → triage → assemble → train → deploy

voicepipe train needs the CUDA-box extras: pip install --index-url https://download.pytorch.org/whl/cu128 torch==2.11.0 && pip install -e ".[train]" -c constraints-train.txt (the constraints file pins the Blackwell-sm_120 known-good set). voicepipe deploy needs [deploy]

  • a llama.cpp checkout. Both can run on a different machine than the GUI — set its address in the app's Settings → "Connect to a remote engine" (the box runs voicepipe serve --host 0.0.0.0 --auth-token …), and the GUI drives jobs there.

To build the desktop app yourself: desktop/build-sidecar.sh && (cd desktop && cargo tauri build) — see desktop/README.md.

Layout

pipeline/                  the generalized engine (a Python package)
  project.py               the Project config schema + load_project(dir)  ← the product's data model
  cli.py / __main__.py      `voicepipe <command>` / `python -m pipeline <command>`
  scaffold.py              create a project from a bundled template
  templates/               bundled project templates (character, blank)
  events.py                structured progress events (NDJSON; opt-in via env var; no-op for plain CLI)
  jobs.py                  process supervision — run a stage as a tracked subprocess
  server.py                FastAPI control server + the web GUI host  (`voicepipe serve`)
  webui/                   the web UI (dependency-free HTML/CSS/JS)
  synthesize.py            generate (user, response) pairs
  dedup.py                 cosine dedup + over-saturated-phrase caps
  triage.py                LLM-judge score 1-5 + policy flags
  assemble.py              combine kept synth + seeds + salvage → train/val
  train.py                 QLoRA fine-tune  ← config-driven, runs on a CUDA box
  deploy.py                LoRA → GGUF → Modelfile → ollama create → push  ← config-driven
  categorize.py            propose weighted prompt categories from a corpus (LLM)
  eval.py / infer.py       LLM-judge eval; quick inference test grid  (infer is --project-wired; eval not yet)
  providers/               LLM provider abstraction (Ollama Cloud; any OpenAI-compatible endpoint)
  serve/discord_bot.py     a Discord front-end (with an output moderation layer)
  GUI_API.md               the REST API + event schema (the GUI/CLI contract)
  REFACTOR_STATUS.md       what's config-driven, what isn't yet
desktop/                   Tauri (Rust) desktop shell — webview + a loopback `voicepipe serve` sidecar
deploy/                    the web-only deployment: a systemd unit + install script + PACKAGING.md
examples/                  shipped reference projects
  oscar-wilde/             a fully-worked configuration — corpus, seeds, prompts, project.toml
projects/                  per-project configs (your own go here)
scratch/                   .gitignored — working data, experiments, the dec-bot WIP
  dec-bot/
    project.toml           the dec-bot configuration (everything: modes, categories, hyperparams, ...)
    prompts/               long prose blocks referenced from project.toml
    corpus/ seeds/ dataset/  the dec-bot inputs and produced artifacts
    DEC_BOT_NOTES.md       project-specific guidance ; WRITEUP.md  the v1 runbook
pyproject.toml             package metadata; extras: [gui] [train] [serve] [deploy]

A project

A project is a directory with a project.toml plus its corpus/seeds. project.toml carries all the configuration — scalars and small lists inline, long prose blocks referenced by filename (prompts/*.md). pipeline.project.load_project(dir) reads it and fills every unspecified field with a default, so a minimal project is just name, a corpus, and a mode. voicepipe new <name> scaffolds one; the GUI's "New project" does the same; or copy examples/oscar-wilde/ (the fully-worked example). See pipeline/project.py for the schema.

Commands

voicepipe new NAME [--template character|blank] [--description ...]   # scaffold a project
voicepipe serve [--port 8765] [--host 0.0.0.0 --auth-token T] [--unix-socket PATH]   # control server + web GUI
voicepipe categorize  --project DIR [--n 12] [--adopt]               # propose prompt categories from the corpus
voicepipe synthesize  --project DIR                                  # generate (user, response) pairs
voicepipe dedup       --project DIR
voicepipe triage      --project DIR
voicepipe assemble    --project DIR
voicepipe train       --project DIR [--smoke] [--gpu N]              # QLoRA  (needs [train])
voicepipe deploy      --project DIR --adapter dataset/adapter/final --llama-cpp-dir PATH [--push]   # (needs [deploy])
voicepipe infer       --project DIR                                  # quick inference grid against the trained adapter

The GUI exposes exactly these — nothing it can do is unavailable on the CLI, and vice versa. Set VOICEPIPE_EVENTS_FILE=path (or pass --unix-socket/run via the GUI) to get a structured NDJSON progress stream out of any stage; without it the stages just print their usual human output.

The GUI / control server

voicepipe serve runs a small FastAPI app: a REST API under /v1/ (projects, config, stage runs, jobs, an SSE event stream) plus the web UI at /. Bound to 127.0.0.1 it needs no auth; bound to a public host it requires --auth-token (the web UI prompts for it). The desktop app (desktop/) wraps the same server on a loopback port — see deploy/PACKAGING.md for the two packaging stories (native app vs. web-only-with-systemd) and pipeline/GUI_API.md for the API.

Status

  • Config-driven, end-to-end: project.py (schema), synthesize, dedup, triage, assemble, train, deploy, categorize, cli, events, jobs, server, scaffold, providers. Each stage is --project DIR-driven and emits structured events. The examples/oscar-wilde/ project is the worked example; two models have been built end-to-end with the pipeline (see "Models built with voicepipe" above).
  • Partial: eval.py isn't --project-wired yet (infer.py is). serve/discord_bot.py is env-parametrized; its moderation patterns could become a project config block. The synthesis/triage stages call the Ollama Cloud client directly — generalizing them to honor a per-project provider entry is a small follow-up. The Tauri desktop scaffold builds against a PATH voicepipe; bundling a standalone Python (and the "bind a UDS, no TCP port" refinement) is TODO. See pipeline/REFACTOR_STATUS.md and pipeline/GUI_API.md.

Content note

Every project ships absolute content rules in prompts/content_rules.md (enforced at synthesis time) and the corresponding flags in prompts/triage_rubric.md (enforced at curation time): no slurs, no group-attributed villainy, no post-cutoff references for the character's era, no breaking frame, no serious real-world-harm instructions. These are the same rules the deployed model is held to. The character template seeds them as defaults for any new project; the examples/oscar-wilde/ project carries them verbatim.

For voices that touch sensitive material — like the dec-bot proof-of-concept, whose source corpus is paranoid-schizophrenic writing from the 1970s–80s — clean the corpus upstream and add a deploy-time output moderation layer (see serve/discord_bot.py for the dec-bot's).

Research applications

voicepipe is, primarily, a creative-writing pipeline. The same engine generalizes to a broader spectrum of research applications — detection ("is this text human-authored or voicepipe-emulated?"), watermarking and provenance, organizational and authorial style modeling, synthetic-data generation for red/blue exercises, and impersonation-defense tooling. We're intellectually honest that the technology is dual-use; the literary framing is where the project lives and is evaluated, but pretending the dual-use surface doesn't exist wouldn't change anything except the depth of the conversation.

A natural follow-up research question sits one step away from voicepipe: can ML adversaries reliably detect voicepipe-finetuned output from genuinely-authored or generic-LLM text, and on what features? If you're working on that question — or any of the others above — drop a note in issues; we'd love to compare findings.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

voicepipe_engine-0.2.1.tar.gz (121.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

voicepipe_engine-0.2.1-py3-none-any.whl (132.5 kB view details)

Uploaded Python 3

File details

Details for the file voicepipe_engine-0.2.1.tar.gz.

File metadata

  • Download URL: voicepipe_engine-0.2.1.tar.gz
  • Upload date:
  • Size: 121.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for voicepipe_engine-0.2.1.tar.gz
Algorithm Hash digest
SHA256 12c75569140c94618d73d07d3c6733049ee850a59ee914bfa78ea092cdc0131f
MD5 b03c7bb969eb95911cc227e58c2851cf
BLAKE2b-256 a3bbc9e0b21cc902e47208a91e07965da12d9687094e1768d5b4171dd41701be

See more details on using hashes here.

File details

Details for the file voicepipe_engine-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for voicepipe_engine-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8cb7ad13be7eb4d00ed9fd78b66f6d6cff59b75e59c9df166c2bd46a0dba76c8
MD5 fc6eeb9cdaee8c3ab5425c9477b36bd0
BLAKE2b-256 b5894300f1e5498c9dc01dab97cf9293a6e9f8380b85b700c442c4707de9a25a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page