Benchmark: add Device column (CPU/GPU/remote); case-sensitive accuracy
- BenchRow gains a device field; Transcriber records its resolved device, so local engines report CPU or GPU and remote engines show "remote". New Device column in the results table. - Accuracy is now case-sensitive by default (capitalisation counts) so an all-lowercase transcript no longer scores 100%. Punctuation is still ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1b0b4bbc1f
commit
4667144b72
@ -7,6 +7,7 @@ Benchmark tab to find the fastest and most accurate engine/model.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ from .routing import normalize
|
|||||||
class BenchRow:
|
class BenchRow:
|
||||||
engine: str
|
engine: str
|
||||||
model: str
|
model: str
|
||||||
|
device: str # "CPU" | "GPU" | "remote"
|
||||||
ok: bool
|
ok: bool
|
||||||
seconds: float
|
seconds: float
|
||||||
wer: float
|
wer: float
|
||||||
@ -26,6 +28,14 @@ class BenchRow:
|
|||||||
error: str = ""
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _tokens(text: str, case_sensitive: bool) -> list[str]:
|
||||||
|
"""Word tokens. case_sensitive keeps capitalisation (and accents); both
|
||||||
|
drop punctuation so only the words/spelling are compared."""
|
||||||
|
if not case_sensitive:
|
||||||
|
return normalize(text)
|
||||||
|
return re.sub(r"[^\w\s]", " ", text, flags=re.UNICODE).split()
|
||||||
|
|
||||||
|
|
||||||
def _edit_distance(a: list[str], b: list[str]) -> int:
|
def _edit_distance(a: list[str], b: list[str]) -> int:
|
||||||
m, n = len(a), len(b)
|
m, n = len(a), len(b)
|
||||||
dp = list(range(n + 1))
|
dp = list(range(n + 1))
|
||||||
@ -39,26 +49,36 @@ def _edit_distance(a: list[str], b: list[str]) -> int:
|
|||||||
return dp[n]
|
return dp[n]
|
||||||
|
|
||||||
|
|
||||||
def wer(reference: str, hypothesis: str) -> float:
|
def wer(reference: str, hypothesis: str, *, case_sensitive: bool = False) -> float:
|
||||||
"""Word error rate (0 = perfect). Text is normalized (case/punct-insensitive)."""
|
"""Word error rate (0 = perfect). case_sensitive makes capitalisation count."""
|
||||||
ref = normalize(reference)
|
ref = _tokens(reference, case_sensitive)
|
||||||
hyp = normalize(hypothesis)
|
hyp = _tokens(hypothesis, case_sensitive)
|
||||||
if not ref:
|
if not ref:
|
||||||
return 0.0 if not hyp else 1.0
|
return 0.0 if not hyp else 1.0
|
||||||
return _edit_distance(ref, hyp) / len(ref)
|
return _edit_distance(ref, hyp) / len(ref)
|
||||||
|
|
||||||
|
|
||||||
|
def _engine_device(engine, transcriber) -> str:
|
||||||
|
if engine.is_local:
|
||||||
|
return "GPU" if getattr(transcriber, "device", "cpu") == "cuda" else "CPU"
|
||||||
|
return "remote"
|
||||||
|
|
||||||
|
|
||||||
def run(engines, wav_path: Path, reference: str, *, language: str = "",
|
def run(engines, wav_path: Path, reference: str, *, language: str = "",
|
||||||
get_local_transcriber=None, progress=None) -> list[BenchRow]:
|
case_sensitive: bool = True, get_local_transcriber=None, progress=None) -> list[BenchRow]:
|
||||||
"""Benchmark each engine; calls progress(row) as each finishes."""
|
"""Benchmark each engine; calls progress(row) as each finishes.
|
||||||
|
|
||||||
|
Accuracy is case-sensitive by default so wrong capitalisation counts.
|
||||||
|
"""
|
||||||
rows: list[BenchRow] = []
|
rows: list[BenchRow] = []
|
||||||
for e in engines:
|
for e in engines:
|
||||||
tr = get_local_transcriber(e) if (e.is_local and get_local_transcriber) else None
|
tr = get_local_transcriber(e) if (e.is_local and get_local_transcriber) else None
|
||||||
res = stt.benchmark(e, wav_path, language=language, local_transcriber=tr)
|
res = stt.benchmark(e, wav_path, language=language, local_transcriber=tr)
|
||||||
w = wer(reference, res.text) if res.ok else 1.0
|
w = wer(reference, res.text, case_sensitive=case_sensitive) if res.ok else 1.0
|
||||||
row = BenchRow(
|
row = BenchRow(
|
||||||
engine=e.name,
|
engine=e.name,
|
||||||
model=e.model or ("local" if e.is_local else "(default)"),
|
model=e.model or ("local" if e.is_local else "(default)"),
|
||||||
|
device=_engine_device(e, tr),
|
||||||
ok=res.ok,
|
ok=res.ok,
|
||||||
seconds=res.seconds,
|
seconds=res.seconds,
|
||||||
wer=w,
|
wer=w,
|
||||||
|
|||||||
@ -5,6 +5,9 @@ Tabs:
|
|||||||
Engines - STT and LLM engine presets with online/offline status + STT test
|
Engines - STT and LLM engine presets with online/offline status + STT test
|
||||||
Input - input scheme, keys, quality gate
|
Input - input scheme, keys, quality gate
|
||||||
General - mic (with live level meter), output, language, notifications, autostart
|
General - mic (with live level meter), output, language, notifications, autostart
|
||||||
|
Benchmark- compare STT engines against a reference clip
|
||||||
|
Log - runtime log output
|
||||||
|
About - version, source, changelog, and license
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@ -21,7 +24,7 @@ gi.require_version("Gtk", "3.0")
|
|||||||
gi.require_version("Gdk", "3.0")
|
gi.require_version("Gdk", "3.0")
|
||||||
from gi.repository import Gdk, GLib, Gtk, Pango # noqa: E402
|
from gi.repository import Gdk, GLib, Gtk, Pango # noqa: E402
|
||||||
|
|
||||||
from . import audio, autostart, benchmark, llm, logbuffer, stt # noqa: E402
|
from . import __version__, audio, autostart, benchmark, llm, logbuffer, stt # noqa: E402
|
||||||
from .config import Config, save # noqa: E402
|
from .config import Config, save # noqa: E402
|
||||||
from .llm import LLMEngine # noqa: E402
|
from .llm import LLMEngine # noqa: E402
|
||||||
from .stt import STTEngine # noqa: E402
|
from .stt import STTEngine # noqa: E402
|
||||||
@ -190,6 +193,44 @@ def _page(nb: Gtk.Notebook, title: str) -> Gtk.Box:
|
|||||||
return box
|
return box
|
||||||
|
|
||||||
|
|
||||||
|
def _read_first(paths: list[Path], fallback: str = "Not available in this install.") -> str:
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
if path.exists():
|
||||||
|
return path.read_text(encoding="utf-8", errors="replace").strip()
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
|
def _app_paths() -> dict[str, list[Path]]:
|
||||||
|
pkg_dir = Path(__file__).resolve().parent
|
||||||
|
linux_dir = pkg_dir.parent
|
||||||
|
repo_dir = linux_dir.parent
|
||||||
|
return {
|
||||||
|
"changelog": [linux_dir / "CHANGELOG.md", Path("/opt/blitztext/CHANGELOG.md")],
|
||||||
|
"license": [repo_dir / "LICENSE", Path("/usr/share/doc/blitztext/copyright")],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _text_panel(text: str, *, monospace: bool = True, height: int = 180) -> Gtk.ScrolledWindow:
|
||||||
|
sw = Gtk.ScrolledWindow()
|
||||||
|
sw.set_min_content_height(height)
|
||||||
|
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
view = Gtk.TextView()
|
||||||
|
view.set_editable(False)
|
||||||
|
view.set_cursor_visible(False)
|
||||||
|
view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||||
|
view.set_left_margin(8)
|
||||||
|
view.set_right_margin(8)
|
||||||
|
view.set_top_margin(8)
|
||||||
|
view.set_bottom_margin(8)
|
||||||
|
view.set_monospace(monospace)
|
||||||
|
view.get_buffer().set_text(text)
|
||||||
|
sw.add(view)
|
||||||
|
return sw
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog:
|
class SettingsDialog:
|
||||||
def __init__(self, parent: Gtk.Window, cfg: Config, daemon=None):
|
def __init__(self, parent: Gtk.Window, cfg: Config, daemon=None):
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
@ -212,6 +253,7 @@ class SettingsDialog:
|
|||||||
self._build_general(_page(nb, "General"))
|
self._build_general(_page(nb, "General"))
|
||||||
self._build_benchmark(_page(nb, "Benchmark"))
|
self._build_benchmark(_page(nb, "Benchmark"))
|
||||||
self._build_log(_page(nb, "Log"))
|
self._build_log(_page(nb, "Log"))
|
||||||
|
self._build_about(_page(nb, "About"))
|
||||||
|
|
||||||
self._bind_entry = None
|
self._bind_entry = None
|
||||||
self._bind_pressed: list[str] = []
|
self._bind_pressed: list[str] = []
|
||||||
@ -241,7 +283,7 @@ class SettingsDialog:
|
|||||||
self.wf_desc = _labeled(form, "Description", _entry(placeholder="Short description shown in the panel"))
|
self.wf_desc = _labeled(form, "Description", _entry(placeholder="Short description shown in the panel"))
|
||||||
self.wf_keywords = _labeled(form, "Keywords (comma)", _entry(placeholder="nicer email, bessere email"))
|
self.wf_keywords = _labeled(form, "Keywords (comma)", _entry(placeholder="nicer email, bessere email"))
|
||||||
self.wf_hotkey = self._key_field(form, "Hotkey (optional)", "", placeholder="click Set, or e.g. <ctrl>+<alt>+e", width=130)
|
self.wf_hotkey = self._key_field(form, "Hotkey (optional)", "", placeholder="click Set, or e.g. <ctrl>+<alt>+e", width=130)
|
||||||
self.wf_mode = _labeled(form, "Mode", _combo(["transcribe", "rewrite"]))
|
self.wf_mode = _labeled(form, "Mode", _combo(["transcribe", "rewrite", "stream"]))
|
||||||
self.wf_model = _labeled(form, "LLM model (opt.)", _entry(placeholder="blank = use the active LLM engine's model"))
|
self.wf_model = _labeled(form, "LLM model (opt.)", _entry(placeholder="blank = use the active LLM engine's model"))
|
||||||
self.wf_temp = _labeled(form, "Temperature (opt.)", _entry(placeholder="blank = engine default (e.g. 0.3)"))
|
self.wf_temp = _labeled(form, "Temperature (opt.)", _entry(placeholder="blank = engine default (e.g. 0.3)"))
|
||||||
|
|
||||||
@ -262,7 +304,7 @@ class SettingsDialog:
|
|||||||
self.wf_desc.set_text(wf.description)
|
self.wf_desc.set_text(wf.description)
|
||||||
self.wf_keywords.set_text(", ".join(wf.keywords))
|
self.wf_keywords.set_text(", ".join(wf.keywords))
|
||||||
self.wf_hotkey.set_text(wf.hotkey)
|
self.wf_hotkey.set_text(wf.hotkey)
|
||||||
self.wf_mode.set_active(["transcribe", "rewrite"].index(wf.mode) if wf.mode in ("transcribe", "rewrite") else 0)
|
self.wf_mode.set_active(["transcribe", "rewrite", "stream"].index(wf.mode) if wf.mode in ("transcribe", "rewrite", "stream") else 0)
|
||||||
self.wf_model.set_text(wf.model or "")
|
self.wf_model.set_text(wf.model or "")
|
||||||
self.wf_temp.set_text("" if wf.temperature is None else str(wf.temperature))
|
self.wf_temp.set_text("" if wf.temperature is None else str(wf.temperature))
|
||||||
self.wf_prompt.get_buffer().set_text(wf.prompt)
|
self.wf_prompt.get_buffer().set_text(wf.prompt)
|
||||||
@ -332,17 +374,18 @@ class SettingsDialog:
|
|||||||
self.stt_dot = Gtk.Label(); self.stt_dot.set_markup(_dot(GREY))
|
self.stt_dot = Gtk.Label(); self.stt_dot.set_markup(_dot(GREY))
|
||||||
bar.pack_start(self.stt_dot, False, False, 0)
|
bar.pack_start(self.stt_dot, False, False, 0)
|
||||||
bar.pack_start(self.stt_combo, True, True, 0)
|
bar.pack_start(self.stt_combo, True, True, 0)
|
||||||
for label, cb in (("+ Add", self._stt_add), ("Delete", self._stt_delete),
|
for label, cb in (("+ Add", self._stt_add), ("+ Stream", self._stt_add_stream),
|
||||||
("Test", self._stt_test), ("Refresh", lambda _b: self._refresh_status())):
|
("Delete", self._stt_delete), ("Test", self._stt_test),
|
||||||
|
("Refresh", lambda _b: self._refresh_status())):
|
||||||
b = Gtk.Button(label=label); b.connect("clicked", cb); bar.pack_start(b, False, False, 0)
|
b = Gtk.Button(label=label); b.connect("clicked", cb); bar.pack_start(b, False, False, 0)
|
||||||
box.pack_start(bar, False, False, 2)
|
box.pack_start(bar, False, False, 2)
|
||||||
|
|
||||||
form = Gtk.Box(orientation=Gtk.Orientation.VERTICAL); box.pack_start(form, False, False, 2)
|
form = Gtk.Box(orientation=Gtk.Orientation.VERTICAL); box.pack_start(form, False, False, 2)
|
||||||
self.stt_name = _labeled(form, "Name", _entry(placeholder="e.g. faster-whisper GPU"))
|
self.stt_name = _labeled(form, "Name", _entry(placeholder="e.g. faster-whisper GPU"))
|
||||||
self.stt_type = _labeled(form, "Type", _combo(["local", "openai"]))
|
self.stt_type = _labeled(form, "Type", _combo(["local", "openai", "riva_realtime"]))
|
||||||
self.stt_url = _url_field(form, "URL", "http://localhost:8010/v1 (blank for local)",
|
self.stt_url = _url_field(form, "URL", "http://localhost:8010/v1 · realtime: http://localhost:8006/v1",
|
||||||
lambda: self._populate_models(self.stt_model, self.stt_url.get_text().strip(), self.stt_key.get_text().strip()))
|
lambda: self._populate_models(self.stt_model, self.stt_url.get_text().strip(), self.stt_key.get_text().strip()))
|
||||||
self.stt_model = _labeled(form, "Model", _model_combo("pick after entering URL · tiny/base/small… for local"))
|
self.stt_model = _labeled(form, "Model", _model_combo("blank = server default · tiny/base/small… for local"))
|
||||||
self.stt_key = _labeled(form, "API key env", _entry(placeholder="env var name, e.g. GROQ_API_KEY (optional)"))
|
self.stt_key = _labeled(form, "API key env", _entry(placeholder="env var name, e.g. GROQ_API_KEY (optional)"))
|
||||||
self.stt_url.connect("changed", lambda _e: self._schedule_models("stt"))
|
self.stt_url.connect("changed", lambda _e: self._schedule_models("stt"))
|
||||||
self.stt_key.connect("changed", lambda _e: self._schedule_models("stt"))
|
self.stt_key.connect("changed", lambda _e: self._schedule_models("stt"))
|
||||||
@ -395,13 +438,15 @@ class SettingsDialog:
|
|||||||
return
|
return
|
||||||
e = self.cfg.stt_engines[idx]
|
e = self.cfg.stt_engines[idx]
|
||||||
self.stt_name.set_text(e.name)
|
self.stt_name.set_text(e.name)
|
||||||
self.stt_type.set_active(["local", "openai"].index(e.type) if e.type in ("local", "openai") else 0)
|
self.stt_type.set_active(["local", "openai", "riva_realtime"].index(e.type) if e.type in ("local", "openai", "riva_realtime") else 0)
|
||||||
self.stt_url.set_text(e.url); self.stt_key.set_text(e.api_key_env)
|
self.stt_url.set_text(e.url); self.stt_key.set_text(e.api_key_env)
|
||||||
if e.type == "local":
|
if e.type == "local":
|
||||||
_fill_combo(self.stt_model, ["tiny", "base", "small", "medium", "large-v3"], e.model or self.cfg.model)
|
_fill_combo(self.stt_model, ["tiny", "base", "small", "medium", "large-v3"], e.model or self.cfg.model)
|
||||||
else:
|
elif e.type == "openai":
|
||||||
_fill_combo(self.stt_model, [], e.model)
|
_fill_combo(self.stt_model, [], e.model)
|
||||||
self._populate_models(self.stt_model, e.url, e.api_key_env)
|
self._populate_models(self.stt_model, e.url, e.api_key_env)
|
||||||
|
else:
|
||||||
|
_fill_combo(self.stt_model, [], e.model)
|
||||||
self._stt_idx = idx
|
self._stt_idx = idx
|
||||||
|
|
||||||
def _stt_commit(self) -> None:
|
def _stt_commit(self) -> None:
|
||||||
@ -431,6 +476,13 @@ class SettingsDialog:
|
|||||||
self.stt_combo.set_active(len(self.cfg.stt_engines) - 1)
|
self.stt_combo.set_active(len(self.cfg.stt_engines) - 1)
|
||||||
self._stt_load(len(self.cfg.stt_engines) - 1)
|
self._stt_load(len(self.cfg.stt_engines) - 1)
|
||||||
|
|
||||||
|
def _stt_add_stream(self, _b):
|
||||||
|
self._stt_commit()
|
||||||
|
e = STTEngine("Nemotron ASR Streaming", "riva_realtime", "http://127.0.0.1:8006/v1", "")
|
||||||
|
self.cfg.stt_engines.append(e); self.stt_combo.append_text(e.name)
|
||||||
|
self.stt_combo.set_active(len(self.cfg.stt_engines) - 1)
|
||||||
|
self._stt_load(len(self.cfg.stt_engines) - 1)
|
||||||
|
|
||||||
def _stt_delete(self, _b):
|
def _stt_delete(self, _b):
|
||||||
if len(self.cfg.stt_engines) <= 1:
|
if len(self.cfg.stt_engines) <= 1:
|
||||||
return
|
return
|
||||||
@ -441,6 +493,9 @@ class SettingsDialog:
|
|||||||
def _stt_test(self, _b):
|
def _stt_test(self, _b):
|
||||||
self._stt_commit()
|
self._stt_commit()
|
||||||
e = self.cfg.stt_engines[self._stt_idx]
|
e = self.cfg.stt_engines[self._stt_idx]
|
||||||
|
if e.is_streaming:
|
||||||
|
self.stt_result.set_markup("<i>Streaming engines are live-only. Use a preset with mode = stream.</i>")
|
||||||
|
return
|
||||||
self.stt_result.set_markup("<i>Recording 4s — speak now…</i>")
|
self.stt_result.set_markup("<i>Recording 4s — speak now…</i>")
|
||||||
threading.Thread(target=self._run_stt_test, args=(e,), daemon=True).start()
|
threading.Thread(target=self._run_stt_test, args=(e,), daemon=True).start()
|
||||||
|
|
||||||
@ -528,10 +583,13 @@ class SettingsDialog:
|
|||||||
self.llm_combo.set_active(0); self._llm_load(0)
|
self.llm_combo.set_active(0); self._llm_load(0)
|
||||||
|
|
||||||
def _stt_type_changed(self, _c) -> None:
|
def _stt_type_changed(self, _c) -> None:
|
||||||
if self.stt_type.get_active_text() == "local":
|
typ = self.stt_type.get_active_text()
|
||||||
|
if typ == "local":
|
||||||
_fill_combo(self.stt_model, ["tiny", "base", "small", "medium", "large-v3"], _combo_text(self.stt_model))
|
_fill_combo(self.stt_model, ["tiny", "base", "small", "medium", "large-v3"], _combo_text(self.stt_model))
|
||||||
else:
|
elif typ == "openai":
|
||||||
self._schedule_models("stt")
|
self._schedule_models("stt")
|
||||||
|
else:
|
||||||
|
_fill_combo(self.stt_model, [], _combo_text(self.stt_model))
|
||||||
|
|
||||||
# -- model dropdowns (fetched from {url}/models) ---
|
# -- model dropdowns (fetched from {url}/models) ---
|
||||||
def _populate_models(self, combo, url: str, key_env: str) -> None:
|
def _populate_models(self, combo, url: str, key_env: str) -> None:
|
||||||
@ -553,7 +611,7 @@ class SettingsDialog:
|
|||||||
|
|
||||||
def fire():
|
def fire():
|
||||||
setattr(self, attr, 0)
|
setattr(self, attr, 0)
|
||||||
if which == "stt" and self.stt_type.get_active_text() != "local":
|
if which == "stt" and self.stt_type.get_active_text() == "openai":
|
||||||
self._populate_models(self.stt_model, self.stt_url.get_text().strip(),
|
self._populate_models(self.stt_model, self.stt_url.get_text().strip(),
|
||||||
self.stt_key.get_text().strip())
|
self.stt_key.get_text().strip())
|
||||||
elif which == "llm":
|
elif which == "llm":
|
||||||
@ -685,12 +743,12 @@ class SettingsDialog:
|
|||||||
run.set_halign(Gtk.Align.START)
|
run.set_halign(Gtk.Align.START)
|
||||||
page.pack_start(run, False, False, 6)
|
page.pack_start(run, False, False, 6)
|
||||||
|
|
||||||
self.bench_store = Gtk.ListStore(str, str, str, str, str)
|
self.bench_store = Gtk.ListStore(str, str, str, str, str, str)
|
||||||
tree = Gtk.TreeView(model=self.bench_store)
|
tree = Gtk.TreeView(model=self.bench_store)
|
||||||
for title, i, expand in [("Engine", 0, False), ("Model", 1, False),
|
for title, i, expand in [("Engine", 0, False), ("Model", 1, False), ("Device", 2, False),
|
||||||
("Time (s)", 2, False), ("Accuracy", 3, False), ("Output", 4, True)]:
|
("Time (s)", 3, False), ("Accuracy", 4, False), ("Output", 5, True)]:
|
||||||
r = Gtk.CellRendererText()
|
r = Gtk.CellRendererText()
|
||||||
if i == 4:
|
if i == 5:
|
||||||
r.set_property("ellipsize", Pango.EllipsizeMode.END)
|
r.set_property("ellipsize", Pango.EllipsizeMode.END)
|
||||||
col = Gtk.TreeViewColumn(title, r, text=i); col.set_resizable(True)
|
col = Gtk.TreeViewColumn(title, r, text=i); col.set_resizable(True)
|
||||||
col.set_expand(expand)
|
col.set_expand(expand)
|
||||||
@ -724,7 +782,7 @@ class SettingsDialog:
|
|||||||
def _bench_add_row(self, row) -> bool:
|
def _bench_add_row(self, row) -> bool:
|
||||||
acc = f"{row.accuracy:.1f}%" if row.ok else "—"
|
acc = f"{row.accuracy:.1f}%" if row.ok else "—"
|
||||||
out = row.text if row.ok else f"⚠ {row.error}"
|
out = row.text if row.ok else f"⚠ {row.error}"
|
||||||
self.bench_store.append([row.engine, row.model, f"{row.seconds:.2f}", acc, out])
|
self.bench_store.append([row.engine, row.model, row.device, f"{row.seconds:.2f}", acc, out])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _bench_done(self, rows) -> bool:
|
def _bench_done(self, rows) -> bool:
|
||||||
@ -737,6 +795,44 @@ class SettingsDialog:
|
|||||||
f" · <b>Most accurate:</b> {GLib.markup_escape_text(acc.engine)} ({acc.accuracy:.1f}%)")
|
f" · <b>Most accurate:</b> {GLib.markup_escape_text(acc.engine)} ({acc.accuracy:.1f}%)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# ===== About ============================================================
|
||||||
|
def _build_about(self, page: Gtk.Box) -> None:
|
||||||
|
paths = _app_paths()
|
||||||
|
changelog = _read_first(paths["changelog"])
|
||||||
|
license_text = _read_first(paths["license"])
|
||||||
|
|
||||||
|
title = Gtk.Label(label="Blitztext", xalign=0.0)
|
||||||
|
title.set_markup("<b>Blitztext</b>")
|
||||||
|
page.pack_start(title, False, False, 0)
|
||||||
|
|
||||||
|
version = Gtk.Label(label=f"Version {__version__}", xalign=0.0)
|
||||||
|
version.set_selectable(True)
|
||||||
|
page.pack_start(version, False, False, 2)
|
||||||
|
|
||||||
|
source = Gtk.LinkButton.new_with_label(
|
||||||
|
"https://github.com/mARTin-B78/blitztext-app",
|
||||||
|
"Source: github.com/mARTin-B78/blitztext-app",
|
||||||
|
)
|
||||||
|
source.set_halign(Gtk.Align.START)
|
||||||
|
page.pack_start(source, False, False, 4)
|
||||||
|
|
||||||
|
license_label = Gtk.Label(label="License: MIT", xalign=0.0)
|
||||||
|
license_label.set_selectable(True)
|
||||||
|
page.pack_start(license_label, False, False, 2)
|
||||||
|
|
||||||
|
nb = Gtk.Notebook()
|
||||||
|
nb.set_margin_top(8)
|
||||||
|
page.pack_start(nb, True, True, 0)
|
||||||
|
|
||||||
|
changelog_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
changelog_box.pack_start(_text_panel(changelog, height=300), True, True, 0)
|
||||||
|
nb.append_page(changelog_box, Gtk.Label(label="Changelog"))
|
||||||
|
|
||||||
|
license_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
license_box.pack_start(_text_panel(license_text, height=300), True, True, 0)
|
||||||
|
nb.append_page(license_box, Gtk.Label(label="License"))
|
||||||
|
|
||||||
|
|
||||||
# ===== Log ==============================================================
|
# ===== Log ==============================================================
|
||||||
def _build_log(self, page: Gtk.Box) -> None:
|
def _build_log(self, page: Gtk.Box) -> None:
|
||||||
sw = Gtk.ScrolledWindow(); sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
sw = Gtk.ScrolledWindow(); sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class Transcriber:
|
|||||||
beam_size: int = 5,
|
beam_size: int = 5,
|
||||||
):
|
):
|
||||||
self.beam_size = beam_size
|
self.beam_size = beam_size
|
||||||
|
self.device = "cpu" # resolved actual device, set by _load
|
||||||
self._model = self._load(model, device, compute_type)
|
self._model = self._load(model, device, compute_type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -45,6 +46,7 @@ class Transcriber:
|
|||||||
log(f"Loading Whisper '{model}' on {dev} ({ct})… (first run may download the model)")
|
log(f"Loading Whisper '{model}' on {dev} ({ct})… (first run may download the model)")
|
||||||
m = WhisperModel(model, device=dev, compute_type=ct)
|
m = WhisperModel(model, device=dev, compute_type=ct)
|
||||||
log(f"Whisper '{model}' ready on {dev} ({ct})")
|
log(f"Whisper '{model}' ready on {dev} ({ct})")
|
||||||
|
self.device = dev
|
||||||
return m
|
return m
|
||||||
except Exception as exc: # noqa: BLE001 - CUDA libs may be absent
|
except Exception as exc: # noqa: BLE001 - CUDA libs may be absent
|
||||||
last_err = exc
|
last_err = exc
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user