Real-time AI pipeline that ingests a live audio stream, transcribes it incrementally, and emits structured LLM summaries (a short summary plus exactly three keywords) over rolling 300-second windows with minimal latency. The pipeline is built as composable Juturna nodes; audio is delivered over WebRTC through a Janus gateway.
audio (Opus RTP via Janus)
→ chunk (short overlapping windows)
→ incremental ASR (faster-whisper large-v3-turbo)
→ hallucination filter
→ novel-chunk extraction (dedup overlap)
→ 300 s window aggregation
→ summarize (vLLM, Qwen3.5-4B-AWQ, in-process)
→ store locally + POST
All inference is CUDA-native via vLLM, loaded in-process at warmup — no daemon, no external LLM service.
Each window produces one JSON object with exactly these keys:
{
"from": 0.0,
"to": 300.0,
"summary": "...",
"keywords": ["...", "...", "..."],
"proc_time": 1.03
}Sample outputs: submission/sample_outputs/.
- NVIDIA GPU, CUDA 12.4, ≥ 24 GB VRAM
- Docker + Docker Compose with the NVIDIA Container Toolkit
docker compose buildThe build fetches and bakes the model weights (Qwen3.5-4B-AWQ summarizer +
faster-whisper ASR) into the image. This is the only step that needs network
access. The runtime container is fully offline (HF_HUB_OFFLINE=1).
docker compose upThe pipeline listens for an Opus RTP stream on 0.0.0.0:8888 (forwarded by the
Janus container). Window results are written to ./results/ and, when a target
is configured, POSTed to it:
DESTINATION_ENDPOINT=https://your.endpoint/submit docker compose upAudio reaches the pipeline only through the Janus gateway — never directly into
the container. Janus runs the VideoRoom plugin: a WebRTC client joins room
1234 as a publisher and publishes an Opus audio track, then Janus
rtp_forwards that audio as plain RTP to the pipeline's audio_rtp node
(audio_pt 97 → udp/8888). Any WebRTC source — a browser, an upstream SFU,
or the bundled reference publisher — feeds the pipeline this way; nothing on the
pipeline side changes for a different source.
For a remote source, expose the gateway so the source can reach it:
8088/tcp— Janus HTTP signaling API (path/janus)10000-10050/udp— WebRTC media (ICE) ports
Then have the remote source join VideoRoom room 1234 as a publisher and issue
the rtp_forward. tools/send_audio.py is the reference client for that
handshake and runs from any host — point --janus-url at the gateway and leave
--pipeline-host as Janus's own (co-located) route to the pipeline container:
uv sync --extra client
# audio = any ffmpeg-readable input (local file path or a stream URL)
uv run python tools/send_audio.py <audio> \
--janus-url http://<gateway-host>:8088/janus \
--room 1234Window results land in ./results/ and POST to DESTINATION_ENDPOINT (when
set) exactly as for a co-located source.
The complete pipeline graph is a single file —
pipelines/config-submission.json: eight
nodes, a fixed 300-second summary window, the locked Qwen3.5-4B-AWQ summarizer,
and the result sink. The container launches it directly (no runtime assembly).
Real-time meeting intelligence from a live audio stream. Per-chunk score
C_i = B_i + K_i + L_i (B: LLM judge, 5 Likert criteria incl. conciseness,
max 25; K: keyword relevance, max 6; L: latency reward,
10·e^(−0.5·proc_time) gated by B_i ≥ 10). Audio-level score averages
chunk scores and adds a flat +4 Janus bonus.
Eight sequential Juturna nodes, wired in pipelines/config-submission.json:
audio_rtp— receives mono Opus/48k RTP on0.0.0.0:8888. Janus gateway forwards audio into this port. Config:channels: 1,encoding_clock_chan: "opus/48000/1".audio_chunker— splits the live stream into 5 s overlapping chunks (1 s overlap) so the ASR sees audio incrementally, not as one blob.transcriber_whisper— faster-whisperlarge-v3-turbo,device: auto(GPU when available),compute_type: float16. OpenAI's distilled large-v3 (~809M params) — near large-v3 WER at half the size. Loads ~2 GB on GPU alongside the summarizer; the 4B summarizer plus ASR plus KV cache fits comfortably in 24 GB. Linked against CTranslate2 with PyTorch-bundled cuBLAS via anLD_LIBRARY_PATHexport in the container entry point so the stack stays self-contained.language: "en"set explicitly (multilingual model, no.ensuffix).hallucination_filter— post-processes ASR output to drop known hallucinatory sentences before aggregation.novel_extractor— deduplicates the overlapping ASR output, keeps only the new content each chunk contributes.window_aggregator— concatenates novel transcript segments into a rolling 300-second context window. Emits one message per completed window.summarizer_vllm— native CUDA via vLLM in-process. Loads the model from./models/<name>/at warmup, generates{summary, keywords: [3]}JSON per window. No daemon, no middleware. Refuses to start if the model directory is missing.result_transmitter— maps internal keys to the challenge contract (from,to,summary,keywords,proc_time), writes the result locally, and POSTs todestination_endpointwhen configured.
- Janus WebRTC bonus. Audio enters through a Janus gateway in both
local dev and the evaluation environment — no environment-specific code
paths. This earns the flat
+4audio-level bonus at zero architectural cost. - ASR model — faster-whisper
large-v3-turbo. OpenAI's distilled large-v3 (~809M params, ~2 GB float16). Near-identical WER to large-v3 at half the compute. Runs on GPU (float16) co-resident with vLLM — the 4B summarizer + KV cache + ASR fit in 24 GB with margin. GPU placement removes the ~2 s/chunk CPU bottleneck the int8/CPU variant had, lifting WER (better transcripts → higher B_i) without costing latency. - Summarizer backend — vLLM in-process. Chosen over Ollama (server),
MLX (Apple-only), and raw
transformers(slower):- No daemon. Inference is a Python library call. The submission Docker has one process to launch; nothing to start, nothing to health-check, nothing to crash independently of the pipeline.
- Paged attention + batching. vLLM's continuous batching and
paged KV cache keep
proc_timelow even at larger model sizes, which is exactly the lever forL_i = 10·e^(−0.5·proc_time). - Single-GPU fit. A 8–9 B fp16 model fits inside the 32 GB
RTX Pro 4500 (Blackwell) — and even the tighter 24 GB dev RTX 4090 —
with headroom for KV cache; vLLM's
gpu_memory_utilizationknob controls the reservation explicitly. - SGLang considered, not adopted. A May-2026 engine survey found
SGLang (
sgl.Engine, RadixAttention) ~29% higher throughput / lower TTFT than vLLM as a near-drop-in replacement, and TensorRT-LLM faster still at the cost of per-model compilation. vLLM is kept for its widest model support and zero compilation; SGLang remains the primary latency lever to revisit ifL_ibecomes the binding constraint.
- Model weights ship with the submission.
tools/fetch_models.shpopulates./models/<name>/during development; the same script is invoked atdocker buildtime so the resulting image carries weights. The pipeline aborts at warmup if the directory is missing — failures are loud, never silent. - Single summarizer profile. One Juturna node, one locked summarizer baked into the image. No MLX/Ollama variants to maintain or drift between.
- Judge runs offline, sequentially. Quality is scored after the
pipeline exits and the summarizer is unloaded: the judge model is
loaded with vLLM, scores every window in
results/.../, and writes its verdicts alongside. This avoids VRAM co-residency (an 8 B summarizer + 7 B judge cannot share 24 GB at fp16) and reflects reality: the held-out judge is the only one that matters at submission time. - Prompt engineering for conciseness + specificity. Prompts demand
1–2 sentences, facts present in the transcript only, and specific
noun phrases for keywords. This doubles up: higher
B_iconciseness score and lowerproc_time(smallermax_tokenssuffices), which boostsL_i. - Keyword validation.
ensure_three_keywordsstrips banned generic terms (general,discussion,meeting, …) that would be scored as irrelevant, and backfills missing slots by extracting proper-noun / content words from the transcript. Turns the previous−6worst case on LLM failure into a+0floor: even a broken summarizer no longer tanksK_i. - Robust JSON extraction. The summarizer strips ChatML special
tokens (
<|im_end|>,<|endoftext|>) and extracts the outermost balanced JSON object from the raw output — tolerates trailing stop tokens or commentary the model may emit after the JSON. - Hallucination filter. Inline post-processing guards against
invented names, numbers, or facts — a known failure mode for small
LLMs on short transcripts. Protects
B_ifactual-consistency. - Quality gates latency. The scoring rule zeroes
L_iwhenB_i < 10. The pipeline is tuned so each window clears that floor before optimizing latency further. Bigger models / longer prompts are preferred to faster but lower-quality combinations. - Extractive fallback (never empty summary). The summarizer node has
two guarded paths into a deterministic extractive summary (first
sentence(s) of the transcript, capped at 300 chars) under
_summarizer_common/extractive.py: (a) transcripts shorter thanmin_transcript_chars=80skip the LLM entirely; (b) LLM failures (parse error, empty summary, exception) fall through to the same helper. Output is always non-empty, faithful (verbatim), and effectively zero proc_time — preserves the L bonus on edge windows. - End-of-stream flush watchdog. The window aggregator runs an
inactivity watchdog (default 30 s). When no chunks arrive for
flush_timeoutseconds, the partial window is flushed with its actualchunk_end(notwindow_start + duration). Captures the tail of finite audio that would otherwise be dropped.
Submission summarizer Qwen3.5-4B-AWQ (AWQ-int4), 300 s windows.
Scored offline by the cross-family 3-judge consensus panel
(mistral-small-24b-awq, phi-4-awq, gemma3-27b-it-int4-awq); each
value is the per-window mean over the three judges, averaged over windows
and over repeated runs per fixture. Janus +4 applied at the audio level.
| Fixture (300 s windows) | B (≤25) | K (≤6) | L (≤10) | C (avg) | Janus | Final |
|---|---|---|---|---|---|---|
| rev16 ep27 (What We Own) | 24.5 | 6.0 | 6.57 | 37.02 | +4 | 41.02 |
| rev16 ep10 (Own Lane) | 23.9 | 5.3 | 7.06 | 36.32 | +4 | 40.32 |
| ietf Media-Over-QUIC | 23.8 | 5.8 | 6.73 | 36.24 | +4 | 40.24 |
| ietf Computing-Aware-Tr | 23.6 | 5.9 | 6.79 | 36.18 | +4 | 40.18 |
| rev16 ep11 (Podcast Tips) | 23.2 | 5.1 | 6.79 | 35.07 | +4 | 39.07 |
| mean ± sd (5 fixtures) | 23.8 | 5.6 | 6.79 | 36.2 | +4 | 40.2 ± 0.6 |
L ≈ 6.8 corresponds to ~0.77 s proc_time per window; every window
clears the B_i ≥ 10 gate so the latency reward always applies.
Judge agreement is high (per-window C stdev across the panel ≈ 0.3–0.5),
so the consensus is corroborated rather than driven by a single model.
Dockerfiletargetsnvidia/cuda:12.4.1-runtime-ubuntu22.04, installs Python 3.12 + uv, runsuv sync --no-install-project(vLLM andhuggingface-hubare base dependencies; no extras enter the image). The build then invokes./tools/fetch_models.sh cyankiwi/Qwen3.5-4B-AWQ-BF16-INT4 qwen3.5-4b-awqto bake the summarizer weights, andhuggingface_hub.snapshot_download(...)to bakefaster-whisper-large-v3-turbo. The resulting image carries every model byte; zero network dependency at runtime.docker-compose.ymlruns the Janus service and the pipeline service. Janus forwards RTP to the pipeline container on UDP/8888.- Entry point
docker/entrypoint-pipeline.shlaunches the single pre-assembled graphpipelines/config-submission.jsondirectly (no runtime assembly — the submission is locked to one summarizer). The launch uses juturna's--autoflag so the container starts without interactive input. destination_endpointis left empty in the committedpipelines/config-submission.json— the challenge spec publishes no fixed receiving URL, and the graded artifact is the local progressive-summary JSON. Every window is always written to./results/<model>/<window_s>/window_N.json(exposed via the host volume mount). To enable POSTing, the evaluator sets theDESTINATION_ENDPOINTenvironment variable (DESTINATION_ENDPOINT=... docker compose up);result_transmitterreads it as a fallback when the config field is empty and POSTs every window (httpx, configurable timeout) in addition to the local write — no file edit required..dockerignorekeeps the image lean (~20 GB content vs ~150 GB with default context) by excludingmodels/(baked separately),datasets/,results/, and.venv/.