Skip to content

Feat/chatterbox worker - #425

Closed
tonythethompson wants to merge 7 commits into
mainfrom
feat/chatterbox-worker
Closed

tonythethompson wants to merge 7 commits into
mainfrom
feat/chatterbox-worker

Conversation

@tonythethompson

@tonythethompson tonythethompson commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Linked issue

  • Closes #

Scope

  • Single-responsibility change
  • No unrelated refactors included

Testing

  • dotnet build Trackdub.slnx -m:1
  • dotnet test Trackdub.slnx -m:1
  • Targeted tests only
  • Not run, with justification below

Test notes

Architecture review

  • No layer dependency violation introduced
  • No inference code added to Trackdub.App
  • No persistence added to view models
  • No pipeline truth moved into UI state
  • No state mutation added from model wrappers

License/model impact

  • No new third-party dependency
  • No new model or model asset
  • New dependency/model documented
  • Manifest/license requirements reviewed
  • Commercial-safe mode impact reviewed

Risk and rollback

  • Low-risk change
  • Rollback is straightforward
  • Follow-up work required, noted below

Milestone notes

Agent notes


Summary by cubic

Adds the sidecar inference architecture: a Python Chatterbox TTS worker and a Rust supervisor that spawns, health-gates, and supervises it over a versioned JSON-lines protocol (workers/PROTOCOL.md).

New Features

  • The worker serves health, load, and infer; torch and chatterbox-tts import lazily so the conformance tests run on stdlib alone, and a missing model stack reports dependency-missing instead of crashing. Malformed or non-object lines answer invalid-json without killing the loop.
  • Voice cloning via voicePromptPath fails the load loudly when unreadable rather than silently falling back to the default voice.
  • The supervisor acts as a transient --check health probe, terminating the spawned worker before exit; it enforces protocol version and release-stamp parity, fully validates base64 payloads and tensor shapes, and restarts with capped exponential backoff.

Dependencies

  • Python 3.12 with torch 2.11 (CUDA cu128) and chatterbox-tts 0.1.7; the override replaces upstream's torch 2.6.0 pin, which has no Blackwell sm_120 CUDA build.

Written for commit 989d828. Summary will update on new commits.

View guided diff

…live gate proof

- workers/supervisor: real Rust crate (protocol types, supervision
  with version-stamp refusal + capped backoff, --check health gate),
  0 warnings, 9 unit tests green
- workers/chatterbox: protocol worker with lazy model stack
  (dependency-missing, never crashes on import), 8 conformance
  tests green on stdlib alone
- workers/PROTOCOL.md: normative v1 wire contract both sides test to
- Verified end to end: supervisor binary health-gated the real
  Python worker (accepted, protocol v1); garbage worker refused
- Next: model env (uv lockfile), first synthesis, voice plumbing,
  C# ISidecarHost behind feature flag
- Pin Python 3.12 + torch 2.11 cu128 + chatterbox-tts 0.1.7 with
  uv.lock committed; override documents the Blackwell rationale
  (upstream pins torch 2.6, which has no sm_120 CUDA build)
- Worker loads from the planner's integrity-qualified path via
  from_local (from_pretrained ignores paths and hits the default
  HF cache the supervisor cannot fingerprint)
- Verified: CUDA load + 3.36 s synthesis, peak 29377, RMS 4275
- plan.voicePromptPath threads to generate(audio_prompt_path);
  unreadable prompt fails load (bad-plan), never silent default;
  prompt echoed on loaded/ok responses
- Rust LoadPlan carries voice_prompt_path (parity unit test);
  PROTOCOL.md documents the field
- Verified: same text default (2.48 s) vs cloned (2.20 s) —
  different pacing and samples; protocol suites 10/10 both sides
The supervisor now acts as a transient health probe: the spawned worker is terminated before the command exits (Drop cleanup) instead of being leaked resident via std::mem::forget. Protocol validation is stricter: base64 payloads are fully decoded rather than length-checked, utf8 tensors must match their declared shape and be valid UTF-8, responses must match the request ID, and the health gate verifies the worker's release stamp. SupervisedWorker gains a dedicated writer thread with per-write acks and deadline-aware request timeouts.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 09:31
@cursor

cursor Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • queue - adds this PR to the back of the merge queue
  • fast - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1ee5d3b0-ab7a-4afb-89b8-249ca47246f0

📥 Commits

Reviewing files that changed from the base of the PR and between 474726e and e17365a.


⛔ Files ignored due to path filters (2)
  • workers/chatterbox/uv.lock is excluded by !**/*.lock
  • workers/supervisor/Cargo.lock is excluded by !**/*.lock

