whisper-dictation/whisper_app/vocab_window.py

242 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import tkinter as tk
from whisper_app import config as cfg
def open(root: tk.Tk) -> None:
"""Open the vocabulary window as a Toplevel of *root*."""
_open_main(root)
def _open_main(root: tk.Tk) -> None:
BG = "#18181f"
BG2 = "#22222c"
BG3 = "#2c2c38"
BORDER = "#38384a"
FG = "#e8e8f0"
FG2 = "#7878a0"
AMBER = "#f5a623"
AMBER2 = "#c8831a"
RED = "#f87171"
_mono = "Consolas" if os.name == "nt" else "monospace"
_sans = "Segoe UI" if os.name == "nt" else "sans-serif"
FONT = (_sans, 11)
FONT_B = (_sans, 11, "bold")
FONT_S = (_sans, 9)
FONT_H = (_sans, 14, "bold")
FONT_M = (_mono, 10)
win = tk.Toplevel(root)
win.title("Vokabular")
win.configure(bg=BG)
win.attributes("-topmost", True)
win.resizable(True, True)
win.minsize(600, 480)
win.option_add("*Menu.background", BG3)
win.option_add("*Menu.foreground", FG)
win.option_add("*Menu.activeBackground", AMBER)
win.option_add("*Menu.activeForeground", BG)
# ── Header ──
hdr = tk.Frame(win, bg=BG2)
hdr.pack(fill="x")
tk.Frame(hdr, bg=AMBER, height=3).pack(fill="x")
tk.Label(hdr, text="VOKABULAR & KORREKTUREN", font=FONT_H,
bg=BG2, fg=FG, pady=14).pack()
tk.Label(hdr, text="Wörter lernen · Ersetzungen definieren",
font=FONT_S, bg=BG2, fg=FG2).pack(pady=(0, 10))
content = tk.Frame(win, bg=BG, padx=28, pady=12)
content.pack(fill="both", expand=True)
# ── Add-word form ─────────────────────────────────────────────────────────
is_correction = tk.BooleanVar(value=False)
form = tk.Frame(content, bg=BG3, padx=16, pady=14)
form.pack(fill="x", pady=(0, 16))
# Toggle row
tog_row = tk.Frame(form, bg=BG3)
tog_row.pack(fill="x", pady=(0, 10))
tk.Label(tog_row, text="Korrektur (falsch → richtig)", font=FONT,
bg=BG3, fg=FG).pack(side="left")
def toggle_form(*_):
if is_correction.get():
entry_from.pack(side="left", padx=(0, 6))
arrow_lbl.pack(side="left", padx=4)
entry_to.pack(side="left")
entry_word.pack_forget()
else:
entry_word.pack(side="left", fill="x", expand=True)
entry_from.pack_forget()
arrow_lbl.pack_forget()
entry_to.pack_forget()
tog_btn = tk.Checkbutton(tog_row, variable=is_correction, command=toggle_form,
bg=BG3, fg=FG2, activebackground=BG3,
selectcolor=AMBER, relief="flat", bd=0,
indicatoron=True)
tog_btn.pack(side="right")
# Input row
inp_row = tk.Frame(form, bg=BG3)
inp_row.pack(fill="x")
entry_style = dict(font=FONT_M, bg=BG, fg=FG, insertbackground=AMBER,
relief="flat", bd=6, highlightbackground=BORDER, highlightthickness=1)
entry_word = tk.Entry(inp_row, width=32, **entry_style)
entry_word.insert(0, "")
entry_from = tk.Entry(inp_row, width=14, **entry_style)
arrow_lbl = tk.Label(inp_row, text="", font=("Segoe UI", 14), bg=BG3, fg=AMBER)
entry_to = tk.Entry(inp_row, width=14, **entry_style)
entry_word.pack(side="left", fill="x", expand=True)
def add_entry():
if is_correction.get():
frm = entry_from.get().strip()
to = entry_to.get().strip()
if frm and to:
cfg.vocab["replacements"].append({"from": frm, "to": to})
entry_from.delete(0, tk.END)
entry_to.delete(0, tk.END)
else:
w = entry_word.get().strip()
if w and w not in cfg.vocab["words"]:
cfg.vocab["words"].append(w)
entry_word.delete(0, tk.END)
cfg.save_vocab()
refresh_lists()
win.bind("<Return>", lambda _: add_entry())
add_btn = tk.Button(inp_row, text="Hinzufügen", command=add_entry,
bg=AMBER, fg=BG, font=FONT_B,
relief="flat", padx=14, pady=5, cursor="hand2", bd=0)
add_btn.pack(side="right", padx=(10, 0))
add_btn.bind("<Enter>", lambda _: add_btn.config(bg=AMBER2))
add_btn.bind("<Leave>", lambda _: add_btn.config(bg=AMBER))
# ── Lists ─────────────────────────────────────────────────────────────────
lists_frame = tk.Frame(content, bg=BG)
lists_frame.pack(fill="both", expand=True)
lists_frame.columnconfigure(0, weight=1)
lists_frame.columnconfigure(1, weight=2)
def section_label(parent, text):
tk.Label(parent, text=text, font=("Consolas", 9, "bold"),
bg=BG, fg=AMBER).pack(anchor="w", pady=(0, 6))
# Words column
col_w = tk.Frame(lists_frame, bg=BG)
col_w.grid(row=0, column=0, sticky="nsew", padx=(0, 12))
section_label(col_w, "WÖRTER")
words_box = tk.Listbox(col_w, font=FONT_M, bg=BG3, fg=FG,
selectbackground=AMBER, selectforeground=BG,
relief="flat", bd=0, highlightthickness=0,
activestyle="none", height=10)
words_box.pack(fill="both", expand=True)
def edit_word(_event=None):
sel = words_box.curselection()
if not sel:
return
word = cfg.vocab["words"].pop(sel[0])
cfg.save_vocab()
if is_correction.get():
is_correction.set(False)
toggle_form()
entry_word.delete(0, tk.END)
entry_word.insert(0, word)
entry_word.focus_set()
refresh_lists()
words_box.bind("<Double-1>", edit_word)
def del_word():
sel = words_box.curselection()
if sel:
cfg.vocab["words"].pop(sel[0])
cfg.save_vocab()
refresh_lists()
word_btns = tk.Frame(col_w, bg=BG)
word_btns.pack(anchor="e", pady=(4, 0))
tk.Button(word_btns, text="✎ Bearbeiten", command=edit_word,
bg=BG3, fg=AMBER, font=FONT_S, relief="flat",
padx=8, pady=3, cursor="hand2", bd=0).pack(side="left", padx=(0, 6))
tk.Button(word_btns, text=" Entfernen", command=del_word,
bg=BG3, fg=RED, font=FONT_S, relief="flat",
padx=8, pady=3, cursor="hand2", bd=0).pack(side="left")
# Replacements column
col_r = tk.Frame(lists_frame, bg=BG)
col_r.grid(row=0, column=1, sticky="nsew")
section_label(col_r, "KORREKTUREN")
repl_box = tk.Listbox(col_r, font=FONT_M, bg=BG3, fg=FG,
selectbackground=AMBER, selectforeground=BG,
relief="flat", bd=0, highlightthickness=0,
activestyle="none", height=10)
repl_box.pack(fill="both", expand=True)
def edit_repl(_event=None):
sel = repl_box.curselection()
if not sel:
return
repl = cfg.vocab["replacements"].pop(sel[0])
cfg.save_vocab()
if not is_correction.get():
is_correction.set(True)
toggle_form()
entry_from.delete(0, tk.END)
entry_from.insert(0, repl["from"])
entry_to.delete(0, tk.END)
entry_to.insert(0, repl["to"])
entry_from.focus_set()
refresh_lists()
repl_box.bind("<Double-1>", edit_repl)
def del_repl():
sel = repl_box.curselection()
if sel:
cfg.vocab["replacements"].pop(sel[0])
cfg.save_vocab()
refresh_lists()
repl_btns = tk.Frame(col_r, bg=BG)
repl_btns.pack(anchor="e", pady=(4, 0))
tk.Button(repl_btns, text="✎ Bearbeiten", command=edit_repl,
bg=BG3, fg=AMBER, font=FONT_S, relief="flat",
padx=8, pady=3, cursor="hand2", bd=0).pack(side="left", padx=(0, 6))
tk.Button(repl_btns, text=" Entfernen", command=del_repl,
bg=BG3, fg=RED, font=FONT_S, relief="flat",
padx=8, pady=3, cursor="hand2", bd=0).pack(side="left")
def refresh_lists():
words_box.delete(0, tk.END)
for w in cfg.vocab.get("words", []):
words_box.insert(tk.END, f" {w}")
repl_box.delete(0, tk.END)
for r in cfg.vocab.get("replacements", []):
repl_box.insert(tk.END, f" {r['from']}{r['to']}")
refresh_lists()
# ── Footer ──
tk.Frame(win, bg=BORDER, height=1).pack(fill="x")
tk.Label(win, text="Wörter fließen als Kontext in Whisper ein · Korrekturen werden nach der Transkription angewendet",
font=FONT_S, bg=BG2, fg=FG2, pady=8).pack()
# Center on screen after layout
win.update_idletasks()
sw = win.winfo_screenwidth()
sh = win.winfo_screenheight()
w = max(win.winfo_reqwidth(), 600)
h = min(max(win.winfo_reqheight(), 480), sh - 100)
win.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")