improved gui

This commit is contained in:
2026-02-25 11:37:35 -07:00
parent 25b76d2451
commit 93ce9e417e
2 changed files with 50 additions and 16 deletions

View File

@ -18,11 +18,13 @@ from pathlib import Path
from kokoro import KPipeline from kokoro import KPipeline
# ── Config ───────────────────────────────────────────────────────────────────── # ── Config ─────────────────────────────────────────────────────────────────────
SOURCE_FILE = Path("Audio Master Nem Full.txt") _FIXED_FILE = Path("Audio Master Nem Full (TTS Fixed).txt")
OUTPUT_DIR = Path("output_audiobook") _ORIG_FILE = Path("Audio Master Nem Full.txt")
SAMPLE_RATE = 24000 SOURCE_FILE = _FIXED_FILE if _FIXED_FILE.exists() else _ORIG_FILE
SPEED = 1.0 OUTPUT_DIR = Path("output_audiobook")
LANG_CODE = "a" # 'a' = American English SAMPLE_RATE = 24000
SPEED = 1.0
LANG_CODE = "a" # 'a' = American English
# ── Available Kokoro voices (American English, lang_code='a') ────────────────── # ── Available Kokoro voices (American English, lang_code='a') ──────────────────
# af_heart warm American female [downloaded] # af_heart warm American female [downloaded]
@ -145,7 +147,9 @@ def main() -> None:
OUTPUT_DIR.mkdir(exist_ok=True) OUTPUT_DIR.mkdir(exist_ok=True)
print(f"\nParsing '{SOURCE_FILE}'") print(f"\nSource: '{SOURCE_FILE}'"
+ (" ✓ (TTS fixed)" if SOURCE_FILE == _FIXED_FILE else
" ⚠ (original — run 'Apply Fixes to Text' in the GUI to use phonetic fixes)"))
sections = load_and_split(SOURCE_FILE, BOOKS) sections = load_and_split(SOURCE_FILE, BOOKS)
print(f" Found {len(sections)} sections.\n") print(f" Found {len(sections)} sections.\n")

View File

