---
estimated_steps: 45
estimated_files: 3
skills_used: []
---

# T02: Implement voice dial prompt modifier and re-synthesis scoring flow

## Description

Build the voice dial module that modifies the stage 5 synthesis prompt based on a voice_level parameter (0.0–1.0), and wire it into the scorer so `--voice-level` triggers re-synthesis from source moments before scoring. This completes the slice by enabling the key demo: running the scorer at voice_level 0.2 vs 0.8 produces measurably different voice preservation scores.

## Failure Modes

| Dependency | On error | On timeout | On malformed response |
|------------|----------|-----------|----------------------|
| LLM endpoint (re-synthesis) | Same connectivity error pattern — print URL, exit 1 | Same | If synthesis returns unparseable JSON, log raw excerpt and exit 1 with message |
| LLM endpoint (scoring judge) | Same as T01 scorer | Same | Same as T01 — zero scores with detail |
| prompts/stage5_synthesis.txt | FileNotFoundError caught, print "Prompt file not found: {path}", exit 1 | N/A | N/A |

## Steps

1. Read the existing `prompts/stage5_synthesis.txt` to understand what voice language is already present (the research says it's roughly voice_level 0.6-0.7 baseline).
2. Read `backend/pipeline/stages.py` functions `_load_prompt()`, `_get_stage_config()`, and `_synthesize_chunk()` to understand the exact synthesis call pattern — system_prompt + `<creator>...</creator>\n<moments>...</moments>` user prompt format.
3. Create `backend/pipeline/quality/voice_dial.py` with:
   - `VoiceDial` class:
     - `__init__(self, base_prompt: str)` — stores the base stage 5 system prompt.
     - `modify(self, voice_level: float) -> str` — returns the modified system prompt.
       - 3 bands: low (0.0–0.33), mid (0.34–0.66), high (0.67–1.0).
       - Low band: append instruction to suppress direct quotes, write in neutral third-person encyclopedia style, avoid attributing opinions, minimize personality markers.
       - Mid band: return base prompt unmodified (the existing prompt already has moderate voice preservation).
       - High band: append instruction to maximize direct quotes from transcript, preserve every memorable phrase, prioritize creator's exact words over paraphrase, include personality and strong opinions.
     - Band boundaries at 0.33 and 0.67. Within each band, no continuous interpolation — just the band's modifier.
4. Add `ScoreRunner.synthesize_and_score(moments: list[dict], creator_name: str, voice_level: float) -> ScoreResult` method to `backend/pipeline/quality/scorer.py`:
   - Loads stage5_synthesis.txt via `_load_prompt('stage5_synthesis.txt')` (import from pipeline.stages).
   - Creates `VoiceDial(base_prompt)` and calls `modify(voice_level)` to get modified prompt.
   - Gets stage config via `_get_stage_config(5)` for model_override and modality.
   - Builds user prompt in the same format as `_synthesize_chunk`: `<creator>{name}</creator>\n<moments>\n{moments_json}\n</moments>`.
   - Calls `self.client.complete()` with the modified prompt, parses response as SynthesisResult using `self.client.parse_response(raw, SynthesisResult)`.
   - If synthesis returns valid pages, takes the first page and calls `self.score_page()` on it.
   - Returns the ScoreResult.
5. Update `backend/pipeline/quality/__main__.py` — in the `score` command handler:
   - If `--voice-level` is provided and `--file` is used: load moments from file, call `runner.synthesize_and_score(moments, creator_name, voice_level)` instead of `score_page()`.
   - If `--voice-level` is provided without `--file`: error — voice-level requires moments input (--file or --slug with DB).
6. Verify: `cd backend && python -c "from pipeline.quality.voice_dial import VoiceDial; print('import ok')"` exits 0.
7. Verify: `cd backend && python -c "from pipeline.quality.voice_dial import VoiceDial; vd = VoiceDial('base prompt'); low = vd.modify(0.1); mid = vd.modify(0.5); high = vd.modify(0.9); assert low != mid; assert high != mid; assert 'suppress' in low.lower() or 'neutral' in low.lower(); assert 'quote' in high.lower() or 'direct' in high.lower(); print('dial ok')"` — voice dial produces distinct prompts per band.
8. Verify: `cd backend && python -m pipeline.quality score --file pipeline/quality/fixtures/sample_moments.json --voice-level 0.3` — connectivity error, exit 1, no traceback.

## Must-Haves

- [ ] VoiceDial class with 3 bands (low/mid/high) producing distinct prompt modifications
- [ ] Low band suppresses voice, high band amplifies it, mid band passes through unmodified
- [ ] ScoreRunner.synthesize_and_score() re-synthesizes from moments using modified prompt, then scores
- [ ] --voice-level wired into CLI and triggers re-synthesis flow
- [ ] Stage 5 prompt loaded from prompts/stage5_synthesis.txt (not hardcoded)
- [ ] Synthesis output parsed as SynthesisResult (reuses existing schema)

## Verification

- `cd backend && python -c "from pipeline.quality.voice_dial import VoiceDial; print('import ok')"` — exits 0
- `cd backend && python -c "from pipeline.quality.voice_dial import VoiceDial; vd = VoiceDial('base'); assert vd.modify(0.1) != vd.modify(0.5) != vd.modify(0.9); print('bands ok')"` — three distinct outputs
- `cd backend && python -m pipeline.quality score --file pipeline/quality/fixtures/sample_moments.json --voice-level 0.3` — connectivity error, exit 1, no traceback

## Inputs

- `backend/pipeline/quality/scorer.py`
- `backend/pipeline/quality/__main__.py`
- `backend/pipeline/quality/fixtures/sample_moments.json`
- `backend/pipeline/llm_client.py`
- `backend/pipeline/stages.py`
- `backend/pipeline/schemas.py`
- `prompts/stage5_synthesis.txt`

## Expected Output

- `backend/pipeline/quality/voice_dial.py`
- `backend/pipeline/quality/scorer.py`
- `backend/pipeline/quality/__main__.py`

## Verification

cd backend && python -c "from pipeline.quality.voice_dial import VoiceDial; vd = VoiceDial('base'); assert vd.modify(0.1) != vd.modify(0.5); assert vd.modify(0.5) != vd.modify(0.9); print('bands ok')" && python -m pipeline.quality score --help
