scribeio 0.1.4__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.
- scribeio/__init__.py +8 -0
- scribeio/check_report.py +298 -0
- scribeio/checks.py +404 -0
- scribeio/cli.py +982 -0
- scribeio/config.py +249 -0
- scribeio/craig.py +435 -0
- scribeio/defaults.yaml +83 -0
- scribeio/errors.py +34 -0
- scribeio/ffmpeg.py +338 -0
- scribeio/init.py +233 -0
- scribeio/master/__init__.py +1 -0
- scribeio/master/catalogue.py +63 -0
- scribeio/master/config.py +278 -0
- scribeio/master/excerpt.py +175 -0
- scribeio/master/measuring.py +44 -0
- scribeio/master/mix.py +410 -0
- scribeio/master/preview.py +339 -0
- scribeio/master/render.py +183 -0
- scribeio/progress.py +305 -0
- scribeio/templates/glossary.txt +17 -0
- scribeio/templates/scribe.yaml +31 -0
- scribeio/transcribe/__init__.py +5 -0
- scribeio/transcribe/cleanup.py +143 -0
- scribeio/transcribe/energy.py +102 -0
- scribeio/transcribe/glossary.py +135 -0
- scribeio/transcribe/lines.py +207 -0
- scribeio/transcribe/markdown.py +68 -0
- scribeio/transcribe/models.py +381 -0
- scribeio/transcribe/pipeline.py +155 -0
- scribeio/transcribe/report.py +95 -0
- scribeio/transcribe/session.py +103 -0
- scribeio/transcribe/session_json.py +109 -0
- scribeio/transcribe/whisper.py +534 -0
- scribeio-0.1.4.dist-info/METADATA +187 -0
- scribeio-0.1.4.dist-info/RECORD +39 -0
- scribeio-0.1.4.dist-info/WHEEL +5 -0
- scribeio-0.1.4.dist-info/entry_points.txt +2 -0
- scribeio-0.1.4.dist-info/licenses/LICENSE +21 -0
- scribeio-0.1.4.dist-info/top_level.txt +1 -0
scribeio/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""scribe — transcripts of TTRPG sessions recorded with Craig (Discord)."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("scribeio")
|
|
7
|
+
except PackageNotFoundError: # source tree without `pip install -e .`
|
|
8
|
+
__version__ = "0.0.0"
|
scribeio/check_report.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""The `scribe check` report: what scribe needs, what it finds, per use.
|
|
2
|
+
|
|
3
|
+
Offline: it reads the local caches only, unless asked to look up model
|
|
4
|
+
sizes and test the model sources (``--online``). The settings checked are the
|
|
5
|
+
``scribe.yaml`` given with ``-c``, else the one in the current folder, else
|
|
6
|
+
the defaults.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import platform
|
|
12
|
+
import sys
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from contextlib import AbstractContextManager, nullcontext
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from scribeio import __version__, checks, init
|
|
20
|
+
from scribeio.checks import Item
|
|
21
|
+
from scribeio.config import load_config
|
|
22
|
+
from scribeio.errors import ScribeError
|
|
23
|
+
from scribeio.progress import FIRST_RUN
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from scribeio.transcribe.models import Model
|
|
27
|
+
|
|
28
|
+
SYMBOLS = {"ok": "✔", "missing": "✘", "warning": "⚠", "info": "ℹ"}
|
|
29
|
+
NOT_INSTALLED = "not installed (audio mastering only)"
|
|
30
|
+
NONE_YET = "none yet: downloaded on your first transcription"
|
|
31
|
+
# Shown while WhisperX and PyTorch load: the slow part of a first check.
|
|
32
|
+
LOADING = f"loading WhisperX and PyTorch {FIRST_RUN}"
|
|
33
|
+
# Model source -> the host its downloads come from (checks.SOURCES)
|
|
34
|
+
SOURCE_HOSTS = {"huggingface": "huggingface.co", "torchaudio": "download.pytorch.org"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Checked:
|
|
39
|
+
"""The settings the report checks.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
path: The config file, or None when there is none.
|
|
43
|
+
settings: Its settings; the defaults without a config or when it
|
|
44
|
+
does not load.
|
|
45
|
+
error: Why the config does not load, or ``""``.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
path: Path | None
|
|
49
|
+
settings: dict[str, Any]
|
|
50
|
+
error: str = ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Group:
|
|
55
|
+
"""One use (audio mastering, transcription): its verdict and items."""
|
|
56
|
+
|
|
57
|
+
title: str
|
|
58
|
+
verdict: str
|
|
59
|
+
items: list[Item]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_settings(config: Path | None, cwd: Path) -> Checked:
|
|
63
|
+
"""The config from ``-c``, else ``scribe.yaml`` in `cwd`, else defaults."""
|
|
64
|
+
path = config or init.find_config(cwd)
|
|
65
|
+
if path is None:
|
|
66
|
+
return Checked(None, load_config().settings)
|
|
67
|
+
try:
|
|
68
|
+
return Checked(path, load_config(path).settings)
|
|
69
|
+
except ScribeError as e:
|
|
70
|
+
return Checked(path, load_config().settings, str(e))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def header() -> str:
|
|
74
|
+
"""scribeio and Python versions, and how it was installed."""
|
|
75
|
+
route = "installed with uv" if checks.uv_install() else f"in {sys.prefix}"
|
|
76
|
+
return f"scribeio {__version__}, Python {platform.python_version()} ({route})"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def audio_group(settings: dict[str, Any]) -> Group:
|
|
80
|
+
"""ffmpeg, and the encoder of the configured output format."""
|
|
81
|
+
from scribeio.master.config import BITRATES
|
|
82
|
+
|
|
83
|
+
found = checks.ffmpeg_item()
|
|
84
|
+
items = [found]
|
|
85
|
+
if found.status != "missing":
|
|
86
|
+
fmt = settings["master"]["format"]
|
|
87
|
+
bitrate = settings["master"]["bitrate"] or BITRATES[fmt]
|
|
88
|
+
items.append(checks.encoder_item(fmt, bitrate))
|
|
89
|
+
return Group("Audio mastering", _verdict(items), items)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def transcription_group(settings: dict[str, Any], ffmpeg: Item) -> Group:
|
|
93
|
+
"""WhisperX, PyTorch and the GPU for the configured device.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
settings: The checked settings (``transcribe.whisper.device``).
|
|
97
|
+
ffmpeg: The ffmpeg item: transcription decodes audio with ffmpeg.
|
|
98
|
+
"""
|
|
99
|
+
wx = checks.whisperx_item()
|
|
100
|
+
if wx.message == checks.WHISPERX_NOT_INSTALLED:
|
|
101
|
+
return Group("Transcription", NOT_INSTALLED, [wx])
|
|
102
|
+
items = [wx]
|
|
103
|
+
if wx.status != "missing":
|
|
104
|
+
device = settings["transcribe"]["whisper"]["device"]
|
|
105
|
+
torch = checks.torch_item(device)
|
|
106
|
+
items.append(torch)
|
|
107
|
+
# with a CPU-only PyTorch the GPU's reason would repeat PyTorch's
|
|
108
|
+
if torch.status != "missing":
|
|
109
|
+
items.append(checks.gpu_item(device))
|
|
110
|
+
if ffmpeg.status != "missing":
|
|
111
|
+
# the detail starts with the version ("9.0.2 C:\...\ffmpeg.exe")
|
|
112
|
+
info = checks.audio_library_item(ffmpeg.detail.split(" ")[0])
|
|
113
|
+
if info is not None:
|
|
114
|
+
items.append(info)
|
|
115
|
+
verdict = "not ready" if ffmpeg.status == "missing" else _verdict(items)
|
|
116
|
+
return Group("Transcription", verdict, items)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _verdict(items: list[Item]) -> str:
|
|
120
|
+
return "not ready" if any(i.status == "missing" for i in items) else "ready"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def group_lines(group: Group) -> list[str]:
|
|
124
|
+
"""The title line with the verdict, then one line per item."""
|
|
125
|
+
lines = [f"{group.title:<53}{group.verdict}"]
|
|
126
|
+
if group.verdict == NOT_INSTALLED:
|
|
127
|
+
[wx] = group.items
|
|
128
|
+
return [
|
|
129
|
+
*lines,
|
|
130
|
+
" " + SYMBOLS["info"] + " " + checks.line(wx),
|
|
131
|
+
" see " + checks.guide_url("transcription"),
|
|
132
|
+
]
|
|
133
|
+
for item in group.items:
|
|
134
|
+
lines.append(" " + SYMBOLS[item.status] + " " + checks.line(item))
|
|
135
|
+
if item.status == "missing":
|
|
136
|
+
lines.append(" see " + checks.guide_url(item.guide))
|
|
137
|
+
return lines
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def needed_models(checked: Checked, whisperx: bool) -> tuple[list[Model], str]:
|
|
141
|
+
"""The models the checked config needs, or why they cannot be named.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
The models and ``""``; no models without a config that loads or
|
|
145
|
+
without WhisperX; no models and the error for an unknown model.
|
|
146
|
+
"""
|
|
147
|
+
from scribeio.transcribe import models
|
|
148
|
+
|
|
149
|
+
if checked.path is None or checked.error or not whisperx:
|
|
150
|
+
return [], ""
|
|
151
|
+
try:
|
|
152
|
+
return models.needed(checked.settings["transcribe"]["whisper"]), ""
|
|
153
|
+
except ScribeError as e:
|
|
154
|
+
return [], str(e)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def model_lines(
|
|
158
|
+
checked: Checked,
|
|
159
|
+
cwd: Path,
|
|
160
|
+
whisperx: bool,
|
|
161
|
+
needed: tuple[list[Model], str],
|
|
162
|
+
online: bool = False,
|
|
163
|
+
reachable: frozenset[str] = frozenset(),
|
|
164
|
+
) -> list[str]:
|
|
165
|
+
"""The models on disk (with WhisperX loaded) and the settings checked.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
checked: The settings checked.
|
|
169
|
+
cwd: The current folder, named when it holds no config.
|
|
170
|
+
whisperx: True if WhisperX loaded; models are listed only then.
|
|
171
|
+
needed: `needed_models` for these settings.
|
|
172
|
+
online: Add the download size of the missing models.
|
|
173
|
+
reachable: Model sources that answered; sizes are looked up only
|
|
174
|
+
there (a dropped connection would stall each lookup).
|
|
175
|
+
"""
|
|
176
|
+
from scribeio.transcribe import models
|
|
177
|
+
|
|
178
|
+
lines = []
|
|
179
|
+
if whisperx:
|
|
180
|
+
ready, damaged = models.local_alignment()
|
|
181
|
+
lines += [
|
|
182
|
+
f"Models ({models.cache_folder()})",
|
|
183
|
+
" Whisper: " + (", ".join(models.cached_whisper_models()) or NONE_YET),
|
|
184
|
+
" Alignment: " + (", ".join(ready) or NONE_YET),
|
|
185
|
+
]
|
|
186
|
+
lines += [
|
|
187
|
+
f" ⚠ alignment {language}: damaged; replaced on the next transcription"
|
|
188
|
+
for language in damaged
|
|
189
|
+
]
|
|
190
|
+
else:
|
|
191
|
+
lines.append("Settings") # no Models block to hold the settings lines
|
|
192
|
+
if checked.path is None:
|
|
193
|
+
return [
|
|
194
|
+
*lines,
|
|
195
|
+
f" No {init.CONFIG_NAME} in {cwd} (the folder this terminal is in).",
|
|
196
|
+
# with the recording: a bare `scribe init` writes example accounts
|
|
197
|
+
" To set up a session: go to the folder with your recording and run "
|
|
198
|
+
'scribe init "<your recording>.zip"',
|
|
199
|
+
]
|
|
200
|
+
# absolute(), not resolve(): a mapped drive (Z:\) would become \\server\...
|
|
201
|
+
path = checked.path.absolute()
|
|
202
|
+
found, error = needed
|
|
203
|
+
error = checked.error or error
|
|
204
|
+
if error:
|
|
205
|
+
# config errors start with the file name ("scribe.yaml:3: ..."):
|
|
206
|
+
# the full path takes its place, so the name is not said twice
|
|
207
|
+
if error.startswith(path.name):
|
|
208
|
+
text = str(path) + error.removeprefix(path.name)
|
|
209
|
+
else:
|
|
210
|
+
text = f"{path}: {error}"
|
|
211
|
+
return [*lines, f" ⚠ Settings checked: {text}"]
|
|
212
|
+
lines.append(f" Settings checked: {path}")
|
|
213
|
+
if not found: # without WhisperX the models cannot be named
|
|
214
|
+
return lines
|
|
215
|
+
|
|
216
|
+
def need(m: Model) -> str:
|
|
217
|
+
name = f"alignment {m.language}" if m.kind == "alignment model" else m.name
|
|
218
|
+
if m.cached:
|
|
219
|
+
return f"{name} ✔"
|
|
220
|
+
size = ""
|
|
221
|
+
if online:
|
|
222
|
+
nbytes = models.size(m) if SOURCE_HOSTS.get(m.source) in reachable else None
|
|
223
|
+
size = f"{models.amount(nbytes)}, "
|
|
224
|
+
return f"{name} ↓ ({size}downloaded on first transcription)"
|
|
225
|
+
|
|
226
|
+
return [*lines, " needs " + ", ".join(need(m) for m in found)]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def connection_items(needed: list[Model]) -> list[Item]:
|
|
230
|
+
"""Whether the model sources answer: Hugging Face always, PyTorch's
|
|
231
|
+
download site when a needed alignment model comes from there."""
|
|
232
|
+
hosts = ["huggingface.co"]
|
|
233
|
+
if any(m.source == "torchaudio" for m in needed):
|
|
234
|
+
hosts.append("download.pytorch.org")
|
|
235
|
+
return [checks.reach(host, checks.SOURCES[host]) for host in hosts]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def connection_lines(items: list[Item]) -> list[str]:
|
|
239
|
+
"""The Connection block, with a hint when a source does not answer."""
|
|
240
|
+
lines = ["Connection"]
|
|
241
|
+
lines += [" " + SYMBOLS[i.status] + " " + checks.line(i) for i in items]
|
|
242
|
+
if any(i.status != "ok" for i in items):
|
|
243
|
+
lines += [
|
|
244
|
+
" " + SYMBOLS["info"] + " " + checks.CONNECTION_INFO,
|
|
245
|
+
" see " + checks.guide_url("connection"),
|
|
246
|
+
]
|
|
247
|
+
return lines
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def report(
|
|
251
|
+
config: Path | None,
|
|
252
|
+
cwd: Path,
|
|
253
|
+
online: bool,
|
|
254
|
+
show: Callable[[list[str]], None] | None = None,
|
|
255
|
+
busy: Callable[[str], AbstractContextManager[None]] | None = None,
|
|
256
|
+
) -> tuple[list[str], int]:
|
|
257
|
+
"""The whole report and the exit code (1 if a use is not ready).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
config: The ``-c`` config, or None.
|
|
261
|
+
cwd: The current folder (its ``scribe.yaml`` is checked without -c).
|
|
262
|
+
online: Also look up missing model sizes and test the model sources;
|
|
263
|
+
neither changes the exit code.
|
|
264
|
+
show: Called with each section's lines as soon as it is done, so the
|
|
265
|
+
instant parts show while the slow ones run.
|
|
266
|
+
busy: Wraps each slow part, given what it does (e.g. `LOADING`).
|
|
267
|
+
"""
|
|
268
|
+
lines: list[str] = []
|
|
269
|
+
|
|
270
|
+
def add(part: list[str]) -> None:
|
|
271
|
+
lines.extend(part)
|
|
272
|
+
if show is not None:
|
|
273
|
+
show(part)
|
|
274
|
+
|
|
275
|
+
def wait(label: str) -> AbstractContextManager[None]:
|
|
276
|
+
return busy(label) if busy is not None else nullcontext()
|
|
277
|
+
|
|
278
|
+
checked = load_settings(config, cwd)
|
|
279
|
+
audio = audio_group(checked.settings)
|
|
280
|
+
add([header(), "", *group_lines(audio), ""])
|
|
281
|
+
with wait(LOADING):
|
|
282
|
+
transcription = transcription_group(checked.settings, audio.items[0])
|
|
283
|
+
add([*group_lines(transcription), ""])
|
|
284
|
+
whisperx = transcription.items[0].status != "missing"
|
|
285
|
+
with wait("checking the models"):
|
|
286
|
+
needed = needed_models(checked, whisperx)
|
|
287
|
+
# tested first, so the size lookups skip a source that does not answer
|
|
288
|
+
connection: list[Item] = []
|
|
289
|
+
if online:
|
|
290
|
+
with wait("testing the model sources"):
|
|
291
|
+
connection = connection_items(needed[0])
|
|
292
|
+
reachable = frozenset(i.name for i in connection if i.status == "ok")
|
|
293
|
+
with wait("checking the models"):
|
|
294
|
+
add(model_lines(checked, cwd, whisperx, needed, online, reachable))
|
|
295
|
+
if online:
|
|
296
|
+
add(["", *connection_lines(connection)])
|
|
297
|
+
code = 1 if "not ready" in (audio.verdict, transcription.verdict) else 0
|
|
298
|
+
return lines, code
|