Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ __pycache__/
*$py.class

# C extensions
*.so
*.so*

# Distribution / packaging
.Python
Expand Down Expand Up @@ -153,4 +153,6 @@ dmypy.json
# Pyre type checker
.pyre/

.vscode


2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ pybind11_add_module(_pywhispercpp
src/main.cpp
)

target_link_libraries (_pywhispercpp PRIVATE whisper)
target_link_libraries (_pywhispercpp PRIVATE whisper parakeet)

24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# pywhispercpp
Python bindings for [whisper.cpp](https://github.com/ggerganov/whisper.cpp) with a simple Pythonic API on top of it.
Python bindings for [whisper.cpp](https://github.com/ggerganov/whisper.cpp) with a simple Pythonic API on top of it. Supports both Whisper and Parakeet models.

[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Wheels](https://github.com/absadiki/pywhispercpp/actions/workflows/wheels.yml/badge.svg?branch=main&event=push)](https://github.com/absadiki/pywhispercpp/actions/workflows/wheels.yml)
Expand All @@ -15,6 +15,8 @@ Python bindings for [whisper.cpp](https://github.com/ggerganov/whisper.cpp) with
* [CoreML support](#coreml-support)
* [Vulkan support](#vulkan-support)
* [Quick start](#quick-start)
* [Whisper](#whisper)
* [Parakeet](#parakeet)
* [Examples](#examples)
* [CLI](#cli)
* [GUI](#gui)
Expand Down Expand Up @@ -103,28 +105,30 @@ Note that the toolkit for Ubuntu22 works on Ubuntu24

# Quick start

### Whisper

```python
from pywhispercpp.model import Model
from pywhispercpp import WhisperModel, WhisperParams

model = Model('base.en')
model = WhisperModel(WhisperModel.AvailableModels.BASE_EN, params=WhisperParams(n_threads=2))
segments = model.transcribe('file.wav')
for segment in segments:
print(segment.text)
```

You can also assign a custom `new_segment_callback`
### Parakeet

```python
from pywhispercpp.model import Model
from pywhispercpp import ParakeetModel, ParakeetParams

model = Model('base.en', print_realtime=False, print_progress=False)
segments = model.transcribe('file.mp3', new_segment_callback=print)
model = ParakeetModel(ParakeetModel.AvailableModels.TDT_0_6B_V3_Q4_0, params=ParakeetParams(n_threads=2))
segments = model.transcribe('file.wav')
for segment in segments:
print(segment.text)
```


* The model will be downloaded automatically, or you can use the path to a local model.
* You can pass any `whisper.cpp` [parameter](https://absadiki.github.io/pywhispercpp/#pywhispercpp.constants.PARAMS_SCHEMA) as a keyword argument to the `Model` class or to the `transcribe` function.
* Check the [Model](https://absadiki.github.io/pywhispercpp/#pywhispercpp.model.Model) class documentation for more details.
* Check the [WhisperModel](https://absadiki.github.io/pywhispercpp/#pywhispercpp.whisper_model.WhisperModel) and [ParakeetModel](https://absadiki.github.io/pywhispercpp/#pywhispercpp.parakeet_model.ParakeetModel) documentation for more details.

# Examples

Expand Down
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# PyWhisperCpp API Reference


::: pywhispercpp.whisper_model

::: pywhispercpp.parakeet_model

::: pywhispercpp.model

::: pywhispercpp.constants
Expand Down
4 changes: 3 additions & 1 deletion pywhispercpp/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@

from pywhispercpp.base import Segment
from pywhispercpp.whisper_model import WhisperModel, WhisperParams, WhisperContextParams
from pywhispercpp.parakeet_model import ParakeetModel, ParakeetParams, ParakeetContextParams
46 changes: 46 additions & 0 deletions pywhispercpp/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from abc import ABC, abstractmethod
from typing import List, Union

import numpy as np


class Segment:
"""
A small class representing a transcription segment
"""

def __init__(self, t0: int, t1: int, text: str, probability: float = np.nan):
"""
:param t0: start time
:param t1: end time
:param text: text
:param probability: Confidence score for the segment, computed as the geometric mean of
the token probabilities for the segment (NaN if not calculated).
This makes it interpretable as a probability in [0, 1].
"""
self.t0 = t0
self.t1 = t1
self.text = text
self.probability = probability

def __str__(self):
return f"t0={self.t0}, t1={self.t1}, text={self.text}, probability={self.probability}"

def __repr__(self):
return str(self)


class BaseModel(ABC):
"""
Abstract base class for all transcription models (whisper, parakeet, etc.).
Defines the public contract that every model must implement.
"""

@abstractmethod
def transcribe(self, media: Union[str, np.ndarray], **params) -> List[Segment]:
"""
Transcribe audio media and return a list of segments.
:param media: file path or numpy array of audio data
:return: list of Segment objects
"""
...
107 changes: 14 additions & 93 deletions pywhispercpp/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@
[whisper.cpp](https://github.com/ggerganov/whisper.cpp) API.
"""
import importlib.metadata
import subprocess
import os
import logging
import shutil
import sys
import tempfile
import wave
import warnings
from pathlib import Path
from time import time
from typing import Any, Union, Callable, List, TextIO, Tuple, Optional, Dict, TypedDict
Expand All @@ -21,6 +17,7 @@
import numpy as np
import pywhispercpp.constants as constants
import pywhispercpp.utils as utils
from pywhispercpp.base import BaseModel, Segment

__author__ = "absadiki"
__copyright__ = "Copyright 2023, "
Expand All @@ -43,36 +40,13 @@ class ContextParams(TypedDict, total=False):
_CONTEXT_PARAM_KEYS = frozenset(ContextParams.__annotations__)


class Segment:
"""
A small class representing a transcription segment
"""

def __init__(self, t0: int, t1: int, text: str, probability: float = np.nan):
"""
:param t0: start time
:param t1: end time
:param text: text
:param probability: Confidence score for the segment, computed as the geometric mean of
the token probabilities for the segment (NaN if not calculated).
This makes it interpretable as a probability in [0, 1].
"""
self.t0 = t0
self.t1 = t1
self.text = text
self.probability = probability

def __str__(self):
return f"t0={self.t0}, t1={self.t1}, text={self.text}, probability={self.probability}"

def __repr__(self):
return str(self)


class Model:
class Model(BaseModel):
"""
This classes defines a Whisper.cpp model.

.. deprecated::
Use :class:`pywhispercpp.whisper_model.WhisperModel` instead.

Example usage.
```python
model = Model('base.en', n_threads=6)
Expand All @@ -82,8 +56,6 @@ class Model:
```
"""



def __init__(self,
model: str = 'tiny',
models_dir: Optional[str] = None,
Expand Down Expand Up @@ -157,6 +129,11 @@ def __init__(self,
- `vad`: enable VAD. Default `False`.
- `vad_model_path`: path to the VAD model. Default `None`.
"""
warnings.warn(
"Model is deprecated, use pywhispercpp.whisper_model.WhisperModel instead.",
DeprecationWarning,
stacklevel=2,
)
self.model_path = utils.resolve_model_path(model, models_dir)
self._ctx = None
self._context_params = self._resolve_context_params(context_params)
Expand Down Expand Up @@ -204,7 +181,7 @@ def transcribe(self,
else:
if not Path(media).exists():
raise FileNotFoundError(media)
audio = self._load_audio(media)
audio = utils.load_audio(media, sample_rate=pw.WHISPER_SAMPLE_RATE)

# update params if any
self._set_params(params)
Expand Down Expand Up @@ -266,6 +243,7 @@ def _get_segments(ctx, start: int, end: int, extract_probability: bool = False)
res.append(Segment(t0, t1, text.strip(), probability=float(avg_prob)))
return res


def get_params(self) -> dict:
"""
Returns a `dict` representation of the actual params
Expand Down Expand Up @@ -435,63 +413,6 @@ def __call_new_segment_callback(self, ctx, n_new, user_data=None) -> None:
if self._new_segment_callback is not None:
self._new_segment_callback(segment)

@staticmethod
def _load_audio(media_file_path: str) -> np.ndarray:
"""
Helper method to return a `np.array` object from a media file
If the media file is not a WAV file, it will try to convert it using ffmpeg

:param media_file_path: Path of the media file
:return: Numpy array
"""

def wav_to_np(file_path):
with wave.open(file_path, 'rb') as wf:
num_channels = wf.getnchannels()
sample_width = wf.getsampwidth()
sample_rate = wf.getframerate()
num_frames = wf.getnframes()

if num_channels not in (1, 2):
raise Exception(f"WAV file must be mono or stereo")

if sample_rate != pw.WHISPER_SAMPLE_RATE:
raise Exception(f"WAV file must be {pw.WHISPER_SAMPLE_RATE} Hz")

if sample_width != 2:
raise Exception(f"WAV file must be 16-bit")

raw = wf.readframes(num_frames)
wf.close()
audio = np.frombuffer(raw, dtype=np.int16).astype(np.float32)
n = num_frames
if num_channels == 1:
pcmf32 = audio / 32768.0
else:
audio = audio.reshape(-1, 2)
# Averaging the two channels
pcmf32 = (audio[:, 0] + audio[:, 1]) / 65536.0
return pcmf32

if media_file_path.endswith('.wav'):
return wav_to_np(media_file_path)
else:
if shutil.which('ffmpeg') is None:
raise Exception(
"FFMPEG is not installed or not in PATH. Please install it, or provide a WAV file or a NumPy array instead!")

temp_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
temp_file_path = temp_file.name
temp_file.close()
try:
subprocess.run([
'ffmpeg', '-i', media_file_path, '-ac', '1', '-ar', '16000',
temp_file_path, '-y'
], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return wav_to_np(temp_file_path)
finally:
os.remove(temp_file_path)

def auto_detect_language(self, media: Union[str, np.ndarray], offset_ms: Optional[int] = None, n_threads: Optional[int] = None) -> Tuple[Tuple[str, np.float32], Dict[str, np.float32]]:
"""
Automatic language detection using whisper.cpp/whisper_pcm_to_mel and whisper.cpp/whisper_lang_auto_detect
Expand All @@ -506,7 +427,7 @@ def auto_detect_language(self, media: Union[str, np.ndarray], offset_ms: Optiona
else:
if not Path(media).exists():
raise FileNotFoundError(media)
audio = self._load_audio(media)
audio = utils.load_audio(media, sample_rate=pw.WHISPER_SAMPLE_RATE)

if offset_ms is None:
offset_ms = self._params.offset_ms
Expand Down
Loading
Loading