Skip to content

Commit f94a045

Browse files
authored
Merge pull request #274 from kaixxx/true_headless_mode
True headless mode
2 parents 9e92596 + d12a8c3 commit f94a045

File tree

1 file changed

+133
-36
lines changed

1 file changed

+133
-36
lines changed

noScribe.py

Lines changed: 133 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,34 +1020,39 @@ def _update_progress_display(self):
10201020
font=("", font_size)
10211021
)
10221022

1023+
def _init_app_state(app):
1024+
app._headless = False
1025+
app.user_models_dir = os.path.join(config_dir, 'whisper_models')
1026+
os.makedirs(app.user_models_dir, exist_ok=True)
1027+
whisper_models_readme = os.path.join(app.user_models_dir, 'readme.txt')
1028+
if not os.path.exists(whisper_models_readme):
1029+
with open(whisper_models_readme, 'w') as file:
1030+
file.write('You can download custom Whisper-models for the transcription into this folder. \n'
1031+
'See here for more information: https://github.com/kaixxx/noScribe/wiki/Add-custom-Whisper-models-for-transcription')
1032+
1033+
app.queue = TranscriptionQueue()
1034+
app.audio_files_list = []
1035+
app.transcript_files_list = []
1036+
app.log_file = None
1037+
app.cancel = False # if set to True, transcription will be canceled
1038+
# If True, cancel only the currently running job (triggered from queue row "X")
1039+
app._cancel_job_only = False
1040+
app.current_progress = -1
1041+
# Track background activity for robust shutdown
1042+
app._worker_threads = []
1043+
app._mp_proc = None
1044+
app._mp_queue = None
1045+
app._ffmpeg_proc = None
1046+
app._shutting_down = False
1047+
1048+
10231049
class App(ctk.CTk):
10241050
def __init__(self):
10251051
super().__init__()
10261052

10271053
self.protocol("WM_DELETE_WINDOW", self.on_closing)
1028-
1029-
self.user_models_dir = os.path.join(config_dir, 'whisper_models')
1030-
os.makedirs(self.user_models_dir, exist_ok=True)
1031-
whisper_models_readme = os.path.join(self.user_models_dir, 'readme.txt')
1032-
if not os.path.exists(whisper_models_readme):
1033-
with open(whisper_models_readme, 'w') as file:
1034-
file.write('You can download custom Whisper-models for the transcription into this folder. \n'
1035-
'See here for more information: https://github.com/kaixxx/noScribe/wiki/Add-custom-Whisper-models-for-transcription')
1036-
1037-
self.queue = TranscriptionQueue()
1038-
self.audio_files_list = []
1039-
self.transcript_files_list = []
1040-
self.log_file = None
1041-
self.cancel = False # if set to True, transcription will be canceled
1042-
# If True, cancel only the currently running job (triggered from queue row "X")
1043-
self._cancel_job_only = False
1044-
self.current_progress = -1
1045-
# Track background activity for robust shutdown
1046-
self._worker_threads = []
1047-
self._mp_proc = None
1048-
self._mp_queue = None
1049-
self._ffmpeg_proc = None
1050-
self._shutting_down = False
1054+
1055+
_init_app_state(self)
10511056

10521057
# configure window
10531058
self.title('noScribe - ' + t('app_header'))
@@ -1500,6 +1505,8 @@ def update_scrollbar_visibility(self):
15001505

15011506
def update_queue_table(self):
15021507
"""Update the queue table by diffing: update existing rows, add new ones, remove missing."""
1508+
if getattr(self, '_headless', False):
1509+
return
15031510
current_keys = []
15041511
for i in range(len(self.queue.jobs)):
15051512
job = self.queue.jobs[i]
@@ -1801,6 +1808,8 @@ def update_queue_table(self):
18011808

