feat(tooling): out-of-worktree sr-voice discovery + distrobox wrap + prompt capture (#833)
Three additions unlocked by the gfx1201 ROCm debug session. 1. _find_sr_voice() resolves the default binary path to ~/Projects/settled-reach/binaries/sr-voice-rocm (persistent across worktree lifetimes) with a legacy fallback to the main workdir's cargo target dir. Matches #850's plan to ship platform binaries outside the repo. 2. --distrobox <name> wraps the sr-voice subprocess in `distrobox enter <name> --` when the built binary depends on libs that only exist inside a dev container (libhipblas.so.2 on a Bazzite host). Stdio JSONL protocol flows through unchanged. 3. --dump-prompts PATH captures the attempt-0 prompt for every feature as JSONL without calling an LLM. Force --mock and short-circuit name_feature to return a unique deterministic placeholder. Used to feed the same prompt set to alternate backends (Haiku agent, other models) for offline A/B comparison of naming quality independent of the sampling backend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,11 +65,51 @@ DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
|
||||
WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems"
|
||||
BLOCKLIST_PATH = TOOLING_DIR / "earth_blocklist.txt"
|
||||
|
||||
# Default binary + model paths point at the main workdir (sibling worktree
|
||||
# where the sr-voice binary and gemma2.gguf live). Override via --sr-voice
|
||||
# / --model if your layout differs.
|
||||
# When --dump-prompts is set, name_feature short-circuits: it builds the
|
||||
# attempt-0 prompt for each feature, writes a JSONL line to this file,
|
||||
# and returns a unique deterministic placeholder so the pipeline runs to
|
||||
# completion without touching a real LLM. Lets us capture the exact prompt
|
||||
# set any backend (Gemma CPU, Gemma GPU, Haiku) would receive on first
|
||||
# attempt — so an offline backend can replay them and the resulting names
|
||||
# can be A/B compared.
|
||||
_CAPTURE_FILE = None # set in main() when --dump-prompts is used
|
||||
|
||||
# Default binary + model paths. The sr-voice binary is platform-specific
|
||||
# (GPU backend baked in per-build) and lives OUTSIDE any git worktree so
|
||||
# it survives sprint-worktree cleanup:
|
||||
#
|
||||
# ~/Projects/settled-reach/binaries/sr-voice-rocm (AMD / ROCm)
|
||||
# ~/Projects/settled-reach/binaries/sr-voice-cuda (NVIDIA, future)
|
||||
# ~/Projects/settled-reach/binaries/sr-voice-vulkan (cross-vendor, future)
|
||||
# ~/Projects/settled-reach/binaries/sr-voice-cpu (fallback)
|
||||
#
|
||||
# See #850 for the multi-backend release-binary ticket. The container
|
||||
# used to build these is created via the distrobox recipe documented in
|
||||
# the sr-voice README.
|
||||
#
|
||||
# The Gemma 2 model weights live under main/server/models and are shared
|
||||
# across worktrees (too large to duplicate).
|
||||
HOME_PROJECTS = Path.home() / "Projects" / "settled-reach"
|
||||
BINARIES_DIR = HOME_PROJECTS / "binaries"
|
||||
MAIN_WORKDIR = Path("/var/mnt/data/projects/settled-reach/main")
|
||||
DEFAULT_SR_VOICE = MAIN_WORKDIR / "server" / "sr-voice" / "target" / "release" / "sr-voice"
|
||||
|
||||
|
||||
def _find_sr_voice() -> Path:
|
||||
"""Resolve the default sr-voice binary path.
|
||||
|
||||
Preference order:
|
||||
1. $HOME/Projects/settled-reach/binaries/sr-voice-rocm — persistent
|
||||
across worktree lifetimes, the canonical dev location.
|
||||
2. main workdir's target/release/sr-voice — legacy, for
|
||||
backward-compatibility with older layouts.
|
||||
"""
|
||||
rocm_bin = BINARIES_DIR / "sr-voice-rocm"
|
||||
if rocm_bin.exists():
|
||||
return rocm_bin
|
||||
return MAIN_WORKDIR / "server" / "sr-voice" / "target" / "release" / "sr-voice"
|
||||
|
||||
|
||||
DEFAULT_SR_VOICE = _find_sr_voice()
|
||||
DEFAULT_MODEL = MAIN_WORKDIR / "server" / "models" / "gemma2.gguf"
|
||||
MOCK_STDIO = REPO_ROOT / "server" / "sr-voice" / "mock-stdio.sh"
|
||||
|
||||
@@ -1085,12 +1125,14 @@ class VoiceSubprocess:
|
||||
mock: bool,
|
||||
refresh_every: int,
|
||||
verbose: bool,
|
||||
distrobox: str | None = None,
|
||||
):
|
||||
self.sr_voice_bin = sr_voice_bin
|
||||
self.model_path = model_path
|
||||
self.mock = mock
|
||||
self.refresh_every = max(1, refresh_every)
|
||||
self.verbose = verbose
|
||||
self.distrobox = distrobox
|
||||
self.proc: subprocess.Popen | None = None
|
||||
self.request_count = 0
|
||||
self._start()
|
||||
@@ -1098,10 +1140,19 @@ class VoiceSubprocess:
|
||||
def _build_cmd(self) -> list[str]:
|
||||
if self.mock:
|
||||
return [str(MOCK_STDIO), "serve", "--stdio"]
|
||||
cmd = [str(self.sr_voice_bin), "serve", "--stdio"]
|
||||
inner_cmd = [str(self.sr_voice_bin), "serve", "--stdio"]
|
||||
if self.model_path is not None:
|
||||
cmd += ["--model", str(self.model_path)]
|
||||
return cmd
|
||||
inner_cmd += ["--model", str(self.model_path)]
|
||||
# If a distrobox container was requested, invoke the binary
|
||||
# inside the container. Needed when the built binary depends on
|
||||
# libs (e.g. libhipblas.so.2) that only exist in the container,
|
||||
# not on the host. `distrobox enter <name> -- <command>`
|
||||
# forwards stdin/stdout through the container's podman exec
|
||||
# pipe, which is exactly what VoiceSubprocess needs — the
|
||||
# stdio JSONL protocol flows through unchanged.
|
||||
if self.distrobox:
|
||||
return ["distrobox", "enter", self.distrobox, "--"] + inner_cmd
|
||||
return inner_cmd
|
||||
|
||||
def _start(self) -> None:
|
||||
cmd = self._build_cmd()
|
||||
@@ -1294,6 +1345,39 @@ def name_feature(
|
||||
dedup_key = (corridor, feature_type)
|
||||
used = corpus.setdefault(dedup_key, set())
|
||||
|
||||
# Capture mode: build the attempt-0 prompt, log it, return a unique
|
||||
# placeholder. No LLM call, no validation gauntlet — we want the raw
|
||||
# prompt set as Gemma would first see it, before retries kick in.
|
||||
if _CAPTURE_FILE is not None:
|
||||
prompt = _build_prompt(
|
||||
feature_type,
|
||||
inflection=palette["inflection"],
|
||||
planet_class=planet_class,
|
||||
body_id=body_id,
|
||||
local_id=local_id,
|
||||
attempt=0,
|
||||
system_hook=system_hook,
|
||||
system_name=ctx.get("system_proper_name"),
|
||||
body_name=ctx.get("body_proper_name"),
|
||||
)
|
||||
seed = _seed_for(world_seed, body_id, local_id, 0)
|
||||
_CAPTURE_FILE.write(json.dumps({
|
||||
"body_id": body_id,
|
||||
"local_id": local_id,
|
||||
"feature_type": feature_type,
|
||||
"corridor": corridor,
|
||||
"seed": seed,
|
||||
"prompt": prompt,
|
||||
}) + "\n")
|
||||
_CAPTURE_FILE.flush()
|
||||
# Unique deterministic placeholder. Embeds the seed so every
|
||||
# call returns a distinct string — passes dedup. Two letters +
|
||||
# hex keep it short, no _PLACEHOLDER_TOKENS, no length issues.
|
||||
placeholder = f"Zq{seed:08x}"
|
||||
used.add(placeholder)
|
||||
body_used.add(placeholder)
|
||||
return placeholder
|
||||
|
||||
def _is_duplicate(candidate: str) -> bool:
|
||||
lc = candidate.lower()
|
||||
return (
|
||||
@@ -1726,6 +1810,15 @@ def main():
|
||||
default=str(DEFAULT_MODEL),
|
||||
help="Path to the Gemma 2 GGUF model (ignored in --mock mode)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--distrobox",
|
||||
default=None,
|
||||
help="Name of a distrobox container to invoke sr-voice inside. "
|
||||
"Use this when the built binary depends on libraries (e.g. "
|
||||
"libhipblas.so.2) that are only installed in the container, "
|
||||
"not on the host. Example: --distrobox reach-build. "
|
||||
"Default: run sr-voice directly on the host.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--refresh",
|
||||
type=int,
|
||||
@@ -1760,8 +1853,25 @@ def main():
|
||||
help="Print per-feature retry detail (noisy) and subprocess "
|
||||
"lifecycle events",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dump-prompts",
|
||||
default=None,
|
||||
help="Capture mode: write the attempt-0 prompt for every feature "
|
||||
"to this JSONL path and return unique placeholders, without "
|
||||
"calling any LLM. Use to feed the same prompt set to a "
|
||||
"different backend (e.g. a Claude agent) for A/B comparison.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
global _CAPTURE_FILE
|
||||
if args.dump_prompts:
|
||||
Path(args.dump_prompts).parent.mkdir(parents=True, exist_ok=True)
|
||||
_CAPTURE_FILE = open(args.dump_prompts, "w")
|
||||
# Capture mode short-circuits inside name_feature and never
|
||||
# actually calls the voice subprocess. Force --mock so we boot
|
||||
# the cheap mock-stdio shell instead of loading a real model.
|
||||
args.mock = True
|
||||
|
||||
log_path = None if args.log == "-" else Path(args.log)
|
||||
|
||||
db_path = Path(args.db)
|
||||
@@ -1876,6 +1986,7 @@ def main():
|
||||
mock=args.mock,
|
||||
refresh_every=args.refresh,
|
||||
verbose=args.verbose,
|
||||
distrobox=args.distrobox,
|
||||
) as voice:
|
||||
for i, markers_path in enumerate(markers_paths):
|
||||
body_id, system_id = _body_id_from_path(markers_path)
|
||||
|
||||
Reference in New Issue
Block a user