@ -209,8 +209,8 @@ class ProperNounAuditor(tk.Tk):
self.manifest: dict[str, str] = manifest self.manifest: dict[str, str] = manifest
self.all_words: list[str] = sorted(manifest.keys(), key=str.casefold) self.all_words: list[str] = sorted(manifest.keys(), key=str.casefold)
# Persistent data # Persistent data — correct is newest-first; fixes dict preserves insertion order
self.correct: set[str] = set(load_json(CORRECT_FILE, [])) self.correct: list[str] = load_json(CORRECT_FILE, [])
self.fixes: dict[str, str] = load_json(FIXES_FILE, {}) self.fixes: dict[str, str] = load_json(FIXES_FILE, {})
self._build_ui() self._build_ui()
@ -364,6 +364,8 @@ class ProperNounAuditor(tk.Tk):
tk.Label(action_bar, text="", bg=BG3, fg=FG_DIM).pack(side="left", padx=4) tk.Label(action_bar, text="", bg=BG3, fg=FG_DIM).pack(side="left", padx=4)
styled_btn(action_bar, "⇄ Apply Fixes to Text", styled_btn(action_bar, "⇄ Apply Fixes to Text",
self._apply_fixes, color=YELLOW, bg=BG2).pack(side="left", padx=4) self._apply_fixes, color=YELLOW, bg=BG2).pack(side="left", padx=4)
styled_btn(action_bar, "⬇ Export Remaining",
self._export_remaining, color=BLUE, bg=BG2).pack(side="left", padx=4)
tk.Label(action_bar, text="", bg=BG3, fg=FG_DIM).pack(side="left", padx=4) tk.Label(action_bar, text="", bg=BG3, fg=FG_DIM).pack(side="left", padx=4)
self._pregen_btn = styled_btn( self._pregen_btn = styled_btn(
@ -378,7 +380,7 @@ class ProperNounAuditor(tk.Tk):
# ── Refresh helpers ──────────────────────────────────────────────────────── # ── Refresh helpers ────────────────────────────────────────────────────────
def _review_words(self) -> list[str]: def _review_words(self) -> list[str]:
excluded = self.correct | set(self.fixes.keys()) excluded = set(self.correct) | set(self.fixes.keys())
q = self.search_var.get().strip().casefold() q = self.search_var.get().strip().casefold()
words = [w for w in self.all_words if w not in excluded] words = [w for w in self.all_words if w not in excluded]
if q: if q:
@ -394,13 +396,13 @@ class ProperNounAuditor(tk.Tk):
def _refresh_correct(self) -> None: def _refresh_correct(self) -> None:
self.correct_lb.delete(0, "end") self.correct_lb.delete(0, "end")
for w in sorted(self.correct, key=str.casefold): for w in self.correct: # already newest-first
self.correct_lb.insert("end", f" {w}") self.correct_lb.insert("end", f" {w}")
self.correct_count_var.set(f"{len(self.correct)}") self.correct_count_var.set(f"{len(self.correct)}")
def _refresh_fixes(self) -> None: def _refresh_fixes(self) -> None:
self.fixes_lb.delete(0, "end") self.fixes_lb.delete(0, "end")
for orig, rep in sorted(self.fixes.items(), key=lambda x: x[0].casefold()): for orig, rep in reversed(list(self.fixes.items())): # newest-first
self.fixes_lb.insert("end", f" {orig}{rep}") self.fixes_lb.insert("end", f" {orig}{rep}")
self.fixes_count_var.set(f"{len(self.fixes)}") self.fixes_count_var.set(f"{len(self.fixes)}")
@ -556,21 +558,36 @@ class ProperNounAuditor(tk.Tk):
self.review_lb.event_generate("<<ListboxSelect>>") self.review_lb.event_generate("<<ListboxSelect>>")
def _advance_review(self, from_idx: int = 0) -> None: def _advance_review(self, from_idx: int = 0) -> None:
"""After an action, select the item that was at from_idx (or the last one).""" """Select the item at from_idx (clamped), positioned in the upper portion
of the viewport so the word doesn't end up in the bottom half unless
the list can't scroll any further down."""
size = self.review_lb.size() size = self.review_lb.size()
if size == 0: if size == 0:
return return
target = min(from_idx, size - 1) target = min(from_idx, size - 1)
self.review_lb.selection_clear(0, "end") self.review_lb.selection_clear(0, "end")
self.review_lb.selection_set(target) self.review_lb.selection_set(target)
# First call see() to let tk calculate the viewport, then reposition.
self.review_lb.see(target) self.review_lb.see(target)
self.review_lb.update_idletasks()
first, last = self.review_lb.yview()
visible_count = max(1, round((last - first) * size))
# Ideal top-of-viewport: put target ~1/4 down from the top
ideal_top = target - visible_count // 4
ideal_top = max(0, ideal_top)
self.review_lb.yview_moveto(ideal_top / size)
self.review_lb.event_generate("<<ListboxSelect>>") self.review_lb.event_generate("<<ListboxSelect>>")
def _mark_correct_word(self, word: str) -> None: def _mark_correct_word(self, word: str) -> None:
idx = self.review_lb.curselection() idx = self.review_lb.curselection()
from_idx = idx[0] if idx else 0 from_idx = idx[0] if idx else 0
self.correct.add(word) if word not in self.correct:
save_json(CORRECT_FILE, sorted(self.correct)) self.correct.insert(0, word)
save_json(CORRECT_FILE, self.correct)
self._fix_entry_word = "" self._fix_entry_word = ""
self.fix_var.set("") self.fix_var.set("")
self.now_playing_var.set("") self.now_playing_var.set("")
@ -588,6 +605,8 @@ class ProperNounAuditor(tk.Tk):
def _add_fix_for_word(self, word: str, replacement: str) -> None: def _add_fix_for_word(self, word: str, replacement: str) -> None:
idx = self.review_lb.curselection() idx = self.review_lb.curselection()
from_idx = idx[0] if idx else 0 from_idx = idx[0] if idx else 0
# Remove and re-add so updated entries bubble to the top
self.fixes.pop(word, None)
self.fixes[word] = replacement self.fixes[word] = replacement
save_json(FIXES_FILE, self.fixes) save_json(FIXES_FILE, self.fixes)
self._fix_entry_word = "" self._fix_entry_word = ""
@ -618,8 +637,9 @@ class ProperNounAuditor(tk.Tk):
self.fixes.pop(raw, None) self.fixes.pop(raw, None)
save_json(FIXES_FILE, self.fixes) save_json(FIXES_FILE, self.fixes)
else: else:
self.correct.discard(raw) if raw in self.correct:
save_json(CORRECT_FILE, sorted(self.correct)) self.correct.remove(raw)
save_json(CORRECT_FILE, self.correct)
self._refresh_all() self._refresh_all()
# ── Apply fixes to source text ───────────────────────────────────────────── # ── Apply fixes to source text ─────────────────────────────────────────────
@ -662,6 +682,16 @@ class ProperNounAuditor(tk.Tk):
threading.Thread(target=_run, daemon=True).start() threading.Thread(target=_run, daemon=True).start()
def _export_remaining(self) -> None:
words = self._review_words()
if not words:
messagebox.showinfo("Nothing to export", "No words left to review.")
return
out = OUTPUT_DIR / "remaining_review.txt"
out.write_text("\n".join(words), encoding="utf-8")
messagebox.showinfo("Exported",
f"{len(words)} words written to:\n{out}")
def _apply_fixes(self) -> None: def _apply_fixes(self) -> None:
if not self.fixes: if not self.fixes:
messagebox.showinfo("No fixes", "The Fixes list is empty.") messagebox.showinfo("No fixes", "The Fixes list is empty.")