Skip to content

Commit 73f40ec

Browse files
committed
Added audio preview.
1 parent a9929ae commit 73f40ec

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

gui.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22
import time
33
import json
4+
import re
5+
import winsound
46
import customtkinter as ctk
57
from tkinter import filedialog, messagebox
68
import threading
@@ -272,6 +274,9 @@ def create_widgets(self):
272274

273275
btn_frame = ctk.CTkFrame(action_frame, fg_color="transparent")
274276
btn_frame.pack(fill="x", pady=10)
277+
278+
self.preview_btn = ctk.CTkButton(btn_frame, text="Preview Audio", command=self.preview_conversion, height=40, fg_color="#2B719E", hover_color="#205578")
279+
self.preview_btn.pack(side="left", fill="x", expand=True, padx=5)
275280

276281
self.start_btn = ctk.CTkButton(btn_frame, text="Start Generation", command=self.start_conversion, height=40, font=("Roboto", 14, "bold"))
277282
self.start_btn.pack(side="left", fill="x", expand=True, padx=5)
@@ -367,6 +372,7 @@ def set_ui_state(self, is_running):
367372
cancel_state = "normal" if is_running else "disabled"
368373

369374
self.start_btn.configure(state=state)
375+
self.preview_btn.configure(state=state)
370376
self.cancel_btn.configure(state=cancel_state)
371377
self.thread_minus_btn.configure(state=state)
372378
self.thread_plus_btn.configure(state=state)
@@ -375,6 +381,58 @@ def set_ui_state(self, is_running):
375381
if not is_running:
376382
self.progress_bar.set(0 if self.engine.cancel_event.is_set() else 1)
377383

384+
def preview_conversion(self):
385+
# 1. Get Text
386+
current_tab = self.tab_view.get()
387+
text_data = ""
388+
389+
if current_tab == "Direct Text":
390+
text_data = self.text_entry.get("1.0", "end").strip()
391+
else:
392+
fpath = self.file_path_var.get()
393+
if os.path.exists(fpath):
394+
try:
395+
text_data = self.engine.extract_text_from_file(fpath)
396+
except:
397+
pass
398+
399+
if not text_data:
400+
text_data = "This is a sample audio preview using the Koh-koh-ro TTS engine. It demonstrates the voice quality and speed settings."
401+
402+
# Truncate to 2 sentences
403+
sentences = re.split(r'(?<=[.!?])\s+', text_data)
404+
preview_text = " ".join(sentences[:2])
405+
if len(preview_text) > 500: # Safety cap
406+
preview_text = preview_text[:500]
407+
408+
# Config
409+
voice = self.voice_var.get()
410+
speed = self.speed_var.get()
411+
412+
# Temp file
413+
import tempfile
414+
tmp_path = os.path.join(tempfile.gettempdir(), "kokoro_preview.wav")
415+
416+
self.status_label.configure(text="Generating preview...", text_color="blue")
417+
418+
def _on_preview_done(future):
419+
def _ui_update():
420+
try:
421+
success = future.result()
422+
if success:
423+
self.status_label.configure(text="Playing preview...", text_color="green")
424+
winsound.PlaySound(tmp_path, winsound.SND_FILENAME | winsound.SND_ASYNC)
425+
self.after(3000, lambda: self.status_label.configure(text="Ready", text_color="gray"))
426+
else:
427+
self.status_label.configure(text="Preview failed.", text_color="red")
428+
except Exception as e:
429+
self.status_label.configure(text=f"Preview error: {e}", text_color="red")
430+
431+
self.after(0, _ui_update)
432+
433+
future = self.engine.worker.run_coro(self.engine.generate_preview(preview_text, voice, speed, tmp_path))
434+
future.add_done_callback(_on_preview_done)
435+
378436
def start_conversion(self):
379437
# 0. Validate Threads
380438
try:

kokoro_engine.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import concurrent.futures
66
import soundfile as sf
77
import torch
8+
import numpy as np
89
import pypdf
910
import ebooklib
1011
from ebooklib import epub
@@ -67,6 +68,36 @@ async def init_pipeline_async(self):
6768
if self.on_status: self.on_status(f"Pipeline Init Failed: {e}", True)
6869
return False
6970

71+
async def generate_preview(self, text, voice, speed, output_path):
72+
def _gen():
73+
if not self.pipeline:
74+
# Try to use thread local if main pipeline not available, or init new
75+
p = get_thread_pipeline()
76+
if not p: return False
77+
else:
78+
p = self.pipeline
79+
80+
try:
81+
# Generate
82+
generator = p(text, voice=voice, speed=speed, split_pattern=r"\n+")
83+
pieces = []
84+
for _, _, audio in generator:
85+
if isinstance(audio, torch.Tensor):
86+
audio = audio.cpu().numpy()
87+
pieces.append(audio)
88+
89+
if not pieces:
90+
return False
91+
92+
full_audio = np.concatenate(pieces)
93+
sf.write(output_path, full_audio, 24000)
94+
return True
95+
except Exception as e:
96+
print(f"Preview error: {e}")
97+
return False
98+
99+
return await asyncio.to_thread(_gen)
100+
70101
def extract_text_from_file(self, fpath):
71102
if not os.path.exists(fpath):
72103
raise FileNotFoundError("File does not exist.")

0 commit comments

Comments
 (0)