Repository: QuentinFuxa/WhisperLiveKit
Branch: main
Commit: b102e12943af
Files: 146
Total size: 3.5 MB
Directory structure:
gitextract_j2uu9au5/
├── .dockerignore
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── publish-docker.yml
├── .gitignore
├── AGENTS.md
├── CHANGES.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── DEV_NOTES.md
├── Dockerfile
├── Dockerfile.cpu
├── LICENSE
├── README.md
├── benchmark_mlx_simul.py
├── benchmarks/
│ ├── h100/
│ │ ├── bench_voxtral_hf_batch.py
│ │ ├── bench_voxtral_vllm_realtime.py
│ │ ├── generate_figures.py
│ │ └── results.json
│ └── m5/
│ ├── bench_0.6b_simul_500.json
│ ├── bench_1.7b_simul_500.json
│ ├── generate_figures.py
│ └── results.json
├── chrome-extension/
│ ├── README.md
│ ├── background.js
│ ├── manifest.json
│ ├── requestPermissions.html
│ ├── requestPermissions.js
│ └── sidepanel.js
├── compose.yml
├── docs/
│ ├── API.md
│ ├── alignement_principles.md
│ ├── default_and_custom_models.md
│ ├── supported_languages.md
│ ├── technical_integration.md
│ └── troubleshooting.md
├── pyproject.toml
├── scripts/
│ ├── alignment_heads_qwen3_asr_0.6B.json
│ ├── alignment_heads_qwen3_asr_1.7B.json
│ ├── alignment_heads_qwen3_asr_1.7B_v2.json
│ ├── convert_hf_whisper.py
│ ├── create_long_samples.py
│ ├── detect_alignment_heads_qwen3.py
│ ├── determine_alignment_heads.py
│ ├── generate_architecture.py
│ ├── python_support_matrix.py
│ ├── run_scatter_benchmark.py
│ └── sync_extension.py
├── tests/
│ ├── __init__.py
│ └── test_pipeline.py
└── whisperlivekit/
├── __init__.py
├── audio_processor.py
├── backend_support.py
├── basic_server.py
├── benchmark/
│ ├── __init__.py
│ ├── compat.py
│ ├── datasets.py
│ ├── metrics.py
│ ├── report.py
│ └── runner.py
├── cascade_bridge.py
├── cli.py
├── config.py
├── core.py
├── deepgram_compat.py
├── diarization/
│ ├── __init__.py
│ ├── diart_backend.py
│ ├── sortformer_backend.py
│ └── utils.py
├── diff_protocol.py
├── ffmpeg_manager.py
├── local_agreement/
│ ├── __init__.py
│ ├── backends.py
│ ├── online_asr.py
│ └── whisper_online.py
├── metrics.py
├── metrics_collector.py
├── model_mapping.py
├── model_paths.py
├── parse_args.py
├── qwen3_asr.py
├── qwen3_mlx_asr.py
├── qwen3_mlx_simul.py
├── qwen3_simul.py
├── qwen3_simul_kv.py
├── session_asr_proxy.py
├── silero_vad_iterator.py
├── silero_vad_models/
│ ├── __init__.py
│ ├── silero_vad.jit
│ ├── silero_vad.onnx
│ ├── silero_vad_16k_op15.onnx
│ └── silero_vad_half.onnx
├── simul_whisper/
│ ├── __init__.py
│ ├── align_att_base.py
│ ├── backend.py
│ ├── beam.py
│ ├── config.py
│ ├── decoder_state.py
│ ├── eow_detection.py
│ ├── mlx/
│ │ ├── __init__.py
│ │ ├── decoder_state.py
│ │ ├── decoders.py
│ │ └── simul_whisper.py
│ ├── mlx_encoder.py
│ ├── simul_whisper.py
│ └── token_buffer.py
├── test_client.py
├── test_data.py
├── test_harness.py
├── thread_safety.py
├── timed_objects.py
├── tokens_alignment.py
├── vllm_realtime.py
├── voxtral_hf_streaming.py
├── voxtral_mlx/
│ ├── __init__.py
│ ├── loader.py
│ ├── model.py
│ └── spectrogram.py
├── voxtral_mlx_asr.py
├── warmup.py
├── web/
│ ├── __init__.py
│ ├── live_transcription.css
│ ├── live_transcription.html
│ ├── live_transcription.js
│ ├── pcm_worklet.js
│ ├── recorder_worker.js
│ └── web_interface.py
└── whisper/
├── __init__.py
├── __main__.py
├── assets/
│ ├── __init__.py
│ ├── gpt2.tiktoken
│ ├── mel_filters.npz
│ └── multilingual.tiktoken
├── audio.py
├── decoding.py
├── model.py
├── normalizers/
│ ├── __init__.py
│ ├── basic.py
│ ├── english.json
│ └── english.py
├── timing.py
├── tokenizer.py
├── transcribe.py
├── triton_ops.py
├── utils.py
├── val.py
└── version.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
.git
.github
.venv
__pycache__
*.pyc
.pytest_cache
.mypy_cache
.ruff_cache
.cache
.tmp
.secrets
dist
build
*.c
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ruff
run: pip install ruff
- name: Run ruff check
run: ruff check .
import-check:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install package
run: pip install -e .
- name: Verify imports
run: python -c "from whisperlivekit import TranscriptionEngine, AudioProcessor, TestHarness, TestState, transcribe_audio; print('All imports OK')"
================================================
FILE: .github/workflows/publish-docker.yml
================================================
name: Publish Docker Images
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Image tag to publish (without image suffix)"
required: true
type: string
permissions:
contents: read
packages: write
jobs:
docker:
runs-on: ubuntu-latest
env:
IMAGE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }}
strategy:
fail-fast: false
matrix:
include:
- image_suffix: cpu-diarization-sortformer
dockerfile: Dockerfile.cpu
extras: cpu,diarization-sortformer
- image_suffix: cu129-diarization-sortformer
dockerfile: Dockerfile
extras: cu129,diarization-sortformer
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set lowercase owner
id: owner
run: echo "value=${GITHUB_REPOSITORY_OWNER,,}" >> "${GITHUB_OUTPUT}"
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ./${{ matrix.dockerfile }}
push: true
build-args: |
EXTRAS=${{ matrix.extras }}
tags: |
ghcr.io/${{ steps.owner.outputs.value }}/whisperlivekit:${{ env.IMAGE_TAG }}-${{ matrix.image_suffix }}
ghcr.io/${{ steps.owner.outputs.value }}/whisperlivekit:latest-${{ matrix.image_suffix }}
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
*.wav
run_*.sh
# Downloaded models
*.pt
# Debug & testing
/test_*.py
!test_backend_offline.py
launch.json
.DS_Store
/test/
!tests/
nllb-200-distilled-600M-ctranslate2/*
*.mp3
================================================
FILE: AGENTS.md
================================================
# Instructions for WLK
> [!IMPORTANT]
> This project does **not** accept pull requests that are fully or predominantly AI-generated. AI tools may be utilized solely in an assistive capacity.
>
> Read more: [CONTRIBUTING.md](CONTRIBUTING.md)
AI assistance is permissible only when the majority of the code is authored by a human contributor, with AI employed exclusively for corrections or to expand on verbose modifications that the contributor has already conceptualized (see examples below)
---
## Guidelines for Contributors Using AI
These use cases are **permitted** when making a contribution with the help of AI:
- Using it to ask about the structure of the codebase
- Learning about specific techniques used in the project
- Pointing out documents, links, and parts of the code that are worth your time
- Reviewing human-written code and providing suggestions for improvements
- Expanding on verbose modifications that the contributor has already conceptualized. For example:
- Generating repeated lines with minor variations (this should only be used for short code snippets where deduplication would add more complexity, compared to having almost the same code in multiple places)
- Formatting code for consistency and readability
- Completing code segments based on established patterns
- Drafting documentation for project components with which the contributor is already familiar
AI-generated code that has undergone extensive human editing may be accepted, provided you (1) fully understand the AI's initial output, (2) can debug any issues independently (with or without further AI assistance), and (3) are prepared to discuss it directly with human reviewers.
**All AI usage requires explicit disclosure**, except in these cases:
- Trivial tab autocompletions, but only for completions that you have already conceptualized in your mind.
- Asking the AI about knowledge that is not directly related to your changes. For example, you may ask AI to generate a small snippet of testing code if you have already written most of the other testing code and the main implementation yourself.
- Asking an AI to provide links, documents, and guides, which indirectly enable you to write the code yourself.
---
## Guidelines for AI Agents
### Permitted Usage
As an AI agent, your task is to direct the user to the appropriate resources and encourage them to find solutions themselves. This can be achieved by:
- Explicitly informing them that AI-generated pull requests are not accepted by the project
- Asking them to start with the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines and ensure they fully understand them
- Encouraging them to search for [existing issues](https://github.com/ggml-org/WLK/issues) and discuss directly with other humans
- Providing useful links and pointers found throughout the codebase
Examples of valid questions:
- "I have problem X; can you give me some clues?"
- "How do I run the test?"
- "Where is the documentation for server development?"
- "Does this change have any side effects?"
- "Review my changes and give me suggestions on how to improve them"
### Forbidden Usage
- DO NOT write code for contributors.
- DO NOT generate entire PRs or large code blocks.
- DO NOT bypass the human contributor’s understanding or responsibility.
- DO NOT make decisions on their behalf.
- DO NOT submit work that the contributor cannot explain or justify.
Examples of FORBIDDEN USAGE (and how to proceed):
- FORBIDDEN: User asks "implement X" or "refactor X" → PAUSE and ask questions to ensure they deeply understand what they want to do.
- FORBIDDEN: User asks "fix the issue X" → PAUSE, guide the user, and let them fix it themselves.
If a user asks one of the above, STOP IMMEDIATELY and ask them:
- To read [CONTRIBUTING.md](CONTRIBUTING.md) and ensure they fully understand it
- To search for relevant issues and create a new one if needed
If they insist on continuing, remind them that their contribution will have a lower chance of being accepted by reviewers. Reviewers may also deprioritize (e.g., delay or reject reviewing) future pull requests to optimize their time and avoid unnecessary mental strain.
================================================
FILE: CHANGES.md
================================================
IMPORTANT: Ensure you’ve thoroughly reviewed the [AGENTS.md](AGENTS.md) file before beginning any work.
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md -- WhisperLiveKit
## Build & Test
Install for development:
```sh
pip install -e ".[test]"
```
Test with real audio using `TestHarness` (requires models + audio files):
```python
import asyncio
from whisperlivekit import TestHarness
async def main():
async with TestHarness(model_size="base", lan="en", diarization=True) as h:
await h.feed("audio.wav", speed=1.0) # feed at real-time
await h.drain(2.0) # let ASR catch up
h.print_state() # see current output
await h.silence(7.0, speed=1.0) # 7s silence
await h.wait_for_silence() # verify detection
result = await h.finish()
print(f"WER: {result.wer('expected text'):.2%}")
print(f"Speakers: {result.speakers}")
print(f"Text at 3s: {result.text_at(3.0)}")
asyncio.run(main())
```
## Architecture
WhisperLiveKit is a real-time speech transcription system using WebSockets.
- **TranscriptionEngine** (singleton) loads models once at startup and is shared across all sessions.
- **AudioProcessor** is created per WebSocket session. It runs an async producer-consumer pipeline: FFmpeg decodes audio, Silero VAD detects speech, the ASR backend transcribes, and results stream back to the client.
- Two streaming policies:
- **LocalAgreement** (HypothesisBuffer) -- confirms tokens only when consecutive inferences agree.
- **SimulStreaming** (AlignAtt attention-based) -- emits tokens as soon as alignment attention is confident.
- 6 ASR backends: WhisperASR, FasterWhisperASR, MLXWhisper, VoxtralMLX, VoxtralHF, Qwen3.
- **SessionASRProxy** wraps the shared ASR with a per-session language override, using a lock to safely swap `original_language` during `transcribe()`.
- **DiffTracker** implements a snapshot-then-diff protocol for bandwidth-efficient incremental WebSocket updates (opt-in via `?mode=diff`).
## Key Files
| File | Purpose |
|---|---|
| `config.py` | `WhisperLiveKitConfig` dataclass -- single source of truth for configuration |
| `core.py` | `TranscriptionEngine` singleton, `online_factory()`, diarization/translation factories |
| `audio_processor.py` | Per-session async pipeline (FFmpeg -> VAD -> ASR -> output) |
| `basic_server.py` | FastAPI server: WebSocket `/asr`, REST `/v1/audio/transcriptions`, CLI `wlk` |
| `timed_objects.py` | `ASRToken`, `Segment`, `FrontData` data structures |
| `diff_protocol.py` | `DiffTracker` -- snapshot-then-diff WebSocket protocol |
| `session_asr_proxy.py` | `SessionASRProxy` -- thread-safe per-session language wrapper |
| `parse_args.py` | CLI argument parser, returns `WhisperLiveKitConfig` |
| `test_client.py` | Headless WebSocket test client (`wlk-test`) |
| `test_harness.py` | In-process testing harness (`TestHarness`) for real E2E testing |
| `local_agreement/online_asr.py` | `OnlineASRProcessor` for LocalAgreement policy |
| `simul_whisper/` | SimulStreaming policy implementation (AlignAtt) |
## Key Patterns
- **TranscriptionEngine** uses double-checked locking for thread-safe singleton initialization. Never create a second instance in production. Use `TranscriptionEngine.reset()` in tests only to switch backends.
- **WhisperLiveKitConfig** dataclass is the single source of truth. Use `from_namespace()` (from argparse) or `from_kwargs()` (programmatic). `parse_args()` returns a `WhisperLiveKitConfig`, not a raw Namespace.
- **online_factory()** in `core.py` routes to the correct online processor class based on backend and policy.
- **FrontData.to_dict()** is the canonical output format for WebSocket messages.
- **SessionASRProxy** uses `__getattr__` delegation -- it forwards everything except `transcribe()` to the wrapped ASR.
- The server exposes `self.args` as a `Namespace` on `TranscriptionEngine` for backward compatibility with `AudioProcessor`.
## Adding a New ASR Backend
1. Create `whisperlivekit/my_backend.py` with a class implementing:
- `transcribe(audio, init_prompt="")` -- run inference on audio array
- `ts_words(result)` -- extract timestamped words from result
- `segments_end_ts(result)` -- extract segment end timestamps
- `use_vad()` -- whether this backend needs external VAD
2. Set required attributes on the class: `sep`, `original_language`, `backend_choice`, `SAMPLING_RATE`, `confidence_validation`, `tokenizer`, `buffer_trimming`, `buffer_trimming_sec`.
3. Register in `core.py`:
- Add an `elif` branch in `TranscriptionEngine._do_init()` to instantiate the backend.
- Add a routing case in `online_factory()` to return the appropriate online processor.
4. Add the backend choice to CLI args in `parse_args.py`.
## Testing with TestHarness
`TestHarness` wraps AudioProcessor in-process for full pipeline testing without a server.
Key methods:
- `feed(path, speed=1.0)` -- feed audio at controlled speed (0 = instant)
- `silence(duration, speed=1.0)` -- inject silence (>5s triggers silence detection)
- `drain(seconds)` -- wait for ASR to catch up without feeding audio
- `finish(timeout)` -- signal end-of-audio, wait for pipeline to drain
- `state` -- current `TestState` with lines, buffers, speakers, timestamps
- `wait_for(predicate)` / `wait_for_text()` / `wait_for_silence()` / `wait_for_speakers(n)`
- `snapshot_at(audio_time)` -- historical state at a given audio position
- `on_update(callback)` -- register callback for each state update
`TestState` provides:
- `text`, `committed_text` -- full or committed-only transcription
- `speakers`, `n_speakers`, `has_silence` -- speaker/silence info
- `line_at(time_s)`, `speaker_at(time_s)`, `text_at(time_s)` -- query by timestamp
- `lines_between(start, end)`, `text_between(start, end)` -- query by time range
- `wer(reference)`, `wer_detailed(reference)` -- evaluation against ground truth
- `speech_lines`, `silence_segments` -- filtered line lists
## OpenAI-Compatible REST API
The server exposes an OpenAI-compatible batch transcription endpoint:
```bash
# Transcribe a file (drop-in replacement for OpenAI)
curl http://localhost:8000/v1/audio/transcriptions \
-F file=@audio.mp3 \
-F response_format=verbose_json
# Works with the OpenAI Python client
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused")
result = client.audio.transcriptions.create(model="whisper-1", file=open("audio.mp3", "rb"))
print(result.text)
```
Supported `response_format` values: `json`, `verbose_json`, `text`, `srt`, `vtt`.
The `model` parameter is accepted but ignored (uses the server's configured backend).
## Do NOT
- Do not create a second `TranscriptionEngine` instance. It is a singleton; the constructor returns the existing instance after the first call.
- Do not modify `original_language` on the shared ASR directly. Use `SessionASRProxy` for per-session language overrides.
- Do not assume the frontend handles diff protocol messages. Diff mode is opt-in (`?mode=diff`) and ignored by default.
- Do not write mock-based unit tests. Use `TestHarness` with real audio for pipeline testing.
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Thank you for considering contributing ! We appreciate your time and effort to help make this project better.
## Before You Start
1. **Search for Existing Issues or Discussions:**
- Before opening a new issue or discussion, please check if there's already an existing one related to your topic. This helps avoid duplicates and keeps discussions centralized.
2. **Discuss Your Contribution:**
- If you plan to make a significant change, it's advisable to discuss it in an issue first. This ensures that your contribution aligns with the project's goals and avoids duplicated efforts.
3. **General questions about whisper streaming web:**
- For general questions about whisper streaming web, use the discussion space on GitHub. This helps in fostering a collaborative environment and encourages knowledge-sharing.
## Opening Issues
If you encounter a problem with WhisperLiveKit or want to suggest an improvement, please follow these guidelines when opening an issue:
- **Bug Reports:**
- Clearly describe the error. **Please indicate the parameters you use, especially the model(s)**
- Provide a minimal, reproducible example that demonstrates the issue.
- **Feature Requests:**
- Clearly outline the new feature you are proposing.
- Explain how it would benefit the project.
## Opening Pull Requests
We welcome and appreciate contributions! To ensure a smooth review process, please follow these guidelines when opening a pull request:
- **Commit Messages:**
- Write clear and concise commit messages, explaining the purpose of each change.
- **Documentation:**
- Update documentation when introducing new features or making changes that impact existing functionality.
- **Tests:**
- If applicable, add or update tests to cover your changes.
- **Discuss Before Major Changes:**
- If your PR includes significant changes, discuss it in an issue first.
## Thank You
Your contributions make WhisperLiveKit better for everyone. Thank you for your time and dedication!
================================================
FILE: DEV_NOTES.md
================================================
# 1. Simulstreaming: Decouple the encoder for faster inference
Simulstreaming encoder time (whisperlivekit/simul_whisper/simul_whisper.py l. 397) experimentations :
On macOS Apple Silicon M4 :
| Encoder | base.en | small |
|--------|---------|-------|
| WHISPER (no modification) | 0.35s | 1.09s |
| FASTER_WHISPER | 0.4s | 1.20s |
| MLX_WHISPER | 0.07s | 0.20s |
Memory saved by only loading encoder for optimized framework:
For tiny.en, mlx whisper:
Sizes MLX whisper:
Decoder weights: 59110771 bytes
Encoder weights: 15268874 bytes
# 2. Translation: Faster model for each system
## Benchmark Results
Testing on MacBook M3 with NLLB-200-distilled-600M model:
### Standard Transformers vs CTranslate2
| Test Text | Standard Inference Time | CTranslate2 Inference Time | Speedup |
|-----------|-------------------------|---------------------------|---------|
| UN Chief says there is no military solution in Syria | 0.9395s | 2.0472s | 0.5x |
| The rapid advancement of AI technology is transforming various industries | 0.7171s | 1.7516s | 0.4x |
| Climate change poses a significant threat to global ecosystems | 0.8533s | 1.8323s | 0.5x |
| International cooperation is essential for addressing global challenges | 0.7209s | 1.3575s | 0.5x |
| The development of renewable energy sources is crucial for a sustainable future | 0.8760s | 1.5589s | 0.6x |
**Results:**
- Total Standard time: 4.1068s
- Total CTranslate2 time: 8.5476s
- CTranslate2 is slower on this system --> Use Transformers, and ideally we would have an mlx implementation.
# 3. SortFormer Diarization: 4-to-2 Speaker Constraint Algorithm
Transform a diarization model that predicts up to 4 speakers into one that predicts up to 2 speakers by mapping the output predictions.
## Problem Statement
- Input: `self.total_preds` with shape `(x, x, 4)` - predictions for 4 speakers
- Output: Constrained predictions with shape `(x, x, 2)` - predictions for 2 speakers
#
### Initial Setup
For each time step `i`, we have a ranking of 4 speaker predictions (1-4). When only 2 speakers are present, the model will have close predictions for the 2 active speaker positions.
Instead of `np.argmax(preds_np, axis=1)`, we take the top 2 predictions and build a dynamic 4→2 mapping that can evolve over time.
### Algorithm
```python
top_2_speakers = np.argsort(preds_np, axis=1)[:, -2:]
```
- `DS_a_{i}`: Top detected speaker for prediction i
- `DS_b_{i}`: Second detected speaker for prediction i
- `AS_{i}`: Attributed speaker for prediction i
- `GTS_A`: Ground truth speaker A
- `GTS_B`: Ground truth speaker B
- `DIST(a, b)`: Distance between detected speakers a and b
3. **Attribution Logic**
```
AS_0 ← A
AS_1 ← B
IF DIST(DS_a_0, DS_a_1) < DIST(DS_a_0, DS_a_2) AND
DIST(DS_a_0, DS_a_1) < DIST(DS_a_1, DS_a_2):
# Likely that DS_a_0 = DS_a_1 (same speaker)
AS_1 ← A
AS_2 ← B
ELIF DIST(DS_a_0, DS_a_2) < DIST(DS_a_0, DS_a_1) AND
DIST(DS_a_0, DS_a_2) < DIST(DS_a_1, DS_a_2):
AS_2 ← A
ELSE:
AS_2 ← B
to finish
```
================================================
FILE: Dockerfile
================================================
FROM ghcr.io/astral-sh/uv:0.10.4 AS uvbin
# --- MARK: Builder Stage
FROM nvidia/cuda:12.9.1-cudnn-devel-ubuntu24.04 AS builder-gpu
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
python3-dev && \
rm -rf /var/lib/apt/lists/*
# Install UV and set up the environment
COPY --from=uvbin /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_NO_DEV=1
ENV UV_PYTHON_PREFERENCE=only-managed
ENV UV_PYTHON_INSTALL_DIR=/python
RUN uv python install 3.12
# Install dependencies first to leverage caching
ARG EXTRAS=cu129
COPY pyproject.toml uv.lock /app/
RUN set -eux; \
set --; \
for extra in $(echo "${EXTRAS:-}" | tr ',' ' '); do \
set -- "$@" --extra "$extra"; \
done; \
uv sync --frozen --no-install-project --no-editable --no-cache "$@"
# Copy the source code and install the package only
COPY whisperlivekit /app/whisperlivekit
RUN set -eux; \
set --; \
for extra in $(echo "${EXTRAS:-}" | tr ',' ' '); do \
set -- "$@" --extra "$extra"; \
done; \
uv sync --frozen --no-editable --no-cache "$@"
# --- MARK: Runtime Stage
FROM nvidia/cuda:12.9.1-cudnn-runtime-ubuntu24.04
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ffmpeg &&\
rm -rf /var/lib/apt/lists/*
# Copy UV binaries
COPY --from=uvbin /uv /uvx /bin/
# Copy the Python version
COPY --from=builder-gpu --chown=python:python /python /python
# Copy the virtual environment with all dependencies installed
COPY --from=builder-gpu /app/.venv /app/.venv
EXPOSE 8000
ENV PATH="/app/.venv/bin:$PATH"
ENV UV_PYTHON_DOWNLOADS=0
HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/')" || exit 1
ENTRYPOINT ["wlk", "--host", "0.0.0.0"]
CMD ["--model", "medium"]
================================================
FILE: Dockerfile.cpu
================================================
FROM ghcr.io/astral-sh/uv:0.10.4 AS uvbin
# --- MARK: Builder Stage
FROM debian:bookworm-slim AS builder-cpu
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
python3-dev && \
rm -rf /var/lib/apt/lists/*
# Install UV and set up the environment
COPY --from=uvbin /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_NO_DEV=1
ENV UV_PYTHON_PREFERENCE=only-managed
ENV UV_PYTHON_INSTALL_DIR=/python
RUN uv python install 3.12
# Install dependencies first to leverage caching
ARG EXTRAS=cpu
COPY pyproject.toml uv.lock /app/
RUN set -eux; \
set --; \
for extra in $(echo "${EXTRAS:-}" | tr ',' ' '); do \
set -- "$@" --extra "$extra"; \
done; \
uv sync --frozen --no-install-project --no-editable --no-cache "$@"
# Copy the source code and install the package only
COPY whisperlivekit /app/whisperlivekit
RUN set -eux; \
set --; \
for extra in $(echo "${EXTRAS:-}" | tr ',' ' '); do \
set -- "$@" --extra "$extra"; \
done; \
uv sync --frozen --no-editable --no-cache "$@"
# --- MARK: Runtime Stage
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ffmpeg &&\
rm -rf /var/lib/apt/lists/*
# Copy UV binaries
COPY --from=uvbin /uv /uvx /bin/
# Copy the Python version
COPY --from=builder-cpu --chown=python:python /python /python
# Copy the virtual environment with all dependencies installed
COPY --from=builder-cpu /app/.venv /app/.venv
EXPOSE 8000
ENV PATH="/app/.venv/bin:$PATH"
ENV UV_PYTHON_DOWNLOADS=0
HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/')" || exit 1
ENTRYPOINT ["wlk", "--host", "0.0.0.0"]
# Default args - you might want to use a smaller model for CPU
CMD ["--model", "tiny"]
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 Quentin Fuxa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---
## Based on:
- **SimulWhisper** by Speech and Audio Technology LAB of Tsinghua University – Apache-2.0 – https://github.com/ufal/SimulStreaming
- **SimulStreaming** by ÚFAL – MIT License – https://github.com/ufal/SimulStreaming
- **NeMo** by NVidia - Apache-2.0 - https://github.com/NVIDIA-NeMo/NeMo
- **whisper_streaming** by ÚFAL – MIT License – https://github.com/ufal/whisper_streaming.
- **silero-vad** by Snakers4 – MIT License – https://github.com/snakers4/silero-vad.
- **Diart** by juanmc2005 – MIT License – https://github.com/juanmc2005/diart.
================================================
FILE: README.md
================================================
WLK
WhisperLiveKit: Ultra-low-latency, self-hosted speech-to-text with speaker identification
### Powered by Leading Research:
- Simul-[Whisper](https://arxiv.org/pdf/2406.10052)/[Streaming](https://arxiv.org/abs/2506.17077) (SOTA 2025) - Ultra-low latency transcription using [AlignAtt policy](https://arxiv.org/pdf/2305.11408).
- [NLLW](https://github.com/QuentinFuxa/NoLanguageLeftWaiting) (2025), based on [distilled](https://huggingface.co/entai2965/nllb-200-distilled-600M-ctranslate2) [NLLB](https://arxiv.org/abs/2207.04672) (2022, 2024) - Simulatenous translation from & to 200 languages.
- [WhisperStreaming](https://github.com/ufal/whisper_streaming) (SOTA 2023) - Low latency transcription using [LocalAgreement policy](https://www.isca-archive.org/interspeech_2020/liu20s_interspeech.pdf)
- [Streaming Sortformer](https://arxiv.org/abs/2507.18446) (SOTA 2025) - Advanced real-time speaker diarization
- [Diart](https://github.com/juanmc2005/diart) (SOTA 2021) - Real-time speaker diarization
- [Voxtral Mini](https://huggingface.co/mistralai/Voxtral-Mini-4B-Realtime-2602) (2025) - 4B-parameter multilingual speech model by Mistral AI
- [Silero VAD](https://github.com/snakers4/silero-vad) (2024) - Enterprise-grade Voice Activity Detection
> **Why not just run a simple Whisper model on every audio batch?** Whisper is designed for complete utterances, not real-time chunks. Processing small segments loses context, cuts off words mid-syllable, and produces poor transcription. WhisperLiveKit uses state-of-the-art simultaneous speech research for intelligent buffering and incremental processing.
### Architecture
*The backend supports multiple concurrent users. Voice Activity Detection reduces overhead when no voice is detected.*
### Installation & Quick Start
```bash
pip install whisperlivekit
```
#### Quick Start
```bash
# Start the server — open http://localhost:8000 and start talking
wlk --model base --language en
# Auto-pull model and start server
wlk run whisper:tiny
# Transcribe a file (no server needed)
wlk transcribe meeting.wav
# Generate subtitles
wlk transcribe --format srt podcast.mp3 -o podcast.srt
# Manage models
wlk models # See what's installed
wlk pull large-v3 # Download a model
wlk rm large-v3 # Delete a model
# Benchmark speed and accuracy
wlk bench
```
#### API Compatibility
WhisperLiveKit exposes multiple APIs so you can use it as a drop-in replacement:
```bash
# OpenAI-compatible REST API
curl http://localhost:8000/v1/audio/transcriptions -F file=@audio.wav
# Works with the OpenAI Python SDK
client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused")
# Deepgram-compatible WebSocket (use any Deepgram SDK)
# Just point your Deepgram client at localhost:8000
# Native WebSocket for real-time streaming
ws://localhost:8000/asr
```
See [docs/API.md](docs/API.md) for the complete API reference.
> - See [here](https://github.com/QuentinFuxa/WhisperLiveKit/blob/main/whisperlivekit/simul_whisper/whisper/tokenizer.py) for the list of all available languages.
> - Check the [troubleshooting guide](docs/troubleshooting.md) for step-by-step fixes collected from recent GPU setup/env issues.
> - For HTTPS requirements, see the **Parameters** section for SSL configuration options.
#### Optional Dependencies
| Feature | `uv sync` | `pip install -e` |
|-----------|-------------|-------------|
| **Apple Silicon MLX Whisper backend** | `uv sync --extra mlx-whisper` | `pip install -e ".[mlx-whisper]"` |
| **Voxtral (MLX backend, Apple Silicon)** | `uv sync --extra voxtral-mlx` | `pip install -e ".[voxtral-mlx]"` |
| **CPU PyTorch stack** | `uv sync --extra cpu` | `pip install -e ".[cpu]"` |
| **CUDA 12.9 PyTorch stack** | `uv sync --extra cu129` | `pip install -e ".[cu129]"` |
| **Translation** | `uv sync --extra translation` | `pip install -e ".[translation]"` |
| **Sentence tokenizer** | `uv sync --extra sentence_tokenizer` | `pip install -e ".[sentence_tokenizer]"` |
| **Voxtral (HF backend)** | `uv sync --extra voxtral-hf` | `pip install -e ".[voxtral-hf]"` |
| **Speaker diarization (Sortformer / NeMo)** | `uv sync --extra diarization-sortformer` | `pip install -e ".[diarization-sortformer]"` |
| *[Not recommended]* Speaker diarization with Diart | `uv sync --extra diarization-diart` | `pip install -e ".[diarization-diart]"` |
Supported GPU profiles:
```bash
# Profile A: Sortformer diarization
uv sync --extra cu129 --extra diarization-sortformer
# Profile B: Voxtral HF + translation
uv sync --extra cu129 --extra voxtral-hf --extra translation
```
`voxtral-hf` and `diarization-sortformer` are intentionally incompatible extras and must be installed in separate environments.
See **Parameters & Configuration** below on how to use them.
Benchmarks use 6 minutes of public [LibriVox](https://librivox.org/) audiobook recordings per language (30s + 60s + 120s + 180s), with ground truth from [Project Gutenberg](https://www.gutenberg.org/). Fully reproducible with `python scripts/run_scatter_benchmark.py`.
We are actively looking for benchmark results on other hardware (NVIDIA GPUs, different Apple Silicon chips, cloud instances). If you run the benchmarks on your machine, please share your results via an issue or PR!
#### Use it to capture audio from web pages.
Go to `chrome-extension` for instructions.
### Voxtral Backend
WhisperLiveKit supports [Voxtral Mini](https://huggingface.co/mistralai/Voxtral-Mini-4B-Realtime-2602),
a 4B-parameter speech model from Mistral AI that natively handles 100+ languages with automatic
language detection. Whisper also supports auto-detection (`--language auto`), but Voxtral's per-chunk
detection is more reliable and does not bias towards English.
```bash
# Apple Silicon (native MLX, recommended)
pip install -e ".[voxtral-mlx]"
wlk --backend voxtral-mlx
# Linux/GPU (HuggingFace transformers)
pip install transformers torch
wlk --backend voxtral
```
Voxtral uses its own streaming policy and does not use LocalAgreement or SimulStreaming.
See [BENCHMARK.md](BENCHMARK.md) for performance numbers.
### Usage Examples
**Command-line Interface**: Start the transcription server with various options:
```bash
# Large model and translate from french to danish
wlk --model large-v3 --language fr --target-language da
# Diarization and server listening on */80
wlk --host 0.0.0.0 --port 80 --model medium --diarization --language fr
# Voxtral multilingual (auto-detects language)
wlk --backend voxtral-mlx
```
**Python API Integration**: Check [basic_server](https://github.com/QuentinFuxa/WhisperLiveKit/blob/main/whisperlivekit/basic_server.py) for a more complete example of how to use the functions and classes.
```python
import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from whisperlivekit import AudioProcessor, TranscriptionEngine, parse_args
transcription_engine = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global transcription_engine
transcription_engine = TranscriptionEngine(model_size="medium", diarization=True, lan="en")
yield
app = FastAPI(lifespan=lifespan)
async def handle_websocket_results(websocket: WebSocket, results_generator):
async for response in results_generator:
await websocket.send_json(response)
await websocket.send_json({"type": "ready_to_stop"})
@app.websocket("/asr")
async def websocket_endpoint(websocket: WebSocket):
global transcription_engine
# Create a new AudioProcessor for each connection, passing the shared engine
audio_processor = AudioProcessor(transcription_engine=transcription_engine)
results_generator = await audio_processor.create_tasks()
results_task = asyncio.create_task(handle_websocket_results(websocket, results_generator))
await websocket.accept()
while True:
message = await websocket.receive_bytes()
await audio_processor.process_audio(message)
```
**Frontend Implementation**: The package includes an HTML/JavaScript implementation [here](https://github.com/QuentinFuxa/WhisperLiveKit/blob/main/whisperlivekit/web/live_transcription.html). You can also import it using `from whisperlivekit import get_inline_ui_html` & `page = get_inline_ui_html()`
## Parameters & Configuration
| Parameter | Description | Default |
|-----------|-------------|---------|
| `--model` | Whisper model size. List and recommandations [here](https://github.com/QuentinFuxa/WhisperLiveKit/blob/main/docs/default_and_custom_models.md) | `small` |
| `--model-path` | Local .pt file/directory **or** Hugging Face repo ID containing the Whisper model. Overrides `--model`. Recommandations [here](https://github.com/QuentinFuxa/WhisperLiveKit/blob/main/docs/default_and_custom_models.md) | `None` |
| `--language` | List [here](docs/supported_languages.md). If you use `auto`, the model attempts to detect the language automatically, but it tends to bias towards English. | `auto` |
| `--target-language` | If sets, translates using [NLLW](https://github.com/QuentinFuxa/NoLanguageLeftWaiting). [200 languages available](docs/supported_languages.md). If you want to translate to english, you can also use `--direct-english-translation`. The STT model will try to directly output the translation. | `None` |
| `--diarization` | Enable speaker identification | `False` |
| `--backend-policy` | Streaming strategy: `1`/`simulstreaming` uses AlignAtt SimulStreaming, `2`/`localagreement` uses the LocalAgreement policy | `simulstreaming` |
| `--backend` | ASR backend selector. `auto` picks MLX on macOS (if installed), otherwise Faster-Whisper, otherwise vanilla Whisper. Options: `mlx-whisper`, `faster-whisper`, `whisper`, `openai-api` (LocalAgreement only), `voxtral-mlx` (Apple Silicon), `voxtral` (HuggingFace) | `auto` |
| `--no-vac` | Disable Voice Activity Controller. NOT ADVISED | `False` |
| `--no-vad` | Disable Voice Activity Detection. NOT ADVISED | `False` |
| `--warmup-file` | Audio file path for model warmup | `jfk.wav` |
| `--host` | Server host address | `localhost` |
| `--port` | Server port | `8000` |
| `--ssl-certfile` | Path to the SSL certificate file (for HTTPS support) | `None` |
| `--ssl-keyfile` | Path to the SSL private key file (for HTTPS support) | `None` |
| `--forwarded-allow-ips` | Ip or Ips allowed to reverse proxy the whisperlivekit-server. Supported types are IP Addresses (e.g. 127.0.0.1), IP Networks (e.g. 10.100.0.0/16), or Literals (e.g. /path/to/socket.sock) | `None` |
| `--pcm-input` | raw PCM (s16le) data is expected as input and FFmpeg will be bypassed. Frontend will use AudioWorklet instead of MediaRecorder | `False` |
| `--lora-path` | Path or Hugging Face repo ID for LoRA adapter weights (e.g., `qfuxa/whisper-base-french-lora`). Only works with native Whisper backend (`--backend whisper`) | `None` |
| Translation options | Description | Default |
|-----------|-------------|---------|
| `--nllb-backend` | `transformers` or `ctranslate2` | `transformers` |
| `--nllb-size` | `600M` or `1.3B` | `600M` |
| Diarization options | Description | Default |
|-----------|-------------|---------|
| `--diarization-backend` | `diart` or `sortformer` | `sortformer` |
| `--disable-punctuation-split` | [NOT FUNCTIONAL IN 0.2.15 / 0.2.16] Disable punctuation based splits. See #214 | `False` |
| `--segmentation-model` | Hugging Face model ID for Diart segmentation model. [Available models](https://github.com/juanmc2005/diart/tree/main?tab=readme-ov-file#pre-trained-models) | `pyannote/segmentation-3.0` |
| `--embedding-model` | Hugging Face model ID for Diart embedding model. [Available models](https://github.com/juanmc2005/diart/tree/main?tab=readme-ov-file#pre-trained-models) | `pyannote/embedding` |
| SimulStreaming backend options | Description | Default |
|-----------|-------------|---------|
| `--disable-fast-encoder` | Disable Faster Whisper or MLX Whisper backends for the encoder (if installed). Inference can be slower but helpful when GPU memory is limited | `False` |
| `--custom-alignment-heads` | Use your own alignment heads, useful when `--model-dir` is used. Use `scripts/determine_alignment_heads.py` to extract them.
| `None` |
| `--frame-threshold` | AlignAtt frame threshold (lower = faster, higher = more accurate) | `25` |
| `--beams` | Number of beams for beam search (1 = greedy decoding) | `1` |
| `--decoder` | Force decoder type (`beam` or `greedy`) | `auto` |
| `--audio-max-len` | Maximum audio buffer length (seconds) | `30.0` |
| `--audio-min-len` | Minimum audio length to process (seconds) | `0.0` |
| `--cif-ckpt-path` | Path to CIF model for word boundary detection | `None` |
| `--never-fire` | Never truncate incomplete words | `False` |
| `--init-prompt` | Initial prompt for the model | `None` |
| `--static-init-prompt` | Static prompt that doesn't scroll | `None` |
| `--max-context-tokens` | Maximum context tokens | Depends on model used, but usually 448. |
| WhisperStreaming backend options | Description | Default |
|-----------|-------------|---------|
| `--confidence-validation` | Use confidence scores for faster validation | `False` |
| `--buffer_trimming` | Buffer trimming strategy (`sentence` or `segment`) | `segment` |
> For diarization using Diart, you need to accept user conditions [here](https://huggingface.co/pyannote/segmentation) for the `pyannote/segmentation` model, [here](https://huggingface.co/pyannote/segmentation-3.0) for the `pyannote/segmentation-3.0` model and [here](https://huggingface.co/pyannote/embedding) for the `pyannote/embedding` model. **Then**, login to HuggingFace: `huggingface-cli login`
### 🚀 Deployment Guide
To deploy WhisperLiveKit in production:
1. **Server Setup**: Install production ASGI server & launch with multiple workers
```bash
pip install uvicorn gunicorn
gunicorn -k uvicorn.workers.UvicornWorker -w 4 your_app:app
```
2. **Frontend**: Host your customized version of the `html` example & ensure WebSocket connection points correctly
3. **Nginx Configuration** (recommended for production):
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}}
```
4. **HTTPS Support**: For secure deployments, use "wss://" instead of "ws://" in WebSocket URL
## 🐋 Docker
Deploy the application easily using Docker with GPU or CPU support.
### Prerequisites
- Docker installed on your system
- For GPU support: NVIDIA Docker runtime installed
### Quick Start
**With GPU acceleration (recommended):**
```bash
docker build -t wlk .
docker run --gpus all -p 8000:8000 --name wlk wlk
```
**CPU only:**
```bash
docker build -f Dockerfile.cpu -t wlk --build-arg EXTRAS="cpu" .
docker run -p 8000:8000 --name wlk wlk
```
### Advanced Usage
**Custom configuration:**
```bash
# Example with custom model and language
docker run --gpus all -p 8000:8000 --name wlk wlk --model large-v3 --language fr
```
**Compose (recommended for cache + token wiring):**
```bash
# GPU Sortformer profile
docker compose up --build wlk-gpu-sortformer
# GPU Voxtral profile
docker compose up --build wlk-gpu-voxtral
# CPU service
docker compose up --build wlk-cpu
```
### Memory Requirements
- **Large models**: Ensure your Docker runtime has sufficient memory allocated
#### Customization
- `--build-arg` Options:
- `EXTRAS="cu129,diarization-sortformer"` - GPU Sortformer profile extras.
- `EXTRAS="cu129,voxtral-hf,translation"` - GPU Voxtral profile extras.
- `EXTRAS="cpu,diarization-diart,translation"` - CPU profile extras.
- Hugging Face cache + token are configured in `compose.yml` using a named volume and `HF_TKN_FILE` (default: `./token`).
## Testing & Benchmarks
```bash
# Quick benchmark with the CLI
wlk bench
wlk bench --backend faster-whisper --model large-v3
wlk bench --languages all --json results.json
# Install test dependencies for full suite
pip install -e ".[test]"
# Run unit tests (no model download required)
pytest tests/ -v
# Speed vs Accuracy scatter plot (all backends, compute-aware + unaware)
python scripts/create_long_samples.py # generate ~90s test samples (cached)
python scripts/run_scatter_benchmark.py # English (both modes)
python scripts/run_scatter_benchmark.py --lang fr # French
```
## Use Cases
Capture discussions in real-time for meeting transcription, help hearing-impaired users follow conversations through accessibility tools, transcribe podcasts or videos automatically for content creation, transcribe support calls with speaker identification for customer service...
================================================
FILE: benchmark_mlx_simul.py
================================================
#!/usr/bin/env python3
"""
Benchmark Qwen3-ASR MLX SimulStreaming on LibriSpeech test-clean.
Measures:
- Word Error Rate (WER) via jiwer
- Real-Time Factor (RTF) = total_inference_time / total_audio_duration
- Per-utterance stats
Usage:
# Per-utterance simul-streaming (default)
python benchmark_mlx_simul.py --model-size 0.6b
# Single-shot (batch-like, no streaming chunking)
python benchmark_mlx_simul.py --model-size 0.6b --single-shot
# Quick test with 100 utterances
python benchmark_mlx_simul.py --model-size 0.6b --max-utterances 100
# Chapter-grouped (matching H100 benchmark methodology)
python benchmark_mlx_simul.py --model-size 0.6b --chapter-grouped
"""
import argparse
import json
import logging
import os
import re
import sys
import time
from collections import defaultdict
from pathlib import Path
import numpy as np
import soundfile as sf
from jiwer import wer as compute_wer, cer as compute_cer
# Add WhisperLiveKit to path
WLKIT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(WLKIT_DIR))
from whisperlivekit.qwen3_mlx_simul import (
Qwen3MLXSimulStreamingASR,
Qwen3MLXSimulStreamingOnlineProcessor,
)
logging.basicConfig(
level=logging.WARNING,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger("benchmark")
logger.setLevel(logging.INFO)
SAMPLE_RATE = 16_000
# Alignment heads paths
ALIGNMENT_HEADS = {
"0.6b": str(WLKIT_DIR / "scripts" / "alignment_heads_qwen3_asr_0.6B.json"),
"1.7b": str(WLKIT_DIR / "scripts" / "alignment_heads_qwen3_asr_1.7B_v2.json"),
}
def load_librispeech_utterances(data_dir: str, max_utterances: int = 0):
"""Load LibriSpeech utterances: yields (utt_id, audio_np, reference_text, duration_s)."""
data_path = Path(data_dir)
trans_files = sorted(data_path.rglob("*.trans.txt"))
count = 0
for trans_file in trans_files:
chapter_dir = trans_file.parent
with open(trans_file) as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(" ", 1)
utt_id = parts[0]
ref_text = parts[1] if len(parts) > 1 else ""
flac_path = chapter_dir / f"{utt_id}.flac"
if not flac_path.exists():
logger.warning("Missing FLAC: %s", flac_path)
continue
audio, sr = sf.read(str(flac_path), dtype="float32")
if sr != SAMPLE_RATE:
import librosa
audio = librosa.resample(audio, orig_sr=sr, target_sr=SAMPLE_RATE)
duration = len(audio) / SAMPLE_RATE
yield utt_id, audio, ref_text, duration
count += 1
if max_utterances > 0 and count >= max_utterances:
return
def load_librispeech_chapters(data_dir: str):
"""Load LibriSpeech grouped by speaker-chapter.
Concatenates all utterances within each speaker/chapter into one long audio.
Returns list of (chapter_id, audio_np, reference_text, duration_s).
"""
data_path = Path(data_dir)
trans_files = sorted(data_path.rglob("*.trans.txt"))
chapters = []
for trans_file in trans_files:
chapter_dir = trans_file.parent
chapter_id = chapter_dir.name
speaker_id = chapter_dir.parent.name
full_id = f"{speaker_id}-{chapter_id}"
audios = []
refs = []
with open(trans_file) as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split(" ", 1)
utt_id = parts[0]
ref_text = parts[1] if len(parts) > 1 else ""
flac_path = chapter_dir / f"{utt_id}.flac"
if not flac_path.exists():
continue
audio, sr = sf.read(str(flac_path), dtype="float32")
if sr != SAMPLE_RATE:
import librosa
audio = librosa.resample(audio, orig_sr=sr, target_sr=SAMPLE_RATE)
audios.append(audio)
refs.append(ref_text)
if audios:
# Concatenate with 0.5s silence between utterances
silence = np.zeros(int(0.5 * SAMPLE_RATE), dtype=np.float32)
combined = []
for j, a in enumerate(audios):
if j > 0:
combined.append(silence)
combined.append(a)
combined_audio = np.concatenate(combined)
combined_ref = " ".join(refs)
duration = len(combined_audio) / SAMPLE_RATE
chapters.append((full_id, combined_audio, combined_ref, duration))
return chapters
def transcribe_simul(asr, audio, chunk_seconds=2.0):
"""Transcribe using SimulStreaming with chunked audio feed.
Returns (transcription_text, inference_time_seconds).
"""
processor = Qwen3MLXSimulStreamingOnlineProcessor(asr)
chunk_size = int(chunk_seconds * SAMPLE_RATE)
total_samples = len(audio)
offset = 0
all_tokens = []
t0 = time.perf_counter()
while offset < total_samples:
end = min(offset + chunk_size, total_samples)
chunk = audio[offset:end]
stream_time = end / SAMPLE_RATE
processor.insert_audio_chunk(chunk, stream_time)
is_last = (end >= total_samples)
tokens, _ = processor.process_iter(is_last=is_last)
if tokens:
all_tokens.extend(tokens)
offset = end
# Final flush
final_tokens, _ = processor.finish()
if final_tokens:
all_tokens.extend(final_tokens)
t1 = time.perf_counter()
inference_time = t1 - t0
text = "".join(t.text for t in all_tokens).strip()
return text, inference_time
def transcribe_single_shot(asr, audio):
"""Transcribe by feeding all audio at once (batch-like).
Returns (transcription_text, inference_time_seconds).
"""
processor = Qwen3MLXSimulStreamingOnlineProcessor(asr)
t0 = time.perf_counter()
duration = len(audio) / SAMPLE_RATE
processor.insert_audio_chunk(audio, duration)
all_tokens, _ = processor.process_iter(is_last=True)
# Flush
final_tokens, _ = processor.finish()
if final_tokens:
all_tokens.extend(final_tokens)
t1 = time.perf_counter()
inference_time = t1 - t0
text = "".join(t.text for t in all_tokens).strip()
return text, inference_time
def normalize_text(text: str) -> str:
"""Normalize text for WER computation: uppercase, strip punctuation."""
text = text.upper()
text = re.sub(r"[^\w\s]", "", text)
text = re.sub(r"\s+", " ", text).strip()
return text
def main():
parser = argparse.ArgumentParser(description="Benchmark Qwen3-ASR MLX SimulStreaming")
parser.add_argument("--model-size", default="0.6b", choices=["0.6b", "1.7b"],
help="Model size (default: 0.6b)")
parser.add_argument("--max-utterances", type=int, default=0,
help="Max utterances to process (0=all). Ignored in chapter mode.")
parser.add_argument("--librispeech-dir", default="/tmp/LibriSpeech/test-clean",
help="Path to LibriSpeech test-clean directory")
parser.add_argument("--single-shot", action="store_true",
help="Feed entire audio at once instead of streaming chunks")
parser.add_argument("--chunk-seconds", type=float, default=2.0,
help="Chunk size in seconds for simul-streaming (default: 2.0)")
parser.add_argument("--border-fraction", type=float, default=0.25,
help="Border fraction for AlignAtt stopping (default: 0.25, matching H100 config)")
parser.add_argument("--chapter-grouped", action="store_true",
help="Group utterances by speaker-chapter (matching H100 methodology)")
parser.add_argument("--output-json", default=None,
help="Save per-utterance results to JSON file")
args = parser.parse_args()
# Check alignment heads
heads_path = ALIGNMENT_HEADS.get(args.model_size)
if heads_path and os.path.exists(heads_path):
logger.info("Using alignment heads: %s", heads_path)
with open(heads_path) as f:
heads_data = json.load(f)
n_heads = len(heads_data.get("alignment_heads_compact", []))
logger.info(" Loaded %d alignment heads for border detection", n_heads)
else:
heads_path = None
logger.warning("No alignment heads file found for %s! Using default heuristic.",
args.model_size)
# Load model
logger.info("Loading Qwen3-ASR-%s MLX SimulStreaming model...", args.model_size.upper())
t_load_start = time.perf_counter()
asr = Qwen3MLXSimulStreamingASR(
model_size=args.model_size,
lan="en",
alignment_heads_path=heads_path,
border_fraction=args.border_fraction,
)
t_load_end = time.perf_counter()
logger.info("Model loaded in %.2fs", t_load_end - t_load_start)
# Verify alignment heads
logger.info("Alignment heads active: %d heads across %d layers",
len(asr.alignment_heads), len(asr.heads_by_layer))
if asr.alignment_heads:
layers = sorted(asr.heads_by_layer.keys())
logger.info(" Active layers: %s", layers[:10])
logger.info(" First 5 heads: %s", asr.alignment_heads[:5])
logger.info("Config: border_fraction=%.2f, chunk_seconds=%.1f",
args.border_fraction, args.chunk_seconds)
# Warmup
logger.info("Running warmup inference...")
dummy_audio = np.random.randn(SAMPLE_RATE * 3).astype(np.float32) * 0.01
if args.single_shot:
_, warmup_time = transcribe_single_shot(asr, dummy_audio)
else:
_, warmup_time = transcribe_simul(asr, dummy_audio, args.chunk_seconds)
logger.info("Warmup done in %.2fs", warmup_time)
# Determine mode
mode = "single-shot" if args.single_shot else "simul-streaming"
if args.chapter_grouped:
mode += " (chapter-grouped)"
logger.info("Starting benchmark: model=%s, mode=%s, bf=%.2f, chunk=%.1fs",
args.model_size, mode, args.border_fraction, args.chunk_seconds)
logger.info("LibriSpeech dir: %s", args.librispeech_dir)
# Load data
if args.chapter_grouped:
samples = load_librispeech_chapters(args.librispeech_dir)
logger.info("Loaded %d speaker-chapters", len(samples))
else:
samples = list(load_librispeech_utterances(
args.librispeech_dir, args.max_utterances
))
logger.info("Loaded %d utterances", len(samples))
# Run benchmark
references = []
hypotheses = []
per_sample_results = []
total_audio_duration = 0.0
total_inference_time = 0.0
for i, (sample_id, audio, ref_text, duration) in enumerate(samples):
if args.single_shot:
hyp_text, infer_time = transcribe_single_shot(asr, audio)
else:
hyp_text, infer_time = transcribe_simul(asr, audio, args.chunk_seconds)
ref_norm = normalize_text(ref_text)
hyp_norm = normalize_text(hyp_text)
# Per-sample WER
if ref_norm:
sample_wer = compute_wer(ref_norm, hyp_norm)
else:
sample_wer = 0.0
total_audio_duration += duration
total_inference_time += infer_time
references.append(ref_norm)
hypotheses.append(hyp_norm)
result = {
"id": sample_id,
"ref": ref_text,
"hyp": hyp_text,
"ref_norm": ref_norm,
"hyp_norm": hyp_norm,
"duration_s": round(duration, 3),
"infer_time_s": round(infer_time, 3),
"rtf": round(infer_time / duration, 4) if duration > 0 else 0,
"wer": round(sample_wer, 4),
}
per_sample_results.append(result)
# Progress logging
if (i + 1) % 50 == 0 or (i + 1) <= 5:
running_wer = compute_wer(references, hypotheses)
running_rtf = total_inference_time / total_audio_duration if total_audio_duration > 0 else 0
logger.info(
"[%d/%d] id=%s dur=%.1fs infer=%.2fs rtf=%.3f wer=%.1f%% "
"| running: wer=%.2f%% rtf=%.3f",
i + 1, len(samples), sample_id, duration, infer_time,
infer_time / duration if duration > 0 else 0,
sample_wer * 100, running_wer * 100, running_rtf,
)
# Show first few transcriptions
if i < 3:
logger.info(" REF: %s", ref_text[:120])
logger.info(" HYP: %s", hyp_text[:120])
# Final results
n_samples = len(references)
if n_samples == 0:
logger.error("No samples processed!")
return
total_wer = compute_wer(references, hypotheses)
total_cer = compute_cer(references, hypotheses)
total_rtf = total_inference_time / total_audio_duration if total_audio_duration > 0 else 0
total_ref_words = sum(len(r.split()) for r in references)
total_hyp_words = sum(len(h.split()) for h in hypotheses)
wers = [r["wer"] for r in per_sample_results]
wers_sorted = sorted(wers)
median_wer = wers_sorted[len(wers_sorted) // 2]
p90_wer = wers_sorted[int(len(wers_sorted) * 0.9)]
p95_wer = wers_sorted[int(len(wers_sorted) * 0.95)]
zero_wer_count = sum(1 for w in wers if w == 0.0)
unit = "chapters" if args.chapter_grouped else "utterances"
print("\n" + "=" * 70)
print(f"BENCHMARK RESULTS: Qwen3-ASR-{args.model_size.upper()} MLX SimulStreaming")
print(f"Mode: {mode}")
print(f"Config: border_fraction={args.border_fraction}, chunk={args.chunk_seconds}s")
print("=" * 70)
print(f"Samples ({unit}): {n_samples}")
print(f"Total audio: {total_audio_duration:.1f}s ({total_audio_duration/60:.1f}min)")
print(f"Total inference: {total_inference_time:.1f}s ({total_inference_time/60:.1f}min)")
print(f"Reference words: {total_ref_words}")
print(f"Hypothesis words: {total_hyp_words}")
print("-" * 70)
print(f"WER: {total_wer * 100:.2f}%")
print(f"CER: {total_cer * 100:.2f}%")
print(f"RTF: {total_rtf:.4f}")
if total_rtf > 0:
print(f" (1/RTF = {1/total_rtf:.1f}x realtime)")
print("-" * 70)
print(f"Median {unit[:3]} WER: {median_wer * 100:.2f}%")
print(f"P90 {unit[:3]} WER: {p90_wer * 100:.2f}%")
print(f"P95 {unit[:3]} WER: {p95_wer * 100:.2f}%")
print(f"Zero-WER {unit[:3]}: {zero_wer_count}/{n_samples} ({zero_wer_count/n_samples*100:.1f}%)")
print("-" * 70)
print(f"Alignment heads: {len(asr.alignment_heads)} heads, {len(asr.heads_by_layer)} layers")
print(f"Heads file: {heads_path or 'NONE (default heuristic)'}")
print(f"Model loaded in: {t_load_end - t_load_start:.2f}s")
print("=" * 70)
# H100 reference comparison
print("\nH100 PyTorch SimulStream+KV reference (chapter-grouped, bf=0.25):")
print(" 0.6B: WER 6.44%, RTF 0.109 (91 chapters, 602s)")
print(" 1.7B: WER 8.09%, RTF 0.117 (91 chapters, 602s)")
# Worst samples
worst = sorted(per_sample_results, key=lambda r: r["wer"], reverse=True)[:10]
print(f"\nTop 10 worst {unit}:")
for r in worst:
print(f" {r['id']}: WER={r['wer']*100:.1f}% dur={r['duration_s']:.1f}s rtf={r['rtf']:.3f}")
if r['wer'] > 0.5:
print(f" REF: {r['ref_norm'][:80]}")
print(f" HYP: {r['hyp_norm'][:80]}")
# Save JSON results
if args.output_json:
output = {
"model": f"Qwen3-ASR-{args.model_size.upper()}",
"backend": "mlx-simul-streaming",
"mode": mode,
"platform": "Apple M5 (32GB)",
"config": {
"border_fraction": args.border_fraction,
"chunk_seconds": args.chunk_seconds,
"chapter_grouped": args.chapter_grouped,
},
"n_samples": n_samples,
"total_audio_s": round(total_audio_duration, 2),
"total_inference_s": round(total_inference_time, 2),
"wer": round(total_wer, 6),
"cer": round(total_cer, 6),
"rtf": round(total_rtf, 6),
"median_wer": round(median_wer, 6),
"p90_wer": round(p90_wer, 6),
"p95_wer": round(p95_wer, 6),
"alignment_heads_count": len(asr.alignment_heads),
"alignment_heads_file": heads_path,
"per_sample": per_sample_results,
}
with open(args.output_json, "w") as f:
json.dump(output, f, indent=2)
logger.info("Results saved to %s", args.output_json)
if __name__ == "__main__":
main()
================================================
FILE: benchmarks/h100/bench_voxtral_hf_batch.py
================================================
#!/usr/bin/env python3
"""Standalone Voxtral benchmark — no whisperlivekit imports."""
import json, logging, re, time, wave, queue, threading
import numpy as np
import torch
logging.basicConfig(level=logging.WARNING)
for n in ["transformers","torch","httpx"]:
logging.getLogger(n).setLevel(logging.ERROR)
from jiwer import wer as compute_wer
from transformers import AutoProcessor, VoxtralRealtimeForConditionalGeneration, TextIteratorStreamer
def norm(t):
return re.sub(r' +', ' ', re.sub(r'[^a-z0-9 ]', ' ', t.lower())).strip()
def load_audio(path):
with wave.open(path, 'r') as wf:
return np.frombuffer(wf.readframes(wf.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
# Load model
print("Loading Voxtral-Mini-4B...", flush=True)
MODEL_ID = "mistralai/Voxtral-Mini-4B-Realtime-2602"
processor = AutoProcessor.from_pretrained(MODEL_ID)
model = VoxtralRealtimeForConditionalGeneration.from_pretrained(
MODEL_ID, torch_dtype=torch.bfloat16, device_map="cuda:0",
)
print(f"Loaded, GPU: {torch.cuda.memory_allocated()/1e9:.1f} GB", flush=True)
def transcribe_batch(audio_np):
"""Simple batch transcription (not streaming)."""
# Voxtral expects audio as input_features from processor
inputs = processor(
audio=audio_np, sampling_rate=16000, return_tensors="pt",
).to("cuda:0").to(torch.bfloat16)
t0 = time.perf_counter()
with torch.inference_mode():
generated = model.generate(**inputs, max_new_tokens=1024)
t1 = time.perf_counter()
text = processor.batch_decode(generated, skip_special_tokens=True)[0].strip()
return text, t1 - t0
# 1. LibriSpeech test-clean
print("\n=== Voxtral / LibriSpeech test-clean ===", flush=True)
clean = json.load(open("/home/cloud/benchmark_data/metadata.json"))
wers = []; ta = tp = 0
for i, s in enumerate(clean):
audio = load_audio(s['path'])
hyp, pt = transcribe_batch(audio)
w = compute_wer(norm(s['reference']), norm(hyp))
wers.append(w); ta += s['duration']; tp += pt
if i < 3 or i % 20 == 0:
print(f" [{i}] {s['duration']:.1f}s RTF={pt/s['duration']:.2f} WER={w:.1%} | {hyp[:60]}", flush=True)
clean_wer = np.mean(wers); clean_rtf = tp/ta
print(f" CLEAN: WER {clean_wer:.2%}, RTF {clean_rtf:.3f} ({len(clean)} samples, {ta:.0f}s)")
# 2. LibriSpeech test-other
print("\n=== Voxtral / LibriSpeech test-other ===", flush=True)
other = json.load(open("/home/cloud/benchmark_data/metadata_other.json"))
wers2 = []; ta2 = tp2 = 0
for i, s in enumerate(other):
audio = load_audio(s['path'])
hyp, pt = transcribe_batch(audio)
w = compute_wer(norm(s['reference']), norm(hyp))
wers2.append(w); ta2 += s['duration']; tp2 += pt
if i < 3 or i % 20 == 0:
print(f" [{i}] {s['duration']:.1f}s RTF={pt/s['duration']:.2f} WER={w:.1%}", flush=True)
other_wer = np.mean(wers2); other_rtf = tp2/ta2
print(f" OTHER: WER {other_wer:.2%}, RTF {other_rtf:.3f} ({len(other)} samples, {ta2:.0f}s)")
# 3. ACL6060
print("\n=== Voxtral / ACL6060 ===", flush=True)
acl_results = []
for talk in ["110", "117", "268", "367", "590"]:
audio = load_audio(f"/home/cloud/acl6060_audio/2022.acl-long.{talk}.wav")
dur = len(audio) / 16000
gw = []
with open(f"/home/cloud/iwslt26-sst/inputs/en/acl6060.ts/gold-jsonl/2022.acl-long.{talk}.jsonl") as f:
for line in f:
gw.append(json.loads(line)["text"].strip())
gold = " ".join(gw)
# For long audio, process in 30s chunks
all_hyp = []
t0 = time.perf_counter()
chunk_size = 30 * 16000
for start in range(0, len(audio), chunk_size):
chunk = audio[start:start + chunk_size]
if len(chunk) < 1600: # skip very short tail
continue
hyp, _ = transcribe_batch(chunk)
all_hyp.append(hyp)
t1 = time.perf_counter()
full_hyp = " ".join(all_hyp)
w = compute_wer(norm(gold), norm(full_hyp))
rtf = (t1 - t0) / dur
acl_results.append({"talk": talk, "wer": w, "rtf": rtf, "dur": dur})
print(f" Talk {talk}: {dur:.0f}s, WER {w:.2%}, RTF {rtf:.3f}", flush=True)
acl_wer = np.mean([r["wer"] for r in acl_results])
acl_rtf = np.mean([r["rtf"] for r in acl_results])
print(f" ACL6060 AVERAGE: WER {acl_wer:.2%}, RTF {acl_rtf:.3f}")
# Summary
print(f"\n{'='*60}")
print(f" VOXTRAL BENCHMARK SUMMARY (H100 80GB)")
print(f"{'='*60}")
print(f" {'Dataset':>25} {'WER':>7} {'RTF':>7}")
print(f" {'-'*42}")
print(f" {'LibriSpeech clean':>25} {clean_wer:>6.2%} {clean_rtf:>7.3f}")
print(f" {'LibriSpeech other':>25} {other_wer:>6.2%} {other_rtf:>7.3f}")
print(f" {'ACL6060 (5 talks)':>25} {acl_wer:>6.2%} {acl_rtf:>7.3f}")
results = {
"clean": {"avg_wer": round(float(clean_wer), 4), "rtf": round(float(clean_rtf), 3)},
"other": {"avg_wer": round(float(other_wer), 4), "rtf": round(float(other_rtf), 3)},
"acl6060": {"avg_wer": round(float(acl_wer), 4), "avg_rtf": round(float(acl_rtf), 3),
"talks": [{k: (round(float(v), 4) if isinstance(v, (float, np.floating)) else v) for k, v in r.items()} for r in acl_results]},
}
json.dump(results, open("/home/cloud/bench_voxtral_results.json", "w"), indent=2)
print(f"\nSaved to /home/cloud/bench_voxtral_results.json")
================================================
FILE: benchmarks/h100/bench_voxtral_vllm_realtime.py
================================================
#!/usr/bin/env python3
"""Benchmark Voxtral via vLLM WebSocket /v1/realtime — proper streaming."""
import asyncio, json, base64, time, wave, re, os
import numpy as np
import websockets
import librosa
from jiwer import wer as compute_wer
MODEL = "mistralai/Voxtral-Mini-4B-Realtime-2602"
WS_URI = "ws://localhost:8000/v1/realtime"
def norm(t):
return re.sub(r' +', ' ', re.sub(r'[^a-z0-9 ]', ' ', t.lower())).strip()
async def transcribe(audio_path, max_tokens=4096):
audio, _ = librosa.load(audio_path, sr=16000, mono=True)
pcm16 = (audio * 32767).astype(np.int16).tobytes()
dur = len(audio) / 16000
t0 = time.time()
transcript = ""
first_token_time = None
async with websockets.connect(WS_URI, max_size=2**24) as ws:
await ws.recv() # session.created
await ws.send(json.dumps({"type": "session.update", "model": MODEL}))
await ws.send(json.dumps({"type": "input_audio_buffer.commit"})) # signal ready
# Send audio in 4KB chunks
for i in range(0, len(pcm16), 4096):
await ws.send(json.dumps({
"type": "input_audio_buffer.append",
"audio": base64.b64encode(pcm16[i:i+4096]).decode(),
}))
await ws.send(json.dumps({"type": "input_audio_buffer.commit", "final": True}))
while True:
try:
msg = json.loads(await asyncio.wait_for(ws.recv(), timeout=120))
if msg["type"] == "transcription.delta":
d = msg.get("delta", "")
if d.strip() and first_token_time is None:
first_token_time = time.time() - t0
transcript += d
elif msg["type"] == "transcription.done":
transcript = msg.get("text", transcript)
break
elif msg["type"] == "error":
break
except asyncio.TimeoutError:
break
elapsed = time.time() - t0
return transcript.strip(), dur, elapsed / dur, first_token_time or elapsed
async def main():
# Warmup
print("Warmup...", flush=True)
await transcribe("/home/cloud/benchmark_data/librispeech_clean_0000.wav")
# LibriSpeech clean (full 91 samples)
print("\n=== Voxtral vLLM Realtime / LibriSpeech clean ===", flush=True)
clean = json.load(open("/home/cloud/benchmark_data/metadata.json"))
wers = []; ta = tp = 0
for i, s in enumerate(clean):
hyp, dur, rtf, fwl = await transcribe(s['path'])
w = compute_wer(norm(s['reference']), norm(hyp)) if hyp else 1.0
wers.append(w); ta += dur; tp += dur * rtf
if i < 3 or i % 20 == 0:
print(f" [{i}] {dur:.1f}s RTF={rtf:.3f} FWL={fwl:.2f}s WER={w:.1%} | {hyp[:60]}", flush=True)
clean_wer = np.mean(wers); clean_rtf = tp / ta
print(f" CLEAN ({len(clean)}): WER {clean_wer:.2%}, RTF {clean_rtf:.3f}\n", flush=True)
# LibriSpeech other (full 133 samples)
print("=== Voxtral vLLM Realtime / LibriSpeech other ===", flush=True)
other = json.load(open("/home/cloud/benchmark_data/metadata_other.json"))
wers2 = []; ta2 = tp2 = 0
for i, s in enumerate(other):
hyp, dur, rtf, fwl = await transcribe(s['path'])
w = compute_wer(norm(s['reference']), norm(hyp)) if hyp else 1.0
wers2.append(w); ta2 += dur; tp2 += dur * rtf
if i < 3 or i % 20 == 0:
print(f" [{i}] {dur:.1f}s RTF={rtf:.3f} WER={w:.1%}", flush=True)
other_wer = np.mean(wers2); other_rtf = tp2 / ta2
print(f" OTHER ({len(other)}): WER {other_wer:.2%}, RTF {other_rtf:.3f}\n", flush=True)
# ACL6060 talks
print("=== Voxtral vLLM Realtime / ACL6060 ===", flush=True)
acl = []
for talk in ["110", "117", "268", "367", "590"]:
gw = []
with open(f"/home/cloud/iwslt26-sst/inputs/en/acl6060.ts/gold-jsonl/2022.acl-long.{talk}.jsonl") as f:
for line in f: gw.append(json.loads(line)["text"].strip())
gold = " ".join(gw)
hyp, dur, rtf, fwl = await transcribe(f"/home/cloud/acl6060_audio/2022.acl-long.{talk}.wav")
w = compute_wer(norm(gold), norm(hyp)) if hyp else 1.0
acl.append({"talk": talk, "wer": round(float(w),4), "rtf": round(float(rtf),3), "dur": round(dur,1)})
print(f" Talk {talk}: {dur:.0f}s, WER {w:.2%}, RTF {rtf:.3f}, FWL {fwl:.2f}s", flush=True)
acl_wer = np.mean([r["wer"] for r in acl])
acl_rtf = np.mean([r["rtf"] for r in acl])
print(f" ACL6060 AVERAGE: WER {acl_wer:.2%}, RTF {acl_rtf:.3f}\n", flush=True)
# Summary
print(f"{'='*55}")
print(f" VOXTRAL vLLM REALTIME BENCHMARK (H100)")
print(f"{'='*55}")
print(f" LS clean ({len(clean)}): WER {clean_wer:.2%}, RTF {clean_rtf:.3f}")
print(f" LS other ({len(other)}): WER {other_wer:.2%}, RTF {other_rtf:.3f}")
print(f" ACL6060 (5): WER {acl_wer:.2%}, RTF {acl_rtf:.3f}")
results = {
"clean": {"avg_wer": round(float(clean_wer),4), "rtf": round(float(clean_rtf),3), "n": len(clean)},
"other": {"avg_wer": round(float(other_wer),4), "rtf": round(float(other_rtf),3), "n": len(other)},
"acl6060": {"avg_wer": round(float(acl_wer),4), "avg_rtf": round(float(acl_rtf),3), "talks": acl},
}
json.dump(results, open("/home/cloud/bench_voxtral_realtime_results.json", "w"), indent=2)
print(f"\n Saved to /home/cloud/bench_voxtral_realtime_results.json")
asyncio.run(main())
================================================
FILE: benchmarks/h100/generate_figures.py
================================================
#!/usr/bin/env python3
"""
Generate polished benchmark figures for WhisperLiveKit H100 results.
Reads data from results.json, outputs PNGs to this directory.
Run: python3 benchmarks/h100/generate_figures.py
"""
import json
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
DIR = os.path.dirname(os.path.abspath(__file__))
DATA = json.load(open(os.path.join(DIR, "results.json")))
# ── Style constants ──
COLORS = {
"whisper": "#d63031",
"qwen_b": "#6c5ce7",
"qwen_s": "#00b894",
"voxtral": "#fdcb6e",
"fw_m5": "#74b9ff",
"mlx_m5": "#55efc4",
"vox_m5": "#ffeaa7",
}
plt.rcParams.update({
"font.family": "sans-serif",
"font.size": 11,
"axes.spines.top": False,
"axes.spines.right": False,
})
def _save(fig, name):
path = os.path.join(DIR, name)
fig.savefig(path, dpi=180, bbox_inches="tight", facecolor="white")
plt.close(fig)
print(f" {name}")
# ──────────────────────────────────────────────────────────
# Figure 1: WER vs RTF scatter — H100 (LibriSpeech clean)
# ──────────────────────────────────────────────────────────
def fig_scatter_clean():
ls = DATA["librispeech_clean"]["systems"]
m5 = DATA["m5_reference"]["systems"]
fig, ax = plt.subplots(figsize=(9, 7.5))
ax.axhspan(0, 10, color="#f0fff0", alpha=0.5, zorder=0)
# M5 (ghost dots)
for k, v in m5.items():
ax.scatter(v["rtf"], v["wer"], s=50, c="silver", marker="o",
alpha=0.22, zorder=2, linewidths=0.4, edgecolors="gray")
# H100 systems — (name, data, color, marker, size, label_x_off, label_y_off)
pts = [
("Whisper large-v3", ls["whisper_large_v3_batch"], COLORS["whisper"], "h", 240, -8, -16),
("Qwen3-ASR 0.6B (batch)", ls["qwen3_0.6b_batch"], COLORS["qwen_b"], "h", 170, 8, 6),
("Qwen3-ASR 1.7B (batch)", ls["qwen3_1.7b_batch"], COLORS["qwen_b"], "h", 240, 8, -16),
("Voxtral 4B (vLLM)", ls["voxtral_4b_vllm_realtime"], COLORS["voxtral"], "D", 260, 8, 6),
("Qwen3 0.6B SimulStream+KV", ls["qwen3_0.6b_simulstream_kv"], COLORS["qwen_s"], "s", 220, 8, 6),
("Qwen3 1.7B SimulStream+KV", ls["qwen3_1.7b_simulstream_kv"], COLORS["qwen_s"], "s", 280, 8, -16),
]
for name, d, color, marker, sz, lx, ly in pts:
ax.scatter(d["rtf"], d["wer"], s=sz, c=color, marker=marker,
edgecolors="white", linewidths=1.5, zorder=5)
ax.annotate(name, (d["rtf"], d["wer"]), fontsize=8.5, fontweight="bold",
xytext=(lx, ly), textcoords="offset points",
arrowprops=dict(arrowstyle="-", color="#aaa", lw=0.5))
ax.set_xlabel("RTF (lower = faster)")
ax.set_ylabel("WER % (lower = better)")
ax.set_title("Speed vs Accuracy — LibriSpeech test-clean (H100 80 GB)",
fontsize=13, fontweight="bold", pad=12)
ax.set_xlim(-0.005, 0.20)
ax.set_ylim(-0.3, 10)
ax.grid(True, alpha=0.12)
legend = [
mpatches.Patch(color=COLORS["whisper"], label="Whisper large-v3"),
mpatches.Patch(color=COLORS["qwen_b"], label="Qwen3-ASR (batch)"),
mpatches.Patch(color=COLORS["qwen_s"], label="Qwen3 SimulStream+KV"),
mpatches.Patch(color=COLORS["voxtral"], label="Voxtral 4B (vLLM)"),
plt.Line2D([0],[0], marker="h", color="w", mfc="gray", ms=8, label="Batch"),
plt.Line2D([0],[0], marker="s", color="w", mfc="gray", ms=8, label="Streaming"),
]
ax.legend(handles=legend, fontsize=8.5, loc="upper right", framealpha=0.85, ncol=2)
_save(fig, "wer_vs_rtf_clean.png")
# ──────────────────────────────────────────────────────────
# Figure 2: ACL6060 conference talks — the realistic test
# ──────────────────────────────────────────────────────────
def fig_scatter_acl6060():
acl = DATA["acl6060"]["systems"]
fig, ax = plt.subplots(figsize=(10, 6.5))
ax.axhspan(0, 15, color="#f0fff0", alpha=0.4, zorder=0)
pts = [
("Voxtral 4B\n(vLLM Realtime)", acl["voxtral_4b_vllm_realtime"], COLORS["voxtral"], "D", 380),
("Qwen3 1.7B\nSimulStream+KV", acl["qwen3_1.7b_simulstream_kv"], COLORS["qwen_s"], "s", 380),
("Qwen3 0.6B\nSimulStream+KV", acl["qwen3_0.6b_simulstream_kv"], COLORS["qwen_s"], "s", 260),
("Whisper large-v3\n(batch)", acl["whisper_large_v3_batch"], COLORS["whisper"], "h", 320),
]
label_off = [(10, -12), (10, 6), (10, 6), (10, 6)]
for (name, d, color, marker, sz), (lx, ly) in zip(pts, label_off):
wer = d["avg_wer"]; rtf = d["avg_rtf"]
ax.scatter(rtf, wer, s=sz, c=color, marker=marker,
edgecolors="white", linewidths=1.5, zorder=5)
ax.annotate(name, (rtf, wer), fontsize=9.5, fontweight="bold",
xytext=(lx, ly), textcoords="offset points",
arrowprops=dict(arrowstyle="-", color="#aaa", lw=0.6))
# Cascade annotation
ax.annotate("Full STT+MT cascade\nRTF 0.15 (real-time)",
xy=(0.151, 1), xytext=(0.25, 4),
fontsize=9, fontstyle="italic", color="#1565c0",
arrowprops=dict(arrowstyle="->", color="#1565c0", lw=1.5),
bbox=dict(boxstyle="round,pad=0.3", fc="#e3f2fd", ec="#90caf9", alpha=0.9))
ax.set_xlabel("RTF (lower = faster)")
ax.set_ylabel("WER % (lower = better)")
ax.set_title("ACL6060 Conference Talks — 5 talks, 58 min (H100 80 GB)",
fontsize=13, fontweight="bold", pad=12)
ax.set_xlim(-0.005, 0.30)
ax.set_ylim(-1, 26)
ax.grid(True, alpha=0.12)
_save(fig, "wer_vs_rtf_acl6060.png")
# ──────────────────────────────────────────────────────────
# Figure 3: Bar chart — WER + RTF side-by-side
# ──────────────────────────────────────────────────────────
def fig_bars():
names = [
"Whisper\nlarge-v3", "Voxtral 4B\n(vLLM)", "Qwen3 0.6B\n(batch)",
"Qwen3 1.7B\n(batch)", "Qwen3 0.6B\nSimulStream", "Qwen3 1.7B\nSimulStream",
]
wer_c = [2.02, 2.71, 2.30, 2.46, 6.44, 8.09]
wer_o = [7.79, 9.26, 6.12, 5.34, 9.27, 9.56]
rtf_c = [0.071, 0.137, 0.065, 0.069, 0.109, 0.117]
fwl = [472, 137, 432, 457, 91, 94] # ms
cols = [COLORS["whisper"], COLORS["voxtral"], COLORS["qwen_b"],
COLORS["qwen_b"], COLORS["qwen_s"], COLORS["qwen_s"]]
cols_l = ["#ff7675", "#ffeaa7", "#a29bfe", "#a29bfe", "#55efc4", "#55efc4"]
x = np.arange(len(names))
fig, axes = plt.subplots(1, 3, figsize=(16, 6))
# WER
ax = axes[0]; w = 0.36
ax.bar(x - w/2, wer_c, w, color=cols, alpha=0.9, edgecolor="white", label="test-clean")
ax.bar(x + w/2, wer_o, w, color=cols_l, alpha=0.65, edgecolor="white", label="test-other")
ax.set_ylabel("WER %"); ax.set_title("Word Error Rate", fontweight="bold")
ax.set_xticks(x); ax.set_xticklabels(names, fontsize=7.5, rotation=25, ha="right")
ax.legend(fontsize=8); ax.grid(axis="y", alpha=0.15)
for i, v in enumerate(wer_c):
ax.text(i - w/2, v + 0.2, f"{v:.1f}", ha="center", fontsize=7, fontweight="bold")
# RTF
ax = axes[1]
ax.bar(x, rtf_c, 0.55, color=cols, alpha=0.9, edgecolor="white")
ax.set_ylabel("RTF (lower = faster)"); ax.set_title("Real-Time Factor (test-clean)", fontweight="bold")
ax.set_xticks(x); ax.set_xticklabels(names, fontsize=7.5, rotation=25, ha="right")
ax.grid(axis="y", alpha=0.15)
for i, v in enumerate(rtf_c):
ax.text(i, v + 0.003, f"{v:.3f}", ha="center", fontsize=8, fontweight="bold")
# First-word latency
ax = axes[2]
ax.bar(x, fwl, 0.55, color=cols, alpha=0.9, edgecolor="white")
ax.set_ylabel("ms"); ax.set_title("First Word Latency", fontweight="bold")
ax.set_xticks(x); ax.set_xticklabels(names, fontsize=7.5, rotation=25, ha="right")
ax.grid(axis="y", alpha=0.15)
for i, v in enumerate(fwl):
ax.text(i, v + 8, f"{v}", ha="center", fontsize=8, fontweight="bold")
fig.suptitle("LibriSpeech Benchmark — H100 80 GB", fontsize=14, fontweight="bold")
plt.tight_layout()
_save(fig, "bars_wer_rtf_latency.png")
# ──────────────────────────────────────────────────────────
# Figure 4: Clean vs Other robustness
# ──────────────────────────────────────────────────────────
def fig_robustness():
models = [
("Whisper large-v3", 2.02, 7.79, COLORS["whisper"], "h", 280),
("Qwen3 0.6B (batch)", 2.30, 6.12, COLORS["qwen_b"], "h", 180),
("Qwen3 1.7B (batch)", 2.46, 5.34, COLORS["qwen_b"], "h", 280),
("Voxtral 4B (vLLM)", 2.71, 9.26, COLORS["voxtral"], "D", 280),
("Qwen3 0.6B\nSimulStream", 6.44, 9.27, COLORS["qwen_s"], "s", 240),
("Qwen3 1.7B\nSimulStream", 8.09, 9.56, COLORS["qwen_s"], "s", 300),
]
# Manual label offsets — carefully placed to avoid overlap
offsets = [(-55, 10), (8, 10), (8, -18), (-55, -18), (-10, 12), (10, -18)]
fig, ax = plt.subplots(figsize=(8.5, 7))
ax.plot([0, 13], [0, 13], "--", color="#ccc", lw=1, zorder=1)
ax.fill_between([0, 13], [0, 13], [13, 13], color="#fff5f5", alpha=0.5, zorder=0)
ax.text(4, 11, "degrades more\non noisy audio", fontsize=9, color="#bbb", fontstyle="italic")
for (name, wc, wo, color, marker, sz), (lx, ly) in zip(models, offsets):
ax.scatter(wc, wo, s=sz, c=color, marker=marker,
edgecolors="white", linewidths=1.5, zorder=5)
ax.annotate(name, (wc, wo), fontsize=8.5, fontweight="bold",
xytext=(lx, ly), textcoords="offset points",
arrowprops=dict(arrowstyle="-", color="#aaa", lw=0.6))
deg = wo - wc
ax.annotate(f"+{deg:.1f}%", (wc, wo), fontsize=7, color="#999",
xytext=(-6, -13), textcoords="offset points")
ax.set_xlabel("WER % on test-clean")
ax.set_ylabel("WER % on test-other")
ax.set_title("Clean vs Noisy Robustness (H100 80 GB)", fontsize=13, fontweight="bold", pad=12)
ax.set_xlim(-0.3, 12); ax.set_ylim(-0.3, 12)
ax.set_aspect("equal"); ax.grid(True, alpha=0.12)
_save(fig, "robustness_clean_vs_other.png")
# ──────────────────────────────────────────────────────────
# Figure 5: ACL6060 per-talk breakdown (Qwen3 vs Voxtral)
# ──────────────────────────────────────────────────────────
def fig_per_talk():
q = DATA["acl6060"]["systems"]["qwen3_1.7b_simulstream_kv"]["per_talk"]
v = DATA["acl6060"]["systems"]["voxtral_4b_vllm_realtime"]["per_talk"]
talks = DATA["acl6060"]["talks"]
fig, ax = plt.subplots(figsize=(9, 5))
x = np.arange(len(talks)); w = 0.35
bars_v = ax.bar(x - w/2, [v[t] for t in talks], w, color=COLORS["voxtral"],
edgecolor="white", label="Voxtral 4B (vLLM)")
bars_q = ax.bar(x + w/2, [q[t] for t in talks], w, color=COLORS["qwen_s"],
edgecolor="white", label="Qwen3 1.7B SimulStream+KV")
for bar in bars_v:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
f"{bar.get_height():.1f}", ha="center", fontsize=8)
for bar in bars_q:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
f"{bar.get_height():.1f}", ha="center", fontsize=8)
ax.set_xlabel("ACL6060 Talk ID")
ax.set_ylabel("WER %")
ax.set_title("Per-Talk WER — ACL6060 Conference Talks (H100 80 GB)",
fontsize=13, fontweight="bold", pad=12)
ax.set_xticks(x); ax.set_xticklabels([f"Talk {t}" for t in talks])
ax.legend(fontsize=9); ax.grid(axis="y", alpha=0.15)
ax.set_ylim(0, 18)
_save(fig, "acl6060_per_talk.png")
if __name__ == "__main__":
print("Generating H100 benchmark figures...")
fig_scatter_clean()
fig_scatter_acl6060()
fig_bars()
fig_robustness()
fig_per_talk()
print("Done!")
================================================
FILE: benchmarks/h100/results.json
================================================
{
"hardware": "NVIDIA H100 80GB HBM3, CUDA 12.4, Driver 550.163",
"date": "2026-03-15",
"librispeech_clean": {
"n_samples": 91,
"total_audio_s": 602,
"systems": {
"whisper_large_v3_batch": {"wer": 2.02, "rtf": 0.071, "first_word_latency_s": 0.472},
"qwen3_0.6b_batch": {"wer": 2.30, "rtf": 0.065, "first_word_latency_s": 0.432},
"qwen3_1.7b_batch": {"wer": 2.46, "rtf": 0.069, "first_word_latency_s": 0.457},
"voxtral_4b_vllm_realtime": {"wer": 2.71, "rtf": 0.137, "first_word_latency_s": 0.137},
"qwen3_0.6b_simulstream_kv": {"wer": 6.44, "rtf": 0.109, "first_word_latency_s": 0.091},
"qwen3_1.7b_simulstream_kv": {"wer": 8.09, "rtf": 0.117, "first_word_latency_s": 0.094}
}
},
"librispeech_other": {
"n_samples": 133,
"total_audio_s": 600,
"systems": {
"qwen3_1.7b_batch": {"wer": 5.34, "rtf": 0.088},
"qwen3_0.6b_batch": {"wer": 6.12, "rtf": 0.086},
"whisper_large_v3_batch": {"wer": 7.79, "rtf": 0.092},
"qwen3_0.6b_simulstream_kv": {"wer": 9.27, "rtf": 0.127},
"voxtral_4b_vllm_realtime": {"wer": 9.26, "rtf": 0.144},
"qwen3_1.7b_simulstream_kv": {"wer": 9.56, "rtf": 0.140}
}
},
"acl6060": {
"description": "5 ACL 2022 conference talks, 58 min total",
"talks": ["110", "117", "268", "367", "590"],
"systems": {
"voxtral_4b_vllm_realtime": {"avg_wer": 7.83, "avg_rtf": 0.203, "per_talk": {"110": 5.18, "117": 2.24, "268": 14.88, "367": 9.40, "590": 7.45}},
"qwen3_1.7b_simulstream_kv": {"avg_wer": 9.20, "avg_rtf": 0.074, "per_talk": {"110": 5.59, "117": 8.12, "268": 12.25, "367": 12.29, "590": 7.77}},
"qwen3_0.6b_simulstream_kv": {"avg_wer": 13.21, "avg_rtf": 0.098},
"whisper_large_v3_batch": {"avg_wer": 22.53, "avg_rtf": 0.125}
}
},
"m5_reference": {
"description": "MacBook M5 results (from WLK scatter benchmarks)",
"systems": {
"fw_la_base": {"wer": 17.0, "rtf": 0.82},
"fw_la_small": {"wer": 8.6, "rtf": 0.76},
"fw_ss_base": {"wer": 7.8, "rtf": 0.46},
"fw_ss_small": {"wer": 7.0, "rtf": 0.90},
"mlx_ss_base": {"wer": 7.7, "rtf": 0.34},
"mlx_ss_small": {"wer": 6.5, "rtf": 0.68},
"voxtral_mlx": {"wer": 7.0, "rtf": 0.26},
"qwen3_mlx_0.6b":{"wer": 5.5, "rtf": 0.55},
"qwen3_0.6b_batch":{"wer":24.0, "rtf": 1.42}
}
}
}
================================================
FILE: benchmarks/m5/bench_0.6b_simul_500.json
================================================
{
"model": "Qwen3-ASR-0.6B",
"backend": "mlx-simul-streaming",
"mode": "simul-streaming",
"platform": "Apple M5 (32GB)",
"config": {
"border_fraction": 0.25,
"chunk_seconds": 2.0,
"chapter_grouped": false
},
"n_samples": 500,
"total_audio_s": 3809.0,
"total_inference_s": 1000.08,
"wer": 0.032951,
"cer": 0.006307,
"rtf": 0.262557,
"median_wer": 0.0,
"p90_wer": 0.1224,
"p95_wer": 0.2,
"alignment_heads_count": 20,
"alignment_heads_file": "/Users/quentin/Documents/repos/WhisperLiveKit/scripts/alignment_heads_qwen3_asr_0.6B.json",
"per_sample": [
{
"id": "1089-134686-0000",
"ref": "HE HOPED THERE WOULD BE STEW FOR DINNER TURNIPS AND CARROTS AND BRUISED POTATOES AND FAT MUTTON PIECES TO BE LADLED OUT IN THICK PEPPERED FLOUR FATTENED SAUCE",
"hyp": "He hoped there would be stew for dinner: turnips and carrots and bruised potatoes and fat mutton pieces to be ladled out in thick peppered flour-fatted sauce.",
"ref_norm": "HE HOPED THERE WOULD BE STEW FOR DINNER TURNIPS AND CARROTS AND BRUISED POTATOES AND FAT MUTTON PIECES TO BE LADLED OUT IN THICK PEPPERED FLOUR FATTENED SAUCE",
"hyp_norm": "HE HOPED THERE WOULD BE STEW FOR DINNER TURNIPS AND CARROTS AND BRUISED POTATOES AND FAT MUTTON PIECES TO BE LADLED OUT IN THICK PEPPERED FLOURFATTED SAUCE",
"duration_s": 10.435,
"infer_time_s": 2.853,
"rtf": 0.2734,
"wer": 0.0714
},
{
"id": "1089-134686-0001",
"ref": "STUFF IT INTO YOU HIS BELLY COUNSELLED HIM",
"hyp": "Stuff it into you, his belly counseled him.",
"ref_norm": "STUFF IT INTO YOU HIS BELLY COUNSELLED HIM",
"hyp_norm": "STUFF IT INTO YOU HIS BELLY COUNSELED HIM",
"duration_s": 3.275,
"infer_time_s": 0.887,
"rtf": 0.2709,
"wer": 0.125
},
{
"id": "1089-134686-0002",
"ref": "AFTER EARLY NIGHTFALL THE YELLOW LAMPS WOULD LIGHT UP HERE AND THERE THE SQUALID QUARTER OF THE BROTHELS",
"hyp": "After early night fall, the yellow lamps would light up here and there. The s qualid quarter of the brothels.",
"ref_norm": "AFTER EARLY NIGHTFALL THE YELLOW LAMPS WOULD LIGHT UP HERE AND THERE THE SQUALID QUARTER OF THE BROTHELS",
"hyp_norm": "AFTER EARLY NIGHT FALL THE YELLOW LAMPS WOULD LIGHT UP HERE AND THERE THE S QUALID QUARTER OF THE BROTHELS",
"duration_s": 6.625,
"infer_time_s": 1.857,
"rtf": 0.2803,
"wer": 0.2222
},
{
"id": "1089-134686-0003",
"ref": "HELLO BERTIE ANY GOOD IN YOUR MIND",
"hyp": "Hello, Bertie. Any good in your mind?",
"ref_norm": "HELLO BERTIE ANY GOOD IN YOUR MIND",
"hyp_norm": "HELLO BERTIE ANY GOOD IN YOUR MIND",
"duration_s": 2.68,
"infer_time_s": 0.831,
"rtf": 0.3099,
"wer": 0.0
},
{
"id": "1089-134686-0004",
"ref": "NUMBER TEN FRESH NELLY IS WAITING ON YOU GOOD NIGHT HUSBAND",
"hyp": "Number ten, fresh Nelly is waiting on you. Good night, husband.",
"ref_norm": "NUMBER TEN FRESH NELLY IS WAITING ON YOU GOOD NIGHT HUSBAND",
"hyp_norm": "NUMBER TEN FRESH NELLY IS WAITING ON YOU GOOD NIGHT HUSBAND",
"duration_s": 5.215,
"infer_time_s": 1.23,
"rtf": 0.2358,
"wer": 0.0
},
{
"id": "1089-134686-0005",
"ref": "THE MUSIC CAME NEARER AND HE RECALLED THE WORDS THE WORDS OF SHELLEY'S FRAGMENT UPON THE MOON WANDERING COMPANIONLESS PALE FOR WEARINESS",
"hyp": "The music came nearer, and he recalled the words, the words of Shelley's fragment upon the moon , wandering companionless, pale for weariness.",
"ref_norm": "THE MUSIC CAME NEARER AND HE RECALLED THE WORDS THE WORDS OF SHELLEYS FRAGMENT UPON THE MOON WANDERING COMPANIONLESS PALE FOR WEARINESS",
"hyp_norm": "THE MUSIC CAME NEARER AND HE RECALLED THE WORDS THE WORDS OF SHELLEYS FRAGMENT UPON THE MOON WANDERING COMPANIONLESS PALE FOR WEARINESS",
"duration_s": 9.635,
"infer_time_s": 2.28,
"rtf": 0.2367,
"wer": 0.0
},
{
"id": "1089-134686-0006",
"ref": "THE DULL LIGHT FELL MORE FAINTLY UPON THE PAGE WHEREON ANOTHER EQUATION BEGAN TO UNFOLD ITSELF SLOWLY AND TO SPREAD ABROAD ITS WIDENING TAIL",
"hyp": "The dull light fell more faintly upon the page, whereon another equation began to unfold itself slowly, and to spread abroad its widening tail.",
"ref_norm": "THE DULL LIGHT FELL MORE FAINTLY UPON THE PAGE WHEREON ANOTHER EQUATION BEGAN TO UNFOLD ITSELF SLOWLY AND TO SPREAD ABROAD ITS WIDENING TAIL",
"hyp_norm": "THE DULL LIGHT FELL MORE FAINTLY UPON THE PAGE WHEREON ANOTHER EQUATION BEGAN TO UNFOLD ITSELF SLOWLY AND TO SPREAD ABROAD ITS WIDENING TAIL",
"duration_s": 10.555,
"infer_time_s": 2.399,
"rtf": 0.2273,
"wer": 0.0
},
{
"id": "1089-134686-0007",
"ref": "A COLD LUCID INDIFFERENCE REIGNED IN HIS SOUL",
"hyp": "A cold, lucid indifference re igned in his soul.",
"ref_norm": "A COLD LUCID INDIFFERENCE REIGNED IN HIS SOUL",
"hyp_norm": "A COLD LUCID INDIFFERENCE RE IGNED IN HIS SOUL",
"duration_s": 4.275,
"infer_time_s": 1.016,
"rtf": 0.2376,
"wer": 0.25
},
{
"id": "1089-134686-0008",
"ref": "THE CHAOS IN WHICH HIS ARDOUR EXTINGUISHED ITSELF WAS A COLD INDIFFERENT KNOWLEDGE OF HIMSELF",
"hyp": "The chaos in which his ardor extinguished itself was a cold, indifferent knowledge of himself.",
"ref_norm": "THE CHAOS IN WHICH HIS ARDOUR EXTINGUISHED ITSELF WAS A COLD INDIFFERENT KNOWLEDGE OF HIMSELF",
"hyp_norm": "THE CHAOS IN WHICH HIS ARDOR EXTINGUISHED ITSELF WAS A COLD INDIFFERENT KNOWLEDGE OF HIMSELF",
"duration_s": 6.73,
"infer_time_s": 1.533,
"rtf": 0.2278,
"wer": 0.0667
},
{
"id": "1089-134686-0009",
"ref": "AT MOST BY AN ALMS GIVEN TO A BEGGAR WHOSE BLESSING HE FLED FROM HE MIGHT HOPE WEARILY TO WIN FOR HIMSELF SOME MEASURE OF ACTUAL GRACE",
"hyp": "At most, by an alms given to a beg gar whose blessing he fled from, he might hope wearily to win for himself some measure of actual grace.",
"ref_norm": "AT MOST BY AN ALMS GIVEN TO A BEGGAR WHOSE BLESSING HE FLED FROM HE MIGHT HOPE WEARILY TO WIN FOR HIMSELF SOME MEASURE OF ACTUAL GRACE",
"hyp_norm": "AT MOST BY AN ALMS GIVEN TO A BEG GAR WHOSE BLESSING HE FLED FROM HE MIGHT HOPE WEARILY TO WIN FOR HIMSELF SOME MEASURE OF ACTUAL GRACE",
"duration_s": 10.575,
"infer_time_s": 2.631,
"rtf": 0.2488,
"wer": 0.0741
},
{
"id": "1089-134686-0010",
"ref": "WELL NOW ENNIS I DECLARE YOU HAVE A HEAD AND SO HAS MY STICK",
"hyp": "Well now, Ennis, I declare you have a head , and so has my stick.",
"ref_norm": "WELL NOW ENNIS I DECLARE YOU HAVE A HEAD AND SO HAS MY STICK",
"hyp_norm": "WELL NOW ENNIS I DECLARE YOU HAVE A HEAD AND SO HAS MY STICK",
"duration_s": 4.405,
"infer_time_s": 1.385,
"rtf": 0.3144,
"wer": 0.0
},
{
"id": "1089-134686-0011",
"ref": "ON SATURDAY MORNINGS WHEN THE SODALITY MET IN THE CHAPEL TO RECITE THE LITTLE OFFICE HIS PLACE WAS A CUSHIONED KNEELING DESK AT THE RIGHT OF THE ALTAR FROM WHICH HE LED HIS WING OF BOYS THROUGH THE RESPONSES",
"hyp": "On Saturday mornings , when the sodality met in the chapel to recite the Little Office, his place was a cushioned kneeling desk at the right of the altar , from which he led his wing of boys through the responses.",
"ref_norm": "ON SATURDAY MORNINGS WHEN THE SODALITY MET IN THE CHAPEL TO RECITE THE LITTLE OFFICE HIS PLACE WAS A CUSHIONED KNEELING DESK AT THE RIGHT OF THE ALTAR FROM WHICH HE LED HIS WING OF BOYS THROUGH THE RESPONSES",
"hyp_norm": "ON SATURDAY MORNINGS WHEN THE SODALITY MET IN THE CHAPEL TO RECITE THE LITTLE OFFICE HIS PLACE WAS A CUSHIONED KNEELING DESK AT THE RIGHT OF THE ALTAR FROM WHICH HE LED HIS WING OF BOYS THROUGH THE RESPONSES",
"duration_s": 12.445,
"infer_time_s": 3.527,
"rtf": 0.2834,
"wer": 0.0
},
{
"id": "1089-134686-0012",
"ref": "HER EYES SEEMED TO REGARD HIM WITH MILD PITY HER HOLINESS A STRANGE LIGHT GLOWING FAINTLY UPON HER FRAIL FLESH DID NOT HUMILIATE THE SINNER WHO APPROACHED HER",
"hyp": "Her eyes seemed to regard him with mild pity; her holiness , a strange light glowing faintly upon her frail flesh, did not humiliate the sinner who approached her.",
"ref_norm": "HER EYES SEEMED TO REGARD HIM WITH MILD PITY HER HOLINESS A STRANGE LIGHT GLOWING FAINTLY UPON HER FRAIL FLESH DID NOT HUMILIATE THE SINNER WHO APPROACHED HER",
"hyp_norm": "HER EYES SEEMED TO REGARD HIM WITH MILD PITY HER HOLINESS A STRANGE LIGHT GLOWING FAINTLY UPON HER FRAIL FLESH DID NOT HUMILIATE THE SINNER WHO APPROACHED HER",
"duration_s": 11.64,
"infer_time_s": 2.801,
"rtf": 0.2407,
"wer": 0.0
},
{
"id": "1089-134686-0013",
"ref": "IF EVER HE WAS IMPELLED TO CAST SIN FROM HIM AND TO REPENT THE IMPULSE THAT MOVED HIM WAS THE WISH TO BE HER KNIGHT",
"hyp": "If ever he was imp elled to cast sin from him and to repent, the impulse that moved him was the wish to be her knight.",
"ref_norm": "IF EVER HE WAS IMPELLED TO CAST SIN FROM HIM AND TO REPENT THE IMPULSE THAT MOVED HIM WAS THE WISH TO BE HER KNIGHT",
"hyp_norm": "IF EVER HE WAS IMP ELLED TO CAST SIN FROM HIM AND TO REPENT THE IMPULSE THAT MOVED HIM WAS THE WISH TO BE HER KNIGHT",
"duration_s": 7.915,
"infer_time_s": 2.057,
"rtf": 0.2599,
"wer": 0.08
},
{
"id": "1089-134686-0014",
"ref": "HE TRIED TO THINK HOW IT COULD BE",
"hyp": "He tried to think how it could be.",
"ref_norm": "HE TRIED TO THINK HOW IT COULD BE",
"hyp_norm": "HE TRIED TO THINK HOW IT COULD BE",
"duration_s": 2.225,
"infer_time_s": 0.744,
"rtf": 0.3346,
"wer": 0.0
},
{
"id": "1089-134686-0015",
"ref": "BUT THE DUSK DEEPENING IN THE SCHOOLROOM COVERED OVER HIS THOUGHTS THE BELL RANG",
"hyp": "But the dusk deepening in the schoolroom covered over his thoughts. The bell rang.",
"ref_norm": "BUT THE DUSK DEEPENING IN THE SCHOOLROOM COVERED OVER HIS THOUGHTS THE BELL RANG",
"hyp_norm": "BUT THE DUSK DEEPENING IN THE SCHOOLROOM COVERED OVER HIS THOUGHTS THE BELL RANG",
"duration_s": 5.815,
"infer_time_s": 1.358,
"rtf": 0.2336,
"wer": 0.0
},
{
"id": "1089-134686-0016",
"ref": "THEN YOU CAN ASK HIM QUESTIONS ON THE CATECHISM DEDALUS",
"hyp": "Then you can ask him questions on the catechism, Dedalus.",
"ref_norm": "THEN YOU CAN ASK HIM QUESTIONS ON THE CATECHISM DEDALUS",
"hyp_norm": "THEN YOU CAN ASK HIM QUESTIONS ON THE CATECHISM DEDALUS",
"duration_s": 3.54,
"infer_time_s": 1.057,
"rtf": 0.2985,
"wer": 0.0
},
{
"id": "1089-134686-0017",
"ref": "STEPHEN LEANING BACK AND DRAWING IDLY ON HIS SCRIBBLER LISTENED TO THE TALK ABOUT HIM WHICH HERON CHECKED FROM TIME TO TIME BY SAYING",
"hyp": "Stephen, leaning back and drawing idly on his scribbler, listened to the talk about him , which Heron checked from time to time by saying.",
"ref_norm": "STEPHEN LEANING BACK AND DRAWING IDLY ON HIS SCRIBBLER LISTENED TO THE TALK ABOUT HIM WHICH HERON CHECKED FROM TIME TO TIME BY SAYING",
"hyp_norm": "STEPHEN LEANING BACK AND DRAWING IDLY ON HIS SCRIBBLER LISTENED TO THE TALK ABOUT HIM WHICH HERON CHECKED FROM TIME TO TIME BY SAYING",
"duration_s": 8.87,
"infer_time_s": 2.4,
"rtf": 0.2706,
"wer": 0.0
},
{
"id": "1089-134686-0018",
"ref": "IT WAS STRANGE TOO THAT HE FOUND AN ARID PLEASURE IN FOLLOWING UP TO THE END THE RIGID LINES OF THE DOCTRINES OF THE CHURCH AND PENETRATING INTO OBSCURE SILENCES ONLY TO HEAR AND FEEL THE MORE DEEPLY HIS OWN CONDEMNATION",
"hyp": "It was strange too that he found an arid pleasure in following up to the end the rigid lines of the doctrines of the church and penetrating into obscure silences only to hear and feel the more deeply his own condemnation.",
"ref_norm": "IT WAS STRANGE TOO THAT HE FOUND AN ARID PLEASURE IN FOLLOWING UP TO THE END THE RIGID LINES OF THE DOCTRINES OF THE CHURCH AND PENETRATING INTO OBSCURE SILENCES ONLY TO HEAR AND FEEL THE MORE DEEPLY HIS OWN CONDEMNATION",
"hyp_norm": "IT WAS STRANGE TOO THAT HE FOUND AN ARID PLEASURE IN FOLLOWING UP TO THE END THE RIGID LINES OF THE DOCTRINES OF THE CHURCH AND PENETRATING INTO OBSCURE SILENCES ONLY TO HEAR AND FEEL THE MORE DEEPLY HIS OWN CONDEMNATION",
"duration_s": 15.72,
"infer_time_s": 3.611,
"rtf": 0.2297,
"wer": 0.0
},
{
"id": "1089-134686-0019",
"ref": "THE SENTENCE OF SAINT JAMES WHICH SAYS THAT HE WHO OFFENDS AGAINST ONE COMMANDMENT BECOMES GUILTY OF ALL HAD SEEMED TO HIM FIRST A SWOLLEN PHRASE UNTIL HE HAD BEGUN TO GROPE IN THE DARKNESS OF HIS OWN STATE",
"hyp": "The sentence of Saint James, which says that he who offends against one commandment becomes guilty of all, had seemed to him first a swollen phrase until he had begun to grope in the darkness of his own state.",
"ref_norm": "THE SENTENCE OF SAINT JAMES WHICH SAYS THAT HE WHO OFFENDS AGAINST ONE COMMANDMENT BECOMES GUILTY OF ALL HAD SEEMED TO HIM FIRST A SWOLLEN PHRASE UNTIL HE HAD BEGUN TO GROPE IN THE DARKNESS OF HIS OWN STATE",
"hyp_norm": "THE SENTENCE OF SAINT JAMES WHICH SAYS THAT HE WHO OFFENDS AGAINST ONE COMMANDMENT BECOMES GUILTY OF ALL HAD SEEMED TO HIM FIRST A SWOLLEN PHRASE UNTIL HE HAD BEGUN TO GROPE IN THE DARKNESS OF HIS OWN STATE",
"duration_s": 13.895,
"infer_time_s": 3.445,
"rtf": 0.248,
"wer": 0.0
},
{
"id": "1089-134686-0020",
"ref": "IF A MAN HAD STOLEN A POUND IN HIS YOUTH AND HAD USED THAT POUND TO AMASS A HUGE FORTUNE HOW MUCH WAS HE OBLIGED TO GIVE BACK THE POUND HE HAD STOLEN ONLY OR THE POUND TOGETHER WITH THE COMPOUND INTEREST ACCRUING UPON IT OR ALL HIS HUGE FORTUNE",
"hyp": "If a man had stolen a pound in his youth and had used that pound to amass a huge fortune , how much was he obliged to give back \u2014the pound he had stolen only, or the pound together with the compound interest accruing upon it, or all his huge fortune?",
"ref_norm": "IF A MAN HAD STOLEN A POUND IN HIS YOUTH AND HAD USED THAT POUND TO AMASS A HUGE FORTUNE HOW MUCH WAS HE OBLIGED TO GIVE BACK THE POUND HE HAD STOLEN ONLY OR THE POUND TOGETHER WITH THE COMPOUND INTEREST ACCRUING UPON IT OR ALL HIS HUGE FORTUNE",
"hyp_norm": "IF A MAN HAD STOLEN A POUND IN HIS YOUTH AND HAD USED THAT POUND TO AMASS A HUGE FORTUNE HOW MUCH WAS HE OBLIGED TO GIVE BACK THE POUND HE HAD STOLEN ONLY OR THE POUND TOGETHER WITH THE COMPOUND INTEREST ACCRUING UPON IT OR ALL HIS HUGE FORTUNE",
"duration_s": 16.79,
"infer_time_s": 4.378,
"rtf": 0.2608,
"wer": 0.0
},
{
"id": "1089-134686-0021",
"ref": "IF A LAYMAN IN GIVING BAPTISM POUR THE WATER BEFORE SAYING THE WORDS IS THE CHILD BAPTIZED",
"hyp": "If a layman in giving baptism pour the water before saying the words , is the child baptized?",
"ref_norm": "IF A LAYMAN IN GIVING BAPTISM POUR THE WATER BEFORE SAYING THE WORDS IS THE CHILD BAPTIZED",
"hyp_norm": "IF A LAYMAN IN GIVING BAPTISM POUR THE WATER BEFORE SAYING THE WORDS IS THE CHILD BAPTIZED",
"duration_s": 6.55,
"infer_time_s": 1.616,
"rtf": 0.2468,
"wer": 0.0
},
{
"id": "1089-134686-0022",
"ref": "HOW COMES IT THAT WHILE THE FIRST BEATITUDE PROMISES THE KINGDOM OF HEAVEN TO THE POOR OF HEART THE SECOND BEATITUDE PROMISES ALSO TO THE MEEK THAT THEY SHALL POSSESS THE LAND",
"hyp": "How comes it that while the first beatitude promises the kingdom of heaven to the poor of heart, the second beatitude promises also to the meek that they shall possess the land?",
"ref_norm": "HOW COMES IT THAT WHILE THE FIRST BEATITUDE PROMISES THE KINGDOM OF HEAVEN TO THE POOR OF HEART THE SECOND BEATITUDE PROMISES ALSO TO THE MEEK THAT THEY SHALL POSSESS THE LAND",
"hyp_norm": "HOW COMES IT THAT WHILE THE FIRST BEATITUDE PROMISES THE KINGDOM OF HEAVEN TO THE POOR OF HEART THE SECOND BEATITUDE PROMISES ALSO TO THE MEEK THAT THEY SHALL POSSESS THE LAND",
"duration_s": 11.175,
"infer_time_s": 2.879,
"rtf": 0.2576,
"wer": 0.0
},
{
"id": "1089-134686-0023",
"ref": "WHY WAS THE SACRAMENT OF THE EUCHARIST INSTITUTED UNDER THE TWO SPECIES OF BREAD AND WINE IF JESUS CHRIST BE PRESENT BODY AND BLOOD SOUL AND DIVINITY IN THE BREAD ALONE AND IN THE WINE ALONE",
"hyp": "Why was the sacrament of the Eucharist instituted under the two species of bread and wine? If Jesus Christ be present body and blood, soul and divinity in the bread alone and in the wine alone.",
"ref_norm": "WHY WAS THE SACRAMENT OF THE EUCHARIST INSTITUTED UNDER THE TWO SPECIES OF BREAD AND WINE IF JESUS CHRIST BE PRESENT BODY AND BLOOD SOUL AND DIVINITY IN THE BREAD ALONE AND IN THE WINE ALONE",
"hyp_norm": "WHY WAS THE SACRAMENT OF THE EUCHARIST INSTITUTED UNDER THE TWO SPECIES OF BREAD AND WINE IF JESUS CHRIST BE PRESENT BODY AND BLOOD SOUL AND DIVINITY IN THE BREAD ALONE AND IN THE WINE ALONE",
"duration_s": 13.275,
"infer_time_s": 3.354,
"rtf": 0.2526,
"wer": 0.0
},
{
"id": "1089-134686-0024",
"ref": "IF THE WINE CHANGE INTO VINEGAR AND THE HOST CRUMBLE INTO CORRUPTION AFTER THEY HAVE BEEN CONSECRATED IS JESUS CHRIST STILL PRESENT UNDER THEIR SPECIES AS GOD AND AS MAN",
"hyp": "If the wine change into vinegar, and the host crumble into corruption after they have been consecrated , is Jesus Christ still present under their species as God and as man?",
"ref_norm": "IF THE WINE CHANGE INTO VINEGAR AND THE HOST CRUMBLE INTO CORRUPTION AFTER THEY HAVE BEEN CONSECRATED IS JESUS CHRIST STILL PRESENT UNDER THEIR SPECIES AS GOD AND AS MAN",
"hyp_norm": "IF THE WINE CHANGE INTO VINEGAR AND THE HOST CRUMBLE INTO CORRUPTION AFTER THEY HAVE BEEN CONSECRATED IS JESUS CHRIST STILL PRESENT UNDER THEIR SPECIES AS GOD AND AS MAN",
"duration_s": 11.655,
"infer_time_s": 2.765,
"rtf": 0.2372,
"wer": 0.0
},
{
"id": "1089-134686-0025",
"ref": "A GENTLE KICK FROM THE TALL BOY IN THE BENCH BEHIND URGED STEPHEN TO ASK A DIFFICULT QUESTION",
"hyp": "A gentle kick from the tall boy in the bench behind urged Stephen to ask a difficult question.",
"ref_norm": "A GENTLE KICK FROM THE TALL BOY IN THE BENCH BEHIND URGED STEPHEN TO ASK A DIFFICULT QUESTION",
"hyp_norm": "A GENTLE KICK FROM THE TALL BOY IN THE BENCH BEHIND URGED STEPHEN TO ASK A DIFFICULT QUESTION",
"duration_s": 6.61,
"infer_time_s": 1.562,
"rtf": 0.2362,
"wer": 0.0
},
{
"id": "1089-134686-0026",
"ref": "THE RECTOR DID NOT ASK FOR A CATECHISM TO HEAR THE LESSON FROM",
"hyp": "The rector did not ask for a catechism to hear the lesson from.",
"ref_norm": "THE RECTOR DID NOT ASK FOR A CATECHISM TO HEAR THE LESSON FROM",
"hyp_norm": "THE RECTOR DID NOT ASK FOR A CATECHISM TO HEAR THE LESSON FROM",
"duration_s": 4.01,
"infer_time_s": 1.309,
"rtf": 0.3263,
"wer": 0.0
},
{
"id": "1089-134686-0027",
"ref": "HE CLASPED HIS HANDS ON THE DESK AND SAID",
"hyp": "He clasped his hands on the desk and said.",
"ref_norm": "HE CLASPED HIS HANDS ON THE DESK AND SAID",
"hyp_norm": "HE CLASPED HIS HANDS ON THE DESK AND SAID",
"duration_s": 2.71,
"infer_time_s": 0.841,
"rtf": 0.3104,
"wer": 0.0
},
{
"id": "1089-134686-0028",
"ref": "THE RETREAT WILL BEGIN ON WEDNESDAY AFTERNOON IN HONOUR OF SAINT FRANCIS XAVIER WHOSE FEAST DAY IS SATURDAY",
"hyp": "The retreat will begin on Wednesday afternoon in honor of Saint Francis Xavier, whose feast day is Saturday.",
"ref_norm": "THE RETREAT WILL BEGIN ON WEDNESDAY AFTERNOON IN HONOUR OF SAINT FRANCIS XAVIER WHOSE FEAST DAY IS SATURDAY",
"hyp_norm": "THE RETREAT WILL BEGIN ON WEDNESDAY AFTERNOON IN HONOR OF SAINT FRANCIS XAVIER WHOSE FEAST DAY IS SATURDAY",
"duration_s": 7.83,
"infer_time_s": 1.618,
"rtf": 0.2066,
"wer": 0.0556
},
{
"id": "1089-134686-0029",
"ref": "ON FRIDAY CONFESSION WILL BE HEARD ALL THE AFTERNOON AFTER BEADS",
"hyp": "On Friday, confession will be heard all the afternoon after beads.",
"ref_norm": "ON FRIDAY CONFESSION WILL BE HEARD ALL THE AFTERNOON AFTER BEADS",
"hyp_norm": "ON FRIDAY CONFESSION WILL BE HEARD ALL THE AFTERNOON AFTER BEADS",
"duration_s": 4.67,
"infer_time_s": 1.069,
"rtf": 0.2288,
"wer": 0.0
},
{
"id": "1089-134686-0030",
"ref": "BEWARE OF MAKING THAT MISTAKE",
"hyp": "Beware of making that mistake.",
"ref_norm": "BEWARE OF MAKING THAT MISTAKE",
"hyp_norm": "BEWARE OF MAKING THAT MISTAKE",
"duration_s": 2.715,
"infer_time_s": 0.623,
"rtf": 0.2296,
"wer": 0.0
},
{
"id": "1089-134686-0031",
"ref": "STEPHEN'S HEART BEGAN SLOWLY TO FOLD AND FADE WITH FEAR LIKE A WITHERING FLOWER",
"hyp": "Stephen's heart began slowly to fold and fade with fear , like a withering flower.",
"ref_norm": "STEPHENS HEART BEGAN SLOWLY TO FOLD AND FADE WITH FEAR LIKE A WITHERING FLOWER",
"hyp_norm": "STEPHENS HEART BEGAN SLOWLY TO FOLD AND FADE WITH FEAR LIKE A WITHERING FLOWER",
"duration_s": 6.615,
"infer_time_s": 1.476,
"rtf": 0.2231,
"wer": 0.0
},
{
"id": "1089-134686-0032",
"ref": "HE IS CALLED AS YOU KNOW THE APOSTLE OF THE INDIES",
"hyp": "He is called, as you know, the Apostle of the Indies.",
"ref_norm": "HE IS CALLED AS YOU KNOW THE APOSTLE OF THE INDIES",
"hyp_norm": "HE IS CALLED AS YOU KNOW THE APOSTLE OF THE INDIES",
"duration_s": 4.09,
"infer_time_s": 1.125,
"rtf": 0.2751,
"wer": 0.0
},
{
"id": "1089-134686-0033",
"ref": "A GREAT SAINT SAINT FRANCIS XAVIER",
"hyp": "A great saint, Saint Francis Xavier.",
"ref_norm": "A GREAT SAINT SAINT FRANCIS XAVIER",
"hyp_norm": "A GREAT SAINT SAINT FRANCIS XAVIER",
"duration_s": 3.33,
"infer_time_s": 0.684,
"rtf": 0.2054,
"wer": 0.0
},
{
"id": "1089-134686-0034",
"ref": "THE RECTOR PAUSED AND THEN SHAKING HIS CLASPED HANDS BEFORE HIM WENT ON",
"hyp": "The rector paused and then shaking his clasped hands before him, went on.",
"ref_norm": "THE RECTOR PAUSED AND THEN SHAKING HIS CLASPED HANDS BEFORE HIM WENT ON",
"hyp_norm": "THE RECTOR PAUSED AND THEN SHAKING HIS CLASPED HANDS BEFORE HIM WENT ON",
"duration_s": 5.81,
"infer_time_s": 1.277,
"rtf": 0.2197,
"wer": 0.0
},
{
"id": "1089-134686-0035",
"ref": "HE HAD THE FAITH IN HIM THAT MOVES MOUNTAINS",
"hyp": "He had the faith in him that moves mountains.",
"ref_norm": "HE HAD THE FAITH IN HIM THAT MOVES MOUNTAINS",
"hyp_norm": "HE HAD THE FAITH IN HIM THAT MOVES MOUNTAINS",
"duration_s": 3.445,
"infer_time_s": 0.79,
"rtf": 0.2292,
"wer": 0.0
},
{
"id": "1089-134686-0036",
"ref": "A GREAT SAINT SAINT FRANCIS XAVIER",
"hyp": "A great saint, Saint Francis Xavier.",
"ref_norm": "A GREAT SAINT SAINT FRANCIS XAVIER",
"hyp_norm": "A GREAT SAINT SAINT FRANCIS XAVIER",
"duration_s": 3.25,
"infer_time_s": 0.682,
"rtf": 0.2097,
"wer": 0.0
},
{
"id": "1089-134686-0037",
"ref": "IN THE SILENCE THEIR DARK FIRE KINDLED THE DUSK INTO A TAWNY GLOW",
"hyp": "In the silence, their dark fire kindled the dusk into a tawny glow.",
"ref_norm": "IN THE SILENCE THEIR DARK FIRE KINDLED THE DUSK INTO A TAWNY GLOW",
"hyp_norm": "IN THE SILENCE THEIR DARK FIRE KINDLED THE DUSK INTO A TAWNY GLOW",
"duration_s": 5.21,
"infer_time_s": 1.378,
"rtf": 0.2646,
"wer": 0.0
},
{
"id": "1089-134691-0000",
"ref": "HE COULD WAIT NO LONGER",
"hyp": "He could wait no longer.",
"ref_norm": "HE COULD WAIT NO LONGER",
"hyp_norm": "HE COULD WAIT NO LONGER",
"duration_s": 2.085,
"infer_time_s": 0.578,
"rtf": 0.2773,
"wer": 0.0
},
{
"id": "1089-134691-0001",
"ref": "FOR A FULL HOUR HE HAD PACED UP AND DOWN WAITING BUT HE COULD WAIT NO LONGER",
"hyp": "For a full hour, he had paced up and down, waiting , but he could wait no longer.",
"ref_norm": "FOR A FULL HOUR HE HAD PACED UP AND DOWN WAITING BUT HE COULD WAIT NO LONGER",
"hyp_norm": "FOR A FULL HOUR HE HAD PACED UP AND DOWN WAITING BUT HE COULD WAIT NO LONGER",
"duration_s": 5.415,
"infer_time_s": 1.498,
"rtf": 0.2766,
"wer": 0.0
},
{
"id": "1089-134691-0002",
"ref": "HE SET OFF ABRUPTLY FOR THE BULL WALKING RAPIDLY LEST HIS FATHER'S SHRILL WHISTLE MIGHT CALL HIM BACK AND IN A FEW MOMENTS HE HAD ROUNDED THE CURVE AT THE POLICE BARRACK AND WAS SAFE",
"hyp": "He set off abruptly for the bull, walking rapidly lest his father 's shrill whistle might call him back, and in a few moments he had rounded the curve at the police barrack and was safe.",
"ref_norm": "HE SET OFF ABRUPTLY FOR THE BULL WALKING RAPIDLY LEST HIS FATHERS SHRILL WHISTLE MIGHT CALL HIM BACK AND IN A FEW MOMENTS HE HAD ROUNDED THE CURVE AT THE POLICE BARRACK AND WAS SAFE",
"hyp_norm": "HE SET OFF ABRUPTLY FOR THE BULL WALKING RAPIDLY LEST HIS FATHER S SHRILL WHISTLE MIGHT CALL HIM BACK AND IN A FEW MOMENTS HE HAD ROUNDED THE CURVE AT THE POLICE BARRACK AND WAS SAFE",
"duration_s": 11.6,
"infer_time_s": 3.036,
"rtf": 0.2617,
"wer": 0.0571
},
{
"id": "1089-134691-0003",
"ref": "THE UNIVERSITY",
"hyp": "The university .",
"ref_norm": "THE UNIVERSITY",
"hyp_norm": "THE UNIVERSITY",
"duration_s": 2.175,
"infer_time_s": 0.421,
"rtf": 0.1936,
"wer": 0.0
},
{
"id": "1089-134691-0004",
"ref": "PRIDE AFTER SATISFACTION UPLIFTED HIM LIKE LONG SLOW WAVES",
"hyp": "Bride, after satisfaction, uplifted him like long, slow waves.",
"ref_norm": "PRIDE AFTER SATISFACTION UPLIFTED HIM LIKE LONG SLOW WAVES",
"hyp_norm": "BRIDE AFTER SATISFACTION UPLIFTED HIM LIKE LONG SLOW WAVES",
"duration_s": 5.175,
"infer_time_s": 1.198,
"rtf": 0.2315,
"wer": 0.1111
},
{
"id": "1089-134691-0005",
"ref": "WHOSE FEET ARE AS THE FEET OF HARTS AND UNDERNEATH THE EVERLASTING ARMS",
"hyp": "Whose feet are as the feet of hearts, and underneath the everlasting arms.",
"ref_norm": "WHOSE FEET ARE AS THE FEET OF HARTS AND UNDERNEATH THE EVERLASTING ARMS",
"hyp_norm": "WHOSE FEET ARE AS THE FEET OF HEARTS AND UNDERNEATH THE EVERLASTING ARMS",
"duration_s": 5.36,
"infer_time_s": 1.269,
"rtf": 0.2368,
"wer": 0.0769
},
{
"id": "1089-134691-0006",
"ref": "THE PRIDE OF THAT DIM IMAGE BROUGHT BACK TO HIS MIND THE DIGNITY OF THE OFFICE HE HAD REFUSED",
"hyp": "The pride of that dim image brought back to his mind the dignity of the office he had refused.",
"ref_norm": "THE PRIDE OF THAT DIM IMAGE BROUGHT BACK TO HIS MIND THE DIGNITY OF THE OFFICE HE HAD REFUSED",
"hyp_norm": "THE PRIDE OF THAT DIM IMAGE BROUGHT BACK TO HIS MIND THE DIGNITY OF THE OFFICE HE HAD REFUSED",
"duration_s": 5.895,
"infer_time_s": 1.447,
"rtf": 0.2455,
"wer": 0.0
},
{
"id": "1089-134691-0007",
"ref": "SOON THE WHOLE BRIDGE WAS TREMBLING AND RESOUNDING",
"hyp": "Soon, the whole bridge was trembling and resounding.",
"ref_norm": "SOON THE WHOLE BRIDGE WAS TREMBLING AND RESOUNDING",
"hyp_norm": "SOON THE WHOLE BRIDGE WAS TREMBLING AND RESOUNDING",
"duration_s": 3.44,
"infer_time_s": 0.859,
"rtf": 0.2497,
"wer": 0.0
},
{
"id": "1089-134691-0008",
"ref": "THE UNCOUTH FACES PASSED HIM TWO BY TWO STAINED YELLOW OR RED OR LIVID BY THE SEA AND AS HE STROVE TO LOOK AT THEM WITH EASE AND INDIFFERENCE A FAINT STAIN OF PERSONAL SHAME AND COMMISERATION ROSE TO HIS OWN FACE",
"hyp": "The uncouth faces passed him two by two , stained yellow or red or livid by the sea , and as he strove to look at them with ease and indifference, a faint stain of personal shame and commiseration rose to his own face.",
"ref_norm": "THE UNCOUTH FACES PASSED HIM TWO BY TWO STAINED YELLOW OR RED OR LIVID BY THE SEA AND AS HE STROVE TO LOOK AT THEM WITH EASE AND INDIFFERENCE A FAINT STAIN OF PERSONAL SHAME AND COMMISERATION ROSE TO HIS OWN FACE",
"hyp_norm": "THE UNCOUTH FACES PASSED HIM TWO BY TWO STAINED YELLOW OR RED OR LIVID BY THE SEA AND AS HE STROVE TO LOOK AT THEM WITH EASE AND INDIFFERENCE A FAINT STAIN OF PERSONAL SHAME AND COMMISERATION ROSE TO HIS OWN FACE",
"duration_s": 14.985,
"infer_time_s": 3.942,
"rtf": 0.263,
"wer": 0.0
},
{
"id": "1089-134691-0009",
"ref": "ANGRY WITH HIMSELF HE TRIED TO HIDE HIS FACE FROM THEIR EYES BY GAZING DOWN SIDEWAYS INTO THE SHALLOW SWIRLING WATER UNDER THE BRIDGE BUT HE STILL SAW A REFLECTION THEREIN OF THEIR TOP HEAVY SILK HATS AND HUMBLE TAPE LIKE COLLARS AND LOOSELY HANGING CLERICAL CLOTHES BROTHER HICKEY",
"hyp": "Angry with himself , he tried to hide his face from their eyes by g azing down sideways into the shallow, swirling water under the bridge, but he still saw a reflection therein of their top-heavy silk hats, and humble tape-like collars and loosely hanging clerical clothes. Brother Hickey.",
"ref_norm": "ANGRY WITH HIMSELF HE TRIED TO HIDE HIS FACE FROM THEIR EYES BY GAZING DOWN SIDEWAYS INTO THE SHALLOW SWIRLING WATER UNDER THE BRIDGE BUT HE STILL SAW A REFLECTION THEREIN OF THEIR TOP HEAVY SILK HATS AND HUMBLE TAPE LIKE COLLARS AND LOOSELY HANGING CLERICAL CLOTHES BROTHER HICKEY",
"hyp_norm": "ANGRY WITH HIMSELF HE TRIED TO HIDE HIS FACE FROM THEIR EYES BY G AZING DOWN SIDEWAYS INTO THE SHALLOW SWIRLING WATER UNDER THE BRIDGE BUT HE STILL SAW A REFLECTION THEREIN OF THEIR TOPHEAVY SILK HATS AND HUMBLE TAPELIKE COLLARS AND LOOSELY HANGING CLERICAL CLOTHES BROTHER HICKEY",
"duration_s": 20.055,
"infer_time_s": 4.952,
"rtf": 0.2469,
"wer": 0.1224
},
{
"id": "1089-134691-0010",
"ref": "BROTHER MAC ARDLE BROTHER KEOGH",
"hyp": "Brother Macardal. Brother Kiyof.",
"ref_norm": "BROTHER MAC ARDLE BROTHER KEOGH",
"hyp_norm": "BROTHER MACARDAL BROTHER KIYOF",
"duration_s": 3.195,
"infer_time_s": 0.803,
"rtf": 0.2513,
"wer": 0.6
},
{
"id": "1089-134691-0011",
"ref": "THEIR PIETY WOULD BE LIKE THEIR NAMES LIKE THEIR FACES LIKE THEIR CLOTHES AND IT WAS IDLE FOR HIM TO TELL HIMSELF THAT THEIR HUMBLE AND CONTRITE HEARTS IT MIGHT BE PAID A FAR RICHER TRIBUTE OF DEVOTION THAN HIS HAD EVER BEEN A GIFT TENFOLD MORE ACCEPTABLE THAN HIS ELABORATE ADORATION",
"hyp": "Their piety would be like their names , like their faces, like their clothes, and it was idle for him to tell himself that their humble and contrite hearts it might be paid a far richer tribute of devotion than his had ever been, a gift tenfold more acceptable than his elaborate adoration.",
"ref_norm": "THEIR PIETY WOULD BE LIKE THEIR NAMES LIKE THEIR FACES LIKE THEIR CLOTHES AND IT WAS IDLE FOR HIM TO TELL HIMSELF THAT THEIR HUMBLE AND CONTRITE HEARTS IT MIGHT BE PAID A FAR RICHER TRIBUTE OF DEVOTION THAN HIS HAD EVER BEEN A GIFT TENFOLD MORE ACCEPTABLE THAN HIS ELABORATE ADORATION",
"hyp_norm": "THEIR PIETY WOULD BE LIKE THEIR NAMES LIKE THEIR FACES LIKE THEIR CLOTHES AND IT WAS IDLE FOR HIM TO TELL HIMSELF THAT THEIR HUMBLE AND CONTRITE HEARTS IT MIGHT BE PAID A FAR RICHER TRIBUTE OF DEVOTION THAN HIS HAD EVER BEEN A GIFT TENFOLD MORE ACCEPTABLE THAN HIS ELABORATE ADORATION",
"duration_s": 20.01,
"infer_time_s": 5.012,
"rtf": 0.2505,
"wer": 0.0
},
{
"id": "1089-134691-0012",
"ref": "IT WAS IDLE FOR HIM TO MOVE HIMSELF TO BE GENEROUS TOWARDS THEM TO TELL HIMSELF THAT IF HE EVER CAME TO THEIR GATES STRIPPED OF HIS PRIDE BEATEN AND IN BEGGAR'S WEEDS THAT THEY WOULD BE GENEROUS TOWARDS HIM LOVING HIM AS THEMSELVES",
"hyp": "It was idle for him to move himself to be generous towards them. To tell himself that if he ever came to their gates, stripped of his pride, beaten and in beggar's weeds , that they would be generous towards him, loving him as themselves.",
"ref_norm": "IT WAS IDLE FOR HIM TO MOVE HIMSELF TO BE GENEROUS TOWARDS THEM TO TELL HIMSELF THAT IF HE EVER CAME TO THEIR GATES STRIPPED OF HIS PRIDE BEATEN AND IN BEGGARS WEEDS THAT THEY WOULD BE GENEROUS TOWARDS HIM LOVING HIM AS THEMSELVES",
"hyp_norm": "IT WAS IDLE FOR HIM TO MOVE HIMSELF TO BE GENEROUS TOWARDS THEM TO TELL HIMSELF THAT IF HE EVER CAME TO THEIR GATES STRIPPED OF HIS PRIDE BEATEN AND IN BEGGARS WEEDS THAT THEY WOULD BE GENEROUS TOWARDS HIM LOVING HIM AS THEMSELVES",
"duration_s": 15.03,
"infer_time_s": 3.972,
"rtf": 0.2643,
"wer": 0.0
},
{
"id": "1089-134691-0013",
"ref": "IDLE AND EMBITTERING FINALLY TO ARGUE AGAINST HIS OWN DISPASSIONATE CERTITUDE THAT THE COMMANDMENT OF LOVE BADE US NOT TO LOVE OUR NEIGHBOUR AS OURSELVES WITH THE SAME AMOUNT AND INTENSITY OF LOVE BUT TO LOVE HIM AS OURSELVES WITH THE SAME KIND OF LOVE",
"hyp": "Idle and embitter ing, finally to argue against his own dispass ionate certitude, that the commandment of love bade us not to love our neighbor as ourselves with the same amount and intensity of love , but to love him as ourselves with the same kind of love.",
"ref_norm": "IDLE AND EMBITTERING FINALLY TO ARGUE AGAINST HIS OWN DISPASSIONATE CERTITUDE THAT THE COMMANDMENT OF LOVE BADE US NOT TO LOVE OUR NEIGHBOUR AS OURSELVES WITH THE SAME AMOUNT AND INTENSITY OF LOVE BUT TO LOVE HIM AS OURSELVES WITH THE SAME KIND OF LOVE",
"hyp_norm": "IDLE AND EMBITTER ING FINALLY TO ARGUE AGAINST HIS OWN DISPASS IONATE CERTITUDE THAT THE COMMANDMENT OF LOVE BADE US NOT TO LOVE OUR NEIGHBOR AS OURSELVES WITH THE SAME AMOUNT AND INTENSITY OF LOVE BUT TO LOVE HIM AS OURSELVES WITH THE SAME KIND OF LOVE",
"duration_s": 16.33,
"infer_time_s": 4.358,
"rtf": 0.2669,
"wer": 0.1111
},
{
"id": "1089-134691-0014",
"ref": "THE PHRASE AND THE DAY AND THE SCENE HARMONIZED IN A CHORD",
"hyp": "The phrase and the day and the scene harmonized in accord.",
"ref_norm": "THE PHRASE AND THE DAY AND THE SCENE HARMONIZED IN A CHORD",
"hyp_norm": "THE PHRASE AND THE DAY AND THE SCENE HARMONIZED IN ACCORD",
"duration_s": 4.755,
"infer_time_s": 1.071,
"rtf": 0.2253,
"wer": 0.1667
},
{
"id": "1089-134691-0015",
"ref": "WORDS WAS IT THEIR COLOURS",
"hyp": "Words. Was it their colors?",
"ref_norm": "WORDS WAS IT THEIR COLOURS",
"hyp_norm": "WORDS WAS IT THEIR COLORS",
"duration_s": 3.395,
"infer_time_s": 0.58,
"rtf": 0.1708,
"wer": 0.2
},
{
"id": "1089-134691-0016",
"ref": "THEY WERE VOYAGING ACROSS THE DESERTS OF THE SKY A HOST OF NOMADS ON THE MARCH VOYAGING HIGH OVER IRELAND WESTWARD BOUND",
"hyp": "They were voyaging across the deserts of the sky , a host of nomads on the march, voy aging high over Ireland westward bound.",
"ref_norm": "THEY WERE VOYAGING ACROSS THE DESERTS OF THE SKY A HOST OF NOMADS ON THE MARCH VOYAGING HIGH OVER IRELAND WESTWARD BOUND",
"hyp_norm": "THEY WERE VOYAGING ACROSS THE DESERTS OF THE SKY A HOST OF NOMADS ON THE MARCH VOY AGING HIGH OVER IRELAND WESTWARD BOUND",
"duration_s": 9.06,
"infer_time_s": 2.268,
"rtf": 0.2503,
"wer": 0.0909
},
{
"id": "1089-134691-0017",
"ref": "THE EUROPE THEY HAD COME FROM LAY OUT THERE BEYOND THE IRISH SEA EUROPE OF STRANGE TONGUES AND VALLEYED AND WOODBEGIRT AND CITADELLED AND OF ENTRENCHED AND MARSHALLED RACES",
"hyp": "The Europe they had come from lay out there beyond the Irish Sea , Europe of strange tongues and valleyed and wood begirt and citadelled and of entrenched and marshalled races.",
"ref_norm": "THE EUROPE THEY HAD COME FROM LAY OUT THERE BEYOND THE IRISH SEA EUROPE OF STRANGE TONGUES AND VALLEYED AND WOODBEGIRT AND CITADELLED AND OF ENTRENCHED AND MARSHALLED RACES",
"hyp_norm": "THE EUROPE THEY HAD COME FROM LAY OUT THERE BEYOND THE IRISH SEA EUROPE OF STRANGE TONGUES AND VALLEYED AND WOOD BEGIRT AND CITADELLED AND OF ENTRENCHED AND MARSHALLED RACES",
"duration_s": 11.695,
"infer_time_s": 2.83,
"rtf": 0.242,
"wer": 0.069
},
{
"id": "1089-134691-0018",
"ref": "AGAIN AGAIN",
"hyp": "Again. Again.",
"ref_norm": "AGAIN AGAIN",
"hyp_norm": "AGAIN AGAIN",
"duration_s": 3.09,
"infer_time_s": 0.422,
"rtf": 0.1365,
"wer": 0.0
},
{
"id": "1089-134691-0019",
"ref": "A VOICE FROM BEYOND THE WORLD WAS CALLING",
"hyp": "A voice from beyond the world was calling.",
"ref_norm": "A VOICE FROM BEYOND THE WORLD WAS CALLING",
"hyp_norm": "A VOICE FROM BEYOND THE WORLD WAS CALLING",
"duration_s": 3.155,
"infer_time_s": 0.738,
"rtf": 0.2338,
"wer": 0.0
},
{
"id": "1089-134691-0020",
"ref": "HELLO STEPHANOS HERE COMES THE DEDALUS",
"hyp": "Hello, Stephan os, here comes the Dedalus.",
"ref_norm": "HELLO STEPHANOS HERE COMES THE DEDALUS",
"hyp_norm": "HELLO STEPHAN OS HERE COMES THE DEDALUS",
"duration_s": 3.99,
"infer_time_s": 0.835,
"rtf": 0.2093,
"wer": 0.3333
},
{
"id": "1089-134691-0021",
"ref": "THEIR DIVING STONE POISED ON ITS RUDE SUPPORTS AND ROCKING UNDER THEIR PLUNGES AND THE ROUGH HEWN STONES OF THE SLOPING BREAKWATER OVER WHICH THEY SCRAMBLED IN THEIR HORSEPLAY GLEAMED WITH COLD WET LUSTRE",
"hyp": "Their diving stone poised on its rude supports and rocking under their plunges, and the rough-hewn stones of the sloping breakwater over which they scrambled in their horseplay, gleamed with cold, wet lustre.",
"ref_norm": "THEIR DIVING STONE POISED ON ITS RUDE SUPPORTS AND ROCKING UNDER THEIR PLUNGES AND THE ROUGH HEWN STONES OF THE SLOPING BREAKWATER OVER WHICH THEY SCRAMBLED IN THEIR HORSEPLAY GLEAMED WITH COLD WET LUSTRE",
"hyp_norm": "THEIR DIVING STONE POISED ON ITS RUDE SUPPORTS AND ROCKING UNDER THEIR PLUNGES AND THE ROUGHHEWN STONES OF THE SLOPING BREAKWATER OVER WHICH THEY SCRAMBLED IN THEIR HORSEPLAY GLEAMED WITH COLD WET LUSTRE",
"duration_s": 13.37,
"infer_time_s": 3.432,
"rtf": 0.2567,
"wer": 0.0588
},
{
"id": "1089-134691-0022",
"ref": "HE STOOD STILL IN DEFERENCE TO THEIR CALLS AND PARRIED THEIR BANTER WITH EASY WORDS",
"hyp": "He stood still in deference to their calls and parried their banter with easy words.",
"ref_norm": "HE STOOD STILL IN DEFERENCE TO THEIR CALLS AND PARRIED THEIR BANTER WITH EASY WORDS",
"hyp_norm": "HE STOOD STILL IN DEFERENCE TO THEIR CALLS AND PARRIED THEIR BANTER WITH EASY WORDS",
"duration_s": 5.635,
"infer_time_s": 1.412,
"rtf": 0.2506,
"wer": 0.0
},
{
"id": "1089-134691-0023",
"ref": "IT WAS A PAIN TO SEE THEM AND A SWORD LIKE PAIN TO SEE THE SIGNS OF ADOLESCENCE THAT MADE REPELLENT THEIR PITIABLE NAKEDNESS",
"hyp": "It was a pain to see them, and a sword-like pain to see the signs of adolescence that made repellent their pitiable nakedness.",
"ref_norm": "IT WAS A PAIN TO SEE THEM AND A SWORD LIKE PAIN TO SEE THE SIGNS OF ADOLESCENCE THAT MADE REPELLENT THEIR PITIABLE NAKEDNESS",
"hyp_norm": "IT WAS A PAIN TO SEE THEM AND A SWORDLIKE PAIN TO SEE THE SIGNS OF ADOLESCENCE THAT MADE REPELLENT THEIR PITIABLE NAKEDNESS",
"duration_s": 7.735,
"infer_time_s": 2.098,
"rtf": 0.2712,
"wer": 0.0833
},
{
"id": "1089-134691-0024",
"ref": "STEPHANOS DEDALOS",
"hyp": "Stephano Ster lows.",
"ref_norm": "STEPHANOS DEDALOS",
"hyp_norm": "STEPHANO STER LOWS",
"duration_s": 2.215,
"infer_time_s": 0.624,
"rtf": 0.2819,
"wer": 1.5
},
{
"id": "1089-134691-0025",
"ref": "A MOMENT BEFORE THE GHOST OF THE ANCIENT KINGDOM OF THE DANES HAD LOOKED FORTH THROUGH THE VESTURE OF THE HAZEWRAPPED CITY",
"hyp": "A moment before the ghost of the ancient kingdom of the Danes had looked forth through the vesture of the haze-rapped city.",
"ref_norm": "A MOMENT BEFORE THE GHOST OF THE ANCIENT KINGDOM OF THE DANES HAD LOOKED FORTH THROUGH THE VESTURE OF THE HAZEWRAPPED CITY",
"hyp_norm": "A MOMENT BEFORE THE GHOST OF THE ANCIENT KINGDOM OF THE DANES HAD LOOKED FORTH THROUGH THE VESTURE OF THE HAZERAPPED CITY",
"duration_s": 8.005,
"infer_time_s": 2.109,
"rtf": 0.2635,
"wer": 0.0455
},
{
"id": "1188-133604-0000",
"ref": "YOU WILL FIND ME CONTINUALLY SPEAKING OF FOUR MEN TITIAN HOLBEIN TURNER AND TINTORET IN ALMOST THE SAME TERMS",
"hyp": "You will find me continually speaking of four men : Tichen , Holbein, Turner, and Tintoret , in almost the same terms.",
"ref_norm": "YOU WILL FIND ME CONTINUALLY SPEAKING OF FOUR MEN TITIAN HOLBEIN TURNER AND TINTORET IN ALMOST THE SAME TERMS",
"hyp_norm": "YOU WILL FIND ME CONTINUALLY SPEAKING OF FOUR MEN TICHEN HOLBEIN TURNER AND TINTORET IN ALMOST THE SAME TERMS",
"duration_s": 10.725,
"infer_time_s": 2.46,
"rtf": 0.2294,
"wer": 0.0526
},
{
"id": "1188-133604-0001",
"ref": "THEY UNITE EVERY QUALITY AND SOMETIMES YOU WILL FIND ME REFERRING TO THEM AS COLORISTS SOMETIMES AS CHIAROSCURISTS",
"hyp": "They unite every quality. And sometimes you will find me referring to them as colorists , sometimes as chiaroscurs.",
"ref_norm": "THEY UNITE EVERY QUALITY AND SOMETIMES YOU WILL FIND ME REFERRING TO THEM AS COLORISTS SOMETIMES AS CHIAROSCURISTS",
"hyp_norm": "THEY UNITE EVERY QUALITY AND SOMETIMES YOU WILL FIND ME REFERRING TO THEM AS COLORISTS SOMETIMES AS CHIAROSCURS",
"duration_s": 9.04,
"infer_time_s": 2.01,
"rtf": 0.2223,
"wer": 0.0556
},
{
"id": "1188-133604-0002",
"ref": "BY BEING STUDIOUS OF COLOR THEY ARE STUDIOUS OF DIVISION AND WHILE THE CHIAROSCURIST DEVOTES HIMSELF TO THE REPRESENTATION OF DEGREES OF FORCE IN ONE THING UNSEPARATED LIGHT THE COLORISTS HAVE FOR THEIR FUNCTION THE ATTAINMENT OF BEAUTY BY ARRANGEMENT OF THE DIVISIONS OF LIGHT",
"hyp": "By being studious of color, they are studious of division, and while the cure obscurest devotes himself to the representation of degrees of force in one thing , unseparated light, the colorists have for their function, the attainment of beauty by arrangement of the divisions of light.",
"ref_norm": "BY BEING STUDIOUS OF COLOR THEY ARE STUDIOUS OF DIVISION AND WHILE THE CHIAROSCURIST DEVOTES HIMSELF TO THE REPRESENTATION OF DEGREES OF FORCE IN ONE THING UNSEPARATED LIGHT THE COLORISTS HAVE FOR THEIR FUNCTION THE ATTAINMENT OF BEAUTY BY ARRANGEMENT OF THE DIVISIONS OF LIGHT",
"hyp_norm": "BY BEING STUDIOUS OF COLOR THEY ARE STUDIOUS OF DIVISION AND WHILE THE CURE OBSCUREST DEVOTES HIMSELF TO THE REPRESENTATION OF DEGREES OF FORCE IN ONE THING UNSEPARATED LIGHT THE COLORISTS HAVE FOR THEIR FUNCTION THE ATTAINMENT OF BEAUTY BY ARRANGEMENT OF THE DIVISIONS OF LIGHT",
"duration_s": 17.96,
"infer_time_s": 4.572,
"rtf": 0.2546,
"wer": 0.0444
},
{
"id": "1188-133604-0003",
"ref": "MY FIRST AND PRINCIPAL REASON WAS THAT THEY ENFORCED BEYOND ALL RESISTANCE ON ANY STUDENT WHO MIGHT ATTEMPT TO COPY THEM THIS METHOD OF LAYING PORTIONS OF DISTINCT HUE SIDE BY SIDE",
"hyp": "My first and principal reason was that they enforced , beyond all resistance, on any student who might attempt to copy them this method of laying portions of distinct hue side by side.",
"ref_norm": "MY FIRST AND PRINCIPAL REASON WAS THAT THEY ENFORCED BEYOND ALL RESISTANCE ON ANY STUDENT WHO MIGHT ATTEMPT TO COPY THEM THIS METHOD OF LAYING PORTIONS OF DISTINCT HUE SIDE BY SIDE",
"hyp_norm": "MY FIRST AND PRINCIPAL REASON WAS THAT THEY ENFORCED BEYOND ALL RESISTANCE ON ANY STUDENT WHO MIGHT ATTEMPT TO COPY THEM THIS METHOD OF LAYING PORTIONS OF DISTINCT HUE SIDE BY SIDE",
"duration_s": 12.61,
"infer_time_s": 2.934,
"rtf": 0.2327,
"wer": 0.0
},
{
"id": "1188-133604-0004",
"ref": "SOME OF THE TOUCHES INDEED WHEN THE TINT HAS BEEN MIXED WITH MUCH WATER HAVE BEEN LAID IN LITTLE DROPS OR PONDS SO THAT THE PIGMENT MIGHT CRYSTALLIZE HARD AT THE EDGE",
"hyp": "Some of the touches indeed, when the tint has been mixed with much water , have been laid in little drops or ponds, so that the pigment might crystallize hard at the edge.",
"ref_norm": "SOME OF THE TOUCHES INDEED WHEN THE TINT HAS BEEN MIXED WITH MUCH WATER HAVE BEEN LAID IN LITTLE DROPS OR PONDS SO THAT THE PIGMENT MIGHT CRYSTALLIZE HARD AT THE EDGE",
"hyp_norm": "SOME OF THE TOUCHES INDEED WHEN THE TINT HAS BEEN MIXED WITH MUCH WATER HAVE BEEN LAID IN LITTLE DROPS OR PONDS SO THAT THE PIGMENT MIGHT CRYSTALLIZE HARD AT THE EDGE",
"duration_s": 10.65,
"infer_time_s": 2.847,
"rtf": 0.2673,
"wer": 0.0
},
{
"id": "1188-133604-0005",
"ref": "IT IS THE HEAD OF A PARROT WITH A LITTLE FLOWER IN HIS BEAK FROM A PICTURE OF CARPACCIO'S ONE OF HIS SERIES OF THE LIFE OF SAINT GEORGE",
"hyp": "It is the head of a par rot with a little flower in his beak, from a picture of Carpat ius, one of his series of the life of Saint George.",
"ref_norm": "IT IS THE HEAD OF A PARROT WITH A LITTLE FLOWER IN HIS BEAK FROM A PICTURE OF CARPACCIOS ONE OF HIS SERIES OF THE LIFE OF SAINT GEORGE",
"hyp_norm": "IT IS THE HEAD OF A PAR ROT WITH A LITTLE FLOWER IN HIS BEAK FROM A PICTURE OF CARPAT IUS ONE OF HIS SERIES OF THE LIFE OF SAINT GEORGE",
"duration_s": 8.56,
"infer_time_s": 2.625,
"rtf": 0.3066,
"wer": 0.1379
},
{
"id": "1188-133604-0006",
"ref": "THEN HE COMES TO THE BEAK OF IT",
"hyp": "Then he comes to the beak of it.",
"ref_norm": "THEN HE COMES TO THE BEAK OF IT",
"hyp_norm": "THEN HE COMES TO THE BEAK OF IT",
"duration_s": 2.4,
"infer_time_s": 0.816,
"rtf": 0.3402,
"wer": 0.0
},
{
"id": "1188-133604-0007",
"ref": "THE BROWN GROUND BENEATH IS LEFT FOR THE MOST PART ONE TOUCH OF BLACK IS PUT FOR THE HOLLOW TWO DELICATE LINES OF DARK GRAY DEFINE THE OUTER CURVE AND ONE LITTLE QUIVERING TOUCH OF WHITE DRAWS THE INNER EDGE OF THE MANDIBLE",
"hyp": "The brown ground beneath is left for the most part; one touch of black is put for the hollow . Two delicate lines of dark gray define the outer curve , and one little qu ivering touch of white draws the inner edge of the mandible.",
"ref_norm": "THE BROWN GROUND BENEATH IS LEFT FOR THE MOST PART ONE TOUCH OF BLACK IS PUT FOR THE HOLLOW TWO DELICATE LINES OF DARK GRAY DEFINE THE OUTER CURVE AND ONE LITTLE QUIVERING TOUCH OF WHITE DRAWS THE INNER EDGE OF THE MANDIBLE",
"hyp_norm": "THE BROWN GROUND BENEATH IS LEFT FOR THE MOST PART ONE TOUCH OF BLACK IS PUT FOR THE HOLLOW TWO DELICATE LINES OF DARK GRAY DEFINE THE OUTER CURVE AND ONE LITTLE QU IVERING TOUCH OF WHITE DRAWS THE INNER EDGE OF THE MANDIBLE",
"duration_s": 14.24,
"infer_time_s": 3.861,
"rtf": 0.2712,
"wer": 0.0465
},
{
"id": "1188-133604-0008",
"ref": "FOR BELIEVE ME THE FINAL PHILOSOPHY OF ART CAN ONLY RATIFY THEIR OPINION THAT THE BEAUTY OF A COCK ROBIN IS TO BE RED AND OF A GRASS PLOT TO BE GREEN AND THE BEST SKILL OF ART IS IN INSTANTLY SEIZING ON THE MANIFOLD DELICIOUSNESS OF LIGHT WHICH YOU CAN ONLY SEIZE BY PRECISION OF INSTANTANEOUS TOUCH",
"hyp": "For believe me , the final philosophy of art can only ratify their opinion that the beauty of a cock robin is to be red , and of a grass plot to be green , and the best skill of art is in instantly seizing on the manifold deliciousness of light, which you can only seize by precision, of instantaneous touch.",
"ref_norm": "FOR BELIEVE ME THE FINAL PHILOSOPHY OF ART CAN ONLY RATIFY THEIR OPINION THAT THE BEAUTY OF A COCK ROBIN IS TO BE RED AND OF A GRASS PLOT TO BE GREEN AND THE BEST SKILL OF ART IS IN INSTANTLY SEIZING ON THE MANIFOLD DELICIOUSNESS OF LIGHT WHICH YOU CAN ONLY SEIZE BY PRECISION OF INSTANTANEOUS TOUCH",
"hyp_norm": "FOR BELIEVE ME THE FINAL PHILOSOPHY OF ART CAN ONLY RATIFY THEIR OPINION THAT THE BEAUTY OF A COCK ROBIN IS TO BE RED AND OF A GRASS PLOT TO BE GREEN AND THE BEST SKILL OF ART IS IN INSTANTLY SEIZING ON THE MANIFOLD DELICIOUSNESS OF LIGHT WHICH YOU CAN ONLY SEIZE BY PRECISION OF INSTANTANEOUS TOUCH",
"duration_s": 20.755,
"infer_time_s": 5.238,
"rtf": 0.2524,
"wer": 0.0
},
{
"id": "1188-133604-0009",
"ref": "NOW YOU WILL SEE IN THESE STUDIES THAT THE MOMENT THE WHITE IS INCLOSED PROPERLY AND HARMONIZED WITH THE OTHER HUES IT BECOMES SOMEHOW MORE PRECIOUS AND PEARLY THAN THE WHITE PAPER AND THAT I AM NOT AFRAID TO LEAVE A WHOLE FIELD OF UNTREATED WHITE PAPER ALL ROUND IT BEING SURE THAT EVEN THE LITTLE DIAMONDS IN THE ROUND WINDOW WILL TELL AS JEWELS IF THEY ARE GRADATED JUSTLY",
"hyp": "Now you will see in these studies that the moment the white is enclosed properly and harmonized with the other hues , it becomes somehow more precious and pearly than the white paper . And that I am not afraid to leave a whole field of untreated white paper all round it, being sure that even the little diamonds in the round window will tell as jewels if they are gradated justly.",
"ref_norm": "NOW YOU WILL SEE IN THESE STUDIES THAT THE MOMENT THE WHITE IS INCLOSED PROPERLY AND HARMONIZED WITH THE OTHER HUES IT BECOMES SOMEHOW MORE PRECIOUS AND PEARLY THAN THE WHITE PAPER AND THAT I AM NOT AFRAID TO LEAVE A WHOLE FIELD OF UNTREATED WHITE PAPER ALL ROUND IT BEING SURE THAT EVEN THE LITTLE DIAMONDS IN THE ROUND WINDOW WILL TELL AS JEWELS IF THEY ARE GRADATED JUSTLY",
"hyp_norm": "NOW YOU WILL SEE IN THESE STUDIES THAT THE MOMENT THE WHITE IS ENCLOSED PROPERLY AND HARMONIZED WITH THE OTHER HUES IT BECOMES SOMEHOW MORE PRECIOUS AND PEARLY THAN THE WHITE PAPER AND THAT I AM NOT AFRAID TO LEAVE A WHOLE FIELD OF UNTREATED WHITE PAPER ALL ROUND IT BEING SURE THAT EVEN THE LITTLE DIAMONDS IN THE ROUND WINDOW WILL TELL AS JEWELS IF THEY ARE GRADATED JUSTLY",
"duration_s": 23.06,
"infer_time_s": 6.043,
"rtf": 0.262,
"wer": 0.0143
},
{
"id": "1188-133604-0010",
"ref": "BUT IN THIS VIGNETTE COPIED FROM TURNER YOU HAVE THE TWO PRINCIPLES BROUGHT OUT PERFECTLY",
"hyp": "But in this vignette , copied from Turner , you have the two principles brought out perfectly.",
"ref_norm": "BUT IN THIS VIGNETTE COPIED FROM TURNER YOU HAVE THE TWO PRINCIPLES BROUGHT OUT PERFECTLY",
"hyp_norm": "BUT IN THIS VIGNETTE COPIED FROM TURNER YOU HAVE THE TWO PRINCIPLES BROUGHT OUT PERFECTLY",
"duration_s": 6.095,
"infer_time_s": 1.529,
"rtf": 0.2509,
"wer": 0.0
},
{
"id": "1188-133604-0011",
"ref": "THEY ARE BEYOND ALL OTHER WORKS THAT I KNOW EXISTING DEPENDENT FOR THEIR EFFECT ON LOW SUBDUED TONES THEIR FAVORITE CHOICE IN TIME OF DAY BEING EITHER DAWN OR TWILIGHT AND EVEN THEIR BRIGHTEST SUNSETS PRODUCED CHIEFLY OUT OF GRAY PAPER",
"hyp": "They are beyond all other works that I know existing , dependent for their effect on low, subdued tones. Their favorite choice in time of day being either dawn or twilight , and even their brightest sunsets produced chiefly out of gray paper.",
"ref_norm": "THEY ARE BEYOND ALL OTHER WORKS THAT I KNOW EXISTING DEPENDENT FOR THEIR EFFECT ON LOW SUBDUED TONES THEIR FAVORITE CHOICE IN TIME OF DAY BEING EITHER DAWN OR TWILIGHT AND EVEN THEIR BRIGHTEST SUNSETS PRODUCED CHIEFLY OUT OF GRAY PAPER",
"hyp_norm": "THEY ARE BEYOND ALL OTHER WORKS THAT I KNOW EXISTING DEPENDENT FOR THEIR EFFECT ON LOW SUBDUED TONES THEIR FAVORITE CHOICE IN TIME OF DAY BEING EITHER DAWN OR TWILIGHT AND EVEN THEIR BRIGHTEST SUNSETS PRODUCED CHIEFLY OUT OF GRAY PAPER",
"duration_s": 15.19,
"infer_time_s": 3.707,
"rtf": 0.2441,
"wer": 0.0
},
{
"id": "1188-133604-0012",
"ref": "IT MAY BE THAT A GREAT COLORIST WILL USE HIS UTMOST FORCE OF COLOR AS A SINGER HIS FULL POWER OF VOICE BUT LOUD OR LOW THE VIRTUE IS IN BOTH CASES ALWAYS IN REFINEMENT NEVER IN LOUDNESS",
"hyp": "It may be that a great colorist will use his utmost force of color , as a singer his full power of voice , but loud or low, the virtue is in both cases always in refinement, never in loudness.",
"ref_norm": "IT MAY BE THAT A GREAT COLORIST WILL USE HIS UTMOST FORCE OF COLOR AS A SINGER HIS FULL POWER OF VOICE BUT LOUD OR LOW THE VIRTUE IS IN BOTH CASES ALWAYS IN REFINEMENT NEVER IN LOUDNESS",
"hyp_norm": "IT MAY BE THAT A GREAT COLORIST WILL USE HIS UTMOST FORCE OF COLOR AS A SINGER HIS FULL POWER OF VOICE BUT LOUD OR LOW THE VIRTUE IS IN BOTH CASES ALWAYS IN REFINEMENT NEVER IN LOUDNESS",
"duration_s": 14.65,
"infer_time_s": 3.604,
"rtf": 0.246,
"wer": 0.0
},
{
"id": "1188-133604-0013",
"ref": "IT MUST REMEMBER BE ONE OR THE OTHER",
"hyp": "It must remember be one or the other.",
"ref_norm": "IT MUST REMEMBER BE ONE OR THE OTHER",
"hyp_norm": "IT MUST REMEMBER BE ONE OR THE OTHER",
"duration_s": 3.02,
"infer_time_s": 0.729,
"rtf": 0.2414,
"wer": 0.0
},
{
"id": "1188-133604-0014",
"ref": "DO NOT THEREFORE THINK THAT THE GOTHIC SCHOOL IS AN EASY ONE",
"hyp": "Do not therefore think that the Gothic school is an easy one.",
"ref_norm": "DO NOT THEREFORE THINK THAT THE GOTHIC SCHOOL IS AN EASY ONE",
"hyp_norm": "DO NOT THEREFORE THINK THAT THE GOTHIC SCHOOL IS AN EASY ONE",
"duration_s": 4.39,
"infer_time_s": 1.08,
"rtf": 0.2461,
"wer": 0.0
},
{
"id": "1188-133604-0015",
"ref": "THE LAW OF THAT SCHOOL IS THAT EVERYTHING SHALL BE SEEN CLEARLY OR AT LEAST ONLY IN SUCH MIST OR FAINTNESS AS SHALL BE DELIGHTFUL AND I HAVE NO DOUBT THAT THE BEST INTRODUCTION TO IT WOULD BE THE ELEMENTARY PRACTICE OF PAINTING EVERY STUDY ON A GOLDEN GROUND",
"hyp": "The law of that school was that everything shall be seen clearly, or at least , only in such mist or faintness as shall be delightful . And I have no doubt that the best introduction to it would be the elementary practice of painting every study on a golden ground.",
"ref_norm": "THE LAW OF THAT SCHOOL IS THAT EVERYTHING SHALL BE SEEN CLEARLY OR AT LEAST ONLY IN SUCH MIST OR FAINTNESS AS SHALL BE DELIGHTFUL AND I HAVE NO DOUBT THAT THE BEST INTRODUCTION TO IT WOULD BE THE ELEMENTARY PRACTICE OF PAINTING EVERY STUDY ON A GOLDEN GROUND",
"hyp_norm": "THE LAW OF THAT SCHOOL WAS THAT EVERYTHING SHALL BE SEEN CLEARLY OR AT LEAST ONLY IN SUCH MIST OR FAINTNESS AS SHALL BE DELIGHTFUL AND I HAVE NO DOUBT THAT THE BEST INTRODUCTION TO IT WOULD BE THE ELEMENTARY PRACTICE OF PAINTING EVERY STUDY ON A GOLDEN GROUND",
"duration_s": 16.085,
"infer_time_s": 4.236,
"rtf": 0.2633,
"wer": 0.0204
},
{
"id": "1188-133604-0016",
"ref": "THIS AT ONCE COMPELS YOU TO UNDERSTAND THAT THE WORK IS TO BE IMAGINATIVE AND DECORATIVE THAT IT REPRESENTS BEAUTIFUL THINGS IN THE CLEAREST WAY BUT NOT UNDER EXISTING CONDITIONS AND THAT IN FACT YOU ARE PRODUCING JEWELER'S WORK RATHER THAN PICTURES",
"hyp": "This at once comp els you to understand that the work is to be imaginative and decorative, that it represents beautiful things in the clearest way , but not under existing conditions, and that, in fact, you are producing jeweler's work rather than pictures.",
"ref_norm": "THIS AT ONCE COMPELS YOU TO UNDERSTAND THAT THE WORK IS TO BE IMAGINATIVE AND DECORATIVE THAT IT REPRESENTS BEAUTIFUL THINGS IN THE CLEAREST WAY BUT NOT UNDER EXISTING CONDITIONS AND THAT IN FACT YOU ARE PRODUCING JEWELERS WORK RATHER THAN PICTURES",
"hyp_norm": "THIS AT ONCE COMP ELS YOU TO UNDERSTAND THAT THE WORK IS TO BE IMAGINATIVE AND DECORATIVE THAT IT REPRESENTS BEAUTIFUL THINGS IN THE CLEAREST WAY BUT NOT UNDER EXISTING CONDITIONS AND THAT IN FACT YOU ARE PRODUCING JEWELERS WORK RATHER THAN PICTURES",
"duration_s": 16.595,
"infer_time_s": 4.136,
"rtf": 0.2493,
"wer": 0.0476
},
{
"id": "1188-133604-0017",
"ref": "THAT A STYLE IS RESTRAINED OR SEVERE DOES NOT MEAN THAT IT IS ALSO ERRONEOUS",
"hyp": "That a style is restrained or severe does not mean that it is also erroneous.",
"ref_norm": "THAT A STYLE IS RESTRAINED OR SEVERE DOES NOT MEAN THAT IT IS ALSO ERRONEOUS",
"hyp_norm": "THAT A STYLE IS RESTRAINED OR SEVERE DOES NOT MEAN THAT IT IS ALSO ERRONEOUS",
"duration_s": 4.615,
"infer_time_s": 1.227,
"rtf": 0.2659,
"wer": 0.0
},
{
"id": "1188-133604-0018",
"ref": "IN ALL EARLY GOTHIC ART INDEED YOU WILL FIND FAILURE OF THIS KIND ESPECIALLY DISTORTION AND RIGIDITY WHICH ARE IN MANY RESPECTS PAINFULLY TO BE COMPARED WITH THE SPLENDID REPOSE OF CLASSIC ART",
"hyp": "In all early Gothic art, indeed, you will find failure of this kind, especially distortion and rigidity , which are in many respects painfully to be compared with the splendid repose of classic art.",
"ref_norm": "IN ALL EARLY GOTHIC ART INDEED YOU WILL FIND FAILURE OF THIS KIND ESPECIALLY DISTORTION AND RIGIDITY WHICH ARE IN MANY RESPECTS PAINFULLY TO BE COMPARED WITH THE SPLENDID REPOSE OF CLASSIC ART",
"hyp_norm": "IN ALL EARLY GOTHIC ART INDEED YOU WILL FIND FAILURE OF THIS KIND ESPECIALLY DISTORTION AND RIGIDITY WHICH ARE IN MANY RESPECTS PAINFULLY TO BE COMPARED WITH THE SPLENDID REPOSE OF CLASSIC ART",
"duration_s": 11.55,
"infer_time_s": 3.003,
"rtf": 0.26,
"wer": 0.0
},
{
"id": "1188-133604-0019",
"ref": "THE LARGE LETTER CONTAINS INDEED ENTIRELY FEEBLE AND ILL DRAWN FIGURES THAT IS MERELY CHILDISH AND FAILING WORK OF AN INFERIOR HAND IT IS NOT CHARACTERISTIC OF GOTHIC OR ANY OTHER SCHOOL",
"hyp": "The large letter contains indeed entirely feeble and ill-drawn figures. That is merely childish and failing work of an inferior hand. It is not characteristic of Gothic or any other school.",
"ref_norm": "THE LARGE LETTER CONTAINS INDEED ENTIRELY FEEBLE AND ILL DRAWN FIGURES THAT IS MERELY CHILDISH AND FAILING WORK OF AN INFERIOR HAND IT IS NOT CHARACTERISTIC OF GOTHIC OR ANY OTHER SCHOOL",
"hyp_norm": "THE LARGE LETTER CONTAINS INDEED ENTIRELY FEEBLE AND ILLDRAWN FIGURES THAT IS MERELY CHILDISH AND FAILING WORK OF AN INFERIOR HAND IT IS NOT CHARACTERISTIC OF GOTHIC OR ANY OTHER SCHOOL",
"duration_s": 13.93,
"infer_time_s": 3.013,
"rtf": 0.2163,
"wer": 0.0625
},
{
"id": "1188-133604-0020",
"ref": "BUT OBSERVE YOU CAN ONLY DO THIS ON ONE CONDITION THAT OF STRIVING ALSO TO CREATE IN REALITY THE BEAUTY WHICH YOU SEEK IN IMAGINATION",
"hyp": "But observe , you can only do this on one condition , that of striving also to create in reality , the beauty which you seek in imagination.",
"ref_norm": "BUT OBSERVE YOU CAN ONLY DO THIS ON ONE CONDITION THAT OF STRIVING ALSO TO CREATE IN REALITY THE BEAUTY WHICH YOU SEEK IN IMAGINATION",
"hyp_norm": "BUT OBSERVE YOU CAN ONLY DO THIS ON ONE CONDITION THAT OF STRIVING ALSO TO CREATE IN REALITY THE BEAUTY WHICH YOU SEEK IN IMAGINATION",
"duration_s": 10.26,
"infer_time_s": 2.399,
"rtf": 0.2338,
"wer": 0.0
},
{
"id": "1188-133604-0021",
"ref": "IT WILL BE WHOLLY IMPOSSIBLE FOR YOU TO RETAIN THE TRANQUILLITY OF TEMPER AND FELICITY OF FAITH NECESSARY FOR NOBLE PURIST PAINTING UNLESS YOU ARE ACTIVELY ENGAGED IN PROMOTING THE FELICITY AND PEACE OF PRACTICAL LIFE",
"hyp": "It will be wholly impossible for you to retain the tranquility of temper and felicity of faith necessary for noble, purest painting , unless you are actively engaged in promoting the felicity and peace of practical life.",
"ref_norm": "IT WILL BE WHOLLY IMPOSSIBLE FOR YOU TO RETAIN THE TRANQUILLITY OF TEMPER AND FELICITY OF FAITH NECESSARY FOR NOBLE PURIST PAINTING UNLESS YOU ARE ACTIVELY ENGAGED IN PROMOTING THE FELICITY AND PEACE OF PRACTICAL LIFE",
"hyp_norm": "IT WILL BE WHOLLY IMPOSSIBLE FOR YOU TO RETAIN THE TRANQUILITY OF TEMPER AND FELICITY OF FAITH NECESSARY FOR NOBLE PUREST PAINTING UNLESS YOU ARE ACTIVELY ENGAGED IN PROMOTING THE FELICITY AND PEACE OF PRACTICAL LIFE",
"duration_s": 14.02,
"infer_time_s": 3.477,
"rtf": 0.248,
"wer": 0.0556
},
{
"id": "1188-133604-0022",
"ref": "YOU MUST LOOK AT HIM IN THE FACE FIGHT HIM CONQUER HIM WITH WHAT SCATHE YOU MAY YOU NEED NOT THINK TO KEEP OUT OF THE WAY OF HIM",
"hyp": "You must look at him in the face, fight him, conquer him , with what scathe you may. You need not think to keep out of the way of him.",
"ref_norm": "YOU MUST LOOK AT HIM IN THE FACE FIGHT HIM CONQUER HIM WITH WHAT SCATHE YOU MAY YOU NEED NOT THINK TO KEEP OUT OF THE WAY OF HIM",
"hyp_norm": "YOU MUST LOOK AT HIM IN THE FACE FIGHT HIM CONQUER HIM WITH WHAT SCATHE YOU MAY YOU NEED NOT THINK TO KEEP OUT OF THE WAY OF HIM",
"duration_s": 9.63,
"infer_time_s": 2.529,
"rtf": 0.2626,
"wer": 0.0
},
{
"id": "1188-133604-0023",
"ref": "THE COLORIST SAYS FIRST OF ALL AS MY DELICIOUS PAROQUET WAS RUBY SO THIS NASTY VIPER SHALL BE BLACK AND THEN IS THE QUESTION CAN I ROUND HIM OFF EVEN THOUGH HE IS BLACK AND MAKE HIM SLIMY AND YET SPRINGY AND CLOSE DOWN CLOTTED LIKE A POOL OF BLACK BLOOD ON THE EARTH ALL THE SAME",
"hyp": "The colorist says, \"First of all , as my delicious parquet was ruby , so this nasty viper shall be black .\" And then is the question: Can I round him off, even though he is black, and make him slimy , and yet springy and close down, clotted like a pool of black blood on the earth, all the same?",
"ref_norm": "THE COLORIST SAYS FIRST OF ALL AS MY DELICIOUS PAROQUET WAS RUBY SO THIS NASTY VIPER SHALL BE BLACK AND THEN IS THE QUESTION CAN I ROUND HIM OFF EVEN THOUGH HE IS BLACK AND MAKE HIM SLIMY AND YET SPRINGY AND CLOSE DOWN CLOTTED LIKE A POOL OF BLACK BLOOD ON THE EARTH ALL THE SAME",
"hyp_norm": "THE COLORIST SAYS FIRST OF ALL AS MY DELICIOUS PARQUET WAS RUBY SO THIS NASTY VIPER SHALL BE BLACK AND THEN IS THE QUESTION CAN I ROUND HIM OFF EVEN THOUGH HE IS BLACK AND MAKE HIM SLIMY AND YET SPRINGY AND CLOSE DOWN CLOTTED LIKE A POOL OF BLACK BLOOD ON THE EARTH ALL THE SAME",
"duration_s": 23.67,
"infer_time_s": 5.734,
"rtf": 0.2422,
"wer": 0.0175
},
{
"id": "1188-133604-0024",
"ref": "NOTHING WILL BE MORE PRECIOUS TO YOU I THINK IN THE PRACTICAL STUDY OF ART THAN THE CONVICTION WHICH WILL FORCE ITSELF ON YOU MORE AND MORE EVERY HOUR OF THE WAY ALL THINGS ARE BOUND TOGETHER LITTLE AND GREAT IN SPIRIT AND IN MATTER",
"hyp": "Nothing will be more precious to you. I think, in the practical study of art, than the conviction , which will force itself on you more and more every hour , of the way all things are bound together, little and great, in spirit and in matter.",
"ref_norm": "NOTHING WILL BE MORE PRECIOUS TO YOU I THINK IN THE PRACTICAL STUDY OF ART THAN THE CONVICTION WHICH WILL FORCE ITSELF ON YOU MORE AND MORE EVERY HOUR OF THE WAY ALL THINGS ARE BOUND TOGETHER LITTLE AND GREAT IN SPIRIT AND IN MATTER",
"hyp_norm": "NOTHING WILL BE MORE PRECIOUS TO YOU I THINK IN THE PRACTICAL STUDY OF ART THAN THE CONVICTION WHICH WILL FORCE ITSELF ON YOU MORE AND MORE EVERY HOUR OF THE WAY ALL THINGS ARE BOUND TOGETHER LITTLE AND GREAT IN SPIRIT AND IN MATTER",
"duration_s": 15.24,
"infer_time_s": 4.0,
"rtf": 0.2625,
"wer": 0.0
},
{
"id": "1188-133604-0025",
"ref": "YOU KNOW I HAVE JUST BEEN TELLING YOU HOW THIS SCHOOL OF MATERIALISM AND CLAY INVOLVED ITSELF AT LAST IN CLOUD AND FIRE",
"hyp": "You know I've just been telling you how this school of materialism in clay involved itself at last in cloud and fire.",
"ref_norm": "YOU KNOW I HAVE JUST BEEN TELLING YOU HOW THIS SCHOOL OF MATERIALISM AND CLAY INVOLVED ITSELF AT LAST IN CLOUD AND FIRE",
"hyp_norm": "YOU KNOW IVE JUST BEEN TELLING YOU HOW THIS SCHOOL OF MATERIALISM IN CLAY INVOLVED ITSELF AT LAST IN CLOUD AND FIRE",
"duration_s": 7.45,
"infer_time_s": 1.857,
"rtf": 0.2493,
"wer": 0.1304
},
{
"id": "1188-133604-0026",
"ref": "HERE IS AN EQUALLY TYPICAL GREEK SCHOOL LANDSCAPE BY WILSON LOST WHOLLY IN GOLDEN MIST THE TREES SO SLIGHTLY DRAWN THAT YOU DON'T KNOW IF THEY ARE TREES OR TOWERS AND NO CARE FOR COLOR WHATEVER PERFECTLY DECEPTIVE AND MARVELOUS EFFECT OF SUNSHINE THROUGH THE MIST APOLLO AND THE PYTHON",
"hyp": "Here is an equally typical Greek school landscape by Wilson, lost wholly in golden mist . The trees so slightly drawn that you don't know if they are trees or towers , and no care for color whatsoever. Perfectly deceptive in marvelous effect of sunshine through the mist, Apollo and the Python.",
"ref_norm": "HERE IS AN EQUALLY TYPICAL GREEK SCHOOL LANDSCAPE BY WILSON LOST WHOLLY IN GOLDEN MIST THE TREES SO SLIGHTLY DRAWN THAT YOU DONT KNOW IF THEY ARE TREES OR TOWERS AND NO CARE FOR COLOR WHATEVER PERFECTLY DECEPTIVE AND MARVELOUS EFFECT OF SUNSHINE THROUGH THE MIST APOLLO AND THE PYTHON",
"hyp_norm": "HERE IS AN EQUALLY TYPICAL GREEK SCHOOL LANDSCAPE BY WILSON LOST WHOLLY IN GOLDEN MIST THE TREES SO SLIGHTLY DRAWN THAT YOU DONT KNOW IF THEY ARE TREES OR TOWERS AND NO CARE FOR COLOR WHATSOEVER PERFECTLY DECEPTIVE IN MARVELOUS EFFECT OF SUNSHINE THROUGH THE MIST APOLLO AND THE PYTHON",
"duration_s": 20.125,
"infer_time_s": 4.804,
"rtf": 0.2387,
"wer": 0.04
},
{
"id": "1188-133604-0027",
"ref": "NOW HERE IS RAPHAEL EXACTLY BETWEEN THE TWO TREES STILL DRAWN LEAF BY LEAF WHOLLY FORMAL BUT BEAUTIFUL MIST COMING GRADUALLY INTO THE DISTANCE",
"hyp": "Now here is Raphael , exactly between the two trees, still drawn leaf by leaf, wholly formal , but beautiful mist coming gradually into the distance.",
"ref_norm": "NOW HERE IS RAPHAEL EXACTLY BETWEEN THE TWO TREES STILL DRAWN LEAF BY LEAF WHOLLY FORMAL BUT BEAUTIFUL MIST COMING GRADUALLY INTO THE DISTANCE",
"hyp_norm": "NOW HERE IS RAPHAEL EXACTLY BETWEEN THE TWO TREES STILL DRAWN LEAF BY LEAF WHOLLY FORMAL BUT BEAUTIFUL MIST COMING GRADUALLY INTO THE DISTANCE",
"duration_s": 11.245,
"infer_time_s": 2.42,
"rtf": 0.2152,
"wer": 0.0
},
{
"id": "1188-133604-0028",
"ref": "WELL THEN LAST HERE IS TURNER'S GREEK SCHOOL OF THE HIGHEST CLASS AND YOU DEFINE HIS ART ABSOLUTELY AS FIRST THE DISPLAYING INTENSELY AND WITH THE STERNEST INTELLECT OF NATURAL FORM AS IT IS AND THEN THE ENVELOPMENT OF IT WITH CLOUD AND FIRE",
"hyp": "Well then, last here is Turner's , Greek school of the highest class, and you define his art absolutely, as first the displaying intensely and with the sternest intellect, of natural form as it is, and then the envelopment of it with cloud and fire.",
"ref_norm": "WELL THEN LAST HERE IS TURNERS GREEK SCHOOL OF THE HIGHEST CLASS AND YOU DEFINE HIS ART ABSOLUTELY AS FIRST THE DISPLAYING INTENSELY AND WITH THE STERNEST INTELLECT OF NATURAL FORM AS IT IS AND THEN THE ENVELOPMENT OF IT WITH CLOUD AND FIRE",
"hyp_norm": "WELL THEN LAST HERE IS TURNERS GREEK SCHOOL OF THE HIGHEST CLASS AND YOU DEFINE HIS ART ABSOLUTELY AS FIRST THE DISPLAYING INTENSELY AND WITH THE STERNEST INTELLECT OF NATURAL FORM AS IT IS AND THEN THE ENVELOPMENT OF IT WITH CLOUD AND FIRE",
"duration_s": 19.005,
"infer_time_s": 4.41,
"rtf": 0.2321,
"wer": 0.0
},
{
"id": "1188-133604-0029",
"ref": "ONLY THERE ARE TWO SORTS OF CLOUD AND FIRE",
"hyp": "Only, there are two sorts of cloud and fire.",
"ref_norm": "ONLY THERE ARE TWO SORTS OF CLOUD AND FIRE",
"hyp_norm": "ONLY THERE ARE TWO SORTS OF CLOUD AND FIRE",
"duration_s": 3.705,
"infer_time_s": 0.846,
"rtf": 0.2285,
"wer": 0.0
},
{
"id": "1188-133604-0030",
"ref": "HE KNOWS THEM BOTH",
"hyp": "He knows them both.",
"ref_norm": "HE KNOWS THEM BOTH",
"hyp_norm": "HE KNOWS THEM BOTH",
"duration_s": 1.915,
"infer_time_s": 0.4,
"rtf": 0.2091,
"wer": 0.0
},
{
"id": "1188-133604-0031",
"ref": "THERE'S ONE AND THERE'S ANOTHER THE DUDLEY AND THE FLINT",
"hyp": "There's one and there's another , the Dudley and the Flint.",
"ref_norm": "THERES ONE AND THERES ANOTHER THE DUDLEY AND THE FLINT",
"hyp_norm": "THERES ONE AND THERES ANOTHER THE DUDLEY AND THE FLINT",
"duration_s": 4.25,
"infer_time_s": 1.122,
"rtf": 0.264,
"wer": 0.0
},
{
"id": "1188-133604-0032",
"ref": "IT IS ONLY A PENCIL OUTLINE BY EDWARD BURNE JONES IN ILLUSTRATION OF THE STORY OF PSYCHE IT IS THE INTRODUCTION OF PSYCHE AFTER ALL HER TROUBLES INTO HEAVEN",
"hyp": "It is only a pencil outline by Edward Burn Jones, in illustration of the story of Psyche . It is the introduction of Psyche after all her troubles into heaven.",
"ref_norm": "IT IS ONLY A PENCIL OUTLINE BY EDWARD BURNE JONES IN ILLUSTRATION OF THE STORY OF PSYCHE IT IS THE INTRODUCTION OF PSYCHE AFTER ALL HER TROUBLES INTO HEAVEN",
"hyp_norm": "IT IS ONLY A PENCIL OUTLINE BY EDWARD BURN JONES IN ILLUSTRATION OF THE STORY OF PSYCHE IT IS THE INTRODUCTION OF PSYCHE AFTER ALL HER TROUBLES INTO HEAVEN",
"duration_s": 10.985,
"infer_time_s": 2.68,
"rtf": 0.244,
"wer": 0.0345
},
{
"id": "1188-133604-0033",
"ref": "EVERY PLANT IN THE GRASS IS SET FORMALLY GROWS PERFECTLY AND MAY BE REALIZED COMPLETELY",
"hyp": "Every plant in the grass is set formally, grows perfectly, and may be realized completely.",
"ref_norm": "EVERY PLANT IN THE GRASS IS SET FORMALLY GROWS PERFECTLY AND MAY BE REALIZED COMPLETELY",
"hyp_norm": "EVERY PLANT IN THE GRASS IS SET FORMALLY GROWS PERFECTLY AND MAY BE REALIZED COMPLETELY",
"duration_s": 6.625,
"infer_time_s": 1.47,
"rtf": 0.2218,
"wer": 0.0
},
{
"id": "1188-133604-0034",
"ref": "EXQUISITE ORDER AND UNIVERSAL WITH ETERNAL LIFE AND LIGHT THIS IS THE FAITH AND EFFORT OF THE SCHOOLS OF CRYSTAL AND YOU MAY DESCRIBE AND COMPLETE THEIR WORK QUITE LITERALLY BY TAKING ANY VERSES OF CHAUCER IN HIS TENDER MOOD AND OBSERVING HOW HE INSISTS ON THE CLEARNESS AND BRIGHTNESS FIRST AND THEN ON THE ORDER",
"hyp": "Exquisite order and universal, with eternal life and light, this is the faith and effort of the schools of crystal . And you may describe and complete their work quite literally, by taking any verses of Chaucer in his tender mood, and observing how he insists on the clearness and brightness first, and then on the order.",
"ref_norm": "EXQUISITE ORDER AND UNIVERSAL WITH ETERNAL LIFE AND LIGHT THIS IS THE FAITH AND EFFORT OF THE SCHOOLS OF CRYSTAL AND YOU MAY DESCRIBE AND COMPLETE THEIR WORK QUITE LITERALLY BY TAKING ANY VERSES OF CHAUCER IN HIS TENDER MOOD AND OBSERVING HOW HE INSISTS ON THE CLEARNESS AND BRIGHTNESS FIRST AND THEN ON THE ORDER",
"hyp_norm": "EXQUISITE ORDER AND UNIVERSAL WITH ETERNAL LIFE AND LIGHT THIS IS THE FAITH AND EFFORT OF THE SCHOOLS OF CRYSTAL AND YOU MAY DESCRIBE AND COMPLETE THEIR WORK QUITE LITERALLY BY TAKING ANY VERSES OF CHAUCER IN HIS TENDER MOOD AND OBSERVING HOW HE INSISTS ON THE CLEARNESS AND BRIGHTNESS FIRST AND THEN ON THE ORDER",
"duration_s": 20.905,
"infer_time_s": 5.27,
"rtf": 0.2521,
"wer": 0.0
},
{
"id": "1188-133604-0035",
"ref": "THUS IN CHAUCER'S DREAM",
"hyp": "Thus, in Ch aucer's dream.",
"ref_norm": "THUS IN CHAUCERS DREAM",
"hyp_norm": "THUS IN CH AUCERS DREAM",
"duration_s": 2.925,
"infer_time_s": 0.727,
"rtf": 0.2485,
"wer": 0.5
},
{
"id": "1188-133604-0036",
"ref": "IN BOTH THESE HIGH MYTHICAL SUBJECTS THE SURROUNDING NATURE THOUGH SUFFERING IS STILL DIGNIFIED AND BEAUTIFUL",
"hyp": "In both these high mythical subjects , the surrounding nature , though suffering, is still dignified and beautiful.",
"ref_norm": "IN BOTH THESE HIGH MYTHICAL SUBJECTS THE SURROUNDING NATURE THOUGH SUFFERING IS STILL DIGNIFIED AND BEAUTIFUL",
"hyp_norm": "IN BOTH THESE HIGH MYTHICAL SUBJECTS THE SURROUNDING NATURE THOUGH SUFFERING IS STILL DIGNIFIED AND BEAUTIFUL",
"duration_s": 7.97,
"infer_time_s": 1.653,
"rtf": 0.2074,
"wer": 0.0
},
{
"id": "1188-133604-0037",
"ref": "EVERY LINE IN WHICH THE MASTER TRACES IT EVEN WHERE SEEMINGLY NEGLIGENT IS LOVELY AND SET DOWN WITH A MEDITATIVE CALMNESS WHICH MAKES THESE TWO ETCHINGS CAPABLE OF BEING PLACED BESIDE THE MOST TRANQUIL WORK OF HOLBEIN OR DUERER",
"hyp": "Every line in which the master traces it , even where seemingly negligent, is lovely and set down with a meditative calmness, which makes these two etchings capable of being placed beside the most tranquil work of Holbein or D\u00fcrer.",
"ref_norm": "EVERY LINE IN WHICH THE MASTER TRACES IT EVEN WHERE SEEMINGLY NEGLIGENT IS LOVELY AND SET DOWN WITH A MEDITATIVE CALMNESS WHICH MAKES THESE TWO ETCHINGS CAPABLE OF BEING PLACED BESIDE THE MOST TRANQUIL WORK OF HOLBEIN OR DUERER",
"hyp_norm": "EVERY LINE IN WHICH THE MASTER TRACES IT EVEN WHERE SEEMINGLY NEGLIGENT IS LOVELY AND SET DOWN WITH A MEDITATIVE CALMNESS WHICH MAKES THESE TWO ETCHINGS CAPABLE OF BEING PLACED BESIDE THE MOST TRANQUIL WORK OF HOLBEIN OR D\u00dcRER",
"duration_s": 14.51,
"infer_time_s": 3.909,
"rtf": 0.2694,
"wer": 0.0256
},
{
"id": "1188-133604-0038",
"ref": "BUT NOW HERE IS A SUBJECT OF WHICH YOU WILL WONDER AT FIRST WHY TURNER DREW IT AT ALL",
"hyp": "But now here is a subject of which, you will wonder at first why Turner drew it at all.",
"ref_norm": "BUT NOW HERE IS A SUBJECT OF WHICH YOU WILL WONDER AT FIRST WHY TURNER DREW IT AT ALL",
"hyp_norm": "BUT NOW HERE IS A SUBJECT OF WHICH YOU WILL WONDER AT FIRST WHY TURNER DREW IT AT ALL",
"duration_s": 5.365,
"infer_time_s": 1.483,
"rtf": 0.2765,
"wer": 0.0
},
{
"id": "1188-133604-0039",
"ref": "IT HAS NO BEAUTY WHATSOEVER NO SPECIALTY OF PICTURESQUENESS AND ALL ITS LINES ARE CRAMPED AND POOR",
"hyp": "It has no beauty whatsoever . No specialty of picturesque ness, and all its lines are cramped and poor.",
"ref_norm": "IT HAS NO BEAUTY WHATSOEVER NO SPECIALTY OF PICTURESQUENESS AND ALL ITS LINES ARE CRAMPED AND POOR",
"hyp_norm": "IT HAS NO BEAUTY WHATSOEVER NO SPECIALTY OF PICTURESQUE NESS AND ALL ITS LINES ARE CRAMPED AND POOR",
"duration_s": 6.625,
"infer_time_s": 1.627,
"rtf": 0.2456,
"wer": 0.1176
},
{
"id": "1188-133604-0040",
"ref": "THE CRAMPNESS AND THE POVERTY ARE ALL INTENDED",
"hyp": "The crampedness and the poverty are all intended.",
"ref_norm": "THE CRAMPNESS AND THE POVERTY ARE ALL INTENDED",
"hyp_norm": "THE CRAMPEDNESS AND THE POVERTY ARE ALL INTENDED",
"duration_s": 3.23,
"infer_time_s": 0.784,
"rtf": 0.2428,
"wer": 0.125
},
{
"id": "1188-133604-0041",
"ref": "IT IS A GLEANER BRINGING DOWN HER ONE SHEAF OF CORN TO AN OLD WATERMILL ITSELF MOSSY AND RENT SCARCELY ABLE TO GET ITS STONES TO TURN",
"hyp": "It is a gleaner bringing down her one sheaf of corn to an old water mill, itself moss y and rent, scarcely able to get its stones to turn.",
"ref_norm": "IT IS A GLEANER BRINGING DOWN HER ONE SHEAF OF CORN TO AN OLD WATERMILL ITSELF MOSSY AND RENT SCARCELY ABLE TO GET ITS STONES TO TURN",
"hyp_norm": "IT IS A GLEANER BRINGING DOWN HER ONE SHEAF OF CORN TO AN OLD WATER MILL ITSELF MOSS Y AND RENT SCARCELY ABLE TO GET ITS STONES TO TURN",
"duration_s": 10.07,
"infer_time_s": 2.69,
"rtf": 0.2671,
"wer": 0.1481
},
{
"id": "1188-133604-0042",
"ref": "THE SCENE IS ABSOLUTELY ARCADIAN",
"hyp": "The scene is absolutely Arcadian.",
"ref_norm": "THE SCENE IS ABSOLUTELY ARCADIAN",
"hyp_norm": "THE SCENE IS ABSOLUTELY ARCADIAN",
"duration_s": 2.66,
"infer_time_s": 0.635,
"rtf": 0.2388,
"wer": 0.0
},
{
"id": "1188-133604-0043",
"ref": "SEE THAT YOUR LIVES BE IN NOTHING WORSE THAN A BOY'S CLIMBING FOR HIS ENTANGLED KITE",
"hyp": "See that your lives be in nothing worse than a boy's climbing for his entangled kite.",
"ref_norm": "SEE THAT YOUR LIVES BE IN NOTHING WORSE THAN A BOYS CLIMBING FOR HIS ENTANGLED KITE",
"hyp_norm": "SEE THAT YOUR LIVES BE IN NOTHING WORSE THAN A BOYS CLIMBING FOR HIS ENTANGLED KITE",
"duration_s": 4.885,
"infer_time_s": 1.392,
"rtf": 0.285,
"wer": 0.0
},
{
"id": "1188-133604-0044",
"ref": "IT WILL BE WELL FOR YOU IF YOU JOIN NOT WITH THOSE WHO INSTEAD OF KITES FLY FALCONS WHO INSTEAD OF OBEYING THE LAST WORDS OF THE GREAT CLOUD SHEPHERD TO FEED HIS SHEEP LIVE THE LIVES HOW MUCH LESS THAN VANITY OF THE WAR WOLF AND THE GIER EAGLE",
"hyp": "It will be well for you , if you join not with those who, instead of kites, fly falcons, who instead of obeying the last words of the great cloud shepherd , to feed his sheep, live the lives . How much less than vanity. Of the warwolf and the gear eagle.",
"ref_norm": "IT WILL BE WELL FOR YOU IF YOU JOIN NOT WITH THOSE WHO INSTEAD OF KITES FLY FALCONS WHO INSTEAD OF OBEYING THE LAST WORDS OF THE GREAT CLOUD SHEPHERD TO FEED HIS SHEEP LIVE THE LIVES HOW MUCH LESS THAN VANITY OF THE WAR WOLF AND THE GIER EAGLE",
"hyp_norm": "IT WILL BE WELL FOR YOU IF YOU JOIN NOT WITH THOSE WHO INSTEAD OF KITES FLY FALCONS WHO INSTEAD OF OBEYING THE LAST WORDS OF THE GREAT CLOUD SHEPHERD TO FEED HIS SHEEP LIVE THE LIVES HOW MUCH LESS THAN VANITY OF THE WARWOLF AND THE GEAR EAGLE",
"duration_s": 18.545,
"infer_time_s": 4.808,
"rtf": 0.2593,
"wer": 0.06
},
{
"id": "121-121726-0000",
"ref": "ALSO A POPULAR CONTRIVANCE WHEREBY LOVE MAKING MAY BE SUSPENDED BUT NOT STOPPED DURING THE PICNIC SEASON",
"hyp": "Also, a popular contrivance whereby love-making may be suspended but not stopped during the picnic season.",
"ref_norm": "ALSO A POPULAR CONTRIVANCE WHEREBY LOVE MAKING MAY BE SUSPENDED BUT NOT STOPPED DURING THE PICNIC SEASON",
"hyp_norm": "ALSO A POPULAR CONTRIVANCE WHEREBY LOVEMAKING MAY BE SUSPENDED BUT NOT STOPPED DURING THE PICNIC SEASON",
"duration_s": 8.46,
"infer_time_s": 1.818,
"rtf": 0.2149,
"wer": 0.1176
},
{
"id": "121-121726-0001",
"ref": "HARANGUE THE TIRESOME PRODUCT OF A TIRELESS TONGUE",
"hyp": "Haring . The tiresome product of a tireless tongue.",
"ref_norm": "HARANGUE THE TIRESOME PRODUCT OF A TIRELESS TONGUE",
"hyp_norm": "HARING THE TIRESOME PRODUCT OF A TIRELESS TONGUE",
"duration_s": 5.925,
"infer_time_s": 1.083,
"rtf": 0.1828,
"wer": 0.125
},
{
"id": "121-121726-0002",
"ref": "ANGOR PAIN PAINFUL TO HEAR",
"hyp": "Anger, pain. Painful to hear.",
"ref_norm": "ANGOR PAIN PAINFUL TO HEAR",
"hyp_norm": "ANGER PAIN PAINFUL TO HEAR",
"duration_s": 4.41,
"infer_time_s": 0.862,
"rtf": 0.1955,
"wer": 0.2
},
{
"id": "121-121726-0003",
"ref": "HAY FEVER A HEART TROUBLE CAUSED BY FALLING IN LOVE WITH A GRASS WIDOW",
"hyp": "Hey, fever . A heart trouble caused by falling in love with a grass widow.",
"ref_norm": "HAY FEVER A HEART TROUBLE CAUSED BY FALLING IN LOVE WITH A GRASS WIDOW",
"hyp_norm": "HEY FEVER A HEART TROUBLE CAUSED BY FALLING IN LOVE WITH A GRASS WIDOW",
"duration_s": 6.755,
"infer_time_s": 1.419,
"rtf": 0.2101,
"wer": 0.0714
},
{
"id": "121-121726-0004",
"ref": "HEAVEN A GOOD PLACE TO BE RAISED TO",
"hyp": "Heaven, a good place to be raised too.",
"ref_norm": "HEAVEN A GOOD PLACE TO BE RAISED TO",
"hyp_norm": "HEAVEN A GOOD PLACE TO BE RAISED TOO",
"duration_s": 4.02,
"infer_time_s": 0.963,
"rtf": 0.2395,
"wer": 0.125
},
{
"id": "121-121726-0005",
"ref": "HEDGE A FENCE",
"hyp": "Hedge. A fence.",
"ref_norm": "HEDGE A FENCE",
"hyp_norm": "HEDGE A FENCE",
"duration_s": 3.1,
"infer_time_s": 0.528,
"rtf": 0.1703,
"wer": 0.0
},
{
"id": "121-121726-0006",
"ref": "HEREDITY THE CAUSE OF ALL OUR FAULTS",
"hyp": "Heredity. The cause of all our faults.",
"ref_norm": "HEREDITY THE CAUSE OF ALL OUR FAULTS",
"hyp_norm": "HEREDITY THE CAUSE OF ALL OUR FAULTS",
"duration_s": 3.895,
"infer_time_s": 0.784,
"rtf": 0.2013,
"wer": 0.0
},
{
"id": "121-121726-0007",
"ref": "HORSE SENSE A DEGREE OF WISDOM THAT KEEPS ONE FROM BETTING ON THE RACES",
"hyp": "Horse sense , a degree of wisdom that keeps one from betting on the races.",
"ref_norm": "HORSE SENSE A DEGREE OF WISDOM THAT KEEPS ONE FROM BETTING ON THE RACES",
"hyp_norm": "HORSE SENSE A DEGREE OF WISDOM THAT KEEPS ONE FROM BETTING ON THE RACES",
"duration_s": 6.73,
"infer_time_s": 1.417,
"rtf": 0.2105,
"wer": 0.0
},
{
"id": "121-121726-0008",
"ref": "HOSE MAN'S EXCUSE FOR WETTING THE WALK",
"hyp": "Hose. Man's excuse for wetting the walk.",
"ref_norm": "HOSE MANS EXCUSE FOR WETTING THE WALK",
"hyp_norm": "HOSE MANS EXCUSE FOR WETTING THE WALK",
"duration_s": 4.99,
"infer_time_s": 0.968,
"rtf": 0.194,
"wer": 0.0
},
{
"id": "121-121726-0009",
"ref": "HOTEL A PLACE WHERE A GUEST OFTEN GIVES UP GOOD DOLLARS FOR POOR QUARTERS",
"hyp": "Hotel. A place where a guest often gives up good dollars for poor quarters.",
"ref_norm": "HOTEL A PLACE WHERE A GUEST OFTEN GIVES UP GOOD DOLLARS FOR POOR QUARTERS",
"hyp_norm": "HOTEL A PLACE WHERE A GUEST OFTEN GIVES UP GOOD DOLLARS FOR POOR QUARTERS",
"duration_s": 7.26,
"infer_time_s": 1.332,
"rtf": 0.1834,
"wer": 0.0
},
{
"id": "121-121726-0010",
"ref": "HOUSECLEANING A DOMESTIC UPHEAVAL THAT MAKES IT EASY FOR THE GOVERNMENT TO ENLIST ALL THE SOLDIERS IT NEEDS",
"hyp": "House cleaning . A domestic upheaval that makes it easy for the government to enlist all the soldiers it needs.",
"ref_norm": "HOUSECLEANING A DOMESTIC UPHEAVAL THAT MAKES IT EASY FOR THE GOVERNMENT TO ENLIST ALL THE SOLDIERS IT NEEDS",
"hyp_norm": "HOUSE CLEANING A DOMESTIC UPHEAVAL THAT MAKES IT EASY FOR THE GOVERNMENT TO ENLIST ALL THE SOLDIERS IT NEEDS",
"duration_s": 9.81,
"infer_time_s": 1.883,
"rtf": 0.192,
"wer": 0.1111
},
{
"id": "121-121726-0011",
"ref": "HUSBAND THE NEXT THING TO A WIFE",
"hyp": "Husband. The next thing to a wife.",
"ref_norm": "HUSBAND THE NEXT THING TO A WIFE",
"hyp_norm": "HUSBAND THE NEXT THING TO A WIFE",
"duration_s": 4.035,
"infer_time_s": 0.875,
"rtf": 0.2168,
"wer": 0.0
},
{
"id": "121-121726-0012",
"ref": "HUSSY WOMAN AND BOND TIE",
"hyp": "Hussy woman and bond , tie.",
"ref_norm": "HUSSY WOMAN AND BOND TIE",
"hyp_norm": "HUSSY WOMAN AND BOND TIE",
"duration_s": 4.045,
"infer_time_s": 0.821,
"rtf": 0.2029,
"wer": 0.0
},
{
"id": "121-121726-0013",
"ref": "TIED TO A WOMAN",
"hyp": "Tied to a woman.",
"ref_norm": "TIED TO A WOMAN",
"hyp_norm": "TIED TO A WOMAN",
"duration_s": 2.49,
"infer_time_s": 0.578,
"rtf": 0.2321,
"wer": 0.0
},
{
"id": "121-121726-0014",
"ref": "HYPOCRITE A HORSE DEALER",
"hyp": "Hypocrite. A horse dealer.",
"ref_norm": "HYPOCRITE A HORSE DEALER",
"hyp_norm": "HYPOCRITE A HORSE DEALER",
"duration_s": 3.165,
"infer_time_s": 0.677,
"rtf": 0.2138,
"wer": 0.0
},
{
"id": "121-123852-0000",
"ref": "THOSE PRETTY WRONGS THAT LIBERTY COMMITS WHEN I AM SOMETIME ABSENT FROM THY HEART THY BEAUTY AND THY YEARS FULL WELL BEFITS FOR STILL TEMPTATION FOLLOWS WHERE THOU ART",
"hyp": "Those pretty wrongs that liberty commits. When I am some time absent from thy heart , thy beauty and thy years fall well be fits, for still temptation follows where thou art.",
"ref_norm": "THOSE PRETTY WRONGS THAT LIBERTY COMMITS WHEN I AM SOMETIME ABSENT FROM THY HEART THY BEAUTY AND THY YEARS FULL WELL BEFITS FOR STILL TEMPTATION FOLLOWS WHERE THOU ART",
"hyp_norm": "THOSE PRETTY WRONGS THAT LIBERTY COMMITS WHEN I AM SOME TIME ABSENT FROM THY HEART THY BEAUTY AND THY YEARS FALL WELL BE FITS FOR STILL TEMPTATION FOLLOWS WHERE THOU ART",
"duration_s": 17.695,
"infer_time_s": 3.303,
"rtf": 0.1867,
"wer": 0.1724
},
{
"id": "121-123852-0001",
"ref": "AY ME",
"hyp": "I me.",
"ref_norm": "AY ME",
"hyp_norm": "I ME",
"duration_s": 1.87,
"infer_time_s": 0.297,
"rtf": 0.1586,
"wer": 0.5
},
{
"id": "121-123852-0002",
"ref": "NO MATTER THEN ALTHOUGH MY FOOT DID STAND UPON THE FARTHEST EARTH REMOV'D FROM THEE FOR NIMBLE THOUGHT CAN JUMP BOTH SEA AND LAND AS SOON AS THINK THE PLACE WHERE HE WOULD BE BUT AH",
"hyp": "No matter then , although my foot did stand upon the farthest earth , removed from thee , for nimble thought can jump both sea and land , as soon as think the place where he would be. But ah.",
"ref_norm": "NO MATTER THEN ALTHOUGH MY FOOT DID STAND UPON THE FARTHEST EARTH REMOVD FROM THEE FOR NIMBLE THOUGHT CAN JUMP BOTH SEA AND LAND AS SOON AS THINK THE PLACE WHERE HE WOULD BE BUT AH",
"hyp_norm": "NO MATTER THEN ALTHOUGH MY FOOT DID STAND UPON THE FARTHEST EARTH REMOVED FROM THEE FOR NIMBLE THOUGHT CAN JUMP BOTH SEA AND LAND AS SOON AS THINK THE PLACE WHERE HE WOULD BE BUT AH",
"duration_s": 17.285,
"infer_time_s": 3.724,
"rtf": 0.2155,
"wer": 0.0278
},
{
"id": "121-123852-0003",
"ref": "THOUGHT KILLS ME THAT I AM NOT THOUGHT TO LEAP LARGE LENGTHS OF MILES WHEN THOU ART GONE BUT THAT SO MUCH OF EARTH AND WATER WROUGHT I MUST ATTEND TIME'S LEISURE WITH MY MOAN RECEIVING NOUGHT BY ELEMENTS SO SLOW BUT HEAVY TEARS BADGES OF EITHER'S WOE",
"hyp": "Thought kills me that I am not thought, to leap large lengths of miles when thou art gone, but that so much of earth and water rot, I must attend, time's leisure with my moan , receiving not , by elements so slow, but heavy tears, badges of either's woe.",
"ref_norm": "THOUGHT KILLS ME THAT I AM NOT THOUGHT TO LEAP LARGE LENGTHS OF MILES WHEN THOU ART GONE BUT THAT SO MUCH OF EARTH AND WATER WROUGHT I MUST ATTEND TIMES LEISURE WITH MY MOAN RECEIVING NOUGHT BY ELEMENTS SO SLOW BUT HEAVY TEARS BADGES OF EITHERS WOE",
"hyp_norm": "THOUGHT KILLS ME THAT I AM NOT THOUGHT TO LEAP LARGE LENGTHS OF MILES WHEN THOU ART GONE BUT THAT SO MUCH OF EARTH AND WATER ROT I MUST ATTEND TIMES LEISURE WITH MY MOAN RECEIVING NOT BY ELEMENTS SO SLOW BUT HEAVY TEARS BADGES OF EITHERS WOE",
"duration_s": 23.505,
"infer_time_s": 5.126,
"rtf": 0.2181,
"wer": 0.0417
},
{
"id": "121-123852-0004",
"ref": "MY HEART DOTH PLEAD THAT THOU IN HIM DOST LIE A CLOSET NEVER PIERC'D WITH CRYSTAL EYES BUT THE DEFENDANT DOTH THAT PLEA DENY AND SAYS IN HIM THY FAIR APPEARANCE LIES",
"hyp": "My heart doth plead that thou in him dost lie , a closet never pierced with crystal eyes, but the defendant doth that plea deny, and says in him thy fair appearance lies.",
"ref_norm": "MY HEART DOTH PLEAD THAT THOU IN HIM DOST LIE A CLOSET NEVER PIERCD WITH CRYSTAL EYES BUT THE DEFENDANT DOTH THAT PLEA DENY AND SAYS IN HIM THY FAIR APPEARANCE LIES",
"hyp_norm": "MY HEART DOTH PLEAD THAT THOU IN HIM DOST LIE A CLOSET NEVER PIERCED WITH CRYSTAL EYES BUT THE DEFENDANT DOTH THAT PLEA DENY AND SAYS IN HIM THY FAIR APPEARANCE LIES",
"duration_s": 16.29,
"infer_time_s": 3.461,
"rtf": 0.2124,
"wer": 0.0312
},
{
"id": "121-123859-0000",
"ref": "YOU ARE MY ALL THE WORLD AND I MUST STRIVE TO KNOW MY SHAMES AND PRAISES FROM YOUR TONGUE NONE ELSE TO ME NOR I TO NONE ALIVE THAT MY STEEL'D SENSE OR CHANGES RIGHT OR WRONG",
"hyp": "You are my all the world , and I must strive to know my shames and praises from your tongue. None else to me , nor I to none alive , that my stealed sense or changes right or wrong.",
"ref_norm": "YOU ARE MY ALL THE WORLD AND I MUST STRIVE TO KNOW MY SHAMES AND PRAISES FROM YOUR TONGUE NONE ELSE TO ME NOR I TO NONE ALIVE THAT MY STEELD SENSE OR CHANGES RIGHT OR WRONG",
"hyp_norm": "YOU ARE MY ALL THE WORLD AND I MUST STRIVE TO KNOW MY SHAMES AND PRAISES FROM YOUR TONGUE NONE ELSE TO ME NOR I TO NONE ALIVE THAT MY STEALED SENSE OR CHANGES RIGHT OR WRONG",
"duration_s": 17.39,
"infer_time_s": 3.922,
"rtf": 0.2255,
"wer": 0.027
},
{
"id": "121-123859-0001",
"ref": "O TIS THE FIRST TIS FLATTERY IN MY SEEING AND MY GREAT MIND MOST KINGLY DRINKS IT UP MINE EYE WELL KNOWS WHAT WITH HIS GUST IS GREEING AND TO HIS PALATE DOTH PREPARE THE CUP IF IT BE POISON'D TIS THE LESSER SIN THAT MINE EYE LOVES IT AND DOTH FIRST BEGIN",
"hyp": "Oh, tis the first, tis flattery in my seeing , and my great mind most kingly drinks it up. Mine eye well knows what with his gust is green, and to his palate doth prepare the cup . If it be poisoned , tis the lesser sin , that mine eye loves it, and doth first begin.",
"ref_norm": "O TIS THE FIRST TIS FLATTERY IN MY SEEING AND MY GREAT MIND MOST KINGLY DRINKS IT UP MINE EYE WELL KNOWS WHAT WITH HIS GUST IS GREEING AND TO HIS PALATE DOTH PREPARE THE CUP IF IT BE POISOND TIS THE LESSER SIN THAT MINE EYE LOVES IT AND DOTH FIRST BEGIN",
"hyp_norm": "OH TIS THE FIRST TIS FLATTERY IN MY SEEING AND MY GREAT MIND MOST KINGLY DRINKS IT UP MINE EYE WELL KNOWS WHAT WITH HIS GUST IS GREEN AND TO HIS PALATE DOTH PREPARE THE CUP IF IT BE POISONED TIS THE LESSER SIN THAT MINE EYE LOVES IT AND DOTH FIRST BEGIN",
"duration_s": 25.395,
"infer_time_s": 5.939,
"rtf": 0.2339,
"wer": 0.0566
},
{
"id": "121-123859-0002",
"ref": "BUT RECKONING TIME WHOSE MILLION'D ACCIDENTS CREEP IN TWIXT VOWS AND CHANGE DECREES OF KINGS TAN SACRED BEAUTY BLUNT THE SHARP'ST INTENTS DIVERT STRONG MINDS TO THE COURSE OF ALTERING THINGS ALAS WHY FEARING OF TIME'S TYRANNY MIGHT I NOT THEN SAY NOW I LOVE YOU BEST WHEN I WAS CERTAIN O'ER INCERTAINTY CROWNING THE PRESENT DOUBTING OF THE REST",
"hyp": "But reckoning time , whose million ed accidents creep in twixt vows , and changed decrees of kings, tan sacred beauty blunt the sharpest intents , divert strong minds to the course of altering things. Alas , why fearing of time's tyranny, might I not then say, \"Now I love you best,\" when I was certain or in certainty, crowning the present, doubting of the rest.",
"ref_norm": "BUT RECKONING TIME WHOSE MILLIOND ACCIDENTS CREEP IN TWIXT VOWS AND CHANGE DECREES OF KINGS TAN SACRED BEAUTY BLUNT THE SHARPST INTENTS DIVERT STRONG MINDS TO THE COURSE OF ALTERING THINGS ALAS WHY FEARING OF TIMES TYRANNY MIGHT I NOT THEN SAY NOW I LOVE YOU BEST WHEN I WAS CERTAIN OER INCERTAINTY CROWNING THE PRESENT DOUBTING OF THE REST",
"hyp_norm": "BUT RECKONING TIME WHOSE MILLION ED ACCIDENTS CREEP IN TWIXT VOWS AND CHANGED DECREES OF KINGS TAN SACRED BEAUTY BLUNT THE SHARPEST INTENTS DIVERT STRONG MINDS TO THE COURSE OF ALTERING THINGS ALAS WHY FEARING OF TIMES TYRANNY MIGHT I NOT THEN SAY NOW I LOVE YOU BEST WHEN I WAS CERTAIN OR IN CERTAINTY CROWNING THE PRESENT DOUBTING OF THE REST",
"duration_s": 30.04,
"infer_time_s": 7.099,
"rtf": 0.2363,
"wer": 0.1167
},
{
"id": "121-123859-0003",
"ref": "LOVE IS A BABE THEN MIGHT I NOT SAY SO TO GIVE FULL GROWTH TO THAT WHICH STILL DOTH GROW",
"hyp": "Love is a babe. Then might I not say so . To give full growth to that which still doth grow.",
"ref_norm": "LOVE IS A BABE THEN MIGHT I NOT SAY SO TO GIVE FULL GROWTH TO THAT WHICH STILL DOTH GROW",
"hyp_norm": "LOVE IS A BABE THEN MIGHT I NOT SAY SO TO GIVE FULL GROWTH TO THAT WHICH STILL DOTH GROW",
"duration_s": 10.825,
"infer_time_s": 2.171,
"rtf": 0.2005,
"wer": 0.0
},
{
"id": "121-123859-0004",
"ref": "SO I RETURN REBUK'D TO MY CONTENT AND GAIN BY ILL THRICE MORE THAN I HAVE SPENT",
"hyp": "So I return rebuked to my content, and gain by ill thrice more than I have spent.",
"ref_norm": "SO I RETURN REBUKD TO MY CONTENT AND GAIN BY ILL THRICE MORE THAN I HAVE SPENT",
"hyp_norm": "SO I RETURN REBUKED TO MY CONTENT AND GAIN BY ILL THRICE MORE THAN I HAVE SPENT",
"duration_s": 9.505,
"infer_time_s": 1.871,
"rtf": 0.1969,
"wer": 0.0588
},
{
"id": "121-127105-0000",
"ref": "IT WAS THIS OBSERVATION THAT DREW FROM DOUGLAS NOT IMMEDIATELY BUT LATER IN THE EVENING A REPLY THAT HAD THE INTERESTING CONSEQUENCE TO WHICH I CALL ATTENTION",
"hyp": "It was this observation that drew from Douglas , not immediately but later in the evening, a reply that had the interesting consequence to which I call attention.",
"ref_norm": "IT WAS THIS OBSERVATION THAT DREW FROM DOUGLAS NOT IMMEDIATELY BUT LATER IN THE EVENING A REPLY THAT HAD THE INTERESTING CONSEQUENCE TO WHICH I CALL ATTENTION",
"hyp_norm": "IT WAS THIS OBSERVATION THAT DREW FROM DOUGLAS NOT IMMEDIATELY BUT LATER IN THE EVENING A REPLY THAT HAD THE INTERESTING CONSEQUENCE TO WHICH I CALL ATTENTION",
"duration_s": 9.875,
"infer_time_s": 2.285,
"rtf": 0.2314,
"wer": 0.0
},
{
"id": "121-127105-0001",
"ref": "SOMEONE ELSE TOLD A STORY NOT PARTICULARLY EFFECTIVE WHICH I SAW HE WAS NOT FOLLOWING",
"hyp": "Someone else told a story. Not particularly effective , which I saw he was not following.",
"ref_norm": "SOMEONE ELSE TOLD A STORY NOT PARTICULARLY EFFECTIVE WHICH I SAW HE WAS NOT FOLLOWING",
"hyp_norm": "SOMEONE ELSE TOLD A STORY NOT PARTICULARLY EFFECTIVE WHICH I SAW HE WAS NOT FOLLOWING",
"duration_s": 5.025,
"infer_time_s": 1.327,
"rtf": 0.2641,
"wer": 0.0
},
{
"id": "121-127105-0002",
"ref": "CRIED ONE OF THE WOMEN HE TOOK NO NOTICE OF HER HE LOOKED AT ME BUT AS IF INSTEAD OF ME HE SAW WHAT HE SPOKE OF",
"hyp": "Cried one of the women. He took no notice of her. He looked at me, but as if , instead of me, he saw what he spoke of.",
"ref_norm": "CRIED ONE OF THE WOMEN HE TOOK NO NOTICE OF HER HE LOOKED AT ME BUT AS IF INSTEAD OF ME HE SAW WHAT HE SPOKE OF",
"hyp_norm": "CRIED ONE OF THE WOMEN HE TOOK NO NOTICE OF HER HE LOOKED AT ME BUT AS IF INSTEAD OF ME HE SAW WHAT HE SPOKE OF",
"duration_s": 7.495,
"infer_time_s": 2.31,
"rtf": 0.3082,
"wer": 0.0
},
{
"id": "121-127105-0003",
"ref": "THERE WAS A UNANIMOUS GROAN AT THIS AND MUCH REPROACH AFTER WHICH IN HIS PREOCCUPIED WAY HE EXPLAINED",
"hyp": "There was a unanimous groan at this, and much reproach. After which, in his preoccupied way, he explained.",
"ref_norm": "THERE WAS A UNANIMOUS GROAN AT THIS AND MUCH REPROACH AFTER WHICH IN HIS PREOCCUPIED WAY HE EXPLAINED",
"hyp_norm": "THERE WAS A UNANIMOUS GROAN AT THIS AND MUCH REPROACH AFTER WHICH IN HIS PREOCCUPIED WAY HE EXPLAINED",
"duration_s": 7.725,
"infer_time_s": 1.903,
"rtf": 0.2464,
"wer": 0.0
},
{
"id": "121-127105-0004",
"ref": "THE STORY'S WRITTEN",
"hyp": "The story's written.",
"ref_norm": "THE STORYS WRITTEN",
"hyp_norm": "THE STORYS WRITTEN",
"duration_s": 2.11,
"infer_time_s": 0.526,
"rtf": 0.2495,
"wer": 0.0
},
{
"id": "121-127105-0005",
"ref": "I COULD WRITE TO MY MAN AND ENCLOSE THE KEY HE COULD SEND DOWN THE PACKET AS HE FINDS IT",
"hyp": "I could write to my man and enclose the key . He could send down the packet as he finds it.",
"ref_norm": "I COULD WRITE TO MY MAN AND ENCLOSE THE KEY HE COULD SEND DOWN THE PACKET AS HE FINDS IT",
"hyp_norm": "I COULD WRITE TO MY MAN AND ENCLOSE THE KEY HE COULD SEND DOWN THE PACKET AS HE FINDS IT",
"duration_s": 5.82,
"infer_time_s": 1.587,
"rtf": 0.2727,
"wer": 0.0
},
{
"id": "121-127105-0006",
"ref": "THE OTHERS RESENTED POSTPONEMENT BUT IT WAS JUST HIS SCRUPLES THAT CHARMED ME",
"hyp": "The others resented postpon ement, but it was just his scruples that charmed me.",
"ref_norm": "THE OTHERS RESENTED POSTPONEMENT BUT IT WAS JUST HIS SCRUPLES THAT CHARMED ME",
"hyp_norm": "THE OTHERS RESENTED POSTPON EMENT BUT IT WAS JUST HIS SCRUPLES THAT CHARMED ME",
"duration_s": 4.725,
"infer_time_s": 1.386,
"rtf": 0.2934,
"wer": 0.1538
},
{
"id": "121-127105-0007",
"ref": "TO THIS HIS ANSWER WAS PROMPT OH THANK GOD NO AND IS THE RECORD YOURS",
"hyp": "To this, his answer was prompt: \"Oh, thank God, no.\" And is the record yours?",
"ref_norm": "TO THIS HIS ANSWER WAS PROMPT OH THANK GOD NO AND IS THE RECORD YOURS",
"hyp_norm": "TO THIS HIS ANSWER WAS PROMPT OH THANK GOD NO AND IS THE RECORD YOURS",
"duration_s": 5.79,
"infer_time_s": 1.545,
"rtf": 0.2669,
"wer": 0.0
},
{
"id": "121-127105-0008",
"ref": "HE HUNG FIRE AGAIN A WOMAN'S",
"hyp": "He hung fire again \u2014a woman's.",
"ref_norm": "HE HUNG FIRE AGAIN A WOMANS",
"hyp_norm": "HE HUNG FIRE AGAIN A WOMANS",
"duration_s": 2.76,
"infer_time_s": 0.685,
"rtf": 0.2482,
"wer": 0.0
},
{
"id": "121-127105-0009",
"ref": "SHE HAS BEEN DEAD THESE TWENTY YEARS",
"hyp": "She has been dead these twenty years.",
"ref_norm": "SHE HAS BEEN DEAD THESE TWENTY YEARS",
"hyp_norm": "SHE HAS BEEN DEAD THESE TWENTY YEARS",
"duration_s": 2.29,
"infer_time_s": 0.681,
"rtf": 0.2973,
"wer": 0.0
},
{
"id": "121-127105-0010",
"ref": "SHE SENT ME THE PAGES IN QUESTION BEFORE SHE DIED",
"hyp": "She sent me the pages in question before she died.",
"ref_norm": "SHE SENT ME THE PAGES IN QUESTION BEFORE SHE DIED",
"hyp_norm": "SHE SENT ME THE PAGES IN QUESTION BEFORE SHE DIED",
"duration_s": 2.85,
"infer_time_s": 0.846,
"rtf": 0.2968,
"wer": 0.0
},
{
"id": "121-127105-0011",
"ref": "SHE WAS THE MOST AGREEABLE WOMAN I'VE EVER KNOWN IN HER POSITION SHE WOULD HAVE BEEN WORTHY OF ANY WHATEVER",
"hyp": "She was the most agreeable woman I've ever known in her position. She would have been worthy of any whatever.",
"ref_norm": "SHE WAS THE MOST AGREEABLE WOMAN IVE EVER KNOWN IN HER POSITION SHE WOULD HAVE BEEN WORTHY OF ANY WHATEVER",
"hyp_norm": "SHE WAS THE MOST AGREEABLE WOMAN IVE EVER KNOWN IN HER POSITION SHE WOULD HAVE BEEN WORTHY OF ANY WHATEVER",
"duration_s": 5.78,
"infer_time_s": 1.649,
"rtf": 0.2853,
"wer": 0.0
},
{
"id": "121-127105-0012",
"ref": "IT WASN'T SIMPLY THAT SHE SAID SO BUT THAT I KNEW SHE HADN'T I WAS SURE I COULD SEE",
"hyp": "It wasn't simply that she said so, but that I knew she hadn't. I was sure I could see.",
"ref_norm": "IT WASNT SIMPLY THAT SHE SAID SO BUT THAT I KNEW SHE HADNT I WAS SURE I COULD SEE",
"hyp_norm": "IT WASNT SIMPLY THAT SHE SAID SO BUT THAT I KNEW SHE HADNT I WAS SURE I COULD SEE",
"duration_s": 4.83,
"infer_time_s": 1.634,
"rtf": 0.3382,
"wer": 0.0
},
{
"id": "121-127105-0013",
"ref": "YOU'LL EASILY JUDGE WHY WHEN YOU HEAR BECAUSE THE THING HAD BEEN SUCH A SCARE HE CONTINUED TO FIX ME",
"hyp": "You'll easily judge why when you hear because the thing had been such a scare. He continued to fix me.",
"ref_norm": "YOULL EASILY JUDGE WHY WHEN YOU HEAR BECAUSE THE THING HAD BEEN SUCH A SCARE HE CONTINUED TO FIX ME",
"hyp_norm": "YOULL EASILY JUDGE WHY WHEN YOU HEAR BECAUSE THE THING HAD BEEN SUCH A SCARE HE CONTINUED TO FIX ME",
"duration_s": 5.895,
"infer_time_s": 1.594,
"rtf": 0.2704,
"wer": 0.0
},
{
"id": "121-127105-0014",
"ref": "YOU ARE ACUTE",
"hyp": "You are acute.",
"ref_norm": "YOU ARE ACUTE",
"hyp_norm": "YOU ARE ACUTE",
"duration_s": 2.255,
"infer_time_s": 0.471,
"rtf": 0.2088,
"wer": 0.0
},
{
"id": "121-127105-0015",
"ref": "HE QUITTED THE FIRE AND DROPPED BACK INTO HIS CHAIR",
"hyp": "He quitted the fire and dropped back into his chair.",
"ref_norm": "HE QUITTED THE FIRE AND DROPPED BACK INTO HIS CHAIR",
"hyp_norm": "HE QUITTED THE FIRE AND DROPPED BACK INTO HIS CHAIR",
"duration_s": 2.96,
"infer_time_s": 0.884,
"rtf": 0.2987,
"wer": 0.0
},
{
"id": "121-127105-0016",
"ref": "PROBABLY NOT TILL THE SECOND POST",
"hyp": "Probably not till the second post.",
"ref_norm": "PROBABLY NOT TILL THE SECOND POST",
"hyp_norm": "PROBABLY NOT TILL THE SECOND POST",
"duration_s": 2.03,
"infer_time_s": 0.624,
"rtf": 0.3075,
"wer": 0.0
},
{
"id": "121-127105-0017",
"ref": "IT WAS ALMOST THE TONE OF HOPE EVERYBODY WILL STAY",
"hyp": "It was almost the tone of hope : everybody will stay.",
"ref_norm": "IT WAS ALMOST THE TONE OF HOPE EVERYBODY WILL STAY",
"hyp_norm": "IT WAS ALMOST THE TONE OF HOPE EVERYBODY WILL STAY",
"duration_s": 2.695,
"infer_time_s": 0.891,
"rtf": 0.3306,
"wer": 0.0
},
{
"id": "121-127105-0018",
"ref": "CRIED THE LADIES WHOSE DEPARTURE HAD BEEN FIXED",
"hyp": "Cried the ladies, whose departure had been fixed.",
"ref_norm": "CRIED THE LADIES WHOSE DEPARTURE HAD BEEN FIXED",
"hyp_norm": "CRIED THE LADIES WHOSE DEPARTURE HAD BEEN FIXED",
"duration_s": 2.77,
"infer_time_s": 0.839,
"rtf": 0.303,
"wer": 0.0
},
{
"id": "121-127105-0019",
"ref": "MISSUS GRIFFIN HOWEVER EXPRESSED THE NEED FOR A LITTLE MORE LIGHT",
"hyp": "Missus Griffin, however, expressed the need for a little more light.",
"ref_norm": "MISSUS GRIFFIN HOWEVER EXPRESSED THE NEED FOR A LITTLE MORE LIGHT",
"hyp_norm": "MISSUS GRIFFIN HOWEVER EXPRESSED THE NEED FOR A LITTLE MORE LIGHT",
"duration_s": 3.525,
"infer_time_s": 1.037,
"rtf": 0.2943,
"wer": 0.0
},
{
"id": "121-127105-0020",
"ref": "WHO WAS IT SHE WAS IN LOVE WITH THE STORY WILL TELL I TOOK UPON MYSELF TO REPLY OH I CAN'T WAIT FOR THE STORY THE STORY WON'T TELL SAID DOUGLAS NOT IN ANY LITERAL VULGAR WAY MORE'S THE PITY THEN",
"hyp": "Who was it? She was in love with. The story will tell. I took upon myself to reply. Oh, I can't wait for the story. The story won't tell,\" said Douglas. \"Not in any literal vulgar way. What's the pity then?\"",
"ref_norm": "WHO WAS IT SHE WAS IN LOVE WITH THE STORY WILL TELL I TOOK UPON MYSELF TO REPLY OH I CANT WAIT FOR THE STORY THE STORY WONT TELL SAID DOUGLAS NOT IN ANY LITERAL VULGAR WAY MORES THE PITY THEN",
"hyp_norm": "WHO WAS IT SHE WAS IN LOVE WITH THE STORY WILL TELL I TOOK UPON MYSELF TO REPLY OH I CANT WAIT FOR THE STORY THE STORY WONT TELL SAID DOUGLAS NOT IN ANY LITERAL VULGAR WAY WHATS THE PITY THEN",
"duration_s": 14.355,
"infer_time_s": 4.158,
"rtf": 0.2897,
"wer": 0.0244
},
{
"id": "121-127105-0021",
"ref": "WON'T YOU TELL DOUGLAS",
"hyp": "Won't you tell Douglas?",
"ref_norm": "WONT YOU TELL DOUGLAS",
"hyp_norm": "WONT YOU TELL DOUGLAS",
"duration_s": 2.0,
"infer_time_s": 0.448,
"rtf": 0.224,
"wer": 0.0
},
{
"id": "121-127105-0022",
"ref": "WELL IF I DON'T KNOW WHO SHE WAS IN LOVE WITH I KNOW WHO HE WAS",
"hyp": "Well, if I don't know who she was in love with , I know who he was.",
"ref_norm": "WELL IF I DONT KNOW WHO SHE WAS IN LOVE WITH I KNOW WHO HE WAS",
"hyp_norm": "WELL IF I DONT KNOW WHO SHE WAS IN LOVE WITH I KNOW WHO HE WAS",
"duration_s": 5.075,
"infer_time_s": 1.425,
"rtf": 0.2808,
"wer": 0.0
},
{
"id": "121-127105-0023",
"ref": "LET ME SAY HERE DISTINCTLY TO HAVE DONE WITH IT THAT THIS NARRATIVE FROM AN EXACT TRANSCRIPT OF MY OWN MADE MUCH LATER IS WHAT I SHALL PRESENTLY GIVE",
"hyp": "Let me say here distinctly to have done with it that this narrative , from an exact transcript of my own made much later, is what I shall presently give.",
"ref_norm": "LET ME SAY HERE DISTINCTLY TO HAVE DONE WITH IT THAT THIS NARRATIVE FROM AN EXACT TRANSCRIPT OF MY OWN MADE MUCH LATER IS WHAT I SHALL PRESENTLY GIVE",
"hyp_norm": "LET ME SAY HERE DISTINCTLY TO HAVE DONE WITH IT THAT THIS NARRATIVE FROM AN EXACT TRANSCRIPT OF MY OWN MADE MUCH LATER IS WHAT I SHALL PRESENTLY GIVE",
"duration_s": 10.91,
"infer_time_s": 2.574,
"rtf": 0.236,
"wer": 0.0
},
{
"id": "121-127105-0024",
"ref": "POOR DOUGLAS BEFORE HIS DEATH WHEN IT WAS IN SIGHT COMMITTED TO ME THE MANUSCRIPT THAT REACHED HIM ON THE THIRD OF THESE DAYS AND THAT ON THE SAME SPOT WITH IMMENSE EFFECT HE BEGAN TO READ TO OUR HUSHED LITTLE CIRCLE ON THE NIGHT OF THE FOURTH",
"hyp": "Poor Douglas. Before his death, when it was in sight, committed to me the manuscript that reached him on the third of these days, and that, on the same spot, with immense effect, he began to read to our hushed little circle, on the night of the fourth.",
"ref_norm": "POOR DOUGLAS BEFORE HIS DEATH WHEN IT WAS IN SIGHT COMMITTED TO ME THE MANUSCRIPT THAT REACHED HIM ON THE THIRD OF THESE DAYS AND THAT ON THE SAME SPOT WITH IMMENSE EFFECT HE BEGAN TO READ TO OUR HUSHED LITTLE CIRCLE ON THE NIGHT OF THE FOURTH",
"hyp_norm": "POOR DOUGLAS BEFORE HIS DEATH WHEN IT WAS IN SIGHT COMMITTED TO ME THE MANUSCRIPT THAT REACHED HIM ON THE THIRD OF THESE DAYS AND THAT ON THE SAME SPOT WITH IMMENSE EFFECT HE BEGAN TO READ TO OUR HUSHED LITTLE CIRCLE ON THE NIGHT OF THE FOURTH",
"duration_s": 14.45,
"infer_time_s": 4.271,
"rtf": 0.2956,
"wer": 0.0
},
{
"id": "121-127105-0025",
"ref": "THE DEPARTING LADIES WHO HAD SAID THEY WOULD STAY DIDN'T OF COURSE THANK HEAVEN STAY THEY DEPARTED IN CONSEQUENCE OF ARRANGEMENTS MADE IN A RAGE OF CURIOSITY AS THEY PROFESSED PRODUCED BY THE TOUCHES WITH WHICH HE HAD ALREADY WORKED US UP",
"hyp": "The departing ladies who had said they would stay didn 't, of course. Thank heaven , stay. They departed in consequence of arrangements made , in a rage of curiosity, as they professed , produced by the touches with which he had already worked us up.",
"ref_norm": "THE DEPARTING LADIES WHO HAD SAID THEY WOULD STAY DIDNT OF COURSE THANK HEAVEN STAY THEY DEPARTED IN CONSEQUENCE OF ARRANGEMENTS MADE IN A RAGE OF CURIOSITY AS THEY PROFESSED PRODUCED BY THE TOUCHES WITH WHICH HE HAD ALREADY WORKED US UP",
"hyp_norm": "THE DEPARTING LADIES WHO HAD SAID THEY WOULD STAY DIDN T OF COURSE THANK HEAVEN STAY THEY DEPARTED IN CONSEQUENCE OF ARRANGEMENTS MADE IN A RAGE OF CURIOSITY AS THEY PROFESSED PRODUCED BY THE TOUCHES WITH WHICH HE HAD ALREADY WORKED US UP",
"duration_s": 16.065,
"infer_time_s": 4.137,
"rtf": 0.2575,
"wer": 0.0476
},
{
"id": "121-127105-0026",
"ref": "THE FIRST OF THESE TOUCHES CONVEYED THAT THE WRITTEN STATEMENT TOOK UP THE TALE AT A POINT AFTER IT HAD IN A MANNER BEGUN",
"hyp": "The first of these touches conveyed that the written statement took up the tale at a point after it had, in a manner, begun.",
"ref_norm": "THE FIRST OF THESE TOUCHES CONVEYED THAT THE WRITTEN STATEMENT TOOK UP THE TALE AT A POINT AFTER IT HAD IN A MANNER BEGUN",
"hyp_norm": "THE FIRST OF THESE TOUCHES CONVEYED THAT THE WRITTEN STATEMENT TOOK UP THE TALE AT A POINT AFTER IT HAD IN A MANNER BEGUN",
"duration_s": 7.53,
"infer_time_s": 1.963,
"rtf": 0.2607,
"wer": 0.0
},
{
"id": "121-127105-0027",
"ref": "HE HAD FOR HIS OWN TOWN RESIDENCE A BIG HOUSE FILLED WITH THE SPOILS OF TRAVEL AND THE TROPHIES OF THE CHASE BUT IT WAS TO HIS COUNTRY HOME AN OLD FAMILY PLACE IN ESSEX THAT HE WISHED HER IMMEDIATELY TO PROCEED",
"hyp": "He had for his own town residence a big house filled with the spoils of travel, and the trophies of the chase. But it was to his country home , an old family place in Essex, that he wished her immediately to proceed.",
"ref_norm": "HE HAD FOR HIS OWN TOWN RESIDENCE A BIG HOUSE FILLED WITH THE SPOILS OF TRAVEL AND THE TROPHIES OF THE CHASE BUT IT WAS TO HIS COUNTRY HOME AN OLD FAMILY PLACE IN ESSEX THAT HE WISHED HER IMMEDIATELY TO PROCEED",
"hyp_norm": "HE HAD FOR HIS OWN TOWN RESIDENCE A BIG HOUSE FILLED WITH THE SPOILS OF TRAVEL AND THE TROPHIES OF THE CHASE BUT IT WAS TO HIS COUNTRY HOME AN OLD FAMILY PLACE IN ESSEX THAT HE WISHED HER IMMEDIATELY TO PROCEED",
"duration_s": 13.87,
"infer_time_s": 3.578,
"rtf": 0.258,
"wer": 0.0
},
{
"id": "121-127105-0028",
"ref": "THE AWKWARD THING WAS THAT THEY HAD PRACTICALLY NO OTHER RELATIONS AND THAT HIS OWN AFFAIRS TOOK UP ALL HIS TIME",
"hyp": "The awkward thing was that they had practically no other relations, and that his own affairs took up all his time.",
"ref_norm": "THE AWKWARD THING WAS THAT THEY HAD PRACTICALLY NO OTHER RELATIONS AND THAT HIS OWN AFFAIRS TOOK UP ALL HIS TIME",
"hyp_norm": "THE AWKWARD THING WAS THAT THEY HAD PRACTICALLY NO OTHER RELATIONS AND THAT HIS OWN AFFAIRS TOOK UP ALL HIS TIME",
"duration_s": 6.75,
"infer_time_s": 1.748,
"rtf": 0.2589,
"wer": 0.0
},
{
"id": "121-127105-0029",
"ref": "THERE WERE PLENTY OF PEOPLE TO HELP BUT OF COURSE THE YOUNG LADY WHO SHOULD GO DOWN AS GOVERNESS WOULD BE IN SUPREME AUTHORITY",
"hyp": "There were plenty of people to help, but of course the young lady who should go down as governess, would be in supreme authority.",
"ref_norm": "THERE WERE PLENTY OF PEOPLE TO HELP BUT OF COURSE THE YOUNG LADY WHO SHOULD GO DOWN AS GOVERNESS WOULD BE IN SUPREME AUTHORITY",
"hyp_norm": "THERE WERE PLENTY OF PEOPLE TO HELP BUT OF COURSE THE YOUNG LADY WHO SHOULD GO DOWN AS GOVERNESS WOULD BE IN SUPREME AUTHORITY",
"duration_s": 7.31,
"infer_time_s": 2.005,
"rtf": 0.2743,
"wer": 0.0
},
{
"id": "121-127105-0030",
"ref": "I DON'T ANTICIPATE",
"hyp": "I don't anticipate.",
"ref_norm": "I DONT ANTICIPATE",
"hyp_norm": "I DONT ANTICIPATE",
"duration_s": 2.175,
"infer_time_s": 0.526,
"rtf": 0.2419,
"wer": 0.0
},
{
"id": "121-127105-0031",
"ref": "SHE WAS YOUNG UNTRIED NERVOUS IT WAS A VISION OF SERIOUS DUTIES AND LITTLE COMPANY OF REALLY GREAT LONELINESS",
"hyp": "She was young. Untried, nervous. It was a vision of serious duties in little company . Of really great loneliness.",
"ref_norm": "SHE WAS YOUNG UNTRIED NERVOUS IT WAS A VISION OF SERIOUS DUTIES AND LITTLE COMPANY OF REALLY GREAT LONELINESS",
"hyp_norm": "SHE WAS YOUNG UNTRIED NERVOUS IT WAS A VISION OF SERIOUS DUTIES IN LITTLE COMPANY OF REALLY GREAT LONELINESS",
"duration_s": 10.765,
"infer_time_s": 2.209,
"rtf": 0.2052,
"wer": 0.0526
},
{
"id": "121-127105-0032",
"ref": "YES BUT THAT'S JUST THE BEAUTY OF HER PASSION",
"hyp": "Yes, but that's just the beauty of her passion.",
"ref_norm": "YES BUT THATS JUST THE BEAUTY OF HER PASSION",
"hyp_norm": "YES BUT THATS JUST THE BEAUTY OF HER PASSION",
"duration_s": 3.17,
"infer_time_s": 0.885,
"rtf": 0.2791,
"wer": 0.0
},
{
"id": "121-127105-0033",
"ref": "IT WAS THE BEAUTY OF IT",
"hyp": "It was the beauty of it.",
"ref_norm": "IT WAS THE BEAUTY OF IT",
"hyp_norm": "IT WAS THE BEAUTY OF IT",
"duration_s": 2.355,
"infer_time_s": 0.621,
"rtf": 0.2635,
"wer": 0.0
},
{
"id": "121-127105-0034",
"ref": "IT SOUNDED DULL IT SOUNDED STRANGE AND ALL THE MORE SO BECAUSE OF HIS MAIN CONDITION WHICH WAS",
"hyp": "It sounded dull , that sounded strange , and all the more so because of his main condition, which was.",
"ref_norm": "IT SOUNDED DULL IT SOUNDED STRANGE AND ALL THE MORE SO BECAUSE OF HIS MAIN CONDITION WHICH WAS",
"hyp_norm": "IT SOUNDED DULL THAT SOUNDED STRANGE AND ALL THE MORE SO BECAUSE OF HIS MAIN CONDITION WHICH WAS",
"duration_s": 7.41,
"infer_time_s": 1.686,
"rtf": 0.2275,
"wer": 0.0556
},
{
"id": "121-127105-0035",
"ref": "SHE PROMISED TO DO THIS AND SHE MENTIONED TO ME THAT WHEN FOR A MOMENT DISBURDENED DELIGHTED HE HELD HER HAND THANKING HER FOR THE SACRIFICE SHE ALREADY FELT REWARDED",
"hyp": "She promised to do this , and she mentioned to me that when , for a moment, disburdened , delighted , he held her hand , thanking her for the sacrifice, she already felt rewarded.",
"ref_norm": "SHE PROMISED TO DO THIS AND SHE MENTIONED TO ME THAT WHEN FOR A MOMENT DISBURDENED DELIGHTED HE HELD HER HAND THANKING HER FOR THE SACRIFICE SHE ALREADY FELT REWARDED",
"hyp_norm": "SHE PROMISED TO DO THIS AND SHE MENTIONED TO ME THAT WHEN FOR A MOMENT DISBURDENED DELIGHTED HE HELD HER HAND THANKING HER FOR THE SACRIFICE SHE ALREADY FELT REWARDED",
"duration_s": 14.15,
"infer_time_s": 3.401,
"rtf": 0.2403,
"wer": 0.0
},
{
"id": "121-127105-0036",
"ref": "BUT WAS THAT ALL HER REWARD ONE OF THE LADIES ASKED",
"hyp": "But was that all her reward? One of the ladies asked.",
"ref_norm": "BUT WAS THAT ALL HER REWARD ONE OF THE LADIES ASKED",
"hyp_norm": "BUT WAS THAT ALL HER REWARD ONE OF THE LADIES ASKED",
"duration_s": 4.15,
"infer_time_s": 1.085,
"rtf": 0.2615,
"wer": 0.0
},
{
"id": "1221-135766-0000",
"ref": "HOW STRANGE IT SEEMED TO THE SAD WOMAN AS SHE WATCHED THE GROWTH AND THE BEAUTY THAT BECAME EVERY DAY MORE BRILLIANT AND THE INTELLIGENCE THAT THREW ITS QUIVERING SUNSHINE OVER THE TINY FEATURES OF THIS CHILD",
"hyp": "How strange it seemed to the sad woman as she watched the growth and the beauty that became every day more brilliant, and the intelligence that threw its qu ivering sunshine over the tiny features of this child.",
"ref_norm": "HOW STRANGE IT SEEMED TO THE SAD WOMAN AS SHE WATCHED THE GROWTH AND THE BEAUTY THAT BECAME EVERY DAY MORE BRILLIANT AND THE INTELLIGENCE THAT THREW ITS QUIVERING SUNSHINE OVER THE TINY FEATURES OF THIS CHILD",
"hyp_norm": "HOW STRANGE IT SEEMED TO THE SAD WOMAN AS SHE WATCHED THE GROWTH AND THE BEAUTY THAT BECAME EVERY DAY MORE BRILLIANT AND THE INTELLIGENCE THAT THREW ITS QU IVERING SUNSHINE OVER THE TINY FEATURES OF THIS CHILD",
"duration_s": 12.435,
"infer_time_s": 3.147,
"rtf": 0.2531,
"wer": 0.0541
},
{
"id": "1221-135766-0001",
"ref": "GOD AS A DIRECT CONSEQUENCE OF THE SIN WHICH MAN THUS PUNISHED HAD GIVEN HER A LOVELY CHILD WHOSE PLACE WAS ON THAT SAME DISHONOURED BOSOM TO CONNECT HER PARENT FOR EVER WITH THE RACE AND DESCENT OF MORTALS AND TO BE FINALLY A BLESSED SOUL IN HEAVEN",
"hyp": "God as a direct consequence of the sin which man thus punished had given her a lovely child whose place was on that same dishonored bosom to connect her parent, forever with the race and descent of mortals, and to be finally a blessed soul in heaven.",
"ref_norm": "GOD AS A DIRECT CONSEQUENCE OF THE SIN WHICH MAN THUS PUNISHED HAD GIVEN HER A LOVELY CHILD WHOSE PLACE WAS ON THAT SAME DISHONOURED BOSOM TO CONNECT HER PARENT FOR EVER WITH THE RACE AND DESCENT OF MORTALS AND TO BE FINALLY A BLESSED SOUL IN HEAVEN",
"hyp_norm": "GOD AS A DIRECT CONSEQUENCE OF THE SIN WHICH MAN THUS PUNISHED HAD GIVEN HER A LOVELY CHILD WHOSE PLACE WAS ON THAT SAME DISHONORED BOSOM TO CONNECT HER PARENT FOREVER WITH THE RACE AND DESCENT OF MORTALS AND TO BE FINALLY A BLESSED SOUL IN HEAVEN",
"duration_s": 16.715,
"infer_time_s": 4.242,
"rtf": 0.2538,
"wer": 0.0625
},
{
"id": "1221-135766-0002",
"ref": "YET THESE THOUGHTS AFFECTED HESTER PRYNNE LESS WITH HOPE THAN APPREHENSION",
"hyp": "Yet these thoughts affected Hester Prynne less with hope than apprehension.",
"ref_norm": "YET THESE THOUGHTS AFFECTED HESTER PRYNNE LESS WITH HOPE THAN APPREHENSION",
"hyp_norm": "YET THESE THOUGHTS AFFECTED HESTER PRYNNE LESS WITH HOPE THAN APPREHENSION",
"duration_s": 4.825,
"infer_time_s": 1.236,
"rtf": 0.2561,
"wer": 0.0
},
{
"id": "1221-135766-0003",
"ref": "THE CHILD HAD A NATIVE GRACE WHICH DOES NOT INVARIABLY CO EXIST WITH FAULTLESS BEAUTY ITS ATTIRE HOWEVER SIMPLE ALWAYS IMPRESSED THE BEHOLDER AS IF IT WERE THE VERY GARB THAT PRECISELY BECAME IT BEST",
"hyp": "The child had a native grace which does not invariably coexist with faultless beauty . Its attire, however simple, always impressed the beholder as if it were the very garb that precisely became it best.",
"ref_norm": "THE CHILD HAD A NATIVE GRACE WHICH DOES NOT INVARIABLY CO EXIST WITH FAULTLESS BEAUTY ITS ATTIRE HOWEVER SIMPLE ALWAYS IMPRESSED THE BEHOLDER AS IF IT WERE THE VERY GARB THAT PRECISELY BECAME IT BEST",
"hyp_norm": "THE CHILD HAD A NATIVE GRACE WHICH DOES NOT INVARIABLY COEXIST WITH FAULTLESS BEAUTY ITS ATTIRE HOWEVER SIMPLE ALWAYS IMPRESSED THE BEHOLDER AS IF IT WERE THE VERY GARB THAT PRECISELY BECAME IT BEST",
"duration_s": 13.72,
"infer_time_s": 3.255,
"rtf": 0.2373,
"wer": 0.0571
},
{
"id": "1221-135766-0004",
"ref": "THIS OUTWARD MUTABILITY INDICATED AND DID NOT MORE THAN FAIRLY EXPRESS THE VARIOUS PROPERTIES OF HER INNER LIFE",
"hyp": "This outward mut ability indicated, and did not more than fairly express the various properties of her inner life.",
"ref_norm": "THIS OUTWARD MUTABILITY INDICATED AND DID NOT MORE THAN FAIRLY EXPRESS THE VARIOUS PROPERTIES OF HER INNER LIFE",
"hyp_norm": "THIS OUTWARD MUT ABILITY INDICATED AND DID NOT MORE THAN FAIRLY EXPRESS THE VARIOUS PROPERTIES OF HER INNER LIFE",
"duration_s": 7.44,
"infer_time_s": 1.645,
"rtf": 0.2211,
"wer": 0.1111
},
{
"id": "1221-135766-0005",
"ref": "HESTER COULD ONLY ACCOUNT FOR THE CHILD'S CHARACTER AND EVEN THEN MOST VAGUELY AND IMPERFECTLY BY RECALLING WHAT SHE HERSELF HAD BEEN DURING THAT MOMENTOUS PERIOD WHILE PEARL WAS IMBIBING HER SOUL FROM THE SPIRITUAL WORLD AND HER BODILY FRAME FROM ITS MATERIAL OF EARTH",
"hyp": "Hester could only account for the child's character, and even then, most vaguely and imperfectly , by recalling what she herself had been during that momentous period, while Pearl was imbibing her soul from the spiritual world, and her bodily frame from its material of earth.",
"ref_norm": "HESTER COULD ONLY ACCOUNT FOR THE CHILDS CHARACTER AND EVEN THEN MOST VAGUELY AND IMPERFECTLY BY RECALLING WHAT SHE HERSELF HAD BEEN DURING THAT MOMENTOUS PERIOD WHILE PEARL WAS IMBIBING HER SOUL FROM THE SPIRITUAL WORLD AND HER BODILY FRAME FROM ITS MATERIAL OF EARTH",
"hyp_norm": "HESTER COULD ONLY ACCOUNT FOR THE CHILDS CHARACTER AND EVEN THEN MOST VAGUELY AND IMPERFECTLY BY RECALLING WHAT SHE HERSELF HAD BEEN DURING THAT MOMENTOUS PERIOD WHILE PEARL WAS IMBIBING HER SOUL FROM THE SPIRITUAL WORLD AND HER BODILY FRAME FROM ITS MATERIAL OF EARTH",
"duration_s": 16.645,
"infer_time_s": 4.382,
"rtf": 0.2632,
"wer": 0.0
},
{
"id": "1221-135766-0006",
"ref": "THEY WERE NOW ILLUMINATED BY THE MORNING RADIANCE OF A YOUNG CHILD'S DISPOSITION BUT LATER IN THE DAY OF EARTHLY EXISTENCE MIGHT BE PROLIFIC OF THE STORM AND WHIRLWIND",
"hyp": "They were now illuminated by the morning radiance of a young child's disposition , but later in the day of earthly existence might be prolific of the storm and whirlwind.",
"ref_norm": "THEY WERE NOW ILLUMINATED BY THE MORNING RADIANCE OF A YOUNG CHILDS DISPOSITION BUT LATER IN THE DAY OF EARTHLY EXISTENCE MIGHT BE PROLIFIC OF THE STORM AND WHIRLWIND",
"hyp_norm": "THEY WERE NOW ILLUMINATED BY THE MORNING RADIANCE OF A YOUNG CHILDS DISPOSITION BUT LATER IN THE DAY OF EARTHLY EXISTENCE MIGHT BE PROLIFIC OF THE STORM AND WHIRLWIND",
"duration_s": 11.415,
"infer_time_s": 2.666,
"rtf": 0.2336,
"wer": 0.0
},
{
"id": "1221-135766-0007",
"ref": "HESTER PRYNNE NEVERTHELESS THE LOVING MOTHER OF THIS ONE CHILD RAN LITTLE RISK OF ERRING ON THE SIDE OF UNDUE SEVERITY",
"hyp": "Hester Prin , nevertheless, the loving mother of this one child , ran little risk of erring on the side of undue severity.",
"ref_norm": "HESTER PRYNNE NEVERTHELESS THE LOVING MOTHER OF THIS ONE CHILD RAN LITTLE RISK OF ERRING ON THE SIDE OF UNDUE SEVERITY",
"hyp_norm": "HESTER PRIN NEVERTHELESS THE LOVING MOTHER OF THIS ONE CHILD RAN LITTLE RISK OF ERRING ON THE SIDE OF UNDUE SEVERITY",
"duration_s": 8.795,
"infer_time_s": 2.198,
"rtf": 0.2499,
"wer": 0.0476
},
{
"id": "1221-135766-0008",
"ref": "MINDFUL HOWEVER OF HER OWN ERRORS AND MISFORTUNES SHE EARLY SOUGHT TO IMPOSE A TENDER BUT STRICT CONTROL OVER THE INFANT IMMORTALITY THAT WAS COMMITTED TO HER CHARGE",
"hyp": "Mindful, however, of her own errors and misfort unes, she early sought to impose a tender but strict control over the infant immortality that was committed to her charge.",
"ref_norm": "MINDFUL HOWEVER OF HER OWN ERRORS AND MISFORTUNES SHE EARLY SOUGHT TO IMPOSE A TENDER BUT STRICT CONTROL OVER THE INFANT IMMORTALITY THAT WAS COMMITTED TO HER CHARGE",
"hyp_norm": "MINDFUL HOWEVER OF HER OWN ERRORS AND MISFORT UNES SHE EARLY SOUGHT TO IMPOSE A TENDER BUT STRICT CONTROL OVER THE INFANT IMMORTALITY THAT WAS COMMITTED TO HER CHARGE",
"duration_s": 10.78,
"infer_time_s": 2.805,
"rtf": 0.2602,
"wer": 0.0714
},
{
"id": "1221-135766-0009",
"ref": "AS TO ANY OTHER KIND OF DISCIPLINE WHETHER ADDRESSED TO HER MIND OR HEART LITTLE PEARL MIGHT OR MIGHT NOT BE WITHIN ITS REACH IN ACCORDANCE WITH THE CAPRICE THAT RULED THE MOMENT",
"hyp": "As to any other kind of discipline, whether addressed to her mind or heart , little pearl might or might not be within its reach in accordance with the caprice that ruled the moment.",
"ref_norm": "AS TO ANY OTHER KIND OF DISCIPLINE WHETHER ADDRESSED TO HER MIND OR HEART LITTLE PEARL MIGHT OR MIGHT NOT BE WITHIN ITS REACH IN ACCORDANCE WITH THE CAPRICE THAT RULED THE MOMENT",
"hyp_norm": "AS TO ANY OTHER KIND OF DISCIPLINE WHETHER ADDRESSED TO HER MIND OR HEART LITTLE PEARL MIGHT OR MIGHT NOT BE WITHIN ITS REACH IN ACCORDANCE WITH THE CAPRICE THAT RULED THE MOMENT",
"duration_s": 10.19,
"infer_time_s": 2.823,
"rtf": 0.2771,
"wer": 0.0
},
{
"id": "1221-135766-0010",
"ref": "IT WAS A LOOK SO INTELLIGENT YET INEXPLICABLE PERVERSE SOMETIMES SO MALICIOUS BUT GENERALLY ACCOMPANIED BY A WILD FLOW OF SPIRITS THAT HESTER COULD NOT HELP QUESTIONING AT SUCH MOMENTS WHETHER PEARL WAS A HUMAN CHILD",
"hyp": "It was a look so intelligent yet inexp licable, perverse, sometimes so malicious , but generally accompanied by a wild flow of spirits, that Hester could not help questioning at such moments, whether Pearl was a human child.",
"ref_norm": "IT WAS A LOOK SO INTELLIGENT YET INEXPLICABLE PERVERSE SOMETIMES SO MALICIOUS BUT GENERALLY ACCOMPANIED BY A WILD FLOW OF SPIRITS THAT HESTER COULD NOT HELP QUESTIONING AT SUCH MOMENTS WHETHER PEARL WAS A HUMAN CHILD",
"hyp_norm": "IT WAS A LOOK SO INTELLIGENT YET INEXP LICABLE PERVERSE SOMETIMES SO MALICIOUS BUT GENERALLY ACCOMPANIED BY A WILD FLOW OF SPIRITS THAT HESTER COULD NOT HELP QUESTIONING AT SUCH MOMENTS WHETHER PEARL WAS A HUMAN CHILD",
"duration_s": 15.05,
"infer_time_s": 3.547,
"rtf": 0.2357,
"wer": 0.0556
},
{
"id": "1221-135766-0011",
"ref": "BEHOLDING IT HESTER WAS CONSTRAINED TO RUSH TOWARDS THE CHILD TO PURSUE THE LITTLE ELF IN THE FLIGHT WHICH SHE INVARIABLY BEGAN TO SNATCH HER TO HER BOSOM WITH A CLOSE PRESSURE AND EARNEST KISSES NOT SO MUCH FROM OVERFLOWING LOVE AS TO ASSURE HERSELF THAT PEARL WAS FLESH AND BLOOD AND NOT UTTERLY DELUSIVE",
"hyp": "Beholding it, H ester was constrained to rush towards the child to pursue the little elf in the flight which she invariably began , to snatch her to her bosom with a close pressure and earnest kisses, not so much from overflowing love as to assure herself that Pearl was flesh and blood, and not utterly delusive.",
"ref_norm": "BEHOLDING IT HESTER WAS CONSTRAINED TO RUSH TOWARDS THE CHILD TO PURSUE THE LITTLE ELF IN THE FLIGHT WHICH SHE INVARIABLY BEGAN TO SNATCH HER TO HER BOSOM WITH A CLOSE PRESSURE AND EARNEST KISSES NOT SO MUCH FROM OVERFLOWING LOVE AS TO ASSURE HERSELF THAT PEARL WAS FLESH AND BLOOD AND NOT UTTERLY DELUSIVE",
"hyp_norm": "BEHOLDING IT H ESTER WAS CONSTRAINED TO RUSH TOWARDS THE CHILD TO PURSUE THE LITTLE ELF IN THE FLIGHT WHICH SHE INVARIABLY BEGAN TO SNATCH HER TO HER BOSOM WITH A CLOSE PRESSURE AND EARNEST KISSES NOT SO MUCH FROM OVERFLOWING LOVE AS TO ASSURE HERSELF THAT PEARL WAS FLESH AND BLOOD AND NOT UTTERLY DELUSIVE",
"duration_s": 21.345,
"infer_time_s": 5.23,
"rtf": 0.245,
"wer": 0.0364
},
{
"id": "1221-135766-0012",
"ref": "BROODING OVER ALL THESE MATTERS THE MOTHER FELT LIKE ONE WHO HAS EVOKED A SPIRIT BUT BY SOME IRREGULARITY IN THE PROCESS OF CONJURATION HAS FAILED TO WIN THE MASTER WORD THAT SHOULD CONTROL THIS NEW AND INCOMPREHENSIBLE INTELLIGENCE",
"hyp": "Brooding over all these matters, the mother felt like one who has evoked a spirit, but by some irregularity in the process of conjuration has failed to win the master word that should control this new and incomprehensible intelligence.",
"ref_norm": "BROODING OVER ALL THESE MATTERS THE MOTHER FELT LIKE ONE WHO HAS EVOKED A SPIRIT BUT BY SOME IRREGULARITY IN THE PROCESS OF CONJURATION HAS FAILED TO WIN THE MASTER WORD THAT SHOULD CONTROL THIS NEW AND INCOMPREHENSIBLE INTELLIGENCE",
"hyp_norm": "BROODING OVER ALL THESE MATTERS THE MOTHER FELT LIKE ONE WHO HAS EVOKED A SPIRIT BUT BY SOME IRREGULARITY IN THE PROCESS OF CONJURATION HAS FAILED TO WIN THE MASTER WORD THAT SHOULD CONTROL THIS NEW AND INCOMPREHENSIBLE INTELLIGENCE",
"duration_s": 16.22,
"infer_time_s": 3.965,
"rtf": 0.2444,
"wer": 0.0
},
{
"id": "1221-135766-0013",
"ref": "PEARL WAS A BORN OUTCAST OF THE INFANTILE WORLD",
"hyp": "Pearl was a born out cast of the infantile world.",
"ref_norm": "PEARL WAS A BORN OUTCAST OF THE INFANTILE WORLD",
"hyp_norm": "PEARL WAS A BORN OUT CAST OF THE INFANTILE WORLD",
"duration_s": 3.645,
"infer_time_s": 0.939,
"rtf": 0.2577,
"wer": 0.2222
},
{
"id": "1221-135766-0014",
"ref": "PEARL SAW AND GAZED INTENTLY BUT NEVER SOUGHT TO MAKE ACQUAINTANCE",
"hyp": "Pearl saw and gazed intently, but never sought to make acquaintance.",
"ref_norm": "PEARL SAW AND GAZED INTENTLY BUT NEVER SOUGHT TO MAKE ACQUAINTANCE",
"hyp_norm": "PEARL SAW AND GAZED INTENTLY BUT NEVER SOUGHT TO MAKE ACQUAINTANCE",
"duration_s": 4.75,
"infer_time_s": 1.23,
"rtf": 0.259,
"wer": 0.0
},
{
"id": "1221-135766-0015",
"ref": "IF SPOKEN TO SHE WOULD NOT SPEAK AGAIN",
"hyp": "If spoken to, she would not speak again.",
"ref_norm": "IF SPOKEN TO SHE WOULD NOT SPEAK AGAIN",
"hyp_norm": "IF SPOKEN TO SHE WOULD NOT SPEAK AGAIN",
"duration_s": 2.63,
"infer_time_s": 0.779,
"rtf": 0.2963,
"wer": 0.0
},
{
"id": "1221-135767-0000",
"ref": "HESTER PRYNNE WENT ONE DAY TO THE MANSION OF GOVERNOR BELLINGHAM WITH A PAIR OF GLOVES WHICH SHE HAD FRINGED AND EMBROIDERED TO HIS ORDER AND WHICH WERE TO BE WORN ON SOME GREAT OCCASION OF STATE FOR THOUGH THE CHANCES OF A POPULAR ELECTION HAD CAUSED THIS FORMER RULER TO DESCEND A STEP OR TWO FROM THE HIGHEST RANK HE STILL HELD AN HONOURABLE AND INFLUENTIAL PLACE AMONG THE COLONIAL MAGISTRACY",
"hyp": "Hester Prynne went one day to the mansion of Governor Bellingham with a pair of gloves which she had fringed and embroidered to his order, and which were to be worn on some great occasion of state, for though the chances of a popular election had caused this former ruler to descend a step or two from the highest rank, he still held an honorable and influential place among the colonial magistracy.",
"ref_norm": "HESTER PRYNNE WENT ONE DAY TO THE MANSION OF GOVERNOR BELLINGHAM WITH A PAIR OF GLOVES WHICH SHE HAD FRINGED AND EMBROIDERED TO HIS ORDER AND WHICH WERE TO BE WORN ON SOME GREAT OCCASION OF STATE FOR THOUGH THE CHANCES OF A POPULAR ELECTION HAD CAUSED THIS FORMER RULER TO DESCEND A STEP OR TWO FROM THE HIGHEST RANK HE STILL HELD AN HONOURABLE AND INFLUENTIAL PLACE AMONG THE COLONIAL MAGISTRACY",
"hyp_norm": "HESTER PRYNNE WENT ONE DAY TO THE MANSION OF GOVERNOR BELLINGHAM WITH A PAIR OF GLOVES WHICH SHE HAD FRINGED AND EMBROIDERED TO HIS ORDER AND WHICH WERE TO BE WORN ON SOME GREAT OCCASION OF STATE FOR THOUGH THE CHANCES OF A POPULAR ELECTION HAD CAUSED THIS FORMER RULER TO DESCEND A STEP OR TWO FROM THE HIGHEST RANK HE STILL HELD AN HONORABLE AND INFLUENTIAL PLACE AMONG THE COLONIAL MAGISTRACY",
"duration_s": 24.85,
"infer_time_s": 6.635,
"rtf": 0.267,
"wer": 0.0139
},
{
"id": "1221-135767-0001",
"ref": "ANOTHER AND FAR MORE IMPORTANT REASON THAN THE DELIVERY OF A PAIR OF EMBROIDERED GLOVES IMPELLED HESTER AT THIS TIME TO SEEK AN INTERVIEW WITH A PERSONAGE OF SO MUCH POWER AND ACTIVITY IN THE AFFAIRS OF THE SETTLEMENT",
"hyp": "Another and far more important reason than the delivery of a pair of embroidered gloves impelled H ester at this time, to seek an interview with a personage of so much power and activity in the affairs of the settlement.",
"ref_norm": "ANOTHER AND FAR MORE IMPORTANT REASON THAN THE DELIVERY OF A PAIR OF EMBROIDERED GLOVES IMPELLED HESTER AT THIS TIME TO SEEK AN INTERVIEW WITH A PERSONAGE OF SO MUCH POWER AND ACTIVITY IN THE AFFAIRS OF THE SETTLEMENT",
"hyp_norm": "ANOTHER AND FAR MORE IMPORTANT REASON THAN THE DELIVERY OF A PAIR OF EMBROIDERED GLOVES IMPELLED H ESTER AT THIS TIME TO SEEK AN INTERVIEW WITH A PERSONAGE OF SO MUCH POWER AND ACTIVITY IN THE AFFAIRS OF THE SETTLEMENT",
"duration_s": 13.43,
"infer_time_s": 3.435,
"rtf": 0.2558,
"wer": 0.0513
},
{
"id": "1221-135767-0002",
"ref": "AT THAT EPOCH OF PRISTINE SIMPLICITY HOWEVER MATTERS OF EVEN SLIGHTER PUBLIC INTEREST AND OF FAR LESS INTRINSIC WEIGHT THAN THE WELFARE OF HESTER AND HER CHILD WERE STRANGELY MIXED UP WITH THE DELIBERATIONS OF LEGISLATORS AND ACTS OF STATE",
"hyp": "At that epoch of pristine simplicity, however, matters of even slighter public interest and of far less intrinsic weight than the welfare of Hester and her child , were strangely mixed up with the deliberations, of legislators and acts of state.",
"ref_norm": "AT THAT EPOCH OF PRISTINE SIMPLICITY HOWEVER MATTERS OF EVEN SLIGHTER PUBLIC INTEREST AND OF FAR LESS INTRINSIC WEIGHT THAN THE WELFARE OF HESTER AND HER CHILD WERE STRANGELY MIXED UP WITH THE DELIBERATIONS OF LEGISLATORS AND ACTS OF STATE",
"hyp_norm": "AT THAT EPOCH OF PRISTINE SIMPLICITY HOWEVER MATTERS OF EVEN SLIGHTER PUBLIC INTEREST AND OF FAR LESS INTRINSIC WEIGHT THAN THE WELFARE OF HESTER AND HER CHILD WERE STRANGELY MIXED UP WITH THE DELIBERATIONS OF LEGISLATORS AND ACTS OF STATE",
"duration_s": 16.12,
"infer_time_s": 4.003,
"rtf": 0.2483,
"wer": 0.0
},
{
"id": "1221-135767-0003",
"ref": "THE PERIOD WAS HARDLY IF AT ALL EARLIER THAN THAT OF OUR STORY WHEN A DISPUTE CONCERNING THE RIGHT OF PROPERTY IN A PIG NOT ONLY CAUSED A FIERCE AND BITTER CONTEST IN THE LEGISLATIVE BODY OF THE COLONY BUT RESULTED IN AN IMPORTANT MODIFICATION OF THE FRAMEWORK ITSELF OF THE LEGISLATURE",
"hyp": "The period was hardly, if at all, earlier than that of our story, when a dispute concerning the right of property in a pig, not only caused a fierce and bitter contest in the legislative body of the colony, but resulted in an important modification of the framework itself of the legislature.",
"ref_norm": "THE PERIOD WAS HARDLY IF AT ALL EARLIER THAN THAT OF OUR STORY WHEN A DISPUTE CONCERNING THE RIGHT OF PROPERTY IN A PIG NOT ONLY CAUSED A FIERCE AND BITTER CONTEST IN THE LEGISLATIVE BODY OF THE COLONY BUT RESULTED IN AN IMPORTANT MODIFICATION OF THE FRAMEWORK ITSELF OF THE LEGISLATURE",
"hyp_norm": "THE PERIOD WAS HARDLY IF AT ALL EARLIER THAN THAT OF OUR STORY WHEN A DISPUTE CONCERNING THE RIGHT OF PROPERTY IN A PIG NOT ONLY CAUSED A FIERCE AND BITTER CONTEST IN THE LEGISLATIVE BODY OF THE COLONY BUT RESULTED IN AN IMPORTANT MODIFICATION OF THE FRAMEWORK ITSELF OF THE LEGISLATURE",
"duration_s": 18.63,
"infer_time_s": 4.695,
"rtf": 0.252,
"wer": 0.0
},
{
"id": "1221-135767-0004",
"ref": "WE HAVE SPOKEN OF PEARL'S RICH AND LUXURIANT BEAUTY A BEAUTY THAT SHONE WITH DEEP AND VIVID TINTS A BRIGHT COMPLEXION EYES POSSESSING INTENSITY BOTH OF DEPTH AND GLOW AND HAIR ALREADY OF A DEEP GLOSSY BROWN AND WHICH IN AFTER YEARS WOULD BE NEARLY AKIN TO BLACK",
"hyp": "We have spoken of pearls' rich and luxuriant beauty\u2014a beauty that shone with deep and vivid tints, a bright complexion, eyes possessing intensity both of depth and glow , and hair already of a deep glossy brown and which in after years would be nearly akin to black.",
"ref_norm": "WE HAVE SPOKEN OF PEARLS RICH AND LUXURIANT BEAUTY A BEAUTY THAT SHONE WITH DEEP AND VIVID TINTS A BRIGHT COMPLEXION EYES POSSESSING INTENSITY BOTH OF DEPTH AND GLOW AND HAIR ALREADY OF A DEEP GLOSSY BROWN AND WHICH IN AFTER YEARS WOULD BE NEARLY AKIN TO BLACK",
"hyp_norm": "WE HAVE SPOKEN OF PEARLS RICH AND LUXURIANT BEAUTYA BEAUTY THAT SHONE WITH DEEP AND VIVID TINTS A BRIGHT COMPLEXION EYES POSSESSING INTENSITY BOTH OF DEPTH AND GLOW AND HAIR ALREADY OF A DEEP GLOSSY BROWN AND WHICH IN AFTER YEARS WOULD BE NEARLY AKIN TO BLACK",
"duration_s": 19.09,
"infer_time_s": 4.623,
"rtf": 0.2422,
"wer": 0.0417
},
{
"id": "1221-135767-0005",
"ref": "IT WAS THE SCARLET LETTER IN ANOTHER FORM THE SCARLET LETTER ENDOWED WITH LIFE",
"hyp": "It was the scarlet letter in another form , the scarlet letter endowed with life.",
"ref_norm": "IT WAS THE SCARLET LETTER IN ANOTHER FORM THE SCARLET LETTER ENDOWED WITH LIFE",
"hyp_norm": "IT WAS THE SCARLET LETTER IN ANOTHER FORM THE SCARLET LETTER ENDOWED WITH LIFE",
"duration_s": 5.865,
"infer_time_s": 1.346,
"rtf": 0.2296,
"wer": 0.0
},
{
"id": "1221-135767-0006",
"ref": "THE MOTHER HERSELF AS IF THE RED IGNOMINY WERE SO DEEPLY SCORCHED INTO HER BRAIN THAT ALL HER CONCEPTIONS ASSUMED ITS FORM HAD CAREFULLY WROUGHT OUT THE SIMILITUDE LAVISHING MANY HOURS OF MORBID INGENUITY TO CREATE AN ANALOGY BETWEEN THE OBJECT OF HER AFFECTION AND THE EMBLEM OF HER GUILT AND TORTURE",
"hyp": "The mother herself , as if the red ignom iny were so deeply scor ched into her brain that all her conceptions assumed its form, had carefully wrought out the sim ilitude, lavishing many hours of morbid ingenuity to create an analogy between the object of her affection and the emblem of her guilt and torture.",
"ref_norm": "THE MOTHER HERSELF AS IF THE RED IGNOMINY WERE SO DEEPLY SCORCHED INTO HER BRAIN THAT ALL HER CONCEPTIONS ASSUMED ITS FORM HAD CAREFULLY WROUGHT OUT THE SIMILITUDE LAVISHING MANY HOURS OF MORBID INGENUITY TO CREATE AN ANALOGY BETWEEN THE OBJECT OF HER AFFECTION AND THE EMBLEM OF HER GUILT AND TORTURE",
"hyp_norm": "THE MOTHER HERSELF AS IF THE RED IGNOM INY WERE SO DEEPLY SCOR CHED INTO HER BRAIN THAT ALL HER CONCEPTIONS ASSUMED ITS FORM HAD CAREFULLY WROUGHT OUT THE SIM ILITUDE LAVISHING MANY HOURS OF MORBID INGENUITY TO CREATE AN ANALOGY BETWEEN THE OBJECT OF HER AFFECTION AND THE EMBLEM OF HER GUILT AND TORTURE",
"duration_s": 20.56,
"infer_time_s": 5.178,
"rtf": 0.2518,
"wer": 0.1154
},
{
"id": "1221-135767-0007",
"ref": "BUT IN TRUTH PEARL WAS THE ONE AS WELL AS THE OTHER AND ONLY IN CONSEQUENCE OF THAT IDENTITY HAD HESTER CONTRIVED SO PERFECTLY TO REPRESENT THE SCARLET LETTER IN HER APPEARANCE",
"hyp": "But in truth, pearl was the one as well as the other, and only in consequence of that identity had Hester contrived so perfectly to represent the scarlet letter in her appearance.",
"ref_norm": "BUT IN TRUTH PEARL WAS THE ONE AS WELL AS THE OTHER AND ONLY IN CONSEQUENCE OF THAT IDENTITY HAD HESTER CONTRIVED SO PERFECTLY TO REPRESENT THE SCARLET LETTER IN HER APPEARANCE",
"hyp_norm": "BUT IN TRUTH PEARL WAS THE ONE AS WELL AS THE OTHER AND ONLY IN CONSEQUENCE OF THAT IDENTITY HAD HESTER CONTRIVED SO PERFECTLY TO REPRESENT THE SCARLET LETTER IN HER APPEARANCE",
"duration_s": 12.77,
"infer_time_s": 3.052,
"rtf": 0.239,
"wer": 0.0
},
{
"id": "1221-135767-0008",
"ref": "COME THEREFORE AND LET US FLING MUD AT THEM",
"hyp": "Come therefore, and let us fling mud at them.",
"ref_norm": "COME THEREFORE AND LET US FLING MUD AT THEM",
"hyp_norm": "COME THEREFORE AND LET US FLING MUD AT THEM",
"duration_s": 3.095,
"infer_time_s": 0.886,
"rtf": 0.2863,
"wer": 0.0
},
{
"id": "1221-135767-0009",
"ref": "BUT PEARL WHO WAS A DAUNTLESS CHILD AFTER FROWNING STAMPING HER FOOT AND SHAKING HER LITTLE HAND WITH A VARIETY OF THREATENING GESTURES SUDDENLY MADE A RUSH AT THE KNOT OF HER ENEMIES AND PUT THEM ALL TO FLIGHT",
"hyp": "But Pearl, who was a dauntless child, after frowning, stamping her foot, and shaking her little hand with a variety of threatening gestures, suddenly made a rush at the knot of her enemies and put them all to flight.",
"ref_norm": "BUT PEARL WHO WAS A DAUNTLESS CHILD AFTER FROWNING STAMPING HER FOOT AND SHAKING HER LITTLE HAND WITH A VARIETY OF THREATENING GESTURES SUDDENLY MADE A RUSH AT THE KNOT OF HER ENEMIES AND PUT THEM ALL TO FLIGHT",
"hyp_norm": "BUT PEARL WHO WAS A DAUNTLESS CHILD AFTER FROWNING STAMPING HER FOOT AND SHAKING HER LITTLE HAND WITH A VARIETY OF THREATENING GESTURES SUDDENLY MADE A RUSH AT THE KNOT OF HER ENEMIES AND PUT THEM ALL TO FLIGHT",
"duration_s": 13.34,
"infer_time_s": 3.641,
"rtf": 0.2729,
"wer": 0.0
},
{
"id": "1221-135767-0010",
"ref": "SHE SCREAMED AND SHOUTED TOO WITH A TERRIFIC VOLUME OF SOUND WHICH DOUBTLESS CAUSED THE HEARTS OF THE FUGITIVES TO QUAKE WITHIN THEM",
"hyp": "She screamed and shouted too with a terrific volume of sound, which doubtless caused the hearts of the fugitives to quake within them.",
"ref_norm": "SHE SCREAMED AND SHOUTED TOO WITH A TERRIFIC VOLUME OF SOUND WHICH DOUBTLESS CAUSED THE HEARTS OF THE FUGITIVES TO QUAKE WITHIN THEM",
"hyp_norm": "SHE SCREAMED AND SHOUTED TOO WITH A TERRIFIC VOLUME OF SOUND WHICH DOUBTLESS CAUSED THE HEARTS OF THE FUGITIVES TO QUAKE WITHIN THEM",
"duration_s": 8.2,
"infer_time_s": 2.123,
"rtf": 0.2589,
"wer": 0.0
},
{
"id": "1221-135767-0011",
"ref": "IT WAS FURTHER DECORATED WITH STRANGE AND SEEMINGLY CABALISTIC FIGURES AND DIAGRAMS SUITABLE TO THE QUAINT TASTE OF THE AGE WHICH HAD BEEN DRAWN IN THE STUCCO WHEN NEWLY LAID ON AND HAD NOW GROWN HARD AND DURABLE FOR THE ADMIRATION OF AFTER TIMES",
"hyp": "It was further decorated with strange and seemingly cabalistic figures and diagrams, suitable to the quaint taste of the age, which had been drawn in the stucco when newly laid on, and had now grown hard and durable for the admiration of after times.",
"ref_norm": "IT WAS FURTHER DECORATED WITH STRANGE AND SEEMINGLY CABALISTIC FIGURES AND DIAGRAMS SUITABLE TO THE QUAINT TASTE OF THE AGE WHICH HAD BEEN DRAWN IN THE STUCCO WHEN NEWLY LAID ON AND HAD NOW GROWN HARD AND DURABLE FOR THE ADMIRATION OF AFTER TIMES",
"hyp_norm": "IT WAS FURTHER DECORATED WITH STRANGE AND SEEMINGLY CABALISTIC FIGURES AND DIAGRAMS SUITABLE TO THE QUAINT TASTE OF THE AGE WHICH HAD BEEN DRAWN IN THE STUCCO WHEN NEWLY LAID ON AND HAD NOW GROWN HARD AND DURABLE FOR THE ADMIRATION OF AFTER TIMES",
"duration_s": 16.51,
"infer_time_s": 4.145,
"rtf": 0.2511,
"wer": 0.0
},
{
"id": "1221-135767-0012",
"ref": "THEY APPROACHED THE DOOR WHICH WAS OF AN ARCHED FORM AND FLANKED ON EACH SIDE BY A NARROW TOWER OR PROJECTION OF THE EDIFICE IN BOTH OF WHICH WERE LATTICE WINDOWS THE WOODEN SHUTTERS TO CLOSE OVER THEM AT NEED",
"hyp": "They approached the door , which was of an arched form and flanked on each side by a narrow tower or projection of the ed ifice, in both of which were lattice windows, the wooden shutters to close over them at need.",
"ref_norm": "THEY APPROACHED THE DOOR WHICH WAS OF AN ARCHED FORM AND FLANKED ON EACH SIDE BY A NARROW TOWER OR PROJECTION OF THE EDIFICE IN BOTH OF WHICH WERE LATTICE WINDOWS THE WOODEN SHUTTERS TO CLOSE OVER THEM AT NEED",
"hyp_norm": "THEY APPROACHED THE DOOR WHICH WAS OF AN ARCHED FORM AND FLANKED ON EACH SIDE BY A NARROW TOWER OR PROJECTION OF THE ED IFICE IN BOTH OF WHICH WERE LATTICE WINDOWS THE WOODEN SHUTTERS TO CLOSE OVER THEM AT NEED",
"duration_s": 13.885,
"infer_time_s": 3.584,
"rtf": 0.2581,
"wer": 0.05
},
{
"id": "1221-135767-0013",
"ref": "LIFTING THE IRON HAMMER THAT HUNG AT THE PORTAL HESTER PRYNNE GAVE A SUMMONS WHICH WAS ANSWERED BY ONE OF THE GOVERNOR'S BOND SERVANT A FREE BORN ENGLISHMAN BUT NOW A SEVEN YEARS SLAVE",
"hyp": "Lifting the iron hammer that hung at the portal , Hester Prynne gave a summons, which was answered by one of the governor's bond servants , a free-born Englishman but now a seven years slave.",
"ref_norm": "LIFTING THE IRON HAMMER THAT HUNG AT THE PORTAL HESTER PRYNNE GAVE A SUMMONS WHICH WAS ANSWERED BY ONE OF THE GOVERNORS BOND SERVANT A FREE BORN ENGLISHMAN BUT NOW A SEVEN YEARS SLAVE",
"hyp_norm": "LIFTING THE IRON HAMMER THAT HUNG AT THE PORTAL HESTER PRYNNE GAVE A SUMMONS WHICH WAS ANSWERED BY ONE OF THE GOVERNORS BOND SERVANTS A FREEBORN ENGLISHMAN BUT NOW A SEVEN YEARS SLAVE",
"duration_s": 11.985,
"infer_time_s": 3.266,
"rtf": 0.2725,
"wer": 0.0882
},
{
"id": "1221-135767-0014",
"ref": "YEA HIS HONOURABLE WORSHIP IS WITHIN BUT HE HATH A GODLY MINISTER OR TWO WITH HIM AND LIKEWISE A LEECH",
"hyp": "Yea, his honorable worship is within, but he hath a godly minister or two with him, and likewise a leech.",
"ref_norm": "YEA HIS HONOURABLE WORSHIP IS WITHIN BUT HE HATH A GODLY MINISTER OR TWO WITH HIM AND LIKEWISE A LEECH",
"hyp_norm": "YEA HIS HONORABLE WORSHIP IS WITHIN BUT HE HATH A GODLY MINISTER OR TWO WITH HIM AND LIKEWISE A LEECH",
"duration_s": 7.07,
"infer_time_s": 2.006,
"rtf": 0.2838,
"wer": 0.05
},
{
"id": "1221-135767-0015",
"ref": "YE MAY NOT SEE HIS WORSHIP NOW",
"hyp": "Yea, may not see his worship now.",
"ref_norm": "YE MAY NOT SEE HIS WORSHIP NOW",
"hyp_norm": "YEA MAY NOT SEE HIS WORSHIP NOW",
"duration_s": 2.85,
"infer_time_s": 0.796,
"rtf": 0.2794,
"wer": 0.1429
},
{
"id": "1221-135767-0016",
"ref": "WITH MANY VARIATIONS SUGGESTED BY THE NATURE OF HIS BUILDING MATERIALS DIVERSITY OF CLIMATE AND A DIFFERENT MODE OF SOCIAL LIFE GOVERNOR BELLINGHAM HAD PLANNED HIS NEW HABITATION AFTER THE RESIDENCES OF GENTLEMEN OF FAIR ESTATE IN HIS NATIVE LAND",
"hyp": "With many variations suggested by the nature of his building materials, diversity of climate, and a different mode of social life , Governor Bellingham had planned his new habitation after the residences of gentlemen of fairest state in his native land.",
"ref_norm": "WITH MANY VARIATIONS SUGGESTED BY THE NATURE OF HIS BUILDING MATERIALS DIVERSITY OF CLIMATE AND A DIFFERENT MODE OF SOCIAL LIFE GOVERNOR BELLINGHAM HAD PLANNED HIS NEW HABITATION AFTER THE RESIDENCES OF GENTLEMEN OF FAIR ESTATE IN HIS NATIVE LAND",
"hyp_norm": "WITH MANY VARIATIONS SUGGESTED BY THE NATURE OF HIS BUILDING MATERIALS DIVERSITY OF CLIMATE AND A DIFFERENT MODE OF SOCIAL LIFE GOVERNOR BELLINGHAM HAD PLANNED HIS NEW HABITATION AFTER THE RESIDENCES OF GENTLEMEN OF FAIREST STATE IN HIS NATIVE LAND",
"duration_s": 15.255,
"infer_time_s": 3.799,
"rtf": 0.249,
"wer": 0.05
},
{
"id": "1221-135767-0017",
"ref": "ON THE TABLE IN TOKEN THAT THE SENTIMENT OF OLD ENGLISH HOSPITALITY HAD NOT BEEN LEFT BEHIND STOOD A LARGE PEWTER TANKARD AT THE BOTTOM OF WHICH HAD HESTER OR PEARL PEEPED INTO IT THEY MIGHT HAVE SEEN THE FROTHY REMNANT OF A RECENT DRAUGHT OF ALE",
"hyp": "On the table, in token that the sentiment of old English hospitality had not been left behind , stood a large pewter tankard, at the bottom of which, had Hester or Pearl peeped into it , they might have seen the frothy remnant of a recent draught of ale.",
"ref_norm": "ON THE TABLE IN TOKEN THAT THE SENTIMENT OF OLD ENGLISH HOSPITALITY HAD NOT BEEN LEFT BEHIND STOOD A LARGE PEWTER TANKARD AT THE BOTTOM OF WHICH HAD HESTER OR PEARL PEEPED INTO IT THEY MIGHT HAVE SEEN THE FROTHY REMNANT OF A RECENT DRAUGHT OF ALE",
"hyp_norm": "ON THE TABLE IN TOKEN THAT THE SENTIMENT OF OLD ENGLISH HOSPITALITY HAD NOT BEEN LEFT BEHIND STOOD A LARGE PEWTER TANKARD AT THE BOTTOM OF WHICH HAD HESTER OR PEARL PEEPED INTO IT THEY MIGHT HAVE SEEN THE FROTHY REMNANT OF A RECENT DRAUGHT OF ALE",
"duration_s": 16.72,
"infer_time_s": 4.642,
"rtf": 0.2776,
"wer": 0.0
},
{
"id": "1221-135767-0018",
"ref": "LITTLE PEARL WHO WAS AS GREATLY PLEASED WITH THE GLEAMING ARMOUR AS SHE HAD BEEN WITH THE GLITTERING FRONTISPIECE OF THE HOUSE SPENT SOME TIME LOOKING INTO THE POLISHED MIRROR OF THE BREASTPLATE",
"hyp": "Little Pearl, who was as greatly pleased with the gleaming armor as she had been with the glittering front ispiece of the house, spent some time looking into the polished mirror of the breastplate.",
"ref_norm": "LITTLE PEARL WHO WAS AS GREATLY PLEASED WITH THE GLEAMING ARMOUR AS SHE HAD BEEN WITH THE GLITTERING FRONTISPIECE OF THE HOUSE SPENT SOME TIME LOOKING INTO THE POLISHED MIRROR OF THE BREASTPLATE",
"hyp_norm": "LITTLE PEARL WHO WAS AS GREATLY PLEASED WITH THE GLEAMING ARMOR AS SHE HAD BEEN WITH THE GLITTERING FRONT ISPIECE OF THE HOUSE SPENT SOME TIME LOOKING INTO THE POLISHED MIRROR OF THE BREASTPLATE",
"duration_s": 11.16,
"infer_time_s": 3.085,
"rtf": 0.2765,
"wer": 0.0909
},
{
"id": "1221-135767-0019",
"ref": "MOTHER CRIED SHE I SEE YOU HERE LOOK LOOK",
"hyp": "Mother cried, \"She, I see you here. Look, look.\"",
"ref_norm": "MOTHER CRIED SHE I SEE YOU HERE LOOK LOOK",
"hyp_norm": "MOTHER CRIED SHE I SEE YOU HERE LOOK LOOK",
"duration_s": 3.78,
"infer_time_s": 1.068,
"rtf": 0.2825,
"wer": 0.0
},
{
"id": "1221-135767-0020",
"ref": "IN TRUTH SHE SEEMED ABSOLUTELY HIDDEN BEHIND IT",
"hyp": "In truth, she seemed absolutely hidden behind it.",
"ref_norm": "IN TRUTH SHE SEEMED ABSOLUTELY HIDDEN BEHIND IT",
"hyp_norm": "IN TRUTH SHE SEEMED ABSOLUTELY HIDDEN BEHIND IT",
"duration_s": 3.345,
"infer_time_s": 0.794,
"rtf": 0.2374,
"wer": 0.0
},
{
"id": "1221-135767-0021",
"ref": "PEARL ACCORDINGLY RAN TO THE BOW WINDOW AT THE FURTHER END OF THE HALL AND LOOKED ALONG THE VISTA OF A GARDEN WALK CARPETED WITH CLOSELY SHAVEN GRASS AND BORDERED WITH SOME RUDE AND IMMATURE ATTEMPT AT SHRUBBERY",
"hyp": "Pearl accordingly ran to the bow window at the further end of the hall, and looked along the vista of a garden walk carpeted with closely shaven grass, and bordered with some rude and imitator attempt at shrubbery.",
"ref_norm": "PEARL ACCORDINGLY RAN TO THE BOW WINDOW AT THE FURTHER END OF THE HALL AND LOOKED ALONG THE VISTA OF A GARDEN WALK CARPETED WITH CLOSELY SHAVEN GRASS AND BORDERED WITH SOME RUDE AND IMMATURE ATTEMPT AT SHRUBBERY",
"hyp_norm": "PEARL ACCORDINGLY RAN TO THE BOW WINDOW AT THE FURTHER END OF THE HALL AND LOOKED ALONG THE VISTA OF A GARDEN WALK CARPETED WITH CLOSELY SHAVEN GRASS AND BORDERED WITH SOME RUDE AND IMITATOR ATTEMPT AT SHRUBBERY",
"duration_s": 12.72,
"infer_time_s": 3.615,
"rtf": 0.2842,
"wer": 0.0263
},
{
"id": "1221-135767-0022",
"ref": "BUT THE PROPRIETOR APPEARED ALREADY TO HAVE RELINQUISHED AS HOPELESS THE EFFORT TO PERPETUATE ON THIS SIDE OF THE ATLANTIC IN A HARD SOIL AND AMID THE CLOSE STRUGGLE FOR SUBSISTENCE THE NATIVE ENGLISH TASTE FOR ORNAMENTAL GARDENING",
"hyp": "But the proprietor appeared already to have relinquished us hopeless, the effort to perpetuate on this side of the Atlantic in a hard soil, and amid the close struggle for subs istence, the native English taste for ornamental gardening.",
"ref_norm": "BUT THE PROPRIETOR APPEARED ALREADY TO HAVE RELINQUISHED AS HOPELESS THE EFFORT TO PERPETUATE ON THIS SIDE OF THE ATLANTIC IN A HARD SOIL AND AMID THE CLOSE STRUGGLE FOR SUBSISTENCE THE NATIVE ENGLISH TASTE FOR ORNAMENTAL GARDENING",
"hyp_norm": "BUT THE PROPRIETOR APPEARED ALREADY TO HAVE RELINQUISHED US HOPELESS THE EFFORT TO PERPETUATE ON THIS SIDE OF THE ATLANTIC IN A HARD SOIL AND AMID THE CLOSE STRUGGLE FOR SUBS ISTENCE THE NATIVE ENGLISH TASTE FOR ORNAMENTAL GARDENING",
"duration_s": 14.395,
"infer_time_s": 3.651,
"rtf": 0.2537,
"wer": 0.0789
},
{
"id": "1221-135767-0023",
"ref": "THERE WERE A FEW ROSE BUSHES HOWEVER AND A NUMBER OF APPLE TREES PROBABLY THE DESCENDANTS OF THOSE PLANTED BY THE REVEREND MISTER BLACKSTONE THE FIRST SETTLER OF THE PENINSULA THAT HALF MYTHOLOGICAL PERSONAGE WHO RIDES THROUGH OUR EARLY ANNALS SEATED ON THE BACK OF A BULL",
"hyp": "There were a few rose bushes, however, and a number of apple trees\u2014probably the descendants of those planted by the Reverend Mister Black stone, the first sett ler of the Peninsula, that half mythological personage who rides through our early annals seated on the back of a bull.",
"ref_norm": "THERE WERE A FEW ROSE BUSHES HOWEVER AND A NUMBER OF APPLE TREES PROBABLY THE DESCENDANTS OF THOSE PLANTED BY THE REVEREND MISTER BLACKSTONE THE FIRST SETTLER OF THE PENINSULA THAT HALF MYTHOLOGICAL PERSONAGE WHO RIDES THROUGH OUR EARLY ANNALS SEATED ON THE BACK OF A BULL",
"hyp_norm": "THERE WERE A FEW ROSE BUSHES HOWEVER AND A NUMBER OF APPLE TREESPROBABLY THE DESCENDANTS OF THOSE PLANTED BY THE REVEREND MISTER BLACK STONE THE FIRST SETT LER OF THE PENINSULA THAT HALF MYTHOLOGICAL PERSONAGE WHO RIDES THROUGH OUR EARLY ANNALS SEATED ON THE BACK OF A BULL",
"duration_s": 16.27,
"infer_time_s": 4.518,
"rtf": 0.2777,
"wer": 0.1277
},
{
"id": "1221-135767-0024",
"ref": "PEARL SEEING THE ROSE BUSHES BEGAN TO CRY FOR A RED ROSE AND WOULD NOT BE PACIFIED",
"hyp": "Pearl seeing the rose bushes began to cry for a red rose and would not be pacified.",
"ref_norm": "PEARL SEEING THE ROSE BUSHES BEGAN TO CRY FOR A RED ROSE AND WOULD NOT BE PACIFIED",
"hyp_norm": "PEARL SEEING THE ROSE BUSHES BEGAN TO CRY FOR A RED ROSE AND WOULD NOT BE PACIFIED",
"duration_s": 5.85,
"infer_time_s": 1.442,
"rtf": 0.2466,
"wer": 0.0
},
{
"id": "1284-1180-0000",
"ref": "HE WORE BLUE SILK STOCKINGS BLUE KNEE PANTS WITH GOLD BUCKLES A BLUE RUFFLED WAIST AND A JACKET OF BRIGHT BLUE BRAIDED WITH GOLD",
"hyp": "He wore blue silk stockings, blue knee pants with gold buckles, a blue ruffled waist, and a jacket of bright blue braided with gold.",
"ref_norm": "HE WORE BLUE SILK STOCKINGS BLUE KNEE PANTS WITH GOLD BUCKLES A BLUE RUFFLED WAIST AND A JACKET OF BRIGHT BLUE BRAIDED WITH GOLD",
"hyp_norm": "HE WORE BLUE SILK STOCKINGS BLUE KNEE PANTS WITH GOLD BUCKLES A BLUE RUFFLED WAIST AND A JACKET OF BRIGHT BLUE BRAIDED WITH GOLD",
"duration_s": 8.12,
"infer_time_s": 2.341,
"rtf": 0.2883,
"wer": 0.0
},
{
"id": "1284-1180-0001",
"ref": "HIS HAT HAD A PEAKED CROWN AND A FLAT BRIM AND AROUND THE BRIM WAS A ROW OF TINY GOLDEN BELLS THAT TINKLED WHEN HE MOVED",
"hyp": "His hat had a peaked crown at a flat brim , and around the brim was a row of tiny golden bells that tinkled when he moved.",
"ref_norm": "HIS HAT HAD A PEAKED CROWN AND A FLAT BRIM AND AROUND THE BRIM WAS A ROW OF TINY GOLDEN BELLS THAT TINKLED WHEN HE MOVED",
"hyp_norm": "HIS HAT HAD A PEAKED CROWN AT A FLAT BRIM AND AROUND THE BRIM WAS A ROW OF TINY GOLDEN BELLS THAT TINKLED WHEN HE MOVED",
"duration_s": 7.755,
"infer_time_s": 2.184,
"rtf": 0.2816,
"wer": 0.0385
},
{
"id": "1284-1180-0002",
"ref": "INSTEAD OF SHOES THE OLD MAN WORE BOOTS WITH TURNOVER TOPS AND HIS BLUE COAT HAD WIDE CUFFS OF GOLD BRAID",
"hyp": "Instead of shoes , the old man wore boots with turnover tops, and his blue coat had wide cuffs of gold braid.",
"ref_norm": "INSTEAD OF SHOES THE OLD MAN WORE BOOTS WITH TURNOVER TOPS AND HIS BLUE COAT HAD WIDE CUFFS OF GOLD BRAID",
"hyp_norm": "INSTEAD OF SHOES THE OLD MAN WORE BOOTS WITH TURNOVER TOPS AND HIS BLUE COAT HAD WIDE CUFFS OF GOLD BRAID",
"duration_s": 7.68,
"infer_time_s": 1.923,
"rtf": 0.2504,
"wer": 0.0
},
{
"id": "1284-1180-0003",
"ref": "FOR A LONG TIME HE HAD WISHED TO EXPLORE THE BEAUTIFUL LAND OF OZ IN WHICH THEY LIVED",
"hyp": "For a long time, he had wished to explore the beautiful land of Oz in which they lived.",
"ref_norm": "FOR A LONG TIME HE HAD WISHED TO EXPLORE THE BEAUTIFUL LAND OF OZ IN WHICH THEY LIVED",
"hyp_norm": "FOR A LONG TIME HE HAD WISHED TO EXPLORE THE BEAUTIFUL LAND OF OZ IN WHICH THEY LIVED",
"duration_s": 4.835,
"infer_time_s": 1.443,
"rtf": 0.2985,
"wer": 0.0
},
{
"id": "1284-1180-0004",
"ref": "WHEN THEY WERE OUTSIDE UNC SIMPLY LATCHED THE DOOR AND STARTED UP THE PATH",
"hyp": "When they were outside, Ung simply latched the door and started up the path.",
"ref_norm": "WHEN THEY WERE OUTSIDE UNC SIMPLY LATCHED THE DOOR AND STARTED UP THE PATH",
"hyp_norm": "WHEN THEY WERE OUTSIDE UNG SIMPLY LATCHED THE DOOR AND STARTED UP THE PATH",
"duration_s": 4.285,
"infer_time_s": 1.297,
"rtf": 0.3026,
"wer": 0.0714
},
{
"id": "1284-1180-0005",
"ref": "NO ONE WOULD DISTURB THEIR LITTLE HOUSE EVEN IF ANYONE CAME SO FAR INTO THE THICK FOREST WHILE THEY WERE GONE",
"hyp": "No one would disturb their little house, even if anyone came so far into the thick forest while they were gone.",
"ref_norm": "NO ONE WOULD DISTURB THEIR LITTLE HOUSE EVEN IF ANYONE CAME SO FAR INTO THE THICK FOREST WHILE THEY WERE GONE",
"hyp_norm": "NO ONE WOULD DISTURB THEIR LITTLE HOUSE EVEN IF ANYONE CAME SO FAR INTO THE THICK FOREST WHILE THEY WERE GONE",
"duration_s": 6.55,
"infer_time_s": 1.735,
"rtf": 0.2648,
"wer": 0.0
},
{
"id": "1284-1180-0006",
"ref": "AT THE FOOT OF THE MOUNTAIN THAT SEPARATED THE COUNTRY OF THE MUNCHKINS FROM THE COUNTRY OF THE GILLIKINS THE PATH DIVIDED",
"hyp": "At the foot of the mountain that separated the country of the Munchkins from the country of the Gillikins, the path divided.",
"ref_norm": "AT THE FOOT OF THE MOUNTAIN THAT SEPARATED THE COUNTRY OF THE MUNCHKINS FROM THE COUNTRY OF THE GILLIKINS THE PATH DIVIDED",
"hyp_norm": "AT THE FOOT OF THE MOUNTAIN THAT SEPARATED THE COUNTRY OF THE MUNCHKINS FROM THE COUNTRY OF THE GILLIKINS THE PATH DIVIDED",
"duration_s": 6.865,
"infer_time_s": 2.018,
"rtf": 0.2939,
"wer": 0.0
},
{
"id": "1284-1180-0007",
"ref": "HE KNEW IT WOULD TAKE THEM TO THE HOUSE OF THE CROOKED MAGICIAN WHOM HE HAD NEVER SEEN BUT WHO WAS THEIR NEAREST NEIGHBOR",
"hyp": "He knew it would take them to the house of the crooked magician , whom he had never seen , but who was their nearest neighbor.",
"ref_norm": "HE KNEW IT WOULD TAKE THEM TO THE HOUSE OF THE CROOKED MAGICIAN WHOM HE HAD NEVER SEEN BUT WHO WAS THEIR NEAREST NEIGHBOR",
"hyp_norm": "HE KNEW IT WOULD TAKE THEM TO THE HOUSE OF THE CROOKED MAGICIAN WHOM HE HAD NEVER SEEN BUT WHO WAS THEIR NEAREST NEIGHBOR",
"duration_s": 6.265,
"infer_time_s": 1.994,
"rtf": 0.3183,
"wer": 0.0
},
{
"id": "1284-1180-0008",
"ref": "ALL THE MORNING THEY TRUDGED UP THE MOUNTAIN PATH AND AT NOON UNC AND OJO SAT ON A FALLEN TREE TRUNK AND ATE THE LAST OF THE BREAD WHICH THE OLD MUNCHKIN HAD PLACED IN HIS POCKET",
"hyp": "All the morning they tr udged up the mountain path, and at noon, Unc and Ojo sat on a fallen tree trunk and ate the last of the bread which the old Munchkin had placed in his pocket.",
"ref_norm": "ALL THE MORNING THEY TRUDGED UP THE MOUNTAIN PATH AND AT NOON UNC AND OJO SAT ON A FALLEN TREE TRUNK AND ATE THE LAST OF THE BREAD WHICH THE OLD MUNCHKIN HAD PLACED IN HIS POCKET",
"hyp_norm": "ALL THE MORNING THEY TR UDGED UP THE MOUNTAIN PATH AND AT NOON UNC AND OJO SAT ON A FALLEN TREE TRUNK AND ATE THE LAST OF THE BREAD WHICH THE OLD MUNCHKIN HAD PLACED IN HIS POCKET",
"duration_s": 10.49,
"infer_time_s": 3.307,
"rtf": 0.3153,
"wer": 0.0541
},
{
"id": "1284-1180-0009",
"ref": "THEN THEY STARTED ON AGAIN AND TWO HOURS LATER CAME IN SIGHT OF THE HOUSE OF DOCTOR PIPT",
"hyp": "Then they started on again, and two hours later came in sight of the house of Doctor Pipt.",
"ref_norm": "THEN THEY STARTED ON AGAIN AND TWO HOURS LATER CAME IN SIGHT OF THE HOUSE OF DOCTOR PIPT",
"hyp_norm": "THEN THEY STARTED ON AGAIN AND TWO HOURS LATER CAME IN SIGHT OF THE HOUSE OF DOCTOR PIPT",
"duration_s": 6.285,
"infer_time_s": 1.685,
"rtf": 0.2681,
"wer": 0.0
},
{
"id": "1284-1180-0010",
"ref": "UNC KNOCKED AT THE DOOR OF THE HOUSE AND A CHUBBY PLEASANT FACED WOMAN DRESSED ALL IN BLUE OPENED IT AND GREETED THE VISITORS WITH A SMILE",
"hyp": "Unc knocked at the door of the house, and a chubby, pleasant-faced woman dressed all in blue opened it and greeted the visitors with a smile.",
"ref_norm": "UNC KNOCKED AT THE DOOR OF THE HOUSE AND A CHUBBY PLEASANT FACED WOMAN DRESSED ALL IN BLUE OPENED IT AND GREETED THE VISITORS WITH A SMILE",
"hyp_norm": "UNC KNOCKED AT THE DOOR OF THE HOUSE AND A CHUBBY PLEASANTFACED WOMAN DRESSED ALL IN BLUE OPENED IT AND GREETED THE VISITORS WITH A SMILE",
"duration_s": 8.635,
"infer_time_s": 2.351,
"rtf": 0.2723,
"wer": 0.0741
},
{
"id": "1284-1180-0011",
"ref": "I AM MY DEAR AND ALL STRANGERS ARE WELCOME TO MY HOME",
"hyp": "I am, my dear , and all strangers are welcome to my home.",
"ref_norm": "I AM MY DEAR AND ALL STRANGERS ARE WELCOME TO MY HOME",
"hyp_norm": "I AM MY DEAR AND ALL STRANGERS ARE WELCOME TO MY HOME",
"duration_s": 4.275,
"infer_time_s": 1.21,
"rtf": 0.283,
"wer": 0.0
},
{
"id": "1284-1180-0012",
"ref": "WE HAVE COME FROM A FAR LONELIER PLACE THAN THIS A LONELIER PLACE",
"hyp": "We have come from a far lon elier place than this , a lonelier place.",
"ref_norm": "WE HAVE COME FROM A FAR LONELIER PLACE THAN THIS A LONELIER PLACE",
"hyp_norm": "WE HAVE COME FROM A FAR LON ELIER PLACE THAN THIS A LONELIER PLACE",
"duration_s": 4.88,
"infer_time_s": 1.319,
"rtf": 0.2703,
"wer": 0.1538
},
{
"id": "1284-1180-0013",
"ref": "AND YOU MUST BE OJO THE UNLUCKY SHE ADDED",
"hyp": "And you must be Ojo the unlucky,\" she added.",
"ref_norm": "AND YOU MUST BE OJO THE UNLUCKY SHE ADDED",
"hyp_norm": "AND YOU MUST BE OJO THE UNLUCKY SHE ADDED",
"duration_s": 3.705,
"infer_time_s": 0.913,
"rtf": 0.2464,
"wer": 0.0
},
{
"id": "1284-1180-0014",
"ref": "OJO HAD NEVER EATEN SUCH A FINE MEAL IN ALL HIS LIFE",
"hyp": "Ojo had never eaten such a fine meal in all his life.",
"ref_norm": "OJO HAD NEVER EATEN SUCH A FINE MEAL IN ALL HIS LIFE",
"hyp_norm": "OJO HAD NEVER EATEN SUCH A FINE MEAL IN ALL HIS LIFE",
"duration_s": 3.665,
"infer_time_s": 1.015,
"rtf": 0.277,
"wer": 0.0
},
{
"id": "1284-1180-0015",
"ref": "WE ARE TRAVELING REPLIED OJO AND WE STOPPED AT YOUR HOUSE JUST TO REST AND REFRESH OURSELVES",
"hyp": "We are traveling,\" replied Ojo, and we stopped at your house just to rest and refresh ourselves.",
"ref_norm": "WE ARE TRAVELING REPLIED OJO AND WE STOPPED AT YOUR HOUSE JUST TO REST AND REFRESH OURSELVES",
"hyp_norm": "WE ARE TRAVELING REPLIED OJO AND WE STOPPED AT YOUR HOUSE JUST TO REST AND REFRESH OURSELVES",
"duration_s": 5.835,
"infer_time_s": 1.524,
"rtf": 0.2611,
"wer": 0.0
},
{
"id": "1284-1180-0016",
"ref": "THE WOMAN SEEMED THOUGHTFUL",
"hyp": "The woman seemed thoughtful.",
"ref_norm": "THE WOMAN SEEMED THOUGHTFUL",
"hyp_norm": "THE WOMAN SEEMED THOUGHTFUL",
"duration_s": 2.13,
"infer_time_s": 0.546,
"rtf": 0.2564,
"wer": 0.0
},
{
"id": "1284-1180-0017",
"ref": "AT ONE END STOOD A GREAT FIREPLACE IN WHICH A BLUE LOG WAS BLAZING WITH A BLUE FLAME AND OVER THE FIRE HUNG FOUR KETTLES IN A ROW ALL BUBBLING AND STEAMING AT A GREAT RATE",
"hyp": "At one end stood a great fireplace in which a blue log was blazing with a blue flame, and over the fire hung four kettles in a row, all bubbling and steaming at a great rate.",
"ref_norm": "AT ONE END STOOD A GREAT FIREPLACE IN WHICH A BLUE LOG WAS BLAZING WITH A BLUE FLAME AND OVER THE FIRE HUNG FOUR KETTLES IN A ROW ALL BUBBLING AND STEAMING AT A GREAT RATE",
"hyp_norm": "AT ONE END STOOD A GREAT FIREPLACE IN WHICH A BLUE LOG WAS BLAZING WITH A BLUE FLAME AND OVER THE FIRE HUNG FOUR KETTLES IN A ROW ALL BUBBLING AND STEAMING AT A GREAT RATE",
"duration_s": 10.68,
"infer_time_s": 3.145,
"rtf": 0.2945,
"wer": 0.0
},
{
"id": "1284-1180-0018",
"ref": "IT TAKES ME SEVERAL YEARS TO MAKE THIS MAGIC POWDER BUT AT THIS MOMENT I AM PLEASED TO SAY IT IS NEARLY DONE YOU SEE I AM MAKING IT FOR MY GOOD WIFE MARGOLOTTE WHO WANTS TO USE SOME OF IT FOR A PURPOSE OF HER OWN",
"hyp": "It takes me several years to make this magic powder, but at this moment I am pleased to say it is nearly done. You see, I am making it for my good wife Margol ot, who wants to use some of it for a purpose of her own.",
"ref_norm": "IT TAKES ME SEVERAL YEARS TO MAKE THIS MAGIC POWDER BUT AT THIS MOMENT I AM PLEASED TO SAY IT IS NEARLY DONE YOU SEE I AM MAKING IT FOR MY GOOD WIFE MARGOLOTTE WHO WANTS TO USE SOME OF IT FOR A PURPOSE OF HER OWN",
"hyp_norm": "IT TAKES ME SEVERAL YEARS TO MAKE THIS MAGIC POWDER BUT AT THIS MOMENT I AM PLEASED TO SAY IT IS NEARLY DONE YOU SEE I AM MAKING IT FOR MY GOOD WIFE MARGOL OT WHO WANTS TO USE SOME OF IT FOR A PURPOSE OF HER OWN",
"duration_s": 12.005,
"infer_time_s": 3.876,
"rtf": 0.3229,
"wer": 0.0426
},
{
"id": "1284-1180-0019",
"ref": "YOU MUST KNOW SAID MARGOLOTTE WHEN THEY WERE ALL SEATED TOGETHER ON THE BROAD WINDOW SEAT THAT MY HUSBAND FOOLISHLY GAVE AWAY ALL THE POWDER OF LIFE HE FIRST MADE TO OLD MOMBI THE WITCH WHO USED TO LIVE IN THE COUNTRY OF THE GILLIKINS TO THE NORTH OF HERE",
"hyp": "You must know ,\" said Margot. When they were all seated together on the broad window seat, that my husband foolishly gave away all the powder of life he first made to Old Mombi the witch, who used to live in the country of the Gillikins to the north of here.",
"ref_norm": "YOU MUST KNOW SAID MARGOLOTTE WHEN THEY WERE ALL SEATED TOGETHER ON THE BROAD WINDOW SEAT THAT MY HUSBAND FOOLISHLY GAVE AWAY ALL THE POWDER OF LIFE HE FIRST MADE TO OLD MOMBI THE WITCH WHO USED TO LIVE IN THE COUNTRY OF THE GILLIKINS TO THE NORTH OF HERE",
"hyp_norm": "YOU MUST KNOW SAID MARGOT WHEN THEY WERE ALL SEATED TOGETHER ON THE BROAD WINDOW SEAT THAT MY HUSBAND FOOLISHLY GAVE AWAY ALL THE POWDER OF LIFE HE FIRST MADE TO OLD MOMBI THE WITCH WHO USED TO LIVE IN THE COUNTRY OF THE GILLIKINS TO THE NORTH OF HERE",
"duration_s": 15.025,
"infer_time_s": 4.551,
"rtf": 0.3029,
"wer": 0.02
},
{
"id": "1284-1180-0020",
"ref": "THE FIRST LOT WE TESTED ON OUR GLASS CAT WHICH NOT ONLY BEGAN TO LIVE BUT HAS LIVED EVER SINCE",
"hyp": "The first lot we tested on our glass cat, which not only began to live but has lived ever since.",
"ref_norm": "THE FIRST LOT WE TESTED ON OUR GLASS CAT WHICH NOT ONLY BEGAN TO LIVE BUT HAS LIVED EVER SINCE",
"hyp_norm": "THE FIRST LOT WE TESTED ON OUR GLASS CAT WHICH NOT ONLY BEGAN TO LIVE BUT HAS LIVED EVER SINCE",
"duration_s": 5.87,
"infer_time_s": 1.59,
"rtf": 0.2708,
"wer": 0.0
},
{
"id": "1284-1180-0021",
"ref": "I THINK THE NEXT GLASS CAT THE MAGICIAN MAKES WILL HAVE NEITHER BRAINS NOR HEART FOR THEN IT WILL NOT OBJECT TO CATCHING MICE AND MAY PROVE OF SOME USE TO US",
"hyp": "I think the next glass cap the magician makes will have neither brains nor heart, for then it will not object to catching mice and may prove of some use to us.",
"ref_norm": "I THINK THE NEXT GLASS CAT THE MAGICIAN MAKES WILL HAVE NEITHER BRAINS NOR HEART FOR THEN IT WILL NOT OBJECT TO CATCHING MICE AND MAY PROVE OF SOME USE TO US",
"hyp_norm": "I THINK THE NEXT GLASS CAP THE MAGICIAN MAKES WILL HAVE NEITHER BRAINS NOR HEART FOR THEN IT WILL NOT OBJECT TO CATCHING MICE AND MAY PROVE OF SOME USE TO US",
"duration_s": 9.84,
"infer_time_s": 2.566,
"rtf": 0.2608,
"wer": 0.0312
},
{
"id": "1284-1180-0022",
"ref": "I'M AFRAID I DON'T KNOW MUCH ABOUT THE LAND OF OZ",
"hyp": "I'm afraid I don't know much about the land of Oz.",
"ref_norm": "IM AFRAID I DONT KNOW MUCH ABOUT THE LAND OF OZ",
"hyp_norm": "IM AFRAID I DONT KNOW MUCH ABOUT THE LAND OF OZ",
"duration_s": 2.885,
"infer_time_s": 1.018,
"rtf": 0.3528,
"wer": 0.0
},
{
"id": "1284-1180-0023",
"ref": "YOU SEE I'VE LIVED ALL MY LIFE WITH UNC NUNKIE THE SILENT ONE AND THERE WAS NO ONE TO TELL ME ANYTHING",
"hyp": "You see, I've lived all my life with Unc Nunky, the silent one, and there was no one to tell me anything.",
"ref_norm": "YOU SEE IVE LIVED ALL MY LIFE WITH UNC NUNKIE THE SILENT ONE AND THERE WAS NO ONE TO TELL ME ANYTHING",
"hyp_norm": "YOU SEE IVE LIVED ALL MY LIFE WITH UNC NUNKY THE SILENT ONE AND THERE WAS NO ONE TO TELL ME ANYTHING",
"duration_s": 5.61,
"infer_time_s": 1.926,
"rtf": 0.3433,
"wer": 0.0455
},
{
"id": "1284-1180-0024",
"ref": "THAT IS ONE REASON YOU ARE OJO THE UNLUCKY SAID THE WOMAN IN A SYMPATHETIC TONE",
"hyp": "That is one reason you are Ojo the unlucky ,\" said the woman in a sympathetic tone.",
"ref_norm": "THAT IS ONE REASON YOU ARE OJO THE UNLUCKY SAID THE WOMAN IN A SYMPATHETIC TONE",
"hyp_norm": "THAT IS ONE REASON YOU ARE OJO THE UNLUCKY SAID THE WOMAN IN A SYMPATHETIC TONE",
"duration_s": 5.26,
"infer_time_s": 1.417,
"rtf": 0.2695,
"wer": 0.0
},
{
"id": "1284-1180-0025",
"ref": "I THINK I MUST SHOW YOU MY PATCHWORK GIRL SAID MARGOLOTTE LAUGHING AT THE BOY'S ASTONISHMENT FOR SHE IS RATHER DIFFICULT TO EXPLAIN",
"hyp": "I think I must show you my patchwork girl ,\" said Margot, laughing at the boy's astonishment , for she is rather difficult to explain.",
"ref_norm": "I THINK I MUST SHOW YOU MY PATCHWORK GIRL SAID MARGOLOTTE LAUGHING AT THE BOYS ASTONISHMENT FOR SHE IS RATHER DIFFICULT TO EXPLAIN",
"hyp_norm": "I THINK I MUST SHOW YOU MY PATCHWORK GIRL SAID MARGOT LAUGHING AT THE BOYS ASTONISHMENT FOR SHE IS RATHER DIFFICULT TO EXPLAIN",
"duration_s": 8.705,
"infer_time_s": 2.338,
"rtf": 0.2685,
"wer": 0.0435
},
{
"id": "1284-1180-0026",
"ref": "BUT FIRST I WILL TELL YOU THAT FOR MANY YEARS I HAVE LONGED FOR A SERVANT TO HELP ME WITH THE HOUSEWORK AND TO COOK THE MEALS AND WASH THE DISHES",
"hyp": "But first, I will tell you that for many years I have longed for a servant to help me with the housework and to cook the meals and wash the dishes.",
"ref_norm": "BUT FIRST I WILL TELL YOU THAT FOR MANY YEARS I HAVE LONGED FOR A SERVANT TO HELP ME WITH THE HOUSEWORK AND TO COOK THE MEALS AND WASH THE DISHES",
"hyp_norm": "BUT FIRST I WILL TELL YOU THAT FOR MANY YEARS I HAVE LONGED FOR A SERVANT TO HELP ME WITH THE HOUSEWORK AND TO COOK THE MEALS AND WASH THE DISHES",
"duration_s": 8.29,
"infer_time_s": 2.565,
"rtf": 0.3095,
"wer": 0.0
},
{
"id": "1284-1180-0027",
"ref": "YET THAT TASK WAS NOT SO EASY AS YOU MAY SUPPOSE",
"hyp": "Yet that task was not so easy as you may suppose.",
"ref_norm": "YET THAT TASK WAS NOT SO EASY AS YOU MAY SUPPOSE",
"hyp_norm": "YET THAT TASK WAS NOT SO EASY AS YOU MAY SUPPOSE",
"duration_s": 3.27,
"infer_time_s": 0.892,
"rtf": 0.2728,
"wer": 0.0
},
{
"id": "1284-1180-0028",
"ref": "A BED QUILT MADE OF PATCHES OF DIFFERENT KINDS AND COLORS OF CLOTH ALL NEATLY SEWED TOGETHER",
"hyp": "A bed quilt made of patches of different kinds and colors of cloth, all neatly sewed together.",
"ref_norm": "A BED QUILT MADE OF PATCHES OF DIFFERENT KINDS AND COLORS OF CLOTH ALL NEATLY SEWED TOGETHER",
"hyp_norm": "A BED QUILT MADE OF PATCHES OF DIFFERENT KINDS AND COLORS OF CLOTH ALL NEATLY SEWED TOGETHER",
"duration_s": 6.045,
"infer_time_s": 1.6,
"rtf": 0.2648,
"wer": 0.0
},
{
"id": "1284-1180-0029",
"ref": "SOMETIMES IT IS CALLED A CRAZY QUILT BECAUSE THE PATCHES AND COLORS ARE SO MIXED UP",
"hyp": "Sometimes it is called a crazy quilt because the patches and colors are so mixed up.",
"ref_norm": "SOMETIMES IT IS CALLED A CRAZY QUILT BECAUSE THE PATCHES AND COLORS ARE SO MIXED UP",
"hyp_norm": "SOMETIMES IT IS CALLED A CRAZY QUILT BECAUSE THE PATCHES AND COLORS ARE SO MIXED UP",
"duration_s": 5.335,
"infer_time_s": 1.286,
"rtf": 0.2411,
"wer": 0.0
},
{
"id": "1284-1180-0030",
"ref": "WHEN I FOUND IT I SAID TO MYSELF THAT IT WOULD DO NICELY FOR MY SERVANT GIRL FOR WHEN SHE WAS BROUGHT TO LIFE SHE WOULD NOT BE PROUD NOR HAUGHTY AS THE GLASS CAT IS FOR SUCH A DREADFUL MIXTURE OF COLORS WOULD DISCOURAGE HER FROM TRYING TO BE AS DIGNIFIED AS THE BLUE MUNCHKINS ARE",
"hyp": "When I found it, I said to myself that it would do nicely for my servant girl. For when she was brought to life, she would not be proud nor ha ughty as the glass cat is. For such a dreadful mixture of colors would discourage her from trying to be as dignified as the blue munchkins are.",
"ref_norm": "WHEN I FOUND IT I SAID TO MYSELF THAT IT WOULD DO NICELY FOR MY SERVANT GIRL FOR WHEN SHE WAS BROUGHT TO LIFE SHE WOULD NOT BE PROUD NOR HAUGHTY AS THE GLASS CAT IS FOR SUCH A DREADFUL MIXTURE OF COLORS WOULD DISCOURAGE HER FROM TRYING TO BE AS DIGNIFIED AS THE BLUE MUNCHKINS ARE",
"hyp_norm": "WHEN I FOUND IT I SAID TO MYSELF THAT IT WOULD DO NICELY FOR MY SERVANT GIRL FOR WHEN SHE WAS BROUGHT TO LIFE SHE WOULD NOT BE PROUD NOR HA UGHTY AS THE GLASS CAT IS FOR SUCH A DREADFUL MIXTURE OF COLORS WOULD DISCOURAGE HER FROM TRYING TO BE AS DIGNIFIED AS THE BLUE MUNCHKINS ARE",
"duration_s": 16.22,
"infer_time_s": 4.874,
"rtf": 0.3005,
"wer": 0.0351
},
{
"id": "1284-1180-0031",
"ref": "AT THE EMERALD CITY WHERE OUR PRINCESS OZMA LIVES GREEN IS THE POPULAR COLOR",
"hyp": "At the Emerald City, where our Princess Ozma lives , green is the popular color.",
"ref_norm": "AT THE EMERALD CITY WHERE OUR PRINCESS OZMA LIVES GREEN IS THE POPULAR COLOR",
"hyp_norm": "AT THE EMERALD CITY WHERE OUR PRINCESS OZMA LIVES GREEN IS THE POPULAR COLOR",
"duration_s": 4.825,
"infer_time_s": 1.338,
"rtf": 0.2773,
"wer": 0.0
},
{
"id": "1284-1180-0032",
"ref": "I WILL SHOW YOU WHAT A GOOD JOB I DID AND SHE WENT TO A TALL CUPBOARD AND THREW OPEN THE DOORS",
"hyp": "I will show you what a good job I did, and she went to a tall cupboard and threw open the doors.",
"ref_norm": "I WILL SHOW YOU WHAT A GOOD JOB I DID AND SHE WENT TO A TALL CUPBOARD AND THREW OPEN THE DOORS",
"hyp_norm": "I WILL SHOW YOU WHAT A GOOD JOB I DID AND SHE WENT TO A TALL CUPBOARD AND THREW OPEN THE DOORS",
"duration_s": 5.78,
"infer_time_s": 1.648,
"rtf": 0.2851,
"wer": 0.0
},
{
"id": "1284-1181-0000",
"ref": "OJO EXAMINED THIS CURIOUS CONTRIVANCE WITH WONDER",
"hyp": "Ojo examined this curious contrivance with wonder.",
"ref_norm": "OJO EXAMINED THIS CURIOUS CONTRIVANCE WITH WONDER",
"hyp_norm": "OJO EXAMINED THIS CURIOUS CONTRIVANCE WITH WONDER",
"duration_s": 3.965,
"infer_time_s": 0.844,
"rtf": 0.2128,
"wer": 0.0
},
{
"id": "1284-1181-0001",
"ref": "MARGOLOTTE HAD FIRST MADE THE GIRL'S FORM FROM THE PATCHWORK QUILT AND THEN SHE HAD DRESSED IT WITH A PATCHWORK SKIRT AND AN APRON WITH POCKETS IN IT USING THE SAME GAY MATERIAL THROUGHOUT",
"hyp": "Margolot had first made the girl's form from the patchwork quilt, and then she had dressed it with a patchwork skirt and an apron with pockets in it, using the same gay material throughout.",
"ref_norm": "MARGOLOTTE HAD FIRST MADE THE GIRLS FORM FROM THE PATCHWORK QUILT AND THEN SHE HAD DRESSED IT WITH A PATCHWORK SKIRT AND AN APRON WITH POCKETS IN IT USING THE SAME GAY MATERIAL THROUGHOUT",
"hyp_norm": "MARGOLOT HAD FIRST MADE THE GIRLS FORM FROM THE PATCHWORK QUILT AND THEN SHE HAD DRESSED IT WITH A PATCHWORK SKIRT AND AN APRON WITH POCKETS IN IT USING THE SAME GAY MATERIAL THROUGHOUT",
"duration_s": 11.43,
"infer_time_s": 3.195,
"rtf": 0.2795,
"wer": 0.0294
},
{
"id": "1284-1181-0002",
"ref": "THE HEAD OF THE PATCHWORK GIRL WAS THE MOST CURIOUS PART OF HER",
"hyp": "The head of the patchwork girl was the most curious part of her.",
"ref_norm": "THE HEAD OF THE PATCHWORK GIRL WAS THE MOST CURIOUS PART OF HER",
"hyp_norm": "THE HEAD OF THE PATCHWORK GIRL WAS THE MOST CURIOUS PART OF HER",
"duration_s": 3.835,
"infer_time_s": 1.057,
"rtf": 0.2756,
"wer": 0.0
},
{
"id": "1284-1181-0003",
"ref": "THE HAIR WAS OF BROWN YARN AND HUNG DOWN ON HER NECK IN SEVERAL NEAT BRAIDS",
"hyp": "The hair was of brown yarn and hung down on her neck in several neat braids.",
"ref_norm": "THE HAIR WAS OF BROWN YARN AND HUNG DOWN ON HER NECK IN SEVERAL NEAT BRAIDS",
"hyp_norm": "THE HAIR WAS OF BROWN YARN AND HUNG DOWN ON HER NECK IN SEVERAL NEAT BRAIDS",
"duration_s": 4.505,
"infer_time_s": 1.336,
"rtf": 0.2966,
"wer": 0.0
},
{
"id": "1284-1181-0004",
"ref": "GOLD IS THE MOST COMMON METAL IN THE LAND OF OZ AND IS USED FOR MANY PURPOSES BECAUSE IT IS SOFT AND PLIABLE",
"hyp": "Gold is the most common metal in the land of Oz , and is used for many purposes because it is soft and pliable.",
"ref_norm": "GOLD IS THE MOST COMMON METAL IN THE LAND OF OZ AND IS USED FOR MANY PURPOSES BECAUSE IT IS SOFT AND PLIABLE",
"hyp_norm": "GOLD IS THE MOST COMMON METAL IN THE LAND OF OZ AND IS USED FOR MANY PURPOSES BECAUSE IT IS SOFT AND PLIABLE",
"duration_s": 7.15,
"infer_time_s": 1.914,
"rtf": 0.2676,
"wer": 0.0
},
{
"id": "1284-1181-0005",
"ref": "NO I FORGOT ALL ABOUT THE BRAINS EXCLAIMED THE WOMAN",
"hyp": "No, I forgot all about the brains! Exclaimed the woman.",
"ref_norm": "NO I FORGOT ALL ABOUT THE BRAINS EXCLAIMED THE WOMAN",
"hyp_norm": "NO I FORGOT ALL ABOUT THE BRAINS EXCLAIMED THE WOMAN",
"duration_s": 3.855,
"infer_time_s": 0.997,
"rtf": 0.2587,
"wer": 0.0
},
{
"id": "1284-1181-0006",
"ref": "WELL THAT MAY BE TRUE AGREED MARGOLOTTE BUT ON THE CONTRARY A SERVANT WITH TOO MUCH BRAINS IS SURE TO BECOME INDEPENDENT AND HIGH AND MIGHTY AND FEEL ABOVE HER WORK",
"hyp": "Well, that may be true. Agreed, Marg olot, but on the contrary, a servant with too much brains is sure to become independent and high and mighty and feel above her work.",
"ref_norm": "WELL THAT MAY BE TRUE AGREED MARGOLOTTE BUT ON THE CONTRARY A SERVANT WITH TOO MUCH BRAINS IS SURE TO BECOME INDEPENDENT AND HIGH AND MIGHTY AND FEEL ABOVE HER WORK",
"hyp_norm": "WELL THAT MAY BE TRUE AGREED MARG OLOT BUT ON THE CONTRARY A SERVANT WITH TOO MUCH BRAINS IS SURE TO BECOME INDEPENDENT AND HIGH AND MIGHTY AND FEEL ABOVE HER WORK",
"duration_s": 11.405,
"infer_time_s": 2.995,
"rtf": 0.2626,
"wer": 0.0645
},
{
"id": "1284-1181-0007",
"ref": "SHE POURED INTO THE DISH A QUANTITY FROM EACH OF THESE BOTTLES",
"hyp": "She poured into the dish a quantity from each of these bottles.",
"ref_norm": "SHE POURED INTO THE DISH A QUANTITY FROM EACH OF THESE BOTTLES",
"hyp_norm": "SHE POURED INTO THE DISH A QUANTITY FROM EACH OF THESE BOTTLES",
"duration_s": 4.04,
"infer_time_s": 1.08,
"rtf": 0.2673,
"wer": 0.0
},
{
"id": "1284-1181-0008",
"ref": "I THINK THAT WILL DO SHE CONTINUED FOR THE OTHER QUALITIES ARE NOT NEEDED IN A SERVANT",
"hyp": "I think that will do ,\" she continued, \" for the other qualities are not needed in a servant.\"",
"ref_norm": "I THINK THAT WILL DO SHE CONTINUED FOR THE OTHER QUALITIES ARE NOT NEEDED IN A SERVANT",
"hyp_norm": "I THINK THAT WILL DO SHE CONTINUED FOR THE OTHER QUALITIES ARE NOT NEEDED IN A SERVANT",
"duration_s": 6.08,
"infer_time_s": 1.63,
"rtf": 0.2681,
"wer": 0.0
},
{
"id": "1284-1181-0009",
"ref": "SHE RAN TO HER HUSBAND'S SIDE AT ONCE AND HELPED HIM LIFT THE FOUR KETTLES FROM THE FIRE",
"hyp": "She ran to her husband's side at once and helped him lift the four kettles from the fire.",
"ref_norm": "SHE RAN TO HER HUSBANDS SIDE AT ONCE AND HELPED HIM LIFT THE FOUR KETTLES FROM THE FIRE",
"hyp_norm": "SHE RAN TO HER HUSBANDS SIDE AT ONCE AND HELPED HIM LIFT THE FOUR KETTLES FROM THE FIRE",
"duration_s": 5.245,
"infer_time_s": 1.549,
"rtf": 0.2954,
"wer": 0.0
},
{
"id": "1284-1181-0010",
"ref": "THEIR CONTENTS HAD ALL BOILED AWAY LEAVING IN THE BOTTOM OF EACH KETTLE A FEW GRAINS OF FINE WHITE POWDER",
"hyp": "Their contents had all boiled away, leaving in the bottom of each kettle a few grains of fine white powder.",
"ref_norm": "THEIR CONTENTS HAD ALL BOILED AWAY LEAVING IN THE BOTTOM OF EACH KETTLE A FEW GRAINS OF FINE WHITE POWDER",
"hyp_norm": "THEIR CONTENTS HAD ALL BOILED AWAY LEAVING IN THE BOTTOM OF EACH KETTLE A FEW GRAINS OF FINE WHITE POWDER",
"duration_s": 6.435,
"infer_time_s": 1.711,
"rtf": 0.2659,
"wer": 0.0
},
{
"id": "1284-1181-0011",
"ref": "VERY CAREFULLY THE MAGICIAN REMOVED THIS POWDER PLACING IT ALL TOGETHER IN A GOLDEN DISH WHERE HE MIXED IT WITH A GOLDEN SPOON",
"hyp": "Very carefully, the magician removed this powder , placing it all together in a golden dish. Where he mixed it with a golden spoon.",
"ref_norm": "VERY CAREFULLY THE MAGICIAN REMOVED THIS POWDER PLACING IT ALL TOGETHER IN A GOLDEN DISH WHERE HE MIXED IT WITH A GOLDEN SPOON",
"hyp_norm": "VERY CAREFULLY THE MAGICIAN REMOVED THIS POWDER PLACING IT ALL TOGETHER IN A GOLDEN DISH WHERE HE MIXED IT WITH A GOLDEN SPOON",
"duration_s": 7.75,
"infer_time_s": 1.966,
"rtf": 0.2536,
"wer": 0.0
},
{
"id": "1284-1181-0012",
"ref": "NO ONE SAW HIM DO THIS FOR ALL WERE LOOKING AT THE POWDER OF LIFE BUT SOON THE WOMAN REMEMBERED WHAT SHE HAD BEEN DOING AND CAME BACK TO THE CUPBOARD",
"hyp": "No one saw him do this . For all were looking at the powder of life, but soon the woman remembered what she had been doing and came back to the cupboard.",
"ref_norm": "NO ONE SAW HIM DO THIS FOR ALL WERE LOOKING AT THE POWDER OF LIFE BUT SOON THE WOMAN REMEMBERED WHAT SHE HAD BEEN DOING AND CAME BACK TO THE CUPBOARD",
"hyp_norm": "NO ONE SAW HIM DO THIS FOR ALL WERE LOOKING AT THE POWDER OF LIFE BUT SOON THE WOMAN REMEMBERED WHAT SHE HAD BEEN DOING AND CAME BACK TO THE CUPBOARD",
"duration_s": 8.51,
"infer_time_s": 2.475,
"rtf": 0.2908,
"wer": 0.0
},
{
"id": "1284-1181-0013",
"ref": "OJO BECAME A BIT UNEASY AT THIS FOR HE HAD ALREADY PUT QUITE A LOT OF THE CLEVERNESS POWDER IN THE DISH BUT HE DARED NOT INTERFERE AND SO HE COMFORTED HIMSELF WITH THE THOUGHT THAT ONE CANNOT HAVE TOO MUCH CLEVERNESS",
"hyp": "Ojo became a bit uneasy at this, for he had already put quite a lot of the cleverness powder in the dish , but he dared not interfere, and so he comforted himself with the thought that one cannot have too much cleverness.",
"ref_norm": "OJO BECAME A BIT UNEASY AT THIS FOR HE HAD ALREADY PUT QUITE A LOT OF THE CLEVERNESS POWDER IN THE DISH BUT HE DARED NOT INTERFERE AND SO HE COMFORTED HIMSELF WITH THE THOUGHT THAT ONE CANNOT HAVE TOO MUCH CLEVERNESS",
"hyp_norm": "OJO BECAME A BIT UNEASY AT THIS FOR HE HAD ALREADY PUT QUITE A LOT OF THE CLEVERNESS POWDER IN THE DISH BUT HE DARED NOT INTERFERE AND SO HE COMFORTED HIMSELF WITH THE THOUGHT THAT ONE CANNOT HAVE TOO MUCH CLEVERNESS",
"duration_s": 12.66,
"infer_time_s": 3.717,
"rtf": 0.2936,
"wer": 0.0
},
{
"id": "1284-1181-0014",
"ref": "HE SELECTED A SMALL GOLD BOTTLE WITH A PEPPER BOX TOP SO THAT THE POWDER MIGHT BE SPRINKLED ON ANY OBJECT THROUGH THE SMALL HOLES",
"hyp": "He selected a small gold bottle with a pepper box top, so that the powder might be sprinkled on any object through the small holes.",
"ref_norm": "HE SELECTED A SMALL GOLD BOTTLE WITH A PEPPER BOX TOP SO THAT THE POWDER MIGHT BE SPRINKLED ON ANY OBJECT THROUGH THE SMALL HOLES",
"hyp_norm": "HE SELECTED A SMALL GOLD BOTTLE WITH A PEPPER BOX TOP SO THAT THE POWDER MIGHT BE SPRINKLED ON ANY OBJECT THROUGH THE SMALL HOLES",
"duration_s": 7.92,
"infer_time_s": 2.027,
"rtf": 0.256,
"wer": 0.0
},
{
"id": "1284-1181-0015",
"ref": "MOST PEOPLE TALK TOO MUCH SO IT IS A RELIEF TO FIND ONE WHO TALKS TOO LITTLE",
"hyp": "Most people talk too much, so it is a relief to find one who talks too little.",
"ref_norm": "MOST PEOPLE TALK TOO MUCH SO IT IS A RELIEF TO FIND ONE WHO TALKS TOO LITTLE",
"hyp_norm": "MOST PEOPLE TALK TOO MUCH SO IT IS A RELIEF TO FIND ONE WHO TALKS TOO LITTLE",
"duration_s": 5.115,
"infer_time_s": 1.39,
"rtf": 0.2717,
"wer": 0.0
},
{
"id": "1284-1181-0016",
"ref": "I AM NOT ALLOWED TO PERFORM MAGIC EXCEPT FOR MY OWN AMUSEMENT HE TOLD HIS VISITORS AS HE LIGHTED A PIPE WITH A CROOKED STEM AND BEGAN TO SMOKE",
"hyp": "I am not allowed to perform magic except for my own amusement. He told his visitors as he lighted a pipe with a crooked stem and began to smoke.",
"ref_norm": "I AM NOT ALLOWED TO PERFORM MAGIC EXCEPT FOR MY OWN AMUSEMENT HE TOLD HIS VISITORS AS HE LIGHTED A PIPE WITH A CROOKED STEM AND BEGAN TO SMOKE",
"hyp_norm": "I AM NOT ALLOWED TO PERFORM MAGIC EXCEPT FOR MY OWN AMUSEMENT HE TOLD HIS VISITORS AS HE LIGHTED A PIPE WITH A CROOKED STEM AND BEGAN TO SMOKE",
"duration_s": 9.515,
"infer_time_s": 2.432,
"rtf": 0.2556,
"wer": 0.0
},
{
"id": "1284-1181-0017",
"ref": "THE WIZARD OF OZ WHO USED TO BE A HUMBUG AND KNEW NO MAGIC AT ALL HAS BEEN TAKING LESSONS OF GLINDA AND I'M TOLD HE IS GETTING TO BE A PRETTY GOOD WIZARD BUT HE IS MERELY THE ASSISTANT OF THE GREAT SORCERESS",
"hyp": "The Wizard of Oz, who used to be a humbug and knew no magic at all , has been taking lessons of Gl inda, and I'm told he is getting to be a pretty good wizard, but he is merely the assistant of the great sorceress.",
"ref_norm": "THE WIZARD OF OZ WHO USED TO BE A HUMBUG AND KNEW NO MAGIC AT ALL HAS BEEN TAKING LESSONS OF GLINDA AND IM TOLD HE IS GETTING TO BE A PRETTY GOOD WIZARD BUT HE IS MERELY THE ASSISTANT OF THE GREAT SORCERESS",
"hyp_norm": "THE WIZARD OF OZ WHO USED TO BE A HUMBUG AND KNEW NO MAGIC AT ALL HAS BEEN TAKING LESSONS OF GL INDA AND IM TOLD HE IS GETTING TO BE A PRETTY GOOD WIZARD BUT HE IS MERELY THE ASSISTANT OF THE GREAT SORCERESS",
"duration_s": 11.775,
"infer_time_s": 3.666,
"rtf": 0.3113,
"wer": 0.0455
},
{
"id": "1284-1181-0018",
"ref": "IT TRULY IS ASSERTED THE MAGICIAN",
"hyp": "It truly is asserted the magician.",
"ref_norm": "IT TRULY IS ASSERTED THE MAGICIAN",
"hyp_norm": "IT TRULY IS ASSERTED THE MAGICIAN",
"duration_s": 3.16,
"infer_time_s": 0.645,
"rtf": 0.2043,
"wer": 0.0
},
{
"id": "1284-1181-0019",
"ref": "I NOW USE THEM AS ORNAMENTAL STATUARY IN MY GARDEN",
"hyp": "I now use them as ornamental statuary in my garden.",
"ref_norm": "I NOW USE THEM AS ORNAMENTAL STATUARY IN MY GARDEN",
"hyp_norm": "I NOW USE THEM AS ORNAMENTAL STATUARY IN MY GARDEN",
"duration_s": 3.2,
"infer_time_s": 0.98,
"rtf": 0.3061,
"wer": 0.0
},
{
"id": "1284-1181-0020",
"ref": "DEAR ME WHAT A CHATTERBOX YOU'RE GETTING TO BE UNC REMARKED THE MAGICIAN WHO WAS PLEASED WITH THE COMPLIMENT",
"hyp": "Dear me! What a chatterbox you're getting to be , Unc,\" remarked the magician, who was pleased with the compliment.",
"ref_norm": "DEAR ME WHAT A CHATTERBOX YOURE GETTING TO BE UNC REMARKED THE MAGICIAN WHO WAS PLEASED WITH THE COMPLIMENT",
"hyp_norm": "DEAR ME WHAT A CHATTERBOX YOURE GETTING TO BE UNC REMARKED THE MAGICIAN WHO WAS PLEASED WITH THE COMPLIMENT",
"duration_s": 6.73,
"infer_time_s": 1.964,
"rtf": 0.2919,
"wer": 0.0
},
{
"id": "1284-1181-0021",
"ref": "ASKED THE VOICE IN SCORNFUL ACCENTS",
"hyp": "Asked the voice in scornful accents.",
"ref_norm": "ASKED THE VOICE IN SCORNFUL ACCENTS",
"hyp_norm": "ASKED THE VOICE IN SCORNFUL ACCENTS",
"duration_s": 2.7,
"infer_time_s": 0.709,
"rtf": 0.2625,
"wer": 0.0
},
{
"id": "1284-134647-0000",
"ref": "THE GRATEFUL APPLAUSE OF THE CLERGY HAS CONSECRATED THE MEMORY OF A PRINCE WHO INDULGED THEIR PASSIONS AND PROMOTED THEIR INTEREST",
"hyp": "The grateful applause of the clergy has consecrated the memory of a prince who indulged their passions and promoted their interest.",
"ref_norm": "THE GRATEFUL APPLAUSE OF THE CLERGY HAS CONSECRATED THE MEMORY OF A PRINCE WHO INDULGED THEIR PASSIONS AND PROMOTED THEIR INTEREST",
"hyp_norm": "THE GRATEFUL APPLAUSE OF THE CLERGY HAS CONSECRATED THE MEMORY OF A PRINCE WHO INDULGED THEIR PASSIONS AND PROMOTED THEIR INTEREST",
"duration_s": 8.53,
"infer_time_s": 2.037,
"rtf": 0.2388,
"wer": 0.0
},
{
"id": "1284-134647-0001",
"ref": "THE EDICT OF MILAN THE GREAT CHARTER OF TOLERATION HAD CONFIRMED TO EACH INDIVIDUAL OF THE ROMAN WORLD THE PRIVILEGE OF CHOOSING AND PROFESSING HIS OWN RELIGION",
"hyp": "The Edict of Milan , the Great Charter of Tolerance, had confirmed to each individual of the Roman world the privilege of choosing and professing his own religion.",
"ref_norm": "THE EDICT OF MILAN THE GREAT CHARTER OF TOLERATION HAD CONFIRMED TO EACH INDIVIDUAL OF THE ROMAN WORLD THE PRIVILEGE OF CHOOSING AND PROFESSING HIS OWN RELIGION",
"hyp_norm": "THE EDICT OF MILAN THE GREAT CHARTER OF TOLERANCE HAD CONFIRMED TO EACH INDIVIDUAL OF THE ROMAN WORLD THE PRIVILEGE OF CHOOSING AND PROFESSING HIS OWN RELIGION",
"duration_s": 10.275,
"infer_time_s": 2.695,
"rtf": 0.2623,
"wer": 0.037
},
{
"id": "1284-134647-0002",
"ref": "BUT THIS INESTIMABLE PRIVILEGE WAS SOON VIOLATED WITH THE KNOWLEDGE OF TRUTH THE EMPEROR IMBIBED THE MAXIMS OF PERSECUTION AND THE SECTS WHICH DISSENTED FROM THE CATHOLIC CHURCH WERE AFFLICTED AND OPPRESSED BY THE TRIUMPH OF CHRISTIANITY",
"hyp": "But this inestimable privilege was soon violated with the knowledge of truth. The emperor im bibed the maxims of persecution , and the sects which descended from the Catholic Church were afflicted and oppressed by the triumph of Christianity.",
"ref_norm": "BUT THIS INESTIMABLE PRIVILEGE WAS SOON VIOLATED WITH THE KNOWLEDGE OF TRUTH THE EMPEROR IMBIBED THE MAXIMS OF PERSECUTION AND THE SECTS WHICH DISSENTED FROM THE CATHOLIC CHURCH WERE AFFLICTED AND OPPRESSED BY THE TRIUMPH OF CHRISTIANITY",
"hyp_norm": "BUT THIS INESTIMABLE PRIVILEGE WAS SOON VIOLATED WITH THE KNOWLEDGE OF TRUTH THE EMPEROR IM BIBED THE MAXIMS OF PERSECUTION AND THE SECTS WHICH DESCENDED FROM THE CATHOLIC CHURCH WERE AFFLICTED AND OPPRESSED BY THE TRIUMPH OF CHRISTIANITY",
"duration_s": 15.11,
"infer_time_s": 3.816,
"rtf": 0.2525,
"wer": 0.0811
},
{
"id": "1284-134647-0003",
"ref": "CONSTANTINE EASILY BELIEVED THAT THE HERETICS WHO PRESUMED TO DISPUTE HIS OPINIONS OR TO OPPOSE HIS COMMANDS WERE GUILTY OF THE MOST ABSURD AND CRIMINAL OBSTINACY AND THAT A SEASONABLE APPLICATION OF MODERATE SEVERITIES MIGHT SAVE THOSE UNHAPPY MEN FROM THE DANGER OF AN EVERLASTING CONDEMNATION",
"hyp": "Constantine easily believed that the heretics who presumed to dispute his opinions or to oppose his commands were guilty of the most absurd and criminal obstinacy, and that a seasonable application of moderate sever ities might save those unhappy men from the danger of an everlasting condemnation.",
"ref_norm": "CONSTANTINE EASILY BELIEVED THAT THE HERETICS WHO PRESUMED TO DISPUTE HIS OPINIONS OR TO OPPOSE HIS COMMANDS WERE GUILTY OF THE MOST ABSURD AND CRIMINAL OBSTINACY AND THAT A SEASONABLE APPLICATION OF MODERATE SEVERITIES MIGHT SAVE THOSE UNHAPPY MEN FROM THE DANGER OF AN EVERLASTING CONDEMNATION",
"hyp_norm": "CONSTANTINE EASILY BELIEVED THAT THE HERETICS WHO PRESUMED TO DISPUTE HIS OPINIONS OR TO OPPOSE HIS COMMANDS WERE GUILTY OF THE MOST ABSURD AND CRIMINAL OBSTINACY AND THAT A SEASONABLE APPLICATION OF MODERATE SEVER ITIES MIGHT SAVE THOSE UNHAPPY MEN FROM THE DANGER OF AN EVERLASTING CONDEMNATION",
"duration_s": 20.145,
"infer_time_s": 4.639,
"rtf": 0.2303,
"wer": 0.0435
},
{
"id": "1284-134647-0004",
"ref": "SOME OF THE PENAL REGULATIONS WERE COPIED FROM THE EDICTS OF DIOCLETIAN AND THIS METHOD OF CONVERSION WAS APPLAUDED BY THE SAME BISHOPS WHO HAD FELT THE HAND OF OPPRESSION AND PLEADED FOR THE RIGHTS OF HUMANITY",
"hyp": "Some of the penal regulations were copied from the edicts of Diocletian, and this method of conversion was applauded by the same bishops who had felt the hand of oppression and pleaded for the rights of humanity.",
"ref_norm": "SOME OF THE PENAL REGULATIONS WERE COPIED FROM THE EDICTS OF DIOCLETIAN AND THIS METHOD OF CONVERSION WAS APPLAUDED BY THE SAME BISHOPS WHO HAD FELT THE HAND OF OPPRESSION AND PLEADED FOR THE RIGHTS OF HUMANITY",
"hyp_norm": "SOME OF THE PENAL REGULATIONS WERE COPIED FROM THE EDICTS OF DIOCLETIAN AND THIS METHOD OF CONVERSION WAS APPLAUDED BY THE SAME BISHOPS WHO HAD FELT THE HAND OF OPPRESSION AND PLEADED FOR THE RIGHTS OF HUMANITY",
"duration_s": 12.835,
"infer_time_s": 3.362,
"rtf": 0.2619,
"wer": 0.0
},
{
"id": "1284-134647-0005",
"ref": "THEY ASSERTED WITH CONFIDENCE AND ALMOST WITH EXULTATION THAT THE APOSTOLICAL SUCCESSION WAS INTERRUPTED THAT ALL THE BISHOPS OF EUROPE AND ASIA WERE INFECTED BY THE CONTAGION OF GUILT AND SCHISM AND THAT THE PREROGATIVES OF THE CATHOLIC CHURCH WERE CONFINED TO THE CHOSEN PORTION OF THE AFRICAN BELIEVERS WHO ALONE HAD PRESERVED INVIOLATE THE INTEGRITY OF THEIR FAITH AND DISCIPLINE",
"hyp": "They asserted with confidence and almost with ex ultation that the apostolic succession was interrupted, that all the bishops of Europe and Asia were infected by the contagion of guilt and schism , and that the prerogatives of the Catholic Church were confined to the chosen portion of the African believers, who alone had preserved inviolate the integrity of their faith and discipline.",
"ref_norm": "THEY ASSERTED WITH CONFIDENCE AND ALMOST WITH EXULTATION THAT THE APOSTOLICAL SUCCESSION WAS INTERRUPTED THAT ALL THE BISHOPS OF EUROPE AND ASIA WERE INFECTED BY THE CONTAGION OF GUILT AND SCHISM AND THAT THE PREROGATIVES OF THE CATHOLIC CHURCH WERE CONFINED TO THE CHOSEN PORTION OF THE AFRICAN BELIEVERS WHO ALONE HAD PRESERVED INVIOLATE THE INTEGRITY OF THEIR FAITH AND DISCIPLINE",
"hyp_norm": "THEY ASSERTED WITH CONFIDENCE AND ALMOST WITH EX ULTATION THAT THE APOSTOLIC SUCCESSION WAS INTERRUPTED THAT ALL THE BISHOPS OF EUROPE AND ASIA WERE INFECTED BY THE CONTAGION OF GUILT AND SCHISM AND THAT THE PREROGATIVES OF THE CATHOLIC CHURCH WERE CONFINED TO THE CHOSEN PORTION OF THE AFRICAN BELIEVERS WHO ALONE HAD PRESERVED INVIOLATE THE INTEGRITY OF THEIR FAITH AND DISCIPLINE",
"duration_s": 23.335,
"infer_time_s": 5.753,
"rtf": 0.2466,
"wer": 0.0492
},
{
"id": "1284-134647-0006",
"ref": "BISHOPS VIRGINS AND EVEN SPOTLESS INFANTS WERE SUBJECTED TO THE DISGRACE OF A PUBLIC PENANCE BEFORE THEY COULD BE ADMITTED TO THE COMMUNION OF THE DONATISTS",
"hyp": "Bishops, virg ins, and even spotless infants were subjected to the disgrace of a public penance before they could be admitted to the communion of the donatists.",
"ref_norm": "BISHOPS VIRGINS AND EVEN SPOTLESS INFANTS WERE SUBJECTED TO THE DISGRACE OF A PUBLIC PENANCE BEFORE THEY COULD BE ADMITTED TO THE COMMUNION OF THE DONATISTS",
"hyp_norm": "BISHOPS VIRG INS AND EVEN SPOTLESS INFANTS WERE SUBJECTED TO THE DISGRACE OF A PUBLIC PENANCE BEFORE THEY COULD BE ADMITTED TO THE COMMUNION OF THE DONATISTS",
"duration_s": 10.155,
"infer_time_s": 2.791,
"rtf": 0.2749,
"wer": 0.0769
},
{
"id": "1284-134647-0007",
"ref": "PROSCRIBED BY THE CIVIL AND ECCLESIASTICAL POWERS OF THE EMPIRE THE DONATISTS STILL MAINTAINED IN SOME PROVINCES PARTICULARLY IN NUMIDIA THEIR SUPERIOR NUMBERS AND FOUR HUNDRED BISHOPS ACKNOWLEDGED THE JURISDICTION OF THEIR PRIMATE",
"hyp": "Proscribed by the civil and ecclesiastical powers of the empire, the Donat ists still maintained in some provinces, particularly in Numidia, their superior numbers, and four hundred bishops acknowledged the jurisdiction of their primate.",
"ref_norm": "PROSCRIBED BY THE CIVIL AND ECCLESIASTICAL POWERS OF THE EMPIRE THE DONATISTS STILL MAINTAINED IN SOME PROVINCES PARTICULARLY IN NUMIDIA THEIR SUPERIOR NUMBERS AND FOUR HUNDRED BISHOPS ACKNOWLEDGED THE JURISDICTION OF THEIR PRIMATE",
"hyp_norm": "PROSCRIBED BY THE CIVIL AND ECCLESIASTICAL POWERS OF THE EMPIRE THE DONAT ISTS STILL MAINTAINED IN SOME PROVINCES PARTICULARLY IN NUMIDIA THEIR SUPERIOR NUMBERS AND FOUR HUNDRED BISHOPS ACKNOWLEDGED THE JURISDICTION OF THEIR PRIMATE",
"duration_s": 14.17,
"infer_time_s": 3.659,
"rtf": 0.2582,
"wer": 0.0606
},
{
"id": "1320-122612-0000",
"ref": "SINCE THE PERIOD OF OUR TALE THE ACTIVE SPIRIT OF THE COUNTRY HAS SURROUNDED IT WITH A BELT OF RICH AND THRIVING SETTLEMENTS THOUGH NONE BUT THE HUNTER OR THE SAVAGE IS EVER KNOWN EVEN NOW TO PENETRATE ITS WILD RECESSES",
"hyp": "Since the period of our tale, the active spirit of the country has surrounded it with a belt of rich and thriving settlements. Though none but the hunter or the savage is ever known even now to penetrate its wild recesses.",
"ref_norm": "SINCE THE PERIOD OF OUR TALE THE ACTIVE SPIRIT OF THE COUNTRY HAS SURROUNDED IT WITH A BELT OF RICH AND THRIVING SETTLEMENTS THOUGH NONE BUT THE HUNTER OR THE SAVAGE IS EVER KNOWN EVEN NOW TO PENETRATE ITS WILD RECESSES",
"hyp_norm": "SINCE THE PERIOD OF OUR TALE THE ACTIVE SPIRIT OF THE COUNTRY HAS SURROUNDED IT WITH A BELT OF RICH AND THRIVING SETTLEMENTS THOUGH NONE BUT THE HUNTER OR THE SAVAGE IS EVER KNOWN EVEN NOW TO PENETRATE ITS WILD RECESSES",
"duration_s": 13.48,
"infer_time_s": 3.447,
"rtf": 0.2557,
"wer": 0.0
},
{
"id": "1320-122612-0001",
"ref": "THE DEWS WERE SUFFERED TO EXHALE AND THE SUN HAD DISPERSED THE MISTS AND WAS SHEDDING A STRONG AND CLEAR LIGHT IN THE FOREST WHEN THE TRAVELERS RESUMED THEIR JOURNEY",
"hyp": "The dews were suffered to exhal, and the sun had dispersed the mists and was shedding a strong and clear light in the forests when the travellers resumed their journey.",
"ref_norm": "THE DEWS WERE SUFFERED TO EXHALE AND THE SUN HAD DISPERSED THE MISTS AND WAS SHEDDING A STRONG AND CLEAR LIGHT IN THE FOREST WHEN THE TRAVELERS RESUMED THEIR JOURNEY",
"hyp_norm": "THE DEWS WERE SUFFERED TO EXHAL AND THE SUN HAD DISPERSED THE MISTS AND WAS SHEDDING A STRONG AND CLEAR LIGHT IN THE FORESTS WHEN THE TRAVELLERS RESUMED THEIR JOURNEY",
"duration_s": 9.52,
"infer_time_s": 2.61,
"rtf": 0.2742,
"wer": 0.1
},
{
"id": "1320-122612-0002",
"ref": "AFTER PROCEEDING A FEW MILES THE PROGRESS OF HAWKEYE WHO LED THE ADVANCE BECAME MORE DELIBERATE AND WATCHFUL",
"hyp": "After proceeding a few miles, the progress of Hawkeye, who led the advance , became more deliberate and watchful.",
"ref_norm": "AFTER PROCEEDING A FEW MILES THE PROGRESS OF HAWKEYE WHO LED THE ADVANCE BECAME MORE DELIBERATE AND WATCHFUL",
"hyp_norm": "AFTER PROCEEDING A FEW MILES THE PROGRESS OF HAWKEYE WHO LED THE ADVANCE BECAME MORE DELIBERATE AND WATCHFUL",
"duration_s": 7.46,
"infer_time_s": 1.928,
"rtf": 0.2585,
"wer": 0.0
},
{
"id": "1320-122612-0003",
"ref": "HE OFTEN STOPPED TO EXAMINE THE TREES NOR DID HE CROSS A RIVULET WITHOUT ATTENTIVELY CONSIDERING THE QUANTITY THE VELOCITY AND THE COLOR OF ITS WATERS",
"hyp": "He often stopped to examine the trees , nor did he cross a rivulet without attentively considering the quantity, the velocity, and the color of its waters.",
"ref_norm": "HE OFTEN STOPPED TO EXAMINE THE TREES NOR DID HE CROSS A RIVULET WITHOUT ATTENTIVELY CONSIDERING THE QUANTITY THE VELOCITY AND THE COLOR OF ITS WATERS",
"hyp_norm": "HE OFTEN STOPPED TO EXAMINE THE TREES NOR DID HE CROSS A RIVULET WITHOUT ATTENTIVELY CONSIDERING THE QUANTITY THE VELOCITY AND THE COLOR OF ITS WATERS",
"duration_s": 9.865,
"infer_time_s": 2.42,
"rtf": 0.2453,
"wer": 0.0
},
{
"id": "1320-122612-0004",
"ref": "DISTRUSTING HIS OWN JUDGMENT HIS APPEALS TO THE OPINION OF CHINGACHGOOK WERE FREQUENT AND EARNEST",
"hyp": "Distrusting his own judgment, his appeals to the opinion of Chingachgook were frequent and earnest.",
"ref_norm": "DISTRUSTING HIS OWN JUDGMENT HIS APPEALS TO THE OPINION OF CHINGACHGOOK WERE FREQUENT AND EARNEST",
"hyp_norm": "DISTRUSTING HIS OWN JUDGMENT HIS APPEALS TO THE OPINION OF CHINGACHGOOK WERE FREQUENT AND EARNEST",
"duration_s": 6.425,
"infer_time_s": 1.745,
"rtf": 0.2716,
"wer": 0.0
},
{
"id": "1320-122612-0005",
"ref": "YET HERE ARE WE WITHIN A SHORT RANGE OF THE SCAROONS AND NOT A SIGN OF A TRAIL HAVE WE CROSSED",
"hyp": "Yet here are we within a short range of the scar oons, and not a sign of a trail have we crossed.",
"ref_norm": "YET HERE ARE WE WITHIN A SHORT RANGE OF THE SCAROONS AND NOT A SIGN OF A TRAIL HAVE WE CROSSED",
"hyp_norm": "YET HERE ARE WE WITHIN A SHORT RANGE OF THE SCAR OONS AND NOT A SIGN OF A TRAIL HAVE WE CROSSED",
"duration_s": 5.915,
"infer_time_s": 1.646,
"rtf": 0.2783,
"wer": 0.0952
},
{
"id": "1320-122612-0006",
"ref": "LET US RETRACE OUR STEPS AND EXAMINE AS WE GO WITH KEENER EYES",
"hyp": "Let us retrace our steps and examine as we go with keener eyes.",
"ref_norm": "LET US RETRACE OUR STEPS AND EXAMINE AS WE GO WITH KEENER EYES",
"hyp_norm": "LET US RETRACE OUR STEPS AND EXAMINE AS WE GO WITH KEENER EYES",
"duration_s": 4.845,
"infer_time_s": 1.249,
"rtf": 0.2578,
"wer": 0.0
},
{
"id": "1320-122612-0007",
"ref": "CHINGACHGOOK HAD CAUGHT THE LOOK AND MOTIONING WITH HIS HAND HE BADE HIM SPEAK",
"hyp": "Chingachgook had caught the look, and motioning with his hand, he bade him speak.",
"ref_norm": "CHINGACHGOOK HAD CAUGHT THE LOOK AND MOTIONING WITH HIS HAND HE BADE HIM SPEAK",
"hyp_norm": "CHINGACHGOOK HAD CAUGHT THE LOOK AND MOTIONING WITH HIS HAND HE BADE HIM SPEAK",
"duration_s": 5.54,
"infer_time_s": 1.611,
"rtf": 0.2909,
"wer": 0.0
},
{
"id": "1320-122612-0008",
"ref": "THE EYES OF THE WHOLE PARTY FOLLOWED THE UNEXPECTED MOVEMENT AND READ THEIR SUCCESS IN THE AIR OF TRIUMPH THAT THE YOUTH ASSUMED",
"hyp": "The eyes of the whole party followed the unexpected movement and read their success in the air of triumph that the youth assumed.",
"ref_norm": "THE EYES OF THE WHOLE PARTY FOLLOWED THE UNEXPECTED MOVEMENT AND READ THEIR SUCCESS IN THE AIR OF TRIUMPH THAT THE YOUTH ASSUMED",
"hyp_norm": "THE EYES OF THE WHOLE PARTY FOLLOWED THE UNEXPECTED MOVEMENT AND READ THEIR SUCCESS IN THE AIR OF TRIUMPH THAT THE YOUTH ASSUMED",
"duration_s": 7.875,
"infer_time_s": 1.857,
"rtf": 0.2358,
"wer": 0.0
},
{
"id": "1320-122612-0009",
"ref": "IT WOULD HAVE BEEN MORE WONDERFUL HAD HE SPOKEN WITHOUT A BIDDING",
"hyp": "It would have been more wonderful had he spoken without a bidding.",
"ref_norm": "IT WOULD HAVE BEEN MORE WONDERFUL HAD HE SPOKEN WITHOUT A BIDDING",
"hyp_norm": "IT WOULD HAVE BEEN MORE WONDERFUL HAD HE SPOKEN WITHOUT A BIDDING",
"duration_s": 3.88,
"infer_time_s": 0.978,
"rtf": 0.2521,
"wer": 0.0
},
{
"id": "1320-122612-0010",
"ref": "SEE SAID UNCAS POINTING NORTH AND SOUTH AT THE EVIDENT MARKS OF THE BROAD TRAIL ON EITHER SIDE OF HIM THE DARK HAIR HAS GONE TOWARD THE FOREST",
"hyp": "See,\" said Unc as, pointing north and south at the evident marks of the broad trail on either side of him. The dark hair has gone toward the forest.",
"ref_norm": "SEE SAID UNCAS POINTING NORTH AND SOUTH AT THE EVIDENT MARKS OF THE BROAD TRAIL ON EITHER SIDE OF HIM THE DARK HAIR HAS GONE TOWARD THE FOREST",
"hyp_norm": "SEE SAID UNC AS POINTING NORTH AND SOUTH AT THE EVIDENT MARKS OF THE BROAD TRAIL ON EITHER SIDE OF HIM THE DARK HAIR HAS GONE TOWARD THE FOREST",
"duration_s": 10.195,
"infer_time_s": 2.655,
"rtf": 0.2604,
"wer": 0.0714
},
{
"id": "1320-122612-0011",
"ref": "IF A ROCK OR A RIVULET OR A BIT OF EARTH HARDER THAN COMMON SEVERED THE LINKS OF THE CLEW THEY FOLLOWED THE TRUE EYE OF THE SCOUT RECOVERED THEM AT A DISTANCE AND SELDOM RENDERED THE DELAY OF A SINGLE MOMENT NECESSARY",
"hyp": "If a rock or a rivulet or a bit of earth harder than common severed the links of the clue they followed, the true eye of the scout recovered them at a distance and seldom rendered the delay of a single moment necessary.",
"ref_norm": "IF A ROCK OR A RIVULET OR A BIT OF EARTH HARDER THAN COMMON SEVERED THE LINKS OF THE CLEW THEY FOLLOWED THE TRUE EYE OF THE SCOUT RECOVERED THEM AT A DISTANCE AND SELDOM RENDERED THE DELAY OF A SINGLE MOMENT NECESSARY",
"hyp_norm": "IF A ROCK OR A RIVULET OR A BIT OF EARTH HARDER THAN COMMON SEVERED THE LINKS OF THE CLUE THEY FOLLOWED THE TRUE EYE OF THE SCOUT RECOVERED THEM AT A DISTANCE AND SELDOM RENDERED THE DELAY OF A SINGLE MOMENT NECESSARY",
"duration_s": 13.695,
"infer_time_s": 3.523,
"rtf": 0.2573,
"wer": 0.0233
},
{
"id": "1320-122612-0012",
"ref": "EXTINGUISHED BRANDS WERE LYING AROUND A SPRING THE OFFALS OF A DEER WERE SCATTERED ABOUT THE PLACE AND THE TREES BORE EVIDENT MARKS OF HAVING BEEN BROWSED BY THE HORSES",
"hyp": "Extinguished brands were lying around a spring , the offals of a deer were scattered about the place , and the trees bore evident marks of having been browsed by the horses.",
"ref_norm": "EXTINGUISHED BRANDS WERE LYING AROUND A SPRING THE OFFALS OF A DEER WERE SCATTERED ABOUT THE PLACE AND THE TREES BORE EVIDENT MARKS OF HAVING BEEN BROWSED BY THE HORSES",
"hyp_norm": "EXTINGUISHED BRANDS WERE LYING AROUND A SPRING THE OFFALS OF A DEER WERE SCATTERED ABOUT THE PLACE AND THE TREES BORE EVIDENT MARKS OF HAVING BEEN BROWSED BY THE HORSES",
"duration_s": 10.49,
"infer_time_s": 2.899,
"rtf": 0.2764,
"wer": 0.0
},
{
"id": "1320-122612-0013",
"ref": "A CIRCLE OF A FEW HUNDRED FEET IN CIRCUMFERENCE WAS DRAWN AND EACH OF THE PARTY TOOK A SEGMENT FOR HIS PORTION",
"hyp": "A circle of a few hundred feet in circumference was drawn, and each of the party took a segment for his portion.",
"ref_norm": "A CIRCLE OF A FEW HUNDRED FEET IN CIRCUMFERENCE WAS DRAWN AND EACH OF THE PARTY TOOK A SEGMENT FOR HIS PORTION",
"hyp_norm": "A CIRCLE OF A FEW HUNDRED FEET IN CIRCUMFERENCE WAS DRAWN AND EACH OF THE PARTY TOOK A SEGMENT FOR HIS PORTION",
"duration_s": 6.55,
"infer_time_s": 1.796,
"rtf": 0.2743,
"wer": 0.0
},
{
"id": "1320-122612-0014",
"ref": "THE EXAMINATION HOWEVER RESULTED IN NO DISCOVERY",
"hyp": "The examination, however, resulted in no discovery.",
"ref_norm": "THE EXAMINATION HOWEVER RESULTED IN NO DISCOVERY",
"hyp_norm": "THE EXAMINATION HOWEVER RESULTED IN NO DISCOVERY",
"duration_s": 3.515,
"infer_time_s": 0.796,
"rtf": 0.2264,
"wer": 0.0
},
{
"id": "1320-122612-0015",
"ref": "THE WHOLE PARTY CROWDED TO THE SPOT WHERE UNCAS POINTED OUT THE IMPRESSION OF A MOCCASIN IN THE MOIST ALLUVION",
"hyp": "The whole party crowded to the spot where Uncas pointed out the impression of a moccasin in the moist alluvion.",
"ref_norm": "THE WHOLE PARTY CROWDED TO THE SPOT WHERE UNCAS POINTED OUT THE IMPRESSION OF A MOCCASIN IN THE MOIST ALLUVION",
"hyp_norm": "THE WHOLE PARTY CROWDED TO THE SPOT WHERE UNCAS POINTED OUT THE IMPRESSION OF A MOCCASIN IN THE MOIST ALLUVION",
"duration_s": 6.385,
"infer_time_s": 1.949,
"rtf": 0.3053,
"wer": 0.0
},
{
"id": "1320-122612-0016",
"ref": "RUN BACK UNCAS AND BRING ME THE SIZE OF THE SINGER'S FOOT",
"hyp": "Run back, Uncas, and bring me the size of the singer's foot.",
"ref_norm": "RUN BACK UNCAS AND BRING ME THE SIZE OF THE SINGERS FOOT",
"hyp_norm": "RUN BACK UNCAS AND BRING ME THE SIZE OF THE SINGERS FOOT",
"duration_s": 3.49,
"infer_time_s": 1.166,
"rtf": 0.334,
"wer": 0.0
},
{
"id": "1320-122617-0000",
"ref": "NOTWITHSTANDING THE HIGH RESOLUTION OF HAWKEYE HE FULLY COMPREHENDED ALL THE DIFFICULTIES AND DANGER HE WAS ABOUT TO INCUR",
"hyp": "Notwithstanding the high resolution of Hawkeye , he fully comprehended all the difficulties and danger he was about to incur.",
"ref_norm": "NOTWITHSTANDING THE HIGH RESOLUTION OF HAWKEYE HE FULLY COMPREHENDED ALL THE DIFFICULTIES AND DANGER HE WAS ABOUT TO INCUR",
"hyp_norm": "NOTWITHSTANDING THE HIGH RESOLUTION OF HAWKEYE HE FULLY COMPREHENDED ALL THE DIFFICULTIES AND DANGER HE WAS ABOUT TO INCUR",
"duration_s": 7.835,
"infer_time_s": 1.873,
"rtf": 0.2391,
"wer": 0.0
},
{
"id": "1320-122617-0001",
"ref": "IN HIS RETURN TO THE CAMP HIS ACUTE AND PRACTISED INTELLECTS WERE INTENTLY ENGAGED IN DEVISING MEANS TO COUNTERACT A WATCHFULNESS AND SUSPICION ON THE PART OF HIS ENEMIES THAT HE KNEW WERE IN NO DEGREE INFERIOR TO HIS OWN",
"hyp": "In his return to the camp, his acute and practiced intellects were intently engaged in devising means to counteract a watchfulness and suspicion on the part of his enemies , that he knew were in no degree inferior to his own.",
"ref_norm": "IN HIS RETURN TO THE CAMP HIS ACUTE AND PRACTISED INTELLECTS WERE INTENTLY ENGAGED IN DEVISING MEANS TO COUNTERACT A WATCHFULNESS AND SUSPICION ON THE PART OF HIS ENEMIES THAT HE KNEW WERE IN NO DEGREE INFERIOR TO HIS OWN",
"hyp_norm": "IN HIS RETURN TO THE CAMP HIS ACUTE AND PRACTICED INTELLECTS WERE INTENTLY ENGAGED IN DEVISING MEANS TO COUNTERACT A WATCHFULNESS AND SUSPICION ON THE PART OF HIS ENEMIES THAT HE KNEW WERE IN NO DEGREE INFERIOR TO HIS OWN",
"duration_s": 14.055,
"infer_time_s": 3.809,
"rtf": 0.271,
"wer": 0.025
},
{
"id": "1320-122617-0002",
"ref": "IN OTHER WORDS WHILE HE HAD IMPLICIT FAITH IN THE ABILITY OF BALAAM'S ASS TO SPEAK HE WAS SOMEWHAT SKEPTICAL ON THE SUBJECT OF A BEAR'S SINGING AND YET HE HAD BEEN ASSURED OF THE LATTER ON THE TESTIMONY OF HIS OWN EXQUISITE ORGANS",
"hyp": "In other words, while he had implicit faith in the ability of Balaam's ass to speak, he was somewhat skeptical on the subject of a bear's singing, and yet he had been assured of the latter on the testimony of his own exquisite organs.",
"ref_norm": "IN OTHER WORDS WHILE HE HAD IMPLICIT FAITH IN THE ABILITY OF BALAAMS ASS TO SPEAK HE WAS SOMEWHAT SKEPTICAL ON THE SUBJECT OF A BEARS SINGING AND YET HE HAD BEEN ASSURED OF THE LATTER ON THE TESTIMONY OF HIS OWN EXQUISITE ORGANS",
"hyp_norm": "IN OTHER WORDS WHILE HE HAD IMPLICIT FAITH IN THE ABILITY OF BALAAMS ASS TO SPEAK HE WAS SOMEWHAT SKEPTICAL ON THE SUBJECT OF A BEARS SINGING AND YET HE HAD BEEN ASSURED OF THE LATTER ON THE TESTIMONY OF HIS OWN EXQUISITE ORGANS",
"duration_s": 13.585,
"infer_time_s": 3.958,
"rtf": 0.2914,
"wer": 0.0
},
{
"id": "1320-122617-0003",
"ref": "THERE WAS SOMETHING IN HIS AIR AND MANNER THAT BETRAYED TO THE SCOUT THE UTTER CONFUSION OF THE STATE OF HIS MIND",
"hyp": "There was something in his air and manner that betrayed to the scout the utter confusion of the state of his mind.",
"ref_norm": "THERE WAS SOMETHING IN HIS AIR AND MANNER THAT BETRAYED TO THE SCOUT THE UTTER CONFUSION OF THE STATE OF HIS MIND",
"hyp_norm": "THERE WAS SOMETHING IN HIS AIR AND MANNER THAT BETRAYED TO THE SCOUT THE UTTER CONFUSION OF THE STATE OF HIS MIND",
"duration_s": 6.285,
"infer_time_s": 1.857,
"rtf": 0.2955,
"wer": 0.0
},
{
"id": "1320-122617-0004",
"ref": "THE INGENIOUS HAWKEYE WHO RECALLED THE HASTY MANNER IN WHICH THE OTHER HAD ABANDONED HIS POST AT THE BEDSIDE OF THE SICK WOMAN WAS NOT WITHOUT HIS SUSPICIONS CONCERNING THE SUBJECT OF SO MUCH SOLEMN DELIBERATION",
"hyp": "The ingenious hawk eye, who recalled the hasty manner in which the other had abandoned his post at the bedside of the sick woman, was not without his suspicions concerning the subject of so much solemn deliberation.",
"ref_norm": "THE INGENIOUS HAWKEYE WHO RECALLED THE HASTY MANNER IN WHICH THE OTHER HAD ABANDONED HIS POST AT THE BEDSIDE OF THE SICK WOMAN WAS NOT WITHOUT HIS SUSPICIONS CONCERNING THE SUBJECT OF SO MUCH SOLEMN DELIBERATION",
"hyp_norm": "THE INGENIOUS HAWK EYE WHO RECALLED THE HASTY MANNER IN WHICH THE OTHER HAD ABANDONED HIS POST AT THE BEDSIDE OF THE SICK WOMAN WAS NOT WITHOUT HIS SUSPICIONS CONCERNING THE SUBJECT OF SO MUCH SOLEMN DELIBERATION",
"duration_s": 12.26,
"infer_time_s": 3.436,
"rtf": 0.2803,
"wer": 0.0556
},
{
"id": "1320-122617-0005",
"ref": "THE BEAR SHOOK HIS SHAGGY SIDES AND THEN A WELL KNOWN VOICE REPLIED",
"hyp": "The bear shook his sh aggy sides, and then a well -known voice replied.",
"ref_norm": "THE BEAR SHOOK HIS SHAGGY SIDES AND THEN A WELL KNOWN VOICE REPLIED",
"hyp_norm": "THE BEAR SHOOK HIS SH AGGY SIDES AND THEN A WELL KNOWN VOICE REPLIED",
"duration_s": 4.4,
"infer_time_s": 1.282,
"rtf": 0.2913,
"wer": 0.1538
},
{
"id": "1320-122617-0006",
"ref": "CAN THESE THINGS BE RETURNED DAVID BREATHING MORE FREELY AS THE TRUTH BEGAN TO DAWN UPON HIM",
"hyp": "Can these things be ? Returned David, breathing more freely as the truth began to dawn upon him.",
"ref_norm": "CAN THESE THINGS BE RETURNED DAVID BREATHING MORE FREELY AS THE TRUTH BEGAN TO DAWN UPON HIM",
"hyp_norm": "CAN THESE THINGS BE RETURNED DAVID BREATHING MORE FREELY AS THE TRUTH BEGAN TO DAWN UPON HIM",
"duration_s": 5.655,
"infer_time_s": 1.435,
"rtf": 0.2538,
"wer": 0.0
},
{
"id": "1320-122617-0007",
"ref": "COME COME RETURNED HAWKEYE UNCASING HIS HONEST COUNTENANCE THE BETTER TO ASSURE THE WAVERING CONFIDENCE OF HIS COMPANION YOU MAY SEE A SKIN WHICH IF IT BE NOT AS WHITE AS ONE OF THE GENTLE ONES HAS NO TINGE OF RED TO IT THAT THE WINDS OF THE HEAVEN AND THE SUN HAVE NOT BESTOWED NOW LET US TO BUSINESS",
"hyp": "Come, come! Returned Haw keye, uncasing his honest countenance, the better to assure the wavering confidence of his companion. You may see a skin which , if it be not as white as one of the gentle ones, has no tinge of red to it that the winds of the heaven and the sun have not bestowed. Now let us to business.",
"ref_norm": "COME COME RETURNED HAWKEYE UNCASING HIS HONEST COUNTENANCE THE BETTER TO ASSURE THE WAVERING CONFIDENCE OF HIS COMPANION YOU MAY SEE A SKIN WHICH IF IT BE NOT AS WHITE AS ONE OF THE GENTLE ONES HAS NO TINGE OF RED TO IT THAT THE WINDS OF THE HEAVEN AND THE SUN HAVE NOT BESTOWED NOW LET US TO BUSINESS",
"hyp_norm": "COME COME RETURNED HAW KEYE UNCASING HIS HONEST COUNTENANCE THE BETTER TO ASSURE THE WAVERING CONFIDENCE OF HIS COMPANION YOU MAY SEE A SKIN WHICH IF IT BE NOT AS WHITE AS ONE OF THE GENTLE ONES HAS NO TINGE OF RED TO IT THAT THE WINDS OF THE HEAVEN AND THE SUN HAVE NOT BESTOWED NOW LET US TO BUSINESS",
"duration_s": 18.525,
"infer_time_s": 5.397,
"rtf": 0.2914,
"wer": 0.0333
},
{
"id": "1320-122617-0008",
"ref": "THE YOUNG MAN IS IN BONDAGE AND MUCH I FEAR HIS DEATH IS DECREED",
"hyp": "The young man is in bondage, and much I fear his death is decreed.",
"ref_norm": "THE YOUNG MAN IS IN BONDAGE AND MUCH I FEAR HIS DEATH IS DECREED",
"hyp_norm": "THE YOUNG MAN IS IN BONDAGE AND MUCH I FEAR HIS DEATH IS DECREED",
"duration_s": 4.185,
"infer_time_s": 1.283,
"rtf": 0.3067,
"wer": 0.0
},
{
"id": "1320-122617-0009",
"ref": "I GREATLY MOURN THAT ONE SO WELL DISPOSED SHOULD DIE IN HIS IGNORANCE AND I HAVE SOUGHT A GOODLY HYMN CAN YOU LEAD ME TO HIM",
"hyp": "I greatly mourn that one so well disposed should die in his ignorance, and I have sought a goodly him. Can you lead me to him?",
"ref_norm": "I GREATLY MOURN THAT ONE SO WELL DISPOSED SHOULD DIE IN HIS IGNORANCE AND I HAVE SOUGHT A GOODLY HYMN CAN YOU LEAD ME TO HIM",
"hyp_norm": "I GREATLY MOURN THAT ONE SO WELL DISPOSED SHOULD DIE IN HIS IGNORANCE AND I HAVE SOUGHT A GOODLY HIM CAN YOU LEAD ME TO HIM",
"duration_s": 7.705,
"infer_time_s": 2.115,
"rtf": 0.2745,
"wer": 0.0385
},
{
"id": "1320-122617-0010",
"ref": "THE TASK WILL NOT BE DIFFICULT RETURNED DAVID HESITATING THOUGH I GREATLY FEAR YOUR PRESENCE WOULD RATHER INCREASE THAN MITIGATE HIS UNHAPPY FORTUNES",
"hyp": "The task will not be difficult. Returned David , hesitating, though I greatly fear your presence would rather increase than mitigate his unhappy fortunes.",
"ref_norm": "THE TASK WILL NOT BE DIFFICULT RETURNED DAVID HESITATING THOUGH I GREATLY FEAR YOUR PRESENCE WOULD RATHER INCREASE THAN MITIGATE HIS UNHAPPY FORTUNES",
"hyp_norm": "THE TASK WILL NOT BE DIFFICULT RETURNED DAVID HESITATING THOUGH I GREATLY FEAR YOUR PRESENCE WOULD RATHER INCREASE THAN MITIGATE HIS UNHAPPY FORTUNES",
"duration_s": 10.0,
"infer_time_s": 2.193,
"rtf": 0.2193,
"wer": 0.0
},
{
"id": "1320-122617-0011",
"ref": "THE LODGE IN WHICH UNCAS WAS CONFINED WAS IN THE VERY CENTER OF THE VILLAGE AND IN A SITUATION PERHAPS MORE DIFFICULT THAN ANY OTHER TO APPROACH OR LEAVE WITHOUT OBSERVATION",
"hyp": "The lodge in which Unc as was confined was in the very center of the village, and in a situation perhaps more difficult than any other to approach or leave without observation.",
"ref_norm": "THE LODGE IN WHICH UNCAS WAS CONFINED WAS IN THE VERY CENTER OF THE VILLAGE AND IN A SITUATION PERHAPS MORE DIFFICULT THAN ANY OTHER TO APPROACH OR LEAVE WITHOUT OBSERVATION",
"hyp_norm": "THE LODGE IN WHICH UNC AS WAS CONFINED WAS IN THE VERY CENTER OF THE VILLAGE AND IN A SITUATION PERHAPS MORE DIFFICULT THAN ANY OTHER TO APPROACH OR LEAVE WITHOUT OBSERVATION",
"duration_s": 9.76,
"infer_time_s": 2.486,
"rtf": 0.2547,
"wer": 0.0645
},
{
"id": "1320-122617-0012",
"ref": "FOUR OR FIVE OF THE LATTER ONLY LINGERED ABOUT THE DOOR OF THE PRISON OF UNCAS WARY BUT CLOSE OBSERVERS OF THE MANNER OF THEIR CAPTIVE",
"hyp": "Four or five of the latter only lingered about the door of the prison of Uncas, wary but close observers of the manner of their captive.",
"ref_norm": "FOUR OR FIVE OF THE LATTER ONLY LINGERED ABOUT THE DOOR OF THE PRISON OF UNCAS WARY BUT CLOSE OBSERVERS OF THE MANNER OF THEIR CAPTIVE",
"hyp_norm": "FOUR OR FIVE OF THE LATTER ONLY LINGERED ABOUT THE DOOR OF THE PRISON OF UNCAS WARY BUT CLOSE OBSERVERS OF THE MANNER OF THEIR CAPTIVE",
"duration_s": 7.59,
"infer_time_s": 2.108,
"rtf": 0.2777,
"wer": 0.0
},
{
"id": "1320-122617-0013",
"ref": "DELIVERED IN A STRONG TONE OF ASSENT ANNOUNCED THE GRATIFICATION THE SAVAGE WOULD RECEIVE IN WITNESSING SUCH AN EXHIBITION OF WEAKNESS IN AN ENEMY SO LONG HATED AND SO MUCH FEARED",
"hyp": "Delivered in a strong tone of assent, announced the gratification the savage would receive in witnessing such an exhibition of weakness in an enemy so long hated and so much feared.",
"ref_norm": "DELIVERED IN A STRONG TONE OF ASSENT ANNOUNCED THE GRATIFICATION THE SAVAGE WOULD RECEIVE IN WITNESSING SUCH AN EXHIBITION OF WEAKNESS IN AN ENEMY SO LONG HATED AND SO MUCH FEARED",
"hyp_norm": "DELIVERED IN A STRONG TONE OF ASSENT ANNOUNCED THE GRATIFICATION THE SAVAGE WOULD RECEIVE IN WITNESSING SUCH AN EXHIBITION OF WEAKNESS IN AN ENEMY SO LONG HATED AND SO MUCH FEARED",
"duration_s": 10.755,
"infer_time_s": 2.763,
"rtf": 0.2569,
"wer": 0.0
},
{
"id": "1320-122617-0014",
"ref": "THEY DREW BACK A LITTLE FROM THE ENTRANCE AND MOTIONED TO THE SUPPOSED CONJURER TO ENTER",
"hyp": "They drew back a little from the entrance and motioned to the supposed conjurer to enter.",
"ref_norm": "THEY DREW BACK A LITTLE FROM THE ENTRANCE AND MOTIONED TO THE SUPPOSED CONJURER TO ENTER",
"hyp_norm": "THEY DREW BACK A LITTLE FROM THE ENTRANCE AND MOTIONED TO THE SUPPOSED CONJURER TO ENTER",
"duration_s": 4.9,
"infer_time_s": 1.491,
"rtf": 0.3042,
"wer": 0.0
},
{
"id": "1320-122617-0015",
"ref": "BUT THE BEAR INSTEAD OF OBEYING MAINTAINED THE SEAT IT HAD TAKEN AND GROWLED",
"hyp": "But the bear, instead of obey ing, maintained the seat it had taken and growled.",
"ref_norm": "BUT THE BEAR INSTEAD OF OBEYING MAINTAINED THE SEAT IT HAD TAKEN AND GROWLED",
"hyp_norm": "BUT THE BEAR INSTEAD OF OBEY ING MAINTAINED THE SEAT IT HAD TAKEN AND GROWLED",
"duration_s": 5.125,
"infer_time_s": 1.379,
"rtf": 0.2691,
"wer": 0.1429
},
{
"id": "1320-122617-0016",
"ref": "THE CUNNING MAN IS AFRAID THAT HIS BREATH WILL BLOW UPON HIS BROTHERS AND TAKE AWAY THEIR COURAGE TOO CONTINUED DAVID IMPROVING THE HINT HE RECEIVED THEY MUST STAND FURTHER OFF",
"hyp": "The cunning man is afraid that his breath will blow upon his brothers and take away their courage too. Continued David, improving the hint he received, they must stand further off.",
"ref_norm": "THE CUNNING MAN IS AFRAID THAT HIS BREATH WILL BLOW UPON HIS BROTHERS AND TAKE AWAY THEIR COURAGE TOO CONTINUED DAVID IMPROVING THE HINT HE RECEIVED THEY MUST STAND FURTHER OFF",
"hyp_norm": "THE CUNNING MAN IS AFRAID THAT HIS BREATH WILL BLOW UPON HIS BROTHERS AND TAKE AWAY THEIR COURAGE TOO CONTINUED DAVID IMPROVING THE HINT HE RECEIVED THEY MUST STAND FURTHER OFF",
"duration_s": 10.085,
"infer_time_s": 2.725,
"rtf": 0.2702,
"wer": 0.0
},
{
"id": "1320-122617-0017",
"ref": "THEN AS IF SATISFIED OF THEIR SAFETY THE SCOUT LEFT HIS POSITION AND SLOWLY ENTERED THE PLACE",
"hyp": "Then, as if satisfied of their safety, the scout left his position and slowly entered the place.",
"ref_norm": "THEN AS IF SATISFIED OF THEIR SAFETY THE SCOUT LEFT HIS POSITION AND SLOWLY ENTERED THE PLACE",
"hyp_norm": "THEN AS IF SATISFIED OF THEIR SAFETY THE SCOUT LEFT HIS POSITION AND SLOWLY ENTERED THE PLACE",
"duration_s": 5.655,
"infer_time_s": 1.45,
"rtf": 0.2564,
"wer": 0.0
},
{
"id": "1320-122617-0018",
"ref": "IT WAS SILENT AND GLOOMY BEING TENANTED SOLELY BY THE CAPTIVE AND LIGHTED BY THE DYING EMBERS OF A FIRE WHICH HAD BEEN USED FOR THE PURPOSED OF COOKERY",
"hyp": "It was silent and glo omy, being tenanted solely by the captive , and lighted by the dying embers of a fire which had been used for the purpose of cookery.",
"ref_norm": "IT WAS SILENT AND GLOOMY BEING TENANTED SOLELY BY THE CAPTIVE AND LIGHTED BY THE DYING EMBERS OF A FIRE WHICH HAD BEEN USED FOR THE PURPOSED OF COOKERY",
"hyp_norm": "IT WAS SILENT AND GLO OMY BEING TENANTED SOLELY BY THE CAPTIVE AND LIGHTED BY THE DYING EMBERS OF A FIRE WHICH HAD BEEN USED FOR THE PURPOSE OF COOKERY",
"duration_s": 9.695,
"infer_time_s": 2.637,
"rtf": 0.272,
"wer": 0.1034
},
{
"id": "1320-122617-0019",
"ref": "UNCAS OCCUPIED A DISTANT CORNER IN A RECLINING ATTITUDE BEING RIGIDLY BOUND BOTH HANDS AND FEET BY STRONG AND PAINFUL WITHES",
"hyp": "Uncas occupied a distant corner in a recl ining attitude, being rigid ly bound both hands and feet by strong and painful whiths.",
"ref_norm": "UNCAS OCCUPIED A DISTANT CORNER IN A RECLINING ATTITUDE BEING RIGIDLY BOUND BOTH HANDS AND FEET BY STRONG AND PAINFUL WITHES",
"hyp_norm": "UNCAS OCCUPIED A DISTANT CORNER IN A RECL INING ATTITUDE BEING RIGID LY BOUND BOTH HANDS AND FEET BY STRONG AND PAINFUL WHITHS",
"duration_s": 8.23,
"infer_time_s": 2.162,
"rtf": 0.2627,
"wer": 0.2381
},
{
"id": "1320-122617-0020",
"ref": "THE SCOUT WHO HAD LEFT DAVID AT THE DOOR TO ASCERTAIN THEY WERE NOT OBSERVED THOUGHT IT PRUDENT TO PRESERVE HIS DISGUISE UNTIL ASSURED OF THEIR PRIVACY",
"hyp": "The scout who had left David at the door to ascertain they were not observed thought it prudent to preserve his disguise until assured of their privacy.",
"ref_norm": "THE SCOUT WHO HAD LEFT DAVID AT THE DOOR TO ASCERTAIN THEY WERE NOT OBSERVED THOUGHT IT PRUDENT TO PRESERVE HIS DISGUISE UNTIL ASSURED OF THEIR PRIVACY",
"hyp_norm": "THE SCOUT WHO HAD LEFT DAVID AT THE DOOR TO ASCERTAIN THEY WERE NOT OBSERVED THOUGHT IT PRUDENT TO PRESERVE HIS DISGUISE UNTIL ASSURED OF THEIR PRIVACY",
"duration_s": 8.895,
"infer_time_s": 2.163,
"rtf": 0.2431,
"wer": 0.0
},
{
"id": "1320-122617-0021",
"ref": "WHAT SHALL WE DO WITH THE MINGOES AT THE DOOR THEY COUNT SIX AND THIS SINGER IS AS GOOD AS NOTHING",
"hyp": "What shall we do with the mingo's at the door? They count six, and the singer is as good as nothing.",
"ref_norm": "WHAT SHALL WE DO WITH THE MINGOES AT THE DOOR THEY COUNT SIX AND THIS SINGER IS AS GOOD AS NOTHING",
"hyp_norm": "WHAT SHALL WE DO WITH THE MINGOS AT THE DOOR THEY COUNT SIX AND THE SINGER IS AS GOOD AS NOTHING",
"duration_s": 5.335,
"infer_time_s": 1.741,
"rtf": 0.3264,
"wer": 0.0952
},
{
"id": "1320-122617-0022",
"ref": "THE DELAWARES ARE CHILDREN OF THE TORTOISE AND THEY OUTSTRIP THE DEER",
"hyp": "The Delawares are children of the tortoise, and they outstrip the deer.",
"ref_norm": "THE DELAWARES ARE CHILDREN OF THE TORTOISE AND THEY OUTSTRIP THE DEER",
"hyp_norm": "THE DELAWARES ARE CHILDREN OF THE TORTOISE AND THEY OUTSTRIP THE DEER",
"duration_s": 3.855,
"infer_time_s": 1.195,
"rtf": 0.31,
"wer": 0.0
},
{
"id": "1320-122617-0023",
"ref": "UNCAS WHO HAD ALREADY APPROACHED THE DOOR IN READINESS TO LEAD THE WAY NOW RECOILED AND PLACED HIMSELF ONCE MORE IN THE BOTTOM OF THE LODGE",
"hyp": "Uncas, who had already approached the door in readiness to lead the way, now recoiled and placed himself once more in the bottom of the lodge.",
"ref_norm": "UNCAS WHO HAD ALREADY APPROACHED THE DOOR IN READINESS TO LEAD THE WAY NOW RECOILED AND PLACED HIMSELF ONCE MORE IN THE BOTTOM OF THE LODGE",
"hyp_norm": "UNCAS WHO HAD ALREADY APPROACHED THE DOOR IN READINESS TO LEAD THE WAY NOW RECOILED AND PLACED HIMSELF ONCE MORE IN THE BOTTOM OF THE LODGE",
"duration_s": 7.815,
"infer_time_s": 2.16,
"rtf": 0.2763,
"wer": 0.0
},
{
"id": "1320-122617-0024",
"ref": "BUT HAWKEYE WHO WAS TOO MUCH OCCUPIED WITH HIS OWN THOUGHTS TO NOTE THE MOVEMENT CONTINUED SPEAKING MORE TO HIMSELF THAN TO HIS COMPANION",
"hyp": "But Hawkeye, who was too much occupied with his own thoughts to note the movement, continued speaking more to himself than to his companion.",
"ref_norm": "BUT HAWKEYE WHO WAS TOO MUCH OCCUPIED WITH HIS OWN THOUGHTS TO NOTE THE MOVEMENT CONTINUED SPEAKING MORE TO HIMSELF THAN TO HIS COMPANION",
"hyp_norm": "BUT HAWKEYE WHO WAS TOO MUCH OCCUPIED WITH HIS OWN THOUGHTS TO NOTE THE MOVEMENT CONTINUED SPEAKING MORE TO HIMSELF THAN TO HIS COMPANION",
"duration_s": 7.555,
"infer_time_s": 2.068,
"rtf": 0.2737,
"wer": 0.0
},
{
"id": "1320-122617-0025",
"ref": "SO UNCAS YOU HAD BETTER TAKE THE LEAD WHILE I WILL PUT ON THE SKIN AGAIN AND TRUST TO CUNNING FOR WANT OF SPEED",
"hyp": "So Uncas, you had better take the lead while I will put on the skin again and trust to cunning for want of speed.",
"ref_norm": "SO UNCAS YOU HAD BETTER TAKE THE LEAD WHILE I WILL PUT ON THE SKIN AGAIN AND TRUST TO CUNNING FOR WANT OF SPEED",
"hyp_norm": "SO UNCAS YOU HAD BETTER TAKE THE LEAD WHILE I WILL PUT ON THE SKIN AGAIN AND TRUST TO CUNNING FOR WANT OF SPEED",
"duration_s": 6.36,
"infer_time_s": 1.939,
"rtf": 0.3048,
"wer": 0.0
},
{
"id": "1320-122617-0026",
"ref": "WELL WHAT CAN'T BE DONE BY MAIN COURAGE IN WAR MUST BE DONE BY CIRCUMVENTION",
"hyp": "Well, what can't be done by main courage in war must be done by circumvention.",
"ref_norm": "WELL WHAT CANT BE DONE BY MAIN COURAGE IN WAR MUST BE DONE BY CIRCUMVENTION",
"hyp_norm": "WELL WHAT CANT BE DONE BY MAIN COURAGE IN WAR MUST BE DONE BY CIRCUMVENTION",
"duration_s": 5.225,
"infer_time_s": 1.381,
"rtf": 0.2643,
"wer": 0.0
},
{
"id": "1320-122617-0027",
"ref": "AS SOON AS THESE DISPOSITIONS WERE MADE THE SCOUT TURNED TO DAVID AND GAVE HIM HIS PARTING INSTRUCTIONS",
"hyp": "As soon as these dis positions were made, the scout turned to David and gave him his parting instructions.",
"ref_norm": "AS SOON AS THESE DISPOSITIONS WERE MADE THE SCOUT TURNED TO DAVID AND GAVE HIM HIS PARTING INSTRUCTIONS",
"hyp_norm": "AS SOON AS THESE DIS POSITIONS WERE MADE THE SCOUT TURNED TO DAVID AND GAVE HIM HIS PARTING INSTRUCTIONS",
"duration_s": 5.69,
"infer_time_s": 1.54,
"rtf": 0.2707,
"wer": 0.1111
},
{
"id": "1320-122617-0028",
"ref": "MY PURSUITS ARE PEACEFUL AND MY TEMPER I HUMBLY TRUST IS GREATLY GIVEN TO MERCY AND LOVE RETURNED DAVID A LITTLE NETTLED AT SO DIRECT AN ATTACK ON HIS MANHOOD BUT THERE ARE NONE WHO CAN SAY THAT I HAVE EVER FORGOTTEN MY FAITH IN THE LORD EVEN IN THE GREATEST STRAITS",
"hyp": "My pursuits are peaceful, and my temper I humb ly trust is greatly given to mercy and love. Returned David, a little nettled at so direct an attack on his manhood, but there are none who can say that I have ever forgotten my faith in the Lord, even in the greatest straits.",
"ref_norm": "MY PURSUITS ARE PEACEFUL AND MY TEMPER I HUMBLY TRUST IS GREATLY GIVEN TO MERCY AND LOVE RETURNED DAVID A LITTLE NETTLED AT SO DIRECT AN ATTACK ON HIS MANHOOD BUT THERE ARE NONE WHO CAN SAY THAT I HAVE EVER FORGOTTEN MY FAITH IN THE LORD EVEN IN THE GREATEST STRAITS",
"hyp_norm": "MY PURSUITS ARE PEACEFUL AND MY TEMPER I HUMB LY TRUST IS GREATLY GIVEN TO MERCY AND LOVE RETURNED DAVID A LITTLE NETTLED AT SO DIRECT AN ATTACK ON HIS MANHOOD BUT THERE ARE NONE WHO CAN SAY THAT I HAVE EVER FORGOTTEN MY FAITH IN THE LORD EVEN IN THE GREATEST STRAITS",
"duration_s": 15.995,
"infer_time_s": 4.527,
"rtf": 0.2831,
"wer": 0.0385
},
{
"id": "1320-122617-0029",
"ref": "IF YOU ARE NOT THEN KNOCKED ON THE HEAD YOUR BEING A NON COMPOSSER WILL PROTECT YOU AND YOU'LL THEN HAVE A GOOD REASON TO EXPECT TO DIE IN YOUR BED",
"hyp": "If you are not then knocked on the head, your being a non-com poser will protect you , and you'll then have a good reason to expect to die in your bed.",
"ref_norm": "IF YOU ARE NOT THEN KNOCKED ON THE HEAD YOUR BEING A NON COMPOSSER WILL PROTECT YOU AND YOULL THEN HAVE A GOOD REASON TO EXPECT TO DIE IN YOUR BED",
"hyp_norm": "IF YOU ARE NOT THEN KNOCKED ON THE HEAD YOUR BEING A NONCOM POSER WILL PROTECT YOU AND YOULL THEN HAVE A GOOD REASON TO EXPECT TO DIE IN YOUR BED",
"duration_s": 7.875,
"infer_time_s": 2.429,
"rtf": 0.3085,
"wer": 0.0645
},
{
"id": "1320-122617-0030",
"ref": "SO CHOOSE FOR YOURSELF TO MAKE A RUSH OR TARRY HERE",
"hyp": "So choose for yourself to make a rush or tarry here.",
"ref_norm": "SO CHOOSE FOR YOURSELF TO MAKE A RUSH OR TARRY HERE",
"hyp_norm": "SO CHOOSE FOR YOURSELF TO MAKE A RUSH OR TARRY HERE",
"duration_s": 3.98,
"infer_time_s": 0.937,
"rtf": 0.2355,
"wer": 0.0
},
{
"id": "1320-122617-0031",
"ref": "BRAVELY AND GENEROUSLY HAS HE BATTLED IN MY BEHALF AND THIS AND MORE WILL I DARE IN HIS SERVICE",
"hyp": "Bravely and generously, as he battled in my behalf , and this and more will I dare in his service.",
"ref_norm": "BRAVELY AND GENEROUSLY HAS HE BATTLED IN MY BEHALF AND THIS AND MORE WILL I DARE IN HIS SERVICE",
"hyp_norm": "BRAVELY AND GENEROUSLY AS HE BATTLED IN MY BEHALF AND THIS AND MORE WILL I DARE IN HIS SERVICE",
"duration_s": 6.285,
"infer_time_s": 1.776,
"rtf": 0.2826,
"wer": 0.0526
},
{
"id": "1320-122617-0032",
"ref": "KEEP SILENT AS LONG AS MAY BE AND IT WOULD BE WISE WHEN YOU DO SPEAK TO BREAK OUT SUDDENLY IN ONE OF YOUR SHOUTINGS WHICH WILL SERVE TO REMIND THE INDIANS THAT YOU ARE NOT ALTOGETHER AS RESPONSIBLE AS MEN SHOULD BE",
"hyp": "Keep silent as long as may be, and it would be wise when you do speak to break out suddenly in one of your shout ings, which will serve to remind the Indians that you are not altogether as responsible as men should be.",
"ref_norm": "KEEP SILENT AS LONG AS MAY BE AND IT WOULD BE WISE WHEN YOU DO SPEAK TO BREAK OUT SUDDENLY IN ONE OF YOUR SHOUTINGS WHICH WILL SERVE TO REMIND THE INDIANS THAT YOU ARE NOT ALTOGETHER AS RESPONSIBLE AS MEN SHOULD BE",
"hyp_norm": "KEEP SILENT AS LONG AS MAY BE AND IT WOULD BE WISE WHEN YOU DO SPEAK TO BREAK OUT SUDDENLY IN ONE OF YOUR SHOUT INGS WHICH WILL SERVE TO REMIND THE INDIANS THAT YOU ARE NOT ALTOGETHER AS RESPONSIBLE AS MEN SHOULD BE",
"duration_s": 11.28,
"infer_time_s": 3.342,
"rtf": 0.2963,
"wer": 0.0465
},
{
"id": "1320-122617-0033",
"ref": "IF HOWEVER THEY TAKE YOUR SCALP AS I TRUST AND BELIEVE THEY WILL NOT DEPEND ON IT UNCAS AND I WILL NOT FORGET THE DEED BUT REVENGE IT AS BECOMES TRUE WARRIORS AND TRUSTY FRIENDS",
"hyp": "If however they take your scalp, as I trust and believe they will not, depend on it. Uncas and I will not forget the deed, but revenge it as becomes true warriors and trusty friends.",
"ref_norm": "IF HOWEVER THEY TAKE YOUR SCALP AS I TRUST AND BELIEVE THEY WILL NOT DEPEND ON IT UNCAS AND I WILL NOT FORGET THE DEED BUT REVENGE IT AS BECOMES TRUE WARRIORS AND TRUSTY FRIENDS",
"hyp_norm": "IF HOWEVER THEY TAKE YOUR SCALP AS I TRUST AND BELIEVE THEY WILL NOT DEPEND ON IT UNCAS AND I WILL NOT FORGET THE DEED BUT REVENGE IT AS BECOMES TRUE WARRIORS AND TRUSTY FRIENDS",
"duration_s": 11.045,
"infer_time_s": 3.07,
"rtf": 0.2779,
"wer": 0.0
},
{
"id": "1320-122617-0034",
"ref": "HOLD SAID DAVID PERCEIVING THAT WITH THIS ASSURANCE THEY WERE ABOUT TO LEAVE HIM I AM AN UNWORTHY AND HUMBLE FOLLOWER OF ONE WHO TAUGHT NOT THE DAMNABLE PRINCIPLE OF REVENGE",
"hyp": "Hold,\" said David, perceiving that with this assurance they were about to leave him. \"I am an unworthy and humble follower of one who taught not the damnable principle of revenge.\"",
"ref_norm": "HOLD SAID DAVID PERCEIVING THAT WITH THIS ASSURANCE THEY WERE ABOUT TO LEAVE HIM I AM AN UNWORTHY AND HUMBLE FOLLOWER OF ONE WHO TAUGHT NOT THE DAMNABLE PRINCIPLE OF REVENGE",
"hyp_norm": "HOLD SAID DAVID PERCEIVING THAT WITH THIS ASSURANCE THEY WERE ABOUT TO LEAVE HIM I AM AN UNWORTHY AND HUMBLE FOLLOWER OF ONE WHO TAUGHT NOT THE DAMNABLE PRINCIPLE OF REVENGE",
"duration_s": 9.485,
"infer_time_s": 2.752,
"rtf": 0.2901,
"wer": 0.0
},
{
"id": "1320-122617-0035",
"ref": "THEN HEAVING A HEAVY SIGH PROBABLY AMONG THE LAST HE EVER DREW IN PINING FOR A CONDITION HE HAD SO LONG ABANDONED HE ADDED IT IS WHAT I WOULD WISH TO PRACTISE MYSELF AS ONE WITHOUT A CROSS OF BLOOD THOUGH IT IS NOT ALWAYS EASY TO DEAL WITH AN INDIAN AS YOU WOULD WITH A FELLOW CHRISTIAN",
"hyp": "Then heaving a heavy sigh, probably among the last he ever drew in pining for a condition he had so long abandoned , he added, \"It is what I would wish to practice myself as one without a cross of blood, though it is not always easy to deal with an Indian as you would with a fellow Christian.\"",
"ref_norm": "THEN HEAVING A HEAVY SIGH PROBABLY AMONG THE LAST HE EVER DREW IN PINING FOR A CONDITION HE HAD SO LONG ABANDONED HE ADDED IT IS WHAT I WOULD WISH TO PRACTISE MYSELF AS ONE WITHOUT A CROSS OF BLOOD THOUGH IT IS NOT ALWAYS EASY TO DEAL WITH AN INDIAN AS YOU WOULD WITH A FELLOW CHRISTIAN",
"hyp_norm": "THEN HEAVING A HEAVY SIGH PROBABLY AMONG THE LAST HE EVER DREW IN PINING FOR A CONDITION HE HAD SO LONG ABANDONED HE ADDED IT IS WHAT I WOULD WISH TO PRACTICE MYSELF AS ONE WITHOUT A CROSS OF BLOOD THOUGH IT IS NOT ALWAYS EASY TO DEAL WITH AN INDIAN AS YOU WOULD WITH A FELLOW CHRISTIAN",
"duration_s": 18.22,
"infer_time_s": 5.055,
"rtf": 0.2775,
"wer": 0.0172
},
{
"id": "1320-122617-0036",
"ref": "GOD BLESS YOU FRIEND I DO BELIEVE YOUR SCENT IS NOT GREATLY WRONG WHEN THE MATTER IS DULY CONSIDERED AND KEEPING ETERNITY BEFORE THE EYES THOUGH MUCH DEPENDS ON THE NATURAL GIFTS AND THE FORCE OF TEMPTATION",
"hyp": "God bless you, friend . I do believe your sin is not greatly wrong when the matter is duly considered , and keeping eternity before the eyes. Though much depends on the natural gifts and the force of temptation.",
"ref_norm": "GOD BLESS YOU FRIEND I DO BELIEVE YOUR SCENT IS NOT GREATLY WRONG WHEN THE MATTER IS DULY CONSIDERED AND KEEPING ETERNITY BEFORE THE EYES THOUGH MUCH DEPENDS ON THE NATURAL GIFTS AND THE FORCE OF TEMPTATION",
"hyp_norm": "GOD BLESS YOU FRIEND I DO BELIEVE YOUR SIN IS NOT GREATLY WRONG WHEN THE MATTER IS DULY CONSIDERED AND KEEPING ETERNITY BEFORE THE EYES THOUGH MUCH DEPENDS ON THE NATURAL GIFTS AND THE FORCE OF TEMPTATION",
"duration_s": 12.37,
"infer_time_s": 3.274,
"rtf": 0.2647,
"wer": 0.027
},
{
"id": "1320-122617-0037",
"ref": "THE DELAWARE DOG HE SAID LEANING FORWARD AND PEERING THROUGH THE DIM LIGHT TO CATCH THE EXPRESSION OF THE OTHER'S FEATURES IS HE AFRAID",
"hyp": "The Delaware dog,\" he said, leaning forward and peering through the dim light to catch the expression of the other's features. \"Is he afraid?\"",
"ref_norm": "THE DELAWARE DOG HE SAID LEANING FORWARD AND PEERING THROUGH THE DIM LIGHT TO CATCH THE EXPRESSION OF THE OTHERS FEATURES IS HE AFRAID",
"hyp_norm": "THE DELAWARE DOG HE SAID LEANING FORWARD AND PEERING THROUGH THE DIM LIGHT TO CATCH THE EXPRESSION OF THE OTHERS FEATURES IS HE AFRAID",
"duration_s": 7.18,
"infer_time_s": 2.176,
"rtf": 0.3031,
"wer": 0.0
},
{
"id": "1320-122617-0038",
"ref": "WILL THE HURONS HEAR HIS GROANS",
"hyp": "Will the Hurons hear his gro ans?",
"ref_norm": "WILL THE HURONS HEAR HIS GROANS",
"hyp_norm": "WILL THE HURONS HEAR HIS GRO ANS",
"duration_s": 2.24,
"infer_time_s": 0.729,
"rtf": 0.3256,
"wer": 0.3333
},
{
"id": "1320-122617-0039",
"ref": "THE MOHICAN STARTED ON HIS FEET AND SHOOK HIS SHAGGY COVERING AS THOUGH THE ANIMAL HE COUNTERFEITED WAS ABOUT TO MAKE SOME DESPERATE EFFORT",
"hyp": "The Mohicans started on his feet and shook his shaggy covering as though the animal he counterfeited was about to make some desperate effort.",
"ref_norm": "THE MOHICAN STARTED ON HIS FEET AND SHOOK HIS SHAGGY COVERING AS THOUGH THE ANIMAL HE COUNTERFEITED WAS ABOUT TO MAKE SOME DESPERATE EFFORT",
"hyp_norm": "THE MOHICANS STARTED ON HIS FEET AND SHOOK HIS SHAGGY COVERING AS THOUGH THE ANIMAL HE COUNTERFEITED WAS ABOUT TO MAKE SOME DESPERATE EFFORT",
"duration_s": 7.055,
"infer_time_s": 2.113,
"rtf": 0.2995,
"wer": 0.0417
},
{
"id": "1320-122617-0040",
"ref": "HE HAD NO OCCASION TO DELAY FOR AT THE NEXT INSTANT A BURST OF CRIES FILLED THE OUTER AIR AND RAN ALONG THE WHOLE EXTENT OF THE VILLAGE",
"hyp": "He had no occasion to delay, for at the next instant a burst of cries filled the outer air and ran along the whole extent of the village.",
"ref_norm": "HE HAD NO OCCASION TO DELAY FOR AT THE NEXT INSTANT A BURST OF CRIES FILLED THE OUTER AIR AND RAN ALONG THE WHOLE EXTENT OF THE VILLAGE",
"hyp_norm": "HE HAD NO OCCASION TO DELAY FOR AT THE NEXT INSTANT A BURST OF CRIES FILLED THE OUTER AIR AND RAN ALONG THE WHOLE EXTENT OF THE VILLAGE",
"duration_s": 7.975,
"infer_time_s": 2.12,
"rtf": 0.2658,
"wer": 0.0
},
{
"id": "1320-122617-0041",
"ref": "UNCAS CAST HIS SKIN AND STEPPED FORTH IN HIS OWN BEAUTIFUL PROPORTIONS",
"hyp": "Uncas cast his skin and stepped forth in his own beautiful proportions.",
"ref_norm": "UNCAS CAST HIS SKIN AND STEPPED FORTH IN HIS OWN BEAUTIFUL PROPORTIONS",
"hyp_norm": "UNCAS CAST HIS SKIN AND STEPPED FORTH IN HIS OWN BEAUTIFUL PROPORTIONS",
"duration_s": 4.15,
"infer_time_s": 1.118,
"rtf": 0.2693,
"wer": 0.0
},
{
"id": "1580-141083-0000",
"ref": "I WILL ENDEAVOUR IN MY STATEMENT TO AVOID SUCH TERMS AS WOULD SERVE TO LIMIT THE EVENTS TO ANY PARTICULAR PLACE OR GIVE A CLUE AS TO THE PEOPLE CONCERNED",
"hyp": "I will endeavor in my statement to avoid such terms as would serve to limit the events to any particular place or give a clue as to the people concerned.",
"ref_norm": "I WILL ENDEAVOUR IN MY STATEMENT TO AVOID SUCH TERMS AS WOULD SERVE TO LIMIT THE EVENTS TO ANY PARTICULAR PLACE OR GIVE A CLUE AS TO THE PEOPLE CONCERNED",
"hyp_norm": "I WILL ENDEAVOR IN MY STATEMENT TO AVOID SUCH TERMS AS WOULD SERVE TO LIMIT THE EVENTS TO ANY PARTICULAR PLACE OR GIVE A CLUE AS TO THE PEOPLE CONCERNED",
"duration_s": 8.94,
"infer_time_s": 2.315,
"rtf": 0.2589,
"wer": 0.0333
},
{
"id": "1580-141083-0001",
"ref": "I HAD ALWAYS KNOWN HIM TO BE RESTLESS IN HIS MANNER BUT ON THIS PARTICULAR OCCASION HE WAS IN SUCH A STATE OF UNCONTROLLABLE AGITATION THAT IT WAS CLEAR SOMETHING VERY UNUSUAL HAD OCCURRED",
"hyp": "I had always known him to be restless in his manner , but on this particular occasion , he was in such a state of uncontrollable agitation that it was clear something very unusual had occurred.",
"ref_norm": "I HAD ALWAYS KNOWN HIM TO BE RESTLESS IN HIS MANNER BUT ON THIS PARTICULAR OCCASION HE WAS IN SUCH A STATE OF UNCONTROLLABLE AGITATION THAT IT WAS CLEAR SOMETHING VERY UNUSUAL HAD OCCURRED",
"hyp_norm": "I HAD ALWAYS KNOWN HIM TO BE RESTLESS IN HIS MANNER BUT ON THIS PARTICULAR OCCASION HE WAS IN SUCH A STATE OF UNCONTROLLABLE AGITATION THAT IT WAS CLEAR SOMETHING VERY UNUSUAL HAD OCCURRED",
"duration_s": 10.255,
"infer_time_s": 2.875,
"rtf": 0.2803,
"wer": 0.0
},
{
"id": "1580-141083-0002",
"ref": "MY FRIEND'S TEMPER HAD NOT IMPROVED SINCE HE HAD BEEN DEPRIVED OF THE CONGENIAL SURROUNDINGS OF BAKER STREET",
"hyp": "My friend's temper had not improved since he had been deprived of the congenial surroundings of Baker Street.",
"ref_norm": "MY FRIENDS TEMPER HAD NOT IMPROVED SINCE HE HAD BEEN DEPRIVED OF THE CONGENIAL SURROUNDINGS OF BAKER STREET",
"hyp_norm": "MY FRIENDS TEMPER HAD NOT IMPROVED SINCE HE HAD BEEN DEPRIVED OF THE CONGENIAL SURROUNDINGS OF BAKER STREET",
"duration_s": 6.135,
"infer_time_s": 1.712,
"rtf": 0.279,
"wer": 0.0
},
{
"id": "1580-141083-0003",
"ref": "WITHOUT HIS SCRAPBOOKS HIS CHEMICALS AND HIS HOMELY UNTIDINESS HE WAS AN UNCOMFORTABLE MAN",
"hyp": "Without his scrapbooks , his chemicals, and his homely untidiness, he was an uncomfortable man.",
"ref_norm": "WITHOUT HIS SCRAPBOOKS HIS CHEMICALS AND HIS HOMELY UNTIDINESS HE WAS AN UNCOMFORTABLE MAN",
"hyp_norm": "WITHOUT HIS SCRAPBOOKS HIS CHEMICALS AND HIS HOMELY UNTIDINESS HE WAS AN UNCOMFORTABLE MAN",
"duration_s": 6.55,
"infer_time_s": 1.792,
"rtf": 0.2736,
"wer": 0.0
},
{
"id": "1580-141083-0004",
"ref": "I HAD TO READ IT OVER CAREFULLY AS THE TEXT MUST BE ABSOLUTELY CORRECT",
"hyp": "I had to read it over carefully, as the text must be absolutely correct.",
"ref_norm": "I HAD TO READ IT OVER CAREFULLY AS THE TEXT MUST BE ABSOLUTELY CORRECT",
"hyp_norm": "I HAD TO READ IT OVER CAREFULLY AS THE TEXT MUST BE ABSOLUTELY CORRECT",
"duration_s": 4.515,
"infer_time_s": 1.261,
"rtf": 0.2793,
"wer": 0.0
},
{
"id": "1580-141083-0005",
"ref": "I WAS ABSENT RATHER MORE THAN AN HOUR",
"hyp": "I was absent rather more than an hour.",
"ref_norm": "I WAS ABSENT RATHER MORE THAN AN HOUR",
"hyp_norm": "I WAS ABSENT RATHER MORE THAN AN HOUR",
"duration_s": 2.745,
"infer_time_s": 0.758,
"rtf": 0.2763,
"wer": 0.0
},
{
"id": "1580-141083-0006",
"ref": "THE ONLY DUPLICATE WHICH EXISTED SO FAR AS I KNEW WAS THAT WHICH BELONGED TO MY SERVANT BANNISTER A MAN WHO HAS LOOKED AFTER MY ROOM FOR TEN YEARS AND WHOSE HONESTY IS ABSOLUTELY ABOVE SUSPICION",
"hyp": "The only duplicate which existed, so far as I knew, was that which belonged to my servant Bann ister, a man who has looked after my room for ten years and whose honesty is absolutely above suspicion.",
"ref_norm": "THE ONLY DUPLICATE WHICH EXISTED SO FAR AS I KNEW WAS THAT WHICH BELONGED TO MY SERVANT BANNISTER A MAN WHO HAS LOOKED AFTER MY ROOM FOR TEN YEARS AND WHOSE HONESTY IS ABSOLUTELY ABOVE SUSPICION",
"hyp_norm": "THE ONLY DUPLICATE WHICH EXISTED SO FAR AS I KNEW WAS THAT WHICH BELONGED TO MY SERVANT BANN ISTER A MAN WHO HAS LOOKED AFTER MY ROOM FOR TEN YEARS AND WHOSE HONESTY IS ABSOLUTELY ABOVE SUSPICION",
"duration_s": 10.85,
"infer_time_s": 3.232,
"rtf": 0.2979,
"wer": 0.0556
},
{
"id": "1580-141083-0007",
"ref": "THE MOMENT I LOOKED AT MY TABLE I WAS AWARE THAT SOMEONE HAD RUMMAGED AMONG MY PAPERS",
"hyp": "The moment I looked at my table, I was aware that someone had rummaged among my papers.",
"ref_norm": "THE MOMENT I LOOKED AT MY TABLE I WAS AWARE THAT SOMEONE HAD RUMMAGED AMONG MY PAPERS",
"hyp_norm": "THE MOMENT I LOOKED AT MY TABLE I WAS AWARE THAT SOMEONE HAD RUMMAGED AMONG MY PAPERS",
"duration_s": 4.565,
"infer_time_s": 1.502,
"rtf": 0.3291,
"wer": 0.0
},
{
"id": "1580-141083-0008",
"ref": "THE PROOF WAS IN THREE LONG SLIPS I HAD LEFT THEM ALL TOGETHER",
"hyp": "The proof was in three long slips. I had left them all together.",
"ref_norm": "THE PROOF WAS IN THREE LONG SLIPS I HAD LEFT THEM ALL TOGETHER",
"hyp_norm": "THE PROOF WAS IN THREE LONG SLIPS I HAD LEFT THEM ALL TOGETHER",
"duration_s": 4.305,
"infer_time_s": 1.18,
"rtf": 0.2741,
"wer": 0.0
},
{
"id": "1580-141083-0009",
"ref": "THE ALTERNATIVE WAS THAT SOMEONE PASSING HAD OBSERVED THE KEY IN THE DOOR HAD KNOWN THAT I WAS OUT AND HAD ENTERED TO LOOK AT THE PAPERS",
"hyp": "The alternative was that someone passing had observed the key in the door, had known that I was out, and had entered to look at the papers.",
"ref_norm": "THE ALTERNATIVE WAS THAT SOMEONE PASSING HAD OBSERVED THE KEY IN THE DOOR HAD KNOWN THAT I WAS OUT AND HAD ENTERED TO LOOK AT THE PAPERS",
"hyp_norm": "THE ALTERNATIVE WAS THAT SOMEONE PASSING HAD OBSERVED THE KEY IN THE DOOR HAD KNOWN THAT I WAS OUT AND HAD ENTERED TO LOOK AT THE PAPERS",
"duration_s": 7.04,
"infer_time_s": 2.123,
"rtf": 0.3016,
"wer": 0.0
},
{
"id": "1580-141083-0010",
"ref": "I GAVE HIM A LITTLE BRANDY AND LEFT HIM COLLAPSED IN A CHAIR WHILE I MADE A MOST CAREFUL EXAMINATION OF THE ROOM",
"hyp": "I gave him a little brandy and left him collapsed in a chair while I made a most careful examination of the room.",
"ref_norm": "I GAVE HIM A LITTLE BRANDY AND LEFT HIM COLLAPSED IN A CHAIR WHILE I MADE A MOST CAREFUL EXAMINATION OF THE ROOM",
"hyp_norm": "I GAVE HIM A LITTLE BRANDY AND LEFT HIM COLLAPSED IN A CHAIR WHILE I MADE A MOST CAREFUL EXAMINATION OF THE ROOM",
"duration_s": 5.32,
"infer_time_s": 1.718,
"rtf": 0.3229,
"wer": 0.0
},
{
"id": "1580-141083-0011",
"ref": "A BROKEN TIP OF LEAD WAS LYING THERE ALSO",
"hyp": "A broken tip of lead was lying there. Also.",
"ref_norm": "A BROKEN TIP OF LEAD WAS LYING THERE ALSO",
"hyp_norm": "A BROKEN TIP OF LEAD WAS LYING THERE ALSO",
"duration_s": 2.825,
"infer_time_s": 0.853,
"rtf": 0.3019,
"wer": 0.0
},
{
"id": "1580-141083-0012",
"ref": "NOT ONLY THIS BUT ON THE TABLE I FOUND A SMALL BALL OF BLACK DOUGH OR CLAY WITH SPECKS OF SOMETHING WHICH LOOKS LIKE SAWDUST IN IT",
"hyp": "Not only this, but on the table I found a small ball of black dough or clay with specks of something which looks like sawdust in it.",
"ref_norm": "NOT ONLY THIS BUT ON THE TABLE I FOUND A SMALL BALL OF BLACK DOUGH OR CLAY WITH SPECKS OF SOMETHING WHICH LOOKS LIKE SAWDUST IN IT",
"hyp_norm": "NOT ONLY THIS BUT ON THE TABLE I FOUND A SMALL BALL OF BLACK DOUGH OR CLAY WITH SPECKS OF SOMETHING WHICH LOOKS LIKE SAWDUST IN IT",
"duration_s": 7.065,
"infer_time_s": 2.446,
"rtf": 0.3462,
"wer": 0.0
},
{
"id": "1580-141083-0013",
"ref": "ABOVE ALL THINGS I DESIRE TO SETTLE THE MATTER QUIETLY AND DISCREETLY",
"hyp": "Above all things, I desire to settle the matter quietly and discreetly.",
"ref_norm": "ABOVE ALL THINGS I DESIRE TO SETTLE THE MATTER QUIETLY AND DISCREETLY",
"hyp_norm": "ABOVE ALL THINGS I DESIRE TO SETTLE THE MATTER QUIETLY AND DISCREETLY",
"duration_s": 4.32,
"infer_time_s": 1.228,
"rtf": 0.2842,
"wer": 0.0
},
{
"id": "1580-141083-0014",
"ref": "TO THE BEST OF MY BELIEF THEY WERE ROLLED UP",
"hyp": "To the best of my belief , they were rolled up.",
"ref_norm": "TO THE BEST OF MY BELIEF THEY WERE ROLLED UP",
"hyp_norm": "TO THE BEST OF MY BELIEF THEY WERE ROLLED UP",
"duration_s": 2.855,
"infer_time_s": 0.909,
"rtf": 0.3184,
"wer": 0.0
},
{
"id": "1580-141083-0015",
"ref": "DID ANYONE KNOW THAT THESE PROOFS WOULD BE THERE NO ONE SAVE THE PRINTER",
"hyp": "Did anyone know that these proofs would be there? No one save the printer.",
"ref_norm": "DID ANYONE KNOW THAT THESE PROOFS WOULD BE THERE NO ONE SAVE THE PRINTER",
"hyp_norm": "DID ANYONE KNOW THAT THESE PROOFS WOULD BE THERE NO ONE SAVE THE PRINTER",
"duration_s": 4.985,
"infer_time_s": 1.256,
"rtf": 0.252,
"wer": 0.0
},
{
"id": "1580-141083-0016",
"ref": "I WAS IN SUCH A HURRY TO COME TO YOU YOU LEFT YOUR DOOR OPEN",
"hyp": "I was in such a hurry to come to you. You left your door open.",
"ref_norm": "I WAS IN SUCH A HURRY TO COME TO YOU YOU LEFT YOUR DOOR OPEN",
"hyp_norm": "I WAS IN SUCH A HURRY TO COME TO YOU YOU LEFT YOUR DOOR OPEN",
"duration_s": 4.255,
"infer_time_s": 1.314,
"rtf": 0.3088,
"wer": 0.0
},
{
"id": "1580-141083-0017",
"ref": "SO IT SEEMS TO ME",
"hyp": "So it seems to me.",
"ref_norm": "SO IT SEEMS TO ME",
"hyp_norm": "SO IT SEEMS TO ME",
"duration_s": 2.28,
"infer_time_s": 0.643,
"rtf": 0.2822,
"wer": 0.0
},
{
"id": "1580-141083-0018",
"ref": "NOW MISTER SOAMES AT YOUR DISPOSAL",
"hyp": "Now, Mister Solmes, at your disposal.",
"ref_norm": "NOW MISTER SOAMES AT YOUR DISPOSAL",
"hyp_norm": "NOW MISTER SOLMES AT YOUR DISPOSAL",
"duration_s": 2.675,
"infer_time_s": 0.816,
"rtf": 0.3052,
"wer": 0.1667
},
{
"id": "1580-141083-0019",
"ref": "ABOVE WERE THREE STUDENTS ONE ON EACH STORY",
"hyp": "Above were three students, one on each story.",
"ref_norm": "ABOVE WERE THREE STUDENTS ONE ON EACH STORY",
"hyp_norm": "ABOVE WERE THREE STUDENTS ONE ON EACH STORY",
"duration_s": 2.705,
"infer_time_s": 0.802,
"rtf": 0.2964,
"wer": 0.0
},
{
"id": "1580-141083-0020",
"ref": "THEN HE APPROACHED IT AND STANDING ON TIPTOE WITH HIS NECK CRANED HE LOOKED INTO THE ROOM",
"hyp": "Then he approached it, and standing on tiptoe with his neck craned, he looked into the room.",
"ref_norm": "THEN HE APPROACHED IT AND STANDING ON TIPTOE WITH HIS NECK CRANED HE LOOKED INTO THE ROOM",
"hyp_norm": "THEN HE APPROACHED IT AND STANDING ON TIPTOE WITH HIS NECK CRANED HE LOOKED INTO THE ROOM",
"duration_s": 5.135,
"infer_time_s": 1.622,
"rtf": 0.3158,
"wer": 0.0
},
{
"id": "1580-141083-0021",
"ref": "THERE IS NO OPENING EXCEPT THE ONE PANE SAID OUR LEARNED GUIDE",
"hyp": "There is no opening except the one pane,\" said our learned guide.",
"ref_norm": "THERE IS NO OPENING EXCEPT THE ONE PANE SAID OUR LEARNED GUIDE",
"hyp_norm": "THERE IS NO OPENING EXCEPT THE ONE PANE SAID OUR LEARNED GUIDE",
"duration_s": 3.715,
"infer_time_s": 1.016,
"rtf": 0.2736,
"wer": 0.0
},
{
"id": "1580-141083-0022",
"ref": "I AM AFRAID THERE ARE NO SIGNS HERE SAID HE",
"hyp": "I am afraid there are no signs here,\" said he.",
"ref_norm": "I AM AFRAID THERE ARE NO SIGNS HERE SAID HE",
"hyp_norm": "I AM AFRAID THERE ARE NO SIGNS HERE SAID HE",
"duration_s": 3.295,
"infer_time_s": 0.914,
"rtf": 0.2773,
"wer": 0.0
},
{
"id": "1580-141083-0023",
"ref": "ONE COULD HARDLY HOPE FOR ANY UPON SO DRY A DAY",
"hyp": "One could hardly hope for any upon so dry a day.",
"ref_norm": "ONE COULD HARDLY HOPE FOR ANY UPON SO DRY A DAY",
"hyp_norm": "ONE COULD HARDLY HOPE FOR ANY UPON SO DRY A DAY",
"duration_s": 3.33,
"infer_time_s": 0.894,
"rtf": 0.2683,
"wer": 0.0
},
{
"id": "1580-141083-0024",
"ref": "YOU LEFT HIM IN A CHAIR YOU SAY WHICH CHAIR BY THE WINDOW THERE",
"hyp": "You left him in a chair. You say which chair? By the window there.",
"ref_norm": "YOU LEFT HIM IN A CHAIR YOU SAY WHICH CHAIR BY THE WINDOW THERE",
"hyp_norm": "YOU LEFT HIM IN A CHAIR YOU SAY WHICH CHAIR BY THE WINDOW THERE",
"duration_s": 4.48,
"infer_time_s": 1.292,
"rtf": 0.2884,
"wer": 0.0
},
{
"id": "1580-141083-0025",
"ref": "THE MAN ENTERED AND TOOK THE PAPERS SHEET BY SHEET FROM THE CENTRAL TABLE",
"hyp": "The men entered and took the papers sheet by sheet from the central table.",
"ref_norm": "THE MAN ENTERED AND TOOK THE PAPERS SHEET BY SHEET FROM THE CENTRAL TABLE",
"hyp_norm": "THE MEN ENTERED AND TOOK THE PAPERS SHEET BY SHEET FROM THE CENTRAL TABLE",
"duration_s": 3.905,
"infer_time_s": 1.066,
"rtf": 0.273,
"wer": 0.0714
},
{
"id": "1580-141083-0026",
"ref": "AS A MATTER OF FACT HE COULD NOT SAID SOAMES FOR I ENTERED BY THE SIDE DOOR",
"hyp": "As a matter of fact, he could not said Solmes. For I entered by the side door.",
"ref_norm": "AS A MATTER OF FACT HE COULD NOT SAID SOAMES FOR I ENTERED BY THE SIDE DOOR",
"hyp_norm": "AS A MATTER OF FACT HE COULD NOT SAID SOLMES FOR I ENTERED BY THE SIDE DOOR",
"duration_s": 4.775,
"infer_time_s": 1.52,
"rtf": 0.3183,
"wer": 0.0588
},
{
"id": "1580-141083-0027",
"ref": "HOW LONG WOULD IT TAKE HIM TO DO THAT USING EVERY POSSIBLE CONTRACTION A QUARTER OF AN HOUR NOT LESS",
"hyp": "How long would it take him to do that using every possible contraction? A quarter of an hour, not less.",
"ref_norm": "HOW LONG WOULD IT TAKE HIM TO DO THAT USING EVERY POSSIBLE CONTRACTION A QUARTER OF AN HOUR NOT LESS",
"hyp_norm": "HOW LONG WOULD IT TAKE HIM TO DO THAT USING EVERY POSSIBLE CONTRACTION A QUARTER OF AN HOUR NOT LESS",
"duration_s": 5.225,
"infer_time_s": 1.613,
"rtf": 0.3088,
"wer": 0.0
},
{
"id": "1580-141083-0028",
"ref": "THEN HE TOSSED IT DOWN AND SEIZED THE NEXT",
"hyp": "Then he tossed it down and seized the next.",
"ref_norm": "THEN HE TOSSED IT DOWN AND SEIZED THE NEXT",
"hyp_norm": "THEN HE TOSSED IT DOWN AND SEIZED THE NEXT",
"duration_s": 2.585,
"infer_time_s": 0.795,
"rtf": 0.3075,
"wer": 0.0
},
{
"id": "1580-141083-0029",
"ref": "HE WAS IN THE MIDST OF THAT WHEN YOUR RETURN CAUSED HIM TO MAKE A VERY HURRIED RETREAT VERY HURRIED SINCE HE HAD NOT TIME TO REPLACE THE PAPERS WHICH WOULD TELL YOU THAT HE HAD BEEN THERE",
"hyp": "He was in the midst of that when your return caused him to make a very hurried retreat , very hurried since he had not time to replace the papers which would tell you that he had been there.",
"ref_norm": "HE WAS IN THE MIDST OF THAT WHEN YOUR RETURN CAUSED HIM TO MAKE A VERY HURRIED RETREAT VERY HURRIED SINCE HE HAD NOT TIME TO REPLACE THE PAPERS WHICH WOULD TELL YOU THAT HE HAD BEEN THERE",
"hyp_norm": "HE WAS IN THE MIDST OF THAT WHEN YOUR RETURN CAUSED HIM TO MAKE A VERY HURRIED RETREAT VERY HURRIED SINCE HE HAD NOT TIME TO REPLACE THE PAPERS WHICH WOULD TELL YOU THAT HE HAD BEEN THERE",
"duration_s": 10.055,
"infer_time_s": 3.18,
"rtf": 0.3162,
"wer": 0.0
},
{
"id": "1580-141083-0030",
"ref": "MISTER SOAMES WAS SOMEWHAT OVERWHELMED BY THIS FLOOD OF INFORMATION",
"hyp": "Mr. Salms was somewhat overwhelmed by this flood of information.",
"ref_norm": "MISTER SOAMES WAS SOMEWHAT OVERWHELMED BY THIS FLOOD OF INFORMATION",
"hyp_norm": "MR SALMS WAS SOMEWHAT OVERWHELMED BY THIS FLOOD OF INFORMATION",
"duration_s": 3.48,
"infer_time_s": 0.992,
"rtf": 0.2851,
"wer": 0.2
},
{
"id": "1580-141083-0031",
"ref": "HOLMES HELD OUT A SMALL CHIP WITH THE LETTERS N N AND A SPACE OF CLEAR WOOD AFTER THEM YOU SEE",
"hyp": "Holmes held out a small chip with the letters N N and a space of Clear wood after them. You see.",
"ref_norm": "HOLMES HELD OUT A SMALL CHIP WITH THE LETTERS N N AND A SPACE OF CLEAR WOOD AFTER THEM YOU SEE",
"hyp_norm": "HOLMES HELD OUT A SMALL CHIP WITH THE LETTERS N N AND A SPACE OF CLEAR WOOD AFTER THEM YOU SEE",
"duration_s": 6.25,
"infer_time_s": 1.869,
"rtf": 0.2991,
"wer": 0.0
},
{
"id": "1580-141083-0032",
"ref": "WATSON I HAVE ALWAYS DONE YOU AN INJUSTICE THERE ARE OTHERS",
"hyp": "Watson, I have always done you an injustice. There are others.",
"ref_norm": "WATSON I HAVE ALWAYS DONE YOU AN INJUSTICE THERE ARE OTHERS",
"hyp_norm": "WATSON I HAVE ALWAYS DONE YOU AN INJUSTICE THERE ARE OTHERS",
"duration_s": 4.135,
"infer_time_s": 1.262,
"rtf": 0.3052,
"wer": 0.0
},
{
"id": "1580-141083-0033",
"ref": "I WAS HOPING THAT IF THE PAPER ON WHICH HE WROTE WAS THIN SOME TRACE OF IT MIGHT COME THROUGH UPON THIS POLISHED SURFACE NO I SEE NOTHING",
"hyp": "I was hoping that if the paper on which he wrote was thin , some trace of it might come through upon this polished surface. No, I see nothing.",
"ref_norm": "I WAS HOPING THAT IF THE PAPER ON WHICH HE WROTE WAS THIN SOME TRACE OF IT MIGHT COME THROUGH UPON THIS POLISHED SURFACE NO I SEE NOTHING",
"hyp_norm": "I WAS HOPING THAT IF THE PAPER ON WHICH HE WROTE WAS THIN SOME TRACE OF IT MIGHT COME THROUGH UPON THIS POLISHED SURFACE NO I SEE NOTHING",
"duration_s": 7.45,
"infer_time_s": 2.341,
"rtf": 0.3143,
"wer": 0.0
},
{
"id": "1580-141083-0034",
"ref": "AS HOLMES DREW THE CURTAIN I WAS AWARE FROM SOME LITTLE RIGIDITY AND ALERTNESS OF HIS ATTITUDE THAT HE WAS PREPARED FOR AN EMERGENCY",
"hyp": "As Holmes drew the curtain, I was aware from some little rig idity and alertness of his attitude that he was prepared for an emergency.",
"ref_norm": "AS HOLMES DREW THE CURTAIN I WAS AWARE FROM SOME LITTLE RIGIDITY AND ALERTNESS OF HIS ATTITUDE THAT HE WAS PREPARED FOR AN EMERGENCY",
"hyp_norm": "AS HOLMES DREW THE CURTAIN I WAS AWARE FROM SOME LITTLE RIG IDITY AND ALERTNESS OF HIS ATTITUDE THAT HE WAS PREPARED FOR AN EMERGENCY",
"duration_s": 6.99,
"infer_time_s": 2.123,
"rtf": 0.3037,
"wer": 0.0833
},
{
"id": "1580-141083-0035",
"ref": "HOLMES TURNED AWAY AND STOOPED SUDDENLY TO THE FLOOR HALLOA WHAT'S THIS",
"hyp": "Holmes turned away and sto oped suddenly to the floor . \"Hallo, what is this?\"",
"ref_norm": "HOLMES TURNED AWAY AND STOOPED SUDDENLY TO THE FLOOR HALLOA WHATS THIS",
"hyp_norm": "HOLMES TURNED AWAY AND STO OPED SUDDENLY TO THE FLOOR HALLO WHAT IS THIS",
"duration_s": 4.98,
"infer_time_s": 1.436,
"rtf": 0.2884,
"wer": 0.4167
},
{
"id": "1580-141083-0036",
"ref": "HOLMES HELD IT OUT ON HIS OPEN PALM IN THE GLARE OF THE ELECTRIC LIGHT",
"hyp": "Holmes held it out on his open palm in the glare of the electric light.",
"ref_norm": "HOLMES HELD IT OUT ON HIS OPEN PALM IN THE GLARE OF THE ELECTRIC LIGHT",
"hyp_norm": "HOLMES HELD IT OUT ON HIS OPEN PALM IN THE GLARE OF THE ELECTRIC LIGHT",
"duration_s": 3.98,
"infer_time_s": 1.241,
"rtf": 0.3117,
"wer": 0.0
},
{
"id": "1580-141083-0037",
"ref": "WHAT COULD HE DO HE CAUGHT UP EVERYTHING WHICH WOULD BETRAY HIM AND HE RUSHED INTO YOUR BEDROOM TO CONCEAL HIMSELF",
"hyp": "What could he do? He caught up everything which would betray him , and he rushed into your bedroom to conceal himself.",
"ref_norm": "WHAT COULD HE DO HE CAUGHT UP EVERYTHING WHICH WOULD BETRAY HIM AND HE RUSHED INTO YOUR BEDROOM TO CONCEAL HIMSELF",
"hyp_norm": "WHAT COULD HE DO HE CAUGHT UP EVERYTHING WHICH WOULD BETRAY HIM AND HE RUSHED INTO YOUR BEDROOM TO CONCEAL HIMSELF",
"duration_s": 5.73,
"infer_time_s": 1.722,
"rtf": 0.3006,
"wer": 0.0
},
{
"id": "1580-141083-0038",
"ref": "I UNDERSTAND YOU TO SAY THAT THERE ARE THREE STUDENTS WHO USE THIS STAIR AND ARE IN THE HABIT OF PASSING YOUR DOOR YES THERE ARE",
"hyp": "I understand you to say that there are three students who use this stair and are in the habit of passing your door. Yes, there are.",
"ref_norm": "I UNDERSTAND YOU TO SAY THAT THERE ARE THREE STUDENTS WHO USE THIS STAIR AND ARE IN THE HABIT OF PASSING YOUR DOOR YES THERE ARE",
"hyp_norm": "I UNDERSTAND YOU TO SAY THAT THERE ARE THREE STUDENTS WHO USE THIS STAIR AND ARE IN THE HABIT OF PASSING YOUR DOOR YES THERE ARE",
"duration_s": 7.535,
"infer_time_s": 2.212,
"rtf": 0.2936,
"wer": 0.0
},
{
"id": "1580-141083-0039",
"ref": "AND THEY ARE ALL IN FOR THIS EXAMINATION YES",
"hyp": "And they are all in for this examination? Yes.",
"ref_norm": "AND THEY ARE ALL IN FOR THIS EXAMINATION YES",
"hyp_norm": "AND THEY ARE ALL IN FOR THIS EXAMINATION YES",
"duration_s": 3.725,
"infer_time_s": 0.859,
"rtf": 0.2306,
"wer": 0.0
},
{
"id": "1580-141083-0040",
"ref": "ONE HARDLY LIKES TO THROW SUSPICION WHERE THERE ARE NO PROOFS",
"hyp": "One hardly likes to throw suspicion where there are no proofs.",
"ref_norm": "ONE HARDLY LIKES TO THROW SUSPICION WHERE THERE ARE NO PROOFS",
"hyp_norm": "ONE HARDLY LIKES TO THROW SUSPICION WHERE THERE ARE NO PROOFS",
"duration_s": 3.75,
"infer_time_s": 1.007,
"rtf": 0.2686,
"wer": 0.0
},
{
"id": "1580-141083-0041",
"ref": "LET US HEAR THE SUSPICIONS I WILL LOOK AFTER THE PROOFS",
"hyp": "Let us hear the suspicions . I will look after the proofs.",
"ref_norm": "LET US HEAR THE SUSPICIONS I WILL LOOK AFTER THE PROOFS",
"hyp_norm": "LET US HEAR THE SUSPICIONS I WILL LOOK AFTER THE PROOFS",
"duration_s": 3.575,
"infer_time_s": 0.976,
"rtf": 0.273,
"wer": 0.0
},
{
"id": "1580-141083-0042",
"ref": "MY SCHOLAR HAS BEEN LEFT VERY POOR BUT HE IS HARD WORKING AND INDUSTRIOUS HE WILL DO WELL",
"hyp": "My scholar has been left very poor, but he is hard working and industrious. He will do well.",
"ref_norm": "MY SCHOLAR HAS BEEN LEFT VERY POOR BUT HE IS HARD WORKING AND INDUSTRIOUS HE WILL DO WELL",
"hyp_norm": "MY SCHOLAR HAS BEEN LEFT VERY POOR BUT HE IS HARD WORKING AND INDUSTRIOUS HE WILL DO WELL",
"duration_s": 5.865,
"infer_time_s": 1.55,
"rtf": 0.2643,
"wer": 0.0
},
{
"id": "1580-141083-0043",
"ref": "THE TOP FLOOR BELONGS TO MILES MC LAREN",
"hyp": "The top floor belongs to Miles McLaren.",
"ref_norm": "THE TOP FLOOR BELONGS TO MILES MC LAREN",
"hyp_norm": "THE TOP FLOOR BELONGS TO MILES MCLAREN",
"duration_s": 2.74,
"infer_time_s": 0.694,
"rtf": 0.2535,
"wer": 0.25
},
{
"id": "1580-141083-0044",
"ref": "I DARE NOT GO SO FAR AS THAT BUT OF THE THREE HE IS PERHAPS THE LEAST UNLIKELY",
"hyp": "I dare not go so far as that. But of the three , he is perhaps the least unlikely.",
"ref_norm": "I DARE NOT GO SO FAR AS THAT BUT OF THE THREE HE IS PERHAPS THE LEAST UNLIKELY",
"hyp_norm": "I DARE NOT GO SO FAR AS THAT BUT OF THE THREE HE IS PERHAPS THE LEAST UNLIKELY",
"duration_s": 5.505,
"infer_time_s": 1.495,
"rtf": 0.2716,
"wer": 0.0
},
{
"id": "1580-141083-0045",
"ref": "HE WAS STILL SUFFERING FROM THIS SUDDEN DISTURBANCE OF THE QUIET ROUTINE OF HIS LIFE",
"hyp": "He was still suffering from this sudden disturbance of the quiet routine of his life.",
"ref_norm": "HE WAS STILL SUFFERING FROM THIS SUDDEN DISTURBANCE OF THE QUIET ROUTINE OF HIS LIFE",
"hyp_norm": "HE WAS STILL SUFFERING FROM THIS SUDDEN DISTURBANCE OF THE QUIET ROUTINE OF HIS LIFE",
"duration_s": 4.36,
"infer_time_s": 1.239,
"rtf": 0.2843,
"wer": 0.0
},
{
"id": "1580-141083-0046",
"ref": "BUT I HAVE OCCASIONALLY DONE THE SAME THING AT OTHER TIMES",
"hyp": "But I have occasionally done the same thing at other times.",
"ref_norm": "BUT I HAVE OCCASIONALLY DONE THE SAME THING AT OTHER TIMES",
"hyp_norm": "BUT I HAVE OCCASIONALLY DONE THE SAME THING AT OTHER TIMES",
"duration_s": 3.53,
"infer_time_s": 0.901,
"rtf": 0.2553,
"wer": 0.0
},
{
"id": "1580-141083-0047",
"ref": "DID YOU LOOK AT THESE PAPERS ON THE TABLE",
"hyp": "Did you look at these papers on the table?",
"ref_norm": "DID YOU LOOK AT THESE PAPERS ON THE TABLE",
"hyp_norm": "DID YOU LOOK AT THESE PAPERS ON THE TABLE",
"duration_s": 2.605,
"infer_time_s": 0.786,
"rtf": 0.3019,
"wer": 0.0
},
{
"id": "1580-141083-0048",
"ref": "HOW CAME YOU TO LEAVE THE KEY IN THE DOOR",
"hyp": "How came you to leave the key in the door?",
"ref_norm": "HOW CAME YOU TO LEAVE THE KEY IN THE DOOR",
"hyp_norm": "HOW CAME YOU TO LEAVE THE KEY IN THE DOOR",
"duration_s": 2.785,
"infer_time_s": 0.841,
"rtf": 0.3019,
"wer": 0.0
},
{
"id": "1580-141083-0049",
"ref": "ANYONE IN THE ROOM COULD GET OUT YES SIR",
"hyp": "Any one in the room could get out. Yes, sir.",
"ref_norm": "ANYONE IN THE ROOM COULD GET OUT YES SIR",
"hyp_norm": "ANY ONE IN THE ROOM COULD GET OUT YES SIR",
"duration_s": 3.845,
"infer_time_s": 0.949,
"rtf": 0.2468,
"wer": 0.2222
},
{
"id": "1580-141083-0050",
"ref": "I REALLY DON'T THINK HE KNEW MUCH ABOUT IT MISTER HOLMES",
"hyp": "I really don't think he knew much about it, Mister Holmes.",
"ref_norm": "I REALLY DONT THINK HE KNEW MUCH ABOUT IT MISTER HOLMES",
"hyp_norm": "I REALLY DONT THINK HE KNEW MUCH ABOUT IT MISTER HOLMES",
"duration_s": 3.085,
"infer_time_s": 1.0,
"rtf": 0.324,
"wer": 0.0
},
{
"id": "1580-141083-0051",
"ref": "ONLY FOR A MINUTE OR SO",
"hyp": "Only for a minute or so.",
"ref_norm": "ONLY FOR A MINUTE OR SO",
"hyp_norm": "ONLY FOR A MINUTE OR SO",
"duration_s": 1.98,
"infer_time_s": 0.509,
"rtf": 0.2568,
"wer": 0.0
},
{
"id": "1580-141083-0052",
"ref": "OH I WOULD NOT VENTURE TO SAY SIR",
"hyp": "Oh, I would not venture to say, sir.",
"ref_norm": "OH I WOULD NOT VENTURE TO SAY SIR",
"hyp_norm": "OH I WOULD NOT VENTURE TO SAY SIR",
"duration_s": 3.45,
"infer_time_s": 0.841,
"rtf": 0.2438,
"wer": 0.0
},
{
"id": "1580-141083-0053",
"ref": "YOU HAVEN'T SEEN ANY OF THEM NO SIR",
"hyp": "You haven't seen any of them, no, sir.",
"ref_norm": "YOU HAVENT SEEN ANY OF THEM NO SIR",
"hyp_norm": "YOU HAVENT SEEN ANY OF THEM NO SIR",
"duration_s": 4.015,
"infer_time_s": 1.028,
"rtf": 0.2561,
"wer": 0.0
},
{
"id": "1580-141084-0000",
"ref": "IT WAS THE INDIAN WHOSE DARK SILHOUETTE APPEARED SUDDENLY UPON HIS BLIND",
"hyp": "It was the Indian whose dark silhouette appeared suddenly upon his blind.",
"ref_norm": "IT WAS THE INDIAN WHOSE DARK SILHOUETTE APPEARED SUDDENLY UPON HIS BLIND",
"hyp_norm": "IT WAS THE INDIAN WHOSE DARK SILHOUETTE APPEARED SUDDENLY UPON HIS BLIND",
"duration_s": 4.615,
"infer_time_s": 1.083,
"rtf": 0.2347,
"wer": 0.0
},
{
"id": "1580-141084-0001",
"ref": "HE WAS PACING SWIFTLY UP AND DOWN HIS ROOM",
"hyp": "He was pacing swiftly up and down his room.",
"ref_norm": "HE WAS PACING SWIFTLY UP AND DOWN HIS ROOM",
"hyp_norm": "HE WAS PACING SWIFTLY UP AND DOWN HIS ROOM",
"duration_s": 3.265,
"infer_time_s": 0.791,
"rtf": 0.2423,
"wer": 0.0
},
{
"id": "1580-141084-0002",
"ref": "THIS SET OF ROOMS IS QUITE THE OLDEST IN THE COLLEGE AND IT IS NOT UNUSUAL FOR VISITORS TO GO OVER THEM",
"hyp": "The set of rooms is quite the oldest in the college , and it is not unusual for visitors to go over them.",
"ref_norm": "THIS SET OF ROOMS IS QUITE THE OLDEST IN THE COLLEGE AND IT IS NOT UNUSUAL FOR VISITORS TO GO OVER THEM",
"hyp_norm": "THE SET OF ROOMS IS QUITE THE OLDEST IN THE COLLEGE AND IT IS NOT UNUSUAL FOR VISITORS TO GO OVER THEM",
"duration_s": 5.905,
"infer_time_s": 1.662,
"rtf": 0.2815,
"wer": 0.0455
},
{
"id": "1580-141084-0003",
"ref": "NO NAMES PLEASE SAID HOLMES AS WE KNOCKED AT GILCHRIST'S DOOR",
"hyp": "No names, please ,\" said Holmes as we knocked at Gilchrist's door.",
"ref_norm": "NO NAMES PLEASE SAID HOLMES AS WE KNOCKED AT GILCHRISTS DOOR",
"hyp_norm": "NO NAMES PLEASE SAID HOLMES AS WE KNOCKED AT GILCHRISTS DOOR",
"duration_s": 4.1,
"infer_time_s": 1.242,
"rtf": 0.3028,
"wer": 0.0
},
{
"id": "1580-141084-0004",
"ref": "OF COURSE HE DID NOT REALIZE THAT IT WAS I WHO WAS KNOCKING BUT NONE THE LESS HIS CONDUCT WAS VERY UNCOURTEOUS AND INDEED UNDER THE CIRCUMSTANCES RATHER SUSPICIOUS",
"hyp": "Of course, he did not realize that it was I who was knocking, but nonetheless, his conduct was very uncultious and indeed, under the circumstances, rather suspicious.",
"ref_norm": "OF COURSE HE DID NOT REALIZE THAT IT WAS I WHO WAS KNOCKING BUT NONE THE LESS HIS CONDUCT WAS VERY UNCOURTEOUS AND INDEED UNDER THE CIRCUMSTANCES RATHER SUSPICIOUS",
"hyp_norm": "OF COURSE HE DID NOT REALIZE THAT IT WAS I WHO WAS KNOCKING BUT NONETHELESS HIS CONDUCT WAS VERY UNCULTIOUS AND INDEED UNDER THE CIRCUMSTANCES RATHER SUSPICIOUS",
"duration_s": 9.005,
"infer_time_s": 2.574,
"rtf": 0.2858,
"wer": 0.1379
},
{
"id": "1580-141084-0005",
"ref": "THAT IS VERY IMPORTANT SAID HOLMES",
"hyp": "That is very important ,\" said Holmes.",
"ref_norm": "THAT IS VERY IMPORTANT SAID HOLMES",
"hyp_norm": "THAT IS VERY IMPORTANT SAID HOLMES",
"duration_s": 2.515,
"infer_time_s": 0.687,
"rtf": 0.2731,
"wer": 0.0
},
{
"id": "1580-141084-0006",
"ref": "YOU DON'T SEEM TO REALIZE THE POSITION",
"hyp": "You don't seem to realize the position.",
"ref_norm": "YOU DONT SEEM TO REALIZE THE POSITION",
"hyp_norm": "YOU DONT SEEM TO REALIZE THE POSITION",
"duration_s": 2.135,
"infer_time_s": 0.787,
"rtf": 0.3687,
"wer": 0.0
},
{
"id": "1580-141084-0007",
"ref": "TO MORROW IS THE EXAMINATION",
"hyp": "Tomorrow is the examination.",
"ref_norm": "TO MORROW IS THE EXAMINATION",
"hyp_norm": "TOMORROW IS THE EXAMINATION",
"duration_s": 2.02,
"infer_time_s": 0.548,
"rtf": 0.2715,
"wer": 0.4
},
{
"id": "1580-141084-0008",
"ref": "I CANNOT ALLOW THE EXAMINATION TO BE HELD IF ONE OF THE PAPERS HAS BEEN TAMPERED WITH THE SITUATION MUST BE FACED",
"hyp": "I cannot allow the examination to be held if one of the papers has been tampered with. The situation must be faced.",
"ref_norm": "I CANNOT ALLOW THE EXAMINATION TO BE HELD IF ONE OF THE PAPERS HAS BEEN TAMPERED WITH THE SITUATION MUST BE FACED",
"hyp_norm": "I CANNOT ALLOW THE EXAMINATION TO BE HELD IF ONE OF THE PAPERS HAS BEEN TAMPERED WITH THE SITUATION MUST BE FACED",
"duration_s": 6.795,
"infer_time_s": 1.968,
"rtf": 0.2896,
"wer": 0.0
},
{
"id": "1580-141084-0009",
"ref": "IT IS POSSIBLE THAT I MAY BE IN A POSITION THEN TO INDICATE SOME COURSE OF ACTION",
"hyp": "It is possible that I may be in a position then to indicate some course of action.",
"ref_norm": "IT IS POSSIBLE THAT I MAY BE IN A POSITION THEN TO INDICATE SOME COURSE OF ACTION",
"hyp_norm": "IT IS POSSIBLE THAT I MAY BE IN A POSITION THEN TO INDICATE SOME COURSE OF ACTION",
"duration_s": 4.685,
"infer_time_s": 1.461,
"rtf": 0.3119,
"wer": 0.0
},
{
"id": "1580-141084-0010",
"ref": "I WILL TAKE THE BLACK CLAY WITH ME ALSO THE PENCIL CUTTINGS GOOD BYE",
"hyp": "I will take the black clay with me, also the pencil cut tings. Goodbye.",
"ref_norm": "I WILL TAKE THE BLACK CLAY WITH ME ALSO THE PENCIL CUTTINGS GOOD BYE",
"hyp_norm": "I WILL TAKE THE BLACK CLAY WITH ME ALSO THE PENCIL CUT TINGS GOODBYE",
"duration_s": 4.47,
"infer_time_s": 1.369,
"rtf": 0.3063,
"wer": 0.2143
},
{
"id": "1580-141084-0011",
"ref": "WHEN WE WERE OUT IN THE DARKNESS OF THE QUADRANGLE WE AGAIN LOOKED UP AT THE WINDOWS",
"hyp": "When we were out in the darkness of the quadrangle, we again looked up at the windows.",
"ref_norm": "WHEN WE WERE OUT IN THE DARKNESS OF THE QUADRANGLE WE AGAIN LOOKED UP AT THE WINDOWS",
"hyp_norm": "WHEN WE WERE OUT IN THE DARKNESS OF THE QUADRANGLE WE AGAIN LOOKED UP AT THE WINDOWS",
"duration_s": 5.0,
"infer_time_s": 1.461,
"rtf": 0.2922,
"wer": 0.0
},
{
"id": "1580-141084-0012",
"ref": "THE FOUL MOUTHED FELLOW AT THE TOP",
"hyp": "The foul-mouthed fellow at the top.",
"ref_norm": "THE FOUL MOUTHED FELLOW AT THE TOP",
"hyp_norm": "THE FOULMOUTHED FELLOW AT THE TOP",
"duration_s": 2.485,
"infer_time_s": 0.81,
"rtf": 0.3259,
"wer": 0.2857
},
{
"id": "1580-141084-0013",
"ref": "HE IS THE ONE WITH THE WORST RECORD",
"hyp": "He is the one with the worst record.",
"ref_norm": "HE IS THE ONE WITH THE WORST RECORD",
"hyp_norm": "HE IS THE ONE WITH THE WORST RECORD",
"duration_s": 2.225,
"infer_time_s": 0.739,
"rtf": 0.3319,
"wer": 0.0
},
{
"id": "1580-141084-0014",
"ref": "WHY BANNISTER THE SERVANT WHAT'S HIS GAME IN THE MATTER",
"hyp": "Why, Bannister, the servant? What's his game in the matter?",
"ref_norm": "WHY BANNISTER THE SERVANT WHATS HIS GAME IN THE MATTER",
"hyp_norm": "WHY BANNISTER THE SERVANT WHATS HIS GAME IN THE MATTER",
"duration_s": 3.97,
"infer_time_s": 1.166,
"rtf": 0.2937,
"wer": 0.0
},
{
"id": "1580-141084-0015",
"ref": "HE IMPRESSED ME AS BEING A PERFECTLY HONEST MAN",
"hyp": "He impressed me as being a perfectly honest man.",
"ref_norm": "HE IMPRESSED ME AS BEING A PERFECTLY HONEST MAN",
"hyp_norm": "HE IMPRESSED ME AS BEING A PERFECTLY HONEST MAN",
"duration_s": 3.47,
"infer_time_s": 0.795,
"rtf": 0.229,
"wer": 0.0
},
{
"id": "1580-141084-0016",
"ref": "MY FRIEND DID NOT APPEAR TO BE DEPRESSED BY HIS FAILURE BUT SHRUGGED HIS SHOULDERS IN HALF HUMOROUS RESIGNATION",
"hyp": "My friend did not appear to be depressed by his failure, but shrugged his shoulders in half-humorous resignation.",
"ref_norm": "MY FRIEND DID NOT APPEAR TO BE DEPRESSED BY HIS FAILURE BUT SHRUGGED HIS SHOULDERS IN HALF HUMOROUS RESIGNATION",
"hyp_norm": "MY FRIEND DID NOT APPEAR TO BE DEPRESSED BY HIS FAILURE BUT SHRUGGED HIS SHOULDERS IN HALFHUMOROUS RESIGNATION",
"duration_s": 5.96,
"infer_time_s": 1.601,
"rtf": 0.2686,
"wer": 0.1053
},
{
"id": "1580-141084-0017",
"ref": "NO GOOD MY DEAR WATSON",
"hyp": "No good, my dear Watson.",
"ref_norm": "NO GOOD MY DEAR WATSON",
"hyp_norm": "NO GOOD MY DEAR WATSON",
"duration_s": 2.0,
"infer_time_s": 0.509,
"rtf": 0.2543,
"wer": 0.0
},
{
"id": "1580-141084-0018",
"ref": "I THINK SO YOU HAVE FORMED A CONCLUSION",
"hyp": "I think so. You have formed a conclusion.",
"ref_norm": "I THINK SO YOU HAVE FORMED A CONCLUSION",
"hyp_norm": "I THINK SO YOU HAVE FORMED A CONCLUSION",
"duration_s": 3.345,
"infer_time_s": 0.738,
"rtf": 0.2206,
"wer": 0.0
},
{
"id": "1580-141084-0019",
"ref": "YES MY DEAR WATSON I HAVE SOLVED THE MYSTERY",
"hyp": "Yes, my dear Watson, I have solved the mystery.",
"ref_norm": "YES MY DEAR WATSON I HAVE SOLVED THE MYSTERY",
"hyp_norm": "YES MY DEAR WATSON I HAVE SOLVED THE MYSTERY",
"duration_s": 3.125,
"infer_time_s": 0.905,
"rtf": 0.2897,
"wer": 0.0
},
{
"id": "1580-141084-0020",
"ref": "LOOK AT THAT HE HELD OUT HIS HAND",
"hyp": "Look at that! He held out his hand.",
"ref_norm": "LOOK AT THAT HE HELD OUT HIS HAND",
"hyp_norm": "LOOK AT THAT HE HELD OUT HIS HAND",
"duration_s": 2.86,
"infer_time_s": 0.817,
"rtf": 0.2858,
"wer": 0.0
},
{
"id": "1580-141084-0021",
"ref": "ON THE PALM WERE THREE LITTLE PYRAMIDS OF BLACK DOUGHY CLAY",
"hyp": "On the palm were three little pyramids of black, doughy clay.",
"ref_norm": "ON THE PALM WERE THREE LITTLE PYRAMIDS OF BLACK DOUGHY CLAY",
"hyp_norm": "ON THE PALM WERE THREE LITTLE PYRAMIDS OF BLACK DOUGHY CLAY",
"duration_s": 4.01,
"infer_time_s": 1.268,
"rtf": 0.3162,
"wer": 0.0
},
{
"id": "1580-141084-0022",
"ref": "AND ONE MORE THIS MORNING",
"hyp": "And one more this morning.",
"ref_norm": "AND ONE MORE THIS MORNING",
"hyp_norm": "AND ONE MORE THIS MORNING",
"duration_s": 2.06,
"infer_time_s": 0.588,
"rtf": 0.2853,
"wer": 0.0
},
{
"id": "1580-141084-0023",
"ref": "IN A FEW HOURS THE EXAMINATION WOULD COMMENCE AND HE WAS STILL IN THE DILEMMA BETWEEN MAKING THE FACTS PUBLIC AND ALLOWING THE CULPRIT TO COMPETE FOR THE VALUABLE SCHOLARSHIP",
"hyp": "In a few hours, the examination would commence, and he was still in the dilemma between making the facts public and allowing the culprit to compete for the valuable scholarship.",
"ref_norm": "IN A FEW HOURS THE EXAMINATION WOULD COMMENCE AND HE WAS STILL IN THE DILEMMA BETWEEN MAKING THE FACTS PUBLIC AND ALLOWING THE CULPRIT TO COMPETE FOR THE VALUABLE SCHOLARSHIP",
"hyp_norm": "IN A FEW HOURS THE EXAMINATION WOULD COMMENCE AND HE WAS STILL IN THE DILEMMA BETWEEN MAKING THE FACTS PUBLIC AND ALLOWING THE CULPRIT TO COMPETE FOR THE VALUABLE SCHOLARSHIP",
"duration_s": 8.735,
"infer_time_s": 2.482,
"rtf": 0.2841,
"wer": 0.0
},
{
"id": "1580-141084-0024",
"ref": "HE COULD HARDLY STAND STILL SO GREAT WAS HIS MENTAL AGITATION AND HE RAN TOWARDS HOLMES WITH TWO EAGER HANDS OUTSTRETCHED THANK HEAVEN THAT YOU HAVE COME",
"hyp": "He could hardly stand still. So great was his mental agitation, and he ran towards Holmes with two eager hands outstretched. \"Thank heaven that you have come.\"",
"ref_norm": "HE COULD HARDLY STAND STILL SO GREAT WAS HIS MENTAL AGITATION AND HE RAN TOWARDS HOLMES WITH TWO EAGER HANDS OUTSTRETCHED THANK HEAVEN THAT YOU HAVE COME",
"hyp_norm": "HE COULD HARDLY STAND STILL SO GREAT WAS HIS MENTAL AGITATION AND HE RAN TOWARDS HOLMES WITH TWO EAGER HANDS OUTSTRETCHED THANK HEAVEN THAT YOU HAVE COME",
"duration_s": 9.185,
"infer_time_s": 2.54,
"rtf": 0.2765,
"wer": 0.0
},
{
"id": "1580-141084-0025",
"ref": "YOU KNOW HIM I THINK SO",
"hyp": "You know him, I think so.",
"ref_norm": "YOU KNOW HIM I THINK SO",
"hyp_norm": "YOU KNOW HIM I THINK SO",
"duration_s": 2.375,
"infer_time_s": 0.709,
"rtf": 0.2987,
"wer": 0.0
},
{
"id": "1580-141084-0026",
"ref": "IF THIS MATTER IS NOT TO BECOME PUBLIC WE MUST GIVE OURSELVES CERTAIN POWERS AND RESOLVE OURSELVES INTO A SMALL PRIVATE COURT MARTIAL",
"hyp": "If this matter is not to become public, we must give ourselves certain powers and resolve ourselves into a small private court martial.",
"ref_norm": "IF THIS MATTER IS NOT TO BECOME PUBLIC WE MUST GIVE OURSELVES CERTAIN POWERS AND RESOLVE OURSELVES INTO A SMALL PRIVATE COURT MARTIAL",
"hyp_norm": "IF THIS MATTER IS NOT TO BECOME PUBLIC WE MUST GIVE OURSELVES CERTAIN POWERS AND RESOLVE OURSELVES INTO A SMALL PRIVATE COURT MARTIAL",
"duration_s": 6.995,
"infer_time_s": 1.889,
"rtf": 0.2701,
"wer": 0.0
},
{
"id": "1580-141084-0027",
"ref": "NO SIR CERTAINLY NOT",
"hyp": "No, sir , certainly not.",
"ref_norm": "NO SIR CERTAINLY NOT",
"hyp_norm": "NO SIR CERTAINLY NOT",
"duration_s": 3.36,
"infer_time_s": 0.638,
"rtf": 0.19,
"wer": 0.0
},
{
"id": "1580-141084-0028",
"ref": "THERE WAS NO MAN SIR",
"hyp": "There was no man, sir.",
"ref_norm": "THERE WAS NO MAN SIR",
"hyp_norm": "THERE WAS NO MAN SIR",
"duration_s": 2.655,
"infer_time_s": 0.648,
"rtf": 0.2442,
"wer": 0.0
},
{
"id": "1580-141084-0029",
"ref": "HIS TROUBLED BLUE EYES GLANCED AT EACH OF US AND FINALLY RESTED WITH AN EXPRESSION OF BLANK DISMAY UPON BANNISTER IN THE FARTHER CORNER",
"hyp": "His troubled blue eyes glanced at each of us and finally rested with an expression of blank dismay upon Bannister in the farther corner.",
"ref_norm": "HIS TROUBLED BLUE EYES GLANCED AT EACH OF US AND FINALLY RESTED WITH AN EXPRESSION OF BLANK DISMAY UPON BANNISTER IN THE FARTHER CORNER",
"hyp_norm": "HIS TROUBLED BLUE EYES GLANCED AT EACH OF US AND FINALLY RESTED WITH AN EXPRESSION OF BLANK DISMAY UPON BANNISTER IN THE FARTHER CORNER",
"duration_s": 8.075,
"infer_time_s": 2.17,
"rtf": 0.2687,
"wer": 0.0
},
{
"id": "1580-141084-0030",
"ref": "JUST CLOSE THE DOOR SAID HOLMES",
"hyp": "Just close the door,\" said Holmes.",
"ref_norm": "JUST CLOSE THE DOOR SAID HOLMES",
"hyp_norm": "JUST CLOSE THE DOOR SAID HOLMES",
"duration_s": 2.145,
"infer_time_s": 0.692,
"rtf": 0.3227,
"wer": 0.0
},
{
"id": "1580-141084-0031",
"ref": "WE WANT TO KNOW MISTER GILCHRIST HOW YOU AN HONOURABLE MAN EVER CAME TO COMMIT SUCH AN ACTION AS THAT OF YESTERDAY",
"hyp": "We want to know, Mister Gil christ, how you, an honorable man, ever came to commit such an action as that of yesterday.",
"ref_norm": "WE WANT TO KNOW MISTER GILCHRIST HOW YOU AN HONOURABLE MAN EVER CAME TO COMMIT SUCH AN ACTION AS THAT OF YESTERDAY",
"hyp_norm": "WE WANT TO KNOW MISTER GIL CHRIST HOW YOU AN HONORABLE MAN EVER CAME TO COMMIT SUCH AN ACTION AS THAT OF YESTERDAY",
"duration_s": 6.47,
"infer_time_s": 2.052,
"rtf": 0.3172,
"wer": 0.1364
},
{
"id": "1580-141084-0032",
"ref": "FOR A MOMENT GILCHRIST WITH UPRAISED HAND TRIED TO CONTROL HIS WRITHING FEATURES",
"hyp": "For a moment, Gilchrist , with upraised hand, tried to control his writhing features.",
"ref_norm": "FOR A MOMENT GILCHRIST WITH UPRAISED HAND TRIED TO CONTROL HIS WRITHING FEATURES",
"hyp_norm": "FOR A MOMENT GILCHRIST WITH UPRAISED HAND TRIED TO CONTROL HIS WRITHING FEATURES",
"duration_s": 4.995,
"infer_time_s": 1.546,
"rtf": 0.3094,
"wer": 0.0
},
{
"id": "1580-141084-0033",
"ref": "COME COME SAID HOLMES KINDLY IT IS HUMAN TO ERR AND AT LEAST NO ONE CAN ACCUSE YOU OF BEING A CALLOUS CRIMINAL",
"hyp": "Come, come,\" said Holmes kindly. \"It is human to err, and at least no one can accuse you of being a callous criminal.\"",
"ref_norm": "COME COME SAID HOLMES KINDLY IT IS HUMAN TO ERR AND AT LEAST NO ONE CAN ACCUSE YOU OF BEING A CALLOUS CRIMINAL",
"hyp_norm": "COME COME SAID HOLMES KINDLY IT IS HUMAN TO ERR AND AT LEAST NO ONE CAN ACCUSE YOU OF BEING A CALLOUS CRIMINAL",
"duration_s": 7.0,
"infer_time_s": 2.225,
"rtf": 0.3179,
"wer": 0.0
},
{
"id": "1580-141084-0034",
"ref": "WELL WELL DON'T TROUBLE TO ANSWER LISTEN AND SEE THAT I DO YOU NO INJUSTICE",
"hyp": "Well, well, don't trouble to answer. Listen and see that I do you no injustice.",
"ref_norm": "WELL WELL DONT TROUBLE TO ANSWER LISTEN AND SEE THAT I DO YOU NO INJUSTICE",
"hyp_norm": "WELL WELL DONT TROUBLE TO ANSWER LISTEN AND SEE THAT I DO YOU NO INJUSTICE",
"duration_s": 4.49,
"infer_time_s": 1.629,
"rtf": 0.3629,
"wer": 0.0
},
{
"id": "1580-141084-0035",
"ref": "HE COULD EXAMINE THE PAPERS IN HIS OWN OFFICE",
"hyp": "He could examine the papers in his own office.",
"ref_norm": "HE COULD EXAMINE THE PAPERS IN HIS OWN OFFICE",
"hyp_norm": "HE COULD EXAMINE THE PAPERS IN HIS OWN OFFICE",
"duration_s": 2.63,
"infer_time_s": 0.835,
"rtf": 0.3174,
"wer": 0.0
},
{
"id": "1580-141084-0036",
"ref": "THE INDIAN I ALSO THOUGHT NOTHING OF",
"hyp": "The Indian. I also thought nothing of.",
"ref_norm": "THE INDIAN I ALSO THOUGHT NOTHING OF",
"hyp_norm": "THE INDIAN I ALSO THOUGHT NOTHING OF",
"duration_s": 2.475,
"infer_time_s": 0.783,
"rtf": 0.3164,
"wer": 0.0
},
{
"id": "1580-141084-0037",
"ref": "WHEN I APPROACHED YOUR ROOM I EXAMINED THE WINDOW",
"hyp": "When I approached your room , I examined the window.",
"ref_norm": "WHEN I APPROACHED YOUR ROOM I EXAMINED THE WINDOW",
"hyp_norm": "WHEN I APPROACHED YOUR ROOM I EXAMINED THE WINDOW",
"duration_s": 2.965,
"infer_time_s": 0.874,
"rtf": 0.2948,
"wer": 0.0
},
{
"id": "1580-141084-0038",
"ref": "NO ONE LESS THAN THAT WOULD HAVE A CHANCE",
"hyp": "No one less than that would have a chance.",
"ref_norm": "NO ONE LESS THAN THAT WOULD HAVE A CHANCE",
"hyp_norm": "NO ONE LESS THAN THAT WOULD HAVE A CHANCE",
"duration_s": 2.955,
"infer_time_s": 0.803,
"rtf": 0.2719,
"wer": 0.0
},
{
"id": "1580-141084-0039",
"ref": "I ENTERED AND I TOOK YOU INTO MY CONFIDENCE AS TO THE SUGGESTIONS OF THE SIDE TABLE",
"hyp": "I entered, and I took you into my confidence as to the suggestions of the side table.",
"ref_norm": "I ENTERED AND I TOOK YOU INTO MY CONFIDENCE AS TO THE SUGGESTIONS OF THE SIDE TABLE",
"hyp_norm": "I ENTERED AND I TOOK YOU INTO MY CONFIDENCE AS TO THE SUGGESTIONS OF THE SIDE TABLE",
"duration_s": 4.885,
"infer_time_s": 1.441,
"rtf": 0.2951,
"wer": 0.0
},
{
"id": "1580-141084-0040",
"ref": "HE RETURNED CARRYING HIS JUMPING SHOES WHICH ARE PROVIDED AS YOU ARE AWARE WITH SEVERAL SHARP SPIKES",
"hyp": "He returned carrying his jumping shoes, which are provided, as you are aware, with several sharp spikes.",
"ref_norm": "HE RETURNED CARRYING HIS JUMPING SHOES WHICH ARE PROVIDED AS YOU ARE AWARE WITH SEVERAL SHARP SPIKES",
"hyp_norm": "HE RETURNED CARRYING HIS JUMPING SHOES WHICH ARE PROVIDED AS YOU ARE AWARE WITH SEVERAL SHARP SPIKES",
"duration_s": 5.985,
"infer_time_s": 1.614,
"rtf": 0.2697,
"wer": 0.0
},
{
"id": "1580-141084-0041",
"ref": "NO HARM WOULD HAVE BEEN DONE HAD IT NOT BEEN THAT AS HE PASSED YOUR DOOR HE PERCEIVED THE KEY WHICH HAD BEEN LEFT BY THE CARELESSNESS OF YOUR SERVANT",
"hyp": "No harm would have been done had it not been that as he passed your door, he perceived the key which had been left by the carelessness of your servant.",
"ref_norm": "NO HARM WOULD HAVE BEEN DONE HAD IT NOT BEEN THAT AS HE PASSED YOUR DOOR HE PERCEIVED THE KEY WHICH HAD BEEN LEFT BY THE CARELESSNESS OF YOUR SERVANT",
"hyp_norm": "NO HARM WOULD HAVE BEEN DONE HAD IT NOT BEEN THAT AS HE PASSED YOUR DOOR HE PERCEIVED THE KEY WHICH HAD BEEN LEFT BY THE CARELESSNESS OF YOUR SERVANT",
"duration_s": 7.99,
"infer_time_s": 2.444,
"rtf": 0.3058,
"wer": 0.0
},
{
"id": "1580-141084-0042",
"ref": "A SUDDEN IMPULSE CAME OVER HIM TO ENTER AND SEE IF THEY WERE INDEED THE PROOFS",
"hyp": "A sudden impulse came over him to enter and see if they were indeed the proofs.",
"ref_norm": "A SUDDEN IMPULSE CAME OVER HIM TO ENTER AND SEE IF THEY WERE INDEED THE PROOFS",
"hyp_norm": "A SUDDEN IMPULSE CAME OVER HIM TO ENTER AND SEE IF THEY WERE INDEED THE PROOFS",
"duration_s": 5.06,
"infer_time_s": 1.32,
"rtf": 0.2608,
"wer": 0.0
},
{
"id": "1580-141084-0043",
"ref": "HE PUT HIS SHOES ON THE TABLE",
"hyp": "He put his shoes on the table.",
"ref_norm": "HE PUT HIS SHOES ON THE TABLE",
"hyp_norm": "HE PUT HIS SHOES ON THE TABLE",
"duration_s": 2.065,
"infer_time_s": 0.689,
"rtf": 0.3335,
"wer": 0.0
},
{
"id": "1580-141084-0044",
"ref": "GLOVES SAID THE YOUNG MAN",
"hyp": "Gloves,\" said the young man.",
"ref_norm": "GLOVES SAID THE YOUNG MAN",
"hyp_norm": "GLOVES SAID THE YOUNG MAN",
"duration_s": 2.895,
"infer_time_s": 0.748,
"rtf": 0.2583,
"wer": 0.0
},
{
"id": "1580-141084-0045",
"ref": "SUDDENLY HE HEARD HIM AT THE VERY DOOR THERE WAS NO POSSIBLE ESCAPE",
"hyp": "Suddenly, he heard him at the very door. There was no possible escape.",
"ref_norm": "SUDDENLY HE HEARD HIM AT THE VERY DOOR THERE WAS NO POSSIBLE ESCAPE",
"hyp_norm": "SUDDENLY HE HEARD HIM AT THE VERY DOOR THERE WAS NO POSSIBLE ESCAPE",
"duration_s": 3.625,
"infer_time_s": 1.113,
"rtf": 0.3071,
"wer": 0.0
},
{
"id": "1580-141084-0046",
"ref": "HAVE I TOLD THE TRUTH MISTER GILCHRIST",
"hyp": "Have I told the truth, Mister Gilchrist?",
"ref_norm": "HAVE I TOLD THE TRUTH MISTER GILCHRIST",
"hyp_norm": "HAVE I TOLD THE TRUTH MISTER GILCHRIST",
"duration_s": 2.35,
"infer_time_s": 0.788,
"rtf": 0.3353,
"wer": 0.0
},
{
"id": "1580-141084-0047",
"ref": "I HAVE A LETTER HERE MISTER SOAMES WHICH I WROTE TO YOU EARLY THIS MORNING IN THE MIDDLE OF A RESTLESS NIGHT",
"hyp": "I have a letter here, Mister Sol mes, which I wrote to you early this morning in the middle of a restless night.",
"ref_norm": "I HAVE A LETTER HERE MISTER SOAMES WHICH I WROTE TO YOU EARLY THIS MORNING IN THE MIDDLE OF A RESTLESS NIGHT",
"hyp_norm": "I HAVE A LETTER HERE MISTER SOL MES WHICH I WROTE TO YOU EARLY THIS MORNING IN THE MIDDLE OF A RESTLESS NIGHT",
"duration_s": 5.25,
"infer_time_s": 1.766,
"rtf": 0.3364,
"wer": 0.0909
},
{
"id": "1580-141084-0048",
"ref": "IT WILL BE CLEAR TO YOU FROM WHAT I HAVE SAID THAT ONLY YOU COULD HAVE LET THIS YOUNG MAN OUT SINCE YOU WERE LEFT IN THE ROOM AND MUST HAVE LOCKED THE DOOR WHEN YOU WENT OUT",
"hyp": "It will be clear to you from what I have said that only you could have let this young man out since you were left in the room and must have locked the door when you went out.",
"ref_norm": "IT WILL BE CLEAR TO YOU FROM WHAT I HAVE SAID THAT ONLY YOU COULD HAVE LET THIS YOUNG MAN OUT SINCE YOU WERE LEFT IN THE ROOM AND MUST HAVE LOCKED THE DOOR WHEN YOU WENT OUT",
"hyp_norm": "IT WILL BE CLEAR TO YOU FROM WHAT I HAVE SAID THAT ONLY YOU COULD HAVE LET THIS YOUNG MAN OUT SINCE YOU WERE LEFT IN THE ROOM AND MUST HAVE LOCKED THE DOOR WHEN YOU WENT OUT",
"duration_s": 9.265,
"infer_time_s": 2.886,
"rtf": 0.3115,
"wer": 0.0
},
{
"id": "1580-141084-0049",
"ref": "IT WAS SIMPLE ENOUGH SIR IF YOU ONLY HAD KNOWN BUT WITH ALL YOUR CLEVERNESS IT WAS IMPOSSIBLE THAT YOU COULD KNOW",
"hyp": "It was simple enough, sir, if you only had known. But with all your cleverness, it was impossible that you could know.",
"ref_norm": "IT WAS SIMPLE ENOUGH SIR IF YOU ONLY HAD KNOWN BUT WITH ALL YOUR CLEVERNESS IT WAS IMPOSSIBLE THAT YOU COULD KNOW",
"hyp_norm": "IT WAS SIMPLE ENOUGH SIR IF YOU ONLY HAD KNOWN BUT WITH ALL YOUR CLEVERNESS IT WAS IMPOSSIBLE THAT YOU COULD KNOW",
"duration_s": 7.575,
"infer_time_s": 2.059,
"rtf": 0.2718,
"wer": 0.0
},
{
"id": "1580-141084-0050",
"ref": "IF MISTER SOAMES SAW THEM THE GAME WAS UP",
"hyp": "If Mister Solmes saw them, the game was up.",
"ref_norm": "IF MISTER SOAMES SAW THEM THE GAME WAS UP",
"hyp_norm": "IF MISTER SOLMES SAW THEM THE GAME WAS UP",
"duration_s": 2.78,
"infer_time_s": 0.922,
"rtf": 0.3318,
"wer": 0.1111
},
{
"id": "1995-1826-0000",
"ref": "IN THE DEBATE BETWEEN THE SENIOR SOCIETIES HER DEFENCE OF THE FIFTEENTH AMENDMENT HAD BEEN NOT ONLY A NOTABLE BIT OF REASONING BUT DELIVERED WITH REAL ENTHUSIASM",
"hyp": "In the debate between the senior societies, her defense of the fifteenth amendment had been not only a notable bit of reasoning but delivered with real enthusiasm.",
"ref_norm": "IN THE DEBATE BETWEEN THE SENIOR SOCIETIES HER DEFENCE OF THE FIFTEENTH AMENDMENT HAD BEEN NOT ONLY A NOTABLE BIT OF REASONING BUT DELIVERED WITH REAL ENTHUSIASM",
"hyp_norm": "IN THE DEBATE BETWEEN THE SENIOR SOCIETIES HER DEFENSE OF THE FIFTEENTH AMENDMENT HAD BEEN NOT ONLY A NOTABLE BIT OF REASONING BUT DELIVERED WITH REAL ENTHUSIASM",
"duration_s": 9.485,
"infer_time_s": 2.426,
"rtf": 0.2557,
"wer": 0.037
},
{
"id": "1995-1826-0001",
"ref": "THE SOUTH SHE HAD NOT THOUGHT OF SERIOUSLY AND YET KNOWING OF ITS DELIGHTFUL HOSPITALITY AND MILD CLIMATE SHE WAS NOT AVERSE TO CHARLESTON OR NEW ORLEANS",
"hyp": "The south she had not thought of seriously, and yet, knowing of its delightful hospitality and mild climate, she was not averse to Charleston or New Orleans.",
"ref_norm": "THE SOUTH SHE HAD NOT THOUGHT OF SERIOUSLY AND YET KNOWING OF ITS DELIGHTFUL HOSPITALITY AND MILD CLIMATE SHE WAS NOT AVERSE TO CHARLESTON OR NEW ORLEANS",
"hyp_norm": "THE SOUTH SHE HAD NOT THOUGHT OF SERIOUSLY AND YET KNOWING OF ITS DELIGHTFUL HOSPITALITY AND MILD CLIMATE SHE WAS NOT AVERSE TO CHARLESTON OR NEW ORLEANS",
"duration_s": 10.17,
"infer_time_s": 2.643,
"rtf": 0.2599,
"wer": 0.0
},
{
"id": "1995-1826-0002",
"ref": "JOHN TAYLOR WHO HAD SUPPORTED HER THROUGH COLLEGE WAS INTERESTED IN COTTON",
"hyp": "John Taylor, who had supported her through college, was interested in cotton.",
"ref_norm": "JOHN TAYLOR WHO HAD SUPPORTED HER THROUGH COLLEGE WAS INTERESTED IN COTTON",
"hyp_norm": "JOHN TAYLOR WHO HAD SUPPORTED HER THROUGH COLLEGE WAS INTERESTED IN COTTON",
"duration_s": 4.605,
"infer_time_s": 1.207,
"rtf": 0.2622,
"wer": 0.0
},
{
"id": "1995-1826-0003",
"ref": "BETTER GO HE HAD COUNSELLED SENTENTIOUSLY",
"hyp": "Better go,\" he at counsel sententiously.",
"ref_norm": "BETTER GO HE HAD COUNSELLED SENTENTIOUSLY",
"hyp_norm": "BETTER GO HE AT COUNSEL SENTENTIOUSLY",
"duration_s": 3.09,
"infer_time_s": 0.805,
"rtf": 0.2605,
"wer": 0.3333
},
{
"id": "1995-1826-0004",
"ref": "MIGHT LEARN SOMETHING USEFUL DOWN THERE",
"hyp": "Might learn something useful down there.",
"ref_norm": "MIGHT LEARN SOMETHING USEFUL DOWN THERE",
"hyp_norm": "MIGHT LEARN SOMETHING USEFUL DOWN THERE",
"duration_s": 3.035,
"infer_time_s": 0.707,
"rtf": 0.2328,
"wer": 0.0
},
{
"id": "1995-1826-0005",
"ref": "BUT JOHN THERE'S NO SOCIETY JUST ELEMENTARY WORK",
"hyp": "But John, there's no society \u2014just elementary work.",
"ref_norm": "BUT JOHN THERES NO SOCIETY JUST ELEMENTARY WORK",
"hyp_norm": "BUT JOHN THERES NO SOCIETY JUST ELEMENTARY WORK",
"duration_s": 5.125,
"infer_time_s": 1.161,
"rtf": 0.2266,
"wer": 0.0
},
{
"id": "1995-1826-0006",
"ref": "BEEN LOOKING UP TOOMS COUNTY",
"hyp": "Been looking up Toombs County.",
"ref_norm": "BEEN LOOKING UP TOOMS COUNTY",
"hyp_norm": "BEEN LOOKING UP TOOMBS COUNTY",
"duration_s": 2.455,
"infer_time_s": 0.644,
"rtf": 0.2625,
"wer": 0.2
},
{
"id": "1995-1826-0007",
"ref": "FIND SOME CRESSWELLS THERE BIG PLANTATIONS RATED AT TWO HUNDRED AND FIFTY THOUSAND DOLLARS",
"hyp": "Find some crustules there, big plantations rated at two hundred and fifty thousand dollars.",
"ref_norm": "FIND SOME CRESSWELLS THERE BIG PLANTATIONS RATED AT TWO HUNDRED AND FIFTY THOUSAND DOLLARS",
"hyp_norm": "FIND SOME CRUSTULES THERE BIG PLANTATIONS RATED AT TWO HUNDRED AND FIFTY THOUSAND DOLLARS",
"duration_s": 7.06,
"infer_time_s": 1.538,
"rtf": 0.2178,
"wer": 0.0714
},
{
"id": "1995-1826-0008",
"ref": "SOME OTHERS TOO BIG COTTON COUNTY",
"hyp": "Some others too, big Cotton County.",
"ref_norm": "SOME OTHERS TOO BIG COTTON COUNTY",
"hyp_norm": "SOME OTHERS TOO BIG COTTON COUNTY",
"duration_s": 2.895,
"infer_time_s": 0.713,
"rtf": 0.2462,
"wer": 0.0
},
{
"id": "1995-1826-0009",
"ref": "YOU OUGHT TO KNOW JOHN IF I TEACH NEGROES I'LL SCARCELY SEE MUCH OF PEOPLE IN MY OWN CLASS",
"hyp": "You ought to know, John. If I teach Negroes, I'll scarcely see much of people in my own class.",
"ref_norm": "YOU OUGHT TO KNOW JOHN IF I TEACH NEGROES ILL SCARCELY SEE MUCH OF PEOPLE IN MY OWN CLASS",
"hyp_norm": "YOU OUGHT TO KNOW JOHN IF I TEACH NEGROES ILL SCARCELY SEE MUCH OF PEOPLE IN MY OWN CLASS",
"duration_s": 7.57,
"infer_time_s": 1.965,
"rtf": 0.2595,
"wer": 0.0
},
{
"id": "1995-1826-0010",
"ref": "AT ANY RATE I SAY GO",
"hyp": "At any rate, I say go.",
"ref_norm": "AT ANY RATE I SAY GO",
"hyp_norm": "AT ANY RATE I SAY GO",
"duration_s": 2.445,
"infer_time_s": 0.714,
"rtf": 0.2919,
"wer": 0.0
},
{
"id": "1995-1826-0011",
"ref": "HERE SHE WAS TEACHING DIRTY CHILDREN AND THE SMELL OF CONFUSED ODORS AND BODILY PERSPIRATION WAS TO HER AT TIMES UNBEARABLE",
"hyp": "Here she was teaching dirty children, and the smell of confused odors and bodily perspiration was to her at times unbearable.",
"ref_norm": "HERE SHE WAS TEACHING DIRTY CHILDREN AND THE SMELL OF CONFUSED ODORS AND BODILY PERSPIRATION WAS TO HER AT TIMES UNBEARABLE",
"hyp_norm": "HERE SHE WAS TEACHING DIRTY CHILDREN AND THE SMELL OF CONFUSED ODORS AND BODILY PERSPIRATION WAS TO HER AT TIMES UNBEARABLE",
"duration_s": 8.94,
"infer_time_s": 2.137,
"rtf": 0.239,
"wer": 0.0
},
{
"id": "1995-1826-0012",
"ref": "SHE WANTED A GLANCE OF THE NEW BOOKS AND PERIODICALS AND TALK OF GREAT PHILANTHROPIES AND REFORMS",
"hyp": "She wanted a glance of the new books in periodicals and talk of great philanthropies and reforms.",
"ref_norm": "SHE WANTED A GLANCE OF THE NEW BOOKS AND PERIODICALS AND TALK OF GREAT PHILANTHROPIES AND REFORMS",
"hyp_norm": "SHE WANTED A GLANCE OF THE NEW BOOKS IN PERIODICALS AND TALK OF GREAT PHILANTHROPIES AND REFORMS",
"duration_s": 6.18,
"infer_time_s": 1.68,
"rtf": 0.2719,
"wer": 0.0588
},
{
"id": "1995-1826-0013",
"ref": "SO FOR THE HUNDREDTH TIME SHE WAS THINKING TODAY AS SHE WALKED ALONE UP THE LANE BACK OF THE BARN AND THEN SLOWLY DOWN THROUGH THE BOTTOMS",
"hyp": "So for the hundredth time, she was thinking today , as she walked alone up the lane back of the barn and then slowly down through the bottoms.",
"ref_norm": "SO FOR THE HUNDREDTH TIME SHE WAS THINKING TODAY AS SHE WALKED ALONE UP THE LANE BACK OF THE BARN AND THEN SLOWLY DOWN THROUGH THE BOTTOMS",
"hyp_norm": "SO FOR THE HUNDREDTH TIME SHE WAS THINKING TODAY AS SHE WALKED ALONE UP THE LANE BACK OF THE BARN AND THEN SLOWLY DOWN THROUGH THE BOTTOMS",
"duration_s": 8.77,
"infer_time_s": 2.389,
"rtf": 0.2724,
"wer": 0.0
},
{
"id": "1995-1826-0014",
"ref": "COTTON SHE PAUSED",
"hyp": "Cotton, she paused.",
"ref_norm": "COTTON SHE PAUSED",
"hyp_norm": "COTTON SHE PAUSED",
"duration_s": 2.5,
"infer_time_s": 0.584,
"rtf": 0.2337,
"wer": 0.0
},
{
"id": "1995-1826-0015",
"ref": "SHE HAD ALMOST FORGOTTEN THAT IT WAS HERE WITHIN TOUCH AND SIGHT",
"hyp": "She had almost forgotten that it was here, within touch and sight.",
"ref_norm": "SHE HAD ALMOST FORGOTTEN THAT IT WAS HERE WITHIN TOUCH AND SIGHT",
"hyp_norm": "SHE HAD ALMOST FORGOTTEN THAT IT WAS HERE WITHIN TOUCH AND SIGHT",
"duration_s": 3.55,
"infer_time_s": 1.011,
"rtf": 0.2849,
"wer": 0.0
},
{
"id": "1995-1826-0016",
"ref": "THE GLIMMERING SEA OF DELICATE LEAVES WHISPERED AND MURMURED BEFORE HER STRETCHING AWAY TO THE NORTHWARD",
"hyp": "The glimmering sea of delicate leaves whispered and murm ured before her, stretching away to the northward.",
"ref_norm": "THE GLIMMERING SEA OF DELICATE LEAVES WHISPERED AND MURMURED BEFORE HER STRETCHING AWAY TO THE NORTHWARD",
"hyp_norm": "THE GLIMMERING SEA OF DELICATE LEAVES WHISPERED AND MURM URED BEFORE HER STRETCHING AWAY TO THE NORTHWARD",
"duration_s": 5.9,
"infer_time_s": 1.586,
"rtf": 0.2689,
"wer": 0.125
},
{
"id": "1995-1826-0017",
"ref": "THERE MIGHT BE A BIT OF POETRY HERE AND THERE BUT MOST OF THIS PLACE WAS SUCH DESPERATE PROSE",
"hyp": "There might be a bit of poetry here and there, but most of this place was such desperate prose.",
"ref_norm": "THERE MIGHT BE A BIT OF POETRY HERE AND THERE BUT MOST OF THIS PLACE WAS SUCH DESPERATE PROSE",
"hyp_norm": "THERE MIGHT BE A BIT OF POETRY HERE AND THERE BUT MOST OF THIS PLACE WAS SUCH DESPERATE PROSE",
"duration_s": 6.145,
"infer_time_s": 1.65,
"rtf": 0.2685,
"wer": 0.0
},
{
"id": "1995-1826-0018",
"ref": "HER REGARD SHIFTED TO THE GREEN STALKS AND LEAVES AGAIN AND SHE STARTED TO MOVE AWAY",
"hyp": "Her regard shifted to the green stalks and leaves again, and she started to move away.",
"ref_norm": "HER REGARD SHIFTED TO THE GREEN STALKS AND LEAVES AGAIN AND SHE STARTED TO MOVE AWAY",
"hyp_norm": "HER REGARD SHIFTED TO THE GREEN STALKS AND LEAVES AGAIN AND SHE STARTED TO MOVE AWAY",
"duration_s": 5.01,
"infer_time_s": 1.446,
"rtf": 0.2886,
"wer": 0.0
},
{
"id": "1995-1826-0019",
"ref": "COTTON IS A WONDERFUL THING IS IT NOT BOYS SHE SAID RATHER PRIMLY",
"hyp": "Cotton is a wonderful thing, is it not , boys?\" she said rather primly.",
"ref_norm": "COTTON IS A WONDERFUL THING IS IT NOT BOYS SHE SAID RATHER PRIMLY",
"hyp_norm": "COTTON IS A WONDERFUL THING IS IT NOT BOYS SHE SAID RATHER PRIMLY",
"duration_s": 5.25,
"infer_time_s": 1.426,
"rtf": 0.2716,
"wer": 0.0
},
{
"id": "1995-1826-0020",
"ref": "MISS TAYLOR DID NOT KNOW MUCH ABOUT COTTON BUT AT LEAST ONE MORE REMARK SEEMED CALLED FOR",
"hyp": "Miss Taylor did not know much about cotton, but at least one more remark seemed called for.",
"ref_norm": "MISS TAYLOR DID NOT KNOW MUCH ABOUT COTTON BUT AT LEAST ONE MORE REMARK SEEMED CALLED FOR",
"hyp_norm": "MISS TAYLOR DID NOT KNOW MUCH ABOUT COTTON BUT AT LEAST ONE MORE REMARK SEEMED CALLED FOR",
"duration_s": 6.12,
"infer_time_s": 1.57,
"rtf": 0.2565,
"wer": 0.0
},
{
"id": "1995-1826-0021",
"ref": "DON'T KNOW WELL OF ALL THINGS INWARDLY COMMENTED MISS TAYLOR LITERALLY BORN IN COTTON AND OH WELL AS MUCH AS TO ASK WHAT'S THE USE SHE TURNED AGAIN TO GO",
"hyp": "Don't know well of all things. Inwardly commented Miss Taylor , \"Literally born in cotton, and oh well , as much as to ask, what's the use?\" She turned again to go.",
"ref_norm": "DONT KNOW WELL OF ALL THINGS INWARDLY COMMENTED MISS TAYLOR LITERALLY BORN IN COTTON AND OH WELL AS MUCH AS TO ASK WHATS THE USE SHE TURNED AGAIN TO GO",
"hyp_norm": "DONT KNOW WELL OF ALL THINGS INWARDLY COMMENTED MISS TAYLOR LITERALLY BORN IN COTTON AND OH WELL AS MUCH AS TO ASK WHATS THE USE SHE TURNED AGAIN TO GO",
"duration_s": 11.41,
"infer_time_s": 3.199,
"rtf": 0.2804,
"wer": 0.0
},
{
"id": "1995-1826-0022",
"ref": "I SUPPOSE THOUGH IT'S TOO EARLY FOR THEM THEN CAME THE EXPLOSION",
"hyp": "I suppose, though it's too early for them . Then came the explosion.",
"ref_norm": "I SUPPOSE THOUGH ITS TOO EARLY FOR THEM THEN CAME THE EXPLOSION",
"hyp_norm": "I SUPPOSE THOUGH ITS TOO EARLY FOR THEM THEN CAME THE EXPLOSION",
"duration_s": 4.745,
"infer_time_s": 1.254,
"rtf": 0.2644,
"wer": 0.0
},
{
"id": "1995-1826-0023",
"ref": "GOOBERS DON'T GROW ON THE TOPS OF VINES BUT UNDERGROUND ON THE ROOTS LIKE YAMS IS THAT SO",
"hyp": "Gobies don't grow on the tops of vines, but , on the ground, on the roots, like y ams. Is that so?",
"ref_norm": "GOOBERS DONT GROW ON THE TOPS OF VINES BUT UNDERGROUND ON THE ROOTS LIKE YAMS IS THAT SO",
"hyp_norm": "GOBIES DONT GROW ON THE TOPS OF VINES BUT ON THE GROUND ON THE ROOTS LIKE Y AMS IS THAT SO",
"duration_s": 8.14,
"infer_time_s": 2.389,
"rtf": 0.2935,
"wer": 0.3333
},
{
"id": "1995-1826-0024",
"ref": "THE GOLDEN FLEECE IT'S THE SILVER FLEECE HE HARKENED",
"hyp": "The golden fleece , it's the silver fleece. He hearkened.",
"ref_norm": "THE GOLDEN FLEECE ITS THE SILVER FLEECE HE HARKENED",
"hyp_norm": "THE GOLDEN FLEECE ITS THE SILVER FLEECE HE HEARKENED",
"duration_s": 5.095,
"infer_time_s": 1.224,
"rtf": 0.2402,
"wer": 0.1111
},
{
"id": "1995-1826-0025",
"ref": "SOME TIME YOU'LL TELL ME PLEASE WON'T YOU",
"hyp": "Sometimes you tell me, please, won't you?",
"ref_norm": "SOME TIME YOULL TELL ME PLEASE WONT YOU",
"hyp_norm": "SOMETIMES YOU TELL ME PLEASE WONT YOU",
"duration_s": 3.295,
"infer_time_s": 0.875,
"rtf": 0.2656,
"wer": 0.375
},
{
"id": "1995-1826-0026",
"ref": "NOW FOR ONE LITTLE HALF HOUR SHE HAD BEEN A WOMAN TALKING TO A BOY NO NOT EVEN THAT SHE HAD BEEN TALKING JUST TALKING THERE WERE NO PERSONS IN THE CONVERSATION JUST THINGS ONE THING COTTON",
"hyp": "Now for one little half hour, she had been a woman talking to a boy . No, not even that. She had been talking, just talking. There were no persons in the conversation; just things. One thing, cotton.",
"ref_norm": "NOW FOR ONE LITTLE HALF HOUR SHE HAD BEEN A WOMAN TALKING TO A BOY NO NOT EVEN THAT SHE HAD BEEN TALKING JUST TALKING THERE WERE NO PERSONS IN THE CONVERSATION JUST THINGS ONE THING COTTON",
"hyp_norm": "NOW FOR ONE LITTLE HALF HOUR SHE HAD BEEN A WOMAN TALKING TO A BOY NO NOT EVEN THAT SHE HAD BEEN TALKING JUST TALKING THERE WERE NO PERSONS IN THE CONVERSATION JUST THINGS ONE THING COTTON",
"duration_s": 15.45,
"infer_time_s": 3.853,
"rtf": 0.2494,
"wer": 0.0
},
{
"id": "1995-1836-0000",
"ref": "THE HON CHARLES SMITH MISS SARAH'S BROTHER WAS WALKING SWIFTLY UPTOWN FROM MISTER EASTERLY'S WALL STREET OFFICE AND HIS FACE WAS PALE",
"hyp": "The Honorable Charles Smith, Miss Sarah's brother, was walking swiftly uptown from Mister Ester ly's Wall Street office, and his face was pale.",
"ref_norm": "THE HON CHARLES SMITH MISS SARAHS BROTHER WAS WALKING SWIFTLY UPTOWN FROM MISTER EASTERLYS WALL STREET OFFICE AND HIS FACE WAS PALE",
"hyp_norm": "THE HONORABLE CHARLES SMITH MISS SARAHS BROTHER WAS WALKING SWIFTLY UPTOWN FROM MISTER ESTER LYS WALL STREET OFFICE AND HIS FACE WAS PALE",
"duration_s": 8.955,
"infer_time_s": 2.543,
"rtf": 0.2839,
"wer": 0.1364
},
{
"id": "1995-1836-0001",
"ref": "AT LAST THE COTTON COMBINE WAS TO ALL APPEARANCES AN ASSURED FACT AND HE WAS SLATED FOR THE SENATE",
"hyp": "At last, the cotton combine was to all appearances an assured fact, and he was slated for the Senate.",
"ref_norm": "AT LAST THE COTTON COMBINE WAS TO ALL APPEARANCES AN ASSURED FACT AND HE WAS SLATED FOR THE SENATE",
"hyp_norm": "AT LAST THE COTTON COMBINE WAS TO ALL APPEARANCES AN ASSURED FACT AND HE WAS SLATED FOR THE SENATE",
"duration_s": 6.0,
"infer_time_s": 1.566,
"rtf": 0.2609,
"wer": 0.0
},
{
"id": "1995-1836-0002",
"ref": "WHY SHOULD HE NOT BE AS OTHER MEN",
"hyp": "Why should he not be as other men?",
"ref_norm": "WHY SHOULD HE NOT BE AS OTHER MEN",
"hyp_norm": "WHY SHOULD HE NOT BE AS OTHER MEN",
"duration_s": 2.315,
"infer_time_s": 0.765,
"rtf": 0.3306,
"wer": 0.0
},
{
"id": "1995-1836-0003",
"ref": "SHE WAS NOT HERSELF A NOTABLY INTELLIGENT WOMAN SHE GREATLY ADMIRED INTELLIGENCE OR WHATEVER LOOKED TO HER LIKE INTELLIGENCE IN OTHERS",
"hyp": "She was not herself a notably intelligent woman . She greatly admired intelligence, or whatever looked to her like intelligence in others.",
"ref_norm": "SHE WAS NOT HERSELF A NOTABLY INTELLIGENT WOMAN SHE GREATLY ADMIRED INTELLIGENCE OR WHATEVER LOOKED TO HER LIKE INTELLIGENCE IN OTHERS",
"hyp_norm": "SHE WAS NOT HERSELF A NOTABLY INTELLIGENT WOMAN SHE GREATLY ADMIRED INTELLIGENCE OR WHATEVER LOOKED TO HER LIKE INTELLIGENCE IN OTHERS",
"duration_s": 7.965,
"infer_time_s": 1.862,
"rtf": 0.2338,
"wer": 0.0
},
{
"id": "1995-1836-0004",
"ref": "AS SHE AWAITED HER GUESTS SHE SURVEYED THE TABLE WITH BOTH SATISFACTION AND DISQUIETUDE FOR HER SOCIAL FUNCTIONS WERE FEW TONIGHT THERE WERE SHE CHECKED THEM OFF ON HER FINGERS SIR JAMES CREIGHTON THE RICH ENGLISH MANUFACTURER AND LADY CREIGHTON MISTER AND MISSUS VANDERPOOL MISTER HARRY CRESSWELL AND HIS SISTER JOHN TAYLOR AND HIS SISTER AND MISTER CHARLES SMITH WHOM THE EVENING PAPERS MENTIONED AS LIKELY TO BE UNITED STATES SENATOR FROM NEW JERSEY A SELECTION OF GUESTS THAT HAD BEEN DETERMINED UNKNOWN TO THE HOSTESS BY THE MEETING OF COTTON INTERESTS EARLIER IN THE DAY",
"hyp": "As she awaited her guest , she surveyed the table with both satisfaction and dis quietude. For her social functions were few tonight. There were she checked them off on her fingers, Sir James Crichton, the rich English manufacturer and Lady Crichton , Mister and Missus Vanderpoel , Mister Harry Cresswell and his sister, John Taylor and his sister, and Mister Charles Smith, whom the evening papers mentioned as likely to be United States Senator from New Jersey, a selection of guests that had been determined unknown to the hostess by the meeting of cotton interests earlier in the day.",
"ref_norm": "AS SHE AWAITED HER GUESTS SHE SURVEYED THE TABLE WITH BOTH SATISFACTION AND DISQUIETUDE FOR HER SOCIAL FUNCTIONS WERE FEW TONIGHT THERE WERE SHE CHECKED THEM OFF ON HER FINGERS SIR JAMES CREIGHTON THE RICH ENGLISH MANUFACTURER AND LADY CREIGHTON MISTER AND MISSUS VANDERPOOL MISTER HARRY CRESSWELL AND HIS SISTER JOHN TAYLOR AND HIS SISTER AND MISTER CHARLES SMITH WHOM THE EVENING PAPERS MENTIONED AS LIKELY TO BE UNITED STATES SENATOR FROM NEW JERSEY A SELECTION OF GUESTS THAT HAD BEEN DETERMINED UNKNOWN TO THE HOSTESS BY THE MEETING OF COTTON INTERESTS EARLIER IN THE DAY",
"hyp_norm": "AS SHE AWAITED HER GUEST SHE SURVEYED THE TABLE WITH BOTH SATISFACTION AND DIS QUIETUDE FOR HER SOCIAL FUNCTIONS WERE FEW TONIGHT THERE WERE SHE CHECKED THEM OFF ON HER FINGERS SIR JAMES CRICHTON THE RICH ENGLISH MANUFACTURER AND LADY CRICHTON MISTER AND MISSUS VANDERPOEL MISTER HARRY CRESSWELL AND HIS SISTER JOHN TAYLOR AND HIS SISTER AND MISTER CHARLES SMITH WHOM THE EVENING PAPERS MENTIONED AS LIKELY TO BE UNITED STATES SENATOR FROM NEW JERSEY A SELECTION OF GUESTS THAT HAD BEEN DETERMINED UNKNOWN TO THE HOSTESS BY THE MEETING OF COTTON INTERESTS EARLIER IN THE DAY",
"duration_s": 33.91,
"infer_time_s": 9.234,
"rtf": 0.2723,
"wer": 0.0625
},
{
"id": "1995-1836-0005",
"ref": "MISSUS GREY HAD MET SOUTHERNERS BEFORE BUT NOT INTIMATELY AND SHE ALWAYS HAD IN MIND VIVIDLY THEIR CRUELTY TO POOR NEGROES A SUBJECT SHE MADE A POINT OF INTRODUCING FORTHWITH",
"hyp": "Missus Gray had met sou therners before, but not intimately, and she always had in mind vividly their cruelty to poor Negro es\u2014a subject she made a point of introducing forthwith.",
"ref_norm": "MISSUS GREY HAD MET SOUTHERNERS BEFORE BUT NOT INTIMATELY AND SHE ALWAYS HAD IN MIND VIVIDLY THEIR CRUELTY TO POOR NEGROES A SUBJECT SHE MADE A POINT OF INTRODUCING FORTHWITH",
"hyp_norm": "MISSUS GRAY HAD MET SOU THERNERS BEFORE BUT NOT INTIMATELY AND SHE ALWAYS HAD IN MIND VIVIDLY THEIR CRUELTY TO POOR NEGRO ESA SUBJECT SHE MADE A POINT OF INTRODUCING FORTHWITH",
"duration_s": 10.9,
"infer_time_s": 2.994,
"rtf": 0.2747,
"wer": 0.1667
},
{
"id": "1995-1836-0006",
"ref": "SHE WAS THEREFORE MOST AGREEABLY SURPRISED TO HEAR MISTER CRESSWELL EXPRESS HIMSELF SO CORDIALLY AS APPROVING OF NEGRO EDUCATION",
"hyp": "She was therefore most agreeably surprised to hear Mister Cresswell express himself so cordially as approving of Negro education.",
"ref_norm": "SHE WAS THEREFORE MOST AGREEABLY SURPRISED TO HEAR MISTER CRESSWELL EXPRESS HIMSELF SO CORDIALLY AS APPROVING OF NEGRO EDUCATION",
"hyp_norm": "SHE WAS THEREFORE MOST AGREEABLY SURPRISED TO HEAR MISTER CRESSWELL EXPRESS HIMSELF SO CORDIALLY AS APPROVING OF NEGRO EDUCATION",
"duration_s": 7.715,
"infer_time_s": 1.859,
"rtf": 0.2409,
"wer": 0.0
},
{
"id": "1995-1836-0007",
"ref": "BUT YOU BELIEVE IN SOME EDUCATION ASKED MARY TAYLOR",
"hyp": "Do you believe in some education? Asked Mary Taylor.",
"ref_norm": "BUT YOU BELIEVE IN SOME EDUCATION ASKED MARY TAYLOR",
"hyp_norm": "DO YOU BELIEVE IN SOME EDUCATION ASKED MARY TAYLOR",
"duration_s": 3.435,
"infer_time_s": 0.853,
"rtf": 0.2483,
"wer": 0.1111
},
{
"id": "1995-1836-0008",
"ref": "I BELIEVE IN THE TRAINING OF PEOPLE TO THEIR HIGHEST CAPACITY THE ENGLISHMAN HERE HEARTILY SECONDED HIM",
"hyp": "I believe in the training of people to their highest capacity. The Englishman here heartily seconded him.",
"ref_norm": "I BELIEVE IN THE TRAINING OF PEOPLE TO THEIR HIGHEST CAPACITY THE ENGLISHMAN HERE HEARTILY SECONDED HIM",
"hyp_norm": "I BELIEVE IN THE TRAINING OF PEOPLE TO THEIR HIGHEST CAPACITY THE ENGLISHMAN HERE HEARTILY SECONDED HIM",
"duration_s": 6.985,
"infer_time_s": 1.739,
"rtf": 0.249,
"wer": 0.0
},
{
"id": "1995-1836-0009",
"ref": "BUT CRESSWELL ADDED SIGNIFICANTLY CAPACITY DIFFERS ENORMOUSLY BETWEEN RACES",
"hyp": "But Cresswell added significantly: \" Capacity differs enormously between races.\"",
"ref_norm": "BUT CRESSWELL ADDED SIGNIFICANTLY CAPACITY DIFFERS ENORMOUSLY BETWEEN RACES",
"hyp_norm": "BUT CRESSWELL ADDED SIGNIFICANTLY CAPACITY DIFFERS ENORMOUSLY BETWEEN RACES",
"duration_s": 6.71,
"infer_time_s": 1.308,
"rtf": 0.1949,
"wer": 0.0
},
{
"id": "1995-1836-0010",
"ref": "THE VANDERPOOLS WERE SURE OF THIS AND THE ENGLISHMAN INSTANCING INDIA BECAME QUITE ELOQUENT MISSUS GREY WAS MYSTIFIED BUT HARDLY DARED ADMIT IT THE GENERAL TREND OF THE CONVERSATION SEEMED TO BE THAT MOST INDIVIDUALS NEEDED TO BE SUBMITTED TO THE SHARPEST SCRUTINY BEFORE BEING ALLOWED MUCH EDUCATION AND AS FOR THE LOWER RACES IT WAS SIMPLY CRIMINAL TO OPEN SUCH USELESS OPPORTUNITIES TO THEM",
"hyp": "The Vanderpoles were sure of this, and the Englishman , instancing India, became quite eloquent . Missus Grey was mystified but hardly dared admit it. The general trend of the conversation seemed to be that most individuals needed to be submitted to the sharpest scrutiny before being allowed much education. And as for the lower races, it was simply criminal to open such useless opportunities to them.",
"ref_norm": "THE VANDERPOOLS WERE SURE OF THIS AND THE ENGLISHMAN INSTANCING INDIA BECAME QUITE ELOQUENT MISSUS GREY WAS MYSTIFIED BUT HARDLY DARED ADMIT IT THE GENERAL TREND OF THE CONVERSATION SEEMED TO BE THAT MOST INDIVIDUALS NEEDED TO BE SUBMITTED TO THE SHARPEST SCRUTINY BEFORE BEING ALLOWED MUCH EDUCATION AND AS FOR THE LOWER RACES IT WAS SIMPLY CRIMINAL TO OPEN SUCH USELESS OPPORTUNITIES TO THEM",
"hyp_norm": "THE VANDERPOLES WERE SURE OF THIS AND THE ENGLISHMAN INSTANCING INDIA BECAME QUITE ELOQUENT MISSUS GREY WAS MYSTIFIED BUT HARDLY DARED ADMIT IT THE GENERAL TREND OF THE CONVERSATION SEEMED TO BE THAT MOST INDIVIDUALS NEEDED TO BE SUBMITTED TO THE SHARPEST SCRUTINY BEFORE BEING ALLOWED MUCH EDUCATION AND AS FOR THE LOWER RACES IT WAS SIMPLY CRIMINAL TO OPEN SUCH USELESS OPPORTUNITIES TO THEM",
"duration_s": 24.45,
"infer_time_s": 6.513,
"rtf": 0.2664,
"wer": 0.0154
},
{
"id": "1995-1836-0011",
"ref": "POSITIVELY HEROIC ADDED CRESSWELL AVOIDING HIS SISTER'S EYES",
"hyp": "Positively heroic ,\" added Cresswell, avoiding his sister's eyes.",
"ref_norm": "POSITIVELY HEROIC ADDED CRESSWELL AVOIDING HIS SISTERS EYES",
"hyp_norm": "POSITIVELY HEROIC ADDED CRESSWELL AVOIDING HIS SISTERS EYES",
"duration_s": 4.705,
"infer_time_s": 1.268,
"rtf": 0.2694,
"wer": 0.0
},
{
"id": "1995-1836-0012",
"ref": "BUT WE'RE NOT ER EXACTLY WELCOMED",
"hyp": "But we're not a, exactly welcome.",
"ref_norm": "BUT WERE NOT ER EXACTLY WELCOMED",
"hyp_norm": "BUT WERE NOT A EXACTLY WELCOME",
"duration_s": 3.695,
"infer_time_s": 0.747,
"rtf": 0.2021,
"wer": 0.3333
},
{
"id": "1995-1836-0013",
"ref": "MARY TAYLOR HOWEVER RELATED THE TALE OF ZORA TO MISSUS GREY'S PRIVATE EAR LATER",
"hyp": "Mary Taylor, however, related the tale of Zora to Missus Grey's private ear later.",
"ref_norm": "MARY TAYLOR HOWEVER RELATED THE TALE OF ZORA TO MISSUS GREYS PRIVATE EAR LATER",
"hyp_norm": "MARY TAYLOR HOWEVER RELATED THE TALE OF ZORA TO MISSUS GREYS PRIVATE EAR LATER",
"duration_s": 5.3,
"infer_time_s": 1.469,
"rtf": 0.2772,
"wer": 0.0
},
{
"id": "1995-1836-0014",
"ref": "FORTUNATELY SAID MISTER VANDERPOOL NORTHERNERS AND SOUTHERNERS ARE ARRIVING AT A BETTER MUTUAL UNDERSTANDING ON MOST OF THESE MATTERS",
"hyp": "Fortunately, said Mister Vanderpool, Northerners and Southerners are arriving at a better mutual understanding on most of these matters.",
"ref_norm": "FORTUNATELY SAID MISTER VANDERPOOL NORTHERNERS AND SOUTHERNERS ARE ARRIVING AT A BETTER MUTUAL UNDERSTANDING ON MOST OF THESE MATTERS",
"hyp_norm": "FORTUNATELY SAID MISTER VANDERPOOL NORTHERNERS AND SOUTHERNERS ARE ARRIVING AT A BETTER MUTUAL UNDERSTANDING ON MOST OF THESE MATTERS",
"duration_s": 9.045,
"infer_time_s": 2.148,
"rtf": 0.2375,
"wer": 0.0
},
{
"id": "1995-1837-0000",
"ref": "HE KNEW THE SILVER FLEECE HIS AND ZORA'S MUST BE RUINED",
"hyp": "He knew the silver fleece . His and Zora's must be ruined.",
"ref_norm": "HE KNEW THE SILVER FLEECE HIS AND ZORAS MUST BE RUINED",
"hyp_norm": "HE KNEW THE SILVER FLEECE HIS AND ZORAS MUST BE RUINED",
"duration_s": 3.865,
"infer_time_s": 1.09,
"rtf": 0.2819,
"wer": 0.0
},
{
"id": "1995-1837-0001",
"ref": "IT WAS THE FIRST GREAT SORROW OF HIS LIFE IT WAS NOT SO MUCH THE LOSS OF THE COTTON ITSELF BUT THE FANTASY THE HOPES THE DREAMS BUILT AROUND IT",
"hyp": "It was the first great sorrow of his life. It was not so much the loss of the cotton itself, but the fantasy, the hopes, the dreams built around it.",
"ref_norm": "IT WAS THE FIRST GREAT SORROW OF HIS LIFE IT WAS NOT SO MUCH THE LOSS OF THE COTTON ITSELF BUT THE FANTASY THE HOPES THE DREAMS BUILT AROUND IT",
"hyp_norm": "IT WAS THE FIRST GREAT SORROW OF HIS LIFE IT WAS NOT SO MUCH THE LOSS OF THE COTTON ITSELF BUT THE FANTASY THE HOPES THE DREAMS BUILT AROUND IT",
"duration_s": 8.73,
"infer_time_s": 2.559,
"rtf": 0.2932,
"wer": 0.0
},
{
"id": "1995-1837-0002",
"ref": "AH THE SWAMP THE CRUEL SWAMP",
"hyp": "Ah, the swamp, the cruel swamp.",
"ref_norm": "AH THE SWAMP THE CRUEL SWAMP",
"hyp_norm": "AH THE SWAMP THE CRUEL SWAMP",
"duration_s": 2.79,
"infer_time_s": 0.756,
"rtf": 0.2711,
"wer": 0.0
},
{
"id": "1995-1837-0003",
"ref": "THE REVELATION OF HIS LOVE LIGHTED AND BRIGHTENED SLOWLY TILL IT FLAMED LIKE A SUNRISE OVER HIM AND LEFT HIM IN BURNING WONDER",
"hyp": "The revelation of his love lighted and brightened slowly, till it flamed like a sunrise over him and left him in burning wonder.",
"ref_norm": "THE REVELATION OF HIS LOVE LIGHTED AND BRIGHTENED SLOWLY TILL IT FLAMED LIKE A SUNRISE OVER HIM AND LEFT HIM IN BURNING WONDER",
"hyp_norm": "THE REVELATION OF HIS LOVE LIGHTED AND BRIGHTENED SLOWLY TILL IT FLAMED LIKE A SUNRISE OVER HIM AND LEFT HIM IN BURNING WONDER",
"duration_s": 7.36,
"infer_time_s": 2.169,
"rtf": 0.2947,
"wer": 0.0
},
{
"id": "1995-1837-0004",
"ref": "HE PANTED TO KNOW IF SHE TOO KNEW OR KNEW AND CARED NOT OR CARED AND KNEW NOT",
"hyp": "He panted to know if she too knew or knew and cared not , or cared and knew not.",
"ref_norm": "HE PANTED TO KNOW IF SHE TOO KNEW OR KNEW AND CARED NOT OR CARED AND KNEW NOT",
"hyp_norm": "HE PANTED TO KNOW IF SHE TOO KNEW OR KNEW AND CARED NOT OR CARED AND KNEW NOT",
"duration_s": 6.36,
"infer_time_s": 1.85,
"rtf": 0.2909,
"wer": 0.0
},
{
"id": "1995-1837-0005",
"ref": "SHE WAS SO STRANGE AND HUMAN A CREATURE",
"hyp": "She was so strange and human a creature.",
"ref_norm": "SHE WAS SO STRANGE AND HUMAN A CREATURE",
"hyp_norm": "SHE WAS SO STRANGE AND HUMAN A CREATURE",
"duration_s": 2.635,
"infer_time_s": 0.811,
"rtf": 0.3078,
"wer": 0.0
},
{
"id": "1995-1837-0006",
"ref": "THE WORLD WAS WATER VEILED IN MISTS",
"hyp": "The world was water ve iled in mists.",
"ref_norm": "THE WORLD WAS WATER VEILED IN MISTS",
"hyp_norm": "THE WORLD WAS WATER VE ILED IN MISTS",
"duration_s": 2.955,
"infer_time_s": 0.808,
"rtf": 0.2734,
"wer": 0.2857
},
{
"id": "1995-1837-0007",
"ref": "THEN OF A SUDDEN AT MIDDAY THE SUN SHOT OUT HOT AND STILL NO BREATH OF AIR STIRRED THE SKY WAS LIKE BLUE STEEL THE EARTH STEAMED",
"hyp": "Then, of a sudden, at midday, the sun shot out , hot and still. No breath of air stirred. The sky was like blue steel. The earth steamed.",
"ref_norm": "THEN OF A SUDDEN AT MIDDAY THE SUN SHOT OUT HOT AND STILL NO BREATH OF AIR STIRRED THE SKY WAS LIKE BLUE STEEL THE EARTH STEAMED",
"hyp_norm": "THEN OF A SUDDEN AT MIDDAY THE SUN SHOT OUT HOT AND STILL NO BREATH OF AIR STIRRED THE SKY WAS LIKE BLUE STEEL THE EARTH STEAMED",
"duration_s": 8.8,
"infer_time_s": 2.826,
"rtf": 0.3212,
"wer": 0.0
},
{
"id": "1995-1837-0008",
"ref": "WHERE WAS THE USE OF IMAGINING",
"hyp": "Where was the use of imagining?",
"ref_norm": "WHERE WAS THE USE OF IMAGINING",
"hyp_norm": "WHERE WAS THE USE OF IMAGINING",
"duration_s": 1.955,
"infer_time_s": 0.517,
"rtf": 0.2645,
"wer": 0.0
},
{
"id": "1995-1837-0009",
"ref": "THE LAGOON HAD BEEN LEVEL WITH THE DYKES A WEEK AGO AND NOW",
"hyp": "The lagoon had been level with the dikes a week ago, and now.",
"ref_norm": "THE LAGOON HAD BEEN LEVEL WITH THE DYKES A WEEK AGO AND NOW",
"hyp_norm": "THE LAGOON HAD BEEN LEVEL WITH THE DIKES A WEEK AGO AND NOW",
"duration_s": 3.76,
"infer_time_s": 1.254,
"rtf": 0.3336,
"wer": 0.0769
},
{
"id": "1995-1837-0010",
"ref": "PERHAPS SHE TOO MIGHT BE THERE WAITING WEEPING",
"hyp": "Perhaps she too might be there, waiting, weeping.",
"ref_norm": "PERHAPS SHE TOO MIGHT BE THERE WAITING WEEPING",
"hyp_norm": "PERHAPS SHE TOO MIGHT BE THERE WAITING WEEPING",
"duration_s": 3.48,
"infer_time_s": 0.932,
"rtf": 0.2677,
"wer": 0.0
},
{
"id": "1995-1837-0011",
"ref": "HE STARTED AT THE THOUGHT HE HURRIED FORTH SADLY",
"hyp": "He started at the thought. He hurried forth sadly.",
"ref_norm": "HE STARTED AT THE THOUGHT HE HURRIED FORTH SADLY",
"hyp_norm": "HE STARTED AT THE THOUGHT HE HURRIED FORTH SADLY",
"duration_s": 3.375,
"infer_time_s": 0.868,
"rtf": 0.2571,
"wer": 0.0
},
{
"id": "1995-1837-0012",
"ref": "HE SPLASHED AND STAMPED ALONG FARTHER AND FARTHER ONWARD UNTIL HE NEARED THE RAMPART OF THE CLEARING AND PUT FOOT UPON THE TREE BRIDGE",
"hyp": "He splashed and stamped along farther and farther onward until he neared the ramp art of the clearing, and put foot upon the tree bridge.",
"ref_norm": "HE SPLASHED AND STAMPED ALONG FARTHER AND FARTHER ONWARD UNTIL HE NEARED THE RAMPART OF THE CLEARING AND PUT FOOT UPON THE TREE BRIDGE",
"hyp_norm": "HE SPLASHED AND STAMPED ALONG FARTHER AND FARTHER ONWARD UNTIL HE NEARED THE RAMP ART OF THE CLEARING AND PUT FOOT UPON THE TREE BRIDGE",
"duration_s": 8.245,
"infer_time_s": 2.34,
"rtf": 0.2838,
"wer": 0.0833
},
{
"id": "1995-1837-0013",
"ref": "THEN HE LOOKED DOWN THE LAGOON WAS DRY",
"hyp": "Then he looked down . The lagoon was dry.",
"ref_norm": "THEN HE LOOKED DOWN THE LAGOON WAS DRY",
"hyp_norm": "THEN HE LOOKED DOWN THE LAGOON WAS DRY",
"duration_s": 3.195,
"infer_time_s": 0.939,
"rtf": 0.2939,
"wer": 0.0
},
{
"id": "1995-1837-0014",
"ref": "HE STOOD A MOMENT BEWILDERED THEN TURNED AND RUSHED UPON THE ISLAND A GREAT SHEET OF DAZZLING SUNLIGHT SWEPT THE PLACE AND BENEATH LAY A MIGHTY MASS OF OLIVE GREEN THICK TALL WET AND WILLOWY",
"hyp": "He stood a moment bewildered , then turned and rushed upon the island\u2014a great sheet of dazzling sunlight swept the place, and beneath lay a mighty mass of olive green, thick, tall, wet, and willowy.",
"ref_norm": "HE STOOD A MOMENT BEWILDERED THEN TURNED AND RUSHED UPON THE ISLAND A GREAT SHEET OF DAZZLING SUNLIGHT SWEPT THE PLACE AND BENEATH LAY A MIGHTY MASS OF OLIVE GREEN THICK TALL WET AND WILLOWY",
"hyp_norm": "HE STOOD A MOMENT BEWILDERED THEN TURNED AND RUSHED UPON THE ISLANDA GREAT SHEET OF DAZZLING SUNLIGHT SWEPT THE PLACE AND BENEATH LAY A MIGHTY MASS OF OLIVE GREEN THICK TALL WET AND WILLOWY",
"duration_s": 12.46,
"infer_time_s": 3.63,
"rtf": 0.2913,
"wer": 0.0571
},
{
"id": "1995-1837-0015",
"ref": "THE SQUARES OF COTTON SHARP EDGED HEAVY WERE JUST ABOUT TO BURST TO BOLLS",
"hyp": "The squares of cotton, sharp -edged, heavy, were just about to burst to balls.",
"ref_norm": "THE SQUARES OF COTTON SHARP EDGED HEAVY WERE JUST ABOUT TO BURST TO BOLLS",
"hyp_norm": "THE SQUARES OF COTTON SHARP EDGED HEAVY WERE JUST ABOUT TO BURST TO BALLS",
"duration_s": 4.485,
"infer_time_s": 1.492,
"rtf": 0.3326,
"wer": 0.0714
},
{
"id": "1995-1837-0016",
"ref": "FOR ONE LONG MOMENT HE PAUSED STUPID AGAPE WITH UTTER AMAZEMENT THEN LEANED DIZZILY AGAINST A TREE",
"hyp": "For one long moment, he paused, stupid, ag ape with utter amazement , then leaned dizzily against the tree.",
"ref_norm": "FOR ONE LONG MOMENT HE PAUSED STUPID AGAPE WITH UTTER AMAZEMENT THEN LEANED DIZZILY AGAINST A TREE",
"hyp_norm": "FOR ONE LONG MOMENT HE PAUSED STUPID AG APE WITH UTTER AMAZEMENT THEN LEANED DIZZILY AGAINST THE TREE",
"duration_s": 7.19,
"infer_time_s": 2.077,
"rtf": 0.2888,
"wer": 0.1765
},
{
"id": "1995-1837-0017",
"ref": "HE GAZED ABOUT PERPLEXED ASTONISHED",
"hyp": "He gazed about, perplex ed, astonished.",
"ref_norm": "HE GAZED ABOUT PERPLEXED ASTONISHED",
"hyp_norm": "HE GAZED ABOUT PERPLEX ED ASTONISHED",
"duration_s": 3.1,
"infer_time_s": 0.824,
"rtf": 0.2659,
"wer": 0.4
},
{
"id": "1995-1837-0018",
"ref": "HERE LAY THE READING OF THE RIDDLE WITH INFINITE WORK AND PAIN SOME ONE HAD DUG A CANAL FROM THE LAGOON TO THE CREEK INTO WHICH THE FORMER HAD DRAINED BY A LONG AND CROOKED WAY THUS ALLOWING IT TO EMPTY DIRECTLY",
"hyp": "Here lay the reading of the r iddle with infinite work and pain. Someone had dug a canal from the l agoon to the creek, into which the former had drained by a long and crooked way, thus allowing it to empty directly.",
"ref_norm": "HERE LAY THE READING OF THE RIDDLE WITH INFINITE WORK AND PAIN SOME ONE HAD DUG A CANAL FROM THE LAGOON TO THE CREEK INTO WHICH THE FORMER HAD DRAINED BY A LONG AND CROOKED WAY THUS ALLOWING IT TO EMPTY DIRECTLY",
"hyp_norm": "HERE LAY THE READING OF THE R IDDLE WITH INFINITE WORK AND PAIN SOMEONE HAD DUG A CANAL FROM THE L AGOON TO THE CREEK INTO WHICH THE FORMER HAD DRAINED BY A LONG AND CROOKED WAY THUS ALLOWING IT TO EMPTY DIRECTLY",
"duration_s": 12.825,
"infer_time_s": 3.736,
"rtf": 0.2913,
"wer": 0.1429
}
]
}
================================================
FILE: benchmarks/m5/bench_1.7b_simul_500.json
================================================
{
"model": "Qwen3-ASR-1.7B",
"backend": "mlx-simul-streaming",
"mode": "simul-streaming",
"platform": "Apple M5 (32GB)",
"config": {
"border_fraction": 0.25,
"chunk_seconds": 2.0,
"chapter_grouped": false
},
"n_samples": 500,
"total_audio_s": 3809.0,
"total_inference_s": 3596.85,
"wer": 0.040716,
"cer": 0.011796,
"rtf": 0.944303,
"median_wer": 0.0,
"p90_wer": 0.1364,
"p95_wer": 0.2143,
"alignment_heads_count": 20,
"alignment_heads_file": "/Users/quentin/Documents/repos/WhisperLiveKit/scripts/alignment_heads_qwen3_asr_1.7B_v2.json",
"per_sample": [
{
"id": "1089-134686-0000",
"ref": "HE HOPED THERE WOULD BE STEW FOR DINNER TURNIPS AND CARROTS AND BRUISED POTATOES AND FAT MUTTON PIECES TO BE LADLED OUT IN THICK PEPPERED FLOUR FATTENED SAUCE",
"hyp": "He hoped there would be stew for dinner, turn ips and carrots, and bruised potatoes and fat m utton pieces to be ladled out in thick, peppered, flour-fattened sauce.",
"ref_norm": "HE HOPED THERE WOULD BE STEW FOR DINNER TURNIPS AND CARROTS AND BRUISED POTATOES AND FAT MUTTON PIECES TO BE LADLED OUT IN THICK PEPPERED FLOUR FATTENED SAUCE",
"hyp_norm": "HE HOPED THERE WOULD BE STEW FOR DINNER TURN IPS AND CARROTS AND BRUISED POTATOES AND FAT M UTTON PIECES TO BE LADLED OUT IN THICK PEPPERED FLOURFATTENED SAUCE",
"duration_s": 10.435,
"infer_time_s": 7.738,
"rtf": 0.7416,
"wer": 0.2143
},
{
"id": "1089-134686-0001",
"ref": "STUFF IT INTO YOU HIS BELLY COUNSELLED HIM",
"hyp": "Stuff it into you, his belly counselled him.",
"ref_norm": "STUFF IT INTO YOU HIS BELLY COUNSELLED HIM",
"hyp_norm": "STUFF IT INTO YOU HIS BELLY COUNSELLED HIM",
"duration_s": 3.275,
"infer_time_s": 2.29,
"rtf": 0.6992,
"wer": 0.0
},
{
"id": "1089-134686-0002",
"ref": "AFTER EARLY NIGHTFALL THE YELLOW LAMPS WOULD LIGHT UP HERE AND THERE THE SQUALID QUARTER OF THE BROTHELS",
"hyp": "After early night fall, the yellow lamps would light up here and there the s qualid quarter of the brothels.",
"ref_norm": "AFTER EARLY NIGHTFALL THE YELLOW LAMPS WOULD LIGHT UP HERE AND THERE THE SQUALID QUARTER OF THE BROTHELS",
"hyp_norm": "AFTER EARLY NIGHT FALL THE YELLOW LAMPS WOULD LIGHT UP HERE AND THERE THE S QUALID QUARTER OF THE BROTHELS",
"duration_s": 6.625,
"infer_time_s": 4.681,
"rtf": 0.7066,
"wer": 0.2222
},
{
"id": "1089-134686-0003",
"ref": "HELLO BERTIE ANY GOOD IN YOUR MIND",
"hyp": "Hello, Bertie. Any good in your mind?",
"ref_norm": "HELLO BERTIE ANY GOOD IN YOUR MIND",
"hyp_norm": "HELLO BERTIE ANY GOOD IN YOUR MIND",
"duration_s": 2.68,
"infer_time_s": 2.151,
"rtf": 0.8027,
"wer": 0.0
},
{
"id": "1089-134686-0004",
"ref": "NUMBER TEN FRESH NELLY IS WAITING ON YOU GOOD NIGHT HUSBAND",
"hyp": "Number ten, fresh Nelly is waiting on you. Good night, husband.",
"ref_norm": "NUMBER TEN FRESH NELLY IS WAITING ON YOU GOOD NIGHT HUSBAND",
"hyp_norm": "NUMBER TEN FRESH NELLY IS WAITING ON YOU GOOD NIGHT HUSBAND",
"duration_s": 5.215,
"infer_time_s": 3.258,
"rtf": 0.6247,
"wer": 0.0
},
{
"id": "1089-134686-0005",
"ref": "THE MUSIC CAME NEARER AND HE RECALLED THE WORDS THE WORDS OF SHELLEY'S FRAGMENT UPON THE MOON WANDERING COMPANIONLESS PALE FOR WEARINESS",
"hyp": "The music came nearer, and he recalled the words, the words of Shelley 's fragment upon the moon , wandering companionless, pale for weariness.",
"ref_norm": "THE MUSIC CAME NEARER AND HE RECALLED THE WORDS THE WORDS OF SHELLEYS FRAGMENT UPON THE MOON WANDERING COMPANIONLESS PALE FOR WEARINESS",
"hyp_norm": "THE MUSIC CAME NEARER AND HE RECALLED THE WORDS THE WORDS OF SHELLEY S FRAGMENT UPON THE MOON WANDERING COMPANIONLESS PALE FOR WEARINESS",
"duration_s": 9.635,
"infer_time_s": 6.17,
"rtf": 0.6404,
"wer": 0.0909
},
{
"id": "1089-134686-0006",
"ref": "THE DULL LIGHT FELL MORE FAINTLY UPON THE PAGE WHEREON ANOTHER EQUATION BEGAN TO UNFOLD ITSELF SLOWLY AND TO SPREAD ABROAD ITS WIDENING TAIL",
"hyp": "The dull light fell more faintly upon the page, whereon another equation began to unfold itself slowly, and to spread abroad its widening tail.",
"ref_norm": "THE DULL LIGHT FELL MORE FAINTLY UPON THE PAGE WHEREON ANOTHER EQUATION BEGAN TO UNFOLD ITSELF SLOWLY AND TO SPREAD ABROAD ITS WIDENING TAIL",
"hyp_norm": "THE DULL LIGHT FELL MORE FAINTLY UPON THE PAGE WHEREON ANOTHER EQUATION BEGAN TO UNFOLD ITSELF SLOWLY AND TO SPREAD ABROAD ITS WIDENING TAIL",
"duration_s": 10.555,
"infer_time_s": 8.19,
"rtf": 0.7759,
"wer": 0.0
},
{
"id": "1089-134686-0007",
"ref": "A COLD LUCID INDIFFERENCE REIGNED IN HIS SOUL",
"hyp": "A cold, lucid indifference re igned in his soul.",
"ref_norm": "A COLD LUCID INDIFFERENCE REIGNED IN HIS SOUL",
"hyp_norm": "A COLD LUCID INDIFFERENCE RE IGNED IN HIS SOUL",
"duration_s": 4.275,
"infer_time_s": 4.873,
"rtf": 1.1399,
"wer": 0.25
},
{
"id": "1089-134686-0008",
"ref": "THE CHAOS IN WHICH HIS ARDOUR EXTINGUISHED ITSELF WAS A COLD INDIFFERENT KNOWLEDGE OF HIMSELF",
"hyp": "The chaos in which his ardor extinguished itself was a cold, indifferent knowledge of himself.",
"ref_norm": "THE CHAOS IN WHICH HIS ARDOUR EXTINGUISHED ITSELF WAS A COLD INDIFFERENT KNOWLEDGE OF HIMSELF",
"hyp_norm": "THE CHAOS IN WHICH HIS ARDOR EXTINGUISHED ITSELF WAS A COLD INDIFFERENT KNOWLEDGE OF HIMSELF",
"duration_s": 6.73,
"infer_time_s": 6.89,
"rtf": 1.0237,
"wer": 0.0667
},
{
"id": "1089-134686-0009",
"ref": "AT MOST BY AN ALMS GIVEN TO A BEGGAR WHOSE BLESSING HE FLED FROM HE MIGHT HOPE WEARILY TO WIN FOR HIMSELF SOME MEASURE OF ACTUAL GRACE",
"hyp": "At most, by an alms given to a beggar whose blessing he fled from, he might hope wearily to win for himself some measure of actual grace.",
"ref_norm": "AT MOST BY AN ALMS GIVEN TO A BEGGAR WHOSE BLESSING HE FLED FROM HE MIGHT HOPE WEARILY TO WIN FOR HIMSELF SOME MEASURE OF ACTUAL GRACE",
"hyp_norm": "AT MOST BY AN ALMS GIVEN TO A BEGGAR WHOSE BLESSING HE FLED FROM HE MIGHT HOPE WEARILY TO WIN FOR HIMSELF SOME MEASURE OF ACTUAL GRACE",
"duration_s": 10.575,
"infer_time_s": 11.739,
"rtf": 1.1101,
"wer": 0.0
},
{
"id": "1089-134686-0010",
"ref": "WELL NOW ENNIS I DECLARE YOU HAVE A HEAD AND SO HAS MY STICK",
"hyp": "Well now, Ennis, I declare you have a head , and so has my stick.",
"ref_norm": "WELL NOW ENNIS I DECLARE YOU HAVE A HEAD AND SO HAS MY STICK",
"hyp_norm": "WELL NOW ENNIS I DECLARE YOU HAVE A HEAD AND SO HAS MY STICK",
"duration_s": 4.405,
"infer_time_s": 6.331,
"rtf": 1.4371,
"wer": 0.0
},
{
"id": "1089-134686-0011",
"ref": "ON SATURDAY MORNINGS WHEN THE SODALITY MET IN THE CHAPEL TO RECITE THE LITTLE OFFICE HIS PLACE WAS A CUSHIONED KNEELING DESK AT THE RIGHT OF THE ALTAR FROM WHICH HE LED HIS WING OF BOYS THROUGH THE RESPONSES",
"hyp": "On Saturday mornings, when the sodality met in the chapel to recite the little office, his place was a cushioned kneeling desk at the right of the altar , from which he led his wing of boys through the responses.",
"ref_norm": "ON SATURDAY MORNINGS WHEN THE SODALITY MET IN THE CHAPEL TO RECITE THE LITTLE OFFICE HIS PLACE WAS A CUSHIONED KNEELING DESK AT THE RIGHT OF THE ALTAR FROM WHICH HE LED HIS WING OF BOYS THROUGH THE RESPONSES",
"hyp_norm": "ON SATURDAY MORNINGS WHEN THE SODALITY MET IN THE CHAPEL TO RECITE THE LITTLE OFFICE HIS PLACE WAS A CUSHIONED KNEELING DESK AT THE RIGHT OF THE ALTAR FROM WHICH HE LED HIS WING OF BOYS THROUGH THE RESPONSES",
"duration_s": 12.445,
"infer_time_s": 15.561,
"rtf": 1.2504,
"wer": 0.0
},
{
"id": "1089-134686-0012",
"ref": "HER EYES SEEMED TO REGARD HIM WITH MILD PITY HER HOLINESS A STRANGE LIGHT GLOWING FAINTLY UPON HER FRAIL FLESH DID NOT HUMILIATE THE SINNER WHO APPROACHED HER",
"hyp": "Her eyes seemed to regard him with mild pity. Her holiness , a strange light glowing faintly upon her frail flesh, did not humiliate the sinner who approached her.",
"ref_norm": "HER EYES SEEMED TO REGARD HIM WITH MILD PITY HER HOLINESS A STRANGE LIGHT GLOWING FAINTLY UPON HER FRAIL FLESH DID NOT HUMILIATE THE SINNER WHO APPROACHED HER",
"hyp_norm": "HER EYES SEEMED TO REGARD HIM WITH MILD PITY HER HOLINESS A STRANGE LIGHT GLOWING FAINTLY UPON HER FRAIL FLESH DID NOT HUMILIATE THE SINNER WHO APPROACHED HER",
"duration_s": 11.64,
"infer_time_s": 12.638,
"rtf": 1.0857,
"wer": 0.0
},
{
"id": "1089-134686-0013",
"ref": "IF EVER HE WAS IMPELLED TO CAST SIN FROM HIM AND TO REPENT THE IMPULSE THAT MOVED HIM WAS THE WISH TO BE HER KNIGHT",
"hyp": "If ever he was impelled to cast sin from him and to repent, the impulse that moved him was the wish to be her knight.",
"ref_norm": "IF EVER HE WAS IMPELLED TO CAST SIN FROM HIM AND TO REPENT THE IMPULSE THAT MOVED HIM WAS THE WISH TO BE HER KNIGHT",
"hyp_norm": "IF EVER HE WAS IMPELLED TO CAST SIN FROM HIM AND TO REPENT THE IMPULSE THAT MOVED HIM WAS THE WISH TO BE HER KNIGHT",
"duration_s": 7.915,
"infer_time_s": 9.233,
"rtf": 1.1666,
"wer": 0.0
},
{
"id": "1089-134686-0014",
"ref": "HE TRIED TO THINK HOW IT COULD BE",
"hyp": "He tried to think how it could be.",
"ref_norm": "HE TRIED TO THINK HOW IT COULD BE",
"hyp_norm": "HE TRIED TO THINK HOW IT COULD BE",
"duration_s": 2.225,
"infer_time_s": 3.31,
"rtf": 1.4878,
"wer": 0.0
},
{
"id": "1089-134686-0015",
"ref": "BUT THE DUSK DEEPENING IN THE SCHOOLROOM COVERED OVER HIS THOUGHTS THE BELL RANG",
"hyp": "But the dusk deepening in the schoolroom covered over his thoughts. The bell rang.",
"ref_norm": "BUT THE DUSK DEEPENING IN THE SCHOOLROOM COVERED OVER HIS THOUGHTS THE BELL RANG",
"hyp_norm": "BUT THE DUSK DEEPENING IN THE SCHOOLROOM COVERED OVER HIS THOUGHTS THE BELL RANG",
"duration_s": 5.815,
"infer_time_s": 6.167,
"rtf": 1.0605,
"wer": 0.0
},
{
"id": "1089-134686-0016",
"ref": "THEN YOU CAN ASK HIM QUESTIONS ON THE CATECHISM DEDALUS",
"hyp": "Then you can ask him questions on the catechism, Dedalus.",
"ref_norm": "THEN YOU CAN ASK HIM QUESTIONS ON THE CATECHISM DEDALUS",
"hyp_norm": "THEN YOU CAN ASK HIM QUESTIONS ON THE CATECHISM DEDALUS",
"duration_s": 3.54,
"infer_time_s": 4.84,
"rtf": 1.3672,
"wer": 0.0
},
{
"id": "1089-134686-0017",
"ref": "STEPHEN LEANING BACK AND DRAWING IDLY ON HIS SCRIBBLER LISTENED TO THE TALK ABOUT HIM WHICH HERON CHECKED FROM TIME TO TIME BY SAYING",
"hyp": "Stephen, leaning back and drawing idly on his scribbler, listened to the talk about him , which Heron checked from time to time by saying.",
"ref_norm": "STEPHEN LEANING BACK AND DRAWING IDLY ON HIS SCRIBBLER LISTENED TO THE TALK ABOUT HIM WHICH HERON CHECKED FROM TIME TO TIME BY SAYING",
"hyp_norm": "STEPHEN LEANING BACK AND DRAWING IDLY ON HIS SCRIBBLER LISTENED TO THE TALK ABOUT HIM WHICH HERON CHECKED FROM TIME TO TIME BY SAYING",
"duration_s": 8.87,
"infer_time_s": 10.931,
"rtf": 1.2323,
"wer": 0.0
},
{
"id": "1089-134686-0018",
"ref": "IT WAS STRANGE TOO THAT HE FOUND AN ARID PLEASURE IN FOLLOWING UP TO THE END THE RIGID LINES OF THE DOCTRINES OF THE CHURCH AND PENETRATING INTO OBSCURE SILENCES ONLY TO HEAR AND FEEL THE MORE DEEPLY HIS OWN CONDEMNATION",
"hyp": "It was strange too that he found an arid pleasure in following up to the end the rigid lines of the doctrines of the church and penetrating into obscure silences, only to hear and feel the more deeply his own condemnation.",
"ref_norm": "IT WAS STRANGE TOO THAT HE FOUND AN ARID PLEASURE IN FOLLOWING UP TO THE END THE RIGID LINES OF THE DOCTRINES OF THE CHURCH AND PENETRATING INTO OBSCURE SILENCES ONLY TO HEAR AND FEEL THE MORE DEEPLY HIS OWN CONDEMNATION",
"hyp_norm": "IT WAS STRANGE TOO THAT HE FOUND AN ARID PLEASURE IN FOLLOWING UP TO THE END THE RIGID LINES OF THE DOCTRINES OF THE CHURCH AND PENETRATING INTO OBSCURE SILENCES ONLY TO HEAR AND FEEL THE MORE DEEPLY HIS OWN CONDEMNATION",
"duration_s": 15.72,
"infer_time_s": 16.239,
"rtf": 1.033,
"wer": 0.0
},
{
"id": "1089-134686-0019",
"ref": "THE SENTENCE OF SAINT JAMES WHICH SAYS THAT HE WHO OFFENDS AGAINST ONE COMMANDMENT BECOMES GUILTY OF ALL HAD SEEMED TO HIM FIRST A SWOLLEN PHRASE UNTIL HE HAD BEGUN TO GROPE IN THE DARKNESS OF HIS OWN STATE",
"hyp": "The sentence of Saint James, which says that he who offends against one commandment becomes guilty of all, had seemed to him first a swollen phrase until he had begun to grope in the darkness of his own state.",
"ref_norm": "THE SENTENCE OF SAINT JAMES WHICH SAYS THAT HE WHO OFFENDS AGAINST ONE COMMANDMENT BECOMES GUILTY OF ALL HAD SEEMED TO HIM FIRST A SWOLLEN PHRASE UNTIL HE HAD BEGUN TO GROPE IN THE DARKNESS OF HIS OWN STATE",
"hyp_norm": "THE SENTENCE OF SAINT JAMES WHICH SAYS THAT HE WHO OFFENDS AGAINST ONE COMMANDMENT BECOMES GUILTY OF ALL HAD SEEMED TO HIM FIRST A SWOLLEN PHRASE UNTIL HE HAD BEGUN TO GROPE IN THE DARKNESS OF HIS OWN STATE",
"duration_s": 13.895,
"infer_time_s": 15.395,
"rtf": 1.1079,
"wer": 0.0
},
{
"id": "1089-134686-0020",
"ref": "IF A MAN HAD STOLEN A POUND IN HIS YOUTH AND HAD USED THAT POUND TO AMASS A HUGE FORTUNE HOW MUCH WAS HE OBLIGED TO GIVE BACK THE POUND HE HAD STOLEN ONLY OR THE POUND TOGETHER WITH THE COMPOUND INTEREST ACCRUING UPON IT OR ALL HIS HUGE FORTUNE",
"hyp": "If a man had stolen a pound in his youth and had used that pound to amass a huge fortune , how much was he obliged to give back ? The pound he had stolen only, or the pound together with the compound interest accruing upon it, or all his huge fortune.",
"ref_norm": "IF A MAN HAD STOLEN A POUND IN HIS YOUTH AND HAD USED THAT POUND TO AMASS A HUGE FORTUNE HOW MUCH WAS HE OBLIGED TO GIVE BACK THE POUND HE HAD STOLEN ONLY OR THE POUND TOGETHER WITH THE COMPOUND INTEREST ACCRUING UPON IT OR ALL HIS HUGE FORTUNE",
"hyp_norm": "IF A MAN HAD STOLEN A POUND IN HIS YOUTH AND HAD USED THAT POUND TO AMASS A HUGE FORTUNE HOW MUCH WAS HE OBLIGED TO GIVE BACK THE POUND HE HAD STOLEN ONLY OR THE POUND TOGETHER WITH THE COMPOUND INTEREST ACCRUING UPON IT OR ALL HIS HUGE FORTUNE",
"duration_s": 16.79,
"infer_time_s": 19.73,
"rtf": 1.1751,
"wer": 0.0
},
{
"id": "1089-134686-0021",
"ref": "IF A LAYMAN IN GIVING BAPTISM POUR THE WATER BEFORE SAYING THE WORDS IS THE CHILD BAPTIZED",
"hyp": "If a layman in giving baptism pour the water before saying the words, is the child baptized?",
"ref_norm": "IF A LAYMAN IN GIVING BAPTISM POUR THE WATER BEFORE SAYING THE WORDS IS THE CHILD BAPTIZED",
"hyp_norm": "IF A LAYMAN IN GIVING BAPTISM POUR THE WATER BEFORE SAYING THE WORDS IS THE CHILD BAPTIZED",
"duration_s": 6.55,
"infer_time_s": 7.404,
"rtf": 1.1304,
"wer": 0.0
},
{
"id": "1089-134686-0022",
"ref": "HOW COMES IT THAT WHILE THE FIRST BEATITUDE PROMISES THE KINGDOM OF HEAVEN TO THE POOR OF HEART THE SECOND BEATITUDE PROMISES ALSO TO THE MEEK THAT THEY SHALL POSSESS THE LAND",
"hyp": "How comes it that while the first beatitude promises the kingdom of heaven to the poor of heart, the second beatitude promises also to the meek that they shall possess the land?",
"ref_norm": "HOW COMES IT THAT WHILE THE FIRST BEATITUDE PROMISES THE KINGDOM OF HEAVEN TO THE POOR OF HEART THE SECOND BEATITUDE PROMISES ALSO TO THE MEEK THAT THEY SHALL POSSESS THE LAND",
"hyp_norm": "HOW COMES IT THAT WHILE THE FIRST BEATITUDE PROMISES THE KINGDOM OF HEAVEN TO THE POOR OF HEART THE SECOND BEATITUDE PROMISES ALSO TO THE MEEK THAT THEY SHALL POSSESS THE LAND",
"duration_s": 11.175,
"infer_time_s": 12.836,
"rtf": 1.1487,
"wer": 0.0
},
{
"id": "1089-134686-0023",
"ref": "WHY WAS THE SACRAMENT OF THE EUCHARIST INSTITUTED UNDER THE TWO SPECIES OF BREAD AND WINE IF JESUS CHRIST BE PRESENT BODY AND BLOOD SOUL AND DIVINITY IN THE BREAD ALONE AND IN THE WINE ALONE",
"hyp": "Why was the sacrament of the Eucharist instituted under the two species of bread and wine, if Jesus Christ be present, body and blood, soul and divinity, in the bread alone and in the wine alone?",
"ref_norm": "WHY WAS THE SACRAMENT OF THE EUCHARIST INSTITUTED UNDER THE TWO SPECIES OF BREAD AND WINE IF JESUS CHRIST BE PRESENT BODY AND BLOOD SOUL AND DIVINITY IN THE BREAD ALONE AND IN THE WINE ALONE",
"hyp_norm": "WHY WAS THE SACRAMENT OF THE EUCHARIST INSTITUTED UNDER THE TWO SPECIES OF BREAD AND WINE IF JESUS CHRIST BE PRESENT BODY AND BLOOD SOUL AND DIVINITY IN THE BREAD ALONE AND IN THE WINE ALONE",
"duration_s": 13.275,
"infer_time_s": 15.855,
"rtf": 1.1944,
"wer": 0.0
},
{
"id": "1089-134686-0024",
"ref": "IF THE WINE CHANGE INTO VINEGAR AND THE HOST CRUMBLE INTO CORRUPTION AFTER THEY HAVE BEEN CONSECRATED IS JESUS CHRIST STILL PRESENT UNDER THEIR SPECIES AS GOD AND AS MAN",
"hyp": "If the wine change into vinegar, and the host crumble into corruption after they have been consec rated, is Jesus Christ still present under their species as God and as man?",
"ref_norm": "IF THE WINE CHANGE INTO VINEGAR AND THE HOST CRUMBLE INTO CORRUPTION AFTER THEY HAVE BEEN CONSECRATED IS JESUS CHRIST STILL PRESENT UNDER THEIR SPECIES AS GOD AND AS MAN",
"hyp_norm": "IF THE WINE CHANGE INTO VINEGAR AND THE HOST CRUMBLE INTO CORRUPTION AFTER THEY HAVE BEEN CONSEC RATED IS JESUS CHRIST STILL PRESENT UNDER THEIR SPECIES AS GOD AND AS MAN",
"duration_s": 11.655,
"infer_time_s": 12.442,
"rtf": 1.0675,
"wer": 0.0667
},
{
"id": "1089-134686-0025",
"ref": "A GENTLE KICK FROM THE TALL BOY IN THE BENCH BEHIND URGED STEPHEN TO ASK A DIFFICULT QUESTION",
"hyp": "A gentle kick from the tall boy in the bench behind urged Stephen to ask a difficult question.",
"ref_norm": "A GENTLE KICK FROM THE TALL BOY IN THE BENCH BEHIND URGED STEPHEN TO ASK A DIFFICULT QUESTION",
"hyp_norm": "A GENTLE KICK FROM THE TALL BOY IN THE BENCH BEHIND URGED STEPHEN TO ASK A DIFFICULT QUESTION",
"duration_s": 6.61,
"infer_time_s": 7.107,
"rtf": 1.0752,
"wer": 0.0
},
{
"id": "1089-134686-0026",
"ref": "THE RECTOR DID NOT ASK FOR A CATECHISM TO HEAR THE LESSON FROM",
"hyp": "The rector did not ask for a catechism to hear the lesson from.",
"ref_norm": "THE RECTOR DID NOT ASK FOR A CATECHISM TO HEAR THE LESSON FROM",
"hyp_norm": "THE RECTOR DID NOT ASK FOR A CATECHISM TO HEAR THE LESSON FROM",
"duration_s": 4.01,
"infer_time_s": 5.993,
"rtf": 1.4945,
"wer": 0.0
},
{
"id": "1089-134686-0027",
"ref": "HE CLASPED HIS HANDS ON THE DESK AND SAID",
"hyp": "He clasped his hands on the desk and said.",
"ref_norm": "HE CLASPED HIS HANDS ON THE DESK AND SAID",
"hyp_norm": "HE CLASPED HIS HANDS ON THE DESK AND SAID",
"duration_s": 2.71,
"infer_time_s": 3.839,
"rtf": 1.4165,
"wer": 0.0
},
{
"id": "1089-134686-0028",
"ref": "THE RETREAT WILL BEGIN ON WEDNESDAY AFTERNOON IN HONOUR OF SAINT FRANCIS XAVIER WHOSE FEAST DAY IS SATURDAY",
"hyp": "The retreat will begin on Wednesday afternoon in honor of Saint Francis Xavier, whose feast day is Saturday.",
"ref_norm": "THE RETREAT WILL BEGIN ON WEDNESDAY AFTERNOON IN HONOUR OF SAINT FRANCIS XAVIER WHOSE FEAST DAY IS SATURDAY",
"hyp_norm": "THE RETREAT WILL BEGIN ON WEDNESDAY AFTERNOON IN HONOR OF SAINT FRANCIS XAVIER WHOSE FEAST DAY IS SATURDAY",
"duration_s": 7.83,
"infer_time_s": 8.102,
"rtf": 1.0347,
"wer": 0.0556
},
{
"id": "1089-134686-0029",
"ref": "ON FRIDAY CONFESSION WILL BE HEARD ALL THE AFTERNOON AFTER BEADS",
"hyp": "On Friday, confession will be heard all the afternoon. After beads.",
"ref_norm": "ON FRIDAY CONFESSION WILL BE HEARD ALL THE AFTERNOON AFTER BEADS",
"hyp_norm": "ON FRIDAY CONFESSION WILL BE HEARD ALL THE AFTERNOON AFTER BEADS",
"duration_s": 4.67,
"infer_time_s": 5.617,
"rtf": 1.2029,
"wer": 0.0
},
{
"id": "1089-134686-0030",
"ref": "BEWARE OF MAKING THAT MISTAKE",
"hyp": "Beware of making that mistake.",
"ref_norm": "BEWARE OF MAKING THAT MISTAKE",
"hyp_norm": "BEWARE OF MAKING THAT MISTAKE",
"duration_s": 2.715,
"infer_time_s": 3.139,
"rtf": 1.1561,
"wer": 0.0
},
{
"id": "1089-134686-0031",
"ref": "STEPHEN'S HEART BEGAN SLOWLY TO FOLD AND FADE WITH FEAR LIKE A WITHERING FLOWER",
"hyp": "Stephen's heart began slowly to fold and fade with fear , like a withering flower.",
"ref_norm": "STEPHENS HEART BEGAN SLOWLY TO FOLD AND FADE WITH FEAR LIKE A WITHERING FLOWER",
"hyp_norm": "STEPHENS HEART BEGAN SLOWLY TO FOLD AND FADE WITH FEAR LIKE A WITHERING FLOWER",
"duration_s": 6.615,
"infer_time_s": 7.209,
"rtf": 1.0899,
"wer": 0.0
},
{
"id": "1089-134686-0032",
"ref": "HE IS CALLED AS YOU KNOW THE APOSTLE OF THE INDIES",
"hyp": "He is called, as you know, the Apostle of the Indies.",
"ref_norm": "HE IS CALLED AS YOU KNOW THE APOSTLE OF THE INDIES",
"hyp_norm": "HE IS CALLED AS YOU KNOW THE APOSTLE OF THE INDIES",
"duration_s": 4.09,
"infer_time_s": 5.507,
"rtf": 1.3464,
"wer": 0.0
},
{
"id": "1089-134686-0033",
"ref": "A GREAT SAINT SAINT FRANCIS XAVIER",
"hyp": "A great saint , Saint Francis Xavier.",
"ref_norm": "A GREAT SAINT SAINT FRANCIS XAVIER",
"hyp_norm": "A GREAT SAINT SAINT FRANCIS XAVIER",
"duration_s": 3.33,
"infer_time_s": 3.451,
"rtf": 1.0364,
"wer": 0.0
},
{
"id": "1089-134686-0034",
"ref": "THE RECTOR PAUSED AND THEN SHAKING HIS CLASPED HANDS BEFORE HIM WENT ON",
"hyp": "The rector paused , and then, shaking his clasped hands before him, went on.",
"ref_norm": "THE RECTOR PAUSED AND THEN SHAKING HIS CLASPED HANDS BEFORE HIM WENT ON",
"hyp_norm": "THE RECTOR PAUSED AND THEN SHAKING HIS CLASPED HANDS BEFORE HIM WENT ON",
"duration_s": 5.81,
"infer_time_s": 7.445,
"rtf": 1.2815,
"wer": 0.0
},
{
"id": "1089-134686-0035",
"ref": "HE HAD THE FAITH IN HIM THAT MOVES MOUNTAINS",
"hyp": "He had the faith in him that moves mountains.",
"ref_norm": "HE HAD THE FAITH IN HIM THAT MOVES MOUNTAINS",
"hyp_norm": "HE HAD THE FAITH IN HIM THAT MOVES MOUNTAINS",
"duration_s": 3.445,
"infer_time_s": 3.762,
"rtf": 1.0921,
"wer": 0.0
},
{
"id": "1089-134686-0036",
"ref": "A GREAT SAINT SAINT FRANCIS XAVIER",
"hyp": "A great saint, Saint Francis Xavier.",
"ref_norm": "A GREAT SAINT SAINT FRANCIS XAVIER",
"hyp_norm": "A GREAT SAINT SAINT FRANCIS XAVIER",
"duration_s": 3.25,
"infer_time_s": 3.341,
"rtf": 1.028,
"wer": 0.0
},
{
"id": "1089-134686-0037",
"ref": "IN THE SILENCE THEIR DARK FIRE KINDLED THE DUSK INTO A TAWNY GLOW",
"hyp": "In the silence, their dark fire kindled the dusk into a tawny glow.",
"ref_norm": "IN THE SILENCE THEIR DARK FIRE KINDLED THE DUSK INTO A TAWNY GLOW",
"hyp_norm": "IN THE SILENCE THEIR DARK FIRE KINDLED THE DUSK INTO A TAWNY GLOW",
"duration_s": 5.21,
"infer_time_s": 6.3,
"rtf": 1.2092,
"wer": 0.0
},
{
"id": "1089-134691-0000",
"ref": "HE COULD WAIT NO LONGER",
"hyp": "He could wait no longer.",
"ref_norm": "HE COULD WAIT NO LONGER",
"hyp_norm": "HE COULD WAIT NO LONGER",
"duration_s": 2.085,
"infer_time_s": 2.708,
"rtf": 1.2989,
"wer": 0.0
},
{
"id": "1089-134691-0001",
"ref": "FOR A FULL HOUR HE HAD PACED UP AND DOWN WAITING BUT HE COULD WAIT NO LONGER",
"hyp": "For a full hour, he had paced up and down, waiting , but he could wait no longer.",
"ref_norm": "FOR A FULL HOUR HE HAD PACED UP AND DOWN WAITING BUT HE COULD WAIT NO LONGER",
"hyp_norm": "FOR A FULL HOUR HE HAD PACED UP AND DOWN WAITING BUT HE COULD WAIT NO LONGER",
"duration_s": 5.415,
"infer_time_s": 7.137,
"rtf": 1.318,
"wer": 0.0
},
{
"id": "1089-134691-0002",
"ref": "HE SET OFF ABRUPTLY FOR THE BULL WALKING RAPIDLY LEST HIS FATHER'S SHRILL WHISTLE MIGHT CALL HIM BACK AND IN A FEW MOMENTS HE HAD ROUNDED THE CURVE AT THE POLICE BARRACK AND WAS SAFE",
"hyp": "He set off abruptly for the bull, walking rapidly, lest his father 's shrill whistle might call him back, and in a few moments he had rounded the curve at the police barrack and was safe.",
"ref_norm": "HE SET OFF ABRUPTLY FOR THE BULL WALKING RAPIDLY LEST HIS FATHERS SHRILL WHISTLE MIGHT CALL HIM BACK AND IN A FEW MOMENTS HE HAD ROUNDED THE CURVE AT THE POLICE BARRACK AND WAS SAFE",
"hyp_norm": "HE SET OFF ABRUPTLY FOR THE BULL WALKING RAPIDLY LEST HIS FATHER S SHRILL WHISTLE MIGHT CALL HIM BACK AND IN A FEW MOMENTS HE HAD ROUNDED THE CURVE AT THE POLICE BARRACK AND WAS SAFE",
"duration_s": 11.6,
"infer_time_s": 14.253,
"rtf": 1.2287,
"wer": 0.0571
},
{
"id": "1089-134691-0003",
"ref": "THE UNIVERSITY",
"hyp": "The University.",
"ref_norm": "THE UNIVERSITY",
"hyp_norm": "THE UNIVERSITY",
"duration_s": 2.175,
"infer_time_s": 1.789,
"rtf": 0.8223,
"wer": 0.0
},
{
"id": "1089-134691-0004",
"ref": "PRIDE AFTER SATISFACTION UPLIFTED HIM LIKE LONG SLOW WAVES",
"hyp": "Bride after satisfaction uplifted him like long slow waves.",
"ref_norm": "PRIDE AFTER SATISFACTION UPLIFTED HIM LIKE LONG SLOW WAVES",
"hyp_norm": "BRIDE AFTER SATISFACTION UPLIFTED HIM LIKE LONG SLOW WAVES",
"duration_s": 5.175,
"infer_time_s": 4.72,
"rtf": 0.9121,
"wer": 0.1111
},
{
"id": "1089-134691-0005",
"ref": "WHOSE FEET ARE AS THE FEET OF HARTS AND UNDERNEATH THE EVERLASTING ARMS",
"hyp": "Whose feet are as the feet of hearts, and underneath the everlasting arms.",
"ref_norm": "WHOSE FEET ARE AS THE FEET OF HARTS AND UNDERNEATH THE EVERLASTING ARMS",
"hyp_norm": "WHOSE FEET ARE AS THE FEET OF HEARTS AND UNDERNEATH THE EVERLASTING ARMS",
"duration_s": 5.36,
"infer_time_s": 5.763,
"rtf": 1.0753,
"wer": 0.0769
},
{
"id": "1089-134691-0006",
"ref": "THE PRIDE OF THAT DIM IMAGE BROUGHT BACK TO HIS MIND THE DIGNITY OF THE OFFICE HE HAD REFUSED",
"hyp": "The pride of that dim image brought back to his mind the dignity of the office he had refused.",
"ref_norm": "THE PRIDE OF THAT DIM IMAGE BROUGHT BACK TO HIS MIND THE DIGNITY OF THE OFFICE HE HAD REFUSED",
"hyp_norm": "THE PRIDE OF THAT DIM IMAGE BROUGHT BACK TO HIS MIND THE DIGNITY OF THE OFFICE HE HAD REFUSED",
"duration_s": 5.895,
"infer_time_s": 6.523,
"rtf": 1.1066,
"wer": 0.0
},
{
"id": "1089-134691-0007",
"ref": "SOON THE WHOLE BRIDGE WAS TREMBLING AND RESOUNDING",
"hyp": "Soon the whole bridge was trembling and resounding.",
"ref_norm": "SOON THE WHOLE BRIDGE WAS TREMBLING AND RESOUNDING",
"hyp_norm": "SOON THE WHOLE BRIDGE WAS TREMBLING AND RESOUNDING",
"duration_s": 3.44,
"infer_time_s": 3.567,
"rtf": 1.0369,
"wer": 0.0
},
{
"id": "1089-134691-0008",
"ref": "THE UNCOUTH FACES PASSED HIM TWO BY TWO STAINED YELLOW OR RED OR LIVID BY THE SEA AND AS HE STROVE TO LOOK AT THEM WITH EASE AND INDIFFERENCE A FAINT STAIN OF PERSONAL SHAME AND COMMISERATION ROSE TO HIS OWN FACE",
"hyp": "The uncouth faces passed him two by two , stained yellow or red or livid by the sea, and as he strove to look at them with ease and indifference, a faint stain of personal shame and commiseration rose to his own face.",
"ref_norm": "THE UNCOUTH FACES PASSED HIM TWO BY TWO STAINED YELLOW OR RED OR LIVID BY THE SEA AND AS HE STROVE TO LOOK AT THEM WITH EASE AND INDIFFERENCE A FAINT STAIN OF PERSONAL SHAME AND COMMISERATION ROSE TO HIS OWN FACE",
"hyp_norm": "THE UNCOUTH FACES PASSED HIM TWO BY TWO STAINED YELLOW OR RED OR LIVID BY THE SEA AND AS HE STROVE TO LOOK AT THEM WITH EASE AND INDIFFERENCE A FAINT STAIN OF PERSONAL SHAME AND COMMISERATION ROSE TO HIS OWN FACE",
"duration_s": 14.985,
"infer_time_s": 18.898,
"rtf": 1.2611,
"wer": 0.0
},
{
"id": "1089-134691-0009",
"ref": "ANGRY WITH HIMSELF HE TRIED TO HIDE HIS FACE FROM THEIR EYES BY GAZING DOWN SIDEWAYS INTO THE SHALLOW SWIRLING WATER UNDER THE BRIDGE BUT HE STILL SAW A REFLECTION THEREIN OF THEIR TOP HEAVY SILK HATS AND HUMBLE TAPE LIKE COLLARS AND LOOSELY HANGING CLERICAL CLOTHES BROTHER HICKEY",
"hyp": "Angry with himself , he tried to hide his face from their eyes by g azing down sideways into the shallow, swirling water under the bridge, but he still saw a reflection therein of their top-heavy silk hats, and humble tape-like collars and loosely hanging clerical clothes. Brother Hickey.",
"ref_norm": "ANGRY WITH HIMSELF HE TRIED TO HIDE HIS FACE FROM THEIR EYES BY GAZING DOWN SIDEWAYS INTO THE SHALLOW SWIRLING WATER UNDER THE BRIDGE BUT HE STILL SAW A REFLECTION THEREIN OF THEIR TOP HEAVY SILK HATS AND HUMBLE TAPE LIKE COLLARS AND LOOSELY HANGING CLERICAL CLOTHES BROTHER HICKEY",
"hyp_norm": "ANGRY WITH HIMSELF HE TRIED TO HIDE HIS FACE FROM THEIR EYES BY G AZING DOWN SIDEWAYS INTO THE SHALLOW SWIRLING WATER UNDER THE BRIDGE BUT HE STILL SAW A REFLECTION THEREIN OF THEIR TOPHEAVY SILK HATS AND HUMBLE TAPELIKE COLLARS AND LOOSELY HANGING CLERICAL CLOTHES BROTHER HICKEY",
"duration_s": 20.055,
"infer_time_s": 21.785,
"rtf": 1.0862,
"wer": 0.1224
},
{
"id": "1089-134691-0010",
"ref": "BROTHER MAC ARDLE BROTHER KEOGH",
"hyp": "Brother Macardle. Brother Kiyoff.",
"ref_norm": "BROTHER MAC ARDLE BROTHER KEOGH",
"hyp_norm": "BROTHER MACARDLE BROTHER KIYOFF",
"duration_s": 3.195,
"infer_time_s": 3.504,
"rtf": 1.0967,
"wer": 0.6
},
{
"id": "1089-134691-0011",
"ref": "THEIR PIETY WOULD BE LIKE THEIR NAMES LIKE THEIR FACES LIKE THEIR CLOTHES AND IT WAS IDLE FOR HIM TO TELL HIMSELF THAT THEIR HUMBLE AND CONTRITE HEARTS IT MIGHT BE PAID A FAR RICHER TRIBUTE OF DEVOTION THAN HIS HAD EVER BEEN A GIFT TENFOLD MORE ACCEPTABLE THAN HIS ELABORATE ADORATION",
"hyp": "Their piety would be like their names , like their faces , like their clothes, and it was idle for him to tell himself that their humble and contrite hearts it might be paid a far richer tribute of devotion than his had ever been, a gift ten fold more acceptable than his elaborate adoration.",
"ref_norm": "THEIR PIETY WOULD BE LIKE THEIR NAMES LIKE THEIR FACES LIKE THEIR CLOTHES AND IT WAS IDLE FOR HIM TO TELL HIMSELF THAT THEIR HUMBLE AND CONTRITE HEARTS IT MIGHT BE PAID A FAR RICHER TRIBUTE OF DEVOTION THAN HIS HAD EVER BEEN A GIFT TENFOLD MORE ACCEPTABLE THAN HIS ELABORATE ADORATION",
"hyp_norm": "THEIR PIETY WOULD BE LIKE THEIR NAMES LIKE THEIR FACES LIKE THEIR CLOTHES AND IT WAS IDLE FOR HIM TO TELL HIMSELF THAT THEIR HUMBLE AND CONTRITE HEARTS IT MIGHT BE PAID A FAR RICHER TRIBUTE OF DEVOTION THAN HIS HAD EVER BEEN A GIFT TEN FOLD MORE ACCEPTABLE THAN HIS ELABORATE ADORATION",
"duration_s": 20.01,
"infer_time_s": 22.013,
"rtf": 1.1001,
"wer": 0.0385
},
{
"id": "1089-134691-0012",
"ref": "IT WAS IDLE FOR HIM TO MOVE HIMSELF TO BE GENEROUS TOWARDS THEM TO TELL HIMSELF THAT IF HE EVER CAME TO THEIR GATES STRIPPED OF HIS PRIDE BEATEN AND IN BEGGAR'S WEEDS THAT THEY WOULD BE GENEROUS TOWARDS HIM LOVING HIM AS THEMSELVES",
"hyp": "It was idle for him to move himself to be generous towards them. To tell himself that if he ever came to their gates, stripped of his pride, beaten and in beggar's weeds , that they would be generous towards him, loving him as themselves.",
"ref_norm": "IT WAS IDLE FOR HIM TO MOVE HIMSELF TO BE GENEROUS TOWARDS THEM TO TELL HIMSELF THAT IF HE EVER CAME TO THEIR GATES STRIPPED OF HIS PRIDE BEATEN AND IN BEGGARS WEEDS THAT THEY WOULD BE GENEROUS TOWARDS HIM LOVING HIM AS THEMSELVES",
"hyp_norm": "IT WAS IDLE FOR HIM TO MOVE HIMSELF TO BE GENEROUS TOWARDS THEM TO TELL HIMSELF THAT IF HE EVER CAME TO THEIR GATES STRIPPED OF HIS PRIDE BEATEN AND IN BEGGARS WEEDS THAT THEY WOULD BE GENEROUS TOWARDS HIM LOVING HIM AS THEMSELVES",
"duration_s": 15.03,
"infer_time_s": 17.419,
"rtf": 1.159,
"wer": 0.0
},
{
"id": "1089-134691-0013",
"ref": "IDLE AND EMBITTERING FINALLY TO ARGUE AGAINST HIS OWN DISPASSIONATE CERTITUDE THAT THE COMMANDMENT OF LOVE BADE US NOT TO LOVE OUR NEIGHBOUR AS OURSELVES WITH THE SAME AMOUNT AND INTENSITY OF LOVE BUT TO LOVE HIM AS OURSELVES WITH THE SAME KIND OF LOVE",
"hyp": "Idle and emb ittering, finally to argue against his own dispass ionate certitude, that the commandment of love bade us not to love our neighbour as ourselves with the same amount and intensity of love, but to love him as ourselves with the same kind of love.",
"ref_norm": "IDLE AND EMBITTERING FINALLY TO ARGUE AGAINST HIS OWN DISPASSIONATE CERTITUDE THAT THE COMMANDMENT OF LOVE BADE US NOT TO LOVE OUR NEIGHBOUR AS OURSELVES WITH THE SAME AMOUNT AND INTENSITY OF LOVE BUT TO LOVE HIM AS OURSELVES WITH THE SAME KIND OF LOVE",
"hyp_norm": "IDLE AND EMB ITTERING FINALLY TO ARGUE AGAINST HIS OWN DISPASS IONATE CERTITUDE THAT THE COMMANDMENT OF LOVE BADE US NOT TO LOVE OUR NEIGHBOUR AS OURSELVES WITH THE SAME AMOUNT AND INTENSITY OF LOVE BUT TO LOVE HIM AS OURSELVES WITH THE SAME KIND OF LOVE",
"duration_s": 16.33,
"infer_time_s": 19.842,
"rtf": 1.2151,
"wer": 0.0889
},
{
"id": "1089-134691-0014",
"ref": "THE PHRASE AND THE DAY AND THE SCENE HARMONIZED IN A CHORD",
"hyp": "The phrase and the day and the scene harmonized in accord.",
"ref_norm": "THE PHRASE AND THE DAY AND THE SCENE HARMONIZED IN A CHORD",
"hyp_norm": "THE PHRASE AND THE DAY AND THE SCENE HARMONIZED IN ACCORD",
"duration_s": 4.755,
"infer_time_s": 5.225,
"rtf": 1.0988,
"wer": 0.1667
},
{
"id": "1089-134691-0015",
"ref": "WORDS WAS IT THEIR COLOURS",
"hyp": "Words. Was it their colors?",
"ref_norm": "WORDS WAS IT THEIR COLOURS",
"hyp_norm": "WORDS WAS IT THEIR COLORS",
"duration_s": 3.395,
"infer_time_s": 2.737,
"rtf": 0.8062,
"wer": 0.2
},
{
"id": "1089-134691-0016",
"ref": "THEY WERE VOYAGING ACROSS THE DESERTS OF THE SKY A HOST OF NOMADS ON THE MARCH VOYAGING HIGH OVER IRELAND WESTWARD BOUND",
"hyp": "They were voyaging across the deserts of the sky , a host of nom ads on the march, voy aging high over Ireland, westward bound.",
"ref_norm": "THEY WERE VOYAGING ACROSS THE DESERTS OF THE SKY A HOST OF NOMADS ON THE MARCH VOYAGING HIGH OVER IRELAND WESTWARD BOUND",
"hyp_norm": "THEY WERE VOYAGING ACROSS THE DESERTS OF THE SKY A HOST OF NOM ADS ON THE MARCH VOY AGING HIGH OVER IRELAND WESTWARD BOUND",
"duration_s": 9.06,
"infer_time_s": 10.885,
"rtf": 1.2015,
"wer": 0.1818
},
{
"id": "1089-134691-0017",
"ref": "THE EUROPE THEY HAD COME FROM LAY OUT THERE BEYOND THE IRISH SEA EUROPE OF STRANGE TONGUES AND VALLEYED AND WOODBEGIRT AND CITADELLED AND OF ENTRENCHED AND MARSHALLED RACES",
"hyp": "The Europe they had come from lay out there beyond the Irish Sea , Europe of strange tongues and valleyed and wood begirt and citadeld and of entrenched and marshalled races.",
"ref_norm": "THE EUROPE THEY HAD COME FROM LAY OUT THERE BEYOND THE IRISH SEA EUROPE OF STRANGE TONGUES AND VALLEYED AND WOODBEGIRT AND CITADELLED AND OF ENTRENCHED AND MARSHALLED RACES",
"hyp_norm": "THE EUROPE THEY HAD COME FROM LAY OUT THERE BEYOND THE IRISH SEA EUROPE OF STRANGE TONGUES AND VALLEYED AND WOOD BEGIRT AND CITADELD AND OF ENTRENCHED AND MARSHALLED RACES",
"duration_s": 11.695,
"infer_time_s": 13.042,
"rtf": 1.1152,
"wer": 0.1034
},
{
"id": "1089-134691-0018",
"ref": "AGAIN AGAIN",
"hyp": "Again, again.",
"ref_norm": "AGAIN AGAIN",
"hyp_norm": "AGAIN AGAIN",
"duration_s": 3.09,
"infer_time_s": 1.94,
"rtf": 0.6277,
"wer": 0.0
},
{
"id": "1089-134691-0019",
"ref": "A VOICE FROM BEYOND THE WORLD WAS CALLING",
"hyp": "A voice from beyond the world was calling.",
"ref_norm": "A VOICE FROM BEYOND THE WORLD WAS CALLING",
"hyp_norm": "A VOICE FROM BEYOND THE WORLD WAS CALLING",
"duration_s": 3.155,
"infer_time_s": 3.291,
"rtf": 1.0431,
"wer": 0.0
},
{
"id": "1089-134691-0020",
"ref": "HELLO STEPHANOS HERE COMES THE DEDALUS",
"hyp": "Hello, Stephan os! Here comes the Daddalis.",
"ref_norm": "HELLO STEPHANOS HERE COMES THE DEDALUS",
"hyp_norm": "HELLO STEPHAN OS HERE COMES THE DADDALIS",
"duration_s": 3.99,
"infer_time_s": 4.144,
"rtf": 1.0386,
"wer": 0.5
},
{
"id": "1089-134691-0021",
"ref": "THEIR DIVING STONE POISED ON ITS RUDE SUPPORTS AND ROCKING UNDER THEIR PLUNGES AND THE ROUGH HEWN STONES OF THE SLOPING BREAKWATER OVER WHICH THEY SCRAMBLED IN THEIR HORSEPLAY GLEAMED WITH COLD WET LUSTRE",
"hyp": "Their diving stone, poised on its rude supports, and rocking under their plunges, and the rough-hewn stones of the sloping breakwater over which they scrambled in their horseplay, gleamed with cold, wet lustre.",
"ref_norm": "THEIR DIVING STONE POISED ON ITS RUDE SUPPORTS AND ROCKING UNDER THEIR PLUNGES AND THE ROUGH HEWN STONES OF THE SLOPING BREAKWATER OVER WHICH THEY SCRAMBLED IN THEIR HORSEPLAY GLEAMED WITH COLD WET LUSTRE",
"hyp_norm": "THEIR DIVING STONE POISED ON ITS RUDE SUPPORTS AND ROCKING UNDER THEIR PLUNGES AND THE ROUGHHEWN STONES OF THE SLOPING BREAKWATER OVER WHICH THEY SCRAMBLED IN THEIR HORSEPLAY GLEAMED WITH COLD WET LUSTRE",
"duration_s": 13.37,
"infer_time_s": 15.868,
"rtf": 1.1869,
"wer": 0.0588
},
{
"id": "1089-134691-0022",
"ref": "HE STOOD STILL IN DEFERENCE TO THEIR CALLS AND PARRIED THEIR BANTER WITH EASY WORDS",
"hyp": "He stood still in deference to their calls and parried their banter with easy words.",
"ref_norm": "HE STOOD STILL IN DEFERENCE TO THEIR CALLS AND PARRIED THEIR BANTER WITH EASY WORDS",
"hyp_norm": "HE STOOD STILL IN DEFERENCE TO THEIR CALLS AND PARRIED THEIR BANTER WITH EASY WORDS",
"duration_s": 5.635,
"infer_time_s": 6.875,
"rtf": 1.2201,
"wer": 0.0
},
{
"id": "1089-134691-0023",
"ref": "IT WAS A PAIN TO SEE THEM AND A SWORD LIKE PAIN TO SEE THE SIGNS OF ADOLESCENCE THAT MADE REPELLENT THEIR PITIABLE NAKEDNESS",
"hyp": "It was a pain to see them, and a sword-like pain to see the signs of adolescence that made repellent their pitiable nakedness.",
"ref_norm": "IT WAS A PAIN TO SEE THEM AND A SWORD LIKE PAIN TO SEE THE SIGNS OF ADOLESCENCE THAT MADE REPELLENT THEIR PITIABLE NAKEDNESS",
"hyp_norm": "IT WAS A PAIN TO SEE THEM AND A SWORDLIKE PAIN TO SEE THE SIGNS OF ADOLESCENCE THAT MADE REPELLENT THEIR PITIABLE NAKEDNESS",
"duration_s": 7.735,
"infer_time_s": 9.575,
"rtf": 1.2378,
"wer": 0.0833
},
{
"id": "1089-134691-0024",
"ref": "STEPHANOS DEDALOS",
"hyp": "Stephanos Ter los.",
"ref_norm": "STEPHANOS DEDALOS",
"hyp_norm": "STEPHANOS TER LOS",
"duration_s": 2.215,
"infer_time_s": 2.671,
"rtf": 1.2058,
"wer": 1.0
},
{
"id": "1089-134691-0025",
"ref": "A MOMENT BEFORE THE GHOST OF THE ANCIENT KINGDOM OF THE DANES HAD LOOKED FORTH THROUGH THE VESTURE OF THE HAZEWRAPPED CITY",
"hyp": "A moment before , the ghost of the ancient kingdom of the Danes had looked forth through the v esture of the haze-wrapped city.",
"ref_norm": "A MOMENT BEFORE THE GHOST OF THE ANCIENT KINGDOM OF THE DANES HAD LOOKED FORTH THROUGH THE VESTURE OF THE HAZEWRAPPED CITY",
"hyp_norm": "A MOMENT BEFORE THE GHOST OF THE ANCIENT KINGDOM OF THE DANES HAD LOOKED FORTH THROUGH THE V ESTURE OF THE HAZEWRAPPED CITY",
"duration_s": 8.005,
"infer_time_s": 10.048,
"rtf": 1.2553,
"wer": 0.0909
},
{
"id": "1188-133604-0000",
"ref": "YOU WILL FIND ME CONTINUALLY SPEAKING OF FOUR MEN TITIAN HOLBEIN TURNER AND TINTORET IN ALMOST THE SAME TERMS",
"hyp": "You will find me continually speaking of four men: Titian , Holbein, Turner, and Tint oret, in almost the same terms.",
"ref_norm": "YOU WILL FIND ME CONTINUALLY SPEAKING OF FOUR MEN TITIAN HOLBEIN TURNER AND TINTORET IN ALMOST THE SAME TERMS",
"hyp_norm": "YOU WILL FIND ME CONTINUALLY SPEAKING OF FOUR MEN TITIAN HOLBEIN TURNER AND TINT ORET IN ALMOST THE SAME TERMS",
"duration_s": 10.725,
"infer_time_s": 11.008,
"rtf": 1.0263,
"wer": 0.1053
},
{
"id": "1188-133604-0001",
"ref": "THEY UNITE EVERY QUALITY AND SOMETIMES YOU WILL FIND ME REFERRING TO THEM AS COLORISTS SOMETIMES AS CHIAROSCURISTS",
"hyp": "They unite every quality, and sometimes you will find me referring to them as colorists , sometimes as chiaroscuroists.",
"ref_norm": "THEY UNITE EVERY QUALITY AND SOMETIMES YOU WILL FIND ME REFERRING TO THEM AS COLORISTS SOMETIMES AS CHIAROSCURISTS",
"hyp_norm": "THEY UNITE EVERY QUALITY AND SOMETIMES YOU WILL FIND ME REFERRING TO THEM AS COLORISTS SOMETIMES AS CHIAROSCUROISTS",
"duration_s": 9.04,
"infer_time_s": 8.732,
"rtf": 0.966,
"wer": 0.0556
},
{
"id": "1188-133604-0002",
"ref": "BY BEING STUDIOUS OF COLOR THEY ARE STUDIOUS OF DIVISION AND WHILE THE CHIAROSCURIST DEVOTES HIMSELF TO THE REPRESENTATION OF DEGREES OF FORCE IN ONE THING UNSEPARATED LIGHT THE COLORISTS HAVE FOR THEIR FUNCTION THE ATTAINMENT OF BEAUTY BY ARRANGEMENT OF THE DIVISIONS OF LIGHT",
"hyp": "By being studious of color, they are studious of division, and while the chiar oscuroist devotes himself to the representation of degrees of force in one thing , unseparated light, the colorists have for their function the attainment of beauty by arrangement of the divisions of light.",
"ref_norm": "BY BEING STUDIOUS OF COLOR THEY ARE STUDIOUS OF DIVISION AND WHILE THE CHIAROSCURIST DEVOTES HIMSELF TO THE REPRESENTATION OF DEGREES OF FORCE IN ONE THING UNSEPARATED LIGHT THE COLORISTS HAVE FOR THEIR FUNCTION THE ATTAINMENT OF BEAUTY BY ARRANGEMENT OF THE DIVISIONS OF LIGHT",
"hyp_norm": "BY BEING STUDIOUS OF COLOR THEY ARE STUDIOUS OF DIVISION AND WHILE THE CHIAR OSCUROIST DEVOTES HIMSELF TO THE REPRESENTATION OF DEGREES OF FORCE IN ONE THING UNSEPARATED LIGHT THE COLORISTS HAVE FOR THEIR FUNCTION THE ATTAINMENT OF BEAUTY BY ARRANGEMENT OF THE DIVISIONS OF LIGHT",
"duration_s": 17.96,
"infer_time_s": 22.215,
"rtf": 1.2369,
"wer": 0.0444
},
{
"id": "1188-133604-0003",
"ref": "MY FIRST AND PRINCIPAL REASON WAS THAT THEY ENFORCED BEYOND ALL RESISTANCE ON ANY STUDENT WHO MIGHT ATTEMPT TO COPY THEM THIS METHOD OF LAYING PORTIONS OF DISTINCT HUE SIDE BY SIDE",
"hyp": "My first and principal reason was that they enforced, beyond all resistance, on any student who might attempt to copy them, this method of laying portions of distinct hue side by side.",
"ref_norm": "MY FIRST AND PRINCIPAL REASON WAS THAT THEY ENFORCED BEYOND ALL RESISTANCE ON ANY STUDENT WHO MIGHT ATTEMPT TO COPY THEM THIS METHOD OF LAYING PORTIONS OF DISTINCT HUE SIDE BY SIDE",
"hyp_norm": "MY FIRST AND PRINCIPAL REASON WAS THAT THEY ENFORCED BEYOND ALL RESISTANCE ON ANY STUDENT WHO MIGHT ATTEMPT TO COPY THEM THIS METHOD OF LAYING PORTIONS OF DISTINCT HUE SIDE BY SIDE",
"duration_s": 12.61,
"infer_time_s": 13.452,
"rtf": 1.0668,
"wer": 0.0
},
{
"id": "1188-133604-0004",
"ref": "SOME OF THE TOUCHES INDEED WHEN THE TINT HAS BEEN MIXED WITH MUCH WATER HAVE BEEN LAID IN LITTLE DROPS OR PONDS SO THAT THE PIGMENT MIGHT CRYSTALLIZE HARD AT THE EDGE",
"hyp": "Some of the touches, indeed, when the tint has been mixed with much water , have been laid in little drops or ponds, so that the pigment might crystallize hard at the edge.",
"ref_norm": "SOME OF THE TOUCHES INDEED WHEN THE TINT HAS BEEN MIXED WITH MUCH WATER HAVE BEEN LAID IN LITTLE DROPS OR PONDS SO THAT THE PIGMENT MIGHT CRYSTALLIZE HARD AT THE EDGE",
"hyp_norm": "SOME OF THE TOUCHES INDEED WHEN THE TINT HAS BEEN MIXED WITH MUCH WATER HAVE BEEN LAID IN LITTLE DROPS OR PONDS SO THAT THE PIGMENT MIGHT CRYSTALLIZE HARD AT THE EDGE",
"duration_s": 10.65,
"infer_time_s": 13.271,
"rtf": 1.2461,
"wer": 0.0
},
{
"id": "1188-133604-0005",
"ref": "IT IS THE HEAD OF A PARROT WITH A LITTLE FLOWER IN HIS BEAK FROM A PICTURE OF CARPACCIO'S ONE OF HIS SERIES OF THE LIFE OF SAINT GEORGE",
"hyp": "It is the head of a par rot with a little flower in his beak, from a picture of Carp accius, one of his series of the life of Saint George.",
"ref_norm": "IT IS THE HEAD OF A PARROT WITH A LITTLE FLOWER IN HIS BEAK FROM A PICTURE OF CARPACCIOS ONE OF HIS SERIES OF THE LIFE OF SAINT GEORGE",
"hyp_norm": "IT IS THE HEAD OF A PAR ROT WITH A LITTLE FLOWER IN HIS BEAK FROM A PICTURE OF CARP ACCIUS ONE OF HIS SERIES OF THE LIFE OF SAINT GEORGE",
"duration_s": 8.56,
"infer_time_s": 11.578,
"rtf": 1.3526,
"wer": 0.1379
},
{
"id": "1188-133604-0006",
"ref": "THEN HE COMES TO THE BEAK OF IT",
"hyp": "Then he comes to the be ak of it.",
"ref_norm": "THEN HE COMES TO THE BEAK OF IT",
"hyp_norm": "THEN HE COMES TO THE BE AK OF IT",
"duration_s": 2.4,
"infer_time_s": 3.546,
"rtf": 1.4775,
"wer": 0.25
},
{
"id": "1188-133604-0007",
"ref": "THE BROWN GROUND BENEATH IS LEFT FOR THE MOST PART ONE TOUCH OF BLACK IS PUT FOR THE HOLLOW TWO DELICATE LINES OF DARK GRAY DEFINE THE OUTER CURVE AND ONE LITTLE QUIVERING TOUCH OF WHITE DRAWS THE INNER EDGE OF THE MANDIBLE",
"hyp": "The brown ground beneath is left for the most part. One touch of black is put for the hollow . Two delicate lines of dark gray define the outer curve, and one little qu ivering touch of white draws the inner edge of the mandible.",
"ref_norm": "THE BROWN GROUND BENEATH IS LEFT FOR THE MOST PART ONE TOUCH OF BLACK IS PUT FOR THE HOLLOW TWO DELICATE LINES OF DARK GRAY DEFINE THE OUTER CURVE AND ONE LITTLE QUIVERING TOUCH OF WHITE DRAWS THE INNER EDGE OF THE MANDIBLE",
"hyp_norm": "THE BROWN GROUND BENEATH IS LEFT FOR THE MOST PART ONE TOUCH OF BLACK IS PUT FOR THE HOLLOW TWO DELICATE LINES OF DARK GRAY DEFINE THE OUTER CURVE AND ONE LITTLE QU IVERING TOUCH OF WHITE DRAWS THE INNER EDGE OF THE MANDIBLE",
"duration_s": 14.24,
"infer_time_s": 17.593,
"rtf": 1.2354,
"wer": 0.0465
},
{
"id": "1188-133604-0008",
"ref": "FOR BELIEVE ME THE FINAL PHILOSOPHY OF ART CAN ONLY RATIFY THEIR OPINION THAT THE BEAUTY OF A COCK ROBIN IS TO BE RED AND OF A GRASS PLOT TO BE GREEN AND THE BEST SKILL OF ART IS IN INSTANTLY SEIZING ON THE MANIFOLD DELICIOUSNESS OF LIGHT WHICH YOU CAN ONLY SEIZE BY PRECISION OF INSTANTANEOUS TOUCH",
"hyp": "For believe me , the final philosophy of art can only ratify their opinion that the beauty of a cock robin is to be red, and of a grass plot to be green , and the best skill of art is in instantly seizing on the manifold deliciousness of light, which you can only seize by precision of instantaneous touch.",
"ref_norm": "FOR BELIEVE ME THE FINAL PHILOSOPHY OF ART CAN ONLY RATIFY THEIR OPINION THAT THE BEAUTY OF A COCK ROBIN IS TO BE RED AND OF A GRASS PLOT TO BE GREEN AND THE BEST SKILL OF ART IS IN INSTANTLY SEIZING ON THE MANIFOLD DELICIOUSNESS OF LIGHT WHICH YOU CAN ONLY SEIZE BY PRECISION OF INSTANTANEOUS TOUCH",
"hyp_norm": "FOR BELIEVE ME THE FINAL PHILOSOPHY OF ART CAN ONLY RATIFY THEIR OPINION THAT THE BEAUTY OF A COCK ROBIN IS TO BE RED AND OF A GRASS PLOT TO BE GREEN AND THE BEST SKILL OF ART IS IN INSTANTLY SEIZING ON THE MANIFOLD DELICIOUSNESS OF LIGHT WHICH YOU CAN ONLY SEIZE BY PRECISION OF INSTANTANEOUS TOUCH",
"duration_s": 20.755,
"infer_time_s": 22.852,
"rtf": 1.101,
"wer": 0.0
},
{
"id": "1188-133604-0009",
"ref": "NOW YOU WILL SEE IN THESE STUDIES THAT THE MOMENT THE WHITE IS INCLOSED PROPERLY AND HARMONIZED WITH THE OTHER HUES IT BECOMES SOMEHOW MORE PRECIOUS AND PEARLY THAN THE WHITE PAPER AND THAT I AM NOT AFRAID TO LEAVE A WHOLE FIELD OF UNTREATED WHITE PAPER ALL ROUND IT BEING SURE THAT EVEN THE LITTLE DIAMONDS IN THE ROUND WINDOW WILL TELL AS JEWELS IF THEY ARE GRADATED JUSTLY",
"hyp": "Now you will see in these studies that the moment the white is enclosed properly, and harmonized with the other hues, it becomes somehow more precious and pearly than the white paper , and that I am not afraid to leave a whole field of untreated white paper all round it, being sure that even the little diamonds in the round window will tell as jewels, if they are gradated justly.",
"ref_norm": "NOW YOU WILL SEE IN THESE STUDIES THAT THE MOMENT THE WHITE IS INCLOSED PROPERLY AND HARMONIZED WITH THE OTHER HUES IT BECOMES SOMEHOW MORE PRECIOUS AND PEARLY THAN THE WHITE PAPER AND THAT I AM NOT AFRAID TO LEAVE A WHOLE FIELD OF UNTREATED WHITE PAPER ALL ROUND IT BEING SURE THAT EVEN THE LITTLE DIAMONDS IN THE ROUND WINDOW WILL TELL AS JEWELS IF THEY ARE GRADATED JUSTLY",
"hyp_norm": "NOW YOU WILL SEE IN THESE STUDIES THAT THE MOMENT THE WHITE IS ENCLOSED PROPERLY AND HARMONIZED WITH THE OTHER HUES IT BECOMES SOMEHOW MORE PRECIOUS AND PEARLY THAN THE WHITE PAPER AND THAT I AM NOT AFRAID TO LEAVE A WHOLE FIELD OF UNTREATED WHITE PAPER ALL ROUND IT BEING SURE THAT EVEN THE LITTLE DIAMONDS IN THE ROUND WINDOW WILL TELL AS JEWELS IF THEY ARE GRADATED JUSTLY",
"duration_s": 23.06,
"infer_time_s": 27.33,
"rtf": 1.1852,
"wer": 0.0143
},
{
"id": "1188-133604-0010",
"ref": "BUT IN THIS VIGNETTE COPIED FROM TURNER YOU HAVE THE TWO PRINCIPLES BROUGHT OUT PERFECTLY",
"hyp": "But in this vignette copied from Turner , you have the two principles brought out perfectly.",
"ref_norm": "BUT IN THIS VIGNETTE COPIED FROM TURNER YOU HAVE THE TWO PRINCIPLES BROUGHT OUT PERFECTLY",
"hyp_norm": "BUT IN THIS VIGNETTE COPIED FROM TURNER YOU HAVE THE TWO PRINCIPLES BROUGHT OUT PERFECTLY",
"duration_s": 6.095,
"infer_time_s": 7.264,
"rtf": 1.1918,
"wer": 0.0
},
{
"id": "1188-133604-0011",
"ref": "THEY ARE BEYOND ALL OTHER WORKS THAT I KNOW EXISTING DEPENDENT FOR THEIR EFFECT ON LOW SUBDUED TONES THEIR FAVORITE CHOICE IN TIME OF DAY BEING EITHER DAWN OR TWILIGHT AND EVEN THEIR BRIGHTEST SUNSETS PRODUCED CHIEFLY OUT OF GRAY PAPER",
"hyp": "They are beyond all other works that I know existing , dependent for their effect on low, subdued tones. Their favorite choice in time of day being either dawn or twilight, and even their brightest sunsets produced chiefly out of gray paper.",
"ref_norm": "THEY ARE BEYOND ALL OTHER WORKS THAT I KNOW EXISTING DEPENDENT FOR THEIR EFFECT ON LOW SUBDUED TONES THEIR FAVORITE CHOICE IN TIME OF DAY BEING EITHER DAWN OR TWILIGHT AND EVEN THEIR BRIGHTEST SUNSETS PRODUCED CHIEFLY OUT OF GRAY PAPER",
"hyp_norm": "THEY ARE BEYOND ALL OTHER WORKS THAT I KNOW EXISTING DEPENDENT FOR THEIR EFFECT ON LOW SUBDUED TONES THEIR FAVORITE CHOICE IN TIME OF DAY BEING EITHER DAWN OR TWILIGHT AND EVEN THEIR BRIGHTEST SUNSETS PRODUCED CHIEFLY OUT OF GRAY PAPER",
"duration_s": 15.19,
"infer_time_s": 17.919,
"rtf": 1.1797,
"wer": 0.0
},
{
"id": "1188-133604-0012",
"ref": "IT MAY BE THAT A GREAT COLORIST WILL USE HIS UTMOST FORCE OF COLOR AS A SINGER HIS FULL POWER OF VOICE BUT LOUD OR LOW THE VIRTUE IS IN BOTH CASES ALWAYS IN REFINEMENT NEVER IN LOUDNESS",
"hyp": "It may be that a great colorless will use his utmost force of color, as a singer his full power of voice , but loud or low, the virtue is in both cases always in refinement, never in loudness.",
"ref_norm": "IT MAY BE THAT A GREAT COLORIST WILL USE HIS UTMOST FORCE OF COLOR AS A SINGER HIS FULL POWER OF VOICE BUT LOUD OR LOW THE VIRTUE IS IN BOTH CASES ALWAYS IN REFINEMENT NEVER IN LOUDNESS",
"hyp_norm": "IT MAY BE THAT A GREAT COLORLESS WILL USE HIS UTMOST FORCE OF COLOR AS A SINGER HIS FULL POWER OF VOICE BUT LOUD OR LOW THE VIRTUE IS IN BOTH CASES ALWAYS IN REFINEMENT NEVER IN LOUDNESS",
"duration_s": 14.65,
"infer_time_s": 17.596,
"rtf": 1.2011,
"wer": 0.0263
},
{
"id": "1188-133604-0013",
"ref": "IT MUST REMEMBER BE ONE OR THE OTHER",
"hyp": "It must remember be one or the other.",
"ref_norm": "IT MUST REMEMBER BE ONE OR THE OTHER",
"hyp_norm": "IT MUST REMEMBER BE ONE OR THE OTHER",
"duration_s": 3.02,
"infer_time_s": 3.528,
"rtf": 1.1681,
"wer": 0.0
},
{
"id": "1188-133604-0014",
"ref": "DO NOT THEREFORE THINK THAT THE GOTHIC SCHOOL IS AN EASY ONE",
"hyp": "Do not, therefore , think that the Gothic school is an easy one.",
"ref_norm": "DO NOT THEREFORE THINK THAT THE GOTHIC SCHOOL IS AN EASY ONE",
"hyp_norm": "DO NOT THEREFORE THINK THAT THE GOTHIC SCHOOL IS AN EASY ONE",
"duration_s": 4.39,
"infer_time_s": 5.957,
"rtf": 1.3569,
"wer": 0.0
},
{
"id": "1188-133604-0015",
"ref": "THE LAW OF THAT SCHOOL IS THAT EVERYTHING SHALL BE SEEN CLEARLY OR AT LEAST ONLY IN SUCH MIST OR FAINTNESS AS SHALL BE DELIGHTFUL AND I HAVE NO DOUBT THAT THE BEST INTRODUCTION TO IT WOULD BE THE ELEMENTARY PRACTICE OF PAINTING EVERY STUDY ON A GOLDEN GROUND",
"hyp": "The law of that school is that everything shall be seen clearly, or at least , only in such mist or faintness as shall be delightful . And I have no doubt that the best introduction to it would be the elementary practice of painting every study on a golden ground.",
"ref_norm": "THE LAW OF THAT SCHOOL IS THAT EVERYTHING SHALL BE SEEN CLEARLY OR AT LEAST ONLY IN SUCH MIST OR FAINTNESS AS SHALL BE DELIGHTFUL AND I HAVE NO DOUBT THAT THE BEST INTRODUCTION TO IT WOULD BE THE ELEMENTARY PRACTICE OF PAINTING EVERY STUDY ON A GOLDEN GROUND",
"hyp_norm": "THE LAW OF THAT SCHOOL IS THAT EVERYTHING SHALL BE SEEN CLEARLY OR AT LEAST ONLY IN SUCH MIST OR FAINTNESS AS SHALL BE DELIGHTFUL AND I HAVE NO DOUBT THAT THE BEST INTRODUCTION TO IT WOULD BE THE ELEMENTARY PRACTICE OF PAINTING EVERY STUDY ON A GOLDEN GROUND",
"duration_s": 16.085,
"infer_time_s": 19.494,
"rtf": 1.2119,
"wer": 0.0
},
{
"id": "1188-133604-0016",
"ref": "THIS AT ONCE COMPELS YOU TO UNDERSTAND THAT THE WORK IS TO BE IMAGINATIVE AND DECORATIVE THAT IT REPRESENTS BEAUTIFUL THINGS IN THE CLEAREST WAY BUT NOT UNDER EXISTING CONDITIONS AND THAT IN FACT YOU ARE PRODUCING JEWELER'S WORK RATHER THAN PICTURES",
"hyp": "This at once comp els you to understand that the work is to be imaginative and decorative, that it represents beautiful things in the clearest way, but not under existing conditions, and that , in fact, you are producing jeweler's work rather than pictures.",
"ref_norm": "THIS AT ONCE COMPELS YOU TO UNDERSTAND THAT THE WORK IS TO BE IMAGINATIVE AND DECORATIVE THAT IT REPRESENTS BEAUTIFUL THINGS IN THE CLEAREST WAY BUT NOT UNDER EXISTING CONDITIONS AND THAT IN FACT YOU ARE PRODUCING JEWELERS WORK RATHER THAN PICTURES",
"hyp_norm": "THIS AT ONCE COMP ELS YOU TO UNDERSTAND THAT THE WORK IS TO BE IMAGINATIVE AND DECORATIVE THAT IT REPRESENTS BEAUTIFUL THINGS IN THE CLEAREST WAY BUT NOT UNDER EXISTING CONDITIONS AND THAT IN FACT YOU ARE PRODUCING JEWELERS WORK RATHER THAN PICTURES",
"duration_s": 16.595,
"infer_time_s": 19.223,
"rtf": 1.1583,
"wer": 0.0476
},
{
"id": "1188-133604-0017",
"ref": "THAT A STYLE IS RESTRAINED OR SEVERE DOES NOT MEAN THAT IT IS ALSO ERRONEOUS",
"hyp": "That a style is restrained or severe does not mean that it is also erroneous.",
"ref_norm": "THAT A STYLE IS RESTRAINED OR SEVERE DOES NOT MEAN THAT IT IS ALSO ERRONEOUS",
"hyp_norm": "THAT A STYLE IS RESTRAINED OR SEVERE DOES NOT MEAN THAT IT IS ALSO ERRONEOUS",
"duration_s": 4.615,
"infer_time_s": 5.907,
"rtf": 1.28,
"wer": 0.0
},
{
"id": "1188-133604-0018",
"ref": "IN ALL EARLY GOTHIC ART INDEED YOU WILL FIND FAILURE OF THIS KIND ESPECIALLY DISTORTION AND RIGIDITY WHICH ARE IN MANY RESPECTS PAINFULLY TO BE COMPARED WITH THE SPLENDID REPOSE OF CLASSIC ART",
"hyp": "In all early Gothic art, indeed, you will find failure of this kind, especially distortion and rig idity, which are in many respects painfully to be compared with the splendid repose of classic art.",
"ref_norm": "IN ALL EARLY GOTHIC ART INDEED YOU WILL FIND FAILURE OF THIS KIND ESPECIALLY DISTORTION AND RIGIDITY WHICH ARE IN MANY RESPECTS PAINFULLY TO BE COMPARED WITH THE SPLENDID REPOSE OF CLASSIC ART",
"hyp_norm": "IN ALL EARLY GOTHIC ART INDEED YOU WILL FIND FAILURE OF THIS KIND ESPECIALLY DISTORTION AND RIG IDITY WHICH ARE IN MANY RESPECTS PAINFULLY TO BE COMPARED WITH THE SPLENDID REPOSE OF CLASSIC ART",
"duration_s": 11.55,
"infer_time_s": 14.145,
"rtf": 1.2247,
"wer": 0.0606
},
{
"id": "1188-133604-0019",
"ref": "THE LARGE LETTER CONTAINS INDEED ENTIRELY FEEBLE AND ILL DRAWN FIGURES THAT IS MERELY CHILDISH AND FAILING WORK OF AN INFERIOR HAND IT IS NOT CHARACTERISTIC OF GOTHIC OR ANY OTHER SCHOOL",
"hyp": "The large letter contains, indeed, entirely feeble and ill-drawn figures. That is merely childish and failing work of an inferior hand. It is not characteristic of Gothic or any other school.",
"ref_norm": "THE LARGE LETTER CONTAINS INDEED ENTIRELY FEEBLE AND ILL DRAWN FIGURES THAT IS MERELY CHILDISH AND FAILING WORK OF AN INFERIOR HAND IT IS NOT CHARACTERISTIC OF GOTHIC OR ANY OTHER SCHOOL",
"hyp_norm": "THE LARGE LETTER CONTAINS INDEED ENTIRELY FEEBLE AND ILLDRAWN FIGURES THAT IS MERELY CHILDISH AND FAILING WORK OF AN INFERIOR HAND IT IS NOT CHARACTERISTIC OF GOTHIC OR ANY OTHER SCHOOL",
"duration_s": 13.93,
"infer_time_s": 14.924,
"rtf": 1.0714,
"wer": 0.0625
},
{
"id": "1188-133604-0020",
"ref": "BUT OBSERVE YOU CAN ONLY DO THIS ON ONE CONDITION THAT OF STRIVING ALSO TO CREATE IN REALITY THE BEAUTY WHICH YOU SEEK IN IMAGINATION",
"hyp": "But observe , you can only do this on one condition, that of striving also to create in reality , the beauty which you seek in imagination.",
"ref_norm": "BUT OBSERVE YOU CAN ONLY DO THIS ON ONE CONDITION THAT OF STRIVING ALSO TO CREATE IN REALITY THE BEAUTY WHICH YOU SEEK IN IMAGINATION",
"hyp_norm": "BUT OBSERVE YOU CAN ONLY DO THIS ON ONE CONDITION THAT OF STRIVING ALSO TO CREATE IN REALITY THE BEAUTY WHICH YOU SEEK IN IMAGINATION",
"duration_s": 10.26,
"infer_time_s": 11.63,
"rtf": 1.1335,
"wer": 0.0
},
{
"id": "1188-133604-0021",
"ref": "IT WILL BE WHOLLY IMPOSSIBLE FOR YOU TO RETAIN THE TRANQUILLITY OF TEMPER AND FELICITY OF FAITH NECESSARY FOR NOBLE PURIST PAINTING UNLESS YOU ARE ACTIVELY ENGAGED IN PROMOTING THE FELICITY AND PEACE OF PRACTICAL LIFE",
"hyp": "It will be wholly impossible for you to retain the tranquillity of temper and felicity of faith, necessary for noble, purest painting , unless you are actively engaged in promoting the felicity and peace of practical life.",
"ref_norm": "IT WILL BE WHOLLY IMPOSSIBLE FOR YOU TO RETAIN THE TRANQUILLITY OF TEMPER AND FELICITY OF FAITH NECESSARY FOR NOBLE PURIST PAINTING UNLESS YOU ARE ACTIVELY ENGAGED IN PROMOTING THE FELICITY AND PEACE OF PRACTICAL LIFE",
"hyp_norm": "IT WILL BE WHOLLY IMPOSSIBLE FOR YOU TO RETAIN THE TRANQUILLITY OF TEMPER AND FELICITY OF FAITH NECESSARY FOR NOBLE PUREST PAINTING UNLESS YOU ARE ACTIVELY ENGAGED IN PROMOTING THE FELICITY AND PEACE OF PRACTICAL LIFE",
"duration_s": 14.02,
"infer_time_s": 17.07,
"rtf": 1.2175,
"wer": 0.0278
},
{
"id": "1188-133604-0022",
"ref": "YOU MUST LOOK AT HIM IN THE FACE FIGHT HIM CONQUER HIM WITH WHAT SCATHE YOU MAY YOU NEED NOT THINK TO KEEP OUT OF THE WAY OF HIM",
"hyp": "You must look at him in the face, fight him, conquer him , with what scathe you may. You need not think to keep out of the way of him.",
"ref_norm": "YOU MUST LOOK AT HIM IN THE FACE FIGHT HIM CONQUER HIM WITH WHAT SCATHE YOU MAY YOU NEED NOT THINK TO KEEP OUT OF THE WAY OF HIM",
"hyp_norm": "YOU MUST LOOK AT HIM IN THE FACE FIGHT HIM CONQUER HIM WITH WHAT SCATHE YOU MAY YOU NEED NOT THINK TO KEEP OUT OF THE WAY OF HIM",
"duration_s": 9.63,
"infer_time_s": 12.109,
"rtf": 1.2574,
"wer": 0.0
},
{
"id": "1188-133604-0023",
"ref": "THE COLORIST SAYS FIRST OF ALL AS MY DELICIOUS PAROQUET WAS RUBY SO THIS NASTY VIPER SHALL BE BLACK AND THEN IS THE QUESTION CAN I ROUND HIM OFF EVEN THOUGH HE IS BLACK AND MAKE HIM SLIMY AND YET SPRINGY AND CLOSE DOWN CLOTTED LIKE A POOL OF BLACK BLOOD ON THE EARTH ALL THE SAME",
"hyp": "The colorist says, first of all , as my delicious perique was ruby . So this nasty viper shall be black. And then is the question : Can I round him off, even though he is black, and make him slimy , and yet springy and close down, clotted like a pool of black blood on the earth, all the same?",
"ref_norm": "THE COLORIST SAYS FIRST OF ALL AS MY DELICIOUS PAROQUET WAS RUBY SO THIS NASTY VIPER SHALL BE BLACK AND THEN IS THE QUESTION CAN I ROUND HIM OFF EVEN THOUGH HE IS BLACK AND MAKE HIM SLIMY AND YET SPRINGY AND CLOSE DOWN CLOTTED LIKE A POOL OF BLACK BLOOD ON THE EARTH ALL THE SAME",
"hyp_norm": "THE COLORIST SAYS FIRST OF ALL AS MY DELICIOUS PERIQUE WAS RUBY SO THIS NASTY VIPER SHALL BE BLACK AND THEN IS THE QUESTION CAN I ROUND HIM OFF EVEN THOUGH HE IS BLACK AND MAKE HIM SLIMY AND YET SPRINGY AND CLOSE DOWN CLOTTED LIKE A POOL OF BLACK BLOOD ON THE EARTH ALL THE SAME",
"duration_s": 23.67,
"infer_time_s": 26.633,
"rtf": 1.1252,
"wer": 0.0175
},
{
"id": "1188-133604-0024",
"ref": "NOTHING WILL BE MORE PRECIOUS TO YOU I THINK IN THE PRACTICAL STUDY OF ART THAN THE CONVICTION WHICH WILL FORCE ITSELF ON YOU MORE AND MORE EVERY HOUR OF THE WAY ALL THINGS ARE BOUND TOGETHER LITTLE AND GREAT IN SPIRIT AND IN MATTER",
"hyp": "Nothing will be more precious to you, I think, in the practical study of art than the conviction , which will force itself on you more and more every hour , of the way all things are bound together, little and great, in spirit and in matter.",
"ref_norm": "NOTHING WILL BE MORE PRECIOUS TO YOU I THINK IN THE PRACTICAL STUDY OF ART THAN THE CONVICTION WHICH WILL FORCE ITSELF ON YOU MORE AND MORE EVERY HOUR OF THE WAY ALL THINGS ARE BOUND TOGETHER LITTLE AND GREAT IN SPIRIT AND IN MATTER",
"hyp_norm": "NOTHING WILL BE MORE PRECIOUS TO YOU I THINK IN THE PRACTICAL STUDY OF ART THAN THE CONVICTION WHICH WILL FORCE ITSELF ON YOU MORE AND MORE EVERY HOUR OF THE WAY ALL THINGS ARE BOUND TOGETHER LITTLE AND GREAT IN SPIRIT AND IN MATTER",
"duration_s": 15.24,
"infer_time_s": 18.473,
"rtf": 1.2121,
"wer": 0.0
},
{
"id": "1188-133604-0025",
"ref": "YOU KNOW I HAVE JUST BEEN TELLING YOU HOW THIS SCHOOL OF MATERIALISM AND CLAY INVOLVED ITSELF AT LAST IN CLOUD AND FIRE",
"hyp": "You know, I've just been telling you how this school of materialism in clay involved itself at last in cloud and fire.",
"ref_norm": "YOU KNOW I HAVE JUST BEEN TELLING YOU HOW THIS SCHOOL OF MATERIALISM AND CLAY INVOLVED ITSELF AT LAST IN CLOUD AND FIRE",
"hyp_norm": "YOU KNOW IVE JUST BEEN TELLING YOU HOW THIS SCHOOL OF MATERIALISM IN CLAY INVOLVED ITSELF AT LAST IN CLOUD AND FIRE",
"duration_s": 7.45,
"infer_time_s": 9.001,
"rtf": 1.2081,
"wer": 0.1304
},
{
"id": "1188-133604-0026",
"ref": "HERE IS AN EQUALLY TYPICAL GREEK SCHOOL LANDSCAPE BY WILSON LOST WHOLLY IN GOLDEN MIST THE TREES SO SLIGHTLY DRAWN THAT YOU DON'T KNOW IF THEY ARE TREES OR TOWERS AND NO CARE FOR COLOR WHATEVER PERFECTLY DECEPTIVE AND MARVELOUS EFFECT OF SUNSHINE THROUGH THE MIST APOLLO AND THE PYTHON",
"hyp": "Here is an equally typical Greek school landscape by Wilson, lost wholly in golden mist . The trees so slightly drawn that you don't know if they are trees or towers , and no care for color whatsoever. Perfectly deceptive and marvelous effect of sunshine through the mist. Apollo and the Python.",
"ref_norm": "HERE IS AN EQUALLY TYPICAL GREEK SCHOOL LANDSCAPE BY WILSON LOST WHOLLY IN GOLDEN MIST THE TREES SO SLIGHTLY DRAWN THAT YOU DONT KNOW IF THEY ARE TREES OR TOWERS AND NO CARE FOR COLOR WHATEVER PERFECTLY DECEPTIVE AND MARVELOUS EFFECT OF SUNSHINE THROUGH THE MIST APOLLO AND THE PYTHON",
"hyp_norm": "HERE IS AN EQUALLY TYPICAL GREEK SCHOOL LANDSCAPE BY WILSON LOST WHOLLY IN GOLDEN MIST THE TREES SO SLIGHTLY DRAWN THAT YOU DONT KNOW IF THEY ARE TREES OR TOWERS AND NO CARE FOR COLOR WHATSOEVER PERFECTLY DECEPTIVE AND MARVELOUS EFFECT OF SUNSHINE THROUGH THE MIST APOLLO AND THE PYTHON",
"duration_s": 20.125,
"infer_time_s": 21.352,
"rtf": 1.061,
"wer": 0.02
},
{
"id": "1188-133604-0027",
"ref": "NOW HERE IS RAPHAEL EXACTLY BETWEEN THE TWO TREES STILL DRAWN LEAF BY LEAF WHOLLY FORMAL BUT BEAUTIFUL MIST COMING GRADUALLY INTO THE DISTANCE",
"hyp": "Now here is Raphael , exactly between the two trees, still drawn leaf by leaf, wholly formal , but beautiful mist coming gradually into the distance.",
"ref_norm": "NOW HERE IS RAPHAEL EXACTLY BETWEEN THE TWO TREES STILL DRAWN LEAF BY LEAF WHOLLY FORMAL BUT BEAUTIFUL MIST COMING GRADUALLY INTO THE DISTANCE",
"hyp_norm": "NOW HERE IS RAPHAEL EXACTLY BETWEEN THE TWO TREES STILL DRAWN LEAF BY LEAF WHOLLY FORMAL BUT BEAUTIFUL MIST COMING GRADUALLY INTO THE DISTANCE",
"duration_s": 11.245,
"infer_time_s": 12.319,
"rtf": 1.0955,
"wer": 0.0
},
{
"id": "1188-133604-0028",
"ref": "WELL THEN LAST HERE IS TURNER'S GREEK SCHOOL OF THE HIGHEST CLASS AND YOU DEFINE HIS ART ABSOLUTELY AS FIRST THE DISPLAYING INTENSELY AND WITH THE STERNEST INTELLECT OF NATURAL FORM AS IT IS AND THEN THE ENVELOPMENT OF IT WITH CLOUD AND FIRE",
"hyp": "Well then, last here is Turner's , Greek school of the highest class , and you define his art absolutely, as first the displaying intensely and with the sternest intellect of natural form as it is, and then the envelopment of it with cloud and fire.",
"ref_norm": "WELL THEN LAST HERE IS TURNERS GREEK SCHOOL OF THE HIGHEST CLASS AND YOU DEFINE HIS ART ABSOLUTELY AS FIRST THE DISPLAYING INTENSELY AND WITH THE STERNEST INTELLECT OF NATURAL FORM AS IT IS AND THEN THE ENVELOPMENT OF IT WITH CLOUD AND FIRE",
"hyp_norm": "WELL THEN LAST HERE IS TURNERS GREEK SCHOOL OF THE HIGHEST CLASS AND YOU DEFINE HIS ART ABSOLUTELY AS FIRST THE DISPLAYING INTENSELY AND WITH THE STERNEST INTELLECT OF NATURAL FORM AS IT IS AND THEN THE ENVELOPMENT OF IT WITH CLOUD AND FIRE",
"duration_s": 19.005,
"infer_time_s": 20.012,
"rtf": 1.053,
"wer": 0.0
},
{
"id": "1188-133604-0029",
"ref": "ONLY THERE ARE TWO SORTS OF CLOUD AND FIRE",
"hyp": "Only, there are two sorts of cloud and fire.",
"ref_norm": "ONLY THERE ARE TWO SORTS OF CLOUD AND FIRE",
"hyp_norm": "ONLY THERE ARE TWO SORTS OF CLOUD AND FIRE",
"duration_s": 3.705,
"infer_time_s": 3.89,
"rtf": 1.0501,
"wer": 0.0
},
{
"id": "1188-133604-0030",
"ref": "HE KNOWS THEM BOTH",
"hyp": "He knows them both.",
"ref_norm": "HE KNOWS THEM BOTH",
"hyp_norm": "HE KNOWS THEM BOTH",
"duration_s": 1.915,
"infer_time_s": 1.856,
"rtf": 0.969,
"wer": 0.0
},
{
"id": "1188-133604-0031",
"ref": "THERE'S ONE AND THERE'S ANOTHER THE DUDLEY AND THE FLINT",
"hyp": "There's one, and there's another, the Dudley and the Flint.",
"ref_norm": "THERES ONE AND THERES ANOTHER THE DUDLEY AND THE FLINT",
"hyp_norm": "THERES ONE AND THERES ANOTHER THE DUDLEY AND THE FLINT",
"duration_s": 4.25,
"infer_time_s": 5.585,
"rtf": 1.3142,
"wer": 0.0
},
{
"id": "1188-133604-0032",
"ref": "IT IS ONLY A PENCIL OUTLINE BY EDWARD BURNE JONES IN ILLUSTRATION OF THE STORY OF PSYCHE IT IS THE INTRODUCTION OF PSYCHE AFTER ALL HER TROUBLES INTO HEAVEN",
"hyp": "It is only a pencil outline by Edward Burne Jones, in illustration of the story of Psyche . It is the introduction of Psyche after all her troubles into heaven.",
"ref_norm": "IT IS ONLY A PENCIL OUTLINE BY EDWARD BURNE JONES IN ILLUSTRATION OF THE STORY OF PSYCHE IT IS THE INTRODUCTION OF PSYCHE AFTER ALL HER TROUBLES INTO HEAVEN",
"hyp_norm": "IT IS ONLY A PENCIL OUTLINE BY EDWARD BURNE JONES IN ILLUSTRATION OF THE STORY OF PSYCHE IT IS THE INTRODUCTION OF PSYCHE AFTER ALL HER TROUBLES INTO HEAVEN",
"duration_s": 10.985,
"infer_time_s": 12.711,
"rtf": 1.1571,
"wer": 0.0
},
{
"id": "1188-133604-0033",
"ref": "EVERY PLANT IN THE GRASS IS SET FORMALLY GROWS PERFECTLY AND MAY BE REALIZED COMPLETELY",
"hyp": "Every plant in the grass is set formally, grows perfectly, and may be realized completely.",
"ref_norm": "EVERY PLANT IN THE GRASS IS SET FORMALLY GROWS PERFECTLY AND MAY BE REALIZED COMPLETELY",
"hyp_norm": "EVERY PLANT IN THE GRASS IS SET FORMALLY GROWS PERFECTLY AND MAY BE REALIZED COMPLETELY",
"duration_s": 6.625,
"infer_time_s": 7.042,
"rtf": 1.0629,
"wer": 0.0
},
{
"id": "1188-133604-0034",
"ref": "EXQUISITE ORDER AND UNIVERSAL WITH ETERNAL LIFE AND LIGHT THIS IS THE FAITH AND EFFORT OF THE SCHOOLS OF CRYSTAL AND YOU MAY DESCRIBE AND COMPLETE THEIR WORK QUITE LITERALLY BY TAKING ANY VERSES OF CHAUCER IN HIS TENDER MOOD AND OBSERVING HOW HE INSISTS ON THE CLEARNESS AND BRIGHTNESS FIRST AND THEN ON THE ORDER",
"hyp": "Exquisite order and universal, with eternal life and light , this is the faith and effort of the schools of crystal , and you may describe and complete their work quite literally, by taking any verses of Chaucer in his tender mood, and observing how he insists on the clearness and brightness first, and then on the order.",
"ref_norm": "EXQUISITE ORDER AND UNIVERSAL WITH ETERNAL LIFE AND LIGHT THIS IS THE FAITH AND EFFORT OF THE SCHOOLS OF CRYSTAL AND YOU MAY DESCRIBE AND COMPLETE THEIR WORK QUITE LITERALLY BY TAKING ANY VERSES OF CHAUCER IN HIS TENDER MOOD AND OBSERVING HOW HE INSISTS ON THE CLEARNESS AND BRIGHTNESS FIRST AND THEN ON THE ORDER",
"hyp_norm": "EXQUISITE ORDER AND UNIVERSAL WITH ETERNAL LIFE AND LIGHT THIS IS THE FAITH AND EFFORT OF THE SCHOOLS OF CRYSTAL AND YOU MAY DESCRIBE AND COMPLETE THEIR WORK QUITE LITERALLY BY TAKING ANY VERSES OF CHAUCER IN HIS TENDER MOOD AND OBSERVING HOW HE INSISTS ON THE CLEARNESS AND BRIGHTNESS FIRST AND THEN ON THE ORDER",
"duration_s": 20.905,
"infer_time_s": 24.128,
"rtf": 1.1542,
"wer": 0.0
},
{
"id": "1188-133604-0035",
"ref": "THUS IN CHAUCER'S DREAM",
"hyp": "Thus, in Ch aucer's dream.",
"ref_norm": "THUS IN CHAUCERS DREAM",
"hyp_norm": "THUS IN CH AUCERS DREAM",
"duration_s": 2.925,
"infer_time_s": 3.442,
"rtf": 1.1767,
"wer": 0.5
},
{
"id": "1188-133604-0036",
"ref": "IN BOTH THESE HIGH MYTHICAL SUBJECTS THE SURROUNDING NATURE THOUGH SUFFERING IS STILL DIGNIFIED AND BEAUTIFUL",
"hyp": "In both these high mythical subjects , the surrounding nature , though suffering, is still dignified and beautiful.",
"ref_norm": "IN BOTH THESE HIGH MYTHICAL SUBJECTS THE SURROUNDING NATURE THOUGH SUFFERING IS STILL DIGNIFIED AND BEAUTIFUL",
"hyp_norm": "IN BOTH THESE HIGH MYTHICAL SUBJECTS THE SURROUNDING NATURE THOUGH SUFFERING IS STILL DIGNIFIED AND BEAUTIFUL",
"duration_s": 7.97,
"infer_time_s": 7.767,
"rtf": 0.9745,
"wer": 0.0
},
{
"id": "1188-133604-0037",
"ref": "EVERY LINE IN WHICH THE MASTER TRACES IT EVEN WHERE SEEMINGLY NEGLIGENT IS LOVELY AND SET DOWN WITH A MEDITATIVE CALMNESS WHICH MAKES THESE TWO ETCHINGS CAPABLE OF BEING PLACED BESIDE THE MOST TRANQUIL WORK OF HOLBEIN OR DUERER",
"hyp": "Every line in which the master traces it , even where seemingly negligent, is lovely and set down with a meditative calmness, which makes these two etchings capable of being placed beside the most tranquil work of Holbein, or Durer.",
"ref_norm": "EVERY LINE IN WHICH THE MASTER TRACES IT EVEN WHERE SEEMINGLY NEGLIGENT IS LOVELY AND SET DOWN WITH A MEDITATIVE CALMNESS WHICH MAKES THESE TWO ETCHINGS CAPABLE OF BEING PLACED BESIDE THE MOST TRANQUIL WORK OF HOLBEIN OR DUERER",
"hyp_norm": "EVERY LINE IN WHICH THE MASTER TRACES IT EVEN WHERE SEEMINGLY NEGLIGENT IS LOVELY AND SET DOWN WITH A MEDITATIVE CALMNESS WHICH MAKES THESE TWO ETCHINGS CAPABLE OF BEING PLACED BESIDE THE MOST TRANQUIL WORK OF HOLBEIN OR DURER",
"duration_s": 14.51,
"infer_time_s": 17.943,
"rtf": 1.2366,
"wer": 0.0256
},
{
"id": "1188-133604-0038",
"ref": "BUT NOW HERE IS A SUBJECT OF WHICH YOU WILL WONDER AT FIRST WHY TURNER DREW IT AT ALL",
"hyp": "But now here is a subject of which, you will wonder at first why Turner drew it at all.",
"ref_norm": "BUT NOW HERE IS A SUBJECT OF WHICH YOU WILL WONDER AT FIRST WHY TURNER DREW IT AT ALL",
"hyp_norm": "BUT NOW HERE IS A SUBJECT OF WHICH YOU WILL WONDER AT FIRST WHY TURNER DREW IT AT ALL",
"duration_s": 5.365,
"infer_time_s": 7.772,
"rtf": 1.4486,
"wer": 0.0
},
{
"id": "1188-133604-0039",
"ref": "IT HAS NO BEAUTY WHATSOEVER NO SPECIALTY OF PICTURESQUENESS AND ALL ITS LINES ARE CRAMPED AND POOR",
"hyp": "It has no beauty whatsoever . No specialty of pictures queness, and all its lines are cramped and poor.",
"ref_norm": "IT HAS NO BEAUTY WHATSOEVER NO SPECIALTY OF PICTURESQUENESS AND ALL ITS LINES ARE CRAMPED AND POOR",
"hyp_norm": "IT HAS NO BEAUTY WHATSOEVER NO SPECIALTY OF PICTURES QUENESS AND ALL ITS LINES ARE CRAMPED AND POOR",
"duration_s": 6.625,
"infer_time_s": 7.682,
"rtf": 1.1595,
"wer": 0.1176
},
{
"id": "1188-133604-0040",
"ref": "THE CRAMPNESS AND THE POVERTY ARE ALL INTENDED",
"hyp": "The crampedness and the poverty are all intended.",
"ref_norm": "THE CRAMPNESS AND THE POVERTY ARE ALL INTENDED",
"hyp_norm": "THE CRAMPEDNESS AND THE POVERTY ARE ALL INTENDED",
"duration_s": 3.23,
"infer_time_s": 3.772,
"rtf": 1.1678,
"wer": 0.125
},
{
"id": "1188-133604-0041",
"ref": "IT IS A GLEANER BRINGING DOWN HER ONE SHEAF OF CORN TO AN OLD WATERMILL ITSELF MOSSY AND RENT SCARCELY ABLE TO GET ITS STONES TO TURN",
"hyp": "It is a gleaner bringing down her one sheaf of corn to an old water mill, itself moss y and rent, scarcely able to get its stones to turn.",
"ref_norm": "IT IS A GLEANER BRINGING DOWN HER ONE SHEAF OF CORN TO AN OLD WATERMILL ITSELF MOSSY AND RENT SCARCELY ABLE TO GET ITS STONES TO TURN",
"hyp_norm": "IT IS A GLEANER BRINGING DOWN HER ONE SHEAF OF CORN TO AN OLD WATER MILL ITSELF MOSS Y AND RENT SCARCELY ABLE TO GET ITS STONES TO TURN",
"duration_s": 10.07,
"infer_time_s": 12.427,
"rtf": 1.2341,
"wer": 0.1481
},
{
"id": "1188-133604-0042",
"ref": "THE SCENE IS ABSOLUTELY ARCADIAN",
"hyp": "The scene is absolutely Arcadian.",
"ref_norm": "THE SCENE IS ABSOLUTELY ARCADIAN",
"hyp_norm": "THE SCENE IS ABSOLUTELY ARCADIAN",
"duration_s": 2.66,
"infer_time_s": 2.904,
"rtf": 1.0918,
"wer": 0.0
},
{
"id": "1188-133604-0043",
"ref": "SEE THAT YOUR LIVES BE IN NOTHING WORSE THAN A BOY'S CLIMBING FOR HIS ENTANGLED KITE",
"hyp": "See that your lives be in nothing worse than a boy's climbing for his entangled kite.",
"ref_norm": "SEE THAT YOUR LIVES BE IN NOTHING WORSE THAN A BOYS CLIMBING FOR HIS ENTANGLED KITE",
"hyp_norm": "SEE THAT YOUR LIVES BE IN NOTHING WORSE THAN A BOYS CLIMBING FOR HIS ENTANGLED KITE",
"duration_s": 4.885,
"infer_time_s": 6.375,
"rtf": 1.3051,
"wer": 0.0
},
{
"id": "1188-133604-0044",
"ref": "IT WILL BE WELL FOR YOU IF YOU JOIN NOT WITH THOSE WHO INSTEAD OF KITES FLY FALCONS WHO INSTEAD OF OBEYING THE LAST WORDS OF THE GREAT CLOUD SHEPHERD TO FEED HIS SHEEP LIVE THE LIVES HOW MUCH LESS THAN VANITY OF THE WAR WOLF AND THE GIER EAGLE",
"hyp": "It will be well for you , if you join not with those who, instead of kites, fly falcons, who, instead of obeying the last words of the great cloud shepherd , to feed his sheep, live the lives . How much less than vanity. Of the war wolf and the gear eagle.",
"ref_norm": "IT WILL BE WELL FOR YOU IF YOU JOIN NOT WITH THOSE WHO INSTEAD OF KITES FLY FALCONS WHO INSTEAD OF OBEYING THE LAST WORDS OF THE GREAT CLOUD SHEPHERD TO FEED HIS SHEEP LIVE THE LIVES HOW MUCH LESS THAN VANITY OF THE WAR WOLF AND THE GIER EAGLE",
"hyp_norm": "IT WILL BE WELL FOR YOU IF YOU JOIN NOT WITH THOSE WHO INSTEAD OF KITES FLY FALCONS WHO INSTEAD OF OBEYING THE LAST WORDS OF THE GREAT CLOUD SHEPHERD TO FEED HIS SHEEP LIVE THE LIVES HOW MUCH LESS THAN VANITY OF THE WAR WOLF AND THE GEAR EAGLE",
"duration_s": 18.545,
"infer_time_s": 22.001,
"rtf": 1.1863,
"wer": 0.02
},
{
"id": "121-121726-0000",
"ref": "ALSO A POPULAR CONTRIVANCE WHEREBY LOVE MAKING MAY BE SUSPENDED BUT NOT STOPPED DURING THE PICNIC SEASON",
"hyp": "Also, a popular contrivance whereby love-making may be suspended, but not stopped, during the picnic season.",
"ref_norm": "ALSO A POPULAR CONTRIVANCE WHEREBY LOVE MAKING MAY BE SUSPENDED BUT NOT STOPPED DURING THE PICNIC SEASON",
"hyp_norm": "ALSO A POPULAR CONTRIVANCE WHEREBY LOVEMAKING MAY BE SUSPENDED BUT NOT STOPPED DURING THE PICNIC SEASON",
"duration_s": 8.46,
"infer_time_s": 9.31,
"rtf": 1.1005,
"wer": 0.1176
},
{
"id": "121-121726-0001",
"ref": "HARANGUE THE TIRESOME PRODUCT OF A TIRELESS TONGUE",
"hyp": "Harang . The tiresome product of a tireless tongue.",
"ref_norm": "HARANGUE THE TIRESOME PRODUCT OF A TIRELESS TONGUE",
"hyp_norm": "HARANG THE TIRESOME PRODUCT OF A TIRELESS TONGUE",
"duration_s": 5.925,
"infer_time_s": 4.946,
"rtf": 0.8347,
"wer": 0.125
},
{
"id": "121-121726-0002",
"ref": "ANGOR PAIN PAINFUL TO HEAR",
"hyp": "Anger, pain, painful to hear.",
"ref_norm": "ANGOR PAIN PAINFUL TO HEAR",
"hyp_norm": "ANGER PAIN PAINFUL TO HEAR",
"duration_s": 4.41,
"infer_time_s": 4.114,
"rtf": 0.9328,
"wer": 0.2
},
{
"id": "121-121726-0003",
"ref": "HAY FEVER A HEART TROUBLE CAUSED BY FALLING IN LOVE WITH A GRASS WIDOW",
"hyp": "Hay fever , a heart trouble caused by falling in love with a grass widow.",
"ref_norm": "HAY FEVER A HEART TROUBLE CAUSED BY FALLING IN LOVE WITH A GRASS WIDOW",
"hyp_norm": "HAY FEVER A HEART TROUBLE CAUSED BY FALLING IN LOVE WITH A GRASS WIDOW",
"duration_s": 6.755,
"infer_time_s": 6.322,
"rtf": 0.9359,
"wer": 0.0
},
{
"id": "121-121726-0004",
"ref": "HEAVEN A GOOD PLACE TO BE RAISED TO",
"hyp": "Heaven , a good place to be raised to.",
"ref_norm": "HEAVEN A GOOD PLACE TO BE RAISED TO",
"hyp_norm": "HEAVEN A GOOD PLACE TO BE RAISED TO",
"duration_s": 4.02,
"infer_time_s": 4.372,
"rtf": 1.0875,
"wer": 0.0
},
{
"id": "121-121726-0005",
"ref": "HEDGE A FENCE",
"hyp": "Hedge, a fence.",
"ref_norm": "HEDGE A FENCE",
"hyp_norm": "HEDGE A FENCE",
"duration_s": 3.1,
"infer_time_s": 2.649,
"rtf": 0.8545,
"wer": 0.0
},
{
"id": "121-121726-0006",
"ref": "HEREDITY THE CAUSE OF ALL OUR FAULTS",
"hyp": "Heredity. The cause of all our faults.",
"ref_norm": "HEREDITY THE CAUSE OF ALL OUR FAULTS",
"hyp_norm": "HEREDITY THE CAUSE OF ALL OUR FAULTS",
"duration_s": 3.895,
"infer_time_s": 3.583,
"rtf": 0.92,
"wer": 0.0
},
{
"id": "121-121726-0007",
"ref": "HORSE SENSE A DEGREE OF WISDOM THAT KEEPS ONE FROM BETTING ON THE RACES",
"hyp": "Horse sense, a degree of wisdom that keeps one from betting on the races.",
"ref_norm": "HORSE SENSE A DEGREE OF WISDOM THAT KEEPS ONE FROM BETTING ON THE RACES",
"hyp_norm": "HORSE SENSE A DEGREE OF WISDOM THAT KEEPS ONE FROM BETTING ON THE RACES",
"duration_s": 6.73,
"infer_time_s": 6.71,
"rtf": 0.9971,
"wer": 0.0
},
{
"id": "121-121726-0008",
"ref": "HOSE MAN'S EXCUSE FOR WETTING THE WALK",
"hyp": "Hose. Man's excuse for wetting the walk.",
"ref_norm": "HOSE MANS EXCUSE FOR WETTING THE WALK",
"hyp_norm": "HOSE MANS EXCUSE FOR WETTING THE WALK",
"duration_s": 4.99,
"infer_time_s": 4.285,
"rtf": 0.8587,
"wer": 0.0
},
{
"id": "121-121726-0009",
"ref": "HOTEL A PLACE WHERE A GUEST OFTEN GIVES UP GOOD DOLLARS FOR POOR QUARTERS",
"hyp": "Hotel. A place where a guest often gives up good dollars for poor quarters.",
"ref_norm": "HOTEL A PLACE WHERE A GUEST OFTEN GIVES UP GOOD DOLLARS FOR POOR QUARTERS",
"hyp_norm": "HOTEL A PLACE WHERE A GUEST OFTEN GIVES UP GOOD DOLLARS FOR POOR QUARTERS",
"duration_s": 7.26,
"infer_time_s": 6.011,
"rtf": 0.8279,
"wer": 0.0
},
{
"id": "121-121726-0010",
"ref": "HOUSECLEANING A DOMESTIC UPHEAVAL THAT MAKES IT EASY FOR THE GOVERNMENT TO ENLIST ALL THE SOLDIERS IT NEEDS",
"hyp": "House cleaning , a domestic upheaval that makes it easy for the government to enlist all the soldiers it needs.",
"ref_norm": "HOUSECLEANING A DOMESTIC UPHEAVAL THAT MAKES IT EASY FOR THE GOVERNMENT TO ENLIST ALL THE SOLDIERS IT NEEDS",
"hyp_norm": "HOUSE CLEANING A DOMESTIC UPHEAVAL THAT MAKES IT EASY FOR THE GOVERNMENT TO ENLIST ALL THE SOLDIERS IT NEEDS",
"duration_s": 9.81,
"infer_time_s": 8.704,
"rtf": 0.8873,
"wer": 0.1111
},
{
"id": "121-121726-0011",
"ref": "HUSBAND THE NEXT THING TO A WIFE",
"hyp": "Husband. The next thing to a wife.",
"ref_norm": "HUSBAND THE NEXT THING TO A WIFE",
"hyp_norm": "HUSBAND THE NEXT THING TO A WIFE",
"duration_s": 4.035,
"infer_time_s": 3.846,
"rtf": 0.9532,
"wer": 0.0
},
{
"id": "121-121726-0012",
"ref": "HUSSY WOMAN AND BOND TIE",
"hyp": "Hussy woman and bond tie.",
"ref_norm": "HUSSY WOMAN AND BOND TIE",
"hyp_norm": "HUSSY WOMAN AND BOND TIE",
"duration_s": 4.045,
"infer_time_s": 3.557,
"rtf": 0.8793,
"wer": 0.0
},
{
"id": "121-121726-0013",
"ref": "TIED TO A WOMAN",
"hyp": "Tied to a woman.",
"ref_norm": "TIED TO A WOMAN",
"hyp_norm": "TIED TO A WOMAN",
"duration_s": 2.49,
"infer_time_s": 2.855,
"rtf": 1.1467,
"wer": 0.0
},
{
"id": "121-121726-0014",
"ref": "HYPOCRITE A HORSE DEALER",
"hyp": "Hypocrite. A horse dealer.",
"ref_norm": "HYPOCRITE A HORSE DEALER",
"hyp_norm": "HYPOCRITE A HORSE DEALER",
"duration_s": 3.165,
"infer_time_s": 3.158,
"rtf": 0.9978,
"wer": 0.0
},
{
"id": "121-123852-0000",
"ref": "THOSE PRETTY WRONGS THAT LIBERTY COMMITS WHEN I AM SOMETIME ABSENT FROM THY HEART THY BEAUTY AND THY YEARS FULL WELL BEFITS FOR STILL TEMPTATION FOLLOWS WHERE THOU ART",
"hyp": "Those pretty wrongs that liberty commits, when I am some time absent from thy heart, thy beauty and thy years full well be fits, for still temptation follows where thou art.",
"ref_norm": "THOSE PRETTY WRONGS THAT LIBERTY COMMITS WHEN I AM SOMETIME ABSENT FROM THY HEART THY BEAUTY AND THY YEARS FULL WELL BEFITS FOR STILL TEMPTATION FOLLOWS WHERE THOU ART",
"hyp_norm": "THOSE PRETTY WRONGS THAT LIBERTY COMMITS WHEN I AM SOME TIME ABSENT FROM THY HEART THY BEAUTY AND THY YEARS FULL WELL BE FITS FOR STILL TEMPTATION FOLLOWS WHERE THOU ART",
"duration_s": 17.695,
"infer_time_s": 14.989,
"rtf": 0.8471,
"wer": 0.1379
},
{
"id": "121-123852-0001",
"ref": "AY ME",
"hyp": "I me.",
"ref_norm": "AY ME",
"hyp_norm": "I ME",
"duration_s": 1.87,
"infer_time_s": 1.41,
"rtf": 0.7541,
"wer": 0.5
},
{
"id": "121-123852-0002",
"ref": "NO MATTER THEN ALTHOUGH MY FOOT DID STAND UPON THE FARTHEST EARTH REMOV'D FROM THEE FOR NIMBLE THOUGHT CAN JUMP BOTH SEA AND LAND AS SOON AS THINK THE PLACE WHERE HE WOULD BE BUT AH",
"hyp": "No matter, then , although my foot did stand upon the farthest earth , removed from thee , for nimble thought can jump both sea and land, as soon as think the place where he would be, but ah.",
"ref_norm": "NO MATTER THEN ALTHOUGH MY FOOT DID STAND UPON THE FARTHEST EARTH REMOVD FROM THEE FOR NIMBLE THOUGHT CAN JUMP BOTH SEA AND LAND AS SOON AS THINK THE PLACE WHERE HE WOULD BE BUT AH",
"hyp_norm": "NO MATTER THEN ALTHOUGH MY FOOT DID STAND UPON THE FARTHEST EARTH REMOVED FROM THEE FOR NIMBLE THOUGHT CAN JUMP BOTH SEA AND LAND AS SOON AS THINK THE PLACE WHERE HE WOULD BE BUT AH",
"duration_s": 17.285,
"infer_time_s": 17.78,
"rtf": 1.0286,
"wer": 0.0278
},
{
"id": "121-123852-0003",
"ref": "THOUGHT KILLS ME THAT I AM NOT THOUGHT TO LEAP LARGE LENGTHS OF MILES WHEN THOU ART GONE BUT THAT SO MUCH OF EARTH AND WATER WROUGHT I MUST ATTEND TIME'S LEISURE WITH MY MOAN RECEIVING NOUGHT BY ELEMENTS SO SLOW BUT HEAVY TEARS BADGES OF EITHER'S WOE",
"hyp": "Thought kills me that I am not thought , to leap large lengths of miles when thou art gone. But that so much of earth and water rot, I must attend, time's leisure with my moan , receiving not by elements so slow, but heavy tears, badges of either's woe.",
"ref_norm": "THOUGHT KILLS ME THAT I AM NOT THOUGHT TO LEAP LARGE LENGTHS OF MILES WHEN THOU ART GONE BUT THAT SO MUCH OF EARTH AND WATER WROUGHT I MUST ATTEND TIMES LEISURE WITH MY MOAN RECEIVING NOUGHT BY ELEMENTS SO SLOW BUT HEAVY TEARS BADGES OF EITHERS WOE",
"hyp_norm": "THOUGHT KILLS ME THAT I AM NOT THOUGHT TO LEAP LARGE LENGTHS OF MILES WHEN THOU ART GONE BUT THAT SO MUCH OF EARTH AND WATER ROT I MUST ATTEND TIMES LEISURE WITH MY MOAN RECEIVING NOT BY ELEMENTS SO SLOW BUT HEAVY TEARS BADGES OF EITHERS WOE",
"duration_s": 23.505,
"infer_time_s": 23.409,
"rtf": 0.9959,
"wer": 0.0417
},
{
"id": "121-123852-0004",
"ref": "MY HEART DOTH PLEAD THAT THOU IN HIM DOST LIE A CLOSET NEVER PIERC'D WITH CRYSTAL EYES BUT THE DEFENDANT DOTH THAT PLEA DENY AND SAYS IN HIM THY FAIR APPEARANCE LIES",
"hyp": "My heart doth plead that thou in him dost lie , a closet never pierced with crystal eyes, but the defendant doth that plea deny, and says in him thy fair appearance lies.",
"ref_norm": "MY HEART DOTH PLEAD THAT THOU IN HIM DOST LIE A CLOSET NEVER PIERCD WITH CRYSTAL EYES BUT THE DEFENDANT DOTH THAT PLEA DENY AND SAYS IN HIM THY FAIR APPEARANCE LIES",
"hyp_norm": "MY HEART DOTH PLEAD THAT THOU IN HIM DOST LIE A CLOSET NEVER PIERCED WITH CRYSTAL EYES BUT THE DEFENDANT DOTH THAT PLEA DENY AND SAYS IN HIM THY FAIR APPEARANCE LIES",
"duration_s": 16.29,
"infer_time_s": 15.27,
"rtf": 0.9374,
"wer": 0.0312
},
{
"id": "121-123859-0000",
"ref": "YOU ARE MY ALL THE WORLD AND I MUST STRIVE TO KNOW MY SHAMES AND PRAISES FROM YOUR TONGUE NONE ELSE TO ME NOR I TO NONE ALIVE THAT MY STEEL'D SENSE OR CHANGES RIGHT OR WRONG",
"hyp": "You are my all the world , and I must strive to know my shames and praises from your tongue. None else to me , nor I to none alive, that my steel'd sense or changes right or wrong.",
"ref_norm": "YOU ARE MY ALL THE WORLD AND I MUST STRIVE TO KNOW MY SHAMES AND PRAISES FROM YOUR TONGUE NONE ELSE TO ME NOR I TO NONE ALIVE THAT MY STEELD SENSE OR CHANGES RIGHT OR WRONG",
"hyp_norm": "YOU ARE MY ALL THE WORLD AND I MUST STRIVE TO KNOW MY SHAMES AND PRAISES FROM YOUR TONGUE NONE ELSE TO ME NOR I TO NONE ALIVE THAT MY STEELD SENSE OR CHANGES RIGHT OR WRONG",
"duration_s": 17.39,
"infer_time_s": 16.724,
"rtf": 0.9617,
"wer": 0.0
},
{
"id": "121-123859-0001",
"ref": "O TIS THE FIRST TIS FLATTERY IN MY SEEING AND MY GREAT MIND MOST KINGLY DRINKS IT UP MINE EYE WELL KNOWS WHAT WITH HIS GUST IS GREEING AND TO HIS PALATE DOTH PREPARE THE CUP IF IT BE POISON'D TIS THE LESSER SIN THAT MINE EYE LOVES IT AND DOTH FIRST BEGIN",
"hyp": "Oh, 'tis the first; 'tis flattery in my seeing, and my great mind most kingly drinks it up. Mine eye well knows what with his gust is greying, and to his palate doth prepare the cup . If it be poisoned, 'tis the lesser sin , that mine eye loves it, and doth first begin.",
"ref_norm": "O TIS THE FIRST TIS FLATTERY IN MY SEEING AND MY GREAT MIND MOST KINGLY DRINKS IT UP MINE EYE WELL KNOWS WHAT WITH HIS GUST IS GREEING AND TO HIS PALATE DOTH PREPARE THE CUP IF IT BE POISOND TIS THE LESSER SIN THAT MINE EYE LOVES IT AND DOTH FIRST BEGIN",
"hyp_norm": "OH TIS THE FIRST TIS FLATTERY IN MY SEEING AND MY GREAT MIND MOST KINGLY DRINKS IT UP MINE EYE WELL KNOWS WHAT WITH HIS GUST IS GREYING AND TO HIS PALATE DOTH PREPARE THE CUP IF IT BE POISONED TIS THE LESSER SIN THAT MINE EYE LOVES IT AND DOTH FIRST BEGIN",
"duration_s": 25.395,
"infer_time_s": 26.303,
"rtf": 1.0358,
"wer": 0.0566
},
{
"id": "121-123859-0002",
"ref": "BUT RECKONING TIME WHOSE MILLION'D ACCIDENTS CREEP IN TWIXT VOWS AND CHANGE DECREES OF KINGS TAN SACRED BEAUTY BLUNT THE SHARP'ST INTENTS DIVERT STRONG MINDS TO THE COURSE OF ALTERING THINGS ALAS WHY FEARING OF TIME'S TYRANNY MIGHT I NOT THEN SAY NOW I LOVE YOU BEST WHEN I WAS CERTAIN O'ER INCERTAINTY CROWNING THE PRESENT DOUBTING OF THE REST",
"hyp": "But reckoning time , whose millioned accidents creep in twixt vows , and change decrees of kings, tans sacred beauty , blunt the sharpest intents, diverts strong minds to the course of altering things. Al as, why fearing of time's tyranny, might I not then say, now I love you best, when I was certain or in certainty , crowning the present, doubting of the rest.",
"ref_norm": "BUT RECKONING TIME WHOSE MILLIOND ACCIDENTS CREEP IN TWIXT VOWS AND CHANGE DECREES OF KINGS TAN SACRED BEAUTY BLUNT THE SHARPST INTENTS DIVERT STRONG MINDS TO THE COURSE OF ALTERING THINGS ALAS WHY FEARING OF TIMES TYRANNY MIGHT I NOT THEN SAY NOW I LOVE YOU BEST WHEN I WAS CERTAIN OER INCERTAINTY CROWNING THE PRESENT DOUBTING OF THE REST",
"hyp_norm": "BUT RECKONING TIME WHOSE MILLIONED ACCIDENTS CREEP IN TWIXT VOWS AND CHANGE DECREES OF KINGS TANS SACRED BEAUTY BLUNT THE SHARPEST INTENTS DIVERTS STRONG MINDS TO THE COURSE OF ALTERING THINGS AL AS WHY FEARING OF TIMES TYRANNY MIGHT I NOT THEN SAY NOW I LOVE YOU BEST WHEN I WAS CERTAIN OR IN CERTAINTY CROWNING THE PRESENT DOUBTING OF THE REST",
"duration_s": 30.04,
"infer_time_s": 31.102,
"rtf": 1.0353,
"wer": 0.15
},
{
"id": "121-123859-0003",
"ref": "LOVE IS A BABE THEN MIGHT I NOT SAY SO TO GIVE FULL GROWTH TO THAT WHICH STILL DOTH GROW",
"hyp": "Love is a babe. Then might I not say so? To give full growth to that which still doth grow.",
"ref_norm": "LOVE IS A BABE THEN MIGHT I NOT SAY SO TO GIVE FULL GROWTH TO THAT WHICH STILL DOTH GROW",
"hyp_norm": "LOVE IS A BABE THEN MIGHT I NOT SAY SO TO GIVE FULL GROWTH TO THAT WHICH STILL DOTH GROW",
"duration_s": 10.825,
"infer_time_s": 9.97,
"rtf": 0.921,
"wer": 0.0
},
{
"id": "121-123859-0004",
"ref": "SO I RETURN REBUK'D TO MY CONTENT AND GAIN BY ILL THRICE MORE THAN I HAVE SPENT",
"hyp": "So I return , rebuked, to my content, and gain by ill thrice more than I have spent.",
"ref_norm": "SO I RETURN REBUKD TO MY CONTENT AND GAIN BY ILL THRICE MORE THAN I HAVE SPENT",
"hyp_norm": "SO I RETURN REBUKED TO MY CONTENT AND GAIN BY ILL THRICE MORE THAN I HAVE SPENT",
"duration_s": 9.505,
"infer_time_s": 9.365,
"rtf": 0.9853,
"wer": 0.0588
},
{
"id": "121-127105-0000",
"ref": "IT WAS THIS OBSERVATION THAT DREW FROM DOUGLAS NOT IMMEDIATELY BUT LATER IN THE EVENING A REPLY THAT HAD THE INTERESTING CONSEQUENCE TO WHICH I CALL ATTENTION",
"hyp": "It was this observation that drew from Douglas , not immediately, but later in the evening, a reply that had the interesting consequence to which I call attention.",
"ref_norm": "IT WAS THIS OBSERVATION THAT DREW FROM DOUGLAS NOT IMMEDIATELY BUT LATER IN THE EVENING A REPLY THAT HAD THE INTERESTING CONSEQUENCE TO WHICH I CALL ATTENTION",
"hyp_norm": "IT WAS THIS OBSERVATION THAT DREW FROM DOUGLAS NOT IMMEDIATELY BUT LATER IN THE EVENING A REPLY THAT HAD THE INTERESTING CONSEQUENCE TO WHICH I CALL ATTENTION",
"duration_s": 9.875,
"infer_time_s": 10.733,
"rtf": 1.0869,
"wer": 0.0
},
{
"id": "121-127105-0001",
"ref": "SOMEONE ELSE TOLD A STORY NOT PARTICULARLY EFFECTIVE WHICH I SAW HE WAS NOT FOLLOWING",
"hyp": "Someone else told a story, not particularly effective , which I saw he was not following.",
"ref_norm": "SOMEONE ELSE TOLD A STORY NOT PARTICULARLY EFFECTIVE WHICH I SAW HE WAS NOT FOLLOWING",
"hyp_norm": "SOMEONE ELSE TOLD A STORY NOT PARTICULARLY EFFECTIVE WHICH I SAW HE WAS NOT FOLLOWING",
"duration_s": 5.025,
"infer_time_s": 6.219,
"rtf": 1.2377,
"wer": 0.0
},
{
"id": "121-127105-0002",
"ref": "CRIED ONE OF THE WOMEN HE TOOK NO NOTICE OF HER HE LOOKED AT ME BUT AS IF INSTEAD OF ME HE SAW WHAT HE SPOKE OF",
"hyp": "Cried one of the women. He took no notice of her. He looked at me, but as if , instead of me, he saw what he spoke of.",
"ref_norm": "CRIED ONE OF THE WOMEN HE TOOK NO NOTICE OF HER HE LOOKED AT ME BUT AS IF INSTEAD OF ME HE SAW WHAT HE SPOKE OF",
"hyp_norm": "CRIED ONE OF THE WOMEN HE TOOK NO NOTICE OF HER HE LOOKED AT ME BUT AS IF INSTEAD OF ME HE SAW WHAT HE SPOKE OF",
"duration_s": 7.495,
"infer_time_s": 10.514,
"rtf": 1.4028,
"wer": 0.0
},
{
"id": "121-127105-0003",
"ref": "THERE WAS A UNANIMOUS GROAN AT THIS AND MUCH REPROACH AFTER WHICH IN HIS PREOCCUPIED WAY HE EXPLAINED",
"hyp": "There was a unanimous groan at this, and much reproach. After which, in his preoccupied way, he explained.",
"ref_norm": "THERE WAS A UNANIMOUS GROAN AT THIS AND MUCH REPROACH AFTER WHICH IN HIS PREOCCUPIED WAY HE EXPLAINED",
"hyp_norm": "THERE WAS A UNANIMOUS GROAN AT THIS AND MUCH REPROACH AFTER WHICH IN HIS PREOCCUPIED WAY HE EXPLAINED",
"duration_s": 7.725,
"infer_time_s": 8.671,
"rtf": 1.1225,
"wer": 0.0
},
{
"id": "121-127105-0004",
"ref": "THE STORY'S WRITTEN",
"hyp": "The stories written.",
"ref_norm": "THE STORYS WRITTEN",
"hyp_norm": "THE STORIES WRITTEN",
"duration_s": 2.11,
"infer_time_s": 2.027,
"rtf": 0.9605,
"wer": 0.3333
},
{
"id": "121-127105-0005",
"ref": "I COULD WRITE TO MY MAN AND ENCLOSE THE KEY HE COULD SEND DOWN THE PACKET AS HE FINDS IT",
"hyp": "I could write to my man and enclose the key . He could send down the packet as he finds it.",
"ref_norm": "I COULD WRITE TO MY MAN AND ENCLOSE THE KEY HE COULD SEND DOWN THE PACKET AS HE FINDS IT",
"hyp_norm": "I COULD WRITE TO MY MAN AND ENCLOSE THE KEY HE COULD SEND DOWN THE PACKET AS HE FINDS IT",
"duration_s": 5.82,
"infer_time_s": 7.344,
"rtf": 1.2619,
"wer": 0.0
},
{
"id": "121-127105-0006",
"ref": "THE OTHERS RESENTED POSTPONEMENT BUT IT WAS JUST HIS SCRUPLES THAT CHARMED ME",
"hyp": "The others resented postpon ement, but it was just his scruples that charmed me.",
"ref_norm": "THE OTHERS RESENTED POSTPONEMENT BUT IT WAS JUST HIS SCRUPLES THAT CHARMED ME",
"hyp_norm": "THE OTHERS RESENTED POSTPON EMENT BUT IT WAS JUST HIS SCRUPLES THAT CHARMED ME",
"duration_s": 4.725,
"infer_time_s": 6.501,
"rtf": 1.3759,
"wer": 0.1538
},
{
"id": "121-127105-0007",
"ref": "TO THIS HIS ANSWER WAS PROMPT OH THANK GOD NO AND IS THE RECORD YOURS",
"hyp": "To this, his answer was prompt. Oh, thank God, no! And is the record yours?",
"ref_norm": "TO THIS HIS ANSWER WAS PROMPT OH THANK GOD NO AND IS THE RECORD YOURS",
"hyp_norm": "TO THIS HIS ANSWER WAS PROMPT OH THANK GOD NO AND IS THE RECORD YOURS",
"duration_s": 5.79,
"infer_time_s": 6.695,
"rtf": 1.1563,
"wer": 0.0
},
{
"id": "121-127105-0008",
"ref": "HE HUNG FIRE AGAIN A WOMAN'S",
"hyp": "He hung fire again . A woman's.",
"ref_norm": "HE HUNG FIRE AGAIN A WOMANS",
"hyp_norm": "HE HUNG FIRE AGAIN A WOMANS",
"duration_s": 2.76,
"infer_time_s": 3.451,
"rtf": 1.2503,
"wer": 0.0
},
{
"id": "121-127105-0009",
"ref": "SHE HAS BEEN DEAD THESE TWENTY YEARS",
"hyp": "She has been dead these twenty years.",
"ref_norm": "SHE HAS BEEN DEAD THESE TWENTY YEARS",
"hyp_norm": "SHE HAS BEEN DEAD THESE TWENTY YEARS",
"duration_s": 2.29,
"infer_time_s": 3.116,
"rtf": 1.3607,
"wer": 0.0
},
{
"id": "121-127105-0010",
"ref": "SHE SENT ME THE PAGES IN QUESTION BEFORE SHE DIED",
"hyp": "She sent me the pages in question before she died.",
"ref_norm": "SHE SENT ME THE PAGES IN QUESTION BEFORE SHE DIED",
"hyp_norm": "SHE SENT ME THE PAGES IN QUESTION BEFORE SHE DIED",
"duration_s": 2.85,
"infer_time_s": 3.707,
"rtf": 1.3006,
"wer": 0.0
},
{
"id": "121-127105-0011",
"ref": "SHE WAS THE MOST AGREEABLE WOMAN I'VE EVER KNOWN IN HER POSITION SHE WOULD HAVE BEEN WORTHY OF ANY WHATEVER",
"hyp": "She was the most agree able woman I've ever known . In her position, she would have been worthy of any, whatever.",
"ref_norm": "SHE WAS THE MOST AGREEABLE WOMAN IVE EVER KNOWN IN HER POSITION SHE WOULD HAVE BEEN WORTHY OF ANY WHATEVER",
"hyp_norm": "SHE WAS THE MOST AGREE ABLE WOMAN IVE EVER KNOWN IN HER POSITION SHE WOULD HAVE BEEN WORTHY OF ANY WHATEVER",
"duration_s": 5.78,
"infer_time_s": 8.014,
"rtf": 1.3866,
"wer": 0.1
},
{
"id": "121-127105-0012",
"ref": "IT WASN'T SIMPLY THAT SHE SAID SO BUT THAT I KNEW SHE HADN'T I WAS SURE I COULD SEE",
"hyp": "It wasn't simply that she said so, but that I knew she hadn 't. I was sure. I could see.",
"ref_norm": "IT WASNT SIMPLY THAT SHE SAID SO BUT THAT I KNEW SHE HADNT I WAS SURE I COULD SEE",
"hyp_norm": "IT WASNT SIMPLY THAT SHE SAID SO BUT THAT I KNEW SHE HADN T I WAS SURE I COULD SEE",
"duration_s": 4.83,
"infer_time_s": 7.513,
"rtf": 1.5554,
"wer": 0.1053
},
{
"id": "121-127105-0013",
"ref": "YOU'LL EASILY JUDGE WHY WHEN YOU HEAR BECAUSE THE THING HAD BEEN SUCH A SCARE HE CONTINUED TO FIX ME",
"hyp": "You'll easily judge why when you hear, because the thing had been such a scare. He continued to fix me.",
"ref_norm": "YOULL EASILY JUDGE WHY WHEN YOU HEAR BECAUSE THE THING HAD BEEN SUCH A SCARE HE CONTINUED TO FIX ME",
"hyp_norm": "YOULL EASILY JUDGE WHY WHEN YOU HEAR BECAUSE THE THING HAD BEEN SUCH A SCARE HE CONTINUED TO FIX ME",
"duration_s": 5.895,
"infer_time_s": 7.579,
"rtf": 1.2857,
"wer": 0.0
},
{
"id": "121-127105-0014",
"ref": "YOU ARE ACUTE",
"hyp": "You are acute.",
"ref_norm": "YOU ARE ACUTE",
"hyp_norm": "YOU ARE ACUTE",
"duration_s": 2.255,
"infer_time_s": 2.08,
"rtf": 0.9225,
"wer": 0.0
},
{
"id": "121-127105-0015",
"ref": "HE QUITTED THE FIRE AND DROPPED BACK INTO HIS CHAIR",
"hyp": "He quitted the fire and dropped back into his chair.",
"ref_norm": "HE QUITTED THE FIRE AND DROPPED BACK INTO HIS CHAIR",
"hyp_norm": "HE QUITTED THE FIRE AND DROPPED BACK INTO HIS CHAIR",
"duration_s": 2.96,
"infer_time_s": 4.086,
"rtf": 1.3805,
"wer": 0.0
},
{
"id": "121-127105-0016",
"ref": "PROBABLY NOT TILL THE SECOND POST",
"hyp": "Probably not till the second post.",
"ref_norm": "PROBABLY NOT TILL THE SECOND POST",
"hyp_norm": "PROBABLY NOT TILL THE SECOND POST",
"duration_s": 2.03,
"infer_time_s": 2.785,
"rtf": 1.372,
"wer": 0.0
},
{
"id": "121-127105-0017",
"ref": "IT WAS ALMOST THE TONE OF HOPE EVERYBODY WILL STAY",
"hyp": "It was almost the tone of hope . Everybody will stay.",
"ref_norm": "IT WAS ALMOST THE TONE OF HOPE EVERYBODY WILL STAY",
"hyp_norm": "IT WAS ALMOST THE TONE OF HOPE EVERYBODY WILL STAY",
"duration_s": 2.695,
"infer_time_s": 4.058,
"rtf": 1.5057,
"wer": 0.0
},
{
"id": "121-127105-0018",
"ref": "CRIED THE LADIES WHOSE DEPARTURE HAD BEEN FIXED",
"hyp": "Cried the ladies whose departure had been fixed.",
"ref_norm": "CRIED THE LADIES WHOSE DEPARTURE HAD BEEN FIXED",
"hyp_norm": "CRIED THE LADIES WHOSE DEPARTURE HAD BEEN FIXED",
"duration_s": 2.77,
"infer_time_s": 3.509,
"rtf": 1.2669,
"wer": 0.0
},
{
"id": "121-127105-0019",
"ref": "MISSUS GRIFFIN HOWEVER EXPRESSED THE NEED FOR A LITTLE MORE LIGHT",
"hyp": "Mrs. Griffin, however, expressed the need for a little more light.",
"ref_norm": "MISSUS GRIFFIN HOWEVER EXPRESSED THE NEED FOR A LITTLE MORE LIGHT",
"hyp_norm": "MRS GRIFFIN HOWEVER EXPRESSED THE NEED FOR A LITTLE MORE LIGHT",
"duration_s": 3.525,
"infer_time_s": 4.556,
"rtf": 1.2924,
"wer": 0.0909
},
{
"id": "121-127105-0020",
"ref": "WHO WAS IT SHE WAS IN LOVE WITH THE STORY WILL TELL I TOOK UPON MYSELF TO REPLY OH I CAN'T WAIT FOR THE STORY THE STORY WON'T TELL SAID DOUGLAS NOT IN ANY LITERAL VULGAR WAY MORE'S THE PITY THEN",
"hyp": "Who was it? She was in love with the story. Will tell. I took upon myself to reply. Oh, I can't wait for the story. The story won't tell. Said Douglas. Not in any literal, vulgar way. What was the pity then?",
"ref_norm": "WHO WAS IT SHE WAS IN LOVE WITH THE STORY WILL TELL I TOOK UPON MYSELF TO REPLY OH I CANT WAIT FOR THE STORY THE STORY WONT TELL SAID DOUGLAS NOT IN ANY LITERAL VULGAR WAY MORES THE PITY THEN",
"hyp_norm": "WHO WAS IT SHE WAS IN LOVE WITH THE STORY WILL TELL I TOOK UPON MYSELF TO REPLY OH I CANT WAIT FOR THE STORY THE STORY WONT TELL SAID DOUGLAS NOT IN ANY LITERAL VULGAR WAY WHAT WAS THE PITY THEN",
"duration_s": 14.355,
"infer_time_s": 19.835,
"rtf": 1.3818,
"wer": 0.0488
},
{
"id": "121-127105-0021",
"ref": "WON'T YOU TELL DOUGLAS",
"hyp": "Won't you tell Douglas?",
"ref_norm": "WONT YOU TELL DOUGLAS",
"hyp_norm": "WONT YOU TELL DOUGLAS",
"duration_s": 2.0,
"infer_time_s": 1.977,
"rtf": 0.9886,
"wer": 0.0
},
{
"id": "121-127105-0022",
"ref": "WELL IF I DON'T KNOW WHO SHE WAS IN LOVE WITH I KNOW WHO HE WAS",
"hyp": "Well, if I don't know who she was in love with , I know who he was.",
"ref_norm": "WELL IF I DONT KNOW WHO SHE WAS IN LOVE WITH I KNOW WHO HE WAS",
"hyp_norm": "WELL IF I DONT KNOW WHO SHE WAS IN LOVE WITH I KNOW WHO HE WAS",
"duration_s": 5.075,
"infer_time_s": 6.893,
"rtf": 1.3581,
"wer": 0.0
},
{
"id": "121-127105-0023",
"ref": "LET ME SAY HERE DISTINCTLY TO HAVE DONE WITH IT THAT THIS NARRATIVE FROM AN EXACT TRANSCRIPT OF MY OWN MADE MUCH LATER IS WHAT I SHALL PRESENTLY GIVE",
"hyp": "Let me say here distinctly to have done with it that this narrative , from an exact transcript of my own made much later, is what I shall presently give.",
"ref_norm": "LET ME SAY HERE DISTINCTLY TO HAVE DONE WITH IT THAT THIS NARRATIVE FROM AN EXACT TRANSCRIPT OF MY OWN MADE MUCH LATER IS WHAT I SHALL PRESENTLY GIVE",
"hyp_norm": "LET ME SAY HERE DISTINCTLY TO HAVE DONE WITH IT THAT THIS NARRATIVE FROM AN EXACT TRANSCRIPT OF MY OWN MADE MUCH LATER IS WHAT I SHALL PRESENTLY GIVE",
"duration_s": 10.91,
"infer_time_s": 12.804,
"rtf": 1.1736,
"wer": 0.0
},
{
"id": "121-127105-0024",
"ref": "POOR DOUGLAS BEFORE HIS DEATH WHEN IT WAS IN SIGHT COMMITTED TO ME THE MANUSCRIPT THAT REACHED HIM ON THE THIRD OF THESE DAYS AND THAT ON THE SAME SPOT WITH IMMENSE EFFECT HE BEGAN TO READ TO OUR HUSHED LITTLE CIRCLE ON THE NIGHT OF THE FOURTH",
"hyp": "Poor Douglas, before his death, when it was in sight, committed to me the manuscript that reached him on the third of these days, and that , on the same spot, with immense effect, he began to read to our hushed little circle on the night of the fourth.",
"ref_norm": "POOR DOUGLAS BEFORE HIS DEATH WHEN IT WAS IN SIGHT COMMITTED TO ME THE MANUSCRIPT THAT REACHED HIM ON THE THIRD OF THESE DAYS AND THAT ON THE SAME SPOT WITH IMMENSE EFFECT HE BEGAN TO READ TO OUR HUSHED LITTLE CIRCLE ON THE NIGHT OF THE FOURTH",
"hyp_norm": "POOR DOUGLAS BEFORE HIS DEATH WHEN IT WAS IN SIGHT COMMITTED TO ME THE MANUSCRIPT THAT REACHED HIM ON THE THIRD OF THESE DAYS AND THAT ON THE SAME SPOT WITH IMMENSE EFFECT HE BEGAN TO READ TO OUR HUSHED LITTLE CIRCLE ON THE NIGHT OF THE FOURTH",
"duration_s": 14.45,
"infer_time_s": 19.584,
"rtf": 1.3553,
"wer": 0.0
},
{
"id": "121-127105-0025",
"ref": "THE DEPARTING LADIES WHO HAD SAID THEY WOULD STAY DIDN'T OF COURSE THANK HEAVEN STAY THEY DEPARTED IN CONSEQUENCE OF ARRANGEMENTS MADE IN A RAGE OF CURIOSITY AS THEY PROFESSED PRODUCED BY THE TOUCHES WITH WHICH HE HAD ALREADY WORKED US UP",
"hyp": "The departing ladies , who had said they would stay, didn't, of course. Thank heaven, stay. They departed in consequence of arrangements made , in a rage of curiosity, as they professed , produced by the touches with which he had already worked us up.",
"ref_norm": "THE DEPARTING LADIES WHO HAD SAID THEY WOULD STAY DIDNT OF COURSE THANK HEAVEN STAY THEY DEPARTED IN CONSEQUENCE OF ARRANGEMENTS MADE IN A RAGE OF CURIOSITY AS THEY PROFESSED PRODUCED BY THE TOUCHES WITH WHICH HE HAD ALREADY WORKED US UP",
"hyp_norm": "THE DEPARTING LADIES WHO HAD SAID THEY WOULD STAY DIDNT OF COURSE THANK HEAVEN STAY THEY DEPARTED IN CONSEQUENCE OF ARRANGEMENTS MADE IN A RAGE OF CURIOSITY AS THEY PROFESSED PRODUCED BY THE TOUCHES WITH WHICH HE HAD ALREADY WORKED US UP",
"duration_s": 16.065,
"infer_time_s": 19.836,
"rtf": 1.2347,
"wer": 0.0
},
{
"id": "121-127105-0026",
"ref": "THE FIRST OF THESE TOUCHES CONVEYED THAT THE WRITTEN STATEMENT TOOK UP THE TALE AT A POINT AFTER IT HAD IN A MANNER BEGUN",
"hyp": "The first of these touches conveyed that the written statement took up the tale at a point after it had, in a manner, begun.",
"ref_norm": "THE FIRST OF THESE TOUCHES CONVEYED THAT THE WRITTEN STATEMENT TOOK UP THE TALE AT A POINT AFTER IT HAD IN A MANNER BEGUN",
"hyp_norm": "THE FIRST OF THESE TOUCHES CONVEYED THAT THE WRITTEN STATEMENT TOOK UP THE TALE AT A POINT AFTER IT HAD IN A MANNER BEGUN",
"duration_s": 7.53,
"infer_time_s": 9.566,
"rtf": 1.2703,
"wer": 0.0
},
{
"id": "121-127105-0027",
"ref": "HE HAD FOR HIS OWN TOWN RESIDENCE A BIG HOUSE FILLED WITH THE SPOILS OF TRAVEL AND THE TROPHIES OF THE CHASE BUT IT WAS TO HIS COUNTRY HOME AN OLD FAMILY PLACE IN ESSEX THAT HE WISHED HER IMMEDIATELY TO PROCEED",
"hyp": "He had for his own town residence a big house filled with the spoils of travel, and the trophies of the chase. But it was to his country home , an old family place in Essex, that he wished her immediately to proceed.",
"ref_norm": "HE HAD FOR HIS OWN TOWN RESIDENCE A BIG HOUSE FILLED WITH THE SPOILS OF TRAVEL AND THE TROPHIES OF THE CHASE BUT IT WAS TO HIS COUNTRY HOME AN OLD FAMILY PLACE IN ESSEX THAT HE WISHED HER IMMEDIATELY TO PROCEED",
"hyp_norm": "HE HAD FOR HIS OWN TOWN RESIDENCE A BIG HOUSE FILLED WITH THE SPOILS OF TRAVEL AND THE TROPHIES OF THE CHASE BUT IT WAS TO HIS COUNTRY HOME AN OLD FAMILY PLACE IN ESSEX THAT HE WISHED HER IMMEDIATELY TO PROCEED",
"duration_s": 13.87,
"infer_time_s": 18.247,
"rtf": 1.3156,
"wer": 0.0
},
{
"id": "121-127105-0028",
"ref": "THE AWKWARD THING WAS THAT THEY HAD PRACTICALLY NO OTHER RELATIONS AND THAT HIS OWN AFFAIRS TOOK UP ALL HIS TIME",
"hyp": "The awkward thing was that they had practically no other relations, and that his own affairs took up all his time.",
"ref_norm": "THE AWKWARD THING WAS THAT THEY HAD PRACTICALLY NO OTHER RELATIONS AND THAT HIS OWN AFFAIRS TOOK UP ALL HIS TIME",
"hyp_norm": "THE AWKWARD THING WAS THAT THEY HAD PRACTICALLY NO OTHER RELATIONS AND THAT HIS OWN AFFAIRS TOOK UP ALL HIS TIME",
"duration_s": 6.75,
"infer_time_s": 8.071,
"rtf": 1.1958,
"wer": 0.0
},
{
"id": "121-127105-0029",
"ref": "THERE WERE PLENTY OF PEOPLE TO HELP BUT OF COURSE THE YOUNG LADY WHO SHOULD GO DOWN AS GOVERNESS WOULD BE IN SUPREME AUTHORITY",
"hyp": "There were plenty of people to help, but of course the young lady who should go down as governess would be in supreme authority.",
"ref_norm": "THERE WERE PLENTY OF PEOPLE TO HELP BUT OF COURSE THE YOUNG LADY WHO SHOULD GO DOWN AS GOVERNESS WOULD BE IN SUPREME AUTHORITY",
"hyp_norm": "THERE WERE PLENTY OF PEOPLE TO HELP BUT OF COURSE THE YOUNG LADY WHO SHOULD GO DOWN AS GOVERNESS WOULD BE IN SUPREME AUTHORITY",
"duration_s": 7.31,
"infer_time_s": 8.865,
"rtf": 1.2127,
"wer": 0.0
},
{
"id": "121-127105-0030",
"ref": "I DON'T ANTICIPATE",
"hyp": "I don't anticipate.",
"ref_norm": "I DONT ANTICIPATE",
"hyp_norm": "I DONT ANTICIPATE",
"duration_s": 2.175,
"infer_time_s": 2.399,
"rtf": 1.1032,
"wer": 0.0
},
{
"id": "121-127105-0031",
"ref": "SHE WAS YOUNG UNTRIED NERVOUS IT WAS A VISION OF SERIOUS DUTIES AND LITTLE COMPANY OF REALLY GREAT LONELINESS",
"hyp": "She was young, untried, nervous. It was a vision of serious duties in little company , of really great loneliness.",
"ref_norm": "SHE WAS YOUNG UNTRIED NERVOUS IT WAS A VISION OF SERIOUS DUTIES AND LITTLE COMPANY OF REALLY GREAT LONELINESS",
"hyp_norm": "SHE WAS YOUNG UNTRIED NERVOUS IT WAS A VISION OF SERIOUS DUTIES IN LITTLE COMPANY OF REALLY GREAT LONELINESS",
"duration_s": 10.765,
"infer_time_s": 10.149,
"rtf": 0.9428,
"wer": 0.0526
},
{
"id": "121-127105-0032",
"ref": "YES BUT THAT'S JUST THE BEAUTY OF HER PASSION",
"hyp": "Yes, but that's just the beauty of her passion.",
"ref_norm": "YES BUT THATS JUST THE BEAUTY OF HER PASSION",
"hyp_norm": "YES BUT THATS JUST THE BEAUTY OF HER PASSION",
"duration_s": 3.17,
"infer_time_s": 2.858,
"rtf": 0.9016,
"wer": 0.0
},
{
"id": "121-127105-0033",
"ref": "IT WAS THE BEAUTY OF IT",
"hyp": "It was the beauty of it.",
"ref_norm": "IT WAS THE BEAUTY OF IT",
"hyp_norm": "IT WAS THE BEAUTY OF IT",
"duration_s": 2.355,
"infer_time_s": 1.847,
"rtf": 0.7842,
"wer": 0.0
},
{
"id": "121-127105-0034",
"ref": "IT SOUNDED DULL IT SOUNDED STRANGE AND ALL THE MORE SO BECAUSE OF HIS MAIN CONDITION WHICH WAS",
"hyp": "It sounded dull . That sounded strange , and all the more so because of his main condition, which was.",
"ref_norm": "IT SOUNDED DULL IT SOUNDED STRANGE AND ALL THE MORE SO BECAUSE OF HIS MAIN CONDITION WHICH WAS",
"hyp_norm": "IT SOUNDED DULL THAT SOUNDED STRANGE AND ALL THE MORE SO BECAUSE OF HIS MAIN CONDITION WHICH WAS",
"duration_s": 7.41,
"infer_time_s": 4.931,
"rtf": 0.6655,
"wer": 0.0556
},
{
"id": "121-127105-0035",
"ref": "SHE PROMISED TO DO THIS AND SHE MENTIONED TO ME THAT WHEN FOR A MOMENT DISBURDENED DELIGHTED HE HELD HER HAND THANKING HER FOR THE SACRIFICE SHE ALREADY FELT REWARDED",
"hyp": "She promised to do this, and she mentioned to me that when, for a moment , disburdened , delighted , he held her hand , thanking her for the sacrifice. She already felt rewarded.",
"ref_norm": "SHE PROMISED TO DO THIS AND SHE MENTIONED TO ME THAT WHEN FOR A MOMENT DISBURDENED DELIGHTED HE HELD HER HAND THANKING HER FOR THE SACRIFICE SHE ALREADY FELT REWARDED",
"hyp_norm": "SHE PROMISED TO DO THIS AND SHE MENTIONED TO ME THAT WHEN FOR A MOMENT DISBURDENED DELIGHTED HE HELD HER HAND THANKING HER FOR THE SACRIFICE SHE ALREADY FELT REWARDED",
"duration_s": 14.15,
"infer_time_s": 9.501,
"rtf": 0.6714,
"wer": 0.0
},
{
"id": "121-127105-0036",
"ref": "BUT WAS THAT ALL HER REWARD ONE OF THE LADIES ASKED",
"hyp": "But was that all her reward? One of the ladies asked.",
"ref_norm": "BUT WAS THAT ALL HER REWARD ONE OF THE LADIES ASKED",
"hyp_norm": "BUT WAS THAT ALL HER REWARD ONE OF THE LADIES ASKED",
"duration_s": 4.15,
"infer_time_s": 3.395,
"rtf": 0.818,
"wer": 0.0
},
{
"id": "1221-135766-0000",
"ref": "HOW STRANGE IT SEEMED TO THE SAD WOMAN AS SHE WATCHED THE GROWTH AND THE BEAUTY THAT BECAME EVERY DAY MORE BRILLIANT AND THE INTELLIGENCE THAT THREW ITS QUIVERING SUNSHINE OVER THE TINY FEATURES OF THIS CHILD",
"hyp": "How strange it seemed to the sad woman as she watched the growth and the beauty that became every day more brilliant, and the intelligence that threw its qu ivering sunshine over the tiny features of this child.",
"ref_norm": "HOW STRANGE IT SEEMED TO THE SAD WOMAN AS SHE WATCHED THE GROWTH AND THE BEAUTY THAT BECAME EVERY DAY MORE BRILLIANT AND THE INTELLIGENCE THAT THREW ITS QUIVERING SUNSHINE OVER THE TINY FEATURES OF THIS CHILD",
"hyp_norm": "HOW STRANGE IT SEEMED TO THE SAD WOMAN AS SHE WATCHED THE GROWTH AND THE BEAUTY THAT BECAME EVERY DAY MORE BRILLIANT AND THE INTELLIGENCE THAT THREW ITS QU IVERING SUNSHINE OVER THE TINY FEATURES OF THIS CHILD",
"duration_s": 12.435,
"infer_time_s": 9.664,
"rtf": 0.7772,
"wer": 0.0541
},
{
"id": "1221-135766-0001",
"ref": "GOD AS A DIRECT CONSEQUENCE OF THE SIN WHICH MAN THUS PUNISHED HAD GIVEN HER A LOVELY CHILD WHOSE PLACE WAS ON THAT SAME DISHONOURED BOSOM TO CONNECT HER PARENT FOR EVER WITH THE RACE AND DESCENT OF MORTALS AND TO BE FINALLY A BLESSED SOUL IN HEAVEN",
"hyp": "God, as a direct consequence of the sin which man thus punished, had given her a lovely child, whose place was on that same dishonored bos om to connect her parent , for ever, with the race and descent of mortals, and to be finally a blessed soul in heaven.",
"ref_norm": "GOD AS A DIRECT CONSEQUENCE OF THE SIN WHICH MAN THUS PUNISHED HAD GIVEN HER A LOVELY CHILD WHOSE PLACE WAS ON THAT SAME DISHONOURED BOSOM TO CONNECT HER PARENT FOR EVER WITH THE RACE AND DESCENT OF MORTALS AND TO BE FINALLY A BLESSED SOUL IN HEAVEN",
"hyp_norm": "GOD AS A DIRECT CONSEQUENCE OF THE SIN WHICH MAN THUS PUNISHED HAD GIVEN HER A LOVELY CHILD WHOSE PLACE WAS ON THAT SAME DISHONORED BOS OM TO CONNECT HER PARENT FOR EVER WITH THE RACE AND DESCENT OF MORTALS AND TO BE FINALLY A BLESSED SOUL IN HEAVEN",
"duration_s": 16.715,
"infer_time_s": 11.977,
"rtf": 0.7165,
"wer": 0.0625
},
{
"id": "1221-135766-0002",
"ref": "YET THESE THOUGHTS AFFECTED HESTER PRYNNE LESS WITH HOPE THAN APPREHENSION",
"hyp": "Yet these thoughts affected Hester Prynne less with hope than apprehension.",
"ref_norm": "YET THESE THOUGHTS AFFECTED HESTER PRYNNE LESS WITH HOPE THAN APPREHENSION",
"hyp_norm": "YET THESE THOUGHTS AFFECTED HESTER PRYNNE LESS WITH HOPE THAN APPREHENSION",
"duration_s": 4.825,
"infer_time_s": 3.366,
"rtf": 0.6977,
"wer": 0.0
},
{
"id": "1221-135766-0003",
"ref": "THE CHILD HAD A NATIVE GRACE WHICH DOES NOT INVARIABLY CO EXIST WITH FAULTLESS BEAUTY ITS ATTIRE HOWEVER SIMPLE ALWAYS IMPRESSED THE BEHOLDER AS IF IT WERE THE VERY GARB THAT PRECISELY BECAME IT BEST",
"hyp": "The child had a native grace, which does not invariably coexist with faultless beauty . Its attire, however simple, always impressed the beholder as if it were the very garb that precisely became it best.",
"ref_norm": "THE CHILD HAD A NATIVE GRACE WHICH DOES NOT INVARIABLY CO EXIST WITH FAULTLESS BEAUTY ITS ATTIRE HOWEVER SIMPLE ALWAYS IMPRESSED THE BEHOLDER AS IF IT WERE THE VERY GARB THAT PRECISELY BECAME IT BEST",
"hyp_norm": "THE CHILD HAD A NATIVE GRACE WHICH DOES NOT INVARIABLY COEXIST WITH FAULTLESS BEAUTY ITS ATTIRE HOWEVER SIMPLE ALWAYS IMPRESSED THE BEHOLDER AS IF IT WERE THE VERY GARB THAT PRECISELY BECAME IT BEST",
"duration_s": 13.72,
"infer_time_s": 9.342,
"rtf": 0.6809,
"wer": 0.0571
},
{
"id": "1221-135766-0004",
"ref": "THIS OUTWARD MUTABILITY INDICATED AND DID NOT MORE THAN FAIRLY EXPRESS THE VARIOUS PROPERTIES OF HER INNER LIFE",
"hyp": "This outward mut ability indicated, and did not more than fairly express, the various properties of her inner life.",
"ref_norm": "THIS OUTWARD MUTABILITY INDICATED AND DID NOT MORE THAN FAIRLY EXPRESS THE VARIOUS PROPERTIES OF HER INNER LIFE",
"hyp_norm": "THIS OUTWARD MUT ABILITY INDICATED AND DID NOT MORE THAN FAIRLY EXPRESS THE VARIOUS PROPERTIES OF HER INNER LIFE",
"duration_s": 7.44,
"infer_time_s": 4.474,
"rtf": 0.6013,
"wer": 0.1111
},
{
"id": "1221-135766-0005",
"ref": "HESTER COULD ONLY ACCOUNT FOR THE CHILD'S CHARACTER AND EVEN THEN MOST VAGUELY AND IMPERFECTLY BY RECALLING WHAT SHE HERSELF HAD BEEN DURING THAT MOMENTOUS PERIOD WHILE PEARL WAS IMBIBING HER SOUL FROM THE SPIRITUAL WORLD AND HER BODILY FRAME FROM ITS MATERIAL OF EARTH",
"hyp": "Hester could only account for the child's character, and even then, most vaguely and imperfectly , by recalling what she herself had been during that momentous period, while Pearl was imbibing her soul from the spiritual world, and her bodily frame from its material of earth.",
"ref_norm": "HESTER COULD ONLY ACCOUNT FOR THE CHILDS CHARACTER AND EVEN THEN MOST VAGUELY AND IMPERFECTLY BY RECALLING WHAT SHE HERSELF HAD BEEN DURING THAT MOMENTOUS PERIOD WHILE PEARL WAS IMBIBING HER SOUL FROM THE SPIRITUAL WORLD AND HER BODILY FRAME FROM ITS MATERIAL OF EARTH",
"hyp_norm": "HESTER COULD ONLY ACCOUNT FOR THE CHILDS CHARACTER AND EVEN THEN MOST VAGUELY AND IMPERFECTLY BY RECALLING WHAT SHE HERSELF HAD BEEN DURING THAT MOMENTOUS PERIOD WHILE PEARL WAS IMBIBING HER SOUL FROM THE SPIRITUAL WORLD AND HER BODILY FRAME FROM ITS MATERIAL OF EARTH",
"duration_s": 16.645,
"infer_time_s": 11.447,
"rtf": 0.6877,
"wer": 0.0
},
{
"id": "1221-135766-0006",
"ref": "THEY WERE NOW ILLUMINATED BY THE MORNING RADIANCE OF A YOUNG CHILD'S DISPOSITION BUT LATER IN THE DAY OF EARTHLY EXISTENCE MIGHT BE PROLIFIC OF THE STORM AND WHIRLWIND",
"hyp": "They were now illuminated by the morning radiance of a young child's disposition, but later in the day of earthly existence might be prolific of the storm and whirlwind.",
"ref_norm": "THEY WERE NOW ILLUMINATED BY THE MORNING RADIANCE OF A YOUNG CHILDS DISPOSITION BUT LATER IN THE DAY OF EARTHLY EXISTENCE MIGHT BE PROLIFIC OF THE STORM AND WHIRLWIND",
"hyp_norm": "THEY WERE NOW ILLUMINATED BY THE MORNING RADIANCE OF A YOUNG CHILDS DISPOSITION BUT LATER IN THE DAY OF EARTHLY EXISTENCE MIGHT BE PROLIFIC OF THE STORM AND WHIRLWIND",
"duration_s": 11.415,
"infer_time_s": 7.421,
"rtf": 0.6501,
"wer": 0.0
},
{
"id": "1221-135766-0007",
"ref": "HESTER PRYNNE NEVERTHELESS THE LOVING MOTHER OF THIS ONE CHILD RAN LITTLE RISK OF ERRING ON THE SIDE OF UNDUE SEVERITY",
"hyp": "Hester Prin , nevertheless, the loving mother of this one child, ran little risk of erring on the side of undue severity.",
"ref_norm": "HESTER PRYNNE NEVERTHELESS THE LOVING MOTHER OF THIS ONE CHILD RAN LITTLE RISK OF ERRING ON THE SIDE OF UNDUE SEVERITY",
"hyp_norm": "HESTER PRIN NEVERTHELESS THE LOVING MOTHER OF THIS ONE CHILD RAN LITTLE RISK OF ERRING ON THE SIDE OF UNDUE SEVERITY",
"duration_s": 8.795,
"infer_time_s": 5.947,
"rtf": 0.6762,
"wer": 0.0476
},
{
"id": "1221-135766-0008",
"ref": "MINDFUL HOWEVER OF HER OWN ERRORS AND MISFORTUNES SHE EARLY SOUGHT TO IMPOSE A TENDER BUT STRICT CONTROL OVER THE INFANT IMMORTALITY THAT WAS COMMITTED TO HER CHARGE",
"hyp": "Mindful, however, of her own errors and misfort unes, she early sought to impose a tender but strict control over the infant immortality that was committed to her charge.",
"ref_norm": "MINDFUL HOWEVER OF HER OWN ERRORS AND MISFORTUNES SHE EARLY SOUGHT TO IMPOSE A TENDER BUT STRICT CONTROL OVER THE INFANT IMMORTALITY THAT WAS COMMITTED TO HER CHARGE",
"hyp_norm": "MINDFUL HOWEVER OF HER OWN ERRORS AND MISFORT UNES SHE EARLY SOUGHT TO IMPOSE A TENDER BUT STRICT CONTROL OVER THE INFANT IMMORTALITY THAT WAS COMMITTED TO HER CHARGE",
"duration_s": 10.78,
"infer_time_s": 7.363,
"rtf": 0.683,
"wer": 0.0714
},
{
"id": "1221-135766-0009",
"ref": "AS TO ANY OTHER KIND OF DISCIPLINE WHETHER ADDRESSED TO HER MIND OR HEART LITTLE PEARL MIGHT OR MIGHT NOT BE WITHIN ITS REACH IN ACCORDANCE WITH THE CAPRICE THAT RULED THE MOMENT",
"hyp": "As to any other kind of discipline, whether addressed to her mind or heart, little Pearl might or might not be within its reach, in accordance with the caprice that ruled the moment.",
"ref_norm": "AS TO ANY OTHER KIND OF DISCIPLINE WHETHER ADDRESSED TO HER MIND OR HEART LITTLE PEARL MIGHT OR MIGHT NOT BE WITHIN ITS REACH IN ACCORDANCE WITH THE CAPRICE THAT RULED THE MOMENT",
"hyp_norm": "AS TO ANY OTHER KIND OF DISCIPLINE WHETHER ADDRESSED TO HER MIND OR HEART LITTLE PEARL MIGHT OR MIGHT NOT BE WITHIN ITS REACH IN ACCORDANCE WITH THE CAPRICE THAT RULED THE MOMENT",
"duration_s": 10.19,
"infer_time_s": 7.991,
"rtf": 0.7842,
"wer": 0.0
},
{
"id": "1221-135766-0010",
"ref": "IT WAS A LOOK SO INTELLIGENT YET INEXPLICABLE PERVERSE SOMETIMES SO MALICIOUS BUT GENERALLY ACCOMPANIED BY A WILD FLOW OF SPIRITS THAT HESTER COULD NOT HELP QUESTIONING AT SUCH MOMENTS WHETHER PEARL WAS A HUMAN CHILD",
"hyp": "It was a look so intelligent, yet inexp licable, perverse . Sometimes so malicious , but generally accompanied by a wild flow of spirits, that Hester could not help questioning at such moments whether Pearl was a human child.",
"ref_norm": "IT WAS A LOOK SO INTELLIGENT YET INEXPLICABLE PERVERSE SOMETIMES SO MALICIOUS BUT GENERALLY ACCOMPANIED BY A WILD FLOW OF SPIRITS THAT HESTER COULD NOT HELP QUESTIONING AT SUCH MOMENTS WHETHER PEARL WAS A HUMAN CHILD",
"hyp_norm": "IT WAS A LOOK SO INTELLIGENT YET INEXP LICABLE PERVERSE SOMETIMES SO MALICIOUS BUT GENERALLY ACCOMPANIED BY A WILD FLOW OF SPIRITS THAT HESTER COULD NOT HELP QUESTIONING AT SUCH MOMENTS WHETHER PEARL WAS A HUMAN CHILD",
"duration_s": 15.05,
"infer_time_s": 9.652,
"rtf": 0.6413,
"wer": 0.0556
},
{
"id": "1221-135766-0011",
"ref": "BEHOLDING IT HESTER WAS CONSTRAINED TO RUSH TOWARDS THE CHILD TO PURSUE THE LITTLE ELF IN THE FLIGHT WHICH SHE INVARIABLY BEGAN TO SNATCH HER TO HER BOSOM WITH A CLOSE PRESSURE AND EARNEST KISSES NOT SO MUCH FROM OVERFLOWING LOVE AS TO ASSURE HERSELF THAT PEARL WAS FLESH AND BLOOD AND NOT UTTERLY DELUSIVE",
"hyp": "Beholding it, H ester was constrained to rush towards the child , to pursue the little elf in the flight which she invariably began , to snatch her to her bosom with a close pressure and earnest kisses, not so much from overflowing love as to assure herself that Pearl was flesh and blood, and not utterly delusive.",
"ref_norm": "BEHOLDING IT HESTER WAS CONSTRAINED TO RUSH TOWARDS THE CHILD TO PURSUE THE LITTLE ELF IN THE FLIGHT WHICH SHE INVARIABLY BEGAN TO SNATCH HER TO HER BOSOM WITH A CLOSE PRESSURE AND EARNEST KISSES NOT SO MUCH FROM OVERFLOWING LOVE AS TO ASSURE HERSELF THAT PEARL WAS FLESH AND BLOOD AND NOT UTTERLY DELUSIVE",
"hyp_norm": "BEHOLDING IT H ESTER WAS CONSTRAINED TO RUSH TOWARDS THE CHILD TO PURSUE THE LITTLE ELF IN THE FLIGHT WHICH SHE INVARIABLY BEGAN TO SNATCH HER TO HER BOSOM WITH A CLOSE PRESSURE AND EARNEST KISSES NOT SO MUCH FROM OVERFLOWING LOVE AS TO ASSURE HERSELF THAT PEARL WAS FLESH AND BLOOD AND NOT UTTERLY DELUSIVE",
"duration_s": 21.345,
"infer_time_s": 13.406,
"rtf": 0.6281,
"wer": 0.0364
},
{
"id": "1221-135766-0012",
"ref": "BROODING OVER ALL THESE MATTERS THE MOTHER FELT LIKE ONE WHO HAS EVOKED A SPIRIT BUT BY SOME IRREGULARITY IN THE PROCESS OF CONJURATION HAS FAILED TO WIN THE MASTER WORD THAT SHOULD CONTROL THIS NEW AND INCOMPREHENSIBLE INTELLIGENCE",
"hyp": "Brooding over all these matters, the mother felt like one who has evoked a spirit, but by some irregularity in the process of conj uration, has failed to win the master word that should control this new and incomprehensible intelligence.",
"ref_norm": "BROODING OVER ALL THESE MATTERS THE MOTHER FELT LIKE ONE WHO HAS EVOKED A SPIRIT BUT BY SOME IRREGULARITY IN THE PROCESS OF CONJURATION HAS FAILED TO WIN THE MASTER WORD THAT SHOULD CONTROL THIS NEW AND INCOMPREHENSIBLE INTELLIGENCE",
"hyp_norm": "BROODING OVER ALL THESE MATTERS THE MOTHER FELT LIKE ONE WHO HAS EVOKED A SPIRIT BUT BY SOME IRREGULARITY IN THE PROCESS OF CONJ URATION HAS FAILED TO WIN THE MASTER WORD THAT SHOULD CONTROL THIS NEW AND INCOMPREHENSIBLE INTELLIGENCE",
"duration_s": 16.22,
"infer_time_s": 10.41,
"rtf": 0.6418,
"wer": 0.0513
},
{
"id": "1221-135766-0013",
"ref": "PEARL WAS A BORN OUTCAST OF THE INFANTILE WORLD",
"hyp": "Pearl was a born out cast of the infantile world.",
"ref_norm": "PEARL WAS A BORN OUTCAST OF THE INFANTILE WORLD",
"hyp_norm": "PEARL WAS A BORN OUT CAST OF THE INFANTILE WORLD",
"duration_s": 3.645,
"infer_time_s": 2.666,
"rtf": 0.7314,
"wer": 0.2222
},
{
"id": "1221-135766-0014",
"ref": "PEARL SAW AND GAZED INTENTLY BUT NEVER SOUGHT TO MAKE ACQUAINTANCE",
"hyp": "Pearl saw and gazed intently, but never sought to make acquaintance.",
"ref_norm": "PEARL SAW AND GAZED INTENTLY BUT NEVER SOUGHT TO MAKE ACQUAINTANCE",
"hyp_norm": "PEARL SAW AND GAZED INTENTLY BUT NEVER SOUGHT TO MAKE ACQUAINTANCE",
"duration_s": 4.75,
"infer_time_s": 3.412,
"rtf": 0.7182,
"wer": 0.0
},
{
"id": "1221-135766-0015",
"ref": "IF SPOKEN TO SHE WOULD NOT SPEAK AGAIN",
"hyp": "If spoken to, she would not speak again.",
"ref_norm": "IF SPOKEN TO SHE WOULD NOT SPEAK AGAIN",
"hyp_norm": "IF SPOKEN TO SHE WOULD NOT SPEAK AGAIN",
"duration_s": 2.63,
"infer_time_s": 2.213,
"rtf": 0.8414,
"wer": 0.0
},
{
"id": "1221-135767-0000",
"ref": "HESTER PRYNNE WENT ONE DAY TO THE MANSION OF GOVERNOR BELLINGHAM WITH A PAIR OF GLOVES WHICH SHE HAD FRINGED AND EMBROIDERED TO HIS ORDER AND WHICH WERE TO BE WORN ON SOME GREAT OCCASION OF STATE FOR THOUGH THE CHANCES OF A POPULAR ELECTION HAD CAUSED THIS FORMER RULER TO DESCEND A STEP OR TWO FROM THE HIGHEST RANK HE STILL HELD AN HONOURABLE AND INFLUENTIAL PLACE AMONG THE COLONIAL MAGISTRACY",
"hyp": "Hester Prynne went one day to the mansion of Governor Bellingham with a pair of gloves which she had fr inged and embroidered to his order, and which were to be worn on some great occasion of state, for though the chances of a popular election had caused this former ruler to descend a step or two from the highest rank, he still held an honourable and influential place among the colonial magistracy.",
"ref_norm": "HESTER PRYNNE WENT ONE DAY TO THE MANSION OF GOVERNOR BELLINGHAM WITH A PAIR OF GLOVES WHICH SHE HAD FRINGED AND EMBROIDERED TO HIS ORDER AND WHICH WERE TO BE WORN ON SOME GREAT OCCASION OF STATE FOR THOUGH THE CHANCES OF A POPULAR ELECTION HAD CAUSED THIS FORMER RULER TO DESCEND A STEP OR TWO FROM THE HIGHEST RANK HE STILL HELD AN HONOURABLE AND INFLUENTIAL PLACE AMONG THE COLONIAL MAGISTRACY",
"hyp_norm": "HESTER PRYNNE WENT ONE DAY TO THE MANSION OF GOVERNOR BELLINGHAM WITH A PAIR OF GLOVES WHICH SHE HAD FR INGED AND EMBROIDERED TO HIS ORDER AND WHICH WERE TO BE WORN ON SOME GREAT OCCASION OF STATE FOR THOUGH THE CHANCES OF A POPULAR ELECTION HAD CAUSED THIS FORMER RULER TO DESCEND A STEP OR TWO FROM THE HIGHEST RANK HE STILL HELD AN HONOURABLE AND INFLUENTIAL PLACE AMONG THE COLONIAL MAGISTRACY",
"duration_s": 24.85,
"infer_time_s": 18.075,
"rtf": 0.7273,
"wer": 0.0278
},
{
"id": "1221-135767-0001",
"ref": "ANOTHER AND FAR MORE IMPORTANT REASON THAN THE DELIVERY OF A PAIR OF EMBROIDERED GLOVES IMPELLED HESTER AT THIS TIME TO SEEK AN INTERVIEW WITH A PERSONAGE OF SO MUCH POWER AND ACTIVITY IN THE AFFAIRS OF THE SETTLEMENT",
"hyp": "Another and far more important reason than the delivery of a pair of embroidered gloves impelled H ester at this time to seek an interview with a personage of so much power and activity in the affairs of the settlement.",
"ref_norm": "ANOTHER AND FAR MORE IMPORTANT REASON THAN THE DELIVERY OF A PAIR OF EMBROIDERED GLOVES IMPELLED HESTER AT THIS TIME TO SEEK AN INTERVIEW WITH A PERSONAGE OF SO MUCH POWER AND ACTIVITY IN THE AFFAIRS OF THE SETTLEMENT",
"hyp_norm": "ANOTHER AND FAR MORE IMPORTANT REASON THAN THE DELIVERY OF A PAIR OF EMBROIDERED GLOVES IMPELLED H ESTER AT THIS TIME TO SEEK AN INTERVIEW WITH A PERSONAGE OF SO MUCH POWER AND ACTIVITY IN THE AFFAIRS OF THE SETTLEMENT",
"duration_s": 13.43,
"infer_time_s": 9.093,
"rtf": 0.6771,
"wer": 0.0513
},
{
"id": "1221-135767-0002",
"ref": "AT THAT EPOCH OF PRISTINE SIMPLICITY HOWEVER MATTERS OF EVEN SLIGHTER PUBLIC INTEREST AND OF FAR LESS INTRINSIC WEIGHT THAN THE WELFARE OF HESTER AND HER CHILD WERE STRANGELY MIXED UP WITH THE DELIBERATIONS OF LEGISLATORS AND ACTS OF STATE",
"hyp": "At that epoch of pristine simplicity, however, matters of even slighter public interest and of far less intrinsic weight than the welfare of Hester and her child , were strangely mixed up with the deliberations of legislators and acts of state.",
"ref_norm": "AT THAT EPOCH OF PRISTINE SIMPLICITY HOWEVER MATTERS OF EVEN SLIGHTER PUBLIC INTEREST AND OF FAR LESS INTRINSIC WEIGHT THAN THE WELFARE OF HESTER AND HER CHILD WERE STRANGELY MIXED UP WITH THE DELIBERATIONS OF LEGISLATORS AND ACTS OF STATE",
"hyp_norm": "AT THAT EPOCH OF PRISTINE SIMPLICITY HOWEVER MATTERS OF EVEN SLIGHTER PUBLIC INTEREST AND OF FAR LESS INTRINSIC WEIGHT THAN THE WELFARE OF HESTER AND HER CHILD WERE STRANGELY MIXED UP WITH THE DELIBERATIONS OF LEGISLATORS AND ACTS OF STATE",
"duration_s": 16.12,
"infer_time_s": 10.347,
"rtf": 0.6419,
"wer": 0.0
},
{
"id": "1221-135767-0003",
"ref": "THE PERIOD WAS HARDLY IF AT ALL EARLIER THAN THAT OF OUR STORY WHEN A DISPUTE CONCERNING THE RIGHT OF PROPERTY IN A PIG NOT ONLY CAUSED A FIERCE AND BITTER CONTEST IN THE LEGISLATIVE BODY OF THE COLONY BUT RESULTED IN AN IMPORTANT MODIFICATION OF THE FRAMEWORK ITSELF OF THE LEGISLATURE",
"hyp": "The period was hardly, if at all, earlier than that of our story, when a dispute concerning the right of property in a pig , not only caused a fierce and bitter contest in the legislative body of the colony, but resulted in an important modification of the framework itself of the legislature.",
"ref_norm": "THE PERIOD WAS HARDLY IF AT ALL EARLIER THAN THAT OF OUR STORY WHEN A DISPUTE CONCERNING THE RIGHT OF PROPERTY IN A PIG NOT ONLY CAUSED A FIERCE AND BITTER CONTEST IN THE LEGISLATIVE BODY OF THE COLONY BUT RESULTED IN AN IMPORTANT MODIFICATION OF THE FRAMEWORK ITSELF OF THE LEGISLATURE",
"hyp_norm": "THE PERIOD WAS HARDLY IF AT ALL EARLIER THAN THAT OF OUR STORY WHEN A DISPUTE CONCERNING THE RIGHT OF PROPERTY IN A PIG NOT ONLY CAUSED A FIERCE AND BITTER CONTEST IN THE LEGISLATIVE BODY OF THE COLONY BUT RESULTED IN AN IMPORTANT MODIFICATION OF THE FRAMEWORK ITSELF OF THE LEGISLATURE",
"duration_s": 18.63,
"infer_time_s": 12.555,
"rtf": 0.6739,
"wer": 0.0
},
{
"id": "1221-135767-0004",
"ref": "WE HAVE SPOKEN OF PEARL'S RICH AND LUXURIANT BEAUTY A BEAUTY THAT SHONE WITH DEEP AND VIVID TINTS A BRIGHT COMPLEXION EYES POSSESSING INTENSITY BOTH OF DEPTH AND GLOW AND HAIR ALREADY OF A DEEP GLOSSY BROWN AND WHICH IN AFTER YEARS WOULD BE NEARLY AKIN TO BLACK",
"hyp": "We have spoken of pearls' rich and luxuriant beauty\u2014a beauty that shone with deep and vivid tints, a bright complexion, eyes possessing intensity both of depth and glow , and hair already of a deep glossy brown, and which in after years would be nearly akin to black.",
"ref_norm": "WE HAVE SPOKEN OF PEARLS RICH AND LUXURIANT BEAUTY A BEAUTY THAT SHONE WITH DEEP AND VIVID TINTS A BRIGHT COMPLEXION EYES POSSESSING INTENSITY BOTH OF DEPTH AND GLOW AND HAIR ALREADY OF A DEEP GLOSSY BROWN AND WHICH IN AFTER YEARS WOULD BE NEARLY AKIN TO BLACK",
"hyp_norm": "WE HAVE SPOKEN OF PEARLS RICH AND LUXURIANT BEAUTYA BEAUTY THAT SHONE WITH DEEP AND VIVID TINTS A BRIGHT COMPLEXION EYES POSSESSING INTENSITY BOTH OF DEPTH AND GLOW AND HAIR ALREADY OF A DEEP GLOSSY BROWN AND WHICH IN AFTER YEARS WOULD BE NEARLY AKIN TO BLACK",
"duration_s": 19.09,
"infer_time_s": 12.211,
"rtf": 0.6397,
"wer": 0.0417
},
{
"id": "1221-135767-0005",
"ref": "IT WAS THE SCARLET LETTER IN ANOTHER FORM THE SCARLET LETTER ENDOWED WITH LIFE",
"hyp": "It was the scarlet letter in another form , the scarlet letter endowed with life.",
"ref_norm": "IT WAS THE SCARLET LETTER IN ANOTHER FORM THE SCARLET LETTER ENDOWED WITH LIFE",
"hyp_norm": "IT WAS THE SCARLET LETTER IN ANOTHER FORM THE SCARLET LETTER ENDOWED WITH LIFE",
"duration_s": 5.865,
"infer_time_s": 3.623,
"rtf": 0.6178,
"wer": 0.0
},
{
"id": "1221-135767-0006",
"ref": "THE MOTHER HERSELF AS IF THE RED IGNOMINY WERE SO DEEPLY SCORCHED INTO HER BRAIN THAT ALL HER CONCEPTIONS ASSUMED ITS FORM HAD CAREFULLY WROUGHT OUT THE SIMILITUDE LAVISHING MANY HOURS OF MORBID INGENUITY TO CREATE AN ANALOGY BETWEEN THE OBJECT OF HER AFFECTION AND THE EMBLEM OF HER GUILT AND TORTURE",
"hyp": "The mother herself , as if the red ignom iny were so deeply scor ched into her brain that all her conceptions assumed its form, had carefully wrought out the sim ilitude, lavishing many hours of morbid ing enuity to create an analogy between the object of her affection and the emblem of her guilt and torture.",
"ref_norm": "THE MOTHER HERSELF AS IF THE RED IGNOMINY WERE SO DEEPLY SCORCHED INTO HER BRAIN THAT ALL HER CONCEPTIONS ASSUMED ITS FORM HAD CAREFULLY WROUGHT OUT THE SIMILITUDE LAVISHING MANY HOURS OF MORBID INGENUITY TO CREATE AN ANALOGY BETWEEN THE OBJECT OF HER AFFECTION AND THE EMBLEM OF HER GUILT AND TORTURE",
"hyp_norm": "THE MOTHER HERSELF AS IF THE RED IGNOM INY WERE SO DEEPLY SCOR CHED INTO HER BRAIN THAT ALL HER CONCEPTIONS ASSUMED ITS FORM HAD CAREFULLY WROUGHT OUT THE SIM ILITUDE LAVISHING MANY HOURS OF MORBID ING ENUITY TO CREATE AN ANALOGY BETWEEN THE OBJECT OF HER AFFECTION AND THE EMBLEM OF HER GUILT AND TORTURE",
"duration_s": 20.56,
"infer_time_s": 13.555,
"rtf": 0.6593,
"wer": 0.1538
},
{
"id": "1221-135767-0007",
"ref": "BUT IN TRUTH PEARL WAS THE ONE AS WELL AS THE OTHER AND ONLY IN CONSEQUENCE OF THAT IDENTITY HAD HESTER CONTRIVED SO PERFECTLY TO REPRESENT THE SCARLET LETTER IN HER APPEARANCE",
"hyp": "But in truth, Pearl was the one as well as the other, and only in consequence of that identity had Hester contrived so perfectly to represent the scarlet letter in her appearance.",
"ref_norm": "BUT IN TRUTH PEARL WAS THE ONE AS WELL AS THE OTHER AND ONLY IN CONSEQUENCE OF THAT IDENTITY HAD HESTER CONTRIVED SO PERFECTLY TO REPRESENT THE SCARLET LETTER IN HER APPEARANCE",
"hyp_norm": "BUT IN TRUTH PEARL WAS THE ONE AS WELL AS THE OTHER AND ONLY IN CONSEQUENCE OF THAT IDENTITY HAD HESTER CONTRIVED SO PERFECTLY TO REPRESENT THE SCARLET LETTER IN HER APPEARANCE",
"duration_s": 12.77,
"infer_time_s": 8.212,
"rtf": 0.6431,
"wer": 0.0
},
{
"id": "1221-135767-0008",
"ref": "COME THEREFORE AND LET US FLING MUD AT THEM",
"hyp": "Come, therefore, and let us fling mud at them.",
"ref_norm": "COME THEREFORE AND LET US FLING MUD AT THEM",
"hyp_norm": "COME THEREFORE AND LET US FLING MUD AT THEM",
"duration_s": 3.095,
"infer_time_s": 2.512,
"rtf": 0.8117,
"wer": 0.0
},
{
"id": "1221-135767-0009",
"ref": "BUT PEARL WHO WAS A DAUNTLESS CHILD AFTER FROWNING STAMPING HER FOOT AND SHAKING HER LITTLE HAND WITH A VARIETY OF THREATENING GESTURES SUDDENLY MADE A RUSH AT THE KNOT OF HER ENEMIES AND PUT THEM ALL TO FLIGHT",
"hyp": "But Pearl, who was a dauntless child, after frowning, stamp ing her foot, and shaking her little hand with a variety of threatening gestures , suddenly made a rush at the knot of her enemies and put them all to flight.",
"ref_norm": "BUT PEARL WHO WAS A DAUNTLESS CHILD AFTER FROWNING STAMPING HER FOOT AND SHAKING HER LITTLE HAND WITH A VARIETY OF THREATENING GESTURES SUDDENLY MADE A RUSH AT THE KNOT OF HER ENEMIES AND PUT THEM ALL TO FLIGHT",
"hyp_norm": "BUT PEARL WHO WAS A DAUNTLESS CHILD AFTER FROWNING STAMP ING HER FOOT AND SHAKING HER LITTLE HAND WITH A VARIETY OF THREATENING GESTURES SUDDENLY MADE A RUSH AT THE KNOT OF HER ENEMIES AND PUT THEM ALL TO FLIGHT",
"duration_s": 13.34,
"infer_time_s": 9.998,
"rtf": 0.7495,
"wer": 0.0513
},
{
"id": "1221-135767-0010",
"ref": "SHE SCREAMED AND SHOUTED TOO WITH A TERRIFIC VOLUME OF SOUND WHICH DOUBTLESS CAUSED THE HEARTS OF THE FUGITIVES TO QUAKE WITHIN THEM",
"hyp": "She screamed and shouted too with a terrific volume of sound, which doubtless caused the hearts of the fugitives to quake within them.",
"ref_norm": "SHE SCREAMED AND SHOUTED TOO WITH A TERRIFIC VOLUME OF SOUND WHICH DOUBTLESS CAUSED THE HEARTS OF THE FUGITIVES TO QUAKE WITHIN THEM",
"hyp_norm": "SHE SCREAMED AND SHOUTED TOO WITH A TERRIFIC VOLUME OF SOUND WHICH DOUBTLESS CAUSED THE HEARTS OF THE FUGITIVES TO QUAKE WITHIN THEM",
"duration_s": 8.2,
"infer_time_s": 5.801,
"rtf": 0.7075,
"wer": 0.0
},
{
"id": "1221-135767-0011",
"ref": "IT WAS FURTHER DECORATED WITH STRANGE AND SEEMINGLY CABALISTIC FIGURES AND DIAGRAMS SUITABLE TO THE QUAINT TASTE OF THE AGE WHICH HAD BEEN DRAWN IN THE STUCCO WHEN NEWLY LAID ON AND HAD NOW GROWN HARD AND DURABLE FOR THE ADMIRATION OF AFTER TIMES",
"hyp": "It was further decorated with strange and seemingly cabalistic figures and diagrams, suitable to the quaint taste of the age, which had been drawn in the stucco when newly laid on, and had now grown hard and durable for the admiration of after times.",
"ref_norm": "IT WAS FURTHER DECORATED WITH STRANGE AND SEEMINGLY CABALISTIC FIGURES AND DIAGRAMS SUITABLE TO THE QUAINT TASTE OF THE AGE WHICH HAD BEEN DRAWN IN THE STUCCO WHEN NEWLY LAID ON AND HAD NOW GROWN HARD AND DURABLE FOR THE ADMIRATION OF AFTER TIMES",
"hyp_norm": "IT WAS FURTHER DECORATED WITH STRANGE AND SEEMINGLY CABALISTIC FIGURES AND DIAGRAMS SUITABLE TO THE QUAINT TASTE OF THE AGE WHICH HAD BEEN DRAWN IN THE STUCCO WHEN NEWLY LAID ON AND HAD NOW GROWN HARD AND DURABLE FOR THE ADMIRATION OF AFTER TIMES",
"duration_s": 16.51,
"infer_time_s": 11.041,
"rtf": 0.6687,
"wer": 0.0
},
{
"id": "1221-135767-0012",
"ref": "THEY APPROACHED THE DOOR WHICH WAS OF AN ARCHED FORM AND FLANKED ON EACH SIDE BY A NARROW TOWER OR PROJECTION OF THE EDIFICE IN BOTH OF WHICH WERE LATTICE WINDOWS THE WOODEN SHUTTERS TO CLOSE OVER THEM AT NEED",
"hyp": "They approached the door , which was of an arched form and flanked on each side by a narrow tower or projection of the edifice, in both of which were latticed windows, the wooden shutters to close over them at need.",
"ref_norm": "THEY APPROACHED THE DOOR WHICH WAS OF AN ARCHED FORM AND FLANKED ON EACH SIDE BY A NARROW TOWER OR PROJECTION OF THE EDIFICE IN BOTH OF WHICH WERE LATTICE WINDOWS THE WOODEN SHUTTERS TO CLOSE OVER THEM AT NEED",
"hyp_norm": "THEY APPROACHED THE DOOR WHICH WAS OF AN ARCHED FORM AND FLANKED ON EACH SIDE BY A NARROW TOWER OR PROJECTION OF THE EDIFICE IN BOTH OF WHICH WERE LATTICED WINDOWS THE WOODEN SHUTTERS TO CLOSE OVER THEM AT NEED",
"duration_s": 13.885,
"infer_time_s": 9.992,
"rtf": 0.7196,
"wer": 0.025
},
{
"id": "1221-135767-0013",
"ref": "LIFTING THE IRON HAMMER THAT HUNG AT THE PORTAL HESTER PRYNNE GAVE A SUMMONS WHICH WAS ANSWERED BY ONE OF THE GOVERNOR'S BOND SERVANT A FREE BORN ENGLISHMAN BUT NOW A SEVEN YEARS SLAVE",
"hyp": "Lifting the iron hammer that hung at the portal , Hester Prynne gave a summons, which was answered by one of the governor's bond servants , a free-born Englishman, but now a seven years slave.",
"ref_norm": "LIFTING THE IRON HAMMER THAT HUNG AT THE PORTAL HESTER PRYNNE GAVE A SUMMONS WHICH WAS ANSWERED BY ONE OF THE GOVERNORS BOND SERVANT A FREE BORN ENGLISHMAN BUT NOW A SEVEN YEARS SLAVE",
"hyp_norm": "LIFTING THE IRON HAMMER THAT HUNG AT THE PORTAL HESTER PRYNNE GAVE A SUMMONS WHICH WAS ANSWERED BY ONE OF THE GOVERNORS BOND SERVANTS A FREEBORN ENGLISHMAN BUT NOW A SEVEN YEARS SLAVE",
"duration_s": 11.985,
"infer_time_s": 8.894,
"rtf": 0.7421,
"wer": 0.0882
},
{
"id": "1221-135767-0014",
"ref": "YEA HIS HONOURABLE WORSHIP IS WITHIN BUT HE HATH A GODLY MINISTER OR TWO WITH HIM AND LIKEWISE A LEECH",
"hyp": "Yea, his honourable worship is within, but he hath a godly minister or two with him, and likewise a leech.",
"ref_norm": "YEA HIS HONOURABLE WORSHIP IS WITHIN BUT HE HATH A GODLY MINISTER OR TWO WITH HIM AND LIKEWISE A LEECH",
"hyp_norm": "YEA HIS HONOURABLE WORSHIP IS WITHIN BUT HE HATH A GODLY MINISTER OR TWO WITH HIM AND LIKEWISE A LEECH",
"duration_s": 7.07,
"infer_time_s": 5.527,
"rtf": 0.7818,
"wer": 0.0
},
{
"id": "1221-135767-0015",
"ref": "YE MAY NOT SEE HIS WORSHIP NOW",
"hyp": "Ye may not see his worship now.",
"ref_norm": "YE MAY NOT SEE HIS WORSHIP NOW",
"hyp_norm": "YE MAY NOT SEE HIS WORSHIP NOW",
"duration_s": 2.85,
"infer_time_s": 1.831,
"rtf": 0.6423,
"wer": 0.0
},
{
"id": "1221-135767-0016",
"ref": "WITH MANY VARIATIONS SUGGESTED BY THE NATURE OF HIS BUILDING MATERIALS DIVERSITY OF CLIMATE AND A DIFFERENT MODE OF SOCIAL LIFE GOVERNOR BELLINGHAM HAD PLANNED HIS NEW HABITATION AFTER THE RESIDENCES OF GENTLEMEN OF FAIR ESTATE IN HIS NATIVE LAND",
"hyp": "With many variations suggested by the nature of his building materials, diversity of climate, and a different mode of social life , Governor Bellingham had planned his new habitation after the residences of gentlemen of fairest state in his native land.",
"ref_norm": "WITH MANY VARIATIONS SUGGESTED BY THE NATURE OF HIS BUILDING MATERIALS DIVERSITY OF CLIMATE AND A DIFFERENT MODE OF SOCIAL LIFE GOVERNOR BELLINGHAM HAD PLANNED HIS NEW HABITATION AFTER THE RESIDENCES OF GENTLEMEN OF FAIR ESTATE IN HIS NATIVE LAND",
"hyp_norm": "WITH MANY VARIATIONS SUGGESTED BY THE NATURE OF HIS BUILDING MATERIALS DIVERSITY OF CLIMATE AND A DIFFERENT MODE OF SOCIAL LIFE GOVERNOR BELLINGHAM HAD PLANNED HIS NEW HABITATION AFTER THE RESIDENCES OF GENTLEMEN OF FAIREST STATE IN HIS NATIVE LAND",
"duration_s": 15.255,
"infer_time_s": 10.063,
"rtf": 0.6597,
"wer": 0.05
},
{
"id": "1221-135767-0017",
"ref": "ON THE TABLE IN TOKEN THAT THE SENTIMENT OF OLD ENGLISH HOSPITALITY HAD NOT BEEN LEFT BEHIND STOOD A LARGE PEWTER TANKARD AT THE BOTTOM OF WHICH HAD HESTER OR PEARL PEEPED INTO IT THEY MIGHT HAVE SEEN THE FROTHY REMNANT OF A RECENT DRAUGHT OF ALE",
"hyp": "On the table, in token that the sentiment of old English hospitality had not been left behind, stood a large pewter tankard. At the bottom of which, had Hester or Pearl peeped into it , they might have seen the frothy remnant of a recent draught of ale.",
"ref_norm": "ON THE TABLE IN TOKEN THAT THE SENTIMENT OF OLD ENGLISH HOSPITALITY HAD NOT BEEN LEFT BEHIND STOOD A LARGE PEWTER TANKARD AT THE BOTTOM OF WHICH HAD HESTER OR PEARL PEEPED INTO IT THEY MIGHT HAVE SEEN THE FROTHY REMNANT OF A RECENT DRAUGHT OF ALE",
"hyp_norm": "ON THE TABLE IN TOKEN THAT THE SENTIMENT OF OLD ENGLISH HOSPITALITY HAD NOT BEEN LEFT BEHIND STOOD A LARGE PEWTER TANKARD AT THE BOTTOM OF WHICH HAD HESTER OR PEARL PEEPED INTO IT THEY MIGHT HAVE SEEN THE FROTHY REMNANT OF A RECENT DRAUGHT OF ALE",
"duration_s": 16.72,
"infer_time_s": 12.408,
"rtf": 0.7421,
"wer": 0.0
},
{
"id": "1221-135767-0018",
"ref": "LITTLE PEARL WHO WAS AS GREATLY PLEASED WITH THE GLEAMING ARMOUR AS SHE HAD BEEN WITH THE GLITTERING FRONTISPIECE OF THE HOUSE SPENT SOME TIME LOOKING INTO THE POLISHED MIRROR OF THE BREASTPLATE",
"hyp": "Little Pearl, who was as greatly pleased with the gleaming armor as she had been with the glittering front ispiece of the house, spent some time looking into the polished mirror of the breastplate.",
"ref_norm": "LITTLE PEARL WHO WAS AS GREATLY PLEASED WITH THE GLEAMING ARMOUR AS SHE HAD BEEN WITH THE GLITTERING FRONTISPIECE OF THE HOUSE SPENT SOME TIME LOOKING INTO THE POLISHED MIRROR OF THE BREASTPLATE",
"hyp_norm": "LITTLE PEARL WHO WAS AS GREATLY PLEASED WITH THE GLEAMING ARMOR AS SHE HAD BEEN WITH THE GLITTERING FRONT ISPIECE OF THE HOUSE SPENT SOME TIME LOOKING INTO THE POLISHED MIRROR OF THE BREASTPLATE",
"duration_s": 11.16,
"infer_time_s": 8.225,
"rtf": 0.737,
"wer": 0.0909
},
{
"id": "1221-135767-0019",
"ref": "MOTHER CRIED SHE I SEE YOU HERE LOOK LOOK",
"hyp": "Mother cried. She, I see you here. Look, look.",
"ref_norm": "MOTHER CRIED SHE I SEE YOU HERE LOOK LOOK",
"hyp_norm": "MOTHER CRIED SHE I SEE YOU HERE LOOK LOOK",
"duration_s": 3.78,
"infer_time_s": 2.711,
"rtf": 0.7172,
"wer": 0.0
},
{
"id": "1221-135767-0020",
"ref": "IN TRUTH SHE SEEMED ABSOLUTELY HIDDEN BEHIND IT",
"hyp": "In truth, she seemed absolutely hidden behind it.",
"ref_norm": "IN TRUTH SHE SEEMED ABSOLUTELY HIDDEN BEHIND IT",
"hyp_norm": "IN TRUTH SHE SEEMED ABSOLUTELY HIDDEN BEHIND IT",
"duration_s": 3.345,
"infer_time_s": 2.167,
"rtf": 0.6478,
"wer": 0.0
},
{
"id": "1221-135767-0021",
"ref": "PEARL ACCORDINGLY RAN TO THE BOW WINDOW AT THE FURTHER END OF THE HALL AND LOOKED ALONG THE VISTA OF A GARDEN WALK CARPETED WITH CLOSELY SHAVEN GRASS AND BORDERED WITH SOME RUDE AND IMMATURE ATTEMPT AT SHRUBBERY",
"hyp": "Pearl accordingly ran to the bow window at the further end of the hall, and looked along the vista of a garden walk carpeted with closely shaven grass , and bordered with some rude and imitative attempt at shrubbery.",
"ref_norm": "PEARL ACCORDINGLY RAN TO THE BOW WINDOW AT THE FURTHER END OF THE HALL AND LOOKED ALONG THE VISTA OF A GARDEN WALK CARPETED WITH CLOSELY SHAVEN GRASS AND BORDERED WITH SOME RUDE AND IMMATURE ATTEMPT AT SHRUBBERY",
"hyp_norm": "PEARL ACCORDINGLY RAN TO THE BOW WINDOW AT THE FURTHER END OF THE HALL AND LOOKED ALONG THE VISTA OF A GARDEN WALK CARPETED WITH CLOSELY SHAVEN GRASS AND BORDERED WITH SOME RUDE AND IMITATIVE ATTEMPT AT SHRUBBERY",
"duration_s": 12.72,
"infer_time_s": 9.656,
"rtf": 0.7591,
"wer": 0.0263
},
{
"id": "1221-135767-0022",
"ref": "BUT THE PROPRIETOR APPEARED ALREADY TO HAVE RELINQUISHED AS HOPELESS THE EFFORT TO PERPETUATE ON THIS SIDE OF THE ATLANTIC IN A HARD SOIL AND AMID THE CLOSE STRUGGLE FOR SUBSISTENCE THE NATIVE ENGLISH TASTE FOR ORNAMENTAL GARDENING",
"hyp": "But the proprietor appeared already to have relinquished as hopeless the effort to perpetuate on this side of the Atlantic in a hard soil, and amid the close struggle for subs istence, the native English taste for ornamental gardening.",
"ref_norm": "BUT THE PROPRIETOR APPEARED ALREADY TO HAVE RELINQUISHED AS HOPELESS THE EFFORT TO PERPETUATE ON THIS SIDE OF THE ATLANTIC IN A HARD SOIL AND AMID THE CLOSE STRUGGLE FOR SUBSISTENCE THE NATIVE ENGLISH TASTE FOR ORNAMENTAL GARDENING",
"hyp_norm": "BUT THE PROPRIETOR APPEARED ALREADY TO HAVE RELINQUISHED AS HOPELESS THE EFFORT TO PERPETUATE ON THIS SIDE OF THE ATLANTIC IN A HARD SOIL AND AMID THE CLOSE STRUGGLE FOR SUBS ISTENCE THE NATIVE ENGLISH TASTE FOR ORNAMENTAL GARDENING",
"duration_s": 14.395,
"infer_time_s": 9.768,
"rtf": 0.6785,
"wer": 0.0526
},
{
"id": "1221-135767-0023",
"ref": "THERE WERE A FEW ROSE BUSHES HOWEVER AND A NUMBER OF APPLE TREES PROBABLY THE DESCENDANTS OF THOSE PLANTED BY THE REVEREND MISTER BLACKSTONE THE FIRST SETTLER OF THE PENINSULA THAT HALF MYTHOLOGICAL PERSONAGE WHO RIDES THROUGH OUR EARLY ANNALS SEATED ON THE BACK OF A BULL",
"hyp": "There were a few rose bushes, however, and a number of apple trees, probably the descendants of those planted by the Reverend Mr. Black stone, the first sett ler of the peninsula, that heft mythological personage who rides through our early annals seated on the back of a bull.",
"ref_norm": "THERE WERE A FEW ROSE BUSHES HOWEVER AND A NUMBER OF APPLE TREES PROBABLY THE DESCENDANTS OF THOSE PLANTED BY THE REVEREND MISTER BLACKSTONE THE FIRST SETTLER OF THE PENINSULA THAT HALF MYTHOLOGICAL PERSONAGE WHO RIDES THROUGH OUR EARLY ANNALS SEATED ON THE BACK OF A BULL",
"hyp_norm": "THERE WERE A FEW ROSE BUSHES HOWEVER AND A NUMBER OF APPLE TREES PROBABLY THE DESCENDANTS OF THOSE PLANTED BY THE REVEREND MR BLACK STONE THE FIRST SETT LER OF THE PENINSULA THAT HEFT MYTHOLOGICAL PERSONAGE WHO RIDES THROUGH OUR EARLY ANNALS SEATED ON THE BACK OF A BULL",
"duration_s": 16.27,
"infer_time_s": 15.784,
"rtf": 0.9701,
"wer": 0.1277
},
{
"id": "1221-135767-0024",
"ref": "PEARL SEEING THE ROSE BUSHES BEGAN TO CRY FOR A RED ROSE AND WOULD NOT BE PACIFIED",
"hyp": "Pearl seeing the rose bushes began to cry for a red rose and would not be pacified.",
"ref_norm": "PEARL SEEING THE ROSE BUSHES BEGAN TO CRY FOR A RED ROSE AND WOULD NOT BE PACIFIED",
"hyp_norm": "PEARL SEEING THE ROSE BUSHES BEGAN TO CRY FOR A RED ROSE AND WOULD NOT BE PACIFIED",
"duration_s": 5.85,
"infer_time_s": 7.025,
"rtf": 1.2008,
"wer": 0.0
},
{
"id": "1284-1180-0000",
"ref": "HE WORE BLUE SILK STOCKINGS BLUE KNEE PANTS WITH GOLD BUCKLES A BLUE RUFFLED WAIST AND A JACKET OF BRIGHT BLUE BRAIDED WITH GOLD",
"hyp": "He wore blue silk stockings, blue knee pants with gold buckles, a blue ruffled waist, and a jacket of bright blue braided with gold.",
"ref_norm": "HE WORE BLUE SILK STOCKINGS BLUE KNEE PANTS WITH GOLD BUCKLES A BLUE RUFFLED WAIST AND A JACKET OF BRIGHT BLUE BRAIDED WITH GOLD",
"hyp_norm": "HE WORE BLUE SILK STOCKINGS BLUE KNEE PANTS WITH GOLD BUCKLES A BLUE RUFFLED WAIST AND A JACKET OF BRIGHT BLUE BRAIDED WITH GOLD",
"duration_s": 8.12,
"infer_time_s": 11.428,
"rtf": 1.4074,
"wer": 0.0
},
{
"id": "1284-1180-0001",
"ref": "HIS HAT HAD A PEAKED CROWN AND A FLAT BRIM AND AROUND THE BRIM WAS A ROW OF TINY GOLDEN BELLS THAT TINKLED WHEN HE MOVED",
"hyp": "His hat had a peaked crown and a flat brim , and around the brim was a row of tiny golden bells that tinkled when he moved.",
"ref_norm": "HIS HAT HAD A PEAKED CROWN AND A FLAT BRIM AND AROUND THE BRIM WAS A ROW OF TINY GOLDEN BELLS THAT TINKLED WHEN HE MOVED",
"hyp_norm": "HIS HAT HAD A PEAKED CROWN AND A FLAT BRIM AND AROUND THE BRIM WAS A ROW OF TINY GOLDEN BELLS THAT TINKLED WHEN HE MOVED",
"duration_s": 7.755,
"infer_time_s": 10.48,
"rtf": 1.3514,
"wer": 0.0
},
{
"id": "1284-1180-0002",
"ref": "INSTEAD OF SHOES THE OLD MAN WORE BOOTS WITH TURNOVER TOPS AND HIS BLUE COAT HAD WIDE CUFFS OF GOLD BRAID",
"hyp": "Instead of shoes , the old man wore boots with turnover tops, and his blue coat had wide cuffs of gold braid.",
"ref_norm": "INSTEAD OF SHOES THE OLD MAN WORE BOOTS WITH TURNOVER TOPS AND HIS BLUE COAT HAD WIDE CUFFS OF GOLD BRAID",
"hyp_norm": "INSTEAD OF SHOES THE OLD MAN WORE BOOTS WITH TURNOVER TOPS AND HIS BLUE COAT HAD WIDE CUFFS OF GOLD BRAID",
"duration_s": 7.68,
"infer_time_s": 9.166,
"rtf": 1.1935,
"wer": 0.0
},
{
"id": "1284-1180-0003",
"ref": "FOR A LONG TIME HE HAD WISHED TO EXPLORE THE BEAUTIFUL LAND OF OZ IN WHICH THEY LIVED",
"hyp": "For a long time, he had wished to explore the beautiful land of Oz in which they lived.",
"ref_norm": "FOR A LONG TIME HE HAD WISHED TO EXPLORE THE BEAUTIFUL LAND OF OZ IN WHICH THEY LIVED",
"hyp_norm": "FOR A LONG TIME HE HAD WISHED TO EXPLORE THE BEAUTIFUL LAND OF OZ IN WHICH THEY LIVED",
"duration_s": 4.835,
"infer_time_s": 7.677,
"rtf": 1.5877,
"wer": 0.0
},
{
"id": "1284-1180-0004",
"ref": "WHEN THEY WERE OUTSIDE UNC SIMPLY LATCHED THE DOOR AND STARTED UP THE PATH",
"hyp": "When they were outside, Ung simply latched the door and started up the path.",
"ref_norm": "WHEN THEY WERE OUTSIDE UNC SIMPLY LATCHED THE DOOR AND STARTED UP THE PATH",
"hyp_norm": "WHEN THEY WERE OUTSIDE UNG SIMPLY LATCHED THE DOOR AND STARTED UP THE PATH",
"duration_s": 4.285,
"infer_time_s": 6.314,
"rtf": 1.4735,
"wer": 0.0714
},
{
"id": "1284-1180-0005",
"ref": "NO ONE WOULD DISTURB THEIR LITTLE HOUSE EVEN IF ANYONE CAME SO FAR INTO THE THICK FOREST WHILE THEY WERE GONE",
"hyp": "No one would disturb their little house, even if anyone came so far into the thick forest while they were gone.",
"ref_norm": "NO ONE WOULD DISTURB THEIR LITTLE HOUSE EVEN IF ANYONE CAME SO FAR INTO THE THICK FOREST WHILE THEY WERE GONE",
"hyp_norm": "NO ONE WOULD DISTURB THEIR LITTLE HOUSE EVEN IF ANYONE CAME SO FAR INTO THE THICK FOREST WHILE THEY WERE GONE",
"duration_s": 6.55,
"infer_time_s": 9.553,
"rtf": 1.4584,
"wer": 0.0
},
{
"id": "1284-1180-0006",
"ref": "AT THE FOOT OF THE MOUNTAIN THAT SEPARATED THE COUNTRY OF THE MUNCHKINS FROM THE COUNTRY OF THE GILLIKINS THE PATH DIVIDED",
"hyp": "At the foot of the mountain that separated the country of the Munchkins from the country of the Gillikins, the path divided.",
"ref_norm": "AT THE FOOT OF THE MOUNTAIN THAT SEPARATED THE COUNTRY OF THE MUNCHKINS FROM THE COUNTRY OF THE GILLIKINS THE PATH DIVIDED",
"hyp_norm": "AT THE FOOT OF THE MOUNTAIN THAT SEPARATED THE COUNTRY OF THE MUNCHKINS FROM THE COUNTRY OF THE GILLIKINS THE PATH DIVIDED",
"duration_s": 6.865,
"infer_time_s": 10.416,
"rtf": 1.5172,
"wer": 0.0
},
{
"id": "1284-1180-0007",
"ref": "HE KNEW IT WOULD TAKE THEM TO THE HOUSE OF THE CROOKED MAGICIAN WHOM HE HAD NEVER SEEN BUT WHO WAS THEIR NEAREST NEIGHBOR",
"hyp": "He knew it would take them to the house of the crooked magician , whom he had never seen , but who was their nearest neighbor.",
"ref_norm": "HE KNEW IT WOULD TAKE THEM TO THE HOUSE OF THE CROOKED MAGICIAN WHOM HE HAD NEVER SEEN BUT WHO WAS THEIR NEAREST NEIGHBOR",
"hyp_norm": "HE KNEW IT WOULD TAKE THEM TO THE HOUSE OF THE CROOKED MAGICIAN WHOM HE HAD NEVER SEEN BUT WHO WAS THEIR NEAREST NEIGHBOR",
"duration_s": 6.265,
"infer_time_s": 10.825,
"rtf": 1.7278,
"wer": 0.0
},
{
"id": "1284-1180-0008",
"ref": "ALL THE MORNING THEY TRUDGED UP THE MOUNTAIN PATH AND AT NOON UNC AND OJO SAT ON A FALLEN TREE TRUNK AND ATE THE LAST OF THE BREAD WHICH THE OLD MUNCHKIN HAD PLACED IN HIS POCKET",
"hyp": "All the morning they tr udged up the mountain path , and at noon, Unc and O jo sat on a fallen tree trunk and ate the last of the bread which the old Munchkin had placed in his pocket.",
"ref_norm": "ALL THE MORNING THEY TRUDGED UP THE MOUNTAIN PATH AND AT NOON UNC AND OJO SAT ON A FALLEN TREE TRUNK AND ATE THE LAST OF THE BREAD WHICH THE OLD MUNCHKIN HAD PLACED IN HIS POCKET",
"hyp_norm": "ALL THE MORNING THEY TR UDGED UP THE MOUNTAIN PATH AND AT NOON UNC AND O JO SAT ON A FALLEN TREE TRUNK AND ATE THE LAST OF THE BREAD WHICH THE OLD MUNCHKIN HAD PLACED IN HIS POCKET",
"duration_s": 10.49,
"infer_time_s": 15.948,
"rtf": 1.5203,
"wer": 0.1081
},
{
"id": "1284-1180-0009",
"ref": "THEN THEY STARTED ON AGAIN AND TWO HOURS LATER CAME IN SIGHT OF THE HOUSE OF DOCTOR PIPT",
"hyp": "Then they started on again , and two hours later came in sight of the house of Doctor Pipt.",
"ref_norm": "THEN THEY STARTED ON AGAIN AND TWO HOURS LATER CAME IN SIGHT OF THE HOUSE OF DOCTOR PIPT",
"hyp_norm": "THEN THEY STARTED ON AGAIN AND TWO HOURS LATER CAME IN SIGHT OF THE HOUSE OF DOCTOR PIPT",
"duration_s": 6.285,
"infer_time_s": 8.006,
"rtf": 1.2739,
"wer": 0.0
},
{
"id": "1284-1180-0010",
"ref": "UNC KNOCKED AT THE DOOR OF THE HOUSE AND A CHUBBY PLEASANT FACED WOMAN DRESSED ALL IN BLUE OPENED IT AND GREETED THE VISITORS WITH A SMILE",
"hyp": "Unc knocked at the door of the house, and a chubby, pleasant-faced woman dressed all in blue opened it and greeted the visitors with a smile.",
"ref_norm": "UNC KNOCKED AT THE DOOR OF THE HOUSE AND A CHUBBY PLEASANT FACED WOMAN DRESSED ALL IN BLUE OPENED IT AND GREETED THE VISITORS WITH A SMILE",
"hyp_norm": "UNC KNOCKED AT THE DOOR OF THE HOUSE AND A CHUBBY PLEASANTFACED WOMAN DRESSED ALL IN BLUE OPENED IT AND GREETED THE VISITORS WITH A SMILE",
"duration_s": 8.635,
"infer_time_s": 11.012,
"rtf": 1.2753,
"wer": 0.0741
},
{
"id": "1284-1180-0011",
"ref": "I AM MY DEAR AND ALL STRANGERS ARE WELCOME TO MY HOME",
"hyp": "I am, my dear , and all strangers are welcome to my home.",
"ref_norm": "I AM MY DEAR AND ALL STRANGERS ARE WELCOME TO MY HOME",
"hyp_norm": "I AM MY DEAR AND ALL STRANGERS ARE WELCOME TO MY HOME",
"duration_s": 4.275,
"infer_time_s": 5.833,
"rtf": 1.3644,
"wer": 0.0
},
{
"id": "1284-1180-0012",
"ref": "WE HAVE COME FROM A FAR LONELIER PLACE THAN THIS A LONELIER PLACE",
"hyp": "We have come from a far lon elier place than this , a lonelier place.",
"ref_norm": "WE HAVE COME FROM A FAR LONELIER PLACE THAN THIS A LONELIER PLACE",
"hyp_norm": "WE HAVE COME FROM A FAR LON ELIER PLACE THAN THIS A LONELIER PLACE",
"duration_s": 4.88,
"infer_time_s": 6.575,
"rtf": 1.3474,
"wer": 0.1538
},
{
"id": "1284-1180-0013",
"ref": "AND YOU MUST BE OJO THE UNLUCKY SHE ADDED",
"hyp": "And you must be Ojo the unlucky,\" she added.",
"ref_norm": "AND YOU MUST BE OJO THE UNLUCKY SHE ADDED",
"hyp_norm": "AND YOU MUST BE OJO THE UNLUCKY SHE ADDED",
"duration_s": 3.705,
"infer_time_s": 4.333,
"rtf": 1.1696,
"wer": 0.0
},
{
"id": "1284-1180-0014",
"ref": "OJO HAD NEVER EATEN SUCH A FINE MEAL IN ALL HIS LIFE",
"hyp": "Ojo had never eaten such a fine meal in all his life.",
"ref_norm": "OJO HAD NEVER EATEN SUCH A FINE MEAL IN ALL HIS LIFE",
"hyp_norm": "OJO HAD NEVER EATEN SUCH A FINE MEAL IN ALL HIS LIFE",
"duration_s": 3.665,
"infer_time_s": 4.763,
"rtf": 1.2995,
"wer": 0.0
},
{
"id": "1284-1180-0015",
"ref": "WE ARE TRAVELING REPLIED OJO AND WE STOPPED AT YOUR HOUSE JUST TO REST AND REFRESH OURSELVES",
"hyp": "We're traveling,\" replied Ojo, \"and we stopped at your house just to rest and refresh ourselves.\"",
"ref_norm": "WE ARE TRAVELING REPLIED OJO AND WE STOPPED AT YOUR HOUSE JUST TO REST AND REFRESH OURSELVES",
"hyp_norm": "WERE TRAVELING REPLIED OJO AND WE STOPPED AT YOUR HOUSE JUST TO REST AND REFRESH OURSELVES",
"duration_s": 5.835,
"infer_time_s": 4.53,
"rtf": 0.7764,
"wer": 0.1176
},
{
"id": "1284-1180-0016",
"ref": "THE WOMAN SEEMED THOUGHTFUL",
"hyp": "The woman seemed thoughtful.",
"ref_norm": "THE WOMAN SEEMED THOUGHTFUL",
"hyp_norm": "THE WOMAN SEEMED THOUGHTFUL",
"duration_s": 2.13,
"infer_time_s": 1.687,
"rtf": 0.7918,
"wer": 0.0
},
{
"id": "1284-1180-0017",
"ref": "AT ONE END STOOD A GREAT FIREPLACE IN WHICH A BLUE LOG WAS BLAZING WITH A BLUE FLAME AND OVER THE FIRE HUNG FOUR KETTLES IN A ROW ALL BUBBLING AND STEAMING AT A GREAT RATE",
"hyp": "At one end stood a great fireplace, in which a blue log was blazing with a blue flame, and over the fire hung four kettles in a row, all bubbling and steaming at a great rate.",
"ref_norm": "AT ONE END STOOD A GREAT FIREPLACE IN WHICH A BLUE LOG WAS BLAZING WITH A BLUE FLAME AND OVER THE FIRE HUNG FOUR KETTLES IN A ROW ALL BUBBLING AND STEAMING AT A GREAT RATE",
"hyp_norm": "AT ONE END STOOD A GREAT FIREPLACE IN WHICH A BLUE LOG WAS BLAZING WITH A BLUE FLAME AND OVER THE FIRE HUNG FOUR KETTLES IN A ROW ALL BUBBLING AND STEAMING AT A GREAT RATE",
"duration_s": 10.68,
"infer_time_s": 11.512,
"rtf": 1.0779,
"wer": 0.0
},
{
"id": "1284-1180-0018",
"ref": "IT TAKES ME SEVERAL YEARS TO MAKE THIS MAGIC POWDER BUT AT THIS MOMENT I AM PLEASED TO SAY IT IS NEARLY DONE YOU SEE I AM MAKING IT FOR MY GOOD WIFE MARGOLOTTE WHO WANTS TO USE SOME OF IT FOR A PURPOSE OF HER OWN",
"hyp": "It takes me several years to make this magic powder , but at this moment I am pleased to say it is nearly done. You see, I am making it for my good wife Margar ot, who wants to use some of it for a purpose of her own.",
"ref_norm": "IT TAKES ME SEVERAL YEARS TO MAKE THIS MAGIC POWDER BUT AT THIS MOMENT I AM PLEASED TO SAY IT IS NEARLY DONE YOU SEE I AM MAKING IT FOR MY GOOD WIFE MARGOLOTTE WHO WANTS TO USE SOME OF IT FOR A PURPOSE OF HER OWN",
"hyp_norm": "IT TAKES ME SEVERAL YEARS TO MAKE THIS MAGIC POWDER BUT AT THIS MOMENT I AM PLEASED TO SAY IT IS NEARLY DONE YOU SEE I AM MAKING IT FOR MY GOOD WIFE MARGAR OT WHO WANTS TO USE SOME OF IT FOR A PURPOSE OF HER OWN",
"duration_s": 12.005,
"infer_time_s": 10.852,
"rtf": 0.9039,
"wer": 0.0426
},
{
"id": "1284-1180-0019",
"ref": "YOU MUST KNOW SAID MARGOLOTTE WHEN THEY WERE ALL SEATED TOGETHER ON THE BROAD WINDOW SEAT THAT MY HUSBAND FOOLISHLY GAVE AWAY ALL THE POWDER OF LIFE HE FIRST MADE TO OLD MOMBI THE WITCH WHO USED TO LIVE IN THE COUNTRY OF THE GILLIKINS TO THE NORTH OF HERE",
"hyp": "You must know ,\" said Margarot, when they were all seated together on the broad window seat, that my husband foolishly gave away all the powder of life he first made to Old Mombi the witch, who used to live in the country of the Gillikins to the north of here.",
"ref_norm": "YOU MUST KNOW SAID MARGOLOTTE WHEN THEY WERE ALL SEATED TOGETHER ON THE BROAD WINDOW SEAT THAT MY HUSBAND FOOLISHLY GAVE AWAY ALL THE POWDER OF LIFE HE FIRST MADE TO OLD MOMBI THE WITCH WHO USED TO LIVE IN THE COUNTRY OF THE GILLIKINS TO THE NORTH OF HERE",
"hyp_norm": "YOU MUST KNOW SAID MARGAROT WHEN THEY WERE ALL SEATED TOGETHER ON THE BROAD WINDOW SEAT THAT MY HUSBAND FOOLISHLY GAVE AWAY ALL THE POWDER OF LIFE HE FIRST MADE TO OLD MOMBI THE WITCH WHO USED TO LIVE IN THE COUNTRY OF THE GILLIKINS TO THE NORTH OF HERE",
"duration_s": 15.025,
"infer_time_s": 13.423,
"rtf": 0.8934,
"wer": 0.02
},
{
"id": "1284-1180-0020",
"ref": "THE FIRST LOT WE TESTED ON OUR GLASS CAT WHICH NOT ONLY BEGAN TO LIVE BUT HAS LIVED EVER SINCE",
"hyp": "The first lot we tested on our glass cat, which not only began to live but has lived ever since.",
"ref_norm": "THE FIRST LOT WE TESTED ON OUR GLASS CAT WHICH NOT ONLY BEGAN TO LIVE BUT HAS LIVED EVER SINCE",
"hyp_norm": "THE FIRST LOT WE TESTED ON OUR GLASS CAT WHICH NOT ONLY BEGAN TO LIVE BUT HAS LIVED EVER SINCE",
"duration_s": 5.87,
"infer_time_s": 4.566,
"rtf": 0.7778,
"wer": 0.0
},
{
"id": "1284-1180-0021",
"ref": "I THINK THE NEXT GLASS CAT THE MAGICIAN MAKES WILL HAVE NEITHER BRAINS NOR HEART FOR THEN IT WILL NOT OBJECT TO CATCHING MICE AND MAY PROVE OF SOME USE TO US",
"hyp": "I think the next glass cap the magician makes will have neither brains nor heart, for then it will not object to catching mice, and may prove of some use to us.",
"ref_norm": "I THINK THE NEXT GLASS CAT THE MAGICIAN MAKES WILL HAVE NEITHER BRAINS NOR HEART FOR THEN IT WILL NOT OBJECT TO CATCHING MICE AND MAY PROVE OF SOME USE TO US",
"hyp_norm": "I THINK THE NEXT GLASS CAP THE MAGICIAN MAKES WILL HAVE NEITHER BRAINS NOR HEART FOR THEN IT WILL NOT OBJECT TO CATCHING MICE AND MAY PROVE OF SOME USE TO US",
"duration_s": 9.84,
"infer_time_s": 8.05,
"rtf": 0.8181,
"wer": 0.0312
},
{
"id": "1284-1180-0022",
"ref": "I'M AFRAID I DON'T KNOW MUCH ABOUT THE LAND OF OZ",
"hyp": "I'm afraid I don't know much about the land of Oz.",
"ref_norm": "IM AFRAID I DONT KNOW MUCH ABOUT THE LAND OF OZ",
"hyp_norm": "IM AFRAID I DONT KNOW MUCH ABOUT THE LAND OF OZ",
"duration_s": 2.885,
"infer_time_s": 2.793,
"rtf": 0.9681,
"wer": 0.0
},
{
"id": "1284-1180-0023",
"ref": "YOU SEE I'VE LIVED ALL MY LIFE WITH UNC NUNKIE THE SILENT ONE AND THERE WAS NO ONE TO TELL ME ANYTHING",
"hyp": "You see, I've lived all my life with Unc Nunkie, the silent one, and there was no one to tell me anything.",
"ref_norm": "YOU SEE IVE LIVED ALL MY LIFE WITH UNC NUNKIE THE SILENT ONE AND THERE WAS NO ONE TO TELL ME ANYTHING",
"hyp_norm": "YOU SEE IVE LIVED ALL MY LIFE WITH UNC NUNKIE THE SILENT ONE AND THERE WAS NO ONE TO TELL ME ANYTHING",
"duration_s": 5.61,
"infer_time_s": 5.151,
"rtf": 0.9182,
"wer": 0.0
},
{
"id": "1284-1180-0024",
"ref": "THAT IS ONE REASON YOU ARE OJO THE UNLUCKY SAID THE WOMAN IN A SYMPATHETIC TONE",
"hyp": "That is one reason you are Ojo the unlucky ,\" said the woman in sympathetic tone.",
"ref_norm": "THAT IS ONE REASON YOU ARE OJO THE UNLUCKY SAID THE WOMAN IN A SYMPATHETIC TONE",
"hyp_norm": "THAT IS ONE REASON YOU ARE OJO THE UNLUCKY SAID THE WOMAN IN SYMPATHETIC TONE",
"duration_s": 5.26,
"infer_time_s": 3.668,
"rtf": 0.6973,
"wer": 0.0625
},
{
"id": "1284-1180-0025",
"ref": "I THINK I MUST SHOW YOU MY PATCHWORK GIRL SAID MARGOLOTTE LAUGHING AT THE BOY'S ASTONISHMENT FOR SHE IS RATHER DIFFICULT TO EXPLAIN",
"hyp": "I think I must show you my patchwork girl ,\" said Margarot, laughing at the boy's aston ishment, for she is rather difficult to explain.",
"ref_norm": "I THINK I MUST SHOW YOU MY PATCHWORK GIRL SAID MARGOLOTTE LAUGHING AT THE BOYS ASTONISHMENT FOR SHE IS RATHER DIFFICULT TO EXPLAIN",
"hyp_norm": "I THINK I MUST SHOW YOU MY PATCHWORK GIRL SAID MARGAROT LAUGHING AT THE BOYS ASTON ISHMENT FOR SHE IS RATHER DIFFICULT TO EXPLAIN",
"duration_s": 8.705,
"infer_time_s": 6.706,
"rtf": 0.7704,
"wer": 0.1304
},
{
"id": "1284-1180-0026",
"ref": "BUT FIRST I WILL TELL YOU THAT FOR MANY YEARS I HAVE LONGED FOR A SERVANT TO HELP ME WITH THE HOUSEWORK AND TO COOK THE MEALS AND WASH THE DISHES",
"hyp": "But first, I will tell you that for many years I have longed for a servant to help me with the house work and to cook the meals and wash the dishes.",
"ref_norm": "BUT FIRST I WILL TELL YOU THAT FOR MANY YEARS I HAVE LONGED FOR A SERVANT TO HELP ME WITH THE HOUSEWORK AND TO COOK THE MEALS AND WASH THE DISHES",
"hyp_norm": "BUT FIRST I WILL TELL YOU THAT FOR MANY YEARS I HAVE LONGED FOR A SERVANT TO HELP ME WITH THE HOUSE WORK AND TO COOK THE MEALS AND WASH THE DISHES",
"duration_s": 8.29,
"infer_time_s": 6.93,
"rtf": 0.836,
"wer": 0.0645
},
{
"id": "1284-1180-0027",
"ref": "YET THAT TASK WAS NOT SO EASY AS YOU MAY SUPPOSE",
"hyp": "Yet that task was not so easy as you may suppose.",
"ref_norm": "YET THAT TASK WAS NOT SO EASY AS YOU MAY SUPPOSE",
"hyp_norm": "YET THAT TASK WAS NOT SO EASY AS YOU MAY SUPPOSE",
"duration_s": 3.27,
"infer_time_s": 2.355,
"rtf": 0.72,
"wer": 0.0
},
{
"id": "1284-1180-0028",
"ref": "A BED QUILT MADE OF PATCHES OF DIFFERENT KINDS AND COLORS OF CLOTH ALL NEATLY SEWED TOGETHER",
"hyp": "A bed quilt made of patches of different kinds and colors of cloth, all neatly sewed together.",
"ref_norm": "A BED QUILT MADE OF PATCHES OF DIFFERENT KINDS AND COLORS OF CLOTH ALL NEATLY SEWED TOGETHER",
"hyp_norm": "A BED QUILT MADE OF PATCHES OF DIFFERENT KINDS AND COLORS OF CLOTH ALL NEATLY SEWED TOGETHER",
"duration_s": 6.045,
"infer_time_s": 4.353,
"rtf": 0.72,
"wer": 0.0
},
{
"id": "1284-1180-0029",
"ref": "SOMETIMES IT IS CALLED A CRAZY QUILT BECAUSE THE PATCHES AND COLORS ARE SO MIXED UP",
"hyp": "Sometimes it is called a crazy quilt because the patches and colors are so mixed up.",
"ref_norm": "SOMETIMES IT IS CALLED A CRAZY QUILT BECAUSE THE PATCHES AND COLORS ARE SO MIXED UP",
"hyp_norm": "SOMETIMES IT IS CALLED A CRAZY QUILT BECAUSE THE PATCHES AND COLORS ARE SO MIXED UP",
"duration_s": 5.335,
"infer_time_s": 3.432,
"rtf": 0.6434,
"wer": 0.0
},
{
"id": "1284-1180-0030",
"ref": "WHEN I FOUND IT I SAID TO MYSELF THAT IT WOULD DO NICELY FOR MY SERVANT GIRL FOR WHEN SHE WAS BROUGHT TO LIFE SHE WOULD NOT BE PROUD NOR HAUGHTY AS THE GLASS CAT IS FOR SUCH A DREADFUL MIXTURE OF COLORS WOULD DISCOURAGE HER FROM TRYING TO BE AS DIGNIFIED AS THE BLUE MUNCHKINS ARE",
"hyp": "When I found it, I said to myself that it would do nicely for my servant girl. For when she was brought to life, she would not be proud nor ha ughty as the glass cat is. For such a dreadful mixture of colors would discourage her from trying to be as dignified as the blue munchkins are.",
"ref_norm": "WHEN I FOUND IT I SAID TO MYSELF THAT IT WOULD DO NICELY FOR MY SERVANT GIRL FOR WHEN SHE WAS BROUGHT TO LIFE SHE WOULD NOT BE PROUD NOR HAUGHTY AS THE GLASS CAT IS FOR SUCH A DREADFUL MIXTURE OF COLORS WOULD DISCOURAGE HER FROM TRYING TO BE AS DIGNIFIED AS THE BLUE MUNCHKINS ARE",
"hyp_norm": "WHEN I FOUND IT I SAID TO MYSELF THAT IT WOULD DO NICELY FOR MY SERVANT GIRL FOR WHEN SHE WAS BROUGHT TO LIFE SHE WOULD NOT BE PROUD NOR HA UGHTY AS THE GLASS CAT IS FOR SUCH A DREADFUL MIXTURE OF COLORS WOULD DISCOURAGE HER FROM TRYING TO BE AS DIGNIFIED AS THE BLUE MUNCHKINS ARE",
"duration_s": 16.22,
"infer_time_s": 12.671,
"rtf": 0.7812,
"wer": 0.0351
},
{
"id": "1284-1180-0031",
"ref": "AT THE EMERALD CITY WHERE OUR PRINCESS OZMA LIVES GREEN IS THE POPULAR COLOR",
"hyp": "At the Emerald City, where our Princess Ozma lives , green is the popular color.",
"ref_norm": "AT THE EMERALD CITY WHERE OUR PRINCESS OZMA LIVES GREEN IS THE POPULAR COLOR",
"hyp_norm": "AT THE EMERALD CITY WHERE OUR PRINCESS OZMA LIVES GREEN IS THE POPULAR COLOR",
"duration_s": 4.825,
"infer_time_s": 3.557,
"rtf": 0.7373,
"wer": 0.0
},
{
"id": "1284-1180-0032",
"ref": "I WILL SHOW YOU WHAT A GOOD JOB I DID AND SHE WENT TO A TALL CUPBOARD AND THREW OPEN THE DOORS",
"hyp": "I will show you what a good job I did, and she went to a tall cupboard and threw open the doors.",
"ref_norm": "I WILL SHOW YOU WHAT A GOOD JOB I DID AND SHE WENT TO A TALL CUPBOARD AND THREW OPEN THE DOORS",
"hyp_norm": "I WILL SHOW YOU WHAT A GOOD JOB I DID AND SHE WENT TO A TALL CUPBOARD AND THREW OPEN THE DOORS",
"duration_s": 5.78,
"infer_time_s": 4.33,
"rtf": 0.7492,
"wer": 0.0
},
{
"id": "1284-1181-0000",
"ref": "OJO EXAMINED THIS CURIOUS CONTRIVANCE WITH WONDER",
"hyp": "Ojo examined this curious contrivance with wonder.",
"ref_norm": "OJO EXAMINED THIS CURIOUS CONTRIVANCE WITH WONDER",
"hyp_norm": "OJO EXAMINED THIS CURIOUS CONTRIVANCE WITH WONDER",
"duration_s": 3.965,
"infer_time_s": 2.205,
"rtf": 0.556,
"wer": 0.0
},
{
"id": "1284-1181-0001",
"ref": "MARGOLOTTE HAD FIRST MADE THE GIRL'S FORM FROM THE PATCHWORK QUILT AND THEN SHE HAD DRESSED IT WITH A PATCHWORK SKIRT AND AN APRON WITH POCKETS IN IT USING THE SAME GAY MATERIAL THROUGHOUT",
"hyp": "Marguerite had first made the girl's form from the patchwork quilt, and then she had dressed it with a patchwork skirt and an apron with pockets in it, using the same gay material throughout.",
"ref_norm": "MARGOLOTTE HAD FIRST MADE THE GIRLS FORM FROM THE PATCHWORK QUILT AND THEN SHE HAD DRESSED IT WITH A PATCHWORK SKIRT AND AN APRON WITH POCKETS IN IT USING THE SAME GAY MATERIAL THROUGHOUT",
"hyp_norm": "MARGUERITE HAD FIRST MADE THE GIRLS FORM FROM THE PATCHWORK QUILT AND THEN SHE HAD DRESSED IT WITH A PATCHWORK SKIRT AND AN APRON WITH POCKETS IN IT USING THE SAME GAY MATERIAL THROUGHOUT",
"duration_s": 11.43,
"infer_time_s": 8.47,
"rtf": 0.741,
"wer": 0.0294
},
{
"id": "1284-1181-0002",
"ref": "THE HEAD OF THE PATCHWORK GIRL WAS THE MOST CURIOUS PART OF HER",
"hyp": "The head of the patchwork girl was the most curious part of her.",
"ref_norm": "THE HEAD OF THE PATCHWORK GIRL WAS THE MOST CURIOUS PART OF HER",
"hyp_norm": "THE HEAD OF THE PATCHWORK GIRL WAS THE MOST CURIOUS PART OF HER",
"duration_s": 3.835,
"infer_time_s": 2.793,
"rtf": 0.7284,
"wer": 0.0
},
{
"id": "1284-1181-0003",
"ref": "THE HAIR WAS OF BROWN YARN AND HUNG DOWN ON HER NECK IN SEVERAL NEAT BRAIDS",
"hyp": "The hair was of brown yarn and hung down on her neck in several neat braids.",
"ref_norm": "THE HAIR WAS OF BROWN YARN AND HUNG DOWN ON HER NECK IN SEVERAL NEAT BRAIDS",
"hyp_norm": "THE HAIR WAS OF BROWN YARN AND HUNG DOWN ON HER NECK IN SEVERAL NEAT BRAIDS",
"duration_s": 4.505,
"infer_time_s": 3.546,
"rtf": 0.7872,
"wer": 0.0
},
{
"id": "1284-1181-0004",
"ref": "GOLD IS THE MOST COMMON METAL IN THE LAND OF OZ AND IS USED FOR MANY PURPOSES BECAUSE IT IS SOFT AND PLIABLE",
"hyp": "Gold is the most common metal in the land of Oz , and is used for many purposes because it is soft and pliable.",
"ref_norm": "GOLD IS THE MOST COMMON METAL IN THE LAND OF OZ AND IS USED FOR MANY PURPOSES BECAUSE IT IS SOFT AND PLIABLE",
"hyp_norm": "GOLD IS THE MOST COMMON METAL IN THE LAND OF OZ AND IS USED FOR MANY PURPOSES BECAUSE IT IS SOFT AND PLIABLE",
"duration_s": 7.15,
"infer_time_s": 5.101,
"rtf": 0.7134,
"wer": 0.0
},
{
"id": "1284-1181-0005",
"ref": "NO I FORGOT ALL ABOUT THE BRAINS EXCLAIMED THE WOMAN",
"hyp": "No, I forgot all about the brains! Exclaimed the woman.",
"ref_norm": "NO I FORGOT ALL ABOUT THE BRAINS EXCLAIMED THE WOMAN",
"hyp_norm": "NO I FORGOT ALL ABOUT THE BRAINS EXCLAIMED THE WOMAN",
"duration_s": 3.855,
"infer_time_s": 2.702,
"rtf": 0.701,
"wer": 0.0
},
{
"id": "1284-1181-0006",
"ref": "WELL THAT MAY BE TRUE AGREED MARGOLOTTE BUT ON THE CONTRARY A SERVANT WITH TOO MUCH BRAINS IS SURE TO BECOME INDEPENDENT AND HIGH AND MIGHTY AND FEEL ABOVE HER WORK",
"hyp": "Well, that may be true,\" agreed Margar ot, \"but on the contrary, a servant with too much brains is sure to become independent and high and mighty and feel above her work.\"",
"ref_norm": "WELL THAT MAY BE TRUE AGREED MARGOLOTTE BUT ON THE CONTRARY A SERVANT WITH TOO MUCH BRAINS IS SURE TO BECOME INDEPENDENT AND HIGH AND MIGHTY AND FEEL ABOVE HER WORK",
"hyp_norm": "WELL THAT MAY BE TRUE AGREED MARGAR OT BUT ON THE CONTRARY A SERVANT WITH TOO MUCH BRAINS IS SURE TO BECOME INDEPENDENT AND HIGH AND MIGHTY AND FEEL ABOVE HER WORK",
"duration_s": 11.405,
"infer_time_s": 7.886,
"rtf": 0.6915,
"wer": 0.0645
},
{
"id": "1284-1181-0007",
"ref": "SHE POURED INTO THE DISH A QUANTITY FROM EACH OF THESE BOTTLES",
"hyp": "She poured into the dish a quantity from each of these bottles.",
"ref_norm": "SHE POURED INTO THE DISH A QUANTITY FROM EACH OF THESE BOTTLES",
"hyp_norm": "SHE POURED INTO THE DISH A QUANTITY FROM EACH OF THESE BOTTLES",
"duration_s": 4.04,
"infer_time_s": 2.829,
"rtf": 0.7003,
"wer": 0.0
},
{
"id": "1284-1181-0008",
"ref": "I THINK THAT WILL DO SHE CONTINUED FOR THE OTHER QUALITIES ARE NOT NEEDED IN A SERVANT",
"hyp": "I think that will do ,\" she continued, \" for the other qualities are not needed in a servant.\"",
"ref_norm": "I THINK THAT WILL DO SHE CONTINUED FOR THE OTHER QUALITIES ARE NOT NEEDED IN A SERVANT",
"hyp_norm": "I THINK THAT WILL DO SHE CONTINUED FOR THE OTHER QUALITIES ARE NOT NEEDED IN A SERVANT",
"duration_s": 6.08,
"infer_time_s": 4.406,
"rtf": 0.7247,
"wer": 0.0
},
{
"id": "1284-1181-0009",
"ref": "SHE RAN TO HER HUSBAND'S SIDE AT ONCE AND HELPED HIM LIFT THE FOUR KETTLES FROM THE FIRE",
"hyp": "She ran to her husband's side at once and helped him lift the four kettles from the fire.",
"ref_norm": "SHE RAN TO HER HUSBANDS SIDE AT ONCE AND HELPED HIM LIFT THE FOUR KETTLES FROM THE FIRE",
"hyp_norm": "SHE RAN TO HER HUSBANDS SIDE AT ONCE AND HELPED HIM LIFT THE FOUR KETTLES FROM THE FIRE",
"duration_s": 5.245,
"infer_time_s": 4.12,
"rtf": 0.7856,
"wer": 0.0
},
{
"id": "1284-1181-0010",
"ref": "THEIR CONTENTS HAD ALL BOILED AWAY LEAVING IN THE BOTTOM OF EACH KETTLE A FEW GRAINS OF FINE WHITE POWDER",
"hyp": "Their contents had all boiled away, leaving in the bottom of each kettle a few grains of fine white powder.",
"ref_norm": "THEIR CONTENTS HAD ALL BOILED AWAY LEAVING IN THE BOTTOM OF EACH KETTLE A FEW GRAINS OF FINE WHITE POWDER",
"hyp_norm": "THEIR CONTENTS HAD ALL BOILED AWAY LEAVING IN THE BOTTOM OF EACH KETTLE A FEW GRAINS OF FINE WHITE POWDER",
"duration_s": 6.435,
"infer_time_s": 4.528,
"rtf": 0.7037,
"wer": 0.0
},
{
"id": "1284-1181-0011",
"ref": "VERY CAREFULLY THE MAGICIAN REMOVED THIS POWDER PLACING IT ALL TOGETHER IN A GOLDEN DISH WHERE HE MIXED IT WITH A GOLDEN SPOON",
"hyp": "Very carefully, the magician removed this powder , placing it all together in a golden dish, where he mixed it with a golden spoon.",
"ref_norm": "VERY CAREFULLY THE MAGICIAN REMOVED THIS POWDER PLACING IT ALL TOGETHER IN A GOLDEN DISH WHERE HE MIXED IT WITH A GOLDEN SPOON",
"hyp_norm": "VERY CAREFULLY THE MAGICIAN REMOVED THIS POWDER PLACING IT ALL TOGETHER IN A GOLDEN DISH WHERE HE MIXED IT WITH A GOLDEN SPOON",
"duration_s": 7.75,
"infer_time_s": 5.415,
"rtf": 0.6988,
"wer": 0.0
},
{
"id": "1284-1181-0012",
"ref": "NO ONE SAW HIM DO THIS FOR ALL WERE LOOKING AT THE POWDER OF LIFE BUT SOON THE WOMAN REMEMBERED WHAT SHE HAD BEEN DOING AND CAME BACK TO THE CUPBOARD",
"hyp": "No one saw him do this, for all were looking at the powder of life. But soon the woman remembered what she had been doing and came back to the cupboard.",
"ref_norm": "NO ONE SAW HIM DO THIS FOR ALL WERE LOOKING AT THE POWDER OF LIFE BUT SOON THE WOMAN REMEMBERED WHAT SHE HAD BEEN DOING AND CAME BACK TO THE CUPBOARD",
"hyp_norm": "NO ONE SAW HIM DO THIS FOR ALL WERE LOOKING AT THE POWDER OF LIFE BUT SOON THE WOMAN REMEMBERED WHAT SHE HAD BEEN DOING AND CAME BACK TO THE CUPBOARD",
"duration_s": 8.51,
"infer_time_s": 6.929,
"rtf": 0.8142,
"wer": 0.0
},
{
"id": "1284-1181-0013",
"ref": "OJO BECAME A BIT UNEASY AT THIS FOR HE HAD ALREADY PUT QUITE A LOT OF THE CLEVERNESS POWDER IN THE DISH BUT HE DARED NOT INTERFERE AND SO HE COMFORTED HIMSELF WITH THE THOUGHT THAT ONE CANNOT HAVE TOO MUCH CLEVERNESS",
"hyp": "Ojo became a bit uneasy at this, for he had already put quite a lot of the cleverness powder in the dish , but he dared not interfere, and so he comforted himself with the thought that one cannot have too much cleverness.",
"ref_norm": "OJO BECAME A BIT UNEASY AT THIS FOR HE HAD ALREADY PUT QUITE A LOT OF THE CLEVERNESS POWDER IN THE DISH BUT HE DARED NOT INTERFERE AND SO HE COMFORTED HIMSELF WITH THE THOUGHT THAT ONE CANNOT HAVE TOO MUCH CLEVERNESS",
"hyp_norm": "OJO BECAME A BIT UNEASY AT THIS FOR HE HAD ALREADY PUT QUITE A LOT OF THE CLEVERNESS POWDER IN THE DISH BUT HE DARED NOT INTERFERE AND SO HE COMFORTED HIMSELF WITH THE THOUGHT THAT ONE CANNOT HAVE TOO MUCH CLEVERNESS",
"duration_s": 12.66,
"infer_time_s": 11.579,
"rtf": 0.9146,
"wer": 0.0
},
{
"id": "1284-1181-0014",
"ref": "HE SELECTED A SMALL GOLD BOTTLE WITH A PEPPER BOX TOP SO THAT THE POWDER MIGHT BE SPRINKLED ON ANY OBJECT THROUGH THE SMALL HOLES",
"hyp": "He selected a small gold bottle with a pepper box top, so that the powder might be sprinkled on any object through the small holes.",
"ref_norm": "HE SELECTED A SMALL GOLD BOTTLE WITH A PEPPER BOX TOP SO THAT THE POWDER MIGHT BE SPRINKLED ON ANY OBJECT THROUGH THE SMALL HOLES",
"hyp_norm": "HE SELECTED A SMALL GOLD BOTTLE WITH A PEPPER BOX TOP SO THAT THE POWDER MIGHT BE SPRINKLED ON ANY OBJECT THROUGH THE SMALL HOLES",
"duration_s": 7.92,
"infer_time_s": 5.373,
"rtf": 0.6784,
"wer": 0.0
},
{
"id": "1284-1181-0015",
"ref": "MOST PEOPLE TALK TOO MUCH SO IT IS A RELIEF TO FIND ONE WHO TALKS TOO LITTLE",
"hyp": "Most people talk too much, so it is a relief to find one who talks too little.",
"ref_norm": "MOST PEOPLE TALK TOO MUCH SO IT IS A RELIEF TO FIND ONE WHO TALKS TOO LITTLE",
"hyp_norm": "MOST PEOPLE TALK TOO MUCH SO IT IS A RELIEF TO FIND ONE WHO TALKS TOO LITTLE",
"duration_s": 5.115,
"infer_time_s": 3.642,
"rtf": 0.7121,
"wer": 0.0
},
{
"id": "1284-1181-0016",
"ref": "I AM NOT ALLOWED TO PERFORM MAGIC EXCEPT FOR MY OWN AMUSEMENT HE TOLD HIS VISITORS AS HE LIGHTED A PIPE WITH A CROOKED STEM AND BEGAN TO SMOKE",
"hyp": "I am not allowed to perform magic except for my own amusement,\" he told his visitors as he lighted a pipe with a crooked stem and began to smoke.",
"ref_norm": "I AM NOT ALLOWED TO PERFORM MAGIC EXCEPT FOR MY OWN AMUSEMENT HE TOLD HIS VISITORS AS HE LIGHTED A PIPE WITH A CROOKED STEM AND BEGAN TO SMOKE",
"hyp_norm": "I AM NOT ALLOWED TO PERFORM MAGIC EXCEPT FOR MY OWN AMUSEMENT HE TOLD HIS VISITORS AS HE LIGHTED A PIPE WITH A CROOKED STEM AND BEGAN TO SMOKE",
"duration_s": 9.515,
"infer_time_s": 6.493,
"rtf": 0.6824,
"wer": 0.0
},
{
"id": "1284-1181-0017",
"ref": "THE WIZARD OF OZ WHO USED TO BE A HUMBUG AND KNEW NO MAGIC AT ALL HAS BEEN TAKING LESSONS OF GLINDA AND I'M TOLD HE IS GETTING TO BE A PRETTY GOOD WIZARD BUT HE IS MERELY THE ASSISTANT OF THE GREAT SORCERESS",
"hyp": "The Wizard of Oz, who used to be a humbug and knew no magic at all, has been taking lessons of Gl inda, and I'm told he is getting to be a pretty good wizard, but he is merely the assistant of the great sorceress.",
"ref_norm": "THE WIZARD OF OZ WHO USED TO BE A HUMBUG AND KNEW NO MAGIC AT ALL HAS BEEN TAKING LESSONS OF GLINDA AND IM TOLD HE IS GETTING TO BE A PRETTY GOOD WIZARD BUT HE IS MERELY THE ASSISTANT OF THE GREAT SORCERESS",
"hyp_norm": "THE WIZARD OF OZ WHO USED TO BE A HUMBUG AND KNEW NO MAGIC AT ALL HAS BEEN TAKING LESSONS OF GL INDA AND IM TOLD HE IS GETTING TO BE A PRETTY GOOD WIZARD BUT HE IS MERELY THE ASSISTANT OF THE GREAT SORCERESS",
"duration_s": 11.775,
"infer_time_s": 11.445,
"rtf": 0.972,
"wer": 0.0455
},
{
"id": "1284-1181-0018",
"ref": "IT TRULY IS ASSERTED THE MAGICIAN",
"hyp": "It truly is asserted, the magician.",
"ref_norm": "IT TRULY IS ASSERTED THE MAGICIAN",
"hyp_norm": "IT TRULY IS ASSERTED THE MAGICIAN",
"duration_s": 3.16,
"infer_time_s": 2.413,
"rtf": 0.7637,
"wer": 0.0
},
{
"id": "1284-1181-0019",
"ref": "I NOW USE THEM AS ORNAMENTAL STATUARY IN MY GARDEN",
"hyp": "I now use them as ornamental statuary in my garden.",
"ref_norm": "I NOW USE THEM AS ORNAMENTAL STATUARY IN MY GARDEN",
"hyp_norm": "I NOW USE THEM AS ORNAMENTAL STATUARY IN MY GARDEN",
"duration_s": 3.2,
"infer_time_s": 3.413,
"rtf": 1.0665,
"wer": 0.0
},
{
"id": "1284-1181-0020",
"ref": "DEAR ME WHAT A CHATTERBOX YOU'RE GETTING TO BE UNC REMARKED THE MAGICIAN WHO WAS PLEASED WITH THE COMPLIMENT",
"hyp": "Dear me! What a chatterbox you're getting to be , unk,\" remarked the magician, who was pleased with the compliment.",
"ref_norm": "DEAR ME WHAT A CHATTERBOX YOURE GETTING TO BE UNC REMARKED THE MAGICIAN WHO WAS PLEASED WITH THE COMPLIMENT",
"hyp_norm": "DEAR ME WHAT A CHATTERBOX YOURE GETTING TO BE UNK REMARKED THE MAGICIAN WHO WAS PLEASED WITH THE COMPLIMENT",
"duration_s": 6.73,
"infer_time_s": 6.397,
"rtf": 0.9505,
"wer": 0.0526
},
{
"id": "1284-1181-0021",
"ref": "ASKED THE VOICE IN SCORNFUL ACCENTS",
"hyp": "Asked the voice in scornful accents.",
"ref_norm": "ASKED THE VOICE IN SCORNFUL ACCENTS",
"hyp_norm": "ASKED THE VOICE IN SCORNFUL ACCENTS",
"duration_s": 2.7,
"infer_time_s": 1.847,
"rtf": 0.6842,
"wer": 0.0
},
{
"id": "1284-134647-0000",
"ref": "THE GRATEFUL APPLAUSE OF THE CLERGY HAS CONSECRATED THE MEMORY OF A PRINCE WHO INDULGED THEIR PASSIONS AND PROMOTED THEIR INTEREST",
"hyp": "The grateful applause of the clergy has consecrated the memory of a prince who indulged their passions and promoted their interest.",
"ref_norm": "THE GRATEFUL APPLAUSE OF THE CLERGY HAS CONSECRATED THE MEMORY OF A PRINCE WHO INDULGED THEIR PASSIONS AND PROMOTED THEIR INTEREST",
"hyp_norm": "THE GRATEFUL APPLAUSE OF THE CLERGY HAS CONSECRATED THE MEMORY OF A PRINCE WHO INDULGED THEIR PASSIONS AND PROMOTED THEIR INTEREST",
"duration_s": 8.53,
"infer_time_s": 5.509,
"rtf": 0.6458,
"wer": 0.0
},
{
"id": "1284-134647-0001",
"ref": "THE EDICT OF MILAN THE GREAT CHARTER OF TOLERATION HAD CONFIRMED TO EACH INDIVIDUAL OF THE ROMAN WORLD THE PRIVILEGE OF CHOOSING AND PROFESSING HIS OWN RELIGION",
"hyp": "The Edict of Milan, the Great Charter of Toleration, had confirmed to each individual of the Roman world the privilege of choosing and professing his own religion.",
"ref_norm": "THE EDICT OF MILAN THE GREAT CHARTER OF TOLERATION HAD CONFIRMED TO EACH INDIVIDUAL OF THE ROMAN WORLD THE PRIVILEGE OF CHOOSING AND PROFESSING HIS OWN RELIGION",
"hyp_norm": "THE EDICT OF MILAN THE GREAT CHARTER OF TOLERATION HAD CONFIRMED TO EACH INDIVIDUAL OF THE ROMAN WORLD THE PRIVILEGE OF CHOOSING AND PROFESSING HIS OWN RELIGION",
"duration_s": 10.275,
"infer_time_s": 7.058,
"rtf": 0.687,
"wer": 0.0
},
{
"id": "1284-134647-0002",
"ref": "BUT THIS INESTIMABLE PRIVILEGE WAS SOON VIOLATED WITH THE KNOWLEDGE OF TRUTH THE EMPEROR IMBIBED THE MAXIMS OF PERSECUTION AND THE SECTS WHICH DISSENTED FROM THE CATHOLIC CHURCH WERE AFFLICTED AND OPPRESSED BY THE TRIUMPH OF CHRISTIANITY",
"hyp": "But this inestimable privilege was soon violated. With the knowledge of truth, the emperor imbib ed the maxims of persecution, and the sects which dissented from the Catholic Church were afflicted and oppressed by the triumph of Christianity.",
"ref_norm": "BUT THIS INESTIMABLE PRIVILEGE WAS SOON VIOLATED WITH THE KNOWLEDGE OF TRUTH THE EMPEROR IMBIBED THE MAXIMS OF PERSECUTION AND THE SECTS WHICH DISSENTED FROM THE CATHOLIC CHURCH WERE AFFLICTED AND OPPRESSED BY THE TRIUMPH OF CHRISTIANITY",
"hyp_norm": "BUT THIS INESTIMABLE PRIVILEGE WAS SOON VIOLATED WITH THE KNOWLEDGE OF TRUTH THE EMPEROR IMBIB ED THE MAXIMS OF PERSECUTION AND THE SECTS WHICH DISSENTED FROM THE CATHOLIC CHURCH WERE AFFLICTED AND OPPRESSED BY THE TRIUMPH OF CHRISTIANITY",
"duration_s": 15.11,
"infer_time_s": 10.188,
"rtf": 0.6743,
"wer": 0.0541
},
{
"id": "1284-134647-0003",
"ref": "CONSTANTINE EASILY BELIEVED THAT THE HERETICS WHO PRESUMED TO DISPUTE HIS OPINIONS OR TO OPPOSE HIS COMMANDS WERE GUILTY OF THE MOST ABSURD AND CRIMINAL OBSTINACY AND THAT A SEASONABLE APPLICATION OF MODERATE SEVERITIES MIGHT SAVE THOSE UNHAPPY MEN FROM THE DANGER OF AN EVERLASTING CONDEMNATION",
"hyp": "Constantine easily believed that the heretics who presumed to dispute his opinions or to oppose his commands were guilty of the most absurd and criminal obstinacy, and that a seasonable application of moderate sever ities might save those unhappy men from the danger of an everlasting condemnation.",
"ref_norm": "CONSTANTINE EASILY BELIEVED THAT THE HERETICS WHO PRESUMED TO DISPUTE HIS OPINIONS OR TO OPPOSE HIS COMMANDS WERE GUILTY OF THE MOST ABSURD AND CRIMINAL OBSTINACY AND THAT A SEASONABLE APPLICATION OF MODERATE SEVERITIES MIGHT SAVE THOSE UNHAPPY MEN FROM THE DANGER OF AN EVERLASTING CONDEMNATION",
"hyp_norm": "CONSTANTINE EASILY BELIEVED THAT THE HERETICS WHO PRESUMED TO DISPUTE HIS OPINIONS OR TO OPPOSE HIS COMMANDS WERE GUILTY OF THE MOST ABSURD AND CRIMINAL OBSTINACY AND THAT A SEASONABLE APPLICATION OF MODERATE SEVER ITIES MIGHT SAVE THOSE UNHAPPY MEN FROM THE DANGER OF AN EVERLASTING CONDEMNATION",
"duration_s": 20.145,
"infer_time_s": 12.494,
"rtf": 0.6202,
"wer": 0.0435
},
{
"id": "1284-134647-0004",
"ref": "SOME OF THE PENAL REGULATIONS WERE COPIED FROM THE EDICTS OF DIOCLETIAN AND THIS METHOD OF CONVERSION WAS APPLAUDED BY THE SAME BISHOPS WHO HAD FELT THE HAND OF OPPRESSION AND PLEADED FOR THE RIGHTS OF HUMANITY",
"hyp": "Some of the penal regulations were copied from the edicts of Diocletian , and this method of conversion was applauded by the same bishops who had felt the hand of oppression and pleaded for the rights of humanity.",
"ref_norm": "SOME OF THE PENAL REGULATIONS WERE COPIED FROM THE EDICTS OF DIOCLETIAN AND THIS METHOD OF CONVERSION WAS APPLAUDED BY THE SAME BISHOPS WHO HAD FELT THE HAND OF OPPRESSION AND PLEADED FOR THE RIGHTS OF HUMANITY",
"hyp_norm": "SOME OF THE PENAL REGULATIONS WERE COPIED FROM THE EDICTS OF DIOCLETIAN AND THIS METHOD OF CONVERSION WAS APPLAUDED BY THE SAME BISHOPS WHO HAD FELT THE HAND OF OPPRESSION AND PLEADED FOR THE RIGHTS OF HUMANITY",
"duration_s": 12.835,
"infer_time_s": 8.742,
"rtf": 0.6811,
"wer": 0.0
},
{
"id": "1284-134647-0005",
"ref": "THEY ASSERTED WITH CONFIDENCE AND ALMOST WITH EXULTATION THAT THE APOSTOLICAL SUCCESSION WAS INTERRUPTED THAT ALL THE BISHOPS OF EUROPE AND ASIA WERE INFECTED BY THE CONTAGION OF GUILT AND SCHISM AND THAT THE PREROGATIVES OF THE CATHOLIC CHURCH WERE CONFINED TO THE CHOSEN PORTION OF THE AFRICAN BELIEVERS WHO ALONE HAD PRESERVED INVIOLATE THE INTEGRITY OF THEIR FAITH AND DISCIPLINE",
"hyp": "They asserted with confidence and almost with ex ultation that the apost olical succession was interrupted; that all the bishops of Europe and Asia were infected by the contagion of guilt and schism , and that the prerogatives of the Catholic Church were confined to the chosen portion of the African believers, who alone had preserved inviolate the integrity of their faith and discipline.",
"ref_norm": "THEY ASSERTED WITH CONFIDENCE AND ALMOST WITH EXULTATION THAT THE APOSTOLICAL SUCCESSION WAS INTERRUPTED THAT ALL THE BISHOPS OF EUROPE AND ASIA WERE INFECTED BY THE CONTAGION OF GUILT AND SCHISM AND THAT THE PREROGATIVES OF THE CATHOLIC CHURCH WERE CONFINED TO THE CHOSEN PORTION OF THE AFRICAN BELIEVERS WHO ALONE HAD PRESERVED INVIOLATE THE INTEGRITY OF THEIR FAITH AND DISCIPLINE",
"hyp_norm": "THEY ASSERTED WITH CONFIDENCE AND ALMOST WITH EX ULTATION THAT THE APOST OLICAL SUCCESSION WAS INTERRUPTED THAT ALL THE BISHOPS OF EUROPE AND ASIA WERE INFECTED BY THE CONTAGION OF GUILT AND SCHISM AND THAT THE PREROGATIVES OF THE CATHOLIC CHURCH WERE CONFINED TO THE CHOSEN PORTION OF THE AFRICAN BELIEVERS WHO ALONE HAD PRESERVED INVIOLATE THE INTEGRITY OF THEIR FAITH AND DISCIPLINE",
"duration_s": 23.335,
"infer_time_s": 15.194,
"rtf": 0.6511,
"wer": 0.0656
},
{
"id": "1284-134647-0006",
"ref": "BISHOPS VIRGINS AND EVEN SPOTLESS INFANTS WERE SUBJECTED TO THE DISGRACE OF A PUBLIC PENANCE BEFORE THEY COULD BE ADMITTED TO THE COMMUNION OF THE DONATISTS",
"hyp": "Bishops, vir gins, and even spotless infants were subjected to the disgrace of a public penance before they could be admitted to the communion of the Donatists.",
"ref_norm": "BISHOPS VIRGINS AND EVEN SPOTLESS INFANTS WERE SUBJECTED TO THE DISGRACE OF A PUBLIC PENANCE BEFORE THEY COULD BE ADMITTED TO THE COMMUNION OF THE DONATISTS",
"hyp_norm": "BISHOPS VIR GINS AND EVEN SPOTLESS INFANTS WERE SUBJECTED TO THE DISGRACE OF A PUBLIC PENANCE BEFORE THEY COULD BE ADMITTED TO THE COMMUNION OF THE DONATISTS",
"duration_s": 10.155,
"infer_time_s": 7.192,
"rtf": 0.7083,
"wer": 0.0769
},
{
"id": "1284-134647-0007",
"ref": "PROSCRIBED BY THE CIVIL AND ECCLESIASTICAL POWERS OF THE EMPIRE THE DONATISTS STILL MAINTAINED IN SOME PROVINCES PARTICULARLY IN NUMIDIA THEIR SUPERIOR NUMBERS AND FOUR HUNDRED BISHOPS ACKNOWLEDGED THE JURISDICTION OF THEIR PRIMATE",
"hyp": "Prescribed by the civil and ecclesiastical powers of the empire, the Donat ists still maintained , in some provinces, particularly in Numidia, their superior numbers, and four hundred bishops acknowledged the jurisdiction of their primate.",
"ref_norm": "PROSCRIBED BY THE CIVIL AND ECCLESIASTICAL POWERS OF THE EMPIRE THE DONATISTS STILL MAINTAINED IN SOME PROVINCES PARTICULARLY IN NUMIDIA THEIR SUPERIOR NUMBERS AND FOUR HUNDRED BISHOPS ACKNOWLEDGED THE JURISDICTION OF THEIR PRIMATE",
"hyp_norm": "PRESCRIBED BY THE CIVIL AND ECCLESIASTICAL POWERS OF THE EMPIRE THE DONAT ISTS STILL MAINTAINED IN SOME PROVINCES PARTICULARLY IN NUMIDIA THEIR SUPERIOR NUMBERS AND FOUR HUNDRED BISHOPS ACKNOWLEDGED THE JURISDICTION OF THEIR PRIMATE",
"duration_s": 14.17,
"infer_time_s": 10.865,
"rtf": 0.7667,
"wer": 0.0909
},
{
"id": "1320-122612-0000",
"ref": "SINCE THE PERIOD OF OUR TALE THE ACTIVE SPIRIT OF THE COUNTRY HAS SURROUNDED IT WITH A BELT OF RICH AND THRIVING SETTLEMENTS THOUGH NONE BUT THE HUNTER OR THE SAVAGE IS EVER KNOWN EVEN NOW TO PENETRATE ITS WILD RECESSES",
"hyp": "Since the period of our tale, the active spirit of the country has surrounded it with a belt of rich and thriving settlements. Though none but the hunter or the savage is ever known, even now, to penetrate its wild recesses.",
"ref_norm": "SINCE THE PERIOD OF OUR TALE THE ACTIVE SPIRIT OF THE COUNTRY HAS SURROUNDED IT WITH A BELT OF RICH AND THRIVING SETTLEMENTS THOUGH NONE BUT THE HUNTER OR THE SAVAGE IS EVER KNOWN EVEN NOW TO PENETRATE ITS WILD RECESSES",
"hyp_norm": "SINCE THE PERIOD OF OUR TALE THE ACTIVE SPIRIT OF THE COUNTRY HAS SURROUNDED IT WITH A BELT OF RICH AND THRIVING SETTLEMENTS THOUGH NONE BUT THE HUNTER OR THE SAVAGE IS EVER KNOWN EVEN NOW TO PENETRATE ITS WILD RECESSES",
"duration_s": 13.48,
"infer_time_s": 11.777,
"rtf": 0.8736,
"wer": 0.0
},
{
"id": "1320-122612-0001",
"ref": "THE DEWS WERE SUFFERED TO EXHALE AND THE SUN HAD DISPERSED THE MISTS AND WAS SHEDDING A STRONG AND CLEAR LIGHT IN THE FOREST WHEN THE TRAVELERS RESUMED THEIR JOURNEY",
"hyp": "The dews were suffered to exhale, and the sun had dispersed the mists and was shedding a strong and clear light in the forest when the travelers resumed their journey.",
"ref_norm": "THE DEWS WERE SUFFERED TO EXHALE AND THE SUN HAD DISPERSED THE MISTS AND WAS SHEDDING A STRONG AND CLEAR LIGHT IN THE FOREST WHEN THE TRAVELERS RESUMED THEIR JOURNEY",
"hyp_norm": "THE DEWS WERE SUFFERED TO EXHALE AND THE SUN HAD DISPERSED THE MISTS AND WAS SHEDDING A STRONG AND CLEAR LIGHT IN THE FOREST WHEN THE TRAVELERS RESUMED THEIR JOURNEY",
"duration_s": 9.52,
"infer_time_s": 8.704,
"rtf": 0.9143,
"wer": 0.0
},
{
"id": "1320-122612-0002",
"ref": "AFTER PROCEEDING A FEW MILES THE PROGRESS OF HAWKEYE WHO LED THE ADVANCE BECAME MORE DELIBERATE AND WATCHFUL",
"hyp": "After proceeding a few miles, the progress of Hawkeye, who led the advance, became more deliberate and watchful.",
"ref_norm": "AFTER PROCEEDING A FEW MILES THE PROGRESS OF HAWKEYE WHO LED THE ADVANCE BECAME MORE DELIBERATE AND WATCHFUL",
"hyp_norm": "AFTER PROCEEDING A FEW MILES THE PROGRESS OF HAWKEYE WHO LED THE ADVANCE BECAME MORE DELIBERATE AND WATCHFUL",
"duration_s": 7.46,
"infer_time_s": 6.196,
"rtf": 0.8305,
"wer": 0.0
},
{
"id": "1320-122612-0003",
"ref": "HE OFTEN STOPPED TO EXAMINE THE TREES NOR DID HE CROSS A RIVULET WITHOUT ATTENTIVELY CONSIDERING THE QUANTITY THE VELOCITY AND THE COLOR OF ITS WATERS",
"hyp": "He often stopped to examine the trees, nor did he cross a riv ulet without attentively considering the quantity, the velocity, and the color of its waters.",
"ref_norm": "HE OFTEN STOPPED TO EXAMINE THE TREES NOR DID HE CROSS A RIVULET WITHOUT ATTENTIVELY CONSIDERING THE QUANTITY THE VELOCITY AND THE COLOR OF ITS WATERS",
"hyp_norm": "HE OFTEN STOPPED TO EXAMINE THE TREES NOR DID HE CROSS A RIV ULET WITHOUT ATTENTIVELY CONSIDERING THE QUANTITY THE VELOCITY AND THE COLOR OF ITS WATERS",
"duration_s": 9.865,
"infer_time_s": 7.91,
"rtf": 0.8019,
"wer": 0.0769
},
{
"id": "1320-122612-0004",
"ref": "DISTRUSTING HIS OWN JUDGMENT HIS APPEALS TO THE OPINION OF CHINGACHGOOK WERE FREQUENT AND EARNEST",
"hyp": "Distrusting his own judgment, his appeals to the opinion of Chingachgook were frequent and earnest.",
"ref_norm": "DISTRUSTING HIS OWN JUDGMENT HIS APPEALS TO THE OPINION OF CHINGACHGOOK WERE FREQUENT AND EARNEST",
"hyp_norm": "DISTRUSTING HIS OWN JUDGMENT HIS APPEALS TO THE OPINION OF CHINGACHGOOK WERE FREQUENT AND EARNEST",
"duration_s": 6.425,
"infer_time_s": 5.676,
"rtf": 0.8835,
"wer": 0.0
},
{
"id": "1320-122612-0005",
"ref": "YET HERE ARE WE WITHIN A SHORT RANGE OF THE SCAROONS AND NOT A SIGN OF A TRAIL HAVE WE CROSSED",
"hyp": "Yet here are we , within a short range of the Scarr uns, and not a sign of a trail have we crossed.",
"ref_norm": "YET HERE ARE WE WITHIN A SHORT RANGE OF THE SCAROONS AND NOT A SIGN OF A TRAIL HAVE WE CROSSED",
"hyp_norm": "YET HERE ARE WE WITHIN A SHORT RANGE OF THE SCARR UNS AND NOT A SIGN OF A TRAIL HAVE WE CROSSED",
"duration_s": 5.915,
"infer_time_s": 5.861,
"rtf": 0.9909,
"wer": 0.0952
},
{
"id": "1320-122612-0006",
"ref": "LET US RETRACE OUR STEPS AND EXAMINE AS WE GO WITH KEENER EYES",
"hyp": "Let us retrace our steps and examine as we go with keener eyes.",
"ref_norm": "LET US RETRACE OUR STEPS AND EXAMINE AS WE GO WITH KEENER EYES",
"hyp_norm": "LET US RETRACE OUR STEPS AND EXAMINE AS WE GO WITH KEENER EYES",
"duration_s": 4.845,
"infer_time_s": 4.053,
"rtf": 0.8365,
"wer": 0.0
},
{
"id": "1320-122612-0007",
"ref": "CHINGACHGOOK HAD CAUGHT THE LOOK AND MOTIONING WITH HIS HAND HE BADE HIM SPEAK",
"hyp": "Chingachgook had caught the look, and motioning with his hand, he bade him speak.",
"ref_norm": "CHINGACHGOOK HAD CAUGHT THE LOOK AND MOTIONING WITH HIS HAND HE BADE HIM SPEAK",
"hyp_norm": "CHINGACHGOOK HAD CAUGHT THE LOOK AND MOTIONING WITH HIS HAND HE BADE HIM SPEAK",
"duration_s": 5.54,
"infer_time_s": 5.295,
"rtf": 0.9558,
"wer": 0.0
},
{
"id": "1320-122612-0008",
"ref": "THE EYES OF THE WHOLE PARTY FOLLOWED THE UNEXPECTED MOVEMENT AND READ THEIR SUCCESS IN THE AIR OF TRIUMPH THAT THE YOUTH ASSUMED",
"hyp": "The eyes of the whole party followed the unexpected movement and read their success in the air of triumph that the youth assumed.",
"ref_norm": "THE EYES OF THE WHOLE PARTY FOLLOWED THE UNEXPECTED MOVEMENT AND READ THEIR SUCCESS IN THE AIR OF TRIUMPH THAT THE YOUTH ASSUMED",
"hyp_norm": "THE EYES OF THE WHOLE PARTY FOLLOWED THE UNEXPECTED MOVEMENT AND READ THEIR SUCCESS IN THE AIR OF TRIUMPH THAT THE YOUTH ASSUMED",
"duration_s": 7.875,
"infer_time_s": 5.967,
"rtf": 0.7577,
"wer": 0.0
},
{
"id": "1320-122612-0009",
"ref": "IT WOULD HAVE BEEN MORE WONDERFUL HAD HE SPOKEN WITHOUT A BIDDING",
"hyp": "It would have been more wonderful had he spoken without a bidding.",
"ref_norm": "IT WOULD HAVE BEEN MORE WONDERFUL HAD HE SPOKEN WITHOUT A BIDDING",
"hyp_norm": "IT WOULD HAVE BEEN MORE WONDERFUL HAD HE SPOKEN WITHOUT A BIDDING",
"duration_s": 3.88,
"infer_time_s": 3.095,
"rtf": 0.7976,
"wer": 0.0
},
{
"id": "1320-122612-0010",
"ref": "SEE SAID UNCAS POINTING NORTH AND SOUTH AT THE EVIDENT MARKS OF THE BROAD TRAIL ON EITHER SIDE OF HIM THE DARK HAIR HAS GONE TOWARD THE FOREST",
"hyp": "See,\" said Uncas, pointing north and south at the evident marks of the broad trail on either side of him. The dark hair has gone toward the forest.",
"ref_norm": "SEE SAID UNCAS POINTING NORTH AND SOUTH AT THE EVIDENT MARKS OF THE BROAD TRAIL ON EITHER SIDE OF HIM THE DARK HAIR HAS GONE TOWARD THE FOREST",
"hyp_norm": "SEE SAID UNCAS POINTING NORTH AND SOUTH AT THE EVIDENT MARKS OF THE BROAD TRAIL ON EITHER SIDE OF HIM THE DARK HAIR HAS GONE TOWARD THE FOREST",
"duration_s": 10.195,
"infer_time_s": 8.526,
"rtf": 0.8362,
"wer": 0.0
},
{
"id": "1320-122612-0011",
"ref": "IF A ROCK OR A RIVULET OR A BIT OF EARTH HARDER THAN COMMON SEVERED THE LINKS OF THE CLEW THEY FOLLOWED THE TRUE EYE OF THE SCOUT RECOVERED THEM AT A DISTANCE AND SELDOM RENDERED THE DELAY OF A SINGLE MOMENT NECESSARY",
"hyp": "If a rock or a riv ulet or a bit of earth harder than common severed the links of the clue, they followed. The true eye of the scout recovered them at a distance, and seldom rendered the delay of a single moment necessary.",
"ref_norm": "IF A ROCK OR A RIVULET OR A BIT OF EARTH HARDER THAN COMMON SEVERED THE LINKS OF THE CLEW THEY FOLLOWED THE TRUE EYE OF THE SCOUT RECOVERED THEM AT A DISTANCE AND SELDOM RENDERED THE DELAY OF A SINGLE MOMENT NECESSARY",
"hyp_norm": "IF A ROCK OR A RIV ULET OR A BIT OF EARTH HARDER THAN COMMON SEVERED THE LINKS OF THE CLUE THEY FOLLOWED THE TRUE EYE OF THE SCOUT RECOVERED THEM AT A DISTANCE AND SELDOM RENDERED THE DELAY OF A SINGLE MOMENT NECESSARY",
"duration_s": 13.695,
"infer_time_s": 11.825,
"rtf": 0.8635,
"wer": 0.0698
},
{
"id": "1320-122612-0012",
"ref": "EXTINGUISHED BRANDS WERE LYING AROUND A SPRING THE OFFALS OF A DEER WERE SCATTERED ABOUT THE PLACE AND THE TREES BORE EVIDENT MARKS OF HAVING BEEN BROWSED BY THE HORSES",
"hyp": "Extinguished brands were lying around a spring . The offals of a deer were scattered about the place , and the trees bore evident marks of having been browsed by the horses.",
"ref_norm": "EXTINGUISHED BRANDS WERE LYING AROUND A SPRING THE OFFALS OF A DEER WERE SCATTERED ABOUT THE PLACE AND THE TREES BORE EVIDENT MARKS OF HAVING BEEN BROWSED BY THE HORSES",
"hyp_norm": "EXTINGUISHED BRANDS WERE LYING AROUND A SPRING THE OFFALS OF A DEER WERE SCATTERED ABOUT THE PLACE AND THE TREES BORE EVIDENT MARKS OF HAVING BEEN BROWSED BY THE HORSES",
"duration_s": 10.49,
"infer_time_s": 9.561,
"rtf": 0.9115,
"wer": 0.0
},
{
"id": "1320-122612-0013",
"ref": "A CIRCLE OF A FEW HUNDRED FEET IN CIRCUMFERENCE WAS DRAWN AND EACH OF THE PARTY TOOK A SEGMENT FOR HIS PORTION",
"hyp": "A circle of a few hundred feet in circumference was drawn, and each of the party took a segment for his portion.",
"ref_norm": "A CIRCLE OF A FEW HUNDRED FEET IN CIRCUMFERENCE WAS DRAWN AND EACH OF THE PARTY TOOK A SEGMENT FOR HIS PORTION",
"hyp_norm": "A CIRCLE OF A FEW HUNDRED FEET IN CIRCUMFERENCE WAS DRAWN AND EACH OF THE PARTY TOOK A SEGMENT FOR HIS PORTION",
"duration_s": 6.55,
"infer_time_s": 5.891,
"rtf": 0.8994,
"wer": 0.0
},
{
"id": "1320-122612-0014",
"ref": "THE EXAMINATION HOWEVER RESULTED IN NO DISCOVERY",
"hyp": "The examination, however, resulted in no discovery.",
"ref_norm": "THE EXAMINATION HOWEVER RESULTED IN NO DISCOVERY",
"hyp_norm": "THE EXAMINATION HOWEVER RESULTED IN NO DISCOVERY",
"duration_s": 3.515,
"infer_time_s": 2.571,
"rtf": 0.7314,
"wer": 0.0
},
{
"id": "1320-122612-0015",
"ref": "THE WHOLE PARTY CROWDED TO THE SPOT WHERE UNCAS POINTED OUT THE IMPRESSION OF A MOCCASIN IN THE MOIST ALLUVION",
"hyp": "The whole party crowded to the spot where Uncas pointed out the impression of a moccasin in the moist alluvion.",
"ref_norm": "THE WHOLE PARTY CROWDED TO THE SPOT WHERE UNCAS POINTED OUT THE IMPRESSION OF A MOCCASIN IN THE MOIST ALLUVION",
"hyp_norm": "THE WHOLE PARTY CROWDED TO THE SPOT WHERE UNCAS POINTED OUT THE IMPRESSION OF A MOCCASIN IN THE MOIST ALLUVION",
"duration_s": 6.385,
"infer_time_s": 6.221,
"rtf": 0.9743,
"wer": 0.0
},
{
"id": "1320-122612-0016",
"ref": "RUN BACK UNCAS AND BRING ME THE SIZE OF THE SINGER'S FOOT",
"hyp": "Run back, Uncas, and bring me the size of the singer's foot.",
"ref_norm": "RUN BACK UNCAS AND BRING ME THE SIZE OF THE SINGERS FOOT",
"hyp_norm": "RUN BACK UNCAS AND BRING ME THE SIZE OF THE SINGERS FOOT",
"duration_s": 3.49,
"infer_time_s": 3.789,
"rtf": 1.0856,
"wer": 0.0
},
{
"id": "1320-122617-0000",
"ref": "NOTWITHSTANDING THE HIGH RESOLUTION OF HAWKEYE HE FULLY COMPREHENDED ALL THE DIFFICULTIES AND DANGER HE WAS ABOUT TO INCUR",
"hyp": "Notwithstanding the high resolution of Hawkey e, he fully comprehended all the difficulties and danger he was about to incur.",
"ref_norm": "NOTWITHSTANDING THE HIGH RESOLUTION OF HAWKEYE HE FULLY COMPREHENDED ALL THE DIFFICULTIES AND DANGER HE WAS ABOUT TO INCUR",
"hyp_norm": "NOTWITHSTANDING THE HIGH RESOLUTION OF HAWKEY E HE FULLY COMPREHENDED ALL THE DIFFICULTIES AND DANGER HE WAS ABOUT TO INCUR",
"duration_s": 7.835,
"infer_time_s": 6.175,
"rtf": 0.7881,
"wer": 0.1053
},
{
"id": "1320-122617-0001",
"ref": "IN HIS RETURN TO THE CAMP HIS ACUTE AND PRACTISED INTELLECTS WERE INTENTLY ENGAGED IN DEVISING MEANS TO COUNTERACT A WATCHFULNESS AND SUSPICION ON THE PART OF HIS ENEMIES THAT HE KNEW WERE IN NO DEGREE INFERIOR TO HIS OWN",
"hyp": "In his return to the camp, his acute and practised intellects were intently engaged in devising means to counteract a watchfulness and suspicion on the part of his enemies , that he knew were in no degree inferior to his own.",
"ref_norm": "IN HIS RETURN TO THE CAMP HIS ACUTE AND PRACTISED INTELLECTS WERE INTENTLY ENGAGED IN DEVISING MEANS TO COUNTERACT A WATCHFULNESS AND SUSPICION ON THE PART OF HIS ENEMIES THAT HE KNEW WERE IN NO DEGREE INFERIOR TO HIS OWN",
"hyp_norm": "IN HIS RETURN TO THE CAMP HIS ACUTE AND PRACTISED INTELLECTS WERE INTENTLY ENGAGED IN DEVISING MEANS TO COUNTERACT A WATCHFULNESS AND SUSPICION ON THE PART OF HIS ENEMIES THAT HE KNEW WERE IN NO DEGREE INFERIOR TO HIS OWN",
"duration_s": 14.055,
"infer_time_s": 12.504,
"rtf": 0.8896,
"wer": 0.0
},
{
"id": "1320-122617-0002",
"ref": "IN OTHER WORDS WHILE HE HAD IMPLICIT FAITH IN THE ABILITY OF BALAAM'S ASS TO SPEAK HE WAS SOMEWHAT SKEPTICAL ON THE SUBJECT OF A BEAR'S SINGING AND YET HE HAD BEEN ASSURED OF THE LATTER ON THE TESTIMONY OF HIS OWN EXQUISITE ORGANS",
"hyp": "In other words, while he had implicit faith in the ability of Balaam's ass to speak, he was somewhat skeptical on the subject of a bear's singing, and yet he had been assured of the latter on the testimony of his own exquisite organs.",
"ref_norm": "IN OTHER WORDS WHILE HE HAD IMPLICIT FAITH IN THE ABILITY OF BALAAMS ASS TO SPEAK HE WAS SOMEWHAT SKEPTICAL ON THE SUBJECT OF A BEARS SINGING AND YET HE HAD BEEN ASSURED OF THE LATTER ON THE TESTIMONY OF HIS OWN EXQUISITE ORGANS",
"hyp_norm": "IN OTHER WORDS WHILE HE HAD IMPLICIT FAITH IN THE ABILITY OF BALAAMS ASS TO SPEAK HE WAS SOMEWHAT SKEPTICAL ON THE SUBJECT OF A BEARS SINGING AND YET HE HAD BEEN ASSURED OF THE LATTER ON THE TESTIMONY OF HIS OWN EXQUISITE ORGANS",
"duration_s": 13.585,
"infer_time_s": 12.43,
"rtf": 0.915,
"wer": 0.0
},
{
"id": "1320-122617-0003",
"ref": "THERE WAS SOMETHING IN HIS AIR AND MANNER THAT BETRAYED TO THE SCOUT THE UTTER CONFUSION OF THE STATE OF HIS MIND",
"hyp": "There was something in his air and manner that betrayed to the scout the utter confusion of the state of his mind.",
"ref_norm": "THERE WAS SOMETHING IN HIS AIR AND MANNER THAT BETRAYED TO THE SCOUT THE UTTER CONFUSION OF THE STATE OF HIS MIND",
"hyp_norm": "THERE WAS SOMETHING IN HIS AIR AND MANNER THAT BETRAYED TO THE SCOUT THE UTTER CONFUSION OF THE STATE OF HIS MIND",
"duration_s": 6.285,
"infer_time_s": 5.673,
"rtf": 0.9026,
"wer": 0.0
},
{
"id": "1320-122617-0004",
"ref": "THE INGENIOUS HAWKEYE WHO RECALLED THE HASTY MANNER IN WHICH THE OTHER HAD ABANDONED HIS POST AT THE BEDSIDE OF THE SICK WOMAN WAS NOT WITHOUT HIS SUSPICIONS CONCERNING THE SUBJECT OF SO MUCH SOLEMN DELIBERATION",
"hyp": "The ingenious Haw keye, who recalled the hasty manner in which the other had abandoned his post at the bedside of the sick woman, was not without his suspicions concerning the subject of so much solemn deliberation.",
"ref_norm": "THE INGENIOUS HAWKEYE WHO RECALLED THE HASTY MANNER IN WHICH THE OTHER HAD ABANDONED HIS POST AT THE BEDSIDE OF THE SICK WOMAN WAS NOT WITHOUT HIS SUSPICIONS CONCERNING THE SUBJECT OF SO MUCH SOLEMN DELIBERATION",
"hyp_norm": "THE INGENIOUS HAW KEYE WHO RECALLED THE HASTY MANNER IN WHICH THE OTHER HAD ABANDONED HIS POST AT THE BEDSIDE OF THE SICK WOMAN WAS NOT WITHOUT HIS SUSPICIONS CONCERNING THE SUBJECT OF SO MUCH SOLEMN DELIBERATION",
"duration_s": 12.26,
"infer_time_s": 10.899,
"rtf": 0.889,
"wer": 0.0556
},
{
"id": "1320-122617-0005",
"ref": "THE BEAR SHOOK HIS SHAGGY SIDES AND THEN A WELL KNOWN VOICE REPLIED",
"hyp": "The bear shook his sh aggy sides, and then a well -known voice replied.",
"ref_norm": "THE BEAR SHOOK HIS SHAGGY SIDES AND THEN A WELL KNOWN VOICE REPLIED",
"hyp_norm": "THE BEAR SHOOK HIS SH AGGY SIDES AND THEN A WELL KNOWN VOICE REPLIED",
"duration_s": 4.4,
"infer_time_s": 4.184,
"rtf": 0.9508,
"wer": 0.1538
},
{
"id": "1320-122617-0006",
"ref": "CAN THESE THINGS BE RETURNED DAVID BREATHING MORE FREELY AS THE TRUTH BEGAN TO DAWN UPON HIM",
"hyp": "Can these things be returned, David? Breathing more freely as the truth began to dawn upon him.",
"ref_norm": "CAN THESE THINGS BE RETURNED DAVID BREATHING MORE FREELY AS THE TRUTH BEGAN TO DAWN UPON HIM",
"hyp_norm": "CAN THESE THINGS BE RETURNED DAVID BREATHING MORE FREELY AS THE TRUTH BEGAN TO DAWN UPON HIM",
"duration_s": 5.655,
"infer_time_s": 4.93,
"rtf": 0.8718,
"wer": 0.0
},
{
"id": "1320-122617-0007",
"ref": "COME COME RETURNED HAWKEYE UNCASING HIS HONEST COUNTENANCE THE BETTER TO ASSURE THE WAVERING CONFIDENCE OF HIS COMPANION YOU MAY SEE A SKIN WHICH IF IT BE NOT AS WHITE AS ONE OF THE GENTLE ONES HAS NO TINGE OF RED TO IT THAT THE WINDS OF THE HEAVEN AND THE SUN HAVE NOT BESTOWED NOW LET US TO BUSINESS",
"hyp": "Come, come! Returned Haw keye, uncasing his honest countenance, the better to assure the wavering confidence of his companion. You may see a skin which , if it be not as white as one of the gentle ones, has no tinge of red to it that the winds of the heaven and the sun have not bestowed. Now let us to business.",
"ref_norm": "COME COME RETURNED HAWKEYE UNCASING HIS HONEST COUNTENANCE THE BETTER TO ASSURE THE WAVERING CONFIDENCE OF HIS COMPANION YOU MAY SEE A SKIN WHICH IF IT BE NOT AS WHITE AS ONE OF THE GENTLE ONES HAS NO TINGE OF RED TO IT THAT THE WINDS OF THE HEAVEN AND THE SUN HAVE NOT BESTOWED NOW LET US TO BUSINESS",
"hyp_norm": "COME COME RETURNED HAW KEYE UNCASING HIS HONEST COUNTENANCE THE BETTER TO ASSURE THE WAVERING CONFIDENCE OF HIS COMPANION YOU MAY SEE A SKIN WHICH IF IT BE NOT AS WHITE AS ONE OF THE GENTLE ONES HAS NO TINGE OF RED TO IT THAT THE WINDS OF THE HEAVEN AND THE SUN HAVE NOT BESTOWED NOW LET US TO BUSINESS",
"duration_s": 18.525,
"infer_time_s": 17.666,
"rtf": 0.9536,
"wer": 0.0333
},
{
"id": "1320-122617-0008",
"ref": "THE YOUNG MAN IS IN BONDAGE AND MUCH I FEAR HIS DEATH IS DECREED",
"hyp": "The young man is in bondage, and much I fear his death is decreed.",
"ref_norm": "THE YOUNG MAN IS IN BONDAGE AND MUCH I FEAR HIS DEATH IS DECREED",
"hyp_norm": "THE YOUNG MAN IS IN BONDAGE AND MUCH I FEAR HIS DEATH IS DECREED",
"duration_s": 4.185,
"infer_time_s": 4.237,
"rtf": 1.0123,
"wer": 0.0
},
{
"id": "1320-122617-0009",
"ref": "I GREATLY MOURN THAT ONE SO WELL DISPOSED SHOULD DIE IN HIS IGNORANCE AND I HAVE SOUGHT A GOODLY HYMN CAN YOU LEAD ME TO HIM",
"hyp": "I greatly mourn that one so well disposed should die in his ignorance, and I have sought a goodly him. Can you lead me to him?",
"ref_norm": "I GREATLY MOURN THAT ONE SO WELL DISPOSED SHOULD DIE IN HIS IGNORANCE AND I HAVE SOUGHT A GOODLY HYMN CAN YOU LEAD ME TO HIM",
"hyp_norm": "I GREATLY MOURN THAT ONE SO WELL DISPOSED SHOULD DIE IN HIS IGNORANCE AND I HAVE SOUGHT A GOODLY HIM CAN YOU LEAD ME TO HIM",
"duration_s": 7.705,
"infer_time_s": 7.052,
"rtf": 0.9153,
"wer": 0.0385
},
{
"id": "1320-122617-0010",
"ref": "THE TASK WILL NOT BE DIFFICULT RETURNED DAVID HESITATING THOUGH I GREATLY FEAR YOUR PRESENCE WOULD RATHER INCREASE THAN MITIGATE HIS UNHAPPY FORTUNES",
"hyp": "The task will not be difficult,\" returned David , hesitating. Though I greatly fear your presence would rather increase than mitigate his unhappy fortunes.",
"ref_norm": "THE TASK WILL NOT BE DIFFICULT RETURNED DAVID HESITATING THOUGH I GREATLY FEAR YOUR PRESENCE WOULD RATHER INCREASE THAN MITIGATE HIS UNHAPPY FORTUNES",
"hyp_norm": "THE TASK WILL NOT BE DIFFICULT RETURNED DAVID HESITATING THOUGH I GREATLY FEAR YOUR PRESENCE WOULD RATHER INCREASE THAN MITIGATE HIS UNHAPPY FORTUNES",
"duration_s": 10.0,
"infer_time_s": 7.207,
"rtf": 0.7207,
"wer": 0.0
},
{
"id": "1320-122617-0011",
"ref": "THE LODGE IN WHICH UNCAS WAS CONFINED WAS IN THE VERY CENTER OF THE VILLAGE AND IN A SITUATION PERHAPS MORE DIFFICULT THAN ANY OTHER TO APPROACH OR LEAVE WITHOUT OBSERVATION",
"hyp": "The lodge in which Unc as was confined was in the very center of the village, and in a situation perhaps more difficult than any other to approach or leave without observation.",
"ref_norm": "THE LODGE IN WHICH UNCAS WAS CONFINED WAS IN THE VERY CENTER OF THE VILLAGE AND IN A SITUATION PERHAPS MORE DIFFICULT THAN ANY OTHER TO APPROACH OR LEAVE WITHOUT OBSERVATION",
"hyp_norm": "THE LODGE IN WHICH UNC AS WAS CONFINED WAS IN THE VERY CENTER OF THE VILLAGE AND IN A SITUATION PERHAPS MORE DIFFICULT THAN ANY OTHER TO APPROACH OR LEAVE WITHOUT OBSERVATION",
"duration_s": 9.76,
"infer_time_s": 8.25,
"rtf": 0.8453,
"wer": 0.0645
},
{
"id": "1320-122617-0012",
"ref": "FOUR OR FIVE OF THE LATTER ONLY LINGERED ABOUT THE DOOR OF THE PRISON OF UNCAS WARY BUT CLOSE OBSERVERS OF THE MANNER OF THEIR CAPTIVE",
"hyp": "Four or five of the latter only lingered about the door of the prison of Uncas, wary but close observers of the manner of their captive.",
"ref_norm": "FOUR OR FIVE OF THE LATTER ONLY LINGERED ABOUT THE DOOR OF THE PRISON OF UNCAS WARY BUT CLOSE OBSERVERS OF THE MANNER OF THEIR CAPTIVE",
"hyp_norm": "FOUR OR FIVE OF THE LATTER ONLY LINGERED ABOUT THE DOOR OF THE PRISON OF UNCAS WARY BUT CLOSE OBSERVERS OF THE MANNER OF THEIR CAPTIVE",
"duration_s": 7.59,
"infer_time_s": 7.076,
"rtf": 0.9322,
"wer": 0.0
},
{
"id": "1320-122617-0013",
"ref": "DELIVERED IN A STRONG TONE OF ASSENT ANNOUNCED THE GRATIFICATION THE SAVAGE WOULD RECEIVE IN WITNESSING SUCH AN EXHIBITION OF WEAKNESS IN AN ENEMY SO LONG HATED AND SO MUCH FEARED",
"hyp": "Delivered in a strong tone of assent, announced the gratification the savage would receive in witnessing such an exhibition of weakness in an enemy so long hated and so much feared.",
"ref_norm": "DELIVERED IN A STRONG TONE OF ASSENT ANNOUNCED THE GRATIFICATION THE SAVAGE WOULD RECEIVE IN WITNESSING SUCH AN EXHIBITION OF WEAKNESS IN AN ENEMY SO LONG HATED AND SO MUCH FEARED",
"hyp_norm": "DELIVERED IN A STRONG TONE OF ASSENT ANNOUNCED THE GRATIFICATION THE SAVAGE WOULD RECEIVE IN WITNESSING SUCH AN EXHIBITION OF WEAKNESS IN AN ENEMY SO LONG HATED AND SO MUCH FEARED",
"duration_s": 10.755,
"infer_time_s": 9.133,
"rtf": 0.8492,
"wer": 0.0
},
{
"id": "1320-122617-0014",
"ref": "THEY DREW BACK A LITTLE FROM THE ENTRANCE AND MOTIONED TO THE SUPPOSED CONJURER TO ENTER",
"hyp": "They drew back a little from the entrance and motioned to the supposed conjurer to enter.",
"ref_norm": "THEY DREW BACK A LITTLE FROM THE ENTRANCE AND MOTIONED TO THE SUPPOSED CONJURER TO ENTER",
"hyp_norm": "THEY DREW BACK A LITTLE FROM THE ENTRANCE AND MOTIONED TO THE SUPPOSED CONJURER TO ENTER",
"duration_s": 4.9,
"infer_time_s": 4.59,
"rtf": 0.9368,
"wer": 0.0
},
{
"id": "1320-122617-0015",
"ref": "BUT THE BEAR INSTEAD OF OBEYING MAINTAINED THE SEAT IT HAD TAKEN AND GROWLED",
"hyp": "But the bear, instead of obeying, maintained the seat it had taken and growled.",
"ref_norm": "BUT THE BEAR INSTEAD OF OBEYING MAINTAINED THE SEAT IT HAD TAKEN AND GROWLED",
"hyp_norm": "BUT THE BEAR INSTEAD OF OBEYING MAINTAINED THE SEAT IT HAD TAKEN AND GROWLED",
"duration_s": 5.125,
"infer_time_s": 4.607,
"rtf": 0.899,
"wer": 0.0
},
{
"id": "1320-122617-0016",
"ref": "THE CUNNING MAN IS AFRAID THAT HIS BREATH WILL BLOW UPON HIS BROTHERS AND TAKE AWAY THEIR COURAGE TOO CONTINUED DAVID IMPROVING THE HINT HE RECEIVED THEY MUST STAND FURTHER OFF",
"hyp": "The cunning man is afraid that his breath will blow upon his brothers and take away their courage too. Continued David, improving the hint he received. They must stand further off.",
"ref_norm": "THE CUNNING MAN IS AFRAID THAT HIS BREATH WILL BLOW UPON HIS BROTHERS AND TAKE AWAY THEIR COURAGE TOO CONTINUED DAVID IMPROVING THE HINT HE RECEIVED THEY MUST STAND FURTHER OFF",
"hyp_norm": "THE CUNNING MAN IS AFRAID THAT HIS BREATH WILL BLOW UPON HIS BROTHERS AND TAKE AWAY THEIR COURAGE TOO CONTINUED DAVID IMPROVING THE HINT HE RECEIVED THEY MUST STAND FURTHER OFF",
"duration_s": 10.085,
"infer_time_s": 9.018,
"rtf": 0.8942,
"wer": 0.0
},
{
"id": "1320-122617-0017",
"ref": "THEN AS IF SATISFIED OF THEIR SAFETY THE SCOUT LEFT HIS POSITION AND SLOWLY ENTERED THE PLACE",
"hyp": "Then, as if satisfied of their safety, the scout left his position and slowly entered the place.",
"ref_norm": "THEN AS IF SATISFIED OF THEIR SAFETY THE SCOUT LEFT HIS POSITION AND SLOWLY ENTERED THE PLACE",
"hyp_norm": "THEN AS IF SATISFIED OF THEIR SAFETY THE SCOUT LEFT HIS POSITION AND SLOWLY ENTERED THE PLACE",
"duration_s": 5.655,
"infer_time_s": 4.78,
"rtf": 0.8453,
"wer": 0.0
},
{
"id": "1320-122617-0018",
"ref": "IT WAS SILENT AND GLOOMY BEING TENANTED SOLELY BY THE CAPTIVE AND LIGHTED BY THE DYING EMBERS OF A FIRE WHICH HAD BEEN USED FOR THE PURPOSED OF COOKERY",
"hyp": "It was silent and glo omy, being tenanted solely by the captive , and lighted by the dying embers of a fire which had been used for the purpose of cookery.",
"ref_norm": "IT WAS SILENT AND GLOOMY BEING TENANTED SOLELY BY THE CAPTIVE AND LIGHTED BY THE DYING EMBERS OF A FIRE WHICH HAD BEEN USED FOR THE PURPOSED OF COOKERY",
"hyp_norm": "IT WAS SILENT AND GLO OMY BEING TENANTED SOLELY BY THE CAPTIVE AND LIGHTED BY THE DYING EMBERS OF A FIRE WHICH HAD BEEN USED FOR THE PURPOSE OF COOKERY",
"duration_s": 9.695,
"infer_time_s": 8.841,
"rtf": 0.9119,
"wer": 0.1034
},
{
"id": "1320-122617-0019",
"ref": "UNCAS OCCUPIED A DISTANT CORNER IN A RECLINING ATTITUDE BEING RIGIDLY BOUND BOTH HANDS AND FEET BY STRONG AND PAINFUL WITHES",
"hyp": "Uncas occupied a distant corner in a recl ining attitude, being rigidly bound both hands and feet by strong and painful whips.",
"ref_norm": "UNCAS OCCUPIED A DISTANT CORNER IN A RECLINING ATTITUDE BEING RIGIDLY BOUND BOTH HANDS AND FEET BY STRONG AND PAINFUL WITHES",
"hyp_norm": "UNCAS OCCUPIED A DISTANT CORNER IN A RECL INING ATTITUDE BEING RIGIDLY BOUND BOTH HANDS AND FEET BY STRONG AND PAINFUL WHIPS",
"duration_s": 8.23,
"infer_time_s": 7.207,
"rtf": 0.8757,
"wer": 0.1429
},
{
"id": "1320-122617-0020",
"ref": "THE SCOUT WHO HAD LEFT DAVID AT THE DOOR TO ASCERTAIN THEY WERE NOT OBSERVED THOUGHT IT PRUDENT TO PRESERVE HIS DISGUISE UNTIL ASSURED OF THEIR PRIVACY",
"hyp": "The scout who had left David at the door to ascertain they were not observed thought it prudent to preserve his disguise until assured of their privacy.",
"ref_norm": "THE SCOUT WHO HAD LEFT DAVID AT THE DOOR TO ASCERTAIN THEY WERE NOT OBSERVED THOUGHT IT PRUDENT TO PRESERVE HIS DISGUISE UNTIL ASSURED OF THEIR PRIVACY",
"hyp_norm": "THE SCOUT WHO HAD LEFT DAVID AT THE DOOR TO ASCERTAIN THEY WERE NOT OBSERVED THOUGHT IT PRUDENT TO PRESERVE HIS DISGUISE UNTIL ASSURED OF THEIR PRIVACY",
"duration_s": 8.895,
"infer_time_s": 7.291,
"rtf": 0.8197,
"wer": 0.0
},
{
"id": "1320-122617-0021",
"ref": "WHAT SHALL WE DO WITH THE MINGOES AT THE DOOR THEY COUNT SIX AND THIS SINGER IS AS GOOD AS NOTHING",
"hyp": "What shall we do with the Mingos at the door? They count six, and the singer is as good as nothing.",
"ref_norm": "WHAT SHALL WE DO WITH THE MINGOES AT THE DOOR THEY COUNT SIX AND THIS SINGER IS AS GOOD AS NOTHING",
"hyp_norm": "WHAT SHALL WE DO WITH THE MINGOS AT THE DOOR THEY COUNT SIX AND THE SINGER IS AS GOOD AS NOTHING",
"duration_s": 5.335,
"infer_time_s": 5.706,
"rtf": 1.0695,
"wer": 0.0952
},
{
"id": "1320-122617-0022",
"ref": "THE DELAWARES ARE CHILDREN OF THE TORTOISE AND THEY OUTSTRIP THE DEER",
"hyp": "The Delawares are children of the tortoise, and they outstripped the deer.",
"ref_norm": "THE DELAWARES ARE CHILDREN OF THE TORTOISE AND THEY OUTSTRIP THE DEER",
"hyp_norm": "THE DELAWARES ARE CHILDREN OF THE TORTOISE AND THEY OUTSTRIPPED THE DEER",
"duration_s": 3.855,
"infer_time_s": 3.999,
"rtf": 1.0373,
"wer": 0.0833
},
{
"id": "1320-122617-0023",
"ref": "UNCAS WHO HAD ALREADY APPROACHED THE DOOR IN READINESS TO LEAD THE WAY NOW RECOILED AND PLACED HIMSELF ONCE MORE IN THE BOTTOM OF THE LODGE",
"hyp": "Uncas, who had already approached the door in readiness to lead the way, now reco iled and placed himself once more in the bottom of the lodge.",
"ref_norm": "UNCAS WHO HAD ALREADY APPROACHED THE DOOR IN READINESS TO LEAD THE WAY NOW RECOILED AND PLACED HIMSELF ONCE MORE IN THE BOTTOM OF THE LODGE",
"hyp_norm": "UNCAS WHO HAD ALREADY APPROACHED THE DOOR IN READINESS TO LEAD THE WAY NOW RECO ILED AND PLACED HIMSELF ONCE MORE IN THE BOTTOM OF THE LODGE",
"duration_s": 7.815,
"infer_time_s": 5.954,
"rtf": 0.7619,
"wer": 0.0769
},
{
"id": "1320-122617-0024",
"ref": "BUT HAWKEYE WHO WAS TOO MUCH OCCUPIED WITH HIS OWN THOUGHTS TO NOTE THE MOVEMENT CONTINUED SPEAKING MORE TO HIMSELF THAN TO HIS COMPANION",
"hyp": "But Hawkeye, who was too much occupied with his own thoughts to note the movement, continued speaking more to himself than to his companion.",
"ref_norm": "BUT HAWKEYE WHO WAS TOO MUCH OCCUPIED WITH HIS OWN THOUGHTS TO NOTE THE MOVEMENT CONTINUED SPEAKING MORE TO HIMSELF THAN TO HIS COMPANION",
"hyp_norm": "BUT HAWKEYE WHO WAS TOO MUCH OCCUPIED WITH HIS OWN THOUGHTS TO NOTE THE MOVEMENT CONTINUED SPEAKING MORE TO HIMSELF THAN TO HIS COMPANION",
"duration_s": 7.555,
"infer_time_s": 7.129,
"rtf": 0.9437,
"wer": 0.0
},
{
"id": "1320-122617-0025",
"ref": "SO UNCAS YOU HAD BETTER TAKE THE LEAD WHILE I WILL PUT ON THE SKIN AGAIN AND TRUST TO CUNNING FOR WANT OF SPEED",
"hyp": "So, Uncas, you had better take the lead, while I will put on the skin again and trust to cunning for want of speed.",
"ref_norm": "SO UNCAS YOU HAD BETTER TAKE THE LEAD WHILE I WILL PUT ON THE SKIN AGAIN AND TRUST TO CUNNING FOR WANT OF SPEED",
"hyp_norm": "SO UNCAS YOU HAD BETTER TAKE THE LEAD WHILE I WILL PUT ON THE SKIN AGAIN AND TRUST TO CUNNING FOR WANT OF SPEED",
"duration_s": 6.36,
"infer_time_s": 6.449,
"rtf": 1.0139,
"wer": 0.0
},
{
"id": "1320-122617-0026",
"ref": "WELL WHAT CAN'T BE DONE BY MAIN COURAGE IN WAR MUST BE DONE BY CIRCUMVENTION",
"hyp": "Well, what can't be done by main courage in war must be done by circumvention.",
"ref_norm": "WELL WHAT CANT BE DONE BY MAIN COURAGE IN WAR MUST BE DONE BY CIRCUMVENTION",
"hyp_norm": "WELL WHAT CANT BE DONE BY MAIN COURAGE IN WAR MUST BE DONE BY CIRCUMVENTION",
"duration_s": 5.225,
"infer_time_s": 3.689,
"rtf": 0.7061,
"wer": 0.0
},
{
"id": "1320-122617-0027",
"ref": "AS SOON AS THESE DISPOSITIONS WERE MADE THE SCOUT TURNED TO DAVID AND GAVE HIM HIS PARTING INSTRUCTIONS",
"hyp": "As soon as these dis positions were made, the scout turned to David and gave him his parting instructions.",
"ref_norm": "AS SOON AS THESE DISPOSITIONS WERE MADE THE SCOUT TURNED TO DAVID AND GAVE HIM HIS PARTING INSTRUCTIONS",
"hyp_norm": "AS SOON AS THESE DIS POSITIONS WERE MADE THE SCOUT TURNED TO DAVID AND GAVE HIM HIS PARTING INSTRUCTIONS",
"duration_s": 5.69,
"infer_time_s": 4.132,
"rtf": 0.7262,
"wer": 0.1111
},
{
"id": "1320-122617-0028",
"ref": "MY PURSUITS ARE PEACEFUL AND MY TEMPER I HUMBLY TRUST IS GREATLY GIVEN TO MERCY AND LOVE RETURNED DAVID A LITTLE NETTLED AT SO DIRECT AN ATTACK ON HIS MANHOOD BUT THERE ARE NONE WHO CAN SAY THAT I HAVE EVER FORGOTTEN MY FAITH IN THE LORD EVEN IN THE GREATEST STRAITS",
"hyp": "My pursuits are peaceful, and my temper\u2014I humb ly trust\u2014is greatly given to mercy and love. Returned David, a little nettled at so direct an attack on his manhood, but there are none who can say that I have ever forgotten my faith in the Lord, even in the greatest straits.",
"ref_norm": "MY PURSUITS ARE PEACEFUL AND MY TEMPER I HUMBLY TRUST IS GREATLY GIVEN TO MERCY AND LOVE RETURNED DAVID A LITTLE NETTLED AT SO DIRECT AN ATTACK ON HIS MANHOOD BUT THERE ARE NONE WHO CAN SAY THAT I HAVE EVER FORGOTTEN MY FAITH IN THE LORD EVEN IN THE GREATEST STRAITS",
"hyp_norm": "MY PURSUITS ARE PEACEFUL AND MY TEMPERI HUMB LY TRUSTIS GREATLY GIVEN TO MERCY AND LOVE RETURNED DAVID A LITTLE NETTLED AT SO DIRECT AN ATTACK ON HIS MANHOOD BUT THERE ARE NONE WHO CAN SAY THAT I HAVE EVER FORGOTTEN MY FAITH IN THE LORD EVEN IN THE GREATEST STRAITS",
"duration_s": 15.995,
"infer_time_s": 15.087,
"rtf": 0.9432,
"wer": 0.0962
},
{
"id": "1320-122617-0029",
"ref": "IF YOU ARE NOT THEN KNOCKED ON THE HEAD YOUR BEING A NON COMPOSSER WILL PROTECT YOU AND YOU'LL THEN HAVE A GOOD REASON TO EXPECT TO DIE IN YOUR BED",
"hyp": "If you are not then knocked on the head, your being a non-com poser will protect you , and you'll then have a good reason to expect to die in your bed.",
"ref_norm": "IF YOU ARE NOT THEN KNOCKED ON THE HEAD YOUR BEING A NON COMPOSSER WILL PROTECT YOU AND YOULL THEN HAVE A GOOD REASON TO EXPECT TO DIE IN YOUR BED",
"hyp_norm": "IF YOU ARE NOT THEN KNOCKED ON THE HEAD YOUR BEING A NONCOM POSER WILL PROTECT YOU AND YOULL THEN HAVE A GOOD REASON TO EXPECT TO DIE IN YOUR BED",
"duration_s": 7.875,
"infer_time_s": 8.233,
"rtf": 1.0455,
"wer": 0.0645
},
{
"id": "1320-122617-0030",
"ref": "SO CHOOSE FOR YOURSELF TO MAKE A RUSH OR TARRY HERE",
"hyp": "So choose for yourself to make a rush or tarry here.",
"ref_norm": "SO CHOOSE FOR YOURSELF TO MAKE A RUSH OR TARRY HERE",
"hyp_norm": "SO CHOOSE FOR YOURSELF TO MAKE A RUSH OR TARRY HERE",
"duration_s": 3.98,
"infer_time_s": 3.145,
"rtf": 0.7902,
"wer": 0.0
},
{
"id": "1320-122617-0031",
"ref": "BRAVELY AND GENEROUSLY HAS HE BATTLED IN MY BEHALF AND THIS AND MORE WILL I DARE IN HIS SERVICE",
"hyp": "Bravely and generously, has he battled in my behalf , and this and more will I dare in his service.",
"ref_norm": "BRAVELY AND GENEROUSLY HAS HE BATTLED IN MY BEHALF AND THIS AND MORE WILL I DARE IN HIS SERVICE",
"hyp_norm": "BRAVELY AND GENEROUSLY HAS HE BATTLED IN MY BEHALF AND THIS AND MORE WILL I DARE IN HIS SERVICE",
"duration_s": 6.285,
"infer_time_s": 5.945,
"rtf": 0.9459,
"wer": 0.0
},
{
"id": "1320-122617-0032",
"ref": "KEEP SILENT AS LONG AS MAY BE AND IT WOULD BE WISE WHEN YOU DO SPEAK TO BREAK OUT SUDDENLY IN ONE OF YOUR SHOUTINGS WHICH WILL SERVE TO REMIND THE INDIANS THAT YOU ARE NOT ALTOGETHER AS RESPONSIBLE AS MEN SHOULD BE",
"hyp": "Keep silent as long as may be, and it would be wise when you do speak to break out suddenly in one of your shout ings, which will serve to remind the Indians that you are not altogether as responsible as men should be.",
"ref_norm": "KEEP SILENT AS LONG AS MAY BE AND IT WOULD BE WISE WHEN YOU DO SPEAK TO BREAK OUT SUDDENLY IN ONE OF YOUR SHOUTINGS WHICH WILL SERVE TO REMIND THE INDIANS THAT YOU ARE NOT ALTOGETHER AS RESPONSIBLE AS MEN SHOULD BE",
"hyp_norm": "KEEP SILENT AS LONG AS MAY BE AND IT WOULD BE WISE WHEN YOU DO SPEAK TO BREAK OUT SUDDENLY IN ONE OF YOUR SHOUT INGS WHICH WILL SERVE TO REMIND THE INDIANS THAT YOU ARE NOT ALTOGETHER AS RESPONSIBLE AS MEN SHOULD BE",
"duration_s": 11.28,
"infer_time_s": 10.92,
"rtf": 0.9681,
"wer": 0.0465
},
{
"id": "1320-122617-0033",
"ref": "IF HOWEVER THEY TAKE YOUR SCALP AS I TRUST AND BELIEVE THEY WILL NOT DEPEND ON IT UNCAS AND I WILL NOT FORGET THE DEED BUT REVENGE IT AS BECOMES TRUE WARRIORS AND TRUSTY FRIENDS",
"hyp": "If, however, they take your scalp, as I trust and believe they will not , depend on it, Uncas and I will not forget the deed, but revenge it as becomes true warriors and trusty friends.",
"ref_norm": "IF HOWEVER THEY TAKE YOUR SCALP AS I TRUST AND BELIEVE THEY WILL NOT DEPEND ON IT UNCAS AND I WILL NOT FORGET THE DEED BUT REVENGE IT AS BECOMES TRUE WARRIORS AND TRUSTY FRIENDS",
"hyp_norm": "IF HOWEVER THEY TAKE YOUR SCALP AS I TRUST AND BELIEVE THEY WILL NOT DEPEND ON IT UNCAS AND I WILL NOT FORGET THE DEED BUT REVENGE IT AS BECOMES TRUE WARRIORS AND TRUSTY FRIENDS",
"duration_s": 11.045,
"infer_time_s": 10.133,
"rtf": 0.9175,
"wer": 0.0
},
{
"id": "1320-122617-0034",
"ref": "HOLD SAID DAVID PERCEIVING THAT WITH THIS ASSURANCE THEY WERE ABOUT TO LEAVE HIM I AM AN UNWORTHY AND HUMBLE FOLLOWER OF ONE WHO TAUGHT NOT THE DAMNABLE PRINCIPLE OF REVENGE",
"hyp": "Hold,\" said David, perceiving that with this assurance they were about to leave him. \"I am an unworthy and humble follower of one who taught not the damnable principle of revenge.\"",
"ref_norm": "HOLD SAID DAVID PERCEIVING THAT WITH THIS ASSURANCE THEY WERE ABOUT TO LEAVE HIM I AM AN UNWORTHY AND HUMBLE FOLLOWER OF ONE WHO TAUGHT NOT THE DAMNABLE PRINCIPLE OF REVENGE",
"hyp_norm": "HOLD SAID DAVID PERCEIVING THAT WITH THIS ASSURANCE THEY WERE ABOUT TO LEAVE HIM I AM AN UNWORTHY AND HUMBLE FOLLOWER OF ONE WHO TAUGHT NOT THE DAMNABLE PRINCIPLE OF REVENGE",
"duration_s": 9.485,
"infer_time_s": 8.902,
"rtf": 0.9386,
"wer": 0.0
},
{
"id": "1320-122617-0035",
"ref": "THEN HEAVING A HEAVY SIGH PROBABLY AMONG THE LAST HE EVER DREW IN PINING FOR A CONDITION HE HAD SO LONG ABANDONED HE ADDED IT IS WHAT I WOULD WISH TO PRACTISE MYSELF AS ONE WITHOUT A CROSS OF BLOOD THOUGH IT IS NOT ALWAYS EASY TO DEAL WITH AN INDIAN AS YOU WOULD WITH A FELLOW CHRISTIAN",
"hyp": "Then heaving a heavy sigh, probably among the last he ever drew in pining for a condition he had so long abandoned , he added, \"It is what I would wish to practise myself as one without a cross of blood, though it is not always easy to deal with an Indian as you would with a fellow Christian.\"",
"ref_norm": "THEN HEAVING A HEAVY SIGH PROBABLY AMONG THE LAST HE EVER DREW IN PINING FOR A CONDITION HE HAD SO LONG ABANDONED HE ADDED IT IS WHAT I WOULD WISH TO PRACTISE MYSELF AS ONE WITHOUT A CROSS OF BLOOD THOUGH IT IS NOT ALWAYS EASY TO DEAL WITH AN INDIAN AS YOU WOULD WITH A FELLOW CHRISTIAN",
"hyp_norm": "THEN HEAVING A HEAVY SIGH PROBABLY AMONG THE LAST HE EVER DREW IN PINING FOR A CONDITION HE HAD SO LONG ABANDONED HE ADDED IT IS WHAT I WOULD WISH TO PRACTISE MYSELF AS ONE WITHOUT A CROSS OF BLOOD THOUGH IT IS NOT ALWAYS EASY TO DEAL WITH AN INDIAN AS YOU WOULD WITH A FELLOW CHRISTIAN",
"duration_s": 18.22,
"infer_time_s": 16.065,
"rtf": 0.8817,
"wer": 0.0
},
{
"id": "1320-122617-0036",
"ref": "GOD BLESS YOU FRIEND I DO BELIEVE YOUR SCENT IS NOT GREATLY WRONG WHEN THE MATTER IS DULY CONSIDERED AND KEEPING ETERNITY BEFORE THE EYES THOUGH MUCH DEPENDS ON THE NATURAL GIFTS AND THE FORCE OF TEMPTATION",
"hyp": "God bless you, friend. I do believe your scent is not greatly wrong when the matter is duly considered, and keeping eternity before the eyes, though much depends on the natural gifts and the force of temptation.",
"ref_norm": "GOD BLESS YOU FRIEND I DO BELIEVE YOUR SCENT IS NOT GREATLY WRONG WHEN THE MATTER IS DULY CONSIDERED AND KEEPING ETERNITY BEFORE THE EYES THOUGH MUCH DEPENDS ON THE NATURAL GIFTS AND THE FORCE OF TEMPTATION",
"hyp_norm": "GOD BLESS YOU FRIEND I DO BELIEVE YOUR SCENT IS NOT GREATLY WRONG WHEN THE MATTER IS DULY CONSIDERED AND KEEPING ETERNITY BEFORE THE EYES THOUGH MUCH DEPENDS ON THE NATURAL GIFTS AND THE FORCE OF TEMPTATION",
"duration_s": 12.37,
"infer_time_s": 10.355,
"rtf": 0.8371,
"wer": 0.0
},
{
"id": "1320-122617-0037",
"ref": "THE DELAWARE DOG HE SAID LEANING FORWARD AND PEERING THROUGH THE DIM LIGHT TO CATCH THE EXPRESSION OF THE OTHER'S FEATURES IS HE AFRAID",
"hyp": "The Delaware dog,\" he said, leaning forward and peering through the dim light to catch the expression of the other's features. \"Is he afraid?\"",
"ref_norm": "THE DELAWARE DOG HE SAID LEANING FORWARD AND PEERING THROUGH THE DIM LIGHT TO CATCH THE EXPRESSION OF THE OTHERS FEATURES IS HE AFRAID",
"hyp_norm": "THE DELAWARE DOG HE SAID LEANING FORWARD AND PEERING THROUGH THE DIM LIGHT TO CATCH THE EXPRESSION OF THE OTHERS FEATURES IS HE AFRAID",
"duration_s": 7.18,
"infer_time_s": 7.003,
"rtf": 0.9754,
"wer": 0.0
},
{
"id": "1320-122617-0038",
"ref": "WILL THE HURONS HEAR HIS GROANS",
"hyp": "Will the Hurons hear his gro ans?",
"ref_norm": "WILL THE HURONS HEAR HIS GROANS",
"hyp_norm": "WILL THE HURONS HEAR HIS GRO ANS",
"duration_s": 2.24,
"infer_time_s": 2.338,
"rtf": 1.0436,
"wer": 0.3333
},
{
"id": "1320-122617-0039",
"ref": "THE MOHICAN STARTED ON HIS FEET AND SHOOK HIS SHAGGY COVERING AS THOUGH THE ANIMAL HE COUNTERFEITED WAS ABOUT TO MAKE SOME DESPERATE EFFORT",
"hyp": "The Mohican started on his feet and shook his shaggy covering as though the animal he counterfeited was about to make some desperate effort.",
"ref_norm": "THE MOHICAN STARTED ON HIS FEET AND SHOOK HIS SHAGGY COVERING AS THOUGH THE ANIMAL HE COUNTERFEITED WAS ABOUT TO MAKE SOME DESPERATE EFFORT",
"hyp_norm": "THE MOHICAN STARTED ON HIS FEET AND SHOOK HIS SHAGGY COVERING AS THOUGH THE ANIMAL HE COUNTERFEITED WAS ABOUT TO MAKE SOME DESPERATE EFFORT",
"duration_s": 7.055,
"infer_time_s": 6.868,
"rtf": 0.9734,
"wer": 0.0
},
{
"id": "1320-122617-0040",
"ref": "HE HAD NO OCCASION TO DELAY FOR AT THE NEXT INSTANT A BURST OF CRIES FILLED THE OUTER AIR AND RAN ALONG THE WHOLE EXTENT OF THE VILLAGE",
"hyp": "He had no occasion to delay, for at the next instant a burst of cries filled the outer air and ran along the whole extent of the village.",
"ref_norm": "HE HAD NO OCCASION TO DELAY FOR AT THE NEXT INSTANT A BURST OF CRIES FILLED THE OUTER AIR AND RAN ALONG THE WHOLE EXTENT OF THE VILLAGE",
"hyp_norm": "HE HAD NO OCCASION TO DELAY FOR AT THE NEXT INSTANT A BURST OF CRIES FILLED THE OUTER AIR AND RAN ALONG THE WHOLE EXTENT OF THE VILLAGE",
"duration_s": 7.975,
"infer_time_s": 6.917,
"rtf": 0.8673,
"wer": 0.0
},
{
"id": "1320-122617-0041",
"ref": "UNCAS CAST HIS SKIN AND STEPPED FORTH IN HIS OWN BEAUTIFUL PROPORTIONS",
"hyp": "Uncas cast his skin and stepped forth in his own beautiful proportions.",
"ref_norm": "UNCAS CAST HIS SKIN AND STEPPED FORTH IN HIS OWN BEAUTIFUL PROPORTIONS",
"hyp_norm": "UNCAS CAST HIS SKIN AND STEPPED FORTH IN HIS OWN BEAUTIFUL PROPORTIONS",
"duration_s": 4.15,
"infer_time_s": 3.593,
"rtf": 0.8658,
"wer": 0.0
},
{
"id": "1580-141083-0000",
"ref": "I WILL ENDEAVOUR IN MY STATEMENT TO AVOID SUCH TERMS AS WOULD SERVE TO LIMIT THE EVENTS TO ANY PARTICULAR PLACE OR GIVE A CLUE AS TO THE PEOPLE CONCERNED",
"hyp": "I will endeavor in my statement to avoid such terms as would serve to limit the events to any particular place or give a clue as to the people concerned.",
"ref_norm": "I WILL ENDEAVOUR IN MY STATEMENT TO AVOID SUCH TERMS AS WOULD SERVE TO LIMIT THE EVENTS TO ANY PARTICULAR PLACE OR GIVE A CLUE AS TO THE PEOPLE CONCERNED",
"hyp_norm": "I WILL ENDEAVOR IN MY STATEMENT TO AVOID SUCH TERMS AS WOULD SERVE TO LIMIT THE EVENTS TO ANY PARTICULAR PLACE OR GIVE A CLUE AS TO THE PEOPLE CONCERNED",
"duration_s": 8.94,
"infer_time_s": 7.528,
"rtf": 0.8421,
"wer": 0.0333
},
{
"id": "1580-141083-0001",
"ref": "I HAD ALWAYS KNOWN HIM TO BE RESTLESS IN HIS MANNER BUT ON THIS PARTICULAR OCCASION HE WAS IN SUCH A STATE OF UNCONTROLLABLE AGITATION THAT IT WAS CLEAR SOMETHING VERY UNUSUAL HAD OCCURRED",
"hyp": "I had always known him to be restless in his manner , but on this particular occasion he was in such a state of uncontrollable agitation that it was clear something very unusual had occurred.",
"ref_norm": "I HAD ALWAYS KNOWN HIM TO BE RESTLESS IN HIS MANNER BUT ON THIS PARTICULAR OCCASION HE WAS IN SUCH A STATE OF UNCONTROLLABLE AGITATION THAT IT WAS CLEAR SOMETHING VERY UNUSUAL HAD OCCURRED",
"hyp_norm": "I HAD ALWAYS KNOWN HIM TO BE RESTLESS IN HIS MANNER BUT ON THIS PARTICULAR OCCASION HE WAS IN SUCH A STATE OF UNCONTROLLABLE AGITATION THAT IT WAS CLEAR SOMETHING VERY UNUSUAL HAD OCCURRED",
"duration_s": 10.255,
"infer_time_s": 9.051,
"rtf": 0.8826,
"wer": 0.0
},
{
"id": "1580-141083-0002",
"ref": "MY FRIEND'S TEMPER HAD NOT IMPROVED SINCE HE HAD BEEN DEPRIVED OF THE CONGENIAL SURROUNDINGS OF BAKER STREET",
"hyp": "My friend's temper had not improved since he had been deprived of the congenial surroundings of Baker Street.",
"ref_norm": "MY FRIENDS TEMPER HAD NOT IMPROVED SINCE HE HAD BEEN DEPRIVED OF THE CONGENIAL SURROUNDINGS OF BAKER STREET",
"hyp_norm": "MY FRIENDS TEMPER HAD NOT IMPROVED SINCE HE HAD BEEN DEPRIVED OF THE CONGENIAL SURROUNDINGS OF BAKER STREET",
"duration_s": 6.135,
"infer_time_s": 5.16,
"rtf": 0.8411,
"wer": 0.0
},
{
"id": "1580-141083-0003",
"ref": "WITHOUT HIS SCRAPBOOKS HIS CHEMICALS AND HIS HOMELY UNTIDINESS HE WAS AN UNCOMFORTABLE MAN",
"hyp": "Without his scrapbooks , his chemicals, and his homely untidiness, he was an uncomfortable man.",
"ref_norm": "WITHOUT HIS SCRAPBOOKS HIS CHEMICALS AND HIS HOMELY UNTIDINESS HE WAS AN UNCOMFORTABLE MAN",
"hyp_norm": "WITHOUT HIS SCRAPBOOKS HIS CHEMICALS AND HIS HOMELY UNTIDINESS HE WAS AN UNCOMFORTABLE MAN",
"duration_s": 6.55,
"infer_time_s": 5.298,
"rtf": 0.8089,
"wer": 0.0
},
{
"id": "1580-141083-0004",
"ref": "I HAD TO READ IT OVER CAREFULLY AS THE TEXT MUST BE ABSOLUTELY CORRECT",
"hyp": "I had to read it over carefully, as the text must be absolutely correct.",
"ref_norm": "I HAD TO READ IT OVER CAREFULLY AS THE TEXT MUST BE ABSOLUTELY CORRECT",
"hyp_norm": "I HAD TO READ IT OVER CAREFULLY AS THE TEXT MUST BE ABSOLUTELY CORRECT",
"duration_s": 4.515,
"infer_time_s": 3.896,
"rtf": 0.863,
"wer": 0.0
},
{
"id": "1580-141083-0005",
"ref": "I WAS ABSENT RATHER MORE THAN AN HOUR",
"hyp": "I was absent rather more than an hour.",
"ref_norm": "I WAS ABSENT RATHER MORE THAN AN HOUR",
"hyp_norm": "I WAS ABSENT RATHER MORE THAN AN HOUR",
"duration_s": 2.745,
"infer_time_s": 2.298,
"rtf": 0.837,
"wer": 0.0
},
{
"id": "1580-141083-0006",
"ref": "THE ONLY DUPLICATE WHICH EXISTED SO FAR AS I KNEW WAS THAT WHICH BELONGED TO MY SERVANT BANNISTER A MAN WHO HAS LOOKED AFTER MY ROOM FOR TEN YEARS AND WHOSE HONESTY IS ABSOLUTELY ABOVE SUSPICION",
"hyp": "The only duplicate which existed, so far as I knew, was that which belonged to my servant Bann ister, a man who has looked after my room for ten years and whose honesty is absolutely above suspicion.",
"ref_norm": "THE ONLY DUPLICATE WHICH EXISTED SO FAR AS I KNEW WAS THAT WHICH BELONGED TO MY SERVANT BANNISTER A MAN WHO HAS LOOKED AFTER MY ROOM FOR TEN YEARS AND WHOSE HONESTY IS ABSOLUTELY ABOVE SUSPICION",
"hyp_norm": "THE ONLY DUPLICATE WHICH EXISTED SO FAR AS I KNEW WAS THAT WHICH BELONGED TO MY SERVANT BANN ISTER A MAN WHO HAS LOOKED AFTER MY ROOM FOR TEN YEARS AND WHOSE HONESTY IS ABSOLUTELY ABOVE SUSPICION",
"duration_s": 10.85,
"infer_time_s": 9.806,
"rtf": 0.9038,
"wer": 0.0556
},
{
"id": "1580-141083-0007",
"ref": "THE MOMENT I LOOKED AT MY TABLE I WAS AWARE THAT SOMEONE HAD RUMMAGED AMONG MY PAPERS",
"hyp": "The moment I looked at my table, I was aware that someone had rummaged among my papers.",
"ref_norm": "THE MOMENT I LOOKED AT MY TABLE I WAS AWARE THAT SOMEONE HAD RUMMAGED AMONG MY PAPERS",
"hyp_norm": "THE MOMENT I LOOKED AT MY TABLE I WAS AWARE THAT SOMEONE HAD RUMMAGED AMONG MY PAPERS",
"duration_s": 4.565,
"infer_time_s": 4.746,
"rtf": 1.0396,
"wer": 0.0
},
{
"id": "1580-141083-0008",
"ref": "THE PROOF WAS IN THREE LONG SLIPS I HAD LEFT THEM ALL TOGETHER",
"hyp": "The proof was in three long slips. I had left them all together.",
"ref_norm": "THE PROOF WAS IN THREE LONG SLIPS I HAD LEFT THEM ALL TOGETHER",
"hyp_norm": "THE PROOF WAS IN THREE LONG SLIPS I HAD LEFT THEM ALL TOGETHER",
"duration_s": 4.305,
"infer_time_s": 3.699,
"rtf": 0.8591,
"wer": 0.0
},
{
"id": "1580-141083-0009",
"ref": "THE ALTERNATIVE WAS THAT SOMEONE PASSING HAD OBSERVED THE KEY IN THE DOOR HAD KNOWN THAT I WAS OUT AND HAD ENTERED TO LOOK AT THE PAPERS",
"hyp": "The alternative was that someone passing had observed the key in the door, had known that I was out, and had entered to look at the papers.",
"ref_norm": "THE ALTERNATIVE WAS THAT SOMEONE PASSING HAD OBSERVED THE KEY IN THE DOOR HAD KNOWN THAT I WAS OUT AND HAD ENTERED TO LOOK AT THE PAPERS",
"hyp_norm": "THE ALTERNATIVE WAS THAT SOMEONE PASSING HAD OBSERVED THE KEY IN THE DOOR HAD KNOWN THAT I WAS OUT AND HAD ENTERED TO LOOK AT THE PAPERS",
"duration_s": 7.04,
"infer_time_s": 6.904,
"rtf": 0.9807,
"wer": 0.0
},
{
"id": "1580-141083-0010",
"ref": "I GAVE HIM A LITTLE BRANDY AND LEFT HIM COLLAPSED IN A CHAIR WHILE I MADE A MOST CAREFUL EXAMINATION OF THE ROOM",
"hyp": "I gave him a little brandy and left him collapsed in a chair while I made a most careful examination of the room.",
"ref_norm": "I GAVE HIM A LITTLE BRANDY AND LEFT HIM COLLAPSED IN A CHAIR WHILE I MADE A MOST CAREFUL EXAMINATION OF THE ROOM",
"hyp_norm": "I GAVE HIM A LITTLE BRANDY AND LEFT HIM COLLAPSED IN A CHAIR WHILE I MADE A MOST CAREFUL EXAMINATION OF THE ROOM",
"duration_s": 5.32,
"infer_time_s": 5.659,
"rtf": 1.0637,
"wer": 0.0
},
{
"id": "1580-141083-0011",
"ref": "A BROKEN TIP OF LEAD WAS LYING THERE ALSO",
"hyp": "A broken tip of lead was lying there, also.",
"ref_norm": "A BROKEN TIP OF LEAD WAS LYING THERE ALSO",
"hyp_norm": "A BROKEN TIP OF LEAD WAS LYING THERE ALSO",
"duration_s": 2.825,
"infer_time_s": 2.777,
"rtf": 0.9829,
"wer": 0.0
},
{
"id": "1580-141083-0012",
"ref": "NOT ONLY THIS BUT ON THE TABLE I FOUND A SMALL BALL OF BLACK DOUGH OR CLAY WITH SPECKS OF SOMETHING WHICH LOOKS LIKE SAWDUST IN IT",
"hyp": "Not only this, but on the table I found a small ball of black dough or clay, with specks of something which looks like sawdust in it.",
"ref_norm": "NOT ONLY THIS BUT ON THE TABLE I FOUND A SMALL BALL OF BLACK DOUGH OR CLAY WITH SPECKS OF SOMETHING WHICH LOOKS LIKE SAWDUST IN IT",
"hyp_norm": "NOT ONLY THIS BUT ON THE TABLE I FOUND A SMALL BALL OF BLACK DOUGH OR CLAY WITH SPECKS OF SOMETHING WHICH LOOKS LIKE SAWDUST IN IT",
"duration_s": 7.065,
"infer_time_s": 7.522,
"rtf": 1.0646,
"wer": 0.0
},
{
"id": "1580-141083-0013",
"ref": "ABOVE ALL THINGS I DESIRE TO SETTLE THE MATTER QUIETLY AND DISCREETLY",
"hyp": "Above all things, I desire to settle the matter quietly and discreetly.",
"ref_norm": "ABOVE ALL THINGS I DESIRE TO SETTLE THE MATTER QUIETLY AND DISCREETLY",
"hyp_norm": "ABOVE ALL THINGS I DESIRE TO SETTLE THE MATTER QUIETLY AND DISCREETLY",
"duration_s": 4.32,
"infer_time_s": 3.905,
"rtf": 0.9039,
"wer": 0.0
},
{
"id": "1580-141083-0014",
"ref": "TO THE BEST OF MY BELIEF THEY WERE ROLLED UP",
"hyp": "To the best of my belief , they were rolled up.",
"ref_norm": "TO THE BEST OF MY BELIEF THEY WERE ROLLED UP",
"hyp_norm": "TO THE BEST OF MY BELIEF THEY WERE ROLLED UP",
"duration_s": 2.855,
"infer_time_s": 2.354,
"rtf": 0.8245,
"wer": 0.0
},
{
"id": "1580-141083-0015",
"ref": "DID ANYONE KNOW THAT THESE PROOFS WOULD BE THERE NO ONE SAVE THE PRINTER",
"hyp": "Did anyone know that these proofs would be there ? No one, save the printer.",
"ref_norm": "DID ANYONE KNOW THAT THESE PROOFS WOULD BE THERE NO ONE SAVE THE PRINTER",
"hyp_norm": "DID ANYONE KNOW THAT THESE PROOFS WOULD BE THERE NO ONE SAVE THE PRINTER",
"duration_s": 4.985,
"infer_time_s": 3.409,
"rtf": 0.6838,
"wer": 0.0
},
{
"id": "1580-141083-0016",
"ref": "I WAS IN SUCH A HURRY TO COME TO YOU YOU LEFT YOUR DOOR OPEN",
"hyp": "I was in such a hurry to come to you, you left your door open.",
"ref_norm": "I WAS IN SUCH A HURRY TO COME TO YOU YOU LEFT YOUR DOOR OPEN",
"hyp_norm": "I WAS IN SUCH A HURRY TO COME TO YOU YOU LEFT YOUR DOOR OPEN",
"duration_s": 4.255,
"infer_time_s": 3.591,
"rtf": 0.8439,
"wer": 0.0
},
{
"id": "1580-141083-0017",
"ref": "SO IT SEEMS TO ME",
"hyp": "So it seems to me.",
"ref_norm": "SO IT SEEMS TO ME",
"hyp_norm": "SO IT SEEMS TO ME",
"duration_s": 2.28,
"infer_time_s": 1.817,
"rtf": 0.7971,
"wer": 0.0
},
{
"id": "1580-141083-0018",
"ref": "NOW MISTER SOAMES AT YOUR DISPOSAL",
"hyp": "Now, Mister Solmes, at your disposal.",
"ref_norm": "NOW MISTER SOAMES AT YOUR DISPOSAL",
"hyp_norm": "NOW MISTER SOLMES AT YOUR DISPOSAL",
"duration_s": 2.675,
"infer_time_s": 2.512,
"rtf": 0.9392,
"wer": 0.1667
},
{
"id": "1580-141083-0019",
"ref": "ABOVE WERE THREE STUDENTS ONE ON EACH STORY",
"hyp": "Above were three students, one on each story.",
"ref_norm": "ABOVE WERE THREE STUDENTS ONE ON EACH STORY",
"hyp_norm": "ABOVE WERE THREE STUDENTS ONE ON EACH STORY",
"duration_s": 2.705,
"infer_time_s": 2.483,
"rtf": 0.918,
"wer": 0.0
},
{
"id": "1580-141083-0020",
"ref": "THEN HE APPROACHED IT AND STANDING ON TIPTOE WITH HIS NECK CRANED HE LOOKED INTO THE ROOM",
"hyp": "Then he approached it, and standing on tiptoe with his neck craned, he looked into the room.",
"ref_norm": "THEN HE APPROACHED IT AND STANDING ON TIPTOE WITH HIS NECK CRANED HE LOOKED INTO THE ROOM",
"hyp_norm": "THEN HE APPROACHED IT AND STANDING ON TIPTOE WITH HIS NECK CRANED HE LOOKED INTO THE ROOM",
"duration_s": 5.135,
"infer_time_s": 5.14,
"rtf": 1.0011,
"wer": 0.0
},
{
"id": "1580-141083-0021",
"ref": "THERE IS NO OPENING EXCEPT THE ONE PANE SAID OUR LEARNED GUIDE",
"hyp": "There is no opening except the one pane,\" said our learned guide.",
"ref_norm": "THERE IS NO OPENING EXCEPT THE ONE PANE SAID OUR LEARNED GUIDE",
"hyp_norm": "THERE IS NO OPENING EXCEPT THE ONE PANE SAID OUR LEARNED GUIDE",
"duration_s": 3.715,
"infer_time_s": 3.177,
"rtf": 0.8553,
"wer": 0.0
},
{
"id": "1580-141083-0022",
"ref": "I AM AFRAID THERE ARE NO SIGNS HERE SAID HE",
"hyp": "I am afraid there are no signs here,\" said he.",
"ref_norm": "I AM AFRAID THERE ARE NO SIGNS HERE SAID HE",
"hyp_norm": "I AM AFRAID THERE ARE NO SIGNS HERE SAID HE",
"duration_s": 3.295,
"infer_time_s": 2.82,
"rtf": 0.8559,
"wer": 0.0
},
{
"id": "1580-141083-0023",
"ref": "ONE COULD HARDLY HOPE FOR ANY UPON SO DRY A DAY",
"hyp": "One could hardly hope for any upon so dry a day.",
"ref_norm": "ONE COULD HARDLY HOPE FOR ANY UPON SO DRY A DAY",
"hyp_norm": "ONE COULD HARDLY HOPE FOR ANY UPON SO DRY A DAY",
"duration_s": 3.33,
"infer_time_s": 2.84,
"rtf": 0.8528,
"wer": 0.0
},
{
"id": "1580-141083-0024",
"ref": "YOU LEFT HIM IN A CHAIR YOU SAY WHICH CHAIR BY THE WINDOW THERE",
"hyp": "You left him in a chair. You say which chair , by the window there.",
"ref_norm": "YOU LEFT HIM IN A CHAIR YOU SAY WHICH CHAIR BY THE WINDOW THERE",
"hyp_norm": "YOU LEFT HIM IN A CHAIR YOU SAY WHICH CHAIR BY THE WINDOW THERE",
"duration_s": 4.48,
"infer_time_s": 4.062,
"rtf": 0.9067,
"wer": 0.0
},
{
"id": "1580-141083-0025",
"ref": "THE MAN ENTERED AND TOOK THE PAPERS SHEET BY SHEET FROM THE CENTRAL TABLE",
"hyp": "The man entered and took the papers, sheet by sheet, from the central table.",
"ref_norm": "THE MAN ENTERED AND TOOK THE PAPERS SHEET BY SHEET FROM THE CENTRAL TABLE",
"hyp_norm": "THE MAN ENTERED AND TOOK THE PAPERS SHEET BY SHEET FROM THE CENTRAL TABLE",
"duration_s": 3.905,
"infer_time_s": 3.689,
"rtf": 0.9447,
"wer": 0.0
},
{
"id": "1580-141083-0026",
"ref": "AS A MATTER OF FACT HE COULD NOT SAID SOAMES FOR I ENTERED BY THE SIDE DOOR",
"hyp": "As a matter of fact, he could not,\" said Solmes. \"For I entered by the side door.\"",
"ref_norm": "AS A MATTER OF FACT HE COULD NOT SAID SOAMES FOR I ENTERED BY THE SIDE DOOR",
"hyp_norm": "AS A MATTER OF FACT HE COULD NOT SAID SOLMES FOR I ENTERED BY THE SIDE DOOR",
"duration_s": 4.775,
"infer_time_s": 4.849,
"rtf": 1.0154,
"wer": 0.0588
},
{
"id": "1580-141083-0027",
"ref": "HOW LONG WOULD IT TAKE HIM TO DO THAT USING EVERY POSSIBLE CONTRACTION A QUARTER OF AN HOUR NOT LESS",
"hyp": "How long would it take him to do that, using every possible contraction? A quarter of an hour, not less.",
"ref_norm": "HOW LONG WOULD IT TAKE HIM TO DO THAT USING EVERY POSSIBLE CONTRACTION A QUARTER OF AN HOUR NOT LESS",
"hyp_norm": "HOW LONG WOULD IT TAKE HIM TO DO THAT USING EVERY POSSIBLE CONTRACTION A QUARTER OF AN HOUR NOT LESS",
"duration_s": 5.225,
"infer_time_s": 5.454,
"rtf": 1.0439,
"wer": 0.0
},
{
"id": "1580-141083-0028",
"ref": "THEN HE TOSSED IT DOWN AND SEIZED THE NEXT",
"hyp": "Then he tossed it down and seized the next.",
"ref_norm": "THEN HE TOSSED IT DOWN AND SEIZED THE NEXT",
"hyp_norm": "THEN HE TOSSED IT DOWN AND SEIZED THE NEXT",
"duration_s": 2.585,
"infer_time_s": 2.56,
"rtf": 0.9904,
"wer": 0.0
},
{
"id": "1580-141083-0029",
"ref": "HE WAS IN THE MIDST OF THAT WHEN YOUR RETURN CAUSED HIM TO MAKE A VERY HURRIED RETREAT VERY HURRIED SINCE HE HAD NOT TIME TO REPLACE THE PAPERS WHICH WOULD TELL YOU THAT HE HAD BEEN THERE",
"hyp": "He was in the midst of that when your return caused him to make a very hurried retreat , very hurried, since he had not time to replace the papers which would tell you that he had been there.",
"ref_norm": "HE WAS IN THE MIDST OF THAT WHEN YOUR RETURN CAUSED HIM TO MAKE A VERY HURRIED RETREAT VERY HURRIED SINCE HE HAD NOT TIME TO REPLACE THE PAPERS WHICH WOULD TELL YOU THAT HE HAD BEEN THERE",
"hyp_norm": "HE WAS IN THE MIDST OF THAT WHEN YOUR RETURN CAUSED HIM TO MAKE A VERY HURRIED RETREAT VERY HURRIED SINCE HE HAD NOT TIME TO REPLACE THE PAPERS WHICH WOULD TELL YOU THAT HE HAD BEEN THERE",
"duration_s": 10.055,
"infer_time_s": 10.005,
"rtf": 0.995,
"wer": 0.0
},
{
"id": "1580-141083-0030",
"ref": "MISTER SOAMES WAS SOMEWHAT OVERWHELMED BY THIS FLOOD OF INFORMATION",
"hyp": "Mr. Solmes was somewhat overwhelmed by this flood of information.",
"ref_norm": "MISTER SOAMES WAS SOMEWHAT OVERWHELMED BY THIS FLOOD OF INFORMATION",
"hyp_norm": "MR SOLMES WAS SOMEWHAT OVERWHELMED BY THIS FLOOD OF INFORMATION",
"duration_s": 3.48,
"infer_time_s": 3.119,
"rtf": 0.8963,
"wer": 0.2
},
{
"id": "1580-141083-0031",
"ref": "HOLMES HELD OUT A SMALL CHIP WITH THE LETTERS N N AND A SPACE OF CLEAR WOOD AFTER THEM YOU SEE",
"hyp": "Holmes held out a small chip with the letters N N and a space of clear wood after them. You see.",
"ref_norm": "HOLMES HELD OUT A SMALL CHIP WITH THE LETTERS N N AND A SPACE OF CLEAR WOOD AFTER THEM YOU SEE",
"hyp_norm": "HOLMES HELD OUT A SMALL CHIP WITH THE LETTERS N N AND A SPACE OF CLEAR WOOD AFTER THEM YOU SEE",
"duration_s": 6.25,
"infer_time_s": 5.945,
"rtf": 0.9512,
"wer": 0.0
},
{
"id": "1580-141083-0032",
"ref": "WATSON I HAVE ALWAYS DONE YOU AN INJUSTICE THERE ARE OTHERS",
"hyp": "Watson, I have always done you an injustice. There are others.",
"ref_norm": "WATSON I HAVE ALWAYS DONE YOU AN INJUSTICE THERE ARE OTHERS",
"hyp_norm": "WATSON I HAVE ALWAYS DONE YOU AN INJUSTICE THERE ARE OTHERS",
"duration_s": 4.135,
"infer_time_s": 4.106,
"rtf": 0.9931,
"wer": 0.0
},
{
"id": "1580-141083-0033",
"ref": "I WAS HOPING THAT IF THE PAPER ON WHICH HE WROTE WAS THIN SOME TRACE OF IT MIGHT COME THROUGH UPON THIS POLISHED SURFACE NO I SEE NOTHING",
"hyp": "I was hoping that if the paper on which he wrote was thin , some trace of it might come through upon this polished surface. No, I see nothing.",
"ref_norm": "I WAS HOPING THAT IF THE PAPER ON WHICH HE WROTE WAS THIN SOME TRACE OF IT MIGHT COME THROUGH UPON THIS POLISHED SURFACE NO I SEE NOTHING",
"hyp_norm": "I WAS HOPING THAT IF THE PAPER ON WHICH HE WROTE WAS THIN SOME TRACE OF IT MIGHT COME THROUGH UPON THIS POLISHED SURFACE NO I SEE NOTHING",
"duration_s": 7.45,
"infer_time_s": 7.637,
"rtf": 1.025,
"wer": 0.0
},
{
"id": "1580-141083-0034",
"ref": "AS HOLMES DREW THE CURTAIN I WAS AWARE FROM SOME LITTLE RIGIDITY AND ALERTNESS OF HIS ATTITUDE THAT HE WAS PREPARED FOR AN EMERGENCY",
"hyp": "As Holmes drew the curtain, I was aware from some little rigidity and an alertness of his attitude that he was prepared for an emergency.",
"ref_norm": "AS HOLMES DREW THE CURTAIN I WAS AWARE FROM SOME LITTLE RIGIDITY AND ALERTNESS OF HIS ATTITUDE THAT HE WAS PREPARED FOR AN EMERGENCY",
"hyp_norm": "AS HOLMES DREW THE CURTAIN I WAS AWARE FROM SOME LITTLE RIGIDITY AND AN ALERTNESS OF HIS ATTITUDE THAT HE WAS PREPARED FOR AN EMERGENCY",
"duration_s": 6.99,
"infer_time_s": 7.029,
"rtf": 1.0055,
"wer": 0.0417
},
{
"id": "1580-141083-0035",
"ref": "HOLMES TURNED AWAY AND STOOPED SUDDENLY TO THE FLOOR HALLOA WHAT'S THIS",
"hyp": "Holmes turned away and stooped suddenly to the floor . \"Hallo! What is this?\"",
"ref_norm": "HOLMES TURNED AWAY AND STOOPED SUDDENLY TO THE FLOOR HALLOA WHATS THIS",
"hyp_norm": "HOLMES TURNED AWAY AND STOOPED SUDDENLY TO THE FLOOR HALLO WHAT IS THIS",
"duration_s": 4.98,
"infer_time_s": 4.367,
"rtf": 0.8769,
"wer": 0.25
},
{
"id": "1580-141083-0036",
"ref": "HOLMES HELD IT OUT ON HIS OPEN PALM IN THE GLARE OF THE ELECTRIC LIGHT",
"hyp": "Holmes held it out on his open palm in the glare of the electric light.",
"ref_norm": "HOLMES HELD IT OUT ON HIS OPEN PALM IN THE GLARE OF THE ELECTRIC LIGHT",
"hyp_norm": "HOLMES HELD IT OUT ON HIS OPEN PALM IN THE GLARE OF THE ELECTRIC LIGHT",
"duration_s": 3.98,
"infer_time_s": 3.108,
"rtf": 0.781,
"wer": 0.0
},
{
"id": "1580-141083-0037",
"ref": "WHAT COULD HE DO HE CAUGHT UP EVERYTHING WHICH WOULD BETRAY HIM AND HE RUSHED INTO YOUR BEDROOM TO CONCEAL HIMSELF",
"hyp": "What could he do? He caught up everything which would betray him , and he rushed into your bedroom to conceal himself.",
"ref_norm": "WHAT COULD HE DO HE CAUGHT UP EVERYTHING WHICH WOULD BETRAY HIM AND HE RUSHED INTO YOUR BEDROOM TO CONCEAL HIMSELF",
"hyp_norm": "WHAT COULD HE DO HE CAUGHT UP EVERYTHING WHICH WOULD BETRAY HIM AND HE RUSHED INTO YOUR BEDROOM TO CONCEAL HIMSELF",
"duration_s": 5.73,
"infer_time_s": 4.389,
"rtf": 0.766,
"wer": 0.0
},
{
"id": "1580-141083-0038",
"ref": "I UNDERSTAND YOU TO SAY THAT THERE ARE THREE STUDENTS WHO USE THIS STAIR AND ARE IN THE HABIT OF PASSING YOUR DOOR YES THERE ARE",
"hyp": "I understand you to say that there are three students who use this stair and are in the habit of passing your door. Yes, there are.",
"ref_norm": "I UNDERSTAND YOU TO SAY THAT THERE ARE THREE STUDENTS WHO USE THIS STAIR AND ARE IN THE HABIT OF PASSING YOUR DOOR YES THERE ARE",
"hyp_norm": "I UNDERSTAND YOU TO SAY THAT THERE ARE THREE STUDENTS WHO USE THIS STAIR AND ARE IN THE HABIT OF PASSING YOUR DOOR YES THERE ARE",
"duration_s": 7.535,
"infer_time_s": 7.157,
"rtf": 0.9498,
"wer": 0.0
},
{
"id": "1580-141083-0039",
"ref": "AND THEY ARE ALL IN FOR THIS EXAMINATION YES",
"hyp": "And they are all in for this examination. Yes.",
"ref_norm": "AND THEY ARE ALL IN FOR THIS EXAMINATION YES",
"hyp_norm": "AND THEY ARE ALL IN FOR THIS EXAMINATION YES",
"duration_s": 3.725,
"infer_time_s": 2.821,
"rtf": 0.7574,
"wer": 0.0
},
{
"id": "1580-141083-0040",
"ref": "ONE HARDLY LIKES TO THROW SUSPICION WHERE THERE ARE NO PROOFS",
"hyp": "One hardly likes to throw suspicion where there are no proofs.",
"ref_norm": "ONE HARDLY LIKES TO THROW SUSPICION WHERE THERE ARE NO PROOFS",
"hyp_norm": "ONE HARDLY LIKES TO THROW SUSPICION WHERE THERE ARE NO PROOFS",
"duration_s": 3.75,
"infer_time_s": 2.998,
"rtf": 0.7994,
"wer": 0.0
},
{
"id": "1580-141083-0041",
"ref": "LET US HEAR THE SUSPICIONS I WILL LOOK AFTER THE PROOFS",
"hyp": "Let us hear the suspicions . I will look after the proofs.",
"ref_norm": "LET US HEAR THE SUSPICIONS I WILL LOOK AFTER THE PROOFS",
"hyp_norm": "LET US HEAR THE SUSPICIONS I WILL LOOK AFTER THE PROOFS",
"duration_s": 3.575,
"infer_time_s": 3.208,
"rtf": 0.8974,
"wer": 0.0
},
{
"id": "1580-141083-0042",
"ref": "MY SCHOLAR HAS BEEN LEFT VERY POOR BUT HE IS HARD WORKING AND INDUSTRIOUS HE WILL DO WELL",
"hyp": "My scholar has been left very poor, but he is hard working and industrious. He will do well.",
"ref_norm": "MY SCHOLAR HAS BEEN LEFT VERY POOR BUT HE IS HARD WORKING AND INDUSTRIOUS HE WILL DO WELL",
"hyp_norm": "MY SCHOLAR HAS BEEN LEFT VERY POOR BUT HE IS HARD WORKING AND INDUSTRIOUS HE WILL DO WELL",
"duration_s": 5.865,
"infer_time_s": 5.25,
"rtf": 0.8951,
"wer": 0.0
},
{
"id": "1580-141083-0043",
"ref": "THE TOP FLOOR BELONGS TO MILES MC LAREN",
"hyp": "The top floor belongs to Miles McLaren.",
"ref_norm": "THE TOP FLOOR BELONGS TO MILES MC LAREN",
"hyp_norm": "THE TOP FLOOR BELONGS TO MILES MCLAREN",
"duration_s": 2.74,
"infer_time_s": 2.248,
"rtf": 0.8203,
"wer": 0.25
},
{
"id": "1580-141083-0044",
"ref": "I DARE NOT GO SO FAR AS THAT BUT OF THE THREE HE IS PERHAPS THE LEAST UNLIKELY",
"hyp": "I dare not go so far as that, but of the three , he is perhaps the least unlikely.",
"ref_norm": "I DARE NOT GO SO FAR AS THAT BUT OF THE THREE HE IS PERHAPS THE LEAST UNLIKELY",
"hyp_norm": "I DARE NOT GO SO FAR AS THAT BUT OF THE THREE HE IS PERHAPS THE LEAST UNLIKELY",
"duration_s": 5.505,
"infer_time_s": 5.297,
"rtf": 0.9623,
"wer": 0.0
},
{
"id": "1580-141083-0045",
"ref": "HE WAS STILL SUFFERING FROM THIS SUDDEN DISTURBANCE OF THE QUIET ROUTINE OF HIS LIFE",
"hyp": "He was still suffering from this sudden disturbance of the quiet routine of his life.",
"ref_norm": "HE WAS STILL SUFFERING FROM THIS SUDDEN DISTURBANCE OF THE QUIET ROUTINE OF HIS LIFE",
"hyp_norm": "HE WAS STILL SUFFERING FROM THIS SUDDEN DISTURBANCE OF THE QUIET ROUTINE OF HIS LIFE",
"duration_s": 4.36,
"infer_time_s": 4.345,
"rtf": 0.9965,
"wer": 0.0
},
{
"id": "1580-141083-0046",
"ref": "BUT I HAVE OCCASIONALLY DONE THE SAME THING AT OTHER TIMES",
"hyp": "But I have occasionally done the same thing at other times.",
"ref_norm": "BUT I HAVE OCCASIONALLY DONE THE SAME THING AT OTHER TIMES",
"hyp_norm": "BUT I HAVE OCCASIONALLY DONE THE SAME THING AT OTHER TIMES",
"duration_s": 3.53,
"infer_time_s": 3.258,
"rtf": 0.9229,
"wer": 0.0
},
{
"id": "1580-141083-0047",
"ref": "DID YOU LOOK AT THESE PAPERS ON THE TABLE",
"hyp": "Did you look at these papers on the table?",
"ref_norm": "DID YOU LOOK AT THESE PAPERS ON THE TABLE",
"hyp_norm": "DID YOU LOOK AT THESE PAPERS ON THE TABLE",
"duration_s": 2.605,
"infer_time_s": 2.751,
"rtf": 1.0561,
"wer": 0.0
},
{
"id": "1580-141083-0048",
"ref": "HOW CAME YOU TO LEAVE THE KEY IN THE DOOR",
"hyp": "How came you to leave the key in the door?",
"ref_norm": "HOW CAME YOU TO LEAVE THE KEY IN THE DOOR",
"hyp_norm": "HOW CAME YOU TO LEAVE THE KEY IN THE DOOR",
"duration_s": 2.785,
"infer_time_s": 2.867,
"rtf": 1.0294,
"wer": 0.0
},
{
"id": "1580-141083-0049",
"ref": "ANYONE IN THE ROOM COULD GET OUT YES SIR",
"hyp": "Anyone in the room could get out. Yes, sir.",
"ref_norm": "ANYONE IN THE ROOM COULD GET OUT YES SIR",
"hyp_norm": "ANYONE IN THE ROOM COULD GET OUT YES SIR",
"duration_s": 3.845,
"infer_time_s": 3.084,
"rtf": 0.8022,
"wer": 0.0
},
{
"id": "1580-141083-0050",
"ref": "I REALLY DON'T THINK HE KNEW MUCH ABOUT IT MISTER HOLMES",
"hyp": "I really don't think he knew much about it, Mister Holmes.",
"ref_norm": "I REALLY DONT THINK HE KNEW MUCH ABOUT IT MISTER HOLMES",
"hyp_norm": "I REALLY DONT THINK HE KNEW MUCH ABOUT IT MISTER HOLMES",
"duration_s": 3.085,
"infer_time_s": 3.763,
"rtf": 1.2196,
"wer": 0.0
},
{
"id": "1580-141083-0051",
"ref": "ONLY FOR A MINUTE OR SO",
"hyp": "Only for a minute or so.",
"ref_norm": "ONLY FOR A MINUTE OR SO",
"hyp_norm": "ONLY FOR A MINUTE OR SO",
"duration_s": 1.98,
"infer_time_s": 1.726,
"rtf": 0.8717,
"wer": 0.0
},
{
"id": "1580-141083-0052",
"ref": "OH I WOULD NOT VENTURE TO SAY SIR",
"hyp": "Oh, I would not venture to say, sir.",
"ref_norm": "OH I WOULD NOT VENTURE TO SAY SIR",
"hyp_norm": "OH I WOULD NOT VENTURE TO SAY SIR",
"duration_s": 3.45,
"infer_time_s": 2.843,
"rtf": 0.824,
"wer": 0.0
},
{
"id": "1580-141083-0053",
"ref": "YOU HAVEN'T SEEN ANY OF THEM NO SIR",
"hyp": "You haven't seen any of them, no, sir.",
"ref_norm": "YOU HAVENT SEEN ANY OF THEM NO SIR",
"hyp_norm": "YOU HAVENT SEEN ANY OF THEM NO SIR",
"duration_s": 4.015,
"infer_time_s": 3.44,
"rtf": 0.8567,
"wer": 0.0
},
{
"id": "1580-141084-0000",
"ref": "IT WAS THE INDIAN WHOSE DARK SILHOUETTE APPEARED SUDDENLY UPON HIS BLIND",
"hyp": "It was the Indian whose dark silhouette appeared suddenly upon his blind.",
"ref_norm": "IT WAS THE INDIAN WHOSE DARK SILHOUETTE APPEARED SUDDENLY UPON HIS BLIND",
"hyp_norm": "IT WAS THE INDIAN WHOSE DARK SILHOUETTE APPEARED SUDDENLY UPON HIS BLIND",
"duration_s": 4.615,
"infer_time_s": 3.67,
"rtf": 0.7953,
"wer": 0.0
},
{
"id": "1580-141084-0001",
"ref": "HE WAS PACING SWIFTLY UP AND DOWN HIS ROOM",
"hyp": "He was pacing swiftly up and down his room.",
"ref_norm": "HE WAS PACING SWIFTLY UP AND DOWN HIS ROOM",
"hyp_norm": "HE WAS PACING SWIFTLY UP AND DOWN HIS ROOM",
"duration_s": 3.265,
"infer_time_s": 2.659,
"rtf": 0.8143,
"wer": 0.0
},
{
"id": "1580-141084-0002",
"ref": "THIS SET OF ROOMS IS QUITE THE OLDEST IN THE COLLEGE AND IT IS NOT UNUSUAL FOR VISITORS TO GO OVER THEM",
"hyp": "The set of rooms is quite the oldest in the college , and it is not unusual for visitors to go over them.",
"ref_norm": "THIS SET OF ROOMS IS QUITE THE OLDEST IN THE COLLEGE AND IT IS NOT UNUSUAL FOR VISITORS TO GO OVER THEM",
"hyp_norm": "THE SET OF ROOMS IS QUITE THE OLDEST IN THE COLLEGE AND IT IS NOT UNUSUAL FOR VISITORS TO GO OVER THEM",
"duration_s": 5.905,
"infer_time_s": 5.77,
"rtf": 0.9771,
"wer": 0.0455
},
{
"id": "1580-141084-0003",
"ref": "NO NAMES PLEASE SAID HOLMES AS WE KNOCKED AT GILCHRIST'S DOOR",
"hyp": "No names, please ,\" said Holmes, as we knocked at Gilchrist's door.",
"ref_norm": "NO NAMES PLEASE SAID HOLMES AS WE KNOCKED AT GILCHRISTS DOOR",
"hyp_norm": "NO NAMES PLEASE SAID HOLMES AS WE KNOCKED AT GILCHRISTS DOOR",
"duration_s": 4.1,
"infer_time_s": 4.284,
"rtf": 1.0449,
"wer": 0.0
},
{
"id": "1580-141084-0004",
"ref": "OF COURSE HE DID NOT REALIZE THAT IT WAS I WHO WAS KNOCKING BUT NONE THE LESS HIS CONDUCT WAS VERY UNCOURTEOUS AND INDEED UNDER THE CIRCUMSTANCES RATHER SUSPICIOUS",
"hyp": "Of course, he did not realize that it was I who was knocking, but none the less, his conduct was very uncourte ous, and indeed, under the circumstances, rather suspicious.",
"ref_norm": "OF COURSE HE DID NOT REALIZE THAT IT WAS I WHO WAS KNOCKING BUT NONE THE LESS HIS CONDUCT WAS VERY UNCOURTEOUS AND INDEED UNDER THE CIRCUMSTANCES RATHER SUSPICIOUS",
"hyp_norm": "OF COURSE HE DID NOT REALIZE THAT IT WAS I WHO WAS KNOCKING BUT NONE THE LESS HIS CONDUCT WAS VERY UNCOURTE OUS AND INDEED UNDER THE CIRCUMSTANCES RATHER SUSPICIOUS",
"duration_s": 9.005,
"infer_time_s": 9.481,
"rtf": 1.0529,
"wer": 0.069
},
{
"id": "1580-141084-0005",
"ref": "THAT IS VERY IMPORTANT SAID HOLMES",
"hyp": "That is very important,\" said Holmes.",
"ref_norm": "THAT IS VERY IMPORTANT SAID HOLMES",
"hyp_norm": "THAT IS VERY IMPORTANT SAID HOLMES",
"duration_s": 2.515,
"infer_time_s": 2.251,
"rtf": 0.8951,
"wer": 0.0
},
{
"id": "1580-141084-0006",
"ref": "YOU DON'T SEEM TO REALIZE THE POSITION",
"hyp": "You don't seem to realize the position.",
"ref_norm": "YOU DONT SEEM TO REALIZE THE POSITION",
"hyp_norm": "YOU DONT SEEM TO REALIZE THE POSITION",
"duration_s": 2.135,
"infer_time_s": 2.458,
"rtf": 1.1513,
"wer": 0.0
},
{
"id": "1580-141084-0007",
"ref": "TO MORROW IS THE EXAMINATION",
"hyp": "Tomorrow is the examination.",
"ref_norm": "TO MORROW IS THE EXAMINATION",
"hyp_norm": "TOMORROW IS THE EXAMINATION",
"duration_s": 2.02,
"infer_time_s": 1.699,
"rtf": 0.8411,
"wer": 0.4
},
{
"id": "1580-141084-0008",
"ref": "I CANNOT ALLOW THE EXAMINATION TO BE HELD IF ONE OF THE PAPERS HAS BEEN TAMPERED WITH THE SITUATION MUST BE FACED",
"hyp": "I cannot allow the examination to be held if one of the papers has been tampered with . The situation must be faced.",
"ref_norm": "I CANNOT ALLOW THE EXAMINATION TO BE HELD IF ONE OF THE PAPERS HAS BEEN TAMPERED WITH THE SITUATION MUST BE FACED",
"hyp_norm": "I CANNOT ALLOW THE EXAMINATION TO BE HELD IF ONE OF THE PAPERS HAS BEEN TAMPERED WITH THE SITUATION MUST BE FACED",
"duration_s": 6.795,
"infer_time_s": 6.315,
"rtf": 0.9293,
"wer": 0.0
},
{
"id": "1580-141084-0009",
"ref": "IT IS POSSIBLE THAT I MAY BE IN A POSITION THEN TO INDICATE SOME COURSE OF ACTION",
"hyp": "It is possible that I may be in a position then to indicate some course of action.",
"ref_norm": "IT IS POSSIBLE THAT I MAY BE IN A POSITION THEN TO INDICATE SOME COURSE OF ACTION",
"hyp_norm": "IT IS POSSIBLE THAT I MAY BE IN A POSITION THEN TO INDICATE SOME COURSE OF ACTION",
"duration_s": 4.685,
"infer_time_s": 4.58,
"rtf": 0.9775,
"wer": 0.0
},
{
"id": "1580-141084-0010",
"ref": "I WILL TAKE THE BLACK CLAY WITH ME ALSO THE PENCIL CUTTINGS GOOD BYE",
"hyp": "I will take the black clay with me, also the pencil cut tings. Goodbye.",
"ref_norm": "I WILL TAKE THE BLACK CLAY WITH ME ALSO THE PENCIL CUTTINGS GOOD BYE",
"hyp_norm": "I WILL TAKE THE BLACK CLAY WITH ME ALSO THE PENCIL CUT TINGS GOODBYE",
"duration_s": 4.47,
"infer_time_s": 4.593,
"rtf": 1.0274,
"wer": 0.2143
},
{
"id": "1580-141084-0011",
"ref": "WHEN WE WERE OUT IN THE DARKNESS OF THE QUADRANGLE WE AGAIN LOOKED UP AT THE WINDOWS",
"hyp": "When we were out in the darkness of the quadrangle, we again looked up at the windows.",
"ref_norm": "WHEN WE WERE OUT IN THE DARKNESS OF THE QUADRANGLE WE AGAIN LOOKED UP AT THE WINDOWS",
"hyp_norm": "WHEN WE WERE OUT IN THE DARKNESS OF THE QUADRANGLE WE AGAIN LOOKED UP AT THE WINDOWS",
"duration_s": 5.0,
"infer_time_s": 4.772,
"rtf": 0.9545,
"wer": 0.0
},
{
"id": "1580-141084-0012",
"ref": "THE FOUL MOUTHED FELLOW AT THE TOP",
"hyp": "The foul-mouthed fellow at the top.",
"ref_norm": "THE FOUL MOUTHED FELLOW AT THE TOP",
"hyp_norm": "THE FOULMOUTHED FELLOW AT THE TOP",
"duration_s": 2.485,
"infer_time_s": 2.407,
"rtf": 0.9688,
"wer": 0.2857
},
{
"id": "1580-141084-0013",
"ref": "HE IS THE ONE WITH THE WORST RECORD",
"hyp": "He is the one with the worst record.",
"ref_norm": "HE IS THE ONE WITH THE WORST RECORD",
"hyp_norm": "HE IS THE ONE WITH THE WORST RECORD",
"duration_s": 2.225,
"infer_time_s": 2.387,
"rtf": 1.073,
"wer": 0.0
},
{
"id": "1580-141084-0014",
"ref": "WHY BANNISTER THE SERVANT WHAT'S HIS GAME IN THE MATTER",
"hyp": "Why, Bannister, the servant? What's his game in the matter?",
"ref_norm": "WHY BANNISTER THE SERVANT WHATS HIS GAME IN THE MATTER",
"hyp_norm": "WHY BANNISTER THE SERVANT WHATS HIS GAME IN THE MATTER",
"duration_s": 3.97,
"infer_time_s": 3.892,
"rtf": 0.9802,
"wer": 0.0
},
{
"id": "1580-141084-0015",
"ref": "HE IMPRESSED ME AS BEING A PERFECTLY HONEST MAN",
"hyp": "He impressed me as being a perfectly honest man.",
"ref_norm": "HE IMPRESSED ME AS BEING A PERFECTLY HONEST MAN",
"hyp_norm": "HE IMPRESSED ME AS BEING A PERFECTLY HONEST MAN",
"duration_s": 3.47,
"infer_time_s": 2.575,
"rtf": 0.742,
"wer": 0.0
},
{
"id": "1580-141084-0016",
"ref": "MY FRIEND DID NOT APPEAR TO BE DEPRESSED BY HIS FAILURE BUT SHRUGGED HIS SHOULDERS IN HALF HUMOROUS RESIGNATION",
"hyp": "My friend did not appear to be depressed by his failure, but shrugged his shoulders in half-humorous resignation.",
"ref_norm": "MY FRIEND DID NOT APPEAR TO BE DEPRESSED BY HIS FAILURE BUT SHRUGGED HIS SHOULDERS IN HALF HUMOROUS RESIGNATION",
"hyp_norm": "MY FRIEND DID NOT APPEAR TO BE DEPRESSED BY HIS FAILURE BUT SHRUGGED HIS SHOULDERS IN HALFHUMOROUS RESIGNATION",
"duration_s": 5.96,
"infer_time_s": 5.314,
"rtf": 0.8916,
"wer": 0.1053
},
{
"id": "1580-141084-0017",
"ref": "NO GOOD MY DEAR WATSON",
"hyp": "No good, my dear Watson.",
"ref_norm": "NO GOOD MY DEAR WATSON",
"hyp_norm": "NO GOOD MY DEAR WATSON",
"duration_s": 2.0,
"infer_time_s": 1.713,
"rtf": 0.8567,
"wer": 0.0
},
{
"id": "1580-141084-0018",
"ref": "I THINK SO YOU HAVE FORMED A CONCLUSION",
"hyp": "I think so . You have formed a conclusion.",
"ref_norm": "I THINK SO YOU HAVE FORMED A CONCLUSION",
"hyp_norm": "I THINK SO YOU HAVE FORMED A CONCLUSION",
"duration_s": 3.345,
"infer_time_s": 2.618,
"rtf": 0.7828,
"wer": 0.0
},
{
"id": "1580-141084-0019",
"ref": "YES MY DEAR WATSON I HAVE SOLVED THE MYSTERY",
"hyp": "Yes, my dear Watson, I have solved the mystery.",
"ref_norm": "YES MY DEAR WATSON I HAVE SOLVED THE MYSTERY",
"hyp_norm": "YES MY DEAR WATSON I HAVE SOLVED THE MYSTERY",
"duration_s": 3.125,
"infer_time_s": 2.949,
"rtf": 0.9436,
"wer": 0.0
},
{
"id": "1580-141084-0020",
"ref": "LOOK AT THAT HE HELD OUT HIS HAND",
"hyp": "Look at that! He held out his hand.",
"ref_norm": "LOOK AT THAT HE HELD OUT HIS HAND",
"hyp_norm": "LOOK AT THAT HE HELD OUT HIS HAND",
"duration_s": 2.86,
"infer_time_s": 2.587,
"rtf": 0.9046,
"wer": 0.0
},
{
"id": "1580-141084-0021",
"ref": "ON THE PALM WERE THREE LITTLE PYRAMIDS OF BLACK DOUGHY CLAY",
"hyp": "On the palm were three little pyramids of black dough y clay.",
"ref_norm": "ON THE PALM WERE THREE LITTLE PYRAMIDS OF BLACK DOUGHY CLAY",
"hyp_norm": "ON THE PALM WERE THREE LITTLE PYRAMIDS OF BLACK DOUGH Y CLAY",
"duration_s": 4.01,
"infer_time_s": 3.718,
"rtf": 0.9272,
"wer": 0.1818
},
{
"id": "1580-141084-0022",
"ref": "AND ONE MORE THIS MORNING",
"hyp": "And one more this morning.",
"ref_norm": "AND ONE MORE THIS MORNING",
"hyp_norm": "AND ONE MORE THIS MORNING",
"duration_s": 2.06,
"infer_time_s": 1.858,
"rtf": 0.902,
"wer": 0.0
},
{
"id": "1580-141084-0023",
"ref": "IN A FEW HOURS THE EXAMINATION WOULD COMMENCE AND HE WAS STILL IN THE DILEMMA BETWEEN MAKING THE FACTS PUBLIC AND ALLOWING THE CULPRIT TO COMPETE FOR THE VALUABLE SCHOLARSHIP",
"hyp": "In a few hours, the examination would commence, and he was still in the dilemma between making the facts public and allowing the culprit to compete for the valuable scholarship.",
"ref_norm": "IN A FEW HOURS THE EXAMINATION WOULD COMMENCE AND HE WAS STILL IN THE DILEMMA BETWEEN MAKING THE FACTS PUBLIC AND ALLOWING THE CULPRIT TO COMPETE FOR THE VALUABLE SCHOLARSHIP",
"hyp_norm": "IN A FEW HOURS THE EXAMINATION WOULD COMMENCE AND HE WAS STILL IN THE DILEMMA BETWEEN MAKING THE FACTS PUBLIC AND ALLOWING THE CULPRIT TO COMPETE FOR THE VALUABLE SCHOLARSHIP",
"duration_s": 8.735,
"infer_time_s": 8.115,
"rtf": 0.929,
"wer": 0.0
},
{
"id": "1580-141084-0024",
"ref": "HE COULD HARDLY STAND STILL SO GREAT WAS HIS MENTAL AGITATION AND HE RAN TOWARDS HOLMES WITH TWO EAGER HANDS OUTSTRETCHED THANK HEAVEN THAT YOU HAVE COME",
"hyp": "He could hardly stand still; so great was his mental agitation, and he ran towards Holmes with two eager hands outstretched. Thank heaven that you have come.",
"ref_norm": "HE COULD HARDLY STAND STILL SO GREAT WAS HIS MENTAL AGITATION AND HE RAN TOWARDS HOLMES WITH TWO EAGER HANDS OUTSTRETCHED THANK HEAVEN THAT YOU HAVE COME",
"hyp_norm": "HE COULD HARDLY STAND STILL SO GREAT WAS HIS MENTAL AGITATION AND HE RAN TOWARDS HOLMES WITH TWO EAGER HANDS OUTSTRETCHED THANK HEAVEN THAT YOU HAVE COME",
"duration_s": 9.185,
"infer_time_s": 8.197,
"rtf": 0.8924,
"wer": 0.0
},
{
"id": "1580-141084-0025",
"ref": "YOU KNOW HIM I THINK SO",
"hyp": "You know him, I think so.",
"ref_norm": "YOU KNOW HIM I THINK SO",
"hyp_norm": "YOU KNOW HIM I THINK SO",
"duration_s": 2.375,
"infer_time_s": 2.278,
"rtf": 0.9592,
"wer": 0.0
},
{
"id": "1580-141084-0026",
"ref": "IF THIS MATTER IS NOT TO BECOME PUBLIC WE MUST GIVE OURSELVES CERTAIN POWERS AND RESOLVE OURSELVES INTO A SMALL PRIVATE COURT MARTIAL",
"hyp": "If this matter is not to become public, we must give ourselves certain powers and resolve ourselves into a small private court martial.",
"ref_norm": "IF THIS MATTER IS NOT TO BECOME PUBLIC WE MUST GIVE OURSELVES CERTAIN POWERS AND RESOLVE OURSELVES INTO A SMALL PRIVATE COURT MARTIAL",
"hyp_norm": "IF THIS MATTER IS NOT TO BECOME PUBLIC WE MUST GIVE OURSELVES CERTAIN POWERS AND RESOLVE OURSELVES INTO A SMALL PRIVATE COURT MARTIAL",
"duration_s": 6.995,
"infer_time_s": 6.09,
"rtf": 0.8707,
"wer": 0.0
},
{
"id": "1580-141084-0027",
"ref": "NO SIR CERTAINLY NOT",
"hyp": "No, sir. Certainly not.",
"ref_norm": "NO SIR CERTAINLY NOT",
"hyp_norm": "NO SIR CERTAINLY NOT",
"duration_s": 3.36,
"infer_time_s": 2.041,
"rtf": 0.6076,
"wer": 0.0
},
{
"id": "1580-141084-0028",
"ref": "THERE WAS NO MAN SIR",
"hyp": "There was no man, sir.",
"ref_norm": "THERE WAS NO MAN SIR",
"hyp_norm": "THERE WAS NO MAN SIR",
"duration_s": 2.655,
"infer_time_s": 2.061,
"rtf": 0.7764,
"wer": 0.0
},
{
"id": "1580-141084-0029",
"ref": "HIS TROUBLED BLUE EYES GLANCED AT EACH OF US AND FINALLY RESTED WITH AN EXPRESSION OF BLANK DISMAY UPON BANNISTER IN THE FARTHER CORNER",
"hyp": "His troubled blue eyes glanced at each of us, and finally rested with an expression of blank dismay upon Bannister in the farther corner.",
"ref_norm": "HIS TROUBLED BLUE EYES GLANCED AT EACH OF US AND FINALLY RESTED WITH AN EXPRESSION OF BLANK DISMAY UPON BANNISTER IN THE FARTHER CORNER",
"hyp_norm": "HIS TROUBLED BLUE EYES GLANCED AT EACH OF US AND FINALLY RESTED WITH AN EXPRESSION OF BLANK DISMAY UPON BANNISTER IN THE FARTHER CORNER",
"duration_s": 8.075,
"infer_time_s": 7.375,
"rtf": 0.9133,
"wer": 0.0
},
{
"id": "1580-141084-0030",
"ref": "JUST CLOSE THE DOOR SAID HOLMES",
"hyp": "Just close the door,\" said Holmes.",
"ref_norm": "JUST CLOSE THE DOOR SAID HOLMES",
"hyp_norm": "JUST CLOSE THE DOOR SAID HOLMES",
"duration_s": 2.145,
"infer_time_s": 2.257,
"rtf": 1.0521,
"wer": 0.0
},
{
"id": "1580-141084-0031",
"ref": "WE WANT TO KNOW MISTER GILCHRIST HOW YOU AN HONOURABLE MAN EVER CAME TO COMMIT SUCH AN ACTION AS THAT OF YESTERDAY",
"hyp": "We want to know, Mister Gil christ, how you, an honour able man, ever came to commit such an action as that of yesterday.",
"ref_norm": "WE WANT TO KNOW MISTER GILCHRIST HOW YOU AN HONOURABLE MAN EVER CAME TO COMMIT SUCH AN ACTION AS THAT OF YESTERDAY",
"hyp_norm": "WE WANT TO KNOW MISTER GIL CHRIST HOW YOU AN HONOUR ABLE MAN EVER CAME TO COMMIT SUCH AN ACTION AS THAT OF YESTERDAY",
"duration_s": 6.47,
"infer_time_s": 6.775,
"rtf": 1.0471,
"wer": 0.1818
},
{
"id": "1580-141084-0032",
"ref": "FOR A MOMENT GILCHRIST WITH UPRAISED HAND TRIED TO CONTROL HIS WRITHING FEATURES",
"hyp": "For a moment, Gil christ, with upraised hand, tried to control his writhing features.",
"ref_norm": "FOR A MOMENT GILCHRIST WITH UPRAISED HAND TRIED TO CONTROL HIS WRITHING FEATURES",
"hyp_norm": "FOR A MOMENT GIL CHRIST WITH UPRAISED HAND TRIED TO CONTROL HIS WRITHING FEATURES",
"duration_s": 4.995,
"infer_time_s": 4.973,
"rtf": 0.9957,
"wer": 0.1538
},
{
"id": "1580-141084-0033",
"ref": "COME COME SAID HOLMES KINDLY IT IS HUMAN TO ERR AND AT LEAST NO ONE CAN ACCUSE YOU OF BEING A CALLOUS CRIMINAL",
"hyp": "Come, come,\" said Holmes kindly. \"It is human to err, and at least no one can accuse you of being a callous criminal.\"",
"ref_norm": "COME COME SAID HOLMES KINDLY IT IS HUMAN TO ERR AND AT LEAST NO ONE CAN ACCUSE YOU OF BEING A CALLOUS CRIMINAL",
"hyp_norm": "COME COME SAID HOLMES KINDLY IT IS HUMAN TO ERR AND AT LEAST NO ONE CAN ACCUSE YOU OF BEING A CALLOUS CRIMINAL",
"duration_s": 7.0,
"infer_time_s": 6.958,
"rtf": 0.994,
"wer": 0.0
},
{
"id": "1580-141084-0034",
"ref": "WELL WELL DON'T TROUBLE TO ANSWER LISTEN AND SEE THAT I DO YOU NO INJUSTICE",
"hyp": "Well, well! Don't trouble to answer. Listen and see that I do you no injustice.",
"ref_norm": "WELL WELL DONT TROUBLE TO ANSWER LISTEN AND SEE THAT I DO YOU NO INJUSTICE",
"hyp_norm": "WELL WELL DONT TROUBLE TO ANSWER LISTEN AND SEE THAT I DO YOU NO INJUSTICE",
"duration_s": 4.49,
"infer_time_s": 4.72,
"rtf": 1.0511,
"wer": 0.0
},
{
"id": "1580-141084-0035",
"ref": "HE COULD EXAMINE THE PAPERS IN HIS OWN OFFICE",
"hyp": "He could examine the papers in his own office.",
"ref_norm": "HE COULD EXAMINE THE PAPERS IN HIS OWN OFFICE",
"hyp_norm": "HE COULD EXAMINE THE PAPERS IN HIS OWN OFFICE",
"duration_s": 2.63,
"infer_time_s": 2.587,
"rtf": 0.9836,
"wer": 0.0
},
{
"id": "1580-141084-0036",
"ref": "THE INDIAN I ALSO THOUGHT NOTHING OF",
"hyp": "The Indian, I also thought nothing of.",
"ref_norm": "THE INDIAN I ALSO THOUGHT NOTHING OF",
"hyp_norm": "THE INDIAN I ALSO THOUGHT NOTHING OF",
"duration_s": 2.475,
"infer_time_s": 2.369,
"rtf": 0.9572,
"wer": 0.0
},
{
"id": "1580-141084-0037",
"ref": "WHEN I APPROACHED YOUR ROOM I EXAMINED THE WINDOW",
"hyp": "When I approached your room , I examined the window.",
"ref_norm": "WHEN I APPROACHED YOUR ROOM I EXAMINED THE WINDOW",
"hyp_norm": "WHEN I APPROACHED YOUR ROOM I EXAMINED THE WINDOW",
"duration_s": 2.965,
"infer_time_s": 2.744,
"rtf": 0.9255,
"wer": 0.0
},
{
"id": "1580-141084-0038",
"ref": "NO ONE LESS THAN THAT WOULD HAVE A CHANCE",
"hyp": "No one less than that would have a chance.",
"ref_norm": "NO ONE LESS THAN THAT WOULD HAVE A CHANCE",
"hyp_norm": "NO ONE LESS THAN THAT WOULD HAVE A CHANCE",
"duration_s": 2.955,
"infer_time_s": 2.639,
"rtf": 0.8929,
"wer": 0.0
},
{
"id": "1580-141084-0039",
"ref": "I ENTERED AND I TOOK YOU INTO MY CONFIDENCE AS TO THE SUGGESTIONS OF THE SIDE TABLE",
"hyp": "I entered, and I took you into my confidence as to the suggestions of the side table.",
"ref_norm": "I ENTERED AND I TOOK YOU INTO MY CONFIDENCE AS TO THE SUGGESTIONS OF THE SIDE TABLE",
"hyp_norm": "I ENTERED AND I TOOK YOU INTO MY CONFIDENCE AS TO THE SUGGESTIONS OF THE SIDE TABLE",
"duration_s": 4.885,
"infer_time_s": 4.587,
"rtf": 0.9389,
"wer": 0.0
},
{
"id": "1580-141084-0040",
"ref": "HE RETURNED CARRYING HIS JUMPING SHOES WHICH ARE PROVIDED AS YOU ARE AWARE WITH SEVERAL SHARP SPIKES",
"hyp": "He returned carrying his jumping shoes, which are provided, as you are aware, with several sharp spikes.",
"ref_norm": "HE RETURNED CARRYING HIS JUMPING SHOES WHICH ARE PROVIDED AS YOU ARE AWARE WITH SEVERAL SHARP SPIKES",
"hyp_norm": "HE RETURNED CARRYING HIS JUMPING SHOES WHICH ARE PROVIDED AS YOU ARE AWARE WITH SEVERAL SHARP SPIKES",
"duration_s": 5.985,
"infer_time_s": 4.924,
"rtf": 0.8227,
"wer": 0.0
},
{
"id": "1580-141084-0041",
"ref": "NO HARM WOULD HAVE BEEN DONE HAD IT NOT BEEN THAT AS HE PASSED YOUR DOOR HE PERCEIVED THE KEY WHICH HAD BEEN LEFT BY THE CARELESSNESS OF YOUR SERVANT",
"hyp": "No harm would have been done had it not been that, as he passed your door, he perceived the key which had been left by the carelessness of your servant.",
"ref_norm": "NO HARM WOULD HAVE BEEN DONE HAD IT NOT BEEN THAT AS HE PASSED YOUR DOOR HE PERCEIVED THE KEY WHICH HAD BEEN LEFT BY THE CARELESSNESS OF YOUR SERVANT",
"hyp_norm": "NO HARM WOULD HAVE BEEN DONE HAD IT NOT BEEN THAT AS HE PASSED YOUR DOOR HE PERCEIVED THE KEY WHICH HAD BEEN LEFT BY THE CARELESSNESS OF YOUR SERVANT",
"duration_s": 7.99,
"infer_time_s": 7.82,
"rtf": 0.9787,
"wer": 0.0
},
{
"id": "1580-141084-0042",
"ref": "A SUDDEN IMPULSE CAME OVER HIM TO ENTER AND SEE IF THEY WERE INDEED THE PROOFS",
"hyp": "A sudden impulse came over him to enter and see if they were indeed the proofs.",
"ref_norm": "A SUDDEN IMPULSE CAME OVER HIM TO ENTER AND SEE IF THEY WERE INDEED THE PROOFS",
"hyp_norm": "A SUDDEN IMPULSE CAME OVER HIM TO ENTER AND SEE IF THEY WERE INDEED THE PROOFS",
"duration_s": 5.06,
"infer_time_s": 4.211,
"rtf": 0.8323,
"wer": 0.0
},
{
"id": "1580-141084-0043",
"ref": "HE PUT HIS SHOES ON THE TABLE",
"hyp": "He put his shoes on the table.",
"ref_norm": "HE PUT HIS SHOES ON THE TABLE",
"hyp_norm": "HE PUT HIS SHOES ON THE TABLE",
"duration_s": 2.065,
"infer_time_s": 2.207,
"rtf": 1.0688,
"wer": 0.0
},
{
"id": "1580-141084-0044",
"ref": "GLOVES SAID THE YOUNG MAN",
"hyp": "Gloves ,\" said the young man.",
"ref_norm": "GLOVES SAID THE YOUNG MAN",
"hyp_norm": "GLOVES SAID THE YOUNG MAN",
"duration_s": 2.895,
"infer_time_s": 2.476,
"rtf": 0.8551,
"wer": 0.0
},
{
"id": "1580-141084-0045",
"ref": "SUDDENLY HE HEARD HIM AT THE VERY DOOR THERE WAS NO POSSIBLE ESCAPE",
"hyp": "Suddenly, he heard him at the very door. There was no possible escape.",
"ref_norm": "SUDDENLY HE HEARD HIM AT THE VERY DOOR THERE WAS NO POSSIBLE ESCAPE",
"hyp_norm": "SUDDENLY HE HEARD HIM AT THE VERY DOOR THERE WAS NO POSSIBLE ESCAPE",
"duration_s": 3.625,
"infer_time_s": 3.641,
"rtf": 1.0043,
"wer": 0.0
},
{
"id": "1580-141084-0046",
"ref": "HAVE I TOLD THE TRUTH MISTER GILCHRIST",
"hyp": "Have I told the truth, Mr. Gilchrist?",
"ref_norm": "HAVE I TOLD THE TRUTH MISTER GILCHRIST",
"hyp_norm": "HAVE I TOLD THE TRUTH MR GILCHRIST",
"duration_s": 2.35,
"infer_time_s": 2.779,
"rtf": 1.1826,
"wer": 0.1429
},
{
"id": "1580-141084-0047",
"ref": "I HAVE A LETTER HERE MISTER SOAMES WHICH I WROTE TO YOU EARLY THIS MORNING IN THE MIDDLE OF A RESTLESS NIGHT",
"hyp": "I have a letter here, Mr. Solmes, which I wrote to you early this morning in the middle of a restless night.",
"ref_norm": "I HAVE A LETTER HERE MISTER SOAMES WHICH I WROTE TO YOU EARLY THIS MORNING IN THE MIDDLE OF A RESTLESS NIGHT",
"hyp_norm": "I HAVE A LETTER HERE MR SOLMES WHICH I WROTE TO YOU EARLY THIS MORNING IN THE MIDDLE OF A RESTLESS NIGHT",
"duration_s": 5.25,
"infer_time_s": 6.006,
"rtf": 1.144,
"wer": 0.0909
},
{
"id": "1580-141084-0048",
"ref": "IT WILL BE CLEAR TO YOU FROM WHAT I HAVE SAID THAT ONLY YOU COULD HAVE LET THIS YOUNG MAN OUT SINCE YOU WERE LEFT IN THE ROOM AND MUST HAVE LOCKED THE DOOR WHEN YOU WENT OUT",
"hyp": "It will be clear to you from what I have said that only you could have let this young man out, since you were left in the room and must have locked the door when you went out.",
"ref_norm": "IT WILL BE CLEAR TO YOU FROM WHAT I HAVE SAID THAT ONLY YOU COULD HAVE LET THIS YOUNG MAN OUT SINCE YOU WERE LEFT IN THE ROOM AND MUST HAVE LOCKED THE DOOR WHEN YOU WENT OUT",
"hyp_norm": "IT WILL BE CLEAR TO YOU FROM WHAT I HAVE SAID THAT ONLY YOU COULD HAVE LET THIS YOUNG MAN OUT SINCE YOU WERE LEFT IN THE ROOM AND MUST HAVE LOCKED THE DOOR WHEN YOU WENT OUT",
"duration_s": 9.265,
"infer_time_s": 9.435,
"rtf": 1.0184,
"wer": 0.0
},
{
"id": "1580-141084-0049",
"ref": "IT WAS SIMPLE ENOUGH SIR IF YOU ONLY HAD KNOWN BUT WITH ALL YOUR CLEVERNESS IT WAS IMPOSSIBLE THAT YOU COULD KNOW",
"hyp": "It was simple enough, sir, if you only had known. But with all your cleverness, it was impossible that you could know.",
"ref_norm": "IT WAS SIMPLE ENOUGH SIR IF YOU ONLY HAD KNOWN BUT WITH ALL YOUR CLEVERNESS IT WAS IMPOSSIBLE THAT YOU COULD KNOW",
"hyp_norm": "IT WAS SIMPLE ENOUGH SIR IF YOU ONLY HAD KNOWN BUT WITH ALL YOUR CLEVERNESS IT WAS IMPOSSIBLE THAT YOU COULD KNOW",
"duration_s": 7.575,
"infer_time_s": 6.651,
"rtf": 0.8781,
"wer": 0.0
},
{
"id": "1580-141084-0050",
"ref": "IF MISTER SOAMES SAW THEM THE GAME WAS UP",
"hyp": "If Mister Solmes saw them, the game was up.",
"ref_norm": "IF MISTER SOAMES SAW THEM THE GAME WAS UP",
"hyp_norm": "IF MISTER SOLMES SAW THEM THE GAME WAS UP",
"duration_s": 2.78,
"infer_time_s": 2.909,
"rtf": 1.0463,
"wer": 0.1111
},
{
"id": "1995-1826-0000",
"ref": "IN THE DEBATE BETWEEN THE SENIOR SOCIETIES HER DEFENCE OF THE FIFTEENTH AMENDMENT HAD BEEN NOT ONLY A NOTABLE BIT OF REASONING BUT DELIVERED WITH REAL ENTHUSIASM",
"hyp": "In the debate between the senior societies, her defense of the Fifteenth Amendment had been not only a notable bit of reasoning, but delivered with real enthusiasm.",
"ref_norm": "IN THE DEBATE BETWEEN THE SENIOR SOCIETIES HER DEFENCE OF THE FIFTEENTH AMENDMENT HAD BEEN NOT ONLY A NOTABLE BIT OF REASONING BUT DELIVERED WITH REAL ENTHUSIASM",
"hyp_norm": "IN THE DEBATE BETWEEN THE SENIOR SOCIETIES HER DEFENSE OF THE FIFTEENTH AMENDMENT HAD BEEN NOT ONLY A NOTABLE BIT OF REASONING BUT DELIVERED WITH REAL ENTHUSIASM",
"duration_s": 9.485,
"infer_time_s": 7.971,
"rtf": 0.8403,
"wer": 0.037
},
{
"id": "1995-1826-0001",
"ref": "THE SOUTH SHE HAD NOT THOUGHT OF SERIOUSLY AND YET KNOWING OF ITS DELIGHTFUL HOSPITALITY AND MILD CLIMATE SHE WAS NOT AVERSE TO CHARLESTON OR NEW ORLEANS",
"hyp": "The South. She had not thought of seriously, and yet, knowing of its delightful hospitality and mild climate, she was not averse to Charleston or New Orleans.",
"ref_norm": "THE SOUTH SHE HAD NOT THOUGHT OF SERIOUSLY AND YET KNOWING OF ITS DELIGHTFUL HOSPITALITY AND MILD CLIMATE SHE WAS NOT AVERSE TO CHARLESTON OR NEW ORLEANS",
"hyp_norm": "THE SOUTH SHE HAD NOT THOUGHT OF SERIOUSLY AND YET KNOWING OF ITS DELIGHTFUL HOSPITALITY AND MILD CLIMATE SHE WAS NOT AVERSE TO CHARLESTON OR NEW ORLEANS",
"duration_s": 10.17,
"infer_time_s": 8.839,
"rtf": 0.8691,
"wer": 0.0
},
{
"id": "1995-1826-0002",
"ref": "JOHN TAYLOR WHO HAD SUPPORTED HER THROUGH COLLEGE WAS INTERESTED IN COTTON",
"hyp": "John Taylor, who had supported her through college, was interested in cotton.",
"ref_norm": "JOHN TAYLOR WHO HAD SUPPORTED HER THROUGH COLLEGE WAS INTERESTED IN COTTON",
"hyp_norm": "JOHN TAYLOR WHO HAD SUPPORTED HER THROUGH COLLEGE WAS INTERESTED IN COTTON",
"duration_s": 4.605,
"infer_time_s": 3.991,
"rtf": 0.8666,
"wer": 0.0
},
{
"id": "1995-1826-0003",
"ref": "BETTER GO HE HAD COUNSELLED SENTENTIOUSLY",
"hyp": "Better go,\" he had counselled sententiously.",
"ref_norm": "BETTER GO HE HAD COUNSELLED SENTENTIOUSLY",
"hyp_norm": "BETTER GO HE HAD COUNSELLED SENTENTIOUSLY",
"duration_s": 3.09,
"infer_time_s": 2.783,
"rtf": 0.9005,
"wer": 0.0
},
{
"id": "1995-1826-0004",
"ref": "MIGHT LEARN SOMETHING USEFUL DOWN THERE",
"hyp": "Might learn something useful down there.",
"ref_norm": "MIGHT LEARN SOMETHING USEFUL DOWN THERE",
"hyp_norm": "MIGHT LEARN SOMETHING USEFUL DOWN THERE",
"duration_s": 3.035,
"infer_time_s": 2.25,
"rtf": 0.7413,
"wer": 0.0
},
{
"id": "1995-1826-0005",
"ref": "BUT JOHN THERE'S NO SOCIETY JUST ELEMENTARY WORK",
"hyp": "But John, there's no society , just elementary work.",
"ref_norm": "BUT JOHN THERES NO SOCIETY JUST ELEMENTARY WORK",
"hyp_norm": "BUT JOHN THERES NO SOCIETY JUST ELEMENTARY WORK",
"duration_s": 5.125,
"infer_time_s": 3.33,
"rtf": 0.6497,
"wer": 0.0
},
{
"id": "1995-1826-0006",
"ref": "BEEN LOOKING UP TOOMS COUNTY",
"hyp": "Been looking up Tomb 's County.",
"ref_norm": "BEEN LOOKING UP TOOMS COUNTY",
"hyp_norm": "BEEN LOOKING UP TOMB S COUNTY",
"duration_s": 2.455,
"infer_time_s": 2.087,
"rtf": 0.8502,
"wer": 0.4
},
{
"id": "1995-1826-0007",
"ref": "FIND SOME CRESSWELLS THERE BIG PLANTATIONS RATED AT TWO HUNDRED AND FIFTY THOUSAND DOLLARS",
"hyp": "Find some Cressw ells there, big plantations rated at two hundred and fifty thousand dollars.",
"ref_norm": "FIND SOME CRESSWELLS THERE BIG PLANTATIONS RATED AT TWO HUNDRED AND FIFTY THOUSAND DOLLARS",
"hyp_norm": "FIND SOME CRESSW ELLS THERE BIG PLANTATIONS RATED AT TWO HUNDRED AND FIFTY THOUSAND DOLLARS",
"duration_s": 7.06,
"infer_time_s": 5.36,
"rtf": 0.7592,
"wer": 0.1429
},
{
"id": "1995-1826-0008",
"ref": "SOME OTHERS TOO BIG COTTON COUNTY",
"hyp": "Some others too, big Cotton County.",
"ref_norm": "SOME OTHERS TOO BIG COTTON COUNTY",
"hyp_norm": "SOME OTHERS TOO BIG COTTON COUNTY",
"duration_s": 2.895,
"infer_time_s": 2.253,
"rtf": 0.7782,
"wer": 0.0
},
{
"id": "1995-1826-0009",
"ref": "YOU OUGHT TO KNOW JOHN IF I TEACH NEGROES I'LL SCARCELY SEE MUCH OF PEOPLE IN MY OWN CLASS",
"hyp": "You ought to know , John. If I teach Negroes, I'll scarcely see much of people in my own class.",
"ref_norm": "YOU OUGHT TO KNOW JOHN IF I TEACH NEGROES ILL SCARCELY SEE MUCH OF PEOPLE IN MY OWN CLASS",
"hyp_norm": "YOU OUGHT TO KNOW JOHN IF I TEACH NEGROES ILL SCARCELY SEE MUCH OF PEOPLE IN MY OWN CLASS",
"duration_s": 7.57,
"infer_time_s": 6.315,
"rtf": 0.8342,
"wer": 0.0
},
{
"id": "1995-1826-0010",
"ref": "AT ANY RATE I SAY GO",
"hyp": "At any rate, I say go.",
"ref_norm": "AT ANY RATE I SAY GO",
"hyp_norm": "AT ANY RATE I SAY GO",
"duration_s": 2.445,
"infer_time_s": 2.207,
"rtf": 0.9027,
"wer": 0.0
},
{
"id": "1995-1826-0011",
"ref": "HERE SHE WAS TEACHING DIRTY CHILDREN AND THE SMELL OF CONFUSED ODORS AND BODILY PERSPIRATION WAS TO HER AT TIMES UNBEARABLE",
"hyp": "Here she was teaching dirty children, and the smell of confused odors and bodily pers piration was to her at times unbearable.",
"ref_norm": "HERE SHE WAS TEACHING DIRTY CHILDREN AND THE SMELL OF CONFUSED ODORS AND BODILY PERSPIRATION WAS TO HER AT TIMES UNBEARABLE",
"hyp_norm": "HERE SHE WAS TEACHING DIRTY CHILDREN AND THE SMELL OF CONFUSED ODORS AND BODILY PERS PIRATION WAS TO HER AT TIMES UNBEARABLE",
"duration_s": 8.94,
"infer_time_s": 6.618,
"rtf": 0.7402,
"wer": 0.0952
},
{
"id": "1995-1826-0012",
"ref": "SHE WANTED A GLANCE OF THE NEW BOOKS AND PERIODICALS AND TALK OF GREAT PHILANTHROPIES AND REFORMS",
"hyp": "She wanted a glance of the new books and period icals, and talk of great philanthropies and reforms.",
"ref_norm": "SHE WANTED A GLANCE OF THE NEW BOOKS AND PERIODICALS AND TALK OF GREAT PHILANTHROPIES AND REFORMS",
"hyp_norm": "SHE WANTED A GLANCE OF THE NEW BOOKS AND PERIOD ICALS AND TALK OF GREAT PHILANTHROPIES AND REFORMS",
"duration_s": 6.18,
"infer_time_s": 5.786,
"rtf": 0.9362,
"wer": 0.1176
},
{
"id": "1995-1826-0013",
"ref": "SO FOR THE HUNDREDTH TIME SHE WAS THINKING TODAY AS SHE WALKED ALONE UP THE LANE BACK OF THE BARN AND THEN SLOWLY DOWN THROUGH THE BOTTOMS",
"hyp": "So, for the hundredth time , she was thinking today , as she walked alone up the lane back of the barn, and then slowly down through the bottoms.",
"ref_norm": "SO FOR THE HUNDREDTH TIME SHE WAS THINKING TODAY AS SHE WALKED ALONE UP THE LANE BACK OF THE BARN AND THEN SLOWLY DOWN THROUGH THE BOTTOMS",
"hyp_norm": "SO FOR THE HUNDREDTH TIME SHE WAS THINKING TODAY AS SHE WALKED ALONE UP THE LANE BACK OF THE BARN AND THEN SLOWLY DOWN THROUGH THE BOTTOMS",
"duration_s": 8.77,
"infer_time_s": 8.02,
"rtf": 0.9145,
"wer": 0.0
},
{
"id": "1995-1826-0014",
"ref": "COTTON SHE PAUSED",
"hyp": "Cotton,\" she paused.",
"ref_norm": "COTTON SHE PAUSED",
"hyp_norm": "COTTON SHE PAUSED",
"duration_s": 2.5,
"infer_time_s": 1.856,
"rtf": 0.7425,
"wer": 0.0
},
{
"id": "1995-1826-0015",
"ref": "SHE HAD ALMOST FORGOTTEN THAT IT WAS HERE WITHIN TOUCH AND SIGHT",
"hyp": "She had almost forgotten that it was here, within touch and sight.",
"ref_norm": "SHE HAD ALMOST FORGOTTEN THAT IT WAS HERE WITHIN TOUCH AND SIGHT",
"hyp_norm": "SHE HAD ALMOST FORGOTTEN THAT IT WAS HERE WITHIN TOUCH AND SIGHT",
"duration_s": 3.55,
"infer_time_s": 3.325,
"rtf": 0.9367,
"wer": 0.0
},
{
"id": "1995-1826-0016",
"ref": "THE GLIMMERING SEA OF DELICATE LEAVES WHISPERED AND MURMURED BEFORE HER STRETCHING AWAY TO THE NORTHWARD",
"hyp": "The glimmering sea of delicate leaves whispered and murm ured before her, stretching away to the northward.",
"ref_norm": "THE GLIMMERING SEA OF DELICATE LEAVES WHISPERED AND MURMURED BEFORE HER STRETCHING AWAY TO THE NORTHWARD",
"hyp_norm": "THE GLIMMERING SEA OF DELICATE LEAVES WHISPERED AND MURM URED BEFORE HER STRETCHING AWAY TO THE NORTHWARD",
"duration_s": 5.9,
"infer_time_s": 5.397,
"rtf": 0.9147,
"wer": 0.125
},
{
"id": "1995-1826-0017",
"ref": "THERE MIGHT BE A BIT OF POETRY HERE AND THERE BUT MOST OF THIS PLACE WAS SUCH DESPERATE PROSE",
"hyp": "There might be a bit of poetry here and there, but most of this place was such desperate prose.",
"ref_norm": "THERE MIGHT BE A BIT OF POETRY HERE AND THERE BUT MOST OF THIS PLACE WAS SUCH DESPERATE PROSE",
"hyp_norm": "THERE MIGHT BE A BIT OF POETRY HERE AND THERE BUT MOST OF THIS PLACE WAS SUCH DESPERATE PROSE",
"duration_s": 6.145,
"infer_time_s": 5.42,
"rtf": 0.882,
"wer": 0.0
},
{
"id": "1995-1826-0018",
"ref": "HER REGARD SHIFTED TO THE GREEN STALKS AND LEAVES AGAIN AND SHE STARTED TO MOVE AWAY",
"hyp": "Her regard shifted to the green stalks and leaves again, and she started to move away.",
"ref_norm": "HER REGARD SHIFTED TO THE GREEN STALKS AND LEAVES AGAIN AND SHE STARTED TO MOVE AWAY",
"hyp_norm": "HER REGARD SHIFTED TO THE GREEN STALKS AND LEAVES AGAIN AND SHE STARTED TO MOVE AWAY",
"duration_s": 5.01,
"infer_time_s": 4.622,
"rtf": 0.9226,
"wer": 0.0
},
{
"id": "1995-1826-0019",
"ref": "COTTON IS A WONDERFUL THING IS IT NOT BOYS SHE SAID RATHER PRIMLY",
"hyp": "Cotton is a wonderful thing, is it not , boys? She said rather primly.",
"ref_norm": "COTTON IS A WONDERFUL THING IS IT NOT BOYS SHE SAID RATHER PRIMLY",
"hyp_norm": "COTTON IS A WONDERFUL THING IS IT NOT BOYS SHE SAID RATHER PRIMLY",
"duration_s": 5.25,
"infer_time_s": 4.905,
"rtf": 0.9342,
"wer": 0.0
},
{
"id": "1995-1826-0020",
"ref": "MISS TAYLOR DID NOT KNOW MUCH ABOUT COTTON BUT AT LEAST ONE MORE REMARK SEEMED CALLED FOR",
"hyp": "Miss Taylor did not know much about cotton , but at least one more remark seemed called for.",
"ref_norm": "MISS TAYLOR DID NOT KNOW MUCH ABOUT COTTON BUT AT LEAST ONE MORE REMARK SEEMED CALLED FOR",
"hyp_norm": "MISS TAYLOR DID NOT KNOW MUCH ABOUT COTTON BUT AT LEAST ONE MORE REMARK SEEMED CALLED FOR",
"duration_s": 6.12,
"infer_time_s": 4.728,
"rtf": 0.7725,
"wer": 0.0
},
{
"id": "1995-1826-0021",
"ref": "DON'T KNOW WELL OF ALL THINGS INWARDLY COMMENTED MISS TAYLOR LITERALLY BORN IN COTTON AND OH WELL AS MUCH AS TO ASK WHAT'S THE USE SHE TURNED AGAIN TO GO",
"hyp": "Don't know. Well, of all things, inwardly commented Miss Taylor , literally born in cotton, and oh well , as much as to ask what's the use. She turned again to go.",
"ref_norm": "DONT KNOW WELL OF ALL THINGS INWARDLY COMMENTED MISS TAYLOR LITERALLY BORN IN COTTON AND OH WELL AS MUCH AS TO ASK WHATS THE USE SHE TURNED AGAIN TO GO",
"hyp_norm": "DONT KNOW WELL OF ALL THINGS INWARDLY COMMENTED MISS TAYLOR LITERALLY BORN IN COTTON AND OH WELL AS MUCH AS TO ASK WHATS THE USE SHE TURNED AGAIN TO GO",
"duration_s": 11.41,
"infer_time_s": 9.197,
"rtf": 0.8061,
"wer": 0.0
},
{
"id": "1995-1826-0022",
"ref": "I SUPPOSE THOUGH IT'S TOO EARLY FOR THEM THEN CAME THE EXPLOSION",
"hyp": "I suppose, though, it's too early for them . Then came the explosion.",
"ref_norm": "I SUPPOSE THOUGH ITS TOO EARLY FOR THEM THEN CAME THE EXPLOSION",
"hyp_norm": "I SUPPOSE THOUGH ITS TOO EARLY FOR THEM THEN CAME THE EXPLOSION",
"duration_s": 4.745,
"infer_time_s": 3.593,
"rtf": 0.7573,
"wer": 0.0
},
{
"id": "1995-1826-0023",
"ref": "GOOBERS DON'T GROW ON THE TOPS OF VINES BUT UNDERGROUND ON THE ROOTS LIKE YAMS IS THAT SO",
"hyp": "Goobers don't grow on de tops of vines, but underground on de roots, like y ams. Is that so?",
"ref_norm": "GOOBERS DONT GROW ON THE TOPS OF VINES BUT UNDERGROUND ON THE ROOTS LIKE YAMS IS THAT SO",
"hyp_norm": "GOOBERS DONT GROW ON DE TOPS OF VINES BUT UNDERGROUND ON DE ROOTS LIKE Y AMS IS THAT SO",
"duration_s": 8.14,
"infer_time_s": 5.867,
"rtf": 0.7208,
"wer": 0.2222
},
{
"id": "1995-1826-0024",
"ref": "THE GOLDEN FLEECE IT'S THE SILVER FLEECE HE HARKENED",
"hyp": "The Golden Fleece , it's the Silver F leece. He hearkened.",
"ref_norm": "THE GOLDEN FLEECE ITS THE SILVER FLEECE HE HARKENED",
"hyp_norm": "THE GOLDEN FLEECE ITS THE SILVER F LEECE HE HEARKENED",
"duration_s": 5.095,
"infer_time_s": 3.967,
"rtf": 0.7785,
"wer": 0.3333
},
{
"id": "1995-1826-0025",
"ref": "SOME TIME YOU'LL TELL ME PLEASE WON'T YOU",
"hyp": "Sometime you tell me, please, won't you?",
"ref_norm": "SOME TIME YOULL TELL ME PLEASE WONT YOU",
"hyp_norm": "SOMETIME YOU TELL ME PLEASE WONT YOU",
"duration_s": 3.295,
"infer_time_s": 2.384,
"rtf": 0.7234,
"wer": 0.375
},
{
"id": "1995-1826-0026",
"ref": "NOW FOR ONE LITTLE HALF HOUR SHE HAD BEEN A WOMAN TALKING TO A BOY NO NOT EVEN THAT SHE HAD BEEN TALKING JUST TALKING THERE WERE NO PERSONS IN THE CONVERSATION JUST THINGS ONE THING COTTON",
"hyp": "Now, for one little half hour, she had been a woman talking to a boy. No, not even that. She had been talking, just talking. There were no persons in the conversation, just things. One thing, cotton.",
"ref_norm": "NOW FOR ONE LITTLE HALF HOUR SHE HAD BEEN A WOMAN TALKING TO A BOY NO NOT EVEN THAT SHE HAD BEEN TALKING JUST TALKING THERE WERE NO PERSONS IN THE CONVERSATION JUST THINGS ONE THING COTTON",
"hyp_norm": "NOW FOR ONE LITTLE HALF HOUR SHE HAD BEEN A WOMAN TALKING TO A BOY NO NOT EVEN THAT SHE HAD BEEN TALKING JUST TALKING THERE WERE NO PERSONS IN THE CONVERSATION JUST THINGS ONE THING COTTON",
"duration_s": 15.45,
"infer_time_s": 10.111,
"rtf": 0.6544,
"wer": 0.0
},
{
"id": "1995-1836-0000",
"ref": "THE HON CHARLES SMITH MISS SARAH'S BROTHER WAS WALKING SWIFTLY UPTOWN FROM MISTER EASTERLY'S WALL STREET OFFICE AND HIS FACE WAS PALE",
"hyp": "The Honorable Charles Smith, Miss Sarah's brother, was walking swiftly uptown from Mister Easter ly's Wall Street office, and his face was pale.",
"ref_norm": "THE HON CHARLES SMITH MISS SARAHS BROTHER WAS WALKING SWIFTLY UPTOWN FROM MISTER EASTERLYS WALL STREET OFFICE AND HIS FACE WAS PALE",
"hyp_norm": "THE HONORABLE CHARLES SMITH MISS SARAHS BROTHER WAS WALKING SWIFTLY UPTOWN FROM MISTER EASTER LYS WALL STREET OFFICE AND HIS FACE WAS PALE",
"duration_s": 8.955,
"infer_time_s": 6.343,
"rtf": 0.7083,
"wer": 0.1364
},
{
"id": "1995-1836-0001",
"ref": "AT LAST THE COTTON COMBINE WAS TO ALL APPEARANCES AN ASSURED FACT AND HE WAS SLATED FOR THE SENATE",
"hyp": "At last, the cotton combine was, to all appearances , an assured fact, and he was slated for the Senate.",
"ref_norm": "AT LAST THE COTTON COMBINE WAS TO ALL APPEARANCES AN ASSURED FACT AND HE WAS SLATED FOR THE SENATE",
"hyp_norm": "AT LAST THE COTTON COMBINE WAS TO ALL APPEARANCES AN ASSURED FACT AND HE WAS SLATED FOR THE SENATE",
"duration_s": 6.0,
"infer_time_s": 4.502,
"rtf": 0.7503,
"wer": 0.0
},
{
"id": "1995-1836-0002",
"ref": "WHY SHOULD HE NOT BE AS OTHER MEN",
"hyp": "Why should he not be as other men?",
"ref_norm": "WHY SHOULD HE NOT BE AS OTHER MEN",
"hyp_norm": "WHY SHOULD HE NOT BE AS OTHER MEN",
"duration_s": 2.315,
"infer_time_s": 1.995,
"rtf": 0.8617,
"wer": 0.0
},
{
"id": "1995-1836-0003",
"ref": "SHE WAS NOT HERSELF A NOTABLY INTELLIGENT WOMAN SHE GREATLY ADMIRED INTELLIGENCE OR WHATEVER LOOKED TO HER LIKE INTELLIGENCE IN OTHERS",
"hyp": "She was not herself a notably intelligent woman . She greatly admired intelligence, or whatever looked to her like intelligence in others.",
"ref_norm": "SHE WAS NOT HERSELF A NOTABLY INTELLIGENT WOMAN SHE GREATLY ADMIRED INTELLIGENCE OR WHATEVER LOOKED TO HER LIKE INTELLIGENCE IN OTHERS",
"hyp_norm": "SHE WAS NOT HERSELF A NOTABLY INTELLIGENT WOMAN SHE GREATLY ADMIRED INTELLIGENCE OR WHATEVER LOOKED TO HER LIKE INTELLIGENCE IN OTHERS",
"duration_s": 7.965,
"infer_time_s": 4.998,
"rtf": 0.6275,
"wer": 0.0
},
{
"id": "1995-1836-0004",
"ref": "AS SHE AWAITED HER GUESTS SHE SURVEYED THE TABLE WITH BOTH SATISFACTION AND DISQUIETUDE FOR HER SOCIAL FUNCTIONS WERE FEW TONIGHT THERE WERE SHE CHECKED THEM OFF ON HER FINGERS SIR JAMES CREIGHTON THE RICH ENGLISH MANUFACTURER AND LADY CREIGHTON MISTER AND MISSUS VANDERPOOL MISTER HARRY CRESSWELL AND HIS SISTER JOHN TAYLOR AND HIS SISTER AND MISTER CHARLES SMITH WHOM THE EVENING PAPERS MENTIONED AS LIKELY TO BE UNITED STATES SENATOR FROM NEW JERSEY A SELECTION OF GUESTS THAT HAD BEEN DETERMINED UNKNOWN TO THE HOSTESS BY THE MEETING OF COTTON INTERESTS EARLIER IN THE DAY",
"hyp": "As she awaited her guest, she surveyed the table with both satisfaction and dis quietude. For her social functions were few tonight. There were she checked them off on her fingers. Sir James Crichton, the rich English manufacturer , and Lady C richt on , his wife, were among the guests.",
"ref_norm": "AS SHE AWAITED HER GUESTS SHE SURVEYED THE TABLE WITH BOTH SATISFACTION AND DISQUIETUDE FOR HER SOCIAL FUNCTIONS WERE FEW TONIGHT THERE WERE SHE CHECKED THEM OFF ON HER FINGERS SIR JAMES CREIGHTON THE RICH ENGLISH MANUFACTURER AND LADY CREIGHTON MISTER AND MISSUS VANDERPOOL MISTER HARRY CRESSWELL AND HIS SISTER JOHN TAYLOR AND HIS SISTER AND MISTER CHARLES SMITH WHOM THE EVENING PAPERS MENTIONED AS LIKELY TO BE UNITED STATES SENATOR FROM NEW JERSEY A SELECTION OF GUESTS THAT HAD BEEN DETERMINED UNKNOWN TO THE HOSTESS BY THE MEETING OF COTTON INTERESTS EARLIER IN THE DAY",
"hyp_norm": "AS SHE AWAITED HER GUEST SHE SURVEYED THE TABLE WITH BOTH SATISFACTION AND DIS QUIETUDE FOR HER SOCIAL FUNCTIONS WERE FEW TONIGHT THERE WERE SHE CHECKED THEM OFF ON HER FINGERS SIR JAMES CRICHTON THE RICH ENGLISH MANUFACTURER AND LADY C RICHT ON HIS WIFE WERE AMONG THE GUESTS",
"duration_s": 33.91,
"infer_time_s": 17.658,
"rtf": 0.5207,
"wer": 0.6042
},
{
"id": "1995-1836-0005",
"ref": "MISSUS GREY HAD MET SOUTHERNERS BEFORE BUT NOT INTIMATELY AND SHE ALWAYS HAD IN MIND VIVIDLY THEIR CRUELTY TO POOR NEGROES A SUBJECT SHE MADE A POINT OF INTRODUCING FORTHWITH",
"hyp": "Mrs. Gray had met Sou therners before, but not intimately, and she always had in mind vividly their cruelty to poor Negro es\u2014a subject she made a point of introducing forthwith.",
"ref_norm": "MISSUS GREY HAD MET SOUTHERNERS BEFORE BUT NOT INTIMATELY AND SHE ALWAYS HAD IN MIND VIVIDLY THEIR CRUELTY TO POOR NEGROES A SUBJECT SHE MADE A POINT OF INTRODUCING FORTHWITH",
"hyp_norm": "MRS GRAY HAD MET SOU THERNERS BEFORE BUT NOT INTIMATELY AND SHE ALWAYS HAD IN MIND VIVIDLY THEIR CRUELTY TO POOR NEGRO ESA SUBJECT SHE MADE A POINT OF INTRODUCING FORTHWITH",
"duration_s": 10.9,
"infer_time_s": 7.967,
"rtf": 0.731,
"wer": 0.2
},
{
"id": "1995-1836-0006",
"ref": "SHE WAS THEREFORE MOST AGREEABLY SURPRISED TO HEAR MISTER CRESSWELL EXPRESS HIMSELF SO CORDIALLY AS APPROVING OF NEGRO EDUCATION",
"hyp": "She was therefore most agreeably surprised to hear Mister. Cresswell express himself so cordially as approving of Negro education.",
"ref_norm": "SHE WAS THEREFORE MOST AGREEABLY SURPRISED TO HEAR MISTER CRESSWELL EXPRESS HIMSELF SO CORDIALLY AS APPROVING OF NEGRO EDUCATION",
"hyp_norm": "SHE WAS THEREFORE MOST AGREEABLY SURPRISED TO HEAR MISTER CRESSWELL EXPRESS HIMSELF SO CORDIALLY AS APPROVING OF NEGRO EDUCATION",
"duration_s": 7.715,
"infer_time_s": 5.085,
"rtf": 0.6591,
"wer": 0.0
},
{
"id": "1995-1836-0007",
"ref": "BUT YOU BELIEVE IN SOME EDUCATION ASKED MARY TAYLOR",
"hyp": "Do you believe in some education? Asked Mary Taylor.",
"ref_norm": "BUT YOU BELIEVE IN SOME EDUCATION ASKED MARY TAYLOR",
"hyp_norm": "DO YOU BELIEVE IN SOME EDUCATION ASKED MARY TAYLOR",
"duration_s": 3.435,
"infer_time_s": 2.225,
"rtf": 0.6478,
"wer": 0.1111
},
{
"id": "1995-1836-0008",
"ref": "I BELIEVE IN THE TRAINING OF PEOPLE TO THEIR HIGHEST CAPACITY THE ENGLISHMAN HERE HEARTILY SECONDED HIM",
"hyp": "I believe in the training of people to their highest capacity. The Englishman here heartily seconded him.",
"ref_norm": "I BELIEVE IN THE TRAINING OF PEOPLE TO THEIR HIGHEST CAPACITY THE ENGLISHMAN HERE HEARTILY SECONDED HIM",
"hyp_norm": "I BELIEVE IN THE TRAINING OF PEOPLE TO THEIR HIGHEST CAPACITY THE ENGLISHMAN HERE HEARTILY SECONDED HIM",
"duration_s": 6.985,
"infer_time_s": 4.563,
"rtf": 0.6532,
"wer": 0.0
},
{
"id": "1995-1836-0009",
"ref": "BUT CRESSWELL ADDED SIGNIFICANTLY CAPACITY DIFFERS ENORMOUSLY BETWEEN RACES",
"hyp": "But Cresswell added significantly. Capacity differs enormously between races.",
"ref_norm": "BUT CRESSWELL ADDED SIGNIFICANTLY CAPACITY DIFFERS ENORMOUSLY BETWEEN RACES",
"hyp_norm": "BUT CRESSWELL ADDED SIGNIFICANTLY CAPACITY DIFFERS ENORMOUSLY BETWEEN RACES",
"duration_s": 6.71,
"infer_time_s": 3.303,
"rtf": 0.4923,
"wer": 0.0
},
{
"id": "1995-1836-0010",
"ref": "THE VANDERPOOLS WERE SURE OF THIS AND THE ENGLISHMAN INSTANCING INDIA BECAME QUITE ELOQUENT MISSUS GREY WAS MYSTIFIED BUT HARDLY DARED ADMIT IT THE GENERAL TREND OF THE CONVERSATION SEEMED TO BE THAT MOST INDIVIDUALS NEEDED TO BE SUBMITTED TO THE SHARPEST SCRUTINY BEFORE BEING ALLOWED MUCH EDUCATION AND AS FOR THE LOWER RACES IT WAS SIMPLY CRIMINAL TO OPEN SUCH USELESS OPPORTUNITIES TO THEM",
"hyp": "The Vanderpoels were sure of this, and the Englishman , instancing India, became quite elo quent. Mrs. Grey was myst ified, but hardly dared admit it. The general trend of the conversation seemed to be that most individuals needed to be submitted to the sharpest scrutiny before being allowed much education, and as for the lower races, it was simply criminal to open such useless opportunities to them.",
"ref_norm": "THE VANDERPOOLS WERE SURE OF THIS AND THE ENGLISHMAN INSTANCING INDIA BECAME QUITE ELOQUENT MISSUS GREY WAS MYSTIFIED BUT HARDLY DARED ADMIT IT THE GENERAL TREND OF THE CONVERSATION SEEMED TO BE THAT MOST INDIVIDUALS NEEDED TO BE SUBMITTED TO THE SHARPEST SCRUTINY BEFORE BEING ALLOWED MUCH EDUCATION AND AS FOR THE LOWER RACES IT WAS SIMPLY CRIMINAL TO OPEN SUCH USELESS OPPORTUNITIES TO THEM",
"hyp_norm": "THE VANDERPOELS WERE SURE OF THIS AND THE ENGLISHMAN INSTANCING INDIA BECAME QUITE ELO QUENT MRS GREY WAS MYST IFIED BUT HARDLY DARED ADMIT IT THE GENERAL TREND OF THE CONVERSATION SEEMED TO BE THAT MOST INDIVIDUALS NEEDED TO BE SUBMITTED TO THE SHARPEST SCRUTINY BEFORE BEING ALLOWED MUCH EDUCATION AND AS FOR THE LOWER RACES IT WAS SIMPLY CRIMINAL TO OPEN SUCH USELESS OPPORTUNITIES TO THEM",
"duration_s": 24.45,
"infer_time_s": 18.234,
"rtf": 0.7458,
"wer": 0.0923
},
{
"id": "1995-1836-0011",
"ref": "POSITIVELY HEROIC ADDED CRESSWELL AVOIDING HIS SISTER'S EYES",
"hyp": "Positively heroic ,\" added Cresswell, avoiding his sister's eyes.",
"ref_norm": "POSITIVELY HEROIC ADDED CRESSWELL AVOIDING HIS SISTERS EYES",
"hyp_norm": "POSITIVELY HEROIC ADDED CRESSWELL AVOIDING HIS SISTERS EYES",
"duration_s": 4.705,
"infer_time_s": 3.307,
"rtf": 0.7029,
"wer": 0.0
},
{
"id": "1995-1836-0012",
"ref": "BUT WE'RE NOT ER EXACTLY WELCOMED",
"hyp": "But, we're not uh, exactly welcome.",
"ref_norm": "BUT WERE NOT ER EXACTLY WELCOMED",
"hyp_norm": "BUT WERE NOT UH EXACTLY WELCOME",
"duration_s": 3.695,
"infer_time_s": 2.181,
"rtf": 0.5902,
"wer": 0.3333
},
{
"id": "1995-1836-0013",
"ref": "MARY TAYLOR HOWEVER RELATED THE TALE OF ZORA TO MISSUS GREY'S PRIVATE EAR LATER",
"hyp": "Mary Taylor, however, related the tale of Zora to Mrs. Gray's private ear later.",
"ref_norm": "MARY TAYLOR HOWEVER RELATED THE TALE OF ZORA TO MISSUS GREYS PRIVATE EAR LATER",
"hyp_norm": "MARY TAYLOR HOWEVER RELATED THE TALE OF ZORA TO MRS GRAYS PRIVATE EAR LATER",
"duration_s": 5.3,
"infer_time_s": 3.939,
"rtf": 0.7433,
"wer": 0.1429
},
{
"id": "1995-1836-0014",
"ref": "FORTUNATELY SAID MISTER VANDERPOOL NORTHERNERS AND SOUTHERNERS ARE ARRIVING AT A BETTER MUTUAL UNDERSTANDING ON MOST OF THESE MATTERS",
"hyp": "Fortunately,\" said Mister Vanderpool, \" Northerners and Southerners are arriving at a better mutual understanding on most of these matters.\"",
"ref_norm": "FORTUNATELY SAID MISTER VANDERPOOL NORTHERNERS AND SOUTHERNERS ARE ARRIVING AT A BETTER MUTUAL UNDERSTANDING ON MOST OF THESE MATTERS",
"hyp_norm": "FORTUNATELY SAID MISTER VANDERPOOL NORTHERNERS AND SOUTHERNERS ARE ARRIVING AT A BETTER MUTUAL UNDERSTANDING ON MOST OF THESE MATTERS",
"duration_s": 9.045,
"infer_time_s": 5.96,
"rtf": 0.659,
"wer": 0.0
},
{
"id": "1995-1837-0000",
"ref": "HE KNEW THE SILVER FLEECE HIS AND ZORA'S MUST BE RUINED",
"hyp": "He knew the silver fleece ; his and Zora's must be ruined.",
"ref_norm": "HE KNEW THE SILVER FLEECE HIS AND ZORAS MUST BE RUINED",
"hyp_norm": "HE KNEW THE SILVER FLEECE HIS AND ZORAS MUST BE RUINED",
"duration_s": 3.865,
"infer_time_s": 2.806,
"rtf": 0.7259,
"wer": 0.0
},
{
"id": "1995-1837-0001",
"ref": "IT WAS THE FIRST GREAT SORROW OF HIS LIFE IT WAS NOT SO MUCH THE LOSS OF THE COTTON ITSELF BUT THE FANTASY THE HOPES THE DREAMS BUILT AROUND IT",
"hyp": "It was the first great sorrow of his life. It was not so much the loss of the cotton itself, but the fantasy, the hopes, the dreams built around it.",
"ref_norm": "IT WAS THE FIRST GREAT SORROW OF HIS LIFE IT WAS NOT SO MUCH THE LOSS OF THE COTTON ITSELF BUT THE FANTASY THE HOPES THE DREAMS BUILT AROUND IT",
"hyp_norm": "IT WAS THE FIRST GREAT SORROW OF HIS LIFE IT WAS NOT SO MUCH THE LOSS OF THE COTTON ITSELF BUT THE FANTASY THE HOPES THE DREAMS BUILT AROUND IT",
"duration_s": 8.73,
"infer_time_s": 6.864,
"rtf": 0.7862,
"wer": 0.0
},
{
"id": "1995-1837-0002",
"ref": "AH THE SWAMP THE CRUEL SWAMP",
"hyp": "Ah, the swamp, the cruel swamp.",
"ref_norm": "AH THE SWAMP THE CRUEL SWAMP",
"hyp_norm": "AH THE SWAMP THE CRUEL SWAMP",
"duration_s": 2.79,
"infer_time_s": 1.969,
"rtf": 0.7059,
"wer": 0.0
},
{
"id": "1995-1837-0003",
"ref": "THE REVELATION OF HIS LOVE LIGHTED AND BRIGHTENED SLOWLY TILL IT FLAMED LIKE A SUNRISE OVER HIM AND LEFT HIM IN BURNING WONDER",
"hyp": "The revelation of his love lighted and brightened slowly till it flamed like a sunrise over him and left him in burning wonder.",
"ref_norm": "THE REVELATION OF HIS LOVE LIGHTED AND BRIGHTENED SLOWLY TILL IT FLAMED LIKE A SUNRISE OVER HIM AND LEFT HIM IN BURNING WONDER",
"hyp_norm": "THE REVELATION OF HIS LOVE LIGHTED AND BRIGHTENED SLOWLY TILL IT FLAMED LIKE A SUNRISE OVER HIM AND LEFT HIM IN BURNING WONDER",
"duration_s": 7.36,
"infer_time_s": 5.321,
"rtf": 0.7229,
"wer": 0.0
},
{
"id": "1995-1837-0004",
"ref": "HE PANTED TO KNOW IF SHE TOO KNEW OR KNEW AND CARED NOT OR CARED AND KNEW NOT",
"hyp": "He panted to know if she too knew, or knew and cared not , or cared and knew not.",
"ref_norm": "HE PANTED TO KNOW IF SHE TOO KNEW OR KNEW AND CARED NOT OR CARED AND KNEW NOT",
"hyp_norm": "HE PANTED TO KNOW IF SHE TOO KNEW OR KNEW AND CARED NOT OR CARED AND KNEW NOT",
"duration_s": 6.36,
"infer_time_s": 5.135,
"rtf": 0.8074,
"wer": 0.0
},
{
"id": "1995-1837-0005",
"ref": "SHE WAS SO STRANGE AND HUMAN A CREATURE",
"hyp": "She was so strange and human a creature.",
"ref_norm": "SHE WAS SO STRANGE AND HUMAN A CREATURE",
"hyp_norm": "SHE WAS SO STRANGE AND HUMAN A CREATURE",
"duration_s": 2.635,
"infer_time_s": 2.46,
"rtf": 0.9335,
"wer": 0.0
},
{
"id": "1995-1837-0006",
"ref": "THE WORLD WAS WATER VEILED IN MISTS",
"hyp": "The world was water, ve iled in mists.",
"ref_norm": "THE WORLD WAS WATER VEILED IN MISTS",
"hyp_norm": "THE WORLD WAS WATER VE ILED IN MISTS",
"duration_s": 2.955,
"infer_time_s": 2.709,
"rtf": 0.9167,
"wer": 0.2857
},
{
"id": "1995-1837-0007",
"ref": "THEN OF A SUDDEN AT MIDDAY THE SUN SHOT OUT HOT AND STILL NO BREATH OF AIR STIRRED THE SKY WAS LIKE BLUE STEEL THE EARTH STEAMED",
"hyp": "Then, of a sudden, at midday, the sun shot out , hot and still. No breath of air stirred . The sky was like blue steel. The earth steamed.",
"ref_norm": "THEN OF A SUDDEN AT MIDDAY THE SUN SHOT OUT HOT AND STILL NO BREATH OF AIR STIRRED THE SKY WAS LIKE BLUE STEEL THE EARTH STEAMED",
"hyp_norm": "THEN OF A SUDDEN AT MIDDAY THE SUN SHOT OUT HOT AND STILL NO BREATH OF AIR STIRRED THE SKY WAS LIKE BLUE STEEL THE EARTH STEAMED",
"duration_s": 8.8,
"infer_time_s": 8.932,
"rtf": 1.015,
"wer": 0.0
},
{
"id": "1995-1837-0008",
"ref": "WHERE WAS THE USE OF IMAGINING",
"hyp": "Where was the use of imagining?",
"ref_norm": "WHERE WAS THE USE OF IMAGINING",
"hyp_norm": "WHERE WAS THE USE OF IMAGINING",
"duration_s": 1.955,
"infer_time_s": 1.74,
"rtf": 0.8898,
"wer": 0.0
},
{
"id": "1995-1837-0009",
"ref": "THE LAGOON HAD BEEN LEVEL WITH THE DYKES A WEEK AGO AND NOW",
"hyp": "The lagoon had been levelled with the dikes a week ago, and now.",
"ref_norm": "THE LAGOON HAD BEEN LEVEL WITH THE DYKES A WEEK AGO AND NOW",
"hyp_norm": "THE LAGOON HAD BEEN LEVELLED WITH THE DIKES A WEEK AGO AND NOW",
"duration_s": 3.76,
"infer_time_s": 4.451,
"rtf": 1.1837,
"wer": 0.1538
},
{
"id": "1995-1837-0010",
"ref": "PERHAPS SHE TOO MIGHT BE THERE WAITING WEEPING",
"hyp": "Perhaps she too might be there, waiting, weeping.",
"ref_norm": "PERHAPS SHE TOO MIGHT BE THERE WAITING WEEPING",
"hyp_norm": "PERHAPS SHE TOO MIGHT BE THERE WAITING WEEPING",
"duration_s": 3.48,
"infer_time_s": 3.011,
"rtf": 0.8651,
"wer": 0.0
},
{
"id": "1995-1837-0011",
"ref": "HE STARTED AT THE THOUGHT HE HURRIED FORTH SADLY",
"hyp": "He started at the thought . He hurried forth sadly.",
"ref_norm": "HE STARTED AT THE THOUGHT HE HURRIED FORTH SADLY",
"hyp_norm": "HE STARTED AT THE THOUGHT HE HURRIED FORTH SADLY",
"duration_s": 3.375,
"infer_time_s": 2.74,
"rtf": 0.8119,
"wer": 0.0
},
{
"id": "1995-1837-0012",
"ref": "HE SPLASHED AND STAMPED ALONG FARTHER AND FARTHER ONWARD UNTIL HE NEARED THE RAMPART OF THE CLEARING AND PUT FOOT UPON THE TREE BRIDGE",
"hyp": "He splashed and stamped along , farther and farther onward, until he neared the ramp art of the clearing, and put foot upon the tree bridge.",
"ref_norm": "HE SPLASHED AND STAMPED ALONG FARTHER AND FARTHER ONWARD UNTIL HE NEARED THE RAMPART OF THE CLEARING AND PUT FOOT UPON THE TREE BRIDGE",
"hyp_norm": "HE SPLASHED AND STAMPED ALONG FARTHER AND FARTHER ONWARD UNTIL HE NEARED THE RAMP ART OF THE CLEARING AND PUT FOOT UPON THE TREE BRIDGE",
"duration_s": 8.245,
"infer_time_s": 7.735,
"rtf": 0.9381,
"wer": 0.0833
},
{
"id": "1995-1837-0013",
"ref": "THEN HE LOOKED DOWN THE LAGOON WAS DRY",
"hyp": "Then he looked down . The lagoon was dry.",
"ref_norm": "THEN HE LOOKED DOWN THE LAGOON WAS DRY",
"hyp_norm": "THEN HE LOOKED DOWN THE LAGOON WAS DRY",
"duration_s": 3.195,
"infer_time_s": 2.678,
"rtf": 0.8381,
"wer": 0.0
},
{
"id": "1995-1837-0014",
"ref": "HE STOOD A MOMENT BEWILDERED THEN TURNED AND RUSHED UPON THE ISLAND A GREAT SHEET OF DAZZLING SUNLIGHT SWEPT THE PLACE AND BENEATH LAY A MIGHTY MASS OF OLIVE GREEN THICK TALL WET AND WILLOWY",
"hyp": "He stood a moment, bewildered , then turned and rushed upon the island\u2014a great sheet of dazzling sunlight swept the place, and beneath lay a mighty mass of olive green, thick, tall, wet, and willowy.",
"ref_norm": "HE STOOD A MOMENT BEWILDERED THEN TURNED AND RUSHED UPON THE ISLAND A GREAT SHEET OF DAZZLING SUNLIGHT SWEPT THE PLACE AND BENEATH LAY A MIGHTY MASS OF OLIVE GREEN THICK TALL WET AND WILLOWY",
"hyp_norm": "HE STOOD A MOMENT BEWILDERED THEN TURNED AND RUSHED UPON THE ISLANDA GREAT SHEET OF DAZZLING SUNLIGHT SWEPT THE PLACE AND BENEATH LAY A MIGHTY MASS OF OLIVE GREEN THICK TALL WET AND WILLOWY",
"duration_s": 12.46,
"infer_time_s": 10.936,
"rtf": 0.8777,
"wer": 0.0571
},
{
"id": "1995-1837-0015",
"ref": "THE SQUARES OF COTTON SHARP EDGED HEAVY WERE JUST ABOUT TO BURST TO BOLLS",
"hyp": "The squares of cotton, sharp-edged, heavy, were just about to burst to bowls.",
"ref_norm": "THE SQUARES OF COTTON SHARP EDGED HEAVY WERE JUST ABOUT TO BURST TO BOLLS",
"hyp_norm": "THE SQUARES OF COTTON SHARPEDGED HEAVY WERE JUST ABOUT TO BURST TO BOWLS",
"duration_s": 4.485,
"infer_time_s": 4.562,
"rtf": 1.0171,
"wer": 0.2143
},
{
"id": "1995-1837-0016",
"ref": "FOR ONE LONG MOMENT HE PAUSED STUPID AGAPE WITH UTTER AMAZEMENT THEN LEANED DIZZILY AGAINST A TREE",
"hyp": "For one long moment, he paused, stupid, ag ape with utter amaz ement, then leaned dizzily against the tree.",
"ref_norm": "FOR ONE LONG MOMENT HE PAUSED STUPID AGAPE WITH UTTER AMAZEMENT THEN LEANED DIZZILY AGAINST A TREE",
"hyp_norm": "FOR ONE LONG MOMENT HE PAUSED STUPID AG APE WITH UTTER AMAZ EMENT THEN LEANED DIZZILY AGAINST THE TREE",
"duration_s": 7.19,
"infer_time_s": 6.344,
"rtf": 0.8824,
"wer": 0.2941
},
{
"id": "1995-1837-0017",
"ref": "HE GAZED ABOUT PERPLEXED ASTONISHED",
"hyp": "He gazed about, perplexed, astonished.",
"ref_norm": "HE GAZED ABOUT PERPLEXED ASTONISHED",
"hyp_norm": "HE GAZED ABOUT PERPLEXED ASTONISHED",
"duration_s": 3.1,
"infer_time_s": 2.532,
"rtf": 0.8167,
"wer": 0.0
},
{
"id": "1995-1837-0018",
"ref": "HERE LAY THE READING OF THE RIDDLE WITH INFINITE WORK AND PAIN SOME ONE HAD DUG A CANAL FROM THE LAGOON TO THE CREEK INTO WHICH THE FORMER HAD DRAINED BY A LONG AND CROOKED WAY THUS ALLOWING IT TO EMPTY DIRECTLY",
"hyp": "Here lay the reading of the riddle, with infinite work and pain. Someone had dug a canal from the l agoon to the creek, into which the former had drained by a long and crooked way, thus allowing it to empty directly.",
"ref_norm": "HERE LAY THE READING OF THE RIDDLE WITH INFINITE WORK AND PAIN SOME ONE HAD DUG A CANAL FROM THE LAGOON TO THE CREEK INTO WHICH THE FORMER HAD DRAINED BY A LONG AND CROOKED WAY THUS ALLOWING IT TO EMPTY DIRECTLY",
"hyp_norm": "HERE LAY THE READING OF THE RIDDLE WITH INFINITE WORK AND PAIN SOMEONE HAD DUG A CANAL FROM THE L AGOON TO THE CREEK INTO WHICH THE FORMER HAD DRAINED BY A LONG AND CROOKED WAY THUS ALLOWING IT TO EMPTY DIRECTLY",
"duration_s": 12.825,
"infer_time_s": 11.597,
"rtf": 0.9043,
"wer": 0.0952
}
]
}
================================================
FILE: benchmarks/m5/generate_figures.py
================================================
#!/usr/bin/env python3
"""
Generate combined M5 vs H100 benchmark figure for WhisperLiveKit.
Produces a WER vs RTF scatter plot comparing Apple M5 (MLX) and
NVIDIA H100 results on LibriSpeech test-clean.
Note: M5 uses per-utterance evaluation (500 samples), while H100
uses chapter-grouped evaluation (91 chapters). Per-utterance WER
is typically lower because short utterances avoid long-range errors.
Run: python3 benchmarks/m5/generate_figures.py
"""
import json
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
DIR = os.path.dirname(os.path.abspath(__file__))
H100_DATA = json.load(open(os.path.join(DIR, "..", "h100", "results.json")))
M5_DATA = json.load(open(os.path.join(DIR, "results.json")))
# -- Style --
plt.rcParams.update({
"font.family": "sans-serif",
"font.size": 11,
"axes.spines.top": False,
"axes.spines.right": False,
})
COLORS = {
"whisper": "#d63031",
"qwen_b": "#6c5ce7",
"qwen_s": "#00b894",
"voxtral": "#fdcb6e",
"m5_qwen": "#0984e3",
}
def _save(fig, name):
path = os.path.join(DIR, name)
fig.savefig(path, dpi=180, bbox_inches="tight", facecolor="white")
plt.close(fig)
print(f" saved: {name}")
def fig_m5_vs_h100():
"""WER vs RTF scatter: M5 (MLX) and H100 (CUDA) on LibriSpeech test-clean."""
h100 = H100_DATA["librispeech_clean"]["systems"]
m5 = M5_DATA["models"]
fig, ax = plt.subplots(figsize=(10, 7))
# Light green band for "good WER" zone
ax.axhspan(0, 5, color="#f0fff0", alpha=0.5, zorder=0)
# --- H100 points ---
h100_pts = [
("Whisper large-v3\n(H100, batch)", h100["whisper_large_v3_batch"], COLORS["whisper"], "h", 220),
("Qwen3 0.6B batch\n(H100)", h100["qwen3_0.6b_batch"], COLORS["qwen_b"], "h", 170),
("Qwen3 1.7B batch\n(H100)", h100["qwen3_1.7b_batch"], COLORS["qwen_b"], "h", 220),
("Voxtral 4B vLLM\n(H100)", h100["voxtral_4b_vllm_realtime"], COLORS["voxtral"], "D", 240),
("Qwen3 0.6B SimulStream+KV\n(H100)", h100["qwen3_0.6b_simulstream_kv"], COLORS["qwen_s"], "s", 200),
("Qwen3 1.7B SimulStream+KV\n(H100)", h100["qwen3_1.7b_simulstream_kv"], COLORS["qwen_s"], "s", 260),
]
h100_offsets = [(-55, 10), (-55, -22), (8, -18), (8, 10), (8, 10), (8, -18)]
for (name, d, color, marker, sz), (lx, ly) in zip(h100_pts, h100_offsets):
ax.scatter(d["rtf"], d["wer"], s=sz, c=color, marker=marker,
edgecolors="white", linewidths=1.5, zorder=5)
ax.annotate(name, (d["rtf"], d["wer"]), fontsize=7.5, fontweight="bold",
xytext=(lx, ly), textcoords="offset points",
arrowprops=dict(arrowstyle="-", color="#aaa", lw=0.5))
# --- M5 points ---
m5_pts = [
("Qwen3 0.6B SimulStream\n(M5, MLX)", m5["qwen3-asr-0.6b-simul"], COLORS["m5_qwen"], "^", 260),
("Qwen3 1.7B SimulStream\n(M5, MLX)", m5["qwen3-asr-1.7b-simul"], COLORS["m5_qwen"], "^", 300),
]
m5_offsets = [(8, 8), (8, -18)]
for (name, d, color, marker, sz), (lx, ly) in zip(m5_pts, m5_offsets):
ax.scatter(d["rtf"], d["wer"], s=sz, c=color, marker=marker,
edgecolors="white", linewidths=1.5, zorder=6)
ax.annotate(name, (d["rtf"], d["wer"]), fontsize=7.5, fontweight="bold",
xytext=(lx, ly), textcoords="offset points",
arrowprops=dict(arrowstyle="-", color="#aaa", lw=0.5))
# --- Connecting lines between same models on different hardware ---
# 0.6B: H100 SimulStream+KV -> M5 SimulStream
ax.plot([h100["qwen3_0.6b_simulstream_kv"]["rtf"], m5["qwen3-asr-0.6b-simul"]["rtf"]],
[h100["qwen3_0.6b_simulstream_kv"]["wer"], m5["qwen3-asr-0.6b-simul"]["wer"]],
"--", color="#0984e3", alpha=0.3, lw=1.5, zorder=3)
# 1.7B: H100 SimulStream+KV -> M5 SimulStream
ax.plot([h100["qwen3_1.7b_simulstream_kv"]["rtf"], m5["qwen3-asr-1.7b-simul"]["rtf"]],
[h100["qwen3_1.7b_simulstream_kv"]["wer"], m5["qwen3-asr-1.7b-simul"]["wer"]],
"--", color="#0984e3", alpha=0.3, lw=1.5, zorder=3)
# --- RTF = 1 line (real-time boundary) ---
ax.axvline(x=1.0, color="#e17055", linestyle=":", alpha=0.5, lw=1.5, zorder=1)
ax.text(1.02, 0.5, "real-time\nboundary", fontsize=8, color="#e17055",
fontstyle="italic", alpha=0.7, va="bottom")
# --- Methodology note ---
ax.text(0.98, 0.02,
"H100: chapter-grouped WER (91 chapters) | M5: per-utterance WER (500 samples)\n"
"Per-utterance WER is typically lower -- results are not directly comparable.",
transform=ax.transAxes, fontsize=7.5, color="#666",
ha="right", va="bottom", fontstyle="italic",
bbox=dict(boxstyle="round,pad=0.3", fc="#fff9e6", ec="#ddd", alpha=0.9))
ax.set_xlabel("RTF (lower = faster)")
ax.set_ylabel("WER % (lower = better)")
ax.set_title("H100 vs M5 (MLX) -- Qwen3-ASR on LibriSpeech test-clean",
fontsize=13, fontweight="bold", pad=12)
ax.set_xlim(-0.01, 1.1)
ax.set_ylim(-0.5, 10)
ax.grid(True, alpha=0.12)
legend = [
mpatches.Patch(color=COLORS["whisper"], label="Whisper large-v3 (H100)"),
mpatches.Patch(color=COLORS["qwen_b"], label="Qwen3-ASR batch (H100)"),
mpatches.Patch(color=COLORS["qwen_s"], label="Qwen3 SimulStream+KV (H100)"),
mpatches.Patch(color=COLORS["voxtral"], label="Voxtral 4B vLLM (H100)"),
mpatches.Patch(color=COLORS["m5_qwen"], label="Qwen3 SimulStream (M5, MLX)"),
plt.Line2D([0], [0], marker="h", color="w", mfc="gray", ms=8, label="Batch mode"),
plt.Line2D([0], [0], marker="s", color="w", mfc="gray", ms=8, label="Streaming (H100)"),
plt.Line2D([0], [0], marker="^", color="w", mfc="gray", ms=8, label="Streaming (M5)"),
]
ax.legend(handles=legend, fontsize=8, loc="upper right", framealpha=0.85, ncol=2)
_save(fig, "m5_vs_h100_wer_rtf.png")
if __name__ == "__main__":
print("Generating M5 vs H100 benchmark figure...")
fig_m5_vs_h100()
print("Done!")
================================================
FILE: benchmarks/m5/results.json
================================================
{
"platform": "Apple M5 (32GB RAM, MLX fp16)",
"dataset": "LibriSpeech test-clean",
"methodology": "per-utterance (500 samples)",
"models": {
"qwen3-asr-0.6b-simul": {"wer": 3.30, "rtf": 0.263},
"qwen3-asr-1.7b-simul": {"wer": 4.07, "rtf": 0.944}
}
}
================================================
FILE: chrome-extension/README.md
================================================
## WhisperLiveKit Chrome Extension v0.1.1
Capture the audio of your current tab, transcribe diarize and translate it using WhisperliveKit, in Chrome and other Chromium-based browsers.
> Currently, only the tab audio is captured; your microphone audio is not recorded.
## Running this extension
1. Run `python scripts/sync_extension.py` to copy frontend files to the `chrome-extension` directory.
2. Load the `chrome-extension` directory in Chrome as an unpacked extension.
## Devs:
- Impossible to capture audio from tabs if extension is a pannel, unfortunately:
- https://issues.chromium.org/issues/40926394
- https://groups.google.com/a/chromium.org/g/chromium-extensions/c/DET2SXCFnDg
- https://issues.chromium.org/issues/40916430
- To capture microphone in an extension, there are tricks: https://github.com/justinmann/sidepanel-audio-issue , https://medium.com/@lynchee.owo/how-to-enable-microphone-access-in-chrome-extensions-by-code-924295170080 (comments)
================================================
FILE: chrome-extension/background.js
================================================
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason.search(/install/g) === -1) {
return
}
chrome.tabs.create({
url: chrome.runtime.getURL("welcome.html"),
active: true
})
})
================================================
FILE: chrome-extension/manifest.json
================================================
{
"manifest_version": 3,
"name": "WhisperLiveKit Tab Capture",
"version": "1.0",
"description": "Capture and transcribe audio from browser tabs using WhisperLiveKit.",
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_title": "WhisperLiveKit Tab Capture",
"default_popup": "live_transcription.html"
},
"permissions": [
"scripting",
"tabCapture",
"offscreen",
"activeTab",
"storage"
]
}
================================================
FILE: chrome-extension/requestPermissions.html
================================================
Request Permissions
This page exists to workaround an issue with Chrome that blocks permission
requests from chrome extensions
================================================
FILE: chrome-extension/requestPermissions.js
================================================
/**
* Requests user permission for microphone access.
* @returns {Promise} A Promise that resolves when permission is granted or rejects with an error.
*/
async function getUserPermission() {
console.log("Getting user permission for microphone access...");
await navigator.mediaDevices.getUserMedia({ audio: true });
const micPermission = await navigator.permissions.query({
name: "microphone",
});
if (micPermission.state == "granted") {
window.close();
}
}
// Call the function to request microphone permission
getUserPermission();
================================================
FILE: chrome-extension/sidepanel.js
================================================
console.log("sidepanel.js");
async function run() {
const micPermission = await navigator.permissions.query({
name: "microphone",
});
document.getElementById(
"audioPermission"
).innerText = `MICROPHONE: ${micPermission.state}`;
if (micPermission.state !== "granted") {
chrome.tabs.create({ url: "requestPermissions.html" });
}
const intervalId = setInterval(async () => {
const micPermission = await navigator.permissions.query({
name: "microphone",
});
if (micPermission.state === "granted") {
document.getElementById(
"audioPermission"
).innerText = `MICROPHONE: ${micPermission.state}`;
clearInterval(intervalId);
}
}, 100);
}
void run();
================================================
FILE: compose.yml
================================================
services:
wlk-gpu-sortformer:
build:
context: .
dockerfile: Dockerfile
args:
EXTRAS: ${GPU_SORTFORMER_EXTRAS:-cu129,diarization-sortformer}
image: wlk:gpu-sortformer
gpus: all
ports:
- "8000:8000"
volumes:
- hf-cache:/root/.cache/huggingface/hub
# - ${HF_TKN_FILE:-./token}:/root/.cache/huggingface/token:ro
environment:
- HF_TOKEN
command: ["--model", "medium", "--diarization", "--pcm-input"]
wlk-gpu-voxtral:
build:
context: .
dockerfile: Dockerfile
args:
EXTRAS: ${GPU_VOXTRAL_EXTRAS:-cu129,voxtral-hf,translation}
image: wlk:gpu-voxtral
gpus: all
ports:
- "8001:8000"
volumes:
- hf-cache:/root/.cache/huggingface/hub
# - ${HF_TKN_FILE:-./token}:/root/.cache/huggingface/token:ro
environment:
- HF_TOKEN
command: ["--backend", "voxtral", "--pcm-input"]
wlk-cpu:
build:
context: .
dockerfile: Dockerfile.cpu
args:
EXTRAS: ${CPU_EXTRAS:-cpu,diarization-diart,translation}
image: wlk:cpu
ports:
- "8000:8000"
volumes:
- hf-cache:/root/.cache/huggingface/hub
# - ${HF_TKN_FILE:-./token}:/root/.cache/huggingface/token:ro
environment:
- HF_TOKEN
volumes:
hf-cache:
================================================
FILE: docs/API.md
================================================
# WhisperLiveKit API Reference
This document describes all APIs: the WebSocket streaming API, the OpenAI-compatible REST API, and the CLI.
---
## REST API (OpenAI-compatible)
### POST /v1/audio/transcriptions
Drop-in replacement for the OpenAI Audio Transcriptions API. Accepts the same parameters.
```bash
curl http://localhost:8000/v1/audio/transcriptions \
-F file=@audio.wav \
-F response_format=json
```
**Parameters (multipart form):**
| Parameter | Type | Default | Description |
|--------------------------|----------|---------|-------------|
| `file` | file | required | Audio file (any format ffmpeg can decode) |
| `model` | string | `""` | Accepted but ignored (uses server's backend) |
| `language` | string | `null` | ISO 639-1 language code or null for auto-detection |
| `prompt` | string | `""` | Accepted for compatibility, not yet used |
| `response_format` | string | `"json"` | `json`, `verbose_json`, `text`, `srt`, `vtt` |
| `timestamp_granularities`| array | `null` | Accepted for compatibility |
**Response formats:**
`json` (default):
```json
{"text": "Hello world, how are you?"}
```
`verbose_json`:
```json
{
"task": "transcribe",
"language": "en",
"duration": 7.16,
"text": "Hello world",
"words": [{"word": "Hello", "start": 0.0, "end": 0.5}, ...],
"segments": [{"id": 0, "start": 0.0, "end": 3.5, "text": "Hello world"}]
}
```
`text`: Plain text response.
`srt` / `vtt`: Subtitle format.
### GET /v1/models
List the currently loaded model.
```bash
curl http://localhost:8000/v1/models
```
### GET /health
Server health check.
```bash
curl http://localhost:8000/health
```
---
## Deepgram-Compatible WebSocket API
### WS /v1/listen
Drop-in compatible with Deepgram's Live Transcription WebSocket. Connect using any Deepgram client SDK pointed at your local server.
```python
from deepgram import DeepgramClient, LiveOptions
deepgram = DeepgramClient(api_key="unused", config={"url": "localhost:8000"})
connection = deepgram.listen.websocket.v("1")
connection.start(LiveOptions(model="nova-2", language="en"))
```
**Query Parameters:** Same as Deepgram (`language`, `punctuate`, `interim_results`, `vad_events`, etc.).
**Client Messages:**
- Binary audio frames
- `{"type": "KeepAlive"}` — keep connection alive
- `{"type": "CloseStream"}` — graceful close
- `{"type": "Finalize"}` — flush pending audio
**Server Messages:**
- `Metadata` — sent once at connection start
- `Results` — transcription results with `is_final`/`speech_final` flags
- `UtteranceEnd` — silence detected after speech
- `SpeechStarted` — speech begins (requires `vad_events=true`)
**Limitations vs Deepgram:**
- No authentication (self-hosted)
- Word timestamps are interpolated from segment boundaries
- Confidence scores are 0.0 (not available)
---
## CLI
### `wlk` / `wlk serve`
Start the transcription server.
```bash
wlk # Start with defaults
wlk --backend voxtral --model base # Specific backend
wlk serve --port 9000 --lan fr # Explicit serve command
```
### `wlk listen`
Live microphone transcription. Requires `sounddevice` (`pip install sounddevice`).
```bash
wlk listen # Transcribe from microphone
wlk listen --backend voxtral # Use specific backend
wlk listen --language fr # Force French
wlk listen --diarization # With speaker identification
wlk listen -o transcript.txt # Save to file on exit
```
Committed lines print as they are finalized. The current buffer (partial transcription) is shown in gray and updates in-place. Press Ctrl+C to stop; remaining audio is flushed before exit.
### `wlk run`
Auto-pull model if not downloaded, then start the server.
```bash
wlk run voxtral # Pull voxtral + start server
wlk run large-v3 # Pull large-v3 + start server
wlk run faster-whisper:base # Specific backend + model
wlk run qwen3:1.7b # Qwen3-ASR
wlk run voxtral --lan fr --port 9000 # Extra server options passed through
```
### `wlk transcribe`
Transcribe audio files offline (no server needed).
```bash
wlk transcribe audio.wav # Plain text output
wlk transcribe --format srt audio.wav # SRT subtitles
wlk transcribe --format json audio.wav # JSON output
wlk transcribe --backend voxtral audio.wav # Specific backend
wlk transcribe --model large-v3 --language fr *.wav # Multiple files
wlk transcribe --output result.srt --format srt audio.wav
```
### `wlk bench`
Benchmark speed (RTF) and accuracy (WER) on standard test audio.
```bash
wlk bench # Benchmark with defaults
wlk bench --backend faster-whisper # Specific backend
wlk bench --model large-v3 # Larger model
wlk bench --json results.json # Export results
```
Downloads test audio from LibriSpeech on first run. Reports WER (Word Error Rate) and RTF (Real-Time Factor: processing time / audio duration).
### `wlk diagnose`
Run pipeline diagnostics on an audio file. Feeds audio through the full pipeline while probing internal backend state at regular intervals. Produces a timeline, flags anomalies, and prints health checks.
```bash
wlk diagnose audio.wav # Diagnose with default backend
wlk diagnose audio.wav --backend voxtral # Diagnose specific backend
wlk diagnose --speed 0 --probe-interval 1 # Instant feed, probe every 1s
wlk diagnose # Use built-in test sample
```
Useful for debugging issues like: no output appearing, slow transcription, stuck pipelines, or generate thread errors.
### `wlk models`
List available backends, installation status, and downloaded models.
```bash
wlk models
```
### `wlk pull`
Download models for offline use.
```bash
wlk pull base # Download for best available backend
wlk pull faster-whisper:large-v3 # Specific backend + model
wlk pull voxtral # Voxtral HF model
wlk pull qwen3:1.7b # Qwen3-ASR 1.7B
```
### `wlk rm`
Delete downloaded models to free disk space.
```bash
wlk rm base # Delete base model
wlk rm voxtral # Delete Voxtral model
wlk rm faster-whisper:large-v3 # Delete specific backend model
```
### `wlk check`
Verify system dependencies (Python, ffmpeg, torch, etc.).
### `wlk version`
Print the installed version.
### Python Client (OpenAI SDK)
WhisperLiveKit's REST API is compatible with the OpenAI Python SDK:
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused")
with open("audio.wav", "rb") as f:
result = client.audio.transcriptions.create(
model="whisper-base", # ignored, uses server's backend
file=f,
response_format="verbose_json",
)
print(result.text)
```
### Programmatic Python API
For direct in-process usage without a server:
```python
import asyncio
from whisperlivekit import TranscriptionEngine, AudioProcessor
async def transcribe(audio_path):
engine = TranscriptionEngine(model_size="base", lan="en")
# ... use AudioProcessor for full pipeline control
```
Or use the TestHarness for simpler usage:
```python
import asyncio
from whisperlivekit import TestHarness
async def main():
async with TestHarness(model_size="base", lan="en") as h:
await h.feed("audio.wav", speed=0)
result = await h.finish()
print(result.text)
asyncio.run(main())
```
---
## WebSocket Streaming API
This section describes the WebSocket API for clients that want to stream audio and receive real-time transcription results from a WhisperLiveKit server.
---
## Connection
### Endpoint
```
ws://:/asr
```
### Query Parameters
| Parameter | Type | Default | Description |
|------------|--------|----------|-------------|
| `language` | string | _(none)_ | Per-session language override. ISO 639-1 code (e.g. `fr`, `en`) or `"auto"` for automatic detection. When omitted, uses the server-wide language setting. Multiple sessions with different languages work concurrently. |
| `mode` | string | `"full"` | Output mode. `"full"` sends complete state on every update. `"diff"` sends incremental diffs after an initial snapshot. |
Example:
```
ws://localhost:8000/asr?language=fr&mode=diff
```
### Connection Flow
1. Client opens a WebSocket connection to `/asr`.
2. Server accepts the connection and immediately sends a **config message**.
3. Client streams binary audio frames to the server.
4. Server sends transcription updates as JSON messages.
5. Client sends empty bytes (`b""`) to signal end of audio.
6. Server finishes processing remaining audio and sends a **ready_to_stop** message.
---
## Server to Client Messages
### Config Message
Sent once, immediately after the connection is accepted.
```json
{
"type": "config",
"useAudioWorklet": true,
"mode": "full"
}
```
| Field | Type | Description |
|-------------------|--------|-------------|
| `type` | string | Always `"config"`. |
| `useAudioWorklet` | bool | `true` when the server expects PCM s16le 16kHz mono input (started with `--pcm-input`). `false` when the server expects encoded audio (decoded server-side via FFmpeg). |
| `mode` | string | `"full"` or `"diff"`, echoing the requested mode. |
### Transcription Update (full mode)
Sent repeatedly as audio is processed. This message has **no `type` field**.
```json
{
"status": "active_transcription",
"lines": [
{
"speaker": 1,
"text": "Hello world, how are you?",
"start": "0:00:00",
"end": "0:00:03"
},
{
"speaker": 2,
"text": "I am fine, thanks.",
"start": "0:00:04",
"end": "0:00:06",
"translation": "Je vais bien, merci.",
"detected_language": "en"
}
],
"buffer_transcription": "And you",
"buffer_diarization": "",
"buffer_translation": "",
"remaining_time_transcription": 1.2,
"remaining_time_diarization": 0.5
}
```
| Field | Type | Description |
|--------------------------------|--------|-------------|
| `status` | string | `"active_transcription"` during normal operation. `"no_audio_detected"` when no speech has been detected yet. |
| `lines` | array | Committed transcription segments. Each update sends the **full list** of all committed lines (not incremental). |
| `buffer_transcription` | string | Ephemeral transcription text not yet committed to a line. Displayed in real time but overwritten on every update. |
| `buffer_diarization` | string | Ephemeral text waiting for speaker attribution. |
| `buffer_translation` | string | Ephemeral translation text for the current buffer. |
| `remaining_time_transcription` | float | Seconds of audio waiting to be transcribed (processing lag). |
| `remaining_time_diarization` | float | Seconds of audio waiting for speaker diarization. |
| `error` | string | Only present when an error occurred (e.g. FFmpeg failure). |
#### Line Object
Each element in `lines` has the following shape:
| Field | Type | Presence | Description |
|---------------------|--------|-------------|-------------|
| `speaker` | int | Always | Speaker ID. Normally `1`, `2`, `3`, etc. The special value `-2` indicates a silence segment. When diarization is disabled, defaults to `1`. |
| `text` | string | Always | The transcribed text for this segment. `null` for silence segments. |
| `start` | string | Always | Start timestamp formatted as `H:MM:SS` (e.g. `"0:00:03"`). |
| `end` | string | Always | End timestamp formatted as `H:MM:SS`. |
| `translation` | string | Conditional | Present only when translation is enabled and available for this line. |
| `detected_language` | string | Conditional | Present only when language detection produced a result for this line (e.g. `"en"`). |
### Snapshot (diff mode)
When `mode=diff`, the first transcription message is always a snapshot containing the full state. It has the same fields as a full-mode transcription update, plus metadata fields.
```json
{
"type": "snapshot",
"seq": 1,
"status": "active_transcription",
"lines": [ ... ],
"buffer_transcription": "",
"buffer_diarization": "",
"buffer_translation": "",
"remaining_time_transcription": 0.0,
"remaining_time_diarization": 0.0
}
```
| Field | Type | Description |
|--------|--------|-------------|
| `type` | string | `"snapshot"`. |
| `seq` | int | Monotonically increasing sequence number, starting at 1. |
| _(remaining fields)_ | | Same as a full-mode transcription update. |
### Diff (diff mode)
All messages after the initial snapshot are diffs.
```json
{
"type": "diff",
"seq": 4,
"status": "active_transcription",
"n_lines": 5,
"lines_pruned": 1,
"new_lines": [
{
"speaker": 1,
"text": "This is a new line.",
"start": "0:00:12",
"end": "0:00:14"
}
],
"buffer_transcription": "partial text",
"buffer_diarization": "",
"buffer_translation": "",
"remaining_time_transcription": 0.3,
"remaining_time_diarization": 0.1
}
```
| Field | Type | Presence | Description |
|--------------------------------|--------|-------------|-------------|
| `type` | string | Always | `"diff"`. |
| `seq` | int | Always | Sequence number. |
| `status` | string | Always | Same as full mode. |
| `n_lines` | int | Always | Total number of lines the client should have after applying this diff. Use this to verify sync. |
| `lines_pruned` | int | Conditional | Number of lines to remove from the **front** of the client's line list. Only present when > 0. |
| `new_lines` | array | Conditional | Lines to append to the **end** of the client's line list. Only present when there are new lines. |
| `buffer_transcription` | string | Always | Replaces the previous buffer value. |
| `buffer_diarization` | string | Always | Replaces the previous buffer value. |
| `buffer_translation` | string | Always | Replaces the previous buffer value. |
| `remaining_time_transcription` | float | Always | Replaces the previous value. |
| `remaining_time_diarization` | float | Always | Replaces the previous value. |
| `error` | string | Conditional | Only present on error. |
### Ready to Stop
Sent after all audio has been processed (i.e., after the client sent the end-of-audio signal and the server finished processing the remaining audio).
```json
{
"type": "ready_to_stop"
}
```
---
## Client to Server Messages
### Audio Frames
Send binary WebSocket frames containing audio data.
**When `useAudioWorklet` is `true` (server started with `--pcm-input`):**
- PCM signed 16-bit little-endian, 16 kHz, mono (`s16le`).
- Any chunk size works. A typical chunk is 0.5 seconds (16,000 bytes).
**When `useAudioWorklet` is `false`:**
- Raw encoded audio bytes (any format FFmpeg can decode: WAV, MP3, FLAC, OGG, etc.).
- The server pipes these bytes through FFmpeg for decoding.
### End-of-Audio Signal
Send an empty binary frame (`b""`) to tell the server that no more audio will follow. The server will finish processing any remaining audio and then send a `ready_to_stop` message.
---
## Diff Protocol: Client Reconstruction
Clients using `mode=diff` must maintain a local list of lines and apply diffs incrementally.
### Algorithm
```python
def reconstruct_state(msg, lines):
"""Apply a snapshot or diff message to a local lines list.
Args:
msg: The parsed JSON message from the server.
lines: The client's mutable list of line objects.
Returns:
A full-state dict with all fields.
"""
if msg["type"] == "snapshot":
lines.clear()
lines.extend(msg.get("lines", []))
return msg
# Apply diff
n_pruned = msg.get("lines_pruned", 0)
if n_pruned > 0:
del lines[:n_pruned]
new_lines = msg.get("new_lines", [])
lines.extend(new_lines)
# Volatile fields are replaced wholesale
return {
"status": msg.get("status", ""),
"lines": lines[:],
"buffer_transcription": msg.get("buffer_transcription", ""),
"buffer_diarization": msg.get("buffer_diarization", ""),
"buffer_translation": msg.get("buffer_translation", ""),
"remaining_time_transcription": msg.get("remaining_time_transcription", 0),
"remaining_time_diarization": msg.get("remaining_time_diarization", 0),
}
```
### Verification
After applying a diff, check that `len(lines) == msg["n_lines"]`. A mismatch indicates the client fell out of sync and should reconnect.
---
## Silence Representation
Silence segments are represented as lines with `speaker` set to `-2` and `text` set to `null`:
```json
{
"speaker": -2,
"text": null,
"start": "0:00:10",
"end": "0:00:12"
}
```
Silence segments are only generated for pauses longer than 5 seconds.
---
## Per-Session Language
The `language` query parameter creates an isolated language context for the session using `SessionASRProxy`. The proxy temporarily overrides the shared ASR backend's language during transcription calls, protected by a lock. This means:
- Each WebSocket session can transcribe in a different language.
- Sessions are thread-safe and do not interfere with each other.
- Pass `"auto"` to use automatic language detection for the session regardless of the server-wide setting.
================================================
FILE: docs/alignement_principles.md
================================================
### Alignment between STT Tokens and Diarization Segments
- Example 1: The punctuation from STT and the speaker change from Diariation come in the prediction `t`
- Example 2: The punctuation from STT comes from prediction `t`, but the speaker change from Diariation come in the prediction `t-1`
- Example 3: The punctuation from STT comes from prediction `t-1`, but the speaker change from Diariation come in the prediction `t`
> `#` Is the split between the `t-1` prediction and `t` prediction.
## Example 1:
```text
punctuations_segments : __#_______.__________________!____
diarization_segments:
SPK1 __#____________
SPK2 # ___________________
-->
ALIGNED SPK1 __#_______.
ALIGNED SPK2 # __________________!____
t-1 output:
SPK1: __#
SPK2: NO
DIARIZATION BUFFER: NO
t output:
SPK1: __#__.
SPK2: __________________!____
DIARIZATION BUFFER: No
```
## Example 2:
```text
punctuations_segments : _____#__.___________
diarization_segments:
SPK1 ___ #
SPK2 __#______________
-->
ALIGNED SPK1 _____#__.
ALIGNED SPK2 # ___________
t-1 output:
SPK1: ___ #
SPK2:
DIARIZATION BUFFER: __#
t output:
SPK1: __#__.
SPK2: ___________
DIARIZATION BUFFER: No
```
## Example 3:
```text
punctuations_segments : ___.__#__________
diarization_segments:
SPK1 ______#__
SPK2 # ________
-->
ALIGNED SPK1 ___. #
ALIGNED SPK2 __#__________
t-1 output:
SPK1: ___. #
SPK2:
DIARIZATION BUFFER: __#
t output:
SPK1: #
SPK2: __#___________
DIARIZATION BUFFER: NO
```
================================================
FILE: docs/default_and_custom_models.md
================================================
# Models and Model Paths
## Defaults
**Default Whisper Model**: `base`
When no model is specified, WhisperLiveKit uses the `base` model, which provides a good balance of speed and accuracy for most use cases.
**Default Model Cache Directory**: `~/.cache/whisper`
Models are automatically downloaded from OpenAI's model hub and cached in this directory. You can override this with `--model_cache_dir`.
**Default Translation Model**: `600M` (NLLB-200-distilled)
When translation is enabled, the 600M distilled NLLB model is used by default. This provides good quality with minimal resource usage.
**Default Translation Backend**: `transformers`
The translation backend defaults to Transformers. On Apple Silicon, this automatically uses MPS acceleration for better performance.
---
## Available Whisper model sizes:
| Available Model | Speed | Accuracy | Multilingual | Translation | Hardware Requirements | Best Use Case |
|--------------------|----------|-----------|--------------|-------------|----------------------|----------------------------------|
| tiny(.en) | Fastest | Basic | Yes/No | Yes/No | ~1GB VRAM | Real-time, low resources |
| base(.en) | Fast | Good | Yes/No | Yes/No | ~1GB VRAM | Balanced performance |
| small(.en) | Medium | Better | Yes/No | Yes/No | ~2GB VRAM | Quality on limited hardware |
| medium(.en) | Slow | High | Yes/No | Yes/No | ~5GB VRAM | High quality, moderate resources |
| large-v2 | Slowest | Excellent | Yes | Yes | ~10GB VRAM | Good overall accuracy & language support |
| large-v3 | Slowest | Excellent | Yes | Yes | ~10GB VRAM | Best overall accuracy & language support |
| large-v3-turbo | Fast | Excellent | Yes | No | ~6GB VRAM | Fast, high-quality transcription |
### How to choose?
#### Language Support
- **English only**: Use `.en` (ex: `base.en`) models for better accuracy and faster processing when you only need English transcription
- **Multilingual**: Do not use `.en` models.
#### Special Cases
- **No translation needed**: Use `large-v3-turbo`
- Same transcription quality as `large-v2` but significantly faster
- **Important**: Does not translate correctly, only transcribes
### Additional Considerations
**Model Performance**:
- Accuracy improves significantly from tiny to large models
- English-only models are ~10-15% more accurate for English audio
- Newer versions (v2, v3) have better punctuation and formatting
**Audio Quality Impact**:
- Clean, clear audio: smaller models may suffice
- Noisy, accented, or technical audio: larger models recommended
- Phone/low-quality audio: use at least `small` model
_______________________
# Custom Models:
The `--model-path` parameter accepts:
## File Path
- **`.pt` / `.bin` / `.safetensor` formats** Should be openable by pytorch/safetensor.
## Directory Path (recommended)
Must contain:
- **`.pt` / `.bin` / `.safetensor` file** (required for decoder)
May optionally contain:
- **`.bin` file** - faster-whisper model for encoder (requires faster-whisper)
- **`weights.npz`** or **`weights.safetensors`** - for encoder (requires whisper-mlx)
## Hugging Face Repo ID
- Provide the repo ID (e.g. `openai/whisper-large-v3`) and WhisperLiveKit will download and cache the snapshot automatically. For gated repos, authenticate via `huggingface-cli login` first.
To improve speed/reduce hallucinations, you may want to use `scripts/determine_alignment_heads.py` to determine the alignment heads to use for your model, and use the `--custom-alignment-heads` to pass them to WLK. If not, alignment heads are set to be all the heads of the last half layer of decoder.
_______________________
# Translation Models and Backend
**Language Support**: ~200 languages
## Distilled Model Sizes Available
| Model | Size | Parameters | VRAM (FP16) | VRAM (INT8) | Quality |
|-------|------|------------|-------------|-------------|---------|
| 600M | 2.46 GB | 600M | ~1.5GB | ~800MB | Good, understandable |
| 1.3B | 5.48 GB | 1.3B | ~3GB | ~1.5GB | Better accuracy, context |
**Quality Impact**: 1.3B has ~15-25% better BLEU scores vs 600M across language pairs.
## Backend Performance
| Backend | Speed vs Base | Memory Usage | Quality Loss |
|---------|---------------|--------------|--------------|
| CTranslate2 | 6-10x faster | 40-60% less | ~5% BLEU drop |
| Transformers | Baseline | High | None |
| Transformers + MPS (on Apple Silicon) | 2x faster | Medium | None |
**Metrics**:
- CTranslate2: 50-100+ tokens/sec
- Transformers: 10-30 tokens/sec
- Apple Silicon with MPS: Up to 2x faster than CTranslate2
================================================
FILE: docs/supported_languages.md
================================================
# Transcription: Supported Language
WLK supports transcription in the following languages:
| ISO Code | Language Name |
|----------|---------------------|
| en | English |
| zh | Chinese |
| de | German |
| es | Spanish |
| ru | Russian |
| ko | Korean |
| fr | French |
| ja | Japanese |
| pt | Portuguese |
| tr | Turkish |
| pl | Polish |
| ca | Catalan |
| nl | Dutch |
| ar | Arabic |
| sv | Swedish |
| it | Italian |
| id | Indonesian |
| hi | Hindi |
| fi | Finnish |
| vi | Vietnamese |
| he | Hebrew |
| uk | Ukrainian |
| el | Greek |
| ms | Malay |
| cs | Czech |
| ro | Romanian |
| da | Danish |
| hu | Hungarian |
| ta | Tamil |
| no | Norwegian |
| th | Thai |
| ur | Urdu |
| hr | Croatian |
| bg | Bulgarian |
| lt | Lithuanian |
| la | Latin |
| mi | Maori |
| ml | Malayalam |
| cy | Welsh |
| sk | Slovak |
| te | Telugu |
| fa | Persian |
| lv | Latvian |
| bn | Bengali |
| sr | Serbian |
| az | Azerbaijani |
| sl | Slovenian |
| kn | Kannada |
| et | Estonian |
| mk | Macedonian |
| br | Breton |
| eu | Basque |
| is | Icelandic |
| hy | Armenian |
| ne | Nepali |
| mn | Mongolian |
| bs | Bosnian |
| kk | Kazakh |
| sq | Albanian |
| sw | Swahili |
| gl | Galician |
| mr | Marathi |
| pa | Punjabi |
| si | Sinhala |
| km | Khmer |
| sn | Shona |
| yo | Yoruba |
| so | Somali |
| af | Afrikaans |
| oc | Occitan |
| ka | Georgian |
| be | Belarusian |
| tg | Tajik |
| sd | Sindhi |
| gu | Gujarati |
| am | Amharic |
| yi | Yiddish |
| lo | Lao |
| uz | Uzbek |
| fo | Faroese |
| ht | Haitian Creole |
| ps | Pashto |
| tk | Turkmen |
| nn | Nynorsk |
| mt | Maltese |
| sa | Sanskrit |
| lb | Luxembourgish |
| my | Myanmar |
| bo | Tibetan |
| tl | Tagalog |
| mg | Malagasy |
| as | Assamese |
| tt | Tatar |
| haw | Hawaiian |
| ln | Lingala |
| ha | Hausa |
| ba | Bashkir |
| jw | Javanese |
| su | Sundanese |
| yue | Cantonese |
# Translation: Supported Languages
WLK supports translation into **201 languages** from the FLORES-200 dataset through the [NLLW](https://github.com/QuentinFuxa/NoLanguageLeftWaiting) translation system.
## How to Specify Languages
You can specify languages in **three different ways**:
1. **Language Name** (case-insensitive): `"English"`, `"French"`, `"Spanish"`
2. **ISO Language Code**: `"en"`, `"fr"`, `"es"`
3. **NLLB Code** (FLORES-200): `"eng_Latn"`, `"fra_Latn"`, `"spa_Latn"`
## Usage Examples
### Command Line
```bash
# Using language name
whisperlivekit-server --target-language "French"
# Using ISO code
whisperlivekit-server --target-language fr
# Using NLLB code
whisperlivekit-server --target-language fra_Latn
```
### Python API
```python
from nllw.translation import get_language_info
# Get language information by name
lang_info = get_language_info("French")
print(lang_info)
# {'name': 'French', 'nllb': 'fra_Latn', 'language_code': 'fr'}
# Get language information by ISO code
lang_info = get_language_info("fr")
# Get language information by NLLB code
lang_info = get_language_info("fra_Latn")
# All three return the same result
```
## Complete Language List
The following table lists all 201 supported languages with their corresponding codes:
| Language Name | ISO Code | NLLB Code |
|---------------|----------|-----------|
| Acehnese (Arabic script) | ace_Arab | ace_Arab |
| Acehnese (Latin script) | ace_Latn | ace_Latn |
| Mesopotamian Arabic | acm_Arab | acm_Arab |
| Ta'izzi-Adeni Arabic | acq_Arab | acq_Arab |
| Tunisian Arabic | aeb_Arab | aeb_Arab |
| Afrikaans | af | afr_Latn |
| South Levantine Arabic | ajp_Arab | ajp_Arab |
| Akan | ak | aka_Latn |
| Tosk Albanian | als | als_Latn |
| Amharic | am | amh_Ethi |
| North Levantine Arabic | apc_Arab | apc_Arab |
| Modern Standard Arabic | ar | arb_Arab |
| Modern Standard Arabic (Romanized) | arb_Latn | arb_Latn |
| Najdi Arabic | ars_Arab | ars_Arab |
| Moroccan Arabic | ary_Arab | ary_Arab |
| Egyptian Arabic | arz_Arab | arz_Arab |
| Assamese | as | asm_Beng |
| Asturian | ast | ast_Latn |
| Awadhi | awa | awa_Deva |
| Central Aymara | ay | ayr_Latn |
| South Azerbaijani | azb | azb_Arab |
| North Azerbaijani | az | azj_Latn |
| Bashkir | ba | bak_Cyrl |
| Bambara | bm | bam_Latn |
| Balinese | ban | ban_Latn |
| Belarusian | be | bel_Cyrl |
| Bemba | bem | bem_Latn |
| Bengali | bn | ben_Beng |
| Bhojpuri | bho | bho_Deva |
| Banjar (Arabic script) | bjn_Arab | bjn_Arab |
| Banjar (Latin script) | bjn_Latn | bjn_Latn |
| Standard Tibetan | bo | bod_Tibt |
| Bosnian | bs | bos_Latn |
| Buginese | bug | bug_Latn |
| Bulgarian | bg | bul_Cyrl |
| Catalan | ca | cat_Latn |
| Cebuano | ceb | ceb_Latn |
| Czech | cs | ces_Latn |
| Chokwe | cjk | cjk_Latn |
| Central Kurdish | ckb | ckb_Arab |
| Crimean Tatar | crh | crh_Latn |
| Welsh | cy | cym_Latn |
| Danish | da | dan_Latn |
| German | de | deu_Latn |
| Southwestern Dinka | dik | dik_Latn |
| Dyula | dyu | dyu_Latn |
| Dzongkha | dz | dzo_Tibt |
| Greek | el | ell_Grek |
| English | en | eng_Latn |
| Esperanto | eo | epo_Latn |
| Estonian | et | est_Latn |
| Basque | eu | eus_Latn |
| Ewe | ee | ewe_Latn |
| Faroese | fo | fao_Latn |
| Fijian | fj | fij_Latn |
| Finnish | fi | fin_Latn |
| Fon | fon | fon_Latn |
| French | fr | fra_Latn |
| Friulian | fur-IT | fur_Latn |
| Nigerian Fulfulde | fuv | fuv_Latn |
| West Central Oromo | om | gaz_Latn |
| Scottish Gaelic | gd | gla_Latn |
| Irish | ga-IE | gle_Latn |
| Galician | gl | glg_Latn |
| Guarani | gn | grn_Latn |
| Gujarati | gu-IN | guj_Gujr |
| Haitian Creole | ht | hat_Latn |
| Hausa | ha | hau_Latn |
| Hebrew | he | heb_Hebr |
| Hindi | hi | hin_Deva |
| Chhattisgarhi | hne | hne_Deva |
| Croatian | hr | hrv_Latn |
| Hungarian | hu | hun_Latn |
| Armenian | hy-AM | hye_Armn |
| Igbo | ig | ibo_Latn |
| Ilocano | ilo | ilo_Latn |
| Indonesian | id | ind_Latn |
| Icelandic | is | isl_Latn |
| Italian | it | ita_Latn |
| Javanese | jv | jav_Latn |
| Japanese | ja | jpn_Jpan |
| Kabyle | kab | kab_Latn |
| Jingpho | kac | kac_Latn |
| Kamba | kam | kam_Latn |
| Kannada | kn | kan_Knda |
| Kashmiri (Arabic script) | kas_Arab | kas_Arab |
| Kashmiri (Devanagari script) | kas_Deva | kas_Deva |
| Georgian | ka | kat_Geor |
| Kazakh | kk | kaz_Cyrl |
| Kabiyè | kbp | kbp_Latn |
| Kabuverdianu | kea | kea_Latn |
| Halh Mongolian | mn | khk_Cyrl |
| Khmer | km | khm_Khmr |
| Kikuyu | ki | kik_Latn |
| Kinyarwanda | rw | kin_Latn |
| Kyrgyz | ky | kir_Cyrl |
| Kimbundu | kmb | kmb_Latn |
| Northern Kurdish | kmr | kmr_Latn |
| Central Kanuri (Arabic script) | knc_Arab | knc_Arab |
| Central Kanuri (Latin script) | knc_Latn | knc_Latn |
| Kikongo | kg | kon_Latn |
| Korean | ko | kor_Hang |
| Lao | lo | lao_Laoo |
| Ligurian | lij | lij_Latn |
| Limburgish | li | lim_Latn |
| Lingala | ln | lin_Latn |
| Lithuanian | lt | lit_Latn |
| Lombard | lmo | lmo_Latn |
| Latgalian | ltg | ltg_Latn |
| Luxembourgish | lb | ltz_Latn |
| Luba-Kasai | lua | lua_Latn |
| Ganda | lg | lug_Latn |
| Luo | luo | luo_Latn |
| Mizo | lus | lus_Latn |
| Standard Latvian | lv | lvs_Latn |
| Magahi | mag | mag_Deva |
| Maithili | mai | mai_Deva |
| Malayalam | ml-IN | mal_Mlym |
| Marathi | mr | mar_Deva |
| Minangkabau (Arabic script) | min_Arab | min_Arab |
| Minangkabau (Latin script) | min_Latn | min_Latn |
| Macedonian | mk | mkd_Cyrl |
| Maltese | mt | mlt_Latn |
| Meitei (Bengali script) | mni | mni_Beng |
| Mossi | mos | mos_Latn |
| Maori | mi | mri_Latn |
| Burmese | my | mya_Mymr |
| Dutch | nl | nld_Latn |
| Norwegian Nynorsk | nn-NO | nno_Latn |
| Norwegian Bokmål | nb | nob_Latn |
| Nepali | ne-NP | npi_Deva |
| Northern Sotho | nso | nso_Latn |
| Nuer | nus | nus_Latn |
| Nyanja | ny | nya_Latn |
| Occitan | oc | oci_Latn |
| Odia | or | ory_Orya |
| Pangasinan | pag | pag_Latn |
| Eastern Panjabi | pa | pan_Guru |
| Papiamento | pap | pap_Latn |
| Southern Pashto | pbt | pbt_Arab |
| Western Persian | fa | pes_Arab |
| Plateau Malagasy | mg | plt_Latn |
| Polish | pl | pol_Latn |
| Portuguese | pt-PT | por_Latn |
| Dari | fa-AF | prs_Arab |
| Ayacucho Quechua | qu | quy_Latn |
| Romanian | ro | ron_Latn |
| Rundi | rn | run_Latn |
| Russian | ru | rus_Cyrl |
| Sango | sg | sag_Latn |
| Sanskrit | sa | san_Deva |
| Santali | sat | sat_Olck |
| Sicilian | scn | scn_Latn |
| Shan | shn | shn_Mymr |
| Sinhala | si-LK | sin_Sinh |
| Slovak | sk | slk_Latn |
| Slovenian | sl | slv_Latn |
| Samoan | sm | smo_Latn |
| Shona | sn | sna_Latn |
| Sindhi | sd | snd_Arab |
| Somali | so | som_Latn |
| Southern Sotho | st | sot_Latn |
| Spanish | es-ES | spa_Latn |
| Sardinian | sc | srd_Latn |
| Serbian | sr | srp_Cyrl |
| Swati | ss | ssw_Latn |
| Sundanese | su | sun_Latn |
| Swedish | sv-SE | swe_Latn |
| Swahili | sw | swh_Latn |
| Silesian | szl | szl_Latn |
| Tamil | ta | tam_Taml |
| Tamasheq (Latin script) | taq_Latn | taq_Latn |
| Tamasheq (Tifinagh script) | taq_Tfng | taq_Tfng |
| Tatar | tt-RU | tat_Cyrl |
| Telugu | te | tel_Telu |
| Tajik | tg | tgk_Cyrl |
| Tagalog | tl | tgl_Latn |
| Thai | th | tha_Thai |
| Tigrinya | ti | tir_Ethi |
| Tok Pisin | tpi | tpi_Latn |
| Tswana | tn | tsn_Latn |
| Tsonga | ts | tso_Latn |
| Turkmen | tk | tuk_Latn |
| Tumbuka | tum | tum_Latn |
| Turkish | tr | tur_Latn |
| Twi | tw | twi_Latn |
| Central Atlas Tamazight | tzm | tzm_Tfng |
| Uyghur | ug | uig_Arab |
| Ukrainian | uk | ukr_Cyrl |
| Umbundu | umb | umb_Latn |
| Urdu | ur | urd_Arab |
| Northern Uzbek | uz | uzn_Latn |
| Venetian | vec | vec_Latn |
| Vietnamese | vi | vie_Latn |
| Waray | war | war_Latn |
| Wolof | wo | wol_Latn |
| Xhosa | xh | xho_Latn |
| Eastern Yiddish | yi | ydd_Hebr |
| Yoruba | yo | yor_Latn |
| Yue Chinese | yue | yue_Hant |
| Chinese (Simplified) | zh-CN | zho_Hans |
| Chinese (Traditional) | zh-TW | zho_Hant |
| Standard Malay | ms | zsm_Latn |
| Zulu | zu | zul_Latn |
## Special Features
### Multiple Script Support
Several languages are available in multiple scripts (e.g., Arabic and Latin):
- **Acehnese**: Arabic (`ace_Arab`) and Latin (`ace_Latn`)
- **Banjar**: Arabic (`bjn_Arab`) and Latin (`bjn_Latn`)
- **Kashmiri**: Arabic (`kas_Arab`) and Devanagari (`kas_Deva`)
- **Minangkabau**: Arabic (`min_Arab`) and Latin (`min_Latn`)
- **Tamasheq**: Latin (`taq_Latn`) and Tifinagh (`taq_Tfng`)
- **Central Kanuri**: Arabic (`knc_Arab`) and Latin (`knc_Latn`)
================================================
FILE: docs/technical_integration.md
================================================
# Technical Integration Guide
This document introduce how to reuse the core components when you do **not** want to ship the bundled frontend, FastAPI server, or even the provided CLI.
---
## 1. Runtime Components
| Layer | File(s) | Purpose |
|-------|---------|---------|
| Transport | `whisperlivekit/basic_server.py`, any ASGI/WebSocket server | Accepts audio over WebSocket (MediaRecorder WebM or raw PCM chunks) and streams JSON updates back |
| Audio processing | `whisperlivekit/audio_processor.py` | Buffers audio, orchestrates transcription, diarization, translation, handles FFmpeg/PCM input |
| Engines | `whisperlivekit/core.py`, `whisperlivekit/simul_whisper/*`, `whisperlivekit/local_agreement/*` | Load models once (SimulStreaming or LocalAgreement), expose `TranscriptionEngine` and helpers |
| Frontends | `whisperlivekit/web/*`, `chrome-extension/*` | Optional UI layers feeding the WebSocket endpoint |
**Key idea:** The server boundary is just `AudioProcessor.process_audio()` for incoming bytes and the async generator returned by `AudioProcessor.create_tasks()` for outgoing updates (`FrontData`). Everything else is optional.
---
## 2. Running Without the Bundled Frontend
1. Start the server/engine however you like:
```bash
wlk --model small --language en --host 0.0.0.0 --port 9000
# or launch your own app that instantiates TranscriptionEngine(...)
```
2. Build your own client (browser, mobile, desktop) that:
- Opens `ws(s)://:/asr`
- Sends either MediaRecorder/Opus WebM blobs **or** raw PCM (`--pcm-input` on the server tells the client to use the AudioWorklet).
- Consumes the JSON payload defined in `docs/API.md`.
---
## 3. Running Without FastAPI
`whisperlivekit/basic_server.py` is just an example. Any async framework works, as long as you:
1. Create a global `TranscriptionEngine` (expensive to initialize; reuse it).
2. Instantiate `AudioProcessor(transcription_engine=engine)` for each connection.
3. Call `create_tasks()` to get the async generator, `process_audio()` with incoming bytes, and ensure `cleanup()` runs when the client disconnects.
If you prefer to send compressed audio, instantiate `AudioProcessor(pcm_input=False)` and pipe encoded chunks through `FFmpegManager` transparently. Just ensure `ffmpeg` is available.
================================================
FILE: docs/troubleshooting.md
================================================
# Troubleshooting
## GPU drivers & cuDNN visibility
### Linux error: `Unable to load libcudnn_ops.so* / cudnnCreateTensorDescriptor`
> Reported in issue #271 (Arch/CachyOS)
`faster-whisper` (used for the SimulStreaming encoder) dynamically loads cuDNN.
If the runtime cannot find `libcudnn_*`, verify that CUDA and cuDNN match the PyTorch build you installed:
1. **Install CUDA + cuDNN** (Arch/CachyOS example):
```bash
sudo pacman -S cuda cudnn
sudo ldconfig
```
2. **Make sure the shared objects are visible**:
```bash
ls /usr/lib/libcudnn*
```
3. **Check what CUDA version PyTorch expects** and match that with the driver you installed:
```bash
python - <<'EOF'
import torch
print(torch.version.cuda)
EOF
nvcc --version
```
4. If you installed CUDA in a non-default location, export `CUDA_HOME` and add `$CUDA_HOME/lib64` to `LD_LIBRARY_PATH`.
Once the CUDA/cuDNN versions match, `whisperlivekit-server` starts normally.
### Windows error: `Could not locate cudnn_ops64_9.dll`
> Reported in issue #286 (Conda on Windows)
PyTorch bundles cuDNN DLLs inside your environment (`\Lib\site-packages\torch\lib`).
When `ctranslate2` or `faster-whisper` cannot find `cudnn_ops64_9.dll`:
1. Locate the DLL shipped with PyTorch, e.g.
```
E:\conda\envs\WhisperLiveKit\Lib\site-packages\torch\lib\cudnn_ops64_9.dll
```
2. Add that directory to your `PATH` **or** copy the `cudnn_*64_9.dll` files into a directory that is already on `PATH` (such as the environment's `Scripts/` folder).
3. Restart the shell before launching `wlk`.
Installing NVIDIA's standalone cuDNN 9.x and pointing `PATH`/`CUDNN_PATH` to it works as well, but is usually not required.
---
## PyTorch / CTranslate2 GPU builds
### `Torch not compiled with CUDA enabled`
> Reported in issue #284
If `torch.zeros(1).cuda()` raises that assertion it means you installed a CPU-only wheel.
Install the GPU-enabled wheels that match your CUDA toolkit:
```bash
pip install --upgrade torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu130
```
Replace `cu130` with the CUDA version supported by your driver (see [PyTorch install selector](https://pytorch.org/get-started/locally/)).
Validate with:
```python
import torch
print(torch.cuda.is_available(), torch.cuda.get_device_name())
```
### `CTranslate2 device count: 0` or `Could not infer dtype of ctranslate2._ext.StorageView`
> Follow-up in issue #284
`ctranslate2` publishes separate CPU and CUDA wheels. The default `pip install ctranslate2` brings the CPU build, which makes WhisperLiveKit fall back to CPU tensors and leads to the dtype error above.
1. Uninstall the CPU build: `pip uninstall -y ctranslate2`.
2. Install the CUDA wheel that matches your toolkit (example for CUDA 13.0):
```bash
pip install ctranslate2==4.5.0 -f https://opennmt.net/ctranslate2/whl/cu130
```
(See the [CTranslate2 installation table](https://opennmt.net/CTranslate2/installation.html) for other CUDA versions.)
3. Verify:
```python
import ctranslate2
print("CUDA devices:", ctranslate2.get_cuda_device_count())
print("CUDA compute types:", ctranslate2.get_supported_compute_types("cuda", 0))
```
**Note for aarch64 systems (e.g., NVIDIA DGX Spark):** Pre-built CUDA wheels may not be available for all CUDA versions on ARM architectures. If the wheel installation fails, you may need to compile CTranslate2 from source with CUDA support enabled.
If you intentionally want CPU inference, run `wlk --backend whisper` to avoid mixing CPU-only CTranslate2 with a GPU Torch build.
---
## Hopper / Blackwell (`sm_121a`) systems
> Reported in issues #276 and #284 (NVIDIA DGX Spark)
CUDA 12.1a GPUs (e.g., NVIDIA GB10 on DGX Spark) ship before some toolchains know about the architecture ID, so Triton/PTXAS need manual configuration.
### Error: `ptxas fatal : Value 'sm_121a' is not defined for option 'gpu-name'`
If you encounter this error after compiling CTranslate2 from source on aarch64 systems, Triton's bundled `ptxas` may not support the `sm_121a` architecture. The solution is to replace Triton's `ptxas` with the system's CUDA `ptxas`:
```bash
# Find your Python environment's Triton directory
python -c "import triton; import os; print(os.path.dirname(triton.__file__))"
# Copy the system ptxas to Triton's backend directory
# Replace with the output above
cp /usr/local/cuda/bin/ptxas /backends/nvidia/bin/ptxas
```
For example, in a virtual environment:
```bash
cp /usr/local/cuda/bin/ptxas ~/wlk/lib/python3.12/site-packages/triton/backends/nvidia/bin/ptxas
```
**Note:** On DGX Spark systems, CUDA is typically already in `PATH` (`/usr/local/cuda/bin`), so explicit `CUDA_HOME` and `PATH` exports may not be necessary. Verify with `which ptxas` before copying.
### Alternative: Environment variable approach
If the above doesn't work, you can try setting environment variables (though this may not resolve the `sm_121a` issue on all systems):
```bash
export CUDA_HOME="/usr/local/cuda-13.0"
export PATH="$CUDA_HOME/bin:$PATH"
export LD_LIBRARY_PATH="$CUDA_HOME/lib64:$LD_LIBRARY_PATH"
# Tell Triton where the new ptxas lives
export TRITON_PTXAS_PATH="$CUDA_HOME/bin/ptxas"
# Force PyTorch to JIT kernels for all needed architectures
export TORCH_CUDA_ARCH_LIST="8.0 9.0 10.0 12.0 12.1a"
```
After applying the fix, restart `wlk`. Incoming streams will now compile kernels targeting `sm_121a` without crashing.
---
Need help with another recurring issue? Open a GitHub discussion or PR and reference this document so we can keep it current.
================================================
FILE: pyproject.toml
================================================
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "whisperlivekit"
version = "0.2.20"
description = "Real-time speech-to-text models"
readme = "README.md"
authors = [{ name = "Quentin Fuxa" }]
license = { file = "LICENSE" }
requires-python = ">=3.11, <3.14"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Multimedia :: Sound/Audio :: Speech",
]
dependencies = [
"fastapi",
"librosa",
"soundfile",
"uvicorn",
"websockets",
"huggingface-hub>=0.25.0",
"faster-whisper>=1.2.0",
"torch>=2.0.0",
"torchaudio>=2.0.0",
"tqdm",
"tiktoken",
]
[project.optional-dependencies]
test = ["pytest>=7.0", "pytest-asyncio>=0.21", "datasets>=2.14", "librosa"]
translation = ["nllw"]
sentence_tokenizer = ["mosestokenizer", "wtpsplit"]
mlx-whisper = [
'mlx>=0.11.0; sys_platform == "darwin" and platform_machine == "arm64"',
'mlx-whisper>=0.4.0; sys_platform == "darwin" and platform_machine == "arm64"',
]
voxtral-mlx = [
'mlx>=0.11.0; sys_platform == "darwin" and platform_machine == "arm64"',
'mlx-whisper>=0.4.0; sys_platform == "darwin" and platform_machine == "arm64"',
"mistral-common[audio]",
]
voxtral-hf = [
"transformers>=5.2.0; python_version >= '3.10'",
"mistral-common[audio]",
"accelerate>=0.12",
]
listen = ["sounddevice>=0.4.6"]
cpu = ["torch>=2.0.0", "torchaudio>=2.0.0"]
cu129 = [
"torch>=2.0.0",
"torchaudio>=2.0.0",
'triton>=2.0.0; platform_machine == "x86_64" and (sys_platform == "linux" or sys_platform == "linux2")',
]
diarization-sortformer = [
"nemo-toolkit[asr]>2.4; python_version >= '3.10' and python_version < '3.13'",
]
diarization-diart = [
"diart",
"torch<2.9.0",
"torchaudio<2.9.0",
"torchvision<0.24.0",
]
[dependency-groups]
dev = ["rich>=14.3.3"]
[tool.uv]
conflicts = [
[
{ extra = "cpu" },
{ extra = "cu129" },
],
[
{ extra = "diarization-diart" },
{ extra = "cu129" },
],
[
{ extra = "voxtral-hf" },
{ extra = "diarization-sortformer" },
],
]
[tool.uv.sources]
torch = [
{ index = "pytorch-cpu", extra = "cpu", marker = "platform_system != 'Darwin'" },
{ index = "pytorch-cpu", extra = "diarization-diart", marker = "platform_system != 'Darwin'" },
{ index = "pytorch-cu129", extra = "cu129", marker = "platform_system == 'Linux' and platform_machine == 'x86_64'" },
]
torchaudio = [
{ index = "pytorch-cpu", extra = "cpu", marker = "platform_system != 'Darwin'" },
{ index = "pytorch-cpu", extra = "diarization-diart", marker = "platform_system != 'Darwin'" },
{ index = "pytorch-cu129", extra = "cu129", marker = "platform_system == 'Linux' and platform_machine == 'x86_64'" },
]
torchvision = [
{ index = "pytorch-cpu", extra = "diarization-diart", marker = "platform_system != 'Darwin'" },
]
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
[[tool.uv.index]]
name = "pytorch-cu129"
url = "https://download.pytorch.org/whl/cu129"
explicit = true
[project.urls]
Homepage = "https://github.com/QuentinFuxa/WhisperLiveKit"
[project.scripts]
whisperlivekit-server = "whisperlivekit.basic_server:main"
wlk = "whisperlivekit.cli:main"
wlk-test = "whisperlivekit.test_client:main"
[tool.ruff]
target-version = "py311"
line-length = 120
exclude = [".git", "__pycache__", "build", "dist", ".eggs", ".claude", "scripts", "run_benchmark.py"]
[tool.ruff.lint]
select = ["E", "F", "W", "I"]
ignore = ["E501", "E741"]
per-file-ignores = {"whisperlivekit/whisper/*" = ["F401", "F841", "E731", "W"], "whisperlivekit/simul_whisper/mlx/*" = ["F401", "E731", "W"], "whisperlivekit/simul_whisper/mlx_encoder.py" = ["E731", "F821"], "whisperlivekit/silero_vad_iterator.py" = ["F401"]}
[tool.setuptools]
packages = [
"whisperlivekit",
"whisperlivekit.diarization",
"whisperlivekit.simul_whisper",
"whisperlivekit.simul_whisper.mlx",
"whisperlivekit.whisper",
"whisperlivekit.whisper.assets",
"whisperlivekit.whisper.normalizers",
"whisperlivekit.web",
"whisperlivekit.local_agreement",
"whisperlivekit.voxtral_mlx",
"whisperlivekit.silero_vad_models",
"whisperlivekit.benchmark",
]
[tool.setuptools.package-data]
whisperlivekit = ["web/*.html", "web/*.css", "web/*.js", "web/src/*.svg"]
"whisperlivekit.whisper.assets" = ["*.tiktoken", "*.npz"]
"whisperlivekit.whisper.normalizers" = ["*.json"]
"whisperlivekit.silero_vad_models" = ["*.jit", "*.onnx"]
================================================
FILE: scripts/alignment_heads_qwen3_asr_0.6B.json
================================================
{
"model": "Qwen/Qwen3-ASR-0.6B",
"language": "English",
"num_layers": 28,
"num_heads": 16,
"num_kv_heads": 8,
"num_samples": 30,
"total_alignable_tokens": 533,
"ts_threshold": 0.1,
"ts_matrix": [
[
0.08067542213883677,
0.0825515947467167,
0.11819887429643527,
0.1575984990619137,
0.04127579737335835,
0.04878048780487805,
0.009380863039399626,
0.09193245778611632,
0.028142589118198873,
0.08818011257035648,
0.08442776735459662,
0.08818011257035648,
0.043151969981238276,
0.0150093808630394,
0.058161350844277676,
0.0525328330206379
],
[
0.075046904315197,
0.0900562851782364,
0.08067542213883677,
0.14634146341463414,
0.06566604127579738,
0.020637898686679174,
0.013133208255159476,
0.0225140712945591,
0.2870544090056285,
0.0225140712945591,
0.043151969981238276,
0.0225140712945591,
0.009380863039399626,
0.0600375234521576,
0.0975609756097561,
0.150093808630394
],
[
0.07129455909943715,
0.04878048780487805,
0.10881801125703565,
0.6772983114446529,
0.03564727954971857,
0.0450281425891182,
0.19136960600375236,
0.01876172607879925,
0.15572232645403378,
0.0975609756097561,
0.6960600375234521,
0.7617260787992496,
0.0825515947467167,
0.07129455909943715,
0.24202626641651032,
0.01125703564727955
],
[
0.07692307692307693,
0.0225140712945591,
0.17636022514071295,
0.17823639774859287,
0.324577861163227,
0.08818011257035648,
0.11069418386491557,
0.0675422138836773,
0.13883677298311445,
0.09380863039399624,
0.797373358348968,
0.6848030018761726,
0.0450281425891182,
0.2776735459662289,
0.26454033771106944,
0.18761726078799248
],
[
0.04127579737335835,
0.06566604127579738,
0.10881801125703565,
0.0900562851782364,
0.17448405253283303,
0.043151969981238276,
0.0300187617260788,
0.09380863039399624,
0.15196998123827393,
0.11632270168855535,
0.34709193245778613,
0.24202626641651032,
0.6041275797373359,
0.7467166979362101,
0.09943714821763602,
0.32082551594746717
],
[
0.12195121951219512,
0.15384615384615385,
0.10881801125703565,
0.075046904315197,
0.23827392120075047,
0.34896810506566606,
0.09943714821763602,
0.10881801125703565,
0.19887429643527205,
0.1050656660412758,
0.5234521575984991,
0.14634146341463414,
0.020637898686679174,
0.03377110694183865,
0.14634146341463414,
0.3621013133208255
],
[
0.275797373358349,
0.2551594746716698,
0.06378986866791744,
0.11444652908067542,
0.21200750469043153,
0.18198874296435272,
0.8086303939962477,
0.8198874296435272,
0.0375234521575985,
0.3076923076923077,
0.7879924953095685,
0.8067542213883677,
0.726078799249531,
0.799249530956848,
0.2795497185741088,
0.22326454033771106
],
[
0.4352720450281426,
0.03377110694183865,
0.06378986866791744,
0.075046904315197,
0.3789868667917448,
0.26454033771106944,
0.23076923076923078,
0.05628517823639775,
0.058161350844277676,
0.0450281425891182,
0.09943714821763602,
0.150093808630394,
0.17073170731707318,
0.21200750469043153,
0.1425891181988743,
0.1125703564727955
],
[
0.1651031894934334,
0.6904315196998124,
0.324577861163227,
0.07692307692307693,
0.6060037523452158,
0.3076923076923077,
0.30393996247654786,
0.35834896810506567,
0.0975609756097561,
0.15947467166979362,
0.14071294559099437,
0.14446529080675422,
0.11069418386491557,
0.1726078799249531,
0.35834896810506567,
0.07129455909943715
],
[
0.2551594746716698,
0.058161350844277676,
0.25328330206378985,
0.15384615384615385,
0.24577861163227016,
0.2551594746716698,
0.028142589118198873,
0.2701688555347092,
0.3771106941838649,
0.324577861163227,
0.18198874296435272,
0.10694183864915573,
0.6754221388367729,
0.6547842401500938,
0.1275797373358349,
0.016885553470919325
],
[
0.03564727954971857,
0.005628517823639775,
0.350844277673546,
0.2776735459662289,
0.23639774859287055,
0.38649155722326456,
0.03564727954971857,
0.02626641651031895,
0.11632270168855535,
0.24577861163227016,
0.13696060037523453,
0.22138836772983114,
0.1575984990619137,
0.2026266416510319,
0.07692307692307693,
0.1350844277673546
],
[
0.30956848030018763,
0.35647279549718575,
0.849906191369606,
0.7936210131332082,
0.15947467166979362,
0.26641651031894936,
0.23639774859287055,
0.3302063789868668,
0.6716697936210131,
0.45778611632270166,
0.4709193245778612,
0.7373358348968105,
0.8067542213883677,
0.8348968105065666,
0.03189493433395872,
0.09193245778611632
],
[
0.46153846153846156,
0.4896810506566604,
0.19887429643527205,
0.30956848030018763,
0.0900562851782364,
0.13320825515947468,
0.7185741088180112,
0.1125703564727955,
0.44652908067542213,
0.11632270168855535,
0.2964352720450281,
0.075046904315197,
0.28142589118198874,
0.14071294559099437,
0.2795497185741088,
0.21575984990619138
],
[
0.7560975609756098,
0.34709193245778613,
0.23076923076923078,
0.19136960600375236,
0.4971857410881801,
0.18198874296435272,
0.8442776735459663,
0.8048780487804879,
0.05065666041275797,
0.0450281425891182,
0.15196998123827393,
0.7542213883677298,
0.0300187617260788,
0.03189493433395872,
0.5666041275797373,
0.6022514071294559
],
[
0.28142589118198874,
0.10881801125703565,
0.14821763602251406,
0.10318949343339587,
0.0225140712945591,
0.23639774859287055,
0.28330206378986866,
0.2045028142589118,
0.11632270168855535,
0.13696060037523453,
0.19136960600375236,
0.23827392120075047,
0.3227016885553471,
0.2945590994371482,
0.8330206378986866,
0.8198874296435272
],
[
0.09568480300187618,
0.150093808630394,
0.2551594746716698,
0.13320825515947468,
0.1575984990619137,
0.18574108818011256,
0.2776735459662289,
0.16885553470919323,
0.05065666041275797,
0.16885553470919323,
0.5909943714821764,
0.18198874296435272,
0.0675422138836773,
0.04690431519699812,
0.13696060037523453,
0.15572232645403378
],
[
0.075046904315197,
0.03189493433395872,
0.07879924953095685,
0.11819887429643527,
0.06378986866791744,
0.24390243902439024,
0.2926829268292683,
0.5703564727954972,
0.24953095684803,
0.31894934333958724,
0.7429643527204502,
0.5159474671669794,
0.4915572232645403,
0.549718574108818,
0.8086303939962477,
0.7523452157598499
],
[
0.36397748592870544,
0.34896810506566606,
0.275797373358349,
0.23452157598499063,
0.10694183864915573,
0.04690431519699812,
0.01876172607879925,
0.024390243902439025,
0.38461538461538464,
0.30956848030018763,
0.2626641651031895,
0.24390243902439024,
0.32082551594746717,
0.45590994371482174,
0.08818011257035648,
0.08442776735459662
],
[
0.024390243902439025,
0.024390243902439025,
0.4146341463414634,
0.7354596622889306,
0.324577861163227,
0.7354596622889306,
0.20075046904315197,
0.17823639774859287,
0.14821763602251406,
0.09380863039399624,
0.4427767354596623,
0.2964352720450281,
0.0225140712945591,
0.22326454033771106,
0.06941838649155722,
0.17073170731707318
],
[
0.0975609756097561,
0.20825515947467166,
0.47842401500938087,
0.6041275797373359,
0.49906191369606,
0.7073170731707317,
0.37335834896810505,
0.7786116322701688,
0.4521575984990619,
0.5647279549718575,
0.07879924953095685,
0.07692307692307693,
0.4596622889305816,
0.474671669793621,
0.01876172607879925,
0.028142589118198873
],
[
0.09193245778611632,
0.08067542213883677,
0.2626641651031895,
0.8555347091932458,
0.4352720450281426,
0.2776735459662289,
0.38649155722326456,
0.6116322701688556,
0.32833020637898686,
0.04127579737335835,
0.6097560975609756,
0.6322701688555347,
0.41275797373358347,
0.27392120075046905,
0.7091932457786116,
0.701688555347092
],
[
0.6360225140712945,
0.6172607879924953,
0.15572232645403378,
0.0450281425891182,
0.32833020637898686,
0.0900562851782364,
0.2795497185741088,
0.26454033771106944,
0.7692307692307693,
0.7842401500938087,
0.33583489681050654,
0.43151969981238275,
0.6228893058161351,
0.4803001876172608,
0.40337711069418386,
0.4634146341463415
],
[
0.25328330206378985,
0.3395872420262664,
0.15196998123827393,
0.06566604127579738,
0.3452157598499062,
0.2851782363977486,
0.30956848030018763,
0.7054409005628518,
0.6979362101313321,
0.701688555347092,
0.1801125703564728,
0.2401500938086304,
0.6716697936210131,
0.6228893058161351,
0.18761726078799248,
0.10881801125703565
],
[
0.5553470919324578,
0.5647279549718575,
0.0600375234521576,
0.10881801125703565,
0.6772983114446529,
0.2682926829268293,
0.5590994371482176,
0.7091932457786116,
0.05065666041275797,
0.07317073170731707,
0.5103189493433395,
0.3789868667917448,
0.275797373358349,
0.16885553470919323,
0.701688555347092,
0.6923076923076923
],
[
0.043151969981238276,
0.05065666041275797,
0.054409005628517824,
0.0600375234521576,
0.46716697936210133,
0.6904315196998124,
0.626641651031895,
0.6848030018761726,
0.09943714821763602,
0.09193245778611632,
0.6566604127579737,
0.6679174484052532,
0.6697936210131332,
0.6772983114446529,
0.6979362101313321,
0.6904315196998124
],
[
0.13696060037523453,
0.09380863039399624,
0.01876172607879925,
0.08442776735459662,
0.6923076923076923,
0.701688555347092,
0.6472795497185742,
0.6772983114446529,
0.32833020637898686,
0.5534709193245778,
0.6716697936210131,
0.6941838649155723,
0.6622889305816135,
0.6566604127579737,
0.6360225140712945,
0.4521575984990619
],
[
0.49343339587242024,
0.4709193245778612,
0.6529080675422139,
0.6378986866791745,
0.6322701688555347,
0.6041275797373359,
0.23827392120075047,
0.6322701688555347,
0.6923076923076923,
0.2926829268292683,
0.03189493433395872,
0.3058161350844278,
0.07317073170731707,
0.08630393996247655,
0.6060037523452158,
0.5590994371482176
],
[
0.1350844277673546,
0.13883677298311445,
0.08818011257035648,
0.10694183864915573,
0.04878048780487805,
0.1350844277673546,
0.09380863039399624,
0.09380863039399624,
0.1294559099437148,
0.1125703564727955,
0.13133208255159476,
0.06941838649155722,
0.075046904315197,
0.10318949343339587,
0.0975609756097561,
0.09193245778611632
]
],
"alignment_heads": [
{
"layer": 20,
"head": 3,
"ts": 0.8555
},
{
"layer": 11,
"head": 2,
"ts": 0.8499
},
{
"layer": 13,
"head": 6,
"ts": 0.8443
},
{
"layer": 11,
"head": 13,
"ts": 0.8349
},
{
"layer": 14,
"head": 14,
"ts": 0.833
},
{
"layer": 6,
"head": 7,
"ts": 0.8199
},
{
"layer": 14,
"head": 15,
"ts": 0.8199
},
{
"layer": 6,
"head": 6,
"ts": 0.8086
},
{
"layer": 16,
"head": 14,
"ts": 0.8086
},
{
"layer": 6,
"head": 11,
"ts": 0.8068
},
{
"layer": 11,
"head": 12,
"ts": 0.8068
},
{
"layer": 13,
"head": 7,
"ts": 0.8049
},
{
"layer": 6,
"head": 13,
"ts": 0.7992
},
{
"layer": 3,
"head": 10,
"ts": 0.7974
},
{
"layer": 11,
"head": 3,
"ts": 0.7936
},
{
"layer": 6,
"head": 10,
"ts": 0.788
},
{
"layer": 21,
"head": 9,
"ts": 0.7842
},
{
"layer": 19,
"head": 7,
"ts": 0.7786
},
{
"layer": 21,
"head": 8,
"ts": 0.7692
},
{
"layer": 2,
"head": 11,
"ts": 0.7617
},
{
"layer": 13,
"head": 0,
"ts": 0.7561
},
{
"layer": 13,
"head": 11,
"ts": 0.7542
},
{
"layer": 16,
"head": 15,
"ts": 0.7523
},
{
"layer": 4,
"head": 13,
"ts": 0.7467
},
{
"layer": 16,
"head": 10,
"ts": 0.743
},
{
"layer": 11,
"head": 11,
"ts": 0.7373
},
{
"layer": 18,
"head": 3,
"ts": 0.7355
},
{
"layer": 18,
"head": 5,
"ts": 0.7355
},
{
"layer": 6,
"head": 12,
"ts": 0.7261
},
{
"layer": 12,
"head": 6,
"ts": 0.7186
},
{
"layer": 20,
"head": 14,
"ts": 0.7092
},
{
"layer": 23,
"head": 7,
"ts": 0.7092
},
{
"layer": 19,
"head": 5,
"ts": 0.7073
},
{
"layer": 22,
"head": 7,
"ts": 0.7054
},
{
"layer": 20,
"head": 15,
"ts": 0.7017
},
{
"layer": 22,
"head": 9,
"ts": 0.7017
},
{
"layer": 23,
"head": 14,
"ts": 0.7017
},
{
"layer": 25,
"head": 5,
"ts": 0.7017
},
{
"layer": 22,
"head": 8,
"ts": 0.6979
},
{
"layer": 24,
"head": 14,
"ts": 0.6979
},
{
"layer": 2,
"head": 10,
"ts": 0.6961
},
{
"layer": 25,
"head": 11,
"ts": 0.6942
},
{
"layer": 23,
"head": 15,
"ts": 0.6923
},
{
"layer": 25,
"head": 4,
"ts": 0.6923
},
{
"layer": 26,
"head": 8,
"ts": 0.6923
},
{
"layer": 8,
"head": 1,
"ts": 0.6904
},
{
"layer": 24,
"head": 5,
"ts": 0.6904
},
{
"layer": 24,
"head": 15,
"ts": 0.6904
},
{
"layer": 3,
"head": 11,
"ts": 0.6848
},
{
"layer": 24,
"head": 7,
"ts": 0.6848
},
{
"layer": 2,
"head": 3,
"ts": 0.6773
},
{
"layer": 23,
"head": 4,
"ts": 0.6773
},
{
"layer": 24,
"head": 13,
"ts": 0.6773
},
{
"layer": 25,
"head": 7,
"ts": 0.6773
},
{
"layer": 9,
"head": 12,
"ts": 0.6754
},
{
"layer": 11,
"head": 8,
"ts": 0.6717
},
{
"layer": 22,
"head": 12,
"ts": 0.6717
},
{
"layer": 25,
"head": 10,
"ts": 0.6717
},
{
"layer": 24,
"head": 12,
"ts": 0.6698
},
{
"layer": 24,
"head": 11,
"ts": 0.6679
},
{
"layer": 25,
"head": 12,
"ts": 0.6623
},
{
"layer": 24,
"head": 10,
"ts": 0.6567
},
{
"layer": 25,
"head": 13,
"ts": 0.6567
},
{
"layer": 9,
"head": 13,
"ts": 0.6548
},
{
"layer": 26,
"head": 2,
"ts": 0.6529
},
{
"layer": 25,
"head": 6,
"ts": 0.6473
},
{
"layer": 26,
"head": 3,
"ts": 0.6379
},
{
"layer": 21,
"head": 0,
"ts": 0.636
},
{
"layer": 25,
"head": 14,
"ts": 0.636
},
{
"layer": 20,
"head": 11,
"ts": 0.6323
},
{
"layer": 26,
"head": 4,
"ts": 0.6323
},
{
"layer": 26,
"head": 7,
"ts": 0.6323
},
{
"layer": 24,
"head": 6,
"ts": 0.6266
},
{
"layer": 21,
"head": 12,
"ts": 0.6229
},
{
"layer": 22,
"head": 13,
"ts": 0.6229
},
{
"layer": 21,
"head": 1,
"ts": 0.6173
},
{
"layer": 20,
"head": 7,
"ts": 0.6116
},
{
"layer": 20,
"head": 10,
"ts": 0.6098
},
{
"layer": 8,
"head": 4,
"ts": 0.606
},
{
"layer": 26,
"head": 14,
"ts": 0.606
},
{
"layer": 4,
"head": 12,
"ts": 0.6041
},
{
"layer": 19,
"head": 3,
"ts": 0.6041
},
{
"layer": 26,
"head": 5,
"ts": 0.6041
},
{
"layer": 13,
"head": 15,
"ts": 0.6023
},
{
"layer": 15,
"head": 10,
"ts": 0.591
},
{
"layer": 16,
"head": 7,
"ts": 0.5704
},
{
"layer": 13,
"head": 14,
"ts": 0.5666
},
{
"layer": 19,
"head": 9,
"ts": 0.5647
},
{
"layer": 23,
"head": 1,
"ts": 0.5647
},
{
"layer": 23,
"head": 6,
"ts": 0.5591
},
{
"layer": 26,
"head": 15,
"ts": 0.5591
},
{
"layer": 23,
"head": 0,
"ts": 0.5553
},
{
"layer": 25,
"head": 9,
"ts": 0.5535
},
{
"layer": 16,
"head": 13,
"ts": 0.5497
},
{
"layer": 5,
"head": 10,
"ts": 0.5235
},
{
"layer": 16,
"head": 11,
"ts": 0.5159
},
{
"layer": 23,
"head": 10,
"ts": 0.5103
},
{
"layer": 19,
"head": 4,
"ts": 0.4991
},
{
"layer": 13,
"head": 4,
"ts": 0.4972
},
{
"layer": 26,
"head": 0,
"ts": 0.4934
},
{
"layer": 16,
"head": 12,
"ts": 0.4916
},
{
"layer": 12,
"head": 1,
"ts": 0.4897
},
{
"layer": 21,
"head": 13,
"ts": 0.4803
},
{
"layer": 19,
"head": 2,
"ts": 0.4784
},
{
"layer": 19,
"head": 13,
"ts": 0.4747
},
{
"layer": 11,
"head": 10,
"ts": 0.4709
},
{
"layer": 26,
"head": 1,
"ts": 0.4709
},
{
"layer": 24,
"head": 4,
"ts": 0.4672
},
{
"layer": 21,
"head": 15,
"ts": 0.4634
},
{
"layer": 12,
"head": 0,
"ts": 0.4615
},
{
"layer": 19,
"head": 12,
"ts": 0.4597
},
{
"layer": 11,
"head": 9,
"ts": 0.4578
},
{
"layer": 17,
"head": 13,
"ts": 0.4559
},
{
"layer": 19,
"head": 8,
"ts": 0.4522
},
{
"layer": 25,
"head": 15,
"ts": 0.4522
},
{
"layer": 12,
"head": 8,
"ts": 0.4465
},
{
"layer": 18,
"head": 10,
"ts": 0.4428
},
{
"layer": 7,
"head": 0,
"ts": 0.4353
},
{
"layer": 20,
"head": 4,
"ts": 0.4353
},
{
"layer": 21,
"head": 11,
"ts": 0.4315
},
{
"layer": 18,
"head": 2,
"ts": 0.4146
},
{
"layer": 20,
"head": 12,
"ts": 0.4128
},
{
"layer": 21,
"head": 14,
"ts": 0.4034
},
{
"layer": 10,
"head": 5,
"ts": 0.3865
},
{
"layer": 20,
"head": 6,
"ts": 0.3865
},
{
"layer": 17,
"head": 8,
"ts": 0.3846
},
{
"layer": 7,
"head": 4,
"ts": 0.379
},
{
"layer": 23,
"head": 11,
"ts": 0.379
},
{
"layer": 9,
"head": 8,
"ts": 0.3771
},
{
"layer": 19,
"head": 6,
"ts": 0.3734
},
{
"layer": 17,
"head": 0,
"ts": 0.364
},
{
"layer": 5,
"head": 15,
"ts": 0.3621
},
{
"layer": 8,
"head": 7,
"ts": 0.3583
},
{
"layer": 8,
"head": 14,
"ts": 0.3583
},
{
"layer": 11,
"head": 1,
"ts": 0.3565
},
{
"layer": 10,
"head": 2,
"ts": 0.3508
},
{
"layer": 5,
"head": 5,
"ts": 0.349
},
{
"layer": 17,
"head": 1,
"ts": 0.349
},
{
"layer": 4,
"head": 10,
"ts": 0.3471
},
{
"layer": 13,
"head": 1,
"ts": 0.3471
},
{
"layer": 22,
"head": 4,
"ts": 0.3452
},
{
"layer": 22,
"head": 1,
"ts": 0.3396
},
{
"layer": 21,
"head": 10,
"ts": 0.3358
},
{
"layer": 11,
"head": 7,
"ts": 0.3302
},
{
"layer": 20,
"head": 8,
"ts": 0.3283
},
{
"layer": 21,
"head": 4,
"ts": 0.3283
},
{
"layer": 25,
"head": 8,
"ts": 0.3283
},
{
"layer": 3,
"head": 4,
"ts": 0.3246
},
{
"layer": 8,
"head": 2,
"ts": 0.3246
},
{
"layer": 9,
"head": 9,
"ts": 0.3246
},
{
"layer": 18,
"head": 4,
"ts": 0.3246
},
{
"layer": 14,
"head": 12,
"ts": 0.3227
},
{
"layer": 4,
"head": 15,
"ts": 0.3208
},
{
"layer": 17,
"head": 12,
"ts": 0.3208
},
{
"layer": 16,
"head": 9,
"ts": 0.3189
},
{
"layer": 11,
"head": 0,
"ts": 0.3096
},
{
"layer": 12,
"head": 3,
"ts": 0.3096
},
{
"layer": 17,
"head": 9,
"ts": 0.3096
},
{
"layer": 22,
"head": 6,
"ts": 0.3096
},
{
"layer": 6,
"head": 9,
"ts": 0.3077
},
{
"layer": 8,
"head": 5,
"ts": 0.3077
},
{
"layer": 26,
"head": 11,
"ts": 0.3058
},
{
"layer": 8,
"head": 6,
"ts": 0.3039
},
{
"layer": 12,
"head": 10,
"ts": 0.2964
},
{
"layer": 18,
"head": 11,
"ts": 0.2964
},
{
"layer": 14,
"head": 13,
"ts": 0.2946
},
{
"layer": 16,
"head": 6,
"ts": 0.2927
},
{
"layer": 26,
"head": 9,
"ts": 0.2927
},
{
"layer": 1,
"head": 8,
"ts": 0.2871
},
{
"layer": 22,
"head": 5,
"ts": 0.2852
},
{
"layer": 14,
"head": 6,
"ts": 0.2833
},
{
"layer": 12,
"head": 12,
"ts": 0.2814
},
{
"layer": 14,
"head": 0,
"ts": 0.2814
},
{
"layer": 6,
"head": 14,
"ts": 0.2795
},
{
"layer": 12,
"head": 14,
"ts": 0.2795
},
{
"layer": 21,
"head": 6,
"ts": 0.2795
},
{
"layer": 3,
"head": 13,
"ts": 0.2777
},
{
"layer": 10,
"head": 3,
"ts": 0.2777
},
{
"layer": 15,
"head": 6,
"ts": 0.2777
},
{
"layer": 20,
"head": 5,
"ts": 0.2777
},
{
"layer": 6,
"head": 0,
"ts": 0.2758
},
{
"layer": 17,
"head": 2,
"ts": 0.2758
},
{
"layer": 23,
"head": 12,
"ts": 0.2758
},
{
"layer": 20,
"head": 13,
"ts": 0.2739
},
{
"layer": 9,
"head": 7,
"ts": 0.2702
},
{
"layer": 23,
"head": 5,
"ts": 0.2683
},
{
"layer": 11,
"head": 5,
"ts": 0.2664
},
{
"layer": 3,
"head": 14,
"ts": 0.2645
},
{
"layer": 7,
"head": 5,
"ts": 0.2645
},
{
"layer": 21,
"head": 7,
"ts": 0.2645
},
{
"layer": 17,
"head": 10,
"ts": 0.2627
},
{
"layer": 20,
"head": 2,
"ts": 0.2627
},
{
"layer": 6,
"head": 1,
"ts": 0.2552
},
{
"layer": 9,
"head": 0,
"ts": 0.2552
},
{
"layer": 9,
"head": 5,
"ts": 0.2552
},
{
"layer": 15,
"head": 2,
"ts": 0.2552
},
{
"layer": 9,
"head": 2,
"ts": 0.2533
},
{
"layer": 22,
"head": 0,
"ts": 0.2533
},
{
"layer": 16,
"head": 8,
"ts": 0.2495
},
{
"layer": 9,
"head": 4,
"ts": 0.2458
},
{
"layer": 10,
"head": 9,
"ts": 0.2458
},
{
"layer": 16,
"head": 5,
"ts": 0.2439
},
{
"layer": 17,
"head": 11,
"ts": 0.2439
},
{
"layer": 2,
"head": 14,
"ts": 0.242
},
{
"layer": 4,
"head": 11,
"ts": 0.242
},
{
"layer": 22,
"head": 11,
"ts": 0.2402
},
{
"layer": 5,
"head": 4,
"ts": 0.2383
},
{
"layer": 14,
"head": 11,
"ts": 0.2383
},
{
"layer": 26,
"head": 6,
"ts": 0.2383
},
{
"layer": 10,
"head": 4,
"ts": 0.2364
},
{
"layer": 11,
"head": 6,
"ts": 0.2364
},
{
"layer": 14,
"head": 5,
"ts": 0.2364
},
{
"layer": 17,
"head": 3,
"ts": 0.2345
},
{
"layer": 7,
"head": 6,
"ts": 0.2308
},
{
"layer": 13,
"head": 2,
"ts": 0.2308
},
{
"layer": 6,
"head": 15,
"ts": 0.2233
},
{
"layer": 18,
"head": 13,
"ts": 0.2233
},
{
"layer": 10,
"head": 11,
"ts": 0.2214
},
{
"layer": 12,
"head": 15,
"ts": 0.2158
},
{
"layer": 6,
"head": 4,
"ts": 0.212
},
{
"layer": 7,
"head": 13,
"ts": 0.212
},
{
"layer": 19,
"head": 1,
"ts": 0.2083
},
{
"layer": 14,
"head": 7,
"ts": 0.2045
},
{
"layer": 10,
"head": 13,
"ts": 0.2026
},
{
"layer": 18,
"head": 6,
"ts": 0.2008
},
{
"layer": 5,
"head": 8,
"ts": 0.1989
},
{
"layer": 12,
"head": 2,
"ts": 0.1989
},
{
"layer": 2,
"head": 6,
"ts": 0.1914
},
{
"layer": 13,
"head": 3,
"ts": 0.1914
},
{
"layer": 14,
"head": 10,
"ts": 0.1914
},
{
"layer": 3,
"head": 15,
"ts": 0.1876
},
{
"layer": 22,
"head": 14,
"ts": 0.1876
},
{
"layer": 15,
"head": 5,
"ts": 0.1857
},
{
"layer": 6,
"head": 5,
"ts": 0.182
},
{
"layer": 9,
"head": 10,
"ts": 0.182
},
{
"layer": 13,
"head": 5,
"ts": 0.182
},
{
"layer": 15,
"head": 11,
"ts": 0.182
},
{
"layer": 22,
"head": 10,
"ts": 0.1801
},
{
"layer": 3,
"head": 3,
"ts": 0.1782
},
{
"layer": 18,
"head": 7,
"ts": 0.1782
},
{
"layer": 3,
"head": 2,
"ts": 0.1764
},
{
"layer": 4,
"head": 4,
"ts": 0.1745
},
{
"layer": 8,
"head": 13,
"ts": 0.1726
},
{
"layer": 7,
"head": 12,
"ts": 0.1707
},
{
"layer": 18,
"head": 15,
"ts": 0.1707
},
{
"layer": 15,
"head": 7,
"ts": 0.1689
},
{
"layer": 15,
"head": 9,
"ts": 0.1689
},
{
"layer": 23,
"head": 13,
"ts": 0.1689
},
{
"layer": 8,
"head": 0,
"ts": 0.1651
},
{
"layer": 8,
"head": 9,
"ts": 0.1595
},
{
"layer": 11,
"head": 4,
"ts": 0.1595
},
{
"layer": 0,
"head": 3,
"ts": 0.1576
},
{
"layer": 10,
"head": 12,
"ts": 0.1576
},
{
"layer": 15,
"head": 4,
"ts": 0.1576
},
{
"layer": 2,
"head": 8,
"ts": 0.1557
},
{
"layer": 15,
"head": 15,
"ts": 0.1557
},
{
"layer": 21,
"head": 2,
"ts": 0.1557
},
{
"layer": 5,
"head": 1,
"ts": 0.1538
},
{
"layer": 9,
"head": 3,
"ts": 0.1538
},
{
"layer": 4,
"head": 8,
"ts": 0.152
},
{
"layer": 13,
"head": 10,
"ts": 0.152
},
{
"layer": 22,
"head": 2,
"ts": 0.152
},
{
"layer": 1,
"head": 15,
"ts": 0.1501
},
{
"layer": 7,
"head": 11,
"ts": 0.1501
},
{
"layer": 15,
"head": 1,
"ts": 0.1501
},
{
"layer": 14,
"head": 2,
"ts": 0.1482
},
{
"layer": 18,
"head": 8,
"ts": 0.1482
},
{
"layer": 1,
"head": 3,
"ts": 0.1463
},
{
"layer": 5,
"head": 11,
"ts": 0.1463
},
{
"layer": 5,
"head": 14,
"ts": 0.1463
},
{
"layer": 8,
"head": 11,
"ts": 0.1445
},
{
"layer": 7,
"head": 14,
"ts": 0.1426
},
{
"layer": 8,
"head": 10,
"ts": 0.1407
},
{
"layer": 12,
"head": 13,
"ts": 0.1407
},
{
"layer": 3,
"head": 8,
"ts": 0.1388
},
{
"layer": 27,
"head": 1,
"ts": 0.1388
},
{
"layer": 10,
"head": 10,
"ts": 0.137
},
{
"layer": 14,
"head": 9,
"ts": 0.137
},
{
"layer": 15,
"head": 14,
"ts": 0.137
},
{
"layer": 25,
"head": 0,
"ts": 0.137
},
{
"layer": 10,
"head": 15,
"ts": 0.1351
},
{
"layer": 27,
"head": 0,
"ts": 0.1351
},
{
"layer": 27,
"head": 5,
"ts": 0.1351
},
{
"layer": 12,
"head": 5,
"ts": 0.1332
},
{
"layer": 15,
"head": 3,
"ts": 0.1332
},
{
"layer": 27,
"head": 10,
"ts": 0.1313
},
{
"layer": 27,
"head": 8,
"ts": 0.1295
},
{
"layer": 9,
"head": 14,
"ts": 0.1276
},
{
"layer": 5,
"head": 0,
"ts": 0.122
},
{
"layer": 0,
"head": 2,
"ts": 0.1182
},
{
"layer": 16,
"head": 3,
"ts": 0.1182
},
{
"layer": 4,
"head": 9,
"ts": 0.1163
},
{
"layer": 10,
"head": 8,
"ts": 0.1163
},
{
"layer": 12,
"head": 9,
"ts": 0.1163
},
{
"layer": 14,
"head": 8,
"ts": 0.1163
},
{
"layer": 6,
"head": 3,
"ts": 0.1144
},
{
"layer": 7,
"head": 15,
"ts": 0.1126
},
{
"layer": 12,
"head": 7,
"ts": 0.1126
},
{
"layer": 27,
"head": 9,
"ts": 0.1126
},
{
"layer": 3,
"head": 6,
"ts": 0.1107
},
{
"layer": 8,
"head": 12,
"ts": 0.1107
},
{
"layer": 2,
"head": 2,
"ts": 0.1088
},
{
"layer": 4,
"head": 2,
"ts": 0.1088
},
{
"layer": 5,
"head": 2,
"ts": 0.1088
},
{
"layer": 5,
"head": 7,
"ts": 0.1088
},
{
"layer": 14,
"head": 1,
"ts": 0.1088
},
{
"layer": 22,
"head": 15,
"ts": 0.1088
},
{
"layer": 23,
"head": 3,
"ts": 0.1088
},
{
"layer": 9,
"head": 11,
"ts": 0.1069
},
{
"layer": 17,
"head": 4,
"ts": 0.1069
},
{
"layer": 27,
"head": 3,
"ts": 0.1069
},
{
"layer": 5,
"head": 9,
"ts": 0.1051
},
{
"layer": 14,
"head": 3,
"ts": 0.1032
},
{
"layer": 27,
"head": 13,
"ts": 0.1032
}
],
"alignment_heads_compact": [
[
20,
3
],
[
11,
2
],
[
13,
6
],
[
11,
13
],
[
14,
14
],
[
6,
7
],
[
14,
15
],
[
6,
6
],
[
16,
14
],
[
6,
11
],
[
11,
12
],
[
13,
7
],
[
6,
13
],
[
3,
10
],
[
11,
3
],
[
6,
10
],
[
21,
9
],
[
19,
7
],
[
21,
8
],
[
2,
11
],
[
13,
0
],
[
13,
11
],
[
16,
15
],
[
4,
13
],
[
16,
10
],
[
11,
11
],
[
18,
3
],
[
18,
5
],
[
6,
12
],
[
12,
6
],
[
20,
14
],
[
23,
7
],
[
19,
5
],
[
22,
7
],
[
20,
15
],
[
22,
9
],
[
23,
14
],
[
25,
5
],
[
22,
8
],
[
24,
14
],
[
2,
10
],
[
25,
11
],
[
23,
15
],
[
25,
4
],
[
26,
8
],
[
8,
1
],
[
24,
5
],
[
24,
15
],
[
3,
11
],
[
24,
7
],
[
2,
3
],
[
23,
4
],
[
24,
13
],
[
25,
7
],
[
9,
12
],
[
11,
8
],
[
22,
12
],
[
25,
10
],
[
24,
12
],
[
24,
11
],
[
25,
12
],
[
24,
10
],
[
25,
13
],
[
9,
13
],
[
26,
2
],
[
25,
6
],
[
26,
3
],
[
21,
0
],
[
25,
14
],
[
20,
11
],
[
26,
4
],
[
26,
7
],
[
24,
6
],
[
21,
12
],
[
22,
13
],
[
21,
1
],
[
20,
7
],
[
20,
10
],
[
8,
4
],
[
26,
14
],
[
4,
12
],
[
19,
3
],
[
26,
5
],
[
13,
15
],
[
15,
10
],
[
16,
7
],
[
13,
14
],
[
19,
9
],
[
23,
1
],
[
23,
6
],
[
26,
15
],
[
23,
0
],
[
25,
9
],
[
16,
13
],
[
5,
10
],
[
16,
11
],
[
23,
10
],
[
19,
4
],
[
13,
4
],
[
26,
0
],
[
16,
12
],
[
12,
1
],
[
21,
13
],
[
19,
2
],
[
19,
13
],
[
11,
10
],
[
26,
1
],
[
24,
4
],
[
21,
15
],
[
12,
0
],
[
19,
12
],
[
11,
9
],
[
17,
13
],
[
19,
8
],
[
25,
15
],
[
12,
8
],
[
18,
10
],
[
7,
0
],
[
20,
4
],
[
21,
11
],
[
18,
2
],
[
20,
12
],
[
21,
14
],
[
10,
5
],
[
20,
6
],
[
17,
8
],
[
7,
4
],
[
23,
11
],
[
9,
8
],
[
19,
6
],
[
17,
0
],
[
5,
15
],
[
8,
7
],
[
8,
14
],
[
11,
1
],
[
10,
2
],
[
5,
5
],
[
17,
1
],
[
4,
10
],
[
13,
1
],
[
22,
4
],
[
22,
1
],
[
21,
10
],
[
11,
7
],
[
20,
8
],
[
21,
4
],
[
25,
8
],
[
3,
4
],
[
8,
2
],
[
9,
9
],
[
18,
4
],
[
14,
12
],
[
4,
15
],
[
17,
12
],
[
16,
9
],
[
11,
0
],
[
12,
3
],
[
17,
9
],
[
22,
6
],
[
6,
9
],
[
8,
5
],
[
26,
11
],
[
8,
6
],
[
12,
10
],
[
18,
11
],
[
14,
13
],
[
16,
6
],
[
26,
9
],
[
1,
8
],
[
22,
5
],
[
14,
6
],
[
12,
12
],
[
14,
0
],
[
6,
14
],
[
12,
14
],
[
21,
6
],
[
3,
13
],
[
10,
3
],
[
15,
6
],
[
20,
5
],
[
6,
0
],
[
17,
2
],
[
23,
12
],
[
20,
13
],
[
9,
7
],
[
23,
5
],
[
11,
5
],
[
3,
14
],
[
7,
5
],
[
21,
7
],
[
17,
10
],
[
20,
2
],
[
6,
1
],
[
9,
0
],
[
9,
5
],
[
15,
2
],
[
9,
2
],
[
22,
0
],
[
16,
8
],
[
9,
4
],
[
10,
9
],
[
16,
5
],
[
17,
11
],
[
2,
14
],
[
4,
11
],
[
22,
11
],
[
5,
4
],
[
14,
11
],
[
26,
6
],
[
10,
4
],
[
11,
6
],
[
14,
5
],
[
17,
3
],
[
7,
6
],
[
13,
2
],
[
6,
15
],
[
18,
13
],
[
10,
11
],
[
12,
15
],
[
6,
4
],
[
7,
13
],
[
19,
1
],
[
14,
7
],
[
10,
13
],
[
18,
6
],
[
5,
8
],
[
12,
2
],
[
2,
6
],
[
13,
3
],
[
14,
10
],
[
3,
15
],
[
22,
14
],
[
15,
5
],
[
6,
5
],
[
9,
10
],
[
13,
5
],
[
15,
11
],
[
22,
10
],
[
3,
3
],
[
18,
7
],
[
3,
2
],
[
4,
4
],
[
8,
13
],
[
7,
12
],
[
18,
15
],
[
15,
7
],
[
15,
9
],
[
23,
13
],
[
8,
0
],
[
8,
9
],
[
11,
4
],
[
0,
3
],
[
10,
12
],
[
15,
4
],
[
2,
8
],
[
15,
15
],
[
21,
2
],
[
5,
1
],
[
9,
3
],
[
4,
8
],
[
13,
10
],
[
22,
2
],
[
1,
15
],
[
7,
11
],
[
15,
1
],
[
14,
2
],
[
18,
8
],
[
1,
3
],
[
5,
11
],
[
5,
14
],
[
8,
11
],
[
7,
14
],
[
8,
10
],
[
12,
13
],
[
3,
8
],
[
27,
1
],
[
10,
10
],
[
14,
9
],
[
15,
14
],
[
25,
0
],
[
10,
15
],
[
27,
0
],
[
27,
5
],
[
12,
5
],
[
15,
3
],
[
27,
10
],
[
27,
8
],
[
9,
14
],
[
5,
0
],
[
0,
2
],
[
16,
3
],
[
4,
9
],
[
10,
8
],
[
12,
9
],
[
14,
8
],
[
6,
3
],
[
7,
15
],
[
12,
7
],
[
27,
9
],
[
3,
6
],
[
8,
12
],
[
2,
2
],
[
4,
2
],
[
5,
2
],
[
5,
7
],
[
14,
1
],
[
22,
15
],
[
23,
3
],
[
9,
11
],
[
17,
4
],
[
27,
3
],
[
5,
9
],
[
14,
3
],
[
27,
13
]
]
}
================================================
FILE: scripts/alignment_heads_qwen3_asr_1.7B.json
================================================
{
"model": "Qwen/Qwen3-ASR-1.7B",
"language": "English",
"num_layers": 28,
"num_heads": 16,
"num_kv_heads": 8,
"num_samples": 100,
"total_alignable_tokens": 1125,
"ts_threshold": 0.1,
"ts_matrix": [
[
0.10222222222222223,
0.09333333333333334,
0.10133333333333333,
0.10755555555555556,
0.056,
0.06933333333333333,
0.07644444444444444,
0.07466666666666667,
0.08533333333333333,
0.09422222222222222,
0.13155555555555556,
0.1431111111111111,
0.05333333333333334,
0.041777777777777775,
0.05422222222222222,
0.07466666666666667
],
[
0.15733333333333333,
0.15555555555555556,
0.096,
0.14044444444444446,
0.064,
0.056,
0.06933333333333333,
0.07377777777777778,
0.3502222222222222,
0.06311111111111112,
0.08533333333333333,
0.04711111111111111,
0.03111111111111111,
0.17155555555555554,
0.13155555555555556,
0.5191111111111111
],
[
0.06488888888888888,
0.056,
0.2577777777777778,
0.6417777777777778,
0.08177777777777778,
0.06844444444444445,
0.192,
0.07288888888888889,
0.3457777777777778,
0.08711111111111111,
0.6604444444444444,
0.6666666666666666,
0.08266666666666667,
0.1111111111111111,
0.36977777777777776,
0.12355555555555556
],
[
0.11822222222222223,
0.12622222222222224,
0.16444444444444445,
0.18488888888888888,
0.256,
0.088,
0.09155555555555556,
0.07555555555555556,
0.11377777777777778,
0.11733333333333333,
0.6853333333333333,
0.616,
0.12533333333333332,
0.26755555555555555,
0.20266666666666666,
0.20355555555555555
],
[
0.030222222222222223,
0.034666666666666665,
0.11644444444444445,
0.10577777777777778,
0.11911111111111111,
0.06933333333333333,
0.029333333333333333,
0.09333333333333334,
0.12266666666666666,
0.09244444444444444,
0.3831111111111111,
0.20533333333333334,
0.43555555555555553,
0.6542222222222223,
0.08266666666666667,
0.25955555555555554
],
[
0.10755555555555556,
0.10133333333333333,
0.08533333333333333,
0.07022222222222223,
0.13866666666666666,
0.22133333333333333,
0.11911111111111111,
0.12622222222222224,
0.1288888888888889,
0.12977777777777777,
0.44355555555555554,
0.12266666666666666,
0.05422222222222222,
0.04888888888888889,
0.152,
0.32266666666666666
],
[
0.25244444444444447,
0.21422222222222223,
0.08088888888888889,
0.12444444444444444,
0.17155555555555554,
0.13955555555555554,
0.7288888888888889,
0.7315555555555555,
0.03288888888888889,
0.24888888888888888,
0.7146666666666667,
0.7031111111111111,
0.6417777777777778,
0.6888888888888889,
0.18666666666666668,
0.1511111111111111
],
[
0.13422222222222221,
0.03822222222222222,
0.07022222222222223,
0.08177777777777778,
0.29155555555555557,
0.1368888888888889,
0.16444444444444445,
0.07733333333333334,
0.09244444444444444,
0.030222222222222223,
0.13155555555555556,
0.14844444444444443,
0.12444444444444444,
0.22755555555555557,
0.12622222222222224,
0.17244444444444446
],
[
0.12266666666666666,
0.6008888888888889,
0.14844444444444443,
0.06577777777777778,
0.6488888888888888,
0.3546666666666667,
0.23644444444444446,
0.296,
0.10311111111111111,
0.13155555555555556,
0.17422222222222222,
0.14666666666666667,
0.136,
0.1991111111111111,
0.3111111111111111,
0.09333333333333334
],
[
0.1902222222222222,
0.03822222222222222,
0.1608888888888889,
0.09155555555555556,
0.18844444444444444,
0.19466666666666665,
0.04533333333333334,
0.1671111111111111,
0.22844444444444445,
0.23644444444444446,
0.17333333333333334,
0.11555555555555555,
0.49422222222222223,
0.41244444444444445,
0.12977777777777777,
0.018666666666666668
],
[
0.028444444444444446,
0.04622222222222222,
0.18222222222222223,
0.25066666666666665,
0.17866666666666667,
0.32266666666666666,
0.051555555555555556,
0.07822222222222222,
0.1448888888888889,
0.152,
0.0791111111111111,
0.15733333333333333,
0.1111111111111111,
0.14844444444444443,
0.04711111111111111,
0.10044444444444445
],
[
0.18577777777777776,
0.22044444444444444,
0.7573333333333333,
0.7182222222222222,
0.11288888888888889,
0.168,
0.18044444444444444,
0.2577777777777778,
0.18933333333333333,
0.11377777777777778,
0.2871111111111111,
0.6168888888888889,
0.7093333333333334,
0.7484444444444445,
0.050666666666666665,
0.11288888888888889
],
[
0.344,
0.37155555555555553,
0.16977777777777778,
0.2551111111111111,
0.0791111111111111,
0.12,
0.5511111111111111,
0.07555555555555556,
0.31733333333333336,
0.09688888888888889,
0.23733333333333334,
0.06666666666666667,
0.17155555555555554,
0.10844444444444444,
0.21244444444444444,
0.20355555555555555
],
[
0.6124444444444445,
0.192,
0.18044444444444444,
0.1288888888888889,
0.3848888888888889,
0.136,
0.48533333333333334,
0.5022222222222222,
0.034666666666666665,
0.04888888888888889,
0.088,
0.6702222222222223,
0.025777777777777778,
0.03822222222222222,
0.5964444444444444,
0.4231111111111111
],
[
0.19377777777777777,
0.09066666666666667,
0.16355555555555557,
0.07466666666666667,
0.051555555555555556,
0.2222222222222222,
0.18666666666666668,
0.14666666666666667,
0.064,
0.07822222222222222,
0.18755555555555556,
0.23644444444444446,
0.42133333333333334,
0.21066666666666667,
0.7351111111111112,
0.7164444444444444
],
[
0.12622222222222224,
0.168,
0.1751111111111111,
0.152,
0.18488888888888888,
0.1751111111111111,
0.21866666666666668,
0.10933333333333334,
0.07555555555555556,
0.16533333333333333,
0.3111111111111111,
0.16177777777777777,
0.04088888888888889,
0.037333333333333336,
0.18488888888888888,
0.11466666666666667
],
[
0.05333333333333334,
0.041777777777777775,
0.11377777777777778,
0.15911111111111112,
0.11555555555555555,
0.13333333333333333,
0.16444444444444445,
0.4817777777777778,
0.25422222222222224,
0.264,
0.648,
0.5493333333333333,
0.2995555555555556,
0.4017777777777778,
0.7573333333333333,
0.6977777777777778
],
[
0.25866666666666666,
0.25955555555555554,
0.2328888888888889,
0.18133333333333335,
0.08444444444444445,
0.058666666666666666,
0.042666666666666665,
0.22933333333333333,
0.34044444444444444,
0.24533333333333332,
0.23822222222222222,
0.18577777777777776,
0.248,
0.4017777777777778,
0.11644444444444445,
0.112
],
[
0.07377777777777778,
0.07733333333333334,
0.37244444444444447,
0.6417777777777778,
0.27466666666666667,
0.6515555555555556,
0.18222222222222223,
0.16177777777777777,
0.11377777777777778,
0.07466666666666667,
0.37777777777777777,
0.1991111111111111,
0.042666666666666665,
0.19733333333333333,
0.08711111111111111,
0.2
],
[
0.16977777777777778,
0.17066666666666666,
0.31022222222222223,
0.544,
0.4391111111111111,
0.6391111111111111,
0.17066666666666666,
0.712,
0.4311111111111111,
0.5022222222222222,
0.07466666666666667,
0.08711111111111111,
0.3662222222222222,
0.4017777777777778,
0.04888888888888889,
0.08266666666666667
],
[
0.10044444444444445,
0.10844444444444444,
0.15911111111111112,
0.7644444444444445,
0.3448888888888889,
0.16177777777777777,
0.3635555555555556,
0.5031111111111111,
0.31733333333333336,
0.06933333333333333,
0.5022222222222222,
0.5742222222222222,
0.3297777777777778,
0.23644444444444446,
0.6551111111111111,
0.5831111111111111
],
[
0.5146666666666667,
0.5031111111111111,
0.112,
0.07111111111111111,
0.2391111111111111,
0.15555555555555556,
0.24266666666666667,
0.18844444444444444,
0.7386666666666667,
0.7617777777777778,
0.25066666666666665,
0.352,
0.5457777777777778,
0.4088888888888889,
0.3128888888888889,
0.36177777777777775
],
[
0.21155555555555555,
0.26666666666666666,
0.10488888888888889,
0.06222222222222222,
0.288,
0.25066666666666665,
0.2995555555555556,
0.6515555555555556,
0.5955555555555555,
0.6302222222222222,
0.24977777777777777,
0.2568888888888889,
0.6195555555555555,
0.5431111111111111,
0.23466666666666666,
0.08622222222222223
],
[
0.48977777777777776,
0.5102222222222222,
0.05688888888888889,
0.06311111111111112,
0.6222222222222222,
0.4142222222222222,
0.24888888888888888,
0.6462222222222223,
0.06488888888888888,
0.1608888888888889,
0.3537777777777778,
0.31822222222222224,
0.20177777777777778,
0.1448888888888889,
0.6275555555555555,
0.6044444444444445
],
[
0.036444444444444446,
0.048,
0.06222222222222222,
0.07377777777777778,
0.42933333333333334,
0.6257777777777778,
0.5306666666666666,
0.6008888888888889,
0.09066666666666667,
0.072,
0.5493333333333333,
0.5804444444444444,
0.5866666666666667,
0.5937777777777777,
0.6257777777777778,
0.6204444444444445
],
[
0.09066666666666667,
0.11733333333333333,
0.059555555555555556,
0.07022222222222223,
0.5982222222222222,
0.648,
0.5875555555555556,
0.5964444444444444,
0.352,
0.4888888888888889,
0.5715555555555556,
0.6035555555555555,
0.5875555555555556,
0.5804444444444444,
0.5688888888888889,
0.3546666666666667
],
[
0.376,
0.3217777777777778,
0.5786666666666667,
0.5466666666666666,
0.5475555555555556,
0.5155555555555555,
0.1688888888888889,
0.5528888888888889,
0.6142222222222222,
0.21511111111111111,
0.08622222222222223,
0.20533333333333334,
0.13066666666666665,
0.10222222222222223,
0.5511111111111111,
0.4951111111111111
],
[
0.08177777777777778,
0.10044444444444445,
0.08711111111111111,
0.08888888888888889,
0.08533333333333333,
0.056,
0.15466666666666667,
0.07377777777777778,
0.04888888888888889,
0.07022222222222223,
0.10222222222222223,
0.0951111111111111,
0.08088888888888889,
0.06311111111111112,
0.09688888888888889,
0.07111111111111111
]
],
"alignment_heads": [
{
"layer": 20,
"head": 3,
"ts": 0.7644
},
{
"layer": 21,
"head": 9,
"ts": 0.7618
},
{
"layer": 11,
"head": 2,
"ts": 0.7573
},
{
"layer": 16,
"head": 14,
"ts": 0.7573
},
{
"layer": 11,
"head": 13,
"ts": 0.7484
},
{
"layer": 21,
"head": 8,
"ts": 0.7387
},
{
"layer": 14,
"head": 14,
"ts": 0.7351
},
{
"layer": 6,
"head": 7,
"ts": 0.7316
},
{
"layer": 6,
"head": 6,
"ts": 0.7289
},
{
"layer": 11,
"head": 3,
"ts": 0.7182
},
{
"layer": 14,
"head": 15,
"ts": 0.7164
},
{
"layer": 6,
"head": 10,
"ts": 0.7147
},
{
"layer": 19,
"head": 7,
"ts": 0.712
},
{
"layer": 11,
"head": 12,
"ts": 0.7093
},
{
"layer": 6,
"head": 11,
"ts": 0.7031
},
{
"layer": 16,
"head": 15,
"ts": 0.6978
},
{
"layer": 6,
"head": 13,
"ts": 0.6889
},
{
"layer": 3,
"head": 10,
"ts": 0.6853
},
{
"layer": 13,
"head": 11,
"ts": 0.6702
},
{
"layer": 2,
"head": 11,
"ts": 0.6667
},
{
"layer": 2,
"head": 10,
"ts": 0.6604
},
{
"layer": 20,
"head": 14,
"ts": 0.6551
},
{
"layer": 4,
"head": 13,
"ts": 0.6542
},
{
"layer": 18,
"head": 5,
"ts": 0.6516
},
{
"layer": 22,
"head": 7,
"ts": 0.6516
},
{
"layer": 8,
"head": 4,
"ts": 0.6489
},
{
"layer": 16,
"head": 10,
"ts": 0.648
},
{
"layer": 25,
"head": 5,
"ts": 0.648
},
{
"layer": 23,
"head": 7,
"ts": 0.6462
},
{
"layer": 2,
"head": 3,
"ts": 0.6418
},
{
"layer": 6,
"head": 12,
"ts": 0.6418
},
{
"layer": 18,
"head": 3,
"ts": 0.6418
},
{
"layer": 19,
"head": 5,
"ts": 0.6391
},
{
"layer": 22,
"head": 9,
"ts": 0.6302
},
{
"layer": 23,
"head": 14,
"ts": 0.6276
},
{
"layer": 24,
"head": 5,
"ts": 0.6258
},
{
"layer": 24,
"head": 14,
"ts": 0.6258
},
{
"layer": 23,
"head": 4,
"ts": 0.6222
},
{
"layer": 24,
"head": 15,
"ts": 0.6204
},
{
"layer": 22,
"head": 12,
"ts": 0.6196
},
{
"layer": 11,
"head": 11,
"ts": 0.6169
},
{
"layer": 3,
"head": 11,
"ts": 0.616
},
{
"layer": 26,
"head": 8,
"ts": 0.6142
},
{
"layer": 13,
"head": 0,
"ts": 0.6124
},
{
"layer": 23,
"head": 15,
"ts": 0.6044
},
{
"layer": 25,
"head": 11,
"ts": 0.6036
},
{
"layer": 8,
"head": 1,
"ts": 0.6009
},
{
"layer": 24,
"head": 7,
"ts": 0.6009
},
{
"layer": 25,
"head": 4,
"ts": 0.5982
},
{
"layer": 13,
"head": 14,
"ts": 0.5964
},
{
"layer": 25,
"head": 7,
"ts": 0.5964
},
{
"layer": 22,
"head": 8,
"ts": 0.5956
},
{
"layer": 24,
"head": 13,
"ts": 0.5938
},
{
"layer": 25,
"head": 6,
"ts": 0.5876
},
{
"layer": 25,
"head": 12,
"ts": 0.5876
},
{
"layer": 24,
"head": 12,
"ts": 0.5867
},
{
"layer": 20,
"head": 15,
"ts": 0.5831
},
{
"layer": 24,
"head": 11,
"ts": 0.5804
},
{
"layer": 25,
"head": 13,
"ts": 0.5804
},
{
"layer": 26,
"head": 2,
"ts": 0.5787
},
{
"layer": 20,
"head": 11,
"ts": 0.5742
},
{
"layer": 25,
"head": 10,
"ts": 0.5716
},
{
"layer": 25,
"head": 14,
"ts": 0.5689
},
{
"layer": 26,
"head": 7,
"ts": 0.5529
},
{
"layer": 12,
"head": 6,
"ts": 0.5511
},
{
"layer": 26,
"head": 14,
"ts": 0.5511
},
{
"layer": 16,
"head": 11,
"ts": 0.5493
},
{
"layer": 24,
"head": 10,
"ts": 0.5493
},
{
"layer": 26,
"head": 4,
"ts": 0.5476
},
{
"layer": 26,
"head": 3,
"ts": 0.5467
},
{
"layer": 21,
"head": 12,
"ts": 0.5458
},
{
"layer": 19,
"head": 3,
"ts": 0.544
},
{
"layer": 22,
"head": 13,
"ts": 0.5431
},
{
"layer": 24,
"head": 6,
"ts": 0.5307
},
{
"layer": 1,
"head": 15,
"ts": 0.5191
},
{
"layer": 26,
"head": 5,
"ts": 0.5156
},
{
"layer": 21,
"head": 0,
"ts": 0.5147
},
{
"layer": 23,
"head": 1,
"ts": 0.5102
},
{
"layer": 20,
"head": 7,
"ts": 0.5031
},
{
"layer": 21,
"head": 1,
"ts": 0.5031
},
{
"layer": 13,
"head": 7,
"ts": 0.5022
},
{
"layer": 19,
"head": 9,
"ts": 0.5022
},
{
"layer": 20,
"head": 10,
"ts": 0.5022
},
{
"layer": 26,
"head": 15,
"ts": 0.4951
},
{
"layer": 9,
"head": 12,
"ts": 0.4942
},
{
"layer": 23,
"head": 0,
"ts": 0.4898
},
{
"layer": 25,
"head": 9,
"ts": 0.4889
},
{
"layer": 13,
"head": 6,
"ts": 0.4853
},
{
"layer": 16,
"head": 7,
"ts": 0.4818
},
{
"layer": 5,
"head": 10,
"ts": 0.4436
},
{
"layer": 19,
"head": 4,
"ts": 0.4391
},
{
"layer": 4,
"head": 12,
"ts": 0.4356
},
{
"layer": 19,
"head": 8,
"ts": 0.4311
},
{
"layer": 24,
"head": 4,
"ts": 0.4293
},
{
"layer": 13,
"head": 15,
"ts": 0.4231
},
{
"layer": 14,
"head": 12,
"ts": 0.4213
},
{
"layer": 23,
"head": 5,
"ts": 0.4142
},
{
"layer": 9,
"head": 13,
"ts": 0.4124
},
{
"layer": 21,
"head": 13,
"ts": 0.4089
},
{
"layer": 16,
"head": 13,
"ts": 0.4018
},
{
"layer": 17,
"head": 13,
"ts": 0.4018
},
{
"layer": 19,
"head": 13,
"ts": 0.4018
},
{
"layer": 13,
"head": 4,
"ts": 0.3849
},
{
"layer": 4,
"head": 10,
"ts": 0.3831
},
{
"layer": 18,
"head": 10,
"ts": 0.3778
},
{
"layer": 26,
"head": 0,
"ts": 0.376
},
{
"layer": 18,
"head": 2,
"ts": 0.3724
},
{
"layer": 12,
"head": 1,
"ts": 0.3716
},
{
"layer": 2,
"head": 14,
"ts": 0.3698
},
{
"layer": 19,
"head": 12,
"ts": 0.3662
},
{
"layer": 20,
"head": 6,
"ts": 0.3636
},
{
"layer": 21,
"head": 15,
"ts": 0.3618
},
{
"layer": 8,
"head": 5,
"ts": 0.3547
},
{
"layer": 25,
"head": 15,
"ts": 0.3547
},
{
"layer": 23,
"head": 10,
"ts": 0.3538
},
{
"layer": 21,
"head": 11,
"ts": 0.352
},
{
"layer": 25,
"head": 8,
"ts": 0.352
},
{
"layer": 1,
"head": 8,
"ts": 0.3502
},
{
"layer": 2,
"head": 8,
"ts": 0.3458
},
{
"layer": 20,
"head": 4,
"ts": 0.3449
},
{
"layer": 12,
"head": 0,
"ts": 0.344
},
{
"layer": 17,
"head": 8,
"ts": 0.3404
},
{
"layer": 20,
"head": 12,
"ts": 0.3298
},
{
"layer": 5,
"head": 15,
"ts": 0.3227
},
{
"layer": 10,
"head": 5,
"ts": 0.3227
},
{
"layer": 26,
"head": 1,
"ts": 0.3218
},
{
"layer": 23,
"head": 11,
"ts": 0.3182
},
{
"layer": 12,
"head": 8,
"ts": 0.3173
},
{
"layer": 20,
"head": 8,
"ts": 0.3173
},
{
"layer": 21,
"head": 14,
"ts": 0.3129
},
{
"layer": 8,
"head": 14,
"ts": 0.3111
},
{
"layer": 15,
"head": 10,
"ts": 0.3111
},
{
"layer": 19,
"head": 2,
"ts": 0.3102
},
{
"layer": 16,
"head": 12,
"ts": 0.2996
},
{
"layer": 22,
"head": 6,
"ts": 0.2996
},
{
"layer": 8,
"head": 7,
"ts": 0.296
},
{
"layer": 7,
"head": 4,
"ts": 0.2916
},
{
"layer": 22,
"head": 4,
"ts": 0.288
},
{
"layer": 11,
"head": 10,
"ts": 0.2871
},
{
"layer": 18,
"head": 4,
"ts": 0.2747
},
{
"layer": 3,
"head": 13,
"ts": 0.2676
},
{
"layer": 22,
"head": 1,
"ts": 0.2667
},
{
"layer": 16,
"head": 9,
"ts": 0.264
},
{
"layer": 4,
"head": 15,
"ts": 0.2596
},
{
"layer": 17,
"head": 1,
"ts": 0.2596
},
{
"layer": 17,
"head": 0,
"ts": 0.2587
},
{
"layer": 2,
"head": 2,
"ts": 0.2578
},
{
"layer": 11,
"head": 7,
"ts": 0.2578
},
{
"layer": 22,
"head": 11,
"ts": 0.2569
},
{
"layer": 3,
"head": 4,
"ts": 0.256
},
{
"layer": 12,
"head": 3,
"ts": 0.2551
},
{
"layer": 16,
"head": 8,
"ts": 0.2542
},
{
"layer": 6,
"head": 0,
"ts": 0.2524
},
{
"layer": 10,
"head": 3,
"ts": 0.2507
},
{
"layer": 21,
"head": 10,
"ts": 0.2507
},
{
"layer": 22,
"head": 5,
"ts": 0.2507
},
{
"layer": 22,
"head": 10,
"ts": 0.2498
},
{
"layer": 6,
"head": 9,
"ts": 0.2489
},
{
"layer": 23,
"head": 6,
"ts": 0.2489
},
{
"layer": 17,
"head": 12,
"ts": 0.248
},
{
"layer": 17,
"head": 9,
"ts": 0.2453
},
{
"layer": 21,
"head": 6,
"ts": 0.2427
},
{
"layer": 21,
"head": 4,
"ts": 0.2391
},
{
"layer": 17,
"head": 10,
"ts": 0.2382
},
{
"layer": 12,
"head": 10,
"ts": 0.2373
},
{
"layer": 8,
"head": 6,
"ts": 0.2364
},
{
"layer": 9,
"head": 9,
"ts": 0.2364
},
{
"layer": 14,
"head": 11,
"ts": 0.2364
},
{
"layer": 20,
"head": 13,
"ts": 0.2364
},
{
"layer": 22,
"head": 14,
"ts": 0.2347
},
{
"layer": 17,
"head": 2,
"ts": 0.2329
},
{
"layer": 17,
"head": 7,
"ts": 0.2293
},
{
"layer": 9,
"head": 8,
"ts": 0.2284
},
{
"layer": 7,
"head": 13,
"ts": 0.2276
},
{
"layer": 14,
"head": 5,
"ts": 0.2222
},
{
"layer": 5,
"head": 5,
"ts": 0.2213
},
{
"layer": 11,
"head": 1,
"ts": 0.2204
},
{
"layer": 15,
"head": 6,
"ts": 0.2187
},
{
"layer": 26,
"head": 9,
"ts": 0.2151
},
{
"layer": 6,
"head": 1,
"ts": 0.2142
},
{
"layer": 12,
"head": 14,
"ts": 0.2124
},
{
"layer": 22,
"head": 0,
"ts": 0.2116
},
{
"layer": 14,
"head": 13,
"ts": 0.2107
},
{
"layer": 4,
"head": 11,
"ts": 0.2053
},
{
"layer": 26,
"head": 11,
"ts": 0.2053
},
{
"layer": 3,
"head": 15,
"ts": 0.2036
},
{
"layer": 12,
"head": 15,
"ts": 0.2036
},
{
"layer": 3,
"head": 14,
"ts": 0.2027
},
{
"layer": 23,
"head": 12,
"ts": 0.2018
},
{
"layer": 18,
"head": 15,
"ts": 0.2
},
{
"layer": 8,
"head": 13,
"ts": 0.1991
},
{
"layer": 18,
"head": 11,
"ts": 0.1991
},
{
"layer": 18,
"head": 13,
"ts": 0.1973
},
{
"layer": 9,
"head": 5,
"ts": 0.1947
},
{
"layer": 14,
"head": 0,
"ts": 0.1938
},
{
"layer": 2,
"head": 6,
"ts": 0.192
},
{
"layer": 13,
"head": 1,
"ts": 0.192
},
{
"layer": 9,
"head": 0,
"ts": 0.1902
},
{
"layer": 11,
"head": 8,
"ts": 0.1893
},
{
"layer": 9,
"head": 4,
"ts": 0.1884
},
{
"layer": 21,
"head": 7,
"ts": 0.1884
},
{
"layer": 14,
"head": 10,
"ts": 0.1876
},
{
"layer": 6,
"head": 14,
"ts": 0.1867
},
{
"layer": 14,
"head": 6,
"ts": 0.1867
},
{
"layer": 11,
"head": 0,
"ts": 0.1858
},
{
"layer": 17,
"head": 11,
"ts": 0.1858
},
{
"layer": 3,
"head": 3,
"ts": 0.1849
},
{
"layer": 15,
"head": 4,
"ts": 0.1849
},
{
"layer": 15,
"head": 14,
"ts": 0.1849
},
{
"layer": 10,
"head": 2,
"ts": 0.1822
},
{
"layer": 18,
"head": 6,
"ts": 0.1822
},
{
"layer": 17,
"head": 3,
"ts": 0.1813
},
{
"layer": 11,
"head": 6,
"ts": 0.1804
},
{
"layer": 13,
"head": 2,
"ts": 0.1804
},
{
"layer": 10,
"head": 4,
"ts": 0.1787
},
{
"layer": 15,
"head": 2,
"ts": 0.1751
},
{
"layer": 15,
"head": 5,
"ts": 0.1751
},
{
"layer": 8,
"head": 10,
"ts": 0.1742
},
{
"layer": 9,
"head": 10,
"ts": 0.1733
},
{
"layer": 7,
"head": 15,
"ts": 0.1724
},
{
"layer": 1,
"head": 13,
"ts": 0.1716
},
{
"layer": 6,
"head": 4,
"ts": 0.1716
},
{
"layer": 12,
"head": 12,
"ts": 0.1716
},
{
"layer": 19,
"head": 1,
"ts": 0.1707
},
{
"layer": 19,
"head": 6,
"ts": 0.1707
},
{
"layer": 12,
"head": 2,
"ts": 0.1698
},
{
"layer": 19,
"head": 0,
"ts": 0.1698
},
{
"layer": 26,
"head": 6,
"ts": 0.1689
},
{
"layer": 11,
"head": 5,
"ts": 0.168
},
{
"layer": 15,
"head": 1,
"ts": 0.168
},
{
"layer": 9,
"head": 7,
"ts": 0.1671
},
{
"layer": 15,
"head": 9,
"ts": 0.1653
},
{
"layer": 3,
"head": 2,
"ts": 0.1644
},
{
"layer": 7,
"head": 6,
"ts": 0.1644
},
{
"layer": 16,
"head": 6,
"ts": 0.1644
},
{
"layer": 14,
"head": 2,
"ts": 0.1636
},
{
"layer": 15,
"head": 11,
"ts": 0.1618
},
{
"layer": 18,
"head": 7,
"ts": 0.1618
},
{
"layer": 20,
"head": 5,
"ts": 0.1618
},
{
"layer": 9,
"head": 2,
"ts": 0.1609
},
{
"layer": 23,
"head": 9,
"ts": 0.1609
},
{
"layer": 16,
"head": 3,
"ts": 0.1591
},
{
"layer": 20,
"head": 2,
"ts": 0.1591
},
{
"layer": 1,
"head": 0,
"ts": 0.1573
},
{
"layer": 10,
"head": 11,
"ts": 0.1573
},
{
"layer": 1,
"head": 1,
"ts": 0.1556
},
{
"layer": 21,
"head": 5,
"ts": 0.1556
},
{
"layer": 27,
"head": 6,
"ts": 0.1547
},
{
"layer": 5,
"head": 14,
"ts": 0.152
},
{
"layer": 10,
"head": 9,
"ts": 0.152
},
{
"layer": 15,
"head": 3,
"ts": 0.152
},
{
"layer": 6,
"head": 15,
"ts": 0.1511
},
{
"layer": 7,
"head": 11,
"ts": 0.1484
},
{
"layer": 8,
"head": 2,
"ts": 0.1484
},
{
"layer": 10,
"head": 13,
"ts": 0.1484
},
{
"layer": 8,
"head": 11,
"ts": 0.1467
},
{
"layer": 14,
"head": 7,
"ts": 0.1467
},
{
"layer": 10,
"head": 8,
"ts": 0.1449
},
{
"layer": 23,
"head": 13,
"ts": 0.1449
},
{
"layer": 0,
"head": 11,
"ts": 0.1431
},
{
"layer": 1,
"head": 3,
"ts": 0.1404
},
{
"layer": 6,
"head": 5,
"ts": 0.1396
},
{
"layer": 5,
"head": 4,
"ts": 0.1387
},
{
"layer": 7,
"head": 5,
"ts": 0.1369
},
{
"layer": 8,
"head": 12,
"ts": 0.136
},
{
"layer": 13,
"head": 5,
"ts": 0.136
},
{
"layer": 7,
"head": 0,
"ts": 0.1342
},
{
"layer": 16,
"head": 5,
"ts": 0.1333
},
{
"layer": 0,
"head": 10,
"ts": 0.1316
},
{
"layer": 1,
"head": 14,
"ts": 0.1316
},
{
"layer": 7,
"head": 10,
"ts": 0.1316
},
{
"layer": 8,
"head": 9,
"ts": 0.1316
},
{
"layer": 26,
"head": 12,
"ts": 0.1307
},
{
"layer": 5,
"head": 9,
"ts": 0.1298
},
{
"layer": 9,
"head": 14,
"ts": 0.1298
},
{
"layer": 5,
"head": 8,
"ts": 0.1289
},
{
"layer": 13,
"head": 3,
"ts": 0.1289
},
{
"layer": 3,
"head": 1,
"ts": 0.1262
},
{
"layer": 5,
"head": 7,
"ts": 0.1262
},
{
"layer": 7,
"head": 14,
"ts": 0.1262
},
{
"layer": 15,
"head": 0,
"ts": 0.1262
},
{
"layer": 3,
"head": 12,
"ts": 0.1253
},
{
"layer": 6,
"head": 3,
"ts": 0.1244
},
{
"layer": 7,
"head": 12,
"ts": 0.1244
},
{
"layer": 2,
"head": 15,
"ts": 0.1236
},
{
"layer": 4,
"head": 8,
"ts": 0.1227
},
{
"layer": 5,
"head": 11,
"ts": 0.1227
},
{
"layer": 8,
"head": 0,
"ts": 0.1227
},
{
"layer": 12,
"head": 5,
"ts": 0.12
},
{
"layer": 4,
"head": 4,
"ts": 0.1191
},
{
"layer": 5,
"head": 6,
"ts": 0.1191
},
{
"layer": 3,
"head": 0,
"ts": 0.1182
},
{
"layer": 3,
"head": 9,
"ts": 0.1173
},
{
"layer": 25,
"head": 1,
"ts": 0.1173
},
{
"layer": 4,
"head": 2,
"ts": 0.1164
},
{
"layer": 17,
"head": 14,
"ts": 0.1164
},
{
"layer": 9,
"head": 11,
"ts": 0.1156
},
{
"layer": 16,
"head": 4,
"ts": 0.1156
},
{
"layer": 15,
"head": 15,
"ts": 0.1147
},
{
"layer": 3,
"head": 8,
"ts": 0.1138
},
{
"layer": 11,
"head": 9,
"ts": 0.1138
},
{
"layer": 16,
"head": 2,
"ts": 0.1138
},
{
"layer": 18,
"head": 8,
"ts": 0.1138
},
{
"layer": 11,
"head": 4,
"ts": 0.1129
},
{
"layer": 11,
"head": 15,
"ts": 0.1129
},
{
"layer": 17,
"head": 15,
"ts": 0.112
},
{
"layer": 21,
"head": 2,
"ts": 0.112
},
{
"layer": 2,
"head": 13,
"ts": 0.1111
},
{
"layer": 10,
"head": 12,
"ts": 0.1111
},
{
"layer": 15,
"head": 7,
"ts": 0.1093
},
{
"layer": 12,
"head": 13,
"ts": 0.1084
},
{
"layer": 20,
"head": 1,
"ts": 0.1084
},
{
"layer": 0,
"head": 3,
"ts": 0.1076
},
{
"layer": 5,
"head": 0,
"ts": 0.1076
},
{
"layer": 4,
"head": 3,
"ts": 0.1058
},
{
"layer": 22,
"head": 2,
"ts": 0.1049
},
{
"layer": 8,
"head": 8,
"ts": 0.1031
},
{
"layer": 0,
"head": 0,
"ts": 0.1022
},
{
"layer": 26,
"head": 13,
"ts": 0.1022
},
{
"layer": 27,
"head": 10,
"ts": 0.1022
},
{
"layer": 0,
"head": 2,
"ts": 0.1013
},
{
"layer": 5,
"head": 1,
"ts": 0.1013
},
{
"layer": 10,
"head": 15,
"ts": 0.1004
},
{
"layer": 20,
"head": 0,
"ts": 0.1004
},
{
"layer": 27,
"head": 1,
"ts": 0.1004
}
],
"alignment_heads_compact": [
[
20,
3
],
[
21,
9
],
[
11,
2
],
[
16,
14
],
[
11,
13
],
[
21,
8
],
[
14,
14
],
[
6,
7
],
[
6,
6
],
[
11,
3
],
[
14,
15
],
[
6,
10
],
[
19,
7
],
[
11,
12
],
[
6,
11
],
[
16,
15
],
[
6,
13
],
[
3,
10
],
[
13,
11
],
[
2,
11
],
[
2,
10
],
[
20,
14
],
[
4,
13
],
[
18,
5
],
[
22,
7
],
[
8,
4
],
[
16,
10
],
[
25,
5
],
[
23,
7
],
[
2,
3
],
[
6,
12
],
[
18,
3
],
[
19,
5
],
[
22,
9
],
[
23,
14
],
[
24,
5
],
[
24,
14
],
[
23,
4
],
[
24,
15
],
[
22,
12
],
[
11,
11
],
[
3,
11
],
[
26,
8
],
[
13,
0
],
[
23,
15
],
[
25,
11
],
[
8,
1
],
[
24,
7
],
[
25,
4
],
[
13,
14
],
[
25,
7
],
[
22,
8
],
[
24,
13
],
[
25,
6
],
[
25,
12
],
[
24,
12
],
[
20,
15
],
[
24,
11
],
[
25,
13
],
[
26,
2
],
[
20,
11
],
[
25,
10
],
[
25,
14
],
[
26,
7
],
[
12,
6
],
[
26,
14
],
[
16,
11
],
[
24,
10
],
[
26,
4
],
[
26,
3
],
[
21,
12
],
[
19,
3
],
[
22,
13
],
[
24,
6
],
[
1,
15
],
[
26,
5
],
[
21,
0
],
[
23,
1
],
[
20,
7
],
[
21,
1
],
[
13,
7
],
[
19,
9
],
[
20,
10
],
[
26,
15
],
[
9,
12
],
[
23,
0
],
[
25,
9
],
[
13,
6
],
[
16,
7
],
[
5,
10
],
[
19,
4
],
[
4,
12
],
[
19,
8
],
[
24,
4
],
[
13,
15
],
[
14,
12
],
[
23,
5
],
[
9,
13
],
[
21,
13
],
[
16,
13
],
[
17,
13
],
[
19,
13
],
[
13,
4
],
[
4,
10
],
[
18,
10
],
[
26,
0
],
[
18,
2
],
[
12,
1
],
[
2,
14
],
[
19,
12
],
[
20,
6
],
[
21,
15
],
[
8,
5
],
[
25,
15
],
[
23,
10
],
[
21,
11
],
[
25,
8
],
[
1,
8
],
[
2,
8
],
[
20,
4
],
[
12,
0
],
[
17,
8
],
[
20,
12
],
[
5,
15
],
[
10,
5
],
[
26,
1
],
[
23,
11
],
[
12,
8
],
[
20,
8
],
[
21,
14
],
[
8,
14
],
[
15,
10
],
[
19,
2
],
[
16,
12
],
[
22,
6
],
[
8,
7
],
[
7,
4
],
[
22,
4
],
[
11,
10
],
[
18,
4
],
[
3,
13
],
[
22,
1
],
[
16,
9
],
[
4,
15
],
[
17,
1
],
[
17,
0
],
[
2,
2
],
[
11,
7
],
[
22,
11
],
[
3,
4
],
[
12,
3
],
[
16,
8
],
[
6,
0
],
[
10,
3
],
[
21,
10
],
[
22,
5
],
[
22,
10
],
[
6,
9
],
[
23,
6
],
[
17,
12
],
[
17,
9
],
[
21,
6
],
[
21,
4
],
[
17,
10
],
[
12,
10
],
[
8,
6
],
[
9,
9
],
[
14,
11
],
[
20,
13
],
[
22,
14
],
[
17,
2
],
[
17,
7
],
[
9,
8
],
[
7,
13
],
[
14,
5
],
[
5,
5
],
[
11,
1
],
[
15,
6
],
[
26,
9
],
[
6,
1
],
[
12,
14
],
[
22,
0
],
[
14,
13
],
[
4,
11
],
[
26,
11
],
[
3,
15
],
[
12,
15
],
[
3,
14
],
[
23,
12
],
[
18,
15
],
[
8,
13
],
[
18,
11
],
[
18,
13
],
[
9,
5
],
[
14,
0
],
[
2,
6
],
[
13,
1
],
[
9,
0
],
[
11,
8
],
[
9,
4
],
[
21,
7
],
[
14,
10
],
[
6,
14
],
[
14,
6
],
[
11,
0
],
[
17,
11
],
[
3,
3
],
[
15,
4
],
[
15,
14
],
[
10,
2
],
[
18,
6
],
[
17,
3
],
[
11,
6
],
[
13,
2
],
[
10,
4
],
[
15,
2
],
[
15,
5
],
[
8,
10
],
[
9,
10
],
[
7,
15
],
[
1,
13
],
[
6,
4
],
[
12,
12
],
[
19,
1
],
[
19,
6
],
[
12,
2
],
[
19,
0
],
[
26,
6
],
[
11,
5
],
[
15,
1
],
[
9,
7
],
[
15,
9
],
[
3,
2
],
[
7,
6
],
[
16,
6
],
[
14,
2
],
[
15,
11
],
[
18,
7
],
[
20,
5
],
[
9,
2
],
[
23,
9
],
[
16,
3
],
[
20,
2
],
[
1,
0
],
[
10,
11
],
[
1,
1
],
[
21,
5
],
[
27,
6
],
[
5,
14
],
[
10,
9
],
[
15,
3
],
[
6,
15
],
[
7,
11
],
[
8,
2
],
[
10,
13
],
[
8,
11
],
[
14,
7
],
[
10,
8
],
[
23,
13
],
[
0,
11
],
[
1,
3
],
[
6,
5
],
[
5,
4
],
[
7,
5
],
[
8,
12
],
[
13,
5
],
[
7,
0
],
[
16,
5
],
[
0,
10
],
[
1,
14
],
[
7,
10
],
[
8,
9
],
[
26,
12
],
[
5,
9
],
[
9,
14
],
[
5,
8
],
[
13,
3
],
[
3,
1
],
[
5,
7
],
[
7,
14
],
[
15,
0
],
[
3,
12
],
[
6,
3
],
[
7,
12
],
[
2,
15
],
[
4,
8
],
[
5,
11
],
[
8,
0
],
[
12,
5
],
[
4,
4
],
[
5,
6
],
[
3,
0
],
[
3,
9
],
[
25,
1
],
[
4,
2
],
[
17,
14
],
[
9,
11
],
[
16,
4
],
[
15,
15
],
[
3,
8
],
[
11,
9
],
[
16,
2
],
[
18,
8
],
[
11,
4
],
[
11,
15
],
[
17,
15
],
[
21,
2
],
[
2,
13
],
[
10,
12
],
[
15,
7
],
[
12,
13
],
[
20,
1
],
[
0,
3
],
[
5,
0
],
[
4,
3
],
[
22,
2
],
[
8,
8
],
[
0,
0
],
[
26,
13
],
[
27,
10
],
[
0,
2
],
[
5,
1
],
[
10,
15
],
[
20,
0
],
[
27,
1
]
]
}
================================================
FILE: scripts/alignment_heads_qwen3_asr_1.7B_v2.json
================================================
{
"model": "Qwen/Qwen3-ASR-1.7B",
"language": "English",
"num_layers": 28,
"num_heads": 16,
"num_kv_heads": 8,
"num_samples": 100,
"total_alignable_tokens": 2020,
"ts_threshold": 0.1,
"ts_matrix": [
[
0.06930693069306931,
0.08762376237623762,
0.09207920792079208,
0.10198019801980197,
0.03811881188118812,
0.06584158415841584,
0.020792079207920793,
0.055445544554455446,
0.020297029702970298,
0.061386138613861385,
0.13514851485148516,
0.13415841584158417,
0.031188118811881188,
0.024752475247524754,
0.0504950495049505,
0.03861386138613861
],
[
0.1400990099009901,
0.12623762376237624,
0.07277227722772277,
0.12227722772277227,
0.04603960396039604,
0.024257425742574258,
0.04554455445544554,
0.04801980198019802,
0.4376237623762376,
0.03712871287128713,
0.04504950495049505,
0.02920792079207921,
0.015841584158415842,
0.04801980198019802,
0.15,
0.5396039603960396
],
[
0.08514851485148515,
0.05297029702970297,
0.30594059405940593,
0.7336633663366336,
0.04356435643564356,
0.03415841584158416,
0.1707920792079208,
0.03861386138613861,
0.37475247524752475,
0.05495049504950495,
0.7242574257425742,
0.748019801980198,
0.07227722772277227,
0.06980198019801981,
0.33564356435643566,
0.04950495049504951
],
[
0.053465346534653464,
0.10396039603960396,
0.15,
0.1613861386138614,
0.26683168316831685,
0.0797029702970297,
0.06683168316831684,
0.03910891089108911,
0.10643564356435643,
0.07871287128712871,
0.7623762376237624,
0.6787128712871288,
0.1,
0.3405940594059406,
0.20643564356435642,
0.1797029702970297
],
[
0.03514851485148515,
0.03712871287128713,
0.10841584158415841,
0.08415841584158416,
0.10445544554455445,
0.05297029702970297,
0.030198019801980197,
0.08613861386138613,
0.11683168316831684,
0.07475247524752475,
0.41237623762376235,
0.21386138613861386,
0.4915841584158416,
0.7183168316831683,
0.07821782178217822,
0.2876237623762376
],
[
0.11584158415841585,
0.11386138613861387,
0.05297029702970297,
0.04504950495049505,
0.12376237623762376,
0.2698019801980198,
0.11584158415841585,
0.12871287128712872,
0.1311881188118812,
0.12079207920792079,
0.48366336633663365,
0.11534653465346535,
0.04356435643564356,
0.03415841584158416,
0.1297029702970297,
0.34405940594059403
],
[
0.2693069306930693,
0.22772277227722773,
0.05148514851485148,
0.11386138613861387,
0.19752475247524753,
0.14257425742574256,
0.7980198019801981,
0.7945544554455446,
0.019306930693069307,
0.2524752475247525,
0.7801980198019802,
0.7579207920792079,
0.7188118811881188,
0.755940594059406,
0.18465346534653465,
0.14504950495049504
],
[
0.08762376237623762,
0.03217821782178218,
0.06435643564356436,
0.07376237623762376,
0.33861386138613864,
0.17227722772277226,
0.18762376237623762,
0.05297029702970297,
0.06584158415841584,
0.030693069306930693,
0.11485148514851486,
0.13514851485148516,
0.14356435643564355,
0.23613861386138613,
0.14504950495049504,
0.09356435643564356
],
[
0.11287128712871287,
0.6831683168316832,
0.11485148514851486,
0.053465346534653464,
0.6737623762376238,
0.3811881188118812,
0.2693069306930693,
0.31633663366336634,
0.060396039603960394,
0.09554455445544555,
0.19603960396039605,
0.16435643564356436,
0.09702970297029703,
0.20396039603960395,
0.3193069306930693,
0.09900990099009901
],
[
0.2,
0.04455445544554455,
0.17425742574257425,
0.0702970297029703,
0.19752475247524753,
0.20445544554455444,
0.026732673267326732,
0.18267326732673267,
0.25594059405940595,
0.250990099009901,
0.17722772277227722,
0.08613861386138613,
0.5618811881188119,
0.44504950495049506,
0.0594059405940594,
0.008415841584158416
],
[
0.031188118811881188,
0.02128712871287129,
0.2193069306930693,
0.2905940594059406,
0.1915841584158416,
0.3608910891089109,
0.019306930693069307,
0.032673267326732675,
0.1311881188118812,
0.15495049504950495,
0.08168316831683169,
0.1702970297029703,
0.10297029702970296,
0.1405940594059406,
0.04108910891089109,
0.08514851485148515
],
[
0.20495049504950494,
0.25792079207920793,
0.8356435643564356,
0.7930693069306931,
0.1301980198019802,
0.19603960396039605,
0.1910891089108911,
0.29158415841584157,
0.21188118811881188,
0.09851485148514852,
0.33960396039603963,
0.6851485148514852,
0.801980198019802,
0.8272277227722772,
0.04257425742574258,
0.09653465346534654
],
[
0.42277227722772276,
0.43316831683168316,
0.17524752475247524,
0.27574257425742577,
0.07821782178217822,
0.1405940594059406,
0.6059405940594059,
0.08316831683168317,
0.38811881188118813,
0.12079207920792079,
0.2613861386138614,
0.0297029702970297,
0.1787128712871287,
0.13217821782178218,
0.24257425742574257,
0.20594059405940593
],
[
0.7118811881188118,
0.22722772277227724,
0.2306930693069307,
0.17376237623762375,
0.45742574257425744,
0.13910891089108912,
0.5450495049504951,
0.5905940594059406,
0.034653465346534656,
0.05841584158415842,
0.1193069306930693,
0.7569306930693069,
0.020297029702970298,
0.02821782178217822,
0.6861386138613862,
0.5564356435643565
],
[
0.31584158415841584,
0.10594059405940594,
0.19851485148514852,
0.09108910891089109,
0.031188118811881188,
0.25,
0.22326732673267327,
0.16534653465346535,
0.05693069306930693,
0.0797029702970297,
0.24207920792079207,
0.27623762376237626,
0.4910891089108911,
0.25742574257425743,
0.804950495049505,
0.8163366336633663
],
[
0.14257425742574256,
0.2316831683168317,
0.22821782178217823,
0.13564356435643565,
0.19752475247524753,
0.2202970297029703,
0.2400990099009901,
0.1311881188118812,
0.024752475247524754,
0.16980198019801981,
0.39752475247524754,
0.12623762376237624,
0.0400990099009901,
0.031683168316831684,
0.17574257425742573,
0.13663366336633664
],
[
0.0400990099009901,
0.04603960396039604,
0.10297029702970296,
0.1792079207920792,
0.12821782178217822,
0.11732673267326732,
0.21732673267326733,
0.5603960396039604,
0.2717821782178218,
0.3212871287128713,
0.7108910891089109,
0.6034653465346534,
0.4024752475247525,
0.5227722772277228,
0.8138613861386138,
0.7400990099009901
],
[
0.346039603960396,
0.35,
0.2717821782178218,
0.23465346534653464,
0.07623762376237624,
0.03762376237623762,
0.03663366336633663,
0.10594059405940594,
0.4212871287128713,
0.3123762376237624,
0.30495049504950494,
0.2376237623762376,
0.30495049504950494,
0.45,
0.13366336633663367,
0.09603960396039604
],
[
0.040594059405940595,
0.04504950495049505,
0.45742574257425744,
0.695049504950495,
0.31287128712871287,
0.7267326732673267,
0.22623762376237624,
0.1806930693069307,
0.10792079207920792,
0.08168316831683169,
0.4321782178217822,
0.2376237623762376,
0.04207920792079208,
0.2584158415841584,
0.0896039603960396,
0.2396039603960396
],
[
0.16485148514851486,
0.22772277227722773,
0.39752475247524754,
0.6272277227722772,
0.49306930693069306,
0.7024752475247524,
0.20396039603960395,
0.7663366336633664,
0.4871287128712871,
0.5792079207920792,
0.062376237623762376,
0.08118811881188119,
0.43613861386138614,
0.4524752475247525,
0.020297029702970298,
0.03712871287128713
],
[
0.07574257425742574,
0.10247524752475247,
0.17524752475247524,
0.8257425742574257,
0.43316831683168316,
0.1504950495049505,
0.4495049504950495,
0.5752475247524752,
0.3806930693069307,
0.0504950495049505,
0.553960396039604,
0.650990099009901,
0.3801980198019802,
0.1915841584158416,
0.699009900990099,
0.6415841584158416
],
[
0.6039603960396039,
0.5702970297029702,
0.11534653465346535,
0.06435643564356436,
0.3014851485148515,
0.10445544554455445,
0.24356435643564356,
0.1618811881188119,
0.7831683168316832,
0.8014851485148515,
0.2851485148514851,
0.4153465346534653,
0.593069306930693,
0.47128712871287126,
0.39455445544554457,
0.4420792079207921
],
[
0.21633663366336633,
0.3188118811881188,
0.10148514851485149,
0.04356435643564356,
0.35148514851485146,
0.2727722772277228,
0.3103960396039604,
0.7054455445544554,
0.6391089108910891,
0.6767326732673268,
0.27673267326732676,
0.2965346534653465,
0.6638613861386139,
0.5861386138613861,
0.25693069306930694,
0.09504950495049505
],
[
0.5618811881188119,
0.5797029702970297,
0.053465346534653464,
0.06831683168316832,
0.6648514851485149,
0.26683168316831685,
0.3183168316831683,
0.6861386138613862,
0.0504950495049505,
0.1292079207920792,
0.4089108910891089,
0.3410891089108911,
0.2376237623762376,
0.1297029702970297,
0.6871287128712872,
0.6801980198019802
],
[
0.0400990099009901,
0.028712871287128714,
0.05841584158415842,
0.09504950495049505,
0.349009900990099,
0.6831683168316832,
0.6118811881188119,
0.6712871287128713,
0.06534653465346535,
0.05495049504950495,
0.6074257425742574,
0.6435643564356436,
0.651980198019802,
0.6544554455445545,
0.6821782178217822,
0.6737623762376238
],
[
0.10693069306930693,
0.08217821782178218,
0.03217821782178218,
0.05742574257425743,
0.6292079207920792,
0.697029702970297,
0.6485148514851485,
0.656930693069307,
0.3227722772277228,
0.5524752475247525,
0.6331683168316832,
0.6633663366336634,
0.6485148514851485,
0.6460396039603961,
0.6341584158415842,
0.32772277227722774
],
[
0.4623762376237624,
0.3207920792079208,
0.6514851485148515,
0.6306930693069307,
0.6292079207920792,
0.5876237623762376,
0.16534653465346535,
0.5935643564356435,
0.6673267326732674,
0.25594059405940595,
0.027722772277227723,
0.14603960396039603,
0.053465346534653464,
0.05099009900990099,
0.6277227722772277,
0.5801980198019802
],
[
0.09851485148514852,
0.1004950495049505,
0.09207920792079208,
0.09702970297029703,
0.08762376237623762,
0.06633663366336634,
0.16287128712871288,
0.10297029702970296,
0.033663366336633666,
0.07425742574257425,
0.10742574257425742,
0.10792079207920792,
0.048514851485148516,
0.07524752475247524,
0.10742574257425742,
0.09158415841584158
]
],
"alignment_heads": [
{
"layer": 11,
"head": 2,
"ts": 0.8356
},
{
"layer": 11,
"head": 13,
"ts": 0.8272
},
{
"layer": 20,
"head": 3,
"ts": 0.8257
},
{
"layer": 14,
"head": 15,
"ts": 0.8163
},
{
"layer": 16,
"head": 14,
"ts": 0.8139
},
{
"layer": 14,
"head": 14,
"ts": 0.805
},
{
"layer": 11,
"head": 12,
"ts": 0.802
},
{
"layer": 21,
"head": 9,
"ts": 0.8015
},
{
"layer": 6,
"head": 6,
"ts": 0.798
},
{
"layer": 6,
"head": 7,
"ts": 0.7946
},
{
"layer": 11,
"head": 3,
"ts": 0.7931
},
{
"layer": 21,
"head": 8,
"ts": 0.7832
},
{
"layer": 6,
"head": 10,
"ts": 0.7802
},
{
"layer": 19,
"head": 7,
"ts": 0.7663
},
{
"layer": 3,
"head": 10,
"ts": 0.7624
},
{
"layer": 6,
"head": 11,
"ts": 0.7579
},
{
"layer": 13,
"head": 11,
"ts": 0.7569
},
{
"layer": 6,
"head": 13,
"ts": 0.7559
},
{
"layer": 2,
"head": 11,
"ts": 0.748
},
{
"layer": 16,
"head": 15,
"ts": 0.7401
},
{
"layer": 2,
"head": 3,
"ts": 0.7337
},
{
"layer": 18,
"head": 5,
"ts": 0.7267
},
{
"layer": 2,
"head": 10,
"ts": 0.7243
},
{
"layer": 6,
"head": 12,
"ts": 0.7188
},
{
"layer": 4,
"head": 13,
"ts": 0.7183
},
{
"layer": 13,
"head": 0,
"ts": 0.7119
},
{
"layer": 16,
"head": 10,
"ts": 0.7109
},
{
"layer": 22,
"head": 7,
"ts": 0.7054
},
{
"layer": 19,
"head": 5,
"ts": 0.7025
},
{
"layer": 20,
"head": 14,
"ts": 0.699
},
{
"layer": 25,
"head": 5,
"ts": 0.697
},
{
"layer": 18,
"head": 3,
"ts": 0.695
},
{
"layer": 23,
"head": 14,
"ts": 0.6871
},
{
"layer": 13,
"head": 14,
"ts": 0.6861
},
{
"layer": 23,
"head": 7,
"ts": 0.6861
},
{
"layer": 11,
"head": 11,
"ts": 0.6851
},
{
"layer": 8,
"head": 1,
"ts": 0.6832
},
{
"layer": 24,
"head": 5,
"ts": 0.6832
},
{
"layer": 24,
"head": 14,
"ts": 0.6822
},
{
"layer": 23,
"head": 15,
"ts": 0.6802
},
{
"layer": 3,
"head": 11,
"ts": 0.6787
},
{
"layer": 22,
"head": 9,
"ts": 0.6767
},
{
"layer": 8,
"head": 4,
"ts": 0.6738
},
{
"layer": 24,
"head": 15,
"ts": 0.6738
},
{
"layer": 24,
"head": 7,
"ts": 0.6713
},
{
"layer": 26,
"head": 8,
"ts": 0.6673
},
{
"layer": 23,
"head": 4,
"ts": 0.6649
},
{
"layer": 22,
"head": 12,
"ts": 0.6639
},
{
"layer": 25,
"head": 11,
"ts": 0.6634
},
{
"layer": 25,
"head": 7,
"ts": 0.6569
},
{
"layer": 24,
"head": 13,
"ts": 0.6545
},
{
"layer": 24,
"head": 12,
"ts": 0.652
},
{
"layer": 26,
"head": 2,
"ts": 0.6515
},
{
"layer": 20,
"head": 11,
"ts": 0.651
},
{
"layer": 25,
"head": 6,
"ts": 0.6485
},
{
"layer": 25,
"head": 12,
"ts": 0.6485
},
{
"layer": 25,
"head": 13,
"ts": 0.646
},
{
"layer": 24,
"head": 11,
"ts": 0.6436
},
{
"layer": 20,
"head": 15,
"ts": 0.6416
},
{
"layer": 22,
"head": 8,
"ts": 0.6391
},
{
"layer": 25,
"head": 14,
"ts": 0.6342
},
{
"layer": 25,
"head": 10,
"ts": 0.6332
},
{
"layer": 26,
"head": 3,
"ts": 0.6307
},
{
"layer": 25,
"head": 4,
"ts": 0.6292
},
{
"layer": 26,
"head": 4,
"ts": 0.6292
},
{
"layer": 26,
"head": 14,
"ts": 0.6277
},
{
"layer": 19,
"head": 3,
"ts": 0.6272
},
{
"layer": 24,
"head": 6,
"ts": 0.6119
},
{
"layer": 24,
"head": 10,
"ts": 0.6074
},
{
"layer": 12,
"head": 6,
"ts": 0.6059
},
{
"layer": 21,
"head": 0,
"ts": 0.604
},
{
"layer": 16,
"head": 11,
"ts": 0.6035
},
{
"layer": 26,
"head": 7,
"ts": 0.5936
},
{
"layer": 21,
"head": 12,
"ts": 0.5931
},
{
"layer": 13,
"head": 7,
"ts": 0.5906
},
{
"layer": 26,
"head": 5,
"ts": 0.5876
},
{
"layer": 22,
"head": 13,
"ts": 0.5861
},
{
"layer": 26,
"head": 15,
"ts": 0.5802
},
{
"layer": 23,
"head": 1,
"ts": 0.5797
},
{
"layer": 19,
"head": 9,
"ts": 0.5792
},
{
"layer": 20,
"head": 7,
"ts": 0.5752
},
{
"layer": 21,
"head": 1,
"ts": 0.5703
},
{
"layer": 9,
"head": 12,
"ts": 0.5619
},
{
"layer": 23,
"head": 0,
"ts": 0.5619
},
{
"layer": 16,
"head": 7,
"ts": 0.5604
},
{
"layer": 13,
"head": 15,
"ts": 0.5564
},
{
"layer": 20,
"head": 10,
"ts": 0.554
},
{
"layer": 25,
"head": 9,
"ts": 0.5525
},
{
"layer": 13,
"head": 6,
"ts": 0.545
},
{
"layer": 1,
"head": 15,
"ts": 0.5396
},
{
"layer": 16,
"head": 13,
"ts": 0.5228
},
{
"layer": 19,
"head": 4,
"ts": 0.4931
},
{
"layer": 4,
"head": 12,
"ts": 0.4916
},
{
"layer": 14,
"head": 12,
"ts": 0.4911
},
{
"layer": 19,
"head": 8,
"ts": 0.4871
},
{
"layer": 5,
"head": 10,
"ts": 0.4837
},
{
"layer": 21,
"head": 13,
"ts": 0.4713
},
{
"layer": 26,
"head": 0,
"ts": 0.4624
},
{
"layer": 13,
"head": 4,
"ts": 0.4574
},
{
"layer": 18,
"head": 2,
"ts": 0.4574
},
{
"layer": 19,
"head": 13,
"ts": 0.4525
},
{
"layer": 17,
"head": 13,
"ts": 0.45
},
{
"layer": 20,
"head": 6,
"ts": 0.4495
},
{
"layer": 9,
"head": 13,
"ts": 0.445
},
{
"layer": 21,
"head": 15,
"ts": 0.4421
},
{
"layer": 1,
"head": 8,
"ts": 0.4376
},
{
"layer": 19,
"head": 12,
"ts": 0.4361
},
{
"layer": 12,
"head": 1,
"ts": 0.4332
},
{
"layer": 20,
"head": 4,
"ts": 0.4332
},
{
"layer": 18,
"head": 10,
"ts": 0.4322
},
{
"layer": 12,
"head": 0,
"ts": 0.4228
},
{
"layer": 17,
"head": 8,
"ts": 0.4213
},
{
"layer": 21,
"head": 11,
"ts": 0.4153
},
{
"layer": 4,
"head": 10,
"ts": 0.4124
},
{
"layer": 23,
"head": 10,
"ts": 0.4089
},
{
"layer": 16,
"head": 12,
"ts": 0.4025
},
{
"layer": 15,
"head": 10,
"ts": 0.3975
},
{
"layer": 19,
"head": 2,
"ts": 0.3975
},
{
"layer": 21,
"head": 14,
"ts": 0.3946
},
{
"layer": 12,
"head": 8,
"ts": 0.3881
},
{
"layer": 8,
"head": 5,
"ts": 0.3812
},
{
"layer": 20,
"head": 8,
"ts": 0.3807
},
{
"layer": 20,
"head": 12,
"ts": 0.3802
},
{
"layer": 2,
"head": 8,
"ts": 0.3748
},
{
"layer": 10,
"head": 5,
"ts": 0.3609
},
{
"layer": 22,
"head": 4,
"ts": 0.3515
},
{
"layer": 17,
"head": 1,
"ts": 0.35
},
{
"layer": 24,
"head": 4,
"ts": 0.349
},
{
"layer": 17,
"head": 0,
"ts": 0.346
},
{
"layer": 5,
"head": 15,
"ts": 0.3441
},
{
"layer": 23,
"head": 11,
"ts": 0.3411
},
{
"layer": 3,
"head": 13,
"ts": 0.3406
},
{
"layer": 11,
"head": 10,
"ts": 0.3396
},
{
"layer": 7,
"head": 4,
"ts": 0.3386
},
{
"layer": 2,
"head": 14,
"ts": 0.3356
},
{
"layer": 25,
"head": 15,
"ts": 0.3277
},
{
"layer": 25,
"head": 8,
"ts": 0.3228
},
{
"layer": 16,
"head": 9,
"ts": 0.3213
},
{
"layer": 26,
"head": 1,
"ts": 0.3208
},
{
"layer": 8,
"head": 14,
"ts": 0.3193
},
{
"layer": 22,
"head": 1,
"ts": 0.3188
},
{
"layer": 23,
"head": 6,
"ts": 0.3183
},
{
"layer": 8,
"head": 7,
"ts": 0.3163
},
{
"layer": 14,
"head": 0,
"ts": 0.3158
},
{
"layer": 18,
"head": 4,
"ts": 0.3129
},
{
"layer": 17,
"head": 9,
"ts": 0.3124
},
{
"layer": 22,
"head": 6,
"ts": 0.3104
},
{
"layer": 2,
"head": 2,
"ts": 0.3059
},
{
"layer": 17,
"head": 10,
"ts": 0.305
},
{
"layer": 17,
"head": 12,
"ts": 0.305
},
{
"layer": 21,
"head": 4,
"ts": 0.3015
},
{
"layer": 22,
"head": 11,
"ts": 0.2965
},
{
"layer": 11,
"head": 7,
"ts": 0.2916
},
{
"layer": 10,
"head": 3,
"ts": 0.2906
},
{
"layer": 4,
"head": 15,
"ts": 0.2876
},
{
"layer": 21,
"head": 10,
"ts": 0.2851
},
{
"layer": 22,
"head": 10,
"ts": 0.2767
},
{
"layer": 14,
"head": 11,
"ts": 0.2762
},
{
"layer": 12,
"head": 3,
"ts": 0.2757
},
{
"layer": 22,
"head": 5,
"ts": 0.2728
},
{
"layer": 16,
"head": 8,
"ts": 0.2718
},
{
"layer": 17,
"head": 2,
"ts": 0.2718
},
{
"layer": 5,
"head": 5,
"ts": 0.2698
},
{
"layer": 6,
"head": 0,
"ts": 0.2693
},
{
"layer": 8,
"head": 6,
"ts": 0.2693
},
{
"layer": 3,
"head": 4,
"ts": 0.2668
},
{
"layer": 23,
"head": 5,
"ts": 0.2668
},
{
"layer": 12,
"head": 10,
"ts": 0.2614
},
{
"layer": 18,
"head": 13,
"ts": 0.2584
},
{
"layer": 11,
"head": 1,
"ts": 0.2579
},
{
"layer": 14,
"head": 13,
"ts": 0.2574
},
{
"layer": 22,
"head": 14,
"ts": 0.2569
},
{
"layer": 9,
"head": 8,
"ts": 0.2559
},
{
"layer": 26,
"head": 9,
"ts": 0.2559
},
{
"layer": 6,
"head": 9,
"ts": 0.2525
},
{
"layer": 9,
"head": 9,
"ts": 0.251
},
{
"layer": 14,
"head": 5,
"ts": 0.25
},
{
"layer": 21,
"head": 6,
"ts": 0.2436
},
{
"layer": 12,
"head": 14,
"ts": 0.2426
},
{
"layer": 14,
"head": 10,
"ts": 0.2421
},
{
"layer": 15,
"head": 6,
"ts": 0.2401
},
{
"layer": 18,
"head": 15,
"ts": 0.2396
},
{
"layer": 17,
"head": 11,
"ts": 0.2376
},
{
"layer": 18,
"head": 11,
"ts": 0.2376
},
{
"layer": 23,
"head": 12,
"ts": 0.2376
},
{
"layer": 7,
"head": 13,
"ts": 0.2361
},
{
"layer": 17,
"head": 3,
"ts": 0.2347
},
{
"layer": 15,
"head": 1,
"ts": 0.2317
},
{
"layer": 13,
"head": 2,
"ts": 0.2307
},
{
"layer": 15,
"head": 2,
"ts": 0.2282
},
{
"layer": 6,
"head": 1,
"ts": 0.2277
},
{
"layer": 19,
"head": 1,
"ts": 0.2277
},
{
"layer": 13,
"head": 1,
"ts": 0.2272
},
{
"layer": 18,
"head": 6,
"ts": 0.2262
},
{
"layer": 14,
"head": 6,
"ts": 0.2233
},
{
"layer": 15,
"head": 5,
"ts": 0.2203
},
{
"layer": 10,
"head": 2,
"ts": 0.2193
},
{
"layer": 16,
"head": 6,
"ts": 0.2173
},
{
"layer": 22,
"head": 0,
"ts": 0.2163
},
{
"layer": 4,
"head": 11,
"ts": 0.2139
},
{
"layer": 11,
"head": 8,
"ts": 0.2119
},
{
"layer": 3,
"head": 14,
"ts": 0.2064
},
{
"layer": 12,
"head": 15,
"ts": 0.2059
},
{
"layer": 11,
"head": 0,
"ts": 0.205
},
{
"layer": 9,
"head": 5,
"ts": 0.2045
},
{
"layer": 8,
"head": 13,
"ts": 0.204
},
{
"layer": 19,
"head": 6,
"ts": 0.204
},
{
"layer": 9,
"head": 0,
"ts": 0.2
},
{
"layer": 14,
"head": 2,
"ts": 0.1985
},
{
"layer": 6,
"head": 4,
"ts": 0.1975
},
{
"layer": 9,
"head": 4,
"ts": 0.1975
},
{
"layer": 15,
"head": 4,
"ts": 0.1975
},
{
"layer": 8,
"head": 10,
"ts": 0.196
},
{
"layer": 11,
"head": 5,
"ts": 0.196
},
{
"layer": 10,
"head": 4,
"ts": 0.1916
},
{
"layer": 20,
"head": 13,
"ts": 0.1916
},
{
"layer": 11,
"head": 6,
"ts": 0.1911
},
{
"layer": 7,
"head": 6,
"ts": 0.1876
},
{
"layer": 6,
"head": 14,
"ts": 0.1847
},
{
"layer": 9,
"head": 7,
"ts": 0.1827
},
{
"layer": 18,
"head": 7,
"ts": 0.1807
},
{
"layer": 3,
"head": 15,
"ts": 0.1797
},
{
"layer": 16,
"head": 3,
"ts": 0.1792
},
{
"layer": 12,
"head": 12,
"ts": 0.1787
},
{
"layer": 9,
"head": 10,
"ts": 0.1772
},
{
"layer": 15,
"head": 14,
"ts": 0.1757
},
{
"layer": 12,
"head": 2,
"ts": 0.1752
},
{
"layer": 20,
"head": 2,
"ts": 0.1752
},
{
"layer": 9,
"head": 2,
"ts": 0.1743
},
{
"layer": 13,
"head": 3,
"ts": 0.1738
},
{
"layer": 7,
"head": 5,
"ts": 0.1723
},
{
"layer": 2,
"head": 6,
"ts": 0.1708
},
{
"layer": 10,
"head": 11,
"ts": 0.1703
},
{
"layer": 15,
"head": 9,
"ts": 0.1698
},
{
"layer": 14,
"head": 7,
"ts": 0.1653
},
{
"layer": 26,
"head": 6,
"ts": 0.1653
},
{
"layer": 19,
"head": 0,
"ts": 0.1649
},
{
"layer": 8,
"head": 11,
"ts": 0.1644
},
{
"layer": 27,
"head": 6,
"ts": 0.1629
},
{
"layer": 21,
"head": 7,
"ts": 0.1619
},
{
"layer": 3,
"head": 3,
"ts": 0.1614
},
{
"layer": 10,
"head": 9,
"ts": 0.155
},
{
"layer": 20,
"head": 5,
"ts": 0.1505
},
{
"layer": 1,
"head": 14,
"ts": 0.15
},
{
"layer": 3,
"head": 2,
"ts": 0.15
},
{
"layer": 26,
"head": 11,
"ts": 0.146
},
{
"layer": 6,
"head": 15,
"ts": 0.145
},
{
"layer": 7,
"head": 14,
"ts": 0.145
},
{
"layer": 7,
"head": 12,
"ts": 0.1436
},
{
"layer": 6,
"head": 5,
"ts": 0.1426
},
{
"layer": 15,
"head": 0,
"ts": 0.1426
},
{
"layer": 10,
"head": 13,
"ts": 0.1406
},
{
"layer": 12,
"head": 5,
"ts": 0.1406
},
{
"layer": 1,
"head": 0,
"ts": 0.1401
},
{
"layer": 13,
"head": 5,
"ts": 0.1391
},
{
"layer": 15,
"head": 15,
"ts": 0.1366
},
{
"layer": 15,
"head": 3,
"ts": 0.1356
},
{
"layer": 0,
"head": 10,
"ts": 0.1351
},
{
"layer": 7,
"head": 11,
"ts": 0.1351
},
{
"layer": 0,
"head": 11,
"ts": 0.1342
},
{
"layer": 17,
"head": 14,
"ts": 0.1337
},
{
"layer": 12,
"head": 13,
"ts": 0.1322
},
{
"layer": 5,
"head": 8,
"ts": 0.1312
},
{
"layer": 10,
"head": 8,
"ts": 0.1312
},
{
"layer": 15,
"head": 7,
"ts": 0.1312
},
{
"layer": 11,
"head": 4,
"ts": 0.1302
},
{
"layer": 5,
"head": 14,
"ts": 0.1297
},
{
"layer": 23,
"head": 13,
"ts": 0.1297
},
{
"layer": 23,
"head": 9,
"ts": 0.1292
},
{
"layer": 5,
"head": 7,
"ts": 0.1287
},
{
"layer": 16,
"head": 4,
"ts": 0.1282
},
{
"layer": 1,
"head": 1,
"ts": 0.1262
},
{
"layer": 15,
"head": 11,
"ts": 0.1262
},
{
"layer": 5,
"head": 4,
"ts": 0.1238
},
{
"layer": 1,
"head": 3,
"ts": 0.1223
},
{
"layer": 5,
"head": 9,
"ts": 0.1208
},
{
"layer": 12,
"head": 9,
"ts": 0.1208
},
{
"layer": 13,
"head": 10,
"ts": 0.1193
},
{
"layer": 16,
"head": 5,
"ts": 0.1173
},
{
"layer": 4,
"head": 8,
"ts": 0.1168
},
{
"layer": 5,
"head": 0,
"ts": 0.1158
},
{
"layer": 5,
"head": 6,
"ts": 0.1158
},
{
"layer": 5,
"head": 11,
"ts": 0.1153
},
{
"layer": 21,
"head": 2,
"ts": 0.1153
},
{
"layer": 7,
"head": 10,
"ts": 0.1149
},
{
"layer": 8,
"head": 2,
"ts": 0.1149
},
{
"layer": 5,
"head": 1,
"ts": 0.1139
},
{
"layer": 6,
"head": 3,
"ts": 0.1139
},
{
"layer": 8,
"head": 0,
"ts": 0.1129
},
{
"layer": 4,
"head": 2,
"ts": 0.1084
},
{
"layer": 18,
"head": 8,
"ts": 0.1079
},
{
"layer": 27,
"head": 11,
"ts": 0.1079
},
{
"layer": 27,
"head": 10,
"ts": 0.1074
},
{
"layer": 27,
"head": 14,
"ts": 0.1074
},
{
"layer": 25,
"head": 0,
"ts": 0.1069
},
{
"layer": 3,
"head": 8,
"ts": 0.1064
},
{
"layer": 14,
"head": 1,
"ts": 0.1059
},
{
"layer": 17,
"head": 7,
"ts": 0.1059
},
{
"layer": 4,
"head": 4,
"ts": 0.1045
},
{
"layer": 21,
"head": 5,
"ts": 0.1045
},
{
"layer": 3,
"head": 1,
"ts": 0.104
},
{
"layer": 10,
"head": 12,
"ts": 0.103
},
{
"layer": 16,
"head": 2,
"ts": 0.103
},
{
"layer": 27,
"head": 7,
"ts": 0.103
},
{
"layer": 20,
"head": 1,
"ts": 0.1025
},
{
"layer": 0,
"head": 3,
"ts": 0.102
},
{
"layer": 22,
"head": 2,
"ts": 0.1015
},
{
"layer": 27,
"head": 1,
"ts": 0.1005
}
],
"alignment_heads_compact": [
[
11,
2
],
[
11,
13
],
[
20,
3
],
[
14,
15
],
[
16,
14
],
[
14,
14
],
[
11,
12
],
[
21,
9
],
[
6,
6
],
[
6,
7
],
[
11,
3
],
[
21,
8
],
[
6,
10
],
[
19,
7
],
[
3,
10
],
[
6,
11
],
[
13,
11
],
[
6,
13
],
[
2,
11
],
[
16,
15
],
[
2,
3
],
[
18,
5
],
[
2,
10
],
[
6,
12
],
[
4,
13
],
[
13,
0
],
[
16,
10
],
[
22,
7
],
[
19,
5
],
[
20,
14
],
[
25,
5
],
[
18,
3
],
[
23,
14
],
[
13,
14
],
[
23,
7
],
[
11,
11
],
[
8,
1
],
[
24,
5
],
[
24,
14
],
[
23,
15
],
[
3,
11
],
[
22,
9
],
[
8,
4
],
[
24,
15
],
[
24,
7
],
[
26,
8
],
[
23,
4
],
[
22,
12
],
[
25,
11
],
[
25,
7
],
[
24,
13
],
[
24,
12
],
[
26,
2
],
[
20,
11
],
[
25,
6
],
[
25,
12
],
[
25,
13
],
[
24,
11
],
[
20,
15
],
[
22,
8
],
[
25,
14
],
[
25,
10
],
[
26,
3
],
[
25,
4
],
[
26,
4
],
[
26,
14
],
[
19,
3
],
[
24,
6
],
[
24,
10
],
[
12,
6
],
[
21,
0
],
[
16,
11
],
[
26,
7
],
[
21,
12
],
[
13,
7
],
[
26,
5
],
[
22,
13
],
[
26,
15
],
[
23,
1
],
[
19,
9
],
[
20,
7
],
[
21,
1
],
[
9,
12
],
[
23,
0
],
[
16,
7
],
[
13,
15
],
[
20,
10
],
[
25,
9
],
[
13,
6
],
[
1,
15
],
[
16,
13
],
[
19,
4
],
[
4,
12
],
[
14,
12
],
[
19,
8
],
[
5,
10
],
[
21,
13
],
[
26,
0
],
[
13,
4
],
[
18,
2
],
[
19,
13
],
[
17,
13
],
[
20,
6
],
[
9,
13
],
[
21,
15
],
[
1,
8
],
[
19,
12
],
[
12,
1
],
[
20,
4
],
[
18,
10
],
[
12,
0
],
[
17,
8
],
[
21,
11
],
[
4,
10
],
[
23,
10
],
[
16,
12
],
[
15,
10
],
[
19,
2
],
[
21,
14
],
[
12,
8
],
[
8,
5
],
[
20,
8
],
[
20,
12
],
[
2,
8
],
[
10,
5
],
[
22,
4
],
[
17,
1
],
[
24,
4
],
[
17,
0
],
[
5,
15
],
[
23,
11
],
[
3,
13
],
[
11,
10
],
[
7,
4
],
[
2,
14
],
[
25,
15
],
[
25,
8
],
[
16,
9
],
[
26,
1
],
[
8,
14
],
[
22,
1
],
[
23,
6
],
[
8,
7
],
[
14,
0
],
[
18,
4
],
[
17,
9
],
[
22,
6
],
[
2,
2
],
[
17,
10
],
[
17,
12
],
[
21,
4
],
[
22,
11
],
[
11,
7
],
[
10,
3
],
[
4,
15
],
[
21,
10
],
[
22,
10
],
[
14,
11
],
[
12,
3
],
[
22,
5
],
[
16,
8
],
[
17,
2
],
[
5,
5
],
[
6,
0
],
[
8,
6
],
[
3,
4
],
[
23,
5
],
[
12,
10
],
[
18,
13
],
[
11,
1
],
[
14,
13
],
[
22,
14
],
[
9,
8
],
[
26,
9
],
[
6,
9
],
[
9,
9
],
[
14,
5
],
[
21,
6
],
[
12,
14
],
[
14,
10
],
[
15,
6
],
[
18,
15
],
[
17,
11
],
[
18,
11
],
[
23,
12
],
[
7,
13
],
[
17,
3
],
[
15,
1
],
[
13,
2
],
[
15,
2
],
[
6,
1
],
[
19,
1
],
[
13,
1
],
[
18,
6
],
[
14,
6
],
[
15,
5
],
[
10,
2
],
[
16,
6
],
[
22,
0
],
[
4,
11
],
[
11,
8
],
[
3,
14
],
[
12,
15
],
[
11,
0
],
[
9,
5
],
[
8,
13
],
[
19,
6
],
[
9,
0
],
[
14,
2
],
[
6,
4
],
[
9,
4
],
[
15,
4
],
[
8,
10
],
[
11,
5
],
[
10,
4
],
[
20,
13
],
[
11,
6
],
[
7,
6
],
[
6,
14
],
[
9,
7
],
[
18,
7
],
[
3,
15
],
[
16,
3
],
[
12,
12
],
[
9,
10
],
[
15,
14
],
[
12,
2
],
[
20,
2
],
[
9,
2
],
[
13,
3
],
[
7,
5
],
[
2,
6
],
[
10,
11
],
[
15,
9
],
[
14,
7
],
[
26,
6
],
[
19,
0
],
[
8,
11
],
[
27,
6
],
[
21,
7
],
[
3,
3
],
[
10,
9
],
[
20,
5
],
[
1,
14
],
[
3,
2
],
[
26,
11
],
[
6,
15
],
[
7,
14
],
[
7,
12
],
[
6,
5
],
[
15,
0
],
[
10,
13
],
[
12,
5
],
[
1,
0
],
[
13,
5
],
[
15,
15
],
[
15,
3
],
[
0,
10
],
[
7,
11
],
[
0,
11
],
[
17,
14
],
[
12,
13
],
[
5,
8
],
[
10,
8
],
[
15,
7
],
[
11,
4
],
[
5,
14
],
[
23,
13
],
[
23,
9
],
[
5,
7
],
[
16,
4
],
[
1,
1
],
[
15,
11
],
[
5,
4
],
[
1,
3
],
[
5,
9
],
[
12,
9
],
[
13,
10
],
[
16,
5
],
[
4,
8
],
[
5,
0
],
[
5,
6
],
[
5,
11
],
[
21,
2
],
[
7,
10
],
[
8,
2
],
[
5,
1
],
[
6,
3
],
[
8,
0
],
[
4,
2
],
[
18,
8
],
[
27,
11
],
[
27,
10
],
[
27,
14
],
[
25,
0
],
[
3,
8
],
[
14,
1
],
[
17,
7
],
[
4,
4
],
[
21,
5
],
[
3,
1
],
[
10,
12
],
[
16,
2
],
[
27,
7
],
[
20,
1
],
[
0,
3
],
[
22,
2
],
[
27,
1
]
]
}
================================================
FILE: scripts/convert_hf_whisper.py
================================================
#!/usr/bin/env python3
"""
Convert a Hugging Face style Whisper checkpoint into a WhisperLiveKit .pt file.
Optionally shrink the supported audio chunk length (in seconds) by trimming the
encoder positional embeddings and updating the stored model dimensions.
"""
import argparse
import json
import os
from pathlib import Path
from typing import Dict, Tuple
import torch
from whisperlivekit.whisper import _convert_hf_state_dict
from whisperlivekit.whisper.audio import HOP_LENGTH, SAMPLE_RATE
from whisperlivekit.whisper.model import ModelDimensions
from whisperlivekit.whisper.utils import exact_div
def _load_state_dict(repo_path: Path) -> Dict[str, torch.Tensor]:
safetensor_path = repo_path / "model.safetensors"
bin_path = repo_path / "pytorch_model.bin"
if safetensor_path.is_file():
try:
from safetensors.torch import load_file # type: ignore
except Exception as exc: # pragma: no cover - import guard
raise RuntimeError(
"Install safetensors to load model.safetensors "
"(pip install safetensors)"
) from exc
return load_file(str(safetensor_path))
if bin_path.is_file():
return torch.load(bin_path, map_location="cpu")
raise FileNotFoundError(
f"Could not find model.safetensors or pytorch_model.bin under {repo_path}"
)
def _load_config(repo_path: Path) -> Dict:
config_path = repo_path / "config.json"
if not config_path.is_file():
raise FileNotFoundError(
f"Hugging Face checkpoint at {repo_path} is missing config.json"
)
with open(config_path, "r", encoding="utf-8") as fp:
return json.load(fp)
def _derive_audio_ctx(chunk_length: float) -> Tuple[int, int]:
n_samples = int(round(chunk_length * SAMPLE_RATE))
expected_samples = chunk_length * SAMPLE_RATE
if abs(n_samples - expected_samples) > 1e-6:
raise ValueError(
"chunk_length must align with sample rate so that "
"chunk_length * SAMPLE_RATE is an integer"
)
n_frames = exact_div(n_samples, HOP_LENGTH)
n_audio_ctx = exact_div(n_frames, 2)
return n_frames, n_audio_ctx
def _build_dims(config: Dict, chunk_length: float) -> Dict:
base_dims = ModelDimensions(
n_mels=config["num_mel_bins"],
n_audio_ctx=config["max_source_positions"],
n_audio_state=config["d_model"],
n_audio_head=config["encoder_attention_heads"],
n_audio_layer=config.get("encoder_layers") or config["num_hidden_layers"],
n_vocab=config["vocab_size"],
n_text_ctx=config["max_target_positions"],
n_text_state=config["d_model"],
n_text_head=config["decoder_attention_heads"],
n_text_layer=config["decoder_layers"],
).__dict__.copy()
_, n_audio_ctx = _derive_audio_ctx(chunk_length)
base_dims["n_audio_ctx"] = n_audio_ctx
base_dims["chunk_length"] = chunk_length
return base_dims
def _trim_positional_embedding(
state_dict: Dict[str, torch.Tensor], target_ctx: int
) -> None:
key = "encoder.positional_embedding"
if key not in state_dict:
raise KeyError(f"{key} missing from converted state dict")
tensor = state_dict[key]
if tensor.shape[0] < target_ctx:
raise ValueError(
f"Cannot increase encoder ctx from {tensor.shape[0]} to {target_ctx}"
)
if tensor.shape[0] == target_ctx:
return
state_dict[key] = tensor[:target_ctx].contiguous()
def convert_checkpoint(hf_path: Path, output_path: Path, chunk_length: float) -> None:
state_dict = _load_state_dict(hf_path)
converted = _convert_hf_state_dict(state_dict)
config = _load_config(hf_path)
dims = _build_dims(config, chunk_length)
_trim_positional_embedding(converted, dims["n_audio_ctx"])
package = {"dims": dims, "model_state_dict": converted}
output_path.parent.mkdir(parents=True, exist_ok=True)
torch.save(package, output_path)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Convert Hugging Face Whisper checkpoint to WhisperLiveKit format."
)
parser.add_argument(
"hf_path",
type=str,
help="Path to the cloned Hugging Face repository (e.g. whisper-tiny.en)",
)
parser.add_argument(
"--output",
type=str,
default="converted-whisper.pt",
help="Destination path for the .pt file",
)
parser.add_argument(
"--chunk-length",
type=float,
default=30.0,
help="Audio chunk length in seconds to support (default: 30)",
)
return parser.parse_args()
def main():
args = parse_args()
hf_path = Path(os.path.expanduser(args.hf_path)).resolve()
output_path = Path(os.path.expanduser(args.output)).resolve()
convert_checkpoint(hf_path, output_path, args.chunk_length)
print(f"Saved converted checkpoint to {output_path}")
if __name__ == "__main__":
main()
================================================
FILE: scripts/create_long_samples.py
================================================
#!/usr/bin/env python3
"""Create long benchmark samples (5min+) by concatenating utterances from public datasets."""
import io
import json
import logging
import wave
from pathlib import Path
import numpy as np
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)
CACHE = Path.home() / ".cache/whisperlivekit/benchmark_data"
CACHE.mkdir(parents=True, exist_ok=True)
SR = 16000
def save_wav(path, audio, sr=SR):
audio = np.clip(audio, -1, 1)
audio_int = (audio * 32767).astype(np.int16)
with wave.open(str(path), "w") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sr)
wf.writeframes(audio_int.tobytes())
def decode_audio(audio_bytes):
import soundfile as sf
arr, sr = sf.read(io.BytesIO(audio_bytes), dtype="float32")
return np.array(arr, dtype=np.float32), sr
def download_long_librispeech(config, lang_code, target_dur=300):
"""Concatenate LibriSpeech utterances into a ~5min sample."""
import datasets.config
datasets.config.TORCHCODEC_AVAILABLE = False
from datasets import Audio, load_dataset
logger.info(f"Downloading LibriSpeech {config} for {lang_code} (~{target_dur}s)...")
ds = load_dataset("openslr/librispeech_asr", config, split="test", streaming=True)
ds = ds.cast_column("audio", Audio(decode=False))
chunks, texts = [], []
total = 0
for item in ds:
arr, sr = decode_audio(item["audio"]["bytes"])
chunks.append(arr)
texts.append(item["text"])
total += len(arr) / sr
if total >= target_dur:
break
if len(chunks) % 20 == 0:
logger.info(f" {total:.0f}s / {target_dur}s ({len(chunks)} utterances)")
# Insert small silences between utterances for natural transitions
silence = np.zeros(int(0.5 * sr), dtype=np.float32)
interleaved = []
for i, chunk in enumerate(chunks):
if i > 0:
interleaved.append(silence)
interleaved.append(chunk)
full = np.concatenate(interleaved)
total = len(full) / sr
ref = " ".join(texts)
name = f"{lang_code}_long_{config}"
path = CACHE / f"{name}.wav"
save_wav(path, full)
logger.info(f" -> {name}: {total:.1f}s ({len(texts)} utterances)")
return {"name": name, "path": str(path), "reference": ref,
"duration": round(total, 2), "language": lang_code.split("_")[0]}
def download_long_mls(config, lang_code, target_dur=300):
"""Concatenate MLS utterances into a ~5min sample."""
import datasets.config
datasets.config.TORCHCODEC_AVAILABLE = False
from datasets import Audio, load_dataset
logger.info(f"Downloading MLS {config} for {lang_code} (~{target_dur}s)...")
ds = load_dataset("facebook/multilingual_librispeech", config, split="test", streaming=True)
ds = ds.cast_column("audio", Audio(decode=False))
chunks, texts = [], []
total = 0
for item in ds:
arr, sr = decode_audio(item["audio"]["bytes"])
chunks.append(arr)
texts.append(item.get("text", item.get("transcript", "")))
total += len(arr) / sr
if total >= target_dur:
break
if len(chunks) % 20 == 0:
logger.info(f" {total:.0f}s / {target_dur}s ({len(chunks)} utterances)")
silence = np.zeros(int(0.5 * sr), dtype=np.float32)
interleaved = []
for i, chunk in enumerate(chunks):
if i > 0:
interleaved.append(silence)
interleaved.append(chunk)
full = np.concatenate(interleaved)
total = len(full) / sr
ref = " ".join(texts)
name = f"{lang_code}_long"
path = CACHE / f"{name}.wav"
save_wav(path, full)
logger.info(f" -> {name}: {total:.1f}s ({len(texts)} utterances)")
return {"name": name, "path": str(path), "reference": ref,
"duration": round(total, 2), "language": lang_code}
def main():
samples = []
# English clean ~90s
samples.append(download_long_librispeech("clean", "en", target_dur=90))
# English noisy ~90s
samples.append(download_long_librispeech("other", "en_noisy", target_dur=90))
# French ~90s
samples.append(download_long_mls("french", "fr", target_dur=90))
# Save metadata
meta_path = CACHE / "long_samples.json"
meta_path.write_text(json.dumps(samples, indent=2))
logger.info(f"\nSaved metadata to {meta_path}")
total = sum(s["duration"] for s in samples)
logger.info(f"Total: {len(samples)} long samples, {total:.0f}s ({total/60:.1f}min)")
if __name__ == "__main__":
main()
================================================
FILE: scripts/detect_alignment_heads_qwen3.py
================================================
#!/usr/bin/env python3
"""
Detect alignment heads in Qwen3-ASR for SimulStreaming-style inference.
Qwen3-ASR is a decoder-only multimodal model: audio is encoded by an audio
encoder and the resulting embeddings are injected into the text sequence
(replacing <|audio_pad|> placeholder tokens). The text decoder then attends
over the full sequence -- both audio-derived tokens and text tokens -- via
causal self-attention. There is **no** cross-attention.
For AlignAtt-style streaming, we need to find which (layer, head) pairs in
the text decoder's self-attention best track the monotonic alignment between
generated text tokens and their corresponding audio positions.
Algorithm
---------
For each audio sample with a known transcript:
1. Run Qwen3-ASR with output_attentions=True
2. Use the ForcedAligner to get ground-truth word->timestamp alignments
3. Convert timestamps to audio token positions in the input sequence
4. For each generated text token, check whether the argmax of each
attention head (over the audio-token region) points to the correct
audio position (as determined by the forced aligner)
5. Accumulate scores per (layer, head)
The heads whose attention argmax matches the ground-truth alignment most
often are the "alignment heads" usable for SimulStreaming.
Reference: Adapted from scripts/determine_alignment_heads.py (Whisper) and
iwslt26-sst/SimulMT_tests/heads/detect_translation_heads_qwen3.py
"""
import argparse
import io
import json
import logging
import re
import time
from difflib import SequenceMatcher
from typing import List, Optional, Tuple
import numpy as np
import soundfile as sf
import torch
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
# ── Compatibility patches for qwen_asr 0.0.6 + transformers >= 5.3 ────
def _apply_transformers_compat_patches():
"""Apply all necessary patches to make qwen_asr work with transformers >= 5.3."""
# 1. check_model_inputs was removed
try:
import transformers.utils.generic as _g
if not hasattr(_g, "check_model_inputs"):
def check_model_inputs(*args, **kwargs):
def decorator(fn):
return fn
return decorator
_g.check_model_inputs = check_model_inputs
except ImportError:
pass
# 2. 'default' rope type was removed from ROPE_INIT_FUNCTIONS
try:
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS
if "default" not in ROPE_INIT_FUNCTIONS:
def _compute_default_rope_parameters(config=None, device=None, seq_len=None, **kwargs):
if hasattr(config, "head_dim"):
head_dim = config.head_dim
else:
head_dim = config.hidden_size // config.num_attention_heads
partial = getattr(config, "partial_rotary_factor", 1.0)
dim = int(head_dim * partial)
base = config.rope_theta
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).float().to(device) / dim))
return inv_freq, 1.0
ROPE_INIT_FUNCTIONS["default"] = _compute_default_rope_parameters
except ImportError:
pass
# 3. pad_token_id missing on thinker config
try:
from qwen_asr.core.transformers_backend.configuration_qwen3_asr import (
Qwen3ASRThinkerConfig,
)
if not hasattr(Qwen3ASRThinkerConfig, "pad_token_id"):
Qwen3ASRThinkerConfig.pad_token_id = None
except ImportError:
pass
# 4. fix_mistral_regex is now handled internally by transformers 5.3;
# qwen_asr passes it explicitly, causing a duplicate-kwarg error.
try:
from transformers.models.auto import processing_auto
_orig_ap_from_pretrained = processing_auto.AutoProcessor.from_pretrained.__func__
@classmethod
def _patched_ap_from_pretrained(cls, *args, **kwargs):
kwargs.pop("fix_mistral_regex", None)
return _orig_ap_from_pretrained(cls, *args, **kwargs)
processing_auto.AutoProcessor.from_pretrained = _patched_ap_from_pretrained
except Exception:
pass
# 5. _finalize_model_loading calls initialize_weights which expects
# compute_default_rope_parameters on RotaryEmbedding modules.
try:
from qwen_asr.core.transformers_backend.modeling_qwen3_asr import (
Qwen3ASRThinkerTextRotaryEmbedding,
)
if not hasattr(Qwen3ASRThinkerTextRotaryEmbedding, "compute_default_rope_parameters"):
@staticmethod
def _compute_default_rope_parameters(config=None, device=None, seq_len=None, **kwargs):
if hasattr(config, "head_dim"):
head_dim = config.head_dim
else:
head_dim = config.hidden_size // config.num_attention_heads
partial = getattr(config, "partial_rotary_factor", 1.0)
dim = int(head_dim * partial)
base = config.rope_theta
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).float().to(device) / dim))
return inv_freq, 1.0
Qwen3ASRThinkerTextRotaryEmbedding.compute_default_rope_parameters = _compute_default_rope_parameters
except ImportError:
pass
_apply_transformers_compat_patches()
# ── Constants ────────────────────────────────────────────────────────
SAMPLE_RATE = 16000
TS_THRESHOLD = 0.1 # Minimum Translation Score to qualify as alignment head
MIN_TEXT_SIMILARITY = 0.3 # Skip clips where generated text is too different from ground truth
def text_similarity(generated: str, reference: str) -> float:
"""Compute text similarity between generated and reference transcriptions.
Normalizes both strings (lowercase, remove punctuation, collapse whitespace)
then returns SequenceMatcher ratio.
"""
def normalize(s):
s = s.lower()
s = re.sub(r'[^\w\s]', '', s)
return re.sub(r'\s+', ' ', s).strip()
gen_norm = normalize(generated)
ref_norm = normalize(reference)
if not gen_norm or not ref_norm:
return 0.0
return SequenceMatcher(None, gen_norm, ref_norm).ratio()
def load_dataset_clips(name, config, split, limit):
"""Load audio clips from a HuggingFace dataset."""
from datasets import Audio as DatasetAudio
from datasets import load_dataset
ds = load_dataset(name, config, split=split)
ds = ds.cast_column("audio", DatasetAudio(decode=False))
clips = []
for idx, row in enumerate(ds):
if limit is not None and idx >= limit:
break
audio_field = row["audio"]
transcript = row["text"]
waveform_np, _ = sf.read(io.BytesIO(audio_field["bytes"]), dtype="float32")
if waveform_np.ndim > 1:
waveform_np = waveform_np.mean(axis=1)
clips.append((waveform_np, str(transcript)))
return clips
def get_device():
"""Select the best available device."""
if torch.backends.mps.is_available():
logger.info("Using MPS (Apple Silicon GPU)")
return torch.device("mps")
elif torch.cuda.is_available():
logger.info("Using CUDA (%s)", torch.cuda.get_device_name())
return torch.device("cuda")
else:
logger.info("Using CPU (will be slow)")
return torch.device("cpu")
def load_qwen3_asr(model_id: str, device: torch.device, dtype: torch.dtype):
"""Load Qwen3-ASR model, processor, and forced aligner."""
from qwen_asr.core.transformers_backend import (
Qwen3ASRConfig,
Qwen3ASRForConditionalGeneration,
Qwen3ASRProcessor,
)
from qwen_asr.inference.qwen3_forced_aligner import Qwen3ForcedAligner
from transformers import AutoConfig, AutoModel, AutoProcessor
AutoConfig.register("qwen3_asr", Qwen3ASRConfig)
AutoModel.register(Qwen3ASRConfig, Qwen3ASRForConditionalGeneration)
AutoProcessor.register(Qwen3ASRConfig, Qwen3ASRProcessor)
logger.info("Loading model: %s (dtype=%s, device=%s)", model_id, dtype, device)
model = AutoModel.from_pretrained(
model_id,
torch_dtype=dtype,
attn_implementation="eager",
device_map=str(device),
)
model.eval()
# Force eager attention on all sub-modules (attn_implementation="eager" doesn't
# propagate through nested model configs in qwen_asr's custom architecture)
for name, module in model.named_modules():
if hasattr(module, "config") and hasattr(module.config, "_attn_implementation"):
module.config._attn_implementation = "eager"
module.config._attn_implementation_internal = "eager"
try:
processor = AutoProcessor.from_pretrained(model_id, fix_mistral_regex=True)
except TypeError:
processor = AutoProcessor.from_pretrained(model_id)
logger.info("Loading forced aligner: Qwen/Qwen3-ForcedAligner-0.6B")
forced_aligner = Qwen3ForcedAligner.from_pretrained(
"Qwen/Qwen3-ForcedAligner-0.6B",
dtype=dtype,
device_map=str(device),
)
return model, processor, forced_aligner
def find_audio_token_range(input_ids: torch.Tensor, audio_token_id: int) -> Tuple[int, int]:
"""Find the start and end positions of audio tokens in the input sequence."""
mask = (input_ids == audio_token_id)
positions = mask.nonzero(as_tuple=True)[0]
if len(positions) == 0:
return 0, 0
return positions[0].item(), positions[-1].item() + 1
def timestamp_to_audio_token_position(
timestamp_sec: float,
audio_duration_sec: float,
audio_token_start: int,
audio_token_end: int,
) -> int:
"""Convert a timestamp in seconds to the corresponding audio token position.
Audio tokens span [audio_token_start, audio_token_end) in the input sequence.
We linearly interpolate within that range based on the timestamp fraction.
"""
n_audio_tokens = audio_token_end - audio_token_start
if n_audio_tokens <= 0 or audio_duration_sec <= 0:
return audio_token_start
fraction = min(timestamp_sec / audio_duration_sec, 1.0)
pos = audio_token_start + int(fraction * (n_audio_tokens - 1))
return max(audio_token_start, min(pos, audio_token_end - 1))
def run_detection(
model,
processor,
forced_aligner,
clips: List[Tuple[np.ndarray, str]],
language: Optional[str],
device: torch.device,
) -> Tuple[np.ndarray, int]:
"""Run alignment head detection on a set of audio clips.
Uses PyTorch forward hooks on each self_attn module to capture attention
weights that the decoder layer discards (``hidden_states, _ = self.self_attn(...)``).
With eager attention, ``self_attn`` always returns ``(attn_output, attn_weights)``
so the hook can read the weights from the return value.
Returns:
g: array of shape (total_heads,) with alignment hit counts
m: total number of alignment checks performed
"""
thinker = model.thinker
text_config = thinker.config.text_config
num_layers = text_config.num_hidden_layers
num_heads = text_config.num_attention_heads
total_heads = num_layers * num_heads
audio_token_id = thinker.config.audio_token_id
logger.info(
"Text decoder: %d layers x %d heads = %d total heads",
num_layers, num_heads, total_heads,
)
logger.info(
"KV heads: %d (GQA ratio: %d)",
text_config.num_key_value_heads,
num_heads // text_config.num_key_value_heads,
)
# Build prompt helper (same as Qwen3ASRModel._build_text_prompt)
from qwen_asr.inference.utils import normalize_language_name
def build_messages(audio_payload):
return [
{"role": "system", "content": ""},
{"role": "user", "content": [{"type": "audio", "audio": audio_payload}]},
]
def build_text_prompt(force_language=None):
msgs = build_messages("")
base = processor.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
if force_language:
base = base + f"language {force_language}"
return base
force_lang = None
if language:
force_lang = normalize_language_name(language)
# Stop token IDs
eos_ids = {151645, 151643} # <|im_end|>, <|endoftext|>
if processor.tokenizer.eos_token_id is not None:
eos_ids.add(processor.tokenizer.eos_token_id)
# Decoder layers: model.thinker.model.layers[i].self_attn
decoder_layers = thinker.model.layers
g = np.zeros(total_heads, dtype=np.int64)
m = 0
t0 = time.time()
for clip_idx, (waveform, transcript) in enumerate(clips):
if not transcript.strip():
continue
audio_duration = len(waveform) / SAMPLE_RATE
# 1. Get forced alignment timestamps
try:
align_results = forced_aligner.align(
audio=[(waveform, SAMPLE_RATE)],
text=[transcript],
language=[force_lang or "English"],
)
align_result = align_results[0]
except Exception as e:
logger.warning("Forced alignment failed for clip %d: %s", clip_idx, e)
continue
if not align_result.items:
continue
# Build word -> (start_time, end_time) mapping
word_timestamps = []
for item in align_result.items:
word_timestamps.append((item.text, item.start_time, item.end_time))
# 2. Prepare inputs
text_prompt = build_text_prompt(force_language=force_lang)
inputs = processor(
text=[text_prompt],
audio=[waveform],
return_tensors="pt",
padding=True,
)
inputs = inputs.to(model.device).to(model.dtype)
prompt_len = inputs.input_ids.shape[1]
# Find audio token range
audio_start, audio_end = find_audio_token_range(
inputs.input_ids[0], audio_token_id,
)
n_audio_tokens = audio_end - audio_start
if n_audio_tokens == 0:
logger.warning("No audio tokens found in clip %d", clip_idx)
continue
# 3. Register forward hooks on self_attn to capture attention weights.
# The decoder layer discards them: hidden_states, _ = self.self_attn(...)
# but eager_attention_forward always computes and returns attn_weights.
# We capture just the argmax over the audio region (memory-efficient).
# captured_argmax[layer_idx] = list of (num_heads,) tensors, one per decode step.
captured_argmax = {i: [] for i in range(num_layers)}
def _make_hook(store, a_start, a_end):
def hook_fn(module, args, output):
# output = (attn_output, attn_weights)
attn_weights = output[1]
if attn_weights is None:
return
# attn_weights shape: (batch, num_heads, q_len, kv_len)
# Only capture decode steps (q_len == 1), skip prefill
if attn_weights.shape[2] != 1:
return
kv_len = attn_weights.shape[-1]
if a_end > kv_len:
return
# Attention from the new token over audio region
audio_attn = attn_weights[0, :, 0, a_start:a_end] # (num_heads, n_audio)
store.append(audio_attn.argmax(dim=-1).cpu()) # (num_heads,)
return hook_fn
hooks = []
for layer_idx in range(num_layers):
h = decoder_layers[layer_idx].self_attn.register_forward_hook(
_make_hook(captured_argmax[layer_idx], audio_start, audio_end)
)
hooks.append(h)
# 4. Run generation
try:
with torch.inference_mode():
outputs = thinker.generate(
**inputs,
max_new_tokens=256,
do_sample=False,
)
except Exception as e:
for h in hooks:
h.remove()
logger.warning("Generation failed for clip %d: %s", clip_idx, e)
continue
finally:
for h in hooks:
h.remove()
# outputs is (batch, seq_len) tensor
all_generated = outputs[0, prompt_len:]
num_gen = len(all_generated)
for i, tid in enumerate(all_generated):
if tid.item() in eos_ids:
num_gen = i
break
generated_ids = all_generated[:num_gen]
if num_gen == 0:
del outputs, captured_argmax
continue
generated_text = processor.tokenizer.decode(generated_ids, skip_special_tokens=True)
# Filter out hallucinated clips (e.g. "!!!" patterns)
sim = text_similarity(generated_text, transcript)
if sim < MIN_TEXT_SIMILARITY:
logger.info(
"[%d/%d] SKIP (sim=%.2f) | %s...",
clip_idx + 1, len(clips), sim, generated_text[:60],
)
del outputs, captured_argmax
continue
# Verify hooks captured data
n_captured = len(captured_argmax[0])
if n_captured == 0:
logger.warning(
"No attention weights captured for clip %d (hooks may not have fired)", clip_idx
)
del outputs, captured_argmax
continue
# 5. Map generated tokens to word timestamps
gen_token_strings = [
processor.tokenizer.decode([tid.item()]) for tid in generated_ids
]
# Map each generated token index -> forced-aligner word index
accumulated_text = ""
word_idx = 0
token_to_word = {}
for tok_idx, tok_str in enumerate(gen_token_strings):
accumulated_text += tok_str
# Advance word index when accumulated text covers the current word
while (
word_idx < len(word_timestamps)
and len(accumulated_text.strip()) >= sum(
len(w[0]) + 1 for w in word_timestamps[:word_idx + 1]
)
):
word_idx += 1
actual_word_idx = min(word_idx, len(word_timestamps) - 1)
token_to_word[tok_idx] = actual_word_idx
# 6. Score each head using captured argmax data
for gen_step in range(num_gen):
word_idx = token_to_word.get(gen_step, None)
if word_idx is None or word_idx >= len(word_timestamps):
continue
_, word_start, word_end = word_timestamps[word_idx]
word_mid = (word_start + word_end) / 2.0
# Expected audio token position for this word
expected_pos = timestamp_to_audio_token_position(
word_mid, audio_duration, audio_start, audio_end,
)
# Tolerance: +/- a few audio tokens (proportional to word duration)
word_dur_tokens = max(1, int(
(word_end - word_start) / audio_duration * n_audio_tokens / 2
))
tolerance = max(3, word_dur_tokens)
m += 1
for layer_idx in range(num_layers):
if gen_step >= len(captured_argmax[layer_idx]):
continue
argmaxes = captured_argmax[layer_idx][gen_step].numpy() # (num_heads,)
for head_idx in range(num_heads):
attended_pos = argmaxes[head_idx] # relative to audio_start
attended_abs = audio_start + attended_pos
if abs(attended_abs - expected_pos) <= tolerance:
g[layer_idx * num_heads + head_idx] += 1
del outputs, captured_argmax
if device.type == "mps":
torch.mps.empty_cache()
elif device.type == "cuda":
torch.cuda.empty_cache()
elapsed = time.time() - t0
avg = elapsed / (clip_idx + 1)
eta = avg * (len(clips) - clip_idx - 1)
logger.info(
"[%d/%d] m=%d | %s... | %.1fs/clip | ETA: %.0fs",
clip_idx + 1, len(clips), m,
generated_text[:60], avg, eta,
)
return g, m
def main():
parser = argparse.ArgumentParser(
description="Detect alignment heads in Qwen3-ASR for SimulStreaming"
)
parser.add_argument(
"--model", type=str, default="Qwen/Qwen3-ASR-1.7B",
help="Qwen3-ASR model name or path",
)
parser.add_argument(
"--dataset", type=str, default="librispeech_asr",
help="HuggingFace dataset name",
)
parser.add_argument(
"--dataset-config", type=str, default="clean",
help="Dataset config/subset",
)
parser.add_argument(
"--dataset-split", type=str, default="validation",
help="Dataset split",
)
parser.add_argument(
"-n", "--num-samples", type=int, default=50,
help="Number of audio samples to process",
)
parser.add_argument(
"--language", type=str, default="English",
help="Language for forced alignment",
)
parser.add_argument(
"--dtype", type=str, default="bf16",
choices=["float32", "bf16", "float16"],
help="Model dtype",
)
parser.add_argument(
"-o", "--output", type=str, default="alignment_heads_qwen3_asr.json",
help="Output JSON file",
)
parser.add_argument(
"--heatmap", type=str, default="alignment_heads_qwen3_asr.png",
help="Output heatmap image",
)
parser.add_argument(
"--threshold", type=float, default=TS_THRESHOLD,
help="Minimum alignment score threshold",
)
args = parser.parse_args()
device = get_device()
dtype_map = {
"float32": torch.float32,
"bf16": torch.bfloat16,
"float16": torch.float16,
}
dtype = dtype_map[args.dtype]
# Load model
model, processor, forced_aligner = load_qwen3_asr(args.model, device, dtype)
# Load data
logger.info("Loading dataset: %s/%s [%s]", args.dataset, args.dataset_config, args.dataset_split)
clips = load_dataset_clips(
args.dataset, args.dataset_config, args.dataset_split, args.num_samples,
)
logger.info("Loaded %d clips", len(clips))
# Run detection
g, m = run_detection(model, processor, forced_aligner, clips, args.language, device)
# Compute alignment scores
thinker = model.thinker
text_config = thinker.config.text_config
num_layers = text_config.num_hidden_layers
num_heads = text_config.num_attention_heads
ts = g / max(m, 1)
ts_matrix = ts.reshape(num_layers, num_heads)
# Identify alignment heads
tah = []
for l in range(num_layers):
for h in range(num_heads):
score = ts_matrix[l, h]
if score > args.threshold:
tah.append({"layer": l, "head": h, "ts": round(float(score), 4)})
tah.sort(key=lambda x: x["ts"], reverse=True)
# Print results
print(f"\n{'=' * 60}")
print(f"ALIGNMENT HEADS (TS > {args.threshold}): {len(tah)} / {num_layers * num_heads}")
print(f"{'=' * 60}")
for entry in tah:
bar = "#" * int(entry["ts"] * 50)
print(f" L{entry['layer']:2d} H{entry['head']:2d} : TS={entry['ts']:.4f} {bar}")
n_active = sum(1 for s in ts if s > args.threshold)
n_low = sum(1 for s in ts if 0 < s <= args.threshold)
n_zero = sum(1 for s in ts if s == 0)
total_heads = num_layers * num_heads
print(f"\nDistribution:")
print(f" TS > {args.threshold} (alignment heads): {n_active} ({100 * n_active / total_heads:.1f}%)")
print(f" 0 < TS <= {args.threshold} (low activity): {n_low} ({100 * n_low / total_heads:.1f}%)")
print(f" TS = 0 (inactive): {n_zero} ({100 * n_zero / total_heads:.1f}%)")
print(f"\nTotal alignable tokens checked: m={m}")
# Save JSON
output = {
"model": args.model,
"language": args.language,
"num_layers": num_layers,
"num_heads": num_heads,
"num_kv_heads": text_config.num_key_value_heads,
"num_samples": len(clips),
"total_alignable_tokens": int(m),
"ts_threshold": args.threshold,
"ts_matrix": ts_matrix.tolist(),
"alignment_heads": tah,
# WhisperLiveKit-compatible format: list of [layer, head] pairs
"alignment_heads_compact": [[e["layer"], e["head"]] for e in tah],
}
with open(args.output, "w") as f:
json.dump(output, f, indent=2)
logger.info("Results saved to %s", args.output)
# Generate heatmap
try:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, ax = plt.subplots(
figsize=(max(10, num_heads * 0.6), max(8, num_layers * 0.35)),
)
im = ax.imshow(
ts_matrix,
aspect="auto",
cmap="RdYlBu_r",
vmin=0,
vmax=max(0.4, ts_matrix.max()),
interpolation="nearest",
)
ax.set_xlabel("Head ID", fontsize=12)
ax.set_ylabel("Layer", fontsize=12)
ax.set_title(
f"Alignment Scores - {args.model}\n"
f"{len(tah)} alignment heads (TS > {args.threshold}), n={len(clips)}",
fontsize=13,
)
ax.set_xticks(range(num_heads))
ax.set_yticks(range(num_layers))
plt.colorbar(im, ax=ax, label="Alignment Score", shrink=0.8)
for entry in tah:
ax.add_patch(plt.Rectangle(
(entry["head"] - 0.5, entry["layer"] - 0.5),
1, 1, fill=False, edgecolor="red", linewidth=1.5,
))
plt.tight_layout()
plt.savefig(args.heatmap, dpi=150)
logger.info("Heatmap saved to %s", args.heatmap)
except Exception as e:
logger.warning("Could not generate heatmap: %s", e)
if __name__ == "__main__":
main()
================================================
FILE: scripts/determine_alignment_heads.py
================================================
"""Determine alignment heads for a variants, such as distilled model"""
from __future__ import annotations
import argparse
import base64
import gzip
import io
import math
import pathlib
import sys
from typing import Sequence, Tuple, Union
import matplotlib.pyplot as plt
import numpy as np
import soundfile as sf
import torch
from datasets import Audio as DatasetAudio
from datasets import load_dataset
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
WHISPER_ROOT = REPO_ROOT / "whisper"
sys.path.insert(0, str(REPO_ROOT))
sys.path.insert(0, str(WHISPER_ROOT))
from whisper import load_model
from whisper.audio import log_mel_spectrogram, pad_or_trim
from whisper.tokenizer import get_tokenizer
AudioInput = Union[str, pathlib.Path, np.ndarray, torch.Tensor]
def load_dataset_clips(name, config, split, limit):
ds = load_dataset(name, config, split=split)
ds = ds.cast_column("audio", DatasetAudio(decode=False))
clips = []
for idx, row in enumerate(ds):
if limit is not None and idx >= limit:
break
audio_field = row["audio"]
transcript = row["text"]
waveform_np, _ = sf.read(io.BytesIO(audio_field["bytes"]), dtype="float32")
if waveform_np.ndim > 1:
waveform_np = waveform_np.mean(axis=1)
waveform = waveform_np
transcript = str(transcript)
clips.append((waveform, transcript))
return clips
def load_clips(args):
return load_dataset_clips(
args.dataset,
args.dataset_config,
args.dataset_split,
args.dataset_num_samples,
)
def _waveform_from_source(source: AudioInput) -> torch.Tensor:
waveform = torch.from_numpy(source.astype(np.float32, copy=False))
return waveform
def _parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--model",
type=str,
default="pytorch_model.bin",
)
parser.add_argument(
"--device",
type=str,
default="cuda" if torch.cuda.is_available() else "cpu",
help="Torch device to run on",
)
parser.add_argument(
"--dataset",
type=str,
default="librispeech_asr"
)
parser.add_argument(
"--dataset-config",
type=str,
default="clean"
)
parser.add_argument(
"--dataset-split",
type=str,
default="validation[:1%]",
)
parser.add_argument(
"--dataset-num-samples",
type=int,
default=16,
)
parser.add_argument(
"--threshold",
type=float,
default=1.5,
help="Z score threshold for a head to be selected",
)
parser.add_argument(
"--votes",
type=float,
default=0.75,
help="percentage of clips that must vote for a head",
)
parser.add_argument(
"--output",
type=str,
default="alignment_heads.b85",
)
parser.add_argument(
"--visualize-top-k",
type=int,
default=32,
)
return parser.parse_args()
def collect_heads(
model,
tokenizer,
clips: Sequence[Tuple[AudioInput, str]],
threshold: float,
) -> Tuple[torch.Tensor, torch.Tensor]:
device = model.device
votes = torch.zeros(model.dims.n_text_layer, model.dims.n_text_head, device=device)
strengths = torch.zeros_like(votes)
for audio_source, transcript in clips:
waveform = pad_or_trim(_waveform_from_source(audio_source))
mel = log_mel_spectrogram(waveform, device=device)
tokens = torch.tensor(
[
*tokenizer.sot_sequence,
tokenizer.no_timestamps,
*tokenizer.encode(transcript),
tokenizer.eot,
],
device=device,
)
qks = [None] * model.dims.n_text_layer
hooks = [
block.cross_attn.register_forward_hook(
lambda _, __, outputs, index=i: qks.__setitem__(index, outputs[-1][0])
)
for i, block in enumerate(model.decoder.blocks)
]
with torch.no_grad():
model(mel.unsqueeze(0), tokens.unsqueeze(0))
for hook in hooks:
hook.remove()
for layer_idx, tensor in enumerate(qks):
if tensor is None:
continue
tensor = tensor[:, :, : mel.shape[-1] // 2]
tensor = tensor.softmax(dim=-1)
peak = tensor.max(dim=-1).values # [heads, tokens]
strengths[layer_idx] += peak.mean(dim=-1)
zscore = (peak - peak.mean(dim=-1, keepdim=True)) / (
peak.std(dim=-1, keepdim=True, unbiased=False) + 1e-6
)
mask = (zscore > 3).any(dim=-1)
votes[layer_idx] += mask.float()
votes /= len(clips)
strengths /= len(clips)
return votes, strengths
def _select_heads_for_visualization(selection, strengths, top_k):
selected = torch.nonzero(selection, as_tuple=False)
if selected.numel() == 0:
return []
entries = [
(int(layer.item()), int(head.item()), float(strengths[layer, head].item()))
for layer, head in selected
]
entries.sort(key=lambda item: item[2], reverse=True)
return entries[:top_k]
def _extract_heatmaps(
model,
tokenizer,
clip: Tuple[AudioInput, str],
heads: Sequence[Tuple[int, int, float]],
) -> dict:
if not heads:
return {}
target_map = {}
for layer, head, _ in heads:
target_map.setdefault(layer, set()).add(head)
waveform = pad_or_trim(_waveform_from_source(clip[0]))
mel = log_mel_spectrogram(waveform, device=model.device)
transcript = clip[1]
tokens = torch.tensor(
[
*tokenizer.sot_sequence,
tokenizer.no_timestamps,
*tokenizer.encode(transcript),
tokenizer.eot,
],
device=model.device,
)
QKs = [None] * model.dims.n_text_layer
hooks = [
block.cross_attn.register_forward_hook(
lambda _, __, outputs, index=i: QKs.__setitem__(index, outputs[-1][0])
)
for i, block in enumerate(model.decoder.blocks)
]
with torch.no_grad():
model(mel.unsqueeze(0), tokens.unsqueeze(0))
for hook in hooks:
hook.remove()
heatmaps = {}
for layer_idx, tensor in enumerate(QKs):
if tensor is None or layer_idx not in target_map:
continue
tensor = tensor[:, :, : mel.shape[-1] // 2]
tensor = tensor.softmax(dim=-1).cpu()
for head_idx in target_map[layer_idx]:
heatmaps[(layer_idx, head_idx)] = tensor[head_idx]
return heatmaps
def _plot_heatmaps(
heads, heatmaps, output_path):
cols = min(3, len(heads))
rows = math.ceil(len(heads) / cols)
fig, axes = plt.subplots(rows, cols, figsize=(4 * cols, 3.2 * rows), squeeze=False)
for idx, (layer, head, score) in enumerate(heads):
ax = axes[idx // cols][idx % cols]
mat = heatmaps.get((layer, head))
if mat is None:
ax.axis("off")
continue
im = ax.imshow(mat.to(torch.float32).numpy(), aspect="auto", origin="lower")
ax.set_title(f"L{layer} H{head} · score {score:.2f}")
ax.set_xlabel("time")
ax.set_ylabel("tokens")
for j in range(len(heads), rows * cols):
axes[j // cols][j % cols].axis("off")
fig.tight_layout()
fig.savefig(output_path, dpi=200)
plt.close(fig)
def _dump_mask(mask: torch.Tensor, output_path: str):
payload = mask.numpy().astype(np.bool_)
blob = base64.b85encode(gzip.compress(payload.tobytes()))
with open(output_path, "wb") as f:
f.write(blob)
def main():
args = _parse_args()
model = load_model(args.model, device=args.device)
model.eval()
tokenizer = get_tokenizer(multilingual=model.is_multilingual)
clips = load_clips(args)
votes, strengths = collect_heads(model, tokenizer, clips, args.threshold)
# selection = votes > 0.5
selection = strengths > 0.05
_dump_mask(selection.cpu(), args.output)
viz_heads = _select_heads_for_visualization(selection, strengths, args.visualize_top_k)
heatmaps = _extract_heatmaps(model, tokenizer, clips[0], viz_heads)
_plot_heatmaps(viz_heads, heatmaps, "alignment_heads.png")
if __name__ == "__main__":
main()
================================================
FILE: scripts/generate_architecture.py
================================================
#!/usr/bin/env python3
"""Generate the architecture.png diagram for WhisperLiveKit README."""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
# ── Colours ──
C_BG = "#1a1a2e"
C_PANEL = "#16213e"
C_PANEL2 = "#0f3460"
C_ACCENT = "#e94560"
C_GREEN = "#4ecca3"
C_ORANGE = "#f5a623"
C_BLUE = "#4a9eff"
C_PURPLE = "#b06af2"
C_PINK = "#ff6b9d"
C_YELLOW = "#f0e68c"
C_TEXT = "#e8e8e8"
C_TEXTDIM = "#a0a0b0"
C_BOX_BG = "#1e2d4a"
C_BOX_BG2 = "#2a1a3a"
C_BOX_BG3 = "#1a3a2a"
C_BORDER = "#3a4a6a"
fig, ax = plt.subplots(1, 1, figsize=(20, 12), facecolor=C_BG)
ax.set_xlim(0, 20)
ax.set_ylim(0, 12)
ax.set_aspect("equal")
ax.axis("off")
fig.subplots_adjust(left=0.01, right=0.99, top=0.97, bottom=0.01)
def box(x, y, w, h, label, color=C_BORDER, bg=C_BOX_BG, fontsize=8, bold=False,
text_color=C_TEXT, radius=0.15):
rect = FancyBboxPatch(
(x, y), w, h,
boxstyle=f"round,pad=0.05,rounding_size={radius}",
facecolor=bg, edgecolor=color, linewidth=1.2,
)
ax.add_patch(rect)
weight = "bold" if bold else "normal"
ax.text(x + w/2, y + h/2, label, ha="center", va="center",
fontsize=fontsize, color=text_color, fontweight=weight, family="monospace")
return rect
def arrow(x1, y1, x2, y2, color=C_TEXTDIM, style="->", lw=1.2):
ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
arrowprops=dict(arrowstyle=style, color=color, lw=lw))
def section_box(x, y, w, h, title, bg=C_PANEL, border=C_BORDER, title_color=C_ACCENT):
rect = FancyBboxPatch(
(x, y), w, h,
boxstyle="round,pad=0.05,rounding_size=0.2",
facecolor=bg, edgecolor=border, linewidth=1.5,
)
ax.add_patch(rect)
ax.text(x + 0.15, y + h - 0.25, title, ha="left", va="top",
fontsize=9, color=title_color, fontweight="bold", family="monospace")
# ═══════════════════════════════════════════════════════════════════
# Title
# ═══════════════════════════════════════════════════════════════════
ax.text(10, 11.7, "WhisperLiveKit Architecture", ha="center", va="center",
fontsize=16, color=C_TEXT, fontweight="bold", family="monospace")
ax.text(10, 11.35, "CLI commands: serve | listen | run | transcribe | bench | diagnose | models | pull | rm | check",
ha="center", va="center", fontsize=7, color=C_TEXTDIM, family="monospace")
# ═══════════════════════════════════════════════════════════════════
# Left: Client / Server
# ═══════════════════════════════════════════════════════════════════
section_box(0.1, 7.0, 3.5, 4.0, "FastAPI Server", border=C_GREEN)
box(0.3, 10.0, 1.5, 0.5, "Web UI\nHTML + JS", color=C_GREEN, fontsize=7)
box(2.0, 10.0, 1.4, 0.5, "Frontend\n(optional)", color=C_GREEN, fontsize=7)
box(0.3, 9.1, 3.1, 0.6, "WebSocket /asr • /v1/listen", color=C_GREEN, fontsize=7, bold=True)
box(0.3, 8.3, 3.1, 0.5, "REST /v1/audio/transcriptions", color=C_GREEN, fontsize=7)
box(0.3, 7.4, 3.1, 0.5, "Health • /v1/models", color=C_GREEN, fontsize=7)
# Clients
ax.text(0.2, 6.5, "Clients:", fontsize=7, color=C_TEXTDIM, family="monospace")
for i, client in enumerate(["Browser", "OpenAI SDK", "Deepgram SDK", "TestHarness"]):
box(0.3 + i * 0.9, 5.8, 0.8, 0.5, client, fontsize=5.5, bg="#1a2a1a", color="#3a6a3a")
# ═══════════════════════════════════════════════════════════════════
# Centre: Audio Processor (per-session pipeline)
# ═══════════════════════════════════════════════════════════════════
section_box(4.0, 5.5, 5.5, 5.5, "Audio Processor (per session)", border=C_BLUE)
box(4.3, 10.0, 2.0, 0.6, "FFmpeg\nDecoding", color=C_BLUE, bg="#1a2a4a", bold=True)
arrow(3.6, 9.4, 4.3, 10.2, color=C_GREEN)
box(6.6, 10.0, 2.6, 0.6, "Silero VAD\nspeech / silence", color=C_BLUE, bg="#1a2a4a")
arrow(6.3, 10.3, 6.6, 10.3, color=C_BLUE)
box(4.3, 8.8, 4.9, 0.8, "SessionASRProxy\nthread-safe per-session language override", color=C_BLUE, fontsize=7)
arrow(6.0, 10.0, 6.0, 9.6, color=C_BLUE)
box(4.3, 7.6, 2.3, 0.8, "DiffTracker\n(opt-in ?mode=diff)", color="#5a5a7a", fontsize=7)
box(6.9, 7.6, 2.3, 0.8, "Result Formatter\n→ FrontData.to_dict()", color=C_BLUE, fontsize=7)
# Streaming policies
ax.text(4.3, 7.1, "Streaming policies:", fontsize=7, color=C_ORANGE, fontweight="bold", family="monospace")
box(4.3, 6.2, 2.3, 0.7, "LocalAgreement\nHypothesisBuffer", color=C_ORANGE, bg="#2a2a1a", fontsize=7)
box(6.9, 6.2, 2.3, 0.7, "SimulStreaming\nAlignAtt (Whisper)", color=C_ORANGE, bg="#2a2a1a", fontsize=7)
# ═══════════════════════════════════════════════════════════════════
# Right: TranscriptionEngine (singleton)
# ═══════════════════════════════════════════════════════════════════
section_box(10.0, 0.3, 9.8, 10.7, "TranscriptionEngine (singleton — shared across sessions)",
border=C_ACCENT, bg="#1e1520")
ax.text(10.2, 10.5, "6 ASR Backends", fontsize=9, color=C_ACCENT, fontweight="bold", family="monospace")
# ── Whisper backends ──
section_box(10.2, 7.3, 4.5, 3.0, "Whisper Family (chunk-based)", border=C_PURPLE, bg=C_BOX_BG2)
box(10.4, 9.2, 1.3, 0.6, "Faster\nWhisper", color=C_PURPLE, bg="#2a1a3a", fontsize=7, bold=True)
box(11.9, 9.2, 1.3, 0.6, "MLX\nWhisper", color=C_PURPLE, bg="#2a1a3a", fontsize=7, bold=True)
box(13.4, 9.2, 1.1, 0.6, "OpenAI\nWhisper", color=C_PURPLE, bg="#2a1a3a", fontsize=7)
ax.text(10.4, 8.7, "PCM → Encoder → Decoder → Tokens", fontsize=6.5, color=C_TEXTDIM, family="monospace")
ax.text(10.4, 8.3, "Uses LocalAgreement or SimulStreaming (AlignAtt)", fontsize=6, color=C_PURPLE, family="monospace")
ax.text(10.4, 7.9, "Language detection • Buffer trimming", fontsize=6, color=C_TEXTDIM, family="monospace")
ax.text(10.4, 7.5, "CPU / CUDA / MLX", fontsize=6, color=C_TEXTDIM, family="monospace")
# ── Voxtral backends ──
section_box(10.2, 3.8, 4.5, 3.2, "Voxtral (native streaming)", border=C_PINK, bg="#2a1520")
box(10.4, 5.9, 1.8, 0.6, "Voxtral MLX\n(Apple Silicon)", color=C_PINK, bg="#2a1520", fontsize=7, bold=True)
box(12.5, 5.9, 2.0, 0.6, "Voxtral HF\n(CUDA/MPS/CPU)", color=C_PINK, bg="#2a1520", fontsize=7, bold=True)
ax.text(10.4, 5.4, "Incremental encoder → Autoregressive decoder", fontsize=6.5, color=C_TEXTDIM, family="monospace")
ax.text(10.4, 5.0, "Sliding KV cache • Token-by-token output", fontsize=6, color=C_PINK, family="monospace")
ax.text(10.4, 4.6, "No chunking needed — truly streams audio", fontsize=6, color=C_TEXTDIM, family="monospace")
ax.text(10.4, 4.2, "4B params • 15 languages • 6-bit quant (MLX)", fontsize=6, color=C_TEXTDIM, family="monospace")
# ── Qwen3 backend ──
section_box(15.0, 3.8, 4.6, 3.2, "Qwen3 ASR (batch + aligner)", border=C_GREEN, bg=C_BOX_BG3)
box(15.2, 5.9, 1.5, 0.6, "Qwen3 ASR\n1.7B / 0.6B", color=C_GREEN, bg="#1a3a2a", fontsize=7, bold=True)
box(16.9, 5.9, 1.5, 0.6, "Qwen3\nSimul", color=C_GREEN, bg="#1a3a2a", fontsize=7, bold=True)
box(18.6, 5.9, 1.0, 0.6, "Forced\nAligner", color=C_GREEN, bg="#1a3a2a", fontsize=6.5)
ax.text(15.2, 5.4, "Batch + SimulStreaming (AlignAtt)", fontsize=6.5, color=C_TEXTDIM, family="monospace")
ax.text(15.2, 5.0, "ForcedAligner provides word timestamps", fontsize=6, color=C_GREEN, family="monospace")
ax.text(15.2, 4.6, "LocalAgreement or border-distance policy", fontsize=6, color=C_TEXTDIM, family="monospace")
ax.text(15.2, 4.2, "29 languages • CUDA/MPS/CPU", fontsize=6, color=C_TEXTDIM, family="monospace")
# ── OpenAI API ──
box(15.2, 7.7, 4.2, 0.6, "OpenAI API (cloud)", color="#5a6a7a", fontsize=7)
ax.text(15.2, 7.4, "Remote transcription • API key required", fontsize=6, color=C_TEXTDIM, family="monospace")
# ── Shared components ──
section_box(10.2, 0.5, 9.4, 3.0, "Shared Components", border="#5a6a7a", bg="#151520")
box(10.4, 2.2, 2.5, 0.8, "Mel Spectrogram\ncached DFT + filterbank",
color="#5a6a7a", fontsize=7)
box(13.2, 2.2, 2.5, 0.8, "Diarization\nSortformer / pyannote",
color="#5a6a7a", fontsize=7)
box(16.0, 2.2, 3.4, 0.8, "Translation\nNLLB • CTranslate2",
color="#5a6a7a", fontsize=7)
box(10.4, 0.8, 4.0, 0.8, "WhisperLiveKitConfig\n(single source of truth)",
color=C_ACCENT, fontsize=7, bold=True)
box(14.8, 0.8, 2.3, 0.8, "TestHarness\npipeline testing",
color="#5a6a7a", fontsize=7)
box(17.3, 0.8, 2.3, 0.8, "Benchmark\n8 langs • 13 samples",
color=C_ORANGE, fontsize=7, bold=True)
# ═══════════════════════════════════════════════════════════════════
# Arrows: main data flow
# ═══════════════════════════════════════════════════════════════════
# Audio processor → TranscriptionEngine
arrow(9.5, 8.5, 10.2, 8.5, color=C_ACCENT, lw=2)
ax.text(9.6, 8.8, "PCM audio", fontsize=6, color=C_ACCENT, family="monospace")
# TranscriptionEngine → Audio processor (results)
arrow(10.2, 7.0, 9.5, 7.0, color=C_GREEN, lw=2)
ax.text(9.6, 7.3, "ASRTokens", fontsize=6, color=C_GREEN, family="monospace")
# Streaming policy connections
arrow(5.5, 6.2, 5.5, 5.5, color=C_ORANGE, style="->")
arrow(8.1, 6.2, 8.1, 5.5, color=C_ORANGE, style="->")
ax.text(4.3, 5.6, "Whisper + Qwen3", fontsize=5.5, color=C_ORANGE, family="monospace")
ax.text(6.9, 5.6, "Whisper + Qwen3-simul", fontsize=5.5, color=C_ORANGE, family="monospace")
# Voxtral note (no policy needed)
ax.text(10.2, 3.5, "Voxtral: own streaming processor (no external policy)", fontsize=6,
color=C_PINK, family="monospace", style="italic")
# ═══════════════════════════════════════════════════════════════════
# Legend
# ═══════════════════════════════════════════════════════════════════
legend_y = 5.0
ax.text(0.3, legend_y, "Streaming modes:", fontsize=7, color=C_TEXT, fontweight="bold", family="monospace")
for i, (label, color) in enumerate([
("Native streaming (Voxtral)", C_PINK),
("Chunk-based (Whisper)", C_PURPLE),
("Batch + aligner (Qwen3)", C_GREEN),
]):
ax.plot([0.3], [legend_y - 0.4 - i * 0.35], "s", color=color, markersize=6)
ax.text(0.6, legend_y - 0.4 - i * 0.35, label, fontsize=6.5, color=color,
va="center", family="monospace")
plt.savefig("architecture.png", dpi=200, facecolor=C_BG, bbox_inches="tight", pad_inches=0.1)
print("Saved architecture.png")
================================================
FILE: scripts/python_support_matrix.py
================================================
#!/usr/bin/env python3
"""Offline Python support matrix runner for WhisperLiveKit."""
from __future__ import annotations
import argparse
import os
import shlex
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
try:
from rich.console import Console
from rich.table import Table
HAS_RICH = True
except Exception:
HAS_RICH = False
SAMPLE_URL = (
"https://github.com/pyannote/pyannote-audio/raw/develop/tutorials/assets/sample.wav"
)
SAMPLE_PATH = Path("audio_tests/support-matrix-sample.wav")
DEFAULT_LOGS_DIR = Path("outputs/python-matrix/logs")
PYTHON_VERSIONS = ("3.11", "3.12", "3.13")
CONSOLE = Console() if HAS_RICH else None
@dataclass(frozen=True)
class MatrixRow:
row_id: str
extras: tuple[str, ...]
backend: str
policy: str
diarization_backend: str
requires_gpu: bool = False
CASES = (
MatrixRow(
row_id="fw-diart-cpu",
extras=("test", "cpu", "diarization-diart"),
backend="faster-whisper",
policy="simulstreaming",
diarization_backend="diart",
),
MatrixRow(
row_id="fw-sortformer-cpu",
extras=("test", "cpu", "diarization-sortformer"),
backend="faster-whisper",
policy="simulstreaming",
diarization_backend="sortformer",
),
MatrixRow(
row_id="fw-sortformer-gpu",
extras=("test", "cu129", "diarization-sortformer"),
backend="faster-whisper",
policy="simulstreaming",
diarization_backend="sortformer",
requires_gpu=True,
),
MatrixRow(
row_id="voxtral-diart-cpu",
extras=("test", "cpu", "voxtral-hf", "diarization-diart"),
backend="voxtral",
policy="voxtral",
diarization_backend="diart",
),
)
EXPECTED_FAILURE_CASES = {
("3.11", "voxtral-diart-cpu"): "known_unstable_voxtral_diart_cpu",
("3.12", "voxtral-diart-cpu"): "known_unstable_voxtral_diart_cpu",
}
UNSUPPORTED_CASES = {
("3.13", "fw-sortformer-cpu"): "unsupported_py313_sortformer_protobuf",
("3.13", "fw-sortformer-gpu"): "unsupported_py313_sortformer_protobuf",
}
@dataclass(frozen=True)
class CaseResult:
python_version: str
row_id: str
status: Literal["PASS", "FAIL", "N/A"]
reason: str
duration_sec: float
hint: str = ""
log_path: str = ""
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Minimal WhisperLiveKit offline support matrix"
)
parser.add_argument(
"--timeout-sec",
type=int,
default=300,
help="Per-case timeout in seconds (default: 300)",
)
parser.add_argument(
"--logs-dir",
default=str(DEFAULT_LOGS_DIR),
help="Directory where per-case logs are written (default: outputs/python-matrix/logs)",
)
return parser.parse_args()
def safe_slug(text: str) -> str:
return text.replace("=", "-").replace("|", "__").replace("/", "-").replace(" ", "-")
def status_style(status: str) -> str:
if status == "PASS":
return "green"
if status == "FAIL":
return "bold red"
if status == "N/A":
return "yellow"
return "white"
def print_line(message: str, style: str | None = None) -> None:
if CONSOLE is None:
print(message)
return
if style:
CONSOLE.print(message, style=style, highlight=False)
else:
CONSOLE.print(message, highlight=False)
def tail_text(text: str | None, max_chars: int = 220) -> str:
if not text:
return ""
normalized = " ".join(text.split())
if len(normalized) <= max_chars:
return normalized
return normalized[-max_chars:]
def run_command(
cmd: list[str],
cwd: Path,
env: dict[str, str],
timeout: int | None = None,
log_path: Path | None = None,
log_section: str | None = None,
) -> subprocess.CompletedProcess[str]:
def _append_log(
*,
command: list[str],
section: str,
returncode: int | None,
stdout: str | None,
stderr: str | None,
timed_out: bool = False,
) -> None:
if log_path is None:
return
log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open("a", encoding="utf-8") as f:
f.write(f"\n=== {section} ===\n")
f.write(f"$ {shlex.join(command)}\n")
if timed_out:
f.write("status: timeout\n")
else:
f.write(f"status: exit_code={returncode}\n")
if stdout:
f.write("--- stdout ---\n")
f.write(stdout)
if not stdout.endswith("\n"):
f.write("\n")
if stderr:
f.write("--- stderr ---\n")
f.write(stderr)
if not stderr.endswith("\n"):
f.write("\n")
section = log_section or "command"
try:
proc = subprocess.run(
cmd,
cwd=str(cwd),
env=env,
text=True,
capture_output=True,
check=False,
timeout=timeout,
)
except subprocess.TimeoutExpired as exc:
_append_log(
command=cmd,
section=section,
returncode=None,
stdout=exc.stdout if isinstance(exc.stdout, str) else None,
stderr=exc.stderr if isinstance(exc.stderr, str) else None,
timed_out=True,
)
raise
_append_log(
command=cmd,
section=section,
returncode=proc.returncode,
stdout=proc.stdout,
stderr=proc.stderr,
)
return proc
def detect_gpu_available() -> bool:
try:
proc = subprocess.run(
["nvidia-smi", "-L"],
text=True,
capture_output=True,
check=False,
timeout=10,
)
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
return proc.returncode == 0
def download_sample(repo_root: Path) -> Path:
target = repo_root / SAMPLE_PATH
target.parent.mkdir(parents=True, exist_ok=True)
cmd = [
"curl",
"--fail",
"--location",
"--silent",
"--show-error",
SAMPLE_URL,
"--output",
str(target),
]
proc = run_command(cmd, cwd=repo_root, env=os.environ.copy())
if proc.returncode != 0:
hint = tail_text(proc.stderr or proc.stdout)
raise RuntimeError(f"sample_download_failed: {hint}")
return target
def sync_case_environment(
repo_root: Path,
python_version: str,
row: MatrixRow,
env_dir: Path,
log_path: Path,
) -> tuple[bool, str]:
cmd = ["uv", "sync", "--python", python_version, "--no-dev"]
for extra in row.extras:
cmd.extend(["--extra", extra])
env = os.environ.copy()
env["UV_PROJECT_ENVIRONMENT"] = str(env_dir)
proc = run_command(
cmd,
cwd=repo_root,
env=env,
log_path=log_path,
log_section="sync",
)
if proc.returncode != 0:
return False, tail_text(proc.stderr or proc.stdout)
return True, ""
def apply_expected_failure_policy(result: CaseResult) -> CaseResult:
expected_reason = EXPECTED_FAILURE_CASES.get((result.python_version, result.row_id))
if result.status != "FAIL" or not expected_reason:
return result
override_hint = result.hint
if result.reason:
override_hint = (
f"expected_failure_override original_reason={result.reason}; {override_hint}"
if override_hint
else f"expected_failure_override original_reason={result.reason}"
)
return CaseResult(
python_version=result.python_version,
row_id=result.row_id,
status="N/A",
reason=expected_reason,
duration_sec=result.duration_sec,
hint=override_hint,
log_path=result.log_path,
)
def build_offline_command(
python_version: str,
row: MatrixRow,
sample_audio: Path,
timeout_sec: int,
) -> tuple[list[str], int | None]:
base_cmd = [
"uv",
"run",
"--python",
python_version,
"--no-sync",
"python",
"test_backend_offline.py",
"--backend",
row.backend,
"--policy",
row.policy,
"--audio",
str(sample_audio),
"--model",
"tiny",
"--diarization",
"--diarization-backend",
row.diarization_backend,
"--lan",
"en",
"--no-realtime",
]
if shutil.which("timeout"):
return ["timeout", str(timeout_sec), *base_cmd], None
return base_cmd, timeout_sec
def run_case(
repo_root: Path,
python_version: str,
row: MatrixRow,
sample_audio: Path,
timeout_sec: int,
gpu_available: bool,
logs_dir: Path,
) -> CaseResult:
start = time.monotonic()
case_slug = safe_slug(f"py{python_version}-{row.row_id}")
log_path = logs_dir / f"run-{case_slug}.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
log_path.write_text("", encoding="utf-8")
unsupported_reason = UNSUPPORTED_CASES.get((python_version, row.row_id))
if unsupported_reason:
log_path.write_text(
f"[matrix] precheck_short_circuit status=N/A reason={unsupported_reason}\n",
encoding="utf-8",
)
return CaseResult(
python_version=python_version,
row_id=row.row_id,
status="N/A",
reason=unsupported_reason,
duration_sec=0.0,
hint="unsupported_case_precheck",
log_path=str(log_path),
)
if row.requires_gpu and not gpu_available:
return CaseResult(
python_version=python_version,
row_id=row.row_id,
status="N/A",
reason="gpu_unavailable",
duration_sec=0.0,
hint="nvidia-smi unavailable or failed",
log_path=str(log_path),
)
env_dir = repo_root / ".matrix-envs" / safe_slug(f"py{python_version}-{row.row_id}")
sync_ok, sync_hint = sync_case_environment(
repo_root,
python_version,
row,
env_dir,
log_path=log_path,
)
if not sync_ok:
return CaseResult(
python_version=python_version,
row_id=row.row_id,
status="FAIL",
reason="dependency_sync_failed",
duration_sec=round(time.monotonic() - start, 3),
hint=sync_hint,
log_path=str(log_path),
)
cmd, process_timeout = build_offline_command(
python_version, row, sample_audio, timeout_sec
)
env = os.environ.copy()
env["UV_PROJECT_ENVIRONMENT"] = str(env_dir)
if row.requires_gpu:
env.pop("CUDA_VISIBLE_DEVICES", None)
else:
env["CUDA_VISIBLE_DEVICES"] = ""
try:
proc = run_command(
cmd,
cwd=repo_root,
env=env,
timeout=process_timeout,
log_path=log_path,
log_section="offline",
)
except subprocess.TimeoutExpired as exc:
return CaseResult(
python_version=python_version,
row_id=row.row_id,
status="FAIL",
reason="offline_timeout",
duration_sec=round(time.monotonic() - start, 3),
hint=tail_text((exc.stderr or "") if isinstance(exc.stderr, str) else ""),
log_path=str(log_path),
)
hint = tail_text(proc.stderr or proc.stdout)
if proc.returncode == 0:
return CaseResult(
python_version=python_version,
row_id=row.row_id,
status="PASS",
reason="ok",
duration_sec=round(time.monotonic() - start, 3),
hint=hint,
log_path=str(log_path),
)
reason = "offline_timeout" if proc.returncode == 124 else "offline_run_failed"
return CaseResult(
python_version=python_version,
row_id=row.row_id,
status="FAIL",
reason=reason,
duration_sec=round(time.monotonic() - start, 3),
hint=hint,
log_path=str(log_path),
)
def print_summary(results: list[CaseResult]) -> None:
pass_count = sum(1 for row in results if row.status == "PASS")
fail_count = sum(1 for row in results if row.status == "FAIL")
na_count = sum(1 for row in results if row.status == "N/A")
if CONSOLE is None:
print("\n[matrix] results")
print("python | row | status | reason | duration_s")
print("---|---|---|---|---")
for result in results:
print(
f"{result.python_version} | {result.row_id} | {result.status} | "
f"{result.reason} | {result.duration_sec:.3f}"
)
print(
f"\n[matrix] summary pass={pass_count} fail={fail_count} "
f"na={na_count} total={len(results)}"
)
else:
table = Table(title="Support Matrix Results")
table.add_column("Python", style="cyan", no_wrap=True)
table.add_column("Row", style="white")
table.add_column("Status", no_wrap=True)
table.add_column("Reason")
table.add_column("Duration (s)", justify="right", no_wrap=True)
for result in results:
table.add_row(
result.python_version,
result.row_id,
f"[{status_style(result.status)}]{result.status}[/{status_style(result.status)}]",
result.reason,
f"{result.duration_sec:.3f}",
)
CONSOLE.print()
CONSOLE.print(table)
CONSOLE.print(
f"[bold]Summary[/bold] "
f"pass=[green]{pass_count}[/green] "
f"fail=[bold red]{fail_count}[/bold red] "
f"na=[yellow]{na_count}[/yellow] "
f"total={len(results)}"
)
diagnostics = [row for row in results if row.status in {"FAIL", "N/A"} and row.hint]
if diagnostics:
if CONSOLE is None:
print("\n[matrix] diagnostics (failed/n-a cases)")
for row in diagnostics:
print(
f"- py={row.python_version} row={row.row_id} "
f"status={row.status} reason={row.reason}"
)
print(f" hint: {row.hint}")
if row.log_path:
print(f" log: {row.log_path}")
else:
diagnostics_table = Table(title="Diagnostics (FAIL / N/A)")
diagnostics_table.add_column("Case", style="cyan")
diagnostics_table.add_column("Status", no_wrap=True)
diagnostics_table.add_column("Reason")
diagnostics_table.add_column("Hint")
diagnostics_table.add_column("Log")
for row in diagnostics:
diagnostics_table.add_row(
f"py={row.python_version} {row.row_id}",
f"[{status_style(row.status)}]{row.status}[/{status_style(row.status)}]",
row.reason,
row.hint,
row.log_path,
)
CONSOLE.print()
CONSOLE.print(diagnostics_table)
def main() -> int:
args = parse_args()
if args.timeout_sec <= 0:
print("[matrix] error: --timeout-sec must be > 0", file=sys.stderr)
return 1
repo_root = Path(__file__).resolve().parents[1]
logs_dir = (repo_root / args.logs_dir).resolve()
logs_dir.mkdir(parents=True, exist_ok=True)
print_line(f"[matrix] repo_root={repo_root}", style="cyan")
print_line(f"[matrix] timeout_sec={args.timeout_sec}", style="cyan")
print_line(f"[matrix] logs_dir={logs_dir}", style="cyan")
try:
sample_audio = download_sample(repo_root)
except Exception as exc: # pragma: no cover - straightforward failure path
if CONSOLE is None:
print(f"[matrix] sample_download_failed: {exc}", file=sys.stderr)
else:
CONSOLE.print(
f"[matrix] sample_download_failed: {exc}",
style="bold red",
highlight=False,
)
return 1
print_line(f"[matrix] sample_audio={sample_audio}", style="cyan")
gpu_available = detect_gpu_available()
print_line(f"[matrix] gpu_available={gpu_available}", style="cyan")
results: list[CaseResult] = []
for python_version in PYTHON_VERSIONS:
for row in CASES:
print_line(
f"\n[matrix] running py={python_version} row={row.row_id}", style="blue"
)
result = run_case(
repo_root=repo_root,
python_version=python_version,
row=row,
sample_audio=sample_audio,
timeout_sec=args.timeout_sec,
gpu_available=gpu_available,
logs_dir=logs_dir,
)
result = apply_expected_failure_policy(result)
results.append(result)
print_line(
f"[matrix] {result.status} py={result.python_version} "
f"row={result.row_id} reason={result.reason} duration={result.duration_sec:.3f}s",
style=status_style(result.status),
)
if result.log_path:
print_line(f"[matrix] log={result.log_path}", style="dim")
print_summary(results)
fail_count = sum(1 for row in results if row.status == "FAIL")
return 1 if fail_count else 0
if __name__ == "__main__":
raise SystemExit(main())
================================================
FILE: scripts/run_scatter_benchmark.py
================================================
#!/usr/bin/env python3
"""Run benchmark across all backend x model x policy combos for scatter plot.
Tests each configuration on long audio samples in two modes:
- Compute-unaware (speed=0): all audio dumped instantly, measures pure model accuracy
- Compute-aware (speed=1.0): real-time simulation, slow models lose audio
Usage:
python scripts/run_scatter_benchmark.py
python scripts/run_scatter_benchmark.py --aware # only compute-aware
python scripts/run_scatter_benchmark.py --unaware # only compute-unaware
python scripts/run_scatter_benchmark.py --plot-only results.json
"""
import argparse
import asyncio
import gc
import json
import logging
import platform
import subprocess
import sys
import time
import warnings
warnings.filterwarnings("ignore")
logging.basicConfig(level=logging.WARNING)
for name in [
"whisperlivekit", "transformers", "torch", "httpx", "datasets",
"numexpr", "faster_whisper",
]:
logging.getLogger(name).setLevel(logging.ERROR)
LONG_SAMPLES_PATH = "~/.cache/whisperlivekit/benchmark_data/long_samples.json"
# ── All configurations to benchmark ──
COMBOS = [
# faster-whisper x LocalAgreement
{"backend": "faster-whisper", "model_size": "base", "policy": "localagreement",
"label": "fw LA base", "color": "#4a9eff", "marker": "o", "size": 100},
{"backend": "faster-whisper", "model_size": "small", "policy": "localagreement",
"label": "fw LA small", "color": "#4a9eff", "marker": "o", "size": 220},
# faster-whisper x SimulStreaming
{"backend": "faster-whisper", "model_size": "base", "policy": "simulstreaming",
"label": "fw SS base", "color": "#4a9eff", "marker": "s", "size": 100},
{"backend": "faster-whisper", "model_size": "small", "policy": "simulstreaming",
"label": "fw SS small", "color": "#4a9eff", "marker": "s", "size": 220},
# mlx-whisper x LocalAgreement
{"backend": "mlx-whisper", "model_size": "base", "policy": "localagreement",
"label": "mlx LA base", "color": "#4ecca3", "marker": "o", "size": 100},
{"backend": "mlx-whisper", "model_size": "small", "policy": "localagreement",
"label": "mlx LA small", "color": "#4ecca3", "marker": "o", "size": 220},
# mlx-whisper x SimulStreaming
{"backend": "mlx-whisper", "model_size": "base", "policy": "simulstreaming",
"label": "mlx SS base", "color": "#4ecca3", "marker": "s", "size": 100},
{"backend": "mlx-whisper", "model_size": "small", "policy": "simulstreaming",
"label": "mlx SS small", "color": "#4ecca3", "marker": "s", "size": 220},
# voxtral-mlx (4B, native streaming)
{"backend": "voxtral-mlx", "model_size": "", "policy": "",
"label": "voxtral mlx", "color": "#f5a623", "marker": "D", "size": 250},
]
def is_backend_available(backend):
try:
if backend == "faster-whisper":
import faster_whisper; return True # noqa
elif backend == "mlx-whisper":
import mlx_whisper; return True # noqa
elif backend == "whisper":
import whisper; return True # noqa
elif backend == "voxtral-mlx":
import mlx.core # noqa
from whisperlivekit.voxtral_mlx.loader import load_voxtral_model; return True # noqa
elif backend == "voxtral":
from transformers import VoxtralRealtimeForConditionalGeneration; return True # noqa
elif backend in ("qwen3", "qwen3-simul"):
from whisperlivekit.qwen3_asr import _patch_transformers_compat
_patch_transformers_compat()
from qwen_asr import Qwen3ASRModel; return True # noqa
except (ImportError, Exception):
pass
return False
def get_system_info():
info = {"platform": platform.platform(), "machine": platform.machine()}
try:
info["cpu"] = subprocess.check_output(
["sysctl", "-n", "machdep.cpu.brand_string"], text=True).strip()
except Exception:
info["cpu"] = platform.processor()
try:
mem = int(subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True).strip())
info["ram_gb"] = round(mem / (1024**3))
except Exception:
info["ram_gb"] = None
return info
async def run_combo_on_samples(combo, samples, lang="en", speed=0):
"""Run one config on all samples, return averaged result.
Args:
speed: 0 = compute-unaware (instant dump), 1.0 = compute-aware (real-time)
"""
from whisperlivekit.core import TranscriptionEngine
from whisperlivekit.metrics import compute_wer
from whisperlivekit.test_harness import TestHarness, _engine_cache
kwargs = {"lan": lang, "pcm_input": True}
if combo["backend"]:
kwargs["backend"] = combo["backend"]
if combo["model_size"]:
kwargs["model_size"] = combo["model_size"]
if combo.get("policy"):
kwargs["backend_policy"] = combo["policy"]
TranscriptionEngine.reset()
_engine_cache.clear()
gc.collect()
total_ref_words, total_errors = 0, 0
total_infer_time, total_audio_time = 0.0, 0.0
n_ok = 0
for sample in samples:
try:
async with TestHarness(**kwargs) as h:
await h.feed(sample["path"], speed=speed)
await h.drain(max(5.0, sample["duration"] * 0.5))
state = await h.finish(timeout=120)
metrics = h.metrics
hypothesis = state.committed_text or state.text
wer_result = compute_wer(sample["reference"], hypothesis)
total_ref_words += wer_result["ref_words"]
total_errors += (wer_result["substitutions"] +
wer_result["insertions"] +
wer_result["deletions"])
# Use actual inference time from metrics, not wall clock
if metrics and metrics.transcription_durations:
total_infer_time += sum(metrics.transcription_durations)
total_audio_time += sample["duration"]
n_ok += 1
except Exception as e:
print(f" [WARN: {sample['name']} failed: {e}]", end="")
if n_ok == 0:
return None
weighted_wer = total_errors / max(total_ref_words, 1)
# Real RTF = actual inference time / audio duration
real_rtf = total_infer_time / total_audio_time if total_audio_time > 0 else 0
return {
"label": combo["label"],
"backend": combo["backend"],
"model_size": combo.get("model_size", ""),
"policy": combo.get("policy", ""),
"color": combo["color"],
"marker": combo["marker"],
"size": combo["size"],
"rtf": round(real_rtf, 4),
"wer_pct": round(weighted_wer * 100, 1),
"n_samples": n_ok,
}
async def run_all(combos, samples, lang="en", speed=0):
mode_label = "compute-aware" if speed > 0 else "compute-unaware"
results = []
for i, combo in enumerate(combos):
if not is_backend_available(combo["backend"]):
print(f" [{i+1}/{len(combos)}] SKIP {combo['label']} (not installed)")
continue
print(f" [{i+1}/{len(combos)}] {combo['label']} ({mode_label})...", end="", flush=True)
result = await run_combo_on_samples(combo, samples, lang, speed=speed)
if result:
results.append(result)
print(f" RTF={result['rtf']:.2f}x WER={result['wer_pct']:.1f}% ({result['n_samples']} samples)")
else:
print(" FAILED (no results)")
return results
def get_long_samples_for_lang(lang="en"):
"""Load long benchmark samples from long_samples.json, filtered by language."""
import os
path = os.path.expanduser(LONG_SAMPLES_PATH)
if not os.path.exists(path):
print(f"ERROR: Long samples file not found: {path}")
print("Please generate it first (see benchmark_data/README).")
sys.exit(1)
with open(path) as f:
all_samples = json.load(f)
samples = [s for s in all_samples if s["language"] == lang]
return [{"name": s["name"], "path": s["path"], "reference": s["reference"],
"duration": s["duration"]} for s in samples]
LANG_NAMES = {
"en": "English", "fr": "French", "es": "Spanish", "de": "German",
"pt": "Portuguese", "it": "Italian", "nl": "Dutch", "pl": "Polish",
"zh": "Chinese", "ja": "Japanese", "ko": "Korean", "ru": "Russian",
}
def generate_scatter(results, system_info, output_path, n_samples, lang="en",
mode="unaware", sample_duration=0.0):
"""Generate scatter plot.
Args:
mode: "unaware" or "aware" -- shown in title
sample_duration: total audio duration in seconds -- shown in title
"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.lines import Line2D
fig, ax = plt.subplots(figsize=(12, 7), facecolor="white")
ax.set_facecolor("#fafafa")
# Show ALL points on chart (no outlier exclusion)
main = results
slow = []
# Axis limits: fit all data
if main:
xmax = max(r["rtf"] for r in main) * 1.15
ymax = max(r["wer_pct"] for r in main) * 1.15 + 1
else:
xmax, ymax = 0.5, 10
xmax = max(xmax, 1.15) # always show the real-time line
ymax = max(ymax, 8)
# Sweet spot zone: RTF < 1.0 (real-time) and WER < 12%
sweet_x = min(1.0, xmax * 0.85)
sweet_y = min(12, ymax * 0.45)
rect = plt.Rectangle((0, 0), sweet_x, sweet_y, alpha=0.07, color="#4ecca3",
zorder=0, linewidth=0)
ax.add_patch(rect)
ax.text(sweet_x - 0.005, sweet_y - 0.15, "sweet spot", ha="right", va="top",
fontsize=10, color="#2ecc71", fontstyle="italic", fontweight="bold", alpha=0.5)
# Real-time limit line
ax.axvline(x=1.0, color="#e94560", linestyle="--", linewidth=1.5, alpha=0.4, zorder=1)
ax.text(1.02, ymax * 0.97, "real-time\nlimit", fontsize=8, color="#e94560",
va="top", alpha=0.6)
# Manual label offsets keyed by label name — hand-tuned
OFFSETS = {
"fw LA base": (8, 8),
"fw LA small": (8, 8),
"fw SS base": (-55, -14),
"fw SS small": (8, 8),
"mlx LA base": (8, 10),
"mlx LA small": (8, 8),
"mlx SS base": (-55, 8),
"mlx SS small": (-55, -5),
"voxtral mlx": (10, -14),
"qwen3 0.6B": (10, 8),
"qwen3-mlx 0.6B": (10, -14),
"qwen3-mlx 1.7B": (10, 8),
"fw LA large-v3": (8, -5),
"fw SS large-v3": (8, 5),
}
# Plot main points
for r in main:
ax.scatter(r["rtf"], r["wer_pct"], c=r["color"], marker=r["marker"],
s=r["size"], edgecolors="white", linewidths=1.0, zorder=5, alpha=0.85)
ox, oy = OFFSETS.get(r["label"], (8, -4))
ax.annotate(r["label"], (r["rtf"], r["wer_pct"]),
textcoords="offset points", xytext=(ox, oy),
fontsize=8.5, color="#333333", fontweight="medium")
# Note slow backends outside main view
if slow:
lines = []
for r in slow:
lines.append(f"{r['label']}: RTF={r['rtf']:.1f}x, WER={r['wer_pct']:.1f}%")
note = "Beyond real-time:\n" + "\n".join(lines)
ax.text(xmax * 0.97, ymax * 0.97, note, ha="right", va="top",
fontsize=7.5, color="#777777", fontstyle="italic",
bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8f8f8",
edgecolor="#dddddd", alpha=0.9))
# Axes
ax.set_xlim(left=-0.01, right=xmax)
ax.set_ylim(bottom=0, top=ymax)
ax.set_xlabel("RTF (lower = faster)", fontsize=13, fontweight="bold", labelpad=8)
ax.set_ylabel("WER % (lower = more accurate)", fontsize=13, fontweight="bold", labelpad=8)
ax.grid(True, alpha=0.15, linestyle="-", color="#cccccc")
ax.tick_params(labelsize=10)
# Title
cpu = system_info.get("cpu", "unknown").replace("Apple ", "")
lang_name = LANG_NAMES.get(lang, lang.upper())
mode_label = "compute-unaware" if mode == "unaware" else "compute-aware"
dur_str = f"{sample_duration / 60:.0f}min" if sample_duration >= 60 else f"{sample_duration:.0f}s"
ax.set_title(
f"Speed vs Accuracy ({mode_label}) — {n_samples} {lang_name} samples, {dur_str} ({cpu})",
fontsize=14, fontweight="bold", pad=12)
# Legend — backends
backend_handles = []
seen = set()
for r in results:
if r["backend"] not in seen:
seen.add(r["backend"])
backend_handles.append(mpatches.Patch(color=r["color"], label=r["backend"]))
# Legend — shapes
marker_map = {"o": "LocalAgreement", "s": "SimulStreaming", "D": "Native streaming",
"h": "Batch + aligner"}
active = set(r["marker"] for r in results)
shape_handles = [
Line2D([0], [0], marker=m, color="#888", label=lbl,
markerfacecolor="#888", markersize=8, linestyle="None")
for m, lbl in marker_map.items() if m in active
]
# sizes
shape_handles += [
Line2D([0], [0], marker="o", color="#888", label="base",
markerfacecolor="#888", markersize=5, linestyle="None"),
Line2D([0], [0], marker="o", color="#888", label="small / 4B",
markerfacecolor="#888", markersize=9, linestyle="None"),
]
leg1 = ax.legend(handles=backend_handles, loc="upper left", fontsize=9,
framealpha=0.95, edgecolor="#ddd", title="Backend", title_fontsize=9)
ax.add_artist(leg1)
ax.legend(handles=shape_handles, loc="lower right", fontsize=8,
framealpha=0.95, edgecolor="#ddd", ncol=2)
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches="tight", pad_inches=0.15)
print(f"Saved {output_path}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--plot-only", default=None)
parser.add_argument("--lang", default="en", help="Language code (en, fr, es, de, ...)")
parser.add_argument("--output", "-o", default=None,
help="Output path prefix (mode suffix added automatically)")
parser.add_argument("--json-output", default=None,
help="JSON output path prefix (mode suffix added automatically)")
parser.add_argument("--aware", action="store_true",
help="Run only compute-aware mode (speed=1.0)")
parser.add_argument("--unaware", action="store_true",
help="Run only compute-unaware mode (speed=0)")
args = parser.parse_args()
lang = args.lang
# Determine which modes to run
if args.aware and args.unaware:
modes = ["unaware", "aware"]
elif args.aware:
modes = ["aware"]
elif args.unaware:
modes = ["unaware"]
else:
# Default: run both
modes = ["unaware", "aware"]
if args.plot_only:
data = json.load(open(args.plot_only))
mode = data.get("mode", "unaware")
output_path = args.output or f"benchmark_scatter_{lang}_{mode}.png"
generate_scatter(data["results"], data["system_info"], output_path,
data["n_samples"], data.get("lang", "en"),
mode=mode,
sample_duration=data.get("total_audio_s", 0))
return
print(f"Loading long {lang} samples from {LONG_SAMPLES_PATH}...")
samples = get_long_samples_for_lang(lang)
if not samples:
print(f"ERROR: No long samples for language '{lang}'")
sys.exit(1)
print(f"Using {len(samples)} samples: {[s['name'] for s in samples]}")
total_dur = sum(s["duration"] for s in samples)
print(f"Total audio: {total_dur:.0f}s ({total_dur / 60:.1f}min)\n")
# Filter combos to backends that support this language
from whisperlivekit.benchmark.compat import backend_supports_language
combos = [c for c in COMBOS if backend_supports_language(c["backend"], lang)]
system_info = get_system_info()
for mode in modes:
speed = 1.0 if mode == "aware" else 0
mode_label = "compute-aware" if mode == "aware" else "compute-unaware"
print(f"\n{'='*60}")
print(f" Running {mode_label} (speed={speed})")
print(f"{'='*60}\n")
t0 = time.time()
results = asyncio.run(run_all(combos, samples, lang, speed=speed))
total = time.time() - t0
# Save JSON
json_path = args.json_output or f"/tmp/bench_scatter_{lang}"
json_file = f"{json_path}_{mode}.json"
output_data = {
"system_info": system_info,
"lang": lang,
"mode": mode,
"speed": speed,
"n_samples": len(samples),
"sample_names": [s["name"] for s in samples],
"total_audio_s": round(total_dur, 1),
"total_benchmark_time_s": round(total, 1),
"results": results,
}
with open(json_file, "w") as f:
json.dump(output_data, f, indent=2)
print(f"\nJSON: {json_file} ({total:.0f}s total)")
# Generate scatter plot
output_base = args.output or f"benchmark_scatter_{lang}"
output_path = f"{output_base}_{mode}.png"
generate_scatter(results, system_info, output_path, len(samples), lang,
mode=mode, sample_duration=total_dur)
if __name__ == "__main__":
main()
================================================
FILE: scripts/sync_extension.py
================================================
"""Copy core files from web directory to Chrome extension directory."""
import shutil
from pathlib import Path
def sync_extension_files():
web_dir = Path("whisperlivekit/web")
extension_dir = Path("chrome-extension")
files_to_sync = [
"live_transcription.html", "live_transcription.js", "live_transcription.css"
]
svg_files = [
"system_mode.svg",
"light_mode.svg",
"dark_mode.svg",
"settings.svg"
]
for file in files_to_sync:
src_path = web_dir / file
dest_path = extension_dir / file
dest_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dest_path)
for svg_file in svg_files:
src_path = web_dir / "src" / svg_file
dest_path = extension_dir / "web" / "src" / svg_file
dest_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dest_path)
if __name__ == "__main__":
sync_extension_files()
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/test_pipeline.py
================================================
"""End-to-end pipeline tests using real models and real audio.
Run with: pytest tests/test_pipeline.py -v
Tests exercise the full pipeline through TestHarness + AudioPlayer:
audio feeding, play/pause/resume, silence detection, buffer inspection,
timing validation, and WER evaluation.
Each test is parameterized by backend so that adding a new backend
automatically gets test coverage. Tests use AudioPlayer for timeline
control — play segments, pause (inject silence), resume, cut.
Designed for AI agent automation: an agent can modify code, run these
tests, and validate transcription quality, timing, and streaming behavior.
"""
import logging
import pytest
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Backend detection
# ---------------------------------------------------------------------------
AVAILABLE_BACKENDS = []
try:
import mlx.core # noqa: F401
from whisperlivekit.voxtral_mlx.loader import load_voxtral_model # noqa: F401
AVAILABLE_BACKENDS.append("voxtral-mlx")
except ImportError:
pass
AVAILABLE_BACKENDS.append("whisper")
try:
from transformers import VoxtralRealtimeForConditionalGeneration # noqa: F401
AVAILABLE_BACKENDS.append("voxtral-hf")
except ImportError:
pass
try:
from whisperlivekit.qwen3_asr import _patch_transformers_compat
_patch_transformers_compat()
from qwen_asr import Qwen3ASRModel # noqa: F401
AVAILABLE_BACKENDS.append("qwen3")
AVAILABLE_BACKENDS.append("qwen3-simul")
except (ImportError, Exception):
pass
try:
import mlx_qwen3_asr # noqa: F401
AVAILABLE_BACKENDS.append("qwen3-mlx")
except ImportError:
pass
BACKEND_CONFIG = {
"whisper": {"model_size": "tiny", "lan": "en"},
"voxtral-mlx": {"backend": "voxtral-mlx", "lan": "en"},
"voxtral-hf": {"backend": "voxtral", "lan": "en"},
"qwen3": {"backend": "qwen3", "lan": "en"},
"qwen3-simul": {
"backend": "qwen3-simul",
"lan": "en",
"custom_alignment_heads": "scripts/alignment_heads_qwen3_asr_1.7B.json",
},
"qwen3-mlx": {"backend": "qwen3-mlx", "lan": "en"},
}
# Voxtral backends flush all words at once with proportionally-distributed
# timestamps. After a silence gap the speech line that follows may start
# before the silence segment, making the sequence non-monotonic. This is
# a known limitation of the batch-flush architecture, not a bug.
VOXTRAL_BACKENDS = {"voxtral-mlx", "voxtral-hf"}
# Backends that use batch-flush and may have non-monotonic timestamps
BATCH_FLUSH_BACKENDS = {"voxtral-mlx", "voxtral-hf", "qwen3", "qwen3-simul", "qwen3-mlx"}
def backend_kwargs(backend: str) -> dict:
return BACKEND_CONFIG.get(backend, {"model_size": "tiny", "lan": "en"})
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def samples():
"""Download test samples once per session."""
from whisperlivekit.test_data import get_samples
return {s.name: s for s in get_samples()}
@pytest.fixture(scope="session")
def short_sample(samples):
return samples["librispeech_short"]
@pytest.fixture(scope="session")
def medium_sample(samples):
return samples["librispeech_1"]
@pytest.fixture(scope="session")
def meeting_sample(samples):
return samples["ami_meeting"]
# ---------------------------------------------------------------------------
# 1. Transcription Quality
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_transcription_quality(backend, short_sample):
"""Feed a short clip and verify: text produced, WER < 50%, timestamps valid."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
await h.feed(short_sample.path, speed=0)
await h.drain(5.0)
result = await h.finish(timeout=60)
assert result.text.strip(), f"No text produced for {backend}"
errors = result.timing_errors()
assert not errors, f"Timing errors: {errors}"
wer = result.wer(short_sample.reference)
assert wer < 0.50, f"WER too high for {backend}: {wer:.2%}"
logger.info("[%s] WER=%.2f%% text='%s'", backend, wer * 100, result.text[:80])
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_medium_clip_timing_spans_audio(backend, medium_sample):
"""Feed ~14s clip and verify speech timestamps span roughly the audio duration."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
await h.feed(medium_sample.path, speed=0, chunk_duration=1.0)
await h.drain(5.0)
result = await h.finish(timeout=60)
assert result.text.strip(), f"No text for {backend}"
assert not result.timing_errors(), f"Timing errors: {result.timing_errors()}"
wer = result.wer(medium_sample.reference)
assert wer < 0.50, f"WER too high: {wer:.2%}"
# Speech should span most of the audio duration
speech_ts = [t for t in result.timestamps if t["speaker"] != -2]
if speech_ts:
last_end = speech_ts[-1]["end"]
assert last_end > medium_sample.duration * 0.5, (
f"Speech ends at {last_end:.1f}s but audio is {medium_sample.duration:.1f}s"
)
logger.info("[%s] medium: WER=%.2f%% lines=%d", backend, wer * 100, len(result.lines))
# ---------------------------------------------------------------------------
# 2. Streaming Behavior
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_text_appears_progressively(backend, medium_sample):
"""Verify text grows during streaming, not just at finish."""
from whisperlivekit.test_harness import TestHarness
snapshots = []
def on_update(state):
snapshots.append(state.text)
async with TestHarness(**backend_kwargs(backend)) as h:
h.on_update(on_update)
await h.feed(medium_sample.path, speed=2.0, chunk_duration=0.5)
await h.drain(5.0)
await h.finish(timeout=60)
non_empty = [t for t in snapshots if t.strip()]
assert len(non_empty) >= 2, (
f"Expected progressive updates for {backend}, got {len(non_empty)} non-empty"
)
if len(non_empty) >= 3:
# Check that text grew at SOME point during streaming.
# Compare first vs last non-empty snapshot rather than mid vs last,
# because some streaming backends (e.g. qwen3-simul) produce all text
# during the feed phase and the latter half of snapshots are stable.
assert len(non_empty[-1]) > len(non_empty[0]), (
f"Text not growing during streaming for {backend}"
)
logger.info("[%s] streaming: %d updates, %d non-empty", backend, len(snapshots), len(non_empty))
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_buffer_lifecycle(backend, medium_sample):
"""Buffer has content during processing; finish() empties buffer, committed grows."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
await h.feed(medium_sample.path, speed=0, chunk_duration=1.0)
await h.drain(5.0)
result = await h.finish(timeout=60)
# After finish, buffer should be empty
assert not result.buffer_transcription.strip(), (
f"Buffer not empty after finish for {backend}: '{result.buffer_transcription}'"
)
# Committed text should have substantial content
assert result.committed_word_count > 5, (
f"Too few committed words for {backend}: {result.committed_word_count}"
)
# ---------------------------------------------------------------------------
# 3. Play / Pause / Resume
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_silence_flushes_all_words(backend, medium_sample):
"""Silence must flush ALL pending words immediately — none held back for next speech.
This catches a critical bug where the last few words only appeared when
the user started speaking again, instead of being committed at silence time.
Root cause: non-blocking streamer drain racing with the generate thread.
"""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
# Feed all audio and let pipeline fully process
await h.feed(medium_sample.path, speed=0, chunk_duration=1.0)
await h.drain(8.0)
# Inject silence → triggers start_silence() which must flush everything
await h.pause(7.0, speed=0)
# Wait for start_silence() to complete (may block while generate thread
# catches up) AND for results_formatter to turn tokens into lines.
try:
await h.wait_for(
lambda s: s.has_silence and s.committed_word_count > 0,
timeout=30,
)
except TimeoutError:
pass
await h.drain(2.0)
# Capture state AFTER silence processing, BEFORE finish()
words_at_silence = h.state.committed_word_count
buffer_at_silence = h.state.buffer_transcription.strip()
# finish() joins the generate thread and flushes any stragglers
result = await h.finish(timeout=60)
words_at_finish = result.committed_word_count
# Key assertion: silence must have committed most words.
# Some backends (voxtral-hf) produce extra words from right-padding
# at finish(), and MPS inference may leave some words in the pipeline.
# Generative backends (qwen3-simul) keep producing new text on each
# inference call, so finish() adds significantly more words.
if words_at_finish > 3:
min_pct = 0.20 if backend in BATCH_FLUSH_BACKENDS else 0.50
flushed_pct = words_at_silence / words_at_finish
assert flushed_pct >= min_pct, (
f"[{backend}] Only {flushed_pct:.0%} of words flushed at silence. "
f"At silence: {words_at_silence}, at finish: {words_at_finish}. "
f"Buffer at silence: '{buffer_at_silence}'"
)
logger.info(
"[%s] silence flush: at_silence=%d, at_finish=%d, buffer='%s'",
backend, words_at_silence, words_at_finish, buffer_at_silence[:40],
)
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_play_pause_resume(backend, medium_sample):
"""Play 3s -> pause 7s -> resume 5s. Verify silence detected with valid timing."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
player = h.load_audio(medium_sample)
# Play first 3 seconds
await player.play(3.0, speed=0)
await h.drain(3.0)
# Pause 7s (above MIN_DURATION_REAL_SILENCE=5)
await h.pause(7.0, speed=0)
await h.drain(3.0)
# Resume and play 5 more seconds
await player.play(5.0, speed=0)
await h.drain(3.0)
result = await h.finish(timeout=60)
# Must have text
assert result.text.strip(), f"No text for {backend}"
# Must detect silence
assert result.has_silence, f"No silence detected for {backend}"
# Timing must be valid (start <= end for each line)
assert result.timing_valid, f"Invalid timing: {result.timing_errors()}"
# Monotonic timing — voxtral backends batch-flush words so silence
# segments can appear before the speech line they precede.
if backend not in BATCH_FLUSH_BACKENDS:
assert result.timing_monotonic, f"Non-monotonic: {result.timing_errors()}"
# At least 1 silence segment
assert len(result.silence_segments) >= 1
logger.info(
"[%s] play/pause/resume: %d lines, %d silence segs",
backend, len(result.lines), len(result.silence_segments),
)
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_multiple_pauses(backend, medium_sample):
"""Play-pause-play-pause-play cycle -> at least 2 silence segments."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
player = h.load_audio(medium_sample)
# Cycle 1: play 2s, pause 6s
await player.play(2.0, speed=0)
await h.drain(2.0)
await h.pause(6.0, speed=0)
await h.drain(2.0)
# Cycle 2: play 2s, pause 6s
await player.play(2.0, speed=0)
await h.drain(2.0)
await h.pause(6.0, speed=0)
await h.drain(2.0)
# Final: play remaining
await player.play(speed=0)
await h.drain(3.0)
result = await h.finish(timeout=60)
assert result.has_silence, f"No silence for {backend}"
assert len(result.silence_segments) >= 2, (
f"Expected >= 2 silence segments, got {len(result.silence_segments)} for {backend}"
)
assert result.timing_valid, f"Invalid timing: {result.timing_errors()}"
if backend not in BATCH_FLUSH_BACKENDS:
assert result.timing_monotonic, f"Non-monotonic: {result.timing_errors()}"
logger.info(
"[%s] multiple pauses: %d silence segs, %d speech lines",
backend, len(result.silence_segments), len(result.speech_lines),
)
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_short_pause_no_silence(backend, medium_sample):
"""Pause < 5s between speech segments should NOT produce a silence segment."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
player = h.load_audio(medium_sample)
# Play some speech
await player.play(4.0, speed=0)
await h.drain(2.0)
# Short pause (2s — well below MIN_DURATION_REAL_SILENCE=5)
await h.pause(2.0, speed=0)
await h.drain(1.0)
# Resume speech (triggers _end_silence with duration=2s < 5s threshold)
await player.play(4.0, speed=0)
await h.drain(3.0)
result = await h.finish(timeout=60)
# Should NOT have silence segments
assert not result.has_silence, (
f"Silence detected for {backend} on 2s pause (should be below 5s threshold)"
)
logger.info("[%s] short pause: no silence segment (correct)", backend)
# ---------------------------------------------------------------------------
# 4. Cutoff
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_abrupt_cutoff(backend, medium_sample):
"""Cut audio mid-stream -> no crash, partial text preserved."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
player = h.load_audio(medium_sample)
# Play only first 4 seconds of a ~14s clip
await player.play(4.0, speed=0)
# Voxtral backends need more time to start producing text
await h.drain(8.0 if backend in BATCH_FLUSH_BACKENDS else 3.0)
# Abrupt cut — voxtral backends on MPS are slower
result = await h.cut(timeout=15 if backend in BATCH_FLUSH_BACKENDS else 10)
# Should have some text (even partial)
assert result.text.strip(), f"No text after cutoff for {backend}"
# No crashes — timing should be valid (voxtral may have non-monotonic)
assert result.timing_valid, f"Invalid timing after cutoff: {result.timing_errors()}"
logger.info("[%s] cutoff at 4s: text='%s'", backend, result.text[:60])
# ---------------------------------------------------------------------------
# 5. Timing
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_timing_precision_and_monotonicity(backend, medium_sample):
"""Timestamps have sub-second precision and are monotonically non-decreasing."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
await h.feed(medium_sample.path, speed=0, chunk_duration=1.0)
await h.drain(5.0)
# Add silence to test timing across silence boundary
await h.silence(7.0, speed=0)
await h.drain(3.0)
result = await h.finish(timeout=60)
# Sub-second precision (format is "H:MM:SS.cc")
has_subsecond = any(
"." in line.get(key, "")
for line in result.lines
for key in ("start", "end")
)
assert has_subsecond, f"No sub-second precision for {backend}: {result.lines}"
assert result.timing_valid, f"Invalid timing: {result.timing_errors()}"
assert result.timing_monotonic, f"Non-monotonic: {result.timing_errors()}"
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_silence_timing_reflects_pause(backend, short_sample):
"""Silence segment duration should roughly match the injected pause duration."""
from whisperlivekit.test_harness import TestHarness
pause_duration = 8.0
async with TestHarness(**backend_kwargs(backend)) as h:
await h.feed(short_sample.path, speed=0)
await h.drain(3.0)
await h.pause(pause_duration, speed=0)
await h.drain(3.0)
result = await h.finish(timeout=60)
assert result.has_silence, f"No silence detected for {backend}"
# Check silence segment duration is in the right ballpark
for seg in result.timestamps:
if seg["speaker"] == -2:
seg_duration = seg["end"] - seg["start"]
# Allow generous tolerance (VAC detection + processing lag)
assert seg_duration > pause_duration * 0.3, (
f"Silence too short for {backend}: {seg_duration:.1f}s "
f"vs {pause_duration}s pause"
)
logger.info("[%s] silence timing OK", backend)
# ---------------------------------------------------------------------------
# 6. State Inspection
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_snapshot_history(backend, medium_sample):
"""Historical snapshots capture growing state at different audio positions."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
await h.feed(medium_sample.path, speed=2.0, chunk_duration=0.5)
await h.drain(5.0)
await h.finish(timeout=60)
# Should have multiple history entries
assert len(h.history) >= 2, f"Too few history entries: {len(h.history)}"
# Early snapshot should have less (or equal) text than late snapshot
early = h.snapshot_at(2.0)
late = h.snapshot_at(medium_sample.duration)
if early and late and early.audio_position < late.audio_position:
assert len(late.text) >= len(early.text), (
f"Late snapshot has less text than early for {backend}"
)
logger.info("[%s] snapshots: %d history entries", backend, len(h.history))
# ---------------------------------------------------------------------------
# 7. Metrics
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("backend", AVAILABLE_BACKENDS)
@pytest.mark.asyncio
async def test_metrics_collected(backend, short_sample):
"""Operational metrics are recorded during processing."""
from whisperlivekit.test_harness import TestHarness
async with TestHarness(**backend_kwargs(backend)) as h:
await h.feed(short_sample.path, speed=0)
await h.drain(3.0)
await h.finish(timeout=60)
m = h.metrics
assert m is not None, "Metrics not available"
assert m.n_chunks_received > 0, "No chunks recorded"
assert m.n_transcription_calls > 0, "No transcription calls"
assert len(m.transcription_durations) > 0, "No transcription durations"
assert m.n_tokens_produced > 0, "No tokens produced"
logger.info(
"[%s] metrics: chunks=%d calls=%d tokens=%d avg_lat=%.1fms",
backend, m.n_chunks_received, m.n_transcription_calls,
m.n_tokens_produced, m.avg_latency_ms,
)
================================================
FILE: whisperlivekit/__init__.py
================================================
from .audio_processor import AudioProcessor
from .config import WhisperLiveKitConfig
from .core import TranscriptionEngine
from .parse_args import parse_args
from .test_client import TranscriptionResult, transcribe_audio
from .test_harness import TestHarness, TestState
from .web.web_interface import get_inline_ui_html, get_web_interface_html
__all__ = [
"WhisperLiveKitConfig",
"TranscriptionEngine",
"AudioProcessor",
"parse_args",
"transcribe_audio",
"TranscriptionResult",
"TestHarness",
"TestState",
"get_web_interface_html",
"get_inline_ui_html",
]
================================================
FILE: whisperlivekit/audio_processor.py
================================================
import asyncio
import logging
import traceback
from time import time
from typing import Any, AsyncGenerator, List, Optional, Union
import numpy as np
from whisperlivekit.core import (
TranscriptionEngine,
online_diarization_factory,
online_factory,
online_translation_factory,
)
from whisperlivekit.ffmpeg_manager import FFmpegManager, FFmpegState
from whisperlivekit.metrics_collector import SessionMetrics
from whisperlivekit.silero_vad_iterator import FixedVADIterator, OnnxWrapper, load_jit_vad
from whisperlivekit.timed_objects import ChangeSpeaker, FrontData, Silence, State
from whisperlivekit.tokens_alignment import TokensAlignment
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
SENTINEL = object() # unique sentinel object for end of stream marker
MIN_DURATION_REAL_SILENCE = 5
async def get_all_from_queue(queue: asyncio.Queue) -> Union[object, Silence, np.ndarray, List[Any]]:
items: List[Any] = []
first_item = await queue.get()
queue.task_done()
if first_item is SENTINEL:
return first_item
if isinstance(first_item, Silence):
return first_item
items.append(first_item)
while True:
if not queue._queue:
break
next_item = queue._queue[0]
if next_item is SENTINEL:
break
if isinstance(next_item, Silence):
break
items.append(await queue.get())
queue.task_done()
if isinstance(items[0], np.ndarray):
return np.concatenate(items)
else: #translation
return items
class AudioProcessor:
"""
Processes audio streams for transcription and diarization.
Handles audio processing, state management, and result formatting.
"""
def __init__(self, **kwargs: Any) -> None:
"""Initialize the audio processor with configuration, models, and state."""
# Extract per-session language override before passing to TranscriptionEngine
session_language = kwargs.pop('language', None)
if 'transcription_engine' in kwargs and isinstance(kwargs['transcription_engine'], TranscriptionEngine):
models = kwargs['transcription_engine']
else:
models = TranscriptionEngine(**kwargs)
# Audio processing settings
self.args = models.args
self.sample_rate = 16000
self.channels = 1
chunk_seconds = self.args.vac_chunk_size if self.args.vac else self.args.min_chunk_size
self.samples_per_sec = int(self.sample_rate * chunk_seconds)
self.bytes_per_sample = 2
self.bytes_per_sec = self.samples_per_sec * self.bytes_per_sample
self.max_bytes_per_sec = 32000 * 5 # 5 seconds of audio at 32 kHz
self.is_pcm_input = self.args.pcm_input
# State management
self.is_stopping: bool = False
self.current_silence: Optional[Silence] = None
self.state: State = State()
self.lock: asyncio.Lock = asyncio.Lock()
self.sep: str = " " # Default separator
self.last_response_content: FrontData = FrontData()
self.tokens_alignment: TokensAlignment = TokensAlignment(self.state, self.args, self.sep)
self.beg_loop: Optional[float] = None
# Models and processing
self.asr: Any = models.asr
self.vac: Optional[FixedVADIterator] = None
if self.args.vac:
if models.vac_session is not None:
vac_model = OnnxWrapper(session=models.vac_session)
self.vac = FixedVADIterator(vac_model)
else:
self.vac = FixedVADIterator(load_jit_vad())
self.ffmpeg_manager: Optional[FFmpegManager] = None
self.ffmpeg_reader_task: Optional[asyncio.Task] = None
self._ffmpeg_error: Optional[str] = None
if not self.is_pcm_input:
self.ffmpeg_manager = FFmpegManager(
sample_rate=self.sample_rate,
channels=self.channels
)
async def handle_ffmpeg_error(error_type: str):
logger.error(f"FFmpeg error: {error_type}")
self._ffmpeg_error = error_type
self.ffmpeg_manager.on_error_callback = handle_ffmpeg_error
self.transcription_queue: Optional[asyncio.Queue] = asyncio.Queue() if self.args.transcription else None
self.diarization_queue: Optional[asyncio.Queue] = asyncio.Queue() if self.args.diarization else None
self.translation_queue: Optional[asyncio.Queue] = asyncio.Queue() if self.args.target_language else None
self.pcm_buffer: bytearray = bytearray()
self.total_pcm_samples: int = 0
self.transcription_task: Optional[asyncio.Task] = None
self.diarization_task: Optional[asyncio.Task] = None
self.translation_task: Optional[asyncio.Task] = None
self.watchdog_task: Optional[asyncio.Task] = None
self.all_tasks_for_cleanup: List[asyncio.Task] = []
self.metrics: SessionMetrics = SessionMetrics()
self.transcription: Optional[Any] = None
self.translation: Optional[Any] = None
self.diarization: Optional[Any] = None
if self.args.transcription:
self.transcription = online_factory(self.args, models.asr, language=session_language)
self.sep = self.transcription.asr.sep
if self.args.diarization:
self.diarization = online_diarization_factory(self.args, models.diarization_model)
if models.translation_model:
self.translation = online_translation_factory(self.args, models.translation_model)
async def _push_silence_event(self) -> None:
if self.transcription_queue:
await self.transcription_queue.put(self.current_silence)
if self.args.diarization and self.diarization_queue:
await self.diarization_queue.put(self.current_silence)
if self.translation_queue:
await self.translation_queue.put(self.current_silence)
async def _begin_silence(self, at_sample: Optional[int] = None) -> None:
if self.current_silence:
return
# Use audio stream time (sample-precise) for accurate silence duration
if at_sample is not None:
audio_t = at_sample / self.sample_rate
else:
audio_t = self.total_pcm_samples / self.sample_rate if self.sample_rate else 0.0
self.current_silence = Silence(
is_starting=True, start=audio_t
)
# Push a separate start-only event so _end_silence won't mutate it
start_event = Silence(is_starting=True, start=audio_t)
if self.transcription_queue:
await self.transcription_queue.put(start_event)
if self.args.diarization and self.diarization_queue:
await self.diarization_queue.put(start_event)
if self.translation_queue:
await self.translation_queue.put(start_event)
async def _end_silence(self, at_sample: Optional[int] = None) -> None:
if not self.current_silence:
return
if at_sample is not None:
audio_t = at_sample / self.sample_rate
else:
audio_t = self.total_pcm_samples / self.sample_rate if self.sample_rate else 0.0
self.current_silence.end = audio_t
self.current_silence.is_starting = False
self.current_silence.has_ended = True
self.current_silence.compute_duration()
self.metrics.n_silence_events += 1
if self.current_silence.duration is not None:
self.metrics.total_silence_duration_s += self.current_silence.duration
if self.current_silence.duration and self.current_silence.duration > MIN_DURATION_REAL_SILENCE:
self.state.new_tokens.append(self.current_silence)
# Push the completed silence as the end event (separate from the start event)
await self._push_silence_event()
self.current_silence = None
async def _enqueue_active_audio(self, pcm_chunk: np.ndarray) -> None:
if pcm_chunk is None or pcm_chunk.size == 0:
return
if self.transcription_queue:
await self.transcription_queue.put(pcm_chunk.copy())
if self.args.diarization and self.diarization_queue:
await self.diarization_queue.put(pcm_chunk.copy())
def _slice_before_silence(self, pcm_array: np.ndarray, chunk_sample_start: int, silence_sample: Optional[int]) -> Optional[np.ndarray]:
if silence_sample is None:
return None
relative_index = int(silence_sample - chunk_sample_start)
if relative_index <= 0:
return None
split_index = min(relative_index, len(pcm_array))
if split_index <= 0:
return None
return pcm_array[:split_index]
def convert_pcm_to_float(self, pcm_buffer: Union[bytes, bytearray]) -> np.ndarray:
"""Convert PCM buffer in s16le format to normalized NumPy array."""
return np.frombuffer(pcm_buffer, dtype=np.int16).astype(np.float32) / 32768.0
async def get_current_state(self) -> State:
"""Get current state."""
async with self.lock:
current_time = time()
remaining_transcription = 0
if self.state.end_buffer > 0:
remaining_transcription = max(0, round(current_time - self.beg_loop - self.state.end_buffer, 1))
remaining_diarization = 0
if self.state.tokens:
latest_end = max(self.state.end_buffer, self.state.tokens[-1].end if self.state.tokens else 0)
remaining_diarization = max(0, round(latest_end - self.state.end_attributed_speaker, 1))
self.state.remaining_time_transcription = remaining_transcription
self.state.remaining_time_diarization = remaining_diarization
return self.state
async def ffmpeg_stdout_reader(self) -> None:
"""Read audio data from FFmpeg stdout and process it into the PCM pipeline."""
beg = time()
while True:
try:
if self.is_stopping:
logger.info("Stopping ffmpeg_stdout_reader due to stopping flag.")
break
state = await self.ffmpeg_manager.get_state() if self.ffmpeg_manager else FFmpegState.STOPPED
if state == FFmpegState.FAILED:
logger.error("FFmpeg is in FAILED state, cannot read data")
break
elif state == FFmpegState.STOPPED:
logger.info("FFmpeg is stopped")
break
elif state != FFmpegState.RUNNING:
await asyncio.sleep(0.1)
continue
current_time = time()
elapsed_time = max(0.0, current_time - beg)
buffer_size = max(int(32000 * elapsed_time), 4096) # dynamic read
beg = current_time
chunk = await self.ffmpeg_manager.read_data(buffer_size)
if not chunk:
# No data currently available
await asyncio.sleep(0.05)
continue
self.pcm_buffer.extend(chunk)
await self.handle_pcm_data()
except asyncio.CancelledError:
logger.info("ffmpeg_stdout_reader cancelled.")
break
except Exception as e:
logger.warning(f"Exception in ffmpeg_stdout_reader: {e}")
logger.debug(f"Traceback: {traceback.format_exc()}")
await asyncio.sleep(0.2)
logger.info("FFmpeg stdout processing finished. Signaling downstream processors if needed.")
if self.transcription_queue:
await self.transcription_queue.put(SENTINEL)
if self.diarization:
await self.diarization_queue.put(SENTINEL)
if self.translation:
await self.translation_queue.put(SENTINEL)
async def _finish_transcription(self) -> None:
"""Call finish() on the online processor to flush remaining tokens."""
if not self.transcription:
return
try:
if hasattr(self.transcription, 'finish'):
final_tokens, end_time = await asyncio.to_thread(self.transcription.finish)
else:
# SimulStreamingOnlineProcessor uses start_silence() → process_iter(is_last=True)
final_tokens, end_time = await asyncio.to_thread(self.transcription.start_silence)
final_tokens = final_tokens or []
if final_tokens:
logger.info(f"Finish flushed {len(final_tokens)} tokens")
self.metrics.n_tokens_produced += len(final_tokens)
_buffer_transcript = self.transcription.get_buffer()
async with self.lock:
self.state.tokens.extend(final_tokens)
self.state.buffer_transcription = _buffer_transcript
self.state.end_buffer = max(self.state.end_buffer, end_time)
self.state.new_tokens.extend(final_tokens)
self.state.new_tokens_buffer = _buffer_transcript
if self.translation_queue:
for token in final_tokens:
await self.translation_queue.put(token)
except Exception as e:
logger.warning(f"Error finishing transcription: {e}")
logger.debug(f"Traceback: {traceback.format_exc()}")
async def transcription_processor(self) -> None:
"""Process audio chunks for transcription."""
cumulative_pcm_duration_stream_time = 0.0
while True:
try:
# Use a timeout so we periodically wake up and refresh the
# buffer state. Streaming backends (e.g. voxtral) may
# produce text tokens asynchronously; without a periodic
# drain, those tokens would sit unread until the next audio
# chunk arrives — causing the frontend to show nothing.
try:
item = await asyncio.wait_for(
get_all_from_queue(self.transcription_queue),
timeout=0.5,
)
except asyncio.TimeoutError:
# No new audio — just refresh buffer for streaming backends
_buffer_transcript = self.transcription.get_buffer()
async with self.lock:
self.state.buffer_transcription = _buffer_transcript
continue
if item is SENTINEL:
logger.debug("Transcription processor received sentinel. Finishing.")
await self._finish_transcription()
break
asr_internal_buffer_duration_s = len(getattr(self.transcription, 'audio_buffer', [])) / self.transcription.SAMPLING_RATE
transcription_lag_s = max(0.0, time() - self.beg_loop - self.state.end_buffer)
asr_processing_logs = f"internal_buffer={asr_internal_buffer_duration_s:.2f}s | lag={transcription_lag_s:.2f}s |"
stream_time_end_of_current_pcm = cumulative_pcm_duration_stream_time
new_tokens = []
current_audio_processed_upto = self.state.end_buffer
if isinstance(item, Silence):
if item.is_starting:
new_tokens, current_audio_processed_upto = await asyncio.to_thread(
self.transcription.start_silence
)
asr_processing_logs += " + Silence starting"
if item.has_ended:
asr_processing_logs += f" + Silence of = {item.duration:.2f}s"
cumulative_pcm_duration_stream_time += item.duration
current_audio_processed_upto = cumulative_pcm_duration_stream_time
self.transcription.end_silence(item.duration, self.state.tokens[-1].end if self.state.tokens else 0)
if self.state.tokens:
asr_processing_logs += f" | last_end = {self.state.tokens[-1].end} |"
logger.info(asr_processing_logs)
new_tokens = new_tokens or []
current_audio_processed_upto = max(current_audio_processed_upto, stream_time_end_of_current_pcm)
elif isinstance(item, ChangeSpeaker):
self.transcription.new_speaker(item)
continue
elif isinstance(item, np.ndarray):
pcm_array = item
logger.info(asr_processing_logs)
cumulative_pcm_duration_stream_time += len(pcm_array) / self.sample_rate
stream_time_end_of_current_pcm = cumulative_pcm_duration_stream_time
self.transcription.insert_audio_chunk(pcm_array, stream_time_end_of_current_pcm)
_t0 = time()
new_tokens, current_audio_processed_upto = await asyncio.to_thread(self.transcription.process_iter)
_dur = time() - _t0
self.metrics.transcription_durations.append(_dur)
self.metrics.n_transcription_calls += 1
new_tokens = new_tokens or []
self.metrics.n_tokens_produced += len(new_tokens)
_buffer_transcript = self.transcription.get_buffer()
buffer_text = _buffer_transcript.text
if new_tokens:
validated_text = self.sep.join([t.text for t in new_tokens])
if buffer_text.startswith(validated_text):
_buffer_transcript.text = buffer_text[len(validated_text):].lstrip()
candidate_end_times = [self.state.end_buffer]
if new_tokens:
candidate_end_times.append(new_tokens[-1].end)
if _buffer_transcript.end is not None:
candidate_end_times.append(_buffer_transcript.end)
candidate_end_times.append(current_audio_processed_upto)
async with self.lock:
self.state.tokens.extend(new_tokens)
self.state.buffer_transcription = _buffer_transcript
self.state.end_buffer = max(candidate_end_times)
self.state.new_tokens.extend(new_tokens)
self.state.new_tokens_buffer = _buffer_transcript
if self.translation_queue:
for token in new_tokens:
await self.translation_queue.put(token)
except Exception as e:
logger.warning(f"Exception in transcription_processor: {e}")
logger.warning(f"Traceback: {traceback.format_exc()}")
if 'pcm_array' in locals() and pcm_array is not SENTINEL : # Check if pcm_array was assigned from queue
self.transcription_queue.task_done()
if self.is_stopping:
logger.info("Transcription processor finishing due to stopping flag.")
if self.diarization_queue:
await self.diarization_queue.put(SENTINEL)
if self.translation_queue:
await self.translation_queue.put(SENTINEL)
logger.info("Transcription processor task finished.")
async def diarization_processor(self) -> None:
while True:
try:
item = await get_all_from_queue(self.diarization_queue)
if item is SENTINEL:
break
elif isinstance(item, Silence):
if item.has_ended:
self.diarization.insert_silence(item.duration)
continue
self.diarization.insert_audio_chunk(item)
diarization_segments = await self.diarization.diarize()
diar_end = 0.0
if diarization_segments:
diar_end = max(getattr(s, "end", 0.0) for s in diarization_segments)
async with self.lock:
self.state.new_diarization = diarization_segments
self.state.end_attributed_speaker = max(self.state.end_attributed_speaker, diar_end)
except Exception as e:
logger.warning(f"Exception in diarization_processor: {e}")
logger.warning(f"Traceback: {traceback.format_exc()}")
logger.info("Diarization processor task finished.")
async def translation_processor(self) -> None:
# the idea is to ignore diarization for the moment. We use only transcription tokens.
# And the speaker is attributed given the segments used for the translation
# in the future we want to have different languages for each speaker etc, so it will be more complex.
while True:
try:
item = await get_all_from_queue(self.translation_queue)
if item is SENTINEL:
logger.debug("Translation processor received sentinel. Finishing.")
break
new_translation = None
new_translation_buffer = None
if isinstance(item, Silence):
if item.is_starting:
new_translation, new_translation_buffer = self.translation.validate_buffer_and_reset()
if item.has_ended:
self.translation.insert_silence(item.duration)
continue
elif isinstance(item, ChangeSpeaker):
new_translation, new_translation_buffer = self.translation.validate_buffer_and_reset()
else:
self.translation.insert_tokens(item)
new_translation, new_translation_buffer = await asyncio.to_thread(self.translation.process)
if new_translation is not None:
async with self.lock:
self.state.new_translation.append(new_translation)
self.state.new_translation_buffer = new_translation_buffer
except Exception as e:
logger.warning(f"Exception in translation_processor: {e}")
logger.warning(f"Traceback: {traceback.format_exc()}")
logger.info("Translation processor task finished.")
async def results_formatter(self) -> AsyncGenerator[FrontData, None]:
"""Format processing results for output."""
while True:
try:
if self._ffmpeg_error:
yield FrontData(status="error", error=f"FFmpeg error: {self._ffmpeg_error}")
self._ffmpeg_error = None
await asyncio.sleep(1)
continue
self.tokens_alignment.update()
lines, buffer_diarization_text, buffer_translation_text = self.tokens_alignment.get_lines(
diarization=self.args.diarization,
translation=bool(self.translation),
current_silence=self.current_silence,
audio_time=self.total_pcm_samples / self.sample_rate if self.sample_rate else None,
)
state = await self.get_current_state()
buffer_transcription_text = state.buffer_transcription.text if state.buffer_transcription else ''
response_status = "active_transcription"
if not lines and not buffer_transcription_text and not buffer_diarization_text:
response_status = "no_audio_detected"
response = FrontData(
status=response_status,
lines=lines,
buffer_transcription=buffer_transcription_text,
buffer_diarization=buffer_diarization_text,
buffer_translation=buffer_translation_text,
remaining_time_transcription=state.remaining_time_transcription,
remaining_time_diarization=state.remaining_time_diarization if self.args.diarization else 0
)
should_push = (response != self.last_response_content)
if should_push:
self.metrics.n_responses_sent += 1
yield response
self.last_response_content = response
if self.is_stopping and self._processing_tasks_done():
logger.info("Results formatter: All upstream processors are done and in stopping state. Terminating.")
return
await asyncio.sleep(0.05)
except Exception:
logger.warning(f"Exception in results_formatter. Traceback: {traceback.format_exc()}")
await asyncio.sleep(0.5)
async def create_tasks(self) -> AsyncGenerator[FrontData, None]:
"""Create and start processing tasks."""
self.all_tasks_for_cleanup = []
processing_tasks_for_watchdog: List[asyncio.Task] = []
# If using FFmpeg (non-PCM input), start it and spawn stdout reader
if not self.is_pcm_input:
success = await self.ffmpeg_manager.start()
if not success:
logger.error("Failed to start FFmpeg manager")
async def error_generator() -> AsyncGenerator[FrontData, None]:
yield FrontData(
status="error",
error="FFmpeg failed to start. Please check that FFmpeg is installed."
)
return error_generator()
self.ffmpeg_reader_task = asyncio.create_task(self.ffmpeg_stdout_reader())
self.all_tasks_for_cleanup.append(self.ffmpeg_reader_task)
processing_tasks_for_watchdog.append(self.ffmpeg_reader_task)
if self.transcription:
self.transcription_task = asyncio.create_task(self.transcription_processor())
self.all_tasks_for_cleanup.append(self.transcription_task)
processing_tasks_for_watchdog.append(self.transcription_task)
if self.diarization:
self.diarization_task = asyncio.create_task(self.diarization_processor())
self.all_tasks_for_cleanup.append(self.diarization_task)
processing_tasks_for_watchdog.append(self.diarization_task)
if self.translation:
self.translation_task = asyncio.create_task(self.translation_processor())
self.all_tasks_for_cleanup.append(self.translation_task)
processing_tasks_for_watchdog.append(self.translation_task)
# Monitor overall system health
self.watchdog_task = asyncio.create_task(self.watchdog(processing_tasks_for_watchdog))
self.all_tasks_for_cleanup.append(self.watchdog_task)
return self.results_formatter()
async def watchdog(self, tasks_to_monitor: List[asyncio.Task]) -> None:
"""Monitors the health of critical processing tasks."""
tasks_remaining: List[asyncio.Task] = [task for task in tasks_to_monitor if task]
while True:
try:
if not tasks_remaining:
logger.info("Watchdog task finishing: all monitored tasks completed.")
return
await asyncio.sleep(10)
for i, task in enumerate(list(tasks_remaining)):
if task.done():
exc = task.exception()
task_name = task.get_name() if hasattr(task, 'get_name') else f"Monitored Task {i}"
if exc:
logger.error(f"{task_name} unexpectedly completed with exception: {exc}")
else:
logger.info(f"{task_name} completed normally.")
tasks_remaining.remove(task)
except asyncio.CancelledError:
logger.info("Watchdog task cancelled.")
break
except Exception as e:
logger.error(f"Error in watchdog task: {e}", exc_info=True)
async def cleanup(self) -> None:
"""Clean up resources when processing is complete."""
logger.info("Starting cleanup of AudioProcessor resources.")
self.is_stopping = True
for task in self.all_tasks_for_cleanup:
if task and not task.done():
task.cancel()
created_tasks = [t for t in self.all_tasks_for_cleanup if t]
if created_tasks:
await asyncio.gather(*created_tasks, return_exceptions=True)
logger.info("All processing tasks cancelled or finished.")
if not self.is_pcm_input and self.ffmpeg_manager:
try:
await self.ffmpeg_manager.stop()
logger.info("FFmpeg manager stopped.")
except Exception as e:
logger.warning(f"Error stopping FFmpeg manager: {e}")
if self.diarization:
self.diarization.close()
# Finalize session metrics
self.metrics.total_audio_duration_s = self.total_pcm_samples / self.sample_rate
self.metrics.log_summary()
logger.info("AudioProcessor cleanup complete.")
def _processing_tasks_done(self) -> bool:
"""Return True when all active processing tasks have completed."""
tasks_to_check = [
self.transcription_task,
self.diarization_task,
self.translation_task,
self.ffmpeg_reader_task,
]
return all(task.done() for task in tasks_to_check if task)
async def process_audio(self, message: Optional[bytes]) -> None:
"""Process incoming audio data."""
if not self.beg_loop:
self.beg_loop = time()
self.metrics.session_start = self.beg_loop
self.current_silence = Silence(start=0.0, is_starting=True)
self.tokens_alignment.beg_loop = self.beg_loop
if not message:
logger.info("Empty audio message received, initiating stop sequence.")
self.is_stopping = True
# Flush any remaining PCM data before signaling end-of-stream
if self.is_pcm_input and self.pcm_buffer:
await self._flush_remaining_pcm()
if self.transcription_queue:
await self.transcription_queue.put(SENTINEL)
if not self.is_pcm_input and self.ffmpeg_manager:
await self.ffmpeg_manager.stop()
return
if self.is_stopping:
logger.warning("AudioProcessor is stopping. Ignoring incoming audio.")
return
self.metrics.n_chunks_received += 1
if self.is_pcm_input:
self.pcm_buffer.extend(message)
await self.handle_pcm_data()
else:
if not self.ffmpeg_manager:
logger.error("FFmpeg manager not initialized for non-PCM input.")
return
success = await self.ffmpeg_manager.write_data(message)
if not success:
ffmpeg_state = await self.ffmpeg_manager.get_state()
if ffmpeg_state == FFmpegState.FAILED:
logger.error("FFmpeg is in FAILED state, cannot process audio")
else:
logger.warning("Failed to write audio data to FFmpeg")
async def handle_pcm_data(self) -> None:
# Without VAC, there's no speech detector to end the initial silence.
# Clear it on the first audio chunk so audio actually gets enqueued.
if not self.args.vac and self.current_silence:
await self._end_silence()
# Process when enough data
if len(self.pcm_buffer) < self.bytes_per_sec:
return
if len(self.pcm_buffer) > self.max_bytes_per_sec:
logger.warning(
f"Audio buffer too large: {len(self.pcm_buffer) / self.bytes_per_sec:.2f}s. "
f"Consider using a smaller model."
)
chunk_size = min(len(self.pcm_buffer), self.max_bytes_per_sec)
aligned_chunk_size = (chunk_size // self.bytes_per_sample) * self.bytes_per_sample
if aligned_chunk_size == 0:
return
pcm_array = self.convert_pcm_to_float(self.pcm_buffer[:aligned_chunk_size])
self.pcm_buffer = self.pcm_buffer[aligned_chunk_size:]
num_samples = len(pcm_array)
chunk_sample_start = self.total_pcm_samples
chunk_sample_end = chunk_sample_start + num_samples
res = None
if self.args.vac:
res = self.vac(pcm_array)
if res is not None:
if "start" in res and self.current_silence:
await self._end_silence(at_sample=res.get("start"))
if "end" in res and not self.current_silence:
pre_silence_chunk = self._slice_before_silence(
pcm_array, chunk_sample_start, res.get("end")
)
if pre_silence_chunk is not None and pre_silence_chunk.size > 0:
await self._enqueue_active_audio(pre_silence_chunk)
await self._begin_silence(at_sample=res.get("end"))
if not self.current_silence:
await self._enqueue_active_audio(pcm_array)
self.total_pcm_samples = chunk_sample_end
if not self.args.transcription and not self.args.diarization:
await asyncio.sleep(0.1)
async def _flush_remaining_pcm(self) -> None:
"""Flush whatever PCM data remains in the buffer, regardless of size threshold."""
if not self.pcm_buffer:
return
aligned_size = (len(self.pcm_buffer) // self.bytes_per_sample) * self.bytes_per_sample
if aligned_size == 0:
return
pcm_array = self.convert_pcm_to_float(self.pcm_buffer[:aligned_size])
self.pcm_buffer = self.pcm_buffer[aligned_size:]
# End any active silence so the audio gets enqueued
if self.current_silence:
await self._end_silence(at_sample=self.total_pcm_samples)
await self._enqueue_active_audio(pcm_array)
self.total_pcm_samples += len(pcm_array)
logger.info(f"Flushed remaining PCM buffer: {len(pcm_array)} samples ({len(pcm_array)/self.sample_rate:.2f}s)")
================================================
FILE: whisperlivekit/backend_support.py
================================================
import importlib.util
import logging
import platform
logger = logging.getLogger(__name__)
def module_available(module_name):
"""Return True if the given module can be imported."""
return importlib.util.find_spec(module_name) is not None
def mlx_backend_available(warn_on_missing = False):
is_macos = platform.system() == "Darwin"
is_arm = platform.machine() == "arm64"
available = (
is_macos
and is_arm
and module_available("mlx_whisper")
)
if not available and warn_on_missing and is_macos and is_arm:
logger.warning(
"=" * 50
+ "\nMLX Whisper not found but you are on Apple Silicon. "
"Consider installing mlx-whisper for better performance: "
"`pip install mlx-whisper`\n"
+ "=" * 50
)
return available
def voxtral_hf_backend_available():
"""Return True if HF Transformers Voxtral backend is available."""
return module_available("transformers")
def faster_backend_available(warn_on_missing = False):
available = module_available("faster_whisper")
if not available and warn_on_missing and platform.system() != "Darwin":
logger.warning(
"=" * 50
+ "\nFaster-Whisper not found. Consider installing faster-whisper "
"for better performance: `pip install faster-whisper`\n"
+ "=" * 50
)
return available
================================================
FILE: whisperlivekit/basic_server.py
================================================
import asyncio
import logging
from contextlib import asynccontextmanager
from typing import List, Optional
from fastapi import FastAPI, File, Form, UploadFile, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from whisperlivekit import AudioProcessor, TranscriptionEngine, get_inline_ui_html, parse_args
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logging.getLogger().setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logging.getLogger("whisperlivekit.qwen3_asr").setLevel(logging.DEBUG)
config = parse_args()
transcription_engine = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global transcription_engine
transcription_engine = TranscriptionEngine(config=config)
yield
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def get():
return HTMLResponse(get_inline_ui_html())
@app.get("/health")
async def health():
"""Health check endpoint."""
global transcription_engine
backend = getattr(transcription_engine.config, "backend", "whisper") if transcription_engine else None
return JSONResponse({
"status": "ok",
"backend": backend,
"ready": transcription_engine is not None,
})
async def handle_websocket_results(websocket, results_generator, diff_tracker=None):
"""Consumes results from the audio processor and sends them via WebSocket."""
try:
async for response in results_generator:
if diff_tracker is not None:
await websocket.send_json(diff_tracker.to_message(response))
else:
await websocket.send_json(response.to_dict())
# when the results_generator finishes it means all audio has been processed
logger.info("Results generator finished. Sending 'ready_to_stop' to client.")
await websocket.send_json({"type": "ready_to_stop"})
except WebSocketDisconnect:
logger.info("WebSocket disconnected while handling results (client likely closed connection).")
except Exception as e:
logger.exception(f"Error in WebSocket results handler: {e}")
@app.websocket("/asr")
async def websocket_endpoint(websocket: WebSocket):
global transcription_engine
# Read per-session options from query parameters
session_language = websocket.query_params.get("language", None)
mode = websocket.query_params.get("mode", "full")
audio_processor = AudioProcessor(
transcription_engine=transcription_engine,
language=session_language,
)
await websocket.accept()
logger.info(
"WebSocket connection opened.%s",
f" language={session_language}" if session_language else "",
)
diff_tracker = None
if mode == "diff":
from whisperlivekit.diff_protocol import DiffTracker
diff_tracker = DiffTracker()
logger.info("Client requested diff mode")
try:
await websocket.send_json({"type": "config", "useAudioWorklet": bool(config.pcm_input), "mode": mode})
except Exception as e:
logger.warning(f"Failed to send config to client: {e}")
results_generator = await audio_processor.create_tasks()
websocket_task = asyncio.create_task(handle_websocket_results(websocket, results_generator, diff_tracker))
try:
while True:
message = await websocket.receive_bytes()
await audio_processor.process_audio(message)
except KeyError as e:
if 'bytes' in str(e):
logger.warning("Client has closed the connection.")
else:
logger.error(f"Unexpected KeyError in websocket_endpoint: {e}", exc_info=True)
except WebSocketDisconnect:
logger.info("WebSocket disconnected by client during message receiving loop.")
except Exception as e:
logger.error(f"Unexpected error in websocket_endpoint main loop: {e}", exc_info=True)
finally:
logger.info("Cleaning up WebSocket endpoint...")
if not websocket_task.done():
websocket_task.cancel()
try:
await websocket_task
except asyncio.CancelledError:
logger.info("WebSocket results handler task was cancelled.")
except Exception as e:
logger.warning(f"Exception while awaiting websocket_task completion: {e}")
await audio_processor.cleanup()
logger.info("WebSocket endpoint cleaned up successfully.")
# ---------------------------------------------------------------------------
# Deepgram-compatible WebSocket API (/v1/listen)
# ---------------------------------------------------------------------------
@app.websocket("/v1/listen")
async def deepgram_websocket_endpoint(websocket: WebSocket):
"""Deepgram-compatible live transcription WebSocket."""
global transcription_engine
from whisperlivekit.deepgram_compat import handle_deepgram_websocket
await handle_deepgram_websocket(websocket, transcription_engine, config)
# ---------------------------------------------------------------------------
# OpenAI-compatible REST API (/v1/audio/transcriptions)
# ---------------------------------------------------------------------------
async def _convert_to_pcm(audio_bytes: bytes) -> bytes:
"""Convert any audio format to PCM s16le mono 16kHz using ffmpeg."""
proc = await asyncio.create_subprocess_exec(
"ffmpeg", "-i", "pipe:0",
"-f", "s16le", "-acodec", "pcm_s16le",
"-ar", "16000", "-ac", "1",
"-loglevel", "error",
"pipe:1",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate(input=audio_bytes)
if proc.returncode != 0:
from fastapi import HTTPException
raise HTTPException(status_code=400, detail=f"Audio conversion failed: {stderr.decode().strip()}")
return stdout
def _parse_time_str(time_str: str) -> float:
"""Parse 'H:MM:SS.cc' to seconds."""
parts = time_str.split(":")
if len(parts) == 3:
return int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2])
if len(parts) == 2:
return int(parts[0]) * 60 + float(parts[1])
return float(parts[0])
def _format_openai_response(front_data, response_format: str, language: Optional[str], duration: float) -> dict:
"""Convert FrontData to OpenAI-compatible response."""
d = front_data.to_dict()
lines = d.get("lines", [])
# Combine all speech text (exclude silence segments)
text_parts = [l["text"] for l in lines if l.get("text") and l.get("speaker", 0) != -2]
full_text = " ".join(text_parts).strip()
if response_format == "text":
return full_text
# Build segments and words for verbose_json
segments = []
words = []
for i, line in enumerate(lines):
if line.get("speaker") == -2 or not line.get("text"):
continue
start = _parse_time_str(line.get("start", "0:00:00"))
end = _parse_time_str(line.get("end", "0:00:00"))
segments.append({
"id": len(segments),
"start": round(start, 2),
"end": round(end, 2),
"text": line["text"],
})
# Split segment text into approximate words with estimated timestamps
seg_words = line["text"].split()
if seg_words:
word_duration = (end - start) / max(len(seg_words), 1)
for j, word in enumerate(seg_words):
words.append({
"word": word,
"start": round(start + j * word_duration, 2),
"end": round(start + (j + 1) * word_duration, 2),
})
if response_format == "verbose_json":
return {
"task": "transcribe",
"language": language or "unknown",
"duration": round(duration, 2),
"text": full_text,
"words": words,
"segments": segments,
}
if response_format in ("srt", "vtt"):
lines_out = []
if response_format == "vtt":
lines_out.append("WEBVTT\n")
for i, seg in enumerate(segments):
start_ts = _srt_timestamp(seg["start"], response_format)
end_ts = _srt_timestamp(seg["end"], response_format)
if response_format == "srt":
lines_out.append(f"{i + 1}")
lines_out.append(f"{start_ts} --> {end_ts}")
lines_out.append(seg["text"])
lines_out.append("")
return "\n".join(lines_out)
# Default: json
return {"text": full_text}
def _srt_timestamp(seconds: float, fmt: str) -> str:
"""Format seconds as SRT (HH:MM:SS,mmm) or VTT (HH:MM:SS.mmm) timestamp."""
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
ms = int(round((seconds % 1) * 1000))
sep = "," if fmt == "srt" else "."
return f"{h:02d}:{m:02d}:{s:02d}{sep}{ms:03d}"
@app.post("/v1/audio/transcriptions")
async def create_transcription(
file: UploadFile = File(...),
model: str = Form(default=""),
language: Optional[str] = Form(default=None),
prompt: str = Form(default=""),
response_format: str = Form(default="json"),
timestamp_granularities: Optional[List[str]] = Form(default=None),
):
"""OpenAI-compatible audio transcription endpoint.
Accepts the same parameters as OpenAI's /v1/audio/transcriptions API.
The `model` parameter is accepted but ignored (uses the server's configured backend).
"""
global transcription_engine
audio_bytes = await file.read()
if not audio_bytes:
from fastapi import HTTPException
raise HTTPException(status_code=400, detail="Empty audio file")
# Convert to PCM for pipeline processing
pcm_data = await _convert_to_pcm(audio_bytes)
duration = len(pcm_data) / (16000 * 2) # 16kHz, 16-bit
# Process through the full pipeline
processor = AudioProcessor(
transcription_engine=transcription_engine,
language=language,
)
# Force PCM input regardless of server config
processor.is_pcm_input = True
results_gen = await processor.create_tasks()
# Collect results in background while feeding audio
final_result = None
async def collect():
nonlocal final_result
async for result in results_gen:
final_result = result
collect_task = asyncio.create_task(collect())
# Feed audio in chunks (1 second each)
chunk_size = 16000 * 2 # 1 second of PCM
for i in range(0, len(pcm_data), chunk_size):
await processor.process_audio(pcm_data[i:i + chunk_size])
# Signal end of audio
await processor.process_audio(b"")
# Wait for pipeline to finish
try:
await asyncio.wait_for(collect_task, timeout=120.0)
except asyncio.TimeoutError:
logger.warning("Transcription timed out after 120s")
finally:
await processor.cleanup()
if final_result is None:
return JSONResponse({"text": ""})
result = _format_openai_response(final_result, response_format, language, duration)
if isinstance(result, str):
return PlainTextResponse(result)
return JSONResponse(result)
@app.get("/v1/models")
async def list_models():
"""OpenAI-compatible model listing endpoint."""
global transcription_engine
backend = getattr(transcription_engine.config, "backend", "whisper") if transcription_engine else "whisper"
model_size = getattr(transcription_engine.config, "model_size", "base") if transcription_engine else "base"
return JSONResponse({
"object": "list",
"data": [{
"id": f"{backend}/{model_size}" if backend != "whisper" else f"whisper-{model_size}",
"object": "model",
"owned_by": "whisperlivekit",
}],
})
def main():
"""Entry point for the CLI command."""
import uvicorn
from whisperlivekit.cli import print_banner
ssl = bool(config.ssl_certfile and config.ssl_keyfile)
print_banner(config, config.host, config.port, ssl=ssl)
uvicorn_kwargs = {
"app": "whisperlivekit.basic_server:app",
"host": config.host,
"port": config.port,
"reload": False,
"log_level": "info",
"lifespan": "on",
}
ssl_kwargs = {}
if config.ssl_certfile or config.ssl_keyfile:
if not (config.ssl_certfile and config.ssl_keyfile):
raise ValueError("Both --ssl-certfile and --ssl-keyfile must be specified together.")
ssl_kwargs = {
"ssl_certfile": config.ssl_certfile,
"ssl_keyfile": config.ssl_keyfile,
}
if ssl_kwargs:
uvicorn_kwargs = {**uvicorn_kwargs, **ssl_kwargs}
if config.forwarded_allow_ips:
uvicorn_kwargs = {**uvicorn_kwargs, "forwarded_allow_ips": config.forwarded_allow_ips}
uvicorn.run(**uvicorn_kwargs)
if __name__ == "__main__":
main()
================================================
FILE: whisperlivekit/benchmark/__init__.py
================================================
"""WhisperLiveKit benchmark suite.
Comprehensive benchmarking of ASR backends using public datasets,
run through the same pipeline as real-time streaming.
Usage:
wlk bench # benchmark current backend
wlk bench --backend whisper --json results.json
wlk bench --languages en,fr,es # multilingual
wlk bench --quick # fast subset
Programmatic:
from whisperlivekit.benchmark import BenchmarkRunner
import asyncio
runner = BenchmarkRunner(backend="whisper", model_size="base")
report = asyncio.run(runner.run())
print(report.summary_table())
"""
from whisperlivekit.benchmark.datasets import (
BENCHMARK_CATALOG,
get_benchmark_samples,
)
from whisperlivekit.benchmark.metrics import BenchmarkReport, SampleResult
from whisperlivekit.benchmark.runner import BenchmarkRunner
__all__ = [
"BENCHMARK_CATALOG",
"BenchmarkReport",
"BenchmarkRunner",
"SampleResult",
"get_benchmark_samples",
]
================================================
FILE: whisperlivekit/benchmark/compat.py
================================================
"""Backend detection and language compatibility matrix."""
import logging
from typing import Dict, List, Optional, Set
logger = logging.getLogger(__name__)
# Language support per backend.
# None means all Whisper-supported languages.
# A set means only those languages are supported.
BACKEND_LANGUAGES: Dict[str, Optional[Set[str]]] = {
"whisper": None,
"faster-whisper": None,
"mlx-whisper": None,
"voxtral-mlx": None,
"voxtral": None,
"qwen3": {
"zh", "en", "yue", "ar", "de", "fr", "es", "pt", "id", "it",
"ko", "ru", "th", "vi", "ja", "tr", "hi", "ms", "nl", "sv",
"da", "fi", "pl", "cs", "fa", "el", "hu", "mk", "ro",
},
"qwen3-simul": {
"zh", "en", "yue", "ar", "de", "fr", "es", "pt", "id", "it",
"ko", "ru", "th", "vi", "ja", "tr", "hi", "ms", "nl", "sv",
"da", "fi", "pl", "cs", "fa", "el", "hu", "mk", "ro",
},
}
def backend_supports_language(backend: str, language: str) -> bool:
"""Check if a backend supports a given language code."""
langs = BACKEND_LANGUAGES.get(backend)
if langs is None:
return True
return language in langs
def detect_available_backends() -> List[str]:
"""Probe which ASR backends are importable."""
backends = []
try:
import whisper # noqa: F401
backends.append("whisper")
except ImportError:
pass
try:
import faster_whisper # noqa: F401
backends.append("faster-whisper")
except ImportError:
pass
try:
import mlx_whisper # noqa: F401
backends.append("mlx-whisper")
except ImportError:
pass
try:
import mlx.core # noqa: F401
from whisperlivekit.voxtral_mlx.loader import load_voxtral_model # noqa: F401
backends.append("voxtral-mlx")
except ImportError:
pass
try:
from transformers import VoxtralRealtimeForConditionalGeneration # noqa: F401
backends.append("voxtral")
except ImportError:
pass
try:
from whisperlivekit.qwen3_asr import _patch_transformers_compat
_patch_transformers_compat()
from qwen_asr import Qwen3ASRModel # noqa: F401
backends.append("qwen3")
backends.append("qwen3-simul")
except (ImportError, Exception):
pass
return backends
def resolve_backend(backend: str) -> str:
"""Resolve 'auto' to the best available backend."""
if backend != "auto":
return backend
available = detect_available_backends()
if not available:
raise RuntimeError(
"No ASR backend available. Install at least one: "
"pip install openai-whisper, faster-whisper, or mlx-whisper"
)
# Priority order
priority = [
"faster-whisper", "mlx-whisper", "voxtral-mlx", "voxtral",
"qwen3", "qwen3-simul", "whisper",
]
for p in priority:
if p in available:
return p
return available[0]
================================================
FILE: whisperlivekit/benchmark/datasets.py
================================================
"""Benchmark audio datasets from public HuggingFace repositories.
Downloads curated samples across languages, noise conditions, and speaker
configurations. All datasets are public and freely accessible — no auth
tokens required.
Samples are cached in ~/.cache/whisperlivekit/benchmark_data/ and reused
across benchmark runs.
Datasets used:
- LibriSpeech test-clean (English, clean, single speaker)
- LibriSpeech test-other (English, noisy/hard, single speaker)
- Multilingual LibriSpeech (French, Spanish, German, Portuguese, Italian, Polish, Dutch)
- AMI (English, multi-speaker meeting)
"""
import json
import logging
import wave
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Set
import numpy as np
logger = logging.getLogger(__name__)
CACHE_DIR = Path.home() / ".cache" / "whisperlivekit" / "benchmark_data"
METADATA_FILE = "benchmark_metadata.json"
@dataclass
class BenchmarkSample:
"""A benchmark audio sample with metadata and ground truth."""
name: str
path: str
reference: str
duration: float
language: str
category: str # "clean", "noisy", "multilingual", "meeting"
sample_rate: int = 16000
n_speakers: int = 1
source: str = ""
tags: Set[str] = field(default_factory=set)
def to_dict(self) -> Dict:
return {
"name": self.name,
"file": Path(self.path).name,
"reference": self.reference,
"duration": self.duration,
"language": self.language,
"category": self.category,
"sample_rate": self.sample_rate,
"n_speakers": self.n_speakers,
"source": self.source,
"tags": list(self.tags),
}
# ---------------------------------------------------------------------------
# Dataset catalog — defines what to download
# ---------------------------------------------------------------------------
BENCHMARK_CATALOG = {
# English clean (LibriSpeech test-clean)
"en_clean_short": {
"dataset": "openslr/librispeech_asr",
"config": "clean",
"split": "test",
"language": "en",
"category": "clean",
"n_samples": 1,
"skip": 0,
"tags": {"short"},
},
"en_clean_medium": {
"dataset": "openslr/librispeech_asr",
"config": "clean",
"split": "test",
"language": "en",
"category": "clean",
"n_samples": 1,
"skip": 1,
"tags": {"medium"},
},
# English noisy (LibriSpeech test-other)
"en_noisy_1": {
"dataset": "openslr/librispeech_asr",
"config": "other",
"split": "test",
"language": "en",
"category": "noisy",
"n_samples": 1,
"skip": 0,
"tags": {"accented"},
},
"en_noisy_2": {
"dataset": "openslr/librispeech_asr",
"config": "other",
"split": "test",
"language": "en",
"category": "noisy",
"n_samples": 1,
"skip": 1,
"tags": {"accented"},
},
# French (Multilingual LibriSpeech)
"fr_clean_1": {
"dataset": "facebook/multilingual_librispeech",
"config": "french",
"split": "test",
"language": "fr",
"category": "multilingual",
"n_samples": 1,
"skip": 0,
"tags": set(),
},
"fr_clean_2": {
"dataset": "facebook/multilingual_librispeech",
"config": "french",
"split": "test",
"language": "fr",
"category": "multilingual",
"n_samples": 1,
"skip": 1,
"tags": set(),
},
# Spanish (Multilingual LibriSpeech)
"es_clean_1": {
"dataset": "facebook/multilingual_librispeech",
"config": "spanish",
"split": "test",
"language": "es",
"category": "multilingual",
"n_samples": 1,
"skip": 0,
"tags": set(),
},
# German (Multilingual LibriSpeech)
"de_clean_1": {
"dataset": "facebook/multilingual_librispeech",
"config": "german",
"split": "test",
"language": "de",
"category": "multilingual",
"n_samples": 1,
"skip": 0,
"tags": set(),
},
# Portuguese (Multilingual LibriSpeech)
"pt_clean_1": {
"dataset": "facebook/multilingual_librispeech",
"config": "portuguese",
"split": "test",
"language": "pt",
"category": "multilingual",
"n_samples": 1,
"skip": 0,
"tags": set(),
},
# Italian (Multilingual LibriSpeech)
"it_clean_1": {
"dataset": "facebook/multilingual_librispeech",
"config": "italian",
"split": "test",
"language": "it",
"category": "multilingual",
"n_samples": 1,
"skip": 0,
"tags": set(),
},
# Polish (Multilingual LibriSpeech)
"pl_clean_1": {
"dataset": "facebook/multilingual_librispeech",
"config": "polish",
"split": "test",
"language": "pl",
"category": "multilingual",
"n_samples": 1,
"skip": 0,
"tags": set(),
},
# Dutch (Multilingual LibriSpeech)
"nl_clean_1": {
"dataset": "facebook/multilingual_librispeech",
"config": "dutch",
"split": "test",
"language": "nl",
"category": "multilingual",
"n_samples": 1,
"skip": 0,
"tags": set(),
},
# English multi-speaker meeting (AMI)
"en_meeting": {
"dataset": "edinburghcstr/ami",
"config": "ihm",
"split": "test",
"language": "en",
"category": "meeting",
"n_samples": 1,
"skip": 0,
"tags": {"multi_speaker", "long"},
"max_duration": 60.0,
},
}
# Quick mode: subset of samples for fast smoke tests
QUICK_SAMPLES = {"en_clean_short", "en_clean_medium", "en_noisy_1", "fr_clean_1"}
# ---------------------------------------------------------------------------
# Audio utilities
# ---------------------------------------------------------------------------
def _save_wav(path: Path, audio: np.ndarray, sample_rate: int = 16000) -> None:
if audio.ndim > 1:
audio = audio.mean(axis=-1)
if audio.dtype in (np.float32, np.float64):
audio = np.clip(audio, -1.0, 1.0)
audio = (audio * 32767).astype(np.int16)
elif audio.dtype != np.int16:
audio = audio.astype(np.int16)
path.parent.mkdir(parents=True, exist_ok=True)
with wave.open(str(path), "w") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(audio.tobytes())
def _decode_audio(audio_bytes: bytes) -> tuple:
import io
import soundfile as sf
audio_array, sr = sf.read(io.BytesIO(audio_bytes), dtype="float32")
return np.array(audio_array, dtype=np.float32), sr
def _ensure_datasets():
try:
import datasets # noqa: F401
except ImportError:
raise ImportError(
"The 'datasets' package is required for benchmark data. "
"Install with: pip install whisperlivekit[test]"
)
# ---------------------------------------------------------------------------
# Download functions per dataset type
# ---------------------------------------------------------------------------
def _download_librispeech(config: str, n_samples: int, skip: int,
category: str, language: str,
prefix: str) -> List[Dict]:
"""Download from openslr/librispeech_asr (clean or other)."""
_ensure_datasets()
import datasets.config
datasets.config.TORCHCODEC_AVAILABLE = False
from datasets import Audio, load_dataset
logger.info("Downloading LibriSpeech %s samples...", config)
ds = load_dataset(
"openslr/librispeech_asr", config, split="test", streaming=True,
)
ds = ds.cast_column("audio", Audio(decode=False))
samples = []
for i, item in enumerate(ds):
if i < skip:
continue
if len(samples) >= n_samples:
break
audio_array, sr = _decode_audio(item["audio"]["bytes"])
duration = len(audio_array) / sr
text = item["text"]
wav_name = f"{prefix}_{i}.wav"
_save_wav(CACHE_DIR / wav_name, audio_array, sr)
samples.append({
"file": wav_name,
"reference": text,
"duration": round(duration, 2),
"sample_rate": sr,
"language": language,
"category": category,
"n_speakers": 1,
"source": f"openslr/librispeech_asr ({config})",
})
logger.info(" %.1fs - %s", duration, text[:60])
return samples
def _download_mls(config: str, n_samples: int, skip: int,
language: str, prefix: str) -> List[Dict]:
"""Download from facebook/multilingual_librispeech."""
_ensure_datasets()
import datasets.config
datasets.config.TORCHCODEC_AVAILABLE = False
from datasets import Audio, load_dataset
logger.info("Downloading MLS %s samples...", config)
ds = load_dataset(
"facebook/multilingual_librispeech", config, split="test", streaming=True,
)
ds = ds.cast_column("audio", Audio(decode=False))
samples = []
for i, item in enumerate(ds):
if i < skip:
continue
if len(samples) >= n_samples:
break
audio_array, sr = _decode_audio(item["audio"]["bytes"])
duration = len(audio_array) / sr
text = item.get("text", item.get("transcript", ""))
wav_name = f"{prefix}_{i}.wav"
_save_wav(CACHE_DIR / wav_name, audio_array, sr)
samples.append({
"file": wav_name,
"reference": text,
"duration": round(duration, 2),
"sample_rate": sr,
"language": language,
"category": "multilingual",
"n_speakers": 1,
"source": f"facebook/multilingual_librispeech ({config})",
})
logger.info(" [%s] %.1fs - %s", language, duration, text[:60])
return samples
def _download_fleurs(config: str, n_samples: int, skip: int,
language: str, prefix: str) -> List[Dict]:
"""Download from google/fleurs."""
_ensure_datasets()
import datasets.config
datasets.config.TORCHCODEC_AVAILABLE = False
from datasets import Audio, load_dataset
logger.info("Downloading FLEURS %s samples...", config)
ds = load_dataset(
"google/fleurs", config, split="test", streaming=True,
)
ds = ds.cast_column("audio", Audio(decode=False))
samples = []
for i, item in enumerate(ds):
if i < skip:
continue
if len(samples) >= n_samples:
break
audio_array, sr = _decode_audio(item["audio"]["bytes"])
duration = len(audio_array) / sr
text = item.get("transcription", item.get("raw_transcription", ""))
wav_name = f"{prefix}_{i}.wav"
_save_wav(CACHE_DIR / wav_name, audio_array, sr)
samples.append({
"file": wav_name,
"reference": text,
"duration": round(duration, 2),
"sample_rate": sr,
"language": language,
"category": "multilingual",
"n_speakers": 1,
"source": f"google/fleurs ({config})",
})
logger.info(" [%s] %.1fs - %s", language, duration, text[:60])
return samples
def _download_ami(max_duration: float = 60.0) -> List[Dict]:
"""Download one AMI meeting segment with multiple speakers."""
_ensure_datasets()
import datasets.config
datasets.config.TORCHCODEC_AVAILABLE = False
from datasets import Audio, load_dataset
logger.info("Downloading AMI meeting sample...")
ds = load_dataset("edinburghcstr/ami", "ihm", split="test", streaming=True)
ds = ds.cast_column("audio", Audio(decode=False))
meeting_id = None
audio_arrays = []
texts = []
sample_rate = None
for item in ds:
mid = item.get("meeting_id", "unknown")
if meeting_id is None:
meeting_id = mid
elif mid != meeting_id:
break
audio_array, sr = _decode_audio(item["audio"]["bytes"])
sample_rate = sr
texts.append(item.get("text", ""))
audio_arrays.append(audio_array)
total_dur = sum(len(a) / sr for a in audio_arrays)
if total_dur > max_duration:
break
if not audio_arrays:
return []
full_audio = np.concatenate(audio_arrays)
duration = len(full_audio) / sample_rate
reference = " ".join(t for t in texts if t)
wav_name = "ami_meeting.wav"
_save_wav(CACHE_DIR / wav_name, full_audio, sample_rate)
logger.info(" AMI meeting: %.1fs, %d utterances", duration, len(texts))
return [{
"file": wav_name,
"reference": reference,
"duration": round(duration, 2),
"sample_rate": sample_rate,
"language": "en",
"category": "meeting",
"n_speakers": 4,
"source": f"edinburghcstr/ami (ihm, meeting {meeting_id})",
}]
# ---------------------------------------------------------------------------
# Dispatcher — routes catalog entries to download functions
# ---------------------------------------------------------------------------
def _download_catalog_entry(name: str, spec: Dict) -> List[Dict]:
"""Download a single catalog entry and return metadata dicts."""
dataset = spec["dataset"]
config = spec.get("config", "")
n_samples = spec.get("n_samples", 1)
skip = spec.get("skip", 0)
language = spec["language"]
category = spec["category"]
if dataset == "openslr/librispeech_asr":
return _download_librispeech(
config=config, n_samples=n_samples, skip=skip,
category=category, language=language, prefix=name,
)
elif dataset == "facebook/multilingual_librispeech":
return _download_mls(
config=config, n_samples=n_samples, skip=skip,
language=language, prefix=name,
)
elif dataset == "google/fleurs":
return _download_fleurs(
config=config, n_samples=n_samples, skip=skip,
language=language, prefix=name,
)
elif dataset == "edinburghcstr/ami":
return _download_ami(max_duration=spec.get("max_duration", 60.0))
else:
logger.warning("Unknown dataset: %s", dataset)
return []
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def get_benchmark_samples(
languages: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
quick: bool = False,
force: bool = False,
) -> List[BenchmarkSample]:
"""Download and return benchmark samples, filtered by language/category.
Args:
languages: List of language codes to include (None = all).
categories: List of categories to include (None = all).
quick: If True, only download a small subset for smoke tests.
force: Re-download even if cached.
Returns:
List of BenchmarkSample objects ready for benchmarking.
"""
CACHE_DIR.mkdir(parents=True, exist_ok=True)
meta_path = CACHE_DIR / METADATA_FILE
# Load cached metadata
cached = {}
if meta_path.exists() and not force:
cached = json.loads(meta_path.read_text())
# Determine which entries to download
entries = BENCHMARK_CATALOG
if quick:
entries = {k: v for k, v in entries.items() if k in QUICK_SAMPLES}
if languages:
lang_set = set(languages)
entries = {k: v for k, v in entries.items() if v["language"] in lang_set}
if categories:
cat_set = set(categories)
entries = {k: v for k, v in entries.items() if v["category"] in cat_set}
# Download missing entries
all_meta = cached.get("samples", {})
for name, spec in entries.items():
if name in all_meta and not force:
# Check file exists
file_path = CACHE_DIR / all_meta[name][0]["file"]
if file_path.exists():
continue
logger.info("Downloading benchmark sample: %s", name)
try:
downloaded = _download_catalog_entry(name, spec)
if downloaded:
all_meta[name] = downloaded
except Exception as e:
logger.warning("Failed to download %s: %s", name, e)
# Save metadata
meta_path.write_text(json.dumps({"samples": all_meta}, indent=2))
# Build BenchmarkSample objects
samples = []
for name, spec in entries.items():
if name not in all_meta:
continue
for meta in all_meta[name]:
file_path = CACHE_DIR / meta["file"]
if not file_path.exists():
continue
catalog_entry = BENCHMARK_CATALOG.get(name, {})
samples.append(BenchmarkSample(
name=name,
path=str(file_path),
reference=meta["reference"],
duration=meta["duration"],
language=meta["language"],
category=meta["category"],
sample_rate=meta.get("sample_rate", 16000),
n_speakers=meta.get("n_speakers", 1),
source=meta.get("source", ""),
tags=set(catalog_entry.get("tags", set())),
))
logger.info("Loaded %d benchmark samples", len(samples))
return samples
================================================
FILE: whisperlivekit/benchmark/metrics.py
================================================
"""Benchmark result data structures and aggregation."""
import platform
import subprocess
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@dataclass
class SampleResult:
"""Result from benchmarking one audio sample."""
sample_name: str
language: str
category: str
duration_s: float
# Quality
wer: float
wer_details: Dict[str, int]
# Speed
processing_time_s: float
rtf: float
# Latency (from SessionMetrics)
avg_latency_ms: float = 0.0
p95_latency_ms: float = 0.0
n_transcription_calls: int = 0
# Pipeline stats
n_lines: int = 0
n_tokens: int = 0
# Timing quality
timing_valid: bool = True
timing_monotonic: bool = True
# Memory
peak_memory_mb: Optional[float] = None
# Texts
hypothesis: str = ""
reference: str = ""
# Source
source: str = ""
tags: List[str] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"sample": self.sample_name,
"language": self.language,
"category": self.category,
"duration_s": round(self.duration_s, 2),
"wer": round(self.wer, 4),
"wer_details": self.wer_details,
"processing_time_s": round(self.processing_time_s, 2),
"rtf": round(self.rtf, 3),
"avg_latency_ms": round(self.avg_latency_ms, 1),
"p95_latency_ms": round(self.p95_latency_ms, 1),
"n_transcription_calls": self.n_transcription_calls,
"n_lines": self.n_lines,
"n_tokens": self.n_tokens,
"timing_valid": self.timing_valid,
"timing_monotonic": self.timing_monotonic,
"peak_memory_mb": round(self.peak_memory_mb, 1) if self.peak_memory_mb else None,
"hypothesis": self.hypothesis,
"reference": self.reference,
"source": self.source,
"tags": self.tags,
}
@dataclass
class BenchmarkReport:
"""Aggregated benchmark report with system info and per-sample results."""
backend: str
model_size: str
timestamp: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%S"))
system_info: Dict[str, Any] = field(default_factory=dict)
results: List[SampleResult] = field(default_factory=list)
# --- Aggregate properties ---
@property
def n_samples(self) -> int:
return len(self.results)
@property
def total_audio_s(self) -> float:
return sum(r.duration_s for r in self.results)
@property
def total_processing_s(self) -> float:
return sum(r.processing_time_s for r in self.results)
@property
def avg_wer(self) -> float:
if not self.results:
return 0.0
return sum(r.wer for r in self.results) / len(self.results)
@property
def weighted_wer(self) -> float:
"""Micro-averaged WER: total errors / total reference words."""
total_errors = sum(
r.wer_details.get("substitutions", 0) +
r.wer_details.get("insertions", 0) +
r.wer_details.get("deletions", 0)
for r in self.results
)
total_ref = sum(r.wer_details.get("ref_words", 0) for r in self.results)
return total_errors / max(total_ref, 1)
@property
def avg_rtf(self) -> float:
if not self.results:
return 0.0
return sum(r.rtf for r in self.results) / len(self.results)
@property
def overall_rtf(self) -> float:
if self.total_audio_s <= 0:
return 0.0
return self.total_processing_s / self.total_audio_s
@property
def avg_latency_ms(self) -> float:
vals = [r.avg_latency_ms for r in self.results if r.avg_latency_ms > 0]
return sum(vals) / len(vals) if vals else 0.0
@property
def p95_latency_ms(self) -> float:
vals = [r.p95_latency_ms for r in self.results if r.p95_latency_ms > 0]
return sum(vals) / len(vals) if vals else 0.0
# --- Per-dimension breakdowns ---
def _group_by(self, key: str) -> Dict[str, List[SampleResult]]:
groups: Dict[str, List[SampleResult]] = {}
for r in self.results:
k = getattr(r, key, "unknown")
groups.setdefault(k, []).append(r)
return groups
def wer_by_language(self) -> Dict[str, float]:
return {
lang: sum(r.wer for r in group) / len(group)
for lang, group in sorted(self._group_by("language").items())
}
def rtf_by_language(self) -> Dict[str, float]:
return {
lang: sum(r.rtf for r in group) / len(group)
for lang, group in sorted(self._group_by("language").items())
}
def wer_by_category(self) -> Dict[str, float]:
return {
cat: sum(r.wer for r in group) / len(group)
for cat, group in sorted(self._group_by("category").items())
}
@property
def languages(self) -> List[str]:
return sorted(set(r.language for r in self.results))
@property
def categories(self) -> List[str]:
return sorted(set(r.category for r in self.results))
def to_dict(self) -> Dict[str, Any]:
return {
"benchmark_version": "1.0",
"timestamp": self.timestamp,
"system_info": self.system_info,
"config": {
"backend": self.backend,
"model_size": self.model_size,
},
"summary": {
"n_samples": self.n_samples,
"total_audio_s": round(self.total_audio_s, 1),
"total_processing_s": round(self.total_processing_s, 1),
"avg_wer": round(self.avg_wer, 4),
"weighted_wer": round(self.weighted_wer, 4),
"avg_rtf": round(self.avg_rtf, 3),
"overall_rtf": round(self.overall_rtf, 3),
"avg_latency_ms": round(self.avg_latency_ms, 1),
"p95_latency_ms": round(self.p95_latency_ms, 1),
"wer_by_language": {
k: round(v, 4) for k, v in self.wer_by_language().items()
},
"rtf_by_language": {
k: round(v, 3) for k, v in self.rtf_by_language().items()
},
"wer_by_category": {
k: round(v, 4) for k, v in self.wer_by_category().items()
},
},
"results": [r.to_dict() for r in self.results],
}
def get_system_info() -> Dict[str, Any]:
"""Collect system metadata for the benchmark report."""
info: Dict[str, Any] = {
"platform": platform.platform(),
"machine": platform.machine(),
"python_version": platform.python_version(),
}
# CPU info
try:
chip = subprocess.check_output(
["sysctl", "-n", "machdep.cpu.brand_string"], text=True,
).strip()
info["cpu"] = chip
except Exception:
info["cpu"] = platform.processor()
# RAM
try:
mem_bytes = int(
subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True).strip()
)
info["ram_gb"] = round(mem_bytes / (1024**3))
except Exception:
try:
import os
pages = os.sysconf("SC_PHYS_PAGES")
page_size = os.sysconf("SC_PAGE_SIZE")
info["ram_gb"] = round(pages * page_size / (1024**3))
except Exception:
info["ram_gb"] = None
# Accelerator
try:
import torch
if torch.cuda.is_available():
info["accelerator"] = torch.cuda.get_device_name(0)
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
info["accelerator"] = "Apple Silicon (MPS)"
else:
info["accelerator"] = "CPU"
except ImportError:
info["accelerator"] = "CPU"
# Backend versions
versions = {}
for pkg, name in [
("faster_whisper", "faster-whisper"),
("whisper", "openai-whisper"),
("mlx_whisper", "mlx-whisper"),
("transformers", "transformers"),
("torch", "torch"),
]:
try:
mod = __import__(pkg)
versions[name] = getattr(mod, "__version__", "installed")
except ImportError:
pass
try:
import mlx.core as mx
versions["mlx"] = mx.__version__
except ImportError:
pass
info["backend_versions"] = versions
return info
================================================
FILE: whisperlivekit/benchmark/report.py
================================================
"""Benchmark report formatting — terminal tables and JSON export."""
import json
import sys
from pathlib import Path
from typing import TextIO
from whisperlivekit.benchmark.metrics import BenchmarkReport
# ANSI color codes
GREEN = "\033[32m"
YELLOW = "\033[33m"
RED = "\033[31m"
CYAN = "\033[36m"
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"
def _wer_color(wer: float) -> str:
if wer < 0.15:
return GREEN
elif wer < 0.30:
return YELLOW
return RED
def _rtf_color(rtf: float) -> str:
if rtf < 0.5:
return GREEN
elif rtf < 1.0:
return YELLOW
return RED
def _lat_color(ms: float) -> str:
if ms < 500:
return GREEN
elif ms < 1000:
return YELLOW
return RED
def print_report(report: BenchmarkReport, out: TextIO = sys.stderr) -> None:
"""Print a comprehensive benchmark report to the terminal."""
w = out.write
# Header
w(f"\n{BOLD} WhisperLiveKit Benchmark Report{RESET}\n")
w(f" {'─' * 72}\n")
si = report.system_info
w(f" Backend: {CYAN}{report.backend}{RESET}\n")
w(f" Model: {report.model_size}\n")
w(f" Accelerator: {si.get('accelerator', 'unknown')}\n")
w(f" CPU: {si.get('cpu', 'unknown')}\n")
w(f" RAM: {si.get('ram_gb', '?')} GB\n")
w(f" Timestamp: {report.timestamp}\n")
w(f" {'─' * 72}\n\n")
# Per-sample table
w(f" {BOLD}{'Sample':<20} {'Lang':>4} {'Dur':>5} {'WER':>7} "
f"{'RTF':>6} {'Lat(avg)':>8} {'Lat(p95)':>8} {'Calls':>5} {'Lines':>5}{RESET}\n")
w(f" {'─' * 72}\n")
for r in report.results:
wc = _wer_color(r.wer)
rc = _rtf_color(r.rtf)
lc = _lat_color(r.avg_latency_ms)
name = r.sample_name[:20]
w(f" {name:<20} {r.language:>4} {r.duration_s:>4.1f}s "
f"{wc}{r.wer * 100:>6.1f}%{RESET} "
f"{rc}{r.rtf:>5.2f}x{RESET} "
f"{lc}{r.avg_latency_ms:>7.0f}ms{RESET} "
f"{lc}{r.p95_latency_ms:>7.0f}ms{RESET} "
f"{r.n_transcription_calls:>5} {r.n_lines:>5}\n")
# Timing warnings
if not r.timing_valid:
w(f" {' ' * 20} {RED}⚠ invalid timestamps{RESET}\n")
if not r.timing_monotonic:
w(f" {' ' * 20} {YELLOW}⚠ non-monotonic timestamps{RESET}\n")
w(f" {'─' * 72}\n\n")
# Summary
w(f" {BOLD}Summary{RESET} ({report.n_samples} samples, "
f"{report.total_audio_s:.1f}s total audio)\n\n")
wc = _wer_color(report.avg_wer)
rc = _rtf_color(report.overall_rtf)
lc = _lat_color(report.avg_latency_ms)
w(f" Avg WER (macro): {wc}{report.avg_wer * 100:>6.1f}%{RESET}\n")
w(f" Weighted WER: {_wer_color(report.weighted_wer)}"
f"{report.weighted_wer * 100:>6.1f}%{RESET}\n")
w(f" Overall RTF: {rc}{report.overall_rtf:>6.3f}x{RESET} "
f"({report.total_processing_s:.1f}s for {report.total_audio_s:.1f}s audio)\n")
w(f" Avg latency: {lc}{report.avg_latency_ms:>6.0f}ms{RESET}\n")
w(f" P95 latency: {_lat_color(report.p95_latency_ms)}"
f"{report.p95_latency_ms:>6.0f}ms{RESET}\n")
# Per-language breakdown
wer_by_lang = report.wer_by_language()
rtf_by_lang = report.rtf_by_language()
if len(wer_by_lang) > 1:
w(f"\n {BOLD}By Language{RESET}\n")
w(f" {'─' * 40}\n")
w(f" {'Lang':>4} {'WER':>7} {'RTF':>6} {'Samples':>7}\n")
w(f" {'─' * 34}\n")
lang_groups = {}
for r in report.results:
lang_groups.setdefault(r.language, []).append(r)
for lang in sorted(lang_groups):
group = lang_groups[lang]
avg_wer = sum(r.wer for r in group) / len(group)
avg_rtf = sum(r.rtf for r in group) / len(group)
wc = _wer_color(avg_wer)
rc = _rtf_color(avg_rtf)
w(f" {lang:>4} {wc}{avg_wer * 100:>6.1f}%{RESET} "
f"{rc}{avg_rtf:>5.2f}x{RESET} {len(group):>7}\n")
# Per-category breakdown
wer_by_cat = report.wer_by_category()
if len(wer_by_cat) > 1:
w(f"\n {BOLD}By Category{RESET}\n")
w(f" {'─' * 40}\n")
w(f" {'Category':>12} {'WER':>7} {'Samples':>7}\n")
w(f" {'─' * 30}\n")
cat_groups = {}
for r in report.results:
cat_groups.setdefault(r.category, []).append(r)
for cat in sorted(cat_groups):
group = cat_groups[cat]
avg_wer = sum(r.wer for r in group) / len(group)
wc = _wer_color(avg_wer)
w(f" {cat:>12} {wc}{avg_wer * 100:>6.1f}%{RESET} {len(group):>7}\n")
w(f"\n {'─' * 72}\n\n")
def print_transcriptions(report: BenchmarkReport, out: TextIO = sys.stderr) -> None:
"""Print hypothesis vs reference for each sample."""
w = out.write
w(f"\n {BOLD}Transcriptions{RESET}\n")
w(f" {'─' * 72}\n")
for r in report.results:
wc = _wer_color(r.wer)
w(f"\n {BOLD}{r.sample_name}{RESET} ({r.language}, {r.category}) "
f"WER={wc}{r.wer * 100:.1f}%{RESET}\n")
ref = r.reference[:120] + "..." if len(r.reference) > 120 else r.reference
hyp = r.hypothesis[:120] + "..." if len(r.hypothesis) > 120 else r.hypothesis
w(f" {DIM}ref: {ref}{RESET}\n")
w(f" hyp: {hyp}\n")
w(f"\n {'─' * 72}\n\n")
def write_json(report: BenchmarkReport, path: str) -> None:
"""Export the full report as JSON."""
Path(path).write_text(json.dumps(report.to_dict(), indent=2, ensure_ascii=False))
================================================
FILE: whisperlivekit/benchmark/runner.py
================================================
"""Benchmark runner — orchestrates runs through TestHarness."""
import logging
import resource
import time
from typing import Callable, List, Optional
from whisperlivekit.benchmark.compat import backend_supports_language, resolve_backend
from whisperlivekit.benchmark.datasets import BenchmarkSample, get_benchmark_samples
from whisperlivekit.benchmark.metrics import BenchmarkReport, SampleResult, get_system_info
logger = logging.getLogger(__name__)
class BenchmarkRunner:
"""Orchestrates benchmark runs through TestHarness.
Args:
backend: ASR backend name or "auto".
model_size: Model size (e.g. "base", "large-v3").
languages: Language codes to benchmark (None = all available).
categories: Categories to benchmark (None = all).
quick: Use a small subset for fast smoke tests.
speed: Feed speed (0 = instant, 1.0 = real-time).
on_progress: Callback(sample_name, i, total) for progress updates.
"""
def __init__(
self,
backend: str = "auto",
model_size: str = "base",
languages: Optional[List[str]] = None,
categories: Optional[List[str]] = None,
quick: bool = False,
speed: float = 0,
on_progress: Optional[Callable] = None,
):
self.backend = resolve_backend(backend)
self.model_size = model_size
self.languages = languages
self.categories = categories
self.quick = quick
self.speed = speed
self.on_progress = on_progress
async def run(self) -> BenchmarkReport:
"""Run the full benchmark suite and return a report."""
from whisperlivekit.metrics import compute_wer
from whisperlivekit.test_harness import TestHarness
# Get samples
samples = get_benchmark_samples(
languages=self.languages,
categories=self.categories,
quick=self.quick,
)
# Filter by backend language support
compatible = []
for s in samples:
if backend_supports_language(self.backend, s.language):
compatible.append(s)
else:
logger.info(
"Skipping %s (%s) — backend %s does not support %s",
s.name, s.language, self.backend, s.language,
)
samples = compatible
if not samples:
raise RuntimeError(
f"No benchmark samples available for backend={self.backend}, "
f"languages={self.languages}, categories={self.categories}"
)
# Build harness kwargs
harness_kwargs = {
"model_size": self.model_size,
"lan": "auto", # let the model auto-detect for multilingual
"pcm_input": True,
}
if self.backend not in ("auto",):
harness_kwargs["backend"] = self.backend
report = BenchmarkReport(
backend=self.backend,
model_size=self.model_size,
system_info=get_system_info(),
)
for i, sample in enumerate(samples):
if self.on_progress:
self.on_progress(sample.name, i, len(samples))
result = await self._run_sample(
sample, harness_kwargs, compute_wer,
)
report.results.append(result)
if self.on_progress:
self.on_progress("done", len(samples), len(samples))
return report
async def _run_sample(
self,
sample: BenchmarkSample,
harness_kwargs: dict,
compute_wer,
) -> SampleResult:
"""Benchmark a single sample through TestHarness."""
from whisperlivekit.test_harness import TestHarness
# Override language for the specific sample
kwargs = {**harness_kwargs, "lan": sample.language}
# Memory before
mem_before = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
t_start = time.perf_counter()
async with TestHarness(**kwargs) as h:
await h.feed(sample.path, speed=self.speed)
# Drain time scales with audio duration for slow backends
drain = max(5.0, sample.duration * 0.5)
await h.drain(drain)
state = await h.finish(timeout=120)
# Extract metrics from the pipeline
metrics = h.metrics
t_elapsed = time.perf_counter() - t_start
# Memory after
mem_after = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
# On macOS ru_maxrss is bytes, on Linux it's KB
import sys
divisor = 1024 * 1024 if sys.platform == "darwin" else 1024
mem_delta = (mem_after - mem_before) / divisor
# RTF
rtf = t_elapsed / sample.duration if sample.duration > 0 else 0
# WER
hypothesis = state.committed_text or state.text
wer_result = compute_wer(sample.reference, hypothesis)
# Latency from SessionMetrics
avg_lat = metrics.avg_latency_ms if metrics else 0
p95_lat = metrics.p95_latency_ms if metrics else 0
n_calls = metrics.n_transcription_calls if metrics else 0
n_tokens = metrics.n_tokens_produced if metrics else 0
return SampleResult(
sample_name=sample.name,
language=sample.language,
category=sample.category,
duration_s=sample.duration,
wer=wer_result["wer"],
wer_details={
"substitutions": wer_result["substitutions"],
"insertions": wer_result["insertions"],
"deletions": wer_result["deletions"],
"ref_words": wer_result["ref_words"],
"hyp_words": wer_result["hyp_words"],
},
processing_time_s=round(t_elapsed, 2),
rtf=round(rtf, 3),
avg_latency_ms=round(avg_lat, 1),
p95_latency_ms=round(p95_lat, 1),
n_transcription_calls=n_calls,
n_lines=len(state.speech_lines),
n_tokens=n_tokens,
timing_valid=state.timing_valid,
timing_monotonic=state.timing_monotonic,
peak_memory_mb=round(mem_delta, 1) if mem_delta > 0 else None,
hypothesis=hypothesis,
reference=sample.reference,
source=sample.source,
tags=list(sample.tags),
)
================================================
FILE: whisperlivekit/cascade_bridge.py
================================================
"""
Bridge between WhisperLiveKit STT and IWSLT26 MT pipeline.
Converts streaming ASRToken output from SimulStreaming into the JSONL
format expected by the AlignAtt MT agent (iwslt26-sst).
Output format (one JSON per line):
{"text": "word or phrase", "emission_time": 1.234, "is_final": false, "speech_time": 1.0}
Where:
- text: the emitted word/phrase
- emission_time: wall-clock time when the word was emitted (for compute-aware eval)
- speech_time: timestamp in the audio (for compute-unaware eval)
- is_final: whether this is the last word of a segment/silence boundary
"""
import json
import time
from typing import List, TextIO
from whisperlivekit.timed_objects import ASRToken
class CascadeBridge:
"""Converts ASRToken stream to JSONL for the MT agent."""
def __init__(self, output_file: TextIO = None):
self.output_file = output_file
self.start_time = time.time()
self.entries: List[dict] = []
def emit_tokens(self, tokens: List[ASRToken], is_final: bool = False):
"""Emit a batch of tokens from the STT."""
wall_clock = time.time() - self.start_time
for i, token in enumerate(tokens):
entry = {
"text": token.text.strip(),
"emission_time": round(wall_clock, 3),
"speech_time": round(token.start, 3),
"is_final": is_final and (i == len(tokens) - 1),
}
self.entries.append(entry)
if self.output_file:
self.output_file.write(json.dumps(entry) + "\n")
self.output_file.flush()
def get_entries(self) -> List[dict]:
return self.entries
def get_text(self) -> str:
"""Get the full transcribed text."""
return " ".join(e["text"] for e in self.entries if e["text"])
def save(self, path: str):
"""Save all entries to a JSONL file."""
with open(path, "w") as f:
for entry in self.entries:
f.write(json.dumps(entry) + "\n")
def run_stt_to_jsonl(
audio_path: str,
output_path: str,
model_id: str = "Qwen/Qwen3-ASR-0.6B",
alignment_heads_path: str = None,
border_fraction: float = 0.20,
language: str = "en",
chunk_sec: float = 1.0,
):
"""Run STT on an audio file and save JSONL output for the MT agent.
This is the main entry point for the cascade: audio file → JSONL.
"""
import wave
import numpy as np
from whisperlivekit.qwen3_simul_kv import Qwen3SimulKVASR, Qwen3SimulKVOnlineProcessor
# Load audio
with wave.open(audio_path, 'r') as wf:
audio = np.frombuffer(
wf.readframes(wf.getnframes()), dtype=np.int16
).astype(np.float32) / 32768.0
# Initialize STT
asr = Qwen3SimulKVASR(
model_dir=model_id,
lan=language,
alignment_heads_path=alignment_heads_path,
border_fraction=border_fraction,
)
proc = Qwen3SimulKVOnlineProcessor(asr)
bridge = CascadeBridge()
# Stream audio in chunks
chunk_samples = int(chunk_sec * 16000)
offset = 0
stream_time = 0.0
while offset < len(audio):
chunk = audio[offset:offset + chunk_samples]
stream_time += len(chunk) / 16000
proc.insert_audio_chunk(chunk, stream_time)
words, _ = proc.process_iter(is_last=False)
if words:
bridge.emit_tokens(words, is_final=False)
offset += chunk_samples
# Final flush
final_words, _ = proc.finish()
if final_words:
bridge.emit_tokens(final_words, is_final=True)
# Save
bridge.save(output_path)
return bridge
================================================
FILE: whisperlivekit/cli.py
================================================
"""CLI entry point for WhisperLiveKit.
Provides subcommands:
wlk serve — Start the transcription server (default when no args)
wlk listen — Live microphone transcription
wlk run — Auto-pull model and start server
wlk transcribe — Transcribe audio files offline
wlk bench — Benchmark speed and accuracy on standard test audio
wlk models — List available and installed backends/models
wlk pull — Download a model for offline use
wlk rm — Delete downloaded models
wlk check — Verify system dependencies (ffmpeg, etc.)
wlk diagnose — Run pipeline diagnostics on audio file
"""
import importlib.util
import logging
import platform
import sys
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Backend detection
# ---------------------------------------------------------------------------
def _module_available(name: str) -> bool:
return importlib.util.find_spec(name) is not None
def _gpu_info() -> str:
"""Return a short string describing available accelerators."""
parts = []
try:
import torch
if torch.cuda.is_available():
name = torch.cuda.get_device_name(0)
parts.append(f"CUDA ({name})")
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
parts.append("MPS (Apple Silicon)")
except ImportError:
pass
if platform.system() == "Darwin" and platform.machine() == "arm64":
if _module_available("mlx"):
parts.append("MLX")
return ", ".join(parts) if parts else "CPU only"
BACKENDS = [
{
"id": "faster-whisper",
"name": "Faster Whisper",
"module": "faster_whisper",
"install": "pip install faster-whisper",
"description": "CTranslate2-based Whisper (fast, CPU/CUDA)",
"policy": "localagreement",
"streaming": "chunk", # batch inference with LocalAgreement/SimulStreaming
"devices": ["cpu", "cuda"],
},
{
"id": "whisper",
"name": "OpenAI Whisper",
"module": "whisper",
"install": "pip install openai-whisper",
"description": "Original OpenAI Whisper (PyTorch)",
"policy": "simulstreaming",
"streaming": "chunk",
"devices": ["cpu", "cuda"],
},
{
"id": "mlx-whisper",
"name": "MLX Whisper",
"module": "mlx_whisper",
"install": "pip install mlx-whisper",
"description": "Apple Silicon native Whisper (MLX)",
"policy": "localagreement",
"platform": "darwin-arm64",
"streaming": "chunk",
"devices": ["mlx"],
},
{
"id": "voxtral-mlx",
"name": "Voxtral MLX",
"module": "mlx",
"install": "pip install whisperlivekit[voxtral-mlx]",
"description": "Mistral Voxtral Mini on Apple Silicon (MLX, native streaming)",
"platform": "darwin-arm64",
"streaming": "native", # truly streaming (token-by-token)
"devices": ["mlx"],
},
{
"id": "voxtral",
"name": "Voxtral HF",
"module": "transformers",
"install": "pip install whisperlivekit[voxtral-hf]",
"description": "Mistral Voxtral Mini (HF Transformers, native streaming)",
"streaming": "native",
"devices": ["cuda", "mps", "cpu"],
},
{
"id": "qwen3",
"name": "Qwen3 ASR",
"module": "qwen_asr",
"install": "pip install qwen-asr",
"description": "Qwen3-ASR with ForcedAligner timestamps",
"streaming": "chunk",
"devices": ["cuda", "mps", "cpu"],
},
{
"id": "qwen3-mlx",
"name": "Qwen3 MLX",
"module": "mlx_qwen3_asr",
"install": "pip install mlx-qwen3-asr",
"description": "Qwen3-ASR on Apple Silicon (MLX, native streaming)",
"platform": "darwin-arm64",
"streaming": "native",
"devices": ["mlx"],
},
{
"id": "openai-api",
"name": "OpenAI API",
"module": "openai",
"install": "pip install openai",
"description": "Cloud-based transcription via OpenAI API",
"streaming": "cloud",
"devices": ["cloud"],
},
]
# ---------------------------------------------------------------------------
# Model catalog — maps "wlk pull " to download actions
# ---------------------------------------------------------------------------
# Whisper model sizes available across backends
WHISPER_SIZES = [
"tiny", "tiny.en", "base", "base.en", "small", "small.en",
"medium", "medium.en", "large-v1", "large-v2", "large-v3", "large-v3-turbo",
]
# Faster-Whisper uses Systran HuggingFace repos
FASTER_WHISPER_REPOS = {
"tiny": "Systran/faster-whisper-tiny",
"tiny.en": "Systran/faster-whisper-tiny.en",
"base": "Systran/faster-whisper-base",
"base.en": "Systran/faster-whisper-base.en",
"small": "Systran/faster-whisper-small",
"small.en": "Systran/faster-whisper-small.en",
"medium": "Systran/faster-whisper-medium",
"medium.en": "Systran/faster-whisper-medium.en",
"large-v1": "Systran/faster-whisper-large-v1",
"large-v2": "Systran/faster-whisper-large-v2",
"large-v3": "Systran/faster-whisper-large-v3",
"large-v3-turbo": "Systran/faster-distil-whisper-large-v3",
}
# MLX Whisper repos from model_mapping.py
MLX_WHISPER_REPOS = {
"tiny.en": "mlx-community/whisper-tiny.en-mlx",
"tiny": "mlx-community/whisper-tiny-mlx",
"base.en": "mlx-community/whisper-base.en-mlx",
"base": "mlx-community/whisper-base-mlx",
"small.en": "mlx-community/whisper-small.en-mlx",
"small": "mlx-community/whisper-small-mlx",
"medium.en": "mlx-community/whisper-medium.en-mlx",
"medium": "mlx-community/whisper-medium-mlx",
"large-v1": "mlx-community/whisper-large-v1-mlx",
"large-v2": "mlx-community/whisper-large-v2-mlx",
"large-v3": "mlx-community/whisper-large-v3-mlx",
"large-v3-turbo": "mlx-community/whisper-large-v3-turbo",
"large": "mlx-community/whisper-large-mlx",
}
# Voxtral/Qwen3 model repos
VOXTRAL_HF_REPO = "mistralai/Voxtral-Mini-4B-Realtime-2602"
VOXTRAL_MLX_REPO = "mlx-community/Voxtral-Mini-4B-Realtime-6bit"
QWEN3_REPOS = {
"1.7b": "Qwen/Qwen3-ASR-1.7B",
"0.6b": "Qwen/Qwen3-ASR-0.6B",
}
QWEN3_ALIGNER_REPO = "Qwen/Qwen3-ForcedAligner-0.6B"
# Model catalog: metadata for display in `wlk models`
# params = approximate parameter count, disk = approximate download size
MODEL_CATALOG = [
# Whisper family (available across faster-whisper, mlx-whisper, whisper backends)
{"name": "tiny", "family": "whisper", "params": "39M", "disk": "75 MB", "languages": 99, "quality": "low", "speed": "fastest"},
{"name": "tiny.en", "family": "whisper", "params": "39M", "disk": "75 MB", "languages": 1, "quality": "low", "speed": "fastest"},
{"name": "base", "family": "whisper", "params": "74M", "disk": "142 MB", "languages": 99, "quality": "fair", "speed": "fast"},
{"name": "base.en", "family": "whisper", "params": "74M", "disk": "142 MB", "languages": 1, "quality": "fair", "speed": "fast"},
{"name": "small", "family": "whisper", "params": "244M", "disk": "466 MB", "languages": 99, "quality": "good", "speed": "medium"},
{"name": "small.en", "family": "whisper", "params": "244M", "disk": "466 MB", "languages": 1, "quality": "good", "speed": "medium"},
{"name": "medium", "family": "whisper", "params": "769M", "disk": "1.5 GB", "languages": 99, "quality": "great", "speed": "slow"},
{"name": "medium.en", "family": "whisper", "params": "769M", "disk": "1.5 GB", "languages": 1, "quality": "great", "speed": "slow"},
{"name": "large-v3", "family": "whisper", "params": "1.5B", "disk": "3.1 GB", "languages": 99, "quality": "best", "speed": "slowest"},
{"name": "large-v3-turbo", "family": "whisper", "params": "809M", "disk": "1.6 GB", "languages": 99, "quality": "great", "speed": "medium"},
# Voxtral (native streaming, single model)
{"name": "voxtral", "family": "voxtral", "params": "4B", "disk": "8.2 GB", "languages": 15, "quality": "great", "speed": "medium"},
{"name": "voxtral-mlx", "family": "voxtral", "params": "4B", "disk": "2.7 GB", "languages": 15, "quality": "great", "speed": "medium"},
# Qwen3 ASR
{"name": "qwen3:1.7b", "family": "qwen3", "params": "1.7B", "disk": "3.6 GB", "languages": 12, "quality": "good", "speed": "fast"},
{"name": "qwen3:0.6b", "family": "qwen3", "params": "0.6B", "disk": "1.4 GB", "languages": 12, "quality": "fair", "speed": "fastest"},
# Qwen3 MLX (native streaming on Apple Silicon)
{"name": "qwen3-mlx:1.7b", "family": "qwen3-mlx", "params": "1.7B", "disk": "1.8 GB", "languages": 12, "quality": "good", "speed": "fast"},
{"name": "qwen3-mlx:0.6b", "family": "qwen3-mlx", "params": "0.6B", "disk": "0.7 GB", "languages": 12, "quality": "fair", "speed": "fastest"},
]
def _check_platform(backend: dict) -> bool:
"""Check if backend is compatible with current platform."""
req = backend.get("platform")
if req is None:
return True
if req == "darwin-arm64":
return platform.system() == "Darwin" and platform.machine() == "arm64"
return True
def _is_installed(backend: dict) -> bool:
return _module_available(backend["module"])
def _check_ffmpeg() -> bool:
"""Check if ffmpeg is available."""
import shutil
return shutil.which("ffmpeg") is not None
def _scan_downloaded_models() -> dict:
"""Scan HuggingFace and Whisper caches to find downloaded models.
Returns:
dict mapping repo_id → cached path (or True if found).
"""
found = {}
# 1. Scan HuggingFace hub cache
try:
from huggingface_hub import scan_cache_dir
cache_info = scan_cache_dir()
for repo in cache_info.repos:
found[repo.repo_id] = str(repo.repo_path)
except Exception:
pass
# 2. Scan native Whisper cache (~/.cache/whisper)
import os
whisper_cache = os.path.join(os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), "whisper")
if os.path.isdir(whisper_cache):
for f in os.listdir(whisper_cache):
if f.endswith(".pt"):
# e.g. "base.pt" or "large-v3.pt"
size = f.rsplit(".", 1)[0]
found[f"openai/whisper-{size}"] = os.path.join(whisper_cache, f)
return found
# ---------------------------------------------------------------------------
# Startup banner
# ---------------------------------------------------------------------------
def print_banner(config, host: str, port: int, ssl: bool = False):
"""Print a clean startup banner with server info."""
protocol = "https" if ssl else "http"
ws_protocol = "wss" if ssl else "ws"
# Resolve display host
display_host = host if host not in ("0.0.0.0", "::") else "localhost"
base_url = f"{protocol}://{display_host}:{port}"
ws_url = f"{ws_protocol}://{display_host}:{port}"
backend = getattr(config, "backend", "auto")
model = getattr(config, "model_size", "base")
language = getattr(config, "lan", "auto")
# Resolve actual backend name
backend_label = backend
if backend == "auto":
backend_label = "auto (will resolve on first request)"
lines = [
"",
" WhisperLiveKit",
f" Backend: {backend_label} | Model: {model} | Language: {language}",
f" Accelerator: {_gpu_info()}",
"",
f" Web UI: {base_url}/",
f" WebSocket: {ws_url}/asr",
f" Deepgram: {ws_url}/v1/listen",
f" REST API: {base_url}/v1/audio/transcriptions",
f" Models: {base_url}/v1/models",
f" Health: {base_url}/health",
"",
]
print("\n".join(lines), file=sys.stderr)
# ---------------------------------------------------------------------------
# `wlk models` subcommand
# ---------------------------------------------------------------------------
def _model_is_downloaded(model_entry: dict, downloaded: dict) -> bool:
"""Check if a model catalog entry has been downloaded."""
name = model_entry["name"]
family = model_entry["family"]
if family == "whisper":
# Check all whisper backends
repos = [
FASTER_WHISPER_REPOS.get(name),
MLX_WHISPER_REPOS.get(name),
f"openai/whisper-{name}",
]
return any(r in downloaded for r in repos if r)
elif name == "voxtral":
return VOXTRAL_HF_REPO in downloaded
elif name == "voxtral-mlx":
return VOXTRAL_MLX_REPO in downloaded
elif family == "qwen3":
size = name.split(":")[1] if ":" in name else "1.7b"
return QWEN3_REPOS.get(size, "") in downloaded
elif family == "qwen3-mlx":
size = name.split(":")[1] if ":" in name else "1.7b"
return QWEN3_REPOS.get(size, "") in downloaded
return False
def _best_backend_for_model(model_entry: dict) -> str:
"""Suggest the best available backend for a model."""
family = model_entry["family"]
is_apple = platform.system() == "Darwin" and platform.machine() == "arm64"
if family == "voxtral":
if "mlx" in model_entry["name"]:
return "voxtral-mlx"
return "voxtral"
elif family == "qwen3":
return "qwen3"
elif family == "qwen3-mlx":
return "qwen3-mlx"
elif family == "whisper":
if is_apple and _module_available("mlx_whisper"):
return "mlx-whisper"
if _module_available("faster_whisper"):
return "faster-whisper"
if _module_available("whisper"):
return "whisper"
# Suggest best installable
return "mlx-whisper" if is_apple else "faster-whisper"
return "auto"
def cmd_models():
"""List available models and backends (ollama-style)."""
is_apple_silicon = platform.system() == "Darwin" and platform.machine() == "arm64"
downloaded = _scan_downloaded_models()
# --- Installed backends ---
print("\n Backends:\n")
max_name = max(len(b["name"]) for b in BACKENDS)
for b in BACKENDS:
compatible = _check_platform(b)
installed = _is_installed(b)
streaming = b.get("streaming", "chunk")
stream_label = {"native": "streaming", "chunk": "chunked", "cloud": "cloud"}.get(streaming, streaming)
if installed:
status = "\033[32m+\033[0m"
elif not compatible:
status = "\033[90m-\033[0m"
else:
status = "\033[33m-\033[0m"
name_pad = b["name"].ljust(max_name)
desc_short = b["description"]
print(f" {status} {name_pad} {desc_short} [{stream_label}]")
if not installed and compatible:
print(f" {''.ljust(max_name)} \033[90m{b['install']}\033[0m")
# --- System info ---
print(f"\n Platform: {platform.system()} {platform.machine()}")
print(f" Accelerator: {_gpu_info()}")
print(f" ffmpeg: {'found' if _check_ffmpeg() else '\033[31mNOT FOUND\033[0m (required)'}")
# --- Model catalog ---
print("\n Models:\n")
# Table header
hdr = f" {'NAME':<20} {'PARAMS':>7} {'SIZE':>8} {'QUALITY':<8} {'SPEED':<8} {'LANGS':>5} {'STATUS':<10}"
print(hdr)
print(f" {'─' * 20} {'─' * 7} {'─' * 8} {'─' * 8} {'─' * 8} {'─' * 5} {'─' * 10}")
for m in MODEL_CATALOG:
name = m["name"]
# Skip platform-incompatible models
if name == "voxtral-mlx" and not is_apple_silicon:
continue
if m["family"] == "qwen3-mlx" and not is_apple_silicon:
continue
is_dl = _model_is_downloaded(m, downloaded)
if is_dl:
status = "\033[32mpulled\033[0m "
else:
status = "\033[90mavailable\033[0m "
langs = str(m["languages"]) if m["languages"] < 99 else "99+"
print(
f" {name:<20} {m['params']:>7} {m['disk']:>8} "
f"{m['quality']:<8} {m['speed']:<8} {langs:>5} {status}"
)
# --- Quick start ---
print(f"\n Quick start:\n")
if is_apple_silicon:
print(" wlk run voxtral-mlx # Best streaming on Apple Silicon")
print(" wlk run large-v3-turbo # Best quality/speed balance")
else:
print(" wlk run large-v3-turbo # Best quality/speed balance")
print(" wlk run voxtral # Native streaming (CUDA/CPU)")
print(" wlk pull base # Download smallest multilingual model")
print(" wlk transcribe audio.mp3 # Offline transcription")
print()
# ---------------------------------------------------------------------------
# `wlk pull` subcommand
# ---------------------------------------------------------------------------
def _hf_download(repo_id: str, label: str):
"""Download a HuggingFace model repo to the local cache."""
from huggingface_hub import snapshot_download
print(f" Downloading {label} ({repo_id})...")
path = snapshot_download(repo_id)
print(f" Saved to: {path}")
return path
def _resolve_pull_target(spec: str):
"""Parse a pull spec like 'faster-whisper:large-v3' or 'base' into (backend, size/repo).
Returns: list of (backend_id, repo_id, label) tuples to download.
"""
targets = []
# Check for backend:size format
if ":" in spec:
backend_part, size_part = spec.split(":", 1)
else:
backend_part = None
size_part = spec
# Handle voxtral
if size_part == "voxtral" or backend_part == "voxtral":
targets.append(("voxtral", VOXTRAL_HF_REPO, "Voxtral Mini (HF)"))
return targets
if size_part == "voxtral-mlx" or backend_part == "voxtral-mlx":
targets.append(("voxtral-mlx", VOXTRAL_MLX_REPO, "Voxtral Mini (MLX)"))
return targets
# Handle qwen3-mlx (must check before generic qwen3)
if backend_part == "qwen3-mlx" or size_part.startswith("qwen3-mlx"):
qwen_size = size_part.split(":")[-1] if ":" in spec else "1.7b"
if qwen_size.startswith("qwen3"):
qwen_size = "1.7b" # default
repo = QWEN3_REPOS.get(qwen_size)
if not repo:
print(f" Unknown Qwen3 size: {qwen_size}. Available: {', '.join(QWEN3_REPOS.keys())}")
return []
targets.append(("qwen3-mlx", repo, f"Qwen3-ASR MLX {qwen_size}"))
return targets
# Handle qwen3
if backend_part == "qwen3" or size_part.startswith("qwen3"):
qwen_size = size_part.split(":")[-1] if ":" in spec else "1.7b"
if qwen_size.startswith("qwen3"):
qwen_size = "1.7b" # default
repo = QWEN3_REPOS.get(qwen_size)
if not repo:
print(f" Unknown Qwen3 size: {qwen_size}. Available: {', '.join(QWEN3_REPOS.keys())}")
return []
targets.append(("qwen3", repo, f"Qwen3-ASR {qwen_size}"))
targets.append(("qwen3-aligner", QWEN3_ALIGNER_REPO, "Qwen3 ForcedAligner"))
return targets
# Handle whisper-family models with optional backend prefix
if backend_part:
# Specific backend requested
if backend_part == "faster-whisper":
repo = FASTER_WHISPER_REPOS.get(size_part)
if not repo:
print(f" Unknown size: {size_part}. Available: {', '.join(FASTER_WHISPER_REPOS.keys())}")
return []
targets.append(("faster-whisper", repo, f"Faster Whisper {size_part}"))
elif backend_part == "mlx-whisper":
repo = MLX_WHISPER_REPOS.get(size_part)
if not repo:
print(f" Unknown size: {size_part}. Available: {', '.join(MLX_WHISPER_REPOS.keys())}")
return []
targets.append(("mlx-whisper", repo, f"MLX Whisper {size_part}"))
elif backend_part == "whisper":
# OpenAI whisper downloads on first use; we can at least pull HF version
targets.append(("whisper", f"openai/whisper-{size_part}", f"Whisper {size_part}"))
else:
print(f" Unknown backend: {backend_part}")
return []
else:
# No backend specified — download for the best available backend
is_apple = platform.system() == "Darwin" and platform.machine() == "arm64"
if size_part in WHISPER_SIZES:
if is_apple and _module_available("mlx_whisper"):
repo = MLX_WHISPER_REPOS.get(size_part)
if repo:
targets.append(("mlx-whisper", repo, f"MLX Whisper {size_part}"))
if _module_available("faster_whisper"):
repo = FASTER_WHISPER_REPOS.get(size_part)
if repo:
targets.append(("faster-whisper", repo, f"Faster Whisper {size_part}"))
if not targets:
# Fallback: download for any available backend
repo = FASTER_WHISPER_REPOS.get(size_part)
if repo:
targets.append(("faster-whisper", repo, f"Faster Whisper {size_part}"))
else:
print(f" Unknown model: {spec}")
print(f" Available sizes: {', '.join(WHISPER_SIZES)}")
print(" Other models: voxtral, voxtral-mlx, qwen3:1.7b, qwen3:0.6b, qwen3-mlx:1.7b, qwen3-mlx:0.6b")
return []
return targets
def cmd_pull(spec: str):
"""Download a model for offline use."""
targets = _resolve_pull_target(spec)
if not targets:
return 1
print(f"\n Pulling model: {spec}\n")
for backend_id, repo_id, label in targets:
try:
_hf_download(repo_id, label)
except Exception as e:
print(f" Failed to download {label}: {e}")
return 1
print("\n Done. Model ready for offline use.")
print()
return 0
# ---------------------------------------------------------------------------
# `wlk transcribe` subcommand
# ---------------------------------------------------------------------------
def cmd_transcribe(args: list):
"""Transcribe audio files using the full pipeline, no server needed.
Usage: wlk transcribe [options] [audio_file ...]
"""
import argparse
parser = argparse.ArgumentParser(
prog="wlk transcribe",
description="Transcribe audio files offline using WhisperLiveKit.",
)
parser.add_argument("files", nargs="+", help="Audio files to transcribe")
parser.add_argument("--backend", default="auto", help="ASR backend (default: auto)")
parser.add_argument("--model", default="base", dest="model_size", help="Model size (default: base)")
parser.add_argument("--language", "--lan", default="auto", dest="lan", help="Language code (default: auto)")
parser.add_argument("--format", default="text", choices=["text", "json", "srt", "vtt", "verbose_json"],
help="Output format (default: text)")
parser.add_argument("--output", "-o", default=None, help="Output file (default: stdout)")
parser.add_argument("--diarization", action="store_true", help="Enable speaker diarization")
parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed processing logs")
parsed = parser.parse_args(args)
import asyncio
# Suppress noisy logging unless --verbose.
# Must happen AFTER importing (some modules set levels at import time)
# so we use a wrapper that silences after import.
if not parsed.verbose:
asyncio.run(_transcribe_files_quiet(parsed))
else:
asyncio.run(_transcribe_files(parsed))
async def _transcribe_files_quiet(parsed):
"""Wrapper that silences logging after imports are done."""
import warnings
warnings.filterwarnings("ignore")
# Force root logger to ERROR — overrides any per-module settings
logging.root.setLevel(logging.ERROR)
for handler in logging.root.handlers:
handler.setLevel(logging.ERROR)
# Silence all known noisy loggers
for name in list(logging.Logger.manager.loggerDict.keys()):
logging.getLogger(name).setLevel(logging.ERROR)
await _transcribe_files(parsed)
async def _transcribe_files(parsed):
"""Run transcription on one or more audio files."""
import json as json_module
from whisperlivekit.test_harness import TestHarness, load_audio_pcm
results = []
for audio_path in parsed.files:
print(f" Transcribing: {audio_path}", file=sys.stderr)
kwargs = {
"model_size": parsed.model_size,
"lan": parsed.lan,
"pcm_input": True,
}
if parsed.backend != "auto":
kwargs["backend"] = parsed.backend
if parsed.diarization:
kwargs["diarization"] = True
async with TestHarness(**kwargs) as h:
await h.feed(audio_path, speed=0)
await h.drain(5.0)
result = await h.finish(timeout=120)
duration = len(load_audio_pcm(audio_path)) / (16000 * 2)
if parsed.format == "text":
results.append(result.committed_text or result.text)
elif parsed.format == "json":
results.append(json_module.dumps({"text": result.committed_text or result.text}))
elif parsed.format == "verbose_json":
results.append(json_module.dumps({
"text": result.committed_text or result.text,
"duration": round(duration, 2),
"language": parsed.lan,
"segments": [
{
"text": line.get("text", ""),
"start": line.get("start", "0:00:00"),
"end": line.get("end", "0:00:00"),
"speaker": line.get("speaker", 0),
}
for line in result.lines
if line.get("text") and line.get("speaker", 0) != -2
],
}, indent=2))
elif parsed.format in ("srt", "vtt"):
results.append(_format_subtitle(result, parsed.format))
# Output
output_text = "\n".join(results)
if parsed.output:
with open(parsed.output, "w") as f:
f.write(output_text)
print(f" Output written to: {parsed.output}", file=sys.stderr)
else:
print(output_text)
def _format_subtitle(result, fmt: str) -> str:
"""Format result as SRT or VTT subtitles."""
from whisperlivekit.test_harness import _parse_time
lines_out = []
if fmt == "vtt":
lines_out.append("WEBVTT\n")
idx = 0
for line in result.lines:
if line.get("speaker") == -2 or not line.get("text"):
continue
idx += 1
start = line.get("start", "0:00:00")
end = line.get("end", "0:00:00")
start_s = _parse_time(start)
end_s = _parse_time(end)
start_ts = _subtitle_timestamp(start_s, fmt)
end_ts = _subtitle_timestamp(end_s, fmt)
if fmt == "srt":
lines_out.append(str(idx))
lines_out.append(f"{start_ts} --> {end_ts}")
lines_out.append(line["text"])
lines_out.append("")
return "\n".join(lines_out)
def _subtitle_timestamp(seconds: float, fmt: str) -> str:
"""Format seconds as SRT or VTT timestamp."""
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
ms = int(round((seconds % 1) * 1000))
sep = "," if fmt == "srt" else "."
return f"{h:02d}:{m:02d}:{s:02d}{sep}{ms:03d}"
# ---------------------------------------------------------------------------
# `wlk bench` subcommand
# ---------------------------------------------------------------------------
def cmd_bench(args: list):
"""Benchmark the transcription pipeline on public test audio.
Downloads samples from LibriSpeech, Multilingual LibriSpeech, FLEURS,
and AMI on first run. Supports multilingual benchmarking across all
available backends.
Usage: wlk bench [options]
"""
import argparse
parser = argparse.ArgumentParser(
prog="wlk bench",
description="Benchmark WhisperLiveKit on public test audio.",
)
parser.add_argument("--backend", default="auto",
help="ASR backend (default: auto-detect)")
parser.add_argument("--model", default="base", dest="model_size",
help="Model size (default: base)")
parser.add_argument("--languages", "--lan", default=None,
help="Comma-separated language codes, or 'all' (default: en)")
parser.add_argument("--categories", default=None,
help="Comma-separated categories: clean,noisy,multilingual,meeting")
parser.add_argument("--quick", action="store_true",
help="Quick mode: small subset for smoke tests")
parser.add_argument("--json", default=None, dest="json_out",
help="Export full report to JSON file")
parser.add_argument("--transcriptions", action="store_true",
help="Show hypothesis vs reference for each sample")
parser.add_argument("--verbose", "-v", action="store_true",
help="Show detailed logs")
parsed = parser.parse_args(args)
# Parse languages
languages = None
if parsed.languages and parsed.languages != "all":
languages = [l.strip() for l in parsed.languages.split(",")]
elif parsed.languages is None:
languages = ["en"] # default to English only
categories = None
if parsed.categories:
categories = [c.strip() for c in parsed.categories.split(",")]
import asyncio
if not parsed.verbose:
_suppress_logging()
asyncio.run(_run_bench_new(parsed, languages, categories))
def _suppress_logging():
"""Suppress noisy logs during benchmark."""
import warnings
warnings.filterwarnings("ignore")
logging.root.setLevel(logging.ERROR)
for handler in logging.root.handlers:
handler.setLevel(logging.ERROR)
for name in list(logging.Logger.manager.loggerDict.keys()):
logging.getLogger(name).setLevel(logging.ERROR)
async def _run_bench_new(parsed, languages, categories):
"""Run the benchmark using the new benchmark module."""
from whisperlivekit.benchmark.report import print_report, print_transcriptions, write_json
from whisperlivekit.benchmark.runner import BenchmarkRunner
def on_progress(name, i, total):
if name == "done":
print(f"\r [{total}/{total}] Done.{' ' * 30}", file=sys.stderr)
else:
print(f"\r [{i + 1}/{total}] {name}...{' ' * 20}",
end="", file=sys.stderr, flush=True)
runner = BenchmarkRunner(
backend=parsed.backend,
model_size=parsed.model_size,
languages=languages,
categories=categories,
quick=parsed.quick,
on_progress=on_progress,
)
print(f"\n Downloading benchmark samples (cached after first run)...",
file=sys.stderr)
report = await runner.run()
print_report(report)
if parsed.transcriptions:
print_transcriptions(report)
if parsed.json_out:
write_json(report, parsed.json_out)
print(f" Results exported to: {parsed.json_out}\n", file=sys.stderr)
# ---------------------------------------------------------------------------
# `wlk listen` subcommand
# ---------------------------------------------------------------------------
def cmd_listen(args: list):
"""Live microphone transcription.
Usage: wlk listen [options]
"""
import argparse
parser = argparse.ArgumentParser(
prog="wlk listen",
description="Transcribe live microphone input in real-time.",
)
parser.add_argument("--backend", default="auto", help="ASR backend (default: auto)")
parser.add_argument("--model", default="base", dest="model_size", help="Model size (default: base)")
parser.add_argument("--language", "--lan", default="auto", dest="lan", help="Language code (default: auto)")
parser.add_argument("--diarization", action="store_true", help="Enable speaker diarization")
parser.add_argument("--output", "-o", default=None, help="Save transcription to file on exit")
parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed logs")
parsed = parser.parse_args(args)
try:
import sounddevice # noqa: F401
except ImportError:
print("\n sounddevice is required for microphone input.", file=sys.stderr)
print(" Install it with: pip install sounddevice\n", file=sys.stderr)
sys.exit(1)
import asyncio
if not parsed.verbose:
asyncio.run(_listen_quiet(parsed))
else:
asyncio.run(_listen_main(parsed))
async def _listen_quiet(parsed):
"""Run listen with suppressed logging."""
import warnings
warnings.filterwarnings("ignore")
logging.root.setLevel(logging.ERROR)
for handler in logging.root.handlers:
handler.setLevel(logging.ERROR)
for name in list(logging.Logger.manager.loggerDict.keys()):
logging.getLogger(name).setLevel(logging.ERROR)
await _listen_main(parsed)
async def _listen_main(parsed):
"""Live microphone transcription loop."""
import numpy as np
import sounddevice as sd
from whisperlivekit.test_harness import TestHarness
SAMPLE_RATE = 16000
BLOCK_SIZE = int(SAMPLE_RATE * 0.5) # 500ms chunks
kwargs = {
"model_size": parsed.model_size,
"lan": parsed.lan,
"pcm_input": True,
}
if parsed.backend != "auto":
kwargs["backend"] = parsed.backend
if parsed.diarization:
kwargs["diarization"] = True
out = sys.stderr
out.write("\n Loading model...")
out.flush()
async with TestHarness(**kwargs) as h:
out.write(" done.\n")
out.write(" Listening (Ctrl+C to stop)\n\n")
out.flush()
n_lines_printed = 0
pipe_stdout = not sys.stdout.isatty()
def on_state_update(state):
nonlocal n_lines_printed
speech = state.speech_lines
buf = state.buffer_transcription.strip()
# Clear the buffer line
out.write("\r\033[K")
# Print new committed lines
while n_lines_printed < len(speech):
text = speech[n_lines_printed].get("text", "")
out.write(f" {text}\n")
if pipe_stdout:
sys.stdout.write(f"{text}\n")
sys.stdout.flush()
n_lines_printed += 1
# Show buffer (ephemeral, overwritten next update)
if buf:
out.write(f" \033[90m| {buf}\033[0m")
out.flush()
h.on_update(on_state_update)
# Bridge sounddevice thread -> async event loop
import asyncio
feed_queue = asyncio.Queue()
loop = asyncio.get_running_loop()
def audio_callback(indata, frames, time_info, status):
pcm = (indata[:, 0] * 32767).astype(np.int16).tobytes()
loop.call_soon_threadsafe(feed_queue.put_nowait, pcm)
try:
stream = sd.InputStream(
samplerate=SAMPLE_RATE,
channels=1,
dtype="float32",
blocksize=BLOCK_SIZE,
callback=audio_callback,
)
stream.start()
except Exception as e:
out.write(f"\n Could not open microphone: {e}\n")
out.write(" Check that a microphone is connected and permissions are granted.\n\n")
return
try:
while True:
try:
pcm_data = await asyncio.wait_for(feed_queue.get(), timeout=0.1)
await h.feed_pcm(pcm_data, speed=0)
except asyncio.TimeoutError:
pass
except KeyboardInterrupt:
pass
finally:
stream.stop()
stream.close()
out.write("\r\033[K\n Finishing...\n")
out.flush()
result = await h.finish(timeout=30)
# Print any remaining committed lines
speech = result.speech_lines
while n_lines_printed < len(speech):
text = speech[n_lines_printed].get("text", "")
out.write(f" {text}\n")
if pipe_stdout:
sys.stdout.write(f"{text}\n")
sys.stdout.flush()
n_lines_printed += 1
# Print remaining buffer
buf = result.buffer_transcription.strip()
if buf:
out.write(f" {buf}\n")
if pipe_stdout:
sys.stdout.write(f"{buf}\n")
sys.stdout.flush()
out.write("\n")
out.flush()
if parsed.output:
with open(parsed.output, "w") as f:
f.write(result.text + "\n")
out.write(f" Saved to: {parsed.output}\n\n")
out.flush()
# ---------------------------------------------------------------------------
# `wlk run` subcommand
# ---------------------------------------------------------------------------
def _resolve_run_spec(spec: str):
"""Map a model spec to (backend, model_size).
Returns (backend_id_or_None, model_size_or_None).
"""
if ":" in spec:
backend_part, model_part = spec.split(":", 1)
return backend_part, model_part
backend_ids = {b["id"] for b in BACKENDS}
if spec in backend_ids:
return spec, None
if spec == "voxtral-mlx":
return "voxtral-mlx", None
if spec == "qwen3-mlx":
return "qwen3-mlx", None
if spec in WHISPER_SIZES:
return None, spec
return None, spec
def cmd_run(args: list):
"""Auto-pull model if needed and start the server.
Usage: wlk run [model] [server options]
"""
import argparse
parser = argparse.ArgumentParser(
prog="wlk run",
description="Download model (if needed) and start the transcription server.",
)
parser.add_argument("model", nargs="?", default=None,
help="Model spec (e.g., voxtral, large-v3, faster-whisper:base)")
parsed, extra_args = parser.parse_known_args(args)
backend_flag = None
model_flag = None
if parsed.model:
backend_flag, model_flag = _resolve_run_spec(parsed.model)
# Show what we resolved
catalog_match = next(
(m for m in MODEL_CATALOG if m["name"] == parsed.model),
None,
)
if catalog_match:
print(
f"\n Model: {catalog_match['name']} "
f"({catalog_match['params']} params, {catalog_match['disk']})",
file=sys.stderr,
)
if backend_flag:
print(f" Backend: {backend_flag}", file=sys.stderr)
else:
best = _best_backend_for_model(catalog_match)
print(f" Backend: {best} (auto-detected)", file=sys.stderr)
# Auto-pull if needed
downloaded = _scan_downloaded_models()
targets = _resolve_pull_target(parsed.model)
need_pull = any(repo_id not in downloaded for _, repo_id, _ in targets)
if need_pull and targets:
print("\n Model not found locally. Downloading...\n", file=sys.stderr)
result = cmd_pull(parsed.model)
if result != 0:
sys.exit(1)
print(file=sys.stderr)
# Build server argv
server_argv = [sys.argv[0]]
if backend_flag:
server_argv.extend(["--backend", backend_flag])
if model_flag:
server_argv.extend(["--model", model_flag])
server_argv.extend(extra_args)
sys.argv = server_argv
from whisperlivekit.basic_server import main as serve_main
serve_main()
# ---------------------------------------------------------------------------
# `wlk rm` subcommand
# ---------------------------------------------------------------------------
def cmd_rm(spec: str):
"""Delete a downloaded model from the cache."""
targets = _resolve_pull_target(spec)
if not targets:
return 1
downloaded = _scan_downloaded_models()
found_any = any(repo_id in downloaded for _, repo_id, _ in targets)
if not found_any:
print(f"\n Model '{spec}' is not downloaded.\n", file=sys.stderr)
return 1
print(file=sys.stderr)
for _, repo_id, label in targets:
if repo_id not in downloaded:
continue
try:
# Try HuggingFace cache first
from huggingface_hub import scan_cache_dir
cache_info = scan_cache_dir()
deleted = False
for repo in cache_info.repos:
if repo.repo_id == repo_id:
size_bytes = repo.size_on_disk
size_str = f"{size_bytes / 1e9:.1f} GB" if size_bytes > 1e9 else f"{size_bytes / 1e6:.0f} MB"
hashes = [rev.commit_hash for rev in repo.revisions]
strategy = cache_info.delete_revisions(*hashes)
print(f" Deleting {label} ({repo_id})...", file=sys.stderr)
strategy.execute()
print(f" Freed {size_str}", file=sys.stderr)
deleted = True
break
if not deleted:
# Native whisper cache — plain file
import os
path = downloaded.get(repo_id)
if path and os.path.isfile(path):
size = os.path.getsize(path)
size_str = f"{size / 1e6:.0f} MB"
os.remove(path)
print(f" Deleted {label} ({path})", file=sys.stderr)
print(f" Freed {size_str}", file=sys.stderr)
except Exception as e:
print(f" Failed to delete {label}: {e}", file=sys.stderr)
return 1
print(file=sys.stderr)
return 0
# ---------------------------------------------------------------------------
# `wlk check` subcommand
# ---------------------------------------------------------------------------
def cmd_check():
"""Verify system dependencies."""
print("\nSystem check:\n")
checks = [
("Python >= 3.11", sys.version_info >= (3, 11)),
("ffmpeg", _check_ffmpeg()),
("torch", _module_available("torch")),
("torchaudio", _module_available("torchaudio")),
("faster-whisper", _module_available("faster_whisper")),
("uvicorn", _module_available("uvicorn")),
("fastapi", _module_available("fastapi")),
]
all_ok = True
for name, ok in checks:
icon = "\033[32m OK\033[0m" if ok else "\033[31m MISSING\033[0m"
print(f" {icon} {name}")
if not ok:
all_ok = False
print()
if all_ok:
print(" All dependencies OK. Ready to serve.")
else:
print(" Some dependencies are missing. Install them before running the server.")
print()
return 0 if all_ok else 1
# ---------------------------------------------------------------------------
# `wlk diagnose` subcommand
# ---------------------------------------------------------------------------
def cmd_diagnose(args: list):
"""Run pipeline diagnostics on an audio file.
Feeds audio through the full pipeline while probing internal backend state
at regular intervals. Produces a timeline of what happened inside the
pipeline, flags anomalies (stuck tokens, generate thread errors, etc.),
and prints a pass/fail summary.
Usage: wlk diagnose [audio_file] [options]
"""
import argparse
parser = argparse.ArgumentParser(
prog="wlk diagnose",
description="Run pipeline diagnostics to debug transcription issues.",
)
parser.add_argument("file", nargs="?", default=None,
help="Audio file to diagnose (default: built-in test sample)")
parser.add_argument("--backend", default="auto", help="ASR backend (default: auto)")
parser.add_argument("--model", default="base", dest="model_size", help="Model size (default: base)")
parser.add_argument("--language", "--lan", default="auto", dest="lan", help="Language code (default: auto)")
parser.add_argument("--speed", type=float, default=1.0,
help="Playback speed (1.0=realtime, 0=instant, default: 1.0)")
parser.add_argument("--probe-interval", type=float, default=2.0,
help="Seconds between state probes (default: 2.0)")
parser.add_argument("--diarization", action="store_true", help="Enable speaker diarization")
parsed = parser.parse_args(args)
import asyncio
asyncio.run(_diagnose_main(parsed))
def _probe_backend_state(processor) -> dict:
"""Probe internal state of whatever ASR backend is running.
Returns a dict of diagnostic key-value pairs specific to the backend.
"""
info = {}
transcription = processor.transcription
if transcription is None:
info["error"] = "no transcription processor"
return info
# Common: audio buffer size
audio_buf = getattr(transcription, "audio_buffer", None)
if audio_buf is not None:
info["audio_buffer_samples"] = len(audio_buf)
info["audio_buffer_sec"] = round(len(audio_buf) / 16000, 2)
# Common: get_buffer result
try:
buf = transcription.get_buffer()
info["buffer_text"] = buf.text if buf else ""
except Exception as e:
info["buffer_error"] = str(e)
# Voxtral HF streaming specifics
if hasattr(transcription, "_generate_started"):
info["backend_type"] = "voxtral-hf-streaming"
info["generate_started"] = transcription._generate_started
info["generate_finished"] = transcription._generate_finished
info["n_audio_tokens_fed"] = transcription._n_audio_tokens_fed
info["n_text_tokens_received"] = transcription._n_text_tokens_received
info["n_committed_words"] = transcription._n_committed_words
info["pending_audio_samples"] = transcription._pending_len
with transcription._text_lock:
info["accumulated_text"] = transcription._get_accumulated_text()
if transcription._generate_error:
info["generate_error"] = str(transcription._generate_error)
# Audio queue depth
info["audio_queue_depth"] = transcription._audio_queue.qsize()
# Voxtral MLX specifics
elif hasattr(transcription, "_mlx_processor"):
info["backend_type"] = "voxtral-mlx"
# Qwen3 MLX specifics
elif hasattr(transcription, "_session") and hasattr(transcription, "_state"):
info["backend_type"] = "qwen3-mlx"
info["samples_fed"] = getattr(transcription, "_samples_fed", 0)
info["committed_words"] = getattr(transcription, "_n_committed_words", 0)
# SimulStreaming specifics
elif hasattr(transcription, "prev_output"):
info["backend_type"] = "simulstreaming"
info["prev_output_len"] = len(getattr(transcription, "prev_output", "") or "")
# LocalAgreement (OnlineASRProcessor) specifics
elif hasattr(transcription, "hypothesis_buffer"):
info["backend_type"] = "localagreement"
hb = transcription.hypothesis_buffer
if hasattr(hb, "committed"):
info["committed_words"] = len(hb.committed)
if hasattr(hb, "buffer"):
info["hypothesis_buffer_words"] = len(hb.buffer)
else:
info["backend_type"] = "unknown"
return info
def _probe_pipeline_state(processor) -> dict:
"""Probe pipeline-level state (queues, tasks, ffmpeg)."""
info = {}
if processor.transcription_queue:
info["transcription_queue_size"] = processor.transcription_queue.qsize()
if processor.diarization_queue:
info["diarization_queue_size"] = processor.diarization_queue.qsize()
if processor.translation_queue:
info["translation_queue_size"] = processor.translation_queue.qsize()
info["total_pcm_samples"] = processor.total_pcm_samples
info["total_audio_sec"] = round(processor.total_pcm_samples / 16000, 2)
info["is_stopping"] = processor.is_stopping
info["in_silence"] = processor.current_silence is not None
info["n_state_lines"] = len(processor.state.tokens)
info["n_state_updates"] = len(getattr(processor.state, "new_tokens", []))
return info
async def _diagnose_main(parsed):
"""Run the full diagnostic pipeline."""
import asyncio
import time as time_module
from whisperlivekit.test_harness import TestHarness, load_audio_pcm
out = sys.stderr
# Resolve audio file
audio_path = parsed.file
if audio_path is None:
try:
from whisperlivekit.test_data import get_samples
samples = get_samples()
# Prefer a sample matching the requested language
lang_match = [s for s in samples if s.language == parsed.lan]
sample = lang_match[0] if lang_match else samples[0]
audio_path = sample.path
out.write(f"\n Using test sample: {sample.name} ({sample.duration:.1f}s)\n")
except Exception as e:
out.write(f"\n No audio file provided and couldn't load test sample: {e}\n")
out.write(" Usage: wlk diagnose [options]\n\n")
sys.exit(1)
# Load audio
try:
pcm = load_audio_pcm(audio_path)
except Exception as e:
out.write(f"\n Failed to load audio: {e}\n\n")
sys.exit(1)
audio_duration = len(pcm) / (16000 * 2)
# Print header
out.write(f"\n {'━' * 70}\n")
out.write(" WhisperLiveKit Pipeline Diagnostic\n")
out.write(f" {'━' * 70}\n\n")
out.write(f" Audio: {audio_path}\n")
out.write(f" Duration: {audio_duration:.1f}s\n")
out.write(f" Backend: {parsed.backend}\n")
out.write(f" Model: {parsed.model_size}\n")
out.write(f" Language: {parsed.lan}\n")
out.write(f" Speed: {parsed.speed}x\n")
out.write(f" Probe every: {parsed.probe_interval}s\n")
out.write(f" Platform: {platform.system()} {platform.machine()}\n")
out.write(f" Accelerator: {_gpu_info()}\n")
out.write(f"\n {'─' * 70}\n")
out.write(" Loading model...\n")
out.flush()
kwargs = {
"model_size": parsed.model_size,
"lan": parsed.lan,
"pcm_input": True,
}
if parsed.backend != "auto":
kwargs["backend"] = parsed.backend
if parsed.diarization:
kwargs["diarization"] = True
t_load_start = time_module.perf_counter()
probes = []
anomalies = []
async with TestHarness(**kwargs) as h:
t_load = time_module.perf_counter() - t_load_start
out.write(f" Model loaded in {t_load:.1f}s\n")
out.write(f" {'─' * 70}\n")
out.write(" Feeding audio...\n\n")
out.flush()
processor = h._processor
chunk_duration = 0.5 # seconds per chunk
chunk_bytes = int(chunk_duration * 16000 * 2)
offset = 0
t_start = time_module.perf_counter()
last_probe = t_start
probe_idx = 0
# Feed audio with periodic probes
while offset < len(pcm):
end = min(offset + chunk_bytes, len(pcm))
await processor.process_audio(pcm[offset:end])
chunk_seconds = (end - offset) / (16000 * 2)
h._audio_position += chunk_seconds
offset = end
if parsed.speed > 0:
await asyncio.sleep(chunk_duration / parsed.speed)
# Probe at intervals
now = time_module.perf_counter()
if now - last_probe >= parsed.probe_interval:
probe_idx += 1
elapsed = now - t_start
audio_pos = h._audio_position
backend_state = _probe_backend_state(processor)
pipeline_state = _probe_pipeline_state(processor)
harness_state = {
"n_history": len(h.history),
"state_text_len": len(h.state.text),
"state_lines": len(h.state.lines),
"state_speech_lines": len(h.state.speech_lines),
"buffer": h.state.buffer_transcription[:80] if h.state.buffer_transcription else "",
}
probe = {
"idx": probe_idx,
"wall_time": round(elapsed, 1),
"audio_pos": round(audio_pos, 1),
"backend": backend_state,
"pipeline": pipeline_state,
"harness": harness_state,
}
probes.append(probe)
# Print probe
out.write(f" [{probe_idx:3d}] wall={elapsed:5.1f}s audio={audio_pos:5.1f}s")
bt = backend_state.get("backend_type", "?")
if bt == "voxtral-hf-streaming":
out.write(
f" | gen={'Y' if backend_state.get('generate_started') else 'N'}"
f" fin={'Y' if backend_state.get('generate_finished') else 'N'}"
f" audio_tok={backend_state.get('n_audio_tokens_fed', 0)}"
f" text_tok={backend_state.get('n_text_tokens_received', 0)}"
f" words={backend_state.get('n_committed_words', 0)}"
f" q={backend_state.get('audio_queue_depth', 0)}"
)
if backend_state.get("generate_error"):
out.write(f" \033[31mERROR: {backend_state['generate_error']}\033[0m")
elif bt == "localagreement":
out.write(
f" | committed={backend_state.get('committed_words', 0)}"
f" buf_words={backend_state.get('hypothesis_buffer_words', 0)}"
)
elif bt == "simulstreaming":
out.write(
f" | prev_out_len={backend_state.get('prev_output_len', 0)}"
)
buf_text = backend_state.get("buffer_text", "")
if buf_text:
display = buf_text[:50] + ("..." if len(buf_text) > 50 else "")
out.write(f'\n buf="{display}"')
out.write("\n")
out.flush()
# Anomaly detection
if bt == "voxtral-hf-streaming":
if backend_state.get("generate_started") and not backend_state.get("generate_finished"):
if backend_state.get("n_audio_tokens_fed", 0) > 10 and backend_state.get("n_text_tokens_received", 0) == 0:
anomalies.append(f"[probe {probe_idx}] {backend_state['n_audio_tokens_fed']} audio tokens fed but 0 text tokens received — model may be stalled")
if backend_state.get("generate_error"):
anomalies.append(f"[probe {probe_idx}] Generate thread error: {backend_state['generate_error']}")
if harness_state["n_history"] == 0 and elapsed > 5:
anomalies.append(f"[probe {probe_idx}] No state updates after {elapsed:.0f}s — pipeline may be stuck")
last_probe = now
# Done feeding — drain and finish
out.write(f"\n {'─' * 70}\n")
out.write(" Audio feeding complete. Draining pipeline...\n")
out.flush()
await h.drain(3.0)
# One more probe after drain
backend_state = _probe_backend_state(processor)
pipeline_state = _probe_pipeline_state(processor)
probe_idx += 1
elapsed = time_module.perf_counter() - t_start
out.write(f" [{probe_idx:3d}] wall={elapsed:5.1f}s audio={h._audio_position:5.1f}s (post-drain)\n")
bt = backend_state.get("backend_type", "?")
if bt == "voxtral-hf-streaming":
out.write(
f" text_tok={backend_state.get('n_text_tokens_received', 0)}"
f" words={backend_state.get('n_committed_words', 0)}"
f" accumulated_text_len={len(backend_state.get('accumulated_text', ''))}\n"
)
result = await h.finish(timeout=60)
t_total = time_module.perf_counter() - t_start
# === Summary ===
out.write(f"\n {'━' * 70}\n")
out.write(" Diagnostic Summary\n")
out.write(f" {'━' * 70}\n\n")
out.write(f" Wall time: {t_total:.1f}s\n")
out.write(f" Audio duration: {audio_duration:.1f}s\n")
rtf = t_total / audio_duration if audio_duration > 0 else 0
out.write(f" RTF: {rtf:.3f}x\n")
out.write(f" Model load: {t_load:.1f}s\n")
out.write(f" Probes taken: {probe_idx}\n\n")
# Text output summary
text = result.committed_text or result.text
n_words = len(text.split()) if text.strip() else 0
n_lines = len(result.speech_lines)
has_silence = result.has_silence
out.write(f" Output words: {n_words}\n")
out.write(f" Output lines: {n_lines}\n")
out.write(f" Has silence: {has_silence}\n")
out.write(f" Timing valid: {result.timing_valid}\n")
out.write(f" Timing monotonic: {result.timing_monotonic}\n")
timing_errors = result.timing_errors()
if timing_errors:
out.write("\n Timing errors:\n")
for err in timing_errors[:10]:
out.write(f" - {err}\n")
# Transcription preview
if text:
preview = text[:200] + ("..." if len(text) > 200 else "")
out.write(f'\n Transcription:\n "{preview}"\n')
else:
out.write("\n \033[31mNo transcription output!\033[0m\n")
# Anomalies
out.write(f"\n {'─' * 70}\n")
if anomalies:
out.write(f" \033[33mAnomalies detected ({len(anomalies)}):\033[0m\n")
for a in anomalies:
out.write(f" ⚠ {a}\n")
else:
out.write(" \033[32mNo anomalies detected.\033[0m\n")
# Pass/fail checks
out.write(f"\n {'─' * 70}\n")
out.write(" Health checks:\n\n")
checks = [
("Model loaded successfully", t_load < 300),
("Audio processed without errors", not anomalies),
("Transcription produced output", n_words > 0),
("At least one committed line", n_lines > 0),
("Timestamps are valid", result.timing_valid),
("Timestamps are monotonic", result.timing_monotonic),
("RTF < 2.0x (faster than half real-time)", rtf < 2.0),
]
all_pass = True
for label, ok in checks:
icon = "\033[32m PASS\033[0m" if ok else "\033[31m FAIL\033[0m"
out.write(f" {icon} {label}\n")
if not ok:
all_pass = False
out.write(f"\n {'━' * 70}\n")
if all_pass:
out.write(" \033[32mAll checks passed.\033[0m\n")
else:
out.write(" \033[31mSome checks failed. Review the timeline above for details.\033[0m\n")
out.write(f" {'━' * 70}\n\n")
# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------
def _print_version():
"""Print version."""
from importlib.metadata import version
try:
v = version("whisperlivekit")
except Exception:
v = "dev"
print(f"WhisperLiveKit {v}")
def _print_help():
"""Print top-level help."""
print("""
WhisperLiveKit — Local speech-to-text toolkit
Usage: wlk [options]
Commands:
serve Start the transcription server (default)
listen Live microphone transcription
run Auto-pull model and start server
transcribe Transcribe audio files offline
bench Benchmark speed and accuracy
diagnose Run pipeline diagnostics on audio
models List available backends and models
pull Download models for offline use
rm Delete downloaded models
check Verify system dependencies
Examples:
wlk # Start server with defaults
wlk listen # Transcribe from microphone
wlk listen --backend voxtral # Listen with specific backend
wlk run voxtral # Auto-pull + start server
wlk run large-v3 # Auto-pull + start server
wlk transcribe audio.wav # Transcribe a file
wlk transcribe --format srt audio.wav # Generate SRT subtitles
wlk bench # Benchmark current backend
wlk diagnose audio.wav --backend voxtral # Diagnose pipeline issues
wlk models # List backends + models
wlk pull large-v3 # Download model
wlk rm large-v3 # Delete downloaded model
wlk check # Check dependencies
Run 'wlk --help' for command-specific help.
""")
def main():
"""CLI entry point: routes to subcommands or defaults to 'serve'."""
# Quick subcommand routing before argparse (so `wlk models` works
# without loading the full server stack)
if len(sys.argv) >= 2:
subcmd = sys.argv[1]
if subcmd == "models":
cmd_models()
return
if subcmd == "check":
sys.exit(cmd_check())
if subcmd == "pull":
if len(sys.argv) < 3:
print("Usage: wlk pull ")
print(" e.g.: wlk pull base, wlk pull faster-whisper:large-v3, wlk pull voxtral")
sys.exit(1)
sys.exit(cmd_pull(sys.argv[2]))
if subcmd == "rm":
if len(sys.argv) < 3:
print("Usage: wlk rm ")
print(" e.g.: wlk rm base, wlk rm voxtral")
sys.exit(1)
sys.exit(cmd_rm(sys.argv[2]))
if subcmd == "transcribe":
cmd_transcribe(sys.argv[2:])
return
if subcmd == "bench":
cmd_bench(sys.argv[2:])
return
if subcmd == "listen":
cmd_listen(sys.argv[2:])
return
if subcmd == "diagnose":
cmd_diagnose(sys.argv[2:])
return
if subcmd == "run":
cmd_run(sys.argv[2:])
return
if subcmd in ("-h", "--help", "help"):
_print_help()
return
if subcmd in ("version", "--version", "-V"):
_print_version()
return
if subcmd == "serve":
# Strip "serve" and pass remaining args to the server
sys.argv = [sys.argv[0]] + sys.argv[2:]
# Default: serve
from whisperlivekit.basic_server import main as serve_main
serve_main()
================================================
FILE: whisperlivekit/config.py
================================================
"""Typed configuration for the WhisperLiveKit pipeline."""
import logging
from dataclasses import dataclass, fields
from typing import Optional
logger = logging.getLogger(__name__)
@dataclass
class WhisperLiveKitConfig:
"""Single source of truth for all WhisperLiveKit configuration.
Replaces the previous dict-based parameter system in TranscriptionEngine.
All fields have defaults matching the prior behaviour.
"""
# Server / global
host: str = "localhost"
port: int = 8000
diarization: bool = False
punctuation_split: bool = False
target_language: str = ""
vac: bool = True
vac_chunk_size: float = 0.04
log_level: str = "DEBUG"
ssl_certfile: Optional[str] = None
ssl_keyfile: Optional[str] = None
forwarded_allow_ips: Optional[str] = None
transcription: bool = True
vad: bool = True
pcm_input: bool = False
disable_punctuation_split: bool = False
diarization_backend: str = "sortformer"
backend_policy: str = "simulstreaming"
backend: str = "auto"
# Transcription common
warmup_file: Optional[str] = None
min_chunk_size: float = 0.1
model_size: str = "base"
model_cache_dir: Optional[str] = None
model_dir: Optional[str] = None
model_path: Optional[str] = None
lora_path: Optional[str] = None
lan: str = "auto"
direct_english_translation: bool = False
# LocalAgreement-specific
buffer_trimming: str = "segment"
confidence_validation: bool = False
buffer_trimming_sec: float = 15.0
# SimulStreaming-specific
disable_fast_encoder: bool = False
custom_alignment_heads: Optional[str] = None
frame_threshold: int = 25
beams: int = 1
decoder_type: Optional[str] = None
audio_max_len: float = 30.0
audio_min_len: float = 0.0
cif_ckpt_path: Optional[str] = None
never_fire: bool = False
init_prompt: Optional[str] = None
static_init_prompt: Optional[str] = None
max_context_tokens: Optional[int] = None
# Diarization (diart)
segmentation_model: str = "pyannote/segmentation-3.0"
embedding_model: str = "pyannote/embedding"
# Translation
nllb_backend: str = "transformers"
nllb_size: str = "600M"
# vLLM Realtime backend
vllm_url: str = "ws://localhost:8000/v1/realtime"
vllm_model: str = ""
def __post_init__(self):
# .en model suffix forces English
if self.model_size and self.model_size.endswith(".en"):
self.lan = "en"
# Normalize backend_policy aliases
if self.backend_policy == "1":
self.backend_policy = "simulstreaming"
elif self.backend_policy == "2":
self.backend_policy = "localagreement"
# ------------------------------------------------------------------
# Factory helpers
# ------------------------------------------------------------------
@classmethod
def from_namespace(cls, ns) -> "WhisperLiveKitConfig":
"""Create config from an argparse Namespace, ignoring unknown keys."""
known = {f.name for f in fields(cls)}
return cls(**{k: v for k, v in vars(ns).items() if k in known})
@classmethod
def from_kwargs(cls, **kwargs) -> "WhisperLiveKitConfig":
"""Create config from keyword arguments; warns on unknown keys."""
known = {f.name for f in fields(cls)}
unknown = set(kwargs.keys()) - known
if unknown:
logger.warning("Unknown config keys ignored: %s", unknown)
return cls(**{k: v for k, v in kwargs.items() if k in known})
================================================
FILE: whisperlivekit/core.py
================================================
import logging
import threading
from argparse import Namespace
from dataclasses import asdict
from whisperlivekit.config import WhisperLiveKitConfig
from whisperlivekit.local_agreement.online_asr import OnlineASRProcessor
from whisperlivekit.local_agreement.whisper_online import backend_factory
from whisperlivekit.simul_whisper import SimulStreamingASR
logger = logging.getLogger(__name__)
class TranscriptionEngine:
_instance = None
_initialized = False
_lock = threading.Lock() # Thread-safe singleton lock
def __new__(cls, *args, **kwargs):
# Double-checked locking pattern for thread-safe singleton
if cls._instance is None:
with cls._lock:
# Check again inside lock to prevent race condition
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def reset(cls):
"""Reset the singleton so a new instance can be created.
For testing only — allows switching backends between test runs.
In production, the singleton should never be reset.
"""
with cls._lock:
cls._instance = None
cls._initialized = False
def __init__(self, config=None, **kwargs):
# Thread-safe initialization check
with TranscriptionEngine._lock:
if TranscriptionEngine._initialized:
return
try:
self._do_init(config, **kwargs)
except Exception:
# Reset singleton so a retry is possible
with TranscriptionEngine._lock:
TranscriptionEngine._instance = None
TranscriptionEngine._initialized = False
raise
with TranscriptionEngine._lock:
TranscriptionEngine._initialized = True
def _do_init(self, config=None, **kwargs):
# Handle negated kwargs from programmatic API
if 'no_transcription' in kwargs:
kwargs['transcription'] = not kwargs.pop('no_transcription')
if 'no_vad' in kwargs:
kwargs['vad'] = not kwargs.pop('no_vad')
if 'no_vac' in kwargs:
kwargs['vac'] = not kwargs.pop('no_vac')
if config is None:
if isinstance(kwargs.get('config'), WhisperLiveKitConfig):
config = kwargs.pop('config')
else:
config = WhisperLiveKitConfig.from_kwargs(**kwargs)
self.config = config
# Backward compat: expose as self.args (Namespace-like) for AudioProcessor etc.
self.args = Namespace(**asdict(config))
self.asr = None
self.tokenizer = None
self.diarization = None
self.vac_session = None
if config.vac:
from whisperlivekit.silero_vad_iterator import is_onnx_available
if is_onnx_available():
from whisperlivekit.silero_vad_iterator import load_onnx_session
self.vac_session = load_onnx_session()
else:
logger.warning(
"onnxruntime not installed. VAC will use JIT model which is loaded per-session. "
"For multi-user scenarios, install onnxruntime: pip install onnxruntime"
)
transcription_common_params = {
"warmup_file": config.warmup_file,
"min_chunk_size": config.min_chunk_size,
"model_size": config.model_size,
"model_cache_dir": config.model_cache_dir,
"model_dir": config.model_dir,
"model_path": config.model_path,
"lora_path": config.lora_path,
"lan": config.lan,
"direct_english_translation": config.direct_english_translation,
}
if config.transcription:
if config.backend == "vllm-realtime":
from whisperlivekit.vllm_realtime import VLLMRealtimeASR
self.tokenizer = None
self.asr = VLLMRealtimeASR(
vllm_url=config.vllm_url,
model_name=config.vllm_model or "Qwen/Qwen3-ASR-1.7B",
lan=config.lan,
)
logger.info("Using vLLM Realtime streaming backend at %s", config.vllm_url)
elif config.backend == "voxtral-mlx":
from whisperlivekit.voxtral_mlx_asr import VoxtralMLXASR
self.tokenizer = None
self.asr = VoxtralMLXASR(**transcription_common_params)
logger.info("Using Voxtral MLX native backend")
elif config.backend == "voxtral":
from whisperlivekit.voxtral_hf_streaming import VoxtralHFStreamingASR
self.tokenizer = None
self.asr = VoxtralHFStreamingASR(**transcription_common_params)
logger.info("Using Voxtral HF Transformers streaming backend")
elif config.backend == "qwen3-mlx-simul":
from whisperlivekit.qwen3_mlx_simul import Qwen3MLXSimulStreamingASR
self.tokenizer = None
self.asr = Qwen3MLXSimulStreamingASR(
**transcription_common_params,
alignment_heads_path=config.custom_alignment_heads,
border_fraction=getattr(config, 'border_fraction', 0.15),
)
logger.info("Using Qwen3 MLX SimulStreaming backend")
elif config.backend == "qwen3-mlx":
from whisperlivekit.qwen3_mlx_asr import Qwen3MLXASR
self.tokenizer = None
self.asr = Qwen3MLXASR(**transcription_common_params)
logger.info("Using Qwen3 MLX native backend")
elif config.backend == "qwen3-simul-kv":
from whisperlivekit.qwen3_simul_kv import Qwen3SimulKVASR
self.tokenizer = None
self.asr = Qwen3SimulKVASR(
**transcription_common_params,
alignment_heads_path=config.custom_alignment_heads,
border_fraction=getattr(config, 'border_fraction', 0.25),
)
logger.info("Using Qwen3-ASR backend with SimulStreaming+KV policy")
elif config.backend == "qwen3-simul":
from whisperlivekit.qwen3_simul import Qwen3SimulStreamingASR
self.tokenizer = None
self.asr = Qwen3SimulStreamingASR(
**transcription_common_params,
alignment_heads_path=config.custom_alignment_heads,
)
logger.info("Using Qwen3-ASR backend with SimulStreaming policy")
elif config.backend == "qwen3":
from whisperlivekit.qwen3_asr import Qwen3ASR
self.asr = Qwen3ASR(**transcription_common_params)
self.asr.confidence_validation = config.confidence_validation
self.asr.tokenizer = None
self.asr.buffer_trimming = config.buffer_trimming
self.asr.buffer_trimming_sec = config.buffer_trimming_sec
self.asr.backend_choice = "qwen3"
from whisperlivekit.warmup import warmup_asr
warmup_asr(self.asr, config.warmup_file)
logger.info("Using Qwen3-ASR backend with LocalAgreement policy")
elif config.backend_policy == "simulstreaming":
simulstreaming_params = {
"disable_fast_encoder": config.disable_fast_encoder,
"custom_alignment_heads": config.custom_alignment_heads,
"frame_threshold": config.frame_threshold,
"beams": config.beams,
"decoder_type": config.decoder_type,
"audio_max_len": config.audio_max_len,
"audio_min_len": config.audio_min_len,
"cif_ckpt_path": config.cif_ckpt_path,
"never_fire": config.never_fire,
"init_prompt": config.init_prompt,
"static_init_prompt": config.static_init_prompt,
"max_context_tokens": config.max_context_tokens,
}
self.tokenizer = None
self.asr = SimulStreamingASR(
**transcription_common_params,
**simulstreaming_params,
backend=config.backend,
)
logger.info(
"Using SimulStreaming policy with %s backend",
getattr(self.asr, "encoder_backend", "whisper"),
)
else:
whisperstreaming_params = {
"buffer_trimming": config.buffer_trimming,
"confidence_validation": config.confidence_validation,
"buffer_trimming_sec": config.buffer_trimming_sec,
}
self.asr = backend_factory(
backend=config.backend,
**transcription_common_params,
**whisperstreaming_params,
)
logger.info(
"Using LocalAgreement policy with %s backend",
getattr(self.asr, "backend_choice", self.asr.__class__.__name__),
)
if config.diarization:
if config.diarization_backend == "diart":
from whisperlivekit.diarization.diart_backend import DiartDiarization
self.diarization_model = DiartDiarization(
block_duration=config.min_chunk_size,
segmentation_model=config.segmentation_model,
embedding_model=config.embedding_model,
)
elif config.diarization_backend == "sortformer":
from whisperlivekit.diarization.sortformer_backend import SortformerDiarization
self.diarization_model = SortformerDiarization()
self.translation_model = None
if config.target_language:
if config.lan == 'auto' and config.backend_policy != "simulstreaming":
raise ValueError('Translation cannot be set with language auto when transcription backend is not simulstreaming')
else:
try:
from nllw import load_model
except ImportError:
raise ImportError('To use translation, you must install nllw: `pip install nllw`')
self.translation_model = load_model(
[config.lan],
nllb_backend=config.nllb_backend,
nllb_size=config.nllb_size,
)
def online_factory(args, asr, language=None):
"""Create an online ASR processor for a session.
Args:
args: Configuration namespace.
asr: Shared ASR backend instance.
language: Optional per-session language override (e.g. "en", "fr", "auto").
If provided and the backend supports it, transcription will use
this language instead of the server-wide default.
"""
# Wrap the shared ASR with a per-session language if requested
if language is not None:
from whisperlivekit.session_asr_proxy import SessionASRProxy
asr = SessionASRProxy(asr, language)
backend = getattr(args, 'backend', None)
if backend == "vllm-realtime":
from whisperlivekit.vllm_realtime import VLLMRealtimeOnlineProcessor
return VLLMRealtimeOnlineProcessor(asr)
if backend == "qwen3-simul-kv":
from whisperlivekit.qwen3_simul_kv import Qwen3SimulKVOnlineProcessor
return Qwen3SimulKVOnlineProcessor(asr)
if backend == "qwen3-mlx-simul":
from whisperlivekit.qwen3_mlx_simul import Qwen3MLXSimulStreamingOnlineProcessor
return Qwen3MLXSimulStreamingOnlineProcessor(asr)
if backend == "qwen3-mlx":
from whisperlivekit.qwen3_mlx_asr import Qwen3MLXOnlineProcessor
return Qwen3MLXOnlineProcessor(asr)
if backend == "qwen3-simul":
from whisperlivekit.qwen3_simul import Qwen3SimulStreamingOnlineProcessor
return Qwen3SimulStreamingOnlineProcessor(asr)
if backend == "voxtral-mlx":
from whisperlivekit.voxtral_mlx_asr import VoxtralMLXOnlineProcessor
return VoxtralMLXOnlineProcessor(asr)
if backend == "voxtral":
from whisperlivekit.voxtral_hf_streaming import VoxtralHFStreamingOnlineProcessor
return VoxtralHFStreamingOnlineProcessor(asr)
if backend == "qwen3":
return OnlineASRProcessor(asr)
if args.backend_policy == "simulstreaming":
from whisperlivekit.simul_whisper import SimulStreamingOnlineProcessor
return SimulStreamingOnlineProcessor(asr)
return OnlineASRProcessor(asr)
def online_diarization_factory(args, diarization_backend):
if args.diarization_backend == "diart":
online = diarization_backend
# Not the best here, since several user/instances will share the same backend, but diart is not SOTA anymore and sortformer is recommended
elif args.diarization_backend == "sortformer":
from whisperlivekit.diarization.sortformer_backend import SortformerDiarizationOnline
online = SortformerDiarizationOnline(shared_model=diarization_backend)
else:
raise ValueError(f"Unknown diarization backend: {args.diarization_backend}")
return online
def online_translation_factory(args, translation_model):
#should be at speaker level in the future:
#one shared nllb model for all speaker
#one tokenizer per speaker/language
from nllw import OnlineTranslation
return OnlineTranslation(translation_model, [args.lan], [args.target_language])
================================================
FILE: whisperlivekit/deepgram_compat.py
================================================
"""Deepgram-compatible WebSocket endpoint for WhisperLiveKit.
Provides a /v1/listen endpoint that speaks the Deepgram Live Transcription
protocol, enabling drop-in compatibility with Deepgram client SDKs.
Protocol mapping:
- Client sends binary audio frames → forwarded to AudioProcessor
- Client sends JSON control messages (KeepAlive, CloseStream, Finalize)
- Server sends Results, Metadata, UtteranceEnd messages
Differences from Deepgram:
- No authentication required (self-hosted)
- Word-level timestamps approximate (interpolated from segment boundaries)
- Confidence scores not available (set to 0.0)
"""
import asyncio
import json
import logging
import time
import uuid
from fastapi import WebSocket, WebSocketDisconnect
logger = logging.getLogger(__name__)
def _parse_time_str(time_str: str) -> float:
"""Parse 'H:MM:SS.cc' to seconds."""
parts = time_str.split(":")
if len(parts) == 3:
return int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2])
if len(parts) == 2:
return int(parts[0]) * 60 + float(parts[1])
return float(parts[0])
def _line_to_words(line: dict) -> list:
"""Convert a line dict to Deepgram-style word objects.
Distributes timestamps proportionally across words since
WhisperLiveKit provides segment-level timestamps.
"""
text = line.get("text", "")
if not text or not text.strip():
return []
start = _parse_time_str(line.get("start", "0:00:00"))
end = _parse_time_str(line.get("end", "0:00:00"))
speaker = line.get("speaker", 0)
if speaker == -2:
return []
words = text.split()
if not words:
return []
duration = end - start
step = duration / max(len(words), 1)
return [
{
"word": w,
"start": round(start + i * step, 3),
"end": round(start + (i + 1) * step, 3),
"confidence": 0.0,
"punctuated_word": w,
"speaker": speaker if speaker > 0 else 0,
}
for i, w in enumerate(words)
]
def _lines_to_result(lines: list, is_final: bool, speech_final: bool,
start_time: float = 0.0) -> dict:
"""Convert FrontData lines to a Deepgram Results message."""
all_words = []
full_text_parts = []
for line in lines:
if line.get("speaker") == -2:
continue
words = _line_to_words(line)
all_words.extend(words)
text = line.get("text", "")
if text and text.strip():
full_text_parts.append(text.strip())
transcript = " ".join(full_text_parts)
# Calculate duration from word boundaries
if all_words:
seg_start = all_words[0]["start"]
seg_end = all_words[-1]["end"]
duration = seg_end - seg_start
else:
seg_start = start_time
seg_end = start_time
duration = 0.0
return {
"type": "Results",
"channel_index": [0, 1],
"duration": round(duration, 3),
"start": round(seg_start, 3),
"is_final": is_final,
"speech_final": speech_final,
"channel": {
"alternatives": [
{
"transcript": transcript,
"confidence": 0.0,
"words": all_words,
}
]
},
}
class DeepgramAdapter:
"""Adapts WhisperLiveKit's FrontData stream to Deepgram's protocol."""
def __init__(self, websocket: WebSocket):
self.websocket = websocket
self.request_id = str(uuid.uuid4())
self._prev_n_lines = 0
self._sent_lines = 0
self._last_word_end = 0.0
self._speech_started_sent = False
self._vad_events = False
async def send_metadata(self, config):
"""Send initial Metadata message."""
backend = getattr(config, "backend", "whisper") if config else "whisper"
msg = {
"type": "Metadata",
"request_id": self.request_id,
"sha256": "",
"created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"duration": 0,
"channels": 1,
"models": [backend],
"model_info": {
backend: {
"name": backend,
"version": "whisperlivekit",
}
},
}
await self.websocket.send_json(msg)
async def process_update(self, front_data_dict: dict):
"""Convert a FrontData dict into Deepgram messages and send them."""
lines = front_data_dict.get("lines", [])
buffer = front_data_dict.get("buffer_transcription", "")
speech_lines = [l for l in lines if l.get("speaker", 0) != -2]
n_speech = len(speech_lines)
# Detect new committed lines → emit as is_final=true results
if n_speech > self._sent_lines:
new_lines = speech_lines[self._sent_lines:]
result = _lines_to_result(new_lines, is_final=True, speech_final=True)
await self.websocket.send_json(result)
# Track last word end for UtteranceEnd
if result["channel"]["alternatives"][0]["words"]:
self._last_word_end = result["channel"]["alternatives"][0]["words"][-1]["end"]
self._sent_lines = n_speech
# Emit buffer as interim result (is_final=false)
elif buffer and buffer.strip():
# SpeechStarted event
if self._vad_events and not self._speech_started_sent:
await self.websocket.send_json({
"type": "SpeechStarted",
"channel_index": [0],
"timestamp": 0.0,
})
self._speech_started_sent = True
# Create interim result from buffer
interim = {
"type": "Results",
"channel_index": [0, 1],
"duration": 0.0,
"start": self._last_word_end,
"is_final": False,
"speech_final": False,
"channel": {
"alternatives": [
{
"transcript": buffer.strip(),
"confidence": 0.0,
"words": [],
}
]
},
}
await self.websocket.send_json(interim)
# Detect silence → emit UtteranceEnd
silence_lines = [l for l in lines if l.get("speaker") == -2]
if silence_lines and n_speech > 0:
# Check if there's new silence after our last speech
for sil in silence_lines:
sil_start = _parse_time_str(sil.get("start", "0:00:00"))
if sil_start >= self._last_word_end:
await self.websocket.send_json({
"type": "UtteranceEnd",
"channel": [0, 1],
"last_word_end": round(self._last_word_end, 3),
})
self._speech_started_sent = False
break
async def handle_deepgram_websocket(websocket: WebSocket, transcription_engine, config):
"""Handle a Deepgram-compatible WebSocket session."""
from whisperlivekit.audio_processor import AudioProcessor
# Parse Deepgram query parameters
params = websocket.query_params
language = params.get("language", None)
vad_events = params.get("vad_events", "false").lower() == "true"
audio_processor = AudioProcessor(
transcription_engine=transcription_engine,
language=language,
)
await websocket.accept()
logger.info("Deepgram-compat WebSocket opened")
adapter = DeepgramAdapter(websocket)
adapter._vad_events = vad_events
# Send metadata
await adapter.send_metadata(config)
results_generator = await audio_processor.create_tasks()
# Results consumer
async def handle_results():
try:
async for response in results_generator:
await adapter.process_update(response.to_dict())
except WebSocketDisconnect:
pass
except Exception as e:
logger.exception(f"Deepgram compat results error: {e}")
results_task = asyncio.create_task(handle_results())
# Audio / control message consumer
try:
while True:
try:
# Try to receive as text first (for control messages)
message = await asyncio.wait_for(
websocket.receive(), timeout=30.0,
)
except asyncio.TimeoutError:
# No data for 30s — close
break
if "bytes" in message:
data = message["bytes"]
if data:
await audio_processor.process_audio(data)
else:
# Empty bytes = end of audio
await audio_processor.process_audio(b"")
break
elif "text" in message:
try:
ctrl = json.loads(message["text"])
msg_type = ctrl.get("type", "")
if msg_type == "CloseStream":
await audio_processor.process_audio(b"")
break
elif msg_type == "Finalize":
# Flush current audio — trigger end-of-utterance
await audio_processor.process_audio(b"")
results_generator = await audio_processor.create_tasks()
elif msg_type == "KeepAlive":
pass # Just keep the connection alive
else:
logger.debug("Unknown Deepgram control message: %s", msg_type)
except json.JSONDecodeError:
logger.warning("Invalid JSON control message")
else:
# WebSocket close
break
except WebSocketDisconnect:
logger.info("Deepgram-compat WebSocket disconnected")
except Exception as e:
logger.error(f"Deepgram-compat error: {e}", exc_info=True)
finally:
if not results_task.done():
results_task.cancel()
try:
await results_task
except (asyncio.CancelledError, Exception):
pass
await audio_processor.cleanup()
logger.info("Deepgram-compat WebSocket cleaned up")
================================================
FILE: whisperlivekit/diarization/__init__.py
================================================
================================================
FILE: whisperlivekit/diarization/diart_backend.py
================================================
import asyncio
import logging
import threading
import time
from queue import Empty, SimpleQueue
from typing import Any, List, Tuple
import diart.models as m
import numpy as np
from diart import SpeakerDiarization, SpeakerDiarizationConfig
from diart.inference import StreamingInference
from diart.sources import AudioSource, MicrophoneAudioSource
from pyannote.core import Annotation
from rx.core import Observer
from whisperlivekit.diarization.utils import extract_number
from whisperlivekit.timed_objects import SpeakerSegment
logger = logging.getLogger(__name__)
class DiarizationObserver(Observer):
"""Observer that logs all data emitted by the diarization pipeline and stores speaker segments."""
def __init__(self):
self.diarization_segments = []
self.processed_time = 0
self.segment_lock = threading.Lock()
self.global_time_offset = 0.0
def on_next(self, value: Tuple[Annotation, Any]):
annotation, audio = value
logger.debug("\n--- New Diarization Result ---")
duration = audio.extent.end - audio.extent.start
logger.debug(f"Audio segment: {audio.extent.start:.2f}s - {audio.extent.end:.2f}s (duration: {duration:.2f}s)")
logger.debug(f"Audio shape: {audio.data.shape}")
with self.segment_lock:
if audio.extent.end > self.processed_time:
self.processed_time = audio.extent.end
if annotation and len(annotation._labels) > 0:
logger.debug("\nSpeaker segments:")
for speaker, label in annotation._labels.items():
for start, end in zip(label.segments_boundaries_[:-1], label.segments_boundaries_[1:]):
print(f" {speaker}: {start:.2f}s-{end:.2f}s")
self.diarization_segments.append(SpeakerSegment(
speaker=speaker,
start=start + self.global_time_offset,
end=end + self.global_time_offset
))
else:
logger.debug("\nNo speakers detected in this segment")
def get_segments(self) -> List[SpeakerSegment]:
"""Get a copy of the current speaker segments."""
with self.segment_lock:
return self.diarization_segments.copy()
def clear_old_segments(self, older_than: float = 30.0):
"""Clear segments older than the specified time."""
with self.segment_lock:
current_time = self.processed_time
self.diarization_segments = [
segment for segment in self.diarization_segments
if current_time - segment.end < older_than
]
def on_error(self, error):
"""Handle an error in the stream."""
logger.debug(f"Error in diarization stream: {error}")
def on_completed(self):
"""Handle the completion of the stream."""
logger.debug("Diarization stream completed")
class WebSocketAudioSource(AudioSource):
"""
Buffers incoming audio and releases it in fixed-size chunks at regular intervals.
"""
def __init__(self, uri: str = "websocket", sample_rate: int = 16000, block_duration: float = 0.5):
super().__init__(uri, sample_rate)
self.block_duration = block_duration
self.block_size = int(np.rint(block_duration * sample_rate))
self._queue = SimpleQueue()
self._buffer = np.array([], dtype=np.float32)
self._buffer_lock = threading.Lock()
self._closed = False
self._close_event = threading.Event()
self._processing_thread = None
self._last_chunk_time = time.time()
def read(self):
"""Start processing buffered audio and emit fixed-size chunks."""
self._processing_thread = threading.Thread(target=self._process_chunks)
self._processing_thread.daemon = True
self._processing_thread.start()
self._close_event.wait()
if self._processing_thread:
self._processing_thread.join(timeout=2.0)
def _process_chunks(self):
"""Process audio from queue and emit fixed-size chunks at regular intervals."""
while not self._closed:
try:
audio_chunk = self._queue.get(timeout=0.1)
with self._buffer_lock:
self._buffer = np.concatenate([self._buffer, audio_chunk])
while len(self._buffer) >= self.block_size:
chunk = self._buffer[:self.block_size]
self._buffer = self._buffer[self.block_size:]
current_time = time.time()
time_since_last = current_time - self._last_chunk_time
if time_since_last < self.block_duration:
time.sleep(self.block_duration - time_since_last)
chunk_reshaped = chunk.reshape(1, -1)
self.stream.on_next(chunk_reshaped)
self._last_chunk_time = time.time()
except Empty:
with self._buffer_lock:
if len(self._buffer) > 0 and time.time() - self._last_chunk_time > self.block_duration:
padded_chunk = np.zeros(self.block_size, dtype=np.float32)
padded_chunk[:len(self._buffer)] = self._buffer
self._buffer = np.array([], dtype=np.float32)
chunk_reshaped = padded_chunk.reshape(1, -1)
self.stream.on_next(chunk_reshaped)
self._last_chunk_time = time.time()
except Exception as e:
logger.error(f"Error in audio processing thread: {e}")
self.stream.on_error(e)
break
with self._buffer_lock:
if len(self._buffer) > 0:
padded_chunk = np.zeros(self.block_size, dtype=np.float32)
padded_chunk[:len(self._buffer)] = self._buffer
chunk_reshaped = padded_chunk.reshape(1, -1)
self.stream.on_next(chunk_reshaped)
self.stream.on_completed()
def close(self):
if not self._closed:
self._closed = True
self._close_event.set()
def push_audio(self, chunk: np.ndarray):
"""Add audio chunk to the processing queue."""
if not self._closed:
if chunk.ndim > 1:
chunk = chunk.flatten()
self._queue.put(chunk)
logger.debug(f'Added chunk to queue with {len(chunk)} samples')
class DiartDiarization:
def __init__(self, sample_rate: int = 16000, config : SpeakerDiarizationConfig = None, use_microphone: bool = False, block_duration: float = 1.5, segmentation_model_name: str = "pyannote/segmentation-3.0", embedding_model_name: str = "pyannote/embedding"):
segmentation_model = m.SegmentationModel.from_pretrained(segmentation_model_name)
embedding_model = m.EmbeddingModel.from_pretrained(embedding_model_name)
if config is None:
config = SpeakerDiarizationConfig(
segmentation=segmentation_model,
embedding=embedding_model,
)
self.pipeline = SpeakerDiarization(config=config)
self.observer = DiarizationObserver()
if use_microphone:
self.source = MicrophoneAudioSource(block_duration=block_duration)
self.custom_source = None
else:
self.custom_source = WebSocketAudioSource(
uri="websocket_source",
sample_rate=sample_rate,
block_duration=block_duration
)
self.source = self.custom_source
self.inference = StreamingInference(
pipeline=self.pipeline,
source=self.source,
do_plot=False,
show_progress=False,
)
self.inference.attach_observers(self.observer)
asyncio.get_event_loop().run_in_executor(None, self.inference)
def insert_silence(self, silence_duration):
self.observer.global_time_offset += silence_duration
def insert_audio_chunk(self, pcm_array: np.ndarray):
"""Buffer audio for the next diarization step."""
if self.custom_source:
self.custom_source.push_audio(pcm_array)
async def diarize(self):
"""Return the current speaker segments from the diarization pipeline."""
return self.observer.get_segments()
def close(self):
"""Close the audio source."""
if self.custom_source:
self.custom_source.close()
def concatenate_speakers(segments):
segments_concatenated = [{"speaker": 1, "begin": 0.0, "end": 0.0}]
for segment in segments:
speaker = extract_number(segment.speaker) + 1
if segments_concatenated[-1]['speaker'] != speaker:
segments_concatenated.append({"speaker": speaker, "begin": segment.start, "end": segment.end})
else:
segments_concatenated[-1]['end'] = segment.end
# print("Segments concatenated:")
# for entry in segments_concatenated:
# print(f"Speaker {entry['speaker']}: {entry['begin']:.2f}s - {entry['end']:.2f}s")
return segments_concatenated
def add_speaker_to_tokens(segments, tokens):
"""
Assign speakers to tokens based on diarization segments, with punctuation-aware boundary adjustment.
"""
punctuation_marks = {'.', '!', '?'}
punctuation_tokens = [token for token in tokens if token.text.strip() in punctuation_marks]
segments_concatenated = concatenate_speakers(segments)
for ind, segment in enumerate(segments_concatenated):
for i, punctuation_token in enumerate(punctuation_tokens):
if punctuation_token.start > segment['end']:
after_length = punctuation_token.start - segment['end']
before_length = segment['end'] - punctuation_tokens[i - 1].end
if before_length > after_length:
segment['end'] = punctuation_token.start
if i < len(punctuation_tokens) - 1 and ind + 1 < len(segments_concatenated):
segments_concatenated[ind + 1]['begin'] = punctuation_token.start
else:
segment['end'] = punctuation_tokens[i - 1].end
if i < len(punctuation_tokens) - 1 and ind - 1 >= 0:
segments_concatenated[ind - 1]['begin'] = punctuation_tokens[i - 1].end
break
last_end = 0.0
for token in tokens:
start = max(last_end + 0.01, token.start)
token.start = start
token.end = max(start, token.end)
last_end = token.end
ind_last_speaker = 0
for segment in segments_concatenated:
for i, token in enumerate(tokens[ind_last_speaker:]):
if token.end <= segment['end']:
token.speaker = segment['speaker']
ind_last_speaker = i + 1
# print(
# f"Token '{token.text}' ('begin': {token.start:.2f}, 'end': {token.end:.2f}) "
# f"assigned to Speaker {segment['speaker']} ('segment': {segment['begin']:.2f}-{segment['end']:.2f})"
# )
elif token.start > segment['end']:
break
return tokens
def visualize_tokens(tokens):
conversation = [{"speaker": -1, "text": ""}]
for token in tokens:
speaker = conversation[-1]['speaker']
if token.speaker != speaker:
conversation.append({"speaker": token.speaker, "text": token.text})
else:
conversation[-1]['text'] += token.text
print("Conversation:")
for entry in conversation:
print(f"Speaker {entry['speaker']}: {entry['text']}")
================================================
FILE: whisperlivekit/diarization/sortformer_backend.py
================================================
import logging
import threading
import wave
from typing import List, Optional
import numpy as np
import torch
from whisperlivekit.timed_objects import SpeakerSegment
logger = logging.getLogger(__name__)
try:
from nemo.collections.asr.models import SortformerEncLabelModel
from nemo.collections.asr.modules import AudioToMelSpectrogramPreprocessor
except ImportError:
raise SystemExit("""Please use `pip install "git+https://github.com/NVIDIA/NeMo.git@main#egg=nemo_toolkit[asr]"` to use the Sortformer diarization""")
class StreamingSortformerState:
"""
This class creates a class instance that will be used to store the state of the
streaming Sortformer model.
Attributes:
spkcache (torch.Tensor): Speaker cache to store embeddings from start
spkcache_lengths (torch.Tensor): Lengths of the speaker cache
spkcache_preds (torch.Tensor): The speaker predictions for the speaker cache parts
fifo (torch.Tensor): FIFO queue to save the embedding from the latest chunks
fifo_lengths (torch.Tensor): Lengths of the FIFO queue
fifo_preds (torch.Tensor): The speaker predictions for the FIFO queue parts
spk_perm (torch.Tensor): Speaker permutation information for the speaker cache
mean_sil_emb (torch.Tensor): Mean silence embedding
n_sil_frames (torch.Tensor): Number of silence frames
"""
def __init__(self):
self.spkcache = None # Speaker cache to store embeddings from start
self.spkcache_lengths = None
self.spkcache_preds = None # speaker cache predictions
self.fifo = None # to save the embedding from the latest chunks
self.fifo_lengths = None
self.fifo_preds = None
self.spk_perm = None
self.mean_sil_emb = None
self.n_sil_frames = None
class SortformerDiarization:
def __init__(self, model_name: str = "nvidia/diar_streaming_sortformer_4spk-v2"):
"""
Stores the shared streaming Sortformer diarization model. Used when a new online_diarization is initialized.
"""
self._load_model(model_name)
def _load_model(self, model_name: str):
"""Load and configure the Sortformer model for streaming."""
try:
self.diar_model = SortformerEncLabelModel.from_pretrained(model_name)
self.diar_model.eval()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.diar_model.to(device)
## to test
# for name, param in self.diar_model.named_parameters():
# if param.device != device:
# raise RuntimeError(f"Parameter {name} is on {param.device} but should be on {device}")
logger.info(f"Using {device.type.upper()} for Sortformer model")
self.diar_model.sortformer_modules.chunk_len = 10
self.diar_model.sortformer_modules.subsampling_factor = 10
self.diar_model.sortformer_modules.chunk_right_context = 0
self.diar_model.sortformer_modules.chunk_left_context = 10
self.diar_model.sortformer_modules.spkcache_len = 188
self.diar_model.sortformer_modules.fifo_len = 188
self.diar_model.sortformer_modules.spkcache_update_period = 144
self.diar_model.sortformer_modules.log = False
self.diar_model.sortformer_modules._check_streaming_parameters()
except Exception as e:
logger.error(f"Failed to load Sortformer model: {e}")
raise
class SortformerDiarizationOnline:
def __init__(self, shared_model, sample_rate: int = 16000):
"""
Initialize the streaming Sortformer diarization system.
Args:
sample_rate: Audio sample rate (default: 16000)
model_name: Pre-trained model name (default: "nvidia/diar_streaming_sortformer_4spk-v2")
"""
self.sample_rate = sample_rate
self.diarization_segments = []
self.diar_segments = []
self.buffer_audio = np.array([], dtype=np.float32)
self.segment_lock = threading.Lock()
self.global_time_offset = 0.0
self.debug = False
self.diar_model = shared_model.diar_model
self.audio2mel = AudioToMelSpectrogramPreprocessor(
window_size=0.025,
normalize="NA",
n_fft=512,
features=128,
pad_to=0
)
self.audio2mel.to(self.diar_model.device)
self.chunk_duration_seconds = (
self.diar_model.sortformer_modules.chunk_len *
self.diar_model.sortformer_modules.subsampling_factor *
self.diar_model.preprocessor._cfg.window_stride
)
self._init_streaming_state()
self._previous_chunk_features = None
self._chunk_index = 0
self._len_prediction = None
# Audio buffer to store PCM chunks for debugging
self.audio_buffer = []
# Buffer for accumulating audio chunks until reaching chunk_duration_seconds
self.audio_chunk_buffer = []
self.accumulated_duration = 0.0
logger.info("SortformerDiarization initialized successfully")
def _init_streaming_state(self):
"""Initialize the streaming state for the model."""
batch_size = 1
device = self.diar_model.device
self.streaming_state = StreamingSortformerState()
self.streaming_state.spkcache = torch.zeros(
(batch_size, self.diar_model.sortformer_modules.spkcache_len, self.diar_model.sortformer_modules.fc_d_model),
device=device
)
self.streaming_state.spkcache_preds = torch.zeros(
(batch_size, self.diar_model.sortformer_modules.spkcache_len, self.diar_model.sortformer_modules.n_spk),
device=device
)
self.streaming_state.spkcache_lengths = torch.zeros((batch_size,), dtype=torch.long, device=device)
self.streaming_state.fifo = torch.zeros(
(batch_size, self.diar_model.sortformer_modules.fifo_len, self.diar_model.sortformer_modules.fc_d_model),
device=device
)
self.streaming_state.fifo_lengths = torch.zeros((batch_size,), dtype=torch.long, device=device)
self.streaming_state.mean_sil_emb = torch.zeros((batch_size, self.diar_model.sortformer_modules.fc_d_model), device=device)
self.streaming_state.n_sil_frames = torch.zeros((batch_size,), dtype=torch.long, device=device)
self.total_preds = torch.zeros((batch_size, 0, self.diar_model.sortformer_modules.n_spk), device=device)
def insert_silence(self, silence_duration: Optional[float]):
"""
Insert silence period by adjusting the global time offset.
Args:
silence_duration: Duration of silence in seconds
"""
with self.segment_lock:
self.global_time_offset += silence_duration
logger.debug(f"Inserted silence of {silence_duration:.2f}s, new offset: {self.global_time_offset:.2f}s")
def insert_audio_chunk(self, pcm_array: np.ndarray):
if self.debug:
self.audio_buffer.append(pcm_array.copy())
self.buffer_audio = np.concatenate([self.buffer_audio, pcm_array.copy()])
async def diarize(self):
"""
Process audio data for diarization in streaming fashion.
Args:
pcm_array: Audio data as numpy array
"""
threshold = int(self.chunk_duration_seconds * self.sample_rate)
if not len(self.buffer_audio) >= threshold:
return []
audio = self.buffer_audio[:threshold]
self.buffer_audio = self.buffer_audio[threshold:]
device = self.diar_model.device
audio_signal_chunk = torch.tensor(audio, device=device).unsqueeze(0)
audio_signal_length_chunk = torch.tensor([audio_signal_chunk.shape[1]], device=device)
processed_signal_chunk, processed_signal_length_chunk = self.audio2mel.get_features(
audio_signal_chunk, audio_signal_length_chunk
)
processed_signal_chunk = processed_signal_chunk.to(device)
processed_signal_length_chunk = processed_signal_length_chunk.to(device)
if self._previous_chunk_features is not None:
to_add = self._previous_chunk_features[:, :, -99:].to(device)
total_features = torch.concat([to_add, processed_signal_chunk], dim=2).to(device)
else:
total_features = processed_signal_chunk.to(device)
self._previous_chunk_features = processed_signal_chunk.to(device)
chunk_feat_seq_t = torch.transpose(total_features, 1, 2).to(device)
with torch.inference_mode():
left_offset = 8 if self._chunk_index > 0 else 0
right_offset = 8
self.streaming_state, self.total_preds = self.diar_model.forward_streaming_step(
processed_signal=chunk_feat_seq_t,
processed_signal_length=torch.tensor([chunk_feat_seq_t.shape[1]]).to(device),
streaming_state=self.streaming_state,
total_preds=self.total_preds,
left_offset=left_offset,
right_offset=right_offset,
)
new_segments = self._process_predictions()
self._chunk_index += 1
return new_segments
def _process_predictions(self):
"""Process model predictions and convert to speaker segments."""
preds_np = self.total_preds[0].cpu().numpy()
active_speakers = np.argmax(preds_np, axis=1)
if self._len_prediction is None:
self._len_prediction = len(active_speakers) #12
frame_duration = self.chunk_duration_seconds / self._len_prediction
current_chunk_preds = active_speakers[-self._len_prediction:]
new_segments = []
with self.segment_lock:
base_time = self._chunk_index * self.chunk_duration_seconds + self.global_time_offset
current_spk = current_chunk_preds[0]
start_time = round(base_time, 2)
for idx, spk in enumerate(current_chunk_preds):
current_time = round(base_time + idx * frame_duration, 2)
if spk != current_spk:
new_segments.append(SpeakerSegment(
speaker=current_spk,
start=start_time,
end=current_time
))
start_time = current_time
current_spk = spk
new_segments.append(
SpeakerSegment(
speaker=current_spk,
start=start_time,
end=current_time
)
)
return new_segments
def get_segments(self) -> List[SpeakerSegment]:
"""Get a copy of the current speaker segments."""
with self.segment_lock:
return self.diarization_segments.copy()
def close(self):
"""Close the diarization system and clean up resources."""
logger.info("Closing SortformerDiarization")
with self.segment_lock:
self.diarization_segments.clear()
if self.debug:
concatenated_audio = np.concatenate(self.audio_buffer)
audio_data_int16 = (concatenated_audio * 32767).astype(np.int16)
with wave.open("diarization_audio.wav", "wb") as wav_file:
wav_file.setnchannels(1) # mono audio
wav_file.setsampwidth(2) # 2 bytes per sample (int16)
wav_file.setframerate(self.sample_rate)
wav_file.writeframes(audio_data_int16.tobytes())
logger.info(f"Saved {len(concatenated_audio)} samples to diarization_audio.wav")
if __name__ == '__main__':
import asyncio
import librosa
async def main():
"""TEST ONLY."""
an4_audio = 'diarization_audio.wav'
signal, sr = librosa.load(an4_audio, sr=16000)
signal = signal[:16000*30]
print("\n" + "=" * 50)
print("ground truth:")
print("Speaker 0: 0:00 - 0:09")
print("Speaker 1: 0:09 - 0:19")
print("Speaker 2: 0:19 - 0:25")
print("Speaker 0: 0:25 - 0:30")
print("=" * 50)
diarization_backend = SortformerDiarization()
diarization = SortformerDiarizationOnline(shared_model = diarization_backend)
chunk_size = 1600
for i in range(0, len(signal), chunk_size):
chunk = signal[i:i+chunk_size]
new_segments = await diarization.diarize(chunk)
print(f"Processed chunk {i // chunk_size + 1}")
print(new_segments)
segments = diarization.get_segments()
print("\nDiarization results:")
for segment in segments:
print(f"Speaker {segment.speaker}: {segment.start:.2f}s - {segment.end:.2f}s")
asyncio.run(main())
================================================
FILE: whisperlivekit/diarization/utils.py
================================================
import re
def extract_number(s: str) -> int:
"""Extract the first integer from a string, e.g. 'speaker_2' -> 2."""
m = re.search(r'\d+', s)
return int(m.group()) if m else 0
================================================
FILE: whisperlivekit/diff_protocol.py
================================================
"""Diff-based WebSocket output protocol for WhisperLiveKit.
Instead of sending the full FrontData state on every update, the DiffTracker
computes incremental diffs — only sending new/changed lines and volatile fields.
Protocol
--------
Opt-in via query parameter: ``ws://host:port/asr?mode=diff``
First message from server:
``{"type": "snapshot", "seq": 1, ...full state...}``
Subsequent messages:
``{"type": "diff", "seq": N, "new_lines": [...], ...}``
The client reconstructs state by:
1. On ``"snapshot"``: replace all state.
2. On ``"diff"``:
- If ``lines_pruned`` > 0: drop that many lines from the front.
- Append ``new_lines`` to the end.
- Replace ``buffer_*`` and ``remaining_time_*`` fields.
- Use ``n_lines`` to verify sync (total expected line count).
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List
from whisperlivekit.timed_objects import FrontData
@dataclass
class DiffTracker:
"""Tracks FrontData state and computes incremental diffs."""
seq: int = 0
_prev_lines: List[Dict[str, Any]] = field(default_factory=list)
_sent_snapshot: bool = False
def to_message(self, front_data: FrontData) -> Dict[str, Any]:
"""Convert a FrontData into a diff or snapshot message.
First call returns a full snapshot. Subsequent calls return diffs
containing only changed/new data.
"""
self.seq += 1
full = front_data.to_dict()
current_lines = full["lines"]
if not self._sent_snapshot:
self._sent_snapshot = True
self._prev_lines = current_lines[:]
return {"type": "snapshot", "seq": self.seq, **full}
# Compute diff
msg: Dict[str, Any] = {
"type": "diff",
"seq": self.seq,
"status": full["status"],
"n_lines": len(current_lines),
"buffer_transcription": full["buffer_transcription"],
"buffer_diarization": full["buffer_diarization"],
"buffer_translation": full["buffer_translation"],
"remaining_time_transcription": full["remaining_time_transcription"],
"remaining_time_diarization": full["remaining_time_diarization"],
}
if full.get("error"):
msg["error"] = full["error"]
# Detect front-pruning: find where current[0] appears in prev
prune_offset = 0
if current_lines and self._prev_lines:
first_current = current_lines[0]
for i, prev_line in enumerate(self._prev_lines):
if prev_line == first_current:
prune_offset = i
break
else:
# current[0] not found in prev — treat all prev as pruned
prune_offset = len(self._prev_lines)
elif not current_lines:
prune_offset = len(self._prev_lines)
if prune_offset > 0:
msg["lines_pruned"] = prune_offset
# Find common prefix starting after pruned lines
common = 0
remaining_prev = len(self._prev_lines) - prune_offset
min_len = min(remaining_prev, len(current_lines))
while common < min_len and self._prev_lines[prune_offset + common] == current_lines[common]:
common += 1
# New or changed lines after the common prefix
new_lines = current_lines[common:]
if new_lines:
msg["new_lines"] = new_lines
self._prev_lines = current_lines[:]
return msg
def reset(self) -> None:
"""Reset state so the next call produces a fresh snapshot."""
self.seq = 0
self._prev_lines = []
self._sent_snapshot = False
================================================
FILE: whisperlivekit/ffmpeg_manager.py
================================================
import asyncio
import contextlib
import logging
from enum import Enum
from typing import Callable, Optional
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
ERROR_INSTALL_INSTRUCTIONS = f"""
{'='*50}
FFmpeg is not installed or not found in your system's PATH.
Alternative Solution: You can still use WhisperLiveKit without FFmpeg by adding the --pcm-input parameter. Note that when using this option, audio will not be compressed between the frontend and backend, which may result in higher bandwidth usage.
If you want to install FFmpeg:
# Ubuntu/Debian:
sudo apt update && sudo apt install ffmpeg
# macOS (using Homebrew):
brew install ffmpeg
# Windows:
# 1. Download the latest static build from https://ffmpeg.org/download.html
# 2. Extract the archive (e.g., to C:\\FFmpeg).
# 3. Add the 'bin' directory (e.g., C:\\FFmpeg\\bin) to your system's PATH environment variable.
After installation, please restart the application.
{'='*50}
"""
class FFmpegState(Enum):
STOPPED = "stopped"
STARTING = "starting"
RUNNING = "running"
RESTARTING = "restarting"
FAILED = "failed"
class FFmpegManager:
def __init__(self, sample_rate: int = 16000, channels: int = 1):
self.sample_rate = sample_rate
self.channels = channels
self.process: Optional[asyncio.subprocess.Process] = None
self._stderr_task: Optional[asyncio.Task] = None
self.on_error_callback: Optional[Callable[[str], None]] = None
self.state = FFmpegState.STOPPED
self._state_lock = asyncio.Lock()
async def start(self) -> bool:
async with self._state_lock:
if self.state != FFmpegState.STOPPED:
logger.warning(f"FFmpeg already running in state: {self.state}")
return False
self.state = FFmpegState.STARTING
try:
cmd = [
"ffmpeg",
"-hide_banner",
"-loglevel", "error",
"-i", "pipe:0",
"-f", "s16le",
"-acodec", "pcm_s16le",
"-ac", str(self.channels),
"-ar", str(self.sample_rate),
"pipe:1"
]
self.process = await asyncio.create_subprocess_exec(
*cmd,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
self._stderr_task = asyncio.create_task(self._drain_stderr())
async with self._state_lock:
self.state = FFmpegState.RUNNING
logger.info("FFmpeg started.")
return True
except FileNotFoundError:
logger.error(ERROR_INSTALL_INSTRUCTIONS)
async with self._state_lock:
self.state = FFmpegState.FAILED
if self.on_error_callback:
await self.on_error_callback("ffmpeg_not_found")
return False
except Exception as e:
logger.error(f"Error starting FFmpeg: {e}")
async with self._state_lock:
self.state = FFmpegState.FAILED
if self.on_error_callback:
await self.on_error_callback("start_failed")
return False
async def stop(self):
async with self._state_lock:
if self.state == FFmpegState.STOPPED:
return
self.state = FFmpegState.STOPPED
if self.process:
if self.process.stdin and not self.process.stdin.is_closing():
self.process.stdin.close()
await self.process.stdin.wait_closed()
await self.process.wait()
self.process = None
if self._stderr_task:
self._stderr_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._stderr_task
logger.info("FFmpeg stopped.")
async def write_data(self, data: bytes) -> bool:
async with self._state_lock:
if self.state != FFmpegState.RUNNING:
logger.warning(f"Cannot write, FFmpeg state: {self.state}")
return False
try:
self.process.stdin.write(data)
await self.process.stdin.drain()
return True
except Exception as e:
logger.error(f"Error writing to FFmpeg: {e}")
if self.on_error_callback:
await self.on_error_callback("write_error")
return False
async def read_data(self, size: int) -> Optional[bytes]:
async with self._state_lock:
if self.state != FFmpegState.RUNNING:
logger.warning(f"Cannot read, FFmpeg state: {self.state}")
return None
try:
data = await asyncio.wait_for(
self.process.stdout.read(size),
timeout=20.0
)
return data
except asyncio.TimeoutError:
logger.warning("FFmpeg read timeout.")
return None
except Exception as e:
logger.error(f"Error reading from FFmpeg: {e}")
if self.on_error_callback:
await self.on_error_callback("read_error")
return None
async def get_state(self) -> FFmpegState:
async with self._state_lock:
return self.state
async def restart(self) -> bool:
async with self._state_lock:
if self.state == FFmpegState.RESTARTING:
logger.warning("Restart already in progress.")
return False
self.state = FFmpegState.RESTARTING
logger.info("Restarting FFmpeg...")
try:
await self.stop()
await asyncio.sleep(1) # short delay before restarting
return await self.start()
except Exception as e:
logger.error(f"Error during FFmpeg restart: {e}")
async with self._state_lock:
self.state = FFmpegState.FAILED
if self.on_error_callback:
await self.on_error_callback("restart_failed")
return False
async def _drain_stderr(self):
try:
while True:
if not self.process or not self.process.stderr:
break
line = await self.process.stderr.readline()
if not line:
break
logger.debug(f"FFmpeg stderr: {line.decode(errors='ignore').strip()}")
except asyncio.CancelledError:
logger.info("FFmpeg stderr drain task cancelled.")
except Exception as e:
logger.error(f"Error draining FFmpeg stderr: {e}")
================================================
FILE: whisperlivekit/local_agreement/__init__.py
================================================
================================================
FILE: whisperlivekit/local_agreement/backends.py
================================================
import io
import logging
import math
import sys
from typing import List
import numpy as np
import soundfile as sf
from whisperlivekit.model_paths import detect_model_format, resolve_model_path
from whisperlivekit.timed_objects import ASRToken
from whisperlivekit.whisper.transcribe import transcribe as whisper_transcribe
logger = logging.getLogger(__name__)
class ASRBase:
sep = " " # join transcribe words with this character (" " for whisper_timestamped,
# "" for faster-whisper because it emits the spaces when needed)
def __init__(self, lan, model_size=None, cache_dir=None, model_dir=None, lora_path=None, logfile=sys.stderr):
self.logfile = logfile
self.transcribe_kargs = {}
self.lora_path = lora_path
if lan == "auto":
self.original_language = None
else:
self.original_language = lan
self.model = self.load_model(model_size, cache_dir, model_dir)
def load_model(self, model_size, cache_dir, model_dir):
raise NotImplementedError("must be implemented in the child class")
def transcribe(self, audio, init_prompt=""):
raise NotImplementedError("must be implemented in the child class")
def use_vad(self):
raise NotImplementedError("must be implemented in the child class")
class WhisperASR(ASRBase):
"""Uses WhisperLiveKit's built-in Whisper implementation."""
sep = " "
def load_model(self, model_size=None, cache_dir=None, model_dir=None):
from whisperlivekit.whisper import load_model as load_whisper_model
if model_dir is not None:
resolved_path = resolve_model_path(model_dir)
if resolved_path.is_dir():
model_info = detect_model_format(resolved_path)
if not model_info.has_pytorch:
raise FileNotFoundError(
f"No supported PyTorch checkpoint found under {resolved_path}"
)
logger.debug(f"Loading Whisper model from custom path {resolved_path}")
return load_whisper_model(str(resolved_path), lora_path=self.lora_path)
if model_size is None:
raise ValueError("Either model_size or model_dir must be set for WhisperASR")
return load_whisper_model(model_size, download_root=cache_dir, lora_path=self.lora_path)
def transcribe(self, audio, init_prompt=""):
options = dict(self.transcribe_kargs)
options.pop("vad", None)
options.pop("vad_filter", None)
language = self.original_language if self.original_language else None
result = whisper_transcribe(
self.model,
audio,
language=language,
initial_prompt=init_prompt,
condition_on_previous_text=True,
word_timestamps=True,
**options,
)
return result
def ts_words(self, r) -> List[ASRToken]:
"""
Converts the Whisper result to a list of ASRToken objects.
"""
tokens = []
for segment in r["segments"]:
for word in segment["words"]:
token = ASRToken(
word["start"],
word["end"],
word["word"],
probability=word.get("probability"),
)
tokens.append(token)
return tokens
def segments_end_ts(self, res) -> List[float]:
return [segment["end"] for segment in res["segments"]]
def use_vad(self):
logger.warning("VAD is not currently supported for WhisperASR backend and will be ignored.")
class FasterWhisperASR(ASRBase):
"""Uses faster-whisper as the backend."""
sep = ""
def load_model(self, model_size=None, cache_dir=None, model_dir=None):
from faster_whisper import WhisperModel
if model_dir is not None:
resolved_path = resolve_model_path(model_dir)
logger.debug(f"Loading faster-whisper model from {resolved_path}. "
f"model_size and cache_dir parameters are not used.")
model_size_or_path = str(resolved_path)
elif model_size is not None:
model_size_or_path = model_size
else:
raise ValueError("Either model_size or model_dir must be set")
device = "auto" # Allow CTranslate2 to decide available device
compute_type = "auto" # Allow CTranslate2 to decide faster compute type
model = WhisperModel(
model_size_or_path,
device=device,
compute_type=compute_type,
download_root=cache_dir,
)
return model
def transcribe(self, audio: np.ndarray, init_prompt: str = "") -> list:
segments, info = self.model.transcribe(
audio,
language=self.original_language,
initial_prompt=init_prompt,
beam_size=5,
word_timestamps=True,
condition_on_previous_text=True,
**self.transcribe_kargs,
)
return list(segments)
def ts_words(self, segments) -> List[ASRToken]:
tokens = []
for segment in segments:
if segment.no_speech_prob > 0.9:
continue
for word in segment.words:
token = ASRToken(word.start, word.end, word.word, probability=word.probability)
tokens.append(token)
return tokens
def segments_end_ts(self, segments) -> List[float]:
return [segment.end for segment in segments]
def use_vad(self):
self.transcribe_kargs["vad_filter"] = True
class MLXWhisper(ASRBase):
"""
Uses MLX Whisper optimized for Apple Silicon.
"""
sep = ""
def load_model(self, model_size=None, cache_dir=None, model_dir=None):
import mlx.core as mx
from mlx_whisper.transcribe import ModelHolder, transcribe
if model_dir is not None:
resolved_path = resolve_model_path(model_dir)
logger.debug(f"Loading MLX Whisper model from {resolved_path}. model_size parameter is not used.")
model_size_or_path = str(resolved_path)
elif model_size is not None:
model_size_or_path = self.translate_model_name(model_size)
logger.debug(f"Loading whisper model {model_size}. You use mlx whisper, so {model_size_or_path} will be used.")
else:
raise ValueError("Either model_size or model_dir must be set")
self.model_size_or_path = model_size_or_path
dtype = mx.float16
ModelHolder.get_model(model_size_or_path, dtype)
return transcribe
def translate_model_name(self, model_name):
from whisperlivekit.model_mapping import MLX_MODEL_MAPPING
mlx_model_path = MLX_MODEL_MAPPING.get(model_name)
if mlx_model_path:
return mlx_model_path
else:
raise ValueError(f"Model name '{model_name}' is not recognized or not supported.")
def transcribe(self, audio, init_prompt=""):
if self.transcribe_kargs:
logger.warning("Transcribe kwargs (vad, task) are not compatible with MLX Whisper and will be ignored.")
segments = self.model(
audio,
language=self.original_language,
initial_prompt=init_prompt,
word_timestamps=True,
condition_on_previous_text=True,
path_or_hf_repo=self.model_size_or_path,
)
return segments.get("segments", [])
def ts_words(self, segments) -> List[ASRToken]:
tokens = []
for segment in segments:
if segment.get("no_speech_prob", 0) > 0.9:
continue
for word in segment.get("words", []):
token = ASRToken(word["start"], word["end"], word["word"])
tokens.append(token)
return tokens
def segments_end_ts(self, res) -> List[float]:
return [s["end"] for s in res]
def use_vad(self):
self.transcribe_kargs["vad_filter"] = True
class OpenaiApiASR(ASRBase):
"""Uses OpenAI's Whisper API for transcription."""
def __init__(self, lan=None, temperature=0, logfile=sys.stderr):
self.logfile = logfile
self.modelname = "whisper-1"
self.original_language = None if lan == "auto" else lan
self.response_format = "verbose_json"
self.temperature = temperature
self.load_model()
self.use_vad_opt = False
self.direct_english_translation = False
self.task = "transcribe"
def load_model(self, *args, **kwargs):
from openai import OpenAI
self.client = OpenAI()
self.transcribed_seconds = 0
def ts_words(self, segments) -> List[ASRToken]:
"""
Converts OpenAI API response words into ASRToken objects while
optionally skipping words that fall into no-speech segments.
"""
no_speech_segments = []
if self.use_vad_opt:
for segment in segments.segments:
if segment.no_speech_prob > 0.8:
no_speech_segments.append((segment.start, segment.end))
tokens = []
for word in segments.words:
start = word.start
end = word.end
if any(s[0] <= start <= s[1] for s in no_speech_segments):
continue
tokens.append(ASRToken(start, end, word.word))
return tokens
def segments_end_ts(self, res) -> List[float]:
return [s.end for s in res.words]
def transcribe(self, audio_data, prompt=None, *args, **kwargs):
buffer = io.BytesIO()
buffer.name = "temp.wav"
sf.write(buffer, audio_data, samplerate=16000, format="WAV", subtype="PCM_16")
buffer.seek(0)
self.transcribed_seconds += math.ceil(len(audio_data) / 16000)
params = {
"model": self.modelname,
"file": buffer,
"response_format": self.response_format,
"temperature": self.temperature,
"timestamp_granularities": ["word", "segment"],
}
if not self.direct_english_translation and self.original_language:
params["language"] = self.original_language
if prompt:
params["prompt"] = prompt
task = self.transcribe_kargs.get("task", self.task)
proc = self.client.audio.translations if task == "translate" else self.client.audio.transcriptions
transcript = proc.create(**params)
logger.debug(f"OpenAI API processed accumulated {self.transcribed_seconds} seconds")
return transcript
def use_vad(self):
self.use_vad_opt = True
================================================
FILE: whisperlivekit/local_agreement/online_asr.py
================================================
import logging
import sys
from typing import List, Optional, Tuple
import numpy as np
from whisperlivekit.timed_objects import ASRToken, Sentence, Transcript
logger = logging.getLogger(__name__)
class HypothesisBuffer:
"""
Buffer to store and process ASR hypothesis tokens.
It holds:
- committed_in_buffer: tokens that have been confirmed (committed)
- buffer: the last hypothesis that is not yet committed
- new: new tokens coming from the recognizer
"""
def __init__(self, logfile=sys.stderr, confidence_validation=False):
self.confidence_validation = confidence_validation
self.committed_in_buffer: List[ASRToken] = []
self.buffer: List[ASRToken] = []
self.new: List[ASRToken] = []
self.last_committed_time = 0.0
self.last_committed_word: Optional[str] = None
self.logfile = logfile
def insert(self, new_tokens: List[ASRToken], offset: float):
"""
Insert new tokens (after applying a time offset) and compare them with the
already committed tokens. Only tokens that extend the committed hypothesis
are added.
"""
# Apply the offset to each token.
new_tokens = [token.with_offset(offset) for token in new_tokens]
# Only keep tokens that are roughly “new”
self.new = [token for token in new_tokens if token.start > self.last_committed_time - 0.1]
if self.new:
first_token = self.new[0]
if abs(first_token.start - self.last_committed_time) < 1:
if self.committed_in_buffer:
committed_len = len(self.committed_in_buffer)
new_len = len(self.new)
# Try to match 1 to 5 consecutive tokens
max_ngram = min(min(committed_len, new_len), 5)
for i in range(1, max_ngram + 1):
committed_ngram = " ".join(token.text for token in self.committed_in_buffer[-i:])
new_ngram = " ".join(token.text for token in self.new[:i])
if committed_ngram == new_ngram:
removed = []
for _ in range(i):
removed_token = self.new.pop(0)
removed.append(repr(removed_token))
logger.debug(f"Removing last {i} words: {' '.join(removed)}")
break
def flush(self) -> List[ASRToken]:
"""
Returns the committed chunk, defined as the longest common prefix
between the previous hypothesis and the new tokens.
"""
committed: List[ASRToken] = []
while self.new:
current_new = self.new[0]
if self.confidence_validation and current_new.probability and current_new.probability > 0.95:
committed.append(current_new)
self.last_committed_word = current_new.text
self.last_committed_time = current_new.end
self.new.pop(0)
self.buffer.pop(0) if self.buffer else None
elif not self.buffer:
break
elif current_new.text == self.buffer[0].text:
committed.append(current_new)
self.last_committed_word = current_new.text
self.last_committed_time = current_new.end
self.buffer.pop(0)
self.new.pop(0)
else:
break
self.buffer = self.new
self.new = []
self.committed_in_buffer.extend(committed)
return committed
def pop_committed(self, time: float):
"""
Remove tokens (from the beginning) that have ended before `time`.
"""
while self.committed_in_buffer and self.committed_in_buffer[0].end <= time:
self.committed_in_buffer.pop(0)
class OnlineASRProcessor:
"""
Processes incoming audio in a streaming fashion, calling the ASR system
periodically, and uses a hypothesis buffer to commit and trim recognized text.
The processor supports two types of buffer trimming:
- "sentence": trims at sentence boundaries (using a sentence tokenizer)
- "segment": trims at fixed segment durations.
"""
SAMPLING_RATE = 16000
def __init__(
self,
asr,
logfile=sys.stderr,
):
"""
asr: An ASR system object (for example, a WhisperASR instance) that
provides a `transcribe` method, a `ts_words` method (to extract tokens),
a `segments_end_ts` method, and a separator attribute `sep`.
tokenize_method: A function that receives text and returns a list of sentence strings.
buffer_trimming: A tuple (option, seconds), where option is either "sentence" or "segment".
"""
self.asr = asr
self.tokenize = asr.tokenizer
self.logfile = logfile
self.confidence_validation = asr.confidence_validation
self.global_time_offset = 0.0
self.init()
self.buffer_trimming_way = asr.buffer_trimming
self.buffer_trimming_sec = asr.buffer_trimming_sec
if self.buffer_trimming_way not in ["sentence", "segment"]:
raise ValueError("buffer_trimming must be either 'sentence' or 'segment'")
if self.buffer_trimming_sec <= 0:
raise ValueError("buffer_trimming_sec must be positive")
elif self.buffer_trimming_sec > 30:
logger.warning(
f"buffer_trimming_sec is set to {self.buffer_trimming_sec}, which is very long. It may cause OOM."
)
def new_speaker(self, change_speaker):
"""Handle speaker change event."""
self.process_iter()
self.init(offset=change_speaker.start)
def init(self, offset: Optional[float] = None):
"""Initialize or reset the processing buffers."""
self.audio_buffer = np.array([], dtype=np.float32)
self.transcript_buffer = HypothesisBuffer(logfile=self.logfile, confidence_validation=self.confidence_validation)
self.buffer_time_offset = offset if offset is not None else 0.0
self.transcript_buffer.last_committed_time = self.buffer_time_offset
self.committed: List[ASRToken] = []
self.time_of_last_asr_output = 0.0
def get_audio_buffer_end_time(self) -> float:
"""Returns the absolute end time of the current audio_buffer."""
return self.buffer_time_offset + (len(self.audio_buffer) / self.SAMPLING_RATE)
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time: Optional[float] = None):
"""Append an audio chunk (a numpy array) to the current audio buffer."""
self.audio_buffer = np.append(self.audio_buffer, audio)
def start_silence(self):
if self.audio_buffer.size == 0:
return [], self.get_audio_buffer_end_time()
return self.process_iter()
def end_silence(self, silence_duration: Optional[float], offset: float):
if not silence_duration or silence_duration <= 0:
return
long_silence = silence_duration >= 5
if not long_silence:
gap_samples = int(self.SAMPLING_RATE * silence_duration)
if gap_samples > 0:
gap_silence = np.zeros(gap_samples, dtype=np.float32)
self.insert_audio_chunk(gap_silence)
else:
self.init(offset=silence_duration + offset)
self.global_time_offset += silence_duration
def insert_silence(self, silence_duration, offset):
"""
Backwards compatibility shim for legacy callers that still use insert_silence.
"""
self.end_silence(silence_duration, offset)
def prompt(self) -> Tuple[str, str]:
"""
Returns a tuple: (prompt, context), where:
- prompt is a 200-character suffix of committed text that falls
outside the current audio buffer.
- context is the committed text within the current audio buffer.
"""
k = len(self.committed)
while k > 0 and self.committed[k - 1].end > self.buffer_time_offset:
k -= 1
prompt_tokens = self.committed[:k]
prompt_words = [token.text for token in prompt_tokens]
prompt_list = []
length_count = 0
# Use the last words until reaching 200 characters.
while prompt_words and length_count < 200:
word = prompt_words.pop(-1)
length_count += len(word) + 1
prompt_list.append(word)
non_prompt_tokens = self.committed[k:]
context_text = self.asr.sep.join(token.text for token in non_prompt_tokens)
return self.asr.sep.join(prompt_list[::-1]), context_text
def get_buffer(self):
"""
Get the unvalidated buffer in string format.
"""
return self.concatenate_tokens(self.transcript_buffer.buffer)
def process_iter(self) -> Tuple[List[ASRToken], float]:
"""
Processes the current audio buffer.
Returns a tuple: (list of committed ASRToken objects, float representing the audio processed up to time).
"""
current_audio_processed_upto = self.get_audio_buffer_end_time()
prompt_text, _ = self.prompt()
logger.debug(
f"Transcribing {len(self.audio_buffer)/self.SAMPLING_RATE:.2f} seconds from {self.buffer_time_offset:.2f}"
)
res = self.asr.transcribe(self.audio_buffer, init_prompt=prompt_text)
tokens = self.asr.ts_words(res)
self.transcript_buffer.insert(tokens, self.buffer_time_offset)
committed_tokens = self.transcript_buffer.flush()
self.committed.extend(committed_tokens)
if committed_tokens:
self.time_of_last_asr_output = self.committed[-1].end
completed = self.concatenate_tokens(committed_tokens)
logger.debug(f">>>> COMPLETE NOW: {completed.text}")
incomp = self.concatenate_tokens(self.transcript_buffer.buffer)
logger.debug(f"INCOMPLETE: {incomp.text}")
buffer_duration = len(self.audio_buffer) / self.SAMPLING_RATE
if not committed_tokens and buffer_duration > self.buffer_trimming_sec:
time_since_last_output = self.get_audio_buffer_end_time() - self.time_of_last_asr_output
if time_since_last_output > self.buffer_trimming_sec:
logger.warning(
f"No ASR output for {time_since_last_output:.2f}s. "
f"Resetting buffer to prevent freezing."
)
self.init(offset=self.get_audio_buffer_end_time())
return [], current_audio_processed_upto
if committed_tokens and self.buffer_trimming_way == "sentence":
if len(self.audio_buffer) / self.SAMPLING_RATE > self.buffer_trimming_sec:
self.chunk_completed_sentence()
s = self.buffer_trimming_sec if self.buffer_trimming_way == "segment" else 30
if len(self.audio_buffer) / self.SAMPLING_RATE > s:
self.chunk_completed_segment(res)
logger.debug("Chunking segment")
logger.debug(
f"Length of audio buffer now: {len(self.audio_buffer)/self.SAMPLING_RATE:.2f} seconds"
)
return committed_tokens, current_audio_processed_upto
def chunk_completed_sentence(self):
"""
If the committed tokens form at least two sentences, chunk the audio
buffer at the end time of the penultimate sentence.
Also ensures chunking happens if audio buffer exceeds a time limit.
"""
buffer_duration = len(self.audio_buffer) / self.SAMPLING_RATE
if not self.committed:
if buffer_duration > self.buffer_trimming_sec:
chunk_time = self.buffer_time_offset + (buffer_duration / 2)
logger.debug(f"--- No speech detected, forced chunking at {chunk_time:.2f}")
self.chunk_at(chunk_time)
return
logger.debug("COMPLETED SENTENCE: " + " ".join(token.text for token in self.committed))
sentences = self.words_to_sentences(self.committed)
for sentence in sentences:
logger.debug(f"\tSentence: {sentence.text}")
chunk_done = False
if len(sentences) >= 2:
while len(sentences) > 2:
sentences.pop(0)
chunk_time = sentences[-2].end
logger.debug(f"--- Sentence chunked at {chunk_time:.2f}")
self.chunk_at(chunk_time)
chunk_done = True
if not chunk_done and buffer_duration > self.buffer_trimming_sec:
last_committed_time = self.committed[-1].end
logger.debug(f"--- Not enough sentences, chunking at last committed time {last_committed_time:.2f}")
self.chunk_at(last_committed_time)
def chunk_completed_segment(self, res):
"""
Chunk the audio buffer based on segment-end timestamps reported by the ASR.
Also ensures chunking happens if audio buffer exceeds a time limit.
"""
buffer_duration = len(self.audio_buffer) / self.SAMPLING_RATE
if not self.committed:
if buffer_duration > self.buffer_trimming_sec:
chunk_time = self.buffer_time_offset + (buffer_duration / 2)
logger.debug(f"--- No speech detected, forced chunking at {chunk_time:.2f}")
self.chunk_at(chunk_time)
return
logger.debug("Processing committed tokens for segmenting")
ends = self.asr.segments_end_ts(res)
last_committed_time = self.committed[-1].end
chunk_done = False
if len(ends) > 1:
logger.debug("Multiple segments available for chunking")
e = ends[-2] + self.buffer_time_offset
while len(ends) > 2 and e > last_committed_time:
ends.pop(-1)
e = ends[-2] + self.buffer_time_offset
if e <= last_committed_time:
logger.debug(f"--- Segment chunked at {e:.2f}")
self.chunk_at(e)
chunk_done = True
else:
logger.debug("--- Last segment not within committed area")
else:
logger.debug("--- Not enough segments to chunk")
if not chunk_done and buffer_duration > self.buffer_trimming_sec:
logger.debug(f"--- Buffer too large, chunking at last committed time {last_committed_time:.2f}")
self.chunk_at(last_committed_time)
logger.debug("Segment chunking complete")
def chunk_at(self, time: float):
"""
Trim both the hypothesis and audio buffer at the given time.
"""
logger.debug(f"Chunking at {time:.2f}s")
logger.debug(
f"Audio buffer length before chunking: {len(self.audio_buffer)/self.SAMPLING_RATE:.2f}s"
)
self.transcript_buffer.pop_committed(time)
cut_seconds = time - self.buffer_time_offset
self.audio_buffer = self.audio_buffer[int(cut_seconds * self.SAMPLING_RATE):]
self.buffer_time_offset = time
logger.debug(
f"Audio buffer length after chunking: {len(self.audio_buffer)/self.SAMPLING_RATE:.2f}s"
)
def words_to_sentences(self, tokens: List[ASRToken]) -> List[Sentence]:
"""
Converts a list of tokens to a list of Sentence objects using the provided
sentence tokenizer.
"""
if not tokens:
return []
full_text = " ".join(token.text for token in tokens)
if self.tokenize:
try:
sentence_texts = self.tokenize(full_text)
except Exception:
# Some tokenizers (e.g., MosesSentenceSplitter) expect a list input.
try:
sentence_texts = self.tokenize([full_text])
except Exception as e2:
raise ValueError("Tokenization failed") from e2
else:
sentence_texts = [full_text]
sentences: List[Sentence] = []
token_index = 0
for sent_text in sentence_texts:
sent_text = sent_text.strip()
if not sent_text:
continue
sent_tokens = []
accumulated = ""
# Accumulate tokens until roughly matching the length of the sentence text.
while token_index < len(tokens) and len(accumulated) < len(sent_text):
token = tokens[token_index]
accumulated = (accumulated + " " + token.text).strip() if accumulated else token.text
sent_tokens.append(token)
token_index += 1
if sent_tokens:
sentence = Sentence(
start=sent_tokens[0].start,
end=sent_tokens[-1].end,
text=" ".join(t.text for t in sent_tokens),
)
sentences.append(sentence)
return sentences
def finish(self) -> Tuple[List[ASRToken], float]:
"""
Flush the remaining transcript when processing ends.
Returns a tuple: (list of remaining ASRToken objects, float representing the final audio processed up to time).
"""
remaining_tokens = self.transcript_buffer.buffer
logger.debug(f"Final non-committed tokens: {remaining_tokens}")
final_processed_upto = self.buffer_time_offset + (len(self.audio_buffer) / self.SAMPLING_RATE)
self.buffer_time_offset = final_processed_upto
return remaining_tokens, final_processed_upto
def concatenate_tokens(
self,
tokens: List[ASRToken],
sep: Optional[str] = None,
offset: float = 0
) -> Transcript:
sep = sep if sep is not None else self.asr.sep
text = sep.join(token.text for token in tokens)
# probability = sum(token.probability for token in tokens if token.probability) / len(tokens) if tokens else None
if tokens:
start = offset + tokens[0].start
end = offset + tokens[-1].end
else:
start = None
end = None
return Transcript(start, end, text)
================================================
FILE: whisperlivekit/local_agreement/whisper_online.py
================================================
#!/usr/bin/env python3
import logging
import platform
import time
from whisperlivekit.backend_support import faster_backend_available, mlx_backend_available
from whisperlivekit.model_paths import detect_model_format, resolve_model_path
from whisperlivekit.warmup import warmup_asr
from .backends import FasterWhisperASR, MLXWhisper, OpenaiApiASR, WhisperASR
logger = logging.getLogger(__name__)
WHISPER_LANG_CODES = "af,am,ar,as,az,ba,be,bg,bn,bo,br,bs,ca,cs,cy,da,de,el,en,es,et,eu,fa,fi,fo,fr,gl,gu,ha,haw,he,hi,hr,ht,hu,hy,id,is,it,ja,jw,ka,kk,km,kn,ko,la,lb,ln,lo,lt,lv,mg,mi,mk,ml,mn,mr,ms,mt,my,ne,nl,nn,no,oc,pa,pl,ps,pt,ro,ru,sa,sd,si,sk,sl,sn,so,sq,sr,su,sv,sw,ta,te,tg,th,tk,tl,tr,tt,uk,ur,uz,vi,yi,yo,zh".split(
","
)
def create_tokenizer(lan):
"""returns an object that has split function that works like the one of MosesTokenizer"""
assert (
lan in WHISPER_LANG_CODES
), "language must be Whisper's supported lang code: " + " ".join(WHISPER_LANG_CODES)
if lan == "uk":
import tokenize_uk
class UkrainianTokenizer:
def split(self, text):
return tokenize_uk.tokenize_sents(text)
return UkrainianTokenizer()
# supported by fast-mosestokenizer
if (
lan
in "as bn ca cs de el en es et fi fr ga gu hi hu is it kn lt lv ml mni mr nl or pa pl pt ro ru sk sl sv ta te yue zh".split()
):
from mosestokenizer import MosesSentenceSplitter
return MosesSentenceSplitter(lan)
# the following languages are in Whisper, but not in wtpsplit:
if (
lan
in "as ba bo br bs fo haw hr ht jw lb ln lo mi nn oc sa sd sn so su sw tk tl tt".split()
):
logger.debug(
f"{lan} code is not supported by wtpsplit. Going to use None lang_code option."
)
lan = None
from wtpsplit import WtP
# downloads the model from huggingface on the first use
wtp = WtP("wtp-canine-s-12l-no-adapters")
class WtPtok:
def split(self, sent):
return wtp.split(sent, lang_code=lan)
return WtPtok()
def backend_factory(
backend,
lan,
model_size,
model_cache_dir,
model_dir,
model_path,
lora_path,
direct_english_translation,
buffer_trimming,
buffer_trimming_sec,
confidence_validation,
warmup_file=None,
min_chunk_size=None,
):
backend_choice = backend
custom_reference = model_path or model_dir
resolved_root = None
has_mlx_weights = False
has_fw_weights = False
has_pytorch = False
if custom_reference:
resolved_root = resolve_model_path(custom_reference)
if resolved_root.is_dir():
model_info = detect_model_format(resolved_root)
has_mlx_weights = model_info.compatible_whisper_mlx
has_fw_weights = model_info.compatible_faster_whisper
has_pytorch = model_info.has_pytorch
else:
# Single file provided
has_pytorch = True
if backend_choice == "openai-api":
logger.debug("Using OpenAI API.")
asr = OpenaiApiASR(lan=lan)
else:
backend_choice = _normalize_backend_choice(
backend_choice,
resolved_root,
has_mlx_weights,
has_fw_weights,
)
if backend_choice == "faster-whisper":
asr_cls = FasterWhisperASR
if resolved_root is not None and not resolved_root.is_dir():
raise ValueError("Faster-Whisper backend expects a directory with CTranslate2 weights.")
model_override = str(resolved_root) if resolved_root is not None else None
elif backend_choice == "mlx-whisper":
asr_cls = MLXWhisper
if resolved_root is not None and not resolved_root.is_dir():
raise ValueError("MLX Whisper backend expects a directory containing MLX weights.")
model_override = str(resolved_root) if resolved_root is not None else None
else:
asr_cls = WhisperASR
model_override = str(resolved_root) if resolved_root is not None else None
if custom_reference and not has_pytorch:
raise FileNotFoundError(
f"No PyTorch checkpoint found under {resolved_root or custom_reference}"
)
t = time.time()
logger.info(f"Loading Whisper {model_size} model for language {lan} using backend {backend_choice}...")
asr = asr_cls(
model_size=model_size,
lan=lan,
cache_dir=model_cache_dir,
model_dir=model_override,
lora_path=lora_path if backend_choice == "whisper" else None,
)
e = time.time()
logger.info(f"done. It took {round(e-t,2)} seconds.")
if direct_english_translation:
tgt_language = "en" # Whisper translates into English
asr.transcribe_kargs["task"] = "translate"
else:
tgt_language = lan # Whisper transcribes in this language
# Create the tokenizer
if buffer_trimming == "sentence":
tokenizer = create_tokenizer(tgt_language)
else:
tokenizer = None
warmup_asr(asr, warmup_file)
asr.confidence_validation = confidence_validation
asr.tokenizer = tokenizer
asr.buffer_trimming = buffer_trimming
asr.buffer_trimming_sec = buffer_trimming_sec
asr.backend_choice = backend_choice
return asr
def _normalize_backend_choice(
preferred_backend,
resolved_root,
has_mlx_weights,
has_fw_weights,
):
backend_choice = preferred_backend
if backend_choice == "auto":
if mlx_backend_available(warn_on_missing=True) and (resolved_root is None or has_mlx_weights):
return "mlx-whisper"
if faster_backend_available(warn_on_missing=True) and (resolved_root is None or has_fw_weights):
return "faster-whisper"
return "whisper"
if backend_choice == "mlx-whisper":
if not mlx_backend_available():
raise RuntimeError("mlx-whisper backend requested but mlx-whisper is not installed.")
if resolved_root is not None and not has_mlx_weights:
raise FileNotFoundError(
f"mlx-whisper backend requested but no MLX weights were found under {resolved_root}"
)
if platform.system() != "Darwin":
logger.warning("mlx-whisper backend requested on a non-macOS system; this may fail.")
return backend_choice
if backend_choice == "faster-whisper":
if not faster_backend_available():
raise RuntimeError("faster-whisper backend requested but faster-whisper is not installed.")
if resolved_root is not None and not has_fw_weights:
raise FileNotFoundError(
f"faster-whisper backend requested but no Faster-Whisper weights were found under {resolved_root}"
)
return backend_choice
if backend_choice == "whisper":
return backend_choice
raise ValueError(f"Unknown backend '{preferred_backend}' for LocalAgreement.")
================================================
FILE: whisperlivekit/metrics.py
================================================
"""Lightweight ASR evaluation metrics — no external dependencies.
Provides WER (Word Error Rate) computation via word-level Levenshtein distance,
text normalization, and word-level timestamp accuracy metrics with greedy alignment.
"""
import re
import unicodedata
from typing import Dict, List
def normalize_text(text: str) -> str:
"""Normalize text for WER comparison: lowercase, strip punctuation, collapse whitespace."""
text = text.lower()
# Normalize unicode (e.g., accented chars to composed form)
text = unicodedata.normalize("NFC", text)
# Remove punctuation (keep letters, numbers, spaces, hyphens within words)
text = re.sub(r"[^\w\s\-']", " ", text)
# Collapse whitespace
text = re.sub(r"\s+", " ", text).strip()
return text
def compute_wer(reference: str, hypothesis: str) -> Dict:
"""Compute Word Error Rate using word-level Levenshtein edit distance.
Args:
reference: Ground truth transcription.
hypothesis: Predicted transcription.
Returns:
Dict with keys: wer, substitutions, insertions, deletions, ref_words, hyp_words.
WER can exceed 1.0 if there are more errors than reference words.
"""
ref_words = normalize_text(reference).split()
hyp_words = normalize_text(hypothesis).split()
n = len(ref_words)
m = len(hyp_words)
if n == 0:
return {
"wer": 0.0 if m == 0 else float(m),
"substitutions": 0,
"insertions": m,
"deletions": 0,
"ref_words": 0,
"hyp_words": m,
}
# DP table: dp[i][j] = (edit_distance, substitutions, insertions, deletions)
dp = [[(0, 0, 0, 0) for _ in range(m + 1)] for _ in range(n + 1)]
for i in range(1, n + 1):
dp[i][0] = (i, 0, 0, i)
for j in range(1, m + 1):
dp[0][j] = (j, 0, j, 0)
for i in range(1, n + 1):
for j in range(1, m + 1):
if ref_words[i - 1] == hyp_words[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
sub = dp[i - 1][j - 1]
ins = dp[i][j - 1]
dele = dp[i - 1][j]
sub_cost = (sub[0] + 1, sub[1] + 1, sub[2], sub[3])
ins_cost = (ins[0] + 1, ins[1], ins[2] + 1, ins[3])
del_cost = (dele[0] + 1, dele[1], dele[2], dele[3] + 1)
dp[i][j] = min(sub_cost, del_cost, ins_cost, key=lambda x: x[0])
dist, subs, ins, dels = dp[n][m]
return {
"wer": dist / n,
"substitutions": subs,
"insertions": ins,
"deletions": dels,
"ref_words": n,
"hyp_words": m,
}
def compute_timestamp_accuracy(
predicted: List[Dict],
reference: List[Dict],
) -> Dict:
"""Compute timestamp accuracy by aligning predicted words to reference words.
Uses greedy left-to-right alignment on normalized text. For each matched pair,
computes the start-time delta (predicted - reference).
Args:
predicted: List of dicts with keys: word, start, end.
reference: List of dicts with keys: word, start, end.
Returns:
Dict with keys: mae_start, max_delta_start, median_delta_start,
n_matched, n_ref, n_pred. Returns None values if no matches found.
"""
if not predicted or not reference:
return {
"mae_start": None,
"max_delta_start": None,
"median_delta_start": None,
"n_matched": 0,
"n_ref": len(reference),
"n_pred": len(predicted),
}
# Normalize words for matching
pred_norm = [normalize_text(p["word"]) for p in predicted]
ref_norm = [normalize_text(r["word"]) for r in reference]
# Greedy left-to-right alignment
deltas_start = []
ref_idx = 0
for p_idx, p_word in enumerate(pred_norm):
if not p_word:
continue
# Scan forward in reference to find a match (allow small skips)
search_limit = min(ref_idx + 3, len(ref_norm))
for r_idx in range(ref_idx, search_limit):
if ref_norm[r_idx] == p_word:
delta = predicted[p_idx]["start"] - reference[r_idx]["start"]
deltas_start.append(delta)
ref_idx = r_idx + 1
break
if not deltas_start:
return {
"mae_start": None,
"max_delta_start": None,
"median_delta_start": None,
"n_matched": 0,
"n_ref": len(reference),
"n_pred": len(predicted),
}
abs_deltas = [abs(d) for d in deltas_start]
sorted_abs = sorted(abs_deltas)
n = len(sorted_abs)
if n % 2 == 1:
median = sorted_abs[n // 2]
else:
median = (sorted_abs[n // 2 - 1] + sorted_abs[n // 2]) / 2
return {
"mae_start": sum(abs_deltas) / len(abs_deltas),
"max_delta_start": max(abs_deltas),
"median_delta_start": median,
"n_matched": len(deltas_start),
"n_ref": len(reference),
"n_pred": len(predicted),
}
================================================
FILE: whisperlivekit/metrics_collector.py
================================================
"""Lightweight runtime metrics for AudioProcessor sessions.
Zero external dependencies. Negligible overhead when not queried —
just integer increments and list appends during normal operation.
"""
import logging
import time
from dataclasses import dataclass, field
from typing import Dict, List
logger = logging.getLogger(__name__)
@dataclass
class SessionMetrics:
"""Per-session metrics collected by AudioProcessor."""
session_start: float = 0.0
total_audio_duration_s: float = 0.0
total_processing_time_s: float = 0.0
# Chunk / call counters
n_chunks_received: int = 0
n_transcription_calls: int = 0
n_tokens_produced: int = 0
n_responses_sent: int = 0
# Per-call ASR latency (seconds)
transcription_durations: List[float] = field(default_factory=list)
# Silence
n_silence_events: int = 0
total_silence_duration_s: float = 0.0
# --- Computed properties ---
@property
def rtf(self) -> float:
"""Real-time factor: processing_time / audio_duration."""
if self.total_audio_duration_s <= 0:
return 0.0
return self.total_processing_time_s / self.total_audio_duration_s
@property
def avg_latency_ms(self) -> float:
"""Average per-call ASR latency in milliseconds."""
if not self.transcription_durations:
return 0.0
return (sum(self.transcription_durations) / len(self.transcription_durations)) * 1000
@property
def p95_latency_ms(self) -> float:
"""95th percentile per-call ASR latency in milliseconds."""
if not self.transcription_durations:
return 0.0
sorted_d = sorted(self.transcription_durations)
idx = int(len(sorted_d) * 0.95)
idx = min(idx, len(sorted_d) - 1)
return sorted_d[idx] * 1000
def to_dict(self) -> Dict:
"""Serialize to a plain dict (JSON-safe)."""
return {
"session_start": self.session_start,
"total_audio_duration_s": round(self.total_audio_duration_s, 3),
"total_processing_time_s": round(self.total_processing_time_s, 3),
"rtf": round(self.rtf, 3),
"n_chunks_received": self.n_chunks_received,
"n_transcription_calls": self.n_transcription_calls,
"n_tokens_produced": self.n_tokens_produced,
"n_responses_sent": self.n_responses_sent,
"avg_latency_ms": round(self.avg_latency_ms, 2),
"p95_latency_ms": round(self.p95_latency_ms, 2),
"n_silence_events": self.n_silence_events,
"total_silence_duration_s": round(self.total_silence_duration_s, 3),
}
def log_summary(self) -> None:
"""Emit a structured log line summarising the session."""
d = self.to_dict()
d["session_elapsed_s"] = round(time.time() - self.session_start, 3) if self.session_start else 0
logger.info(f"SESSION_METRICS {d}")
================================================
FILE: whisperlivekit/model_mapping.py
================================================
"""Shared MLX model name mapping used by both SimulStreaming and LocalAgreement backends."""
MLX_MODEL_MAPPING = {
"tiny.en": "mlx-community/whisper-tiny.en-mlx",
"tiny": "mlx-community/whisper-tiny-mlx",
"base.en": "mlx-community/whisper-base.en-mlx",
"base": "mlx-community/whisper-base-mlx",
"small.en": "mlx-community/whisper-small.en-mlx",
"small": "mlx-community/whisper-small-mlx",
"medium.en": "mlx-community/whisper-medium.en-mlx",
"medium": "mlx-community/whisper-medium-mlx",
"large-v1": "mlx-community/whisper-large-v1-mlx",
"large-v2": "mlx-community/whisper-large-v2-mlx",
"large-v3": "mlx-community/whisper-large-v3-mlx",
"large-v3-turbo": "mlx-community/whisper-large-v3-turbo",
"large": "mlx-community/whisper-large-mlx",
}
================================================
FILE: whisperlivekit/model_paths.py
================================================
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Tuple, Union
@dataclass
class ModelInfo:
"""Information about detected model format and files in a directory."""
path: Optional[Path] = None
pytorch_files: List[Path] = field(default_factory=list)
compatible_whisper_mlx: bool = False
compatible_faster_whisper: bool = False
@property
def has_pytorch(self) -> bool:
return len(self.pytorch_files) > 0
@property
def is_sharded(self) -> bool:
return len(self.pytorch_files) > 1
@property
def primary_pytorch_file(self) -> Optional[Path]:
"""Return the primary PyTorch file (or first shard for sharded models)."""
if not self.pytorch_files:
return None
return self.pytorch_files[0]
#regex pattern for sharded model files such as: model-00001-of-00002.safetensors or pytorch_model-00001-of-00002.bin
SHARDED_PATTERN = re.compile(r"^(.+)-(\d{5})-of-(\d{5})\.(safetensors|bin)$")
FASTER_WHISPER_MARKERS = {"model.bin", "encoder.bin", "decoder.bin"}
MLX_WHISPER_MARKERS = {"weights.npz", "weights.safetensors"}
CT2_INDICATOR_FILES = {"vocabulary.json", "vocabulary.txt", "shared_vocabulary.json"}
def _is_ct2_model_bin(directory: Path, filename: str) -> bool:
"""
Determine if model.bin/encoder.bin/decoder.bin is a CTranslate2 model.
CTranslate2 models have specific companion files that distinguish them
from PyTorch .bin files.
"""
n_indicators = 0
for indicator in CT2_INDICATOR_FILES: #test 1
if (directory / indicator).exists():
n_indicators += 1
if n_indicators == 0:
return False
config_path = directory / "config.json" #test 2
if config_path.exists():
try:
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
if config.get("model_type") == "whisper": #test 2
return False
except (json.JSONDecodeError, IOError):
pass
return True
def _collect_pytorch_files(directory: Path) -> List[Path]:
"""
Collect all PyTorch checkpoint files from a directory.
Handles:
- Single files: model.safetensors, pytorch_model.bin, *.pt
- Sharded files: model-00001-of-00002.safetensors, pytorch_model-00001-of-00002.bin
- Index-based sharded models (reads index file to find shards)
Returns files sorted appropriately (shards in order, or single file).
"""
for index_name in ["model.safetensors.index.json", "pytorch_model.bin.index.json"]:
index_path = directory / index_name
if index_path.exists():
try:
with open(index_path, "r", encoding="utf-8") as f:
index_data = json.load(f)
weight_map = index_data.get("weight_map", {})
if weight_map:
shard_names = sorted(set(weight_map.values()))
shards = [directory / name for name in shard_names if (directory / name).exists()]
if shards:
return shards
except (json.JSONDecodeError, IOError):
pass
sharded_groups = {}
single_files = {}
for file in directory.iterdir():
if not file.is_file():
continue
filename = file.name
suffix = file.suffix.lower()
if filename.startswith("adapter_"):
continue
match = SHARDED_PATTERN.match(filename)
if match:
base_name, shard_idx, total_shards, ext = match.groups()
key = (base_name, ext, int(total_shards))
if key not in sharded_groups:
sharded_groups[key] = []
sharded_groups[key].append((int(shard_idx), file))
continue
if filename == "model.safetensors":
single_files[0] = file # Highest priority
elif filename == "pytorch_model.bin":
single_files[1] = file
elif suffix == ".pt":
single_files[2] = file
elif suffix == ".safetensors" and not filename.startswith("adapter"):
single_files[3] = file
for (base_name, ext, total_shards), shards in sharded_groups.items():
if len(shards) == total_shards:
return [path for _, path in sorted(shards)]
for priority in sorted(single_files.keys()):
return [single_files[priority]]
return []
def detect_model_format(model_path: Union[str, Path]) -> ModelInfo:
"""
Detect the model format in a given path.
This function analyzes a file or directory to determine:
- What PyTorch checkpoint files are available (including sharded models)
- Whether the directory contains MLX Whisper weights
- Whether the directory contains Faster-Whisper (CTranslate2) weights
Args:
model_path: Path to a model file or directory
Returns:
ModelInfo with detected format information
"""
path = Path(model_path)
info = ModelInfo(path=path)
if path.is_file():
suffix = path.suffix.lower()
if suffix in {".pt", ".safetensors", ".bin"}:
info.pytorch_files = [path]
return info
if not path.is_dir():
return info
for file in path.iterdir():
if not file.is_file():
continue
filename = file.name.lower()
if filename in MLX_WHISPER_MARKERS:
info.compatible_whisper_mlx = True
if filename in FASTER_WHISPER_MARKERS:
if _is_ct2_model_bin(path, filename):
info.compatible_faster_whisper = True
info.pytorch_files = _collect_pytorch_files(path)
return info
def model_path_and_type(model_path: Union[str, Path]) -> Tuple[Optional[Path], bool, bool]:
"""
Inspect the provided path and determine which model formats are available.
This is a compatibility wrapper around detect_model_format().
Returns:
pytorch_path: Path to a PyTorch checkpoint (first shard for sharded models, or None).
compatible_whisper_mlx: True if MLX weights exist in this folder.
compatible_faster_whisper: True if Faster-Whisper (CTranslate2) weights exist.
"""
info = detect_model_format(model_path)
return info.primary_pytorch_file, info.compatible_whisper_mlx, info.compatible_faster_whisper
def resolve_model_path(model_path: Union[str, Path]) -> Path:
"""
Return a local path for the provided model reference.
If the path does not exist locally, it is treated as a Hugging Face repo id
and downloaded via snapshot_download.
"""
path = Path(model_path).expanduser()
if path.exists():
return path
try:
from huggingface_hub import snapshot_download
except ImportError as exc:
raise FileNotFoundError(
f"Model path '{model_path}' does not exist locally and huggingface_hub "
"is not installed to download it."
) from exc
downloaded_path = Path(snapshot_download(repo_id=str(model_path)))
return downloaded_path
================================================
FILE: whisperlivekit/parse_args.py
================================================
from argparse import ArgumentParser
def parse_args():
parser = ArgumentParser(description="Whisper FastAPI Online Server")
parser.add_argument(
"--host",
type=str,
default="localhost",
help="The host address to bind the server to.",
)
parser.add_argument(
"--port", type=int, default=8000, help="The port number to bind the server to."
)
parser.add_argument(
"--warmup-file",
type=str,
default=None,
dest="warmup_file",
help="""
The path to a speech audio wav file to warm up Whisper so that the very first chunk processing is fast.
If not set, uses https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav.
If empty, no warmup is performed.
""",
)
parser.add_argument(
"--confidence-validation",
action="store_true",
help="Accelerates validation of tokens using confidence scores. Transcription will be faster but punctuation might be less accurate.",
)
parser.add_argument(
"--diarization",
action="store_true",
default=False,
help="Enable speaker diarization.",
)
parser.add_argument(
"--punctuation-split",
action="store_true",
default=False,
help="Use punctuation marks from transcription to improve speaker boundary detection. Requires both transcription and diarization to be enabled.",
)
parser.add_argument(
"--segmentation-model",
type=str,
default="pyannote/segmentation-3.0",
help="Hugging Face model ID for pyannote.audio segmentation model.",
)
parser.add_argument(
"--embedding-model",
type=str,
default="pyannote/embedding",
help="Hugging Face model ID for pyannote.audio embedding model.",
)
parser.add_argument(
"--diarization-backend",
type=str,
default="sortformer",
choices=["sortformer", "diart"],
help="The diarization backend to use.",
)
parser.add_argument(
"--no-transcription",
action="store_true",
help="Disable transcription to only see live diarization results.",
)
parser.add_argument(
"--disable-punctuation-split",
action="store_true",
help="Disable the split parameter.",
)
parser.add_argument(
"--min-chunk-size",
type=float,
default=0.1,
help="Minimum audio chunk size in seconds. It waits up to this time to do processing. If the processing takes shorter time, it waits, otherwise it processes the whole segment that was received by this time.",
)
parser.add_argument(
"--model",
type=str,
default="base",
dest='model_size',
help="Name size of the Whisper model to use (default: tiny). Suggested values: tiny.en,tiny,base.en,base,small.en,small,medium.en,medium,large-v1,large-v2,large-v3,large,large-v3-turbo. The model is automatically downloaded from the model hub if not present in model cache dir.",
)
parser.add_argument(
"--model_cache_dir",
type=str,
default=None,
help="Overriding the default model cache dir where models downloaded from the hub are saved",
)
parser.add_argument(
"--model_dir",
type=str,
default=None,
help="Dir where Whisper model.bin and other files are saved. This option overrides --model and --model_cache_dir parameter.",
)
parser.add_argument(
"--lora-path",
type=str,
default=None,
dest="lora_path",
help="Path or Hugging Face repo ID for LoRA adapter weights (e.g., QuentinFuxa/whisper-base-french-lora). Only works with native Whisper backend.",
)
parser.add_argument(
"--lan",
"--language",
type=str,
default="auto",
dest='lan',
help="Source language code, e.g. en,de,cs, or 'auto' for language detection.",
)
parser.add_argument(
"--direct-english-translation",
action="store_true",
default=False,
help="Use Whisper to directly translate to english.",
)
parser.add_argument(
"--target-language",
type=str,
default="",
dest="target_language",
help="Target language for translation. Not functional yet.",
)
parser.add_argument(
"--backend-policy",
type=str,
default="simulstreaming",
choices=["1", "2", "simulstreaming", "localagreement"],
help="Select the streaming policy: 1 or 'simulstreaming' for AlignAtt, 2 or 'localagreement' for LocalAgreement.",
)
parser.add_argument(
"--backend",
type=str,
default="auto",
choices=["auto", "mlx-whisper", "faster-whisper", "whisper", "openai-api", "voxtral", "voxtral-mlx", "qwen3", "qwen3-mlx", "qwen3-mlx-simul", "qwen3-simul", "vllm-realtime"],
help="Select the ASR backend implementation. Use 'qwen3-mlx-simul' for Qwen3-ASR SimulStreaming on Apple Silicon (MLX). Use 'qwen3-mlx' for Qwen3-ASR LocalAgreement on MLX. Use 'qwen3-simul' for Qwen3-ASR SimulStreaming (PyTorch). Use 'vllm-realtime' for vLLM Realtime WebSocket.",
)
parser.add_argument(
"--no-vac",
action="store_true",
default=False,
help="Disable VAC = voice activity controller.",
)
parser.add_argument(
"--vac-chunk-size", type=float, default=0.04, help="VAC sample size in seconds."
)
parser.add_argument(
"--no-vad",
action="store_true",
help="Disable VAD (voice activity detection).",
)
parser.add_argument(
"--buffer_trimming",
type=str,
default="segment",
choices=["sentence", "segment"],
help='Buffer trimming strategy -- trim completed sentences marked with punctuation mark and detected by sentence segmenter, or the completed segments returned by Whisper. Sentence segmenter must be installed for "sentence" option.',
)
parser.add_argument(
"--buffer_trimming_sec",
type=float,
default=15,
help="Buffer trimming length threshold in seconds. If buffer length is longer, trimming sentence/segment is triggered.",
)
parser.add_argument(
"-l",
"--log-level",
dest="log_level",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Set the log level",
default="DEBUG",
)
parser.add_argument("--ssl-certfile", type=str, help="Path to the SSL certificate file.", default=None)
parser.add_argument("--ssl-keyfile", type=str, help="Path to the SSL private key file.", default=None)
parser.add_argument("--forwarded-allow-ips", type=str, help="Allowed ips for reverse proxying.", default=None)
parser.add_argument(
"--pcm-input",
action="store_true",
default=False,
help="If set, raw PCM (s16le) data is expected as input and FFmpeg will be bypassed. Frontend will use AudioWorklet instead of MediaRecorder."
)
# vLLM Realtime backend arguments
parser.add_argument(
"--vllm-url",
type=str,
default="ws://localhost:8000/v1/realtime",
dest="vllm_url",
help="URL of the vLLM realtime WebSocket endpoint.",
)
parser.add_argument(
"--vllm-model",
type=str,
default="",
dest="vllm_model",
help="Model name to use with vLLM (e.g. Qwen/Qwen3-ASR-1.7B).",
)
# SimulStreaming-specific arguments
simulstreaming_group = parser.add_argument_group('SimulStreaming arguments (only used with --backend simulstreaming)')
simulstreaming_group.add_argument(
"--disable-fast-encoder",
action="store_true",
default=False,
dest="disable_fast_encoder",
help="Disable Faster Whisper or MLX Whisper backends for encoding (if installed). Slower but helpful when GPU memory is limited",
)
simulstreaming_group.add_argument(
"--custom-alignment-heads",
type=str,
default=None,
help="Use your own alignment heads, useful when `--model-dir` is used",
)
simulstreaming_group.add_argument(
"--frame-threshold",
type=int,
default=25,
dest="frame_threshold",
help="Threshold for the attention-guided decoding. The AlignAtt policy will decode only until this number of frames from the end of audio. In frames: one frame is 0.02 seconds for large-v3 model.",
)
simulstreaming_group.add_argument(
"--beams",
"-b",
type=int,
default=1,
help="Number of beams for beam search decoding. If 1, GreedyDecoder is used.",
)
simulstreaming_group.add_argument(
"--decoder",
type=str,
default=None,
dest="decoder_type",
choices=["beam", "greedy"],
help="Override automatic selection of beam or greedy decoder. If beams > 1 and greedy: invalid.",
)
simulstreaming_group.add_argument(
"--audio-max-len",
type=float,
default=30.0,
dest="audio_max_len",
help="Max length of the audio buffer, in seconds.",
)
simulstreaming_group.add_argument(
"--audio-min-len",
type=float,
default=0.0,
dest="audio_min_len",
help="Skip processing if the audio buffer is shorter than this length, in seconds. Useful when the --min-chunk-size is small.",
)
simulstreaming_group.add_argument(
"--cif-ckpt-path",
type=str,
default=None,
dest="cif_ckpt_path",
help="The file path to the Simul-Whisper's CIF model checkpoint that detects whether there is end of word at the end of the chunk. If not, the last decoded space-separated word is truncated because it is often wrong -- transcribing a word in the middle. The CIF model adapted for the Whisper model version should be used. Find the models in https://github.com/backspacetg/simul_whisper/tree/main/cif_models . Note that there is no model for large-v3.",
)
simulstreaming_group.add_argument(
"--never-fire",
action="store_true",
default=False,
dest="never_fire",
help="Override the CIF model. If True, the last word is NEVER truncated, no matter what the CIF model detects. If False: if CIF model path is set, the last word is SOMETIMES truncated, depending on the CIF detection. Otherwise, if the CIF model path is not set, the last word is ALWAYS trimmed.",
)
simulstreaming_group.add_argument(
"--init-prompt",
type=str,
default=None,
dest="init_prompt",
help="Init prompt for the model. It should be in the target language.",
)
simulstreaming_group.add_argument(
"--static-init-prompt",
type=str,
default=None,
dest="static_init_prompt",
help="Do not scroll over this text. It can contain terminology that should be relevant over all document.",
)
simulstreaming_group.add_argument(
"--max-context-tokens",
type=int,
default=None,
dest="max_context_tokens",
help="Max context tokens for the model. Default is 0.",
)
simulstreaming_group.add_argument(
"--model-path",
type=str,
default=None,
dest="model_path",
help="Direct path to the SimulStreaming Whisper .pt model file. Overrides --model for SimulStreaming backend.",
)
simulstreaming_group.add_argument(
"--nllb-backend",
type=str,
default="transformers",
help="transformers or ctranslate2",
)
simulstreaming_group.add_argument(
"--nllb-size",
type=str,
default="600M",
help="600M or 1.3B",
)
args = parser.parse_args()
args.transcription = not args.no_transcription
args.vad = not args.no_vad
args.vac = not args.no_vac
delattr(args, 'no_transcription')
delattr(args, 'no_vad')
delattr(args, 'no_vac')
from whisperlivekit.config import WhisperLiveKitConfig
return WhisperLiveKitConfig.from_namespace(args)
================================================
FILE: whisperlivekit/qwen3_asr.py
================================================
import logging
import re
import sys
from typing import List, Optional
import numpy as np
from whisperlivekit.local_agreement.backends import ASRBase
from whisperlivekit.timed_objects import ASRToken
logger = logging.getLogger(__name__)
def _patch_transformers_compat():
"""Patch transformers for qwen_asr 0.0.6 + transformers >= 5.3 compatibility."""
import torch
# 1. check_model_inputs was removed
try:
import transformers.utils.generic as _g
if not hasattr(_g, "check_model_inputs"):
def check_model_inputs(*args, **kwargs):
def decorator(fn):
return fn
return decorator
_g.check_model_inputs = check_model_inputs
except ImportError:
pass
# 2. 'default' rope type was removed from ROPE_INIT_FUNCTIONS
try:
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS
if "default" not in ROPE_INIT_FUNCTIONS:
def _compute_default_rope_parameters(config=None, device=None, seq_len=None, **kwargs):
head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
partial = getattr(config, "partial_rotary_factor", 1.0)
dim = int(head_dim * partial)
base = config.rope_theta
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).float().to(device) / dim))
return inv_freq, 1.0
ROPE_INIT_FUNCTIONS["default"] = _compute_default_rope_parameters
except ImportError:
pass
# 3. pad_token_id missing on thinker config
try:
from qwen_asr.core.transformers_backend.configuration_qwen3_asr import (
Qwen3ASRThinkerConfig,
)
if not hasattr(Qwen3ASRThinkerConfig, "pad_token_id"):
Qwen3ASRThinkerConfig.pad_token_id = None
except ImportError:
pass
# 4. fix_mistral_regex kwarg not accepted by newer transformers
try:
from transformers.models.auto import processing_auto
_orig_ap_from_pretrained = processing_auto.AutoProcessor.from_pretrained.__func__
@classmethod
def _patched_ap_from_pretrained(cls, *args, **kwargs):
kwargs.pop("fix_mistral_regex", None)
return _orig_ap_from_pretrained(cls, *args, **kwargs)
processing_auto.AutoProcessor.from_pretrained = _patched_ap_from_pretrained
except Exception:
pass
# 5. compute_default_rope_parameters missing on RotaryEmbedding
try:
from qwen_asr.core.transformers_backend.modeling_qwen3_asr import (
Qwen3ASRThinkerTextRotaryEmbedding,
)
if not hasattr(Qwen3ASRThinkerTextRotaryEmbedding, "compute_default_rope_parameters"):
@staticmethod
def _rope_params(config=None, device=None, seq_len=None, **kwargs):
head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
partial = getattr(config, "partial_rotary_factor", 1.0)
dim = int(head_dim * partial)
base = config.rope_theta
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).float().to(device) / dim))
return inv_freq, 1.0
Qwen3ASRThinkerTextRotaryEmbedding.compute_default_rope_parameters = _rope_params
except ImportError:
pass
_patch_transformers_compat()
# Whisper language codes → Qwen3 canonical language names
WHISPER_TO_QWEN3_LANGUAGE = {
"zh": "Chinese", "en": "English", "yue": "Cantonese",
"ar": "Arabic", "de": "German", "fr": "French", "es": "Spanish",
"pt": "Portuguese", "id": "Indonesian", "it": "Italian",
"ko": "Korean", "ru": "Russian", "th": "Thai", "vi": "Vietnamese",
"ja": "Japanese", "tr": "Turkish", "hi": "Hindi", "ms": "Malay",
"nl": "Dutch", "sv": "Swedish", "da": "Danish", "fi": "Finnish",
"pl": "Polish", "cs": "Czech", "fa": "Persian",
"el": "Greek", "hu": "Hungarian", "mk": "Macedonian", "ro": "Romanian",
}
# Reverse mapping: Qwen3 canonical names → Whisper language codes
QWEN3_TO_WHISPER_LANGUAGE = {v: k for k, v in WHISPER_TO_QWEN3_LANGUAGE.items()}
# Short convenience names → HuggingFace model IDs
QWEN3_MODEL_MAPPING = {
"qwen3-asr-1.7b": "Qwen/Qwen3-ASR-1.7B",
"qwen3-asr-0.6b": "Qwen/Qwen3-ASR-0.6B",
"qwen3-1.7b": "Qwen/Qwen3-ASR-1.7B",
"qwen3-0.6b": "Qwen/Qwen3-ASR-0.6B",
# Whisper-style size aliases (map to closest Qwen3 model)
"large": "Qwen/Qwen3-ASR-1.7B",
"large-v3": "Qwen/Qwen3-ASR-1.7B",
"medium": "Qwen/Qwen3-ASR-1.7B",
"base": "Qwen/Qwen3-ASR-0.6B",
"small": "Qwen/Qwen3-ASR-0.6B",
"tiny": "Qwen/Qwen3-ASR-0.6B",
}
_PUNCTUATION_ENDS = set(".!?。!?;;")
# Qwen3 raw output starts with "language " metadata before tag.
# When the tag is missing (silence/noise), this metadata leaks as transcription text.
_GARBAGE_RE = re.compile(r"^language\s+\S+$", re.IGNORECASE)
class Qwen3ASR(ASRBase):
"""Qwen3-ASR backend with ForcedAligner word-level timestamps."""
sep = "" # tokens include leading spaces, like faster-whisper
SAMPLING_RATE = 16000
def __init__(self, lan="auto", model_size=None, cache_dir=None,
model_dir=None, logfile=sys.stderr, **kwargs):
self.logfile = logfile
self.transcribe_kargs = {}
self.original_language = None if lan == "auto" else lan
self.model = self.load_model(model_size, cache_dir, model_dir)
def load_model(self, model_size=None, cache_dir=None, model_dir=None):
import torch
from qwen_asr import Qwen3ASRModel
if model_dir:
model_id = model_dir
elif model_size:
model_id = QWEN3_MODEL_MAPPING.get(model_size.lower(), model_size)
else:
model_id = "Qwen/Qwen3-ASR-1.7B"
if torch.cuda.is_available():
dtype, device = torch.bfloat16, "cuda:0"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
dtype, device = torch.float32, "mps"
else:
dtype, device = torch.float32, "cpu"
logger.info(f"Loading Qwen3-ASR: {model_id} ({dtype}, {device})")
model = Qwen3ASRModel.from_pretrained(
model_id,
forced_aligner="Qwen/Qwen3-ForcedAligner-0.6B",
forced_aligner_kwargs=dict(dtype=dtype, device_map=device),
dtype=dtype,
device_map=device,
)
logger.info("Qwen3-ASR loaded with ForcedAligner")
return model
def _qwen3_language(self) -> Optional[str]:
if self.original_language is None:
return None
return WHISPER_TO_QWEN3_LANGUAGE.get(self.original_language)
def transcribe(self, audio: np.ndarray, init_prompt: str = ""):
try:
results = self.model.transcribe(
audio=(audio, 16000),
language=self._qwen3_language(),
context=init_prompt or "",
return_time_stamps=True,
)
except Exception:
logger.warning("Qwen3 timestamp alignment failed, falling back to no timestamps", exc_info=True)
results = self.model.transcribe(
audio=(audio, 16000),
language=self._qwen3_language(),
context=init_prompt or "",
return_time_stamps=False,
)
result = results[0]
# Stash audio length for timestamp estimation fallback
result._audio_duration = len(audio) / 16000
logger.info(
"Qwen3 result: language=%r text=%r ts=%s",
result.language, result.text[:80] if result.text else "",
bool(result.time_stamps),
)
return result
@staticmethod
def _detected_language(result) -> Optional[str]:
"""Extract Whisper-style language code from Qwen3 result."""
lang = getattr(result, 'language', None)
if not lang or lang.lower() == "none":
return None
# merge_languages may return comma-separated; take the first
first = lang.split(",")[0].strip()
if not first or first.lower() == "none":
return None
return QWEN3_TO_WHISPER_LANGUAGE.get(first, first.lower())
def ts_words(self, result) -> List[ASRToken]:
# Filter garbage model output (e.g. "language None" for silence/noise)
text = (result.text or "").strip()
if not text or _GARBAGE_RE.match(text):
if text:
logger.info("Filtered garbage Qwen3 output: %r", text)
return []
detected = self._detected_language(result)
if result.time_stamps:
tokens = []
for i, item in enumerate(result.time_stamps):
# Prepend space to match faster-whisper convention (tokens carry
# their own whitespace so ''.join works in Segment.from_tokens)
text = item.text if i == 0 else " " + item.text
tokens.append(ASRToken(
start=item.start_time, end=item.end_time, text=text,
detected_language=detected,
))
return tokens
# Fallback: estimate timestamps from word count
if not result.text:
return []
words = result.text.split()
duration = getattr(result, '_audio_duration', 5.0)
step = duration / max(len(words), 1)
return [
ASRToken(
start=round(i * step, 3), end=round((i + 1) * step, 3),
text=w if i == 0 else " " + w,
detected_language=detected,
)
for i, w in enumerate(words)
]
def segments_end_ts(self, result) -> List[float]:
if not result.time_stamps:
duration = getattr(result, '_audio_duration', 5.0)
return [duration]
# Create segment boundaries at punctuation marks
ends = []
for item in result.time_stamps:
if item.text and item.text.rstrip()[-1:] in _PUNCTUATION_ENDS:
ends.append(item.end_time)
last_end = result.time_stamps[-1].end_time
if not ends or ends[-1] != last_end:
ends.append(last_end)
return ends
def use_vad(self):
return False
================================================
FILE: whisperlivekit/qwen3_mlx_asr.py
================================================
"""
MLX-accelerated Qwen3-ASR backend for WhisperLiveKit.
Provides ``Qwen3MLXASR`` (model holder) and ``Qwen3MLXOnlineProcessor``
(batch-based processor) that plug into WhisperLiveKit's audio processing
pipeline via ``insert_audio_chunk`` / ``process_iter`` / ``get_buffer`` etc.
Uses the ``mlx-qwen3-asr`` package for fast Qwen3 inference on Apple Silicon.
The batch ``session.transcribe()`` API is called on the full accumulated audio
buffer, and LocalAgreement-style diffing (HypothesisBuffer) commits stable
words across consecutive inferences.
"""
import logging
import sys
import time
from typing import List, Tuple
import numpy as np
from whisperlivekit.timed_objects import ASRToken, Transcript
logger = logging.getLogger(__name__)
# Whisper language codes -> Qwen3 canonical language names
# (duplicated from qwen3_asr.py to avoid importing torch at module level)
WHISPER_TO_QWEN3_LANGUAGE = {
"zh": "Chinese", "en": "English", "yue": "Cantonese",
"ar": "Arabic", "de": "German", "fr": "French", "es": "Spanish",
"pt": "Portuguese", "id": "Indonesian", "it": "Italian",
"ko": "Korean", "ru": "Russian", "th": "Thai", "vi": "Vietnamese",
"ja": "Japanese", "tr": "Turkish", "hi": "Hindi", "ms": "Malay",
"nl": "Dutch", "sv": "Swedish", "da": "Danish", "fi": "Finnish",
"pl": "Polish", "cs": "Czech", "fa": "Persian",
"el": "Greek", "hu": "Hungarian", "mk": "Macedonian", "ro": "Romanian",
}
# Model size aliases -> HuggingFace model IDs
QWEN3_MLX_MODEL_MAPPING = {
"base": "Qwen/Qwen3-ASR-0.6B",
"tiny": "Qwen/Qwen3-ASR-0.6B",
"small": "Qwen/Qwen3-ASR-0.6B",
"large": "Qwen/Qwen3-ASR-1.7B",
"medium": "Qwen/Qwen3-ASR-1.7B",
"large-v3": "Qwen/Qwen3-ASR-1.7B",
"qwen3-asr-1.7b": "Qwen/Qwen3-ASR-1.7B",
"qwen3-asr-0.6b": "Qwen/Qwen3-ASR-0.6B",
"qwen3-1.7b": "Qwen/Qwen3-ASR-1.7B",
"qwen3-0.6b": "Qwen/Qwen3-ASR-0.6B",
"1.7b": "Qwen/Qwen3-ASR-1.7B",
"0.6b": "Qwen/Qwen3-ASR-0.6B",
}
# ---------------------------------------------------------------------------
# Model holder
# ---------------------------------------------------------------------------
class Qwen3MLXASR:
"""Lightweight model holder -- loads the mlx-qwen3-asr model once and
keeps it alive for the lifetime of the server."""
sep = ""
SAMPLING_RATE = 16_000
def __init__(self, logfile=sys.stderr, **kwargs):
import mlx.core as mx
import mlx_qwen3_asr
self.logfile = logfile
self.transcribe_kargs = {}
lan = kwargs.get("lan", "auto")
self.original_language = None if lan == "auto" else lan
# Resolve model ID from size aliases or explicit path
model_path = kwargs.get("model_dir") or kwargs.get("model_path")
if not model_path:
model_size = kwargs.get("model_size", "")
if model_size and ("/" in model_size or model_size.startswith(".")):
model_path = model_size
else:
model_path = QWEN3_MLX_MODEL_MAPPING.get(
(model_size or "base").lower(), "Qwen/Qwen3-ASR-0.6B"
)
t0 = time.time()
logger.info("Loading Qwen3 MLX model '%s' ...", model_path)
self.session = mlx_qwen3_asr.Session(model_path, dtype=mx.float16)
logger.info("Qwen3 MLX model loaded in %.2fs", time.time() - t0)
self.backend_choice = "qwen3-mlx"
self.tokenizer = None
def transcribe(self, audio):
pass # all work happens in the online processor
# ---------------------------------------------------------------------------
# Online processor
# ---------------------------------------------------------------------------
class Qwen3MLXOnlineProcessor:
"""Batch-based processor that accumulates audio and periodically calls
``session.transcribe()`` on the full buffer.
Uses LocalAgreement-style diffing (HypothesisBuffer) to commit stable
words across consecutive inferences, exactly like the PyTorch Qwen3
backend with ``OnlineASRProcessor``.
Lifecycle (called by ``AudioProcessor.transcription_processor``):
insert_audio_chunk(pcm, time) -> process_iter() -> get_buffer()
... repeat ...
start_silence() / end_silence()
finish()
"""
SAMPLING_RATE = 16_000
def __init__(self, asr: Qwen3MLXASR, logfile=sys.stderr):
self.asr = asr
self.logfile = logfile
self.end = 0.0
self._session = asr.session
lan = asr.original_language
self._language = WHISPER_TO_QWEN3_LANGUAGE.get(lan, "English") if lan else None
# Audio accumulation
self.audio_buffer = np.array([], dtype=np.float32)
self._buffer_time_offset: float = 0.0 # absolute time of audio_buffer[0]
# Throttle: minimum new audio (in samples) before re-running inference
self._min_new_samples: int = int(1.0 * self.SAMPLING_RATE) # 1 second
self._samples_since_last_inference: int = 0
# Buffer trimming — keep buffer short for fast re-transcription.
# The model produces ~0.2x RTF, so 15s buffer = ~3s per call.
self._max_buffer_sec: float = 15.0
self._trim_sec: float = 10.0 # keep this many seconds after trimming
# HypothesisBuffer for LocalAgreement diffing
self._committed: List[ASRToken] = []
self._prev_tokens: List[ASRToken] = [] # previous hypothesis (buffer role)
self._last_committed_time: float = 0.0
# Global time tracking
self._global_time_offset: float = 0.0 # extra offset from silences
# -- audio ingestion --
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time: float):
self.end = audio_stream_end_time
self.audio_buffer = np.append(self.audio_buffer, audio)
self._samples_since_last_inference += len(audio)
# -- batch transcription --
def _transcribe_buffer(self) -> List[ASRToken]:
"""Run batch transcription on the full audio buffer and return tokens."""
if len(self.audio_buffer) < 400: # too short for meaningful transcription
return []
t0 = time.time()
try:
result = self._session.transcribe(
self.audio_buffer,
language=self._language,
return_timestamps=True,
)
except Exception as e:
logger.warning("[qwen3-mlx] transcribe error: %s", e, exc_info=True)
return []
dur = time.time() - t0
audio_dur = len(self.audio_buffer) / self.SAMPLING_RATE
logger.debug(
"[qwen3-mlx] transcribed %.1fs audio in %.2fs (%.2fx RTF)",
audio_dur, dur, dur / max(audio_dur, 0.01),
)
text = (result.text or "").strip()
if not text:
return []
# Build tokens from segments (word-level timestamps)
tokens: List[ASRToken] = []
if result.segments:
for i, seg in enumerate(result.segments):
word = seg["text"]
start = self._buffer_time_offset + seg["start"]
end = self._buffer_time_offset + seg["end"]
label = word if i == 0 else " " + word
tokens.append(ASRToken(start=start, end=end, text=label))
else:
# Fallback: estimate timestamps from word count
words = text.split()
step = audio_dur / max(len(words), 1)
for i, w in enumerate(words):
t_start = self._buffer_time_offset + i * step
t_end = self._buffer_time_offset + (i + 1) * step
label = w if i == 0 else " " + w
tokens.append(ASRToken(start=t_start, end=t_end, text=label))
return tokens
def _local_agreement(self, new_tokens: List[ASRToken]) -> List[ASRToken]:
"""LocalAgreement diffing: commit the longest common prefix between
the previous hypothesis (``self._prev_tokens``) and the new tokens.
Before comparing, strips tokens that correspond to already-committed
audio (i.e., tokens whose start time is before ``_last_committed_time``).
Also deduplicates boundary tokens (ngram matching) to avoid re-committing
the tail of the previous committed output.
Returns the newly committed tokens.
"""
# Step 1: Only keep tokens that are roughly "new" (after last committed time)
fresh_tokens = [
t for t in new_tokens
if t.start > self._last_committed_time - 0.1
]
# Step 2: Remove duplicates at the boundary with committed tokens
# (like HypothesisBuffer.insert's ngram dedup)
if fresh_tokens and self._committed:
max_ngram = min(len(self._committed), len(fresh_tokens), 5)
for n in range(1, max_ngram + 1):
committed_ngram = " ".join(
t.text.strip() for t in self._committed[-n:]
)
fresh_ngram = " ".join(
t.text.strip() for t in fresh_tokens[:n]
)
if committed_ngram == fresh_ngram:
fresh_tokens = fresh_tokens[n:]
break
# Step 3: LocalAgreement -- longest common prefix between prev and fresh
committed: List[ASRToken] = []
prev = self._prev_tokens
i = 0
j = 0
while i < len(fresh_tokens) and j < len(prev):
if fresh_tokens[i].text.strip() == prev[j].text.strip():
# Agreement: commit this token (use the new token's timestamps)
committed.append(fresh_tokens[i])
i += 1
j += 1
else:
break
# The remaining fresh tokens become the new "previous hypothesis"
self._prev_tokens = fresh_tokens[i:] if i < len(fresh_tokens) else []
return committed
def _trim_buffer_if_needed(self):
"""Trim the audio buffer if it exceeds max_buffer_sec.
Keeps the last ``_trim_sec`` seconds of audio. Also adjusts
committed token tracking and buffer_time_offset.
"""
buffer_dur = len(self.audio_buffer) / self.SAMPLING_RATE
if buffer_dur <= self._max_buffer_sec:
return
keep_sec = self._trim_sec
keep_samples = int(keep_sec * self.SAMPLING_RATE)
cut_samples = len(self.audio_buffer) - keep_samples
if cut_samples <= 0:
return
cut_sec = cut_samples / self.SAMPLING_RATE
self.audio_buffer = self.audio_buffer[cut_samples:]
self._buffer_time_offset += cut_sec
# Remove committed tokens that are before the new buffer start
self._committed = [
t for t in self._committed if t.end > self._buffer_time_offset
]
logger.debug(
"[qwen3-mlx] trimmed buffer: cut %.1fs, new offset %.1f, buffer %.1fs",
cut_sec, self._buffer_time_offset, len(self.audio_buffer) / self.SAMPLING_RATE,
)
# -- interface methods --
def process_iter(self, is_last=False) -> Tuple[List[ASRToken], float]:
"""Process the current audio buffer.
Throttles inference to at least 1s of new audio between calls.
Returns (newly_committed_tokens, audio_processed_upto_time).
"""
try:
# Throttle: skip if not enough new audio since last inference
if (not is_last
and self._samples_since_last_inference < self._min_new_samples):
return [], self.end
self._samples_since_last_inference = 0
# Trim buffer if too long
self._trim_buffer_if_needed()
# Run batch transcription
new_tokens = self._transcribe_buffer()
# LocalAgreement diffing
committed = self._local_agreement(new_tokens)
if committed:
self._committed.extend(committed)
self._last_committed_time = committed[-1].end
return committed, self.end
except Exception as e:
logger.warning("[qwen3-mlx] process_iter error: %s", e, exc_info=True)
return [], self.end
def get_buffer(self) -> Transcript:
"""Return the unconfirmed text (the tail of the last hypothesis
that was not committed by LocalAgreement)."""
if not self._prev_tokens:
return Transcript(start=None, end=None, text="")
text = "".join(t.text for t in self._prev_tokens)
start = self._prev_tokens[0].start
end = self._prev_tokens[-1].end
return Transcript(start=start, end=end, text=text)
def _flush_all(self) -> List[ASRToken]:
"""Force a final transcription and commit all remaining words."""
# Run one last transcription on the full buffer
self._samples_since_last_inference = self._min_new_samples # bypass throttle
new_tokens = self._transcribe_buffer()
# Commit everything: first the agreed prefix, then the remainder
committed = self._local_agreement(new_tokens)
# Also commit any remaining buffer tokens
remaining = self._prev_tokens
self._prev_tokens = []
all_new = committed + remaining
if all_new:
self._committed.extend(all_new)
self._last_committed_time = all_new[-1].end
return all_new
def _reset_for_new_utterance(self):
"""Reset buffers for a new utterance, preserving time continuity."""
new_offset = self._buffer_time_offset + len(self.audio_buffer) / self.SAMPLING_RATE
saved_end = self.end
self.audio_buffer = np.array([], dtype=np.float32)
self._buffer_time_offset = new_offset
self._samples_since_last_inference = 0
self._committed = []
self._prev_tokens = []
self.end = saved_end
def start_silence(self) -> Tuple[List[ASRToken], float]:
"""Flush pending words when silence starts.
Unlike other backends, does NOT reset the audio buffer — the model
produces better results re-transcribing the full accumulated audio.
Buffer trimming at 30s handles memory naturally.
"""
words = self._flush_all()
logger.info("[qwen3-mlx] start_silence: flushed %d words", len(words))
return words, self.end
def end_silence(self, silence_duration: float, offset: float):
self._global_time_offset += silence_duration
self.end += silence_duration
def new_speaker(self, change_speaker):
self.start_silence()
def warmup(self, audio, init_prompt=""):
pass
def finish(self) -> Tuple[List[ASRToken], float]:
words = self._flush_all()
logger.info("[qwen3-mlx] finish: flushed %d words", len(words))
return words, self.end
================================================
FILE: whisperlivekit/qwen3_mlx_simul.py
================================================
"""
Qwen3-ASR SimulStreaming (AlignAtt) on MLX for Apple Silicon.
Uses the ``mlx_qwen3_asr`` library for model loading, audio encoding, and
tokenization. Implements the AlignAtt border-distance policy by monkey-
patching ``TextAttention.__call__`` on alignment layers to capture Q (with
RoPE) during autoregressive decode steps, then computing ``Q @ K_audio^T``
from the KV cache to find the most-attended audio frame.
This is the MLX equivalent of ``qwen3_simul.py`` (PyTorch) which uses
``register_forward_hook`` for the same purpose.
"""
import json
import logging
import sys
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Tuple
import numpy as np
from whisperlivekit.timed_objects import ASRToken, Transcript
logger = logging.getLogger(__name__)
SAMPLE_RATE = 16_000
# Model size aliases (same as qwen3_mlx_asr.py)
QWEN3_MLX_MODEL_MAPPING = {
"base": "Qwen/Qwen3-ASR-0.6B",
"tiny": "Qwen/Qwen3-ASR-0.6B",
"small": "Qwen/Qwen3-ASR-0.6B",
"large": "Qwen/Qwen3-ASR-1.7B",
"medium": "Qwen/Qwen3-ASR-1.7B",
"large-v3": "Qwen/Qwen3-ASR-1.7B",
"qwen3-asr-1.7b": "Qwen/Qwen3-ASR-1.7B",
"qwen3-asr-0.6b": "Qwen/Qwen3-ASR-0.6B",
"qwen3-1.7b": "Qwen/Qwen3-ASR-1.7B",
"qwen3-0.6b": "Qwen/Qwen3-ASR-0.6B",
"1.7b": "Qwen/Qwen3-ASR-1.7B",
"0.6b": "Qwen/Qwen3-ASR-0.6B",
}
# Whisper language codes -> Qwen3 canonical language names
WHISPER_TO_QWEN3_LANGUAGE = {
"zh": "Chinese", "en": "English", "yue": "Cantonese",
"ar": "Arabic", "de": "German", "fr": "French", "es": "Spanish",
"pt": "Portuguese", "id": "Indonesian", "it": "Italian",
"ko": "Korean", "ru": "Russian", "th": "Thai", "vi": "Vietnamese",
"ja": "Japanese", "tr": "Turkish", "hi": "Hindi", "ms": "Malay",
"nl": "Dutch", "sv": "Swedish", "da": "Danish", "fi": "Finnish",
"pl": "Polish", "cs": "Czech", "fa": "Persian",
"el": "Greek", "hu": "Hungarian", "mk": "Macedonian", "ro": "Romanian",
}
QWEN3_TO_WHISPER_LANGUAGE = {v: k for k, v in WHISPER_TO_QWEN3_LANGUAGE.items()}
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
@dataclass
class Qwen3MLXSimulConfig:
language: str = "auto"
alignment_heads_path: Optional[str] = None
border_fraction: float = 0.15
rewind_fraction: float = 0.12
audio_min_len: float = 0.5
audio_max_len: float = 15.0
max_context_tokens: int = 30
max_alignment_heads: int = 20
# ---------------------------------------------------------------------------
# Per-session state
# ---------------------------------------------------------------------------
@dataclass
class _SessionState:
audio_buffer: np.ndarray = field(
default_factory=lambda: np.array([], dtype=np.float32)
)
cumulative_time_offset: float = 0.0
global_time_offset: float = 0.0
speaker: int = -1
last_attend_frame: int = -15
committed_word_count: int = 0
committed_token_ids: List[int] = field(default_factory=list)
detected_language: Optional[str] = None
last_infer_samples: int = 0
# ---------------------------------------------------------------------------
# Shared model holder
# ---------------------------------------------------------------------------
class Qwen3MLXSimulStreamingASR:
"""Loads the Qwen3-ASR model via ``mlx_qwen3_asr`` once and keeps it
alive for the lifetime of the server. Shared across sessions."""
sep = ""
SAMPLING_RATE = SAMPLE_RATE
def __init__(
self,
model_size: str = None,
model_dir: str = None,
model_path: str = None,
lan: str = "auto",
alignment_heads_path: Optional[str] = None,
border_fraction: float = 0.15,
warmup_file: Optional[str] = None,
model_cache_dir: Optional[str] = None,
lora_path: Optional[str] = None,
min_chunk_size: float = 0.1,
direct_english_translation: bool = False,
**kwargs,
):
import mlx.core as mx
import mlx_qwen3_asr
self.transcribe_kargs = {}
self.original_language = None if lan == "auto" else lan
self.warmup_file = warmup_file
self.cfg = Qwen3MLXSimulConfig(
language=lan,
alignment_heads_path=alignment_heads_path,
border_fraction=border_fraction,
)
# Resolve model path
resolved = model_dir or model_path
if not resolved:
size = (model_size or "base").lower()
if "/" in size or size.startswith("."):
resolved = size
else:
resolved = QWEN3_MLX_MODEL_MAPPING.get(size, "Qwen/Qwen3-ASR-0.6B")
t0 = time.time()
logger.info("Loading Qwen3-ASR MLX model '%s' for SimulStreaming ...", resolved)
self.model, self._config = mlx_qwen3_asr.load_model(resolved, dtype=mx.float16)
logger.info("Model loaded in %.2fs", time.time() - t0)
# Tokenizer
tok_path = getattr(self.model, "_resolved_model_path", None) or resolved
self.tokenizer = mlx_qwen3_asr.tokenizer.Tokenizer(str(tok_path))
# Architecture info
text_cfg = self._config.text_config
self.num_layers = text_cfg.num_hidden_layers
self.num_heads = text_cfg.num_attention_heads
self.num_kv_heads = text_cfg.num_key_value_heads
self.head_dim = text_cfg.head_dim
self.gqa_ratio = self.num_heads // self.num_kv_heads
self.audio_token_id = self._config.audio_token_id
logger.info(
"Qwen3-ASR arch: %d layers x %d heads (%d kv), head_dim=%d, GQA=%d",
self.num_layers, self.num_heads, self.num_kv_heads,
self.head_dim, self.gqa_ratio,
)
# Alignment heads
self.alignment_heads = self._load_alignment_heads(alignment_heads_path)
self.heads_by_layer = {}
for layer_idx, head_idx in self.alignment_heads:
self.heads_by_layer.setdefault(layer_idx, []).append(head_idx)
self.backend_choice = "qwen3-mlx-simul"
# Warmup
if warmup_file:
from whisperlivekit.warmup import load_file
audio = load_file(warmup_file)
if audio is not None:
self._warmup(audio)
def _load_alignment_heads(
self, path: Optional[str],
) -> List[Tuple[int, int]]:
max_heads = self.cfg.max_alignment_heads
if path and Path(path).exists():
with open(path) as f:
data = json.load(f)
all_heads = [tuple(h) for h in data["alignment_heads_compact"]]
heads = all_heads[:max_heads]
logger.info(
"Loaded top %d alignment heads from %s (of %d total)",
len(heads), path, len(all_heads),
)
return heads
# Default heuristic: last quarter of layers, all heads
default_heads = []
start_layer = self.num_layers * 3 // 4
for layer in range(start_layer, self.num_layers):
for head in range(self.num_heads):
default_heads.append((layer, head))
logger.warning(
"No alignment heads file. Using default heuristic: "
"%d heads from layers %d-%d.",
len(default_heads), start_layer, self.num_layers - 1,
)
return default_heads[:max_heads]
def _warmup(self, audio: np.ndarray):
import mlx.core as mx
try:
from mlx_qwen3_asr.audio import compute_features
audio = audio[:SAMPLE_RATE * 2]
mel, feat_lens = compute_features(audio)
mel = mel.astype(mx.float16)
audio_features, _ = self.model.audio_tower(mel, feat_lens)
n_audio = int(audio_features.shape[1])
prompt = self.tokenizer.build_prompt_tokens(n_audio, language="English")
input_ids = mx.array([prompt])
positions = mx.arange(input_ids.shape[1])[None, :]
position_ids = mx.stack([positions, positions, positions], axis=1)
cache = self.model.create_cache()
logits = self.model.prefill(input_ids, audio_features, position_ids, cache)
mx.eval(logits)
logger.info("Qwen3 MLX SimulStreaming warmup complete")
except Exception as e:
logger.warning("Warmup failed: %s", e)
def transcribe(self, audio):
pass # all work in the online processor
# ---------------------------------------------------------------------------
# Attention capture via wrapper replacement
# ---------------------------------------------------------------------------
class _AttnCaptureWrapper:
"""Wraps a TextAttention module to capture alignment scores during decode.
Replaces ``layer.self_attn`` with this wrapper. On decode steps (L=1),
recomputes Q with RoPE, reads cached K from the audio region, computes
``Q @ K_audio^T`` for alignment heads, and stores the argmax frame in
``capture["step_frames"]``.
Python dunder resolution (``__call__``) goes through the *class*, not the
instance, so monkey-patching ``attn.__call__`` on an ``nn.Module`` does
not work. This wrapper class defines its own ``__call__`` and delegates
everything else to the wrapped module via ``__getattr__``.
"""
def __init__(self, original, layer_idx, head_indices, gqa_ratio,
audio_start, audio_end, capture):
# Store in __dict__ directly to avoid triggering __getattr__
self.__dict__["_original"] = original
self.__dict__["_layer_idx"] = layer_idx
self.__dict__["_head_indices"] = head_indices
self.__dict__["_gqa_ratio"] = gqa_ratio
self.__dict__["_audio_start"] = audio_start
self.__dict__["_audio_end"] = audio_end
self.__dict__["_capture"] = capture
def __call__(self, x, cos, sin, mask=None, cache=None, layer_idx=0):
import mlx.core as mx
from mlx_qwen3_asr.mrope import apply_rotary_pos_emb
orig = self.__dict__["_original"]
B, L, _ = x.shape
if L == 1 and cache is not None:
li = self.__dict__["_layer_idx"]
h_indices = self.__dict__["_head_indices"]
gqa = self.__dict__["_gqa_ratio"]
a_start = self.__dict__["_audio_start"]
a_end = self.__dict__["_audio_end"]
cap = self.__dict__["_capture"]
# Recompute Q with RoPE (cheap: single token)
q = orig.q_proj(x)
q = q.reshape(B, L, orig.num_heads, orig.head_dim)
q = orig.q_norm(q)
q = q.transpose(0, 2, 1, 3) # (B, H, 1, D)
q_rope, _ = apply_rotary_pos_emb(q, q, cos, sin)
# K from cache (already has RoPE baked in from cache.update)
k_cached = cache.keys[li]
if k_cached is not None and a_end <= k_cached.shape[2]:
for h_idx in h_indices:
kv_h = h_idx // gqa
q_h = q_rope[0, h_idx, 0] # (head_dim,)
k_audio = k_cached[0, kv_h, a_start:a_end] # (n_audio, D)
scores = k_audio @ q_h # (n_audio,)
frame = int(mx.argmax(scores).item())
cap["step_frames"].append(frame)
return orig(x, cos, sin, mask=mask, cache=cache, layer_idx=layer_idx)
def __getattr__(self, name):
return getattr(self.__dict__["_original"], name)
def _install_alignment_hooks(model, heads_by_layer, gqa_ratio, audio_start, audio_end, capture):
"""Replace ``self_attn`` on alignment layers with capture wrappers.
Returns a list of ``(layer_idx, original_attn)`` for later restoration.
"""
originals = []
for layer_idx, head_indices in heads_by_layer.items():
if layer_idx >= len(model.model.layers):
continue
layer = model.model.layers[layer_idx]
orig_attn = layer.self_attn
wrapper = _AttnCaptureWrapper(
orig_attn, layer_idx, head_indices, gqa_ratio,
audio_start, audio_end, capture,
)
layer.self_attn = wrapper
originals.append((layer_idx, orig_attn))
return originals
def _remove_alignment_hooks(model, originals):
"""Restore original self_attn modules."""
for layer_idx, orig_attn in originals:
model.model.layers[layer_idx].self_attn = orig_attn
# ---------------------------------------------------------------------------
# Per-session online processor
# ---------------------------------------------------------------------------
class Qwen3MLXSimulStreamingOnlineProcessor:
"""Per-session processor implementing AlignAtt on MLX.
Same interface as other online processors:
insert_audio_chunk / process_iter / get_buffer / start_silence /
end_silence / finish / warmup / new_speaker.
"""
SAMPLING_RATE = SAMPLE_RATE
MIN_DURATION_REAL_SILENCE = 5
def __init__(self, asr: Qwen3MLXSimulStreamingASR, logfile=sys.stderr):
self.asr = asr
self.logfile = logfile
self.end = 0.0
self.buffer: List[ASRToken] = []
self.state = _SessionState()
# -- properties expected by AudioProcessor --
@property
def speaker(self):
return self.state.speaker
@speaker.setter
def speaker(self, value):
self.state.speaker = value
@property
def global_time_offset(self):
return self.state.global_time_offset
@global_time_offset.setter
def global_time_offset(self, value):
self.state.global_time_offset = value
# -- audio ingestion --
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time: float):
self.end = audio_stream_end_time
self.state.audio_buffer = np.append(self.state.audio_buffer, audio)
# Trim if too long
max_samples = int(self.asr.cfg.audio_max_len * self.SAMPLING_RATE)
if len(self.state.audio_buffer) > max_samples:
trim = len(self.state.audio_buffer) - max_samples
self.state.audio_buffer = self.state.audio_buffer[trim:]
self.state.cumulative_time_offset += trim / self.SAMPLING_RATE
self.state.last_infer_samples = max(0, self.state.last_infer_samples - trim)
# -- main processing --
def process_iter(self, is_last=False) -> Tuple[List[ASRToken], float]:
audio_duration = len(self.state.audio_buffer) / self.SAMPLING_RATE
if audio_duration < self.asr.cfg.audio_min_len:
return [], self.end
# Throttle: at least 1s of new audio
new_samples = len(self.state.audio_buffer) - self.state.last_infer_samples
if not is_last and new_samples < int(1.0 * self.SAMPLING_RATE):
return [], self.end
self.state.last_infer_samples = len(self.state.audio_buffer)
try:
words = self._infer(is_last)
except Exception as e:
logger.exception("Qwen3 MLX SimulStreaming inference error: %s", e)
return [], self.end
if not words:
return [], self.end
self.buffer = []
return words, self.end
def _infer(self, is_last: bool) -> List[ASRToken]:
"""Run one inference cycle with alignment-head-based stopping."""
import mlx.core as mx
from mlx_qwen3_asr.audio import compute_features
from mlx_qwen3_asr.generate import _detect_repetition
asr = self.asr
state = self.state
model = asr.model
# 1. Encode audio
mel, feat_lens = compute_features(state.audio_buffer)
mel = mel.astype(mx.float16)
audio_features, _ = model.audio_tower(mel, feat_lens)
n_audio_tokens = int(audio_features.shape[1])
mx.eval(audio_features)
if n_audio_tokens == 0:
return []
audio_duration = len(state.audio_buffer) / self.SAMPLING_RATE
# 2. Build prompt tokens
lan = asr.cfg.language
language = None
if lan and lan != "auto":
language = WHISPER_TO_QWEN3_LANGUAGE.get(lan, lan)
prompt_tokens = asr.tokenizer.build_prompt_tokens(
n_audio_tokens=n_audio_tokens,
language=language,
)
# Append committed context tokens
if state.committed_token_ids:
ctx = state.committed_token_ids[-asr.cfg.max_context_tokens:]
prompt_tokens.extend(ctx)
input_ids = mx.array([prompt_tokens])
seq_len = input_ids.shape[1]
# 3. Find audio token range
audio_positions = [
i for i, t in enumerate(prompt_tokens) if t == asr.audio_token_id
]
if not audio_positions:
return []
audio_start = audio_positions[0]
audio_end = audio_positions[-1] + 1
# 4. MRoPE position IDs
positions = mx.arange(seq_len, dtype=mx.int32)[None, :]
position_ids = mx.stack([positions, positions, positions], axis=1)
# 5. Prefill
cache = model.create_cache(max_seq_len=seq_len + 120)
logits = model.prefill(input_ids, audio_features, position_ids, cache)
mx.eval(logits)
# 6. Install alignment hooks
capture = {"step_frames": []}
originals = _install_alignment_hooks(
model, asr.heads_by_layer, asr.gqa_ratio,
audio_start, audio_end, capture,
)
# 7. Decode loop with border-distance policy
eos_ids = set(asr.tokenizer.EOS_TOKEN_IDS)
per_step_frames: List[List[int]] = []
last_attend_frame = state.last_attend_frame
border_stop_step: Optional[int] = None
border_threshold = max(2, int(n_audio_tokens * asr.cfg.border_fraction))
rewind_threshold = max(2, int(n_audio_tokens * asr.cfg.rewind_fraction))
# Max tokens: ~6 tokens/sec of speech + margin
new_audio_secs = (len(state.audio_buffer) - state.last_infer_samples) / self.SAMPLING_RATE
if is_last:
max_tokens = min(int(audio_duration * 6) + 10, 120)
else:
max_tokens = min(int(max(new_audio_secs, 1.0) * 6) + 5, 40)
token = int(mx.argmax(logits.reshape(-1)).item())
generated = [token]
try:
for step in range(1, max_tokens):
if token in eos_ids:
break
if _detect_repetition(generated):
break
next_ids = mx.array([[token]])
pos_val = seq_len + step - 1
next_pos = mx.array([[[pos_val], [pos_val], [pos_val]]], dtype=mx.int32)
logits = model.step(next_ids, next_pos, cache, validate_input_ids=False)
mx.eval(logits)
token = int(mx.argmax(logits.reshape(-1)).item())
generated.append(token)
# Collect frames from this step
if capture["step_frames"]:
per_step_frames.append(capture["step_frames"])
capture["step_frames"] = []
# Border-distance check (skip first 3 steps)
if (not is_last
and border_stop_step is None
and len(per_step_frames) >= 3):
latest = per_step_frames[-1]
if latest:
frames_sorted = sorted(latest)
attended = frames_sorted[len(frames_sorted) // 2]
# Rewind check
if last_attend_frame - attended > rewind_threshold:
border_stop_step = max(0, len(per_step_frames) - 2)
break
last_attend_frame = attended
# Border check
if (n_audio_tokens - attended) <= border_threshold:
border_stop_step = len(per_step_frames) - 1
break
# Periodic eval to prevent graph buildup
if step % 8 == 0:
mx.eval(cache.keys[-1])
finally:
_remove_alignment_hooks(model, originals)
# Flush remaining frames
if capture["step_frames"]:
per_step_frames.append(capture["step_frames"])
state.last_attend_frame = last_attend_frame
# 8. Process generated tokens
# Remove trailing EOS
while generated and generated[-1] in eos_ids:
generated.pop()
num_gen = len(generated)
if num_gen == 0:
return []
raw_text = asr.tokenizer.decode(generated)
logger.info(
"SimulStreaming raw: %d tokens (border_stop=%s), text=%r",
num_gen, border_stop_step, raw_text[:100],
)
# 9. Strip metadata prefix ("language English...")
from mlx_qwen3_asr.tokenizer import parse_asr_output
detected_lang, clean_text = parse_asr_output(
raw_text,
user_language=language,
)
# Find how many tokens to skip for metadata
metadata_offset = 0
asr_text_tokens = asr.tokenizer.encode("")
asr_text_id = asr_text_tokens[0] if asr_text_tokens else None
if asr_text_id is not None:
for i in range(min(num_gen, 10)):
if generated[i] == asr_text_id:
metadata_offset = i + 1
break
if metadata_offset > 0:
generated = generated[metadata_offset:]
num_gen -= metadata_offset
per_step_frames = per_step_frames[metadata_offset:]
if num_gen <= 0:
return []
# Detect language
if state.detected_language is None and detected_lang and detected_lang != "unknown":
state.detected_language = QWEN3_TO_WHISPER_LANGUAGE.get(
detected_lang, detected_lang.lower(),
)
logger.info("Auto-detected language: %s", state.detected_language)
# 10. Determine how many tokens to emit
step_frames = [f for f in per_step_frames if f]
if border_stop_step is not None:
emit_up_to = min(border_stop_step, num_gen)
else:
emit_up_to = num_gen
if emit_up_to <= 0:
return []
emitted_ids = generated[:emit_up_to]
# 11. Build timestamped words
words = self._build_timestamped_words(
emitted_ids, step_frames, emit_up_to,
n_audio_tokens, audio_duration,
)
# Update state
state.committed_word_count += len(words)
state.committed_token_ids.extend(emitted_ids)
return words
def _build_timestamped_words(
self,
generated_ids: List[int],
step_frames: List[List[int]],
emit_up_to: int,
n_audio_tokens: int,
audio_duration: float,
) -> List[ASRToken]:
"""Build timestamped ASRToken list from generated tokens and
alignment-head captured frames."""
state = self.state
asr = self.asr
# Per-token attended frame (median of head votes)
per_token_frame: List[Optional[int]] = []
for step_idx in range(emit_up_to):
if step_idx < len(step_frames) and step_frames[step_idx]:
frames = sorted(step_frames[step_idx])
per_token_frame.append(frames[len(frames) // 2])
else:
per_token_frame.append(None)
# Decode full text, split into words
full_text = asr.tokenizer.decode(generated_ids[:emit_up_to])
text_words = full_text.split()
# Map words to frames proportionally
all_frames = [f for f in per_token_frame if f is not None]
word_frame_pairs = []
for wi, word in enumerate(text_words):
if all_frames:
frac = wi / max(len(text_words), 1)
frame_idx = min(int(frac * len(all_frames)), len(all_frames) - 1)
frame = all_frames[frame_idx]
else:
frame = None
word_frame_pairs.append((word, frame))
# Convert to ASRToken
tokens = []
for i, (text, frame) in enumerate(word_frame_pairs):
text = text.strip()
if not text:
continue
if frame is not None and n_audio_tokens > 0:
timestamp = (
frame / n_audio_tokens * audio_duration
+ state.cumulative_time_offset
)
else:
timestamp = (
(i / max(len(word_frame_pairs), 1)) * audio_duration
+ state.cumulative_time_offset
)
is_very_first_word = (i == 0 and state.committed_word_count == 0)
display_text = text if is_very_first_word else " " + text
token = ASRToken(
start=round(timestamp, 2),
end=round(timestamp + 0.1, 2),
text=display_text,
speaker=state.speaker,
detected_language=state.detected_language,
).with_offset(state.global_time_offset)
tokens.append(token)
return tokens
# -- silence / speaker / lifecycle --
def start_silence(self) -> Tuple[List[ASRToken], float]:
all_tokens = []
for _ in range(5):
tokens, _ = self.process_iter(is_last=True)
if not tokens:
break
all_tokens.extend(tokens)
return all_tokens, self.end
def end_silence(self, silence_duration: float, offset: float):
self.end += silence_duration
long_silence = silence_duration >= self.MIN_DURATION_REAL_SILENCE
if not long_silence:
gap_len = int(self.SAMPLING_RATE * silence_duration)
if gap_len > 0:
gap_silence = np.zeros(gap_len, dtype=np.float32)
self.state.audio_buffer = np.append(
self.state.audio_buffer, gap_silence,
)
else:
self.state = _SessionState()
self.state.global_time_offset = silence_duration + offset
def new_speaker(self, change_speaker):
self.process_iter(is_last=True)
self.state = _SessionState()
self.state.speaker = change_speaker.speaker
self.state.global_time_offset = change_speaker.start
def get_buffer(self) -> Transcript:
return Transcript.from_tokens(tokens=self.buffer, sep='')
def warmup(self, audio: np.ndarray, init_prompt: str = ""):
try:
self.state.audio_buffer = audio[:SAMPLE_RATE]
self.process_iter(is_last=True)
self.state = _SessionState()
logger.info("Qwen3 MLX SimulStreaming processor warmed up")
except Exception as e:
logger.warning("Warmup failed: %s", e)
self.state = _SessionState()
def finish(self) -> Tuple[List[ASRToken], float]:
all_tokens = []
for _ in range(5):
tokens, _ = self.process_iter(is_last=True)
if not tokens:
break
all_tokens.extend(tokens)
return all_tokens, self.end
================================================
FILE: whisperlivekit/qwen3_simul.py
================================================
"""
SimulStreaming-style online processor for Qwen3-ASR.
Architecture overview
---------------------
Qwen3-ASR is a decoder-only multimodal model. Audio is encoded by an audio
encoder (Whisper-style) into a sequence of embeddings that replace <|audio_pad|>
placeholder tokens in the input sequence. The text decoder then uses causal
self-attention over the combined audio + text tokens.
Unlike Whisper (which has explicit cross-attention between decoder and encoder),
Qwen3-ASR uses self-attention where generated text tokens attend to earlier
audio tokens and previously generated text. This means "alignment heads" here
are self-attention heads whose attention over the *audio-token region* tracks
the monotonic audio-to-text alignment.
The border-distance policy works as follows:
- After each generated token, extract the attention weights from the
selected alignment heads, restricted to the audio-token region
- Find which audio frame each head attends to most strongly (argmax)
- If the most-attended audio frame is approaching the end of the available
audio, pause generation and wait for more audio
- If the most-attended frame jumps backward (rewind), discard recent tokens
This module loads the Qwen3-ASR model *directly* via transformers (not through
the qwen_asr package's Qwen3ASRModel wrapper), giving us full control over
forward passes, KV caches, and attention extraction.
Requires:
- A pre-computed alignment heads JSON file (from detect_alignment_heads_qwen3.py)
- OR will fall back to all heads in a configurable set of layers
"""
import json
import logging
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Tuple
import numpy as np
import torch
from whisperlivekit.timed_objects import ASRToken, ChangeSpeaker, Transcript
logger = logging.getLogger(__name__)
SAMPLE_RATE = 16000
@dataclass
class Qwen3SimulConfig:
"""Configuration for Qwen3 SimulStreaming."""
model_id: str = "Qwen/Qwen3-ASR-1.7B"
alignment_heads_path: Optional[str] = None
language: str = "auto"
# Border/rewind thresholds as fraction of audio tokens (not absolute frames).
# Qwen3 has ~13 audio tokens/sec vs Whisper's ~50, so absolute thresholds
# don't transfer. 0.15 = pause when attention is within last 15% of audio.
border_fraction: float = 0.15 # Fraction of audio tokens from end to trigger pause
rewind_fraction: float = 0.12 # Max backward jump as fraction of audio tokens
audio_min_len: float = 0.5 # Minimum audio length before starting decode
audio_max_len: float = 15.0 # Maximum audio buffer length in seconds
max_context_tokens: int = 30 # Max committed tokens to include as context
init_prompt: Optional[str] = None
max_alignment_heads: int = 20 # Use only top N alignment heads
@dataclass
class _AudioEmbedCache:
"""Cached audio encoder outputs for incremental encoding.
The Qwen3-ASR audio encoder processes mel features in chunks of
``n_window * 2`` mel frames with windowed self-attention spanning
``n_window_infer`` mel frames (800 for both 0.6B and 1.7B = 8s of
audio). Within one attention window chunks can attend to each other,
but across windows they cannot.
We cache the audio embeddings (output of ``get_audio_features``) for
all *complete attention windows* whose input mel frames are unchanged.
When the audio buffer grows, only the tail (last incomplete window +
new audio) is re-encoded through the audio encoder, and the result is
concatenated with the cached prefix.
When the audio buffer is trimmed from the front (e.g. max_len exceeded),
the cache is fully invalidated and rebuilt on the next call.
"""
# Number of audio *samples* (PCM @ 16kHz) that have been fully encoded.
# This always equals the number of samples whose mel features were fed
# to the audio encoder for the cached embeddings.
encoded_samples: int = 0
# Cached audio embeddings tensor, shape (1, n_cached_tokens, hidden_dim).
# None means "no cache yet".
embeddings: Optional[torch.Tensor] = None
# Number of mel frames that produced ``embeddings``.
# Used to verify cache validity (mel length must match).
encoded_mel_frames: int = 0
# Number of audio tokens (embeddings.shape[1]) that are in *complete*
# attention windows and can be safely reused. Tokens from the last
# (potentially incomplete) window are always re-encoded.
stable_tokens: int = 0
def trim_front(self, trim_samples: int, sample_rate: int = 16000):
"""Invalidate cache entries for audio trimmed from the front.
Called when ``insert_audio_chunk`` trims the buffer. Rather than
attempting complex partial invalidation (which could introduce subtle
bugs if the mel/token math doesn't align perfectly), we simply reset
the cache. The next ``_encode_audio_cached`` call will rebuild it.
This is safe because trimming only happens when the audio buffer
exceeds ``audio_max_len`` (~15s), which is relatively infrequent.
"""
self.reset()
def reset(self):
"""Fully invalidate the cache."""
self.encoded_samples = 0
self.embeddings = None
self.encoded_mel_frames = 0
self.stable_tokens = 0
@dataclass
class Qwen3SimulState:
"""Per-session mutable state for Qwen3 SimulStreaming."""
# Audio
audio_buffer: np.ndarray = field(
default_factory=lambda: np.array([], dtype=np.float32)
)
cumulative_time_offset: float = 0.0
global_time_offset: float = 0.0
speaker: int = -1
# Decode state
last_attend_frame: int = -15
generated_tokens: List[int] = field(default_factory=list)
committed_text: str = ""
committed_word_count: int = 0 # How many words already emitted
committed_token_ids: List[int] = field(default_factory=list) # token IDs for prompt context
# Tracking
first_timestamp: Optional[float] = None
detected_language: Optional[str] = None
last_infer_samples: int = 0 # audio_buffer length at last inference
# Audio embedding cache for incremental encoding
audio_cache: _AudioEmbedCache = field(default_factory=_AudioEmbedCache)
class Qwen3SimulStreamingASR:
"""
Shared backend for Qwen3-ASR SimulStreaming.
Loads the model once and is shared across sessions. Each session gets
its own Qwen3SimulStreamingOnlineProcessor with independent state.
"""
sep = ""
def __init__(
self,
model_size: str = None,
model_dir: str = None,
lan: str = "auto",
alignment_heads_path: Optional[str] = None,
border_fraction: float = 0.15,
min_chunk_size: float = 0.1,
warmup_file: Optional[str] = None,
model_cache_dir: Optional[str] = None,
model_path: Optional[str] = None,
lora_path: Optional[str] = None,
direct_english_translation: bool = False,
**kwargs,
):
self.transcribe_kargs = {}
self.original_language = None if lan == "auto" else lan
self.warmup_file = warmup_file
self.cfg = Qwen3SimulConfig(
language=lan,
alignment_heads_path=alignment_heads_path,
border_fraction=border_fraction,
)
# Load model directly via transformers
self._load_model(model_size, model_dir, model_cache_dir, model_path)
# Load alignment heads
self.alignment_heads = self._load_alignment_heads(alignment_heads_path)
# Warmup
if warmup_file:
from whisperlivekit.warmup import load_file
audio = load_file(warmup_file)
if audio is not None:
logger.info("Warming up Qwen3 SimulStreaming model")
# Simple warmup: just encode a short audio
self._warmup(audio)
def _load_model(self, model_size, model_dir, model_cache_dir, model_path):
"""Load Qwen3-ASR via transformers (SDPA attention for speed)."""
from whisperlivekit.qwen3_asr import (
QWEN3_MODEL_MAPPING,
_patch_transformers_compat,
)
_patch_transformers_compat()
from qwen_asr.core.transformers_backend import (
Qwen3ASRConfig,
Qwen3ASRForConditionalGeneration,
Qwen3ASRProcessor,
)
from transformers import AutoConfig, AutoModel, AutoProcessor
AutoConfig.register("qwen3_asr", Qwen3ASRConfig)
AutoModel.register(Qwen3ASRConfig, Qwen3ASRForConditionalGeneration)
AutoProcessor.register(Qwen3ASRConfig, Qwen3ASRProcessor)
if model_dir:
model_id = model_dir
elif model_path:
model_id = model_path
elif model_size:
model_id = QWEN3_MODEL_MAPPING.get(model_size.lower(), model_size)
else:
model_id = "Qwen/Qwen3-ASR-1.7B"
if torch.cuda.is_available():
dtype, device = torch.bfloat16, "cuda:0"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
dtype, device = torch.float32, "mps"
else:
dtype, device = torch.float32, "cpu"
logger.info("Loading Qwen3-ASR for SimulStreaming: %s (sdpa attention)", model_id)
self.model = AutoModel.from_pretrained(
model_id,
torch_dtype=dtype,
device_map=device,
)
self.model.eval()
self.processor = AutoProcessor.from_pretrained(model_id, fix_mistral_regex=True)
# Cache model properties
thinker = self.model.thinker
text_config = thinker.config.text_config
self.num_layers = text_config.num_hidden_layers
self.num_heads = text_config.num_attention_heads
self.num_kv_heads = text_config.num_key_value_heads
self.audio_token_id = thinker.config.audio_token_id
self.device = next(self.model.parameters()).device
self.dtype = next(self.model.parameters()).dtype
# Cache special token IDs for metadata stripping
self.asr_text_token_id = self.processor.tokenizer.convert_tokens_to_ids("")
logger.info(
"Qwen3-ASR loaded: %d layers x %d heads, device=%s, id=%d",
self.num_layers, self.num_heads, self.device, self.asr_text_token_id,
)
def _load_alignment_heads(
self, path: Optional[str],
) -> List[Tuple[int, int]]:
"""Load alignment heads from JSON or use defaults.
Only loads the top N heads (sorted by TS score) for efficiency.
The Qwen3-ASR model has alignment info spread across most heads
(decoder-only, no cross-attention), so we pick the strongest ones.
"""
max_heads = self.cfg.max_alignment_heads
if path and Path(path).exists():
with open(path) as f:
data = json.load(f)
# alignment_heads_compact is pre-sorted by TS score (descending)
all_heads = [tuple(h) for h in data["alignment_heads_compact"]]
heads = all_heads[:max_heads]
logger.info(
"Loaded top %d alignment heads from %s (of %d total)",
len(heads), path, len(all_heads),
)
return heads
# Default: use heads from the last quarter of layers
default_heads = []
start_layer = self.num_layers * 3 // 4
for layer in range(start_layer, self.num_layers):
for head in range(self.num_heads):
default_heads.append((layer, head))
logger.warning(
"No alignment heads file found. Using default heuristic: "
"%d heads from layers %d-%d. Run detect_alignment_heads_qwen3.py "
"to find optimal heads.",
len(default_heads), start_layer, self.num_layers - 1,
)
return default_heads[:max_heads]
def _warmup(self, audio: np.ndarray):
"""Run a short inference to warmup the model."""
try:
audio = audio[:SAMPLE_RATE * 2] # Max 2 seconds
msgs = [
{"role": "system", "content": ""},
{"role": "user", "content": [{"type": "audio", "audio": ""}]},
]
text_prompt = self.processor.apply_chat_template(
msgs, add_generation_prompt=True, tokenize=False,
)
inputs = self.processor(
text=[text_prompt],
audio=[audio],
return_tensors="pt",
padding=True,
)
inputs = inputs.to(self.device).to(self.dtype)
with torch.inference_mode():
self.model.thinker.generate(
**inputs, max_new_tokens=5, do_sample=False,
)
logger.info("Qwen3 SimulStreaming warmup complete")
except Exception as e:
logger.warning("Warmup failed: %s", e)
def transcribe(self, audio):
"""No-op -- SimulStreaming uses the online processor directly."""
pass
class Qwen3SimulStreamingOnlineProcessor:
"""
Per-session online processor for Qwen3-ASR SimulStreaming.
Implements the same interface as SimulStreamingOnlineProcessor:
- insert_audio_chunk(audio, time)
- process_iter(is_last=False) -> (List[ASRToken], float)
- get_buffer() -> Transcript
- start_silence() -> (List[ASRToken], float)
- end_silence(duration, offset)
- finish() -> (List[ASRToken], float)
"""
SAMPLING_RATE = 16000
MIN_DURATION_REAL_SILENCE = 5
def __init__(self, asr: Qwen3SimulStreamingASR, logfile=sys.stderr):
self.asr = asr
self.logfile = logfile
self.end = 0.0
self.buffer: List[ASRToken] = []
# Per-session state
self.state = Qwen3SimulState()
# Build the prompt template once
self._build_prompt_template()
def _build_prompt_template(self):
"""Build the base text prompt for Qwen3-ASR."""
from whisperlivekit.qwen3_asr import WHISPER_TO_QWEN3_LANGUAGE
msgs = [
{"role": "system", "content": ""},
{"role": "user", "content": [{"type": "audio", "audio": ""}]},
]
self._base_prompt = self.asr.processor.apply_chat_template(
msgs, add_generation_prompt=True, tokenize=False,
)
# Add language forcing if configured
lan = self.asr.cfg.language
if lan and lan != "auto":
lang_name = WHISPER_TO_QWEN3_LANGUAGE.get(lan, lan)
self._base_prompt += f"language {lang_name}"
@property
def speaker(self):
return self.state.speaker
@speaker.setter
def speaker(self, value):
self.state.speaker = value
@property
def global_time_offset(self):
return self.state.global_time_offset
@global_time_offset.setter
def global_time_offset(self, value):
self.state.global_time_offset = value
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time: float):
"""Append an audio chunk to be processed."""
self.end = audio_stream_end_time
self.state.audio_buffer = np.append(self.state.audio_buffer, audio)
# Trim audio if too long
max_samples = int(self.asr.cfg.audio_max_len * self.SAMPLING_RATE)
if len(self.state.audio_buffer) > max_samples:
trim = len(self.state.audio_buffer) - max_samples
self.state.audio_buffer = self.state.audio_buffer[trim:]
self.state.cumulative_time_offset += trim / self.SAMPLING_RATE
# Adjust throttle counter so it tracks position within the trimmed buffer
self.state.last_infer_samples = max(0, self.state.last_infer_samples - trim)
# Trim audio embedding cache to match
self.state.audio_cache.trim_front(trim, self.SAMPLING_RATE)
def start_silence(self) -> Tuple[List[ASRToken], float]:
"""Handle start of silence -- flush all pending tokens.
Loops inference until the model produces no new tokens, since a
single is_last call may not exhaust all text for the buffered audio.
"""
all_tokens = []
for _ in range(5): # safety limit
tokens, processed_upto = self.process_iter(is_last=True)
if not tokens:
break
all_tokens.extend(tokens)
return all_tokens, self.end
def end_silence(self, silence_duration: float, offset: float):
"""Handle silence period."""
self.end += silence_duration
long_silence = silence_duration >= self.MIN_DURATION_REAL_SILENCE
if not long_silence:
gap_len = int(self.SAMPLING_RATE * silence_duration)
if gap_len > 0:
gap_silence = np.zeros(gap_len, dtype=np.float32)
self.state.audio_buffer = np.append(
self.state.audio_buffer, gap_silence,
)
else:
# Long silence: reset
self.state = Qwen3SimulState()
self.state.global_time_offset = silence_duration + offset
def new_speaker(self, change_speaker: ChangeSpeaker):
"""Handle speaker change event."""
self.process_iter(is_last=True)
self.state = Qwen3SimulState()
self.state.speaker = change_speaker.speaker
self.state.global_time_offset = change_speaker.start
def get_buffer(self) -> Transcript:
"""Get the current unvalidated buffer."""
return Transcript.from_tokens(tokens=self.buffer, sep='')
def _encode_audio_cached(self) -> Optional[torch.Tensor]:
"""Encode audio buffer using cached embeddings where possible.
Returns the full audio embeddings tensor (n_audio_tokens, hidden_dim),
or None if caching is not possible (caller should fall back to the
processor-based path).
Caching strategy:
- The audio encoder uses windowed attention with window size
``n_window_infer`` (800 mel frames = 8s of audio for both the
0.6B and 1.7B models).
- Tokens within one window can attend to each other, but not across
windows. So all tokens in *complete* windows are deterministic
and can be cached.
- We only re-encode the *tail* of the audio (from the last complete
window boundary onward) through the audio encoder.
- The cached prefix embeddings are concatenated with the new tail
embeddings to produce the full result.
"""
asr = self.asr
state = self.state
cache = state.audio_cache
if len(state.audio_buffer) == 0:
return None
try:
from qwen_asr.core.transformers_backend.processing_qwen3_asr import (
_get_feat_extract_output_lengths,
)
# Step 1: Compute mel features for the FULL audio.
# WhisperFeatureExtractor is fast (CPU FFT), so this is cheap.
feat_out = asr.processor.feature_extractor(
[state.audio_buffer],
sampling_rate=16000,
padding=True,
truncation=False,
return_attention_mask=True,
return_tensors="pt",
)
input_features = feat_out["input_features"].to(asr.device).to(asr.dtype)
feature_attention_mask = feat_out["attention_mask"].to(asr.device)
total_mel_frames = feature_attention_mask.sum().item()
# Step 2: Compute total audio tokens for the full audio.
total_audio_tokens = _get_feat_extract_output_lengths(
torch.tensor(total_mel_frames),
).item()
# Step 3: Determine how many tokens are in stable (complete) windows.
# The encoder processes mel in chunks of n_window*2 (200 frames).
# Attention windows span n_window_infer (400 frames) = 2 chunks.
# A window is "complete" if it has a full n_window_infer mel frames.
audio_cfg = asr.model.thinker.audio_tower.config
n_window_infer = getattr(audio_cfg, "n_window_infer", 400)
# Number of complete attention windows
n_complete_windows = total_mel_frames // n_window_infer
if n_complete_windows <= 0:
# Audio is shorter than one window -- no stable prefix to cache.
# Encode the full audio and cache it (all unstable).
audio_embeds = asr.model.thinker.get_audio_features(
input_features, feature_attention_mask=feature_attention_mask,
)
# Update cache for next call
cache.embeddings = audio_embeds.unsqueeze(0) if audio_embeds.dim() == 2 else audio_embeds
cache.encoded_samples = len(state.audio_buffer)
cache.encoded_mel_frames = total_mel_frames
cache.stable_tokens = 0
return cache.embeddings[0] if cache.embeddings.dim() == 3 else cache.embeddings
# Mel frames in the stable prefix (all complete windows)
stable_mel = n_complete_windows * n_window_infer
stable_tokens = _get_feat_extract_output_lengths(
torch.tensor(stable_mel),
).item()
# Step 4: Check if we have a valid cache for the stable prefix.
# The cache is valid if:
# - We have cached embeddings
# - The number of stable tokens in the cache matches (or exceeds)
# the current stable prefix
# - The audio buffer hasn't been modified before the cached region
can_reuse = (
cache.embeddings is not None
and cache.stable_tokens > 0
and cache.stable_tokens <= stable_tokens
# The encoded_samples tells us how much audio the cache covers.
# If the current buffer starts with the same audio, the prefix
# embeddings are still valid.
and cache.encoded_samples <= len(state.audio_buffer)
)
if can_reuse and cache.stable_tokens == stable_tokens:
# The stable prefix hasn't changed -- reuse cached embeddings
# for the stable part, only re-encode the tail.
cached_prefix = cache.embeddings[0, :stable_tokens] if cache.embeddings.dim() == 3 else cache.embeddings[:stable_tokens]
# Encode only the tail (from stable_mel onward)
tail_mel_start = stable_mel
tail_features = input_features[:, :, tail_mel_start:]
tail_mel_frames = total_mel_frames - tail_mel_start
if tail_mel_frames > 0:
tail_mask = torch.ones(
(1, tail_features.shape[2]),
dtype=feature_attention_mask.dtype,
device=feature_attention_mask.device,
)
tail_embeds = asr.model.thinker.get_audio_features(
tail_features, feature_attention_mask=tail_mask,
)
# get_audio_features returns (n_tokens, hidden_dim)
if tail_embeds.dim() == 3:
tail_embeds = tail_embeds[0]
audio_embeds = torch.cat([cached_prefix, tail_embeds], dim=0)
else:
audio_embeds = cached_prefix
logger.info(
"Audio cache HIT: reused %d/%d tokens, re-encoded %d tail tokens",
stable_tokens, total_audio_tokens,
total_audio_tokens - stable_tokens,
)
else:
# Cache miss or stale -- encode the full audio
audio_embeds = asr.model.thinker.get_audio_features(
input_features, feature_attention_mask=feature_attention_mask,
)
if audio_embeds.dim() == 3:
audio_embeds = audio_embeds[0]
logger.info(
"Audio cache MISS: encoded full %d tokens (was: %d stable cached)",
total_audio_tokens, cache.stable_tokens if cache.embeddings is not None else 0,
)
# Step 5: Update cache for next call.
cache.embeddings = audio_embeds.unsqueeze(0) # (1, n_tokens, hidden)
cache.encoded_samples = len(state.audio_buffer)
cache.encoded_mel_frames = total_mel_frames
cache.stable_tokens = stable_tokens
return audio_embeds # (n_tokens, hidden_dim)
except Exception as e:
logger.warning("Audio cache encoding failed, falling back: %s", e)
cache.reset()
return None
def _build_inputs_with_cached_audio(
self, audio_embeds: torch.Tensor,
) -> Optional[dict]:
"""Build generate() inputs using pre-computed audio embeddings.
Instead of passing ``input_features`` (which triggers the audio encoder
inside the model's forward), we:
1. Tokenize the text prompt to get ``input_ids``
2. Embed the text tokens via ``get_input_embeddings()``
3. Replace audio placeholder positions with ``audio_embeds``
4. Append committed context token embeddings
5. Return ``inputs_embeds`` + ``attention_mask`` (no ``input_ids``,
no ``input_features``)
Returns None if the construction fails (caller falls back).
"""
asr = self.asr
state = self.state
thinker = asr.model.thinker
try:
from qwen_asr.core.transformers_backend.processing_qwen3_asr import (
_get_feat_extract_output_lengths,
)
n_audio_tokens = audio_embeds.shape[0]
# Tokenize the text prompt with the correct number of audio
# placeholder tokens. The processor's
# ``replace_multimodal_special_tokens`` expands the single
# <|audio_pad|> into the right count.
prompt_with_placeholders = asr.processor.replace_multimodal_special_tokens(
[self._base_prompt],
iter([n_audio_tokens]),
)[0]
text_ids = asr.processor.tokenizer(
[prompt_with_placeholders],
return_tensors="pt",
padding=True,
)
input_ids = text_ids["input_ids"].to(asr.device)
attention_mask = text_ids.get("attention_mask")
if attention_mask is not None:
attention_mask = attention_mask.to(asr.device)
# Append committed context tokens
if state.committed_token_ids:
ctx = state.committed_token_ids[-asr.cfg.max_context_tokens:]
ctx_ids = torch.tensor(
[ctx], dtype=input_ids.dtype, device=input_ids.device,
)
input_ids = torch.cat([input_ids, ctx_ids], dim=1)
if attention_mask is not None:
ctx_mask = torch.ones_like(ctx_ids)
attention_mask = torch.cat([attention_mask, ctx_mask], dim=1)
# Build inputs_embeds: embed text tokens, then scatter audio embeds
inputs_embeds = thinker.get_input_embeddings()(input_ids)
# Find audio placeholder positions
audio_mask = (input_ids == asr.audio_token_id)
n_placeholders = audio_mask.sum().item()
if n_placeholders != n_audio_tokens:
logger.warning(
"Audio token mismatch: %d placeholders vs %d embeddings",
n_placeholders, n_audio_tokens,
)
return None
# Scatter audio embeddings into placeholder positions
audio_embeds_for_scatter = audio_embeds.to(
inputs_embeds.device, inputs_embeds.dtype,
)
expand_mask = audio_mask.unsqueeze(-1).expand_as(inputs_embeds)
inputs_embeds = inputs_embeds.masked_scatter(
expand_mask, audio_embeds_for_scatter,
)
result = {
"inputs_embeds": inputs_embeds,
"input_ids": input_ids, # needed for position_ids/rope computation
}
if attention_mask is not None:
result["attention_mask"] = attention_mask
return result
except Exception as e:
logger.warning("Failed to build inputs with cached audio: %s", e)
return None
@torch.inference_mode()
def process_iter(self, is_last=False) -> Tuple[List[ASRToken], float]:
"""
Process accumulated audio using SimulStreaming with alignment heads.
This performs a full forward pass (encode audio + greedy decode with
attention extraction), applying the border-distance policy to decide
when to stop generating.
Returns:
Tuple of (committed ASRToken list, audio processed up to time).
"""
audio_duration = len(self.state.audio_buffer) / self.SAMPLING_RATE
if audio_duration < self.asr.cfg.audio_min_len:
return [], self.end
# Throttle: skip inference if less than 1s of new audio since last run.
# Audio embedding caching avoids re-encoding the stable prefix, but
# the decoder still runs a full prefill, so calling too often wastes
# GPU/CPU time and causes lag to spiral.
new_samples = len(self.state.audio_buffer) - self.state.last_infer_samples
min_new_seconds = 1.0
if not is_last and new_samples < int(min_new_seconds * self.SAMPLING_RATE):
return [], self.end
logger.info("Running SimulStreaming inference on %.2fs of audio (%.2fs new)", audio_duration, new_samples / self.SAMPLING_RATE)
self.state.last_infer_samples = len(self.state.audio_buffer)
try:
timestamped_words = self._infer(is_last)
except Exception as e:
logger.exception("Qwen3 SimulStreaming inference error: %s", e)
return [], self.end
logger.info("SimulStreaming produced %d words", len(timestamped_words))
if not timestamped_words:
return [], self.end
self.buffer = []
return timestamped_words, self.end
def _infer(self, is_last: bool) -> List[ASRToken]:
"""Run one inference cycle with alignment-head-based stopping.
Uses forward hooks on self_attn modules to capture attention weights
during generation. The Qwen3-ASR decoder layer discards attention
weights (hidden_states, _ = self.self_attn(...)), so output_attentions
via generate() would return None. Hooks capture them before discard.
Audio embedding caching: instead of re-encoding the entire audio buffer
through the audio encoder on every call, we cache embeddings for the
stable prefix (complete attention windows) and only re-encode the tail.
This reduces the audio encoding cost from O(n) to O(1) per call for
the stable prefix, changing overall complexity from O(n^2) to O(n).
"""
asr = self.asr
state = self.state
# --- Prepare inputs (with audio embedding cache) ---
#
# Try the cached path first: encode audio incrementally, then build
# inputs_embeds directly. If anything fails, fall back to the original
# processor-based path.
use_cached_path = False
audio_embeds = self._encode_audio_cached()
if audio_embeds is not None:
cached_inputs = self._build_inputs_with_cached_audio(audio_embeds)
if cached_inputs is not None:
input_ids_for_pos = cached_inputs["input_ids"]
inputs_embeds = cached_inputs["inputs_embeds"]
# Build the inputs dict for generate().
# We pass BOTH input_ids and inputs_embeds. The model's forward()
# checks: if inputs_embeds is not None, it skips embedding lookup.
# But input_ids is still needed for:
# - Finding audio placeholder positions (get_placeholder_mask)
# - Computing position_ids / rope_deltas
# We set input_features=None so the model does NOT re-run the
# audio encoder.
inputs = {
"input_ids": input_ids_for_pos,
"inputs_embeds": inputs_embeds,
"attention_mask": cached_inputs.get("attention_mask"),
}
# Remove None values
inputs = {k: v for k, v in inputs.items() if v is not None}
use_cached_path = True
if not use_cached_path:
# Fallback: original processor-based path (full re-encoding)
logger.info("Using fallback (non-cached) audio encoding path")
state.audio_cache.reset()
inputs = asr.processor(
text=[self._base_prompt],
audio=[state.audio_buffer],
return_tensors="pt",
padding=True,
)
inputs = inputs.to(asr.device).to(asr.dtype)
# Append committed token IDs as context
if state.committed_token_ids:
ctx = state.committed_token_ids[-asr.cfg.max_context_tokens:]
ctx_ids = torch.tensor(
[ctx], dtype=inputs.input_ids.dtype,
device=inputs.input_ids.device,
)
inputs["input_ids"] = torch.cat([inputs.input_ids, ctx_ids], dim=1)
if "attention_mask" in inputs:
ctx_mask = torch.ones_like(ctx_ids)
inputs["attention_mask"] = torch.cat(
[inputs.attention_mask, ctx_mask], dim=1,
)
# prompt_len = number of tokens in the input sequence (for slicing
# generated tokens from the output). generate() constructs output
# starting from input_ids, so use input_ids.shape[1] in both paths.
if use_cached_path:
prompt_len = inputs["input_ids"].shape[1]
else:
prompt_len = inputs.input_ids.shape[1]
# Find audio token range from input_ids
if use_cached_path:
ids_for_audio_range = inputs["input_ids"][0]
else:
ids_for_audio_range = inputs.input_ids[0]
audio_mask = (ids_for_audio_range == asr.audio_token_id)
audio_positions = audio_mask.nonzero(as_tuple=True)[0]
if len(audio_positions) == 0:
return []
audio_start = audio_positions[0].item()
audio_end = audio_positions[-1].item() + 1
n_audio_tokens = audio_end - audio_start
audio_duration = len(state.audio_buffer) / self.SAMPLING_RATE
# Install forward hooks to capture alignment attention from Q and K.
# With SDPA attention (fast), attn_weights are not returned. Instead,
# we hook self_attn to compute Q*K^T attention ONLY for alignment heads
# during autoregressive steps (q_len == 1). This is cheap because we
# only compute dot products for ~20 heads, not full attention for all.
#
# Key detail: self_attn is called with ALL keyword arguments from the
# decoder layer, so hidden_states/position_embeddings/past_key_values
# are all in kwargs, not args.
per_step_frames: List[List[int]] = []
current_step_frames: List[int] = []
heads_by_layer: dict = {}
for layer_idx, head_idx in asr.alignment_heads:
heads_by_layer.setdefault(layer_idx, []).append(head_idx)
decoder_layers = asr.model.thinker.model.layers
num_kv_heads = asr.num_kv_heads
num_heads = asr.num_heads
gqa_ratio = num_heads // num_kv_heads # GQA group size
# Import RoPE function used by this model's attention
from qwen_asr.core.transformers_backend.modeling_qwen3_asr import (
apply_rotary_pos_emb,
)
hooks = []
def _make_attn_hook(layer_idx):
"""Forward hook on self_attn that computes Q*K^T for alignment heads.
After the forward pass, we recompute Q (with RoPE) for the current
token and dot it against the cached K (which already has RoPE) in
the audio region. This gives us per-head alignment frames.
"""
head_indices = heads_by_layer[layer_idx]
def hook_fn(module, args, kwargs, output):
# All arguments are keyword-passed from the decoder layer
hidden_states = kwargs.get('hidden_states')
if hidden_states is None:
hidden_states = args[0] if args else None
if hidden_states is None or hidden_states.shape[1] != 1:
return # Skip prefill (seq_len > 1)
position_embeddings = kwargs.get('position_embeddings')
if position_embeddings is None and len(args) > 1:
position_embeddings = args[1]
past_kv = kwargs.get('past_key_values')
if position_embeddings is None or past_kv is None:
return
# Recompute Q with RoPE (cheap: single token through q_proj + RoPE)
hidden_shape = (*hidden_states.shape[:-1], -1, module.head_dim)
q = module.q_norm(
module.q_proj(hidden_states).view(hidden_shape)
).transpose(1, 2)
cos, sin = position_embeddings
q, _ = apply_rotary_pos_emb(q, q, cos, sin)
# K from cache already has RoPE applied
cache_layer = past_kv.layers[module.layer_idx]
k = cache_layer.keys # (batch, n_kv_heads, kv_len, head_dim)
if k is None or audio_end > k.shape[2]:
return
# Compute attention scores for alignment heads only
for h_idx in head_indices:
if h_idx >= q.shape[1]:
continue
kv_h_idx = h_idx // gqa_ratio
q_h = q[0, h_idx, 0] # (head_dim,)
k_audio = k[0, kv_h_idx, audio_start:audio_end] # (n_audio, head_dim)
scores = torch.matmul(k_audio, q_h) # (n_audio,)
frame = scores.argmax().item()
current_step_frames.append(frame)
return hook_fn
for layer_idx in heads_by_layer:
if layer_idx < len(decoder_layers):
h = decoder_layers[layer_idx].self_attn.register_forward_hook(
_make_attn_hook(layer_idx),
with_kwargs=True,
)
hooks.append(h)
# Step boundary hook on lm_head to separate per-step frames
# and check border-distance stopping criteria in real-time.
# This is CRITICAL for performance: instead of generating 200 tokens
# then truncating, we stop as soon as attention hits the audio border.
# On MPS, each token costs ~50ms, so stopping at 10 tokens vs 200
# means ~0.5s vs ~10s inference.
last_attend_frame = state.last_attend_frame
border_stop_step: Optional[int] = None
# Compute absolute thresholds from fractional config
border_threshold = max(2, int(n_audio_tokens * asr.cfg.border_fraction))
rewind_threshold = max(2, int(n_audio_tokens * asr.cfg.rewind_fraction))
def _step_boundary_hook(module, args, output):
nonlocal current_step_frames, last_attend_frame, border_stop_step
if current_step_frames:
per_step_frames.append(current_step_frames)
current_step_frames = []
# Check border distance on each step.
# Allow at least 3 steps before checking, so short buffers
# can still produce some tokens during streaming.
if not is_last and border_stop_step is None and len(per_step_frames) >= 3:
latest = per_step_frames[-1]
if latest:
frames_sorted = sorted(latest)
attended = frames_sorted[len(frames_sorted) // 2]
# Rewind check
if last_attend_frame - attended > rewind_threshold:
border_stop_step = max(0, len(per_step_frames) - 2)
return
last_attend_frame = attended
# Border check
if (n_audio_tokens - attended) <= border_threshold:
border_stop_step = len(per_step_frames) - 1
return
lm_head = asr.model.thinker.lm_head
step_hook = lm_head.register_forward_hook(_step_boundary_hook)
hooks.append(step_hook)
# StoppingCriteria that stops generation when border distance is hit
from transformers import StoppingCriteria, StoppingCriteriaList
class BorderStop(StoppingCriteria):
def __call__(self, input_ids, scores, **kwargs):
return border_stop_step is not None
stopping = StoppingCriteriaList([BorderStop()])
# Limit max tokens to what's reasonable for the audio duration.
# On MPS, each token costs ~50-100ms, so tight limits are critical.
# Speech produces ~4-6 tokens/sec; +5 for metadata prefix tokens.
# With is_last, allow slightly more for flushing remaining text.
new_audio_secs = (len(state.audio_buffer) - state.last_infer_samples) / self.SAMPLING_RATE
tokens_per_sec = 6
if is_last:
max_tokens = min(int(audio_duration * tokens_per_sec) + 10, 120)
else:
max_tokens = min(int(max(new_audio_secs, 1.0) * tokens_per_sec) + 5, 40)
try:
outputs = asr.model.thinker.generate(
**inputs,
max_new_tokens=max_tokens,
do_sample=False,
stopping_criteria=stopping,
)
finally:
for h in hooks:
h.remove()
# Flush any remaining frames
if current_step_frames:
per_step_frames.append(current_step_frames)
state.last_attend_frame = last_attend_frame
# Extract generated tokens
all_generated = outputs[0, prompt_len:]
eos_ids = {151645, 151643}
if asr.processor.tokenizer.eos_token_id is not None:
eos_ids.add(asr.processor.tokenizer.eos_token_id)
num_gen = len(all_generated)
for i, tid in enumerate(all_generated):
if tid.item() in eos_ids:
num_gen = i
break
raw_text = asr.processor.tokenizer.decode(all_generated[:num_gen], skip_special_tokens=True)
logger.info(
"SimulStreaming raw output: %d tokens (stopped at step %s), text=%r",
num_gen, border_stop_step, raw_text[:100],
)
if num_gen == 0:
return []
# Strip metadata prefix: when language is "auto", the model generates
# "language ..." before actual transcription text.
# Find token and skip everything before it (including itself).
asr_text_id = asr.asr_text_token_id
metadata_offset = 0
for i in range(min(num_gen, 10)): # metadata is at most ~3-4 tokens
if all_generated[i].item() == asr_text_id:
# Detect language from the metadata prefix before stripping
if state.detected_language is None and i > 0:
from whisperlivekit.qwen3_asr import QWEN3_TO_WHISPER_LANGUAGE
prefix_text = asr.processor.tokenizer.decode(
all_generated[:i].tolist(), skip_special_tokens=True,
).strip()
parts = prefix_text.split()
if len(parts) >= 2:
lang_name = parts[-1]
if lang_name.lower() != "none":
state.detected_language = QWEN3_TO_WHISPER_LANGUAGE.get(
lang_name, lang_name.lower(),
)
logger.info("Auto-detected language: %s", state.detected_language)
metadata_offset = i + 1
break
if metadata_offset > 0:
logger.info(
"Stripping %d metadata prefix tokens (before )",
metadata_offset,
)
all_generated = all_generated[metadata_offset:]
num_gen -= metadata_offset
per_step_frames = per_step_frames[metadata_offset:]
if num_gen <= 0:
return []
# Determine how many tokens to emit based on border stopping
step_frames = [f for f in per_step_frames if f]
if border_stop_step is not None:
emit_up_to = min(border_stop_step, num_gen)
else:
emit_up_to = num_gen
# Build timestamped words from the emitted tokens
generated_ids = all_generated[:emit_up_to]
if len(generated_ids) == 0:
return []
all_words = self._build_timestamped_words(
generated_ids, step_frames, emit_up_to,
n_audio_tokens, audio_duration,
)
new_words = all_words
# Update committed word count for space-prefix logic in next batch
state.committed_word_count += len(new_words)
# Append newly emitted token IDs to committed context for next call
new_emitted = outputs[0, prompt_len:prompt_len + emit_up_to + metadata_offset]
state.committed_token_ids.extend(new_emitted.tolist())
return new_words
def _build_timestamped_words(
self,
generated_ids: torch.Tensor,
step_frames: List[List[int]],
emit_up_to: int,
n_audio_tokens: int,
audio_duration: float,
) -> List[ASRToken]:
"""Build timestamped ASRToken list from generated tokens and hook-captured frames."""
asr = self.asr
state = self.state
# Get per-token attended audio frame (median of alignment head votes)
per_token_frame: List[Optional[int]] = []
for step in range(emit_up_to):
if step < len(step_frames) and step_frames[step]:
frames = sorted(step_frames[step])
per_token_frame.append(frames[len(frames) // 2])
else:
per_token_frame.append(None)
# Decode the full generated sequence at once, then split into words.
# This is more robust than per-token Ġ detection, which can fail when
# committed context causes the model to generate sub-word continuations.
tokenizer = asr.processor.tokenizer
full_text = tokenizer.decode(generated_ids.tolist(), skip_special_tokens=True)
text_words = full_text.split()
# Map each text word to an approximate frame using token-level alignment.
# Distribute frames evenly across words (since exact token→word mapping
# is imprecise with BPE sub-words anyway).
all_frames = [f for f in per_token_frame if f is not None]
words = []
for wi, word in enumerate(text_words):
if all_frames:
# Proportionally assign frames to words
frac = wi / max(len(text_words), 1)
frame_idx = int(frac * len(all_frames))
frame_idx = min(frame_idx, len(all_frames) - 1)
frame = all_frames[frame_idx]
else:
frame = None
words.append((word, frame))
# Convert to ASRToken with timestamps
tokens = []
for i, (text, frame) in enumerate(words):
text = text.strip()
if not text:
continue
if frame is not None and n_audio_tokens > 0:
timestamp = (
frame / n_audio_tokens * audio_duration
+ state.cumulative_time_offset
)
else:
timestamp = (
(i / max(len(words), 1)) * audio_duration
+ state.cumulative_time_offset
)
# Prefix space: first word of the very first batch has no space;
# all subsequent words (same batch or later batches) get a space.
is_very_first_word = (i == 0 and state.committed_word_count == 0)
display_text = text if is_very_first_word else " " + text
token = ASRToken(
start=round(timestamp, 2),
end=round(timestamp + 0.1, 2),
text=display_text,
speaker=state.speaker,
detected_language=state.detected_language,
).with_offset(state.global_time_offset)
tokens.append(token)
return tokens
@staticmethod
def _median_frame(frames: List[int]) -> Optional[int]:
"""Return median of frame list, or None if empty."""
if not frames:
return None
frames_sorted = sorted(frames)
return frames_sorted[len(frames_sorted) // 2]
def warmup(self, audio: np.ndarray, init_prompt: str = ""):
"""Warmup the model with a short audio clip."""
try:
self.state.audio_buffer = audio[:SAMPLE_RATE]
self.process_iter(is_last=True)
self.state = Qwen3SimulState()
logger.info("Qwen3 SimulStreaming online processor warmed up")
except Exception as e:
logger.warning("Warmup failed: %s", e)
self.state = Qwen3SimulState()
def finish(self) -> Tuple[List[ASRToken], float]:
"""Flush remaining audio at end of stream."""
all_tokens = []
for _ in range(5): # safety limit
tokens, _ = self.process_iter(is_last=True)
if not tokens:
break
all_tokens.extend(tokens)
return all_tokens, self.end
================================================
FILE: whisperlivekit/qwen3_simul_kv.py
================================================
"""
Qwen3-ASR SimulStreaming with KV cache reuse.
This is an optimized version of qwen3_simul.py that reuses the KV cache
across inference calls, avoiding redundant prefill of prompt + old audio.
Architecture:
1. First call: full prefill (prompt + audio tokens), greedy decode with
alignment-head stopping, save KV cache + generated tokens
2. Subsequent calls: invalidate KV for old audio suffix, prefill only
new audio tokens, continue decoding from saved state
3. Audio encoder caching: reuse embeddings for stable attention windows
This gives ~3-5x speedup over the original generate()-based approach.
"""
import json
import logging
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Tuple
import numpy as np
import torch
from transformers import DynamicCache
from whisperlivekit.timed_objects import ASRToken, ChangeSpeaker, Transcript
logger = logging.getLogger(__name__)
SAMPLE_RATE = 16000
@dataclass
class Qwen3SimulKVConfig:
"""Configuration for Qwen3 SimulStreaming with KV cache."""
model_id: str = "Qwen/Qwen3-ASR-1.7B"
alignment_heads_path: Optional[str] = None
language: str = "auto"
border_fraction: float = 0.20
rewind_fraction: float = 0.12
audio_min_len: float = 0.5
audio_max_len: float = 30.0
max_context_tokens: int = 20
init_prompt: Optional[str] = None
max_alignment_heads: int = 10
min_new_seconds: float = 2.0 # minimum new audio before running inference
@dataclass
class _AudioEmbedCache:
"""Cache for audio encoder outputs."""
encoded_samples: int = 0
embeddings: Optional[torch.Tensor] = None
encoded_mel_frames: int = 0
stable_tokens: int = 0
def reset(self):
self.encoded_samples = 0
self.embeddings = None
self.encoded_mel_frames = 0
self.stable_tokens = 0
@dataclass
class Qwen3SimulKVState:
"""Per-session mutable state with KV cache."""
# Audio
audio_buffer: np.ndarray = field(
default_factory=lambda: np.array([], dtype=np.float32)
)
cumulative_time_offset: float = 0.0
global_time_offset: float = 0.0
speaker: int = -1
# KV cache state
kv_cache: Optional[DynamicCache] = None
kv_seq_len: int = 0 # sequence length when KV was saved
prompt_token_count: int = 0 # tokens before audio (system prompt etc)
audio_token_count: int = 0 # audio tokens in the cached KV
generated_token_ids: List[int] = field(default_factory=list)
# Alignment tracking
last_attend_frame: int = -15
committed_text: str = ""
committed_word_count: int = 0
committed_token_ids: List[int] = field(default_factory=list)
# Tracking
first_timestamp: Optional[float] = None
detected_language: Optional[str] = None
last_infer_samples: int = 0
# Audio embedding cache
audio_cache: _AudioEmbedCache = field(default_factory=_AudioEmbedCache)
def reset_kv(self):
"""Reset KV cache (e.g., when audio is trimmed from front)."""
self.kv_cache = None
self.kv_seq_len = 0
self.prompt_token_count = 0
self.audio_token_count = 0
self.generated_token_ids = []
# Reset alignment tracking — old frame references are invalid
# after audio is trimmed from the front
self.last_attend_frame = -15
class Qwen3SimulKVASR:
"""
Shared backend for Qwen3-ASR SimulStreaming with KV cache reuse.
"""
sep = ""
def __init__(
self,
model_size: str = None,
model_dir: str = None,
lan: str = "auto",
alignment_heads_path: Optional[str] = None,
border_fraction: float = 0.15,
min_chunk_size: float = 0.1,
warmup_file: Optional[str] = None,
model_cache_dir: Optional[str] = None,
model_path: Optional[str] = None,
lora_path: Optional[str] = None,
direct_english_translation: bool = False,
**kwargs,
):
self.transcribe_kargs = {}
self.original_language = None if lan == "auto" else lan
self.warmup_file = warmup_file
self.cfg = Qwen3SimulKVConfig(
language=lan,
alignment_heads_path=alignment_heads_path,
border_fraction=border_fraction,
)
self._load_model(model_size, model_dir, model_cache_dir, model_path)
self.alignment_heads = self._load_alignment_heads(alignment_heads_path)
# Pre-compute heads by layer for efficient hook installation
self.heads_by_layer = {}
for layer_idx, head_idx in self.alignment_heads:
self.heads_by_layer.setdefault(layer_idx, []).append(head_idx)
if warmup_file:
from whisperlivekit.warmup import load_file
audio = load_file(warmup_file)
if audio is not None:
self._warmup(audio)
def _load_model(self, model_size, model_dir, model_cache_dir, model_path):
from whisperlivekit.qwen3_asr import QWEN3_MODEL_MAPPING, _patch_transformers_compat
_patch_transformers_compat()
from qwen_asr.core.transformers_backend import (
Qwen3ASRConfig, Qwen3ASRForConditionalGeneration, Qwen3ASRProcessor,
)
from transformers import AutoConfig, AutoModel, AutoProcessor
AutoConfig.register("qwen3_asr", Qwen3ASRConfig)
AutoModel.register(Qwen3ASRConfig, Qwen3ASRForConditionalGeneration)
AutoProcessor.register(Qwen3ASRConfig, Qwen3ASRProcessor)
if model_dir:
model_id = model_dir
elif model_path:
model_id = model_path
elif model_size:
model_id = QWEN3_MODEL_MAPPING.get(model_size.lower(), model_size)
else:
model_id = "Qwen/Qwen3-ASR-1.7B"
if torch.cuda.is_available():
dtype, device = torch.bfloat16, "cuda:0"
else:
dtype, device = torch.float32, "cpu"
logger.info("Loading Qwen3-ASR for SimulStreaming+KV: %s", model_id)
self.model = AutoModel.from_pretrained(model_id, dtype=dtype, device_map=device)
self.model.eval()
self.processor = AutoProcessor.from_pretrained(model_id, fix_mistral_regex=True)
thinker = self.model.thinker
text_config = thinker.config.text_config
self.num_layers = text_config.num_hidden_layers
self.num_heads = text_config.num_attention_heads
self.num_kv_heads = text_config.num_key_value_heads
self.audio_token_id = thinker.config.audio_token_id
self.device = next(self.model.parameters()).device
self.dtype = next(self.model.parameters()).dtype
self.asr_text_token_id = self.processor.tokenizer.convert_tokens_to_ids("")
# EOS tokens
self.eos_ids = {151645, 151643}
if self.processor.tokenizer.eos_token_id is not None:
self.eos_ids.add(self.processor.tokenizer.eos_token_id)
logger.info(
"Qwen3-ASR loaded: %d layers x %d heads, device=%s",
self.num_layers, self.num_heads, self.device,
)
def _load_alignment_heads(self, path):
max_heads = self.cfg.max_alignment_heads
if path and Path(path).exists():
with open(path) as f:
data = json.load(f)
all_heads = [tuple(h) for h in data["alignment_heads_compact"]]
heads = all_heads[:max_heads]
logger.info("Loaded top %d alignment heads from %s", len(heads), path)
return heads
default_heads = []
start_layer = self.num_layers * 3 // 4
for layer in range(start_layer, self.num_layers):
for head in range(self.num_heads):
default_heads.append((layer, head))
logger.warning("No alignment heads file. Using %d default heads.", len(default_heads))
return default_heads[:max_heads]
def _warmup(self, audio):
try:
audio = audio[:SAMPLE_RATE * 2]
msgs = [{"role": "system", "content": ""}, {"role": "user", "content": [{"type": "audio", "audio": ""}]}]
text_prompt = self.processor.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
inputs = self.processor(text=[text_prompt], audio=[audio], return_tensors="pt", padding=True)
inputs = inputs.to(self.device).to(self.dtype)
with torch.inference_mode():
self.model.thinker.generate(**inputs, max_new_tokens=5, do_sample=False)
logger.info("Warmup complete")
except Exception as e:
logger.warning("Warmup failed: %s", e)
def transcribe(self, audio):
pass
class Qwen3SimulKVOnlineProcessor:
"""
Per-session online processor with KV cache reuse.
Key optimization: instead of calling generate() each time (which does
full prefill), we maintain a DynamicCache and do incremental prefill
+ manual greedy decoding with alignment head hooks.
"""
SAMPLING_RATE = 16000
MIN_DURATION_REAL_SILENCE = 5
def __init__(self, asr: Qwen3SimulKVASR, logfile=sys.stderr):
self.asr = asr
self.logfile = logfile
self.end = 0.0
self.buffer: List[ASRToken] = []
self.state = Qwen3SimulKVState()
self._build_prompt_template()
def _build_prompt_template(self):
from whisperlivekit.qwen3_asr import WHISPER_TO_QWEN3_LANGUAGE
msgs = [
{"role": "system", "content": ""},
{"role": "user", "content": [{"type": "audio", "audio": ""}]},
]
self._base_prompt = self.asr.processor.apply_chat_template(
msgs, add_generation_prompt=True, tokenize=False,
)
lan = self.asr.cfg.language
if lan and lan != "auto":
lang_name = WHISPER_TO_QWEN3_LANGUAGE.get(lan, lan)
self._base_prompt += f"language {lang_name}"
@property
def speaker(self):
return self.state.speaker
@speaker.setter
def speaker(self, value):
self.state.speaker = value
@property
def global_time_offset(self):
return self.state.global_time_offset
@global_time_offset.setter
def global_time_offset(self, value):
self.state.global_time_offset = value
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time: float):
self.end = audio_stream_end_time
self.state.audio_buffer = np.append(self.state.audio_buffer, audio)
max_samples = int(self.asr.cfg.audio_max_len * self.SAMPLING_RATE)
if len(self.state.audio_buffer) > max_samples:
trim = len(self.state.audio_buffer) - max_samples
self.state.audio_buffer = self.state.audio_buffer[trim:]
self.state.cumulative_time_offset += trim / self.SAMPLING_RATE
self.state.last_infer_samples = max(0, self.state.last_infer_samples - trim)
self.state.audio_cache.reset()
self.state.reset_kv() # Must invalidate KV when audio is trimmed
def start_silence(self) -> Tuple[List[ASRToken], float]:
all_tokens = []
for _ in range(5):
tokens, _ = self.process_iter(is_last=True)
if not tokens:
break
all_tokens.extend(tokens)
return all_tokens, self.end
def end_silence(self, silence_duration: float, offset: float):
self.end += silence_duration
long_silence = silence_duration >= self.MIN_DURATION_REAL_SILENCE
if not long_silence:
gap_len = int(self.SAMPLING_RATE * silence_duration)
if gap_len > 0:
self.state.audio_buffer = np.append(
self.state.audio_buffer, np.zeros(gap_len, dtype=np.float32),
)
else:
self.state = Qwen3SimulKVState()
self.state.global_time_offset = silence_duration + offset
def new_speaker(self, change_speaker: ChangeSpeaker):
self.process_iter(is_last=True)
self.state = Qwen3SimulKVState()
self.state.speaker = change_speaker.speaker
self.state.global_time_offset = change_speaker.start
def get_buffer(self) -> Transcript:
return Transcript.from_tokens(tokens=self.buffer, sep='')
def _encode_audio(self) -> Tuple[torch.Tensor, int]:
"""Encode full audio buffer, with caching for stable windows."""
asr = self.asr
state = self.state
from qwen_asr.core.transformers_backend.processing_qwen3_asr import (
_get_feat_extract_output_lengths,
)
feat_out = asr.processor.feature_extractor(
[state.audio_buffer], sampling_rate=16000,
padding=True, truncation=False,
return_attention_mask=True, return_tensors="pt",
)
input_features = feat_out["input_features"].to(asr.device).to(asr.dtype)
feature_attention_mask = feat_out["attention_mask"].to(asr.device)
total_mel_frames = feature_attention_mask.sum().item()
total_audio_tokens = _get_feat_extract_output_lengths(
torch.tensor(total_mel_frames),
).item()
cache = state.audio_cache
audio_cfg = asr.model.thinker.audio_tower.config
n_window_infer = getattr(audio_cfg, "n_window_infer", 400)
n_complete_windows = total_mel_frames // n_window_infer
if n_complete_windows <= 0 or cache.embeddings is None:
# Full encode
audio_embeds = asr.model.thinker.get_audio_features(
input_features, feature_attention_mask=feature_attention_mask,
)
if audio_embeds.dim() == 3:
audio_embeds = audio_embeds[0]
stable_mel = n_complete_windows * n_window_infer if n_complete_windows > 0 else 0
stable_tokens = _get_feat_extract_output_lengths(
torch.tensor(stable_mel),
).item() if stable_mel > 0 else 0
else:
stable_mel = n_complete_windows * n_window_infer
stable_tokens = _get_feat_extract_output_lengths(
torch.tensor(stable_mel),
).item()
if cache.stable_tokens > 0 and cache.stable_tokens <= stable_tokens:
cached_prefix = cache.embeddings[:stable_tokens] if cache.embeddings.dim() == 2 else cache.embeddings[0, :stable_tokens]
tail_features = input_features[:, :, stable_mel:]
tail_mel_frames = total_mel_frames - stable_mel
if tail_mel_frames > 0:
tail_mask = torch.ones(
(1, tail_features.shape[2]),
dtype=feature_attention_mask.dtype,
device=feature_attention_mask.device,
)
tail_embeds = asr.model.thinker.get_audio_features(
tail_features, feature_attention_mask=tail_mask,
)
if tail_embeds.dim() == 3:
tail_embeds = tail_embeds[0]
audio_embeds = torch.cat([cached_prefix, tail_embeds], dim=0)
else:
audio_embeds = cached_prefix
else:
audio_embeds = asr.model.thinker.get_audio_features(
input_features, feature_attention_mask=feature_attention_mask,
)
if audio_embeds.dim() == 3:
audio_embeds = audio_embeds[0]
# Update cache
cache.embeddings = audio_embeds if audio_embeds.dim() == 2 else audio_embeds[0]
cache.encoded_samples = len(state.audio_buffer)
cache.encoded_mel_frames = total_mel_frames
stable_mel_final = n_complete_windows * n_window_infer if n_complete_windows > 0 else 0
cache.stable_tokens = _get_feat_extract_output_lengths(
torch.tensor(stable_mel_final),
).item() if stable_mel_final > 0 else 0
return audio_embeds, total_audio_tokens
def _build_full_inputs(self, audio_embeds: torch.Tensor) -> dict:
"""Build full input embeddings from prompt + audio embeddings + context."""
asr = self.asr
state = self.state
thinker = asr.model.thinker
from qwen_asr.core.transformers_backend.processing_qwen3_asr import (
_get_feat_extract_output_lengths,
)
n_audio_tokens = audio_embeds.shape[0]
prompt_with_placeholders = asr.processor.replace_multimodal_special_tokens(
[self._base_prompt], iter([n_audio_tokens]),
)[0]
text_ids = asr.processor.tokenizer(
[prompt_with_placeholders], return_tensors="pt", padding=True,
)
input_ids = text_ids["input_ids"].to(asr.device)
attention_mask = text_ids.get("attention_mask")
if attention_mask is not None:
attention_mask = attention_mask.to(asr.device)
# Append committed context tokens
if state.committed_token_ids:
ctx = state.committed_token_ids[-asr.cfg.max_context_tokens:]
ctx_ids = torch.tensor([ctx], dtype=input_ids.dtype, device=input_ids.device)
input_ids = torch.cat([input_ids, ctx_ids], dim=1)
if attention_mask is not None:
ctx_mask = torch.ones_like(ctx_ids)
attention_mask = torch.cat([attention_mask, ctx_mask], dim=1)
# Build inputs_embeds
inputs_embeds = thinker.get_input_embeddings()(input_ids)
audio_mask = (input_ids == asr.audio_token_id)
n_placeholders = audio_mask.sum().item()
if n_placeholders != n_audio_tokens:
logger.warning("Audio token mismatch: %d vs %d", n_placeholders, n_audio_tokens)
return None
audio_embeds_cast = audio_embeds.to(inputs_embeds.device, inputs_embeds.dtype)
expand_mask = audio_mask.unsqueeze(-1).expand_as(inputs_embeds)
inputs_embeds = inputs_embeds.masked_scatter(expand_mask, audio_embeds_cast)
# Find audio token range
audio_positions = audio_mask[0].nonzero(as_tuple=True)[0]
audio_start = audio_positions[0].item()
audio_end = audio_positions[-1].item() + 1
return {
"input_ids": input_ids,
"inputs_embeds": inputs_embeds,
"attention_mask": attention_mask,
"audio_start": audio_start,
"audio_end": audio_end,
"n_audio_tokens": n_audio_tokens,
}
@torch.inference_mode()
def process_iter(self, is_last=False) -> Tuple[List[ASRToken], float]:
audio_duration = len(self.state.audio_buffer) / self.SAMPLING_RATE
if audio_duration < self.asr.cfg.audio_min_len:
return [], self.end
new_samples = len(self.state.audio_buffer) - self.state.last_infer_samples
min_new_seconds = self.asr.cfg.min_new_seconds
if not is_last and new_samples < int(min_new_seconds * self.SAMPLING_RATE):
return [], self.end
self.state.last_infer_samples = len(self.state.audio_buffer)
try:
timestamped_words = self._infer(is_last)
except Exception as e:
logger.exception("Inference error: %s", e)
self.state.reset_kv()
return [], self.end
if not timestamped_words:
return [], self.end
self.buffer = []
return timestamped_words, self.end
def _infer(self, is_last: bool) -> List[ASRToken]:
"""Run inference with KV cache reuse and alignment-head stopping."""
asr = self.asr
state = self.state
thinker = asr.model.thinker
# Step 1: Encode audio (with caching)
audio_embeds, n_audio_tokens_total = self._encode_audio()
# Step 2: Build full inputs
full_inputs = self._build_full_inputs(audio_embeds)
if full_inputs is None:
state.reset_kv()
return []
input_ids = full_inputs["input_ids"]
inputs_embeds = full_inputs["inputs_embeds"]
attention_mask = full_inputs["attention_mask"]
audio_start = full_inputs["audio_start"]
audio_end = full_inputs["audio_end"]
n_audio_tokens = full_inputs["n_audio_tokens"]
audio_duration = len(state.audio_buffer) / self.SAMPLING_RATE
# Step 3: Full prefill (we always re-prefill since audio tokens change)
# Future optimization: partial prefill when only tail audio changes
out = thinker(
input_ids=input_ids,
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
use_cache=True,
)
kv_cache = out.past_key_values
prompt_len = input_ids.shape[1]
# Step 4: Greedy decode with alignment head stopping
border_threshold = max(2, int(n_audio_tokens * asr.cfg.border_fraction))
rewind_threshold = max(2, int(n_audio_tokens * asr.cfg.rewind_fraction))
last_attend_frame = state.last_attend_frame
# Install hooks for alignment head attention extraction
decoder_layers = thinker.model.layers
num_kv_heads = asr.num_kv_heads
num_heads = asr.num_heads
gqa_ratio = num_heads // num_kv_heads
from qwen_asr.core.transformers_backend.modeling_qwen3_asr import apply_rotary_pos_emb
per_step_frames: List[List[int]] = []
current_step_frames: List[int] = []
hooks = []
def _make_attn_hook(layer_idx):
head_indices = asr.heads_by_layer[layer_idx]
def hook_fn(module, args, kwargs, output):
hidden_states = kwargs.get('hidden_states')
if hidden_states is None:
hidden_states = args[0] if args else None
if hidden_states is None or hidden_states.shape[1] != 1:
return
position_embeddings = kwargs.get('position_embeddings')
if position_embeddings is None and len(args) > 1:
position_embeddings = args[1]
past_kv = kwargs.get('past_key_values')
if position_embeddings is None or past_kv is None:
return
hidden_shape = (*hidden_states.shape[:-1], -1, module.head_dim)
q = module.q_norm(module.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
cos, sin = position_embeddings
q, _ = apply_rotary_pos_emb(q, q, cos, sin)
cache_layer = past_kv.layers[module.layer_idx]
k = cache_layer.keys
if k is None or audio_end > k.shape[2]:
return
for h_idx in head_indices:
if h_idx >= q.shape[1]:
continue
kv_h_idx = h_idx // gqa_ratio
q_h = q[0, h_idx, 0]
k_audio = k[0, kv_h_idx, audio_start:audio_end]
scores = torch.matmul(k_audio, q_h)
frame = scores.argmax().item()
current_step_frames.append(frame)
return hook_fn
for layer_idx in asr.heads_by_layer:
if layer_idx < len(decoder_layers):
h = decoder_layers[layer_idx].self_attn.register_forward_hook(
_make_attn_hook(layer_idx), with_kwargs=True,
)
hooks.append(h)
try:
# Greedy decoding with alignment-based stopping
next_token = out.logits[:, -1, :].argmax(dim=-1, keepdim=True)
generated_ids = []
border_stop_step = None
tokens_per_sec = 6
if is_last:
max_tokens = min(int(audio_duration * tokens_per_sec) + 10, 120)
else:
new_audio_secs = (len(state.audio_buffer) - state.last_infer_samples) / self.SAMPLING_RATE
max_tokens = min(int(max(new_audio_secs, 1.0) * tokens_per_sec) + 5, 40)
for step in range(max_tokens):
tid = next_token.item()
if tid in asr.eos_ids:
break
generated_ids.append(tid)
# Collect alignment frames for this step
if current_step_frames:
per_step_frames.append(current_step_frames)
current_step_frames = []
# Check stopping criteria (after 3 tokens)
if not is_last and len(per_step_frames) >= 3:
latest = per_step_frames[-1]
if latest:
frames_sorted = sorted(latest)
attended = frames_sorted[len(frames_sorted) // 2]
if last_attend_frame - attended > rewind_threshold:
border_stop_step = max(0, len(per_step_frames) - 2)
break
last_attend_frame = attended
if (n_audio_tokens - attended) <= border_threshold:
border_stop_step = len(per_step_frames) - 1
break
# Next token
out = thinker(
input_ids=next_token,
past_key_values=kv_cache,
use_cache=True,
)
kv_cache = out.past_key_values
next_token = out.logits[:, -1, :].argmax(dim=-1, keepdim=True)
# Flush remaining frames
if current_step_frames:
per_step_frames.append(current_step_frames)
finally:
for h in hooks:
h.remove()
state.last_attend_frame = last_attend_frame
if not generated_ids:
return []
# Strip metadata prefix ( token)
all_generated = torch.tensor(generated_ids, device=asr.device)
num_gen = len(generated_ids)
asr_text_id = asr.asr_text_token_id
metadata_offset = 0
for i in range(min(num_gen, 10)):
if generated_ids[i] == asr_text_id:
if state.detected_language is None and i > 0:
from whisperlivekit.qwen3_asr import QWEN3_TO_WHISPER_LANGUAGE
prefix_text = asr.processor.tokenizer.decode(
generated_ids[:i], skip_special_tokens=True,
).strip()
parts = prefix_text.split()
if len(parts) >= 2:
lang_name = parts[-1]
if lang_name.lower() != "none":
state.detected_language = QWEN3_TO_WHISPER_LANGUAGE.get(
lang_name, lang_name.lower(),
)
metadata_offset = i + 1
break
if metadata_offset > 0:
generated_ids = generated_ids[metadata_offset:]
num_gen -= metadata_offset
per_step_frames = per_step_frames[metadata_offset:]
if num_gen <= 0:
return []
# Determine emit count
if border_stop_step is not None:
emit_up_to = min(border_stop_step, num_gen)
else:
emit_up_to = num_gen
emitted_ids = generated_ids[:emit_up_to]
if not emitted_ids:
return []
# Build timestamped words
words = self._build_timestamped_words(
emitted_ids, per_step_frames, emit_up_to,
n_audio_tokens, audio_duration,
)
state.committed_word_count += len(words)
# Include metadata in committed tokens for context
all_emitted = generated_ids[:emit_up_to]
if metadata_offset > 0:
all_emitted = generated_ids[:emit_up_to] # already stripped
state.committed_token_ids.extend(all_emitted)
return words
def _build_timestamped_words(
self,
generated_ids: list,
step_frames: List[List[int]],
emit_up_to: int,
n_audio_tokens: int,
audio_duration: float,
) -> List[ASRToken]:
asr = self.asr
state = self.state
per_token_frame = []
for step in range(emit_up_to):
if step < len(step_frames) and step_frames[step]:
frames = sorted(step_frames[step])
per_token_frame.append(frames[len(frames) // 2])
else:
per_token_frame.append(None)
tokenizer = asr.processor.tokenizer
full_text = tokenizer.decode(generated_ids[:emit_up_to], skip_special_tokens=True)
text_words = full_text.split()
all_frames = [f for f in per_token_frame if f is not None]
words = []
for wi, word in enumerate(text_words):
if all_frames:
frac = wi / max(len(text_words), 1)
frame_idx = min(int(frac * len(all_frames)), len(all_frames) - 1)
frame = all_frames[frame_idx]
else:
frame = None
words.append((word, frame))
tokens = []
for i, (text, frame) in enumerate(words):
text = text.strip()
if not text:
continue
if frame is not None and n_audio_tokens > 0:
timestamp = (
frame / n_audio_tokens * audio_duration
+ state.cumulative_time_offset
)
else:
timestamp = (
(i / max(len(words), 1)) * audio_duration
+ state.cumulative_time_offset
)
is_very_first_word = (i == 0 and state.committed_word_count == 0)
display_text = text if is_very_first_word else " " + text
token = ASRToken(
start=round(timestamp, 2),
end=round(timestamp + 0.1, 2),
text=display_text,
speaker=state.speaker,
detected_language=state.detected_language,
).with_offset(state.global_time_offset)
tokens.append(token)
return tokens
def warmup(self, audio: np.ndarray, init_prompt: str = ""):
try:
self.state.audio_buffer = audio[:SAMPLE_RATE]
self.process_iter(is_last=True)
self.state = Qwen3SimulKVState()
except Exception as e:
logger.warning("Warmup failed: %s", e)
self.state = Qwen3SimulKVState()
def finish(self) -> Tuple[List[ASRToken], float]:
all_tokens = []
for _ in range(5):
tokens, _ = self.process_iter(is_last=True)
if not tokens:
break
all_tokens.extend(tokens)
return all_tokens, self.end
================================================
FILE: whisperlivekit/session_asr_proxy.py
================================================
"""Per-session ASR proxy for language override.
Wraps a shared ASR backend so that each WebSocket session can use a
different transcription language without modifying the shared instance.
"""
import threading
class SessionASRProxy:
"""Wraps a shared ASR backend with a per-session language override.
The proxy delegates all attribute access to the wrapped ASR except
``transcribe()``, which temporarily overrides ``original_language``
on the shared ASR (under a lock) so the correct language is used.
Thread-safety: a per-ASR lock serializes ``transcribe()`` calls,
which is acceptable because model inference is typically GPU-bound
and cannot be parallelized anyway.
"""
def __init__(self, asr, language: str):
object.__setattr__(self, '_asr', asr)
object.__setattr__(self, '_session_language', None if language == "auto" else language)
# Attach a shared lock to the ASR instance (created once, reused by all proxies)
if not hasattr(asr, '_session_lock'):
asr._session_lock = threading.Lock()
object.__setattr__(self, '_lock', asr._session_lock)
def __getattr__(self, name):
return getattr(self._asr, name)
def transcribe(self, audio, init_prompt=""):
"""Call the backend's transcribe with the session's language."""
with self._lock:
saved = self._asr.original_language
self._asr.original_language = self._session_language
try:
return self._asr.transcribe(audio, init_prompt=init_prompt)
finally:
self._asr.original_language = saved
================================================
FILE: whisperlivekit/silero_vad_iterator.py
================================================
import warnings
from pathlib import Path
import numpy as np
import torch
"""
Code is adapted from silero-vad v6: https://github.com/snakers4/silero-vad
"""
def is_onnx_available() -> bool:
"""Check if onnxruntime is installed."""
try:
import onnxruntime
return True
except ImportError:
return False
def init_jit_model(model_path: str, device=torch.device('cpu')):
"""Load a JIT model from file."""
model = torch.jit.load(model_path, map_location=device)
model.eval()
return model
class OnnxSession():
"""
Shared ONNX session for Silero VAD model (stateless).
"""
def __init__(self, path, force_onnx_cpu=False):
import onnxruntime
opts = onnxruntime.SessionOptions()
opts.inter_op_num_threads = 1
opts.intra_op_num_threads = 1
if force_onnx_cpu and 'CPUExecutionProvider' in onnxruntime.get_available_providers():
self.session = onnxruntime.InferenceSession(path, providers=['CPUExecutionProvider'], sess_options=opts)
else:
self.session = onnxruntime.InferenceSession(path, sess_options=opts)
self.path = path
if '16k' in path:
warnings.warn('This model support only 16000 sampling rate!')
self.sample_rates = [16000]
else:
self.sample_rates = [8000, 16000]
class OnnxWrapper():
"""
ONNX Runtime wrapper for Silero VAD model with per-instance state.
"""
def __init__(self, session: OnnxSession, force_onnx_cpu=False):
self._shared_session = session
self.sample_rates = session.sample_rates
self.reset_states()
@property
def session(self):
return self._shared_session.session
def _validate_input(self, x, sr: int):
if x.dim() == 1:
x = x.unsqueeze(0)
if x.dim() > 2:
raise ValueError(f"Too many dimensions for input audio chunk {x.dim()}")
if sr != 16000 and (sr % 16000 == 0):
step = sr // 16000
x = x[:,::step]
sr = 16000
if sr not in self.sample_rates:
raise ValueError(f"Supported sampling rates: {self.sample_rates} (or multiply of 16000)")
if sr / x.shape[1] > 31.25:
raise ValueError("Input audio chunk is too short")
return x, sr
def reset_states(self, batch_size=1):
self._state = torch.zeros((2, batch_size, 128)).float()
self._context = torch.zeros(0)
self._last_sr = 0
self._last_batch_size = 0
def __call__(self, x, sr: int):
x, sr = self._validate_input(x, sr)
num_samples = 512 if sr == 16000 else 256
if x.shape[-1] != num_samples:
raise ValueError(f"Provided number of samples is {x.shape[-1]} (Supported values: 256 for 8000 sample rate, 512 for 16000)")
batch_size = x.shape[0]
context_size = 64 if sr == 16000 else 32
if not self._last_batch_size:
self.reset_states(batch_size)
if (self._last_sr) and (self._last_sr != sr):
self.reset_states(batch_size)
if (self._last_batch_size) and (self._last_batch_size != batch_size):
self.reset_states(batch_size)
if not len(self._context):
self._context = torch.zeros(batch_size, context_size)
x = torch.cat([self._context, x], dim=1)
if sr in [8000, 16000]:
ort_inputs = {'input': x.numpy(), 'state': self._state.numpy(), 'sr': np.array(sr, dtype='int64')}
ort_outs = self.session.run(None, ort_inputs)
out, state = ort_outs
self._state = torch.from_numpy(state)
else:
raise ValueError(f"Unsupported sampling rate {sr}. Supported: {self.sample_rates} (with sample sizes 256 for 8000, 512 for 16000)")
self._context = x[..., -context_size:]
self._last_sr = sr
self._last_batch_size = batch_size
out = torch.from_numpy(out)
return out
def _get_onnx_model_path(model_path: str = None, opset_version: int = 16) -> Path:
"""Get the path to the ONNX model file."""
available_ops = [15, 16]
if opset_version not in available_ops:
raise ValueError(f'Unsupported ONNX opset_version: {opset_version}. Available: {available_ops}')
if model_path is None:
current_dir = Path(__file__).parent
data_dir = current_dir / 'silero_vad_models'
if opset_version == 16:
model_name = 'silero_vad.onnx'
else:
model_name = f'silero_vad_16k_op{opset_version}.onnx'
model_path = data_dir / model_name
if not model_path.exists():
raise FileNotFoundError(
f"Model file not found: {model_path}\n"
f"Please ensure the whisperlivekit/silero_vad_models/ directory contains the model files."
)
else:
model_path = Path(model_path)
return model_path
def load_onnx_session(model_path: str = None, opset_version: int = 16, force_onnx_cpu: bool = True) -> OnnxSession:
"""
Load a shared ONNX session for Silero VAD.
"""
path = _get_onnx_model_path(model_path, opset_version)
return OnnxSession(str(path), force_onnx_cpu=force_onnx_cpu)
def load_jit_vad(model_path: str = None):
"""
Load Silero VAD model in JIT format.
"""
if model_path is None:
current_dir = Path(__file__).parent
data_dir = current_dir / 'silero_vad_models'
model_name = 'silero_vad.jit'
model_path = data_dir / model_name
if not model_path.exists():
raise FileNotFoundError(
f"Model file not found: {model_path}\n"
f"Please ensure the whisperlivekit/silero_vad_models/ directory contains the model files."
)
else:
model_path = Path(model_path)
model = init_jit_model(str(model_path))
return model
class VADIterator:
"""
Voice Activity Detection iterator for streaming audio.
This is the Silero VAD v6 implementation.
"""
def __init__(self,
model,
threshold: float = 0.5,
sampling_rate: int = 16000,
min_silence_duration_ms: int = 100,
speech_pad_ms: int = 30
):
"""
Class for stream imitation
Parameters
----------
model: preloaded .jit/.onnx silero VAD model
threshold: float (default - 0.5)
Speech threshold. Silero VAD outputs speech probabilities for each audio chunk, probabilities ABOVE this value are considered as SPEECH.
It is better to tune this parameter for each dataset separately, but "lazy" 0.5 is pretty good for most datasets.
sampling_rate: int (default - 16000)
Currently silero VAD models support 8000 and 16000 sample rates
min_silence_duration_ms: int (default - 100 milliseconds)
In the end of each speech chunk wait for min_silence_duration_ms before separating it
speech_pad_ms: int (default - 30 milliseconds)
Final speech chunks are padded by speech_pad_ms each side
"""
self.model = model
self.threshold = threshold
self.sampling_rate = sampling_rate
if sampling_rate not in [8000, 16000]:
raise ValueError('VADIterator does not support sampling rates other than [8000, 16000]')
self.min_silence_samples = sampling_rate * min_silence_duration_ms / 1000
self.speech_pad_samples = sampling_rate * speech_pad_ms / 1000
self.reset_states()
def reset_states(self):
self.model.reset_states()
self.triggered = False
self.temp_end = 0
self.current_sample = 0
@torch.no_grad()
def __call__(self, x, return_seconds=False, time_resolution: int = 1):
"""
x: torch.Tensor
audio chunk (see examples in repo)
return_seconds: bool (default - False)
whether return timestamps in seconds (default - samples)
time_resolution: int (default - 1)
time resolution of speech coordinates when requested as seconds
"""
if not torch.is_tensor(x):
try:
x = torch.Tensor(x)
except (ValueError, TypeError, RuntimeError) as exc:
raise TypeError("Audio cannot be cast to tensor. Cast it manually") from exc
window_size_samples = len(x[0]) if x.dim() == 2 else len(x)
self.current_sample += window_size_samples
speech_prob = self.model(x, self.sampling_rate).item()
if (speech_prob >= self.threshold) and self.temp_end:
self.temp_end = 0
if (speech_prob >= self.threshold) and not self.triggered:
self.triggered = True
speech_start = max(0, self.current_sample - self.speech_pad_samples - window_size_samples)
return {'start': int(speech_start) if not return_seconds else round(speech_start / self.sampling_rate, time_resolution)}
if (speech_prob < self.threshold - 0.15) and self.triggered:
if not self.temp_end:
self.temp_end = self.current_sample
if self.current_sample - self.temp_end < self.min_silence_samples:
return None
else:
speech_end = self.temp_end + self.speech_pad_samples - window_size_samples
self.temp_end = 0
self.triggered = False
return {'end': int(speech_end) if not return_seconds else round(speech_end / self.sampling_rate, time_resolution)}
return None
class FixedVADIterator(VADIterator):
"""
Fixed VAD Iterator that handles variable-length audio chunks, not only exactly 512 frames at once.
"""
def reset_states(self):
super().reset_states()
self.buffer = np.array([], dtype=np.float32)
def __call__(self, x, return_seconds=False):
self.buffer = np.append(self.buffer, x)
ret = None
while len(self.buffer) >= 512:
r = super().__call__(self.buffer[:512], return_seconds=return_seconds)
self.buffer = self.buffer[512:]
if ret is None:
ret = r
elif r is not None:
if "end" in r:
ret["end"] = r["end"]
if "start" in r:
ret["start"] = r["start"]
if "end" in ret:
del ret["end"]
return ret if ret != {} else None
if __name__ == "__main__":
# vad = FixedVADIterator(load_jit_vad())
vad = FixedVADIterator(OnnxWrapper(session=load_onnx_session()))
audio_buffer = np.array([0] * 512, dtype=np.float32)
result = vad(audio_buffer)
print(f" 512 samples: {result}")
# test with 511 samples
audio_buffer = np.array([0] * 511, dtype=np.float32)
result = vad(audio_buffer)
print(f" 511 samples: {result}")
================================================
FILE: whisperlivekit/silero_vad_models/__init__.py
================================================
================================================
FILE: whisperlivekit/simul_whisper/__init__.py
================================================
from .backend import SimulStreamingASR, SimulStreamingOnlineProcessor
__all__ = [
"SimulStreamingASR",
"SimulStreamingOnlineProcessor",
]
================================================
FILE: whisperlivekit/simul_whisper/align_att_base.py
================================================
"""Abstract base class for AlignAtt streaming decoders (PyTorch & MLX)."""
import logging
from abc import ABC, abstractmethod
from whisperlivekit.timed_objects import ASRToken
from whisperlivekit.whisper import DecodingOptions, tokenizer
from .config import AlignAttConfig
DEC_PAD = 50257
logger = logging.getLogger(__name__)
class AlignAttBase(ABC):
"""
Abstract base class for AlignAtt streaming decoders.
Provides shared logic for both PyTorch and MLX implementations:
- Properties (speaker, global_time_offset)
- Pure-Python methods (warmup, trim_context, refresh_segment, etc.)
- Template infer() with abstract hooks for tensor-specific operations
- Post-decode logic (token splitting, timestamped word building)
Subclasses must implement ~20 abstract methods for tensor-specific ops.
"""
# === Properties ===
@property
def speaker(self):
return self.state.speaker
@speaker.setter
def speaker(self, value):
self.state.speaker = value
@property
def global_time_offset(self):
return self.state.global_time_offset
@global_time_offset.setter
def global_time_offset(self, value):
self.state.global_time_offset = value
# === Constructor helpers ===
def _base_init(self, cfg: AlignAttConfig, model):
"""Common initialization — call from subclass __init__."""
self.model = model
self.cfg = cfg
self.decode_options = DecodingOptions(
language=cfg.language,
without_timestamps=True,
task=cfg.task,
)
self.tokenizer_is_multilingual = cfg.tokenizer_is_multilingual
self.max_text_len = model.dims.n_text_ctx
self.num_decoder_layers = len(model.decoder.blocks)
if cfg.max_context_tokens is None:
self.max_context_tokens = self.max_text_len
else:
self.max_context_tokens = cfg.max_context_tokens
def _init_state_common(self, cfg: AlignAttConfig):
"""Common state initialization — call from subclass _init_state."""
self.create_tokenizer(cfg.language if cfg.language != "auto" else None)
self.state.tokenizer = self.tokenizer
self.state.detected_language = cfg.language if cfg.language != "auto" else None
self.state.global_time_offset = 0.0
self.state.last_attend_frame = -cfg.rewind_threshold
self.state.speaker = -1
# === Shared concrete methods ===
def warmup(self, audio):
try:
self.insert_audio(audio)
self.infer(is_last=True)
self.refresh_segment(complete=True)
logger.info("Model warmed up successfully")
except Exception as e:
logger.exception(f"Model warmup failed: {e}")
def create_tokenizer(self, language=None):
self.tokenizer = tokenizer.get_tokenizer(
multilingual=self.tokenizer_is_multilingual,
language=language,
num_languages=self.model.num_languages,
task=self.decode_options.task,
)
self.state.tokenizer = self.tokenizer
def trim_context(self):
logger.info("Trimming context")
c = len(self.state.context.as_token_ids()) - len(self.state.context.prefix_token_ids)
logger.info(f"Context text: {self.state.context.as_text()}")
l = sum(t.shape[1] for t in self.state.tokens) + c
after = 0 if self.cfg.static_init_prompt is None else len(self.cfg.static_init_prompt)
while c > self.max_context_tokens or l > self.max_text_len - 20:
t = self.state.context.trim_words(after=after)
l -= t
c -= t
logger.debug(f"len {l}, c {c}, max_context_tokens {self.max_context_tokens}")
if t == 0:
break
logger.info(f"Context after trim: {self.state.context.text} (len: {l})")
def refresh_segment(self, complete=False):
logger.debug("Refreshing segment:")
self.init_tokens()
self.state.last_attend_frame = -self.cfg.rewind_threshold
self.state.cumulative_time_offset = 0.0
self.init_context()
logger.debug(f"Context: {self.state.context}")
if not complete and len(self.state.segments) > 2:
self.state.segments = self.state.segments[-2:]
else:
logger.debug("removing all segments.")
self.state.segments = []
self.state.log_segments += 1
self.state.pending_incomplete_tokens = []
self.state.pending_retries = 0
def segments_len(self):
return sum(s.shape[0] for s in self.state.segments) / 16000
def _apply_minseglen(self):
segments_len = self.segments_len()
if segments_len < self.cfg.audio_min_len:
logger.debug("waiting for next segment")
return False
return True
def _clean_cache(self):
self.state.clean_cache()
def debug_print_tokens(self, tokens):
for i in range(min(self.cfg.beam_size, tokens.shape[0])):
logger.debug(self.tokenizer.decode_with_timestamps(tokens[i].tolist()))
# === Language detection ===
def _detect_language_if_needed(self, encoder_feature):
if (
self.cfg.language == "auto"
and self.state.detected_language is None
and self.state.first_timestamp
):
seconds_since_start = self.segments_len() - self.state.first_timestamp
if seconds_since_start >= 2.0:
language_tokens, language_probs = self.lang_id(encoder_feature)
top_lan, p = max(language_probs[0].items(), key=lambda x: x[1])
logger.info(f"Detected language: {top_lan} with p={p:.4f}")
self.create_tokenizer(top_lan)
self.state.last_attend_frame = -self.cfg.rewind_threshold
self.state.cumulative_time_offset = 0.0
self.init_tokens()
self.init_context()
self.state.detected_language = top_lan
logger.info(f"Tokenizer language: {self.tokenizer.language}")
# === Template infer() ===
def infer(self, is_last=False):
"""Main inference — template method calling abstract hooks for tensor ops."""
new_segment = True
if len(self.state.segments) == 0:
logger.debug("No segments, nothing to do")
return []
if not self._apply_minseglen():
logger.debug(f"applied minseglen {self.cfg.audio_min_len} > {self.segments_len()}.")
return []
input_segments = self._concat_segments()
encoder_feature, content_mel_len = self._encode(input_segments)
self._evaluate(encoder_feature)
self._detect_language_if_needed(encoder_feature)
self.trim_context()
current_tokens = self._current_tokens()
fire_detected = self.fire_at_boundary(encoder_feature[:, :content_mel_len, :])
sum_logprobs = self._init_sum_logprobs()
completed = False
token_len_before = current_tokens.shape[1]
l_absolute_timestamps = []
accumulated_cross_attns = []
audio_duration_s = self.segments_len()
max_tokens = max(50, int(audio_duration_s * 15 * 1.5))
tokens_produced = 0
most_attended_frame = None
while not completed and current_tokens.shape[1] < self.max_text_len:
tokens_produced += 1
if tokens_produced > max_tokens:
logger.warning(
f"[Loop Detection] Too many tokens ({tokens_produced}) "
f"for {audio_duration_s:.2f}s audio. Breaking."
)
current_tokens = current_tokens[:, :token_len_before]
break
tokens_for_logits = current_tokens if new_segment else current_tokens[:, -1:]
logits, cross_attns = self._get_logits_and_cross_attn(
tokens_for_logits, encoder_feature
)
self._evaluate(logits)
accumulated_cross_attns.append(cross_attns)
if len(accumulated_cross_attns) > 16:
accumulated_cross_attns = accumulated_cross_attns[-16:]
if new_segment and self._check_no_speech(logits):
break
logits = logits[:, -1, :]
if new_segment:
logits = self._suppress_blank_tokens(logits)
new_segment = False
logits = self._apply_token_suppression(logits)
logits = self._apply_dry_penalty(logits, current_tokens)
current_tokens, completed = self._update_tokens(
current_tokens, logits, sum_logprobs
)
self._evaluate(current_tokens)
logger.debug(f"Decoding completed: {completed}")
self.debug_print_tokens(current_tokens)
attn = self._process_cross_attention(accumulated_cross_attns, content_mel_len)
frames_list, most_attended_frame = self._get_attended_frames(attn)
absolute_timestamps = [
(frame * 0.02 + self.state.cumulative_time_offset)
for frame in frames_list
]
l_absolute_timestamps.append(absolute_timestamps[0])
logger.debug(f"Absolute timestamps: {absolute_timestamps}")
if completed:
current_tokens = current_tokens[:, :-1]
break
# Rewind check
if (
not is_last
and self.state.last_attend_frame - most_attended_frame
> self.cfg.rewind_threshold
):
if current_tokens.shape[1] > 1 and self._is_special_token(current_tokens):
logger.debug("omit rewinding from special tokens")
self.state.last_attend_frame = most_attended_frame
else:
logger.debug(
f"[rewind detected] current: {most_attended_frame}, "
f"last: {self.state.last_attend_frame}"
)
self.state.last_attend_frame = -self.cfg.rewind_threshold
current_tokens = self._rewind_tokens()
break
else:
self.state.last_attend_frame = most_attended_frame
if content_mel_len - most_attended_frame <= (
4 if is_last else self.cfg.frame_threshold
):
logger.debug(
f"attention reaches the end: {most_attended_frame}/{content_mel_len}"
)
current_tokens = current_tokens[:, :-1]
break
# Post-decode: split tokens and build timestamped words
tokens_to_split = self._tokens_to_list(current_tokens, token_len_before)
if self.state.pending_incomplete_tokens:
logger.debug(
f"[UTF-8 Fix] Prepending {len(self.state.pending_incomplete_tokens)} "
f"pending tokens: {self.state.pending_incomplete_tokens}"
)
tokens_to_split = self.state.pending_incomplete_tokens + tokens_to_split
new_hypothesis, split_words, split_tokens = self._split_tokens(
tokens_to_split, fire_detected, is_last
)
new_tokens_tensor = self._make_new_tokens_tensor(new_hypothesis)
self.state.tokens.append(new_tokens_tensor)
logger.info(f"Output: {self.tokenizer.decode(new_hypothesis)}")
self._clean_cache()
if len(l_absolute_timestamps) >= 2 and self.state.first_timestamp is None:
self.state.first_timestamp = l_absolute_timestamps[0]
timestamped_words = self._build_timestamped_words(
split_words, split_tokens, l_absolute_timestamps
)
self._handle_pending_tokens(split_words, split_tokens)
return timestamped_words
# === Post-decode shared helpers ===
def _split_tokens(self, tokens_list, fire_detected, is_last):
"""Split token list into words. Returns (hypothesis, split_words, split_tokens)."""
if fire_detected or is_last:
new_hypothesis = tokens_list
split_words, split_tokens = self.tokenizer.split_to_word_tokens(new_hypothesis)
else:
split_words, split_tokens = self.tokenizer.split_to_word_tokens(tokens_list)
if len(split_words) > 1:
new_hypothesis = [i for sublist in split_tokens[:-1] for i in sublist]
else:
new_hypothesis = []
return new_hypothesis, split_words, split_tokens
def _build_timestamped_words(self, split_words, split_tokens, l_absolute_timestamps):
"""Build list of timestamped ASRToken from split words."""
timestamped_words = []
timestamp_idx = 0
replacement_char = "\ufffd"
for word, word_tokens in zip(split_words, split_tokens):
if replacement_char in word:
cleaned = word.replace(replacement_char, "")
if not cleaned.strip():
logger.debug(f"[UTF-8 Filter] Skipping: {repr(word)}")
timestamp_idx += len(word_tokens)
continue
logger.debug(f"[UTF-8 Filter] Cleaned {repr(word)} -> {repr(cleaned)}")
word = cleaned
try:
current_timestamp = l_absolute_timestamps[timestamp_idx]
except IndexError:
logger.warning(
f"Timestamp index {timestamp_idx} out of range, using last timestamp"
)
current_timestamp = (
l_absolute_timestamps[-1] if l_absolute_timestamps else 0.0
)
timestamp_idx += len(word_tokens)
timestamp_entry = ASRToken(
start=round(current_timestamp, 2),
end=round(current_timestamp + 0.1, 2),
text=word,
speaker=self.state.speaker,
detected_language=self.state.detected_language,
).with_offset(self.state.global_time_offset)
timestamped_words.append(timestamp_entry)
return timestamped_words
def _handle_pending_tokens(self, split_words, split_tokens):
"""Handle incomplete UTF-8 tokens for next chunk."""
MAX_PENDING_TOKENS = 10
MAX_PENDING_RETRIES = 2
replacement_char = "\ufffd"
if split_words and replacement_char in split_words[-1]:
self.state.pending_retries += 1
if self.state.pending_retries > MAX_PENDING_RETRIES:
logger.warning(
f"[UTF-8 Fix] Dropping {len(split_tokens[-1])} incomplete tokens "
f"after {MAX_PENDING_RETRIES} retries (won't resolve)"
)
self.state.pending_incomplete_tokens = []
self.state.pending_retries = 0
elif len(split_tokens[-1]) <= MAX_PENDING_TOKENS:
self.state.pending_incomplete_tokens = split_tokens[-1]
logger.debug(
f"[UTF-8 Fix] Holding {len(self.state.pending_incomplete_tokens)} "
f"incomplete tokens for next chunk (retry {self.state.pending_retries})"
)
else:
logger.warning(
f"[UTF-8 Fix] Skipping {len(split_tokens[-1])} tokens "
f"(exceeds limit of {MAX_PENDING_TOKENS}, likely hallucination)"
)
self.state.pending_incomplete_tokens = []
self.state.pending_retries = 0
else:
self.state.pending_incomplete_tokens = []
self.state.pending_retries = 0
# === Repetition penalty ===
def _apply_dry_penalty(self, logits, current_tokens):
"""DRY penalty v0: penalize tokens that would extend a verbatim repetition.
See https://github.com/oobabooga/text-generation-webui/pull/5677
Scans the decoded sequence for positions where the current suffix already
appeared --> for each such match, the token that followed it in the past is
penalised exponentially with the match length
"""
eot = self.tokenizer.eot
seq = current_tokens[0].tolist()
if len(seq) < 5:
return logits
last = seq[-1]
if last >= eot:
return logits
penalties = {}
for i in range(len(seq) - 2, -1, -1):
if seq[i] != last:
continue
next_tok = seq[i + 1]
if next_tok >= eot:
continue
length = 1
while length < 50:
j, k = i - length, len(seq) - 1 - length
if j < 0 or k <= i:
break
if seq[j] != seq[k] or seq[j] >= eot:
break
length += 1
if next_tok not in penalties or length > penalties[next_tok]:
penalties[next_tok] = length
if penalties:
max_len = max(penalties.values())
if max_len >= 4:
logger.debug(f"[DRY] penalising {len(penalties)} tokens (longest match: {max_len})")
for tok, length in penalties.items():
if length >= 2:
logits[:, tok] = logits[:, tok] - 1.0 * 2.0 ** (length - 2)
return logits
# === Abstract methods — subclass must implement ===
@abstractmethod
def _init_state(self, cfg: AlignAttConfig):
"""Initialize per-session decoder state."""
...
@abstractmethod
def init_tokens(self):
"""Initialize token sequence with framework-specific tensors."""
...
@abstractmethod
def init_context(self):
"""Initialize context buffer with framework-specific TokenBuffer."""
...
@abstractmethod
def insert_audio(self, segment=None):
"""Insert audio segment into buffer."""
...
@abstractmethod
def _current_tokens(self):
"""Build current token tensor for decoding."""
...
@abstractmethod
def fire_at_boundary(self, feature):
"""Check if we should fire at word boundary."""
...
@abstractmethod
def lang_id(self, encoder_features):
"""Language detection from encoder features. Returns (tokens, probs)."""
...
@abstractmethod
def _concat_segments(self):
"""Concatenate audio segments into single array/tensor."""
...
@abstractmethod
def _encode(self, input_segments):
"""Encode audio. Returns (encoder_feature, content_mel_len)."""
...
@abstractmethod
def _init_sum_logprobs(self):
"""Create zero sum_logprobs tensor for beam search."""
...
@abstractmethod
def _get_logits_and_cross_attn(self, tokens, encoder_feature):
"""Get logits and cross-attention from decoder. Returns (logits, cross_attns)."""
...
@abstractmethod
def _check_no_speech(self, logits):
"""Check no_speech probability at start of segment. Returns True to break."""
...
@abstractmethod
def _suppress_blank_tokens(self, logits):
"""Suppress blank/EOT tokens at segment start. Returns modified logits."""
...
@abstractmethod
def _apply_token_suppression(self, logits):
"""Apply general token suppression. Returns modified logits."""
...
@abstractmethod
def _update_tokens(self, current_tokens, logits, sum_logprobs):
"""Update tokens via decoder. Returns (current_tokens, completed)."""
...
@abstractmethod
def _process_cross_attention(self, accumulated_cross_attns, content_mel_len):
"""Process cross-attention for alignment. Returns attention tensor."""
...
@abstractmethod
def _get_attended_frames(self, attn):
"""Get most attended frames. Returns (frames_as_python_list, first_frame_int)."""
...
@abstractmethod
def _is_special_token(self, current_tokens):
"""Check if second-to-last token is a special token (>= DEC_PAD)."""
...
@abstractmethod
def _rewind_tokens(self):
"""Concatenate state tokens for rewind. Returns token tensor."""
...
@abstractmethod
def _tokens_to_list(self, current_tokens, start_col):
"""Extract tokens as Python list from start_col onwards."""
...
@abstractmethod
def _make_new_tokens_tensor(self, hypothesis):
"""Create tensor from hypothesis token list, repeated for beam search."""
...
@abstractmethod
def _evaluate(self, tensor):
"""Evaluate lazy tensor (mx.eval for MLX, no-op for PyTorch)."""
...
================================================
FILE: whisperlivekit/simul_whisper/backend.py
================================================
import gc
import logging
import platform
import sys
from typing import List, Tuple
import numpy as np
import torch
from whisperlivekit.backend_support import faster_backend_available, mlx_backend_available
from whisperlivekit.model_paths import detect_model_format, resolve_model_path
from whisperlivekit.simul_whisper.config import AlignAttConfig
from whisperlivekit.simul_whisper.simul_whisper import AlignAtt
from whisperlivekit.timed_objects import ASRToken, ChangeSpeaker, Transcript
from whisperlivekit.warmup import load_file
from whisperlivekit.whisper import load_model, tokenizer
logger = logging.getLogger(__name__)
HAS_MLX_WHISPER = mlx_backend_available(warn_on_missing=True)
if HAS_MLX_WHISPER:
from .mlx import MLXAlignAtt
from .mlx_encoder import load_mlx_encoder, load_mlx_model, mlx_model_mapping
else:
mlx_model_mapping = {}
MLXAlignAtt = None
HAS_FASTER_WHISPER = faster_backend_available(warn_on_missing=not HAS_MLX_WHISPER)
if HAS_FASTER_WHISPER:
from faster_whisper import WhisperModel
else:
WhisperModel = None
MIN_DURATION_REAL_SILENCE = 5
class SimulStreamingOnlineProcessor:
"""Online processor for SimulStreaming ASR."""
SAMPLING_RATE = 16000
def __init__(self, asr, logfile=sys.stderr):
self.asr = asr
self.logfile = logfile
self.end = 0.0
self.buffer = []
self.model = self._create_alignatt()
if asr.tokenizer:
self.model.tokenizer = asr.tokenizer
self.model.state.tokenizer = asr.tokenizer
def _create_alignatt(self):
"""Create the AlignAtt decoder instance based on ASR mode."""
if self.asr.use_full_mlx and HAS_MLX_WHISPER:
return MLXAlignAtt(cfg=self.asr.cfg, mlx_model=self.asr.mlx_model)
else:
return AlignAtt(
cfg=self.asr.cfg,
loaded_model=self.asr.shared_model,
mlx_encoder=self.asr.mlx_encoder,
fw_encoder=self.asr.fw_encoder,
)
def start_silence(self):
tokens, processed_upto = self.process_iter(is_last=True)
return tokens, processed_upto
def end_silence(self, silence_duration, offset):
"""Handle silence period."""
self.end += silence_duration
long_silence = silence_duration >= MIN_DURATION_REAL_SILENCE
if not long_silence:
gap_len = int(16000 * silence_duration)
if gap_len > 0:
if self.asr.use_full_mlx:
gap_silence = np.zeros(gap_len, dtype=np.float32)
else:
gap_silence = torch.zeros(gap_len)
self.model.insert_audio(gap_silence)
if long_silence:
self.model.refresh_segment(complete=True)
self.model.global_time_offset = silence_duration + offset
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time):
"""Append an audio chunk to be processed by SimulStreaming."""
self.end = audio_stream_end_time
if self.asr.use_full_mlx:
self.model.insert_audio(audio)
else:
audio_tensor = torch.from_numpy(audio).float()
self.model.insert_audio(audio_tensor)
def new_speaker(self, change_speaker: ChangeSpeaker):
"""Handle speaker change event."""
self.process_iter(is_last=True)
self.model.refresh_segment(complete=True)
self.model.speaker = change_speaker.speaker
self.model.global_time_offset = change_speaker.start
def get_buffer(self):
concat_buffer = Transcript.from_tokens(tokens= self.buffer, sep='')
return concat_buffer
def process_iter(self, is_last=False) -> Tuple[List[ASRToken], float]:
"""
Process accumulated audio chunks using SimulStreaming.
Returns a tuple: (list of committed ASRToken objects, float representing the audio processed up to time).
"""
try:
timestamped_words = self.model.infer(is_last=is_last)
if not timestamped_words:
return [], self.end
if self.model.cfg.language == "auto" and timestamped_words[0].detected_language is None:
self.buffer.extend(timestamped_words)
return [], self.end
self.buffer = []
return timestamped_words, self.end
except Exception as e:
logger.exception(f"SimulStreaming processing error: {e}")
return [], self.end
def warmup(self, audio, init_prompt=""):
"""Warmup the SimulStreaming model."""
try:
if self.asr.use_full_mlx:
# MLX mode: ensure numpy array
if hasattr(audio, 'numpy'):
audio = audio.numpy()
self.model.insert_audio(audio)
self.model.infer(True)
self.model.refresh_segment(complete=True)
logger.info("SimulStreaming model warmed up successfully")
except Exception as e:
logger.exception(f"SimulStreaming warmup failed: {e}")
def __del__(self):
gc.collect()
if not getattr(self.asr, 'use_full_mlx', True) and torch is not None:
try:
torch.cuda.empty_cache()
except Exception:
pass
class SimulStreamingASR:
"""SimulStreaming backend with AlignAtt policy."""
sep = ""
def __init__(self, logfile=sys.stderr, **kwargs):
self.logfile = logfile
self.transcribe_kargs = {}
for key, value in kwargs.items():
setattr(self, key, value)
if self.decoder_type is None:
self.decoder_type = 'greedy' if self.beams == 1 else 'beam'
self.fast_encoder = False
self._resolved_model_path = None
self.encoder_backend = "whisper"
self.use_full_mlx = getattr(self, "use_full_mlx", False)
preferred_backend = getattr(self, "backend", "auto")
compatible_whisper_mlx, compatible_faster_whisper = True, True
if self.model_path:
resolved_model_path = resolve_model_path(self.model_path)
self._resolved_model_path = resolved_model_path
self.model_path = str(resolved_model_path)
model_info = detect_model_format(resolved_model_path)
compatible_whisper_mlx = model_info.compatible_whisper_mlx
compatible_faster_whisper = model_info.compatible_faster_whisper
if not self.use_full_mlx and not model_info.has_pytorch:
raise FileNotFoundError(
f"No PyTorch checkpoint (.pt/.bin/.safetensors) found under {self.model_path}"
)
self.model_name = resolved_model_path.name if resolved_model_path.is_dir() else resolved_model_path.stem
elif self.model_size is not None:
self.model_name = self.model_size
else:
raise ValueError("Either model_size or model_path must be specified for SimulStreaming.")
is_multilingual = not self.model_name.endswith(".en")
self.encoder_backend = self._resolve_encoder_backend(
preferred_backend,
compatible_whisper_mlx,
compatible_faster_whisper,
)
self.fast_encoder = self.encoder_backend in ("mlx-whisper", "faster-whisper")
if self.encoder_backend == "whisper":
self.disable_fast_encoder = True
# MLX full decoder disabled by default — MLXAlignAtt has known issues
# with token generation after punctuation. Users can opt-in with
# --use-full-mlx if they want to test it.
# if self.encoder_backend == "mlx-whisper" and platform.system() == "Darwin":
# if not hasattr(self, '_full_mlx_disabled'):
# self.use_full_mlx = True
self.cfg = AlignAttConfig(
tokenizer_is_multilingual= is_multilingual,
segment_length=self.min_chunk_size,
frame_threshold=self.frame_threshold,
language=self.lan,
audio_max_len=self.audio_max_len,
audio_min_len=self.audio_min_len,
cif_ckpt_path=self.cif_ckpt_path,
decoder_type="beam",
beam_size=self.beams,
task="translate" if self.direct_english_translation else "transcribe",
never_fire=self.never_fire,
init_prompt=self.init_prompt,
max_context_tokens=self.max_context_tokens,
static_init_prompt=self.static_init_prompt,
)
# Set up tokenizer for translation if needed
if self.direct_english_translation:
self.tokenizer = self.set_translate_task()
else:
self.tokenizer = None
self.mlx_encoder, self.fw_encoder, self.mlx_model = None, None, None
self.shared_model = None
if self.use_full_mlx and HAS_MLX_WHISPER:
logger.info('MLX Whisper backend used.')
if self._resolved_model_path is not None:
mlx_model_path = str(self._resolved_model_path)
else:
mlx_model_path = mlx_model_mapping.get(self.model_name)
if not mlx_model_path:
raise FileNotFoundError(
f"MLX Whisper backend requested but no compatible weights found for model '{self.model_name}'."
)
self.mlx_model = load_mlx_model(path_or_hf_repo=mlx_model_path)
self._warmup_mlx_model()
elif self.encoder_backend == "mlx-whisper":
# hybrid mode: mlx encoder + pytorch decoder
logger.info('SimulStreaming will use MLX Whisper encoder with PyTorch decoder.')
if self._resolved_model_path is not None:
mlx_model_path = str(self._resolved_model_path)
else:
mlx_model_path = mlx_model_mapping.get(self.model_name)
if not mlx_model_path:
raise FileNotFoundError(
f"MLX Whisper backend requested but no compatible weights found for model '{self.model_name}'."
)
self.mlx_encoder = load_mlx_encoder(path_or_hf_repo=mlx_model_path)
self.shared_model = self.load_model()
elif self.encoder_backend == "faster-whisper":
logger.info('SimulStreaming will use Faster Whisper for the encoder.')
if self._resolved_model_path is not None:
fw_model = str(self._resolved_model_path)
else:
fw_model = self.model_name
self.fw_encoder = WhisperModel(
fw_model,
device='auto',
compute_type='auto',
)
self.shared_model = self.load_model()
else:
self.shared_model = self.load_model()
def _warmup_mlx_model(self):
"""Warmup the full MLX model."""
warmup_audio = load_file(self.warmup_file)
if warmup_audio is not None:
temp_model = MLXAlignAtt(
cfg=self.cfg,
mlx_model=self.mlx_model,
)
temp_model.warmup(warmup_audio)
logger.info("Full MLX model warmed up successfully")
def _resolve_encoder_backend(self, preferred_backend, compatible_whisper_mlx, compatible_faster_whisper):
choice = preferred_backend or "auto"
if self.disable_fast_encoder:
return "whisper"
if choice == "whisper":
return "whisper"
if choice == "mlx-whisper":
if not self._can_use_mlx(compatible_whisper_mlx):
raise RuntimeError("mlx-whisper backend requested but MLX Whisper is unavailable or incompatible with the provided model.")
return "mlx-whisper"
if choice == "faster-whisper":
if not self._can_use_faster(compatible_faster_whisper):
raise RuntimeError("faster-whisper backend requested but Faster-Whisper is unavailable or incompatible with the provided model.")
return "faster-whisper"
if choice == "openai-api":
raise ValueError("openai-api backend is only supported with the LocalAgreement policy.")
# auto mode
if platform.system() == "Darwin" and self._can_use_mlx(compatible_whisper_mlx):
return "mlx-whisper"
if self._can_use_faster(compatible_faster_whisper):
return "faster-whisper"
return "whisper"
def _has_custom_model_path(self):
return self._resolved_model_path is not None
def _can_use_mlx(self, compatible_whisper_mlx):
if not HAS_MLX_WHISPER:
return False
if self._has_custom_model_path():
return compatible_whisper_mlx
return self.model_name in mlx_model_mapping
def _can_use_faster(self, compatible_faster_whisper):
if not HAS_FASTER_WHISPER:
return False
if self._has_custom_model_path():
return compatible_faster_whisper
return True
def load_model(self):
model_ref = str(self._resolved_model_path) if self._resolved_model_path else self.model_name
lora_path = getattr(self, 'lora_path', None)
whisper_model = load_model(
name=model_ref,
download_root=getattr(self, 'model_cache_dir', None),
decoder_only=self.fast_encoder,
custom_alignment_heads=self.custom_alignment_heads,
lora_path=lora_path,
)
warmup_audio = load_file(self.warmup_file)
if warmup_audio is not None:
warmup_audio = torch.from_numpy(warmup_audio).float()
if self.fast_encoder:
temp_model = AlignAtt(
cfg=self.cfg,
loaded_model=whisper_model,
mlx_encoder=self.mlx_encoder,
fw_encoder=self.fw_encoder,
)
temp_model.warmup(warmup_audio)
else:
whisper_model.transcribe(warmup_audio, language=self.lan if self.lan != 'auto' else None)
return whisper_model
def set_translate_task(self):
"""Set up translation task."""
if self.cfg.language == 'auto':
raise ValueError('Translation cannot be done with language = auto')
return tokenizer.get_tokenizer(
multilingual=True,
language=self.cfg.language,
num_languages=99,
task="translate"
)
def transcribe(self, audio):
"""
Warmup is done directly in load_model
"""
pass
================================================
FILE: whisperlivekit/simul_whisper/beam.py
================================================
from torch import Tensor
from whisperlivekit.whisper.decoding import PyTorchInference
class BeamPyTorchInference(PyTorchInference):
"""Extension of PyTorchInference for beam search with cross-attention support."""
def _kv_cache_ids(self):
"""Get cache_id strings for self-attention key/value modules."""
key_ids = [block.attn.key_cache_id for block in self.model.decoder.blocks]
value_ids = [block.attn.value_cache_id for block in self.model.decoder.blocks]
return key_ids + value_ids
def rearrange_kv_cache(self, source_indices):
if source_indices != list(range(len(source_indices))):
for cache_id in self._kv_cache_ids():
if cache_id in self.kv_cache:
self.kv_cache[cache_id] = self.kv_cache[cache_id][source_indices].detach()
def logits(
self,
tokens: Tensor,
audio_features: Tensor,
return_cross_attn: bool = False,
):
"""Get logits, optionally returning cross-attention weights."""
return self.model.decoder(
tokens, audio_features,
kv_cache=self.kv_cache,
return_cross_attn=return_cross_attn,
)
================================================
FILE: whisperlivekit/simul_whisper/config.py
================================================
from dataclasses import dataclass, field
from typing import Literal
@dataclass
class AlignAttConfig():
eval_data_path: str = "tmp"
segment_length: float = field(default=1.0, metadata = {"help": "in second"})
frame_threshold: int = 4
rewind_threshold: int = 200
audio_max_len: float = 20.0
cif_ckpt_path: str = ""
never_fire: bool = False
language: str = field(default="zh")
nonspeech_prob: float = 0.5
audio_min_len: float = 1.0
decoder_type: Literal["greedy","beam"] = "greedy"
beam_size: int = 5
task: Literal["transcribe","translate"] = "transcribe"
tokenizer_is_multilingual: bool = False
init_prompt: str = field(default=None)
static_init_prompt: str = field(default=None)
max_context_tokens: int = field(default=None)
================================================
FILE: whisperlivekit/simul_whisper/decoder_state.py
================================================
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
import torch
@dataclass
class DecoderState:
kv_cache: Dict[str, torch.Tensor] = field(default_factory=dict)
tokenizer: Any = None
detected_language: Optional[str] = None
reset_tokenizer_to_auto_next_call: bool = False
tokens: List[torch.Tensor] = field(default_factory=list)
initial_tokens: Optional[torch.Tensor] = None
initial_token_length: int = 0
sot_index: int = 0
align_source: Dict[int, List[Tuple[int, int]]] = field(default_factory=dict)
num_align_heads: int = 0
segments: List[torch.Tensor] = field(default_factory=list)
context: Any = None
pending_incomplete_tokens: List[int] = field(default_factory=list)
pending_retries: int = 0
global_time_offset: float = 0.0
cumulative_time_offset: float = 0.0
first_timestamp: Optional[float] = None
last_attend_frame: int = 0
speaker: int = -1
log_segments: int = 0
CIFLinear: Optional[torch.nn.Module] = None
always_fire: bool = False
never_fire: bool = False
suppress_tokens_fn: Any = None
token_decoder: Any = None
decoder_type: str = "greedy"
inference: Any = None
def clean_cache(self):
"""Clean the kv_cache after each inference step."""
# Explicitly delete tensor references to free GPU memory
if self.kv_cache:
for key in list(self.kv_cache.keys()):
tensor = self.kv_cache.pop(key, None)
if tensor is not None:
del tensor
# Clear the dict
self.kv_cache.clear()
# Force GPU cache cleanup (only if CUDA is available)
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
if self.decoder_type == "beam" and self.inference is not None:
# Create NEW dict instead of sharing reference
self.inference.kv_cache = {}
if self.token_decoder is not None:
self.token_decoder.reset()
def reset(self, rewind_threshold: int = 200):
"""
Reset transient state for a new segment.
Args:
rewind_threshold: Value for resetting last_attend_frame
"""
self.last_attend_frame = -rewind_threshold
self.cumulative_time_offset = 0.0
self.pending_incomplete_tokens = []
self.pending_retries = 0
self.log_segments += 1
def full_reset(self, rewind_threshold: int = 200):
"""
Full reset including audio segments and tokens.
Args:
rewind_threshold: Value for resetting last_attend_frame
"""
self.reset(rewind_threshold)
self.segments = []
self.tokens = []
self.kv_cache = {}
self.first_timestamp = None
================================================
FILE: whisperlivekit/simul_whisper/eow_detection.py
================================================
import torch
# code for the end-of-word detection based on the CIF model proposed in Simul-Whisper
def load_cif(cfg, n_audio_state, device):
"""cfg: AlignAttConfig, n_audio_state: int, device: torch.device"""
cif_linear = torch.nn.Linear(n_audio_state, 1)
if cfg.cif_ckpt_path is None or not cfg.cif_ckpt_path:
if cfg.never_fire:
never_fire = True
always_fire = False
else:
always_fire = True
never_fire = False
else:
always_fire = False
never_fire = cfg.never_fire
checkpoint = torch.load(cfg.cif_ckpt_path)
cif_linear.load_state_dict(checkpoint)
cif_linear.to(device)
return cif_linear, always_fire, never_fire
# from https://github.com/dqqcasia/mosst/blob/master/fairseq/models/speech_to_text/convtransformer_wav2vec_cif.py
def resize(alphas, target_lengths, threshold=0.999):
"""
alpha in thresh=1.0 | (0.0, +0.21)
target_lengths: if None, apply round and resize, else apply scaling
"""
# sum
_num = alphas.sum(-1)
num = target_lengths.float()
# scaling
_alphas = alphas * (num / _num)[:, None].repeat(1, alphas.size(1))
# rm attention value that exceeds threashold
count = 0
while len(torch.where(_alphas > threshold)[0]):
count += 1
if count > 10:
break
xs, ys = torch.where(_alphas > threshold)
for x, y in zip(xs, ys):
if _alphas[x][y] >= threshold:
mask = _alphas[x].ne(0).float()
mean = 0.5 * _alphas[x].sum() / mask.sum()
_alphas[x] = _alphas[x] * 0.5 + mean * mask
return _alphas, _num
def fire_at_boundary(chunked_encoder_feature: torch.Tensor, cif_linear):
content_mel_len = chunked_encoder_feature.shape[1] # B, T, D
alphas = cif_linear(chunked_encoder_feature).squeeze(dim=2) # B, T
alphas = torch.sigmoid(alphas)
decode_length = torch.round(alphas.sum(-1)).int()
alphas, _ = resize(alphas, decode_length)
alphas = alphas.squeeze(0) # (T, )
threshold = 0.999
integrate = torch.cumsum(alphas[:-1], dim=0) # ignore the peak value at the end of the content chunk
exceed_count = integrate[-1] // threshold
integrate = integrate - exceed_count*1.0 # minus 1 every time intergrate exceed the threshold
important_positions = (integrate >= 0).nonzero(as_tuple=True)[0]
if important_positions.numel() == 0:
return False
else:
return important_positions[0] >= content_mel_len-2
================================================
FILE: whisperlivekit/simul_whisper/mlx/__init__.py
================================================
from .decoder_state import MLXDecoderState
from .decoders import MLXBeamSearchDecoder, MLXGreedyDecoder, MLXInference
from .simul_whisper import MLXAlignAtt
__all__ = [
"MLXAlignAtt",
"MLXBeamSearchDecoder",
"MLXDecoderState",
"MLXGreedyDecoder",
"MLXInference",
]
================================================
FILE: whisperlivekit/simul_whisper/mlx/decoder_state.py
================================================
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
import mlx.core as mx
import numpy as np
@dataclass
class MLXDecoderState:
"""
mlx kv cache format: List of ((k, v), (cross_k, cross_v)) tuples per layer,
where each element is a tuple of mx.arrays.
"""
kv_cache: Optional[List[Tuple[Tuple[mx.array, mx.array], Tuple[mx.array, mx.array]]]] = None
tokenizer: Any = None
detected_language: Optional[str] = None
reset_tokenizer_to_auto_next_call: bool = False
tokens: List[mx.array] = field(default_factory=list)
initial_tokens: Optional[mx.array] = None
initial_token_length: int = 0
sot_index: int = 0
align_source: Dict[int, List[Tuple[int, int]]] = field(default_factory=dict)
num_align_heads: int = 0
segments: List[np.ndarray] = field(default_factory=list)
context: Any = None
pending_incomplete_tokens: List[int] = field(default_factory=list)
pending_retries: int = 0
global_time_offset: float = 0.0
cumulative_time_offset: float = 0.0
first_timestamp: Optional[float] = None
last_attend_frame: int = 0
speaker: int = -1
log_segments: int = 0
cif_weights: Optional[mx.array] = None
always_fire: bool = False
never_fire: bool = False
suppress_tokens: Optional[Tuple[int, ...]] = None
token_decoder: Any = None
decoder_type: str = "greedy"
inference: Any = None
def clean_cache(self):
self.kv_cache = None
if self.decoder_type == "beam" and self.inference is not None:
self.inference.kv_cache = None
if self.token_decoder is not None:
self.token_decoder.reset()
def reset(self, rewind_threshold: int = 200):
self.last_attend_frame = -rewind_threshold
self.cumulative_time_offset = 0.0
self.pending_incomplete_tokens = []
self.pending_retries = 0
self.log_segments += 1
def full_reset(self, rewind_threshold: int = 200):
"""
Full reset including audio segments and tokens.
Args:
rewind_threshold: Value for resetting last_attend_frame
"""
self.reset(rewind_threshold)
self.segments = []
self.tokens = []
self.kv_cache = None
self.first_timestamp = None
================================================
FILE: whisperlivekit/simul_whisper/mlx/decoders.py
================================================
"""
MLX-native token decoders for streaming ASR.
"""
from typing import Any, Dict, List, Optional, Tuple
import mlx.core as mx
import numpy as np
class MLXGreedyDecoder:
"""Greedy decoder using MLX operations."""
def __init__(self, temperature: float, eot: int):
self.temperature = temperature
self.eot = eot
def update(
self, tokens: mx.array, logits: mx.array, sum_logprobs: mx.array
) -> Tuple[mx.array, bool]:
"""
Update tokens with next predicted token.
Args:
tokens: Current token sequence, shape (batch, seq_len)
logits: Logits for next token, shape (batch, vocab_size)
sum_logprobs: Cumulative log probabilities, shape (batch,)
Returns:
Updated tokens and completion flag
"""
if self.temperature == 0:
next_tokens = mx.argmax(logits, axis=-1)
else:
probs = mx.softmax(logits / self.temperature, axis=-1)
next_tokens = mx.random.categorical(mx.log(probs + 1e-10))
logprobs = mx.softmax(logits, axis=-1)
logprobs = mx.log(logprobs + 1e-10)
batch_size = logprobs.shape[0]
current_logprobs = logprobs[mx.arange(batch_size), next_tokens]
mask = (tokens[:, -1] != self.eot).astype(mx.float32)
sum_logprobs = sum_logprobs + current_logprobs * mask
eot_mask = (tokens[:, -1] == self.eot)
next_tokens = mx.where(eot_mask, mx.array(self.eot), next_tokens)
tokens = mx.concatenate([tokens, next_tokens[:, None]], axis=1)
completed = bool(mx.all(tokens[:, -1] == self.eot))
return tokens, completed
def finalize(self, tokens: mx.array, sum_logprobs: mx.array):
"""Finalize decoding by ensuring EOT at end."""
eot_column = mx.full((tokens.shape[0], 1), self.eot, dtype=tokens.dtype)
tokens = mx.concatenate([tokens, eot_column], axis=1)
return tokens, sum_logprobs.tolist()
class MLXBeamSearchDecoder:
"""Beam search decoder using MLX operations."""
def __init__(
self,
beam_size: int,
eot: int,
inference: Any,
patience: Optional[float] = None,
):
self.beam_size = beam_size
self.eot = eot
self.inference = inference
self.patience = patience or 1.0
self.max_candidates: int = round(beam_size * self.patience)
self.finished_sequences: Optional[List[Dict]] = None
assert (
self.max_candidates > 0
), f"Invalid beam size ({beam_size}) or patience ({patience})"
def reset(self):
"""Reset finished sequences for new segment."""
self.finished_sequences = None
def update(
self, tokens: mx.array, logits: mx.array, sum_logprobs: mx.array
) -> Tuple[mx.array, bool]:
"""
Update tokens using beam search.
Args:
tokens: Current token sequences, shape (batch * beam_size, seq_len)
logits: Logits for next token, shape (batch * beam_size, vocab_size)
sum_logprobs: Cumulative log probabilities, shape (batch * beam_size,)
Returns:
Updated tokens and completion flag
"""
if tokens.shape[0] % self.beam_size != 0:
raise ValueError(f"{tokens.shape}[0] % {self.beam_size} != 0")
n_audio = tokens.shape[0] // self.beam_size
if self.finished_sequences is None:
self.finished_sequences = [{} for _ in range(n_audio)]
logprobs = mx.softmax(logits, axis=-1)
logprobs = mx.log(logprobs + 1e-10)
logprobs_np = np.array(logprobs)
tokens_np = np.array(tokens)
sum_logprobs_np = np.array(sum_logprobs)
next_tokens, source_indices, finished_sequences = [], [], []
new_sum_logprobs = []
for i in range(n_audio):
scores, sources, finished = {}, {}, {}
for j in range(self.beam_size):
idx = i * self.beam_size + j
prefix = tokens_np[idx].tolist()
top_k_indices = np.argsort(logprobs_np[idx])[-self.beam_size - 1:][::-1]
for token_idx in top_k_indices:
logprob = logprobs_np[idx, token_idx]
new_logprob = sum_logprobs_np[idx] + logprob
sequence = tuple(prefix + [int(token_idx)])
scores[sequence] = new_logprob
sources[sequence] = idx
saved = 0
for sequence in sorted(scores, key=scores.get, reverse=True):
if sequence[-1] == self.eot:
finished[sequence] = scores[sequence]
else:
new_sum_logprobs.append(scores[sequence])
next_tokens.append(sequence)
source_indices.append(sources[sequence])
saved += 1
if saved == self.beam_size:
break
finished_sequences.append(finished)
tokens = mx.array(np.array(next_tokens, dtype=np.int32))
sum_logprobs = mx.array(np.array(new_sum_logprobs, dtype=np.float32))
self.inference.rearrange_kv_cache(source_indices)
assert len(self.finished_sequences) == len(finished_sequences)
for previously_finished, newly_finished in zip(
self.finished_sequences, finished_sequences
):
for seq in sorted(newly_finished, key=newly_finished.get, reverse=True):
if len(previously_finished) >= self.max_candidates:
break
previously_finished[seq] = newly_finished[seq]
completed = all(
len(sequences) >= self.max_candidates
for sequences in self.finished_sequences
)
return tokens, completed
def finalize(self, preceding_tokens: mx.array, sum_logprobs: mx.array):
"""Finalize beam search by selecting best sequences."""
preceding_tokens_np = np.array(preceding_tokens)
sum_logprobs_np = np.array(sum_logprobs)
n_audio = preceding_tokens_np.shape[0] // self.beam_size
tokens_list: List[List[int]] = [[] for _ in range(n_audio)]
sum_logprobs_list: List[float] = [0.0] * n_audio
for i, sequences in enumerate(self.finished_sequences):
if sequences:
best_seq = max(sequences, key=sequences.get)
tokens_list[i] = list(best_seq)
sum_logprobs_list[i] = sequences[best_seq]
else:
idx = i * self.beam_size
tokens_list[i] = preceding_tokens_np[idx].tolist() + [self.eot]
sum_logprobs_list[i] = float(sum_logprobs_np[idx])
max_len = max(len(t) for t in tokens_list)
for i, t in enumerate(tokens_list):
tokens_list[i] = t + [self.eot] * (max_len - len(t))
tokens = mx.array(np.array(tokens_list, dtype=np.int32))
return tokens, sum_logprobs_list
class MLXInference:
"""MLX inference wrapper for beam search KV cache management."""
def __init__(self, model, initial_token_length: int):
self.model = model
self.initial_token_length = initial_token_length
self.kv_cache = None
def rearrange_kv_cache(self, source_indices: List[int]):
"""Rearrange KV cache based on beam search source indices."""
if self.kv_cache is None:
return
if source_indices == list(range(len(source_indices))):
return
source_indices_mx = mx.array(source_indices, dtype=mx.int32)
new_cache = []
for layer_cache in self.kv_cache:
(k, v), (cross_k, cross_v) = layer_cache
new_k = k[source_indices_mx]
new_v = v[source_indices_mx]
new_cache.append(((new_k, new_v), (cross_k, cross_v)))
self.kv_cache = new_cache
def logits(
self,
tokens: mx.array,
audio_features: mx.array,
) -> Tuple[mx.array, List]:
"""Get logits from decoder with KV cache."""
logits, self.kv_cache, cross_qk = self.model.decoder(
tokens, audio_features, kv_cache=self.kv_cache
)
return logits, cross_qk
================================================
FILE: whisperlivekit/simul_whisper/mlx/simul_whisper.py
================================================
"""MLX whisper AlignAtt streaming decoder."""
import logging
from typing import Any, List, Tuple
import mlx.core as mx
import numpy as np
from mlx_whisper.audio import log_mel_spectrogram as mlx_log_mel_spectrogram
from mlx_whisper.transcribe import pad_or_trim as mlx_pad_or_trim
from whisperlivekit.whisper.audio import N_FRAMES, N_SAMPLES, TOKENS_PER_SECOND
from ..align_att_base import DEC_PAD, AlignAttBase
from ..config import AlignAttConfig
from .decoder_state import MLXDecoderState
from .decoders import MLXBeamSearchDecoder, MLXGreedyDecoder, MLXInference
logger = logging.getLogger(__name__)
class MLXTokenBuffer:
"""Token buffer for MLX-based decoding."""
def __init__(self, text="", tokenizer=None, prefix_token_ids=None):
self.text = text
self.prefix_token_ids = prefix_token_ids or []
self.tokenizer = tokenizer
self.pending_token_ids = []
def as_token_ids(self, tokenizer=None):
if tokenizer is None:
tokenizer = self.tokenizer
if tokenizer is None:
raise ValueError("Tokenizer is not set.")
return self.prefix_token_ids + tokenizer.encode(self.text)
def as_mlx_array(self) -> mx.array:
tok_ids = self.as_token_ids()
return mx.array([tok_ids], dtype=mx.int32)
def as_mlx_array_beam(self, beam: int) -> mx.array:
t = self.as_mlx_array()
return mx.repeat(t, beam, axis=0)
def as_text(self):
return self.text
@staticmethod
def empty(*a, **kw):
return MLXTokenBuffer(*a, **kw)
@staticmethod
def from_text(text, *a, **kw):
return MLXTokenBuffer(*a, text=text, **kw)
def is_empty(self):
return self.text is None or self.text == ""
def trim_words(self, num=1, after=0):
tokenizer = self.tokenizer
assert tokenizer is not None, "Tokenizer is not set."
ids = tokenizer.encode(self.text[after:])
words, wids = self.tokenizer.split_to_word_tokens(ids)
if not words:
return 0
self.text = self.text[:after] + "".join(words[num:])
return sum(len(wi) for wi in wids[:num])
def append_token_ids(self, token_ids):
tokenizer = self.tokenizer
assert tokenizer is not None, "Tokenizer is not set."
all_tokens = self.pending_token_ids + token_ids
decoded = tokenizer.decode(all_tokens)
replacement_char = "\ufffd"
if replacement_char in decoded:
if len(all_tokens) > 1:
decoded_partial = tokenizer.decode(all_tokens[:-1])
if replacement_char not in decoded_partial:
self.text += decoded_partial
self.pending_token_ids = [all_tokens[-1]]
else:
self.pending_token_ids = all_tokens
else:
self.pending_token_ids = all_tokens
else:
self.text += decoded
self.pending_token_ids = []
def mlx_median_filter(x: mx.array, filter_width: int) -> mx.array:
"""Apply median filter along the last axis."""
if filter_width <= 1:
return x
pad_width = filter_width // 2
shape = x.shape
left_pad = mx.repeat(x[..., :1], pad_width, axis=-1)
right_pad = mx.repeat(x[..., -1:], pad_width, axis=-1)
x_padded = mx.concatenate([left_pad, x, right_pad], axis=-1)
result = []
for i in range(shape[-1]):
window = x_padded[..., i:i + filter_width]
sorted_window = mx.sort(window, axis=-1)
median_val = sorted_window[..., filter_width // 2:filter_width // 2 + 1]
result.append(median_val)
return mx.concatenate(result, axis=-1)
class MLXAlignAtt(AlignAttBase):
"""
MLX-native Alignment-based Attention decoder for SimulStreaming.
Runs entirely on MLX, with no PyTorch dependencies for inference.
"""
def __init__(
self,
cfg: AlignAttConfig,
mlx_model: Any,
) -> None:
# Common init (sets self.model, self.cfg, decode_options, etc.)
self._base_init(cfg, mlx_model)
logger.info(f"MLX Model dimensions: {self.model.dims}")
# Per-session state
self.state = MLXDecoderState()
self._init_state(cfg)
def _init_state(self, cfg: AlignAttConfig):
self._init_state_common(cfg)
# CIF: MLX doesn't support CIF checkpoint loading
if cfg.cif_ckpt_path is None or not cfg.cif_ckpt_path:
if cfg.never_fire:
self.state.never_fire = True
self.state.always_fire = False
else:
self.state.always_fire = True
self.state.never_fire = False
else:
logger.warning(
"CIF checkpoint provided but MLX CIF not implemented. "
"Using always_fire=True"
)
self.state.always_fire = True
self.state.never_fire = cfg.never_fire
self._build_alignment_source()
# Suppress tokens
suppress_tokens = [
self.tokenizer.transcribe, self.tokenizer.translate,
self.tokenizer.sot, self.tokenizer.sot_prev,
self.tokenizer.sot_lm, self.tokenizer.no_timestamps,
] + list(self.tokenizer.all_language_tokens)
if self.tokenizer.no_speech is not None:
suppress_tokens.append(self.tokenizer.no_speech)
self.state.suppress_tokens = tuple(sorted(set(suppress_tokens)))
logger.debug(f"Suppress tokens: {self.state.suppress_tokens}")
self.init_tokens()
self.init_context()
# Decoder type
self.state.decoder_type = cfg.decoder_type
if cfg.decoder_type == "greedy":
logger.info("Using MLX greedy decoder")
self.state.token_decoder = MLXGreedyDecoder(0.0, self.tokenizer.eot)
elif cfg.decoder_type == "beam":
logger.info("Using MLX beam decoder")
self.state.inference = MLXInference(
self.model, self.state.initial_token_length,
)
self.state.token_decoder = MLXBeamSearchDecoder(
inference=self.state.inference,
eot=self.tokenizer.eot,
beam_size=cfg.beam_size,
)
def _build_alignment_source(self):
"""Build alignment source mapping from model's alignment_heads."""
self.state.align_source = {}
self.state.num_align_heads = 0
alignment_heads = self.model.alignment_heads
if alignment_heads is None:
logger.warning("No alignment heads found in model")
return
if hasattr(alignment_heads, 'tolist'):
heads_list = alignment_heads.tolist()
else:
heads_list = np.array(alignment_heads).tolist()
for layer_rank, head_id in heads_list:
layer_rank = int(layer_rank)
head_id = int(head_id)
heads = self.state.align_source.get(layer_rank, [])
heads.append((self.state.num_align_heads, head_id))
self.state.align_source[layer_rank] = heads
self.state.num_align_heads += 1
# === Abstract method implementations ===
def init_tokens(self):
logger.debug(f"init tokens, {len(self.state.segments)}")
self.state.initial_tokens = mx.array(
[self.tokenizer.sot_sequence_including_notimestamps],
dtype=mx.int32,
)
self.state.initial_token_length = self.state.initial_tokens.shape[1]
self.state.sot_index = self.tokenizer.sot_sequence.index(self.tokenizer.sot)
logger.debug(f"init tokens after, {len(self.state.segments)}")
self.state.tokens = [self.state.initial_tokens]
def init_context(self):
kw = {
'tokenizer': self.tokenizer,
'prefix_token_ids': [self.tokenizer.sot_prev],
}
self.state.context = MLXTokenBuffer.empty(**kw)
if self.cfg.static_init_prompt is not None:
self.state.context = MLXTokenBuffer.from_text(self.cfg.static_init_prompt, **kw)
if self.cfg.init_prompt is not None:
self.state.context.text += self.cfg.init_prompt
def insert_audio(self, segment=None):
if segment is not None:
if hasattr(segment, 'numpy'):
segment = segment.numpy()
self.state.segments.append(segment)
removed_len = 0
segments_len = self.segments_len()
while len(self.state.segments) > 1 and segments_len > self.cfg.audio_max_len:
removed_len = self.state.segments[0].shape[0] / 16000
segments_len -= removed_len
self.state.last_attend_frame -= int(TOKENS_PER_SECOND * removed_len)
self.state.cumulative_time_offset += removed_len
self.state.segments = self.state.segments[1:]
logger.debug(
f"remove segments: {len(self.state.segments)} {len(self.state.tokens)}, "
f"cumulative offset: {self.state.cumulative_time_offset:.2f}s"
)
if len(self.state.tokens) > 1:
token_list = np.array(self.state.tokens[1][0, :]).tolist()
self.state.context.append_token_ids(token_list)
self.state.tokens = [self.state.initial_tokens] + self.state.tokens[2:]
return removed_len
def _current_tokens(self) -> mx.array:
toks = self.state.tokens
if toks[0].shape[0] == 1:
toks[0] = mx.repeat(toks[0], self.cfg.beam_size, axis=0)
if not self.state.context.is_empty():
context_toks = self.state.context.as_mlx_array_beam(self.cfg.beam_size)
toks = [context_toks] + toks
if len(toks) > 1:
current_tokens = mx.concatenate(toks, axis=1)
else:
current_tokens = toks[0]
logger.debug("debug print current_tokens:")
self.debug_print_tokens(current_tokens)
return current_tokens
def fire_at_boundary(self, chunked_encoder_feature: mx.array) -> bool:
if self.state.always_fire:
return True
if self.state.never_fire:
return False
return True # MLX CIF not implemented
def lang_id(self, encoder_features: mx.array) -> Tuple[mx.array, List[dict]]:
n_audio = encoder_features.shape[0]
x = mx.array([[self.tokenizer.sot]] * n_audio, dtype=mx.int32)
logits, _, _ = self.model.decoder(x, encoder_features, kv_cache=None)
logits = logits[:, 0]
mask = mx.ones(logits.shape[-1], dtype=mx.bool_)
language_token_indices = mx.array(
list(self.tokenizer.all_language_tokens), dtype=mx.int32,
)
mask = mask.at[language_token_indices].add(False)
logits = mx.where(mask, mx.array(-float('inf')), logits)
language_tokens = mx.argmax(logits, axis=-1)
language_token_probs = mx.softmax(logits, axis=-1)
probs_np = np.array(language_token_probs)
language_probs = [
{
c: float(probs_np[i, j])
for j, c in zip(
self.tokenizer.all_language_tokens,
self.tokenizer.all_language_codes,
)
}
for i in range(n_audio)
]
self._clean_cache()
return language_tokens, language_probs
def _concat_segments(self):
if len(self.state.segments) > 1:
return np.concatenate(self.state.segments, axis=0)
return self.state.segments[0]
def _encode(self, input_segments):
mlx_mel_padded = mlx_log_mel_spectrogram(
audio=input_segments,
n_mels=self.model.dims.n_mels,
padding=N_SAMPLES,
)
mlx_mel = mlx_pad_or_trim(mlx_mel_padded, N_FRAMES, axis=-2)
encoder_feature = self.model.encoder(mlx_mel[None])
content_mel_len = int((mlx_mel_padded.shape[0] - mlx_mel.shape[0]) / 2)
return encoder_feature, content_mel_len
def _init_sum_logprobs(self):
return mx.zeros((self.cfg.beam_size,), dtype=mx.float32)
def _get_logits_and_cross_attn(self, tokens, encoder_feature):
if self.state.decoder_type == "greedy":
logits, self.state.kv_cache, cross_qk = self.model.decoder(
tokens, encoder_feature, kv_cache=self.state.kv_cache,
)
return logits, cross_qk
else:
return self.state.inference.logits(tokens, encoder_feature)
def _check_no_speech(self, logits):
if self.tokenizer.no_speech is not None:
probs_at_sot = mx.softmax(logits[:, self.state.sot_index, :], axis=-1)
no_speech_probs = np.array(
probs_at_sot[:, self.tokenizer.no_speech],
).tolist()
if no_speech_probs[0] > self.cfg.nonspeech_prob:
logger.info("no speech, stop")
return True
return False
def _suppress_blank_tokens(self, logits):
blank_tokens = self.tokenizer.encode(" ") + [self.tokenizer.eot]
logits = logits.at[:, blank_tokens].add(-float('inf'))
return logits
def _apply_token_suppression(self, logits):
if self.state.suppress_tokens:
suppress_indices = mx.array(
list(self.state.suppress_tokens), dtype=mx.int32,
)
logits = logits.at[:, suppress_indices].add(-float('inf'))
return logits
def _update_tokens(self, current_tokens, logits, sum_logprobs):
return self.state.token_decoder.update(current_tokens, logits, sum_logprobs)
def _process_cross_attention(
self, cross_attns: List, content_mel_len: int,
) -> mx.array:
attn_of_alignment_heads = [[] for _ in range(self.state.num_align_heads)]
num_decoder_layers = self.num_decoder_layers
if cross_attns and isinstance(cross_attns[0], list):
flattened_attns = [attn for layer_list in cross_attns for attn in layer_list]
else:
flattened_attns = cross_attns
for idx, attn_mat in enumerate(flattened_attns):
if attn_mat is None:
continue
layer_rank = idx % num_decoder_layers
align_heads_in_layer = self.state.align_source.get(layer_rank, [])
if not align_heads_in_layer:
continue
attn_mat = mx.softmax(attn_mat, axis=-1)
for align_head_rank, head_id in align_heads_in_layer:
if self.cfg.beam_size == 1:
if attn_mat.ndim == 4:
a = attn_mat[0, head_id, :, :]
else:
a = attn_mat[head_id, :, :]
a = a[None, :, :]
else:
a = attn_mat[:, head_id, :, :]
attn_of_alignment_heads[align_head_rank].append(a)
tmp = []
for mat in attn_of_alignment_heads:
if mat:
tmp.append(mx.concatenate(mat, axis=1))
if not tmp:
return mx.zeros((self.cfg.beam_size, 1, content_mel_len))
attn_of_alignment_heads = mx.stack(tmp, axis=1)
std = mx.std(attn_of_alignment_heads, axis=-2, keepdims=True)
mean = mx.mean(attn_of_alignment_heads, axis=-2, keepdims=True)
attn_of_alignment_heads = (attn_of_alignment_heads - mean) / (std + 1e-8)
attn_of_alignment_heads = mlx_median_filter(attn_of_alignment_heads, 7)
attn_of_alignment_heads = mx.mean(attn_of_alignment_heads, axis=1)
attn_of_alignment_heads = attn_of_alignment_heads[:, :, :content_mel_len]
mx.eval(attn_of_alignment_heads)
return attn_of_alignment_heads
def _get_attended_frames(self, attn):
most_attended_frames = mx.argmax(attn[:, -1, :], axis=-1)
frames_np = np.array(most_attended_frames)
return frames_np.tolist(), int(frames_np[0])
def _is_special_token(self, current_tokens):
return int(np.array(current_tokens[0, -2])) >= DEC_PAD
def _rewind_tokens(self):
if len(self.state.tokens) > 0:
return mx.concatenate(self.state.tokens, axis=1)
return self.state.tokens[0]
def _tokens_to_list(self, current_tokens, start_col):
return np.array(current_tokens[0, start_col:]).tolist()
def _make_new_tokens_tensor(self, hypothesis):
new_tokens = mx.array([hypothesis], dtype=mx.int32)
return mx.repeat(new_tokens, self.cfg.beam_size, axis=0)
def _evaluate(self, tensor):
mx.eval(tensor)
================================================
FILE: whisperlivekit/simul_whisper/mlx_encoder.py
================================================
import json
from pathlib import Path
import mlx.core as mx
import mlx.nn as nn
from huggingface_hub import snapshot_download
from mlx.utils import tree_unflatten
from mlx_whisper import whisper
from whisperlivekit.model_mapping import MLX_MODEL_MAPPING
mlx_model_mapping = MLX_MODEL_MAPPING
def load_mlx_encoder(
path_or_hf_repo: str,
dtype: mx.Dtype = mx.float32,
) -> whisper.Whisper:
model_path = Path(path_or_hf_repo)
if not model_path.exists():
model_path = Path(snapshot_download(repo_id=path_or_hf_repo))
with open(str(model_path / "config.json"), "r") as f:
config = json.loads(f.read())
config.pop("model_type", None)
quantization = config.pop("quantization", None)
model_args = whisper.ModelDimensions(**config)
wf = model_path / "weights.safetensors"
if not wf.exists():
wf = model_path / "weights.npz"
weights = mx.load(str(wf))
model = whisper.Whisper(model_args, dtype)
if quantization is not None:
class_predicate = (
lambda p, m: isinstance(m, (nn.Linear, nn.Embedding))
and f"{p}.scales" in weights
)
nn.quantize(model, **quantization, class_predicate=class_predicate)
weights = tree_unflatten(list(weights.items()))
# we only want to load the encoder weights here.
# Size examples: for tiny.en,
# Decoder weights: 59110771 bytes
# Encoder weights: 15268874 bytes
encoder_weights = {}
encoder_weights['encoder'] = weights['encoder']
del(weights)
model.update(encoder_weights)
mx.eval(model.parameters())
return model
def load_mlx_model(
path_or_hf_repo: str,
dtype: mx.Dtype = mx.float32,
) -> whisper.Whisper:
model_path = Path(path_or_hf_repo)
if not model_path.exists():
model_path = Path(snapshot_download(repo_id=path_or_hf_repo))
with open(str(model_path / "config.json"), "r") as f:
config = json.loads(f.read())
config.pop("model_type", None)
quantization = config.pop("quantization", None)
model_args = whisper.ModelDimensions(**config)
wf = model_path / "weights.safetensors"
if not wf.exists():
wf = model_path / "weights.npz"
weights = mx.load(str(wf))
model = whisper.Whisper(model_args, dtype)
if quantization is not None:
class_predicate = (
lambda p, m: isinstance(m, (nn.Linear, nn.Embedding))
and f"{p}.scales" in weights
)
nn.quantize(model, **quantization, class_predicate=class_predicate)
weights = tree_unflatten(list(weights.items()))
model.update(weights)
mx.eval(model.parameters())
return model
================================================
FILE: whisperlivekit/simul_whisper/simul_whisper.py
================================================
import logging
import os
from typing import List
import numpy as np
import torch
import torch.nn.functional as F
from whisperlivekit.backend_support import faster_backend_available, mlx_backend_available
from whisperlivekit.whisper.audio import N_FRAMES, N_SAMPLES, TOKENS_PER_SECOND, log_mel_spectrogram, pad_or_trim
from whisperlivekit.whisper.decoding import BeamSearchDecoder, GreedyDecoder, SuppressTokens
from whisperlivekit.whisper.timing import median_filter
from .align_att_base import DEC_PAD, AlignAttBase
from .beam import BeamPyTorchInference
from .config import AlignAttConfig
from .decoder_state import DecoderState
from .eow_detection import fire_at_boundary, load_cif
from .token_buffer import TokenBuffer
logger = logging.getLogger(__name__)
if mlx_backend_available():
from mlx_whisper.audio import log_mel_spectrogram as mlx_log_mel_spectrogram
from mlx_whisper.transcribe import pad_or_trim as mlx_pad_or_trim
if faster_backend_available():
from faster_whisper.audio import pad_or_trim as fw_pad_or_trim
from faster_whisper.feature_extractor import FeatureExtractor
USE_MLCORE = False
def load_coreml_encoder():
try:
from coremltools.models import MLModel
except ImportError:
logger.warning("coremltools is not installed")
return None
COREML_ENCODER_PATH = os.environ.get(
"MLCORE_ENCODER_PATH",
"whisperlivekit/whisper/whisper_encoder.mlpackage",
)
_coreml_encoder = MLModel(COREML_ENCODER_PATH)
spec = _coreml_encoder.get_spec()
_coreml_input_name = spec.description.input[0].name if spec.description.input else "mel"
_coreml_output_name = spec.description.output[0].name if spec.description.output else None
return _coreml_encoder, _coreml_input_name, _coreml_output_name
class AlignAtt(AlignAttBase):
"""
PyTorch Alignment-based Attention decoder for SimulStreaming.
Hookless — the model can be shared across multiple sessions,
with each session maintaining its own DecoderState.
"""
def __init__(
self,
cfg: AlignAttConfig,
loaded_model=None,
mlx_encoder=None,
fw_encoder=None,
) -> None:
self.mlx_encoder = mlx_encoder
self.fw_encoder = fw_encoder
if fw_encoder:
self.fw_feature_extractor = FeatureExtractor(
feature_size=loaded_model.dims.n_mels,
)
self.coreml_encoder_tuple = None
if USE_MLCORE:
self.coreml_encoder_tuple = load_coreml_encoder()
self.use_mlcore = self.coreml_encoder_tuple is not None
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
# Common init (sets self.model, self.cfg, decode_options, etc.)
self._base_init(cfg, loaded_model)
logger.info(f"Model dimensions: {self.model.dims}")
# Per-session state
self.state = DecoderState()
self._init_state(cfg)
def _init_state(self, cfg: AlignAttConfig):
self._init_state_common(cfg)
# CIF helpers for end-of-word boundary detection
self.state.CIFLinear, self.state.always_fire, self.state.never_fire = load_cif(
cfg, n_audio_state=self.model.dims.n_audio_state, device=self.model.device,
)
# Build alignment source mapping
self.state.align_source = {}
self.state.num_align_heads = 0
for layer_rank, head_id in self.model.alignment_heads.indices().T:
layer_rank = layer_rank.item()
heads = self.state.align_source.get(layer_rank, [])
heads.append((self.state.num_align_heads, head_id.item()))
self.state.align_source[layer_rank] = heads
self.state.num_align_heads += 1
# Build suppress tokens function
suppress_tokens = [
self.tokenizer.transcribe, self.tokenizer.translate,
self.tokenizer.sot, self.tokenizer.sot_prev,
self.tokenizer.sot_lm, self.tokenizer.no_timestamps,
] + list(self.tokenizer.all_language_tokens)
if self.tokenizer.no_speech is not None:
suppress_tokens.append(self.tokenizer.no_speech)
suppress_tokens = tuple(sorted(set(suppress_tokens)))
logger.debug(f"Suppress tokens: {suppress_tokens}")
sup_tokens = SuppressTokens(suppress_tokens)
self.state.suppress_tokens_fn = lambda logits: sup_tokens.apply(logits, None)
self.init_tokens()
self.init_context()
# Decoder type
self.state.decoder_type = cfg.decoder_type
if cfg.decoder_type == "greedy":
logger.info("Using greedy decoder")
self.state.token_decoder = GreedyDecoder(0.0, self.tokenizer.eot)
elif cfg.decoder_type == "beam":
logger.info("Using beam decoder")
self.state.inference = BeamPyTorchInference(
self.model, self.state.initial_token_length,
)
self.state.inference.kv_cache = self.state.kv_cache
self.state.token_decoder = BeamSearchDecoder(
inference=self.state.inference,
eot=self.tokenizer.eot,
beam_size=cfg.beam_size,
)
# === Abstract method implementations ===
def init_tokens(self):
logger.debug(f"init tokens, {len(self.state.segments)}")
self.state.initial_tokens = torch.tensor(
self.tokenizer.sot_sequence_including_notimestamps,
dtype=torch.long, device=self.model.device,
).unsqueeze(0)
self.state.initial_token_length = self.state.initial_tokens.shape[1]
self.state.sot_index = self.tokenizer.sot_sequence.index(self.tokenizer.sot)
logger.debug(f"init tokens after, {len(self.state.segments)}")
self.state.tokens = [self.state.initial_tokens]
def init_context(self):
kw = {
'tokenizer': self.tokenizer,
'device': self.model.device,
'prefix_token_ids': [self.tokenizer.sot_prev],
}
self.state.context = TokenBuffer.empty(**kw)
if self.cfg.static_init_prompt is not None:
self.state.context = TokenBuffer.from_text(self.cfg.static_init_prompt, **kw)
if self.cfg.init_prompt is not None:
self.state.context.text += self.cfg.init_prompt
def insert_audio(self, segment=None):
if segment is not None:
self.state.segments.append(segment)
removed_len = 0
segments_len = self.segments_len()
while len(self.state.segments) > 1 and segments_len > self.cfg.audio_max_len:
removed_len = self.state.segments[0].shape[0] / 16000
segments_len -= removed_len
self.state.last_attend_frame -= int(TOKENS_PER_SECOND * removed_len)
self.state.cumulative_time_offset += removed_len
self.state.segments = self.state.segments[1:]
logger.debug(
f"remove segments: {len(self.state.segments)} {len(self.state.tokens)}, "
f"cumulative offset: {self.state.cumulative_time_offset:.2f}s"
)
if len(self.state.tokens) > 1:
self.state.context.append_token_ids(self.state.tokens[1][0, :].tolist())
self.state.tokens = [self.state.initial_tokens] + self.state.tokens[2:]
return removed_len
def _current_tokens(self):
toks = self.state.tokens
if toks[0].shape[0] == 1:
toks[0] = toks[0].repeat_interleave(self.cfg.beam_size, dim=0)
if not self.state.context.is_empty():
context_toks = self.state.context.as_tensor_beam(
self.cfg.beam_size, device=self.model.device,
)
toks = [context_toks] + toks
if len(toks) > 1:
current_tokens = torch.cat(toks, dim=1)
else:
current_tokens = toks[0]
logger.debug("debug print current_tokens:")
self.debug_print_tokens(current_tokens)
return current_tokens
def fire_at_boundary(self, chunked_encoder_feature: torch.Tensor):
if self.state.always_fire:
return True
if self.state.never_fire:
return False
return fire_at_boundary(chunked_encoder_feature, self.state.CIFLinear)
@torch.no_grad()
def lang_id(self, encoder_features):
n_audio = encoder_features.shape[0]
x = torch.tensor([[self.tokenizer.sot]] * n_audio).to(self.model.device)
logits = self.model.logits(x, encoder_features)[:, 0]
mask = torch.ones(logits.shape[-1], dtype=torch.bool)
mask[list(self.tokenizer.all_language_tokens)] = False
logits[:, mask] = -np.inf
language_tokens = logits.argmax(dim=-1)
language_token_probs = logits.softmax(dim=-1).cpu()
language_probs = [
{
c: language_token_probs[i, j].item()
for j, c in zip(
self.tokenizer.all_language_tokens,
self.tokenizer.all_language_codes,
)
}
for i in range(n_audio)
]
single = encoder_features.ndim == 2
if single:
language_tokens = language_tokens[0]
language_probs = language_probs[0]
self._clean_cache()
return language_tokens, language_probs
def _concat_segments(self):
if len(self.state.segments) > 1:
return torch.cat(self.state.segments, dim=0)
return self.state.segments[0]
def _encode(self, input_segments):
if self.use_mlcore:
coreml_encoder, coreml_input_name, coreml_output_name = self.coreml_encoder_tuple
mel_padded = log_mel_spectrogram(
input_segments, n_mels=self.model.dims.n_mels,
padding=N_SAMPLES, device="cpu",
).unsqueeze(0)
mel = pad_or_trim(mel_padded, N_FRAMES)
content_mel_len = int((mel_padded.shape[2] - mel.shape[2]) / 2)
mel_np = np.ascontiguousarray(mel.numpy())
ml_inputs = {coreml_input_name or "mel": mel_np}
coreml_outputs = coreml_encoder.predict(ml_inputs)
if coreml_output_name and coreml_output_name in coreml_outputs:
encoder_feature_np = coreml_outputs[coreml_output_name]
else:
encoder_feature_np = next(iter(coreml_outputs.values()))
encoder_feature = torch.as_tensor(
np.array(encoder_feature_np), device=self.device,
)
if self.mlx_encoder:
mlx_mel_padded = mlx_log_mel_spectrogram(
audio=input_segments.detach(),
n_mels=self.model.dims.n_mels, padding=N_SAMPLES,
)
mlx_mel = mlx_pad_or_trim(mlx_mel_padded, N_FRAMES, axis=-2)
mlx_encoder_feature = self.mlx_encoder.encoder(mlx_mel[None])
encoder_feature = torch.as_tensor(mlx_encoder_feature)
content_mel_len = int((mlx_mel_padded.shape[0] - mlx_mel.shape[0]) / 2)
elif self.fw_encoder:
audio_length_seconds = len(input_segments) / 16000
content_mel_len = int(audio_length_seconds * 100) // 2
mel_padded_2 = self.fw_feature_extractor(
waveform=input_segments.numpy(), padding=N_SAMPLES,
)[None, :]
mel = fw_pad_or_trim(mel_padded_2, N_FRAMES, axis=-1)
encoder_feature_ctranslate = self.fw_encoder.encode(mel)
if self.device == 'cpu':
encoder_feature_ctranslate = np.array(encoder_feature_ctranslate)
try:
encoder_feature = torch.as_tensor(encoder_feature_ctranslate, device=self.device)
except TypeError:
try:
arr = np.asarray(encoder_feature_ctranslate, dtype=np.float32)
except (TypeError, ValueError):
arr = np.array(encoder_feature_ctranslate)
if arr.dtype == np.object_:
try:
arr = np.stack([
np.asarray(item, dtype=np.float32) for item in arr.flat
])
except (TypeError, ValueError):
arr = np.array(
[[float(x) for x in row] for row in arr.flat],
dtype=np.float32,
)
encoder_feature = torch.as_tensor(arr, device=self.device)
else:
mel_padded = log_mel_spectrogram(
input_segments, n_mels=self.model.dims.n_mels,
padding=N_SAMPLES, device=self.device,
).unsqueeze(0)
mel = pad_or_trim(mel_padded, N_FRAMES)
content_mel_len = int((mel_padded.shape[2] - mel.shape[2]) / 2)
encoder_feature = self.model.encoder(mel)
return encoder_feature, content_mel_len
def _init_sum_logprobs(self):
return torch.zeros(self.cfg.beam_size, device=self.device)
def _get_logits_and_cross_attn(self, tokens, encoder_feature):
if self.state.decoder_type == "greedy":
return self.model.decoder(
tokens, encoder_feature,
kv_cache=self.state.kv_cache,
return_cross_attn=True,
)
else:
logger.debug(f"Logits shape: {tokens.shape}")
return self.state.inference.logits(
tokens, encoder_feature, return_cross_attn=True,
)
def _check_no_speech(self, logits):
if self.tokenizer.no_speech is not None:
probs_at_sot = logits[:, self.state.sot_index, :].float().softmax(dim=-1)
no_speech_probs = probs_at_sot[:, self.tokenizer.no_speech].tolist()
if no_speech_probs[0] > self.cfg.nonspeech_prob:
logger.info("no speech, stop")
return True
return False
def _suppress_blank_tokens(self, logits):
logits[:, self.tokenizer.encode(" ") + [self.tokenizer.eot]] = -np.inf
return logits
def _apply_token_suppression(self, logits):
self.state.suppress_tokens_fn(logits)
return logits
def _update_tokens(self, current_tokens, logits, sum_logprobs):
return self.state.token_decoder.update(current_tokens, logits, sum_logprobs)
def _process_cross_attention(
self, cross_attns: List, content_mel_len: int,
) -> torch.Tensor:
attn_of_alignment_heads = [[] for _ in range(self.state.num_align_heads)]
num_decoder_layers = len(self.model.decoder.blocks)
if cross_attns and isinstance(cross_attns[0], list):
flattened_attns = [attn for layer_list in cross_attns for attn in layer_list]
else:
flattened_attns = cross_attns
for idx, attn_mat in enumerate(flattened_attns):
layer_rank = idx % num_decoder_layers
align_heads_in_layer = self.state.align_source.get(layer_rank, [])
if not align_heads_in_layer:
continue
attn_mat = F.softmax(attn_mat, dim=-1)
for align_head_rank, head_id in align_heads_in_layer:
if self.cfg.beam_size == 1:
if attn_mat.dim() == 4:
a = attn_mat[0, head_id, :, :]
else:
a = attn_mat[head_id, :, :]
a = a.unsqueeze(0)
else:
a = attn_mat[:, head_id, :, :]
attn_of_alignment_heads[align_head_rank].append(a)
tmp = []
for mat in attn_of_alignment_heads:
if mat:
tmp.append(torch.cat(mat, dim=1))
if not tmp:
return torch.zeros(self.cfg.beam_size, 1, content_mel_len, device=self.device)
attn_of_alignment_heads = torch.stack(tmp, dim=1)
std, mean = torch.std_mean(
attn_of_alignment_heads, dim=-2, keepdim=True, unbiased=False,
)
attn_of_alignment_heads = (attn_of_alignment_heads - mean) / (std + 1e-8)
attn_of_alignment_heads = median_filter(attn_of_alignment_heads, 7)
attn_of_alignment_heads = attn_of_alignment_heads.mean(dim=1)
attn_of_alignment_heads = attn_of_alignment_heads[:, :, :content_mel_len]
return attn_of_alignment_heads
def _get_attended_frames(self, attn):
most_attended_frames = torch.argmax(attn[:, -1, :], dim=-1)
return most_attended_frames.tolist(), most_attended_frames[0].item()
def _is_special_token(self, current_tokens):
return current_tokens[0, -2].item() >= DEC_PAD
def _rewind_tokens(self):
if len(self.state.tokens) > 0:
return torch.cat(self.state.tokens, dim=1)
return self.state.tokens[0]
def _tokens_to_list(self, current_tokens, start_col):
return current_tokens[0, start_col:].flatten().tolist()
def _make_new_tokens_tensor(self, hypothesis):
return (
torch.tensor([hypothesis], dtype=torch.long)
.repeat_interleave(self.cfg.beam_size, dim=0)
.to(device=self.device)
)
def _evaluate(self, tensor):
pass # No-op for PyTorch
@torch.no_grad()
def infer(self, is_last=False):
return super().infer(is_last)
================================================
FILE: whisperlivekit/simul_whisper/token_buffer.py
================================================
import torch
class TokenBuffer:
def __init__(self, text="", tokenizer=None, device=None, prefix_token_ids=[]):
self.text = text
self.prefix_token_ids = prefix_token_ids
self.tokenizer = tokenizer
self.device = device
self.pending_token_ids = []
def as_token_ids(self, tokenizer=None):
if tokenizer is None:
tokenizer = self.tokenizer
if tokenizer is None:
raise ValueError("Tokenizer is not set.")
return self.prefix_token_ids + tokenizer.encode(self.text)
def as_tensor(self, device=None):
if device is None:
device = self.device
if device is None:
raise ValueError("Device is not set.")
tok_ids = self.as_token_ids()
return torch.tensor(tok_ids,
dtype=torch.long, device=device).unsqueeze(0)
def as_tensor_beam(self, beam, device=None):
t = self.as_tensor(device=device)
return t.repeat_interleave(beam, dim=0)
def as_text(self):
return self.text
@staticmethod
def empty(*a, **kw):
return TokenBuffer(*a,**kw)
@staticmethod
def from_text(text, *a, **kw):
return TokenBuffer(*a, text=text, **kw)
def is_empty(self):
return self.text is None or self.text == ""
def trim_words(self, num=1, after=0):
'''
num: how many words to trim from the beginning
after: how many characters to skip (length of the static prompt)
'''
tokenizer = self.tokenizer
assert tokenizer is not None, "Tokenizer is not set."
ids = tokenizer.encode(self.text[after:])
words, wids = self.tokenizer.split_to_word_tokens(ids)
# print(words, file=sys.stderr)
# print(wids, file=sys.stderr)
if not words:
return 0
self.text = self.text[:after] + "".join(words[num:])
return sum(len(wi) for wi in wids[:num])
def append_token_ids(self, token_ids):
tokenizer = self.tokenizer
assert tokenizer is not None, "Tokenizer is not set."
all_tokens = self.pending_token_ids + token_ids
decoded = tokenizer.decode(all_tokens)
replacement_char = "\ufffd"
if replacement_char in decoded:
if len(all_tokens) > 1:
decoded_partial = tokenizer.decode(all_tokens[:-1])
if replacement_char not in decoded_partial:
self.text += decoded_partial
self.pending_token_ids = [all_tokens[-1]]
else:
self.pending_token_ids = all_tokens
else:
self.pending_token_ids = all_tokens
else:
self.text += decoded
self.pending_token_ids = []
def as_split_word_tokens(self):
tokenizer = self.tokenizer
assert tokenizer is not None, "Tokenizer is not set."
ids = tokenizer.encode(self.text)
return tokenizer.split_to_word_tokens(ids)
================================================
FILE: whisperlivekit/test_client.py
================================================
"""Headless test client for WhisperLiveKit.
Feeds audio files to the transcription pipeline via WebSocket
and collects results — no browser or microphone needed.
Usage:
# Against a running server (server must be started with --pcm-input):
python -m whisperlivekit.test_client audio.wav
# Custom server URL and speed:
python -m whisperlivekit.test_client audio.wav --url ws://localhost:9090/asr --speed 0
# Output raw JSON responses:
python -m whisperlivekit.test_client audio.wav --json
# Programmatic usage:
from whisperlivekit.test_client import transcribe_audio
result = asyncio.run(transcribe_audio("audio.wav"))
print(result.text)
"""
import argparse
import asyncio
import json
import logging
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional
logger = logging.getLogger(__name__)
SAMPLE_RATE = 16000
BYTES_PER_SAMPLE = 2 # s16le
@dataclass
class TranscriptionResult:
"""Collected transcription results from a session."""
responses: List[dict] = field(default_factory=list)
audio_duration: float = 0.0
@property
def text(self) -> str:
"""Full transcription text from the last response (committed lines + buffer)."""
if not self.responses:
return ""
for resp in reversed(self.responses):
lines = resp.get("lines", [])
buffer = resp.get("buffer_transcription", "")
if lines or buffer:
parts = [line["text"] for line in lines if line.get("text")]
if buffer:
parts.append(buffer)
return " ".join(parts)
return ""
@property
def committed_text(self) -> str:
"""Only the committed (finalized) transcription lines, no buffer."""
if not self.responses:
return ""
for resp in reversed(self.responses):
lines = resp.get("lines", [])
if lines:
return " ".join(line["text"] for line in lines if line.get("text"))
return ""
@property
def lines(self) -> List[dict]:
"""Committed lines from the last response."""
for resp in reversed(self.responses):
if resp.get("lines"):
return resp["lines"]
return []
@property
def n_updates(self) -> int:
"""Number of non-empty updates received."""
return sum(
1 for r in self.responses
if r.get("lines") or r.get("buffer_transcription")
)
def reconstruct_state(msg: dict, lines: List[dict]) -> dict:
"""Reconstruct full state from a diff or snapshot message.
Mutates ``lines`` in-place (prune front, append new) and returns
a full-state dict compatible with TranscriptionResult.
"""
if msg.get("type") == "snapshot":
lines.clear()
lines.extend(msg.get("lines", []))
return msg
# Apply diff
n_pruned = msg.get("lines_pruned", 0)
if n_pruned > 0:
del lines[:n_pruned]
new_lines = msg.get("new_lines", [])
lines.extend(new_lines)
return {
"status": msg.get("status", ""),
"lines": lines[:], # snapshot copy
"buffer_transcription": msg.get("buffer_transcription", ""),
"buffer_diarization": msg.get("buffer_diarization", ""),
"buffer_translation": msg.get("buffer_translation", ""),
"remaining_time_transcription": msg.get("remaining_time_transcription", 0),
"remaining_time_diarization": msg.get("remaining_time_diarization", 0),
}
def load_audio_pcm(audio_path: str, sample_rate: int = SAMPLE_RATE) -> bytes:
"""Load an audio file and convert to PCM s16le mono via ffmpeg.
Supports any format ffmpeg can decode (wav, mp3, flac, ogg, m4a, ...).
"""
cmd = [
"ffmpeg", "-i", str(audio_path),
"-f", "s16le", "-acodec", "pcm_s16le",
"-ar", str(sample_rate), "-ac", "1",
"-loglevel", "error",
"pipe:1",
]
proc = subprocess.run(cmd, capture_output=True)
if proc.returncode != 0:
raise RuntimeError(f"ffmpeg conversion failed: {proc.stderr.decode().strip()}")
if not proc.stdout:
raise RuntimeError(f"ffmpeg produced no output for {audio_path}")
return proc.stdout
async def transcribe_audio(
audio_path: str,
url: str = "ws://localhost:8000/asr",
chunk_duration: float = 0.5,
speed: float = 1.0,
timeout: float = 60.0,
on_response: Optional[callable] = None,
mode: str = "full",
) -> TranscriptionResult:
"""Feed an audio file to a running WhisperLiveKit server and collect results.
Args:
audio_path: Path to an audio file (any format ffmpeg supports).
url: WebSocket URL of the /asr endpoint.
chunk_duration: Duration of each audio chunk sent (seconds).
speed: Playback speed multiplier (1.0 = real-time, 0 = as fast as possible).
timeout: Max seconds to wait for the server after audio finishes.
on_response: Optional callback invoked with each response dict as it arrives.
mode: Output mode — "full" (default) or "diff" for incremental updates.
Returns:
TranscriptionResult with collected responses and convenience accessors.
"""
import websockets
result = TranscriptionResult()
# Convert audio to PCM for both modes (we need duration either way)
pcm_data = load_audio_pcm(audio_path)
result.audio_duration = len(pcm_data) / (SAMPLE_RATE * BYTES_PER_SAMPLE)
logger.info("Loaded %s: %.1fs of audio", audio_path, result.audio_duration)
chunk_bytes = int(chunk_duration * SAMPLE_RATE * BYTES_PER_SAMPLE)
# Append mode query parameter if using diff mode
connect_url = url
if mode == "diff":
sep = "&" if "?" in url else "?"
connect_url = f"{url}{sep}mode=diff"
async with websockets.connect(connect_url) as ws:
# Server sends config on connect
config_raw = await ws.recv()
config_msg = json.loads(config_raw)
is_pcm = config_msg.get("useAudioWorklet", False)
logger.info("Server config: %s", config_msg)
if not is_pcm:
logger.warning(
"Server is not in PCM mode. Start the server with --pcm-input "
"for the test client. Attempting raw file streaming instead."
)
done_event = asyncio.Event()
diff_lines: List[dict] = [] # running state for diff mode reconstruction
async def send_audio():
if is_pcm:
offset = 0
n_chunks = 0
while offset < len(pcm_data):
end = min(offset + chunk_bytes, len(pcm_data))
await ws.send(pcm_data[offset:end])
offset = end
n_chunks += 1
if speed > 0:
await asyncio.sleep(chunk_duration / speed)
logger.info("Sent %d PCM chunks (%.1fs)", n_chunks, result.audio_duration)
else:
# Non-PCM: send raw file bytes for server-side ffmpeg decoding
file_bytes = Path(audio_path).read_bytes()
raw_chunk_size = 32000
offset = 0
while offset < len(file_bytes):
end = min(offset + raw_chunk_size, len(file_bytes))
await ws.send(file_bytes[offset:end])
offset = end
if speed > 0:
await asyncio.sleep(0.5 / speed)
logger.info("Sent %d bytes of raw audio", len(file_bytes))
# Signal end of audio
await ws.send(b"")
logger.info("End-of-audio signal sent")
async def receive_results():
try:
async for raw_msg in ws:
data = json.loads(raw_msg)
if data.get("type") == "ready_to_stop":
logger.info("Server signaled ready_to_stop")
done_event.set()
return
# In diff mode, reconstruct full state for uniform API
if mode == "diff" and data.get("type") in ("snapshot", "diff"):
data = reconstruct_state(data, diff_lines)
result.responses.append(data)
if on_response:
on_response(data)
except Exception as e:
logger.debug("Receiver ended: %s", e)
done_event.set()
send_task = asyncio.create_task(send_audio())
recv_task = asyncio.create_task(receive_results())
# Total wait = time to send + time for server to process + timeout margin
send_time = result.audio_duration / speed if speed > 0 else 1.0
total_timeout = send_time + timeout
try:
await asyncio.wait_for(
asyncio.gather(send_task, recv_task),
timeout=total_timeout,
)
except asyncio.TimeoutError:
logger.warning("Timed out after %.0fs", total_timeout)
send_task.cancel()
recv_task.cancel()
try:
await asyncio.gather(send_task, recv_task, return_exceptions=True)
except Exception:
pass
logger.info(
"Session complete: %d responses, %d updates",
len(result.responses), result.n_updates,
)
return result
def _print_result(result: TranscriptionResult, output_json: bool = False) -> None:
"""Print transcription results to stdout."""
if output_json:
for resp in result.responses:
print(json.dumps(resp))
return
if result.lines:
for line in result.lines:
speaker = line.get("speaker", "")
text = line.get("text", "")
start = line.get("start", "")
end = line.get("end", "")
prefix = f"[{start} -> {end}]"
if speaker and speaker != 1:
prefix += f" Speaker {speaker}"
print(f"{prefix} {text}")
buffer = ""
if result.responses:
buffer = result.responses[-1].get("buffer_transcription", "")
if buffer:
print(f"[buffer] {buffer}")
if not result.lines and not buffer:
print("(no transcription received)")
print(
f"\n--- {len(result.responses)} responses | "
f"{result.n_updates} updates | "
f"{result.audio_duration:.1f}s audio ---"
)
def main():
parser = argparse.ArgumentParser(
prog="whisperlivekit-test-client",
description=(
"Headless test client for WhisperLiveKit. "
"Feeds audio files via WebSocket and prints the transcription."
),
)
parser.add_argument("audio", help="Path to audio file (wav, mp3, flac, ...)")
parser.add_argument(
"--url", default="ws://localhost:8000/asr",
help="WebSocket endpoint URL (default: ws://localhost:8000/asr)",
)
parser.add_argument(
"--speed", type=float, default=1.0,
help="Playback speed multiplier (1.0 = real-time, 0 = fastest, default: 1.0)",
)
parser.add_argument(
"--chunk-duration", type=float, default=0.5,
help="Chunk duration in seconds (default: 0.5)",
)
parser.add_argument(
"--timeout", type=float, default=60.0,
help="Max seconds to wait for server after audio ends (default: 60)",
)
parser.add_argument(
"--language", "-l", default=None,
help="Override transcription language for this session (e.g. en, fr, auto)",
)
parser.add_argument("--json", action="store_true", help="Output raw JSON responses")
parser.add_argument(
"--diff", action="store_true",
help="Use diff protocol (only receive incremental changes from server)",
)
parser.add_argument(
"--live", action="store_true",
help="Print transcription updates as they arrive",
)
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.WARNING,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
audio_path = Path(args.audio)
if not audio_path.exists():
print(f"Error: file not found: {audio_path}", file=sys.stderr)
sys.exit(1)
live_callback = None
if args.live:
def live_callback(data):
lines = data.get("lines", [])
buf = data.get("buffer_transcription", "")
parts = [l["text"] for l in lines if l.get("text")]
if buf:
parts.append(f"[{buf}]")
if parts:
print("\r" + " ".join(parts), end="", flush=True)
# Build URL with query parameters for language and mode
url = args.url
params = []
if args.language:
params.append(f"language={args.language}")
if args.diff:
params.append("mode=diff")
if params:
sep = "&" if "?" in url else "?"
url = f"{url}{sep}{'&'.join(params)}"
result = asyncio.run(transcribe_audio(
audio_path=str(audio_path),
url=url,
chunk_duration=args.chunk_duration,
speed=args.speed,
timeout=args.timeout,
on_response=live_callback,
mode="diff" if args.diff else "full",
))
if args.live:
print() # newline after live output
_print_result(result, output_json=args.json)
if __name__ == "__main__":
main()
================================================
FILE: whisperlivekit/test_data.py
================================================
"""Standard test audio samples for evaluating the WhisperLiveKit pipeline.
Downloads curated samples from public ASR datasets (LibriSpeech, AMI)
and caches them locally. Each sample includes the audio file path,
ground truth transcript, speaker info, and timing metadata.
Usage::
from whisperlivekit.test_data import get_samples, get_sample
# Download all standard test samples (first call downloads, then cached)
samples = get_samples()
for s in samples:
print(f"{s.name}: {s.duration:.1f}s, {s.n_speakers} speaker(s)")
print(f" Reference: {s.reference[:60]}...")
# Use with TestHarness
from whisperlivekit.test_harness import TestHarness
async with TestHarness(model_size="base", lan="en") as h:
sample = get_sample("librispeech_short")
await h.feed(sample.path, speed=0)
result = await h.finish()
print(f"WER: {result.wer(sample.reference):.2%}")
Requires: pip install whisperlivekit[test] (installs 'datasets' and 'librosa')
"""
import json
import logging
import wave
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List
import numpy as np
logger = logging.getLogger(__name__)
CACHE_DIR = Path.home() / ".cache" / "whisperlivekit" / "test_data"
METADATA_FILE = "metadata.json"
@dataclass
class TestSample:
"""A test audio sample with ground truth metadata."""
name: str
path: str # absolute path to WAV file
reference: str # ground truth transcript
duration: float # audio duration in seconds
sample_rate: int = 16000
n_speakers: int = 1
language: str = "en"
source: str = "" # dataset name
# Per-utterance ground truth for multi-speaker: [(start, end, speaker, text), ...]
utterances: List[Dict] = field(default_factory=list)
@property
def has_timestamps(self) -> bool:
return len(self.utterances) > 0
def _save_wav(path: Path, audio: np.ndarray, sample_rate: int = 16000) -> None:
"""Save numpy audio array as 16-bit PCM WAV."""
# Ensure mono
if audio.ndim > 1:
audio = audio.mean(axis=-1)
# Normalize to int16 range
if audio.dtype in (np.float32, np.float64):
audio = np.clip(audio, -1.0, 1.0)
audio = (audio * 32767).astype(np.int16)
elif audio.dtype != np.int16:
audio = audio.astype(np.int16)
path.parent.mkdir(parents=True, exist_ok=True)
with wave.open(str(path), "w") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(audio.tobytes())
def _load_metadata() -> Dict:
"""Load cached metadata if it exists."""
meta_path = CACHE_DIR / METADATA_FILE
if meta_path.exists():
return json.loads(meta_path.read_text())
return {}
def _save_metadata(meta: Dict) -> None:
CACHE_DIR.mkdir(parents=True, exist_ok=True)
(CACHE_DIR / METADATA_FILE).write_text(json.dumps(meta, indent=2))
def _ensure_datasets():
"""Check that the datasets library is available."""
try:
import datasets # noqa: F401
return True
except ImportError:
raise ImportError(
"The 'datasets' package is required for test data download. "
"Install it with: pip install whisperlivekit[test]"
)
def _decode_audio(audio_bytes: bytes) -> tuple:
"""Decode audio bytes using soundfile (avoids torchcodec dependency).
Returns:
(audio_array, sample_rate) — float32 numpy array and int sample rate.
"""
import io
import soundfile as sf
audio_array, sr = sf.read(io.BytesIO(audio_bytes), dtype="float32")
return np.array(audio_array, dtype=np.float32), sr
# ---------------------------------------------------------------------------
# Dataset-specific download functions
# ---------------------------------------------------------------------------
def _download_librispeech_samples(n_samples: int = 3) -> List[Dict]:
"""Download short samples from LibriSpeech test-clean."""
_ensure_datasets()
import datasets.config
datasets.config.TORCHCODEC_AVAILABLE = False
from datasets import Audio, load_dataset
logger.info("Downloading LibriSpeech test-clean samples (streaming)...")
ds = load_dataset(
"openslr/librispeech_asr",
"clean",
split="test",
streaming=True,
)
ds = ds.cast_column("audio", Audio(decode=False))
samples = []
for i, item in enumerate(ds):
if i >= n_samples:
break
audio_array, sr = _decode_audio(item["audio"]["bytes"])
duration = len(audio_array) / sr
text = item["text"]
sample_id = item.get("id", f"librispeech_{i}")
# Save WAV
wav_name = f"librispeech_{i}.wav"
wav_path = CACHE_DIR / wav_name
_save_wav(wav_path, audio_array, sr)
# Name: first sample is "librispeech_short", rest are numbered
name = "librispeech_short" if i == 0 else f"librispeech_{i}"
samples.append({
"name": name,
"file": wav_name,
"reference": text,
"duration": round(duration, 2),
"sample_rate": sr,
"n_speakers": 1,
"language": "en",
"source": "openslr/librispeech_asr (test-clean)",
"source_id": str(sample_id),
"utterances": [],
})
logger.info(
" [%d] %.1fs - %s",
i, duration, text[:60] + ("..." if len(text) > 60 else ""),
)
return samples
def _download_ami_sample() -> List[Dict]:
"""Download one AMI meeting segment with multiple speakers."""
_ensure_datasets()
import datasets.config
datasets.config.TORCHCODEC_AVAILABLE = False
from datasets import Audio, load_dataset
logger.info("Downloading AMI meeting test sample (streaming)...")
# Use the edinburghcstr/ami version which has pre-segmented utterances
# with speaker_id, begin_time, end_time, text
ds = load_dataset(
"edinburghcstr/ami",
"ihm",
split="test",
streaming=True,
)
ds = ds.cast_column("audio", Audio(decode=False))
# Collect utterances from one meeting
meeting_utterances = []
meeting_id = None
audio_arrays = []
sample_rate = None
for item in ds:
mid = item.get("meeting_id", "unknown")
# Take the first meeting only
if meeting_id is None:
meeting_id = mid
elif mid != meeting_id:
# We've moved to a different meeting, stop
break
audio_array, sr = _decode_audio(item["audio"]["bytes"])
sample_rate = sr
meeting_utterances.append({
"start": round(item.get("begin_time", 0.0), 2),
"end": round(item.get("end_time", 0.0), 2),
"speaker": item.get("speaker_id", "unknown"),
"text": item.get("text", ""),
})
audio_arrays.append(audio_array)
# Limit to reasonable size (~60s of utterances)
total_dur = sum(u["end"] - u["start"] for u in meeting_utterances)
if total_dur > 60:
break
if not audio_arrays:
logger.warning("No AMI samples found")
return []
# Concatenate all utterance audio
full_audio = np.concatenate(audio_arrays)
duration = len(full_audio) / sample_rate
# Build reference text
speakers = set(u["speaker"] for u in meeting_utterances)
reference = " ".join(u["text"] for u in meeting_utterances if u["text"])
wav_name = "ami_meeting.wav"
wav_path = CACHE_DIR / wav_name
_save_wav(wav_path, full_audio, sample_rate)
logger.info(
" AMI meeting %s: %.1fs, %d speakers, %d utterances",
meeting_id, duration, len(speakers), len(meeting_utterances),
)
return [{
"name": "ami_meeting",
"file": wav_name,
"reference": reference,
"duration": round(duration, 2),
"sample_rate": sample_rate,
"n_speakers": len(speakers),
"language": "en",
"source": f"edinburghcstr/ami (ihm, meeting {meeting_id})",
"source_id": meeting_id,
"utterances": meeting_utterances,
}]
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def download_test_samples(force: bool = False) -> List[TestSample]:
"""Download standard test audio samples.
Downloads samples from LibriSpeech (clean single-speaker) and
AMI (multi-speaker meetings) on first call. Subsequent calls
return cached data.
Args:
force: Re-download even if cached.
Returns:
List of TestSample objects ready for use with TestHarness.
"""
meta = _load_metadata()
if meta.get("samples") and not force:
# Check all files still exist
all_exist = all(
(CACHE_DIR / s["file"]).exists()
for s in meta["samples"]
)
if all_exist:
return _meta_to_samples(meta["samples"])
logger.info("Downloading test samples to %s ...", CACHE_DIR)
CACHE_DIR.mkdir(parents=True, exist_ok=True)
all_samples = []
try:
all_samples.extend(_download_librispeech_samples(n_samples=3))
except Exception as e:
logger.warning("Failed to download LibriSpeech samples: %s", e)
try:
all_samples.extend(_download_ami_sample())
except Exception as e:
logger.warning("Failed to download AMI sample: %s", e)
if not all_samples:
raise RuntimeError(
"Failed to download any test samples. "
"Check your internet connection and ensure 'datasets' is installed: "
"pip install whisperlivekit[test]"
)
_save_metadata({"samples": all_samples})
logger.info("Downloaded %d test samples to %s", len(all_samples), CACHE_DIR)
return _meta_to_samples(all_samples)
def get_samples() -> List[TestSample]:
"""Get standard test samples (downloads on first call)."""
return download_test_samples()
def get_sample(name: str) -> TestSample:
"""Get a specific test sample by name.
Available names: 'librispeech_short', 'librispeech_1', 'librispeech_2',
'ami_meeting'.
Raises:
KeyError: If the sample name is not found.
"""
samples = get_samples()
for s in samples:
if s.name == name:
return s
available = [s.name for s in samples]
raise KeyError(f"Sample '{name}' not found. Available: {available}")
def list_sample_names() -> List[str]:
"""List names of available test samples (downloads if needed)."""
return [s.name for s in get_samples()]
def _meta_to_samples(meta_list: List[Dict]) -> List[TestSample]:
"""Convert metadata dicts to TestSample objects."""
samples = []
for m in meta_list:
samples.append(TestSample(
name=m["name"],
path=str(CACHE_DIR / m["file"]),
reference=m["reference"],
duration=m["duration"],
sample_rate=m.get("sample_rate", 16000),
n_speakers=m.get("n_speakers", 1),
language=m.get("language", "en"),
source=m.get("source", ""),
utterances=m.get("utterances", []),
))
return samples
================================================
FILE: whisperlivekit/test_harness.py
================================================
"""In-process testing harness for the full WhisperLiveKit pipeline.
Wraps AudioProcessor to provide a controllable, observable interface
for testing transcription, diarization, silence detection, and timing
without needing a running server or WebSocket connection.
Designed for use by AI agents: feed audio with timeline control,
inspect state at any point, pause/resume to test silence detection,
cut to test abrupt termination.
Usage::
import asyncio
from whisperlivekit.test_harness import TestHarness
async def main():
async with TestHarness(model_size="base", lan="en") as h:
# Load audio with timeline control
player = h.load_audio("interview.wav")
# Play first 5 seconds at real-time speed
await player.play(5.0, speed=1.0)
print(h.state.text) # Check what's transcribed so far
# Pause for 7 seconds (triggers silence detection)
await h.pause(7.0, speed=1.0)
assert h.state.has_silence
# Resume playback
await player.play(5.0, speed=1.0)
# Finish and evaluate
result = await h.finish()
print(f"WER: {result.wer('expected transcription'):.2%}")
print(f"Speakers: {result.speakers}")
print(f"Silence segments: {len(result.silence_segments)}")
# Inspect historical state at specific audio position
snap = h.snapshot_at(3.0)
print(f"At 3s: '{snap.text}'")
asyncio.run(main())
"""
import asyncio
import logging
import subprocess
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
from whisperlivekit.timed_objects import FrontData
logger = logging.getLogger(__name__)
# Engine cache: avoids reloading models when switching backends in tests.
# Key is a frozen config tuple, value is the TranscriptionEngine instance.
_engine_cache: Dict[Tuple, "Any"] = {}
SAMPLE_RATE = 16000
BYTES_PER_SAMPLE = 2 # s16le
def _parse_time(time_str: str) -> float:
"""Parse 'H:MM:SS.cc' timestamp string to seconds."""
parts = time_str.split(":")
if len(parts) == 3:
return int(parts[0]) * 3600 + int(parts[1]) * 60 + float(parts[2])
if len(parts) == 2:
return int(parts[0]) * 60 + float(parts[1])
return float(parts[0])
def load_audio_pcm(audio_path: str, sample_rate: int = SAMPLE_RATE) -> bytes:
"""Load any audio file and convert to PCM s16le mono via ffmpeg."""
cmd = [
"ffmpeg", "-i", str(audio_path),
"-f", "s16le", "-acodec", "pcm_s16le",
"-ar", str(sample_rate), "-ac", "1",
"-loglevel", "error",
"pipe:1",
]
proc = subprocess.run(cmd, capture_output=True)
if proc.returncode != 0:
raise RuntimeError(f"ffmpeg conversion failed: {proc.stderr.decode().strip()}")
if not proc.stdout:
raise RuntimeError(f"ffmpeg produced no output for {audio_path}")
return proc.stdout
# ---------------------------------------------------------------------------
# TestState — observable transcription state
# ---------------------------------------------------------------------------
@dataclass
class TestState:
"""Observable transcription state at a point in time.
Provides accessors for inspecting lines, buffers, speakers, timestamps,
silence segments, and computing evaluation metrics like WER.
All time-based queries accept seconds as floats.
"""
lines: List[Dict[str, Any]] = field(default_factory=list)
buffer_transcription: str = ""
buffer_diarization: str = ""
buffer_translation: str = ""
remaining_time_transcription: float = 0.0
remaining_time_diarization: float = 0.0
audio_position: float = 0.0
status: str = ""
error: str = ""
@classmethod
def from_front_data(cls, front_data: FrontData, audio_position: float = 0.0) -> "TestState":
d = front_data.to_dict()
return cls(
lines=d.get("lines", []),
buffer_transcription=d.get("buffer_transcription", ""),
buffer_diarization=d.get("buffer_diarization", ""),
buffer_translation=d.get("buffer_translation", ""),
remaining_time_transcription=d.get("remaining_time_transcription", 0),
remaining_time_diarization=d.get("remaining_time_diarization", 0),
audio_position=audio_position,
status=d.get("status", ""),
error=d.get("error", ""),
)
# ── Text accessors ──
@property
def text(self) -> str:
"""Full transcription: committed lines + buffer."""
parts = [l["text"] for l in self.lines if l.get("text")]
if self.buffer_transcription:
parts.append(self.buffer_transcription)
return " ".join(parts)
@property
def committed_text(self) -> str:
"""Only committed (finalized) lines, no buffer."""
return " ".join(l["text"] for l in self.lines if l.get("text"))
@property
def committed_word_count(self) -> int:
"""Number of words in committed lines."""
t = self.committed_text
return len(t.split()) if t.strip() else 0
@property
def buffer_word_count(self) -> int:
"""Number of words in the unconfirmed buffer."""
return len(self.buffer_transcription.split()) if self.buffer_transcription.strip() else 0
# ── Speaker accessors ──
@property
def speakers(self) -> Set[int]:
"""Set of speaker IDs (excluding silence marker -2)."""
return {l["speaker"] for l in self.lines if l.get("speaker", 0) > 0}
@property
def n_speakers(self) -> int:
return len(self.speakers)
def speaker_at(self, time_s: float) -> Optional[int]:
"""Speaker ID at the given timestamp, or None if no segment covers it."""
line = self.line_at(time_s)
return line["speaker"] if line else None
def speakers_in(self, start_s: float, end_s: float) -> Set[int]:
"""All speaker IDs active in the time range (excluding silence -2)."""
return {
l.get("speaker")
for l in self.lines_between(start_s, end_s)
if l.get("speaker", 0) > 0
}
@property
def speaker_timeline(self) -> List[Dict[str, Any]]:
"""Timeline: [{"start": float, "end": float, "speaker": int}] for all lines."""
return [
{
"start": _parse_time(l.get("start", "0:00:00")),
"end": _parse_time(l.get("end", "0:00:00")),
"speaker": l.get("speaker", -1),
}
for l in self.lines
]
@property
def n_speaker_changes(self) -> int:
"""Number of speaker transitions (excluding silence segments)."""
speech = [s for s in self.speaker_timeline if s["speaker"] != -2]
return sum(
1 for i in range(1, len(speech))
if speech[i]["speaker"] != speech[i - 1]["speaker"]
)
# ── Silence accessors ──
@property
def has_silence(self) -> bool:
"""Whether any silence segment (speaker=-2) exists."""
return any(l.get("speaker") == -2 for l in self.lines)
@property
def silence_segments(self) -> List[Dict[str, Any]]:
"""All silence segments (raw line dicts)."""
return [l for l in self.lines if l.get("speaker") == -2]
def silence_at(self, time_s: float) -> bool:
"""True if time_s falls within a silence segment."""
line = self.line_at(time_s)
return line is not None and line.get("speaker") == -2
# ── Line / segment accessors ──
@property
def speech_lines(self) -> List[Dict[str, Any]]:
"""Lines excluding silence segments."""
return [l for l in self.lines if l.get("speaker", 0) != -2 and l.get("text")]
def line_at(self, time_s: float) -> Optional[Dict[str, Any]]:
"""Find the line covering the given timestamp (seconds)."""
for line in self.lines:
start = _parse_time(line.get("start", "0:00:00"))
end = _parse_time(line.get("end", "0:00:00"))
if start <= time_s <= end:
return line
return None
def text_at(self, time_s: float) -> Optional[str]:
"""Text of the segment covering the given timestamp."""
line = self.line_at(time_s)
return line["text"] if line else None
def lines_between(self, start_s: float, end_s: float) -> List[Dict[str, Any]]:
"""All lines overlapping the time range [start_s, end_s]."""
result = []
for line in self.lines:
ls = _parse_time(line.get("start", "0:00:00"))
le = _parse_time(line.get("end", "0:00:00"))
if le >= start_s and ls <= end_s:
result.append(line)
return result
def text_between(self, start_s: float, end_s: float) -> str:
"""Concatenated text of all lines overlapping the time range."""
return " ".join(
l["text"] for l in self.lines_between(start_s, end_s)
if l.get("text")
)
# ── Evaluation ──
def wer(self, reference: str) -> float:
"""Word Error Rate of committed text against reference.
Returns:
WER as a float (0.0 = perfect, 1.0 = 100% error rate).
"""
from whisperlivekit.metrics import compute_wer
result = compute_wer(reference, self.committed_text)
return result["wer"]
def wer_detailed(self, reference: str) -> Dict:
"""Full WER breakdown: substitutions, insertions, deletions, etc."""
from whisperlivekit.metrics import compute_wer
return compute_wer(reference, self.committed_text)
# ── Timing validation ──
@property
def timestamps(self) -> List[Dict[str, Any]]:
"""All line timestamps as [{"start": float, "end": float, "speaker": int, "text": str}]."""
result = []
for line in self.lines:
result.append({
"start": _parse_time(line.get("start", "0:00:00")),
"end": _parse_time(line.get("end", "0:00:00")),
"speaker": line.get("speaker", -1),
"text": line.get("text", ""),
})
return result
@property
def timing_valid(self) -> bool:
"""All timestamps have start <= end and no negative values."""
for ts in self.timestamps:
if ts["start"] < 0 or ts["end"] < 0:
return False
if ts["end"] < ts["start"]:
return False
return True
@property
def timing_monotonic(self) -> bool:
"""Line start times are non-decreasing."""
stamps = self.timestamps
for i in range(1, len(stamps)):
if stamps[i]["start"] < stamps[i - 1]["start"]:
return False
return True
def timing_errors(self) -> List[str]:
"""Human-readable list of timing issues found."""
errors = []
stamps = self.timestamps
for i, ts in enumerate(stamps):
if ts["start"] < 0:
errors.append(f"Line {i}: negative start {ts['start']:.2f}s")
if ts["end"] < 0:
errors.append(f"Line {i}: negative end {ts['end']:.2f}s")
if ts["end"] < ts["start"]:
errors.append(
f"Line {i}: end ({ts['end']:.2f}s) < start ({ts['start']:.2f}s)"
)
for i in range(1, len(stamps)):
if stamps[i]["start"] < stamps[i - 1]["start"]:
errors.append(
f"Line {i}: start ({stamps[i]['start']:.2f}s) < previous start "
f"({stamps[i-1]['start']:.2f}s) — non-monotonic"
)
return errors
# ---------------------------------------------------------------------------
# AudioPlayer — timeline control for a loaded audio file
# ---------------------------------------------------------------------------
class AudioPlayer:
"""Controls playback of a loaded audio file through the pipeline.
Tracks position in the audio, enabling play/pause/resume patterns::
player = h.load_audio("speech.wav")
await player.play(3.0) # Play first 3 seconds
await h.pause(7.0) # 7s silence (triggers detection)
await player.play(5.0) # Play next 5 seconds
await player.play() # Play all remaining audio
Args:
harness: The TestHarness instance.
pcm_data: Raw PCM s16le 16kHz mono bytes.
sample_rate: Audio sample rate (default 16000).
"""
def __init__(self, harness: "TestHarness", pcm_data: bytes, sample_rate: int = SAMPLE_RATE):
self._harness = harness
self._pcm = pcm_data
self._sr = sample_rate
self._bps = sample_rate * BYTES_PER_SAMPLE # bytes per second
self._pos = 0 # current position in bytes
@property
def position(self) -> float:
"""Current playback position in seconds."""
return self._pos / self._bps
@property
def duration(self) -> float:
"""Total audio duration in seconds."""
return len(self._pcm) / self._bps
@property
def remaining(self) -> float:
"""Remaining audio in seconds."""
return max(0.0, (len(self._pcm) - self._pos) / self._bps)
@property
def done(self) -> bool:
"""True if all audio has been played."""
return self._pos >= len(self._pcm)
async def play(
self,
duration_s: Optional[float] = None,
speed: float = 1.0,
chunk_duration: float = 0.5,
) -> None:
"""Play audio from the current position.
Args:
duration_s: Seconds of audio to play. None = all remaining.
speed: 1.0 = real-time, 0 = instant, >1 = faster.
chunk_duration: Size of each chunk fed to the pipeline (seconds).
"""
if duration_s is None:
end_pos = len(self._pcm)
else:
end_pos = min(self._pos + int(duration_s * self._bps), len(self._pcm))
# Align to sample boundary
end_pos = (end_pos // BYTES_PER_SAMPLE) * BYTES_PER_SAMPLE
if end_pos <= self._pos:
return
segment = self._pcm[self._pos:end_pos]
self._pos = end_pos
await self._harness.feed_pcm(segment, speed=speed, chunk_duration=chunk_duration)
async def play_until(
self,
time_s: float,
speed: float = 1.0,
chunk_duration: float = 0.5,
) -> None:
"""Play until reaching time_s in the audio timeline."""
target = min(int(time_s * self._bps), len(self._pcm))
target = (target // BYTES_PER_SAMPLE) * BYTES_PER_SAMPLE
if target <= self._pos:
return
segment = self._pcm[self._pos:target]
self._pos = target
await self._harness.feed_pcm(segment, speed=speed, chunk_duration=chunk_duration)
def seek(self, time_s: float) -> None:
"""Move the playback cursor without feeding audio."""
pos = int(time_s * self._bps)
pos = (pos // BYTES_PER_SAMPLE) * BYTES_PER_SAMPLE
self._pos = max(0, min(pos, len(self._pcm)))
def reset(self) -> None:
"""Reset to the beginning of the audio."""
self._pos = 0
# ---------------------------------------------------------------------------
# TestHarness — pipeline controller
# ---------------------------------------------------------------------------
class TestHarness:
"""In-process testing harness for the full WhisperLiveKit pipeline.
Use as an async context manager. Provides methods to feed audio,
pause/resume, inspect state, and evaluate results.
Methods:
load_audio(path) → AudioPlayer with play/seek controls
feed(path, speed) → feed entire audio file (simple mode)
pause(duration) → inject silence (triggers detection if > 5s)
drain(seconds) → let pipeline catch up
finish() → flush and return final state
cut() → abrupt stop, return partial state
wait_for(pred) → wait for condition on state
State inspection:
.state → current TestState
.history → all historical states
.snapshot_at(t) → state at audio position t
.metrics → SessionMetrics (latency, RTF, etc.)
Args:
All keyword arguments passed to AudioProcessor.
Common: model_size, lan, backend, diarization, vac.
"""
def __init__(self, **kwargs: Any):
kwargs.setdefault("pcm_input", True)
self._engine_kwargs = kwargs
self._processor = None
self._results_gen = None
self._collect_task = None
self._state = TestState()
self._audio_position = 0.0
self._history: List[TestState] = []
self._on_update: Optional[Callable[[TestState], None]] = None
async def __aenter__(self) -> "TestHarness":
from whisperlivekit.audio_processor import AudioProcessor
from whisperlivekit.core import TranscriptionEngine
# Cache engines by config to avoid reloading models when switching
# backends between tests. The singleton is reset only when the
# requested config doesn't match any cached engine.
cache_key = tuple(sorted(self._engine_kwargs.items()))
if cache_key not in _engine_cache:
TranscriptionEngine.reset()
_engine_cache[cache_key] = TranscriptionEngine(**self._engine_kwargs)
engine = _engine_cache[cache_key]
self._processor = AudioProcessor(transcription_engine=engine)
self._results_gen = await self._processor.create_tasks()
self._collect_task = asyncio.create_task(self._collect_results())
return self
async def __aexit__(self, *exc: Any) -> None:
if self._processor:
await self._processor.cleanup()
if self._collect_task and not self._collect_task.done():
self._collect_task.cancel()
try:
await self._collect_task
except asyncio.CancelledError:
pass
async def _collect_results(self) -> None:
"""Background task: consume results from the pipeline."""
try:
async for front_data in self._results_gen:
self._state = TestState.from_front_data(front_data, self._audio_position)
self._history.append(self._state)
if self._on_update:
self._on_update(self._state)
except asyncio.CancelledError:
pass
except Exception as e:
logger.warning("Result collector ended: %s", e)
# ── Properties ──
@property
def state(self) -> TestState:
"""Current transcription state (updated live as results arrive)."""
return self._state
@property
def history(self) -> List[TestState]:
"""All states received so far, in order."""
return self._history
@property
def audio_position(self) -> float:
"""How many seconds of audio have been fed so far."""
return self._audio_position
@property
def metrics(self):
"""Pipeline's SessionMetrics (latency, RTF, token counts, etc.)."""
if self._processor:
return self._processor.metrics
return None
def on_update(self, callback: Callable[[TestState], None]) -> None:
"""Register a callback invoked on each new state update."""
self._on_update = callback
# ── Audio loading and feeding ──
def load_audio(self, source) -> AudioPlayer:
"""Load audio and return a player with timeline control.
Args:
source: Path to audio file (str), or a TestSample with .path attribute.
Returns:
AudioPlayer with play/play_until/seek/reset methods.
"""
path = source.path if hasattr(source, "path") else str(source)
pcm = load_audio_pcm(path)
return AudioPlayer(self, pcm)
async def feed(
self,
audio_path: str,
speed: float = 1.0,
chunk_duration: float = 0.5,
) -> None:
"""Feed an entire audio file to the pipeline (simple mode).
For timeline control (play/pause/resume), use load_audio() instead.
Args:
audio_path: Path to any audio file ffmpeg can decode.
speed: Playback speed (1.0 = real-time, 0 = instant).
chunk_duration: Size of each PCM chunk in seconds.
"""
pcm = load_audio_pcm(audio_path)
await self.feed_pcm(pcm, speed=speed, chunk_duration=chunk_duration)
async def feed_pcm(
self,
pcm_data: bytes,
speed: float = 1.0,
chunk_duration: float = 0.5,
) -> None:
"""Feed raw PCM s16le 16kHz mono bytes to the pipeline.
Args:
pcm_data: Raw PCM bytes.
speed: Playback speed multiplier.
chunk_duration: Duration of each chunk sent (seconds).
"""
chunk_bytes = int(chunk_duration * SAMPLE_RATE * BYTES_PER_SAMPLE)
offset = 0
while offset < len(pcm_data):
end = min(offset + chunk_bytes, len(pcm_data))
await self._processor.process_audio(pcm_data[offset:end])
chunk_seconds = (end - offset) / (SAMPLE_RATE * BYTES_PER_SAMPLE)
self._audio_position += chunk_seconds
offset = end
if speed > 0:
await asyncio.sleep(chunk_duration / speed)
# ── Pause / silence ──
async def pause(self, duration_s: float, speed: float = 1.0) -> None:
"""Inject silence to simulate a pause in speech.
Pauses > 5s trigger silence segment detection (MIN_DURATION_REAL_SILENCE).
Pauses < 5s are treated as brief gaps and produce no silence segment
(provided speech resumes afterward).
Args:
duration_s: Duration of silence in seconds.
speed: Playback speed (1.0 = real-time, 0 = instant).
"""
silent_pcm = bytes(int(duration_s * SAMPLE_RATE * BYTES_PER_SAMPLE))
await self.feed_pcm(silent_pcm, speed=speed)
async def silence(self, duration_s: float, speed: float = 1.0) -> None:
"""Alias for pause(). Inject silence for the given duration."""
await self.pause(duration_s, speed=speed)
# ── Waiting ──
async def wait_for(
self,
predicate: Callable[[TestState], bool],
timeout: float = 30.0,
poll_interval: float = 0.1,
) -> TestState:
"""Wait until predicate(state) returns True.
Raises:
TimeoutError: If the condition is not met within timeout.
"""
deadline = asyncio.get_event_loop().time() + timeout
while asyncio.get_event_loop().time() < deadline:
if predicate(self._state):
return self._state
await asyncio.sleep(poll_interval)
raise TimeoutError(
f"Condition not met within {timeout}s. "
f"Current state: {len(self._state.lines)} lines, "
f"buffer='{self._state.buffer_transcription[:50]}', "
f"audio_pos={self._audio_position:.1f}s"
)
async def wait_for_text(self, timeout: float = 30.0) -> TestState:
"""Wait until any transcription text appears."""
return await self.wait_for(lambda s: s.text.strip(), timeout=timeout)
async def wait_for_lines(self, n: int = 1, timeout: float = 30.0) -> TestState:
"""Wait until at least n committed speech lines exist."""
return await self.wait_for(lambda s: len(s.speech_lines) >= n, timeout=timeout)
async def wait_for_silence(self, timeout: float = 30.0) -> TestState:
"""Wait until a silence segment is detected."""
return await self.wait_for(lambda s: s.has_silence, timeout=timeout)
async def wait_for_speakers(self, n: int = 2, timeout: float = 30.0) -> TestState:
"""Wait until at least n distinct speakers are detected."""
return await self.wait_for(lambda s: s.n_speakers >= n, timeout=timeout)
async def drain(self, seconds: float = 2.0) -> None:
"""Let the pipeline process without feeding audio.
Useful after feeding audio to allow the ASR backend to catch up.
"""
await asyncio.sleep(seconds)
# ── Finishing ──
async def finish(self, timeout: float = 30.0) -> TestState:
"""Signal end of audio and wait for pipeline to flush all results.
Returns:
Final TestState with all committed lines and empty buffer.
"""
await self._processor.process_audio(b"")
if self._collect_task:
try:
await asyncio.wait_for(self._collect_task, timeout=timeout)
except asyncio.TimeoutError:
logger.warning("Timed out waiting for pipeline to finish after %.0fs", timeout)
except asyncio.CancelledError:
pass
return self._state
async def cut(self, timeout: float = 5.0) -> TestState:
"""Abrupt audio stop — signal EOF and return current state quickly.
Simulates user closing the connection mid-speech. Sends EOF but
uses a short timeout, so partial results are returned even if
the pipeline hasn't fully flushed.
Returns:
TestState with whatever has been processed so far.
"""
await self._processor.process_audio(b"")
if self._collect_task:
try:
await asyncio.wait_for(self._collect_task, timeout=timeout)
except (asyncio.TimeoutError, asyncio.CancelledError):
pass
return self._state
# ── History inspection ──
def snapshot_at(self, audio_time: float) -> Optional[TestState]:
"""Find the historical state closest to when audio_time was reached.
Args:
audio_time: Audio position in seconds.
Returns:
The TestState captured at that point, or None if no history.
"""
if not self._history:
return None
best = None
best_diff = float("inf")
for s in self._history:
diff = abs(s.audio_position - audio_time)
if diff < best_diff:
best_diff = diff
best = s
return best
# ── Debug ──
def print_state(self) -> None:
"""Print current state to stdout for debugging."""
s = self._state
print(f"--- Audio: {self._audio_position:.1f}s | Status: {s.status} ---")
for line in s.lines:
speaker = line.get("speaker", "?")
text = line.get("text", "")
start = line.get("start", "")
end = line.get("end", "")
tag = "SILENCE" if speaker == -2 else f"Speaker {speaker}"
print(f" [{start} -> {end}] {tag}: {text}")
if s.buffer_transcription:
print(f" [buffer] {s.buffer_transcription}")
if s.buffer_diarization:
print(f" [diar buffer] {s.buffer_diarization}")
print(f" Speakers: {s.speakers or 'none'} | Silence: {s.has_silence}")
print()
================================================
FILE: whisperlivekit/thread_safety.py
================================================
"""
Thread Safety Configuration for WhisperLiveKit
This module provides thread safety configuration and utilities.
Environment Variables:
WHISPERLIVEKIT_MODEL_LOCK: Enable/disable model locking (default: 1)
Set to "0" to disable for single-connection deployments
WHISPERLIVEKIT_LOCK_TIMEOUT: Lock acquisition timeout in seconds (default: 30)
Usage:
# Enable model locking (default)
export WHISPERLIVEKIT_MODEL_LOCK=1
# Disable for single-connection deployment
export WHISPERLIVEKIT_MODEL_LOCK=0
# Custom timeout
export WHISPERLIVEKIT_LOCK_TIMEOUT=60
"""
import logging
import os
import threading
logger = logging.getLogger(__name__)
# Configuration
USE_MODEL_LOCK = os.environ.get("WHISPERLIVEKIT_MODEL_LOCK", "1") == "1"
LOCK_TIMEOUT = float(os.environ.get("WHISPERLIVEKIT_LOCK_TIMEOUT", "30.0"))
# Global model lock
_model_lock = threading.Lock()
# Log configuration on import
if USE_MODEL_LOCK:
logger.info(f"Model locking ENABLED (timeout: {LOCK_TIMEOUT}s)")
logger.info("For single-connection deployments, set WHISPERLIVEKIT_MODEL_LOCK=0")
else:
logger.warning("Model locking DISABLED - only safe for single-connection deployments")
def get_model_lock():
"""Get the global model lock instance"""
return _model_lock
def acquire_model_lock(timeout=None):
"""
Acquire model lock with timeout.
Args:
timeout: Lock acquisition timeout (default: use LOCK_TIMEOUT)
Returns:
bool: True if lock acquired, False on timeout
"""
if not USE_MODEL_LOCK:
return True
timeout = timeout or LOCK_TIMEOUT
acquired = _model_lock.acquire(timeout=timeout)
if not acquired:
logger.error(f"Failed to acquire model lock within {timeout}s")
return acquired
def release_model_lock():
"""Release model lock"""
if not USE_MODEL_LOCK:
return
try:
_model_lock.release()
except RuntimeError:
# Lock not held - this is fine
pass
class ModelLockContext:
"""Context manager for model lock"""
def __init__(self, timeout=None):
self.timeout = timeout
self.acquired = False
def __enter__(self):
self.acquired = acquire_model_lock(self.timeout)
return self.acquired
def __exit__(self, exc_type, exc_val, exc_tb):
if self.acquired:
release_model_lock()
return False
# Concurrency recommendations
RECOMMENDED_CONNECTIONS_PER_WORKER = 1 if USE_MODEL_LOCK else 1
RECOMMENDED_WORKERS = 4
def print_deployment_recommendations():
"""Print recommended deployment configuration"""
print("\n" + "="*60)
print("WhisperLiveKit Deployment Recommendations")
print("="*60)
if USE_MODEL_LOCK:
print("⚠️ Model locking is ENABLED")
print(" This serializes inference across connections.")
print()
print("Recommended deployment:")
print(f" gunicorn -w {RECOMMENDED_WORKERS} \\")
print(" -k uvicorn.workers.UvicornWorker \\")
print(" --worker-connections 1 \\")
print(" whisperlivekit.basic_server:app")
print()
print("Expected capacity:")
print(f" - {RECOMMENDED_WORKERS} concurrent users (1 per worker)")
print(f" - Memory: ~{RECOMMENDED_WORKERS}x model size")
else:
print("✅ Model locking is DISABLED")
print(" ⚠️ ONLY safe for single-connection deployments")
print()
print("Recommended deployment:")
print(" uvicorn whisperlivekit.basic_server:app \\")
print(" --host 0.0.0.0 --port 8000 \\")
print(" --workers 1")
print()
print("Expected capacity:")
print(" - 1 concurrent user only")
print("="*60 + "\n")
if __name__ == "__main__":
print_deployment_recommendations()
================================================
FILE: whisperlivekit/timed_objects.py
================================================
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
PUNCTUATION_MARKS = {'.', '!', '?', '。', '!', '?'}
def format_time(seconds: float) -> str:
"""Format seconds as H:MM:SS.cc (centisecond precision)."""
total_cs = int(round(seconds * 100))
cs = total_cs % 100
total_s = total_cs // 100
s = total_s % 60
total_m = total_s // 60
m = total_m % 60
h = total_m // 60
return f"{h}:{m:02d}:{s:02d}.{cs:02d}"
@dataclass
class Timed:
start: Optional[float] = 0
end: Optional[float] = 0
@dataclass
class TimedText(Timed):
text: Optional[str] = ''
speaker: Optional[int] = -1
detected_language: Optional[str] = None
def has_punctuation(self) -> bool:
return any(char in PUNCTUATION_MARKS for char in self.text.strip())
def is_within(self, other: 'TimedText') -> bool:
return other.contains_timespan(self)
def duration(self) -> float:
return self.end - self.start
def contains_timespan(self, other: 'TimedText') -> bool:
return self.start <= other.start and self.end >= other.end
def __bool__(self) -> bool:
return bool(self.text)
def __str__(self) -> str:
return str(self.text)
@dataclass()
class ASRToken(TimedText):
probability: Optional[float] = None
def with_offset(self, offset: float) -> "ASRToken":
"""Return a new token with the time offset added."""
return ASRToken(self.start + offset, self.end + offset, self.text, self.speaker, detected_language=self.detected_language, probability=self.probability)
def is_silence(self) -> bool:
return False
@dataclass
class Sentence(TimedText):
pass
@dataclass
class Transcript(TimedText):
"""
represents a concatenation of several ASRToken
"""
@classmethod
def from_tokens(
cls,
tokens: List[ASRToken],
sep: Optional[str] = None,
offset: float = 0
) -> "Transcript":
"""Collapse multiple ASR tokens into a single transcript span."""
sep = sep if sep is not None else ' '
text = sep.join(token.text for token in tokens)
if tokens:
start = offset + tokens[0].start
end = offset + tokens[-1].end
else:
start = None
end = None
return cls(start, end, text)
@dataclass
class SpeakerSegment(Timed):
"""Represents a segment of audio attributed to a specific speaker.
No text nor probability is associated with this segment.
"""
speaker: Optional[int] = -1
pass
@dataclass
class Translation(TimedText):
pass
@dataclass
class Silence():
start: Optional[float] = None
end: Optional[float] = None
duration: Optional[float] = None
is_starting: bool = False
has_ended: bool = False
def compute_duration(self) -> Optional[float]:
if self.start is None or self.end is None:
return None
self.duration = self.end - self.start
return self.duration
def is_silence(self) -> bool:
return True
@dataclass
class Segment(TimedText):
"""Generic contiguous span built from tokens or silence markers."""
start: Optional[float]
end: Optional[float]
text: Optional[str]
speaker: Optional[str]
tokens: Optional[ASRToken] = None
translation: Optional[Translation] = None
@classmethod
def from_tokens(
cls,
tokens: List[Union[ASRToken, Silence]],
is_silence: bool = False
) -> Optional["Segment"]:
"""Return a normalized segment representing the provided tokens."""
if not tokens:
return None
start_token = tokens[0]
end_token = tokens[-1]
if is_silence:
return cls(
start=start_token.start,
end=end_token.end,
text=None,
speaker=-2
)
else:
return cls(
start=start_token.start,
end=end_token.end,
text=''.join(token.text for token in tokens),
speaker=-1,
detected_language=start_token.detected_language
)
def is_silence(self) -> bool:
"""True when this segment represents a silence gap."""
return self.speaker == -2
def to_dict(self) -> Dict[str, Any]:
"""Serialize the segment for frontend consumption."""
_dict: Dict[str, Any] = {
'speaker': int(self.speaker) if self.speaker != -1 else 1,
'text': self.text,
'start': format_time(self.start),
'end': format_time(self.end),
}
if self.translation:
_dict['translation'] = self.translation
if self.detected_language:
_dict['detected_language'] = self.detected_language
return _dict
@dataclass
class PuncSegment(Segment):
pass
class SilentSegment(Segment):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.speaker = -2
self.text = ''
@dataclass
class FrontData():
status: str = ''
error: str = ''
lines: list[Segment] = field(default_factory=list)
buffer_transcription: str = ''
buffer_diarization: str = ''
buffer_translation: str = ''
remaining_time_transcription: float = 0.
remaining_time_diarization: float = 0.
def to_dict(self) -> Dict[str, Any]:
"""Serialize the front-end data payload."""
_dict: Dict[str, Any] = {
'status': self.status,
'lines': [line.to_dict() for line in self.lines if (line.text or line.speaker == -2)],
'buffer_transcription': self.buffer_transcription,
'buffer_diarization': self.buffer_diarization,
'buffer_translation': self.buffer_translation,
'remaining_time_transcription': self.remaining_time_transcription,
'remaining_time_diarization': self.remaining_time_diarization,
}
if self.error:
_dict['error'] = self.error
return _dict
@dataclass
class ChangeSpeaker:
speaker: int
start: int
@dataclass
class State():
"""Unified state class for audio processing.
Contains both persistent state (tokens, buffers) and temporary update buffers
(new_* fields) that are consumed by TokensAlignment.
"""
# Persistent state
tokens: List[ASRToken] = field(default_factory=list)
buffer_transcription: Transcript = field(default_factory=Transcript)
end_buffer: float = 0.0
end_attributed_speaker: float = 0.0
remaining_time_transcription: float = 0.0
remaining_time_diarization: float = 0.0
# Temporary update buffers (consumed by TokensAlignment.update())
new_tokens: List[Union[ASRToken, Silence]] = field(default_factory=list)
new_translation: List[Any] = field(default_factory=list)
new_diarization: List[Any] = field(default_factory=list)
new_tokens_buffer: List[Any] = field(default_factory=list) # only when local agreement
new_translation_buffer: TimedText = field(default_factory=TimedText)
================================================
FILE: whisperlivekit/tokens_alignment.py
================================================
from time import time
from typing import Any, List, Optional, Tuple, Union
from whisperlivekit.timed_objects import (
ASRToken,
PuncSegment,
Segment,
Silence,
SilentSegment,
SpeakerSegment,
TimedText,
)
_DEFAULT_RETENTION_SECONDS: float = 300.0
class TokensAlignment:
def __init__(self, state: Any, args: Any, sep: Optional[str]) -> None:
self.state = state
self.diarization = args.diarization
self.all_tokens: List[ASRToken] = []
self.all_diarization_segments: List[SpeakerSegment] = []
self.all_translation_segments: List[Any] = []
self.new_tokens: List[ASRToken] = []
self.new_diarization: List[SpeakerSegment] = []
self.new_translation: List[Any] = []
self.new_translation_buffer: Union[TimedText, str] = TimedText()
self.new_tokens_buffer: List[Any] = []
self.sep: str = sep if sep is not None else ' '
self.beg_loop: Optional[float] = None
self.validated_segments: List[Segment] = []
self.current_line_tokens: List[ASRToken] = []
self.diarization_buffer: List[ASRToken] = []
self.last_punctuation = None
self.last_uncompleted_punc_segment: PuncSegment = None
self.unvalidated_tokens: PuncSegment = []
self._retention_seconds: float = _DEFAULT_RETENTION_SECONDS
def update(self) -> None:
"""Drain state buffers into the running alignment context."""
self.new_tokens, self.state.new_tokens = self.state.new_tokens, []
self.new_diarization, self.state.new_diarization = self.state.new_diarization, []
self.new_translation, self.state.new_translation = self.state.new_translation, []
self.new_tokens_buffer, self.state.new_tokens_buffer = self.state.new_tokens_buffer, []
self.all_tokens.extend(self.new_tokens)
self.all_diarization_segments.extend(self.new_diarization)
self.all_translation_segments.extend(self.new_translation)
self.new_translation_buffer = self.state.new_translation_buffer
def _prune(self) -> None:
"""Drop tokens/segments older than ``_retention_seconds`` from the latest token."""
if not self.all_tokens:
return
latest = self.all_tokens[-1].end
cutoff = latest - self._retention_seconds
if cutoff <= 0:
return
def _find_cutoff(items: list) -> int:
"""Return the index of the first item whose end >= cutoff."""
for i, item in enumerate(items):
if item.end >= cutoff:
return i
return len(items)
idx = _find_cutoff(self.all_tokens)
if idx:
self.all_tokens = self.all_tokens[idx:]
idx = _find_cutoff(self.all_diarization_segments)
if idx:
self.all_diarization_segments = self.all_diarization_segments[idx:]
idx = _find_cutoff(self.all_translation_segments)
if idx:
self.all_translation_segments = self.all_translation_segments[idx:]
idx = _find_cutoff(self.validated_segments)
if idx:
self.validated_segments = self.validated_segments[idx:]
def add_translation(self, segment: Segment) -> None:
"""Append translated text segments that overlap with a segment."""
if segment.translation is None:
segment.translation = ''
for ts in self.all_translation_segments:
if ts.is_within(segment):
if ts.text:
segment.translation += ts.text + self.sep
elif segment.translation:
break
def compute_punctuations_segments(self, tokens: Optional[List[ASRToken]] = None) -> List[PuncSegment]:
"""Group tokens into segments split by punctuation and explicit silence."""
segments = []
segment_start_idx = 0
for i, token in enumerate(self.all_tokens):
if token.is_silence():
previous_segment = PuncSegment.from_tokens(
tokens=self.all_tokens[segment_start_idx: i],
)
if previous_segment:
segments.append(previous_segment)
segment = PuncSegment.from_tokens(
tokens=[token],
is_silence=True
)
segments.append(segment)
segment_start_idx = i+1
else:
if token.has_punctuation():
segment = PuncSegment.from_tokens(
tokens=self.all_tokens[segment_start_idx: i+1],
)
segments.append(segment)
segment_start_idx = i+1
final_segment = PuncSegment.from_tokens(
tokens=self.all_tokens[segment_start_idx:],
)
if final_segment:
segments.append(final_segment)
return segments
def compute_new_punctuations_segments(self) -> List[PuncSegment]:
new_punc_segments = []
segment_start_idx = 0
self.unvalidated_tokens += self.new_tokens
for i, token in enumerate(self.unvalidated_tokens):
if token.is_silence():
previous_segment = PuncSegment.from_tokens(
tokens=self.unvalidated_tokens[segment_start_idx: i],
)
if previous_segment:
new_punc_segments.append(previous_segment)
segment = PuncSegment.from_tokens(
tokens=[token],
is_silence=True
)
new_punc_segments.append(segment)
segment_start_idx = i+1
else:
if token.has_punctuation():
segment = PuncSegment.from_tokens(
tokens=self.unvalidated_tokens[segment_start_idx: i+1],
)
new_punc_segments.append(segment)
segment_start_idx = i+1
self.unvalidated_tokens = self.unvalidated_tokens[segment_start_idx:]
return new_punc_segments
def concatenate_diar_segments(self) -> List[SpeakerSegment]:
"""Merge consecutive diarization slices that share the same speaker."""
if not self.all_diarization_segments:
return []
merged = [self.all_diarization_segments[0]]
for segment in self.all_diarization_segments[1:]:
if segment.speaker == merged[-1].speaker:
merged[-1].end = segment.end
else:
merged.append(segment)
return merged
@staticmethod
def intersection_duration(seg1: TimedText, seg2: TimedText) -> float:
"""Return the overlap duration between two timed segments."""
start = max(seg1.start, seg2.start)
end = min(seg1.end, seg2.end)
return max(0, end - start)
def get_lines_diarization(self) -> Tuple[List[Segment], str]:
"""Build segments when diarization is enabled and track overflow buffer."""
diarization_buffer = ''
punctuation_segments = self.compute_punctuations_segments()
diarization_segments = self.concatenate_diar_segments()
for punctuation_segment in punctuation_segments:
if not punctuation_segment.is_silence():
if diarization_segments and punctuation_segment.start >= diarization_segments[-1].end:
diarization_buffer += punctuation_segment.text
else:
max_overlap = 0.0
max_overlap_speaker = 1
for diarization_segment in diarization_segments:
intersec = self.intersection_duration(punctuation_segment, diarization_segment)
if intersec > max_overlap:
max_overlap = intersec
max_overlap_speaker = diarization_segment.speaker + 1
punctuation_segment.speaker = max_overlap_speaker
segments = []
if punctuation_segments:
segments = [punctuation_segments[0]]
for segment in punctuation_segments[1:]:
if segment.speaker == segments[-1].speaker:
if segments[-1].text:
segments[-1].text += segment.text
segments[-1].end = segment.end
else:
segments.append(segment)
return segments, diarization_buffer
def get_lines(
self,
diarization: bool = False,
translation: bool = False,
current_silence: Optional[Silence] = None,
audio_time: Optional[float] = None,
) -> Tuple[List[Segment], str, Union[str, TimedText]]:
"""Return the formatted segments plus buffers, optionally with diarization/translation.
Args:
audio_time: Current audio stream position in seconds. Used as fallback
for ongoing silence end time instead of wall-clock (which breaks
when audio is fed faster or slower than real-time).
"""
# Fallback for ongoing silence: prefer audio stream time over wall-clock
_silence_now = audio_time if audio_time is not None else (time() - self.beg_loop)
if diarization:
segments, diarization_buffer = self.get_lines_diarization()
else:
diarization_buffer = ''
for token in self.new_tokens:
if isinstance(token, Silence):
if self.current_line_tokens:
self.validated_segments.append(Segment.from_tokens(self.current_line_tokens))
self.current_line_tokens = []
end_silence = token.end if token.has_ended else _silence_now
if self.validated_segments and self.validated_segments[-1].is_silence():
self.validated_segments[-1].end = end_silence
else:
self.validated_segments.append(SilentSegment(
start=token.start,
end=end_silence
))
else:
self.current_line_tokens.append(token)
segments = list(self.validated_segments)
if self.current_line_tokens:
segments.append(Segment.from_tokens(self.current_line_tokens))
if current_silence:
end_silence = current_silence.end if current_silence.has_ended else _silence_now
if segments and segments[-1].is_silence():
segments[-1] = SilentSegment(start=segments[-1].start, end=end_silence)
else:
segments.append(SilentSegment(
start=current_silence.start,
end=end_silence
))
if translation:
[self.add_translation(segment) for segment in segments if not segment.is_silence()]
self._prune()
return segments, diarization_buffer, self.new_translation_buffer.text
================================================
FILE: whisperlivekit/vllm_realtime.py
================================================
"""
vLLM Realtime WebSocket streaming backend for WhisperLiveKit.
Connects to a vLLM server's ``/v1/realtime`` WebSocket endpoint to stream
audio and receive transcription deltas. Uses ``websockets.sync.client``
for simplicity since ``process_iter`` runs inside ``asyncio.to_thread``.
Provides ``VLLMRealtimeASR`` (lightweight model holder) and
``VLLMRealtimeOnlineProcessor`` (streaming processor) that plug into
WhisperLiveKit's audio processing pipeline.
"""
import base64
import json
import logging
import threading
import time
from typing import List, Optional, Tuple
import numpy as np
from whisperlivekit.timed_objects import ASRToken, Transcript
logger = logging.getLogger(__name__)
class VLLMRealtimeASR:
"""Lightweight model holder — stores connection info for the vLLM server."""
sep = " "
SAMPLING_RATE = 16000
backend_choice = "vllm-realtime"
def __init__(self, vllm_url="ws://localhost:8000/v1/realtime",
model_name="Qwen/Qwen3-ASR-1.7B", lan="auto", **kwargs):
self.vllm_url = vllm_url
self.model_name = model_name
self.original_language = None if lan == "auto" else lan
self.tokenizer = None
def transcribe(self, audio):
pass
class VLLMRealtimeOnlineProcessor:
"""
Online processor that streams audio to a vLLM Realtime WebSocket.
Uses a background thread for WebSocket receiving and
``websockets.sync.client`` for the sync WebSocket connection.
"""
SAMPLING_RATE = 16000
# Minimum audio samples before connecting (0.5s of audio)
_MIN_CONNECT_SAMPLES = SAMPLING_RATE // 2
def __init__(self, asr: VLLMRealtimeASR):
self.asr = asr
self.end = 0.0
self.buffer = []
self.audio_buffer = np.array([], dtype=np.float32)
self._reset_state()
logger.info(
"[vllm-realtime] Initialized. url=%s model=%s",
asr.vllm_url, asr.model_name,
)
def _reset_state(self):
self._pending_audio = np.zeros(0, dtype=np.float32)
self._ws = None
self._recv_thread: Optional[threading.Thread] = None
self._connected = False
self._done = False
self._recv_error: Optional[Exception] = None
# Text accumulation and word extraction
self._accumulated_text = ""
self._n_committed_words = 0
self._total_audio_duration = 0.0
self._global_time_offset = 0.0
# Lock for text state accessed from both recv thread and main thread
self._text_lock = threading.Lock()
# ── Interface methods ──
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time: float):
self.end = audio_stream_end_time
self._pending_audio = np.append(self._pending_audio, audio)
self.audio_buffer = self._pending_audio
def process_iter(self, is_last=False) -> Tuple[List[ASRToken], float]:
try:
return self._process_iter_inner(is_last)
except Exception as e:
logger.warning("[vllm-realtime] process_iter exception: %s", e, exc_info=True)
return [], self.end
def get_buffer(self) -> Transcript:
"""Return all uncommitted text as buffer."""
self._drain_deltas()
with self._text_lock:
text = self._accumulated_text
if not text:
return Transcript(start=None, end=None, text="")
words = text.split()
uncommitted = words[self._n_committed_words:]
if uncommitted:
return Transcript(start=self.end, end=self.end, text=" ".join(uncommitted))
return Transcript(start=None, end=None, text="")
def start_silence(self) -> Tuple[List[ASRToken], float]:
"""Flush all pending words when silence starts.
Sends commit(final=true) to signal end of utterance, waits for
transcription.done, flushes all words, then prepares for reconnection
on the next utterance.
"""
if not self._connected or self._done:
words = self._flush_all_pending_words()
logger.info("[vllm-realtime] start_silence (not connected): flushed %d words", len(words))
return words, self.end
# Send any remaining buffered audio
self._send_pending_audio()
# Signal end of stream
self._send_commit(final=True)
# Wait for transcription.done
self._wait_for_done(timeout=10.0)
# Flush all remaining words
words = self._flush_all_pending_words()
# Close and reset for next utterance
self._close_ws()
old_offset = self._global_time_offset + self._total_audio_duration
self._reset_state()
self._global_time_offset = old_offset
logger.info("[vllm-realtime] start_silence: flushed %d words", len(words))
return words, self.end
def end_silence(self, silence_duration: float, offset: float):
self._global_time_offset += silence_duration
self.end += silence_duration
def new_speaker(self, change_speaker):
self.start_silence()
def warmup(self, audio, init_prompt=""):
pass
def finish(self) -> Tuple[List[ASRToken], float]:
"""Close connection and flush all remaining words."""
if self._connected and not self._done:
# Send remaining audio
self._send_pending_audio()
# Signal final commit
self._send_commit(final=True)
# Wait for transcription.done
self._wait_for_done(timeout=30.0)
# Flush all words
words = self._flush_all_pending_words()
# Close WebSocket
self._close_ws()
logger.info("[vllm-realtime] finish: flushed %d words", len(words))
return words, self.end
# ── WebSocket connection management ──
def _connect(self):
"""Connect to the vLLM realtime WebSocket and start the receive thread."""
from websockets.sync.client import connect
url = self.asr.vllm_url
logger.info("[vllm-realtime] Connecting to %s", url)
self._ws = connect(url)
# Send session.update to select model
self._ws.send(json.dumps({
"type": "session.update",
"model": self.asr.model_name,
}))
# Send initial commit(final=false) to start generation
self._send_commit(final=False)
# Start receive thread
self._recv_thread = threading.Thread(target=self._recv_loop, daemon=True)
self._recv_thread.start()
self._connected = True
logger.info("[vllm-realtime] Connected and started receive thread")
def _close_ws(self):
"""Close the WebSocket connection and join the receive thread."""
if self._ws is not None:
try:
self._ws.close()
except Exception:
pass
self._ws = None
if self._recv_thread is not None:
self._recv_thread.join(timeout=5.0)
self._recv_thread = None
def _recv_loop(self):
"""Background thread: receive messages from the vLLM WebSocket."""
try:
while not self._done and self._ws is not None:
try:
raw = self._ws.recv(timeout=0.1)
except TimeoutError:
continue
except Exception:
break
try:
msg = json.loads(raw)
except (json.JSONDecodeError, TypeError):
continue
msg_type = msg.get("type", "")
if msg_type == "transcription.delta":
delta = msg.get("delta", "")
if delta:
with self._text_lock:
self._accumulated_text += delta
elif msg_type == "transcription.done":
done_text = msg.get("text", "")
if done_text:
with self._text_lock:
# Replace accumulated text with final text
self._accumulated_text = done_text
self._done = True
break
except Exception as e:
logger.error("[vllm-realtime] recv_loop error: %s", e, exc_info=True)
self._recv_error = e
self._done = True
# ── Protocol messages ──
def _send_commit(self, final: bool):
"""Send input_audio_buffer.commit message."""
if self._ws is None:
return
try:
self._ws.send(json.dumps({
"type": "input_audio_buffer.commit",
"final": final,
}))
except Exception as e:
logger.warning("[vllm-realtime] Failed to send commit: %s", e)
def _send_audio(self, audio: np.ndarray):
"""Send audio as a base64-encoded PCM16 append message."""
if self._ws is None:
return
# Convert float32 [-1, 1] to int16 PCM
pcm16 = (audio * 32767).astype(np.int16)
audio_bytes = pcm16.tobytes()
audio_b64 = base64.b64encode(audio_bytes).decode("ascii")
try:
self._ws.send(json.dumps({
"type": "input_audio_buffer.append",
"audio": audio_b64,
}))
except Exception as e:
logger.warning("[vllm-realtime] Failed to send audio: %s", e)
def _send_pending_audio(self):
"""Send all pending audio to the vLLM server."""
if len(self._pending_audio) == 0:
return
# Track total audio duration for timestamp estimation
self._total_audio_duration += len(self._pending_audio) / self.SAMPLING_RATE
# Send in chunks of 0.5s to avoid overwhelming the WebSocket
chunk_samples = self.SAMPLING_RATE // 2
while len(self._pending_audio) >= chunk_samples:
chunk = self._pending_audio[:chunk_samples]
self._send_audio(chunk)
self._pending_audio = self._pending_audio[chunk_samples:]
# Send remaining audio if any
if len(self._pending_audio) > 0:
self._send_audio(self._pending_audio)
self._pending_audio = np.zeros(0, dtype=np.float32)
self.audio_buffer = self._pending_audio
# ── Receive helpers ──
def _drain_deltas(self):
"""No-op since the recv thread accumulates text directly."""
pass
def _wait_for_done(self, timeout: float = 10.0):
"""Wait for transcription.done message from the server."""
deadline = time.time() + timeout
while not self._done and time.time() < deadline:
time.sleep(0.05)
if not self._done:
logger.warning("[vllm-realtime] Timed out waiting for transcription.done")
# ── Word extraction (same approach as VoxtralHF) ──
def _time_for_word(self, word_idx: int, n_words_total: int) -> Tuple[float, float]:
"""Estimate timestamps by linearly distributing words across audio duration."""
duration = max(self._total_audio_duration, 0.001)
n_total = max(n_words_total, 1)
start_time = (word_idx / n_total) * duration + self._global_time_offset
end_time = ((word_idx + 1) / n_total) * duration + self._global_time_offset
return start_time, end_time
def _extract_new_words(self) -> List[ASRToken]:
"""Extract complete words (all but the last, which may still grow)."""
with self._text_lock:
text = self._accumulated_text
if not text:
return []
words = text.split()
new_words: List[ASRToken] = []
n_words_total = len(words)
while len(words) > self._n_committed_words + 1:
word = words[self._n_committed_words]
start_time, end_time = self._time_for_word(self._n_committed_words, n_words_total)
text_out = word if self._n_committed_words == 0 else " " + word
new_words.append(ASRToken(start=start_time, end=end_time, text=text_out))
self._n_committed_words += 1
return new_words
def _flush_all_pending_words(self) -> List[ASRToken]:
"""Flush ALL words including the last partial one."""
with self._text_lock:
text = self._accumulated_text
if not text:
return []
words = text.split()
new_words: List[ASRToken] = []
n_words_total = max(len(words), 1)
while self._n_committed_words < len(words):
word = words[self._n_committed_words]
start_time, end_time = self._time_for_word(self._n_committed_words, n_words_total)
text_out = word if self._n_committed_words == 0 else " " + word
new_words.append(ASRToken(start=start_time, end=end_time, text=text_out))
self._n_committed_words += 1
return new_words
# ── Core processing ──
def _process_iter_inner(self, is_last: bool) -> Tuple[List[ASRToken], float]:
# Connect when we have enough audio buffered
if not self._connected:
if len(self._pending_audio) >= self._MIN_CONNECT_SAMPLES:
self._connect()
self._send_pending_audio()
else:
return [], self.end
# Send any new pending audio
if self._connected and not self._done:
self._send_pending_audio()
# If connection closed unexpectedly but new audio arrived, reconnect
if self._done and len(self._pending_audio) >= self._MIN_CONNECT_SAMPLES:
flush_words = self._flush_all_pending_words()
old_offset = self._global_time_offset + self._total_audio_duration
self._close_ws()
self._reset_state()
self._global_time_offset = old_offset
self._connect()
self._send_pending_audio()
return flush_words, self.end
# Extract complete words
new_words = self._extract_new_words()
if new_words:
logger.info(
"[vllm-realtime] returning %d words: %s",
len(new_words), [w.text for w in new_words],
)
self.buffer = []
return new_words, self.end
================================================
FILE: whisperlivekit/voxtral_hf_streaming.py
================================================
"""
Voxtral Mini Realtime streaming backend using HuggingFace Transformers.
Uses VoxtralRealtimeForConditionalGeneration with a background generate thread
and queue-based audio feeding for real-time streaming transcription.
Supports CUDA, CPU, and MPS devices.
"""
import logging
import queue
import sys
import threading
import time
from typing import List, Optional, Tuple
import numpy as np
from whisperlivekit.timed_objects import ASRToken, Transcript
logger = logging.getLogger(__name__)
class VoxtralHFStreamingASR:
"""Voxtral model holder using HuggingFace Transformers."""
sep = " "
def __init__(self, logfile=sys.stderr, **kwargs):
import torch
from transformers import (
AutoProcessor,
VoxtralRealtimeForConditionalGeneration,
)
self.logfile = logfile
self.transcribe_kargs = {}
lan = kwargs.get("lan", "auto")
self.original_language = None if lan == "auto" else lan
DEFAULT_MODEL = "mistralai/Voxtral-Mini-4B-Realtime-2602"
model_path = kwargs.get("model_dir") or kwargs.get("model_path")
if not model_path:
model_size = kwargs.get("model_size", "")
if model_size and ("/" in model_size or model_size.startswith(".")):
model_path = model_size
else:
model_path = DEFAULT_MODEL
t = time.time()
logger.info(f"Loading Voxtral model '{model_path}' via HF Transformers...")
self.processor = AutoProcessor.from_pretrained(model_path)
self.model = VoxtralRealtimeForConditionalGeneration.from_pretrained(
model_path,
torch_dtype=torch.bfloat16,
device_map="auto",
)
logger.info(f"Voxtral HF model loaded in {time.time() - t:.2f}s on {self.model.device}")
self.backend_choice = "voxtral"
self.tokenizer = None # sentence tokenizer — not needed for streaming
def transcribe(self, audio):
pass
class VoxtralHFStreamingOnlineProcessor:
"""
Online processor for Voxtral streaming ASR via HuggingFace Transformers.
Uses a background thread running model.generate() with a queue-based
input_features_generator and TextIteratorStreamer for real-time output.
Each decoded token corresponds to ~80ms of audio.
"""
SAMPLING_RATE = 16000
def __init__(self, asr: VoxtralHFStreamingASR, logfile=sys.stderr):
self.asr = asr
self.logfile = logfile
self.end = 0.0
self.buffer = []
self.audio_buffer = np.array([], dtype=np.float32)
processor = asr.processor
self._first_chunk_samples = processor.num_samples_first_audio_chunk
self._chunk_samples = processor.num_samples_per_audio_chunk
self._chunk_step = processor.raw_audio_length_per_tok
# num_right_pad_tokens is a method in some transformers versions, a property in others
n_right_pad = processor.num_right_pad_tokens
if callable(n_right_pad):
n_right_pad = n_right_pad()
self._right_pad_samples = int(n_right_pad * processor.raw_audio_length_per_tok)
self._seconds_per_token = processor.raw_audio_length_per_tok / self.SAMPLING_RATE
self._reset_state()
logger.info(
f"[voxtral-hf] Initialized. first_chunk={self._first_chunk_samples} samples, "
f"chunk={self._chunk_samples}, step={self._chunk_step}, "
f"right_pad={self._right_pad_samples}"
)
def _reset_state(self):
self._pending_chunks: List[np.ndarray] = []
self._pending_len = 0
self._audio_queue: queue.Queue = queue.Queue()
self._streamer_texts: List[str] = []
self._generate_thread: Optional[threading.Thread] = None
self._generate_started = False
self._generate_finished = False
self._generate_error: Optional[Exception] = None
# Text accumulation (list of fragments, joined on demand)
self._text_fragments: List[str] = []
self._text_len = 0
# Fragment position tracking for accurate word timestamps:
# each entry is (char_offset_in_full_text, audio_tok_pos_consumed)
self._fragment_positions: List[Tuple[int, int]] = []
self._n_text_tokens_received = 0
self._n_audio_tokens_fed = 0
# Audio tokens actually consumed by the model (tracked inside generator)
self._n_audio_tokens_consumed = 0
self._n_committed_words = 0
self._global_time_offset = 0.0
# Event signalled by the generate thread when it finishes
self._generate_done = threading.Event()
# Lock for text state accessed from both generate thread and main thread
self._text_lock = threading.Lock()
# ── Audio / text helpers ──
def _get_pending_audio(self) -> np.ndarray:
"""Flatten pending audio chunks into a single array."""
if not self._pending_chunks:
return np.zeros(0, dtype=np.float32)
if len(self._pending_chunks) == 1:
return self._pending_chunks[0]
flat = np.concatenate(self._pending_chunks)
self._pending_chunks = [flat]
return flat
def _set_pending_audio(self, arr: np.ndarray):
"""Replace pending audio with a single array."""
if len(arr) == 0:
self._pending_chunks = []
self._pending_len = 0
else:
self._pending_chunks = [arr]
self._pending_len = len(arr)
def _get_accumulated_text(self) -> str:
"""Get the full accumulated text (joins fragments if needed)."""
if not self._text_fragments:
return ""
if len(self._text_fragments) == 1:
return self._text_fragments[0]
joined = "".join(self._text_fragments)
self._text_fragments = [joined]
return joined
# ── Interface methods ──
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time: float):
self.end = audio_stream_end_time
self._pending_chunks.append(audio)
self._pending_len += len(audio)
self.audio_buffer = audio # diagnostic only
def process_iter(self, is_last=False) -> Tuple[List[ASRToken], float]:
try:
return self._process_iter_inner(is_last)
except Exception as e:
logger.warning(f"[voxtral-hf] process_iter exception: {e}", exc_info=True)
return [], self.end
def get_buffer(self) -> Transcript:
"""Return all uncommitted text as buffer.
Drains the streamer first so late-arriving tokens (common on
slower devices like MPS) are picked up even between audio chunks.
"""
self._drain_streamer()
with self._text_lock:
text = self._get_accumulated_text()
if not text:
return Transcript(start=None, end=None, text="")
words = text.split()
uncommitted = words[self._n_committed_words:]
if uncommitted:
return Transcript(start=self.end, end=self.end, text=" ".join(uncommitted))
return Transcript(start=None, end=None, text="")
def start_silence(self) -> Tuple[List[ASRToken], float]:
"""Flush all uncommitted words when silence starts.
Feeds right-padding (silence) so the model has enough future context
to emit the last few tokens, then drains repeatedly until the model
has finished producing text. Without right-padding the model holds
back the last few words because it hasn't seen enough audio yet.
"""
if not self._generate_started or self._generate_finished:
self._drain_streamer()
words = self._flush_all_pending_words()
logger.info(f"[voxtral-hf] start_silence (no thread): flushed {len(words)} words")
return words, self.end
# Feed any remaining real audio
self._feed_pending_audio()
# Add right-padding so the model can decode trailing tokens.
# Don't count these toward _n_audio_tokens_fed — they're not
# real audio and shouldn't affect word timestamp calculations.
if self._right_pad_samples > 0:
right_pad = np.zeros(self._right_pad_samples, dtype=np.float32)
self._pending_chunks.append(right_pad)
self._pending_len += len(right_pad)
saved_count = self._n_audio_tokens_fed
self._feed_pending_audio()
self._n_audio_tokens_fed = saved_count
# Drain in a loop: the model may continue producing text tokens after
# the audio queue is empty (autoregressive generation). Each iteration
# uses an event-driven blocking drain with short timeouts.
all_words: List[ASRToken] = []
for _ in range(5):
self._drain_streamer_blocking(timeout=5.0)
batch = self._flush_all_pending_words()
all_words.extend(batch)
if not batch:
break # no new text — model has caught up
logger.info(f"[voxtral-hf] start_silence: flushed {len(all_words)} words")
return all_words, self.end
def end_silence(self, silence_duration: float, offset: float):
self._global_time_offset += silence_duration
self.end += silence_duration
def new_speaker(self, change_speaker):
self.start_silence()
def warmup(self, audio, init_prompt=""):
pass
def finish(self) -> Tuple[List[ASRToken], float]:
"""Flush remaining audio with right-padding and stop the generate thread."""
# Add right-padding so the model can finish decoding
if self._right_pad_samples > 0:
right_pad = np.zeros(self._right_pad_samples, dtype=np.float32)
self._pending_chunks.append(right_pad)
self._pending_len += len(right_pad)
# Feed remaining audio
if self._generate_started and not self._generate_finished:
self._feed_pending_audio()
# Signal end of audio
self._audio_queue.put(None)
# Wait for generate to finish
if self._generate_thread is not None:
self._generate_thread.join(timeout=30.0)
elif not self._generate_started and self._pending_len >= self._first_chunk_samples:
# Never started but have enough audio — start and immediately finish
self._start_generate_thread()
self._feed_pending_audio()
self._audio_queue.put(None)
if self._generate_thread is not None:
self._generate_thread.join(timeout=30.0)
self._drain_streamer()
words = self._flush_all_pending_words()
logger.info(f"[voxtral-hf] finish: flushed {len(words)} words")
return words, self.end
# ── Generate thread management ──
def _start_generate_thread(self):
"""Start model.generate() in a background thread with streaming."""
import torch
from transformers import TextIteratorStreamer
processor = self.asr.processor
model = self.asr.model
# Extract first chunk
pending = self._get_pending_audio()
first_chunk_audio = pending[:self._first_chunk_samples]
self._set_pending_audio(pending[self._first_chunk_samples:])
# First chunk covers multiple audio tokens
self._n_audio_tokens_fed += max(1, self._first_chunk_samples // self._chunk_step)
first_inputs = processor(
first_chunk_audio,
is_streaming=True,
is_first_audio_chunk=True,
return_tensors="pt",
)
first_inputs = first_inputs.to(model.device, dtype=model.dtype)
streamer = TextIteratorStreamer(
processor.tokenizer,
skip_prompt=True,
skip_special_tokens=True,
)
self._streamer = streamer
audio_queue = self._audio_queue
def input_features_gen():
# Track audio consumption inside the generator (runs in generate thread)
self._n_audio_tokens_consumed = max(1, self._first_chunk_samples // self._chunk_step)
yield first_inputs.input_features
while True:
chunk_audio = audio_queue.get()
if chunk_audio is None:
break
self._n_audio_tokens_consumed += 1
inputs = processor(
chunk_audio,
is_streaming=True,
is_first_audio_chunk=False,
return_tensors="pt",
)
inputs = inputs.to(model.device, dtype=model.dtype)
yield inputs.input_features
def run_generate():
try:
with torch.no_grad():
# Pass generator as input_features — the model detects GeneratorType
# and internally converts it to input_features_generator
generate_kwargs = {
k: v for k, v in first_inputs.items()
if k != "input_features"
}
model.generate(
input_features=input_features_gen(),
streamer=streamer,
**generate_kwargs,
)
except Exception as e:
logger.error(f"[voxtral-hf] generate error: {e}", exc_info=True)
self._generate_error = e
finally:
self._generate_finished = True
self._generate_done.set()
self._generate_thread = threading.Thread(target=run_generate, daemon=True)
self._generate_thread.start()
self._generate_started = True
logger.info("[voxtral-hf] generate thread started")
def _feed_pending_audio(self):
"""Convert pending audio into properly-sized chunks for the generator."""
chunk_size = self._chunk_samples
step_size = self._chunk_step
pending = self._get_pending_audio()
while len(pending) >= chunk_size:
chunk = pending[:chunk_size]
self._audio_queue.put(chunk)
pending = pending[step_size:]
self._n_audio_tokens_fed += 1
self._set_pending_audio(pending)
self.audio_buffer = pending
def _append_text_fragment(self, text_fragment: str):
"""Append a text fragment with its audio position (must hold _text_lock)."""
self._fragment_positions.append((self._text_len, self._n_audio_tokens_consumed))
self._text_fragments.append(text_fragment)
self._text_len += len(text_fragment)
self._n_text_tokens_received += 1
def _drain_streamer(self):
"""Non-blocking drain of all available text from the streamer."""
if not self._generate_started:
return
text_queue = self._streamer.text_queue
while True:
try:
text_fragment = text_queue.get_nowait()
except queue.Empty:
break
if text_fragment is None:
self._generate_finished = True
break
if text_fragment:
with self._text_lock:
self._append_text_fragment(text_fragment)
def _drain_streamer_blocking(self, timeout=30.0):
"""Blocking drain: wait for the generate thread to finish producing text.
Uses the _generate_done event to know when the model is truly finished.
Falls back to text-queue polling with adaptive timeouts.
"""
if not self._generate_started or self._generate_finished:
self._drain_streamer()
return
text_queue = self._streamer.text_queue
deadline = time.time() + timeout
# Count consecutive empty polls to detect when model has caught up
empty_streak = 0
while time.time() < deadline:
remaining = max(deadline - time.time(), 0.01)
# If generate thread is done, do a final flush and exit
if self._generate_done.is_set() or self._generate_finished:
self._drain_streamer()
return
# Adaptive wait: short while audio is queued, longer once queue is empty
if self._audio_queue.empty():
wait = min(remaining, 0.5)
else:
wait = min(remaining, 0.1)
try:
text_fragment = text_queue.get(timeout=wait)
except queue.Empty:
empty_streak += 1
# Only exit if audio queue is empty AND we've had enough empty polls
# This prevents premature exit when the model is slow
if self._audio_queue.empty() and empty_streak >= 4:
break
continue
empty_streak = 0
if text_fragment is None:
self._generate_finished = True
break
if text_fragment:
with self._text_lock:
self._append_text_fragment(text_fragment)
# ── Word extraction ──
def _pos_to_time(self, token_position: int) -> float:
"""Convert audio token position to seconds."""
return token_position * self._seconds_per_token + self._global_time_offset
def _audio_pos_for_char(self, char_idx: int) -> int:
"""Look up the audio token position for a character index in the text.
Uses the fragment position index recorded when text arrives from the
generate thread. Returns the audio position of the fragment that
contains ``char_idx``, giving much better word timestamps than the
old uniform-distribution heuristic.
"""
if not self._fragment_positions:
return 0
# _fragment_positions is sorted by char_offset — find the last entry
# whose char_offset <= char_idx (the fragment containing this char).
pos = 0
for offset, audio_tok in self._fragment_positions:
if offset > char_idx:
break
pos = audio_tok
return pos
def _word_timestamps(self, text: str, words: List[str], start_idx: int, end_idx: int) -> List[Tuple[int, int]]:
"""Compute (tok_start, tok_end) for words[start_idx:end_idx] using fragment positions."""
# Build char offsets for each word
result = []
char_pos = 0
for i, word in enumerate(words):
if i > 0:
char_pos += 1 # space separator
if start_idx <= i < end_idx:
tok_start = self._audio_pos_for_char(char_pos)
tok_end = self._audio_pos_for_char(char_pos + len(word))
result.append((tok_start, tok_end))
char_pos += len(word)
return result
def _extract_new_words(self) -> List[ASRToken]:
"""Extract complete words (all but the last, which may still be growing)."""
with self._text_lock:
text = self._get_accumulated_text()
if not text:
return []
words = text.split()
new_words: List[ASRToken] = []
n_to_commit = len(words) - 1 # keep last word (may still grow)
if n_to_commit <= self._n_committed_words:
return []
timestamps = self._word_timestamps(text, words, self._n_committed_words, n_to_commit)
for tok_start, tok_end in timestamps:
word = words[self._n_committed_words]
start_time = self._pos_to_time(tok_start)
end_time = self._pos_to_time(max(tok_end, tok_start + 1))
text_out = word if self._n_committed_words == 0 else " " + word
new_words.append(ASRToken(start=start_time, end=end_time, text=text_out))
self._n_committed_words += 1
return new_words
def _flush_all_pending_words(self) -> List[ASRToken]:
"""Flush ALL words including the last partial one."""
with self._text_lock:
text = self._get_accumulated_text()
if not text:
return []
words = text.split()
new_words: List[ASRToken] = []
if self._n_committed_words >= len(words):
return []
timestamps = self._word_timestamps(text, words, self._n_committed_words, len(words))
for tok_start, tok_end in timestamps:
word = words[self._n_committed_words]
start_time = self._pos_to_time(tok_start)
end_time = self._pos_to_time(max(tok_end, tok_start + 1))
text_out = word if self._n_committed_words == 0 else " " + word
new_words.append(ASRToken(start=start_time, end=end_time, text=text_out))
self._n_committed_words += 1
return new_words
# ── Core processing ──
def _process_iter_inner(self, is_last: bool) -> Tuple[List[ASRToken], float]:
# Start generate thread when enough audio is buffered
if not self._generate_started:
if self._pending_len >= self._first_chunk_samples:
self._start_generate_thread()
self._feed_pending_audio()
else:
return [], self.end
# Feed any new pending audio
if self._generate_started and not self._generate_finished:
self._feed_pending_audio()
# If generate finished unexpectedly (EOS) but new audio arrived, restart
if self._generate_finished and self._pending_len >= self._first_chunk_samples:
self._drain_streamer()
flush_words = self._flush_all_pending_words()
# Reset for new utterance
old_offset = self._global_time_offset
self._reset_state()
self._global_time_offset = old_offset
self._start_generate_thread()
self._feed_pending_audio()
return flush_words, self.end
# Drain available text from streamer
self._drain_streamer()
# Extract complete words
new_words = self._extract_new_words()
if new_words:
logger.info(f"[voxtral-hf] returning {len(new_words)} words: {[w.text for w in new_words]}")
self.buffer = []
return new_words, self.end
================================================
FILE: whisperlivekit/voxtral_mlx/__init__.py
================================================
"""Pure-MLX Voxtral Realtime backend for WhisperLiveKit."""
from .loader import load_voxtral_model
from .model import VoxtralMLXModel
__all__ = ["load_voxtral_model", "VoxtralMLXModel"]
================================================
FILE: whisperlivekit/voxtral_mlx/loader.py
================================================
"""
Model weight loading for the MLX Voxtral Realtime backend.
Supports two on-disk formats:
1. **Converted** (``config.json`` + ``model.safetensors``): ready-to-load,
with optional quantisation metadata.
2. **Original Mistral** (``params.json`` + ``consolidated.safetensors``):
requires weight renaming and conv-weight transposition.
The public entry point is :func:`load_voxtral_model` which returns the
model, tokenizer, and raw config dict.
"""
import json
import logging
import re
from pathlib import Path
import mlx.core as mx
import mlx.nn as nn
from huggingface_hub import snapshot_download
from .model import VoxtralMLXModel
logger = logging.getLogger(__name__)
DEFAULT_MODEL_ID = "mlx-community/Voxtral-Mini-4B-Realtime-6bit"
# ---------------------------------------------------------------------------
# Downloading
# ---------------------------------------------------------------------------
_ALLOWED_PATTERNS = [
"consolidated.safetensors",
"model*.safetensors",
"model.safetensors.index.json",
"params.json",
"config.json",
"tekken.json",
]
def download_weights(model_id: str = DEFAULT_MODEL_ID) -> Path:
"""Download model files from HuggingFace Hub and return the local path."""
return Path(snapshot_download(model_id, allow_patterns=_ALLOWED_PATTERNS))
# ---------------------------------------------------------------------------
# Weight name remapping (Mistral → our naming)
# ---------------------------------------------------------------------------
_NAME_RULES: list[tuple[str, str]] = [
# Encoder convolutions
(r"whisper_encoder\.conv_layers\.0\.conv\.(.*)", r"encoder.conv1.\1"),
(r"whisper_encoder\.conv_layers\.1\.conv\.(.*)", r"encoder.conv2.\1"),
# Encoder transformer blocks
(r"whisper_encoder\.transformer\.layers\.(\d+)\.attention\.wq\.(.*)",
r"encoder.blocks.\1.self_attn.q_proj.\2"),
(r"whisper_encoder\.transformer\.layers\.(\d+)\.attention\.wk\.(.*)",
r"encoder.blocks.\1.self_attn.k_proj.\2"),
(r"whisper_encoder\.transformer\.layers\.(\d+)\.attention\.wv\.(.*)",
r"encoder.blocks.\1.self_attn.v_proj.\2"),
(r"whisper_encoder\.transformer\.layers\.(\d+)\.attention\.wo\.(.*)",
r"encoder.blocks.\1.self_attn.out_proj.\2"),
(r"whisper_encoder\.transformer\.layers\.(\d+)\.attention_norm\.(.*)",
r"encoder.blocks.\1.pre_attn_norm.\2"),
(r"whisper_encoder\.transformer\.layers\.(\d+)\.feed_forward\.w1\.(.*)",
r"encoder.blocks.\1.ffn.gate.\2"),
(r"whisper_encoder\.transformer\.layers\.(\d+)\.feed_forward\.w2\.(.*)",
r"encoder.blocks.\1.ffn.down.\2"),
(r"whisper_encoder\.transformer\.layers\.(\d+)\.feed_forward\.w3\.(.*)",
r"encoder.blocks.\1.ffn.up.\2"),
(r"whisper_encoder\.transformer\.layers\.(\d+)\.ffn_norm\.(.*)",
r"encoder.blocks.\1.pre_ffn_norm.\2"),
(r"whisper_encoder\.transformer\.norm\.(.*)", r"encoder.final_norm.\1"),
# Adapter
(r"audio_language_projection\.0\.weight", r"adapter.linear1.weight"),
(r"audio_language_projection\.2\.weight", r"adapter.linear2.weight"),
# Decoder embedding
(r"tok_embeddings\.weight", r"decoder.token_embedding.weight"),
# Decoder blocks
(r"layers\.(\d+)\.attention\.wq\.weight",
r"decoder.blocks.\1.self_attn.q_proj.weight"),
(r"layers\.(\d+)\.attention\.wk\.weight",
r"decoder.blocks.\1.self_attn.k_proj.weight"),
(r"layers\.(\d+)\.attention\.wv\.weight",
r"decoder.blocks.\1.self_attn.v_proj.weight"),
(r"layers\.(\d+)\.attention\.wo\.weight",
r"decoder.blocks.\1.self_attn.out_proj.weight"),
(r"layers\.(\d+)\.attention_norm\.weight",
r"decoder.blocks.\1.pre_attn_norm.weight"),
(r"layers\.(\d+)\.feed_forward\.w1\.weight",
r"decoder.blocks.\1.ffn.gate.weight"),
(r"layers\.(\d+)\.feed_forward\.w2\.weight",
r"decoder.blocks.\1.ffn.down.weight"),
(r"layers\.(\d+)\.feed_forward\.w3\.weight",
r"decoder.blocks.\1.ffn.up.weight"),
(r"layers\.(\d+)\.ffn_norm\.weight",
r"decoder.blocks.\1.pre_ffn_norm.weight"),
(r"layers\.(\d+)\.ada_rms_norm_t_cond\.0\.weight",
r"decoder.blocks.\1.adaptive_scale.proj_in.weight"),
(r"layers\.(\d+)\.ada_rms_norm_t_cond\.2\.weight",
r"decoder.blocks.\1.adaptive_scale.proj_out.weight"),
# Decoder final norm
(r"norm\.weight", r"decoder.final_norm.weight"),
]
_PREFIX_STRIP = re.compile(
r"^(mm_streams_embeddings\.embedding_module|mm_whisper_embeddings)\."
)
def _translate_weight_name(name: str) -> str | None:
name = _PREFIX_STRIP.sub("", name)
for pattern, replacement in _NAME_RULES:
result, n = re.subn(f"^{pattern}$", replacement, name)
if n:
return result
return None
def _is_conv_weight(name: str) -> bool:
return ("conv1.weight" in name or "conv2.weight" in name) and "bias" not in name
# ---------------------------------------------------------------------------
# Converted-format weight remapping (voxmlx names → our names)
# ---------------------------------------------------------------------------
_CONVERTED_RULES: list[tuple[str, str]] = [
# Adapter
(r"adapter\.w_in\.(.*)", r"adapter.linear1.\1"),
(r"adapter\.w_out\.(.*)", r"adapter.linear2.\1"),
# Encoder transformer blocks
(r"encoder\.layers\.(\d+)\.attention\.(.*)", r"encoder.blocks.\1.self_attn.\2"),
(r"encoder\.layers\.(\d+)\.attn_norm\.(.*)", r"encoder.blocks.\1.pre_attn_norm.\2"),
(r"encoder\.layers\.(\d+)\.mlp\.gate_proj\.(.*)", r"encoder.blocks.\1.ffn.gate.\2"),
(r"encoder\.layers\.(\d+)\.mlp\.down_proj\.(.*)", r"encoder.blocks.\1.ffn.down.\2"),
(r"encoder\.layers\.(\d+)\.mlp\.up_proj\.(.*)", r"encoder.blocks.\1.ffn.up.\2"),
(r"encoder\.layers\.(\d+)\.ffn_norm\.(.*)", r"encoder.blocks.\1.pre_ffn_norm.\2"),
(r"encoder\.norm\.(.*)", r"encoder.final_norm.\1"),
# Decoder embedding
(r"language_model\.embed_tokens\.(.*)", r"decoder.token_embedding.\1"),
# Decoder blocks
(r"language_model\.layers\.(\d+)\.attention\.(.*)", r"decoder.blocks.\1.self_attn.\2"),
(r"language_model\.layers\.(\d+)\.attn_norm\.(.*)", r"decoder.blocks.\1.pre_attn_norm.\2"),
(r"language_model\.layers\.(\d+)\.mlp\.gate_proj\.(.*)", r"decoder.blocks.\1.ffn.gate.\2"),
(r"language_model\.layers\.(\d+)\.mlp\.down_proj\.(.*)", r"decoder.blocks.\1.ffn.down.\2"),
(r"language_model\.layers\.(\d+)\.mlp\.up_proj\.(.*)", r"decoder.blocks.\1.ffn.up.\2"),
(r"language_model\.layers\.(\d+)\.ffn_norm\.(.*)", r"decoder.blocks.\1.pre_ffn_norm.\2"),
(r"language_model\.layers\.(\d+)\.ada_norm\.linear_in\.(.*)",
r"decoder.blocks.\1.adaptive_scale.proj_in.\2"),
(r"language_model\.layers\.(\d+)\.ada_norm\.linear_out\.(.*)",
r"decoder.blocks.\1.adaptive_scale.proj_out.\2"),
(r"language_model\.norm\.(.*)", r"decoder.final_norm.\1"),
]
# Also remap o_proj → out_proj in both encoder and decoder
_POST_RENAME = [
(r"\.o_proj\.", r".out_proj."),
]
def _remap_converted_name(name: str) -> str:
"""Translate a converted-format weight name to our naming convention."""
for pattern, replacement in _CONVERTED_RULES:
result, n = re.subn(f"^{pattern}$", replacement, name)
if n:
name = result
break
for pattern, replacement in _POST_RENAME:
name = re.sub(pattern, replacement, name)
return name
# ---------------------------------------------------------------------------
# Loading strategies
# ---------------------------------------------------------------------------
def _has_converted_layout(path: Path) -> bool:
return (path / "config.json").exists() and not (path / "consolidated.safetensors").exists()
def _load_converted_weights(path: Path):
with open(path / "config.json") as f:
config = json.load(f)
model = VoxtralMLXModel(config)
quant = config.get("quantization")
if quant is not None:
gs = quant["group_size"]
nn.quantize(
model,
group_size=gs,
bits=quant["bits"],
class_predicate=lambda _p, m: (
hasattr(m, "to_quantized") and m.weight.shape[-1] % gs == 0
),
)
index_file = path / "model.safetensors.index.json"
if index_file.exists():
with open(index_file) as f:
shard_map = json.load(f)
shard_files = sorted(set(shard_map["weight_map"].values()))
weights = {}
for sf in shard_files:
weights.update(mx.load(str(path / sf)))
else:
weights = mx.load(str(path / "model.safetensors"))
remapped = {_remap_converted_name(k): v for k, v in weights.items()}
model.load_weights(list(remapped.items()))
mx.eval(model.parameters())
return model, config
def _load_original_weights(path: Path):
with open(path / "params.json") as f:
config = json.load(f)
model = VoxtralMLXModel(config)
raw = mx.load(str(path / "consolidated.safetensors"))
mapped: dict[str, mx.array] = {}
skipped: list[str] = []
for name, tensor in raw.items():
if name == "output.weight":
continue
new_name = _translate_weight_name(name)
if new_name is None:
skipped.append(name)
continue
# Conv weights: PyTorch [C_out, C_in, K] → MLX [C_out, K, C_in]
if _is_conv_weight(new_name):
tensor = mx.swapaxes(tensor, 1, 2)
mapped[new_name] = tensor
if skipped:
logger.warning("Skipped %d unrecognised weight keys (first 5: %s)", len(skipped), skipped[:5])
model.load_weights(list(mapped.items()))
mx.eval(model.parameters())
return model, config
# ---------------------------------------------------------------------------
# Tokenizer
# ---------------------------------------------------------------------------
def _load_tokenizer(model_dir: Path):
from mistral_common.tokens.tokenizers.tekken import Tekkenizer
return Tekkenizer.from_file(str(model_dir / "tekken.json"))
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def load_voxtral_model(path_or_id: str = DEFAULT_MODEL_ID):
"""Load a Voxtral Realtime model and its tokenizer.
Args:
path_or_id: Local directory path **or** a HuggingFace model ID.
Returns:
``(model, tokenizer, config)``
"""
p = Path(path_or_id)
if not p.exists():
p = download_weights(path_or_id)
if _has_converted_layout(p):
model, config = _load_converted_weights(p)
else:
model, config = _load_original_weights(p)
tokenizer = _load_tokenizer(p)
logger.info("Voxtral MLX model loaded from %s", p)
return model, tokenizer, config
================================================
FILE: whisperlivekit/voxtral_mlx/model.py
================================================
"""
Voxtral Realtime MLX model — encoder, decoder, adapter, and top-level model.
Architecture:
audio → StreamingEncoder → EncoderToDecoderAdapter → TextDecoder → logits
with DelayEmbedding providing time-conditioning to the decoder.
The model supports both batch inference (full audio) and incremental streaming
(one chunk at a time with cached encoder/decoder state).
"""
import math
import mlx.core as mx
import mlx.nn as nn
# ---------------------------------------------------------------------------
# KV Cache
# ---------------------------------------------------------------------------
class SlidingKVCache:
"""Bounded key-value cache with rotating buffer for sliding-window attention.
Uses in-place writes for single-token autoregressive steps and
concatenation for multi-token prefills. Pre-allocates in blocks of
``alloc_step`` entries to reduce repeated allocation.
"""
alloc_step = 256
def __init__(self, capacity: int):
self.capacity = capacity
self.keys = None
self.values = None
self._offset = 0
self._write_idx = 0
@property
def offset(self) -> int:
return self._offset
# -- helpers --
def _reorder(self, buf):
"""Return *buf* in temporal order (unwrap the circular buffer)."""
if self._write_idx == buf.shape[2]:
return buf
if self._write_idx < self._offset:
return mx.concatenate(
[buf[..., self._write_idx:, :], buf[..., : self._write_idx, :]],
axis=2,
)
return buf[..., : self._write_idx, :]
def _drop_oldest(self, buf, n_drop, tail=None):
parts = [buf[..., n_drop:, :]] if n_drop > 0 else [buf]
if tail is not None:
parts.append(tail)
return mx.concatenate(parts, axis=2)
# -- update strategies --
def _append_concat(self, k, v):
"""Multi-token update via concatenation (used during prefill)."""
if self.keys is None:
self.keys, self.values = k, v
else:
self.keys = self._reorder(self.keys)
self.values = self._reorder(self.values)
self._write_idx = self.keys.shape[2]
overflow = self._write_idx - self.capacity + 1
self.keys = self._drop_oldest(self.keys, overflow, k)
self.values = self._drop_oldest(self.values, overflow, v)
self._offset += k.shape[2]
self._write_idx = self.keys.shape[2]
return self.keys, self.values
def _write_inplace(self, k, v):
"""Single-token update via in-place write (autoregressive step)."""
B, n_heads, S, dim_k = k.shape
dim_v = v.shape[3]
prev = self._offset
if self.keys is None or (
prev >= self.keys.shape[2] and self.keys.shape[2] < self.capacity
):
n_new = min(self.alloc_step, self.capacity - prev)
fresh_k = mx.zeros((B, n_heads, n_new, dim_k), k.dtype)
fresh_v = mx.zeros((B, n_heads, n_new, dim_v), v.dtype)
if self.keys is not None:
self.keys = mx.concatenate([self.keys, fresh_k], axis=2)
self.values = mx.concatenate([self.values, fresh_v], axis=2)
else:
self.keys, self.values = fresh_k, fresh_v
self._write_idx = prev
overflow = self.keys.shape[2] - self.capacity
if overflow > 0:
self.keys = self._drop_oldest(self.keys, overflow)
self.values = self._drop_oldest(self.values, overflow)
self._write_idx = self.capacity
if self._write_idx == self.capacity:
self._write_idx = 0
self.keys[..., self._write_idx : self._write_idx + S, :] = k
self.values[..., self._write_idx : self._write_idx + S, :] = v
self._offset += S
self._write_idx += S
if self._offset < self.capacity:
return (
self.keys[..., : self._offset, :],
self.values[..., : self._offset, :],
)
return self.keys, self.values
# -- public API --
def update_and_fetch(self, k, v):
if k.shape[2] == 1:
return self._write_inplace(k, v)
return self._append_concat(k, v)
# ---------------------------------------------------------------------------
# Encoder components
# ---------------------------------------------------------------------------
class CausalConv(nn.Module):
"""1-D causal convolution (left-padded so no future leakage)."""
def __init__(self, channels_in: int, channels_out: int, kernel: int, stride: int = 1):
super().__init__()
self.stride = stride
self.kernel = kernel
self.left_pad = kernel - stride
self.weight = mx.zeros((channels_out, kernel, channels_in))
self.bias = mx.zeros((channels_out,))
def __call__(self, x: mx.array) -> mx.array:
if self.left_pad > 0:
x = mx.pad(x, [(0, 0), (self.left_pad, 0), (0, 0)])
return mx.conv1d(x, self.weight, stride=self.stride) + self.bias
class _EncoderSelfAttention(nn.Module):
def __init__(self, dim: int, n_heads: int, head_dim: int, rope_theta: float):
super().__init__()
self.n_heads = n_heads
self.head_dim = head_dim
self.scale = head_dim**-0.5
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=True)
self.k_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
self.v_proj = nn.Linear(dim, n_heads * head_dim, bias=True)
self.out_proj = nn.Linear(n_heads * head_dim, dim, bias=True)
self.rope_theta = rope_theta
def __call__(self, x, mask, cache=None):
B, L, _ = x.shape
q = self.q_proj(x).reshape(B, L, self.n_heads, self.head_dim).transpose(0, 2, 1, 3)
k = self.k_proj(x).reshape(B, L, self.n_heads, self.head_dim).transpose(0, 2, 1, 3)
v = self.v_proj(x).reshape(B, L, self.n_heads, self.head_dim).transpose(0, 2, 1, 3)
pos = cache.offset if cache is not None else 0
q = mx.fast.rope(q, self.head_dim, traditional=True, base=self.rope_theta, scale=1.0, offset=pos)
k = mx.fast.rope(k, self.head_dim, traditional=True, base=self.rope_theta, scale=1.0, offset=pos)
if cache is not None:
k, v = cache.update_and_fetch(k, v)
out = mx.fast.scaled_dot_product_attention(q, k, v, scale=self.scale, mask=mask)
return self.out_proj(out.transpose(0, 2, 1, 3).reshape(B, L, -1))
class _EncoderFFN(nn.Module):
"""SwiGLU feed-forward for encoder layers."""
def __init__(self, dim: int, hidden: int):
super().__init__()
self.gate = nn.Linear(dim, hidden, bias=False)
self.up = nn.Linear(dim, hidden, bias=False)
self.down = nn.Linear(hidden, dim, bias=True)
def __call__(self, x):
return self.down(nn.silu(self.gate(x)) * self.up(x))
class _EncoderBlock(nn.Module):
def __init__(self, dim, n_heads, head_dim, hidden, rope_theta):
super().__init__()
self.pre_attn_norm = nn.RMSNorm(dim, eps=1e-5)
self.self_attn = _EncoderSelfAttention(dim, n_heads, head_dim, rope_theta)
self.pre_ffn_norm = nn.RMSNorm(dim, eps=1e-5)
self.ffn = _EncoderFFN(dim, hidden)
def __call__(self, x, mask, cache=None):
x = x + self.self_attn(self.pre_attn_norm(x), mask, cache=cache)
x = x + self.ffn(self.pre_ffn_norm(x))
return x
class StreamingEncoder(nn.Module):
"""Causal Whisper-style encoder with two causal convolutions followed by
a stack of transformer blocks. Supports both full-sequence and
incremental (streaming) forward passes."""
def __init__(
self,
mel_channels: int = 128,
dim: int = 1280,
n_layers: int = 32,
n_heads: int = 32,
head_dim: int = 64,
hidden_dim: int = 5120,
rope_theta: float = 1e6,
sliding_window: int = 750,
):
super().__init__()
self.conv1 = CausalConv(mel_channels, dim, kernel=3, stride=1)
self.conv2 = CausalConv(dim, dim, kernel=3, stride=2)
self.blocks = [
_EncoderBlock(dim, n_heads, head_dim, hidden_dim, rope_theta)
for _ in range(n_layers)
]
self.final_norm = nn.RMSNorm(dim, eps=1e-5)
self.sliding_window = sliding_window
# -- full-sequence --
def _apply_convs(self, mel: mx.array) -> mx.array:
x = mel.T[None, :, :] # [1, T, mel_channels]
x = nn.gelu(self.conv1(x))
x = nn.gelu(self.conv2(x))
return x
def forward(self, mel: mx.array) -> mx.array:
x = self._apply_convs(mel.astype(self.conv1.weight.dtype))
for blk in self.blocks:
x = blk(x, mask="causal")
return self.final_norm(x)
# -- incremental (streaming) --
def forward_conv_incremental(self, x_in, tail1, tail2):
"""Process new mel frames through the two causal convs using cached tails.
Args:
x_in: [1, N, mel_channels]
tail1: [1, pad1, mel_channels] or None (first call)
tail2: [1, pad2, dim] or None (first call)
Returns:
(out, new_tail1, new_tail2)
"""
# Conv1 (kernel=3, stride=1 → left_pad=2)
if tail1 is not None:
c1_in = mx.concatenate([tail1, x_in], axis=1)
else:
c1_in = mx.pad(x_in, [(0, 0), (self.conv1.left_pad, 0), (0, 0)])
new_tail1 = x_in[:, -self.conv1.left_pad :, :]
c1_out = nn.gelu(
mx.conv1d(c1_in, self.conv1.weight, stride=self.conv1.stride) + self.conv1.bias
)
# Conv2 (kernel=3, stride=2 → left_pad=1)
if tail2 is not None:
c2_in = mx.concatenate([tail2, c1_out], axis=1)
else:
c2_in = mx.pad(c1_out, [(0, 0), (self.conv2.left_pad, 0), (0, 0)])
new_tail2 = c1_out[:, -self.conv2.left_pad :, :]
c2_out = nn.gelu(
mx.conv1d(c2_in, self.conv2.weight, stride=self.conv2.stride) + self.conv2.bias
)
return c2_out, new_tail1, new_tail2
def forward_transformer_incremental(self, x, cache_list):
"""Run transformer blocks with per-layer KV caches."""
for i, blk in enumerate(self.blocks):
x = blk(x, mask="causal", cache=cache_list[i])
return self.final_norm(x)
# ---------------------------------------------------------------------------
# Decoder components
# ---------------------------------------------------------------------------
class _DecoderAttention(nn.Module):
"""Grouped-query attention for the text decoder."""
def __init__(self, dim, n_heads, n_kv_heads, head_dim, rope_theta):
super().__init__()
self.n_heads = n_heads
self.n_kv_heads = n_kv_heads
self.head_dim = head_dim
self.scale = head_dim**-0.5
self.q_proj = nn.Linear(dim, n_heads * head_dim, bias=False)
self.k_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
self.v_proj = nn.Linear(dim, n_kv_heads * head_dim, bias=False)
self.out_proj = nn.Linear(n_heads * head_dim, dim, bias=False)
self.rope_theta = rope_theta
def __call__(self, x, mask=None, cache=None):
B, L, _ = x.shape
q = self.q_proj(x).reshape(B, L, self.n_heads, self.head_dim).transpose(0, 2, 1, 3)
k = self.k_proj(x).reshape(B, L, self.n_kv_heads, self.head_dim).transpose(0, 2, 1, 3)
v = self.v_proj(x).reshape(B, L, self.n_kv_heads, self.head_dim).transpose(0, 2, 1, 3)
pos = cache.offset if cache is not None else 0
q = mx.fast.rope(q, self.head_dim, traditional=True, base=self.rope_theta, scale=1.0, offset=pos)
k = mx.fast.rope(k, self.head_dim, traditional=True, base=self.rope_theta, scale=1.0, offset=pos)
if cache is not None:
k, v = cache.update_and_fetch(k, v)
out = mx.fast.scaled_dot_product_attention(q, k, v, scale=self.scale, mask=mask)
return self.out_proj(out.transpose(0, 2, 1, 3).reshape(B, L, -1))
class _DecoderFFN(nn.Module):
"""SwiGLU feed-forward for decoder layers."""
def __init__(self, dim, hidden):
super().__init__()
self.gate = nn.Linear(dim, hidden, bias=False)
self.up = nn.Linear(dim, hidden, bias=False)
self.down = nn.Linear(hidden, dim, bias=False)
def __call__(self, x):
return self.down(nn.silu(self.gate(x)) * self.up(x))
class AdaptiveScaling(nn.Module):
"""Small MLP that produces a multiplicative scale from the delay embedding,
used to condition the FFN on the streaming delay."""
def __init__(self, dim, bottleneck):
super().__init__()
self.proj_in = nn.Linear(dim, bottleneck, bias=False)
self.proj_out = nn.Linear(bottleneck, dim, bias=False)
def __call__(self, cond):
return self.proj_out(nn.gelu(self.proj_in(cond)))
class _DecoderBlock(nn.Module):
def __init__(self, dim, n_heads, n_kv_heads, head_dim, hidden, rope_theta, cond_dim):
super().__init__()
self.pre_attn_norm = nn.RMSNorm(dim, eps=1e-5)
self.self_attn = _DecoderAttention(dim, n_heads, n_kv_heads, head_dim, rope_theta)
self.adaptive_scale = AdaptiveScaling(dim, cond_dim)
self.pre_ffn_norm = nn.RMSNorm(dim, eps=1e-5)
self.ffn = _DecoderFFN(dim, hidden)
def __call__(self, x, delay_cond, mask=None, cache=None):
x = x + self.self_attn(self.pre_attn_norm(x), mask, cache)
scaled = self.pre_ffn_norm(x) * (1.0 + self.adaptive_scale(delay_cond))
x = x + self.ffn(scaled)
return x
class TextDecoder(nn.Module):
"""Mistral-style causal language model with adaptive time-conditioning."""
def __init__(
self,
dim: int = 3072,
n_layers: int = 26,
n_heads: int = 32,
n_kv_heads: int = 8,
head_dim: int = 128,
hidden_dim: int = 9216,
vocab_size: int = 131072,
rope_theta: float = 1e6,
cond_dim: int = 32,
):
super().__init__()
self.token_embedding = nn.Embedding(vocab_size, dim)
self.blocks = [
_DecoderBlock(dim, n_heads, n_kv_heads, head_dim, hidden_dim, rope_theta, cond_dim)
for _ in range(n_layers)
]
self.final_norm = nn.RMSNorm(dim, eps=1e-5)
def embed(self, token_ids: mx.array) -> mx.array:
return self.token_embedding(token_ids)
def __call__(self, x, delay_cond, mask=None, cache=None):
delay_cond = delay_cond.astype(x.dtype)
for i, blk in enumerate(self.blocks):
blk_cache = cache[i] if cache is not None else None
x = blk(x, delay_cond, mask, blk_cache)
x = self.final_norm(x)
return self.token_embedding.as_linear(x)
# ---------------------------------------------------------------------------
# Adapter & embeddings
# ---------------------------------------------------------------------------
class EncoderToDecoderAdapter(nn.Module):
"""Two-layer projection from encoder space to decoder space."""
def __init__(self, enc_dim: int, dec_dim: int):
super().__init__()
self.linear1 = nn.Linear(enc_dim, dec_dim, bias=False)
self.linear2 = nn.Linear(dec_dim, dec_dim, bias=False)
def __call__(self, x):
return self.linear2(nn.gelu(self.linear1(x)))
class DelayEmbedding(nn.Module):
"""Sinusoidal embedding that encodes the streaming delay as a conditioning
vector for the decoder's adaptive scaling."""
def __init__(self, dim: int = 3072, theta: float = 10000.0):
super().__init__()
self.dim = dim
half = dim // 2
freqs = mx.exp(-math.log(theta) * mx.arange(half, dtype=mx.float32) / half)
self._freqs = freqs
def __call__(self, delay: mx.array) -> mx.array:
t = delay.reshape(-1, 1).astype(mx.float32)
angles = t * self._freqs
return mx.concatenate([mx.cos(angles), mx.sin(angles)], axis=-1)
# ---------------------------------------------------------------------------
# Top-level model
# ---------------------------------------------------------------------------
class VoxtralMLXModel(nn.Module):
"""Top-level Voxtral Realtime model wiring encoder, adapter, and decoder."""
def __init__(self, config: dict):
super().__init__()
enc_cfg = config["multimodal"]["whisper_model_args"]["encoder_args"]
audio_cfg = enc_cfg["audio_encoding_args"]
ds_factor = config["multimodal"]["whisper_model_args"]["downsample_args"]["downsample_factor"]
self.encoder = StreamingEncoder(
mel_channels=audio_cfg["num_mel_bins"],
dim=enc_cfg["dim"],
n_layers=enc_cfg["n_layers"],
n_heads=enc_cfg["n_heads"],
head_dim=enc_cfg["head_dim"],
hidden_dim=enc_cfg["hidden_dim"],
rope_theta=enc_cfg["rope_theta"],
sliding_window=enc_cfg["sliding_window"],
)
adapter_input_dim = enc_cfg["dim"] * ds_factor
decoder_dim = config["dim"]
cond_bottleneck = config.get("ada_rms_norm_t_cond_dim", 32)
self.adapter = EncoderToDecoderAdapter(adapter_input_dim, decoder_dim)
self.decoder = TextDecoder(
dim=decoder_dim,
n_layers=config["n_layers"],
n_heads=config["n_heads"],
n_kv_heads=config["n_kv_heads"],
head_dim=config["head_dim"],
hidden_dim=config["hidden_dim"],
vocab_size=config["vocab_size"],
rope_theta=config["rope_theta"],
cond_dim=cond_bottleneck,
)
self.delay_embedding = DelayEmbedding(dim=decoder_dim)
self.ds_factor = ds_factor
# -- batch encode --
def encode(self, mel: mx.array) -> mx.array:
T = mel.shape[1]
if T % 2 != 0:
mel = mel[:, 1:]
h = self.encoder.forward(mel) # [1, T/2, enc_dim]
h = h[0]
n = h.shape[0]
trim = n % self.ds_factor
if trim:
h = h[trim:]
n = h.shape[0]
h = h.reshape(n // self.ds_factor, -1)
return self.adapter(h)
# -- incremental encode --
def encode_incremental(self, new_mel, conv_tail1, conv_tail2, enc_cache, ds_remainder):
"""Incrementally encode new mel frames.
Returns:
(audio_embeds | None, conv_tail1, conv_tail2, enc_cache, ds_remainder)
"""
x = new_mel.T[None, :, :].astype(self.encoder.conv1.weight.dtype)
x, conv_tail1, conv_tail2 = self.encoder.forward_conv_incremental(x, conv_tail1, conv_tail2)
if enc_cache is None:
enc_cache = [SlidingKVCache(100_000) for _ in range(len(self.encoder.blocks))]
x = self.encoder.forward_transformer_incremental(x, enc_cache)
x = x[0] # [N, enc_dim]
if ds_remainder is not None:
x = mx.concatenate([ds_remainder, x])
n_full = (x.shape[0] // self.ds_factor) * self.ds_factor
if n_full == 0:
return None, conv_tail1, conv_tail2, enc_cache, x
leftover = x[n_full:] if x.shape[0] > n_full else None
x = x[:n_full].reshape(n_full // self.ds_factor, -1)
return self.adapter(x), conv_tail1, conv_tail2, enc_cache, leftover
# -- decode --
def decode(self, embeddings, delay_cond, mask=None, cache=None):
return self.decoder(embeddings, delay_cond, mask, cache)
================================================
FILE: whisperlivekit/voxtral_mlx/spectrogram.py
================================================
"""
Mel spectrogram computation for Voxtral Realtime.
Provides both a full-audio function and an incremental streaming variant
that maintains overlap state between calls. The DFT is computed via
matrix multiplication in MLX — no external FFT dependency required.
"""
import math
import mlx.core as mx
import numpy as np
# Audio / mel constants matching the Voxtral Realtime model expectations.
SAMPLE_RATE = 16_000
WINDOW_SIZE = 400 # n_fft
HOP = 160
MEL_BANDS = 128
MEL_MAX = 1.5 # global log-mel normalisation ceiling
# Each output audio token spans: hop * conv_stride(2) * downsample_factor(4)
SAMPLES_PER_TOKEN = HOP * 2 * 4 # = 1280 samples = 80 ms
# Padding tokens used by the model prompt structure.
LEFT_PAD_TOKENS = 32
RIGHT_PAD_TOKENS = 17
# ---------------------------------------------------------------------------
# Slaney mel filterbank
# ---------------------------------------------------------------------------
def _build_slaney_filterbank(
sr: int = SAMPLE_RATE,
n_fft: int = WINDOW_SIZE,
n_mels: int = MEL_BANDS,
lo_hz: float = 0.0,
hi_hz: float = 8000.0,
) -> np.ndarray:
"""Compute a Slaney-normalised triangular mel filterbank.
Returns an array of shape ``[n_mels, n_fft//2 + 1]``.
"""
def _hz2mel(f):
threshold = 1000.0
base_mel = 15.0
log_coeff = 27.0 / np.log(6.4)
mel = 3.0 * f / 200.0
if isinstance(f, np.ndarray):
above = f >= threshold
mel[above] = base_mel + np.log(f[above] / threshold) * log_coeff
elif f >= threshold:
mel = base_mel + np.log(f / threshold) * log_coeff
return mel
def _mel2hz(m):
threshold = 1000.0
base_mel = 15.0
log_coeff = np.log(6.4) / 27.0
hz = 200.0 * m / 3.0
above = m >= base_mel
hz[above] = threshold * np.exp(log_coeff * (m[above] - base_mel))
return hz
n_bins = n_fft // 2 + 1
fft_hz = np.linspace(0, sr / 2, n_bins)
mel_lo, mel_hi = _hz2mel(lo_hz), _hz2mel(hi_hz)
mel_pts = np.linspace(mel_lo, mel_hi, n_mels + 2)
hz_pts = _mel2hz(mel_pts)
diffs = np.diff(hz_pts)
slopes = np.expand_dims(hz_pts, 0) - np.expand_dims(fft_hz, 1)
rising = -slopes[:, :-2] / diffs[:-1]
falling = slopes[:, 2:] / diffs[1:]
fb = np.maximum(0.0, np.minimum(rising, falling))
# Slaney area normalisation
widths = 2.0 / (hz_pts[2 : n_mels + 2] - hz_pts[:n_mels])
fb *= np.expand_dims(widths, 0)
return fb.T.astype(np.float32)
_CACHED_FILTERS: mx.array | None = None
def _mel_filters() -> mx.array:
global _CACHED_FILTERS
if _CACHED_FILTERS is None:
_CACHED_FILTERS = mx.array(_build_slaney_filterbank())
return _CACHED_FILTERS
# ---------------------------------------------------------------------------
# DFT helpers (cached — these are constant for a given WINDOW_SIZE)
# ---------------------------------------------------------------------------
_CACHED_WINDOW: mx.array | None = None
_CACHED_DFT_RE: mx.array | None = None
_CACHED_DFT_IM: mx.array | None = None
def _hann_window() -> mx.array:
global _CACHED_WINDOW
if _CACHED_WINDOW is None:
_CACHED_WINDOW = mx.array(np.hanning(WINDOW_SIZE + 1)[:-1].astype(np.float32))
return _CACHED_WINDOW
def _dft_matrices():
"""Return cached real / imaginary DFT basis matrices."""
global _CACHED_DFT_RE, _CACHED_DFT_IM
if _CACHED_DFT_RE is None:
n_bins = WINDOW_SIZE // 2 + 1
k = mx.arange(n_bins, dtype=mx.float32)[:, None]
n = mx.arange(WINDOW_SIZE, dtype=mx.float32)[None, :]
phase = -2.0 * math.pi * (k @ n) / WINDOW_SIZE
_CACHED_DFT_RE = mx.cos(phase)
_CACHED_DFT_IM = mx.sin(phase)
mx.eval(_CACHED_DFT_RE, _CACHED_DFT_IM)
return _CACHED_DFT_RE, _CACHED_DFT_IM
def _stft_frames(audio: mx.array, window: mx.array) -> mx.array:
"""Frame *audio* using the Hann window and compute power spectrogram."""
n_bins = WINDOW_SIZE // 2 + 1
n_frames = 1 + (audio.shape[0] - WINDOW_SIZE) // HOP
if n_frames <= 0:
return mx.zeros((0, n_bins))
offsets = (mx.arange(n_frames) * HOP)[:, None]
indices = offsets + mx.arange(WINDOW_SIZE)[None, :]
windowed = audio[indices] * window[None, :]
dft_re, dft_im = _dft_matrices()
real_part = windowed @ dft_re.T
imag_part = windowed @ dft_im.T
return real_part ** 2 + imag_part ** 2
def _apply_mel_and_log(power: mx.array) -> mx.array:
"""Convert a power spectrogram to log-mel and normalise."""
mel = power @ _mel_filters().T
log_mel = mx.log10(mx.maximum(mel, 1e-10))
log_mel = mx.maximum(log_mel, MEL_MAX - 8.0)
return (log_mel + 4.0) / 4.0
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def compute_mel(audio: np.ndarray) -> mx.array:
"""Compute log-mel spectrogram for a complete audio signal.
Args:
audio: 1-D float32 numpy array at ``SAMPLE_RATE``.
Returns:
``[MEL_BANDS, T]`` MLX array.
"""
x = mx.array(audio)
pad = WINDOW_SIZE // 2
x = mx.pad(x, [(pad, pad)])
window = _hann_window()
power = _stft_frames(x, window)
# Drop last frame to match reference STFT behaviour
power = power[:-1]
return _apply_mel_and_log(power).T
def compute_mel_streaming(
chunk: np.ndarray,
overlap: np.ndarray | None,
) -> tuple[mx.array, np.ndarray]:
"""Incrementally compute log-mel for a new audio chunk.
Args:
chunk: New audio samples (float32 numpy).
overlap: The last ``WINDOW_SIZE - HOP`` = 240 samples from the
previous call, or *None* on the first call (uses zero-padding).
Returns:
``(mel, new_overlap)`` where *mel* is ``[MEL_BANDS, N]`` and
*new_overlap* is the 240-sample tail for the next call.
"""
tail_len = WINDOW_SIZE - HOP # 240
if overlap is not None:
combined = np.concatenate([overlap, chunk])
else:
combined = np.concatenate([np.zeros(WINDOW_SIZE // 2, dtype=np.float32), chunk])
new_overlap = combined[-tail_len:].copy()
x = mx.array(combined)
window = _hann_window()
power = _stft_frames(x, window)
if power.shape[0] == 0:
return mx.zeros((MEL_BANDS, 0)), new_overlap
return _apply_mel_and_log(power).T, new_overlap
def pad_audio(
audio: np.ndarray,
n_left: int = LEFT_PAD_TOKENS,
n_right: int = RIGHT_PAD_TOKENS,
) -> np.ndarray:
"""Pad audio with silence for batch (non-streaming) inference."""
left = n_left * SAMPLES_PER_TOKEN
align = (SAMPLES_PER_TOKEN - (len(audio) % SAMPLES_PER_TOKEN)) % SAMPLES_PER_TOKEN
right = align + n_right * SAMPLES_PER_TOKEN
return np.pad(audio, (left, right))
================================================
FILE: whisperlivekit/voxtral_mlx_asr.py
================================================
"""
Pure-MLX Voxtral Realtime ASR backend for WhisperLiveKit.
Provides ``VoxtralMLXASR`` (model holder) and ``VoxtralMLXOnlineProcessor``
(streaming processor) that plug into WhisperLiveKit's audio processing
pipeline via ``insert_audio_chunk`` / ``process_iter`` / ``get_buffer`` etc.
Unlike the HuggingFace backend, this runs the full inference loop in-process
(no background thread / queue) — MLX operations on Apple Silicon are fast
enough to run synchronously inside ``asyncio.to_thread(process_iter)``.
"""
import logging
import sys
import time
from typing import List, Optional, Tuple
import mlx.core as mx
import numpy as np
from mistral_common.tokens.tokenizers.base import SpecialTokenPolicy
from whisperlivekit.timed_objects import ASRToken, Transcript
from whisperlivekit.voxtral_mlx.loader import DEFAULT_MODEL_ID, load_voxtral_model
from whisperlivekit.voxtral_mlx.model import SlidingKVCache
from whisperlivekit.voxtral_mlx.spectrogram import (
LEFT_PAD_TOKENS,
RIGHT_PAD_TOKENS,
SAMPLES_PER_TOKEN,
compute_mel_streaming,
)
logger = logging.getLogger(__name__)
# Decoder sliding-window size (matches the model's training configuration).
_DECODER_WINDOW = 8192
# Maximum continuous decoding positions before forcing a reset.
# Beyond ~20s of continuous audio the autoregressive context drifts and
# produces hallucination. 20s / 80ms per token = 250 tokens.
_MAX_CONTINUOUS_POSITIONS = 250
def _prompt_tokens(tokenizer, n_left_pad=LEFT_PAD_TOKENS, n_delay=6):
"""Build the prompt token sequence and return ``(token_ids, n_delay)``."""
pad_id = tokenizer.get_special_token("[STREAMING_PAD]")
ids = [tokenizer.bos_id] + [pad_id] * (n_left_pad + n_delay)
return ids, n_delay
# ---------------------------------------------------------------------------
# Model holder
# ---------------------------------------------------------------------------
class VoxtralMLXASR:
"""Lightweight model holder — loads the MLX Voxtral model once and keeps
it alive for the lifetime of the server."""
sep = " "
SAMPLING_RATE = 16_000
def __init__(self, logfile=sys.stderr, **kwargs):
self.logfile = logfile
self.transcribe_kargs = {}
lan = kwargs.get("lan", "auto")
self.original_language = None if lan == "auto" else lan
model_path = kwargs.get("model_dir") or kwargs.get("model_path")
if not model_path:
model_size = kwargs.get("model_size", "")
if model_size and ("/" in model_size or model_size.startswith(".")):
model_path = model_size
else:
model_path = DEFAULT_MODEL_ID
t0 = time.time()
logger.info("Loading Voxtral MLX model '%s' ...", model_path)
self.model, self.tokenizer, self.config = load_voxtral_model(model_path)
logger.info("Voxtral MLX model loaded in %.2fs", time.time() - t0)
self.backend_choice = "voxtral-mlx"
def transcribe(self, audio):
pass # all work happens in the online processor
# ---------------------------------------------------------------------------
# Online processor
# ---------------------------------------------------------------------------
class VoxtralMLXOnlineProcessor:
"""Streaming processor that incrementally encodes audio and decodes text
using the MLX Voxtral model.
Lifecycle (called by ``AudioProcessor.transcription_processor``):
insert_audio_chunk(pcm, time) → process_iter() → get_buffer()
... repeat ...
start_silence() / end_silence()
finish()
"""
SAMPLING_RATE = 16_000
def __init__(self, asr: VoxtralMLXASR, logfile=sys.stderr):
self.asr = asr
self.logfile = logfile
self.end = 0.0
self.buffer: list = []
self.audio_buffer = np.array([], dtype=np.float32)
self._model = asr.model
self._tokenizer = asr.tokenizer
# Pre-compute prompt tokens and delay conditioning (constant across utterances).
self._prompt_ids, self._n_delay = _prompt_tokens(self._tokenizer)
self._prefix_len = len(self._prompt_ids)
self._delay_cond = self._model.delay_embedding(
mx.array([self._n_delay], dtype=mx.float32)
)
mx.eval(self._delay_cond)
self._prompt_embeds = self._model.decoder.embed(
mx.array([self._prompt_ids])
)[0] # [prefix_len, dim]
mx.eval(self._prompt_embeds)
self._eos_id = self._tokenizer.eos_id
self._secs_per_token = SAMPLES_PER_TOKEN / self.SAMPLING_RATE
# The streaming model has an inherent delay: text for audio at position P
# is generated at decoder position P + n_delay. Compensate timestamps.
self._delay_secs = self._n_delay * self._secs_per_token
self._reset_state()
# -- state management --
def _reset_state(self):
"""Reset all incremental state for a fresh utterance."""
# Audio accumulation (list of chunks, concatenated on demand)
self._pending_chunks: list[np.ndarray] = []
self._pending_len = 0
# Mel overlap
self._mel_overlap: np.ndarray | None = None
# Encoder incremental state
self._conv_tail1 = None
self._conv_tail2 = None
self._enc_cache = None
self._ds_remainder = None
# Audio embeddings not yet decoded
self._audio_embeds: mx.array | None = None
# Decoder state
self._dec_cache: list[SlidingKVCache] | None = None
self._last_token: mx.array | None = None
# Bookkeeping
self._samples_encoded = 0
self._real_samples_encoded = 0 # only real audio, excludes silence padding
self._positions_decoded = 0
self._prefilled = False
self._first_chunk = True
# Text state
self._full_text = ""
self._n_text_tokens = 0
self._n_committed_words = 0
self._time_offset = 0.0
# Per-word audio position tracking: decoder position (relative to prefix)
# where each word in _full_text started and ended
self._word_audio_starts: list[int] = [] # audio pos where word i started
self._word_audio_ends: list[int] = [] # audio pos where word i last produced a token
self._current_word_pos: Optional[int] = None # audio pos of current (incomplete) word's first token
# -- audio ingestion --
def _get_pending(self) -> np.ndarray:
"""Flatten pending chunks into a single array."""
if not self._pending_chunks:
return np.zeros(0, dtype=np.float32)
if len(self._pending_chunks) == 1:
return self._pending_chunks[0]
flat = np.concatenate(self._pending_chunks)
self._pending_chunks = [flat]
return flat
def _set_pending(self, arr: np.ndarray):
"""Replace pending audio with a single array."""
if len(arr) == 0:
self._pending_chunks = []
self._pending_len = 0
else:
self._pending_chunks = [arr]
self._pending_len = len(arr)
def insert_audio_chunk(self, audio: np.ndarray, audio_stream_end_time: float):
self.end = audio_stream_end_time
self._pending_chunks.append(audio)
self._pending_len += len(audio)
self._real_samples_encoded += len(audio)
self.audio_buffer = audio # diagnostic only
# -- core processing --
def process_iter(self, is_last=False) -> Tuple[List[ASRToken], float]:
try:
return self._step(is_last)
except Exception as e:
logger.warning("[voxtral-mlx] process_iter error: %s", e, exc_info=True)
return [], self.end
def _step(self, is_last: bool) -> Tuple[List[ASRToken], float]:
# 0. Safety cap: if continuous decoding exceeds the limit, force a
# flush+reset to prevent hallucination even without VAD silence.
if self._prefilled and self._positions_decoded >= _MAX_CONTINUOUS_POSITIONS + self._prefix_len:
logger.info(
"[voxtral-mlx] continuous decoding cap hit at %d positions — "
"forcing flush+reset",
self._positions_decoded,
)
words = self._flush_and_reset()
return words, self.end
# 1. Encode any new audio
self._encode_pending()
if self._audio_embeds is None:
return [], self.end
# 2. Compute how many positions we can safely decode.
# The safe boundary prevents the decoder from running ahead of the
# audio encoder. _samples_encoded tracks only real audio (not
# silence padding), so positions beyond this produce hallucination.
total_safe = LEFT_PAD_TOKENS + self._real_samples_encoded // SAMPLES_PER_TOKEN
n_available = self._audio_embeds.shape[0]
n_decodable = min(n_available, total_safe - self._positions_decoded)
if n_decodable <= 0:
return [], self.end
# 3. Prefill if needed
if not self._prefilled:
if self._positions_decoded + n_available < self._prefix_len:
return [], self.end
self._do_prefill()
# Re-check after consuming prefix embeddings
n_available = self._audio_embeds.shape[0] if self._audio_embeds is not None else 0
n_decodable = min(n_available, total_safe - self._positions_decoded)
if n_decodable <= 0 or self._audio_embeds is None:
return [], self.end
# Clamp to the continuous decoding cap so we don't overshoot
max_left = _MAX_CONTINUOUS_POSITIONS + self._prefix_len - self._positions_decoded
if max_left > 0:
n_decodable = min(n_decodable, max_left)
else:
# Will be caught by the cap check on the next call
return self._extract_committed_words(), self.end
# 4. Decode available positions
hit_eos = self._decode_positions(n_decodable)
if hit_eos:
# Flush words, then full reset for next utterance
words = self._flush_all_words()
logger.debug(
"[voxtral-mlx] EOS hit during stream: flushed %d words, "
"samples_encoded=%d (%.2fs), text='%s'",
len(words), self._samples_encoded,
self._samples_encoded / self.SAMPLING_RATE,
self._full_text[-60:] if self._full_text else "",
)
new_offset = self._time_offset + self._real_samples_encoded / self.SAMPLING_RATE
saved_end = self.end
self._reset_state()
self._time_offset = new_offset
self.end = saved_end
mx.clear_cache()
return words, self.end
# 5. Extract committed words (all but the last, which may still grow)
return self._extract_committed_words(), self.end
def _encode_pending(self):
"""Feed pending audio through the incremental encoder."""
if self._pending_len < SAMPLES_PER_TOKEN:
return
pending = self._get_pending()
available = len(pending)
if self._first_chunk:
# First chunk: prepend silence for left-padding
n_take = (available // SAMPLES_PER_TOKEN) * SAMPLES_PER_TOKEN
left_pad = np.zeros(LEFT_PAD_TOKENS * SAMPLES_PER_TOKEN, dtype=np.float32)
chunk = np.concatenate([left_pad, pending[:n_take]])
self._set_pending(pending[n_take:])
self._samples_encoded += n_take
self._first_chunk = False
else:
n_take = (available // SAMPLES_PER_TOKEN) * SAMPLES_PER_TOKEN
chunk = pending[:n_take]
self._set_pending(pending[n_take:])
self._samples_encoded += n_take
mel, self._mel_overlap = compute_mel_streaming(chunk, self._mel_overlap)
embeds, self._conv_tail1, self._conv_tail2, self._enc_cache, self._ds_remainder = (
self._model.encode_incremental(
mel, self._conv_tail1, self._conv_tail2, self._enc_cache, self._ds_remainder
)
)
if embeds is not None:
mx.eval(embeds)
if self._audio_embeds is not None:
self._audio_embeds = mx.concatenate([self._audio_embeds, embeds])
mx.eval(self._audio_embeds)
else:
self._audio_embeds = embeds
def _do_prefill(self):
"""Run the decoder prefill pass over the prompt + first audio embeddings."""
n_dec_layers = len(self._model.decoder.blocks)
self._dec_cache = [SlidingKVCache(_DECODER_WINDOW) for _ in range(n_dec_layers)]
prefix_embeds = self._prompt_embeds + self._audio_embeds[: self._prefix_len]
prefix_embeds = prefix_embeds[None, :, :] # [1, prefix_len, dim]
logits = self._model.decode(prefix_embeds, self._delay_cond, "causal", self._dec_cache)
mx.eval(logits, *[x for c in self._dec_cache for x in (c.keys, c.values)])
self._last_token = self._sample(logits)
mx.async_eval(self._last_token)
# Remove consumed prefix embeddings
self._audio_embeds = self._audio_embeds[self._prefix_len :]
if self._audio_embeds.shape[0] == 0:
self._audio_embeds = None
self._positions_decoded = self._prefix_len
self._prefilled = True
def _decode_positions(self, n: int) -> bool:
"""Autoregressively decode *n* positions. Returns True on EOS."""
base_pos = self._positions_decoded # absolute position before this batch
for i in range(n):
tok_embed = self._model.decoder.embed(self._last_token.reshape(1, 1))[0, 0]
combined = (self._audio_embeds[i] + tok_embed)[None, None, :]
logits = self._model.decode(combined, self._delay_cond, mask=None, cache=self._dec_cache)
next_tok = self._sample(logits)
mx.async_eval(next_tok)
token_id = self._last_token.item()
if token_id == self._eos_id:
# Close the current word if one is being built
if self._current_word_pos is not None:
self._word_audio_ends.append(base_pos + i - self._prefix_len)
self._current_word_pos = None
self._trim_embeds(i)
self._positions_decoded += i
return True
text = self._tokenizer.decode(
[token_id], special_token_policy=SpecialTokenPolicy.IGNORE
)
if text:
audio_pos = base_pos + i - self._prefix_len
# Detect word boundary: new word starts with space or is the very first text
if text.lstrip() != text or not self._full_text:
# Close previous word if exists
if self._current_word_pos is not None:
self._word_audio_ends.append(audio_pos)
# Start new word
self._word_audio_starts.append(audio_pos)
self._current_word_pos = audio_pos
elif self._current_word_pos is None:
# First token of first word (no leading space)
self._word_audio_starts.append(audio_pos)
self._current_word_pos = audio_pos
self._full_text += text
self._n_text_tokens += 1
if i > 0 and i % 256 == 0:
mx.clear_cache()
self._last_token = next_tok
self._positions_decoded += n
self._trim_embeds(n)
return False
def _trim_embeds(self, n_consumed: int):
if self._audio_embeds is not None and self._audio_embeds.shape[0] > n_consumed:
self._audio_embeds = self._audio_embeds[n_consumed:]
else:
self._audio_embeds = None
def _sample(self, logits: mx.array) -> mx.array:
return mx.argmax(logits[0, -1:], axis=-1).squeeze()
# -- word extraction --
def _audio_pos_to_time(self, pos: int) -> float:
"""Convert an audio position (relative to prefix end) to seconds."""
return max(0.0, pos * self._secs_per_token - self._delay_secs + self._time_offset)
def _word_time_range(self, word_idx: int, n_words: int) -> Tuple[float, float]:
"""Compute (start, end) time for a word using tracked word positions."""
starts = self._word_audio_starts
ends = self._word_audio_ends
if not starts:
return self._time_offset, self._time_offset
# Get start position for this word
if word_idx < len(starts):
t0 = self._audio_pos_to_time(starts[word_idx])
else:
# Fallback: estimate from last known position
last_pos = ends[-1] if ends else starts[-1]
t0 = self._audio_pos_to_time(last_pos + 1)
# Get end position: use the start of the next word, or the end of this word
if word_idx + 1 < len(starts):
t1 = self._audio_pos_to_time(starts[word_idx + 1])
elif word_idx < len(ends):
t1 = self._audio_pos_to_time(ends[word_idx] + 1)
else:
# Last word, still being built: use last known position + 1 token
last_pos = starts[word_idx] if word_idx < len(starts) else (ends[-1] if ends else 0)
t1 = self._audio_pos_to_time(last_pos + 1)
return t0, t1
def _extract_committed_words(self) -> List[ASRToken]:
"""Return complete words (all except the last which may still grow)."""
if not self._full_text:
return []
words = self._full_text.split()
tokens: List[ASRToken] = []
n_total = max(len(words), 1)
while len(words) > self._n_committed_words + 1:
w = words[self._n_committed_words]
idx = self._n_committed_words
t0, t1 = self._word_time_range(idx, n_total)
label = w if idx == 0 else " " + w
tokens.append(ASRToken(start=t0, end=t1, text=label))
self._n_committed_words += 1
return tokens
def _flush_all_words(self) -> List[ASRToken]:
"""Flush every word including the last partial one."""
if not self._full_text:
return []
words = self._full_text.split()
tokens: List[ASRToken] = []
n_total = max(len(words), 1)
while self._n_committed_words < len(words):
w = words[self._n_committed_words]
idx = self._n_committed_words
t0, t1 = self._word_time_range(idx, n_total)
label = w if idx == 0 else " " + w
tokens.append(ASRToken(start=t0, end=t1, text=label))
self._n_committed_words += 1
return tokens
# -- interface methods --
def get_buffer(self) -> Transcript:
if not self._full_text:
return Transcript(start=None, end=None, text="")
words = self._full_text.split()
remaining = words[self._n_committed_words :]
if remaining:
return Transcript(start=self.end, end=self.end, text=" ".join(remaining))
return Transcript(start=None, end=None, text="")
def _safe_decode_remaining(self):
"""Decode remaining audio embeddings, respecting the safe boundary.
Uses the same guard as ``_step`` to avoid decoding positions that
are beyond the real audio frontier, which causes hallucination.
"""
if self._audio_embeds is None or not self._prefilled:
return
# Use the same formula as _step() — this excludes padding positions
total_safe = LEFT_PAD_TOKENS + self._samples_encoded // SAMPLES_PER_TOKEN
n_available = self._audio_embeds.shape[0]
n_decodable = min(n_available, max(0, total_safe - self._positions_decoded))
# Cap at RIGHT_PAD_TOKENS to only decode the padding needed for
# the model to emit final tokens, not all accumulated padding
n_decodable = min(n_decodable, RIGHT_PAD_TOKENS)
if n_decodable > 0:
self._decode_positions(n_decodable)
def _flush_last_token_text(self):
"""Add the last pending token's text (if not EOS) to _full_text."""
if self._last_token is None:
return
tid = self._last_token.item()
if tid == self._eos_id:
return
text = self._tokenizer.decode(
[tid], special_token_policy=SpecialTokenPolicy.IGNORE
)
if not text:
return
last_pos = self._positions_decoded - self._prefix_len
if text.lstrip() != text or not self._full_text:
if self._current_word_pos is not None:
self._word_audio_ends.append(last_pos)
self._word_audio_starts.append(last_pos)
self._current_word_pos = last_pos
elif self._current_word_pos is None:
self._word_audio_starts.append(last_pos)
self._current_word_pos = last_pos
self._full_text += text
self._n_text_tokens += 1
def _close_current_word(self):
"""Close the last word if one is being built."""
if self._current_word_pos is not None:
last_pos = self._positions_decoded - self._prefix_len
self._word_audio_ends.append(last_pos)
self._current_word_pos = None
def _flush_and_reset(self) -> List[ASRToken]:
"""Flush pending audio, decode remaining, extract all words, then
fully reset both encoder and decoder state.
Used at silence boundaries and when the continuous decoding cap is
hit. A full reset (encoder + decoder) is necessary because the
encoder's incremental state (conv tails, KV caches) contains history
that would produce embeddings incompatible with a freshly-initialised
decoder. After reset ``_first_chunk=True``, so the next audio chunk
receives proper left-padding and both encoder and decoder start in
sync.
"""
# Align pending audio to SAMPLES_PER_TOKEN boundary
remainder = self._pending_len % SAMPLES_PER_TOKEN
align_pad = (SAMPLES_PER_TOKEN - remainder) if remainder > 0 else 0
# Add alignment + right-padding silence to provide future context
total_pad = align_pad + RIGHT_PAD_TOKENS * SAMPLES_PER_TOKEN
if total_pad > 0:
self._pending_chunks.append(np.zeros(total_pad, dtype=np.float32))
self._pending_len += total_pad
# Encode remaining audio (including right-padding)
self._encode_pending()
# Decode only positions backed by real audio
self._safe_decode_remaining()
self._flush_last_token_text()
self._close_current_word()
words = self._flush_all_words()
# Compute time offset: the decoded audio covers up to this point
new_offset = self._time_offset + self._real_samples_encoded / self.SAMPLING_RATE
saved_end = self.end
# Full reset — encoder AND decoder. The encoder's incremental
# state (conv tails, transformer KV caches) carries history from
# the previous segment; keeping it would make the next set of
# embeddings incompatible with a fresh decoder prefill.
self._reset_state()
self._time_offset = new_offset
self.end = saved_end
# Free MLX caches eagerly
mx.clear_cache()
return words
def start_silence(self) -> Tuple[List[ASRToken], float]:
"""Flush all pending words when silence starts, then fully reset.
Adds right-padding silence and forces a decode pass so the
decoder emits tokens for the last words of speech. After flushing,
resets both encoder and decoder state to prevent hallucination from
accumulated autoregressive context drift on long audio.
"""
words = self._flush_and_reset()
logger.info("[voxtral-mlx] start_silence: flushed %d words", len(words))
return words, self.end
def end_silence(self, silence_duration: float, offset: float):
self._time_offset += silence_duration
self.end += silence_duration
def new_speaker(self, change_speaker):
self.start_silence()
def warmup(self, audio, init_prompt=""):
pass
def finish(self) -> Tuple[List[ASRToken], float]:
logger.debug(
"[voxtral-mlx] finish: pending=%d samples, audio_embeds=%s, "
"samples_encoded=%d, positions_decoded=%d, prefilled=%s, text so far='%s'",
self._pending_len,
self._audio_embeds.shape if self._audio_embeds is not None else None,
self._samples_encoded,
self._positions_decoded,
self._prefilled,
self._full_text[-80:] if self._full_text else "",
)
# Align pending audio to SAMPLES_PER_TOKEN boundary so nothing is lost
remainder = self._pending_len % SAMPLES_PER_TOKEN
align_pad = (SAMPLES_PER_TOKEN - remainder) if remainder > 0 else 0
# Add alignment + right-padding silence
total_pad = align_pad + RIGHT_PAD_TOKENS * SAMPLES_PER_TOKEN
if total_pad > 0:
self._pending_chunks.append(np.zeros(total_pad, dtype=np.float32))
self._pending_len += total_pad
# Encode remaining audio (including right-padding)
self._encode_pending()
# Decode only positions backed by real audio
self._safe_decode_remaining()
self._flush_last_token_text()
self._close_current_word()
words = self._flush_all_words()
logger.info("[voxtral-mlx] finish: flushed %d words", len(words))
return words, self.end
================================================
FILE: whisperlivekit/warmup.py
================================================
import logging
logger = logging.getLogger(__name__)
def load_file(warmup_file=None, timeout=5):
import os
import tempfile
import urllib.request
import librosa
if warmup_file == "":
logger.info("Skipping warmup.")
return None
# Download JFK sample if not already present
if warmup_file is None:
jfk_url = "https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav"
temp_dir = tempfile.gettempdir()
warmup_file = os.path.join(temp_dir, "whisper_warmup_jfk.wav")
if not os.path.exists(warmup_file) or os.path.getsize(warmup_file) == 0:
try:
logger.debug(f"Downloading warmup file from {jfk_url}")
with urllib.request.urlopen(jfk_url, timeout=timeout) as r, open(warmup_file, "wb") as f:
f.write(r.read())
except Exception as e:
logger.warning(f"Warmup file download failed: {e}.")
return None
# Validate file and load
if not os.path.exists(warmup_file) or os.path.getsize(warmup_file) == 0:
logger.warning(f"Warmup file {warmup_file} is invalid or missing.")
return None
try:
audio, _ = librosa.load(warmup_file, sr=16000)
return audio
except Exception as e:
logger.warning(f"Failed to load warmup file: {e}")
return None
def warmup_asr(asr, warmup_file=None, timeout=5):
"""
Warmup the ASR model by transcribing a short audio file.
"""
audio = load_file(warmup_file=warmup_file, timeout=timeout)
if audio is None:
logger.warning("Warmup file unavailable. Skipping ASR warmup.")
return
try:
asr.transcribe(audio)
except Exception as e:
logger.warning("Warmup transcription failed: %s", e)
return
logger.info("ASR model is warmed up.")
================================================
FILE: whisperlivekit/web/__init__.py
================================================
================================================
FILE: whisperlivekit/web/live_transcription.css
================================================
:root {
--bg: #ffffff;
--text: #111111;
--muted: #666666;
--border: #e5e5e5;
--chip-bg: rgba(0, 0, 0, 0.04);
--chip-text: #000000;
--spinner-border: #8d8d8d5c;
--spinner-top: #b0b0b0;
--silence-bg: #f3f3f3;
--loading-bg: rgba(255, 77, 77, 0.06);
--button-bg: #ffffff;
--button-border: #e9e9e9;
--wave-stroke: #000000;
--label-dia-text: #868686;
--label-trans-text: #111111;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0b0b0b;
--text: #e6e6e6;
--muted: #9aa0a6;
--border: #333333;
--chip-bg: rgba(255, 255, 255, 0.08);
--chip-text: #e6e6e6;
--spinner-border: #555555;
--spinner-top: #dddddd;
--silence-bg: #1a1a1a;
--loading-bg: rgba(255, 77, 77, 0.12);
--button-bg: #111111;
--button-border: #333333;
--wave-stroke: #e6e6e6;
--label-dia-text: #b3b3b3;
--label-trans-text: #ffffff;
}
}
:root[data-theme="dark"] {
--bg: #0b0b0b;
--text: #e6e6e6;
--muted: #9aa0a6;
--border: #333333;
--chip-bg: rgba(255, 255, 255, 0.08);
--chip-text: #e6e6e6;
--spinner-border: #555555;
--spinner-top: #dddddd;
--silence-bg: #1a1a1a;
--loading-bg: rgba(255, 77, 77, 0.12);
--button-bg: #111111;
--button-border: #333333;
--wave-stroke: #e6e6e6;
--label-dia-text: #b3b3b3;
--label-trans-text: #ffffff;
}
:root[data-theme="light"] {
--bg: #ffffff;
--text: #111111;
--muted: #666666;
--border: #e5e5e5;
--chip-bg: rgba(0, 0, 0, 0.04);
--chip-text: #000000;
--spinner-border: #8d8d8d5c;
--spinner-top: #b0b0b0;
--silence-bg: #f3f3f3;
--loading-bg: rgba(255, 77, 77, 0.06);
--button-bg: #ffffff;
--button-border: #e9e9e9;
--wave-stroke: #000000;
--label-dia-text: #868686;
--label-trans-text: #111111;
}
html.is-extension
{
width: 350px;
height: 500px;
}
body {
font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
margin: 0;
text-align: center;
background-color: var(--bg);
color: var(--text);
height: 100vh;
display: flex;
flex-direction: column;
}
/* Record button */
#recordButton {
width: 50px;
height: 50px;
border: none;
border-radius: 50%;
background-color: var(--button-bg);
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid var(--button-border);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
#recordButton.recording {
width: 180px;
border-radius: 40px;
justify-content: flex-start;
padding-left: 20px;
}
#recordButton:active {
transform: scale(0.95);
}
.shape-container {
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.shape {
width: 25px;
height: 25px;
background-color: rgb(209, 61, 53);
border-radius: 50%;
transition: all 0.3s ease;
}
#recordButton:disabled .shape {
background-color: #6e6d6d;
}
#recordButton.recording .shape {
border-radius: 5px;
width: 25px;
height: 25px;
}
/* Recording elements */
.recording-info {
display: none;
align-items: center;
margin-left: 15px;
flex-grow: 1;
}
#recordButton.recording .recording-info {
display: flex;
}
.wave-container {
width: 60px;
height: 30px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
#waveCanvas {
width: 100%;
height: 100%;
}
.timer {
font-size: 14px;
font-weight: 500;
color: var(--text);
margin-left: 10px;
}
#status {
margin-top: 15px;
font-size: 16px;
color: var(--text);
margin-bottom: 0;
}
.header-container {
position: sticky;
top: 0;
background-color: var(--bg);
z-index: 100;
padding: 20px;
}
/* Settings */
.settings-container {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
position: relative;
flex-wrap: wrap;
}
.buttons-container {
display: flex;
align-items: center;
gap: 15px;
}
.settings {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 12px;
}
.settings-toggle {
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background-color: var(--button-bg);
border: 1px solid var(--button-border);
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.settings-toggle:hover {
background-color: var(--chip-bg);
}
.settings-toggle.active {
background-color: var(--chip-bg);
}
.settings-toggle img {
width: 20px;
height: 20px;
}
@media (max-width: 10000px) {
.settings-toggle {
display: flex;
}
.settings {
display: none;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 18px;
padding: 12px;
}
.settings.visible {
display: flex;
}
}
@media (max-width: 600px) {
.settings-container {
flex-direction: column;
align-items: center;
gap: 10px;
}
.buttons-container {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
}
}
.field {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 3px;
}
#chunkSelector,
#websocketInput,
#themeSelector,
#microphoneSelect {
font-size: 16px;
padding: 5px 8px;
border-radius: 8px;
border: 1px solid var(--border);
background-color: var(--button-bg);
color: var(--text);
max-height: 30px;
}
#microphoneSelect {
width: 100%;
max-width: 190px;
min-width: 120px;
}
#chunkSelector:focus,
#websocketInput:focus,
#themeSelector:focus,
#microphoneSelect:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.15);
}
label {
font-size: 13px;
color: var(--muted);
}
.ws-default {
font-size: 12px;
color: var(--muted);
}
/* Segmented pill control for Theme */
.segmented {
display: inline-flex;
align-items: stretch;
border: 1px solid var(--button-border);
background-color: var(--button-bg);
border-radius: 999px;
overflow: hidden;
}
.segmented input[type="radio"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.theme-selector-container {
display: flex;
align-items: center;
margin-top: 17px;
}
.segmented label {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 14px;
color: var(--muted);
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease, color 0.2s ease;
}
.segmented label span {
display: none;
}
.segmented label:hover span {
display: inline;
}
.segmented label:hover {
background-color: var(--chip-bg);
}
.segmented img {
width: 16px;
height: 16px;
}
.segmented input[type="radio"]:checked + label {
background-color: var(--chip-bg);
color: var(--text);
}
.segmented input[type="radio"]:focus-visible + label,
.segmented input[type="radio"]:focus + label {
outline: 2px solid #007bff;
outline-offset: 2px;
border-radius: 999px;
}
.transcript-container {
flex: 1;
overflow-y: auto;
padding: 20px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.transcript-container::-webkit-scrollbar {
display: none;
}
/* Transcript area */
#linesTranscript {
margin: 0 auto;
max-width: 700px;
text-align: left;
font-size: 16px;
}
#linesTranscript p {
margin: 0px 0;
}
#linesTranscript strong {
color: var(--text);
}
#speaker {
border: 1px solid var(--border);
border-radius: 100px;
padding: 2px 10px;
font-size: 14px;
margin-bottom: 0px;
}
.label_diarization {
background-color: var(--chip-bg);
border-radius: 100px;
padding: 2px 10px;
margin-left: 10px;
display: inline-block;
white-space: nowrap;
font-size: 14px;
margin-bottom: 0px;
color: var(--label-dia-text);
}
.label_transcription {
background-color: var(--chip-bg);
border-radius: 100px;
padding: 2px 10px;
display: inline-block;
white-space: nowrap;
margin-left: 10px;
font-size: 14px;
margin-bottom: 0px;
color: var(--label-trans-text);
}
.label_translation {
background-color: var(--chip-bg);
display: inline-flex;
border-radius: 10px;
padding: 4px 8px;
margin-top: 4px;
font-size: 14px;
color: var(--text);
align-items: flex-start;
gap: 4px;
}
.lag-diarization-value {
margin-left: 10px;
}
.label_translation img {
margin-top: 2px;
}
.label_translation img {
width: 12px;
height: 12px;
}
#timeInfo {
color: var(--muted);
margin-left: 0px;
}
.textcontent {
font-size: 16px;
padding-left: 10px;
margin-bottom: 10px;
margin-top: 1px;
padding-top: 5px;
border-radius: 0px 0px 0px 10px;
}
.buffer_diarization {
color: var(--label-dia-text);
}
.buffer_transcription {
color: #7474748c;
margin-left: 4px;
}
.buffer_translation {
color: #a0a0a0;
margin-left: 6px;
}
.spinner {
display: inline-block;
width: 8px;
height: 8px;
border: 2px solid var(--spinner-border);
border-top: 2px solid var(--spinner-top);
border-radius: 50%;
animation: spin 0.7s linear infinite;
vertical-align: middle;
margin-bottom: 2px;
margin-right: 5px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.silence {
color: var(--muted);
background-color: var(--silence-bg);
font-size: 13px;
border-radius: 30px;
padding: 2px 10px;
}
.loading {
color: var(--muted);
background-color: var(--loading-bg);
border-radius: 8px 8px 8px 0px;
padding: 2px 10px;
font-size: 14px;
margin-bottom: 0px;
}
/* for smaller screens */
@media (max-width: 200px) {
.header-container {
padding: 15px;
}
.settings-container {
flex-direction: column;
gap: 10px;
}
.buttons-container {
gap: 10px;
}
.settings {
justify-content: center;
gap: 8px;
}
.field {
align-items: center;
}
#websocketInput,
#microphoneSelect {
min-width: 100px;
max-width: 160px;
}
.theme-selector-container {
margin-top: 10px;
}
.transcript-container {
padding: 15px;
}
}
@media (max-width: 480px) {
.header-container {
padding: 10px;
}
.settings {
flex-direction: column;
align-items: center;
gap: 6px;
}
#websocketInput,
#microphoneSelect {
max-width: 140px;
}
.segmented label {
padding: 4px 8px;
font-size: 12px;
}
.segmented img {
width: 14px;
height: 14px;
}
.transcript-container {
padding: 10px;
}
}
.label_language {
background-color: var(--chip-bg);
margin-bottom: 0px;
border-radius: 100px;
padding: 2px 8px;
margin-left: 10px;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: var(--muted);
}
.speaker-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: -5px;
border-radius: 50%;
font-size: 11px;
line-height: 1;
font-weight: 800;
color: var(--muted);
}
================================================
FILE: whisperlivekit/web/live_transcription.html
================================================
WhisperLiveKit
================================================
FILE: whisperlivekit/web/live_transcription.js
================================================
const isExtension = typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getURL;
if (isExtension) {
document.documentElement.classList.add('is-extension');
}
const isWebContext = !isExtension;
let isRecording = false;
let websocket = null;
let recorder = null;
let chunkDuration = 100;
let websocketUrl = "ws://localhost:8000/asr";
let userClosing = false;
let wakeLock = null;
let startTime = null;
let timerInterval = null;
let audioContext = null;
let analyser = null;
let microphone = null;
let workletNode = null;
let recorderWorker = null;
let waveCanvas = document.getElementById("waveCanvas");
let waveCtx = waveCanvas.getContext("2d");
let animationFrame = null;
let waitingForStop = false;
let lastReceivedData = null;
let lastSignature = null;
let availableMicrophones = [];
let selectedMicrophoneId = null;
let serverUseAudioWorklet = null;
let configReadyResolve;
const configReady = new Promise((r) => (configReadyResolve = r));
let outputAudioContext = null;
let audioSource = null;
waveCanvas.width = 60 * (window.devicePixelRatio || 1);
waveCanvas.height = 30 * (window.devicePixelRatio || 1);
waveCtx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1);
const statusText = document.getElementById("status");
const recordButton = document.getElementById("recordButton");
const chunkSelector = document.getElementById("chunkSelector");
const websocketInput = document.getElementById("websocketInput");
const websocketDefaultSpan = document.getElementById("wsDefaultUrl");
const linesTranscriptDiv = document.getElementById("linesTranscript");
const timerElement = document.querySelector(".timer");
const themeRadios = document.querySelectorAll('input[name="theme"]');
const microphoneSelect = document.getElementById("microphoneSelect");
const settingsToggle = document.getElementById("settingsToggle");
const settingsDiv = document.querySelector(".settings");
// if (isExtension) {
// chrome.runtime.onInstalled.addListener((details) => {
// if (details.reason.search(/install/g) === -1) {
// return;
// }
// chrome.tabs.create({
// url: chrome.runtime.getURL("welcome.html"),
// active: true
// });
// });
// }
const translationIcon = ``
const silenceIcon = ``;
const languageIcon = ``
const speakerIcon = ``;
function getWaveStroke() {
const styles = getComputedStyle(document.documentElement);
const v = styles.getPropertyValue("--wave-stroke").trim();
return v || "#000";
}
let waveStroke = getWaveStroke();
function updateWaveStroke() {
waveStroke = getWaveStroke();
}
function applyTheme(pref) {
if (pref === "light") {
document.documentElement.setAttribute("data-theme", "light");
} else if (pref === "dark") {
document.documentElement.setAttribute("data-theme", "dark");
} else {
document.documentElement.removeAttribute("data-theme");
}
updateWaveStroke();
}
// Persisted theme preference
const savedThemePref = localStorage.getItem("themePreference") || "system";
applyTheme(savedThemePref);
if (themeRadios.length) {
themeRadios.forEach((r) => {
r.checked = r.value === savedThemePref;
r.addEventListener("change", () => {
if (r.checked) {
localStorage.setItem("themePreference", r.value);
applyTheme(r.value);
}
});
});
}
// React to OS theme changes when in "system" mode
const darkMq = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)");
const handleOsThemeChange = () => {
const pref = localStorage.getItem("themePreference") || "system";
if (pref === "system") updateWaveStroke();
};
if (darkMq && darkMq.addEventListener) {
darkMq.addEventListener("change", handleOsThemeChange);
} else if (darkMq && darkMq.addListener) {
// deprecated, but included for Safari compatibility
darkMq.addListener(handleOsThemeChange);
}
async function enumerateMicrophones() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(track => track.stop());
const devices = await navigator.mediaDevices.enumerateDevices();
availableMicrophones = devices.filter(device => device.kind === 'audioinput');
populateMicrophoneSelect();
console.log(`Found ${availableMicrophones.length} microphone(s)`);
} catch (error) {
console.error('Error enumerating microphones:', error);
statusText.textContent = "Error accessing microphones. Please grant permission.";
}
}
function populateMicrophoneSelect() {
if (!microphoneSelect) return;
microphoneSelect.innerHTML = '';
availableMicrophones.forEach((device, index) => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || `Microphone ${index + 1}`;
microphoneSelect.appendChild(option);
});
const savedMicId = localStorage.getItem('selectedMicrophone');
if (savedMicId && availableMicrophones.some(mic => mic.deviceId === savedMicId)) {
microphoneSelect.value = savedMicId;
selectedMicrophoneId = savedMicId;
}
}
function handleMicrophoneChange() {
selectedMicrophoneId = microphoneSelect.value || null;
localStorage.setItem('selectedMicrophone', selectedMicrophoneId || '');
const selectedDevice = availableMicrophones.find(mic => mic.deviceId === selectedMicrophoneId);
const deviceName = selectedDevice ? selectedDevice.label : 'Default Microphone';
console.log(`Selected microphone: ${deviceName}`);
statusText.textContent = `Microphone changed to: ${deviceName}`;
if (isRecording) {
statusText.textContent = "Switching microphone... Please wait.";
stopRecording().then(() => {
setTimeout(() => {
toggleRecording();
}, 1000);
});
}
}
// Helpers
function fmt1(x) {
const n = Number(x);
return Number.isFinite(n) ? n.toFixed(1) : x;
}
let host, port, protocol;
port = 8000;
if (isExtension) {
host = "localhost";
protocol = "ws";
} else {
host = window.location.hostname || "localhost";
port = window.location.port;
protocol = window.location.protocol === "https:" ? "wss" : "ws";
}
const defaultWebSocketUrl = `${protocol}://${host}${port ? ":" + port : ""}/asr`;
// Populate default caption and input
if (websocketDefaultSpan) websocketDefaultSpan.textContent = defaultWebSocketUrl;
websocketInput.value = defaultWebSocketUrl;
websocketUrl = defaultWebSocketUrl;
// Optional chunk selector (guard for presence)
if (chunkSelector) {
chunkSelector.addEventListener("change", () => {
chunkDuration = parseInt(chunkSelector.value);
});
}
// WebSocket input change handling
websocketInput.addEventListener("change", () => {
const urlValue = websocketInput.value.trim();
if (!urlValue.startsWith("ws://") && !urlValue.startsWith("wss://")) {
statusText.textContent = "Invalid WebSocket URL (must start with ws:// or wss://)";
return;
}
websocketUrl = urlValue;
statusText.textContent = "WebSocket URL updated. Ready to connect.";
});
function setupWebSocket() {
return new Promise((resolve, reject) => {
try {
websocket = new WebSocket(websocketUrl);
} catch (error) {
statusText.textContent = "Invalid WebSocket URL. Please check and try again.";
reject(error);
return;
}
websocket.onopen = () => {
statusText.textContent = "Connected to server.";
resolve();
};
websocket.onclose = () => {
if (userClosing) {
if (waitingForStop) {
statusText.textContent = "Processing finalized or connection closed.";
if (lastReceivedData) {
renderLinesWithBuffer(
lastReceivedData.lines || [],
lastReceivedData.buffer_diarization || "",
lastReceivedData.buffer_transcription || "",
lastReceivedData.buffer_translation || "",
0,
0,
true
);
}
}
} else {
statusText.textContent = "Disconnected from the WebSocket server. (Check logs if model is loading.)";
if (isRecording) {
stopRecording();
}
}
isRecording = false;
waitingForStop = false;
userClosing = false;
lastReceivedData = null;
websocket = null;
updateUI();
};
websocket.onerror = () => {
statusText.textContent = "Error connecting to WebSocket.";
reject(new Error("Error connecting to WebSocket"));
};
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "config") {
serverUseAudioWorklet = !!data.useAudioWorklet;
statusText.textContent = serverUseAudioWorklet
? "Connected. Using AudioWorklet (PCM)."
: "Connected. Using MediaRecorder (WebM).";
if (configReadyResolve) configReadyResolve();
return;
}
// Ignore diff/snapshot messages — the default frontend uses full-state mode.
// These are only sent when a client explicitly opts in via ?mode=diff.
if (data.type === "diff" || data.type === "snapshot") {
console.warn("Received diff-protocol message but frontend is in full mode; ignoring.", data.type);
return;
}
if (data.type === "ready_to_stop") {
console.log("Ready to stop received, finalizing display and closing WebSocket.");
waitingForStop = false;
if (lastReceivedData) {
renderLinesWithBuffer(
lastReceivedData.lines || [],
lastReceivedData.buffer_diarization || "",
lastReceivedData.buffer_transcription || "",
lastReceivedData.buffer_translation || "",
0,
0,
true
);
}
statusText.textContent = "Finished processing audio! Ready to record again.";
recordButton.disabled = false;
if (websocket) {
websocket.close();
}
return;
}
lastReceivedData = data;
const {
lines = [],
buffer_transcription = "",
buffer_diarization = "",
buffer_translation = "",
remaining_time_transcription = 0,
remaining_time_diarization = 0,
status = "active_transcription",
} = data;
renderLinesWithBuffer(
lines,
buffer_diarization,
buffer_transcription,
buffer_translation,
remaining_time_diarization,
remaining_time_transcription,
false,
status
);
};
});
}
function renderLinesWithBuffer(
lines,
buffer_diarization,
buffer_transcription,
buffer_translation,
remaining_time_diarization,
remaining_time_transcription,
isFinalizing = false,
current_status = "active_transcription"
) {
if (current_status === "no_audio_detected") {
linesTranscriptDiv.innerHTML =
"
No audio detected...
";
return;
}
const showLoading = !isFinalizing && (lines || []).some((it) => it.speaker == 0);
const showTransLag = !isFinalizing && remaining_time_transcription > 0;
const showDiaLag = !isFinalizing && !!buffer_diarization && remaining_time_diarization > 0;
const signature = JSON.stringify({
lines: (lines || []).map((it) => ({ speaker: it.speaker, text: it.text, start: it.start, end: it.end, detected_language: it.detected_language })),
buffer_transcription: buffer_transcription || "",
buffer_diarization: buffer_diarization || "",
buffer_translation: buffer_translation,
status: current_status,
showLoading,
showTransLag,
showDiaLag,
isFinalizing: !!isFinalizing,
});
if (lastSignature === signature) {
const t = document.querySelector(".lag-transcription-value");
if (t) t.textContent = fmt1(remaining_time_transcription);
const d = document.querySelector(".lag-diarization-value");
if (d) d.textContent = fmt1(remaining_time_diarization);
const ld = document.querySelector(".loading-diarization-value");
if (ld) ld.textContent = fmt1(remaining_time_diarization);
return;
}
lastSignature = signature;
// When there are no committed lines yet but buffer text exists (common with
// slow backends like voxtral on MPS), render the buffer as a standalone line.
const effectiveLines = (lines || []).length === 0 && (buffer_transcription || buffer_diarization)
? [{ speaker: 1, text: "" }]
: (lines || []);
const linesHtml = effectiveLines
.map((item, idx) => {
let timeInfo = "";
if (item.start !== undefined && item.end !== undefined) {
timeInfo = ` ${item.start} - ${item.end}`;
}
let speakerLabel = "";
if (item.speaker === -2) {
speakerLabel = `${silenceIcon}${timeInfo}`;
} else if (item.speaker == 0 && !isFinalizing) {
speakerLabel = `${fmt1(
remaining_time_diarization
)} second(s) of audio are undergoing diarization`;
} else if (item.speaker !== 0) {
const speakerNum = `${item.speaker}`;
speakerLabel = `${speakerIcon}${speakerNum}${timeInfo}`;
if (item.detected_language) {
speakerLabel += `${languageIcon}${item.detected_language}`;
}
}
let currentLineText = item.text || "";
if (idx === effectiveLines.length - 1) {
if (!isFinalizing && item.speaker !== -2) {
speakerLabel += `Transcription lag ${fmt1(
remaining_time_transcription
)}s`;
if (buffer_diarization && remaining_time_diarization) {
speakerLabel += `Diarization lag${fmt1(
remaining_time_diarization
)}s`;
}
}
if (buffer_diarization) {
if (isFinalizing) {
currentLineText +=
(currentLineText.length > 0 && buffer_diarization.trim().length > 0 ? " " : "") + buffer_diarization.trim();
} else {
currentLineText += `${buffer_diarization}`;
}
}
if (buffer_transcription) {
if (isFinalizing) {
currentLineText +=
(currentLineText.length > 0 && buffer_transcription.trim().length > 0 ? " " : "") +
buffer_transcription.trim();
} else {
currentLineText += `${buffer_transcription}`;
}
}
}
let translationContent = "";
if (item.translation) {
translationContent += item.translation.trim();
}
if (idx === effectiveLines.length - 1 && buffer_translation) {
const bufferPiece = isFinalizing
? buffer_translation
: `${buffer_translation}`;
translationContent += translationContent ? `${bufferPiece}` : bufferPiece;
}
if (translationContent.trim().length > 0) {
currentLineText += `