[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - v*\n  pull_request:\n    branches:\n      - master\n\njobs:\n  check-code-format:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Python 3.9\n        uses: actions/setup-python@v5\n        with:\n          python-version: 3.9\n\n      - name: Install module\n        run: |\n          pip install wheel\n          pip install -e .[dev]\n\n      - name: Check code format with Black\n        run: |\n          black --check .\n\n      - name: Check imports order with isort\n        run: |\n          isort --check-only .\n\n      - name: Check code style with Flake8\n        if: ${{ always() }}\n        run: |\n          flake8 .\n\n\n  run-tests:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Python 3.9\n        uses: actions/setup-python@v5\n        with:\n          python-version: 3.9\n\n      - name: Install module\n        run: |\n          pip install wheel\n          pip install -e .[dev]\n\n      - name: Run pytest\n        run: |\n          pytest -v tests/\n\n\n  build-and-push-package:\n    runs-on: ubuntu-latest\n    needs: [check-code-format, run-tests]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Python 3.9\n        uses: actions/setup-python@v5\n        with:\n          python-version: 3.9\n\n      - name: Install dependencies\n        run: |\n          pip install wheel\n\n      - name: Build package\n        run: |\n          python3 setup.py sdist bdist_wheel\n\n      - name: Push package on PyPI\n        if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          user: __token__\n          password: ${{ secrets.PYPI_API_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / Optimized / DLL Files\n*.pyc\n*.pyo\n*.pyd\n__pycache__/\n\n# Distribution / Packaging\nvenv/\n\n# Unit Test\n.pytest_cache/\n\n# Ignore IDE, Editor Files\n.idea/\n.vscode/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to faster-whisper\n\nContributions are welcome! Here are some pointers to help you install the library for development and validate your changes before submitting a pull request.\n\n## Install the library for development\n\nWe recommend installing the module in editable mode with the `dev` extra requirements:\n\n```bash\ngit clone https://github.com/SYSTRAN/faster-whisper.git\ncd faster-whisper/\npip install -e .[dev]\n```\n\n## Validate the changes before creating a pull request\n\n1. Make sure the existing tests are still passing (and consider adding new tests as well!):\n\n```bash\npytest tests/\n```\n\n2. Reformat and validate the code with the following tools:\n\n```bash\nblack .\nisort .\nflake8 .\n```\n\nThese steps are also run automatically in the CI when you open the pull request.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 SYSTRAN\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include faster_whisper/assets/silero_vad_v6.onnx\ninclude requirements.txt\ninclude requirements.conversion.txt\n"
  },
  {
    "path": "README.md",
    "content": "[![CI](https://github.com/SYSTRAN/faster-whisper/workflows/CI/badge.svg)](https://github.com/SYSTRAN/faster-whisper/actions?query=workflow%3ACI) [![PyPI version](https://badge.fury.io/py/faster-whisper.svg)](https://badge.fury.io/py/faster-whisper)\n\n# Faster Whisper transcription with CTranslate2\n\n**faster-whisper** is a reimplementation of OpenAI's Whisper model using [CTranslate2](https://github.com/OpenNMT/CTranslate2/), which is a fast inference engine for Transformer models.\n\nThis implementation is up to 4 times faster than [openai/whisper](https://github.com/openai/whisper) for the same accuracy while using less memory. The efficiency can be further improved with 8-bit quantization on both CPU and GPU.\n\n## Benchmark\n\n### Whisper\n\nFor reference, here's the time and memory usage that are required to transcribe [**13 minutes**](https://www.youtube.com/watch?v=0u7tTptBo9I) of audio using different implementations:\n\n* [openai/whisper](https://github.com/openai/whisper)@[v20240930](https://github.com/openai/whisper/tree/v20240930)\n* [whisper.cpp](https://github.com/ggerganov/whisper.cpp)@[v1.7.2](https://github.com/ggerganov/whisper.cpp/tree/v1.7.2)\n* [transformers](https://github.com/huggingface/transformers)@[v4.46.3](https://github.com/huggingface/transformers/tree/v4.46.3)\n* [faster-whisper](https://github.com/SYSTRAN/faster-whisper)@[v1.1.0](https://github.com/SYSTRAN/faster-whisper/tree/v1.1.0)\n\n### Large-v2 model on GPU\n\n| Implementation | Precision | Beam size | Time | VRAM Usage |\n| --- | --- | --- | --- | --- |\n| openai/whisper | fp16 | 5 | 2m23s | 4708MB |\n| whisper.cpp (Flash Attention) | fp16 | 5 | 1m05s | 4127MB |\n| transformers (SDPA)[^1] | fp16 | 5 | 1m52s | 4960MB |\n| faster-whisper | fp16 | 5 | 1m03s | 4525MB |\n| faster-whisper (`batch_size=8`) | fp16 | 5 | 17s | 6090MB |\n| faster-whisper | int8 | 5 | 59s | 2926MB |\n| faster-whisper (`batch_size=8`) | int8 | 5 | 16s | 4500MB |\n\n### distil-whisper-large-v3 model on GPU\n\n| Implementation | Precision | Beam size | Time | YT Commons WER |\n| --- | --- | --- | --- | --- |\n| transformers (SDPA) (`batch_size=16`) | fp16 | 5 | 46m12s | 14.801 |\n| faster-whisper (`batch_size=16`) | fp16 | 5 | 25m50s | 13.527 |\n\n*GPU Benchmarks are Executed with CUDA 12.4 on a NVIDIA RTX 3070 Ti 8GB.*\n[^1]: transformers OOM for any batch size > 1\n\n### Small model on CPU\n\n| Implementation | Precision | Beam size | Time | RAM Usage |\n| --- | --- | --- | --- | --- |\n| openai/whisper | fp32 | 5 | 6m58s | 2335MB |\n| whisper.cpp | fp32 | 5 | 2m05s | 1049MB |\n| whisper.cpp (OpenVINO) | fp32 | 5 | 1m45s | 1642MB |\n| faster-whisper | fp32 | 5 | 2m37s | 2257MB |\n| faster-whisper (`batch_size=8`) | fp32 | 5 | 1m06s | 4230MB |\n| faster-whisper | int8 | 5 | 1m42s | 1477MB |\n| faster-whisper (`batch_size=8`) | int8 | 5 | 51s | 3608MB |\n\n*Executed with 8 threads on an Intel Core i7-12700K.*\n\n\n## Requirements\n\n* Python 3.9 or greater\n\nUnlike openai-whisper, FFmpeg does **not** need to be installed on the system. The audio is decoded with the Python library [PyAV](https://github.com/PyAV-Org/PyAV) which bundles the FFmpeg libraries in its package.\n\n### GPU\n\nGPU execution requires the following NVIDIA libraries to be installed:\n\n* [cuBLAS for CUDA 12](https://developer.nvidia.com/cublas)\n* [cuDNN 9 for CUDA 12](https://developer.nvidia.com/cudnn)\n\n**Note**: The latest versions of `ctranslate2` only support CUDA 12 and cuDNN 9. For CUDA 11 and cuDNN 8, the current workaround is downgrading to the `3.24.0` version of `ctranslate2`, for CUDA 12 and cuDNN 8, downgrade to the `4.4.0` version of `ctranslate2`, (This can be done with `pip install --force-reinstall ctranslate2==4.4.0` or specifying the version in a `requirements.txt`).\n\nThere are multiple ways to install the NVIDIA libraries mentioned above. The recommended way is described in the official NVIDIA documentation, but we also suggest other installation methods below. \n\n<details>\n<summary>Other installation methods (click to expand)</summary>\n\n\n**Note:** For all these methods below, keep in mind the above note regarding CUDA versions. Depending on your setup, you may need to install the _CUDA 11_ versions of libraries that correspond to the CUDA 12 libraries listed in the instructions below.\n\n#### Use Docker\n\nThe libraries (cuBLAS, cuDNN) are installed in this official NVIDIA CUDA Docker images: `nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04`.\n\n#### Install with `pip` (Linux only)\n\nOn Linux these libraries can be installed with `pip`. Note that `LD_LIBRARY_PATH` must be set before launching Python.\n\n```bash\npip install nvidia-cublas-cu12 nvidia-cudnn-cu12==9.*\n\nexport LD_LIBRARY_PATH=`python3 -c 'import os; import nvidia.cublas.lib; import nvidia.cudnn.lib; print(os.path.dirname(nvidia.cublas.lib.__file__) + \":\" + os.path.dirname(nvidia.cudnn.lib.__file__))'`\n```\n\n#### Download the libraries from Purfview's repository (Windows & Linux)\n\nPurfview's [whisper-standalone-win](https://github.com/Purfview/whisper-standalone-win) provides the required NVIDIA libraries for Windows & Linux in a [single archive](https://github.com/Purfview/whisper-standalone-win/releases/tag/libs). Decompress the archive and place the libraries in a directory included in the `PATH`.\n\n</details>\n\n## Installation\n\nThe module can be installed from [PyPI](https://pypi.org/project/faster-whisper/):\n\n```bash\npip install faster-whisper\n```\n\n<details>\n<summary>Other installation methods (click to expand)</summary>\n\n### Install the master branch\n\n```bash\npip install --force-reinstall \"faster-whisper @ https://github.com/SYSTRAN/faster-whisper/archive/refs/heads/master.tar.gz\"\n```\n\n### Install a specific commit\n\n```bash\npip install --force-reinstall \"faster-whisper @ https://github.com/SYSTRAN/faster-whisper/archive/a4f1cc8f11433e454c3934442b5e1a4ed5e865c3.tar.gz\"\n```\n\n</details>\n\n## Usage\n\n### Faster-whisper\n\n```python\nfrom faster_whisper import WhisperModel\n\nmodel_size = \"large-v3\"\n\n# Run on GPU with FP16\nmodel = WhisperModel(model_size, device=\"cuda\", compute_type=\"float16\")\n\n# or run on GPU with INT8\n# model = WhisperModel(model_size, device=\"cuda\", compute_type=\"int8_float16\")\n# or run on CPU with INT8\n# model = WhisperModel(model_size, device=\"cpu\", compute_type=\"int8\")\n\nsegments, info = model.transcribe(\"audio.mp3\", beam_size=5)\n\nprint(\"Detected language '%s' with probability %f\" % (info.language, info.language_probability))\n\nfor segment in segments:\n    print(\"[%.2fs -> %.2fs] %s\" % (segment.start, segment.end, segment.text))\n```\n\n**Warning:** `segments` is a *generator* so the transcription only starts when you iterate over it. The transcription can be run to completion by gathering the segments in a list or a `for` loop:\n\n```python\nsegments, _ = model.transcribe(\"audio.mp3\")\nsegments = list(segments)  # The transcription will actually run here.\n```\n\n### Batched Transcription\nThe following code snippet illustrates how to run batched transcription on an example audio file. `BatchedInferencePipeline.transcribe` is a drop-in replacement for `WhisperModel.transcribe`\n\n```python\nfrom faster_whisper import WhisperModel, BatchedInferencePipeline\n\nmodel = WhisperModel(\"turbo\", device=\"cuda\", compute_type=\"float16\")\nbatched_model = BatchedInferencePipeline(model=model)\nsegments, info = batched_model.transcribe(\"audio.mp3\", batch_size=16)\n\nfor segment in segments:\n    print(\"[%.2fs -> %.2fs] %s\" % (segment.start, segment.end, segment.text))\n```\n\n### Faster Distil-Whisper\n\nThe Distil-Whisper checkpoints are compatible with the Faster-Whisper package. In particular, the latest [distil-large-v3](https://huggingface.co/distil-whisper/distil-large-v3)\ncheckpoint is intrinsically designed to work with the Faster-Whisper transcription algorithm. The following code snippet \ndemonstrates how to run inference with distil-large-v3 on a specified audio file:\n\n```python\nfrom faster_whisper import WhisperModel\n\nmodel_size = \"distil-large-v3\"\n\nmodel = WhisperModel(model_size, device=\"cuda\", compute_type=\"float16\")\nsegments, info = model.transcribe(\"audio.mp3\", beam_size=5, language=\"en\", condition_on_previous_text=False)\n\nfor segment in segments:\n    print(\"[%.2fs -> %.2fs] %s\" % (segment.start, segment.end, segment.text))\n```\n\nFor more information about the distil-large-v3 model, refer to the original [model card](https://huggingface.co/distil-whisper/distil-large-v3).\n\n### Word-level timestamps\n\n```python\nsegments, _ = model.transcribe(\"audio.mp3\", word_timestamps=True)\n\nfor segment in segments:\n    for word in segment.words:\n        print(\"[%.2fs -> %.2fs] %s\" % (word.start, word.end, word.word))\n```\n\n### VAD filter\n\nThe library integrates the [Silero VAD](https://github.com/snakers4/silero-vad) model to filter out parts of the audio without speech:\n\n```python\nsegments, _ = model.transcribe(\"audio.mp3\", vad_filter=True)\n```\n\nThe default behavior is conservative and only removes silence longer than 2 seconds. See the available VAD parameters and default values in the [source code](https://github.com/SYSTRAN/faster-whisper/blob/master/faster_whisper/vad.py). They can be customized with the dictionary argument `vad_parameters`:\n\n```python\nsegments, _ = model.transcribe(\n    \"audio.mp3\",\n    vad_filter=True,\n    vad_parameters=dict(min_silence_duration_ms=500),\n)\n```\nVad filter is enabled by default for batched transcription.\n\n### Logging\n\nThe library logging level can be configured like this:\n\n```python\nimport logging\n\nlogging.basicConfig()\nlogging.getLogger(\"faster_whisper\").setLevel(logging.DEBUG)\n```\n\n### Going further\n\nSee more model and transcription options in the [`WhisperModel`](https://github.com/SYSTRAN/faster-whisper/blob/master/faster_whisper/transcribe.py) class implementation.\n\n## Community integrations\n\nHere is a non exhaustive list of open-source projects using faster-whisper. Feel free to add your project to the list!\n\n\n* [speaches](https://github.com/speaches-ai/speaches) is an OpenAI compatible server using `faster-whisper`. It's easily deployable with Docker, works with OpenAI SDKs/CLI, supports streaming, and live transcription.\n* [WhisperX](https://github.com/m-bain/whisperX) is an award-winning Python library that offers speaker diarization and accurate word-level timestamps using wav2vec2 alignment\n* [whisper-ctranslate2](https://github.com/Softcatala/whisper-ctranslate2) is a command line client based on faster-whisper and compatible with the original client from openai/whisper.\n* [whisper-diarize](https://github.com/MahmoudAshraf97/whisper-diarization) is a speaker diarization tool that is based on faster-whisper and NVIDIA NeMo.\n* [whisper-standalone-win](https://github.com/Purfview/whisper-standalone-win) Standalone CLI executables of faster-whisper for Windows, Linux & macOS. \n* [asr-sd-pipeline](https://github.com/hedrergudene/asr-sd-pipeline) provides a scalable, modular, end to end multi-speaker speech to text solution implemented using AzureML pipelines.\n* [Open-Lyrics](https://github.com/zh-plus/Open-Lyrics) is a Python library that transcribes voice files using faster-whisper, and translates/polishes the resulting text into `.lrc` files in the desired language using OpenAI-GPT.\n* [wscribe](https://github.com/geekodour/wscribe) is a flexible transcript generation tool supporting faster-whisper, it can export word level transcript and the exported transcript then can be edited with [wscribe-editor](https://github.com/geekodour/wscribe-editor)\n* [aTrain](https://github.com/BANDAS-Center/aTrain) is a graphical user interface implementation of faster-whisper developed at the BANDAS-Center at the University of Graz for transcription and diarization in Windows ([Windows Store App](https://apps.microsoft.com/detail/atrain/9N15Q44SZNS2)) and Linux.\n* [Whisper-Streaming](https://github.com/ufal/whisper_streaming) implements real-time mode for offline Whisper-like speech-to-text models with faster-whisper as the most recommended back-end. It implements a streaming policy with self-adaptive latency based on the actual source complexity, and demonstrates the state of the art.\n* [WhisperLive](https://github.com/collabora/WhisperLive) is a nearly-live implementation of OpenAI's Whisper which uses faster-whisper as the backend to transcribe audio in real-time.\n* [Faster-Whisper-Transcriber](https://github.com/BBC-Esq/ctranslate2-faster-whisper-transcriber) is a simple but reliable voice transcriber that provides a user-friendly interface.\n* [Open-dubbing](https://github.com/softcatala/open-dubbing) is open dubbing is an AI dubbing system which uses machine learning models to automatically translate and synchronize audio dialogue into different languages.\n* [Whisper-FastAPI](https://github.com/heimoshuiyu/whisper-fastapi) whisper-fastapi is a very simple script that provides an API backend compatible with OpenAI, HomeAssistant, and Konele (Android voice typing) formats.\n\n## Model conversion\n\nWhen loading a model from its size such as `WhisperModel(\"large-v3\")`, the corresponding CTranslate2 model is automatically downloaded from the [Hugging Face Hub](https://huggingface.co/Systran).\n\nWe also provide a script to convert any Whisper models compatible with the Transformers library. They could be the original OpenAI models or user fine-tuned models.\n\nFor example the command below converts the [original \"large-v3\" Whisper model](https://huggingface.co/openai/whisper-large-v3) and saves the weights in FP16:\n\n```bash\npip install transformers[torch]>=4.23\n\nct2-transformers-converter --model openai/whisper-large-v3 --output_dir whisper-large-v3-ct2\n--copy_files tokenizer.json preprocessor_config.json --quantization float16\n```\n\n* The option `--model` accepts a model name on the Hub or a path to a model directory.\n* If the option `--copy_files tokenizer.json` is not used, the tokenizer configuration is automatically downloaded when the model is loaded later.\n\nModels can also be converted from the code. See the [conversion API](https://opennmt.net/CTranslate2/python/ctranslate2.converters.TransformersConverter.html).\n\n### Load a converted model\n\n1. Directly load the model from a local directory:\n```python\nmodel = faster_whisper.WhisperModel(\"whisper-large-v3-ct2\")\n```\n\n2. [Upload your model to the Hugging Face Hub](https://huggingface.co/docs/transformers/model_sharing#upload-with-the-web-interface) and load it from its name:\n```python\nmodel = faster_whisper.WhisperModel(\"username/whisper-large-v3-ct2\")\n```\n\n## Comparing performance against other implementations\n\nIf you are comparing the performance against other Whisper implementations, you should make sure to run the comparison with similar settings. In particular:\n\n* Verify that the same transcription options are used, especially the same beam size. For example in openai/whisper, `model.transcribe` uses a default beam size of 1 but here we use a default beam size of 5.\n* Transcription speed is closely affected by the number of words in the transcript, so ensure that other implementations have a similar WER (Word Error Rate) to this one.\n* When running on CPU, make sure to set the same number of threads. Many frameworks will read the environment variable `OMP_NUM_THREADS`, which can be set when running your script:\n\n```bash\nOMP_NUM_THREADS=4 python3 my_script.py\n```\n"
  },
  {
    "path": "benchmark/evaluate_yt_commons.py",
    "content": "import argparse\nimport json\nimport os\n\nfrom io import BytesIO\n\nfrom datasets import load_dataset\nfrom jiwer import wer\nfrom pytubefix import YouTube\nfrom pytubefix.exceptions import VideoUnavailable\nfrom tqdm import tqdm\nfrom transformers.models.whisper.english_normalizer import EnglishTextNormalizer\n\nfrom faster_whisper import BatchedInferencePipeline, WhisperModel, decode_audio\n\n\ndef url_to_audio(row):\n    buffer = BytesIO()\n    yt = YouTube(row[\"link\"])\n    try:\n        video = (\n            yt.streams.filter(only_audio=True, mime_type=\"audio/mp4\")\n            .order_by(\"bitrate\")\n            .desc()\n            .last()\n        )\n        video.stream_to_buffer(buffer)\n        buffer.seek(0)\n        row[\"audio\"] = decode_audio(buffer)\n    except VideoUnavailable:\n        print(f'Failed to download: {row[\"link\"]}')\n        row[\"audio\"] = []\n    return row\n\n\nparser = argparse.ArgumentParser(description=\"WER benchmark\")\nparser.add_argument(\n    \"--audio_numb\",\n    type=int,\n    default=None,\n    help=\"Specify the number of validation audio files in the dataset.\"\n    \" Set to None to retrieve all audio files.\",\n)\nargs = parser.parse_args()\n\nwith open(os.path.join(os.path.dirname(__file__), \"normalizer.json\"), \"r\") as f:\n    normalizer = EnglishTextNormalizer(json.load(f))\n\ndataset = load_dataset(\"mobiuslabsgmbh/youtube-commons-asr-eval\", streaming=True).map(\n    url_to_audio\n)\nmodel = WhisperModel(\"large-v3\", device=\"cuda\")\npipeline = BatchedInferencePipeline(model, device=\"cuda\")\n\n\nall_transcriptions = []\nall_references = []\n# iterate over the dataset and run inference\nfor i, row in tqdm(enumerate(dataset[\"test\"]), desc=\"Evaluating...\"):\n    if not row[\"audio\"]:\n        continue\n    result, info = pipeline.transcribe(\n        row[\"audio\"][0],\n        batch_size=8,\n        word_timestamps=False,\n        without_timestamps=True,\n    )\n\n    all_transcriptions.append(\"\".join(segment.text for segment in result))\n    all_references.append(row[\"text\"][0])\n    if args.audio_numb and i == (args.audio_numb - 1):\n        break\n\n# normalize predictions and references\nall_transcriptions = [normalizer(transcription) for transcription in all_transcriptions]\nall_references = [normalizer(reference) for reference in all_references]\n\n# compute the WER metric\nword_error_rate = 100 * wer(hypothesis=all_transcriptions, reference=all_references)\nprint(\"WER: %.3f\" % word_error_rate)\n"
  },
  {
    "path": "benchmark/memory_benchmark.py",
    "content": "import argparse\nimport time\n\nfrom typing import Callable\n\nimport py3nvml.py3nvml as nvml\n\nfrom memory_profiler import memory_usage\nfrom utils import MyThread, get_logger, inference\n\nlogger = get_logger(\"faster-whisper\")\nparser = argparse.ArgumentParser(description=\"Memory benchmark\")\nparser.add_argument(\n    \"--gpu_memory\", action=\"store_true\", help=\"Measure GPU memory usage\"\n)\nparser.add_argument(\"--device-index\", type=int, default=0, help=\"GPU device index\")\nparser.add_argument(\n    \"--interval\",\n    type=float,\n    default=0.5,\n    help=\"Interval at which measurements are collected\",\n)\nargs = parser.parse_args()\ndevice_idx = args.device_index\ninterval = args.interval\n\n\ndef measure_memory(func: Callable[[], None]):\n    if args.gpu_memory:\n        logger.info(\n            \"Measuring maximum GPU memory usage on GPU device.\"\n            \" Make sure to not have additional processes running on the same GPU.\"\n        )\n        # init nvml\n        nvml.nvmlInit()\n        handle = nvml.nvmlDeviceGetHandleByIndex(device_idx)\n        gpu_name = nvml.nvmlDeviceGetName(handle)\n        gpu_memory_limit = nvml.nvmlDeviceGetMemoryInfo(handle).total >> 20\n        gpu_power_limit = nvml.nvmlDeviceGetPowerManagementLimit(handle) / 1000.0\n        info = {\"gpu_memory_usage\": [], \"gpu_power_usage\": []}\n\n        def _get_gpu_info():\n            while True:\n                info[\"gpu_memory_usage\"].append(\n                    nvml.nvmlDeviceGetMemoryInfo(handle).used >> 20\n                )\n                info[\"gpu_power_usage\"].append(\n                    nvml.nvmlDeviceGetPowerUsage(handle) / 1000\n                )\n                time.sleep(interval)\n\n                if stop:\n                    break\n\n            return info\n\n        stop = False\n        thread = MyThread(_get_gpu_info, params=())\n        thread.start()\n        func()\n        stop = True\n        thread.join()\n        result = thread.get_result()\n\n        # shutdown nvml\n        nvml.nvmlShutdown()\n        max_memory_usage = max(result[\"gpu_memory_usage\"])\n        max_power_usage = max(result[\"gpu_power_usage\"])\n        print(\"GPU name: %s\" % gpu_name)\n        print(\"GPU device index: %s\" % device_idx)\n        print(\n            \"Maximum GPU memory usage: %dMiB / %dMiB (%.2f%%)\"\n            % (\n                max_memory_usage,\n                gpu_memory_limit,\n                (max_memory_usage / gpu_memory_limit) * 100,\n            )\n        )\n        print(\n            \"Maximum GPU power usage: %dW / %dW (%.2f%%)\"\n            % (\n                max_power_usage,\n                gpu_power_limit,\n                (max_power_usage / gpu_power_limit) * 100,\n            )\n        )\n    else:\n        logger.info(\"Measuring maximum increase of memory usage.\")\n        max_usage = memory_usage(func, max_usage=True, interval=interval)\n        print(\"Maximum increase of RAM memory usage: %d MiB\" % max_usage)\n\n\nif __name__ == \"__main__\":\n    measure_memory(inference)\n"
  },
  {
    "path": "benchmark/normalizer.json",
    "content": "{\n  \"accessorise\": \"accessorize\",\n  \"accessorised\": \"accessorized\",\n  \"accessorises\": \"accessorizes\",\n  \"accessorising\": \"accessorizing\",\n  \"acclimatisation\": \"acclimatization\",\n  \"acclimatise\": \"acclimatize\",\n  \"acclimatised\": \"acclimatized\",\n  \"acclimatises\": \"acclimatizes\",\n  \"acclimatising\": \"acclimatizing\",\n  \"accoutrements\": \"accouterments\",\n  \"aeon\": \"eon\",\n  \"aeons\": \"eons\",\n  \"aerogramme\": \"aerogram\",\n  \"aerogrammes\": \"aerograms\",\n  \"aeroplane\": \"airplane\",\n  \"aeroplanes\": \"airplanes\",\n  \"aesthete\": \"esthete\",\n  \"aesthetes\": \"esthetes\",\n  \"aesthetic\": \"esthetic\",\n  \"aesthetically\": \"esthetically\",\n  \"aesthetics\": \"esthetics\",\n  \"aetiology\": \"etiology\",\n  \"ageing\": \"aging\",\n  \"aggrandisement\": \"aggrandizement\",\n  \"agonise\": \"agonize\",\n  \"agonised\": \"agonized\",\n  \"agonises\": \"agonizes\",\n  \"agonising\": \"agonizing\",\n  \"agonisingly\": \"agonizingly\",\n  \"almanack\": \"almanac\",\n  \"almanacks\": \"almanacs\",\n  \"aluminium\": \"aluminum\",\n  \"amortisable\": \"amortizable\",\n  \"amortisation\": \"amortization\",\n  \"amortisations\": \"amortizations\",\n  \"amortise\": \"amortize\",\n  \"amortised\": \"amortized\",\n  \"amortises\": \"amortizes\",\n  \"amortising\": \"amortizing\",\n  \"amphitheatre\": \"amphitheater\",\n  \"amphitheatres\": \"amphitheaters\",\n  \"anaemia\": \"anemia\",\n  \"anaemic\": \"anemic\",\n  \"anaesthesia\": \"anesthesia\",\n  \"anaesthetic\": \"anesthetic\",\n  \"anaesthetics\": \"anesthetics\",\n  \"anaesthetise\": \"anesthetize\",\n  \"anaesthetised\": \"anesthetized\",\n  \"anaesthetises\": \"anesthetizes\",\n  \"anaesthetising\": \"anesthetizing\",\n  \"anaesthetist\": \"anesthetist\",\n  \"anaesthetists\": \"anesthetists\",\n  \"anaesthetize\": \"anesthetize\",\n  \"anaesthetized\": \"anesthetized\",\n  \"anaesthetizes\": \"anesthetizes\",\n  \"anaesthetizing\": \"anesthetizing\",\n  \"analogue\": \"analog\",\n  \"analogues\": \"analogs\",\n  \"analyse\": \"analyze\",\n  \"analysed\": \"analyzed\",\n  \"analyses\": \"analyzes\",\n  \"analysing\": \"analyzing\",\n  \"anglicise\": \"anglicize\",\n  \"anglicised\": \"anglicized\",\n  \"anglicises\": \"anglicizes\",\n  \"anglicising\": \"anglicizing\",\n  \"annualised\": \"annualized\",\n  \"antagonise\": \"antagonize\",\n  \"antagonised\": \"antagonized\",\n  \"antagonises\": \"antagonizes\",\n  \"antagonising\": \"antagonizing\",\n  \"apologise\": \"apologize\",\n  \"apologised\": \"apologized\",\n  \"apologises\": \"apologizes\",\n  \"apologising\": \"apologizing\",\n  \"appal\": \"appall\",\n  \"appals\": \"appalls\",\n  \"appetiser\": \"appetizer\",\n  \"appetisers\": \"appetizers\",\n  \"appetising\": \"appetizing\",\n  \"appetisingly\": \"appetizingly\",\n  \"arbour\": \"arbor\",\n  \"arbours\": \"arbors\",\n  \"archaeologically\": \"archeologically\",\n  \"archaeologist\": \"archeologist\",\n  \"archaeologists\": \"archeologists\",\n  \"archaeology\": \"archeology</span>\",\n  \"archeological\": \"archaeological\",\n  \"ardour\": \"ardor\",\n  \"armour\": \"armor\",\n  \"armoured\": \"armored\",\n  \"armourer\": \"armorer\",\n  \"armourers\": \"armorers\",\n  \"armouries\": \"armories\",\n  \"armoury\": \"armory\",\n  \"artefact\": \"artifact\",\n  \"artefacts\": \"artifacts\",\n  \"authorise\": \"authorize\",\n  \"authorised\": \"authorized\",\n  \"authorises\": \"authorizes\",\n  \"authorising\": \"authorizing\",\n  \"axe\": \"ax\",\n  \"backpedalled\": \"backpedaled\",\n  \"backpedalling\": \"backpedaling\",\n  \"bannister\": \"banister\",\n  \"bannisters\": \"banisters\",\n  \"baptise\": \"baptize\",\n  \"baptised\": \"baptized\",\n  \"baptises\": \"baptizes\",\n  \"baptising\": \"baptizing\",\n  \"bastardise\": \"bastardize\",\n  \"bastardised\": \"bastardized\",\n  \"bastardises\": \"bastardizes\",\n  \"bastardising\": \"bastardizing\",\n  \"battleax\": \"battleaxe\",\n  \"baulk\": \"balk\",\n  \"baulked\": \"balked\",\n  \"baulking\": \"balking\",\n  \"baulks\": \"balks\",\n  \"bedevilled\": \"bedeviled\",\n  \"bedevilling\": \"bedeviling\",\n  \"behaviour\": \"behavior\",\n  \"behavioural\": \"behavioral\",\n  \"behaviourism\": \"behaviorism\",\n  \"behaviourist\": \"behaviorist\",\n  \"behaviourists\": \"behaviorists\",\n  \"behaviours\": \"behaviors\",\n  \"behove\": \"behoove\",\n  \"behoved\": \"behooved\",\n  \"behoves\": \"behooves\",\n  \"bejewelled\": \"bejeweled\",\n  \"belabour\": \"belabor\",\n  \"belaboured\": \"belabored\",\n  \"belabouring\": \"belaboring\",\n  \"belabours\": \"belabors\",\n  \"bevelled\": \"beveled\",\n  \"bevvies\": \"bevies\",\n  \"bevvy\": \"bevy\",\n  \"biassed\": \"biased\",\n  \"biassing\": \"biasing\",\n  \"bingeing\": \"binging\",\n  \"bougainvillaea\": \"bougainvillea\",\n  \"bougainvillaeas\": \"bougainvilleas\",\n  \"bowdlerise\": \"bowdlerize\",\n  \"bowdlerised\": \"bowdlerized\",\n  \"bowdlerises\": \"bowdlerizes\",\n  \"bowdlerising\": \"bowdlerizing\",\n  \"breathalyse\": \"breathalyze\",\n  \"breathalysed\": \"breathalyzed\",\n  \"breathalyser\": \"breathalyzer\",\n  \"breathalysers\": \"breathalyzers\",\n  \"breathalyses\": \"breathalyzes\",\n  \"breathalysing\": \"breathalyzing\",\n  \"brutalise\": \"brutalize\",\n  \"brutalised\": \"brutalized\",\n  \"brutalises\": \"brutalizes\",\n  \"brutalising\": \"brutalizing\",\n  \"busses\": \"buses\",\n  \"bussing\": \"busing\",\n  \"caesarean\": \"cesarean\",\n  \"caesareans\": \"cesareans\",\n  \"calibre\": \"caliber\",\n  \"calibres\": \"calibers\",\n  \"calliper\": \"caliper\",\n  \"callipers\": \"calipers\",\n  \"callisthenics\": \"calisthenics\",\n  \"canalise\": \"canalize\",\n  \"canalised\": \"canalized\",\n  \"canalises\": \"canalizes\",\n  \"canalising\": \"canalizing\",\n  \"cancelation\": \"cancellation\",\n  \"cancelations\": \"cancellations\",\n  \"cancelled\": \"canceled\",\n  \"cancelling\": \"canceling\",\n  \"candour\": \"candor\",\n  \"cannibalise\": \"cannibalize\",\n  \"cannibalised\": \"cannibalized\",\n  \"cannibalises\": \"cannibalizes\",\n  \"cannibalising\": \"cannibalizing\",\n  \"canonise\": \"canonize\",\n  \"canonised\": \"canonized\",\n  \"canonises\": \"canonizes\",\n  \"canonising\": \"canonizing\",\n  \"capitalise\": \"capitalize\",\n  \"capitalised\": \"capitalized\",\n  \"capitalises\": \"capitalizes\",\n  \"capitalising\": \"capitalizing\",\n  \"caramelise\": \"caramelize\",\n  \"caramelised\": \"caramelized\",\n  \"caramelises\": \"caramelizes\",\n  \"caramelising\": \"caramelizing\",\n  \"carbonise\": \"carbonize\",\n  \"carbonised\": \"carbonized\",\n  \"carbonises\": \"carbonizes\",\n  \"carbonising\": \"carbonizing\",\n  \"carolled\": \"caroled\",\n  \"carolling\": \"caroling\",\n  \"catalogue\": \"catalog\",\n  \"catalogued\": \"cataloged\",\n  \"catalogues\": \"catalogs\",\n  \"cataloguing\": \"cataloging\",\n  \"catalyse\": \"catalyze\",\n  \"catalysed\": \"catalyzed\",\n  \"catalyses\": \"catalyzes\",\n  \"catalysing\": \"catalyzing\",\n  \"categorise\": \"categorize\",\n  \"categorised\": \"categorized\",\n  \"categorises\": \"categorizes\",\n  \"categorising\": \"categorizing\",\n  \"cauterise\": \"cauterize\",\n  \"cauterised\": \"cauterized\",\n  \"cauterises\": \"cauterizes\",\n  \"cauterising\": \"cauterizing\",\n  \"cavilled\": \"caviled\",\n  \"cavilling\": \"caviling\",\n  \"centigramme\": \"centigram\",\n  \"centigrammes\": \"centigrams\",\n  \"centilitre\": \"centiliter\",\n  \"centilitres\": \"centiliters\",\n  \"centimetre\": \"centimeter\",\n  \"centimetres\": \"centimeters\",\n  \"centralise\": \"centralize\",\n  \"centralised\": \"centralized\",\n  \"centralises\": \"centralizes\",\n  \"centralising\": \"centralizing\",\n  \"centre\": \"center\",\n  \"centred\": \"centered\",\n  \"centrefold\": \"centerfold\",\n  \"centrefolds\": \"centerfolds\",\n  \"centrepiece\": \"centerpiece\",\n  \"centrepieces\": \"centerpieces\",\n  \"centres\": \"centers\",\n  \"channelled\": \"channeled\",\n  \"channelling\": \"channeling\",\n  \"characterise\": \"characterize\",\n  \"characterised\": \"characterized\",\n  \"characterises\": \"characterizes\",\n  \"characterising\": \"characterizing\",\n  \"cheque\": \"check\",\n  \"chequebook\": \"checkbook\",\n  \"chequebooks\": \"checkbooks\",\n  \"chequered\": \"checkered\",\n  \"cheques\": \"checks\",\n  \"chilli\": \"chili\",\n  \"chimaera\": \"chimera\",\n  \"chimaeras\": \"chimeras\",\n  \"chiselled\": \"chiseled\",\n  \"chiselling\": \"chiseling\",\n  \"circularise\": \"circularize\",\n  \"circularised\": \"circularized\",\n  \"circularises\": \"circularizes\",\n  \"circularising\": \"circularizing\",\n  \"civilise\": \"civilize\",\n  \"civilised\": \"civilized\",\n  \"civilises\": \"civilizes\",\n  \"civilising\": \"civilizing\",\n  \"clamour\": \"clamor\",\n  \"clamoured\": \"clamored\",\n  \"clamouring\": \"clamoring\",\n  \"clamours\": \"clamors\",\n  \"clangour\": \"clangor\",\n  \"clarinettist\": \"clarinetist\",\n  \"clarinettists\": \"clarinetists\",\n  \"collectivise\": \"collectivize\",\n  \"collectivised\": \"collectivized\",\n  \"collectivises\": \"collectivizes\",\n  \"collectivising\": \"collectivizing\",\n  \"colonisation\": \"colonization\",\n  \"colonise\": \"colonize\",\n  \"colonised\": \"colonized\",\n  \"coloniser\": \"colonizer\",\n  \"colonisers\": \"colonizers\",\n  \"colonises\": \"colonizes\",\n  \"colonising\": \"colonizing\",\n  \"colour\": \"color\",\n  \"colourant\": \"colorant\",\n  \"colourants\": \"colorants\",\n  \"coloured\": \"colored\",\n  \"coloureds\": \"coloreds\",\n  \"colourful\": \"colorful\",\n  \"colourfully\": \"colorfully\",\n  \"colouring\": \"coloring\",\n  \"colourize\": \"colorize\",\n  \"colourized\": \"colorized\",\n  \"colourizes\": \"colorizes\",\n  \"colourizing\": \"colorizing\",\n  \"colourless\": \"colorless\",\n  \"colours\": \"colors\",\n  \"commercialise\": \"commercialize\",\n  \"commercialised\": \"commercialized\",\n  \"commercialises\": \"commercializes\",\n  \"commercialising\": \"commercializing\",\n  \"compartmentalise\": \"compartmentalize\",\n  \"compartmentalised\": \"compartmentalized\",\n  \"compartmentalises\": \"compartmentalizes\",\n  \"compartmentalising\": \"compartmentalizing\",\n  \"computerise\": \"computerize\",\n  \"computerised\": \"computerized\",\n  \"computerises\": \"computerizes\",\n  \"computerising\": \"computerizing\",\n  \"conceptualise\": \"conceptualize\",\n  \"conceptualised\": \"conceptualized\",\n  \"conceptualises\": \"conceptualizes\",\n  \"conceptualising\": \"conceptualizing\",\n  \"connexion\": \"connection\",\n  \"connexions\": \"connections\",\n  \"contextualise\": \"contextualize\",\n  \"contextualised\": \"contextualized\",\n  \"contextualises\": \"contextualizes\",\n  \"contextualising\": \"contextualizing\",\n  \"cosier\": \"cozier\",\n  \"cosies\": \"cozies\",\n  \"cosiest\": \"coziest\",\n  \"cosily\": \"cozily\",\n  \"cosiness\": \"coziness\",\n  \"cosy\": \"cozy\",\n  \"councillor\": \"councilor\",\n  \"councillors\": \"councilors\",\n  \"counselled\": \"counseled\",\n  \"counselling\": \"counseling\",\n  \"counsellor\": \"counselor\",\n  \"counsellors\": \"counselors\",\n  \"crenelated\": \"crenellated\",\n  \"criminalise\": \"criminalize\",\n  \"criminalised\": \"criminalized\",\n  \"criminalises\": \"criminalizes\",\n  \"criminalising\": \"criminalizing\",\n  \"criticise\": \"criticize\",\n  \"criticised\": \"criticized\",\n  \"criticises\": \"criticizes\",\n  \"criticising\": \"criticizing\",\n  \"crueller\": \"crueler\",\n  \"cruellest\": \"cruelest\",\n  \"crystallisation\": \"crystallization\",\n  \"crystallise\": \"crystallize\",\n  \"crystallised\": \"crystallized\",\n  \"crystallises\": \"crystallizes\",\n  \"crystallising\": \"crystallizing\",\n  \"cudgelled\": \"cudgeled\",\n  \"cudgelling\": \"cudgeling\",\n  \"customise\": \"customize\",\n  \"customised\": \"customized\",\n  \"customises\": \"customizes\",\n  \"customising\": \"customizing\",\n  \"cypher\": \"cipher\",\n  \"cyphers\": \"ciphers\",\n  \"decentralisation\": \"decentralization\",\n  \"decentralise\": \"decentralize\",\n  \"decentralised\": \"decentralized\",\n  \"decentralises\": \"decentralizes\",\n  \"decentralising\": \"decentralizing\",\n  \"decriminalisation\": \"decriminalization\",\n  \"decriminalise\": \"decriminalize\",\n  \"decriminalised\": \"decriminalized\",\n  \"decriminalises\": \"decriminalizes\",\n  \"decriminalising\": \"decriminalizing\",\n  \"defence\": \"defense\",\n  \"defenceless\": \"defenseless\",\n  \"defences\": \"defenses\",\n  \"dehumanisation\": \"dehumanization\",\n  \"dehumanise\": \"dehumanize\",\n  \"dehumanised\": \"dehumanized\",\n  \"dehumanises\": \"dehumanizes\",\n  \"dehumanising\": \"dehumanizing\",\n  \"demeanour\": \"demeanor\",\n  \"demilitarisation\": \"demilitarization\",\n  \"demilitarise\": \"demilitarize\",\n  \"demilitarised\": \"demilitarized\",\n  \"demilitarises\": \"demilitarizes\",\n  \"demilitarising\": \"demilitarizing\",\n  \"demobilisation\": \"demobilization\",\n  \"demobilise\": \"demobilize\",\n  \"demobilised\": \"demobilized\",\n  \"demobilises\": \"demobilizes\",\n  \"demobilising\": \"demobilizing\",\n  \"democratisation\": \"democratization\",\n  \"democratise\": \"democratize\",\n  \"democratised\": \"democratized\",\n  \"democratises\": \"democratizes\",\n  \"democratising\": \"democratizing\",\n  \"demonise\": \"demonize\",\n  \"demonised\": \"demonized\",\n  \"demonises\": \"demonizes\",\n  \"demonising\": \"demonizing\",\n  \"demoralisation\": \"demoralization\",\n  \"demoralise\": \"demoralize\",\n  \"demoralised\": \"demoralized\",\n  \"demoralises\": \"demoralizes\",\n  \"demoralising\": \"demoralizing\",\n  \"denationalisation\": \"denationalization\",\n  \"denationalise\": \"denationalize\",\n  \"denationalised\": \"denationalized\",\n  \"denationalises\": \"denationalizes\",\n  \"denationalising\": \"denationalizing\",\n  \"deodorise\": \"deodorize\",\n  \"deodorised\": \"deodorized\",\n  \"deodorises\": \"deodorizes\",\n  \"deodorising\": \"deodorizing\",\n  \"depersonalise\": \"depersonalize\",\n  \"depersonalised\": \"depersonalized\",\n  \"depersonalises\": \"depersonalizes\",\n  \"depersonalising\": \"depersonalizing\",\n  \"deputise\": \"deputize\",\n  \"deputised\": \"deputized\",\n  \"deputises\": \"deputizes\",\n  \"deputising\": \"deputizing\",\n  \"desensitisation\": \"desensitization\",\n  \"desensitise\": \"desensitize\",\n  \"desensitised\": \"desensitized\",\n  \"desensitises\": \"desensitizes\",\n  \"desensitising\": \"desensitizing\",\n  \"destabilisation\": \"destabilization\",\n  \"destabilise\": \"destabilize\",\n  \"destabilised\": \"destabilized\",\n  \"destabilises\": \"destabilizes\",\n  \"destabilising\": \"destabilizing\",\n  \"dialled\": \"dialed\",\n  \"dialling\": \"dialing\",\n  \"dialogue\": \"dialog\",\n  \"dialogues\": \"dialogs\",\n  \"diarrhoea\": \"diarrhea\",\n  \"digitise\": \"digitize\",\n  \"digitised\": \"digitized\",\n  \"digitises\": \"digitizes\",\n  \"digitising\": \"digitizing\",\n  \"disc\": \"disk\",\n  \"discolour\": \"discolor\",\n  \"discoloured\": \"discolored\",\n  \"discolouring\": \"discoloring\",\n  \"discolours\": \"discolors\",\n  \"discs\": \"disks\",\n  \"disembowelled\": \"disemboweled\",\n  \"disembowelling\": \"disemboweling\",\n  \"disfavour\": \"disfavor\",\n  \"dishevelled\": \"disheveled\",\n  \"dishonour\": \"dishonor\",\n  \"dishonourable\": \"dishonorable\",\n  \"dishonourably\": \"dishonorably\",\n  \"dishonoured\": \"dishonored\",\n  \"dishonouring\": \"dishonoring\",\n  \"dishonours\": \"dishonors\",\n  \"disorganisation\": \"disorganization\",\n  \"disorganised\": \"disorganized\",\n  \"distil\": \"distill\",\n  \"distils\": \"distills\",\n  \"dramatisation\": \"dramatization\",\n  \"dramatisations\": \"dramatizations\",\n  \"dramatise\": \"dramatize\",\n  \"dramatised\": \"dramatized\",\n  \"dramatises\": \"dramatizes\",\n  \"dramatising\": \"dramatizing\",\n  \"draught\": \"draft\",\n  \"draughtboard\": \"draftboard\",\n  \"draughtboards\": \"draftboards\",\n  \"draughtier\": \"draftier\",\n  \"draughtiest\": \"draftiest\",\n  \"draughts\": \"drafts\",\n  \"draughtsman\": \"draftsman\",\n  \"draughtsmanship\": \"draftsmanship\",\n  \"draughtsmen\": \"draftsmen\",\n  \"draughtswoman\": \"draftswoman\",\n  \"draughtswomen\": \"draftswomen\",\n  \"draughty\": \"drafty\",\n  \"drivelled\": \"driveled\",\n  \"drivelling\": \"driveling\",\n  \"duelled\": \"dueled\",\n  \"duelling\": \"dueling\",\n  \"economise\": \"economize\",\n  \"economised\": \"economized\",\n  \"economises\": \"economizes\",\n  \"economising\": \"economizing\",\n  \"editorialise\": \"editorialize\",\n  \"editorialised\": \"editorialized\",\n  \"editorialises\": \"editorializes\",\n  \"editorialising\": \"editorializing\",\n  \"edoema\": \"edema\",\n  \"empathise\": \"empathize\",\n  \"empathised\": \"empathized\",\n  \"empathises\": \"empathizes\",\n  \"empathising\": \"empathizing\",\n  \"emphasise\": \"emphasize\",\n  \"emphasised\": \"emphasized\",\n  \"emphasises\": \"emphasizes\",\n  \"emphasising\": \"emphasizing\",\n  \"enamelled\": \"enameled\",\n  \"enamelling\": \"enameling\",\n  \"enamoured\": \"enamored\",\n  \"encyclopaedia\": \"encyclopedia\",\n  \"encyclopaedias\": \"encyclopedias\",\n  \"encyclopaedic\": \"encyclopedic\",\n  \"endeavour\": \"endeavor\",\n  \"endeavoured\": \"endeavored\",\n  \"endeavouring\": \"endeavoring\",\n  \"endeavours\": \"endeavors\",\n  \"energise\": \"energize\",\n  \"energised\": \"energized\",\n  \"energises\": \"energizes\",\n  \"energising\": \"energizing\",\n  \"enrol\": \"enroll\",\n  \"enrols\": \"enrolls\",\n  \"enthral\": \"enthrall\",\n  \"enthrals\": \"enthralls\",\n  \"epaulette\": \"epaulet\",\n  \"epaulettes\": \"epaulets\",\n  \"epicentre\": \"epicenter\",\n  \"epicentres\": \"epicenters\",\n  \"epilogue\": \"epilog\",\n  \"epilogues\": \"epilogs\",\n  \"epitomise\": \"epitomize\",\n  \"epitomised\": \"epitomized\",\n  \"epitomises\": \"epitomizes\",\n  \"epitomising\": \"epitomizing\",\n  \"equalisation\": \"equalization\",\n  \"equalise\": \"equalize\",\n  \"equalised\": \"equalized\",\n  \"equaliser\": \"equalizer\",\n  \"equalisers\": \"equalizers\",\n  \"equalises\": \"equalizes\",\n  \"equalising\": \"equalizing\",\n  \"eulogise\": \"eulogize\",\n  \"eulogised\": \"eulogized\",\n  \"eulogises\": \"eulogizes\",\n  \"eulogising\": \"eulogizing\",\n  \"evangelise\": \"evangelize\",\n  \"evangelised\": \"evangelized\",\n  \"evangelises\": \"evangelizes\",\n  \"evangelising\": \"evangelizing\",\n  \"exorcise\": \"exorcize\",\n  \"exorcised\": \"exorcized\",\n  \"exorcises\": \"exorcizes\",\n  \"exorcising\": \"exorcizing\",\n  \"extemporisation\": \"extemporization\",\n  \"extemporise\": \"extemporize\",\n  \"extemporised\": \"extemporized\",\n  \"extemporises\": \"extemporizes\",\n  \"extemporising\": \"extemporizing\",\n  \"externalisation\": \"externalization\",\n  \"externalisations\": \"externalizations\",\n  \"externalise\": \"externalize\",\n  \"externalised\": \"externalized\",\n  \"externalises\": \"externalizes\",\n  \"externalising\": \"externalizing\",\n  \"factorise\": \"factorize\",\n  \"factorised\": \"factorized\",\n  \"factorises\": \"factorizes\",\n  \"factorising\": \"factorizing\",\n  \"faecal\": \"fecal\",\n  \"faeces\": \"feces\",\n  \"familiarisation\": \"familiarization\",\n  \"familiarise\": \"familiarize\",\n  \"familiarised\": \"familiarized\",\n  \"familiarises\": \"familiarizes\",\n  \"familiarising\": \"familiarizing\",\n  \"fantasise\": \"fantasize\",\n  \"fantasised\": \"fantasized\",\n  \"fantasises\": \"fantasizes\",\n  \"fantasising\": \"fantasizing\",\n  \"favour\": \"favor\",\n  \"favourable\": \"favorable\",\n  \"favourably\": \"favorably\",\n  \"favoured\": \"favored\",\n  \"favouring\": \"favoring\",\n  \"favourite\": \"favorite\",\n  \"favourites\": \"favorites\",\n  \"favouritism\": \"favoritism\",\n  \"favours\": \"favors\",\n  \"feminise\": \"feminize\",\n  \"feminised\": \"feminized\",\n  \"feminises\": \"feminizes\",\n  \"feminising\": \"feminizing\",\n  \"fertilisation\": \"fertilization\",\n  \"fertilise\": \"fertilize\",\n  \"fertilised\": \"fertilized\",\n  \"fertiliser\": \"fertilizer\",\n  \"fertilisers\": \"fertilizers\",\n  \"fertilises\": \"fertilizes\",\n  \"fertilising\": \"fertilizing\",\n  \"fervour\": \"fervor\",\n  \"fibre\": \"fiber\",\n  \"fibreglass\": \"fiberglass\",\n  \"fibres\": \"fibers\",\n  \"fictionalisation\": \"fictionalization\",\n  \"fictionalisations\": \"fictionalizations\",\n  \"fictionalise\": \"fictionalize\",\n  \"fictionalised\": \"fictionalized\",\n  \"fictionalises\": \"fictionalizes\",\n  \"fictionalising\": \"fictionalizing\",\n  \"fillet\": \"filet\",\n  \"filleted\": \"fileted\",\n  \"filleting\": \"fileting\",\n  \"fillets\": \"filets\",\n  \"finalisation\": \"finalization\",\n  \"finalise\": \"finalize\",\n  \"finalised\": \"finalized\",\n  \"finalises\": \"finalizes\",\n  \"finalising\": \"finalizing\",\n  \"flautist\": \"flutist\",\n  \"flautists\": \"flutists\",\n  \"flavour\": \"flavor\",\n  \"flavoured\": \"flavored\",\n  \"flavouring\": \"flavoring\",\n  \"flavourings\": \"flavorings\",\n  \"flavourless\": \"flavorless\",\n  \"flavours\": \"flavors\",\n  \"flavoursome\": \"flavorsome\",\n  \"flyer / flier\": \"flier / flyer\",\n  \"foetal\": \"fetal\",\n  \"foetid\": \"fetid\",\n  \"foetus\": \"fetus\",\n  \"foetuses\": \"fetuses\",\n  \"formalisation\": \"formalization\",\n  \"formalise\": \"formalize\",\n  \"formalised\": \"formalized\",\n  \"formalises\": \"formalizes\",\n  \"formalising\": \"formalizing\",\n  \"fossilisation\": \"fossilization\",\n  \"fossilise\": \"fossilize\",\n  \"fossilised\": \"fossilized\",\n  \"fossilises\": \"fossilizes\",\n  \"fossilising\": \"fossilizing\",\n  \"fraternisation\": \"fraternization\",\n  \"fraternise\": \"fraternize\",\n  \"fraternised\": \"fraternized\",\n  \"fraternises\": \"fraternizes\",\n  \"fraternising\": \"fraternizing\",\n  \"fulfil\": \"fulfill\",\n  \"fulfilment\": \"fulfillment\",\n  \"fulfils\": \"fulfills\",\n  \"funnelled\": \"funneled\",\n  \"funnelling\": \"funneling\",\n  \"gage\": \"gauge\",\n  \"gaged\": \"gauged\",\n  \"gages\": \"gauges\",\n  \"gaging\": \"gauging\",\n  \"galvanise\": \"galvanize\",\n  \"galvanised\": \"galvanized\",\n  \"galvanises\": \"galvanizes\",\n  \"galvanising\": \"galvanizing\",\n  \"gambolled\": \"gamboled\",\n  \"gambolling\": \"gamboling\",\n  \"gaol\": \"jail\",\n  \"gaolbird\": \"jailbird\",\n  \"gaolbirds\": \"jailbirds\",\n  \"gaolbreak\": \"jailbreak\",\n  \"gaolbreaks\": \"jailbreaks\",\n  \"gaoled\": \"jailed\",\n  \"gaoler\": \"jailer\",\n  \"gaolers\": \"jailers\",\n  \"gaoling\": \"jailing\",\n  \"gaols\": \"jails\",\n  \"gasses\": \"gases\",\n  \"generalisation\": \"generalization\",\n  \"generalisations\": \"generalizations\",\n  \"generalise\": \"generalize\",\n  \"generalised\": \"generalized\",\n  \"generalises\": \"generalizes\",\n  \"generalising\": \"generalizing\",\n  \"ghettoise\": \"ghettoize\",\n  \"ghettoised\": \"ghettoized\",\n  \"ghettoises\": \"ghettoizes\",\n  \"ghettoising\": \"ghettoizing\",\n  \"gipsies\": \"gypsies\",\n  \"glamor\": \"glamour\",\n  \"glamorise\": \"glamorize\",\n  \"glamorised\": \"glamorized\",\n  \"glamorises\": \"glamorizes\",\n  \"glamorising\": \"glamorizing\",\n  \"globalisation\": \"globalization\",\n  \"globalise\": \"globalize\",\n  \"globalised\": \"globalized\",\n  \"globalises\": \"globalizes\",\n  \"globalising\": \"globalizing\",\n  \"glueing\": \"gluing\",\n  \"goitre\": \"goiter\",\n  \"goitres\": \"goiters\",\n  \"gonorrhoea\": \"gonorrhea\",\n  \"gramme\": \"gram\",\n  \"grammes\": \"grams\",\n  \"gravelled\": \"graveled\",\n  \"grey\": \"gray\",\n  \"greyed\": \"grayed\",\n  \"greying\": \"graying\",\n  \"greyish\": \"grayish\",\n  \"greyness\": \"grayness\",\n  \"greys\": \"grays\",\n  \"grovelled\": \"groveled\",\n  \"grovelling\": \"groveling\",\n  \"groyne\": \"groin\",\n  \"groynes\": \"groins\",\n  \"gruelling\": \"grueling\",\n  \"gruellingly\": \"gruelingly\",\n  \"gryphon\": \"griffin\",\n  \"gryphons\": \"griffins\",\n  \"gynaecological\": \"gynecological\",\n  \"gynaecologist\": \"gynecologist\",\n  \"gynaecologists\": \"gynecologists\",\n  \"gynaecology\": \"gynecology\",\n  \"haematological\": \"hematological\",\n  \"haematologist\": \"hematologist\",\n  \"haematologists\": \"hematologists\",\n  \"haematology\": \"hematology\",\n  \"haemoglobin\": \"hemoglobin\",\n  \"haemophilia\": \"hemophilia\",\n  \"haemophiliac\": \"hemophiliac\",\n  \"haemophiliacs\": \"hemophiliacs\",\n  \"haemorrhage\": \"hemorrhage\",\n  \"haemorrhaged\": \"hemorrhaged\",\n  \"haemorrhages\": \"hemorrhages\",\n  \"haemorrhaging\": \"hemorrhaging\",\n  \"haemorrhoids\": \"hemorrhoids\",\n  \"harbour\": \"harbor\",\n  \"harboured\": \"harbored\",\n  \"harbouring\": \"harboring\",\n  \"harbours\": \"harbors\",\n  \"harmonisation\": \"harmonization\",\n  \"harmonise\": \"harmonize\",\n  \"harmonised\": \"harmonized\",\n  \"harmonises\": \"harmonizes\",\n  \"harmonising\": \"harmonizing\",\n  \"homoeopath\": \"homeopath\",\n  \"homoeopathic\": \"homeopathic\",\n  \"homoeopaths\": \"homeopaths\",\n  \"homoeopathy\": \"homeopathy\",\n  \"homogenise\": \"homogenize\",\n  \"homogenised\": \"homogenized\",\n  \"homogenises\": \"homogenizes\",\n  \"homogenising\": \"homogenizing\",\n  \"honour\": \"honor\",\n  \"honourable\": \"honorable\",\n  \"honourably\": \"honorably\",\n  \"honoured\": \"honored\",\n  \"honouring\": \"honoring\",\n  \"honours\": \"honors\",\n  \"hospitalisation\": \"hospitalization\",\n  \"hospitalise\": \"hospitalize\",\n  \"hospitalised\": \"hospitalized\",\n  \"hospitalises\": \"hospitalizes\",\n  \"hospitalising\": \"hospitalizing\",\n  \"humanise\": \"humanize\",\n  \"humanised\": \"humanized\",\n  \"humanises\": \"humanizes\",\n  \"humanising\": \"humanizing\",\n  \"humour\": \"humor\",\n  \"humoured\": \"humored\",\n  \"humouring\": \"humoring\",\n  \"humourless\": \"humorless\",\n  \"humours\": \"humors\",\n  \"hybridise\": \"hybridize\",\n  \"hybridised\": \"hybridized\",\n  \"hybridises\": \"hybridizes\",\n  \"hybridising\": \"hybridizing\",\n  \"hypnotise\": \"hypnotize\",\n  \"hypnotised\": \"hypnotized\",\n  \"hypnotises\": \"hypnotizes\",\n  \"hypnotising\": \"hypnotizing\",\n  \"hypothesise\": \"hypothesize\",\n  \"hypothesised\": \"hypothesized\",\n  \"hypothesises\": \"hypothesizes\",\n  \"hypothesising\": \"hypothesizing\",\n  \"idealisation\": \"idealization\",\n  \"idealise\": \"idealize\",\n  \"idealised\": \"idealized\",\n  \"idealises\": \"idealizes\",\n  \"idealising\": \"idealizing\",\n  \"idolise\": \"idolize\",\n  \"idolised\": \"idolized\",\n  \"idolises\": \"idolizes\",\n  \"idolising\": \"idolizing\",\n  \"immobilisation\": \"immobilization\",\n  \"immobilise\": \"immobilize\",\n  \"immobilised\": \"immobilized\",\n  \"immobiliser\": \"immobilizer\",\n  \"immobilisers\": \"immobilizers\",\n  \"immobilises\": \"immobilizes\",\n  \"immobilising\": \"immobilizing\",\n  \"immortalise\": \"immortalize\",\n  \"immortalised\": \"immortalized\",\n  \"immortalises\": \"immortalizes\",\n  \"immortalising\": \"immortalizing\",\n  \"immunisation\": \"immunization\",\n  \"immunise\": \"immunize\",\n  \"immunised\": \"immunized\",\n  \"immunises\": \"immunizes\",\n  \"immunising\": \"immunizing\",\n  \"impanelled\": \"impaneled\",\n  \"impanelling\": \"impaneling\",\n  \"imperilled\": \"imperiled\",\n  \"imperilling\": \"imperiling\",\n  \"individualise\": \"individualize\",\n  \"individualised\": \"individualized\",\n  \"individualises\": \"individualizes\",\n  \"individualising\": \"individualizing\",\n  \"industrialise\": \"industrialize\",\n  \"industrialised\": \"industrialized\",\n  \"industrialises\": \"industrializes\",\n  \"industrialising\": \"industrializing\",\n  \"inflexion\": \"inflection\",\n  \"inflexions\": \"inflections\",\n  \"initialise\": \"initialize\",\n  \"initialised\": \"initialized\",\n  \"initialises\": \"initializes\",\n  \"initialising\": \"initializing\",\n  \"initialled\": \"initialed\",\n  \"initialling\": \"initialing\",\n  \"instal\": \"install\",\n  \"instalment\": \"installment\",\n  \"instalments\": \"installments\",\n  \"instals\": \"installs\",\n  \"instil\": \"instill\",\n  \"instils\": \"instills\",\n  \"institutionalisation\": \"institutionalization\",\n  \"institutionalise\": \"institutionalize\",\n  \"institutionalised\": \"institutionalized\",\n  \"institutionalises\": \"institutionalizes\",\n  \"institutionalising\": \"institutionalizing\",\n  \"intellectualise\": \"intellectualize\",\n  \"intellectualised\": \"intellectualized\",\n  \"intellectualises\": \"intellectualizes\",\n  \"intellectualising\": \"intellectualizing\",\n  \"internalisation\": \"internalization\",\n  \"internalise\": \"internalize\",\n  \"internalised\": \"internalized\",\n  \"internalises\": \"internalizes\",\n  \"internalising\": \"internalizing\",\n  \"internationalisation\": \"internationalization\",\n  \"internationalise\": \"internationalize\",\n  \"internationalised\": \"internationalized\",\n  \"internationalises\": \"internationalizes\",\n  \"internationalising\": \"internationalizing\",\n  \"ionisation\": \"ionization\",\n  \"ionise\": \"ionize\",\n  \"ionised\": \"ionized\",\n  \"ioniser\": \"ionizer\",\n  \"ionisers\": \"ionizers\",\n  \"ionises\": \"ionizes\",\n  \"ionising\": \"ionizing\",\n  \"italicise\": \"italicize\",\n  \"italicised\": \"italicized\",\n  \"italicises\": \"italicizes\",\n  \"italicising\": \"italicizing\",\n  \"itemise\": \"itemize\",\n  \"itemised\": \"itemized\",\n  \"itemises\": \"itemizes\",\n  \"itemising\": \"itemizing\",\n  \"jeopardise\": \"jeopardize\",\n  \"jeopardised\": \"jeopardized\",\n  \"jeopardises\": \"jeopardizes\",\n  \"jeopardising\": \"jeopardizing\",\n  \"jewelled\": \"jeweled\",\n  \"jeweller\": \"jeweler\",\n  \"jewellers\": \"jewelers\",\n  \"jewellery\": \"jewelry\",\n  \"judgement\": \"judgment\",\n  \"kilogramme\": \"kilogram\",\n  \"kilogrammes\": \"kilograms\",\n  \"kilometre\": \"kilometer\",\n  \"kilometres\": \"kilometers\",\n  \"labelled\": \"labeled\",\n  \"labelling\": \"labeling\",\n  \"labour\": \"labor\",\n  \"laboured\": \"labored\",\n  \"labourer\": \"laborer\",\n  \"labourers\": \"laborers\",\n  \"labouring\": \"laboring\",\n  \"labours\": \"labors\",\n  \"lacklustre\": \"lackluster\",\n  \"legalisation\": \"legalization\",\n  \"legalise\": \"legalize\",\n  \"legalised\": \"legalized\",\n  \"legalises\": \"legalizes\",\n  \"legalising\": \"legalizing\",\n  \"legitimise\": \"legitimize\",\n  \"legitimised\": \"legitimized\",\n  \"legitimises\": \"legitimizes\",\n  \"legitimising\": \"legitimizing\",\n  \"leukaemia\": \"leukemia\",\n  \"levelled\": \"leveled\",\n  \"leveller\": \"leveler\",\n  \"levellers\": \"levelers\",\n  \"levelling\": \"leveling\",\n  \"libelled\": \"libeled\",\n  \"libelling\": \"libeling\",\n  \"libellous\": \"libelous\",\n  \"liberalisation\": \"liberalization\",\n  \"liberalise\": \"liberalize\",\n  \"liberalised\": \"liberalized\",\n  \"liberalises\": \"liberalizes\",\n  \"liberalising\": \"liberalizing\",\n  \"licence\": \"license\",\n  \"licenced\": \"licensed\",\n  \"licences\": \"licenses\",\n  \"licencing\": \"licensing\",\n  \"likeable\": \"likable\",\n  \"lionisation\": \"lionization\",\n  \"lionise\": \"lionize\",\n  \"lionised\": \"lionized\",\n  \"lionises\": \"lionizes\",\n  \"lionising\": \"lionizing\",\n  \"liquidise\": \"liquidize\",\n  \"liquidised\": \"liquidized\",\n  \"liquidiser\": \"liquidizer\",\n  \"liquidisers\": \"liquidizers\",\n  \"liquidises\": \"liquidizes\",\n  \"liquidising\": \"liquidizing\",\n  \"litre\": \"liter\",\n  \"litres\": \"liters\",\n  \"localise\": \"localize\",\n  \"localised\": \"localized\",\n  \"localises\": \"localizes\",\n  \"localising\": \"localizing\",\n  \"louvre\": \"louver\",\n  \"louvred\": \"louvered\",\n  \"louvres\": \"louvers\",\n  \"lustre\": \"luster\",\n  \"magnetise\": \"magnetize\",\n  \"magnetised\": \"magnetized\",\n  \"magnetises\": \"magnetizes\",\n  \"magnetising\": \"magnetizing\",\n  \"manoeuvrability\": \"maneuverability\",\n  \"manoeuvrable\": \"maneuverable\",\n  \"manoeuvre\": \"maneuver\",\n  \"manoeuvred\": \"maneuvered\",\n  \"manoeuvres\": \"maneuvers\",\n  \"manoeuvring\": \"maneuvering\",\n  \"manoeuvrings\": \"maneuverings\",\n  \"marginalisation\": \"marginalization\",\n  \"marginalise\": \"marginalize\",\n  \"marginalised\": \"marginalized\",\n  \"marginalises\": \"marginalizes\",\n  \"marginalising\": \"marginalizing\",\n  \"marshalled\": \"marshaled\",\n  \"marshalling\": \"marshaling\",\n  \"marvelled\": \"marveled\",\n  \"marvelling\": \"marveling\",\n  \"marvellous\": \"marvelous\",\n  \"marvellously\": \"marvelously\",\n  \"materialisation\": \"materialization\",\n  \"materialise\": \"materialize\",\n  \"materialised\": \"materialized\",\n  \"materialises\": \"materializes\",\n  \"materialising\": \"materializing\",\n  \"maximisation\": \"maximization\",\n  \"maximise\": \"maximize\",\n  \"maximised\": \"maximized\",\n  \"maximises\": \"maximizes\",\n  \"maximising\": \"maximizing\",\n  \"meagre\": \"meager\",\n  \"mechanisation\": \"mechanization\",\n  \"mechanise\": \"mechanize\",\n  \"mechanised\": \"mechanized\",\n  \"mechanises\": \"mechanizes\",\n  \"mechanising\": \"mechanizing\",\n  \"mediaeval\": \"medieval\",\n  \"memorialise\": \"memorialize\",\n  \"memorialised\": \"memorialized\",\n  \"memorialises\": \"memorializes\",\n  \"memorialising\": \"memorializing\",\n  \"memorise\": \"memorize\",\n  \"memorised\": \"memorized\",\n  \"memorises\": \"memorizes\",\n  \"memorising\": \"memorizing\",\n  \"mesmerise\": \"mesmerize\",\n  \"mesmerised\": \"mesmerized\",\n  \"mesmerises\": \"mesmerizes\",\n  \"mesmerising\": \"mesmerizing\",\n  \"metabolise\": \"metabolize\",\n  \"metabolised\": \"metabolized\",\n  \"metabolises\": \"metabolizes\",\n  \"metabolising\": \"metabolizing\",\n  \"metre\": \"meter\",\n  \"metres\": \"meters\",\n  \"mhm\": \"hmm\",\n  \"micrometre\": \"micrometer\",\n  \"micrometres\": \"micrometers\",\n  \"militarise\": \"militarize\",\n  \"militarised\": \"militarized\",\n  \"militarises\": \"militarizes\",\n  \"militarising\": \"militarizing\",\n  \"milligramme\": \"milligram\",\n  \"milligrammes\": \"milligrams\",\n  \"millilitre\": \"milliliter\",\n  \"millilitres\": \"milliliters\",\n  \"millimetre\": \"millimeter\",\n  \"millimetres\": \"millimeters\",\n  \"miniaturisation\": \"miniaturization\",\n  \"miniaturise\": \"miniaturize\",\n  \"miniaturised\": \"miniaturized\",\n  \"miniaturises\": \"miniaturizes\",\n  \"miniaturising\": \"miniaturizing\",\n  \"minibusses\": \"minibuses\",\n  \"minimise\": \"minimize\",\n  \"minimised\": \"minimized\",\n  \"minimises\": \"minimizes\",\n  \"minimising\": \"minimizing\",\n  \"misbehaviour\": \"misbehavior\",\n  \"misdemeanour\": \"misdemeanor\",\n  \"misdemeanours\": \"misdemeanors\",\n  \"misspelt\": \"misspelled\",\n  \"mitre\": \"miter\",\n  \"mitres\": \"miters\",\n  \"mm\": \"hmm\",\n  \"mmm\": \"hmm\",\n  \"mobilisation\": \"mobilization\",\n  \"mobilise\": \"mobilize\",\n  \"mobilised\": \"mobilized\",\n  \"mobilises\": \"mobilizes\",\n  \"mobilising\": \"mobilizing\",\n  \"modelled\": \"modeled\",\n  \"modeller\": \"modeler\",\n  \"modellers\": \"modelers\",\n  \"modelling\": \"modeling\",\n  \"modernise\": \"modernize\",\n  \"modernised\": \"modernized\",\n  \"modernises\": \"modernizes\",\n  \"modernising\": \"modernizing\",\n  \"moisturise\": \"moisturize\",\n  \"moisturised\": \"moisturized\",\n  \"moisturiser\": \"moisturizer\",\n  \"moisturisers\": \"moisturizers\",\n  \"moisturises\": \"moisturizes\",\n  \"moisturising\": \"moisturizing\",\n  \"monologue\": \"monolog\",\n  \"monologues\": \"monologs\",\n  \"monopolisation\": \"monopolization\",\n  \"monopolise\": \"monopolize\",\n  \"monopolised\": \"monopolized\",\n  \"monopolises\": \"monopolizes\",\n  \"monopolising\": \"monopolizing\",\n  \"moralise\": \"moralize\",\n  \"moralised\": \"moralized\",\n  \"moralises\": \"moralizes\",\n  \"moralising\": \"moralizing\",\n  \"motorised\": \"motorized\",\n  \"mould\": \"mold\",\n  \"moulded\": \"molded\",\n  \"moulder\": \"molder\",\n  \"mouldered\": \"moldered\",\n  \"mouldering\": \"moldering\",\n  \"moulders\": \"molders\",\n  \"mouldier\": \"moldier\",\n  \"mouldiest\": \"moldiest\",\n  \"moulding\": \"molding\",\n  \"mouldings\": \"moldings\",\n  \"moulds\": \"molds\",\n  \"mouldy\": \"moldy\",\n  \"moult\": \"molt\",\n  \"moulted\": \"molted\",\n  \"moulting\": \"molting\",\n  \"moults\": \"molts\",\n  \"moustache\": \"mustache\",\n  \"moustached\": \"mustached\",\n  \"moustaches\": \"mustaches\",\n  \"moustachioed\": \"mustachioed\",\n  \"multicoloured\": \"multicolored\",\n  \"nationalisation\": \"nationalization\",\n  \"nationalisations\": \"nationalizations\",\n  \"nationalise\": \"nationalize\",\n  \"nationalised\": \"nationalized\",\n  \"nationalises\": \"nationalizes\",\n  \"nationalising\": \"nationalizing\",\n  \"naturalisation\": \"naturalization\",\n  \"naturalise\": \"naturalize\",\n  \"naturalised\": \"naturalized\",\n  \"naturalises\": \"naturalizes\",\n  \"naturalising\": \"naturalizing\",\n  \"neighbour\": \"neighbor\",\n  \"neighbourhood\": \"neighborhood\",\n  \"neighbourhoods\": \"neighborhoods\",\n  \"neighbouring\": \"neighboring\",\n  \"neighbourliness\": \"neighborliness\",\n  \"neighbourly\": \"neighborly\",\n  \"neighbours\": \"neighbors\",\n  \"neutralisation\": \"neutralization\",\n  \"neutralise\": \"neutralize\",\n  \"neutralised\": \"neutralized\",\n  \"neutralises\": \"neutralizes\",\n  \"neutralising\": \"neutralizing\",\n  \"normalisation\": \"normalization\",\n  \"normalise\": \"normalize\",\n  \"normalised\": \"normalized\",\n  \"normalises\": \"normalizes\",\n  \"normalising\": \"normalizing\",\n  \"odour\": \"odor\",\n  \"odourless\": \"odorless\",\n  \"odours\": \"odors\",\n  \"oesophagus\": \"esophagus\",\n  \"oesophaguses\": \"esophaguses\",\n  \"oestrogen\": \"estrogen\",\n  \"offence\": \"offense\",\n  \"offences\": \"offenses\",\n  \"omelette\": \"omelet\",\n  \"omelettes\": \"omelets\",\n  \"optimise\": \"optimize\",\n  \"optimised\": \"optimized\",\n  \"optimises\": \"optimizes\",\n  \"optimising\": \"optimizing\",\n  \"organisation\": \"organization\",\n  \"organisational\": \"organizational\",\n  \"organisations\": \"organizations\",\n  \"organise\": \"organize\",\n  \"organised\": \"organized\",\n  \"organiser\": \"organizer\",\n  \"organisers\": \"organizers\",\n  \"organises\": \"organizes\",\n  \"organising\": \"organizing\",\n  \"orthopaedic\": \"orthopedic\",\n  \"orthopaedics\": \"orthopedics\",\n  \"ostracise\": \"ostracize\",\n  \"ostracised\": \"ostracized\",\n  \"ostracises\": \"ostracizes\",\n  \"ostracising\": \"ostracizing\",\n  \"outmanoeuvre\": \"outmaneuver\",\n  \"outmanoeuvred\": \"outmaneuvered\",\n  \"outmanoeuvres\": \"outmaneuvers\",\n  \"outmanoeuvring\": \"outmaneuvering\",\n  \"overemphasise\": \"overemphasize\",\n  \"overemphasised\": \"overemphasized\",\n  \"overemphasises\": \"overemphasizes\",\n  \"overemphasising\": \"overemphasizing\",\n  \"oxidisation\": \"oxidization\",\n  \"oxidise\": \"oxidize\",\n  \"oxidised\": \"oxidized\",\n  \"oxidises\": \"oxidizes\",\n  \"oxidising\": \"oxidizing\",\n  \"paederast\": \"pederast\",\n  \"paederasts\": \"pederasts\",\n  \"paediatric\": \"pediatric\",\n  \"paediatrician\": \"pediatrician\",\n  \"paediatricians\": \"pediatricians\",\n  \"paediatrics\": \"pediatrics\",\n  \"paedophile\": \"pedophile\",\n  \"paedophiles\": \"pedophiles\",\n  \"paedophilia\": \"pedophilia\",\n  \"palaeolithic\": \"paleolithic\",\n  \"palaeontologist\": \"paleontologist\",\n  \"palaeontologists\": \"paleontologists\",\n  \"palaeontology\": \"paleontology\",\n  \"panelled\": \"paneled\",\n  \"panelling\": \"paneling\",\n  \"panellist\": \"panelist\",\n  \"panellists\": \"panelists\",\n  \"paralyse\": \"paralyze\",\n  \"paralysed\": \"paralyzed\",\n  \"paralyses\": \"paralyzes\",\n  \"paralysing\": \"paralyzing\",\n  \"parcelled\": \"parceled\",\n  \"parcelling\": \"parceling\",\n  \"parlour\": \"parlor\",\n  \"parlours\": \"parlors\",\n  \"particularise\": \"particularize\",\n  \"particularised\": \"particularized\",\n  \"particularises\": \"particularizes\",\n  \"particularising\": \"particularizing\",\n  \"passivisation\": \"passivization\",\n  \"passivise\": \"passivize\",\n  \"passivised\": \"passivized\",\n  \"passivises\": \"passivizes\",\n  \"passivising\": \"passivizing\",\n  \"pasteurisation\": \"pasteurization\",\n  \"pasteurise\": \"pasteurize\",\n  \"pasteurised\": \"pasteurized\",\n  \"pasteurises\": \"pasteurizes\",\n  \"pasteurising\": \"pasteurizing\",\n  \"patronise\": \"patronize\",\n  \"patronised\": \"patronized\",\n  \"patronises\": \"patronizes\",\n  \"patronising\": \"patronizing\",\n  \"patronisingly\": \"patronizingly\",\n  \"pedalled\": \"pedaled\",\n  \"pedalling\": \"pedaling\",\n  \"pedestrianisation\": \"pedestrianization\",\n  \"pedestrianise\": \"pedestrianize\",\n  \"pedestrianised\": \"pedestrianized\",\n  \"pedestrianises\": \"pedestrianizes\",\n  \"pedestrianising\": \"pedestrianizing\",\n  \"penalise\": \"penalize\",\n  \"penalised\": \"penalized\",\n  \"penalises\": \"penalizes\",\n  \"penalising\": \"penalizing\",\n  \"pencilled\": \"penciled\",\n  \"pencilling\": \"penciling\",\n  \"personalise\": \"personalize\",\n  \"personalised\": \"personalized\",\n  \"personalises\": \"personalizes\",\n  \"personalising\": \"personalizing\",\n  \"pharmacopoeia\": \"pharmacopeia\",\n  \"pharmacopoeias\": \"pharmacopeias\",\n  \"philosophise\": \"philosophize\",\n  \"philosophised\": \"philosophized\",\n  \"philosophises\": \"philosophizes\",\n  \"philosophising\": \"philosophizing\",\n  \"philtre\": \"filter\",\n  \"philtres\": \"filters\",\n  \"phoney\": \"phony\",\n  \"plagiarise\": \"plagiarize\",\n  \"plagiarised\": \"plagiarized\",\n  \"plagiarises\": \"plagiarizes\",\n  \"plagiarising\": \"plagiarizing\",\n  \"plough\": \"plow\",\n  \"ploughed\": \"plowed\",\n  \"ploughing\": \"plowing\",\n  \"ploughman\": \"plowman\",\n  \"ploughmen\": \"plowmen\",\n  \"ploughs\": \"plows\",\n  \"ploughshare\": \"plowshare\",\n  \"ploughshares\": \"plowshares\",\n  \"polarisation\": \"polarization\",\n  \"polarise\": \"polarize\",\n  \"polarised\": \"polarized\",\n  \"polarises\": \"polarizes\",\n  \"polarising\": \"polarizing\",\n  \"politicisation\": \"politicization\",\n  \"politicise\": \"politicize\",\n  \"politicised\": \"politicized\",\n  \"politicises\": \"politicizes\",\n  \"politicising\": \"politicizing\",\n  \"popularisation\": \"popularization\",\n  \"popularise\": \"popularize\",\n  \"popularised\": \"popularized\",\n  \"popularises\": \"popularizes\",\n  \"popularising\": \"popularizing\",\n  \"pouffe\": \"pouf\",\n  \"pouffes\": \"poufs\",\n  \"practise\": \"practice\",\n  \"practised\": \"practiced\",\n  \"practises\": \"practices\",\n  \"practising\": \"practicing\",\n  \"praesidium\": \"presidium\",\n  \"praesidiums\": \"presidiums\",\n  \"pressurisation\": \"pressurization\",\n  \"pressurise\": \"pressurize\",\n  \"pressurised\": \"pressurized\",\n  \"pressurises\": \"pressurizes\",\n  \"pressurising\": \"pressurizing\",\n  \"pretence\": \"pretense\",\n  \"pretences\": \"pretenses\",\n  \"primaeval\": \"primeval\",\n  \"prioritisation\": \"prioritization\",\n  \"prioritise\": \"prioritize\",\n  \"prioritised\": \"prioritized\",\n  \"prioritises\": \"prioritizes\",\n  \"prioritising\": \"prioritizing\",\n  \"privatisation\": \"privatization\",\n  \"privatisations\": \"privatizations\",\n  \"privatise\": \"privatize\",\n  \"privatised\": \"privatized\",\n  \"privatises\": \"privatizes\",\n  \"privatising\": \"privatizing\",\n  \"professionalisation\": \"professionalization\",\n  \"professionalise\": \"professionalize\",\n  \"professionalised\": \"professionalized\",\n  \"professionalises\": \"professionalizes\",\n  \"professionalising\": \"professionalizing\",\n  \"programme\": \"program\",\n  \"programmes\": \"programs\",\n  \"prologue\": \"prolog\",\n  \"prologues\": \"prologs\",\n  \"propagandise\": \"propagandize\",\n  \"propagandised\": \"propagandized\",\n  \"propagandises\": \"propagandizes\",\n  \"propagandising\": \"propagandizing\",\n  \"proselytise\": \"proselytize\",\n  \"proselytised\": \"proselytized\",\n  \"proselytiser\": \"proselytizer\",\n  \"proselytisers\": \"proselytizers\",\n  \"proselytises\": \"proselytizes\",\n  \"proselytising\": \"proselytizing\",\n  \"psychoanalyse\": \"psychoanalyze\",\n  \"psychoanalysed\": \"psychoanalyzed\",\n  \"psychoanalyses\": \"psychoanalyzes\",\n  \"psychoanalysing\": \"psychoanalyzing\",\n  \"publicise\": \"publicize\",\n  \"publicised\": \"publicized\",\n  \"publicises\": \"publicizes\",\n  \"publicising\": \"publicizing\",\n  \"pulverisation\": \"pulverization\",\n  \"pulverise\": \"pulverize\",\n  \"pulverised\": \"pulverized\",\n  \"pulverises\": \"pulverizes\",\n  \"pulverising\": \"pulverizing\",\n  \"pummelled\": \"pummel\",\n  \"pummelling\": \"pummeled\",\n  \"pyjama\": \"pajama\",\n  \"pyjamas\": \"pajamas\",\n  \"pzazz\": \"pizzazz\",\n  \"quarrelled\": \"quarreled\",\n  \"quarrelling\": \"quarreling\",\n  \"radicalise\": \"radicalize\",\n  \"radicalised\": \"radicalized\",\n  \"radicalises\": \"radicalizes\",\n  \"radicalising\": \"radicalizing\",\n  \"rancour\": \"rancor\",\n  \"randomise\": \"randomize\",\n  \"randomised\": \"randomized\",\n  \"randomises\": \"randomizes\",\n  \"randomising\": \"randomizing\",\n  \"rationalisation\": \"rationalization\",\n  \"rationalisations\": \"rationalizations\",\n  \"rationalise\": \"rationalize\",\n  \"rationalised\": \"rationalized\",\n  \"rationalises\": \"rationalizes\",\n  \"rationalising\": \"rationalizing\",\n  \"ravelled\": \"raveled\",\n  \"ravelling\": \"raveling\",\n  \"realisable\": \"realizable\",\n  \"realisation\": \"realization\",\n  \"realisations\": \"realizations\",\n  \"realise\": \"realize\",\n  \"realised\": \"realized\",\n  \"realises\": \"realizes\",\n  \"realising\": \"realizing\",\n  \"recognisable\": \"recognizable\",\n  \"recognisably\": \"recognizably\",\n  \"recognisance\": \"recognizance\",\n  \"recognise\": \"recognize\",\n  \"recognised\": \"recognized\",\n  \"recognises\": \"recognizes\",\n  \"recognising\": \"recognizing\",\n  \"reconnoitre\": \"reconnoiter\",\n  \"reconnoitred\": \"reconnoitered\",\n  \"reconnoitres\": \"reconnoiters\",\n  \"reconnoitring\": \"reconnoitering\",\n  \"refuelled\": \"refueled\",\n  \"refuelling\": \"refueling\",\n  \"regularisation\": \"regularization\",\n  \"regularise\": \"regularize\",\n  \"regularised\": \"regularized\",\n  \"regularises\": \"regularizes\",\n  \"regularising\": \"regularizing\",\n  \"remodelled\": \"remodeled\",\n  \"remodelling\": \"remodeling\",\n  \"remould\": \"remold\",\n  \"remoulded\": \"remolded\",\n  \"remoulding\": \"remolding\",\n  \"remoulds\": \"remolds\",\n  \"reorganisation\": \"reorganization\",\n  \"reorganisations\": \"reorganizations\",\n  \"reorganise\": \"reorganize\",\n  \"reorganised\": \"reorganized\",\n  \"reorganises\": \"reorganizes\",\n  \"reorganising\": \"reorganizing\",\n  \"revelled\": \"reveled\",\n  \"reveller\": \"reveler\",\n  \"revellers\": \"revelers\",\n  \"revelling\": \"reveling\",\n  \"revitalise\": \"revitalize\",\n  \"revitalised\": \"revitalized\",\n  \"revitalises\": \"revitalizes\",\n  \"revitalising\": \"revitalizing\",\n  \"revolutionise\": \"revolutionize\",\n  \"revolutionised\": \"revolutionized\",\n  \"revolutionises\": \"revolutionizes\",\n  \"revolutionising\": \"revolutionizing\",\n  \"rhapsodise\": \"rhapsodize\",\n  \"rhapsodised\": \"rhapsodized\",\n  \"rhapsodises\": \"rhapsodizes\",\n  \"rhapsodising\": \"rhapsodizing\",\n  \"rigour\": \"rigor\",\n  \"rigours\": \"rigors\",\n  \"ritualised\": \"ritualized\",\n  \"rivalled\": \"rivaled\",\n  \"rivalling\": \"rivaling\",\n  \"romanticise\": \"romanticize\",\n  \"romanticised\": \"romanticized\",\n  \"romanticises\": \"romanticizes\",\n  \"romanticising\": \"romanticizing\",\n  \"rumour\": \"rumor\",\n  \"rumoured\": \"rumored\",\n  \"rumours\": \"rumors\",\n  \"sabre\": \"saber\",\n  \"sabres\": \"sabers\",\n  \"saltpetre\": \"saltpeter\",\n  \"sanitise\": \"sanitize\",\n  \"sanitised\": \"sanitized\",\n  \"sanitises\": \"sanitizes\",\n  \"sanitising\": \"sanitizing\",\n  \"satirise\": \"satirize\",\n  \"satirised\": \"satirized\",\n  \"satirises\": \"satirizes\",\n  \"satirising\": \"satirizing\",\n  \"saviour\": \"savior\",\n  \"saviours\": \"saviors\",\n  \"savour\": \"savor\",\n  \"savoured\": \"savored\",\n  \"savouries\": \"savories\",\n  \"savouring\": \"savoring\",\n  \"savours\": \"savors\",\n  \"savoury\": \"savory\",\n  \"scandalise\": \"scandalize\",\n  \"scandalised\": \"scandalized\",\n  \"scandalises\": \"scandalizes\",\n  \"scandalising\": \"scandalizing\",\n  \"sceptic\": \"skeptic\",\n  \"sceptical\": \"skeptical\",\n  \"sceptically\": \"skeptically\",\n  \"scepticism\": \"skepticism\",\n  \"sceptics\": \"skeptics\",\n  \"sceptre\": \"scepter\",\n  \"sceptres\": \"scepters\",\n  \"scrutinise\": \"scrutinize\",\n  \"scrutinised\": \"scrutinized\",\n  \"scrutinises\": \"scrutinizes\",\n  \"scrutinising\": \"scrutinizing\",\n  \"secularisation\": \"secularization\",\n  \"secularise\": \"secularize\",\n  \"secularised\": \"secularized\",\n  \"secularises\": \"secularizes\",\n  \"secularising\": \"secularizing\",\n  \"sensationalise\": \"sensationalize\",\n  \"sensationalised\": \"sensationalized\",\n  \"sensationalises\": \"sensationalizes\",\n  \"sensationalising\": \"sensationalizing\",\n  \"sensitise\": \"sensitize\",\n  \"sensitised\": \"sensitized\",\n  \"sensitises\": \"sensitizes\",\n  \"sensitising\": \"sensitizing\",\n  \"sentimentalise\": \"sentimentalize\",\n  \"sentimentalised\": \"sentimentalized\",\n  \"sentimentalises\": \"sentimentalizes\",\n  \"sentimentalising\": \"sentimentalizing\",\n  \"sepulchre\": \"sepulcher\",\n  \"sepulchres\": \"sepulchers\",\n  \"serialisation\": \"serialization\",\n  \"serialisations\": \"serializations\",\n  \"serialise\": \"serialize\",\n  \"serialised\": \"serialized\",\n  \"serialises\": \"serializes\",\n  \"serialising\": \"serializing\",\n  \"sermonise\": \"sermonize\",\n  \"sermonised\": \"sermonized\",\n  \"sermonises\": \"sermonizes\",\n  \"sermonising\": \"sermonizing\",\n  \"sheikh\": \"sheik\",\n  \"shovelled\": \"shoveled\",\n  \"shovelling\": \"shoveling\",\n  \"shrivelled\": \"shriveled\",\n  \"shrivelling\": \"shriveling\",\n  \"signalise\": \"signalize\",\n  \"signalised\": \"signalized\",\n  \"signalises\": \"signalizes\",\n  \"signalising\": \"signalizing\",\n  \"signalled\": \"signaled\",\n  \"signalling\": \"signaling\",\n  \"smoulder\": \"smolder\",\n  \"smouldered\": \"smoldered\",\n  \"smouldering\": \"smoldering\",\n  \"smoulders\": \"smolders\",\n  \"snivelled\": \"sniveled\",\n  \"snivelling\": \"sniveling\",\n  \"snorkelled\": \"snorkeled\",\n  \"snorkelling\": \"snorkeling\",\n  \"snowplough\": \"snowplow\",\n  \"snowploughs\": \"snowplow\",\n  \"socialisation\": \"socialization\",\n  \"socialise\": \"socialize\",\n  \"socialised\": \"socialized\",\n  \"socialises\": \"socializes\",\n  \"socialising\": \"socializing\",\n  \"sodomise\": \"sodomize\",\n  \"sodomised\": \"sodomized\",\n  \"sodomises\": \"sodomizes\",\n  \"sodomising\": \"sodomizing\",\n  \"solemnise\": \"solemnize\",\n  \"solemnised\": \"solemnized\",\n  \"solemnises\": \"solemnizes\",\n  \"solemnising\": \"solemnizing\",\n  \"sombre\": \"somber\",\n  \"specialisation\": \"specialization\",\n  \"specialisations\": \"specializations\",\n  \"specialise\": \"specialize\",\n  \"specialised\": \"specialized\",\n  \"specialises\": \"specializes\",\n  \"specialising\": \"specializing\",\n  \"spectre\": \"specter\",\n  \"spectres\": \"specters\",\n  \"spiralled\": \"spiraled\",\n  \"spiralling\": \"spiraling\",\n  \"splendour\": \"splendor\",\n  \"splendours\": \"splendors\",\n  \"squirrelled\": \"squirreled\",\n  \"squirrelling\": \"squirreling\",\n  \"stabilisation\": \"stabilization\",\n  \"stabilise\": \"stabilize\",\n  \"stabilised\": \"stabilized\",\n  \"stabiliser\": \"stabilizer\",\n  \"stabilisers\": \"stabilizers\",\n  \"stabilises\": \"stabilizes\",\n  \"stabilising\": \"stabilizing\",\n  \"standardisation\": \"standardization\",\n  \"standardise\": \"standardize\",\n  \"standardised\": \"standardized\",\n  \"standardises\": \"standardizes\",\n  \"standardising\": \"standardizing\",\n  \"stencilled\": \"stenciled\",\n  \"stencilling\": \"stenciling\",\n  \"sterilisation\": \"sterilization\",\n  \"sterilisations\": \"sterilizations\",\n  \"sterilise\": \"sterilize\",\n  \"sterilised\": \"sterilized\",\n  \"steriliser\": \"sterilizer\",\n  \"sterilisers\": \"sterilizers\",\n  \"sterilises\": \"sterilizes\",\n  \"sterilising\": \"sterilizing\",\n  \"stigmatisation\": \"stigmatization\",\n  \"stigmatise\": \"stigmatize\",\n  \"stigmatised\": \"stigmatized\",\n  \"stigmatises\": \"stigmatizes\",\n  \"stigmatising\": \"stigmatizing\",\n  \"storey\": \"story\",\n  \"storeys\": \"stories\",\n  \"subsidisation\": \"subsidization\",\n  \"subsidise\": \"subsidize\",\n  \"subsidised\": \"subsidized\",\n  \"subsidiser\": \"subsidizer\",\n  \"subsidisers\": \"subsidizers\",\n  \"subsidises\": \"subsidizes\",\n  \"subsidising\": \"subsidizing\",\n  \"succour\": \"succor\",\n  \"succoured\": \"succored\",\n  \"succouring\": \"succoring\",\n  \"succours\": \"succors\",\n  \"sulphate\": \"sulfate\",\n  \"sulphates\": \"sulfates\",\n  \"sulphide\": \"sulfide\",\n  \"sulphides\": \"sulfides\",\n  \"sulphur\": \"sulfur\",\n  \"sulphurous\": \"sulfurous\",\n  \"summarise\": \"summarize\",\n  \"summarised\": \"summarized\",\n  \"summarises\": \"summarizes\",\n  \"summarising\": \"summarizing\",\n  \"swivelled\": \"swiveled\",\n  \"swivelling\": \"swiveling\",\n  \"symbolise\": \"symbolize\",\n  \"symbolised\": \"symbolized\",\n  \"symbolises\": \"symbolizes\",\n  \"symbolising\": \"symbolizing\",\n  \"sympathise\": \"sympathize\",\n  \"sympathised\": \"sympathized\",\n  \"sympathiser\": \"sympathizer\",\n  \"sympathisers\": \"sympathizers\",\n  \"sympathises\": \"sympathizes\",\n  \"sympathising\": \"sympathizing\",\n  \"synchronisation\": \"synchronization\",\n  \"synchronise\": \"synchronize\",\n  \"synchronised\": \"synchronized\",\n  \"synchronises\": \"synchronizes\",\n  \"synchronising\": \"synchronizing\",\n  \"synthesise\": \"synthesize\",\n  \"synthesised\": \"synthesized\",\n  \"synthesiser\": \"synthesizer\",\n  \"synthesisers\": \"synthesizers\",\n  \"synthesises\": \"synthesizes\",\n  \"synthesising\": \"synthesizing\",\n  \"syphon\": \"siphon\",\n  \"syphoned\": \"siphoned\",\n  \"syphoning\": \"siphoning\",\n  \"syphons\": \"siphons\",\n  \"systematisation\": \"systematization\",\n  \"systematise\": \"systematize\",\n  \"systematised\": \"systematized\",\n  \"systematises\": \"systematizes\",\n  \"systematising\": \"systematizing\",\n  \"tantalise\": \"tantalize\",\n  \"tantalised\": \"tantalized\",\n  \"tantalises\": \"tantalizes\",\n  \"tantalising\": \"tantalizing\",\n  \"tantalisingly\": \"tantalizingly\",\n  \"tasselled\": \"tasseled\",\n  \"technicolour\": \"technicolor\",\n  \"temporise\": \"temporize\",\n  \"temporised\": \"temporized\",\n  \"temporises\": \"temporizes\",\n  \"temporising\": \"temporizing\",\n  \"tenderise\": \"tenderize\",\n  \"tenderised\": \"tenderized\",\n  \"tenderises\": \"tenderizes\",\n  \"tenderising\": \"tenderizing\",\n  \"terrorise\": \"terrorize\",\n  \"terrorised\": \"terrorized\",\n  \"terrorises\": \"terrorizes\",\n  \"terrorising\": \"terrorizing\",\n  \"theatre\": \"theater\",\n  \"theatregoer\": \"theatergoer\",\n  \"theatregoers\": \"theatergoers\",\n  \"theatres\": \"theaters\",\n  \"theorise\": \"theorize\",\n  \"theorised\": \"theorized\",\n  \"theorises\": \"theorizes\",\n  \"theorising\": \"theorizing\",\n  \"tonne\": \"ton\",\n  \"tonnes\": \"tons\",\n  \"towelled\": \"toweled\",\n  \"towelling\": \"toweling\",\n  \"toxaemia\": \"toxemia\",\n  \"tranquillise\": \"tranquilize\",\n  \"tranquillised\": \"tranquilized\",\n  \"tranquilliser\": \"tranquilizer\",\n  \"tranquillisers\": \"tranquilizers\",\n  \"tranquillises\": \"tranquilizes\",\n  \"tranquillising\": \"tranquilizing\",\n  \"tranquillity\": \"tranquility\",\n  \"tranquillize\": \"tranquilize\",\n  \"tranquillized\": \"tranquilized\",\n  \"tranquillizer\": \"tranquilizer\",\n  \"tranquillizers\": \"tranquilizers\",\n  \"tranquillizes\": \"tranquilizes\",\n  \"tranquillizing\": \"tranquilizing\",\n  \"tranquilly\": \"tranquility\",\n  \"transistorised\": \"transistorized\",\n  \"traumatise\": \"traumatize\",\n  \"traumatised\": \"traumatized\",\n  \"traumatises\": \"traumatizes\",\n  \"traumatising\": \"traumatizing\",\n  \"travelled\": \"traveled\",\n  \"traveller\": \"traveler\",\n  \"travellers\": \"travelers\",\n  \"travelling\": \"traveling\",\n  \"travelog\": \"travelogue\",\n  \"travelogs\": \"travelogues\",\n  \"trialled\": \"trialed\",\n  \"trialling\": \"trialing\",\n  \"tricolour\": \"tricolor\",\n  \"tricolours\": \"tricolors\",\n  \"trivialise\": \"trivialize\",\n  \"trivialised\": \"trivialized\",\n  \"trivialises\": \"trivializes\",\n  \"trivialising\": \"trivializing\",\n  \"tumour\": \"tumor\",\n  \"tumours\": \"tumors\",\n  \"tunnelled\": \"tunneled\",\n  \"tunnelling\": \"tunneling\",\n  \"tyrannise\": \"tyrannize\",\n  \"tyrannised\": \"tyrannized\",\n  \"tyrannises\": \"tyrannizes\",\n  \"tyrannising\": \"tyrannizing\",\n  \"tyre\": \"tire\",\n  \"tyres\": \"tires\",\n  \"unauthorised\": \"unauthorized\",\n  \"uncivilised\": \"uncivilized\",\n  \"underutilised\": \"underutilized\",\n  \"unequalled\": \"unequaled\",\n  \"unfavourable\": \"unfavorable\",\n  \"unfavourably\": \"unfavorably\",\n  \"unionisation\": \"unionization\",\n  \"unionise\": \"unionize\",\n  \"unionised\": \"unionized\",\n  \"unionises\": \"unionizes\",\n  \"unionising\": \"unionizing\",\n  \"unorganised\": \"unorganized\",\n  \"unravelled\": \"unraveled\",\n  \"unravelling\": \"unraveling\",\n  \"unrecognisable\": \"unrecognizable\",\n  \"unrecognised\": \"unrecognized\",\n  \"unrivalled\": \"unrivaled\",\n  \"unsavoury\": \"unsavory\",\n  \"untrammelled\": \"untrammeled\",\n  \"urbanisation\": \"urbanization\",\n  \"urbanise\": \"urbanize\",\n  \"urbanised\": \"urbanized\",\n  \"urbanises\": \"urbanizes\",\n  \"urbanising\": \"urbanizing\",\n  \"utilisable\": \"utilizable\",\n  \"utilisation\": \"utilization\",\n  \"utilise\": \"utilize\",\n  \"utilised\": \"utilized\",\n  \"utilises\": \"utilizes\",\n  \"utilising\": \"utilizing\",\n  \"valour\": \"valor\",\n  \"vandalise\": \"vandalize\",\n  \"vandalised\": \"vandalized\",\n  \"vandalises\": \"vandalizes\",\n  \"vandalising\": \"vandalizing\",\n  \"vaporisation\": \"vaporization\",\n  \"vaporise\": \"vaporize\",\n  \"vaporised\": \"vaporized\",\n  \"vaporises\": \"vaporizes\",\n  \"vaporising\": \"vaporizing\",\n  \"vapour\": \"vapor\",\n  \"vapours\": \"vapors\",\n  \"verbalise\": \"verbalize\",\n  \"verbalised\": \"verbalized\",\n  \"verbalises\": \"verbalizes\",\n  \"verbalising\": \"verbalizing\",\n  \"victimisation\": \"victimization\",\n  \"victimise\": \"victimize\",\n  \"victimised\": \"victimized\",\n  \"victimises\": \"victimizes\",\n  \"victimising\": \"victimizing\",\n  \"videodisc\": \"videodisk\",\n  \"videodiscs\": \"videodisks\",\n  \"vigour\": \"vigor\",\n  \"visualisation\": \"visualization\",\n  \"visualisations\": \"visualizations\",\n  \"visualise\": \"visualize\",\n  \"visualised\": \"visualized\",\n  \"visualises\": \"visualizes\",\n  \"visualising\": \"visualizing\",\n  \"vocalisation\": \"vocalization\",\n  \"vocalisations\": \"vocalizations\",\n  \"vocalise\": \"vocalize\",\n  \"vocalised\": \"vocalized\",\n  \"vocalises\": \"vocalizes\",\n  \"vocalising\": \"vocalizing\",\n  \"vulcanised\": \"vulcanized\",\n  \"vulgarisation\": \"vulgarization\",\n  \"vulgarise\": \"vulgarize\",\n  \"vulgarised\": \"vulgarized\",\n  \"vulgarises\": \"vulgarizes\",\n  \"vulgarising\": \"vulgarizing\",\n  \"waggon\": \"wagon\",\n  \"waggons\": \"wagons\",\n  \"watercolour\": \"watercolor\",\n  \"watercolours\": \"watercolors\",\n  \"weaselled\": \"weaseled\",\n  \"weaselling\": \"weaseling\",\n  \"westernisation\": \"westernization\",\n  \"westernise\": \"westernize\",\n  \"westernised\": \"westernized\",\n  \"westernises\": \"westernizes\",\n  \"westernising\": \"westernizing\",\n  \"womanise\": \"womanize\",\n  \"womanised\": \"womanized\",\n  \"womaniser\": \"womanizer\",\n  \"womanisers\": \"womanizers\",\n  \"womanises\": \"womanizes\",\n  \"womanising\": \"womanizing\",\n  \"woollen\": \"woolen\",\n  \"woollens\": \"woolens\",\n  \"woollies\": \"woolies\",\n  \"woolly\": \"wooly\",\n  \"worshipped\": \"worshiped\",\n  \"worshipper\": \"worshiper\",\n  \"worshipping\": \"worshiping\",\n  \"yodelled\": \"yodeled\",\n  \"yodelling\": \"yodeling\",\n  \"yoghourt\": \"yogurt\",\n  \"yoghourts\": \"yogurts\",\n  \"yoghurt\": \"yogurt\",\n  \"yoghurts\": \"yogurts\"\n}\n"
  },
  {
    "path": "benchmark/requirements.benchmark.txt",
    "content": "transformers\njiwer\ndatasets\nmemory_profiler\npy3nvml\npytubefix\n"
  },
  {
    "path": "benchmark/speed_benchmark.py",
    "content": "import argparse\nimport timeit\n\nfrom typing import Callable\n\nfrom utils import inference\n\nparser = argparse.ArgumentParser(description=\"Speed benchmark\")\nparser.add_argument(\n    \"--repeat\",\n    type=int,\n    default=3,\n    help=\"Times an experiment will be run.\",\n)\nargs = parser.parse_args()\n\n\ndef measure_speed(func: Callable[[], None]):\n    # as written in https://docs.python.org/3/library/timeit.html#timeit.Timer.repeat,\n    # min should be taken rather than the average\n    runtimes = timeit.repeat(\n        func,\n        repeat=args.repeat,\n        number=10,\n    )\n    print(runtimes)\n    print(\"Min execution time: %.3fs\" % (min(runtimes) / 10.0))\n\n\nif __name__ == \"__main__\":\n    measure_speed(inference)\n"
  },
  {
    "path": "benchmark/utils.py",
    "content": "import logging\n\nfrom threading import Thread\nfrom typing import Optional\n\nfrom faster_whisper import WhisperModel\n\nmodel_path = \"large-v3\"\nmodel = WhisperModel(model_path, device=\"cuda\")\n\n\ndef inference():\n    segments, info = model.transcribe(\"benchmark.m4a\", language=\"fr\")\n    for segment in segments:\n        print(\"[%.2fs -> %.2fs] %s\" % (segment.start, segment.end, segment.text))\n\n\ndef get_logger(name: Optional[str] = None) -> logging.Logger:\n    formatter = logging.Formatter(\"%(levelname)s: %(message)s\")\n    logger = logging.getLogger(name)\n    logger.setLevel(logging.DEBUG)\n    handler = logging.StreamHandler()\n    handler.setFormatter(formatter)\n    logger.addHandler(handler)\n    return logger\n\n\nclass MyThread(Thread):\n    def __init__(self, func, params):\n        super(MyThread, self).__init__()\n        self.func = func\n        self.params = params\n        self.result = None\n\n    def run(self):\n        self.result = self.func(*self.params)\n\n    def get_result(self):\n        return self.result\n"
  },
  {
    "path": "benchmark/wer_benchmark.py",
    "content": "import argparse\nimport json\nimport os\n\nfrom datasets import load_dataset\nfrom jiwer import wer\nfrom tqdm import tqdm\nfrom transformers.models.whisper.english_normalizer import EnglishTextNormalizer\n\nfrom faster_whisper import WhisperModel\n\nparser = argparse.ArgumentParser(description=\"WER benchmark\")\nparser.add_argument(\n    \"--audio_numb\",\n    type=int,\n    default=None,\n    help=\"Specify the number of validation audio files in the dataset.\"\n    \" Set to None to retrieve all audio files.\",\n)\nargs = parser.parse_args()\n\nmodel_path = \"large-v3\"\nmodel = WhisperModel(model_path, device=\"cuda\")\n\n# load the dataset with streaming mode\ndataset = load_dataset(\"librispeech_asr\", \"clean\", split=\"validation\", streaming=True)\n\nwith open(os.path.join(os.path.dirname(__file__), \"normalizer.json\"), \"r\") as f:\n    normalizer = EnglishTextNormalizer(json.load(f))\n\n\ndef inference(batch):\n    batch[\"transcription\"] = []\n    for sample in batch[\"audio\"]:\n        segments, info = model.transcribe(sample[\"array\"], language=\"en\")\n        batch[\"transcription\"].append(\"\".join([segment.text for segment in segments]))\n    batch[\"reference\"] = batch[\"text\"]\n    return batch\n\n\ndataset = dataset.map(function=inference, batched=True, batch_size=16)\n\nall_transcriptions = []\nall_references = []\n\n# iterate over the dataset and run inference\nfor i, result in tqdm(enumerate(dataset), desc=\"Evaluating...\"):\n    all_transcriptions.append(result[\"transcription\"])\n    all_references.append(result[\"reference\"])\n    if args.audio_numb and i == (args.audio_numb - 1):\n        break\n\n# normalize predictions and references\nall_transcriptions = [normalizer(transcription) for transcription in all_transcriptions]\nall_references = [normalizer(reference) for reference in all_references]\n\n# compute the WER metric\nword_error_rate = 100 * wer(hypothesis=all_transcriptions, reference=all_references)\nprint(\"WER: %.3f\" % word_error_rate)\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04\nWORKDIR /root\nRUN apt-get update -y && apt-get install -y python3-pip\nCOPY infer.py jfk.flac ./\nRUN pip3 install faster-whisper\nCMD [\"python3\", \"infer.py\"]\n"
  },
  {
    "path": "docker/infer.py",
    "content": "from faster_whisper import WhisperModel\n\njfk_path = \"jfk.flac\"\nmodel = WhisperModel(\"tiny\", device=\"cuda\")\nsegments, info = model.transcribe(jfk_path, word_timestamps=True)\nfor segment in segments:\n    print(\"[%.2fs -> %.2fs] %s\" % (segment.start, segment.end, segment.text))\n"
  },
  {
    "path": "faster_whisper/__init__.py",
    "content": "from faster_whisper.audio import decode_audio\nfrom faster_whisper.transcribe import BatchedInferencePipeline, WhisperModel\nfrom faster_whisper.utils import available_models, download_model, format_timestamp\nfrom faster_whisper.version import __version__\n\n__all__ = [\n    \"available_models\",\n    \"decode_audio\",\n    \"WhisperModel\",\n    \"BatchedInferencePipeline\",\n    \"download_model\",\n    \"format_timestamp\",\n    \"__version__\",\n]\n"
  },
  {
    "path": "faster_whisper/assets/__init__.py",
    "content": ""
  },
  {
    "path": "faster_whisper/audio.py",
    "content": "\"\"\"We use the PyAV library to decode the audio: https://github.com/PyAV-Org/PyAV\n\nThe advantage of PyAV is that it bundles the FFmpeg libraries so there is no additional\nsystem dependencies. FFmpeg does not need to be installed on the system.\n\nHowever, the API is quite low-level so we need to manipulate audio frames directly.\n\"\"\"\n\nimport gc\nimport io\nimport itertools\n\nfrom typing import BinaryIO, Union\n\nimport av\nimport numpy as np\n\n\ndef decode_audio(\n    input_file: Union[str, BinaryIO],\n    sampling_rate: int = 16000,\n    split_stereo: bool = False,\n):\n    \"\"\"Decodes the audio.\n\n    Args:\n      input_file: Path to the input file or a file-like object.\n      sampling_rate: Resample the audio to this sample rate.\n      split_stereo: Return separate left and right channels.\n\n    Returns:\n      A float32 Numpy array.\n\n      If `split_stereo` is enabled, the function returns a 2-tuple with the\n      separated left and right channels.\n    \"\"\"\n    resampler = av.audio.resampler.AudioResampler(\n        format=\"s16\",\n        layout=\"mono\" if not split_stereo else \"stereo\",\n        rate=sampling_rate,\n    )\n\n    raw_buffer = io.BytesIO()\n    dtype = None\n\n    with av.open(input_file, mode=\"r\", metadata_errors=\"ignore\") as container:\n        frames = container.decode(audio=0)\n        frames = _ignore_invalid_frames(frames)\n        frames = _group_frames(frames, 500000)\n        frames = _resample_frames(frames, resampler)\n\n        for frame in frames:\n            array = frame.to_ndarray()\n            dtype = array.dtype\n            raw_buffer.write(array)\n\n    # It appears that some objects related to the resampler are not freed\n    # unless the garbage collector is manually run.\n    # https://github.com/SYSTRAN/faster-whisper/issues/390\n    # note that this slows down loading the audio a little bit\n    # if that is a concern, please use ffmpeg directly as in here:\n    # https://github.com/openai/whisper/blob/25639fc/whisper/audio.py#L25-L62\n    del resampler\n    gc.collect()\n\n    audio = np.frombuffer(raw_buffer.getbuffer(), dtype=dtype)\n\n    # Convert s16 back to f32.\n    audio = audio.astype(np.float32) / 32768.0\n\n    if split_stereo:\n        left_channel = audio[0::2]\n        right_channel = audio[1::2]\n        return left_channel, right_channel\n\n    return audio\n\n\ndef _ignore_invalid_frames(frames):\n    iterator = iter(frames)\n\n    while True:\n        try:\n            yield next(iterator)\n        except StopIteration:\n            break\n        except av.error.InvalidDataError:\n            continue\n\n\ndef _group_frames(frames, num_samples=None):\n    fifo = av.audio.fifo.AudioFifo()\n\n    for frame in frames:\n        frame.pts = None  # Ignore timestamp check.\n        fifo.write(frame)\n\n        if num_samples is not None and fifo.samples >= num_samples:\n            yield fifo.read()\n\n    if fifo.samples > 0:\n        yield fifo.read()\n\n\ndef _resample_frames(frames, resampler):\n    # Add None to flush the resampler.\n    for frame in itertools.chain(frames, [None]):\n        yield from resampler.resample(frame)\n\n\ndef pad_or_trim(array, length: int = 3000, *, axis: int = -1):\n    \"\"\"\n    Pad or trim the Mel features array to 3000, as expected by the encoder.\n    \"\"\"\n    if array.shape[axis] > length:\n        array = array.take(indices=range(length), axis=axis)\n\n    if array.shape[axis] < length:\n        pad_widths = [(0, 0)] * array.ndim\n        pad_widths[axis] = (0, length - array.shape[axis])\n        array = np.pad(array, pad_widths)\n\n    return array\n"
  },
  {
    "path": "faster_whisper/feature_extractor.py",
    "content": "import numpy as np\n\n\nclass FeatureExtractor:\n    def __init__(\n        self,\n        feature_size=80,\n        sampling_rate=16000,\n        hop_length=160,\n        chunk_length=30,\n        n_fft=400,\n    ):\n        self.n_fft = n_fft\n        self.hop_length = hop_length\n        self.chunk_length = chunk_length\n        self.n_samples = chunk_length * sampling_rate\n        self.nb_max_frames = self.n_samples // hop_length\n        self.time_per_frame = hop_length / sampling_rate\n        self.sampling_rate = sampling_rate\n        self.mel_filters = self.get_mel_filters(\n            sampling_rate, n_fft, n_mels=feature_size\n        ).astype(\"float32\")\n\n    @staticmethod\n    def get_mel_filters(sr, n_fft, n_mels=128):\n        # Initialize the weights\n        n_mels = int(n_mels)\n\n        # Center freqs of each FFT bin\n        fftfreqs = np.fft.rfftfreq(n=n_fft, d=1.0 / sr)\n\n        # 'Center freqs' of mel bands - uniformly spaced between limits\n        min_mel = 0.0\n        max_mel = 45.245640471924965\n\n        mels = np.linspace(min_mel, max_mel, n_mels + 2)\n\n        # Fill in the linear scale\n        f_min = 0.0\n        f_sp = 200.0 / 3\n        freqs = f_min + f_sp * mels\n\n        # And now the nonlinear scale\n        min_log_hz = 1000.0  # beginning of log region (Hz)\n        min_log_mel = (min_log_hz - f_min) / f_sp  # same (Mels)\n        logstep = np.log(6.4) / 27.0  # step size for log region\n\n        # If we have vector data, vectorize\n        log_t = mels >= min_log_mel\n        freqs[log_t] = min_log_hz * np.exp(logstep * (mels[log_t] - min_log_mel))\n\n        fdiff = np.diff(freqs)\n        ramps = freqs.reshape(-1, 1) - fftfreqs.reshape(1, -1)\n\n        lower = -ramps[:-2] / np.expand_dims(fdiff[:-1], axis=1)\n        upper = ramps[2:] / np.expand_dims(fdiff[1:], axis=1)\n\n        # Intersect them with each other and zero, vectorized across all i\n        weights = np.maximum(np.zeros_like(lower), np.minimum(lower, upper))\n\n        # Slaney-style mel is scaled to be approx constant energy per channel\n        enorm = 2.0 / (freqs[2 : n_mels + 2] - freqs[:n_mels])\n        weights *= np.expand_dims(enorm, axis=1)\n\n        return weights\n\n    @staticmethod\n    def stft(\n        input_array: np.ndarray,\n        n_fft: int,\n        hop_length: int = None,\n        win_length: int = None,\n        window: np.ndarray = None,\n        center: bool = True,\n        mode: str = \"reflect\",\n        normalized: bool = False,\n        onesided: bool = None,\n        return_complex: bool = None,\n    ):\n        # Default initialization for hop_length and win_length\n        hop_length = hop_length if hop_length is not None else n_fft // 4\n        win_length = win_length if win_length is not None else n_fft\n        input_is_complex = np.iscomplexobj(input_array)\n\n        # Determine if the output should be complex\n        return_complex = (\n            return_complex\n            if return_complex is not None\n            else (input_is_complex or (window is not None and np.iscomplexobj(window)))\n        )\n\n        if not return_complex and return_complex is None:\n            raise ValueError(\n                \"stft requires the return_complex parameter for real inputs.\"\n            )\n\n        # Input checks\n        if not np.issubdtype(input_array.dtype, np.floating) and not input_is_complex:\n            raise ValueError(\n                \"stft: expected an array of floating point or complex values,\"\n                f\" got {input_array.dtype}\"\n            )\n\n        if input_array.ndim > 2 or input_array.ndim < 1:\n            raise ValueError(\n                f\"stft: expected a 1D or 2D array, but got {input_array.ndim}D array\"\n            )\n\n        # Handle 1D input\n        if input_array.ndim == 1:\n            input_array = np.expand_dims(input_array, axis=0)\n            input_array_1d = True\n        else:\n            input_array_1d = False\n\n        # Center padding if required\n        if center:\n            pad_amount = n_fft // 2\n            input_array = np.pad(\n                input_array, ((0, 0), (pad_amount, pad_amount)), mode=mode\n            )\n\n        batch, length = input_array.shape\n\n        # Additional input checks\n        if n_fft <= 0 or n_fft > length:\n            raise ValueError(\n                f\"stft: expected 0 < n_fft <= {length}, but got n_fft={n_fft}\"\n            )\n\n        if hop_length <= 0:\n            raise ValueError(\n                f\"stft: expected hop_length > 0, but got hop_length={hop_length}\"\n            )\n\n        if win_length <= 0 or win_length > n_fft:\n            raise ValueError(\n                f\"stft: expected 0 < win_length <= n_fft, but got win_length={win_length}\"\n            )\n\n        if window is not None:\n            if window.ndim != 1 or window.shape[0] != win_length:\n                raise ValueError(\n                    f\"stft: expected a 1D window array of size equal to win_length={win_length}, \"\n                    f\"but got window with size {window.shape}\"\n                )\n\n        # Handle padding of the window if necessary\n        if win_length < n_fft:\n            left = (n_fft - win_length) // 2\n            window_ = np.zeros(n_fft, dtype=window.dtype)\n            window_[left : left + win_length] = window\n        else:\n            window_ = window\n\n        # Calculate the number of frames\n        n_frames = 1 + (length - n_fft) // hop_length\n\n        # Time to columns\n        input_array = np.lib.stride_tricks.as_strided(\n            input_array,\n            (batch, n_frames, n_fft),\n            (\n                input_array.strides[0],\n                hop_length * input_array.strides[1],\n                input_array.strides[1],\n            ),\n        )\n\n        if window_ is not None:\n            input_array = input_array * window_\n\n        # FFT and transpose\n        complex_fft = input_is_complex\n        onesided = onesided if onesided is not None else not complex_fft\n\n        if normalized:\n            norm = \"ortho\"\n        else:\n            norm = None\n\n        if complex_fft:\n            if onesided:\n                raise ValueError(\n                    \"Cannot have onesided output if window or input is complex\"\n                )\n            output = np.fft.fft(input_array, n=n_fft, axis=-1, norm=norm)\n        else:\n            output = np.fft.rfft(input_array, n=n_fft, axis=-1, norm=norm)\n\n        output = output.transpose((0, 2, 1))\n\n        if input_array_1d:\n            output = output.squeeze(0)\n\n        return output if return_complex else np.real(output)\n\n    def __call__(self, waveform: np.ndarray, padding=160, chunk_length=None):\n        \"\"\"\n        Compute the log-Mel spectrogram of the provided audio.\n        \"\"\"\n\n        if chunk_length is not None:\n            self.n_samples = chunk_length * self.sampling_rate\n            self.nb_max_frames = self.n_samples // self.hop_length\n\n        if waveform.dtype is not np.float32:\n            waveform = waveform.astype(np.float32)\n\n        if padding:\n            waveform = np.pad(waveform, (0, padding))\n\n        window = np.hanning(self.n_fft + 1)[:-1].astype(\"float32\")\n\n        stft = self.stft(\n            waveform,\n            self.n_fft,\n            self.hop_length,\n            window=window,\n            return_complex=True,\n        ).astype(\"complex64\")\n        magnitudes = np.abs(stft[..., :-1]) ** 2\n\n        mel_spec = self.mel_filters @ magnitudes\n\n        log_spec = np.log10(np.clip(mel_spec, a_min=1e-10, a_max=None))\n        log_spec = np.maximum(log_spec, log_spec.max() - 8.0)\n        log_spec = (log_spec + 4.0) / 4.0\n\n        return log_spec\n"
  },
  {
    "path": "faster_whisper/tokenizer.py",
    "content": "import string\n\nfrom functools import cached_property\nfrom typing import List, Optional, Tuple\n\nimport tokenizers\n\n\nclass Tokenizer:\n    \"\"\"Simple wrapper around a tokenizers.Tokenizer.\"\"\"\n\n    def __init__(\n        self,\n        tokenizer: tokenizers.Tokenizer,\n        multilingual: bool,\n        task: Optional[str] = None,\n        language: Optional[str] = None,\n    ):\n        self.tokenizer = tokenizer\n\n        if multilingual:\n            if task not in _TASKS:\n                raise ValueError(\n                    \"'%s' is not a valid task (accepted tasks: %s)\"\n                    % (task, \", \".join(_TASKS))\n                )\n\n            if language not in _LANGUAGE_CODES:\n                raise ValueError(\n                    \"'%s' is not a valid language code (accepted language codes: %s)\"\n                    % (language, \", \".join(_LANGUAGE_CODES))\n                )\n\n            self.task = self.tokenizer.token_to_id(\"<|%s|>\" % task)\n            self.language = self.tokenizer.token_to_id(\"<|%s|>\" % language)\n            self.language_code = language\n        else:\n            self.task = None\n            self.language = None\n            self.language_code = \"en\"\n\n    @cached_property\n    def transcribe(self) -> int:\n        return self.tokenizer.token_to_id(\"<|transcribe|>\")\n\n    @cached_property\n    def translate(self) -> int:\n        return self.tokenizer.token_to_id(\"<|translate|>\")\n\n    @cached_property\n    def sot(self) -> int:\n        return self.tokenizer.token_to_id(\"<|startoftranscript|>\")\n\n    @cached_property\n    def sot_lm(self) -> int:\n        return self.tokenizer.token_to_id(\"<|startoflm|>\")\n\n    @cached_property\n    def sot_prev(self) -> int:\n        return self.tokenizer.token_to_id(\"<|startofprev|>\")\n\n    @cached_property\n    def eot(self) -> int:\n        return self.tokenizer.token_to_id(\"<|endoftext|>\")\n\n    @cached_property\n    def no_timestamps(self) -> int:\n        return self.tokenizer.token_to_id(\"<|notimestamps|>\")\n\n    @cached_property\n    def no_speech(self) -> int:\n        return self.tokenizer.token_to_id(\"<|nospeech|>\") or self.tokenizer.token_to_id(\n            \"<|nocaptions|>\"\n        )\n\n    @property\n    def timestamp_begin(self) -> int:\n        return self.no_timestamps + 1\n\n    @property\n    def sot_sequence(self) -> List[int]:\n        sequence = [self.sot]\n\n        if self.language is not None:\n            sequence.append(self.language)\n\n        if self.task is not None:\n            sequence.append(self.task)\n\n        return sequence\n\n    def encode(self, text: str) -> List[int]:\n        return self.tokenizer.encode(text, add_special_tokens=False).ids\n\n    def decode(self, tokens: List[int]) -> str:\n        text_tokens = [token for token in tokens if token < self.eot]\n        return self.tokenizer.decode(text_tokens)\n\n    def decode_with_timestamps(self, tokens: List[int]) -> str:\n        outputs = [[]]\n\n        for token in tokens:\n            if token >= self.timestamp_begin:\n                timestamp = f\"<|{(token - self.timestamp_begin) * 0.02:.2f}|>\"\n                outputs.append(timestamp)\n                outputs.append([])\n            else:\n                outputs[-1].append(token)\n\n        return \"\".join(\n            [s if isinstance(s, str) else self.tokenizer.decode(s) for s in outputs]\n        )\n\n    @cached_property\n    def non_speech_tokens(self) -> Tuple[int]:\n        \"\"\"\n        Returns the list of tokens to suppress in order to avoid any speaker tags or non-speech\n        annotations, to prevent sampling texts that are not actually spoken in the audio, e.g.\n\n        - ♪♪♪\n        - ( SPEAKING FOREIGN LANGUAGE )\n        - [DAVID] Hey there,\n\n        keeping basic punctuations like commas, periods, question marks, exclamation points, etc.\n        \"\"\"\n        symbols = list('\"#()*+/:;<=>@[\\\\]^_`{|}~「」『』')\n        symbols += (\n            \"<< >> <<< >>> -- --- -( -[ (' (\\\" (( )) ((( ))) [[ ]] {{ }} ♪♪ ♪♪♪\".split()\n        )\n\n        # symbols that may be a single token or multiple tokens depending on the tokenizer.\n        # In case they're multiple tokens, suppress the first token, which is safe because:\n        # These are between U+2640 and U+267F miscellaneous symbols that are okay to suppress\n        # in generations, and in the 3-byte UTF-8 representation they share the first two bytes.\n        miscellaneous = set(\"♩♪♫♬♭♮♯\")\n        assert all(0x2640 <= ord(c) <= 0x267F for c in miscellaneous)\n\n        # allow hyphens \"-\" and single quotes \"'\" between words, but not at the beginning of a word\n        result = {self.encode(\" -\")[0], self.encode(\" '\")[0]}\n        for symbol in symbols + list(miscellaneous):\n            for tokens in [\n                self.encode(symbol),\n                self.encode(\" \" + symbol),\n            ]:\n                if len(tokens) == 1 or symbol in miscellaneous:\n                    result.add(tokens[0])\n\n        return tuple(sorted(result))\n\n    def split_to_word_tokens(\n        self, tokens: List[int]\n    ) -> Tuple[List[str], List[List[int]]]:\n        if self.language_code in {\"zh\", \"ja\", \"th\", \"lo\", \"my\", \"yue\"}:\n            # These languages don't typically use spaces, so it is difficult to split words\n            # without morpheme analysis. Here, we instead split words at any\n            # position where the tokens are decoded as valid unicode points\n            return self.split_tokens_on_unicode(tokens)\n\n        return self.split_tokens_on_spaces(tokens)\n\n    def split_tokens_on_unicode(\n        self, tokens: List[int]\n    ) -> Tuple[List[str], List[List[int]]]:\n        decoded_full = self.decode_with_timestamps(tokens)\n        replacement_char = \"\\ufffd\"\n\n        words = []\n        word_tokens = []\n        current_tokens = []\n        unicode_offset = 0\n\n        for token in tokens:\n            current_tokens.append(token)\n            decoded = self.decode_with_timestamps(current_tokens)\n\n            try:\n                replacement_char_index = decoded.index(replacement_char)\n                replacement_char_index += unicode_offset\n            except ValueError:\n                replacement_char_index = None\n\n            if replacement_char_index is None or (\n                replacement_char_index < len(decoded_full)\n                and decoded_full[replacement_char_index] == replacement_char\n            ):\n                words.append(decoded)\n                word_tokens.append(current_tokens)\n                current_tokens = []\n                unicode_offset += len(decoded)\n\n        return words, word_tokens\n\n    def split_tokens_on_spaces(\n        self, tokens: List[int]\n    ) -> Tuple[List[str], List[List[int]]]:\n        subwords, subword_tokens_list = self.split_tokens_on_unicode(tokens)\n        words = []\n        word_tokens = []\n\n        for subword, subword_tokens in zip(subwords, subword_tokens_list):\n            special = subword_tokens[0] >= self.eot\n            with_space = subword.startswith(\" \")\n            punctuation = subword.strip() in string.punctuation\n            if special or with_space or punctuation or len(words) == 0:\n                words.append(subword)\n                word_tokens.append(subword_tokens)\n            else:\n                words[-1] = words[-1] + subword\n                word_tokens[-1].extend(subword_tokens)\n\n        return words, word_tokens\n\n\n_TASKS = (\n    \"transcribe\",\n    \"translate\",\n)\n\n_LANGUAGE_CODES = (\n    \"af\",\n    \"am\",\n    \"ar\",\n    \"as\",\n    \"az\",\n    \"ba\",\n    \"be\",\n    \"bg\",\n    \"bn\",\n    \"bo\",\n    \"br\",\n    \"bs\",\n    \"ca\",\n    \"cs\",\n    \"cy\",\n    \"da\",\n    \"de\",\n    \"el\",\n    \"en\",\n    \"es\",\n    \"et\",\n    \"eu\",\n    \"fa\",\n    \"fi\",\n    \"fo\",\n    \"fr\",\n    \"gl\",\n    \"gu\",\n    \"ha\",\n    \"haw\",\n    \"he\",\n    \"hi\",\n    \"hr\",\n    \"ht\",\n    \"hu\",\n    \"hy\",\n    \"id\",\n    \"is\",\n    \"it\",\n    \"ja\",\n    \"jw\",\n    \"ka\",\n    \"kk\",\n    \"km\",\n    \"kn\",\n    \"ko\",\n    \"la\",\n    \"lb\",\n    \"ln\",\n    \"lo\",\n    \"lt\",\n    \"lv\",\n    \"mg\",\n    \"mi\",\n    \"mk\",\n    \"ml\",\n    \"mn\",\n    \"mr\",\n    \"ms\",\n    \"mt\",\n    \"my\",\n    \"ne\",\n    \"nl\",\n    \"nn\",\n    \"no\",\n    \"oc\",\n    \"pa\",\n    \"pl\",\n    \"ps\",\n    \"pt\",\n    \"ro\",\n    \"ru\",\n    \"sa\",\n    \"sd\",\n    \"si\",\n    \"sk\",\n    \"sl\",\n    \"sn\",\n    \"so\",\n    \"sq\",\n    \"sr\",\n    \"su\",\n    \"sv\",\n    \"sw\",\n    \"ta\",\n    \"te\",\n    \"tg\",\n    \"th\",\n    \"tk\",\n    \"tl\",\n    \"tr\",\n    \"tt\",\n    \"uk\",\n    \"ur\",\n    \"uz\",\n    \"vi\",\n    \"yi\",\n    \"yo\",\n    \"zh\",\n    \"yue\",\n)\n"
  },
  {
    "path": "faster_whisper/transcribe.py",
    "content": "import itertools\nimport json\nimport logging\nimport os\nimport zlib\n\nfrom dataclasses import asdict, dataclass\nfrom inspect import signature\nfrom math import ceil\nfrom typing import BinaryIO, Iterable, List, Optional, Tuple, Union\nfrom warnings import warn\n\nimport ctranslate2\nimport numpy as np\nimport tokenizers\n\nfrom tqdm import tqdm\n\nfrom faster_whisper.audio import decode_audio, pad_or_trim\nfrom faster_whisper.feature_extractor import FeatureExtractor\nfrom faster_whisper.tokenizer import _LANGUAGE_CODES, Tokenizer\nfrom faster_whisper.utils import download_model, format_timestamp, get_end, get_logger\nfrom faster_whisper.vad import (\n    SpeechTimestampsMap,\n    VadOptions,\n    collect_chunks,\n    get_speech_timestamps,\n)\n\n\n@dataclass\nclass Word:\n    start: float\n    end: float\n    word: str\n    probability: float\n\n    def _asdict(self):\n        warn(\n            \"Word._asdict() method is deprecated, use dataclasses.asdict(Word) instead\",\n            DeprecationWarning,\n            2,\n        )\n        return asdict(self)\n\n\n@dataclass\nclass Segment:\n    id: int\n    seek: int\n    start: float\n    end: float\n    text: str\n    tokens: List[int]\n    avg_logprob: float\n    compression_ratio: float\n    no_speech_prob: float\n    words: Optional[List[Word]]\n    temperature: Optional[float]\n\n    def _asdict(self):\n        warn(\n            \"Segment._asdict() method is deprecated, use dataclasses.asdict(Segment) instead\",\n            DeprecationWarning,\n            2,\n        )\n        return asdict(self)\n\n\n@dataclass\nclass TranscriptionOptions:\n    beam_size: int\n    best_of: int\n    patience: float\n    length_penalty: float\n    repetition_penalty: float\n    no_repeat_ngram_size: int\n    log_prob_threshold: Optional[float]\n    no_speech_threshold: Optional[float]\n    compression_ratio_threshold: Optional[float]\n    condition_on_previous_text: bool\n    prompt_reset_on_temperature: float\n    temperatures: List[float]\n    initial_prompt: Optional[Union[str, Iterable[int]]]\n    prefix: Optional[str]\n    suppress_blank: bool\n    suppress_tokens: Optional[List[int]]\n    without_timestamps: bool\n    max_initial_timestamp: float\n    word_timestamps: bool\n    prepend_punctuations: str\n    append_punctuations: str\n    multilingual: bool\n    max_new_tokens: Optional[int]\n    clip_timestamps: Union[str, List[float]]\n    hallucination_silence_threshold: Optional[float]\n    hotwords: Optional[str]\n\n\n@dataclass\nclass TranscriptionInfo:\n    language: str\n    language_probability: float\n    duration: float\n    duration_after_vad: float\n    all_language_probs: Optional[List[Tuple[str, float]]]\n    transcription_options: TranscriptionOptions\n    vad_options: VadOptions\n\n\nclass BatchedInferencePipeline:\n    def __init__(\n        self,\n        model,\n    ):\n        self.model: WhisperModel = model\n        self.last_speech_timestamp = 0.0\n\n    def forward(self, features, tokenizer, chunks_metadata, options):\n        encoder_output, outputs = self.generate_segment_batched(\n            features, tokenizer, options\n        )\n\n        segmented_outputs = []\n        segment_sizes = []\n        for chunk_metadata, output in zip(chunks_metadata, outputs):\n            duration = chunk_metadata[\"duration\"]\n            segment_size = int(ceil(duration) * self.model.frames_per_second)\n            segment_sizes.append(segment_size)\n            (\n                subsegments,\n                seek,\n                single_timestamp_ending,\n            ) = self.model._split_segments_by_timestamps(\n                tokenizer=tokenizer,\n                tokens=output[\"tokens\"],\n                time_offset=chunk_metadata[\"offset\"],\n                segment_size=segment_size,\n                segment_duration=duration,\n                seek=0,\n            )\n            segmented_outputs.append(\n                [\n                    dict(\n                        text=tokenizer.decode(subsegment[\"tokens\"]),\n                        avg_logprob=output[\"avg_logprob\"],\n                        no_speech_prob=output[\"no_speech_prob\"],\n                        tokens=subsegment[\"tokens\"],\n                        start=subsegment[\"start\"],\n                        end=subsegment[\"end\"],\n                        compression_ratio=get_compression_ratio(\n                            tokenizer.decode(subsegment[\"tokens\"])\n                        ),\n                        seek=int(\n                            chunk_metadata[\"offset\"] * self.model.frames_per_second\n                        ),\n                    )\n                    for subsegment in subsegments\n                ]\n            )\n        if options.word_timestamps:\n            self.last_speech_timestamp = self.model.add_word_timestamps(\n                segmented_outputs,\n                tokenizer,\n                encoder_output,\n                segment_sizes,\n                options.prepend_punctuations,\n                options.append_punctuations,\n                self.last_speech_timestamp,\n            )\n\n        return segmented_outputs\n\n    def generate_segment_batched(\n        self,\n        features: np.ndarray,\n        tokenizer: Tokenizer,\n        options: TranscriptionOptions,\n    ):\n        batch_size = features.shape[0]\n\n        prompt = self.model.get_prompt(\n            tokenizer,\n            previous_tokens=(\n                tokenizer.encode(options.initial_prompt)\n                if options.initial_prompt is not None\n                else []\n            ),\n            without_timestamps=options.without_timestamps,\n            hotwords=options.hotwords,\n        )\n\n        if options.max_new_tokens is not None:\n            max_length = len(prompt) + options.max_new_tokens\n        else:\n            max_length = self.model.max_length\n\n        if max_length > self.model.max_length:\n            raise ValueError(\n                f\"The length of the prompt is {len(prompt)}, and the `max_new_tokens` \"\n                f\"{max_length - len(prompt)}. Thus, the combined length of the prompt \"\n                f\"and `max_new_tokens` is: {max_length}. This exceeds the \"\n                f\"`max_length` of the Whisper model: {self.model.max_length}. \"\n                \"You should either reduce the length of your prompt, or \"\n                \"reduce the value of `max_new_tokens`, \"\n                f\"so that their combined length is less that {self.model.max_length}.\"\n            )\n\n        encoder_output = self.model.encode(features)\n        prompts = [prompt.copy() for _ in range(batch_size)]\n\n        if options.multilingual:\n            language_tokens = [\n                tokenizer.tokenizer.token_to_id(segment_langs[0][0])\n                for segment_langs in self.model.model.detect_language(encoder_output)\n            ]\n            language_token_index = prompt.index(tokenizer.language)\n\n            for i, language_token in enumerate(language_tokens):\n                prompts[i][language_token_index] = language_token\n\n        results = self.model.model.generate(\n            encoder_output,\n            prompts,\n            beam_size=options.beam_size,\n            patience=options.patience,\n            length_penalty=options.length_penalty,\n            max_length=max_length,\n            suppress_blank=options.suppress_blank,\n            suppress_tokens=options.suppress_tokens,\n            return_scores=True,\n            return_no_speech_prob=True,\n            sampling_temperature=options.temperatures[0],\n            repetition_penalty=options.repetition_penalty,\n            no_repeat_ngram_size=options.no_repeat_ngram_size,\n        )\n\n        output = []\n        for result in results:\n            # return scores\n            seq_len = len(result.sequences_ids[0])\n            cum_logprob = result.scores[0] * (seq_len**options.length_penalty)\n\n            output.append(\n                dict(\n                    avg_logprob=cum_logprob / (seq_len + 1),\n                    no_speech_prob=result.no_speech_prob,\n                    tokens=result.sequences_ids[0],\n                )\n            )\n\n        return encoder_output, output\n\n    def transcribe(\n        self,\n        audio: Union[str, BinaryIO, np.ndarray],\n        language: Optional[str] = None,\n        task: str = \"transcribe\",\n        log_progress: bool = False,\n        beam_size: int = 5,\n        best_of: int = 5,\n        patience: float = 1,\n        length_penalty: float = 1,\n        repetition_penalty: float = 1,\n        no_repeat_ngram_size: int = 0,\n        temperature: Union[float, List[float], Tuple[float, ...]] = [\n            0.0,\n            0.2,\n            0.4,\n            0.6,\n            0.8,\n            1.0,\n        ],\n        compression_ratio_threshold: Optional[float] = 2.4,\n        log_prob_threshold: Optional[float] = -1.0,\n        no_speech_threshold: Optional[float] = 0.6,\n        condition_on_previous_text: bool = True,\n        prompt_reset_on_temperature: float = 0.5,\n        initial_prompt: Optional[Union[str, Iterable[int]]] = None,\n        prefix: Optional[str] = None,\n        suppress_blank: bool = True,\n        suppress_tokens: Optional[List[int]] = [-1],\n        without_timestamps: bool = True,\n        max_initial_timestamp: float = 1.0,\n        word_timestamps: bool = False,\n        prepend_punctuations: str = \"\\\"'“¿([{-\",\n        append_punctuations: str = \"\\\"'.。,，!！?？:：”)]}、\",\n        multilingual: bool = False,\n        vad_filter: bool = True,\n        vad_parameters: Optional[Union[dict, VadOptions]] = None,\n        max_new_tokens: Optional[int] = None,\n        chunk_length: Optional[int] = None,\n        clip_timestamps: Optional[List[dict]] = None,\n        hallucination_silence_threshold: Optional[float] = None,\n        batch_size: int = 8,\n        hotwords: Optional[str] = None,\n        language_detection_threshold: Optional[float] = 0.5,\n        language_detection_segments: int = 1,\n    ) -> Tuple[Iterable[Segment], TranscriptionInfo]:\n        \"\"\"transcribe audio in chunks in batched fashion and return with language info.\n\n        Arguments:\n            audio: Path to the input file (or a file-like object), or the audio waveform.\n            language: The language spoken in the audio. It should be a language code such\n                as \"en\" or \"fr\". If not set, the language will be detected in the first 30 seconds\n                of audio.\n            task: Task to execute (transcribe or translate).\n            log_progress: whether to show progress bar or not.\n            beam_size: Beam size to use for decoding.\n            best_of: Number of candidates when sampling with non-zero temperature.\n            patience: Beam search patience factor.\n            length_penalty: Exponential length penalty constant.\n            repetition_penalty: Penalty applied to the score of previously generated tokens\n                (set > 1 to penalize).\n            no_repeat_ngram_size: Prevent repetitions of ngrams with this size (set 0 to disable).\n            temperature: Temperature for sampling. If a list or tuple is passed,\n                only the first value is used.\n            initial_prompt: Optional text string or iterable of token ids to provide as a\n                prompt for the each window.\n            suppress_blank: Suppress blank outputs at the beginning of the sampling.\n            suppress_tokens: List of token IDs to suppress. -1 will suppress a default set\n                of symbols as defined in `tokenizer.non_speech_tokens()`.\n            without_timestamps: Only sample text tokens.\n            word_timestamps: Extract word-level timestamps using the cross-attention pattern\n                and dynamic time warping, and include the timestamps for each word in each segment.\n                Set as False.\n            prepend_punctuations: If word_timestamps is True, merge these punctuation symbols\n                with the next word\n            append_punctuations: If word_timestamps is True, merge these punctuation symbols\n                with the previous word\n            multilingual: Perform language detection on every segment.\n            vad_filter: Enable the voice activity detection (VAD) to filter out parts of the audio\n                without speech. This step is using the Silero VAD model\n                https://github.com/snakers4/silero-vad.\n            vad_parameters: Dictionary of Silero VAD parameters or VadOptions class (see available\n                parameters and default values in the class `VadOptions`).\n            max_new_tokens: Maximum number of new tokens to generate per-chunk. If not set,\n                the maximum will be set by the default max_length.\n            chunk_length: The length of audio segments. If it is not None, it will overwrite the\n                default chunk_length of the FeatureExtractor.\n            clip_timestamps: Optionally provide list of dictionaries each containing \"start\" and\n                \"end\" keys that specify the start and end of the voiced region within\n                `chunk_length` boundary. vad_filter will be ignored if clip_timestamps is used.\n            batch_size: the maximum number of parallel requests to model for decoding.\n            hotwords:\n                Hotwords/hint phrases to the model. Has no effect if prefix is not None.\n            language_detection_threshold: If the maximum probability of the language tokens is\n                higher than this value, the language is detected.\n            language_detection_segments: Number of segments to consider for the language detection.\n\n        Unused Arguments\n            compression_ratio_threshold: If the gzip compression ratio is above this value,\n                treat as failed.\n            log_prob_threshold: If the average log probability over sampled tokens is\n                below this value, treat as failed.\n            no_speech_threshold: If the no_speech probability is higher than this value AND\n                the average log probability over sampled tokens is below `log_prob_threshold`,\n                consider the segment as silent.\n            condition_on_previous_text: If True, the previous output of the model is provided\n                as a prompt for the next window; disabling may make the text inconsistent across\n                windows, but the model becomes less prone to getting stuck in a failure loop,\n                such as repetition looping or timestamps going out of sync. Set as False\n            prompt_reset_on_temperature: Resets prompt if temperature is above this value.\n                Arg has effect only if condition_on_previous_text is True. Set at 0.5\n            prefix: Optional text to provide as a prefix at the beginning of each window.\n            max_initial_timestamp: The initial timestamp cannot be later than this, set at 0.0.\n            hallucination_silence_threshold: Optional[float]\n                When word_timestamps is True, skip silent periods longer than this threshold\n                (in seconds) when a possible hallucination is detected. set as None.\n        Returns:\n          A tuple with:\n\n            - a generator over transcribed segments\n            - an instance of TranscriptionInfo\n        \"\"\"\n\n        sampling_rate = self.model.feature_extractor.sampling_rate\n\n        if multilingual and not self.model.model.is_multilingual:\n            self.model.logger.warning(\n                \"The current model is English-only but the multilingual parameter is set to\"\n                \"True; setting to False instead.\"\n            )\n            multilingual = False\n\n        if not isinstance(audio, np.ndarray):\n            audio = decode_audio(audio, sampling_rate=sampling_rate)\n        duration = audio.shape[0] / sampling_rate\n\n        self.model.logger.info(\n            \"Processing audio with duration %s\", format_timestamp(duration)\n        )\n\n        chunk_length = chunk_length or self.model.feature_extractor.chunk_length\n        # if no segment split is provided, use vad_model and generate segments\n        if not clip_timestamps:\n            if vad_filter:\n                if vad_parameters is None:\n                    vad_parameters = VadOptions(\n                        max_speech_duration_s=chunk_length,\n                        min_silence_duration_ms=160,\n                    )\n                elif isinstance(vad_parameters, dict):\n                    if \"max_speech_duration_s\" in vad_parameters.keys():\n                        vad_parameters.pop(\"max_speech_duration_s\")\n\n                    vad_parameters = VadOptions(\n                        **vad_parameters, max_speech_duration_s=chunk_length\n                    )\n\n                clip_timestamps = get_speech_timestamps(audio, vad_parameters)\n            # run the audio if it is less than 30 sec even without clip_timestamps\n            elif duration < chunk_length:\n                clip_timestamps = [{\"start\": 0, \"end\": audio.shape[0]}]\n            else:\n                raise RuntimeError(\n                    \"No clip timestamps found. \"\n                    \"Set 'vad_filter' to True or provide 'clip_timestamps'.\"\n                )\n\n            clip_timestamps_provided = False\n            audio_chunks, chunks_metadata = collect_chunks(\n                audio, clip_timestamps, max_duration=chunk_length\n            )\n\n        else:\n            clip_timestamps_provided = True\n            clip_timestamps = [\n                {k: int(v * sampling_rate) for k, v in segment.items()}\n                for segment in clip_timestamps\n            ]\n\n            audio_chunks, chunks_metadata = [], []\n            for i, clip in enumerate(clip_timestamps):\n                audio_chunks.append(audio[clip[\"start\"] : clip[\"end\"]])\n\n                clip_duration = (clip[\"end\"] - clip[\"start\"]) / sampling_rate\n                if clip_duration > 30:\n                    self.model.logger.warning(\n                        \"Segment %d is longer than 30 seconds, \"\n                        \"only the first 30 seconds will be transcribed\",\n                        i,\n                    )\n\n                chunks_metadata.append(\n                    {\n                        \"offset\": clip[\"start\"] / sampling_rate,\n                        \"duration\": clip_duration,\n                        \"segments\": [clip],\n                    }\n                )\n\n        duration_after_vad = (\n            sum((segment[\"end\"] - segment[\"start\"]) for segment in clip_timestamps)\n            / sampling_rate\n        )\n\n        self.model.logger.info(\n            \"VAD filter removed %s of audio\",\n            format_timestamp(duration - duration_after_vad),\n        )\n\n        features = (\n            [self.model.feature_extractor(chunk)[..., :-1] for chunk in audio_chunks]\n            if duration_after_vad\n            else []\n        )\n\n        all_language_probs = None\n        # detecting the language if not provided\n        if language is None:\n            if not self.model.model.is_multilingual:\n                language = \"en\"\n                language_probability = 1\n            else:\n                (\n                    language,\n                    language_probability,\n                    all_language_probs,\n                ) = self.model.detect_language(\n                    features=np.concatenate(\n                        features\n                        + [\n                            np.full((self.model.model.n_mels, 1), -1.5, dtype=\"float32\")\n                        ],\n                        axis=1,\n                    ),  # add a dummy feature to account for empty audio\n                    language_detection_segments=language_detection_segments,\n                    language_detection_threshold=language_detection_threshold,\n                )\n\n                self.model.logger.info(\n                    \"Detected language '%s' with probability %.2f\",\n                    language,\n                    language_probability,\n                )\n        else:\n            if not self.model.model.is_multilingual and language != \"en\":\n                self.model.logger.warning(\n                    \"The current model is English-only but the language parameter is set to '%s'; \"\n                    \"using 'en' instead.\" % language\n                )\n                language = \"en\"\n\n            language_probability = 1\n\n        tokenizer = Tokenizer(\n            self.model.hf_tokenizer,\n            self.model.model.is_multilingual,\n            task=task,\n            language=language,\n        )\n\n        features = (\n            np.stack([pad_or_trim(feature) for feature in features]) if features else []\n        )\n\n        options = TranscriptionOptions(\n            beam_size=beam_size,\n            best_of=best_of,\n            patience=patience,\n            length_penalty=length_penalty,\n            repetition_penalty=repetition_penalty,\n            no_repeat_ngram_size=no_repeat_ngram_size,\n            log_prob_threshold=log_prob_threshold,\n            no_speech_threshold=no_speech_threshold,\n            compression_ratio_threshold=compression_ratio_threshold,\n            temperatures=(\n                temperature[:1]\n                if isinstance(temperature, (list, tuple))\n                else [temperature]\n            ),\n            initial_prompt=initial_prompt,\n            prefix=prefix,\n            suppress_blank=suppress_blank,\n            suppress_tokens=(\n                get_suppressed_tokens(tokenizer, suppress_tokens)\n                if suppress_tokens\n                else suppress_tokens\n            ),\n            prepend_punctuations=prepend_punctuations,\n            append_punctuations=append_punctuations,\n            max_new_tokens=max_new_tokens,\n            hotwords=hotwords,\n            word_timestamps=word_timestamps,\n            hallucination_silence_threshold=None,\n            condition_on_previous_text=False,\n            clip_timestamps=clip_timestamps,\n            prompt_reset_on_temperature=0.5,\n            multilingual=multilingual,\n            without_timestamps=without_timestamps,\n            max_initial_timestamp=0.0,\n        )\n\n        info = TranscriptionInfo(\n            language=language,\n            language_probability=language_probability,\n            duration=duration,\n            duration_after_vad=duration_after_vad,\n            transcription_options=options,\n            vad_options=vad_parameters,\n            all_language_probs=all_language_probs,\n        )\n\n        segments = self._batched_segments_generator(\n            features,\n            tokenizer,\n            chunks_metadata,\n            batch_size,\n            options,\n            log_progress,\n        )\n        if not clip_timestamps_provided:\n            segments = restore_speech_timestamps(\n                segments, clip_timestamps, sampling_rate\n            )\n\n        return segments, info\n\n    def _batched_segments_generator(\n        self, features, tokenizer, chunks_metadata, batch_size, options, log_progress\n    ):\n        pbar = tqdm(total=len(features), disable=not log_progress, position=0)\n        seg_idx = 0\n        for i in range(0, len(features), batch_size):\n            results = self.forward(\n                features[i : i + batch_size],\n                tokenizer,\n                chunks_metadata[i : i + batch_size],\n                options,\n            )\n\n            for result in results:\n                for segment in result:\n                    seg_idx += 1\n                    yield Segment(\n                        seek=segment[\"seek\"],\n                        id=seg_idx,\n                        text=segment[\"text\"],\n                        start=round(segment[\"start\"], 3),\n                        end=round(segment[\"end\"], 3),\n                        words=(\n                            None\n                            if not options.word_timestamps\n                            else [Word(**word) for word in segment[\"words\"]]\n                        ),\n                        tokens=segment[\"tokens\"],\n                        avg_logprob=segment[\"avg_logprob\"],\n                        no_speech_prob=segment[\"no_speech_prob\"],\n                        compression_ratio=segment[\"compression_ratio\"],\n                        temperature=options.temperatures[0],\n                    )\n\n                pbar.update(1)\n\n        pbar.close()\n        self.last_speech_timestamp = 0.0\n\n\nclass WhisperModel:\n    def __init__(\n        self,\n        model_size_or_path: str,\n        device: str = \"auto\",\n        device_index: Union[int, List[int]] = 0,\n        compute_type: str = \"default\",\n        cpu_threads: int = 0,\n        num_workers: int = 1,\n        download_root: Optional[str] = None,\n        local_files_only: bool = False,\n        files: dict = None,\n        revision: Optional[str] = None,\n        use_auth_token: Optional[Union[str, bool]] = None,\n        **model_kwargs,\n    ):\n        \"\"\"Initializes the Whisper model.\n\n        Args:\n          model_size_or_path: Size of the model to use (tiny, tiny.en, base, base.en,\n            small, small.en, distil-small.en, medium, medium.en, distil-medium.en, large-v1,\n            large-v2, large-v3, large, distil-large-v2, distil-large-v3, large-v3-turbo, or turbo),\n            a path to a converted model directory, or a CTranslate2-converted Whisper model ID from\n            the HF Hub. When a size or a model ID is configured, the converted model is downloaded\n            from the Hugging Face Hub.\n          device: Device to use for computation (\"cpu\", \"cuda\", \"auto\").\n          device_index: Device ID to use.\n            The model can also be loaded on multiple GPUs by passing a list of IDs\n            (e.g. [0, 1, 2, 3]). In that case, multiple transcriptions can run in parallel\n            when transcribe() is called from multiple Python threads (see also num_workers).\n          compute_type: Type to use for computation.\n            See https://opennmt.net/CTranslate2/quantization.html.\n          cpu_threads: Number of threads to use when running on CPU (4 by default).\n            A non zero value overrides the OMP_NUM_THREADS environment variable.\n          num_workers: When transcribe() is called from multiple Python threads,\n            having multiple workers enables true parallelism when running the model\n            (concurrent calls to self.model.generate() will run in parallel).\n            This can improve the global throughput at the cost of increased memory usage.\n          download_root: Directory where the models should be saved. If not set, the models\n            are saved in the standard Hugging Face cache directory.\n          local_files_only:  If True, avoid downloading the file and return the path to the\n            local cached file if it exists.\n          files: Load model files from the memory. This argument is a dictionary mapping file names\n            to file contents as file-like or bytes objects. If this is set, model_path acts as an\n            identifier for this model.\n          revision:\n            An optional Git revision id which can be a branch name, a tag, or a\n            commit hash.\n          use_auth_token: HuggingFace authentication token or True to use the\n            token stored by the HuggingFace config folder.\n        \"\"\"\n        self.logger = get_logger()\n\n        tokenizer_bytes, preprocessor_bytes = None, None\n        if files:\n            model_path = model_size_or_path\n            tokenizer_bytes = files.pop(\"tokenizer.json\", None)\n            preprocessor_bytes = files.pop(\"preprocessor_config.json\", None)\n        elif os.path.isdir(model_size_or_path):\n            model_path = model_size_or_path\n        else:\n            model_path = download_model(\n                model_size_or_path,\n                local_files_only=local_files_only,\n                cache_dir=download_root,\n                revision=revision,\n                use_auth_token=use_auth_token,\n            )\n\n        self.model = ctranslate2.models.Whisper(\n            model_path,\n            device=device,\n            device_index=device_index,\n            compute_type=compute_type,\n            intra_threads=cpu_threads,\n            inter_threads=num_workers,\n            files=files,\n            **model_kwargs,\n        )\n\n        tokenizer_file = os.path.join(model_path, \"tokenizer.json\")\n        if tokenizer_bytes:\n            self.hf_tokenizer = tokenizers.Tokenizer.from_buffer(tokenizer_bytes)\n        elif os.path.isfile(tokenizer_file):\n            self.hf_tokenizer = tokenizers.Tokenizer.from_file(tokenizer_file)\n        else:\n            self.hf_tokenizer = tokenizers.Tokenizer.from_pretrained(\n                \"openai/whisper-tiny\" + (\"\" if self.model.is_multilingual else \".en\")\n            )\n        self.feat_kwargs = self._get_feature_kwargs(model_path, preprocessor_bytes)\n        self.feature_extractor = FeatureExtractor(**self.feat_kwargs)\n        self.input_stride = 2\n        self.num_samples_per_token = (\n            self.feature_extractor.hop_length * self.input_stride\n        )\n        self.frames_per_second = (\n            self.feature_extractor.sampling_rate // self.feature_extractor.hop_length\n        )\n        self.tokens_per_second = (\n            self.feature_extractor.sampling_rate // self.num_samples_per_token\n        )\n        self.time_precision = 0.02\n        self.max_length = 448\n\n    @property\n    def supported_languages(self) -> List[str]:\n        \"\"\"The languages supported by the model.\"\"\"\n        return list(_LANGUAGE_CODES) if self.model.is_multilingual else [\"en\"]\n\n    def _get_feature_kwargs(self, model_path, preprocessor_bytes=None) -> dict:\n        config = {}\n        try:\n            config_path = os.path.join(model_path, \"preprocessor_config.json\")\n            if preprocessor_bytes:\n                config = json.loads(preprocessor_bytes)\n            elif os.path.isfile(config_path):\n                with open(config_path, \"r\", encoding=\"utf-8\") as file:\n                    config = json.load(file)\n            else:\n                return config\n            valid_keys = signature(FeatureExtractor.__init__).parameters.keys()\n            return {k: v for k, v in config.items() if k in valid_keys}\n        except json.JSONDecodeError as e:\n            self.logger.warning(\"Could not load preprocessor config: %s\", e)\n\n        return config\n\n    def transcribe(\n        self,\n        audio: Union[str, BinaryIO, np.ndarray],\n        language: Optional[str] = None,\n        task: str = \"transcribe\",\n        log_progress: bool = False,\n        beam_size: int = 5,\n        best_of: int = 5,\n        patience: float = 1,\n        length_penalty: float = 1,\n        repetition_penalty: float = 1,\n        no_repeat_ngram_size: int = 0,\n        temperature: Union[float, List[float], Tuple[float, ...]] = [\n            0.0,\n            0.2,\n            0.4,\n            0.6,\n            0.8,\n            1.0,\n        ],\n        compression_ratio_threshold: Optional[float] = 2.4,\n        log_prob_threshold: Optional[float] = -1.0,\n        no_speech_threshold: Optional[float] = 0.6,\n        condition_on_previous_text: bool = True,\n        prompt_reset_on_temperature: float = 0.5,\n        initial_prompt: Optional[Union[str, Iterable[int]]] = None,\n        prefix: Optional[str] = None,\n        suppress_blank: bool = True,\n        suppress_tokens: Optional[List[int]] = [-1],\n        without_timestamps: bool = False,\n        max_initial_timestamp: float = 1.0,\n        word_timestamps: bool = False,\n        prepend_punctuations: str = \"\\\"'“¿([{-\",\n        append_punctuations: str = \"\\\"'.。,，!！?？:：”)]}、\",\n        multilingual: bool = False,\n        vad_filter: bool = False,\n        vad_parameters: Optional[Union[dict, VadOptions]] = None,\n        max_new_tokens: Optional[int] = None,\n        chunk_length: Optional[int] = None,\n        clip_timestamps: Union[str, List[float]] = \"0\",\n        hallucination_silence_threshold: Optional[float] = None,\n        hotwords: Optional[str] = None,\n        language_detection_threshold: Optional[float] = 0.5,\n        language_detection_segments: int = 1,\n    ) -> Tuple[Iterable[Segment], TranscriptionInfo]:\n        \"\"\"Transcribes an input file.\n\n        Arguments:\n          audio: Path to the input file (or a file-like object), or the audio waveform.\n          language: The language spoken in the audio. It should be a language code such\n            as \"en\" or \"fr\". If not set, the language will be detected in the first 30 seconds\n            of audio.\n          task: Task to execute (transcribe or translate).\n          log_progress: whether to show progress bar or not.\n          beam_size: Beam size to use for decoding.\n          best_of: Number of candidates when sampling with non-zero temperature.\n          patience: Beam search patience factor.\n          length_penalty: Exponential length penalty constant.\n          repetition_penalty: Penalty applied to the score of previously generated tokens\n            (set > 1 to penalize).\n          no_repeat_ngram_size: Prevent repetitions of ngrams with this size (set 0 to disable).\n          temperature: Temperature for sampling. It can be a tuple of temperatures,\n            which will be successively used upon failures according to either\n            `compression_ratio_threshold` or `log_prob_threshold`.\n          compression_ratio_threshold: If the gzip compression ratio is above this value,\n            treat as failed.\n          log_prob_threshold: If the average log probability over sampled tokens is\n            below this value, treat as failed.\n          no_speech_threshold: If the no_speech probability is higher than this value AND\n            the average log probability over sampled tokens is below `log_prob_threshold`,\n            consider the segment as silent.\n          condition_on_previous_text: If True, the previous output of the model is provided\n            as a prompt for the next window; disabling may make the text inconsistent across\n            windows, but the model becomes less prone to getting stuck in a failure loop,\n            such as repetition looping or timestamps going out of sync.\n          prompt_reset_on_temperature: Resets prompt if temperature is above this value.\n            Arg has effect only if condition_on_previous_text is True.\n          initial_prompt: Optional text string or iterable of token ids to provide as a\n            prompt for the first window.\n          prefix: Optional text to provide as a prefix for the first window.\n          suppress_blank: Suppress blank outputs at the beginning of the sampling.\n          suppress_tokens: List of token IDs to suppress. -1 will suppress a default set\n            of symbols as defined in `tokenizer.non_speech_tokens()`.\n          without_timestamps: Only sample text tokens.\n          max_initial_timestamp: The initial timestamp cannot be later than this.\n          word_timestamps: Extract word-level timestamps using the cross-attention pattern\n            and dynamic time warping, and include the timestamps for each word in each segment.\n          prepend_punctuations: If word_timestamps is True, merge these punctuation symbols\n            with the next word\n          append_punctuations: If word_timestamps is True, merge these punctuation symbols\n            with the previous word\n          multilingual: Perform language detection on every segment.\n          vad_filter: Enable the voice activity detection (VAD) to filter out parts of the audio\n            without speech. This step is using the Silero VAD model\n            https://github.com/snakers4/silero-vad.\n          vad_parameters: Dictionary of Silero VAD parameters or VadOptions class (see available\n            parameters and default values in the class `VadOptions`).\n          max_new_tokens: Maximum number of new tokens to generate per-chunk. If not set,\n            the maximum will be set by the default max_length.\n          chunk_length: The length of audio segments. If it is not None, it will overwrite the\n            default chunk_length of the FeatureExtractor.\n          clip_timestamps:\n            Comma-separated list start,end,start,end,... timestamps (in seconds) of clips to\n             process. The last end timestamp defaults to the end of the file.\n             vad_filter will be ignored if clip_timestamps is used.\n          hallucination_silence_threshold:\n            When word_timestamps is True, skip silent periods longer than this threshold\n             (in seconds) when a possible hallucination is detected\n          hotwords:\n            Hotwords/hint phrases to provide the model with. Has no effect if prefix is not None.\n          language_detection_threshold: If the maximum probability of the language tokens is higher\n           than this value, the language is detected.\n          language_detection_segments: Number of segments to consider for the language detection.\n        Returns:\n          A tuple with:\n\n            - a generator over transcribed segments\n            - an instance of TranscriptionInfo\n        \"\"\"\n        sampling_rate = self.feature_extractor.sampling_rate\n\n        if multilingual and not self.model.is_multilingual:\n            self.logger.warning(\n                \"The current model is English-only but the multilingual parameter is set to\"\n                \"True; setting to False instead.\"\n            )\n            multilingual = False\n\n        if not isinstance(audio, np.ndarray):\n            audio = decode_audio(audio, sampling_rate=sampling_rate)\n\n        duration = audio.shape[0] / sampling_rate\n        duration_after_vad = duration\n\n        self.logger.info(\n            \"Processing audio with duration %s\", format_timestamp(duration)\n        )\n\n        if vad_filter and clip_timestamps == \"0\":\n            if vad_parameters is None:\n                vad_parameters = VadOptions()\n            elif isinstance(vad_parameters, dict):\n                vad_parameters = VadOptions(**vad_parameters)\n            speech_chunks = get_speech_timestamps(audio, vad_parameters)\n            audio_chunks, chunks_metadata = collect_chunks(audio, speech_chunks)\n            audio = np.concatenate(audio_chunks, axis=0)\n            duration_after_vad = audio.shape[0] / sampling_rate\n\n            self.logger.info(\n                \"VAD filter removed %s of audio\",\n                format_timestamp(duration - duration_after_vad),\n            )\n\n            if self.logger.isEnabledFor(logging.DEBUG):\n                self.logger.debug(\n                    \"VAD filter kept the following audio segments: %s\",\n                    \", \".join(\n                        \"[%s -> %s]\"\n                        % (\n                            format_timestamp(chunk[\"start\"] / sampling_rate),\n                            format_timestamp(chunk[\"end\"] / sampling_rate),\n                        )\n                        for chunk in speech_chunks\n                    ),\n                )\n\n        else:\n            speech_chunks = None\n\n        features = self.feature_extractor(audio, chunk_length=chunk_length)\n\n        encoder_output = None\n        all_language_probs = None\n\n        # detecting the language if not provided\n        if language is None:\n            if not self.model.is_multilingual:\n                language = \"en\"\n                language_probability = 1\n            else:\n                start_timestamp = (\n                    float(clip_timestamps.split(\",\")[0])\n                    if isinstance(clip_timestamps, str)\n                    else clip_timestamps[0]\n                )\n                content_frames = features.shape[-1] - 1\n                seek = (\n                    int(start_timestamp * self.frames_per_second)\n                    if start_timestamp * self.frames_per_second < content_frames\n                    else 0\n                )\n                (\n                    language,\n                    language_probability,\n                    all_language_probs,\n                ) = self.detect_language(\n                    features=features[..., seek:],\n                    language_detection_segments=language_detection_segments,\n                    language_detection_threshold=language_detection_threshold,\n                )\n\n                self.logger.info(\n                    \"Detected language '%s' with probability %.2f\",\n                    language,\n                    language_probability,\n                )\n        else:\n            if not self.model.is_multilingual and language != \"en\":\n                self.logger.warning(\n                    \"The current model is English-only but the language parameter is set to '%s'; \"\n                    \"using 'en' instead.\" % language\n                )\n                language = \"en\"\n\n            language_probability = 1\n\n        tokenizer = Tokenizer(\n            self.hf_tokenizer,\n            self.model.is_multilingual,\n            task=task,\n            language=language,\n        )\n\n        options = TranscriptionOptions(\n            beam_size=beam_size,\n            best_of=best_of,\n            patience=patience,\n            length_penalty=length_penalty,\n            repetition_penalty=repetition_penalty,\n            no_repeat_ngram_size=no_repeat_ngram_size,\n            log_prob_threshold=log_prob_threshold,\n            no_speech_threshold=no_speech_threshold,\n            compression_ratio_threshold=compression_ratio_threshold,\n            condition_on_previous_text=condition_on_previous_text,\n            prompt_reset_on_temperature=prompt_reset_on_temperature,\n            temperatures=(\n                temperature if isinstance(temperature, (list, tuple)) else [temperature]\n            ),\n            initial_prompt=initial_prompt,\n            prefix=prefix,\n            suppress_blank=suppress_blank,\n            suppress_tokens=(\n                get_suppressed_tokens(tokenizer, suppress_tokens)\n                if suppress_tokens\n                else suppress_tokens\n            ),\n            without_timestamps=without_timestamps,\n            max_initial_timestamp=max_initial_timestamp,\n            word_timestamps=word_timestamps,\n            prepend_punctuations=prepend_punctuations,\n            append_punctuations=append_punctuations,\n            multilingual=multilingual,\n            max_new_tokens=max_new_tokens,\n            clip_timestamps=clip_timestamps,\n            hallucination_silence_threshold=hallucination_silence_threshold,\n            hotwords=hotwords,\n        )\n\n        segments = self.generate_segments(\n            features, tokenizer, options, log_progress, encoder_output\n        )\n\n        if speech_chunks:\n            segments = restore_speech_timestamps(segments, speech_chunks, sampling_rate)\n\n        info = TranscriptionInfo(\n            language=language,\n            language_probability=language_probability,\n            duration=duration,\n            duration_after_vad=duration_after_vad,\n            transcription_options=options,\n            vad_options=vad_parameters,\n            all_language_probs=all_language_probs,\n        )\n\n        return segments, info\n\n    def _split_segments_by_timestamps(\n        self,\n        tokenizer: Tokenizer,\n        tokens: List[int],\n        time_offset: float,\n        segment_size: int,\n        segment_duration: float,\n        seek: int,\n    ) -> List[List[int]]:\n        current_segments = []\n        single_timestamp_ending = (\n            len(tokens) >= 2 and tokens[-2] < tokenizer.timestamp_begin <= tokens[-1]\n        )\n\n        consecutive_timestamps = [\n            i\n            for i in range(len(tokens))\n            if i > 0\n            and tokens[i] >= tokenizer.timestamp_begin\n            and tokens[i - 1] >= tokenizer.timestamp_begin\n        ]\n\n        if len(consecutive_timestamps) > 0:\n            slices = list(consecutive_timestamps)\n            if single_timestamp_ending:\n                slices.append(len(tokens))\n\n            last_slice = 0\n            for current_slice in slices:\n                sliced_tokens = tokens[last_slice:current_slice]\n                start_timestamp_position = sliced_tokens[0] - tokenizer.timestamp_begin\n                end_timestamp_position = sliced_tokens[-1] - tokenizer.timestamp_begin\n                start_time = (\n                    time_offset + start_timestamp_position * self.time_precision\n                )\n                end_time = time_offset + end_timestamp_position * self.time_precision\n\n                current_segments.append(\n                    dict(\n                        seek=seek,\n                        start=start_time,\n                        end=end_time,\n                        tokens=sliced_tokens,\n                    )\n                )\n                last_slice = current_slice\n\n            if single_timestamp_ending:\n                # single timestamp at the end means no speech after the last timestamp.\n                seek += segment_size\n            else:\n                # otherwise, ignore the unfinished segment and seek to the last timestamp\n                last_timestamp_position = (\n                    tokens[last_slice - 1] - tokenizer.timestamp_begin\n                )\n                seek += last_timestamp_position * self.input_stride\n\n        else:\n            duration = segment_duration\n            timestamps = [\n                token for token in tokens if token >= tokenizer.timestamp_begin\n            ]\n            if len(timestamps) > 0 and timestamps[-1] != tokenizer.timestamp_begin:\n                last_timestamp_position = timestamps[-1] - tokenizer.timestamp_begin\n                duration = last_timestamp_position * self.time_precision\n\n            current_segments.append(\n                dict(\n                    seek=seek,\n                    start=time_offset,\n                    end=time_offset + duration,\n                    tokens=tokens,\n                )\n            )\n\n            seek += segment_size\n\n        return current_segments, seek, single_timestamp_ending\n\n    def generate_segments(\n        self,\n        features: np.ndarray,\n        tokenizer: Tokenizer,\n        options: TranscriptionOptions,\n        log_progress,\n        encoder_output: Optional[ctranslate2.StorageView] = None,\n    ) -> Iterable[Segment]:\n        content_frames = features.shape[-1] - 1\n        content_duration = float(content_frames * self.feature_extractor.time_per_frame)\n\n        if isinstance(options.clip_timestamps, str):\n            options.clip_timestamps = [\n                float(ts)\n                for ts in (\n                    options.clip_timestamps.split(\",\")\n                    if options.clip_timestamps\n                    else []\n                )\n            ]\n\n        seek_points: List[int] = [\n            round(ts * self.frames_per_second) for ts in options.clip_timestamps\n        ]\n        if len(seek_points) == 0:\n            seek_points.append(0)\n        if len(seek_points) % 2 == 1:\n            seek_points.append(content_frames)\n        seek_clips: List[Tuple[int, int]] = list(\n            zip(seek_points[::2], seek_points[1::2])\n        )\n\n        punctuation = \"\\\"'“¿([{-\\\"'.。,，!！?？:：”)]}、\"\n\n        idx = 0\n        clip_idx = 0\n        seek = seek_clips[clip_idx][0]\n        all_tokens = []\n        prompt_reset_since = 0\n\n        if options.initial_prompt is not None:\n            if isinstance(options.initial_prompt, str):\n                initial_prompt = \" \" + options.initial_prompt.strip()\n                initial_prompt_tokens = tokenizer.encode(initial_prompt)\n                all_tokens.extend(initial_prompt_tokens)\n            else:\n                all_tokens.extend(options.initial_prompt)\n\n        pbar = tqdm(total=content_duration, unit=\"seconds\", disable=not log_progress)\n        last_speech_timestamp = 0.0\n        # NOTE: This loop is obscurely flattened to make the diff readable.\n        # A later commit should turn this into a simpler nested loop.\n        # for seek_clip_start, seek_clip_end in seek_clips:\n        #     while seek < seek_clip_end\n        while clip_idx < len(seek_clips):\n            seek_clip_start, seek_clip_end = seek_clips[clip_idx]\n            if seek_clip_end > content_frames:\n                seek_clip_end = content_frames\n            if seek < seek_clip_start:\n                seek = seek_clip_start\n            if seek >= seek_clip_end:\n                clip_idx += 1\n                if clip_idx < len(seek_clips):\n                    seek = seek_clips[clip_idx][0]\n                continue\n            time_offset = seek * self.feature_extractor.time_per_frame\n            window_end_time = float(\n                (seek + self.feature_extractor.nb_max_frames)\n                * self.feature_extractor.time_per_frame\n            )\n            segment_size = min(\n                self.feature_extractor.nb_max_frames,\n                content_frames - seek,\n                seek_clip_end - seek,\n            )\n            segment = features[:, seek : seek + segment_size]\n            segment_duration = segment_size * self.feature_extractor.time_per_frame\n            segment = pad_or_trim(segment)\n\n            if self.logger.isEnabledFor(logging.DEBUG):\n                self.logger.debug(\n                    \"Processing segment at %s\", format_timestamp(time_offset)\n                )\n\n            previous_tokens = all_tokens[prompt_reset_since:]\n\n            if seek > 0 or encoder_output is None:\n                encoder_output = self.encode(segment)\n\n            if options.multilingual:\n                results = self.model.detect_language(encoder_output)\n                language_token, language_probability = results[0][0]\n                language = language_token[2:-2]\n\n                tokenizer.language = tokenizer.tokenizer.token_to_id(language_token)\n                tokenizer.language_code = language\n\n            prompt = self.get_prompt(\n                tokenizer,\n                previous_tokens,\n                without_timestamps=options.without_timestamps,\n                prefix=options.prefix if seek == 0 else None,\n                hotwords=options.hotwords,\n            )\n\n            (\n                result,\n                avg_logprob,\n                temperature,\n                compression_ratio,\n            ) = self.generate_with_fallback(encoder_output, prompt, tokenizer, options)\n\n            if options.no_speech_threshold is not None:\n                # no voice activity check\n                should_skip = result.no_speech_prob > options.no_speech_threshold\n\n                if (\n                    options.log_prob_threshold is not None\n                    and avg_logprob > options.log_prob_threshold\n                ):\n                    # don't skip if the logprob is high enough, despite the no_speech_prob\n                    should_skip = False\n\n                if should_skip:\n                    self.logger.debug(\n                        \"No speech threshold is met (%f > %f)\",\n                        result.no_speech_prob,\n                        options.no_speech_threshold,\n                    )\n\n                    # fast-forward to the next segment boundary\n                    seek += segment_size\n                    continue\n\n            tokens = result.sequences_ids[0]\n\n            previous_seek = seek\n\n            # anomalous words are very long/short/improbable\n            def word_anomaly_score(word: dict) -> float:\n                probability = word.get(\"probability\", 0.0)\n                duration = word[\"end\"] - word[\"start\"]\n                score = 0.0\n                if probability < 0.15:\n                    score += 1.0\n                if duration < 0.133:\n                    score += (0.133 - duration) * 15\n                if duration > 2.0:\n                    score += duration - 2.0\n                return score\n\n            def is_segment_anomaly(segment: Optional[dict]) -> bool:\n                if segment is None or not segment[\"words\"]:\n                    return False\n                words = [w for w in segment[\"words\"] if w[\"word\"] not in punctuation]\n                words = words[:8]\n                score = sum(word_anomaly_score(w) for w in words)\n                return score >= 3 or score + 0.01 >= len(words)\n\n            def next_words_segment(segments: List[dict]) -> Optional[dict]:\n                return next((s for s in segments if s[\"words\"]), None)\n\n            (\n                current_segments,\n                seek,\n                single_timestamp_ending,\n            ) = self._split_segments_by_timestamps(\n                tokenizer=tokenizer,\n                tokens=tokens,\n                time_offset=time_offset,\n                segment_size=segment_size,\n                segment_duration=segment_duration,\n                seek=seek,\n            )\n\n            if options.word_timestamps:\n                self.add_word_timestamps(\n                    [current_segments],\n                    tokenizer,\n                    encoder_output,\n                    segment_size,\n                    options.prepend_punctuations,\n                    options.append_punctuations,\n                    last_speech_timestamp=last_speech_timestamp,\n                )\n                if not single_timestamp_ending:\n                    last_word_end = get_end(current_segments)\n                    if last_word_end is not None and last_word_end > time_offset:\n                        seek = round(last_word_end * self.frames_per_second)\n\n                # skip silence before possible hallucinations\n                if options.hallucination_silence_threshold is not None:\n                    threshold = options.hallucination_silence_threshold\n\n                    # if first segment might be a hallucination, skip leading silence\n                    first_segment = next_words_segment(current_segments)\n                    if first_segment is not None and is_segment_anomaly(first_segment):\n                        gap = first_segment[\"start\"] - time_offset\n                        if gap > threshold:\n                            seek = previous_seek + round(gap * self.frames_per_second)\n                            continue\n\n                    # skip silence before any possible hallucination that is surrounded\n                    # by silence or more hallucinations\n                    hal_last_end = last_speech_timestamp\n                    for si in range(len(current_segments)):\n                        segment = current_segments[si]\n                        if not segment[\"words\"]:\n                            continue\n                        if is_segment_anomaly(segment):\n                            next_segment = next_words_segment(\n                                current_segments[si + 1 :]\n                            )\n                            if next_segment is not None:\n                                hal_next_start = next_segment[\"words\"][0][\"start\"]\n                            else:\n                                hal_next_start = time_offset + segment_duration\n                            silence_before = (\n                                segment[\"start\"] - hal_last_end > threshold\n                                or segment[\"start\"] < threshold\n                                or segment[\"start\"] - time_offset < 2.0\n                            )\n                            silence_after = (\n                                hal_next_start - segment[\"end\"] > threshold\n                                or is_segment_anomaly(next_segment)\n                                or window_end_time - segment[\"end\"] < 2.0\n                            )\n                            if silence_before and silence_after:\n                                seek = round(\n                                    max(time_offset + 1, segment[\"start\"])\n                                    * self.frames_per_second\n                                )\n                                if content_duration - segment[\"end\"] < threshold:\n                                    seek = content_frames\n                                current_segments[si:] = []\n                                break\n                        hal_last_end = segment[\"end\"]\n\n                last_word_end = get_end(current_segments)\n                if last_word_end is not None:\n                    last_speech_timestamp = last_word_end\n            for segment in current_segments:\n                tokens = segment[\"tokens\"]\n                text = tokenizer.decode(tokens)\n\n                if segment[\"start\"] == segment[\"end\"] or not text.strip():\n                    continue\n\n                all_tokens.extend(tokens)\n                idx += 1\n\n                yield Segment(\n                    id=idx,\n                    seek=previous_seek,\n                    start=segment[\"start\"],\n                    end=segment[\"end\"],\n                    text=text,\n                    tokens=tokens,\n                    temperature=temperature,\n                    avg_logprob=avg_logprob,\n                    compression_ratio=compression_ratio,\n                    no_speech_prob=result.no_speech_prob,\n                    words=(\n                        [Word(**word) for word in segment[\"words\"]]\n                        if options.word_timestamps\n                        else None\n                    ),\n                )\n\n            if (\n                not options.condition_on_previous_text\n                or temperature > options.prompt_reset_on_temperature\n            ):\n                if options.condition_on_previous_text:\n                    self.logger.debug(\n                        \"Reset prompt. prompt_reset_on_temperature threshold is met %f > %f\",\n                        temperature,\n                        options.prompt_reset_on_temperature,\n                    )\n\n                prompt_reset_since = len(all_tokens)\n\n            pbar.update(\n                (min(content_frames, seek) - previous_seek)\n                * self.feature_extractor.time_per_frame,\n            )\n        pbar.close()\n\n    def encode(self, features: np.ndarray) -> ctranslate2.StorageView:\n        # When the model is running on multiple GPUs, the encoder output should be moved\n        # to the CPU since we don't know which GPU will handle the next job.\n        to_cpu = self.model.device == \"cuda\" and len(self.model.device_index) > 1\n\n        if features.ndim == 2:\n            features = np.expand_dims(features, 0)\n        features = get_ctranslate2_storage(features)\n\n        return self.model.encode(features, to_cpu=to_cpu)\n\n    def generate_with_fallback(\n        self,\n        encoder_output: ctranslate2.StorageView,\n        prompt: List[int],\n        tokenizer: Tokenizer,\n        options: TranscriptionOptions,\n    ) -> Tuple[ctranslate2.models.WhisperGenerationResult, float, float, float]:\n        decode_result = None\n        all_results = []\n        below_cr_threshold_results = []\n\n        max_initial_timestamp_index = int(\n            round(options.max_initial_timestamp / self.time_precision)\n        )\n        if options.max_new_tokens is not None:\n            max_length = len(prompt) + options.max_new_tokens\n        else:\n            max_length = self.max_length\n\n        if max_length > self.max_length:\n            raise ValueError(\n                f\"The length of the prompt is {len(prompt)}, and the `max_new_tokens` \"\n                f\"{max_length - len(prompt)}. Thus, the combined length of the prompt \"\n                f\"and `max_new_tokens` is: {max_length}. This exceeds the \"\n                f\"`max_length` of the Whisper model: {self.max_length}. \"\n                \"You should either reduce the length of your prompt, or \"\n                \"reduce the value of `max_new_tokens`, \"\n                f\"so that their combined length is less that {self.max_length}.\"\n            )\n\n        for temperature in options.temperatures:\n            if temperature > 0:\n                kwargs = {\n                    \"beam_size\": 1,\n                    \"num_hypotheses\": options.best_of,\n                    \"sampling_topk\": 0,\n                    \"sampling_temperature\": temperature,\n                }\n            else:\n                kwargs = {\n                    \"beam_size\": options.beam_size,\n                    \"patience\": options.patience,\n                }\n\n            result = self.model.generate(\n                encoder_output,\n                [prompt],\n                length_penalty=options.length_penalty,\n                repetition_penalty=options.repetition_penalty,\n                no_repeat_ngram_size=options.no_repeat_ngram_size,\n                max_length=max_length,\n                return_scores=True,\n                return_no_speech_prob=True,\n                suppress_blank=options.suppress_blank,\n                suppress_tokens=options.suppress_tokens,\n                max_initial_timestamp_index=max_initial_timestamp_index,\n                **kwargs,\n            )[0]\n\n            tokens = result.sequences_ids[0]\n\n            # Recover the average log prob from the returned score.\n            seq_len = len(tokens)\n            cum_logprob = result.scores[0] * (seq_len**options.length_penalty)\n            avg_logprob = cum_logprob / (seq_len + 1)\n\n            text = tokenizer.decode(tokens).strip()\n            compression_ratio = get_compression_ratio(text)\n\n            decode_result = (\n                result,\n                avg_logprob,\n                temperature,\n                compression_ratio,\n            )\n            all_results.append(decode_result)\n\n            needs_fallback = False\n\n            if options.compression_ratio_threshold is not None:\n                if compression_ratio > options.compression_ratio_threshold:\n                    needs_fallback = True  # too repetitive\n\n                    self.logger.debug(\n                        \"Compression ratio threshold is not met with temperature %.1f (%f > %f)\",\n                        temperature,\n                        compression_ratio,\n                        options.compression_ratio_threshold,\n                    )\n                else:\n                    below_cr_threshold_results.append(decode_result)\n\n            if (\n                options.log_prob_threshold is not None\n                and avg_logprob < options.log_prob_threshold\n            ):\n                needs_fallback = True  # average log probability is too low\n\n                self.logger.debug(\n                    \"Log probability threshold is not met with temperature %.1f (%f < %f)\",\n                    temperature,\n                    avg_logprob,\n                    options.log_prob_threshold,\n                )\n\n            if (\n                options.no_speech_threshold is not None\n                and result.no_speech_prob > options.no_speech_threshold\n                and options.log_prob_threshold is not None\n                and avg_logprob < options.log_prob_threshold\n            ):\n                needs_fallback = False  # silence\n\n            if not needs_fallback:\n                break\n        else:\n            # all failed, select the result with the highest average log probability\n            decode_result = max(\n                below_cr_threshold_results or all_results, key=lambda x: x[1]\n            )\n            # to pass final temperature for prompt_reset_on_temperature\n            decode_result = (\n                decode_result[0],\n                decode_result[1],\n                temperature,\n                decode_result[3],\n            )\n\n        return decode_result\n\n    def get_prompt(\n        self,\n        tokenizer: Tokenizer,\n        previous_tokens: List[int],\n        without_timestamps: bool = False,\n        prefix: Optional[str] = None,\n        hotwords: Optional[str] = None,\n    ) -> List[int]:\n        prompt = []\n\n        if previous_tokens or (hotwords and not prefix):\n            prompt.append(tokenizer.sot_prev)\n            if hotwords and not prefix:\n                hotwords_tokens = tokenizer.encode(\" \" + hotwords.strip())\n                if len(hotwords_tokens) >= self.max_length // 2:\n                    hotwords_tokens = hotwords_tokens[: self.max_length // 2 - 1]\n                prompt.extend(hotwords_tokens)\n            if previous_tokens:\n                prompt.extend(previous_tokens[-(self.max_length // 2 - 1) :])\n\n        prompt.extend(tokenizer.sot_sequence)\n\n        if without_timestamps:\n            prompt.append(tokenizer.no_timestamps)\n\n        if prefix:\n            prefix_tokens = tokenizer.encode(\" \" + prefix.strip())\n            if len(prefix_tokens) >= self.max_length // 2:\n                prefix_tokens = prefix_tokens[: self.max_length // 2 - 1]\n            if not without_timestamps:\n                prompt.append(tokenizer.timestamp_begin)\n            prompt.extend(prefix_tokens)\n\n        return prompt\n\n    def add_word_timestamps(\n        self,\n        segments: List[dict],\n        tokenizer: Tokenizer,\n        encoder_output: ctranslate2.StorageView,\n        num_frames: int,\n        prepend_punctuations: str,\n        append_punctuations: str,\n        last_speech_timestamp: float,\n    ) -> float:\n        if len(segments) == 0:\n            return\n\n        text_tokens = []\n        text_tokens_per_segment = []\n        for segment in segments:\n            segment_tokens = [\n                [token for token in subsegment[\"tokens\"] if token < tokenizer.eot]\n                for subsegment in segment\n            ]\n            text_tokens.append(list(itertools.chain.from_iterable(segment_tokens)))\n            text_tokens_per_segment.append(segment_tokens)\n\n        alignments = self.find_alignment(\n            tokenizer, text_tokens, encoder_output, num_frames\n        )\n        median_max_durations = []\n        for alignment in alignments:\n            word_durations = np.array(\n                [word[\"end\"] - word[\"start\"] for word in alignment]\n            )\n            word_durations = word_durations[word_durations.nonzero()]\n            median_duration = (\n                np.median(word_durations) if len(word_durations) > 0 else 0.0\n            )\n            median_duration = min(0.7, float(median_duration))\n            max_duration = median_duration * 2\n\n            # hack: truncate long words at sentence boundaries.\n            # a better segmentation algorithm based on VAD should be able to replace this.\n            if len(word_durations) > 0:\n                sentence_end_marks = \".。!！?？\"\n                # ensure words at sentence boundaries\n                # are not longer than twice the median word duration.\n                for i in range(1, len(alignment)):\n                    if alignment[i][\"end\"] - alignment[i][\"start\"] > max_duration:\n                        if alignment[i][\"word\"] in sentence_end_marks:\n                            alignment[i][\"end\"] = alignment[i][\"start\"] + max_duration\n                        elif alignment[i - 1][\"word\"] in sentence_end_marks:\n                            alignment[i][\"start\"] = alignment[i][\"end\"] - max_duration\n\n            merge_punctuations(alignment, prepend_punctuations, append_punctuations)\n            median_max_durations.append((median_duration, max_duration))\n\n        for segment_idx, segment in enumerate(segments):\n            word_index = 0\n            time_offset = segment[0][\"seek\"] / self.frames_per_second\n            median_duration, max_duration = median_max_durations[segment_idx]\n            for subsegment_idx, subsegment in enumerate(segment):\n                saved_tokens = 0\n                words = []\n\n                while word_index < len(alignments[segment_idx]) and saved_tokens < len(\n                    text_tokens_per_segment[segment_idx][subsegment_idx]\n                ):\n                    timing = alignments[segment_idx][word_index]\n\n                    if timing[\"word\"]:\n                        words.append(\n                            dict(\n                                word=timing[\"word\"],\n                                start=round(time_offset + timing[\"start\"], 2),\n                                end=round(time_offset + timing[\"end\"], 2),\n                                probability=timing[\"probability\"],\n                            )\n                        )\n\n                    saved_tokens += len(timing[\"tokens\"])\n                    word_index += 1\n\n                # hack: truncate long words at segment boundaries.\n                # a better segmentation algorithm based on VAD should be able to replace this.\n                if len(words) > 0:\n                    # ensure the first and second word after a pause is not longer than\n                    # twice the median word duration.\n                    if words[0][\n                        \"end\"\n                    ] - last_speech_timestamp > median_duration * 4 and (\n                        words[0][\"end\"] - words[0][\"start\"] > max_duration\n                        or (\n                            len(words) > 1\n                            and words[1][\"end\"] - words[0][\"start\"] > max_duration * 2\n                        )\n                    ):\n                        if (\n                            len(words) > 1\n                            and words[1][\"end\"] - words[1][\"start\"] > max_duration\n                        ):\n                            boundary = max(\n                                words[1][\"end\"] / 2, words[1][\"end\"] - max_duration\n                            )\n                            words[0][\"end\"] = words[1][\"start\"] = boundary\n                        words[0][\"start\"] = max(0, words[0][\"end\"] - max_duration)\n\n                    # prefer the segment-level start timestamp if the first word is too long.\n                    if (\n                        subsegment[\"start\"] < words[0][\"end\"]\n                        and subsegment[\"start\"] - 0.5 > words[0][\"start\"]\n                    ):\n                        words[0][\"start\"] = max(\n                            0,\n                            min(words[0][\"end\"] - median_duration, subsegment[\"start\"]),\n                        )\n                    else:\n                        subsegment[\"start\"] = words[0][\"start\"]\n\n                    # prefer the segment-level end timestamp if the last word is too long.\n                    if (\n                        subsegment[\"end\"] > words[-1][\"start\"]\n                        and subsegment[\"end\"] + 0.5 < words[-1][\"end\"]\n                    ):\n                        words[-1][\"end\"] = max(\n                            words[-1][\"start\"] + median_duration, subsegment[\"end\"]\n                        )\n                    else:\n                        subsegment[\"end\"] = words[-1][\"end\"]\n\n                    last_speech_timestamp = subsegment[\"end\"]\n                segments[segment_idx][subsegment_idx][\"words\"] = words\n        return last_speech_timestamp\n\n    def find_alignment(\n        self,\n        tokenizer: Tokenizer,\n        text_tokens: List[int],\n        encoder_output: ctranslate2.StorageView,\n        num_frames: int,\n        median_filter_width: int = 7,\n    ) -> List[dict]:\n        if len(text_tokens) == 0:\n            return []\n\n        results = self.model.align(\n            encoder_output,\n            tokenizer.sot_sequence,\n            text_tokens,\n            num_frames,\n            median_filter_width=median_filter_width,\n        )\n        return_list = []\n        for result, text_token in zip(results, text_tokens):\n            text_token_probs = result.text_token_probs\n            alignments = result.alignments\n            text_indices = np.array([pair[0] for pair in alignments])\n            time_indices = np.array([pair[1] for pair in alignments])\n\n            words, word_tokens = tokenizer.split_to_word_tokens(\n                text_token + [tokenizer.eot]\n            )\n            if len(word_tokens) <= 1:\n                # return on eot only\n                # >>> np.pad([], (1, 0))\n                # array([0.])\n                # This results in crashes when we lookup jump_times with float, like\n                # IndexError: arrays used as indices must be of integer (or boolean) type\n                return_list.append([])\n                continue\n            word_boundaries = np.pad(\n                np.cumsum([len(t) for t in word_tokens[:-1]]), (1, 0)\n            )\n            if len(word_boundaries) <= 1:\n                return_list.append([])\n                continue\n\n            jumps = np.pad(np.diff(text_indices), (1, 0), constant_values=1).astype(\n                bool\n            )\n            jump_times = time_indices[jumps] / self.tokens_per_second\n            start_times = jump_times[word_boundaries[:-1]]\n            end_times = jump_times[word_boundaries[1:]]\n            word_probabilities = [\n                np.mean(text_token_probs[i:j])\n                for i, j in zip(word_boundaries[:-1], word_boundaries[1:])\n            ]\n\n            return_list.append(\n                [\n                    dict(\n                        word=word,\n                        tokens=tokens,\n                        start=start,\n                        end=end,\n                        probability=probability,\n                    )\n                    for word, tokens, start, end, probability in zip(\n                        words, word_tokens, start_times, end_times, word_probabilities\n                    )\n                ]\n            )\n        return return_list\n\n    def detect_language(\n        self,\n        audio: Optional[np.ndarray] = None,\n        features: Optional[np.ndarray] = None,\n        vad_filter: bool = False,\n        vad_parameters: Union[dict, VadOptions] = None,\n        language_detection_segments: int = 1,\n        language_detection_threshold: float = 0.5,\n    ) -> Tuple[str, float, List[Tuple[str, float]]]:\n        \"\"\"\n        Use Whisper to detect the language of the input audio or features.\n\n        Arguments:\n            audio: Input audio signal, must be a 1D float array sampled at 16khz.\n            features: Input Mel spectrogram features, must be a float array with\n                shape (n_mels, n_frames), if `audio` is provided, the features will be ignored.\n                Either `audio` or `features` must be provided.\n            vad_filter: Enable the voice activity detection (VAD) to filter out parts of the audio\n                without speech. This step is using the Silero VAD model.\n            vad_parameters: Dictionary of Silero VAD parameters or VadOptions class (see available\n                parameters and default values in the class `VadOptions`).\n            language_detection_threshold: If the maximum probability of the language tokens is\n                higher than this value, the language is detected.\n            language_detection_segments: Number of segments to consider for the language detection.\n\n        Returns:\n            language: Detected language.\n            language_probability: Probability of the detected language.\n            all_language_probs: List of tuples with all language names and probabilities.\n        \"\"\"\n        assert (\n            audio is not None or features is not None\n        ), \"Either `audio` or `features` must be provided.\"\n\n        if audio is not None:\n            if vad_filter:\n                speech_chunks = get_speech_timestamps(audio, vad_parameters)\n                audio_chunks, chunks_metadata = collect_chunks(audio, speech_chunks)\n                audio = np.concatenate(audio_chunks, axis=0)\n\n            audio = audio[\n                : language_detection_segments * self.feature_extractor.n_samples\n            ]\n            features = self.feature_extractor(audio)\n\n        features = features[\n            ..., : language_detection_segments * self.feature_extractor.nb_max_frames\n        ]\n\n        detected_language_info = {}\n        for i in range(0, features.shape[-1], self.feature_extractor.nb_max_frames):\n            encoder_output = self.encode(\n                pad_or_trim(features[..., i : i + self.feature_extractor.nb_max_frames])\n            )\n            # results is a list of tuple[str, float] with language names and probabilities.\n            results = self.model.detect_language(encoder_output)[0]\n\n            # Parse language names to strip out markers\n            all_language_probs = [(token[2:-2], prob) for (token, prob) in results]\n            # Get top language token and probability\n            language, language_probability = all_language_probs[0]\n            if language_probability > language_detection_threshold:\n                break\n            detected_language_info.setdefault(language, []).append(language_probability)\n        else:\n            # If no language detected for all segments, the majority vote of the highest\n            # projected languages for all segments is used to determine the language.\n            language = max(\n                detected_language_info,\n                key=lambda lang: len(detected_language_info[lang]),\n            )\n            language_probability = max(detected_language_info[language])\n\n        return language, language_probability, all_language_probs\n\n\ndef restore_speech_timestamps(\n    segments: Iterable[Segment],\n    speech_chunks: List[dict],\n    sampling_rate: int,\n) -> Iterable[Segment]:\n    ts_map = SpeechTimestampsMap(speech_chunks, sampling_rate)\n\n    for segment in segments:\n        if segment.words:\n            words = []\n            for word in segment.words:\n                # Ensure the word start and end times are resolved to the same chunk.\n                middle = (word.start + word.end) / 2\n                chunk_index = ts_map.get_chunk_index(middle)\n                word.start = ts_map.get_original_time(word.start, chunk_index)\n                word.end = ts_map.get_original_time(word.end, chunk_index)\n                words.append(word)\n\n            segment.start = words[0].start\n            segment.end = words[-1].end\n            segment.words = words\n\n        else:\n            segment.start = ts_map.get_original_time(segment.start)\n            segment.end = ts_map.get_original_time(segment.end, is_end=True)\n\n        yield segment\n\n\ndef get_ctranslate2_storage(segment: np.ndarray) -> ctranslate2.StorageView:\n    segment = np.ascontiguousarray(segment)\n    segment = ctranslate2.StorageView.from_array(segment)\n    return segment\n\n\ndef get_compression_ratio(text: str) -> float:\n    text_bytes = text.encode(\"utf-8\")\n    return len(text_bytes) / len(zlib.compress(text_bytes))\n\n\ndef get_suppressed_tokens(\n    tokenizer: Tokenizer,\n    suppress_tokens: Tuple[int],\n) -> Optional[List[int]]:\n    if -1 in suppress_tokens:\n        suppress_tokens = [t for t in suppress_tokens if t >= 0]\n        suppress_tokens.extend(tokenizer.non_speech_tokens)\n    elif suppress_tokens is None or len(suppress_tokens) == 0:\n        suppress_tokens = []  # interpret empty string as an empty list\n    else:\n        assert isinstance(suppress_tokens, list), \"suppress_tokens must be a list\"\n\n    suppress_tokens.extend(\n        [\n            tokenizer.transcribe,\n            tokenizer.translate,\n            tokenizer.sot,\n            tokenizer.sot_prev,\n            tokenizer.sot_lm,\n            tokenizer.no_speech,\n        ]\n    )\n\n    return tuple(sorted(set(suppress_tokens)))\n\n\ndef merge_punctuations(alignment: List[dict], prepended: str, appended: str) -> None:\n    # merge prepended punctuations\n    i = len(alignment) - 2\n    j = len(alignment) - 1\n    while i >= 0:\n        previous = alignment[i]\n        following = alignment[j]\n        if previous[\"word\"].startswith(\" \") and previous[\"word\"].strip() in prepended:\n            # prepend it to the following word\n            following[\"word\"] = previous[\"word\"] + following[\"word\"]\n            following[\"tokens\"] = previous[\"tokens\"] + following[\"tokens\"]\n            previous[\"word\"] = \"\"\n            previous[\"tokens\"] = []\n        else:\n            j = i\n        i -= 1\n\n    # merge appended punctuations\n    i = 0\n    j = 1\n    while j < len(alignment):\n        previous = alignment[i]\n        following = alignment[j]\n        if not previous[\"word\"].endswith(\" \") and following[\"word\"] in appended:\n            # append it to the previous word\n            previous[\"word\"] = previous[\"word\"] + following[\"word\"]\n            previous[\"tokens\"] = previous[\"tokens\"] + following[\"tokens\"]\n            following[\"word\"] = \"\"\n            following[\"tokens\"] = []\n        else:\n            i = j\n        j += 1\n"
  },
  {
    "path": "faster_whisper/utils.py",
    "content": "import logging\nimport os\nimport re\n\nfrom typing import List, Optional, Union\n\nimport huggingface_hub\n\nfrom tqdm.auto import tqdm\n\n_MODELS = {\n    \"tiny.en\": \"Systran/faster-whisper-tiny.en\",\n    \"tiny\": \"Systran/faster-whisper-tiny\",\n    \"base.en\": \"Systran/faster-whisper-base.en\",\n    \"base\": \"Systran/faster-whisper-base\",\n    \"small.en\": \"Systran/faster-whisper-small.en\",\n    \"small\": \"Systran/faster-whisper-small\",\n    \"medium.en\": \"Systran/faster-whisper-medium.en\",\n    \"medium\": \"Systran/faster-whisper-medium\",\n    \"large-v1\": \"Systran/faster-whisper-large-v1\",\n    \"large-v2\": \"Systran/faster-whisper-large-v2\",\n    \"large-v3\": \"Systran/faster-whisper-large-v3\",\n    \"large\": \"Systran/faster-whisper-large-v3\",\n    \"distil-large-v2\": \"Systran/faster-distil-whisper-large-v2\",\n    \"distil-medium.en\": \"Systran/faster-distil-whisper-medium.en\",\n    \"distil-small.en\": \"Systran/faster-distil-whisper-small.en\",\n    \"distil-large-v3\": \"Systran/faster-distil-whisper-large-v3\",\n    \"distil-large-v3.5\": \"distil-whisper/distil-large-v3.5-ct2\",\n    \"large-v3-turbo\": \"mobiuslabsgmbh/faster-whisper-large-v3-turbo\",\n    \"turbo\": \"mobiuslabsgmbh/faster-whisper-large-v3-turbo\",\n}\n\n\ndef available_models() -> List[str]:\n    \"\"\"Returns the names of available models.\"\"\"\n    return list(_MODELS.keys())\n\n\ndef get_assets_path():\n    \"\"\"Returns the path to the assets directory.\"\"\"\n    return os.path.join(os.path.dirname(os.path.abspath(__file__)), \"assets\")\n\n\ndef get_logger():\n    \"\"\"Returns the module logger.\"\"\"\n    return logging.getLogger(\"faster_whisper\")\n\n\ndef download_model(\n    size_or_id: str,\n    output_dir: Optional[str] = None,\n    local_files_only: bool = False,\n    cache_dir: Optional[str] = None,\n    revision: Optional[str] = None,\n    use_auth_token: Optional[Union[str, bool]] = None,\n):\n    \"\"\"Downloads a CTranslate2 Whisper model from the Hugging Face Hub.\n\n    Args:\n      size_or_id: Size of the model to download from https://huggingface.co/Systran\n        (tiny, tiny.en, base, base.en, small, small.en, distil-small.en, medium, medium.en,\n        distil-medium.en, large-v1, large-v2, large-v3, large, distil-large-v2,\n        distil-large-v3), or a CTranslate2-converted model ID from the Hugging Face Hub\n        (e.g. Systran/faster-whisper-large-v3).\n      output_dir: Directory where the model should be saved. If not set, the model is saved in\n        the cache directory.\n      local_files_only:  If True, avoid downloading the file and return the path to the local\n        cached file if it exists.\n      cache_dir: Path to the folder where cached files are stored.\n      revision: An optional Git revision id which can be a branch name, a tag, or a\n            commit hash.\n      use_auth_token: HuggingFace authentication token or True to use the\n            token stored by the HuggingFace config folder.\n\n    Returns:\n      The path to the downloaded model.\n\n    Raises:\n      ValueError: if the model size is invalid.\n    \"\"\"\n    if re.match(r\".*/.*\", size_or_id):\n        repo_id = size_or_id\n    else:\n        repo_id = _MODELS.get(size_or_id)\n        if repo_id is None:\n            raise ValueError(\n                \"Invalid model size '%s', expected one of: %s\"\n                % (size_or_id, \", \".join(_MODELS.keys()))\n            )\n\n    allow_patterns = [\n        \"config.json\",\n        \"preprocessor_config.json\",\n        \"model.bin\",\n        \"tokenizer.json\",\n        \"vocabulary.*\",\n    ]\n\n    kwargs = {\n        \"local_files_only\": local_files_only,\n        \"allow_patterns\": allow_patterns,\n        \"tqdm_class\": disabled_tqdm,\n        \"revision\": revision,\n    }\n\n    if output_dir is not None:\n        kwargs[\"local_dir\"] = output_dir\n\n    if cache_dir is not None:\n        kwargs[\"cache_dir\"] = cache_dir\n\n    if use_auth_token is not None:\n        kwargs[\"token\"] = use_auth_token\n\n    return huggingface_hub.snapshot_download(repo_id, **kwargs)\n\n\ndef format_timestamp(\n    seconds: float,\n    always_include_hours: bool = False,\n    decimal_marker: str = \".\",\n) -> str:\n    assert seconds >= 0, \"non-negative timestamp expected\"\n    milliseconds = round(seconds * 1000.0)\n\n    hours = milliseconds // 3_600_000\n    milliseconds -= hours * 3_600_000\n\n    minutes = milliseconds // 60_000\n    milliseconds -= minutes * 60_000\n\n    seconds = milliseconds // 1_000\n    milliseconds -= seconds * 1_000\n\n    hours_marker = f\"{hours:02d}:\" if always_include_hours or hours > 0 else \"\"\n    return (\n        f\"{hours_marker}{minutes:02d}:{seconds:02d}{decimal_marker}{milliseconds:03d}\"\n    )\n\n\nclass disabled_tqdm(tqdm):\n    def __init__(self, *args, **kwargs):\n        kwargs[\"disable\"] = True\n        super().__init__(*args, **kwargs)\n\n\ndef get_end(segments: List[dict]) -> Optional[float]:\n    return next(\n        (w[\"end\"] for s in reversed(segments) for w in reversed(s[\"words\"])),\n        segments[-1][\"end\"] if segments else None,\n    )\n"
  },
  {
    "path": "faster_whisper/vad.py",
    "content": "import bisect\nimport functools\nimport os\n\nfrom dataclasses import dataclass\nfrom typing import Dict, List, Optional, Tuple\n\nimport numpy as np\n\nfrom faster_whisper.utils import get_assets_path\n\n\n# The code below is adapted from https://github.com/snakers4/silero-vad.\n@dataclass\nclass VadOptions:\n    \"\"\"VAD options.\n\n    Attributes:\n      threshold: Speech threshold. Silero VAD outputs speech probabilities for each audio chunk,\n        probabilities ABOVE this value are considered as SPEECH. It is better to tune this\n        parameter for each dataset separately, but \"lazy\" 0.5 is pretty good for most datasets.\n      neg_threshold: Silence threshold for determining the end of speech. If a probability is lower\n        than neg_threshold, it is always considered silence. Values higher than neg_threshold\n        are only considered speech if the previous sample was classified as speech; otherwise,\n        they are treated as silence. This parameter helps refine the detection of speech\n         transitions, ensuring smoother segment boundaries.\n      min_speech_duration_ms: Final speech chunks shorter min_speech_duration_ms are thrown out.\n      max_speech_duration_s: Maximum duration of speech chunks in seconds. Chunks longer\n        than max_speech_duration_s will be split at the timestamp of the last silence that\n        lasts more than min_silence_at_max_speech (if any), to prevent aggressive cutting.\n        Otherwise, they will be split aggressively just before max_speech_duration_s.\n      min_silence_duration_ms: In the end of each speech chunk wait for min_silence_duration_ms\n        before separating it\n      speech_pad_ms: Final speech chunks are padded by speech_pad_ms each side\n      min_silence_at_max_speech: Minimum silence duration in ms which is used to avoid abrupt cuts\n          when max_speech_duration_s is reached.\n      use_max_poss_sil_at_max_speech: Whether to use the maximum possible silence at\n          max_speech_duration_s or not. If not, the last silence is used.\n    \"\"\"\n\n    threshold: float = 0.5\n    neg_threshold: float = None\n    min_speech_duration_ms: int = 0\n    max_speech_duration_s: float = float(\"inf\")\n    min_silence_duration_ms: int = 2000\n    speech_pad_ms: int = 400\n    min_silence_at_max_speech: int = 98\n    use_max_poss_sil_at_max_speech: bool = True\n\n\ndef get_speech_timestamps(\n    audio: np.ndarray,\n    vad_options: Optional[VadOptions] = None,\n    sampling_rate: int = 16000,\n    **kwargs,\n) -> List[dict]:\n    \"\"\"This method is used for splitting long audios into speech chunks using silero VAD.\n\n    Args:\n      audio: One dimensional float array.\n      vad_options: Options for VAD processing.\n      sampling rate: Sampling rate of the audio.\n      kwargs: VAD options passed as keyword arguments for backward compatibility.\n\n    Returns:\n      List of dicts containing begin and end samples of each speech chunk.\n    \"\"\"\n    if vad_options is None:\n        vad_options = VadOptions(**kwargs)\n\n    threshold = vad_options.threshold\n    neg_threshold = vad_options.neg_threshold\n    min_speech_duration_ms = vad_options.min_speech_duration_ms\n    max_speech_duration_s = vad_options.max_speech_duration_s\n    min_silence_duration_ms = vad_options.min_silence_duration_ms\n    window_size_samples = 512\n    speech_pad_ms = vad_options.speech_pad_ms\n    min_silence_at_max_speech = vad_options.min_silence_at_max_speech\n    use_max_poss_sil_at_max_speech = vad_options.use_max_poss_sil_at_max_speech\n\n    min_speech_samples = sampling_rate * min_speech_duration_ms / 1000\n    speech_pad_samples = sampling_rate * speech_pad_ms / 1000\n    max_speech_samples = (\n        sampling_rate * max_speech_duration_s\n        - window_size_samples\n        - 2 * speech_pad_samples\n    )\n    min_silence_samples = sampling_rate * min_silence_duration_ms / 1000\n    min_silence_samples_at_max_speech = sampling_rate * min_silence_at_max_speech / 1000\n\n    audio_length_samples = len(audio)\n\n    model = get_vad_model()\n\n    padded_audio = np.pad(\n        audio, (0, window_size_samples - audio.shape[0] % window_size_samples)\n    )\n    speech_probs = model(padded_audio)\n\n    triggered = False\n    speeches = []\n    current_speech = {}\n    possible_ends = []\n\n    if neg_threshold is None:\n        neg_threshold = max(threshold - 0.15, 0.01)\n\n    # to save potential segment end (and tolerate some silence)\n    temp_end = 0\n    # to save potential segment limits in case of maximum segment size reached\n    prev_end = next_start = 0\n\n    for i, speech_prob in enumerate(speech_probs):\n        cur_sample = window_size_samples * i\n\n        if (speech_prob >= threshold) and temp_end:\n            sil_dur = cur_sample - temp_end\n            if sil_dur > min_silence_samples_at_max_speech:\n                possible_ends.append((temp_end, sil_dur))\n            temp_end = 0\n            if next_start < prev_end:\n                next_start = cur_sample\n\n        if (speech_prob >= threshold) and not triggered:\n            triggered = True\n            current_speech[\"start\"] = cur_sample\n            continue\n\n        if triggered and (cur_sample - current_speech[\"start\"] > max_speech_samples):\n            if use_max_poss_sil_at_max_speech and possible_ends:\n                prev_end, dur = max(possible_ends, key=lambda x: x[1])\n                current_speech[\"end\"] = prev_end\n                speeches.append(current_speech)\n                current_speech = {}\n                next_start = prev_end + dur\n\n                if next_start < prev_end + cur_sample:\n                    current_speech[\"start\"] = next_start\n                else:\n                    triggered = False\n                prev_end = next_start = temp_end = 0\n                possible_ends = []\n            else:\n                if prev_end:\n                    current_speech[\"end\"] = prev_end\n                    speeches.append(current_speech)\n                    current_speech = {}\n                    if next_start < prev_end:\n                        triggered = False\n                    else:\n                        current_speech[\"start\"] = next_start\n                    prev_end = next_start = temp_end = 0\n                    possible_ends = []\n                else:\n                    current_speech[\"end\"] = cur_sample\n                    speeches.append(current_speech)\n                    current_speech = {}\n                    prev_end = next_start = temp_end = 0\n                    triggered = False\n                    possible_ends = []\n                    continue\n\n        if (speech_prob < neg_threshold) and triggered:\n            if not temp_end:\n                temp_end = cur_sample\n            sil_dur_now = cur_sample - temp_end\n\n            if (\n                not use_max_poss_sil_at_max_speech\n                and sil_dur_now > min_silence_samples_at_max_speech\n            ):\n                prev_end = temp_end\n\n            if sil_dur_now < min_silence_samples:\n                continue\n            else:\n                current_speech[\"end\"] = temp_end\n                if (\n                    current_speech[\"end\"] - current_speech[\"start\"]\n                ) > min_speech_samples:\n                    speeches.append(current_speech)\n                current_speech = {}\n                prev_end = next_start = temp_end = 0\n                triggered = False\n                possible_ends = []\n                continue\n\n    if (\n        current_speech\n        and (audio_length_samples - current_speech[\"start\"]) > min_speech_samples\n    ):\n        current_speech[\"end\"] = audio_length_samples\n        speeches.append(current_speech)\n\n    for i, speech in enumerate(speeches):\n        if i == 0:\n            speech[\"start\"] = int(max(0, speech[\"start\"] - speech_pad_samples))\n        if i != len(speeches) - 1:\n            silence_duration = speeches[i + 1][\"start\"] - speech[\"end\"]\n            if silence_duration < 2 * speech_pad_samples:\n                speech[\"end\"] += int(silence_duration // 2)\n                speeches[i + 1][\"start\"] = int(\n                    max(0, speeches[i + 1][\"start\"] - silence_duration // 2)\n                )\n            else:\n                speech[\"end\"] = int(\n                    min(audio_length_samples, speech[\"end\"] + speech_pad_samples)\n                )\n                speeches[i + 1][\"start\"] = int(\n                    max(0, speeches[i + 1][\"start\"] - speech_pad_samples)\n                )\n        else:\n            speech[\"end\"] = int(\n                min(audio_length_samples, speech[\"end\"] + speech_pad_samples)\n            )\n\n    return speeches\n\n\ndef collect_chunks(\n    audio: np.ndarray,\n    chunks: List[dict],\n    sampling_rate: int = 16000,\n    max_duration: float = float(\"inf\"),\n) -> Tuple[List[np.ndarray], List[Dict[str, float]]]:\n    \"\"\"This function merges the chunks of audio into chunks of max_duration (s) length.\"\"\"\n    if not chunks:\n        chunk_metadata = {\n            \"offset\": 0,\n            \"duration\": 0,\n            \"segments\": [],\n        }\n        return [np.array([], dtype=np.float32)], [chunk_metadata]\n\n    audio_chunks = []\n    chunks_metadata = []\n\n    current_segments = []\n    current_duration = 0\n    total_duration = 0\n    current_audio = np.array([], dtype=np.float32)\n\n    for chunk in chunks:\n        if (\n            current_duration + chunk[\"end\"] - chunk[\"start\"]\n            > max_duration * sampling_rate\n        ):\n            audio_chunks.append(current_audio)\n            chunk_metadata = {\n                \"offset\": total_duration / sampling_rate,\n                \"duration\": current_duration / sampling_rate,\n                \"segments\": current_segments,\n            }\n            total_duration += current_duration\n            chunks_metadata.append(chunk_metadata)\n\n            current_segments = []\n\n            current_audio = audio[chunk[\"start\"] : chunk[\"end\"]]\n            current_duration = chunk[\"end\"] - chunk[\"start\"]\n        else:\n            current_segments.append(chunk)\n            current_audio = np.concatenate(\n                (current_audio, audio[chunk[\"start\"] : chunk[\"end\"]])\n            )\n\n            current_duration += chunk[\"end\"] - chunk[\"start\"]\n\n    audio_chunks.append(current_audio)\n\n    chunk_metadata = {\n        \"offset\": total_duration / sampling_rate,\n        \"duration\": current_duration / sampling_rate,\n        \"segments\": current_segments,\n    }\n    chunks_metadata.append(chunk_metadata)\n    return audio_chunks, chunks_metadata\n\n\nclass SpeechTimestampsMap:\n    \"\"\"Helper class to restore original speech timestamps.\"\"\"\n\n    def __init__(self, chunks: List[dict], sampling_rate: int, time_precision: int = 2):\n        self.sampling_rate = sampling_rate\n        self.time_precision = time_precision\n        self.chunk_end_sample = []\n        self.total_silence_before = []\n\n        previous_end = 0\n        silent_samples = 0\n\n        for chunk in chunks:\n            silent_samples += chunk[\"start\"] - previous_end\n            previous_end = chunk[\"end\"]\n\n            self.chunk_end_sample.append(chunk[\"end\"] - silent_samples)\n            self.total_silence_before.append(silent_samples / sampling_rate)\n\n    def get_original_time(\n        self,\n        time: float,\n        chunk_index: Optional[int] = None,\n        is_end: bool = False,\n    ) -> float:\n        if chunk_index is None:\n            chunk_index = self.get_chunk_index(time, is_end)\n\n        total_silence_before = self.total_silence_before[chunk_index]\n        return round(total_silence_before + time, self.time_precision)\n\n    def get_chunk_index(self, time: float, is_end: bool = False) -> int:\n        sample = int(time * self.sampling_rate)\n        if sample in self.chunk_end_sample and is_end:\n            return self.chunk_end_sample.index(sample)\n\n        return min(\n            bisect.bisect(self.chunk_end_sample, sample),\n            len(self.chunk_end_sample) - 1,\n        )\n\n\n@functools.lru_cache\ndef get_vad_model():\n    \"\"\"Returns the VAD model instance.\"\"\"\n    path = os.path.join(get_assets_path(), \"silero_vad_v6.onnx\")\n    return SileroVADModel(path)\n\n\nclass SileroVADModel:\n    def __init__(self, path):\n        try:\n            import onnxruntime\n        except ImportError as e:\n            raise RuntimeError(\n                \"Applying the VAD filter requires the onnxruntime package\"\n            ) from e\n\n        opts = onnxruntime.SessionOptions()\n        opts.inter_op_num_threads = 1\n        opts.intra_op_num_threads = 1\n        opts.enable_cpu_mem_arena = False\n        opts.log_severity_level = 4\n\n        self.session = onnxruntime.InferenceSession(\n            path,\n            providers=[\"CPUExecutionProvider\"],\n            sess_options=opts,\n        )\n\n    def __call__(\n        self, audio: np.ndarray, num_samples: int = 512, context_size_samples: int = 64\n    ):\n        assert audio.ndim == 1, \"Input should be a 1D array\"\n        assert (\n            audio.shape[0] % num_samples == 0\n        ), \"Input size should be a multiple of num_samples\"\n\n        h = np.zeros((1, 1, 128), dtype=\"float32\")\n        c = np.zeros((1, 1, 128), dtype=\"float32\")\n        context = np.zeros(\n            (1, context_size_samples),\n            dtype=\"float32\",\n        )\n\n        batched_audio = audio.reshape(-1, num_samples)\n        context = batched_audio[..., -context_size_samples:]\n        context[-1] = 0\n        context = np.roll(context, 1, 0)\n        batched_audio = np.concatenate([context, batched_audio], 1)\n\n        batched_audio = batched_audio.reshape(-1, num_samples + context_size_samples)\n\n        encoder_batch_size = 10000\n        num_segments = batched_audio.shape[0]\n        outputs = []\n        for i in range(0, num_segments, encoder_batch_size):\n            output, h, c = self.session.run(\n                None,\n                {\"input\": batched_audio[i : i + encoder_batch_size], \"h\": h, \"c\": c},\n            )\n            outputs.append(output)\n\n        out = np.concatenate(outputs, axis=0)\n\n        return out\n"
  },
  {
    "path": "faster_whisper/version.py",
    "content": "\"\"\"Version information.\"\"\"\n\n__version__ = \"1.2.1\"\n"
  },
  {
    "path": "requirements.conversion.txt",
    "content": "transformers[torch]>=4.23\n"
  },
  {
    "path": "requirements.txt",
    "content": "ctranslate2>=4.0,<5\nhuggingface_hub>=0.23\ntokenizers>=0.13,<1\nonnxruntime>=1.14,<2 \nav>=11\ntqdm\n"
  },
  {
    "path": "setup.cfg",
    "content": "[flake8]\nmax-line-length = 100\nignore =\n  E203,\n  W503,\n\n[isort]\nprofile=black\nlines_between_types=1\n"
  },
  {
    "path": "setup.py",
    "content": "import os\n\nfrom setuptools import find_packages, setup\n\nbase_dir = os.path.dirname(os.path.abspath(__file__))\n\n\ndef get_long_description():\n    readme_path = os.path.join(base_dir, \"README.md\")\n    with open(readme_path, encoding=\"utf-8\") as readme_file:\n        return readme_file.read()\n\n\ndef get_project_version():\n    version_path = os.path.join(base_dir, \"faster_whisper\", \"version.py\")\n    version = {}\n    with open(version_path, encoding=\"utf-8\") as fp:\n        exec(fp.read(), version)\n    return version[\"__version__\"]\n\n\ndef get_requirements(path):\n    with open(path, encoding=\"utf-8\") as requirements:\n        return [requirement.strip() for requirement in requirements]\n\n\ninstall_requires = get_requirements(os.path.join(base_dir, \"requirements.txt\"))\nconversion_requires = get_requirements(\n    os.path.join(base_dir, \"requirements.conversion.txt\")\n)\n\nsetup(\n    name=\"faster-whisper\",\n    version=get_project_version(),\n    license=\"MIT\",\n    description=\"Faster Whisper transcription with CTranslate2\",\n    long_description=get_long_description(),\n    long_description_content_type=\"text/markdown\",\n    author=\"Guillaume Klein\",\n    url=\"https://github.com/SYSTRAN/faster-whisper\",\n    classifiers=[\n        \"Development Status :: 4 - Beta\",\n        \"Intended Audience :: Developers\",\n        \"Intended Audience :: Science/Research\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Programming Language :: Python :: 3\",\n        \"Programming Language :: Python :: 3 :: Only\",\n        \"Programming Language :: Python :: 3.9\",\n        \"Programming Language :: Python :: 3.10\",\n        \"Programming Language :: Python :: 3.11\",\n        \"Topic :: Scientific/Engineering :: Artificial Intelligence\",\n    ],\n    keywords=\"openai whisper speech ctranslate2 inference quantization transformer\",\n    python_requires=\">=3.9\",\n    install_requires=install_requires,\n    extras_require={\n        \"conversion\": conversion_requires,\n        \"dev\": [\n            \"black==23.*\",\n            \"flake8==6.*\",\n            \"isort==5.*\",\n            \"pytest==7.*\",\n        ],\n    },\n    packages=find_packages(),\n    include_package_data=True,\n)\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "import os\n\nimport pytest\n\n\n@pytest.fixture\ndef data_dir():\n    return os.path.join(os.path.dirname(os.path.abspath(__file__)), \"data\")\n\n\n@pytest.fixture\ndef jfk_path(data_dir):\n    return os.path.join(data_dir, \"jfk.flac\")\n\n\n@pytest.fixture\ndef physcisworks_path(data_dir):\n    return os.path.join(data_dir, \"physicsworks.wav\")\n"
  },
  {
    "path": "tests/test_tokenizer.py",
    "content": "from faster_whisper import WhisperModel\nfrom faster_whisper.tokenizer import Tokenizer\nfrom faster_whisper.transcribe import get_suppressed_tokens\n\n\ndef test_suppressed_tokens_minus_1():\n    model = WhisperModel(\"tiny.en\")\n\n    tokenizer = Tokenizer(model.hf_tokenizer, False)\n    tokens = get_suppressed_tokens(tokenizer, [-1])\n    assert tokens == (\n        1,\n        2,\n        7,\n        8,\n        9,\n        10,\n        14,\n        25,\n        26,\n        27,\n        28,\n        29,\n        31,\n        58,\n        59,\n        60,\n        61,\n        62,\n        63,\n        90,\n        91,\n        92,\n        93,\n        357,\n        366,\n        438,\n        532,\n        685,\n        705,\n        796,\n        930,\n        1058,\n        1220,\n        1267,\n        1279,\n        1303,\n        1343,\n        1377,\n        1391,\n        1635,\n        1782,\n        1875,\n        2162,\n        2361,\n        2488,\n        3467,\n        4008,\n        4211,\n        4600,\n        4808,\n        5299,\n        5855,\n        6329,\n        7203,\n        9609,\n        9959,\n        10563,\n        10786,\n        11420,\n        11709,\n        11907,\n        13163,\n        13697,\n        13700,\n        14808,\n        15306,\n        16410,\n        16791,\n        17992,\n        19203,\n        19510,\n        20724,\n        22305,\n        22935,\n        27007,\n        30109,\n        30420,\n        33409,\n        34949,\n        40283,\n        40493,\n        40549,\n        47282,\n        49146,\n        50257,\n        50357,\n        50358,\n        50359,\n        50360,\n        50361,\n    )\n\n\ndef test_suppressed_tokens_minus_value():\n    model = WhisperModel(\"tiny.en\")\n\n    tokenizer = Tokenizer(model.hf_tokenizer, False)\n    tokens = get_suppressed_tokens(tokenizer, [13])\n    assert tokens == (13, 50257, 50357, 50358, 50359, 50360, 50361)\n\n\ndef test_split_on_unicode():\n    model = WhisperModel(\"tiny\")\n    tokenizer = Tokenizer(model.hf_tokenizer, False)\n\n    tokens = [8404, 871, 287, 6, 246, 526, 3210, 20378]\n    words, word_tokens = tokenizer.split_tokens_on_unicode(tokens)\n\n    assert words == [\" elle\", \" est\", \" l\", \"'\", \"\\ufffd\", \"é\", \"rit\", \"oire\"]\n    assert word_tokens == [[8404], [871], [287], [6], [246], [526], [3210], [20378]]\n"
  },
  {
    "path": "tests/test_transcribe.py",
    "content": "import inspect\nimport os\n\nimport numpy as np\n\nfrom faster_whisper import BatchedInferencePipeline, WhisperModel, decode_audio\n\n\ndef test_supported_languages():\n    model = WhisperModel(\"tiny.en\")\n    assert model.supported_languages == [\"en\"]\n\n\ndef test_transcribe(jfk_path):\n    model = WhisperModel(\"tiny\")\n    segments, info = model.transcribe(jfk_path, word_timestamps=True)\n    assert info.all_language_probs is not None\n\n    assert info.language == \"en\"\n    assert info.language_probability > 0.9\n    assert info.duration == 11\n\n    # Get top language info from all results, which should match the\n    # already existing metadata\n    top_lang, top_lang_score = info.all_language_probs[0]\n    assert info.language == top_lang\n    assert abs(info.language_probability - top_lang_score) < 1e-16\n\n    segments = list(segments)\n\n    assert len(segments) == 1\n\n    segment = segments[0]\n\n    assert segment.text == (\n        \" And so my fellow Americans, ask not what your country can do for you, \"\n        \"ask what you can do for your country.\"\n    )\n\n    assert segment.text == \"\".join(word.word for word in segment.words)\n    assert segment.start == segment.words[0].start\n    assert segment.end == segment.words[-1].end\n    batched_model = BatchedInferencePipeline(model=model)\n    result, info = batched_model.transcribe(\n        jfk_path, word_timestamps=True, vad_filter=False\n    )\n    assert info.language == \"en\"\n    assert info.language_probability > 0.7\n    segments = []\n    for segment in result:\n        segments.append(\n            {\"start\": segment.start, \"end\": segment.end, \"text\": segment.text}\n        )\n\n    assert len(segments) == 1\n    assert segment.text == (\n        \" And so my fellow Americans ask not what your country can do for you, \"\n        \"ask what you can do for your country.\"\n    )\n\n\ndef test_batched_transcribe(physcisworks_path):\n    model = WhisperModel(\"tiny\")\n    batched_model = BatchedInferencePipeline(model=model)\n    result, info = batched_model.transcribe(physcisworks_path, batch_size=16)\n    assert info.language == \"en\"\n    assert info.language_probability > 0.7\n    segments = []\n    for segment in result:\n        segments.append(\n            {\"start\": segment.start, \"end\": segment.end, \"text\": segment.text}\n        )\n    # number of near 30 sec segments\n    assert len(segments) == 6\n\n    result, info = batched_model.transcribe(\n        physcisworks_path,\n        batch_size=16,\n        without_timestamps=False,\n        word_timestamps=True,\n    )\n    segments = []\n    for segment in result:\n        assert segment.words is not None\n        segments.append(\n            {\"start\": segment.start, \"end\": segment.end, \"text\": segment.text}\n        )\n    assert len(segments) > 7\n\n\ndef test_empty_audio():\n    audio = np.asarray([], dtype=\"float32\")\n    model = WhisperModel(\"tiny\")\n    pipeline = BatchedInferencePipeline(model=model)\n    assert list(model.transcribe(audio)[0]) == []\n    assert list(pipeline.transcribe(audio)[0]) == []\n    model.detect_language(audio)\n\n\ndef test_prefix_with_timestamps(jfk_path):\n    model = WhisperModel(\"tiny\")\n    segments, _ = model.transcribe(jfk_path, prefix=\"And so my fellow Americans\")\n    segments = list(segments)\n\n    assert len(segments) == 1\n\n    segment = segments[0]\n\n    assert segment.text == (\n        \" And so my fellow Americans, ask not what your country can do for you, \"\n        \"ask what you can do for your country.\"\n    )\n\n    assert segment.start == 0\n    assert 10 < segment.end <= 11\n\n\ndef test_vad(jfk_path):\n    model = WhisperModel(\"tiny\")\n    segments, info = model.transcribe(\n        jfk_path,\n        vad_filter=True,\n        vad_parameters=dict(min_silence_duration_ms=500, speech_pad_ms=200),\n    )\n    segments = list(segments)\n\n    assert len(segments) == 1\n    segment = segments[0]\n\n    assert segment.text == (\n        \" And so my fellow Americans ask not what your country can do for you, \"\n        \"ask what you can do for your country.\"\n    )\n\n    assert 0 < segment.start < 1\n    assert 10 < segment.end < 11\n\n    assert info.vad_options.min_silence_duration_ms == 500\n    assert info.vad_options.speech_pad_ms == 200\n\n\ndef test_stereo_diarization(data_dir):\n    model = WhisperModel(\"tiny\")\n\n    audio_path = os.path.join(data_dir, \"stereo_diarization.wav\")\n    left, right = decode_audio(audio_path, split_stereo=True)\n\n    segments, _ = model.transcribe(left)\n    transcription = \"\".join(segment.text for segment in segments).strip()\n    assert transcription == (\n        \"He began a confused complaint against the wizard, \"\n        \"who had vanished behind the curtain on the left.\"\n    )\n\n    segments, _ = model.transcribe(right)\n    transcription = \"\".join(segment.text for segment in segments).strip()\n    assert transcription == \"The horizon seems extremely distant.\"\n\n\ndef test_multilingual_transcription(data_dir):\n    model = WhisperModel(\"tiny\")\n    pipeline = BatchedInferencePipeline(model)\n\n    audio_path = os.path.join(data_dir, \"multilingual.mp3\")\n    audio = decode_audio(audio_path)\n\n    segments, info = model.transcribe(\n        audio,\n        multilingual=True,\n        without_timestamps=True,\n        condition_on_previous_text=False,\n    )\n    segments = list(segments)\n\n    assert (\n        segments[0].text\n        == \" Permission is hereby granted, free of charge, to any person obtaining a copy of the\"\n        \" software and associated documentation files to deal in the software without restriction,\"\n        \" including without limitation the rights to use, copy, modify, merge, publish, distribute\"\n        \", sublicence, and or cell copies of the software, and to permit persons to whom the \"\n        \"software is furnished to do so, subject to the following conditions. The above copyright\"\n        \" notice and this permission notice, shall be included in all copies or substantial \"\n        \"portions of the software.\"\n    )\n\n    assert (\n        segments[1].text\n        == \" Jedem, der dieses Software und die dazu gehöregen Dokumentationsdatein erhält, wird \"\n        \"hiermit unengeltlich die Genehmigung erteilt, wird der Software und eingeschränkt zu \"\n        \"verfahren. Dies umfasst insbesondere das Recht, die Software zu verwenden, zu \"\n        \"vervielfältigen, zu modifizieren, zu Samenzofügen, zu veröffentlichen, zu verteilen, \"\n        \"unterzulizenzieren und oder kopieren der Software zu verkaufen und diese Rechte \"\n        \"unterfolgen den Bedingungen anderen zu übertragen.\"\n    )\n\n    segments, info = pipeline.transcribe(audio, multilingual=True)\n    segments = list(segments)\n\n    assert (\n        segments[0].text\n        == \" Permission is hereby granted, free of charge, to any person obtaining a copy of the\"\n        \" software and associated documentation files to deal in the software without restriction,\"\n        \" including without limitation the rights to use, copy, modify, merge, publish, distribute\"\n        \", sublicence, and or cell copies of the software, and to permit persons to whom the \"\n        \"software is furnished to do so, subject to the following conditions. The above copyright\"\n        \" notice and this permission notice, shall be included in all copies or substantial \"\n        \"portions of the software.\"\n    )\n    assert (\n        \"Dokumentationsdatein erhält, wird hiermit unengeltlich die Genehmigung erteilt,\"\n        \" wird der Software und eingeschränkt zu verfahren. Dies umfasst insbesondere das Recht,\"\n        \" die Software zu verwenden, zu vervielfältigen, zu modifizieren\"\n        in segments[1].text\n    )\n\n\ndef test_hotwords(data_dir):\n    model = WhisperModel(\"tiny\")\n    pipeline = BatchedInferencePipeline(model)\n\n    audio_path = os.path.join(data_dir, \"hotwords.mp3\")\n    audio = decode_audio(audio_path)\n\n    segments, info = model.transcribe(audio, hotwords=\"ComfyUI\")\n    segments = list(segments)\n\n    assert \"ComfyUI\" in segments[0].text\n    assert info.transcription_options.hotwords == \"ComfyUI\"\n\n    segments, info = pipeline.transcribe(audio, hotwords=\"ComfyUI\")\n    segments = list(segments)\n\n    assert \"ComfyUI\" in segments[0].text\n    assert info.transcription_options.hotwords == \"ComfyUI\"\n\n\ndef test_transcribe_signature():\n    model_transcribe_args = set(inspect.getargs(WhisperModel.transcribe.__code__).args)\n    pipeline_transcribe_args = set(\n        inspect.getargs(BatchedInferencePipeline.transcribe.__code__).args\n    )\n    pipeline_transcribe_args.remove(\"batch_size\")\n\n    assert model_transcribe_args == pipeline_transcribe_args\n\n\ndef test_monotonic_timestamps(physcisworks_path):\n    model = WhisperModel(\"base\")\n    pipeline = BatchedInferencePipeline(model=model)\n\n    segments, info = model.transcribe(physcisworks_path, word_timestamps=True)\n    segments = list(segments)\n\n    for i in range(len(segments) - 1):\n        assert segments[i].start <= segments[i].end\n        assert segments[i].end <= segments[i + 1].start\n        for word in segments[i].words:\n            assert word.start <= word.end\n            assert word.end <= segments[i].end\n    assert segments[-1].end <= info.duration\n\n    segments, info = pipeline.transcribe(physcisworks_path, word_timestamps=True)\n    segments = list(segments)\n\n    for i in range(len(segments) - 1):\n        assert segments[i].start <= segments[i].end\n        assert segments[i].end <= segments[i + 1].start\n        for word in segments[i].words:\n            assert word.start <= word.end\n            assert word.end <= segments[i].end\n    assert segments[-1].end <= info.duration\n\n\ndef test_cliptimestamps_segments(jfk_path):\n    model = WhisperModel(\"tiny\")\n    pipeline = BatchedInferencePipeline(model=model)\n\n    audio = decode_audio(jfk_path)\n    audio = np.concatenate([audio, audio])\n    clip_timestamps = [{\"start\": 0.0, \"end\": 11.0}, {\"start\": 11.0, \"end\": 22.0}]\n\n    segments, info = pipeline.transcribe(audio, clip_timestamps=clip_timestamps)\n    segments = list(segments)\n\n    assert len(segments) == 2\n    for segment, clip in zip(segments, clip_timestamps):\n        assert segment.start == clip[\"start\"]\n        assert segment.end == clip[\"end\"]\n        assert segment.text == (\n            \" And so my fellow Americans ask not what your country can do for you, \"\n            \"ask what you can do for your country.\"\n        )\n\n\ndef test_cliptimestamps_timings(physcisworks_path):\n    model = WhisperModel(\"tiny\")\n    pipeline = BatchedInferencePipeline(model=model)\n\n    audio = decode_audio(physcisworks_path)\n    clip_timestamps = [{\"start\": 0.0, \"end\": 5.0}, {\"start\": 6.0, \"end\": 15.0}]\n    transcripts = [\n        \" Now I want to return to the conservation of mechanical energy.\",\n        (\n            \" I have here a pendulum. I have an object that weighs 15 kilograms\"\n            \" and I can lift it up one meter, which I have done now.\"\n        ),\n    ]\n    segments, info = pipeline.transcribe(audio, clip_timestamps=clip_timestamps)\n    segments = list(segments)\n\n    assert len(segments) == 2\n    for segment, clip, transcript in zip(segments, clip_timestamps, transcripts):\n        assert clip[\"start\"] == segment.start\n        assert clip[\"end\"] == segment.end\n        assert segment.text == transcript\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import os\n\nfrom faster_whisper import available_models, download_model\n\n\ndef test_available_models():\n    models = available_models()\n    assert isinstance(models, list)\n    assert \"tiny\" in models\n\n\ndef test_download_model(tmpdir):\n    output_dir = str(tmpdir.join(\"model\"))\n\n    model_dir = download_model(\"tiny\", output_dir=output_dir)\n\n    assert model_dir == output_dir\n    assert os.path.isdir(model_dir)\n    assert not os.path.islink(model_dir)\n\n    for filename in os.listdir(model_dir):\n        path = os.path.join(model_dir, filename)\n        assert not os.path.islink(path)\n\n\ndef test_download_model_in_cache(tmpdir):\n    cache_dir = str(tmpdir.join(\"model\"))\n    download_model(\"tiny\", cache_dir=cache_dir)\n    assert os.path.isdir(cache_dir)\n"
  }
]