18021809
def update_queue_controls(self):
18031810
"""Enable/disable and label the queue control buttons based on state."""
1811+
if getattr(self, '_headless', False):
1812+
return
18041813
try:
18051814
has_running = len(self.queue.get_running_jobs()) > 0
18061815
has_pending = self.queue.has_pending_jobs()
@@ -2041,7 +2050,7 @@ def log(self, txt: str = '', tags: list = [], where: str = 'both', link: str = '
20412050
if where != 'file':
20422051
if txt[:-1] != t('welcome_instructions'):
20432052
print(txt, end='')
2044-
if hasattr(self, 'log_textbox') and self.log_textbox.winfo_exists():
2053+
if not getattr(self, '_headless', False) and hasattr(self, 'log_textbox') and self.log_textbox.winfo_exists():
20452054
try:
20462055
self.log_textbox.configure(state=tk.NORMAL)
20472056
# To prevent slowing down the UI, limit the content of log_textbox to max 5000 characters
@@ -2084,7 +2093,7 @@ def logn(self, txt: str = '', tags: list = [], where: str = 'both', link:str = '
20842093

20852094
def logr(self, txt: str = '', tags: list = [], where: str = 'both', link:str = '', tb: str = '') -> None:
20862095
""" Replace the last line of the log """
2087-
if where != 'file':
2096+
if where != 'file' and not getattr(self, '_headless', False) and hasattr(self, 'log_textbox') and self.log_textbox.winfo_exists():
20882097
self.log_textbox.configure(state=ctk.NORMAL)
20892098
tmp_txt = self.log_textbox.get("end-1c linestart", "end-1c")
20902099
self.log_textbox.delete("end-1c linestart", "end-1c")
@@ -2193,6 +2202,8 @@ def button_transcript_file_event(self):
21932202

21942203
def set_progress(self, step, value, speaker_detection='none'):
21952204
""" Update state of the progress bar """
2205+
if getattr(self, '_headless', False):
2206+
return
21962207
progr = -1
21972208
if step == 1:
21982209
progr = value * 0.05 / 100
@@ -2379,13 +2390,14 @@ def transcription_worker(self, start_job_index=None):
23792390
self.logn(t('processing_time', total_time_str=total_time_str))
23802391

23812392
# open editor if only a single file was processed
2382-
if queue_jobs_processed == 1 \
2393+
if not getattr(self, '_headless', False) \
2394+
and queue_jobs_processed == 1 \
23832395
and job \
23842396
and job.file_ext == 'html' \
23852397
and job.status == JobStatus.FINISHED \
23862398
and get_config('auto_edit_transcript', 'True') == 'True':
23872399
self.launch_editor(job.transcript_file)
2388-
elif queue_jobs_processed > 1:
2400+
elif queue_jobs_processed > 1 and not getattr(self, '_headless', False):
23892401
# if more than one job has been processed, switch to queue tab for an overview
23902402
self.tabview.set(self.tabview._name_list[1])
23912403

@@ -3182,6 +3194,11 @@ def _run_whisper_subprocess_stream(self, tmp_audio_file: str, job, on_segment):
31823194
proc.close()
31833195
except Exception:
31843196
pass
3197+
try:
3198+
q.close()
3199+
q.join_thread()
3200+
except Exception:
3201+
pass
31853202
# Clear exposed handles
31863203
self._mp_proc = None
31873204
self._mp_queue = None
@@ -3268,6 +3285,11 @@ def _run_diarize_subprocess(self, tmp_audio_file: str, job):
32683285
proc.close()
32693286
except Exception:
32703287
pass
3288+
try:
3289+
q.close()
3290+
q.join_thread()
3291+
except Exception:
3292+
pass
32713293
self._mp_proc = None
32723294
self._mp_queue = None
32733295

@@ -3365,16 +3387,85 @@ def on_closing(self):
33653387
pass
33663388
self.destroy()
33673389

3368-
def run_cli_mode(args):
3369-
"""Run noScribe in CLI mode"""
3390+
3391+
class HeadlessApp(App):
3392+
def __init__(self):
3393+
# Do not initialize Tk/CTk to avoid DISPLAY requirements
3394+
_init_app_state(self)
3395+
self._headless = True
3396+
3397+
def __getattr__(self, name):
3398+
# Avoid Tk attribute delegation recursion when CTk isn't initialized
3399+
raise AttributeError(name)
3400+
3401+
3402+
def _cleanup_app(app):
3403+
"""Cleanup app instance for proper process exit in CLI mode."""
33703404
try:
3371-
# Create a minimal app instance to access model paths and logging
3372-
app = App()
3373-
# Hide GUI window for headless execution
3405+
# Signal shutdown to stop background activity
3406+
app._shutting_down = True
3407+
app.cancel = True
3408+
3409+
# Terminate active multiprocessing child (diarization/whisper) if present
3410+
if getattr(app, "_mp_proc", None) is not None:
3411+
try:
3412+
if app._mp_proc.is_alive():
3413+
app._mp_proc.terminate()
3414+
app._mp_proc.join(timeout=1.0)
3415+
except Exception:
3416+
pass
3417+
finally:
3418+
app._mp_proc = None
3419+
3420+
# Close multiprocessing queue
3421+
if getattr(app, "_mp_queue", None) is not None:
3422+
try:
3423+
app._mp_queue.close()
3424+
app._mp_queue.join_thread()
3425+
except Exception:
3426+
pass
3427+
finally:
3428+
app._mp_queue = None
3429+
3430+
# Terminate ffmpeg if currently converting
3431+
if getattr(app, "_ffmpeg_proc", None) is not None:
3432+
try:
3433+
if app._ffmpeg_proc.poll() is None:
3434+
app._ffmpeg_proc.terminate()
3435+
app._ffmpeg_proc.wait(timeout=1.0)
3436+
except Exception:
3437+
try:
3438+
app._ffmpeg_proc.kill()
3439+
except Exception:
3440+
pass
3441+
finally:
3442+
app._ffmpeg_proc = None
3443+
3444+
# Join worker threads briefly
3445+
for th in list(getattr(app, "_worker_threads", [])):
3446+
try:
3447+
th.join(timeout=0.5)
3448+
except Exception:
3449+
pass
3450+
except Exception:
3451+
pass
3452+
3453+
if not getattr(app, '_headless', False):
33743454
try:
3375-
app.withdraw()
3455+
app.quit()
33763456
except Exception:
33773457
pass
3458+
try:
3459+
app.destroy()
3460+
except Exception:
3461+
pass
3462+
3463+
def run_cli_mode(args):
3464+
"""Run noScribe in CLI mode"""
3465+
app = None
3466+
try:
3467+
# Create a headless app instance (no GUI initialization)
3468+
app = HeadlessApp()
33783469

33793470
# Validate and set the whisper model
33803471
available_models = app.get_whisper_models()
@@ -3445,12 +3536,16 @@ def run_cli_mode(args):
34453536
except Exception as e:
34463537
print(f"Error: {str(e)}")
34473538
return 1
3539+
finally:
3540+
if app is not None:
3541+
_cleanup_app(app)
34483542

34493543
def show_available_models():
34503544
"""Show available Whisper models"""
3545+
app = None
34513546
try:
3452-
# Create minimal app instance to get models
3453-
app = App()
3547+
# Create headless app instance to get models
3548+
app = HeadlessApp()
34543549
models = app.get_whisper_models()
34553550

34563551
print("Available Whisper models:")
@@ -3459,9 +3554,11 @@ def show_available_models():
34593554

34603555
if not models:
34613556
print(" No models found. Please check your installation.")
3462-
34633557
except Exception as e:
34643558
print(f"Error getting models: {str(e)}")
3559+
finally:
3560+
if app is not None:
3561+
_cleanup_app(app)
34653562

34663563
if __name__ == "__main__":
34673564
# Parse command line arguments

0 commit comments

Comments
 (0)