Skip to content

Commit b70e0e4

Browse files
committed
merge: v1.4 audit/robustness fixes from fix-1.4
Brings the 6 standalone v1.4 audit fixes onto master (no file overlap with master's recent commits → clean merge): - sortformer: guard empty-audio panic + panic-free sort - daemon: read timeout on accepted connections (prevents UI freeze when a client connects without sending its line) - meeting-live: real single-instance lock via flock - tray: track meeting-ui-open/meeting-recording in read_state - tray: About dialog showing the running version + i18n (6 locales)
2 parents 81be6f4 + 5134248 commit b70e0e4

17 files changed

Lines changed: 6399 additions & 13517 deletions

dictee-meeting-live

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A collapsible live preview accordion streams chunk-by-chunk transcript during
99
recording (bonus preview — the real analysis is done by dictee-transcribe at Stop).
1010
"""
1111

12+
import fcntl
1213
import json
1314
import os
1415
import re
@@ -31,8 +32,23 @@ from PyQt6.QtWidgets import (
3132
# Helpers
3233
# ---------------------------------------------------------------------------
3334

34-
def _singleton_path():
35-
return f"/tmp/dictee-meeting-live-{os.getuid()}.sock"
35+
def _acquire_singleton_lock():
36+
"""Ensure a single meeting window per user via an flock'd lock file.
37+
38+
The lock is released automatically when the process dies, so there is no
39+
stale-file problem — unlike the previous os.path.exists guard, which never
40+
triggered because nothing ever created the file it checked. Returns the
41+
open file object (keep it alive for the whole process lifetime) or None if
42+
another instance already holds the lock.
43+
"""
44+
lock_path = f"/tmp/dictee-meeting-live-{os.getuid()}.lock"
45+
fd = open(lock_path, "w")
46+
try:
47+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
48+
except OSError:
49+
fd.close()
50+
return None
51+
return fd
3652

3753

3854
def meeting_dir(timestamp=None):
@@ -897,8 +913,9 @@ def main():
897913
parser.add_argument("--stop", action="store_true", help="Stop running meeting")
898914
args = parser.parse_args()
899915

900-
sock_path = _singleton_path()
901-
if os.path.exists(sock_path):
916+
# Keep the lock fd alive for the whole process lifetime (released on exit).
917+
lock_fd = _acquire_singleton_lock()
918+
if lock_fd is None:
902919
print("[meeting-live] already running", file=sys.stderr)
903920
sys.exit(1)
904921

dictee-tray.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,8 @@ def read_state():
587587
with open(STATE_FILE, "r") as f:
588588
state = f.read().strip()
589589
if state in ("recording", "transcribing", "diarizing", "preparing",
590-
"diarize-ready", "switching", "offline"):
590+
"diarize-ready", "switching", "offline",
591+
"meeting-ui-open", "meeting-recording"):
591592
return state
592593
if state in ("cancelled", "idle"):
593594
return "idle"
@@ -596,6 +597,47 @@ def read_state():
596597
return None
597598

598599

600+
def _read_version():
601+
"""Return the version of the tray code that is actually running.
602+
603+
In a source checkout (dev: /usr/bin/dictee-tray is symlinked into the
604+
repo), resolve the symlink and read Cargo.toml + the current git short
605+
hash, so the About reflects the exact code running — not a previously
606+
installed package. In a real install, fall back to the
607+
/usr/share/dictee/VERSION file shipped by every package target.
608+
"""
609+
real_dir = os.path.dirname(os.path.realpath(__file__))
610+
cargo = os.path.join(real_dir, "Cargo.toml")
611+
if os.path.isfile(cargo):
612+
ver = None
613+
try:
614+
with open(cargo) as cf:
615+
for line in cf:
616+
if line.startswith("version"):
617+
ver = line.split("=", 1)[1].strip().strip('"')
618+
break
619+
except OSError:
620+
pass
621+
if ver:
622+
try:
623+
out = subprocess.run(
624+
["git", "-C", real_dir, "rev-parse", "--short", "HEAD"],
625+
capture_output=True, text=True, timeout=2,
626+
).stdout.strip()
627+
return f"{ver} (dev {out})" if out else f"{ver} (dev)"
628+
except Exception:
629+
return f"{ver} (dev)"
630+
for vpath in ("/usr/share/dictee/VERSION",
631+
os.path.join(real_dir, "VERSION")):
632+
if os.path.isfile(vpath):
633+
try:
634+
with open(vpath) as vf:
635+
return vf.read().strip()
636+
except OSError:
637+
pass
638+
return "dev"
639+
640+
599641
def _detect_backend():
600642
"""Détecte le backend à utiliser : 'appindicator' ou 'qt'."""
601643
desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").upper()
@@ -793,6 +835,10 @@ def _build_menu(self):
793835
item_reset.connect("activate", lambda _: self._reset())
794836
self.menu.append(item_reset)
795837

838+
item_about = Gtk.MenuItem(label=_("About Dictée"))
839+
item_about.connect("activate", lambda _: self._show_about_gtk())
840+
self.menu.append(item_about)
841+
796842
item_quit = Gtk.MenuItem(label=_("Quit icon"))
797843
item_quit.connect("activate", lambda _: self.Gtk.main_quit())
798844
self.menu.append(item_quit)
@@ -1022,6 +1068,19 @@ def _on_force_cpu_toggled_gtk(self, item):
10221068

10231069
def _delayed_daemon_refresh(self):
10241070
self._check_daemon()
1071+
1072+
def _show_about_gtk(self):
1073+
ver = _read_version()
1074+
dialog = self.Gtk.AboutDialog()
1075+
dialog.set_program_name("Dictée")
1076+
dialog.set_version(ver)
1077+
dialog.set_comments(
1078+
_("Voice dictation for Linux (Parakeet / Vosk / Whisper)."))
1079+
dialog.set_website("https://github.com/rcspam/dictee")
1080+
dialog.set_website_label("github.com/rcspam/dictee")
1081+
dialog.set_copyright("© rcspam")
1082+
dialog.run()
1083+
dialog.destroy()
10251084
self._check_state()
10261085
self._apply_state()
10271086
return False # one-shot
@@ -1236,6 +1295,7 @@ def _build_menu(self, QFont):
12361295
self.action_setup = self.menu.addAction(_("Configure Dictée"))
12371296
self.menu.addSeparator()
12381297
self.action_reset = self.menu.addAction(_("! Reset"))
1298+
self.action_about = self.menu.addAction(_("About Dictée"))
12391299
self.action_quit = self.menu.addAction(_("Quit icon"))
12401300
self.menu.triggered.connect(self._on_menu_triggered)
12411301
# Refresh toggles from dictee.conf each time the menu is about to show,
@@ -1281,9 +1341,25 @@ def _on_menu_triggered(self, action):
12811341
_spawn_detached(["dictee-setup"])
12821342
elif action == self.action_reset:
12831343
self._reset()
1344+
elif action == self.action_about:
1345+
self._show_about()
12841346
elif action == self.action_quit:
12851347
self.app.quit()
12861348

1349+
def _show_about(self):
1350+
from PyQt6.QtWidgets import QMessageBox
1351+
ver = _read_version()
1352+
QMessageBox.about(
1353+
None,
1354+
_("About Dictée"),
1355+
_("<b>Dictée</b> {ver}<br><br>"
1356+
"Voice dictation for Linux "
1357+
"(Parakeet / Vosk / Whisper).<br>"
1358+
"<a href='https://github.com/rcspam/dictee'>"
1359+
"github.com/rcspam/dictee</a><br><br>"
1360+
"Maintained by rcspam.").format(ver=ver),
1361+
)
1362+
12871363
def _on_activated(self, reason):
12881364
if reason == self.QSystemTrayIcon.ActivationReason.Trigger:
12891365
modifiers = self.QApplication.keyboardModifiers()

po/de.mo

117 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)