improved gui
This commit is contained in:
@ -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")
|
||||||
|
|
||||||
|
|||||||
@ -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.")
|
||||||
|
|||||||
Reference in New Issue
Block a user