feat: implement multiprocessing for transcription with immediate cancellation
This commit is contained in:
104
app.py
104
app.py
@@ -4,7 +4,8 @@ import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import filedialog
|
||||
from tkinter import messagebox
|
||||
from src._LocalTranscribe import transcribe, get_path, detect_backend
|
||||
from src._LocalTranscribe import transcribe, get_path, detect_backend, _transcribe_worker_process
|
||||
import multiprocessing as mp
|
||||
import customtkinter
|
||||
import threading
|
||||
|
||||
@@ -220,8 +221,10 @@ class App:
|
||||
self.timestamps_switch.pack(side=tk.LEFT, padx=5)
|
||||
# Progress Bar
|
||||
self.progress_bar = ttk.Progressbar(master, length=200, mode='indeterminate')
|
||||
# Stop event for cancellation
|
||||
self._stop_event = threading.Event()
|
||||
# Worker process handle (replaces thread+stop_event for true immediate cancellation)
|
||||
self._proc = None
|
||||
self._parent_conn = None
|
||||
self._child_conn = None
|
||||
# Button actions frame
|
||||
button_frame = customtkinter.CTkFrame(master)
|
||||
button_frame.pack(fill=tk.BOTH, padx=10, pady=10)
|
||||
@@ -262,9 +265,28 @@ class App:
|
||||
print("─" * 46)
|
||||
# Helper functions
|
||||
def _stop_transcription(self):
|
||||
self._stop_event.set()
|
||||
self.stop_button.configure(state=tk.DISABLED)
|
||||
print("⛔ Stop requested — finishing current file…")
|
||||
if self._proc and self._proc.is_alive():
|
||||
self._proc.terminate()
|
||||
try:
|
||||
self._proc.join(timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
if self._proc.is_alive():
|
||||
self._proc.kill()
|
||||
try:
|
||||
self._proc.join(timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
# Close pipe ends — no semaphores, so no leak
|
||||
for conn in (self._parent_conn, self._child_conn):
|
||||
try:
|
||||
if conn:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._parent_conn = self._child_conn = None
|
||||
print("⛔ Transcription stopped by user.")
|
||||
|
||||
def _model_desc_text(self, model_name):
|
||||
info = MODEL_INFO.get(model_name)
|
||||
@@ -287,49 +309,67 @@ class App:
|
||||
self.path_entry.insert(0, folder_path)
|
||||
# Start transcription
|
||||
def start_transcription(self):
|
||||
self._stop_event.clear()
|
||||
self.transcribe_button.configure(state=tk.DISABLED)
|
||||
self.stop_button.configure(state=tk.NORMAL)
|
||||
threading.Thread(target=self.transcribe_thread, daemon=True).start()
|
||||
# Threading
|
||||
def transcribe_thread(self):
|
||||
path = self.path_entry.get()
|
||||
model_display = self.model_combobox.get()
|
||||
# Ignore the visual separator
|
||||
if model_display.startswith('─'):
|
||||
messagebox.showinfo("Invalid selection", "Please select a model, not the separator line.")
|
||||
self.transcribe_button.configure(state=tk.NORMAL)
|
||||
return
|
||||
self.transcribe_button.configure(state=tk.DISABLED)
|
||||
self.stop_button.configure(state=tk.NORMAL)
|
||||
path = self.path_entry.get()
|
||||
model = HF_MODEL_MAP.get(model_display, model_display)
|
||||
lang_label = self.language_combobox.get()
|
||||
language = WHISPER_LANGUAGES.get(lang_label, lang_label) if lang_label else None
|
||||
verbose = True # always show transcription progress in the console panel
|
||||
timestamps = self.timestamps_var.get()
|
||||
# Show progress bar
|
||||
glob_file = get_path(path)
|
||||
self.progress_bar.pack(fill=tk.X, padx=5, pady=5)
|
||||
self.progress_bar.start()
|
||||
# Setting path and files
|
||||
glob_file = get_path(path)
|
||||
#messagebox.showinfo("Message", "Starting transcription!")
|
||||
# Start transcription
|
||||
self._parent_conn, self._child_conn = mp.Pipe(duplex=False)
|
||||
self._proc = mp.Process(
|
||||
target=_transcribe_worker_process,
|
||||
args=(self._child_conn, path, glob_file, model, language, True, timestamps),
|
||||
daemon=True,
|
||||
)
|
||||
self._proc.start()
|
||||
self._child_conn.close() # parent doesn't write; close its write-end
|
||||
self._child_conn = None
|
||||
self.master.after(100, self._poll_worker)
|
||||
|
||||
def _poll_worker(self):
|
||||
done = False
|
||||
result = None
|
||||
try:
|
||||
output_text = transcribe(path, glob_file, model, language, verbose, timestamps, stop_event=self._stop_event)
|
||||
except UnboundLocalError:
|
||||
messagebox.showinfo("Files not found error!", 'Nothing found, choose another folder.')
|
||||
while self._parent_conn and self._parent_conn.poll():
|
||||
msg = self._parent_conn.recv()
|
||||
if isinstance(msg, tuple) and msg[0] == '__done__':
|
||||
done = True
|
||||
result = msg[1]
|
||||
else:
|
||||
sys.stdout.write(msg)
|
||||
sys.stdout.flush()
|
||||
except EOFError:
|
||||
# Child closed the pipe (normal completion or kill)
|
||||
done = True
|
||||
except Exception:
|
||||
pass
|
||||
except ValueError as e:
|
||||
messagebox.showinfo("Error", str(e))
|
||||
# Hide progress bar
|
||||
if done or (self._proc and not self._proc.is_alive()):
|
||||
if self._parent_conn:
|
||||
try:
|
||||
self._parent_conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._parent_conn = None
|
||||
self._on_transcription_done(result)
|
||||
else:
|
||||
self.master.after(100, self._poll_worker)
|
||||
|
||||
def _on_transcription_done(self, output_text):
|
||||
self.progress_bar.stop()
|
||||
self.progress_bar.pack_forget()
|
||||
# Restore buttons
|
||||
self.stop_button.configure(state=tk.DISABLED)
|
||||
self.transcribe_button.configure(state=tk.NORMAL)
|
||||
# Recover output text
|
||||
try:
|
||||
messagebox.showinfo("Finished!", output_text)
|
||||
except UnboundLocalError:
|
||||
pass
|
||||
if output_text:
|
||||
title = "Finished!" if not output_text.startswith('⚠') else "Error"
|
||||
messagebox.showinfo(title, output_text)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Setting custom themes
|
||||
|
||||
@@ -424,3 +424,43 @@ def transcribe(path, glob_file, model=None, language=None, verbose=False, timest
|
||||
print(output_text)
|
||||
print(SEP)
|
||||
return output_text
|
||||
|
||||
|
||||
def _transcribe_worker_process(conn, path, glob_file, model, language, verbose, timestamps):
|
||||
"""Child-process entry point for the UI's multiprocessing backend.
|
||||
|
||||
Redirects stdout/stderr → pipe connection so the main process can display
|
||||
output in the console panel. The main process sends SIGTERM/SIGKILL to
|
||||
stop this process immediately, including any in-progress download or inference.
|
||||
"""
|
||||
import sys
|
||||
|
||||
class _PipeWriter:
|
||||
def __init__(self, c):
|
||||
self.c = c
|
||||
|
||||
def write(self, text):
|
||||
if text:
|
||||
try:
|
||||
self.c.send(text)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
writer = _PipeWriter(conn)
|
||||
sys.stdout = writer
|
||||
sys.stderr = writer
|
||||
|
||||
result = '⚠ No output produced.'
|
||||
try:
|
||||
result = transcribe(path, glob_file, model, language, verbose, timestamps)
|
||||
except Exception as exc:
|
||||
result = f'⚠ Unexpected error: {exc}'
|
||||
finally:
|
||||
try:
|
||||
conn.send(('__done__', result))
|
||||
except Exception:
|
||||
pass
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user