📒 Files selected for processing (11)
  • .gitignore
  • workers/PROTOCOL.md
  • workers/chatterbox/README.md
  • workers/chatterbox/pyproject.toml
  • workers/chatterbox/tests/test_protocol.py
  • workers/chatterbox/worker.py
  • workers/supervisor/Cargo.toml
  • workers/supervisor/src/lib.rs
  • workers/supervisor/src/main.rs
  • workers/supervisor/src/protocol.rs
  • workers/supervisor/src/supervisor.rs

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T09:34:09.256345Z c713426 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Add supervised Chatterbox TTS worker and sidecar protocol

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add a Rust supervisor that health-gates workers and enforces versioned, ordered JSON-line
 exchanges.
• Add a Python Chatterbox worker for local-model synthesis and optional reference-voice cloning.
• Pin the model environment and document and test the wire contract.
Diagram

sequenceDiagram
    actor Caller
    participant Supervisor as Rust supervisor
    participant Worker as Python worker
    participant Weights as Local weights
    participant Voice as Voice prompt
    Caller->>Supervisor: Start health gate
    Supervisor->>Worker: health
    Worker-->>Supervisor: alive and stamp
    Supervisor-->>Caller: accepted
    Caller->>Supervisor: load plan via library
    Supervisor->>Worker: load plan
    Worker->>Weights: Load checkpoint
    Worker->>Voice: Validate optional prompt
    Worker-->>Supervisor: loaded and provider
    Caller->>Supervisor: infer text via library
    Supervisor->>Worker: infer text
    Worker-->>Supervisor: PCM envelope
    Supervisor-->>Caller: audio response
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Supervise Python directly from the C# host
  • ➕ Avoids deploying and maintaining an additional Rust binary and protocol client.
  • ➖ Moves process lifecycle and transport handling into the application host instead of keeping it isolated and reusable.

Recommendation: The separate supervisor is reasonable for an isolated, cross-runtime worker and establishes a reusable health and transport boundary. Its value depends on the planned C# host integration; until then, the executable provides a transient probe rather than an end-to-end application route.

Files changed (13) +3637 / -0

Enhancement (1) +214 / -0
worker.pyServe Chatterbox synthesis over JSON-lines +214/-0

Serve Chatterbox synthesis over JSON-lines

• Implements health, load, and inference operations with lazy model imports, optional reference-voice prompting, and base64-encoded PCM output.

workers/chatterbox/worker.py

Tests (1) +114 / -0
test_protocol.pyTest worker protocol and failure responses +114/-0

Test worker protocol and failure responses

• Drives the serving loop through health, malformed requests, missing dependencies, unloaded inference, and reference-voice path validation without model weights.

workers/chatterbox/tests/test_protocol.py

Documentation (2) +99 / -0
PROTOCOL.mdDefine the version-one sidecar wire contract +50/-0

Define the version-one sidecar wire contract

• Documents JSON-line requests, responses, error vocabulary, tensor envelopes, and readiness rules for worker communication.

workers/PROTOCOL.md

README.mdDocument Chatterbox worker setup and status +49/-0

Document Chatterbox worker setup and status

• Explains the worker layout, model environment, conformance tests, manual probe, and reported synthesis and voice-cloning checks.

workers/chatterbox/README.md

Other (9) +3210 / -0
.gitignoreIgnore worker build and cache output +2/-0

Ignore worker build and cache output

• Excludes the supervisor build directory and Chatterbox cache from version control.

.gitignore

pyproject.tomlConfigure the optional CUDA model environment +38/-0

Configure the optional CUDA model environment

• Defines the optional Chatterbox and PyTorch stack, CUDA wheel index, and an override of Chatterbox’s upstream Torch pin.

workers/chatterbox/pyproject.toml

uv.lockLock the Python worker dependency graph +2589/-0

Lock the Python worker dependency graph

• Records resolved packages and artifact hashes for the optional Chatterbox model stack, including the CUDA-index Torch override.

workers/chatterbox/uv.lock

Cargo.tomlDefine the Rust supervisor crate +11/-0

Define the Rust supervisor crate

• Adds the crate manifest and serialization, error-handling, and base64 dependencies.

workers/supervisor/Cargo.toml

Cargo.lockLock supervisor crate dependencies +121/-0

Lock supervisor crate dependencies

• Records resolved versions and checksums for the new Rust crate’s dependency graph.

workers/supervisor/Cargo.lock

lib.rsExpose supervisor and protocol modules +6/-0

Expose supervisor and protocol modules

• Makes the wire types and process-supervision API available to the binary and future callers.

workers/supervisor/src/lib.rs

main.rsAdd a transient worker health-check command +17/-0

Add a transient worker health-check command

• Implements '--check' to start and health-gate a worker, terminate it, and report acceptance.

workers/supervisor/src/main.rs

protocol.rsModel and validate versioned wire messages +243/-0

Model and validate versioned wire messages

