Skip to content

Commit 65d08df

Browse files
committed
Added a new Lexicon feature.
1 parent 1dd048f commit 65d08df

2 files changed

Lines changed: 106 additions & 3 deletions

File tree

gui.py

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ def load_settings(self):
187187
"combine": True,
188188
"export_subtitles": False,
189189
"normalize": False,
190-
"trim": False
190+
"trim": False,
191+
"lexicon": {}
191192
}
192193
if os.path.exists(CONFIG_FILE):
193194
try:
@@ -677,6 +678,75 @@ def on_lang_ui_change(choice):
677678

678679
ctk.CTkLabel(thread_frame, text="(More threads = High RAM usage)", text_color="orange").pack(side="left", padx=10)
679680

681+
def build_lexicon_tab(self, parent):
682+
parent.grid_columnconfigure(0, weight=1)
683+
parent.grid_rowconfigure(1, weight=1) # List area expands
684+
685+
# 1. Add New Entry
686+
add_frame = ctk.CTkFrame(parent)
687+
add_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=10)
688+
689+
ctk.CTkLabel(add_frame, text="Original Text:").pack(side="left", padx=5)
690+
self.lex_orig_var = ctk.StringVar()
691+
ctk.CTkEntry(add_frame, textvariable=self.lex_orig_var, width=150).pack(side="left", padx=5)
692+
693+
ctk.CTkLabel(add_frame, text="Replacement:").pack(side="left", padx=5)
694+
self.lex_replace_var = ctk.StringVar()
695+
ctk.CTkEntry(add_frame, textvariable=self.lex_replace_var, width=150).pack(side="left", padx=5)
696+
697+
ctk.CTkButton(add_frame, text="Add Rule", command=self.add_lexicon_rule).pack(side="left", padx=10)
698+
699+
# 2. List
700+
self.lex_list_frame = ctk.CTkScrollableFrame(parent)
701+
self.lex_list_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
702+
703+
self.refresh_lexicon_list()
704+
705+
# 3. Help Text
706+
ctk.CTkLabel(parent, text="Note: Replacements are case-insensitive. Applied before generation.", text_color="gray").grid(row=2, column=0, pady=5)
707+
708+
def add_lexicon_rule(self):
709+
orig = self.lex_orig_var.get().strip()
710+
rep = self.lex_replace_var.get().strip()
711+
712+
if not orig:
713+
messagebox.showwarning("Error", "Original text cannot be empty.")
714+
return
715+
716+
if "lexicon" not in self.settings:
717+
self.settings["lexicon"] = {}
718+
719+
self.settings["lexicon"][orig] = rep
720+
self.lex_orig_var.set("")
721+
self.lex_replace_var.set("")
722+
self.save_settings()
723+
self.refresh_lexicon_list()
724+
725+
def delete_lexicon_rule(self, key):
726+
if key in self.settings.get("lexicon", {}):
727+
del self.settings["lexicon"][key]
728+
self.save_settings()
729+
self.refresh_lexicon_list()
730+
731+
def refresh_lexicon_list(self):
732+
for widget in self.lex_list_frame.winfo_children():
733+
widget.destroy()
734+
735+
lexicon = self.settings.get("lexicon", {})
736+
if not lexicon:
737+
ctk.CTkLabel(self.lex_list_frame, text="No rules defined.", text_color="gray").pack(pady=10)
738+
return
739+
740+
for i, (orig, rep) in enumerate(lexicon.items()):
741+
row = ctk.CTkFrame(self.lex_list_frame)
742+
row.pack(fill="x", pady=2)
743+
744+
ctk.CTkLabel(row, text=orig, width=150, anchor="w", font=("Consolas", 12)).pack(side="left", padx=10)
745+
ctk.CTkLabel(row, text="->", width=30).pack(side="left")
746+
ctk.CTkLabel(row, text=rep, width=150, anchor="w", font=("Consolas", 12)).pack(side="left", padx=10)
747+
748+
ctk.CTkButton(row, text="X", width=30, fg_color="#c42b1c", command=lambda k=orig: self.delete_lexicon_rule(k)).pack(side="right", padx=5)
749+
680750
def create_widgets(self):
681751
# Header
682752
self.grid_columnconfigure(0, weight=1)
@@ -699,6 +769,9 @@ def create_widgets(self):
699769
mix_tab = self.main_tabs.add("Custom Voice")
700770
self.build_mixing_tab(mix_tab)
701771

772+
lex_tab = self.main_tabs.add("Lexicon")
773+
self.build_lexicon_tab(lex_tab)
774+
702775
# Actions (Global)
703776
action_frame = ctk.CTkFrame(self)
704777
action_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=10)
@@ -864,7 +937,8 @@ def preview_conversion(self):
864937
'volume': self.volume_var.get(),
865938
'pitch': self.pitch_var.get(),
866939
'normalize': self.normalize_audio.get(),
867-
'trim_silence': self.trim_silence.get()
940+
'trim_silence': self.trim_silence.get(),
941+
'lexicon': self.settings.get('lexicon', {})
868942
}
869943

870944
# Temp file
@@ -941,7 +1015,8 @@ def start_conversion(self):
9411015
'volume': self.volume_var.get(),
9421016
'pitch': self.pitch_var.get(),
9431017
'normalize': self.normalize_audio.get(),
944-
'trim_silence': self.trim_silence.get()
1018+
'trim_silence': self.trim_silence.get(),
1019+
'lexicon': self.settings.get('lexicon', {})
9451020
}
9461021

9471022
# 3. Start

kokoro_engine.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,25 @@ def __init__(self):
6868
self.on_status = None # func(msg, is_error)
6969
self.on_finish = None # func()
7070

71+
def apply_lexicon(self, text, lexicon):
72+
"""
73+
Applies a dictionary of replacements to the text.
74+
Case-insensitive finding, preserves case of replacement.
75+
"""
76+
if not lexicon:
77+
return text
78+
79+
for src, dest in lexicon.items():
80+
if not src: continue
81+
try:
82+
# Escape the search term to treat it as literal text
83+
pattern = re.compile(re.escape(src), re.IGNORECASE)
84+
text = pattern.sub(dest, text)
85+
except Exception as e:
86+
print(f"Lexicon error for '{src}': {e}")
87+
88+
return text
89+
7190
def resolve_voice_path(self, voice_name):
7291
"""
7392
Returns the absolute path if it's a custom voice,
@@ -206,6 +225,10 @@ def _gen():
206225
all_pieces = []
207226

208227
for speaker_name, segment_text in ms_segments:
228+
# Apply Lexicon if provided in extra_config
229+
if extra_config and 'lexicon' in extra_config:
230+
segment_text = self.apply_lexicon(segment_text, extra_config['lexicon'])
231+
209232
# Truncate segment text if too long for preview
210233
if len(segment_text) > 500:
211234
segment_text = segment_text[:500]
@@ -457,7 +480,12 @@ async def _process_text_async(self, text, config):
457480
ms_segments = self.parse_multispeaker_text(text)
458481
tasks_data = []
459482

483+
lexicon = config.get('lexicon', {})
484+
460485
for speaker_name, segment_text in ms_segments:
486+
# Apply Lexicon
487+
segment_text = self.apply_lexicon(segment_text, lexicon)
488+
461489
seg_config = config.copy()
462490
if speaker_name:
463491
preset = self.load_preset(speaker_name)

0 commit comments

Comments
 (0)