Skip to main content
Glama
README.md
# GPU Queue

A faster-whisper transcription service with a built-in job queue, so multiple
agents can share a single GPU without conflicts. The queue/worker/engine core is
transport-agnostic and is exposed through **two interchangeable servers** — pick
the one your clients speak:

| Server | Entry point | Default port | Clients |
|--------|-------------|--------------|---------|
| MCP    | `server.py`        | 8000 | MCP-aware agents (FastMCP) |
| Flask  | `flask_server.py`  | 8001 | Any HTTP/JSON client |

Both wrap the same `QueueManager`, so jobs are still processed one at a time and
the response payloads are identical (shared `app/service.py`).

## How it works

```
submit  ─►  ┌──────────────┐   one worker, one GPU   ┌───────────────┐
            │  job queue   │ ──────────────────────► │ WhisperEngine │
poll    ◄─  └──────────────┘   processed serially     └───────────────┘
```

A job is enqueued and returns a `job_id` immediately; you then poll its status
until it is `done` (or `error`). The single worker drains the queue one job at a
time, so a shared GPU never runs two transcriptions at once.

## Install

```bash
pip install -e .        # or: uv sync
```

This pulls in `fastmcp` (MCP transport), `flask` + `waitress` (HTTP transport),
and `faster-whisper` (the engine).

## Running the server

Choose **one** server (they're independent — neither imports the other):

```bash
# MCP transport (streamable-http on :8000)
python server.py
python server.py --port 9000
python server.py --transport stdio        # for stdio MCP clients

# Flask transport (HTTP/JSON on :8001)
python flask_server.py
python flask_server.py --port 9001
python flask_server.py --debug            # Flask dev server with auto-reload
```

> **Run a single process only** — the queue and job registry live in memory.
> Multiple threads are fine (waitress uses a thread pool); multiple worker
> *processes* are not, as they would each hold a separate, unshared queue.

The Flask server (production `waitress` path included) prints a startup banner
and logs one line per request to stdout, e.g. `GET /queue -> 200 (1.2ms)`.

### Engine configuration (both servers)

Read from environment variables:

- `WHISPER_MODEL` (default `distil-large-v3.5`)
- `WHISPER_DEVICE` (default `auto`)
- `WHISPER_COMPUTE_TYPE` (default `auto`)
- `WHISPER_MODEL_DIR`

## HTTP API (Flask transport)

| Method & path | Description |
|---------------|-------------|
| `GET  /health` | Healthcheck → `200` `{"status":"ok","loop_running":true,"queue":{...}}`, or `503` if the worker loop is dead |
| `GET  /ping` | Liveness check → `{"ok": true, "time": ...}` |
| `POST /transcriptions` | Submit a job: `{"audio_path": "...", "args": "--format text"}` → `202` with `job_id`, `position` |
| `GET  /transcriptions/<job_id>` | Poll status; `output` present when `status` is `done`; `404` if unknown |
| `POST /transcriptions/<job_id>/cancel` | Cancel a queued/running job |
| `GET  /queue` | Running job, queued jobs, completed count |

Example session:

```bash
curl localhost:8001/ping

# submit
curl -X POST localhost:8001/transcriptions \
  -H 'Content-Type: application/json' \
  -d '{"audio_path":"/abs/path/audio.mp3","args":"--format text"}'
# → {"job_id":"j_ab12cd34","position":1, ...}

# poll until "status":"done"
curl localhost:8001/transcriptions/j_ab12cd34

# inspect the queue / cancel
curl localhost:8001/queue
curl -X POST localhost:8001/transcriptions/j_ab12cd34/cancel
```

## MCP API (MCP transport)

Exposes the same operations as five FastMCP tools:

| Tool | Description |
|------|-------------|
| `ping()` | Liveness check |
| `submit_transcription(audio_path, args="")` | Enqueue a job, returns `job_id` + `position` |
| `get_transcription_status(job_id)` | Poll a job; `output` set when done |
| `cancel_job(job_id)` | Cancel a queued/running job |
| `queue_status()` | Running job, queued jobs, completed count |

Quick checks against a running MCP server (uses the `fastmcp` client):

```bash
python test_ping.py                       # ping the server
python test_live.py --audio /path/audio.mp3   # full submit → poll → print transcript
```

## `args` flags (both transports)

The `args` string accepts the same faster-whisper CLI flags on either transport,
e.g. `--format srt`, `--model large-v3`, `--language en`, `--diarize`,
`--word-timestamps`, `--beam-size 5`. See `_ARG_MAP` in
`app/queue_manager.py` for the full list.

📖 **Full HTTP reference:** see [`docs/HTTP_API.md`](docs/HTTP_API.md) for request/
response schemas, status codes, the job lifecycle, and `args` flags.

## Project layout

```
app/
  queue_manager.py   queue, worker, engine lifecycle, CLI-arg → kwargs bridge
  whisper/engine.py  faster-whisper wrapper (model stays loaded between jobs)
  models.py          JobRecord dataclass
  service.py         payload-shaping shared by both transports
  main.py / tools.py MCP transport (create_mcp_app + tool definitions)
  flask_app.py       Flask transport (create_flask_app + routes)
server.py            run the MCP server      (:8000)
flask_server.py      run the Flask server    (:8001)
```

## Docker

The bundled `Dockerfile` / `docker-compose.yml` run the **MCP** server
(`CMD ["python", "server.py"]`, port 8000) with an NVIDIA GPU reservation. To
serve the Flask transport in a container instead, override the command, e.g.:

```yaml
command: python flask_server.py --port 8000
```

## Tests

```bash
pytest -m "not integration"   # unit tests (engine mocked) — covers core, MCP, and Flask
pytest -m integration         # requires faster-whisper + a real model/audio file
```

- `tests/test_queue_manager.py` — queue/worker/cancellation logic
- `tests/test_service.py` — shared payload-shaping
- `tests/test_tools.py` — MCP transport end-to-end (mocked engine)
- `tests/test_flask_app.py` — Flask routes end-to-end via the test client