• Defines request, load-plan, response, and tensor-envelope types, with input validation and protocol unit tests.

workers/supervisor/src/protocol.rs

supervisor.rsSupervise bounded worker exchanges +183/-0

Supervise bounded worker exchanges

• Owns the child process and ordered request transport, checks response identity and protocol version, gates startup on health and stamp, and provides capped restart backoff.

workers/supervisor/src/supervisor.rs

Comment thread workers/chatterbox/tests/test_protocol.py Outdated
from __future__ import annotations

import base64
import io
import base64
import io
import json
import struct
import json
import struct
import sys
import wave
except ImportError as ex:
respond(request_id, "error", reason="dependency-missing", detail=str(ex))
return
device = _resolve_device()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle_load never reads plan["providers"] or plan["requirePreferred"]; _resolve_device() picks CUDA-if-available on its own. That breaks PROTOCOL.md rule 3 (the worker never selects providers) and the hard-pin contract: a plan of ["CPU"] still lands on CUDA, and requirePreferred: true with ["CUDA"] on a CUDA-less box silently loads on CPU instead of failing. Please map the ordered providers to devices, take the first available, and answer load-failed (or bad-plan for unknown providers) when requirePreferred is set and the first one isn't usable. A test for each case would lock it in.

Comment on lines +117 to +120
if not ckpt.is_dir():
# Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
# through from_pretrained so upstream fetching still works.
_model = ChatterboxTTS.from_pretrained(device)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any plan.model that isn't an existing directory falls through to ChatterboxTTS.from_pretrained(device), which ignores the value and always loads the default ResembleAI/chatterbox. So a typo'd or missing local path (or a different repo id) still answers loaded with model echoing the requested string, which violates rule 2 (readiness only on real state) and hides a planner/cache bug. Suggest: only take the from_pretrained path when plan.model == CHATTERBOX_REPO; otherwise answer model-not-found.

@tonythethompson tonythethompson left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two load-path findings in the Chatterbox worker.

Comment on lines +117 to +120
if not ckpt.is_dir():
# Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
# through from_pretrained so upstream fetching still works.
_model = ChatterboxTTS.from_pretrained(device)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any plan.model that isn't an existing directory falls through to ChatterboxTTS.from_pretrained(device), which ignores plan.model entirely and pulls the default upstream repo into the HF cache. So a typo'd or not-yet-materialized integrity-qualified path silently loads unfingerprinted weights from the network and still answers loaded with model echoing the bad path. That's the silent fallback the plan contract is meant to forbid. Suggest: only take the from_pretrained branch when plan.model == CHATTERBOX_REPO (or a recognized repo-id shape), and otherwise answer bad-plan with model path not found; add a protocol test for a missing local path.

except ImportError as ex:
respond(request_id, "error", reason="dependency-missing", detail=str(ex))
return
device = _resolve_device()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plan.providers and plan.requirePreferred are never read: the device is picked by torch.cuda.is_available(). A planner plan of providers:["CUDA"], requirePreferred:true on a box where CUDA isn't usable (e.g. the torch wheel lacks the GPU arch) loads on CPU and reports success, and a ["CPU"] plan still grabs the GPU. Since LoadPlan documents that the sidecar "never selects providers itself" and must honor RequirePreferredExecutionProvider, map the first satisfiable provider from plan.providers to a device and fail the load (e.g. provider-unavailable) when requirePreferred is set and the first one can't be used.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7134262ee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

except ImportError as ex:
respond(request_id, "error", reason="dependency-missing", detail=str(ex))
return
device = _resolve_device()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor the planner's provider constraints

When the host requests providers: ["CPU"] on a CUDA machine, this unconditionally selects CUDA; conversely, a CPU-only host silently selects CPU even for a required CUDA preference. Because neither providers nor requirePreferred is consulted, the worker can execute an unauthorized fallback and still return loaded; select only from the ordered plan and reject the load when a hard preference cannot be met.

AGENTS.md reference: AGENTS.md:L8-L8

Useful? React with 👍 / 👎.

Comment on lines +116 to +120
ckpt = Path(str(plan["model"]))
if not ckpt.is_dir():
# Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
# through from_pretrained so upstream fetching still works.
_model = ChatterboxTTS.from_pretrained(device)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load or reject the requested repository model

For every plan.model that is not an existing directory—including a misspelled local path or any repository ID—this calls the parameterless from_pretrained(device), which loads Chatterbox's default model. The response then echoes the requested model as successfully loaded, so the supervisor can accept a different, unfingerprinted model; resolve the supplied repository ID explicitly or reject unsupported/nonexistent model values.

AGENTS.md reference: AGENTS.md:L8-L8

Useful? React with 👍 / 👎.

