# Whisper Transcription E2E Suite

End-to-end tests for running a Whisper transcription pipeline on Geneva
and embedding the transcript chunks with Qwen3 Embedding.

## Architecture

### Data Source

The suite sources audio from the Hugging Face LibriSpeech dataset
(`openslr/librispeech_asr`). `dataset.py` streams rows from the `clean`
configuration (`test` split) and loads them into a Geneva table with three
columns: `clip_id`, `source`, `num_clips`.

For Hugging Face audio rows that only provide local cache paths,
`dataset.py` embeds the audio bytes into a `data:` URL so Ray workers can
fetch them without requiring filesystem access.

### Pipeline

```mermaid
flowchart LR
    subgraph source["Source Table"]
        init["clip_id<br>source<br>num_clips"]
        j1(["1. download_audio"])
        audio["audio_bytes"]
    end
    j2(["2. chunk_audio_udtf"])
    subgraph mv["Chunk MV (1:N)"]
        inherited["clip_id<br>source"]
        chunks["chunk_id<br>start_sec · end_sec<br>samples"]
        j3(["3. Whisper"])
        text["text"]
        j4(["4. Qwen3 Embed"])
        embed["embedding"]
    end
    init --> j1 --> audio
    audio --> j2
    init -.-> j2
    j2 --> inherited
    j2 --> chunks
    chunks --> j3 --> text
    text --> j4 --> embed
```

1. **Backfill `audio_bytes`** — `download_audio` UDF fetches bytes from URL / data URI.
2. **Scalar UDTF view** — `chunk_audio_udtf` decodes, resamples to 16 kHz, and
   expands each source row into 30 s chunks (1:N via `create_scalar_udtf_view`).
   Source columns `clip_id`/`source` are inherited automatically.
3. **Backfill `text`** — `WhisperChunkTranscriber` transcribes each chunk.
4. **Backfill `embedding`** — `TranscriptEmbedder` (Qwen3, 1024-dim).
   A cosine index is created for vector search.

### UDFs

All implementations live in `src/geneva/udfs/audio/whisper_transcription.py`:

| Function | Type | Description |
|----------|------|-------------|
| `download_audio` | `@udf` | Fetch audio bytes from URL / data URI |
| `chunk_audio_udtf()` | `@scalar_udtf` factory | Decode + resample + chunk into 30s windows (1:N) |
| `WhisperChunkTranscriber` | `@udf` (batched) | Transcribe audio samples with Whisper |
| `TranscriptEmbedder` | `@udf` (batched) | Embed transcript text with Qwen3 |

`chunk_audio_udtf` accepts `max_audio_s` (default 15 min) to skip
excessively long audio.

## Structure

```
whisper_transcription/
├── README.md
├── conftest.py            # Fixtures, cluster definitions, manifest upload
├── dataset.py             # LibriSpeech dataset loader
├── local.py               # Local execution script
├── pipeline.py            # Three-stage pipeline orchestration
├── pyproject.toml         # Test-driver dependencies
├── upload_manifests.py    # Suite-level manifest upload orchestrator
├── test_drivers/          # Tests (no ML deps)
└── udfs/
    └── whisper_transcription/  # profile env + manifest factory
```

## Running Tests

From the workspace root:

```bash
uv sync --all-groups --all-extras --locked
make -f e2e/Makefile test-e2e-whisper-transcription-gcp SLUG=myrun ROW_LIMIT=5 NUM_CLIPS=2 CHECKPOINT_SIZE=2
```

Or directly from the suite directory:

```bash
cd e2e/whisper_transcription
RAY_ENABLE_UV_RUN_RUNTIME_ENV=0 uv run -m pytest -c ../pytest.ini test_drivers \
  --csp=gcp --row-limit=5 --num-clips=2 --checkpoint-size=2 -v
```

If you see timeout errors, prepend: `GENEVA_GCS_UPLOAD_TIMEOUT_S=900`

### Options

- `--csp` (`gcp`|`aws`): Cloud to target (default `gcp`)
- `--test-slug`: Slug for isolating buckets (default random)
- `--bucket-path`: Override the test bucket root
- `--row-limit`: Number of dataset rows to process (default 256)
- `--num-clips`: Max 30s chunks per audio file (default 5)
- `--checkpoint-size`: Backfill checkpoint size (default 10)
- `--skip-gpu`: Use CPU-only cluster

### Notes

- Set `RAY_ENABLE_UV_RUN_RUNTIME_ENV=0` when invoking pytest to avoid uv/Ray
  runtime conflicts.
- The suite uploads the manifest automatically on first run via the
  `audio_table` fixture.
