generic-audio-transcriber 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- audio_transcriber/__init__.py +6 -0
- audio_transcriber/_model.py +32 -0
- audio_transcriber/cli.py +39 -0
- audio_transcriber/py.typed +0 -0
- audio_transcriber/result.py +36 -0
- audio_transcriber/transcriber.py +82 -0
- generic_audio_transcriber-0.1.0.dist-info/METADATA +164 -0
- generic_audio_transcriber-0.1.0.dist-info/RECORD +12 -0
- generic_audio_transcriber-0.1.0.dist-info/WHEEL +5 -0
- generic_audio_transcriber-0.1.0.dist-info/entry_points.txt +2 -0
- generic_audio_transcriber-0.1.0.dist-info/licenses/LICENSE +21 -0
- generic_audio_transcriber-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Lazy, cached loading of the speech recognition model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Any, Dict, Tuple
|
|
7
|
+
|
|
8
|
+
_cache: Dict[Tuple[str, str, str], Any] = {}
|
|
9
|
+
_lock = threading.Lock()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_model(name: str, device: str, compute_type: str) -> Any:
|
|
13
|
+
"""Return a loaded model, loading (and downloading) it only the first time.
|
|
14
|
+
|
|
15
|
+
``faster_whisper`` is imported here rather than at module level so that
|
|
16
|
+
importing this package stays instant and tests can run without it.
|
|
17
|
+
"""
|
|
18
|
+
key = (name, device, compute_type)
|
|
19
|
+
with _lock:
|
|
20
|
+
model = _cache.get(key)
|
|
21
|
+
if model is None:
|
|
22
|
+
from faster_whisper import WhisperModel
|
|
23
|
+
|
|
24
|
+
model = WhisperModel(name, device=device, compute_type=compute_type)
|
|
25
|
+
_cache[key] = model
|
|
26
|
+
return model
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def clear_cache() -> None:
|
|
30
|
+
"""Drop every loaded model so its memory can be reclaimed."""
|
|
31
|
+
with _lock:
|
|
32
|
+
_cache.clear()
|
audio_transcriber/cli.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Command line entry point: ``audio-transcriber FILE`` prints JSON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from .transcriber import DEFAULT_MODEL, TranscriptionError, transcribe
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="audio-transcriber",
|
|
15
|
+
description="Transcribe an audio file to text and print the result as JSON.",
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("audio", help="Path to an audio file, or '-' to read bytes from stdin.")
|
|
18
|
+
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Model size (default: {DEFAULT_MODEL}).")
|
|
19
|
+
parser.add_argument("--language", default=None, help="Language code such as 'pt'. Auto-detected if omitted.")
|
|
20
|
+
parser.add_argument("--text", action="store_true", help="Print only the plain text instead of JSON.")
|
|
21
|
+
return parser
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
25
|
+
args = build_parser().parse_args(argv)
|
|
26
|
+
audio = sys.stdin.buffer.read() if args.audio == "-" else args.audio
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
result = transcribe(audio, model=args.model, language=args.language)
|
|
30
|
+
except TranscriptionError as exc:
|
|
31
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
32
|
+
return 1
|
|
33
|
+
|
|
34
|
+
print(result.text if args.text else result.to_json(indent=2))
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Plain data types returned by :func:`audio_transcriber.transcribe`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from typing import Any, Dict, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Segment:
|
|
12
|
+
"""A timed piece of the transcript. Times are in seconds."""
|
|
13
|
+
|
|
14
|
+
start: float
|
|
15
|
+
end: float
|
|
16
|
+
text: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class TranscriptionResult:
|
|
21
|
+
"""The outcome of a transcription, easy to turn into JSON."""
|
|
22
|
+
|
|
23
|
+
text: str
|
|
24
|
+
language: str
|
|
25
|
+
language_probability: float
|
|
26
|
+
duration: float
|
|
27
|
+
segments: Tuple[Segment, ...] = field(default_factory=tuple)
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
30
|
+
data = asdict(self)
|
|
31
|
+
data["segments"] = [asdict(s) for s in self.segments]
|
|
32
|
+
return data
|
|
33
|
+
|
|
34
|
+
def to_json(self, **kwargs: Any) -> str:
|
|
35
|
+
kwargs.setdefault("ensure_ascii", False)
|
|
36
|
+
return json.dumps(self.to_dict(), **kwargs)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""The public ``transcribe`` function."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import os
|
|
7
|
+
from typing import BinaryIO, Optional, Union
|
|
8
|
+
|
|
9
|
+
from av.error import FFmpegError
|
|
10
|
+
|
|
11
|
+
from . import _model
|
|
12
|
+
from .result import Segment, TranscriptionResult
|
|
13
|
+
|
|
14
|
+
AudioInput = Union[bytes, bytearray, memoryview, str, "os.PathLike[str]", BinaryIO]
|
|
15
|
+
|
|
16
|
+
DEFAULT_MODEL = "small"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TranscriptionError(Exception):
|
|
20
|
+
"""Raised when the audio is empty or cannot be decoded."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _prepare_audio(audio: AudioInput) -> Union[str, BinaryIO]:
|
|
24
|
+
if isinstance(audio, (bytes, bytearray, memoryview)):
|
|
25
|
+
data = bytes(audio)
|
|
26
|
+
if not data:
|
|
27
|
+
raise TranscriptionError("Audio is empty.")
|
|
28
|
+
return io.BytesIO(data)
|
|
29
|
+
if isinstance(audio, (str, os.PathLike)):
|
|
30
|
+
return os.fspath(audio)
|
|
31
|
+
return audio
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def transcribe(
|
|
35
|
+
audio: AudioInput,
|
|
36
|
+
*,
|
|
37
|
+
model: str = DEFAULT_MODEL,
|
|
38
|
+
language: Optional[str] = None,
|
|
39
|
+
device: str = "cpu",
|
|
40
|
+
compute_type: str = "int8",
|
|
41
|
+
beam_size: int = 5,
|
|
42
|
+
vad_filter: bool = True,
|
|
43
|
+
) -> TranscriptionResult:
|
|
44
|
+
"""Convert speech to text.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
audio: Raw audio bytes (mp3, wav, webm, ogg, m4a, ...), a file path,
|
|
48
|
+
or a binary file-like object. The format is detected automatically.
|
|
49
|
+
model: Model size or name: ``tiny``, ``base``, ``small``, ``medium``,
|
|
50
|
+
``large-v3``, ... The model is downloaded on first use and cached.
|
|
51
|
+
language: ISO code such as ``"pt"`` or ``"en"``. ``None`` detects it.
|
|
52
|
+
device: ``"cpu"`` (works everywhere), ``"cuda"`` or ``"auto"``.
|
|
53
|
+
compute_type: Quantization, e.g. ``"int8"`` (light) or ``"float16"``.
|
|
54
|
+
beam_size: Higher is slightly more accurate and slower.
|
|
55
|
+
vad_filter: Skip silent parts, which also reduces hallucinated text.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
TranscriptionError: If the audio is empty or cannot be decoded.
|
|
59
|
+
"""
|
|
60
|
+
source = _prepare_audio(audio)
|
|
61
|
+
whisper = _model.get_model(model, device, compute_type)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
raw_segments, info = whisper.transcribe(
|
|
65
|
+
source,
|
|
66
|
+
language=language,
|
|
67
|
+
beam_size=beam_size,
|
|
68
|
+
vad_filter=vad_filter,
|
|
69
|
+
)
|
|
70
|
+
segments = tuple(
|
|
71
|
+
Segment(start=s.start, end=s.end, text=s.text.strip()) for s in raw_segments
|
|
72
|
+
)
|
|
73
|
+
except FFmpegError as exc:
|
|
74
|
+
raise TranscriptionError(f"Could not decode audio: {exc}") from exc
|
|
75
|
+
|
|
76
|
+
return TranscriptionResult(
|
|
77
|
+
text=" ".join(s.text for s in segments if s.text),
|
|
78
|
+
language=info.language,
|
|
79
|
+
language_probability=info.language_probability,
|
|
80
|
+
duration=info.duration,
|
|
81
|
+
segments=segments,
|
|
82
|
+
)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: generic-audio-transcriber
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in speech-to-text for any Python project: audio bytes in, JSON-ready text out.
|
|
5
|
+
Author: Eduardo Milani
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/EduardoMilani8/generic-audio-transcriber
|
|
8
|
+
Keywords: speech-to-text,transcription,whisper,audio,asr
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: faster-whisper>=1.0
|
|
16
|
+
Requires-Dist: av<19,>=11
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# generic-audio-transcriber
|
|
22
|
+
|
|
23
|
+
Drop-in speech-to-text for any Python project. Audio bytes in, JSON-ready text out.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from audio_transcriber import transcribe
|
|
27
|
+
|
|
28
|
+
result = transcribe(audio_bytes) # mp3, wav, webm, ogg, m4a... detected automatically
|
|
29
|
+
print(result.text) # "Hello, this is a test."
|
|
30
|
+
print(result.to_json()) # {"text": "...", "language": "en", ...}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
No API keys, no servers, no cloud. It runs locally on the CPU, and the model is
|
|
34
|
+
downloaded automatically the first time you use it.
|
|
35
|
+
|
|
36
|
+
## Why
|
|
37
|
+
|
|
38
|
+
Many apps need the same small feature: the user sends or records audio, and the app
|
|
39
|
+
needs the text. This package is that feature as one function call, so you can add it to
|
|
40
|
+
a project without learning anything about speech recognition.
|
|
41
|
+
|
|
42
|
+
Under the hood it uses [faster-whisper](https://github.com/SYSTRAN/faster-whisper)
|
|
43
|
+
(OpenAI's Whisper model), which is accurate, supports about 99 languages, and runs well
|
|
44
|
+
on a plain CPU. You never have to train or tune a model.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install git+https://github.com/EduardoMilani8/generic-audio-transcriber.git
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Requires Python 3.9+. You do **not** need to install ffmpeg: audio decoding is bundled.
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### From bytes (the main use case)
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from audio_transcriber import transcribe
|
|
60
|
+
|
|
61
|
+
result = transcribe(audio_bytes)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### From a file path or file object
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
transcribe("recording.mp3")
|
|
68
|
+
|
|
69
|
+
with open("recording.wav", "rb") as f:
|
|
70
|
+
transcribe(f)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Inside a web endpoint
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from fastapi import FastAPI, UploadFile
|
|
77
|
+
from audio_transcriber import transcribe
|
|
78
|
+
|
|
79
|
+
app = FastAPI()
|
|
80
|
+
|
|
81
|
+
@app.post("/transcribe")
|
|
82
|
+
async def endpoint(file: UploadFile):
|
|
83
|
+
return transcribe(await file.read()).to_dict()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### The result
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
result.text # full transcript
|
|
90
|
+
result.language # detected language code, e.g. "pt"
|
|
91
|
+
result.language_probability # confidence of the language detection
|
|
92
|
+
result.duration # audio length in seconds
|
|
93
|
+
result.segments # timed pieces: .start, .end, .text
|
|
94
|
+
result.to_dict() # plain dict
|
|
95
|
+
result.to_json() # JSON string (non-ASCII kept readable)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Silent audio gives an empty `text`, not an error.
|
|
99
|
+
|
|
100
|
+
## Options
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
transcribe(
|
|
104
|
+
audio,
|
|
105
|
+
model="small", # tiny | base | small | medium | large-v3
|
|
106
|
+
language=None, # "pt", "en", ... None = auto-detect
|
|
107
|
+
device="cpu", # "cpu" | "cuda" | "auto"
|
|
108
|
+
compute_type="int8", # "int8" is light; "float16" suits GPUs
|
|
109
|
+
beam_size=5,
|
|
110
|
+
vad_filter=True, # skip silence, reduces made-up text
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Choosing a model is a trade-off between weight and accuracy:
|
|
115
|
+
|
|
116
|
+
| Model | Size on disk | Speed | Accuracy |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| `tiny` | smallest | fastest | lowest |
|
|
119
|
+
| `base` | small | fast | fair |
|
|
120
|
+
| `small` (default) | medium | good | good |
|
|
121
|
+
| `medium` / `large-v3` | large | slow on CPU | best |
|
|
122
|
+
|
|
123
|
+
The model is loaded once per process and reused, so only the first call is slow.
|
|
124
|
+
Setting `language` explicitly is faster and more accurate than auto-detection.
|
|
125
|
+
|
|
126
|
+
## Errors
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from audio_transcriber import transcribe, TranscriptionError
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
transcribe(data)
|
|
133
|
+
except TranscriptionError as e:
|
|
134
|
+
... # empty input or audio that cannot be decoded
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Command line
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
audio-transcriber recording.mp3 # prints JSON
|
|
141
|
+
audio-transcriber recording.mp3 --text # prints plain text
|
|
142
|
+
audio-transcriber recording.mp3 --model base --language pt
|
|
143
|
+
cat recording.mp3 | audio-transcriber - # read bytes from stdin
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python -m venv .venv && source .venv/bin/activate
|
|
150
|
+
pip install -e ".[dev]"
|
|
151
|
+
pytest # fast unit tests, no model needed
|
|
152
|
+
RUN_SLOW=1 pytest -m slow # end-to-end tests, downloads the tiny model
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Notes
|
|
156
|
+
|
|
157
|
+
- The default device is `cpu` because it works on every machine. Pass `device="cuda"`
|
|
158
|
+
if you have a working CUDA setup and want GPU speed.
|
|
159
|
+
- `av` is pinned below version 19 because faster-whisper still uses an argument that
|
|
160
|
+
newer releases removed. The pin can be lifted once faster-whisper fixes it.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
audio_transcriber/__init__.py,sha256=csz215f2487u2IV6QZlleCPHdBO8UxR1g_WfsCjq2nQ,241
|
|
2
|
+
audio_transcriber/_model.py,sha256=D0eoBLKMuvWF0FCbVLuoaBj-pIDMBmU-mRD2M2Xa09o,958
|
|
3
|
+
audio_transcriber/cli.py,sha256=sdPMxvx-Xzv4DIQPJYGHcxJUf1Z7Uy6YrJUas2j1r3g,1385
|
|
4
|
+
audio_transcriber/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
audio_transcriber/result.py,sha256=ljMlkIA-nN3d4ya9GcnDkPLD-z-LHMMHZeTN1_q8zbs,928
|
|
6
|
+
audio_transcriber/transcriber.py,sha256=WPnkyVSyK6h0ECNDOUDR5vFIHCPkA-RqqorLYHAngpE,2646
|
|
7
|
+
generic_audio_transcriber-0.1.0.dist-info/licenses/LICENSE,sha256=kAmtq-Jj2Hm1y9c4SDWwgaWyptnE6aOXUpZ8tyGfpa0,1071
|
|
8
|
+
generic_audio_transcriber-0.1.0.dist-info/METADATA,sha256=4tKHk8w9KQ_xpLW6qGYP2xidgcObx8M_MLA-TpKi0S0,4857
|
|
9
|
+
generic_audio_transcriber-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
10
|
+
generic_audio_transcriber-0.1.0.dist-info/entry_points.txt,sha256=3KxjmWj9RBiMgOhhq0NRbxZmlJLSyUTLi42Vw9husOE,65
|
|
11
|
+
generic_audio_transcriber-0.1.0.dist-info/top_level.txt,sha256=1VoY9Hc8Rydt3ZgX1wBf0SeukQk-X2yjSr5nCAwAjm0,18
|
|
12
|
+
generic_audio_transcriber-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eduardo Milani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
audio_transcriber
|