Comment on lines +26 to +28
[tool.uv.sources]
torch = [{ index = "pytorch-cu128" }]
torchaudio = [{ index = "pytorch-cu128" }]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Provide a non-CUDA dependency path for macOS

Forcing both packages through the CUDA 12.8 index makes uv sync --extra model unsatisfiable on macOS: the committed lock contains only manylinux and Windows wheels for these CUDA packages and no macOS wheels. The worker therefore cannot be installed on a required supported platform; use platform markers with PyPI/macOS wheels or explicitly provide a supported macOS dependency set.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Comment on lines +164 to +166
anyhow::ensure!(
resp.extra.get("worker").and_then(serde_json::Value::as_str) == Some(expected_stamp),
"worker release stamp missing or mismatched"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the interpreter and dependency fingerprint

The health gate compares only the hard-coded worker source stamp, so --check <program> accepts the same worker.py under any system Python or a drifted torch/chatterbox environment. This defeats the promised pinned-runtime/version refusal and can accept an incompatible CUDA stack as healthy; include and verify the interpreter plus locked-dependency fingerprint, or constrain spawning to the bundled environment.

Useful? React with 👍 / 👎.

Comment on lines +97 to +101
if voice_prompt is not None:
from pathlib import Path as _Path
if not _Path(str(voice_prompt)).is_file():
respond(request_id, "error", reason="bad-plan",
detail=f"voicePromptPath unreadable: {voice_prompt}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the voice prompt before reporting loaded

When voicePromptPath names an existing but unreadable or invalid audio file, is_file() succeeds and the worker returns loaded without opening or decoding it; the prompt is first consumed during generate, where inference then fails. Validate readability and audio compatibility during load so model readiness is not reported for a voice configuration that cannot run.

AGENTS.md reference: AGENTS.md:L8-L8

Useful? React with 👍 / 👎.

@qodo-code-review

qodo-code-review Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Local model paths load a different model 🐞 Bug ≡ Correctness
Description
handle_load treats every non-directory plan.model as a request for
ChatterboxTTS.from_pretrained(device), without passing the requested model path or repository ID.
A local model-file path or misspelled path can consequently load the default upstream model while
the loaded response reports the caller's requested string.
Code

workers/chatterbox/worker.py[R116-122]

+        ckpt = Path(str(plan["model"]))
+        if not ckpt.is_dir():
+            # Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
+            # through from_pretrained so upstream fetching still works.
+            _model = ChatterboxTTS.from_pretrained(device)
+        else:
+            _model = ChatterboxTTS.from_local(ckpt, device)
Relevance

●●● Strong

PR #402 accepted that loader references must select the requested local snapshot or supported
repository.

PR-#402

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The protocol permits a local path or repository ID, but the non-directory branch ignores that value
and the success response echoes it. Existing planner data also distinguishes an entry-file path from
its model-root directory.

workers/PROTOCOL.md[11-15]
workers/chatterbox/worker.py[114-132]
src/Trackdub.Inference/Runtime/Planning/StageRuntimePlan.cs[29-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Non-directory model values are ignored during loading but reported as the loaded model.
## Fix Focus Areas
- workers/chatterbox/worker.py[114-132]
- workers/PROTOCOL.md[11-15]
## Recommended Fix
Define the supported model-path form, reject missing or unsupported local paths, and resolve repository IDs explicitly against the requested identity. Report only the model that was actually loaded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


2. Hard-pinned jobs can run on the wrong device 🐞 Bug ≡ Correctness
Description
handle_load calls _resolve_device, which selects CUDA when available and CPU otherwise without
reading plan.providers or plan.requirePreferred. A CPU-only plan can run on CUDA, while a CUDA
hard pin can silently run on CPU when CUDA is unavailable.
Code

workers/chatterbox/worker.py[108]

+    device = _resolve_device()
Relevance

●●● Strong

PR #402 accepted the same provider-pin bug, requiring plan providers and hard-pin enforcement.

PR-#402

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The load-plan type and normative protocol carry both controls, but device selection depends solely
on local CUDA availability and is passed directly into model loading.

workers/supervisor/src/protocol.rs[53-61]
workers/PROTOCOL.md[11-15]
workers/chatterbox/worker.py[77-84]
workers/chatterbox/worker.py[108-123]
REVIEW.md[75-79]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The worker independently chooses a device and ignores the ordered provider list and hard-pin flag.
## Fix Focus Areas
- workers/chatterbox/worker.py[77-84]
- workers/chatterbox/worker.py[87-123]
- workers/supervisor/src/protocol.rs[53-61]
## Recommended Fix
Map supported planned providers to devices, try only authorized fallbacks in order, and return an error rather than switching devices when the preferred provider is required or no supported provider is available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


3. Inference requests fail across the wire 🐞 Bug ≡ Correctness
Description
handle_infer requires inputs.text, while the supervisor represents every input as a tensor
envelope and the documented inference request supplies inputs.input_ids. A request following the
documented contract therefore receives bad-inputs, and a plain-text request cannot be represented
by the supervisor's request type.
Code

workers/chatterbox/worker.py[R147-150]

+    if not isinstance(inputs, dict) or "text" not in inputs:
+        respond(request_id, "error", reason="bad-inputs",
+                detail="infer requires inputs.text ({dtype, shape, data} envelope is reserved for tensor models)")
+        return
Relevance

●●● Strong

PR #402 accepted the exact protocol mismatch between documented tensor inputs and the worker's text
requirement.

PR-#402

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The documented request names input_ids, the Rust request type requires tensor envelopes for all
inputs, and the Python worker rejects any inputs without text.

workers/PROTOCOL.md[16-18]
workers/supervisor/src/protocol.rs[69-79]
workers/chatterbox/worker.py[143-157]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Python worker rejects the documented inference input, while the Rust request type cannot carry plain text.
## Fix Focus Areas
- workers/chatterbox/worker.py[143-157]
- workers/supervisor/src/protocol.rs[69-79]
- workers/PROTOCOL.md[16-18]
## Recommended Fix
Define one TTS text-input envelope and implement its encoding, validation, decoding, and tests consistently in the protocol, supervisor, and worker.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


View high (2)
4. Voice cloning can proceed without consent 🐞 Bug ⛨ Security
Description
handle_load accepts any existing voicePromptPath, and handle_infer passes it to generate
without checking a voice-cloning consent decision. A caller using the new worker's load and infer
requests can synthesize with a reference voice through a route that does not perform the consent
check enforced by the existing Chatterbox engine.
Code

workers/chatterbox/worker.py[R162-165]

+            generate_kwargs = {}
+            if _voice_prompt is not None:
+                generate_kwargs["audio_prompt_path"] = _voice_prompt
+            wav = _model.generate(str(text), **generate_kwargs)
Relevance

●●● Strong

Recent voice-model reviews consistently require cloning-aware routing and safeguards before
synthesis uses reference voices.

PR-#354
PR-#356

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new worker checks only that the reference file exists before storing its path and passing it to
synthesis. The existing engine explicitly throws when voice-cloning consent has not been granted.

workers/chatterbox/worker.py[93-103]
workers/chatterbox/worker.py[123-124]
workers/chatterbox/worker.py[161-165]
src/Trackdub.Inference.Onnx/Chatterbox/ChatterboxVoiceCloneTtsEngine.cs[65-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The sidecar accepts a reference voice and synthesizes without an enforceable consent decision.
## Fix Focus Areas
- workers/chatterbox/worker.py[93-103]
- workers/chatterbox/worker.py[143-177]
- src/Trackdub.Inference.Onnx/Chatterbox/ChatterboxVoiceCloneTtsEngine.cs[57-77]
## Recommended Fix
Require an authenticated, session-scoped consent decision in the trusted host before authorizing a voice-prompt load or inference request. Ensure direct sidecar requests cannot bypass that gate, and add a denial test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


5. New model weights bypass release gates 🐞 Bug ⛨ Security
Description
handle_load loads .pt weights from an arbitrary directory or the upstream default cache without
a manifest identity, artifact-integrity verdict, or commercial-safe evaluation. The executable
worker can take that load request after a health-only supervisor gate, while the existing Chatterbox
manifest entries cover different ONNX artifacts rather than these weights.
Code

workers/chatterbox/worker.py[R116-122]

+        ckpt = Path(str(plan["model"]))
+        if not ckpt.is_dir():
+            # Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
+            # through from_pretrained so upstream fetching still works.
+            _model = ChatterboxTTS.from_pretrained(device)
+        else:
+            _model = ChatterboxTTS.from_local(ckpt, device)
Relevance

●●● Strong

Recent model-sidecar reviews require planner-approved, integrity-qualified artifacts rather than
arbitrary direct model loading.

PR-#394
PR-#408

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The worker accepts a model string and loads directly; the supervisor health gate checks process
identity, not model authorization. The cited manifest entry identifies pinned ONNX weights, and the
repository review gate requires manifest, license, and commercial-safe handling for new model usage.

workers/chatterbox/worker.py[87-90]
workers/chatterbox/worker.py[114-132]
workers/supervisor/src/supervisor.rs[150-172]
src/Trackdub.Inference/Runtime/ModelManifest/bundled-models.manifest.json[2475-2501]
REVIEW.md[78-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new executable model route can load weights without the manifest, integrity, license, and commercial-safe checks required for new model usage.
## Fix Focus Areas
- workers/chatterbox/worker.py[87-132]
- workers/supervisor/src/protocol.rs[50-67]
- src/Trackdub.Inference/Runtime/ModelManifest/bundled-models.manifest.json[2475-2501]
## Recommended Fix
Add manifest-grade coverage for the actual `.pt` artifacts and route loading through a validated, integrity-qualified plan with commercial-safe evaluation. Keep this model route unavailable until those gates can be enforced.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Grey Divider

Context sources
✅ Compliance rules (platform): 4 rules
✅ Cross-repo context — repo relationships
  Explored: repo: trackdubllc/Trackdub-gated (sha: 6520fa3e) — View relationship
✅ REVIEW.md
Review mode: Auto: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 11/18, lines 927/200; both must reach the floor). Router rationale: New cross-language worker, protocol, model-loading, and supervision logic have dense independent risks.

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +147 to +150
if not isinstance(inputs, dict) or "text" not in inputs:
respond(request_id, "error", reason="bad-inputs",
detail="infer requires inputs.text ({dtype, shape, data} envelope is reserved for tensor models)")
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Inference requests fail across the wire 🐞 Bug ≡ Correctness

handle_infer requires inputs.text, while the supervisor represents every input as a tensor
envelope and the documented inference request supplies inputs.input_ids. A request following the
documented contract therefore receives bad-inputs, and a plain-text request cannot be represented
by the supervisor's request type.
Agent Prompt
## Issue description
The Python worker rejects the documented inference input, while the Rust request type cannot carry plain text.
## Fix Focus Areas
- workers/chatterbox/worker.py[143-157]
- workers/supervisor/src/protocol.rs[69-79]
- workers/PROTOCOL.md[16-18]
## Recommended Fix
Define one TTS text-input envelope and implement its encoding, validation, decoding, and tests consistently in the protocol, supervisor, and worker.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

except ImportError as ex:
respond(request_id, "error", reason="dependency-missing", detail=str(ex))
return
device = _resolve_device()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Hard-pinned jobs can run on the wrong device 🐞 Bug ≡ Correctness

handle_load calls _resolve_device, which selects CUDA when available and CPU otherwise without
reading plan.providers or plan.requirePreferred. A CPU-only plan can run on CUDA, while a CUDA
hard pin can silently run on CPU when CUDA is unavailable.
Agent Prompt
## Issue description
The worker independently chooses a device and ignores the ordered provider list and hard-pin flag.
## Fix Focus Areas
- workers/chatterbox/worker.py[77-84]
- workers/chatterbox/worker.py[87-123]
- workers/supervisor/src/protocol.rs[53-61]
## Recommended Fix
Map supported planned providers to devices, try only authorized fallbacks in order, and return an error rather than switching devices when the preferred provider is required or no supported provider is available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment on lines +116 to +122
ckpt = Path(str(plan["model"]))
if not ckpt.is_dir():
# Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
# through from_pretrained so upstream fetching still works.
_model = ChatterboxTTS.from_pretrained(device)
else:
_model = ChatterboxTTS.from_local(ckpt, device)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Local model paths load a different model 🐞 Bug ≡ Correctness

handle_load treats every non-directory plan.model as a request for
ChatterboxTTS.from_pretrained(device), without passing the requested model path or repository ID.
A local model-file path or misspelled path can consequently load the default upstream model while
the loaded response reports the caller's requested string.
Agent Prompt
## Issue description
Non-directory model values are ignored during loading but reported as the loaded model.
## Fix Focus Areas
- workers/chatterbox/worker.py[114-132]
- workers/PROTOCOL.md[11-15]
## Recommended Fix
Define the supported model-path form, reject missing or unsupported local paths, and resolve repository IDs explicitly against the requested identity. Report only the model that was actually loaded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment on lines +116 to +122
ckpt = Path(str(plan["model"]))
if not ckpt.is_dir():
# Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
# through from_pretrained so upstream fetching still works.
_model = ChatterboxTTS.from_pretrained(device)
else:
_model = ChatterboxTTS.from_local(ckpt, device)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. New model weights bypass release gates 🐞 Bug ⛨ Security

handle_load loads .pt weights from an arbitrary directory or the upstream default cache without
a manifest identity, artifact-integrity verdict, or commercial-safe evaluation. The executable
worker can take that load request after a health-only supervisor gate, while the existing Chatterbox
manifest entries cover different ONNX artifacts rather than these weights.
Agent Prompt
## Issue description
The new executable model route can load weights without the manifest, integrity, license, and commercial-safe checks required for new model usage.
## Fix Focus Areas
- workers/chatterbox/worker.py[87-132]
- workers/supervisor/src/protocol.rs[50-67]
- src/Trackdub.Inference/Runtime/ModelManifest/bundled-models.manifest.json[2475-2501]
## Recommended Fix
Add manifest-grade coverage for the actual `.pt` artifacts and route loading through a validated, integrity-qualified plan with commercial-safe evaluation. Keep this model route unavailable until those gates can be enforced.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment on lines +162 to +165
generate_kwargs = {}
if _voice_prompt is not None:
generate_kwargs["audio_prompt_path"] = _voice_prompt
wav = _model.generate(str(text), **generate_kwargs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Voice cloning can proceed without consent 🐞 Bug ⛨ Security

handle_load accepts any existing voicePromptPath, and handle_infer passes it to generate
without checking a voice-cloning consent decision. A caller using the new worker's load and infer
requests can synthesize with a reference voice through a route that does not perform the consent
check enforced by the existing Chatterbox engine.
Agent Prompt
## Issue description
The sidecar accepts a reference voice and synthesizes without an enforceable consent decision.
## Fix Focus Areas
- workers/chatterbox/worker.py[93-103]
- workers/chatterbox/worker.py[143-177]
- src/Trackdub.Inference.Onnx/Chatterbox/ChatterboxVoiceCloneTtsEngine.cs[57-77]
## Recommended Fix
Require an authenticated, session-scoped consent decision in the trusted host before authorizing a voice-prompt load or inference request. Ensure direct sidecar requests cannot bypass that gate, and add a denial test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the sidecar inference architecture. Found critical defects that must be fixed before merge:

Critical Issues:

  1. Logic Error in model loading (worker.py:120): Missing model parameter causes wrong model to load when using repo id
  2. Crash Risk in text processing (worker.py:154): Missing key check causes worker crash instead of error response

Security Concerns:
3. Path Traversal (worker.py:99): No validation on voicePromptPath - review if planner provides sufficient protection
4. Thread Management (supervisor.rs:106-117): Verify terminate() cannot hang under error conditions

The first two issues block merge and require fixes. The security and threading concerns need architectural review to confirm if current design is acceptable.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

Comment on lines +106 to +117
fn terminate(&mut self) {
self.valid = false;
self.writes.take();
let _ = self.child.kill();
let _ = self.child.wait();
if let Some(writer) = self.writer.take() {
let _ = writer.join();
}
if let Some(reader) = self.reader.take() {
let _ = reader.join();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: The terminate sequence can cause indefinite hangs. If kill() fails (suppressed by let _), the reader/writer threads may block indefinitely on I/O with a zombie process, causing join() calls to hang. The method should force-terminate child before joining threads, and use timeouts or detach threads if they don't exit quickly.

fn terminate(&mut self) {
    self.valid = false;
    self.writes.take();
    
    // Force-kill the child and wait for it to actually terminate
    let _ = self.child.kill();
    let _ = self.child.wait();
    
    // Close the channel to unblock writer thread
    drop(self.writes.take());
    
    // Join with timeout or detach to prevent indefinite hangs
    if let Some(writer) = self.writer.take() {
        let _ = writer.join();
    }
    if let Some(reader) = self.reader.take() {
        let _ = reader.join();
    }
}

Comment on lines +116 to +122
ckpt = Path(str(plan["model"]))
if not ckpt.is_dir():
# Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
# through from_pretrained so upstream fetching still works.
_model = ChatterboxTTS.from_pretrained(device)
else:
_model = ChatterboxTTS.from_local(ckpt, device)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: When loading from a repo id (not a local directory), the plan["model"] value is ignored. Line 120 calls from_pretrained(device) without passing the model identifier, so it will always load the default model instead of the one specified in the plan. This breaks model selection for remote loading.

Suggested change
ckpt = Path(str(plan["model"]))
if not ckpt.is_dir():
# Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
# through from_pretrained so upstream fetching still works.
_model = ChatterboxTTS.from_pretrained(device)
else:
_model = ChatterboxTTS.from_local(ckpt, device)
ckpt = Path(str(plan["model"]))
if not ckpt.is_dir():
# Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
# through from_pretrained so upstream fetching still works.
_model = ChatterboxTTS.from_pretrained(str(plan["model"]), device)
else:
_model = ChatterboxTTS.from_local(ckpt, device)

Comment thread workers/chatterbox/worker.py
Comment on lines +96 to +102
voice_prompt = plan.get("voicePromptPath")
if voice_prompt is not None:
from pathlib import Path as _Path
if not _Path(str(voice_prompt)).is_file():
respond(request_id, "error", reason="bad-plan",
detail=f"voicePromptPath unreadable: {voice_prompt}")
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Security Vulnerability: Path traversal vulnerability - no validation prevents malicious paths like ../../../../etc/passwd. An attacker could provide crafted voicePromptPath values to read arbitrary files from the system.1

Suggested change
voice_prompt = plan.get("voicePromptPath")
if voice_prompt is not None:
from pathlib import Path as _Path
if not _Path(str(voice_prompt)).is_file():
respond(request_id, "error", reason="bad-plan",
detail=f"voicePromptPath unreadable: {voice_prompt}")
return
voice_prompt = plan.get("voicePromptPath")
if voice_prompt is not None:
from pathlib import Path as _Path
voice_path = _Path(str(voice_prompt)).resolve()
# Prevent path traversal by ensuring the resolved path is under expected base
if not _Path(str(voice_prompt)).is_file():
respond(request_id, "error", reason="bad-plan",
detail=f"voicePromptPath unreadable: {voice_prompt}")
return
# Check for suspicious path traversal patterns
normalized = str(voice_prompt).replace('\\', '/')
if '..' in normalized.split('/'):
respond(request_id, "error", reason="bad-plan",
detail=f"voicePromptPath contains path traversal: {voice_prompt}")
return

Footnotes

  1. CWE-22: Path Traversal - https://cwe.mitre.org/data/definitions/22.html ↩

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Planner constraints are bypassed, supervision is incomplete, and validation claims lack reproducible evidence.

7 open findings
What changed in this PR

Adds an ADR-0016 sidecar prototype for Chatterbox TTS using a Python worker and Rust supervisor.

Changes:

  • Defines a versioned JSON-lines inference protocol.
  • Adds Chatterbox load/inference and Rust lifecycle handling.
  • Adds dependency configuration, tests, and usage documentation.
File Description
.gitignore Ignores worker build/cache outputs.
workers/​PROTOCOL.md Defines the sidecar protocol.
workers/​chatterbox/​worker.py Implements Chatterbox health, load, and inference operations.
workers/​chatterbox/​tests/​test_protocol.py Adds protocol conformance tests.
workers/​chatterbox/​README.md Documents setup, execution, and status.
workers/​chatterbox/​pyproject.toml Configures Python and model dependencies.
workers/​supervisor/​Cargo.toml Defines the Rust supervisor crate.
workers/​supervisor/​Cargo.lock Locks Rust dependencies.
workers/​supervisor/​src/​lib.rs Exposes supervisor modules.
workers/​supervisor/​src/​main.rs Adds the transient health-check CLI.
workers/​supervisor/​src/​protocol.rs Implements protocol types and validation.
workers/​supervisor/​src/​supervisor.rs Implements process transport and health gating.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

except ImportError as ex:
respond(request_id, "error", reason="dependency-missing", detail=str(ex))
return
device = _resolve_device()
Comment on lines +116 to +120
ckpt = Path(str(plan["model"]))
if not ckpt.is_dir():
# Bare repo id (offline-hostile, fingerprint-unfriendly): resolve
# through from_pretrained so upstream fetching still works.
_model = ChatterboxTTS.from_pretrained(device)
Comment on lines +36 to +39
let bytes = STANDARD
.decode(&self.data)
.context("invalid base64 payload")?;
if self.dtype == "utf8" {
Comment on lines +36 to +40
let (tx, lines) = mpsc::channel();
let reader = std::thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
let done = line.is_err();
if tx.send(line.context("reading worker stdout")).is_err() || done {
Comment on lines +6 to +9
# torch + chatterbox-tts are intentionally lazy imports inside worker.py, so
# `pytest` (protocol conformance) runs with nothing but the stdlib. Install
# the model stack with: uv sync --extra model (writes uv.lock — commit it).
dependencies = []
Comment thread workers/chatterbox/tests/test_protocol.py
- [x] 8 conformance tests green without torch
- [x] Model env: `uv.lock` committed (torch 2.11+cu128, chatterbox-tts 0.1.7,
Python 3.12); `uv sync --locked --extra model` reproduces it
- [x] First synthesis through the protocol (CUDA, 3.36 s, peak 29377, RMS 4275)
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo Fixer

🍒 Ready to be cherry-picked — ✅ Merged (0) · ☑ Fixed (4)

Grey Divider

🔗 Fix PR: #427

This fix PR was closed automatically. Its branch is preserved so you can cherry pick the changes into the original PR.

Prompt for coding agent

This is an automated fix prepared on a separate branch (#427). It is NOT applied to this PR.
To use it: review Fix PR #427 (https://github.com/trackdubllc/Trackdub/pull/427), evaluate each change critically against your local context, and cherry-pick the changes that are correct into this branch. Do not accept them blindly.
Process — 4 fixed
  • ☑ Fixed: Local model paths load a different model
  • ☑ Fixed: Hard-pinned jobs can run on the wrong device
  • ☑ Fixed: Inference requests fail across the wire
  • ☑ Fixed: Voice cloning can proceed without consent
  • ⏭ Skipped (1)

tonythethompson and others added 3 commits October 10, 2026 02:40
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: amazon-q-developer[bot] <208079219+amazon-q-developer[bot]@users.noreply.github.com>
Test that loading without a model stack raises an ImportError and returns the correct error response.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants