From b7b8c80e7bc559383fdf454d5b131ab292d09497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sun, 19 Jul 2026 16:51:55 +0200 Subject: [PATCH] Dies und das --- __pycache__/speech_ai_gui.cpython-312.pyc | Bin 0 -> 80756 bytes speech_ai_gui.py | 379 +++++++++++++++++++--- 2 files changed, 341 insertions(+), 38 deletions(-) create mode 100644 __pycache__/speech_ai_gui.cpython-312.pyc diff --git a/__pycache__/speech_ai_gui.cpython-312.pyc b/__pycache__/speech_ai_gui.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21fb96d4e00d32860875901e53fcc6ae834b52b6 GIT binary patch literal 80756 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 HKWcE|NUktKiU=DTiH_*Y)=S&*IH7^YGB9 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", self._on_tab_changed) - stt_tab = ttk.Frame(notebook, padding=8) - tts_tab = ttk.Frame(notebook, padding=8) - notebook.add(stt_tab, text="Audio -> Text") - notebook.add(tts_tab, text="Text -> Audio") + stt_tab = ttk.Frame(self.nb, padding=8) + tts_tab = ttk.Frame(self.nb, padding=8) + self.results_tab = ttk.Frame(self.nb, padding=8) + self.nb.add(stt_tab, text="Audio -> Text") + self.nb.add(tts_tab, text="Text -> Audio") + self.nb.add(self.results_tab, text="Remote-Dateien") self._build_stt_tab(stt_tab) self._build_tts_tab(tts_tab) + self._build_results_tab(self.results_tab) + + def _on_tab_changed(self, _event=None): + if self.nb.select() == str(self.results_tab): + self._refresh_results_tab() def _build_stt_tab(self, parent): parent.columnconfigure(1, weight=1) @@ -381,10 +412,18 @@ class SpeechAIGUI: button_row.grid(row=5, column=0, columnspan=3, sticky=tk.W, padx=4, pady=(6, 6)) tip(ttk.Button(button_row, text="Upload audio", command=self._upload_audio_only), "Upload the selected local audio file to the server.").pack(side=tk.LEFT, padx=(0, 6)) tip(ttk.Button(button_row, text="Upload + transcribe", command=self._transcribe_audio), "Upload the file and run faster-whisper on the server.").pack(side=tk.LEFT, padx=6) + tip(ttk.Button(button_row, text="Nur transkribieren", command=self._transcribe_remote_audio), "Run faster-whisper for the audio file that is already stored on the server.").pack(side=tk.LEFT, padx=6) tip(ttk.Button(button_row, text="Download transcript...", command=self._download_transcript), "Save the latest transcript text from the server to a local file.").pack(side=tk.LEFT, padx=6) + progress_row = ttk.Frame(parent) + progress_row.grid(row=6, column=0, columnspan=3, sticky=tk.EW, padx=4, pady=(0, 4)) + progress_row.columnconfigure(0, weight=1) + self.upload_progress = ttk.Progressbar(progress_row, maximum=100, variable=self.v_upload_percent) + self.upload_progress.grid(row=0, column=0, sticky=tk.EW, padx=(0, 8)) + ttk.Label(progress_row, textvariable=self.v_upload_status, width=28).grid(row=0, column=1, sticky=tk.W) + self.transcript_text = scrolledtext.ScrolledText(parent, wrap=tk.WORD, font=("Consolas", 10)) - self.transcript_text.grid(row=6, column=0, columnspan=3, sticky=tk.NSEW, padx=4, pady=4) + self.transcript_text.grid(row=7, column=0, columnspan=3, sticky=tk.NSEW, padx=4, pady=4) def _build_tts_tab(self, parent): parent.columnconfigure(0, weight=1) @@ -430,6 +469,31 @@ class SpeechAIGUI: self.tts_text = scrolledtext.ScrolledText(parent, wrap=tk.WORD, font=("Segoe UI", 11)) self.tts_text.grid(row=3, column=0, sticky=tk.NSEW) + def _build_results_tab(self, parent): + head = ttk.Frame(parent) + head.pack(fill=tk.X, pady=(0, 4)) + + tip(ttk.Label(head, text="Dateien im Speech-Arbeitsverzeichnis:"), "Zeigt den aktuellen Inhalt des Remote-Verzeichnisses an.").pack(side=tk.LEFT) + tip(ttk.Button(head, text="Aktualisieren", command=self._refresh_results_tab), "Liest den Inhalt des Remote-Verzeichnisses erneut vom Server ein.").pack(side=tk.RIGHT, padx=4) + tip(ttk.Button(head, text="Remote-Inhalt löschen", command=self._delete_remote_dir), "Löscht alle Dateien und Unterordner im Remote-Verzeichnis, aber nicht das Verzeichnis selbst.").pack(side=tk.RIGHT, padx=4) + tip(ttk.Button(head, text="Herunterladen", command=self._download_selected_result), "Lädt die ausgewählte Remote-Datei per SFTP herunter.").pack(side=tk.RIGHT, padx=4) + + self.results_info = ttk.Label(parent, text="Noch keine Remote-Dateien geladen.", foreground="gray") + self.results_info.pack(anchor=tk.W, pady=(0, 4)) + + cols = ("name", "size", "modified", "remote") + self.results_tree = ttk.Treeview(parent, columns=cols, show="headings", height=14, selectmode="extended") + self.results_tree.heading("name", text="Datei") + self.results_tree.heading("size", text="Größe") + self.results_tree.heading("modified", text="Geändert") + self.results_tree.heading("remote", text="Remote-Pfad") + self.results_tree.column("name", width=220, anchor=tk.W) + self.results_tree.column("size", width=110, anchor=tk.E) + self.results_tree.column("modified", width=150, anchor=tk.W) + self.results_tree.column("remote", width=520, anchor=tk.W) + self.results_tree.pack(fill=tk.BOTH, expand=True) + self.results_tree.bind("", lambda _e: self._download_selected_result()) + def _build_side_panel(self, parent): side = ttk.LabelFrame(parent, text="Notes", padding=6) side.grid(row=0, column=1, rowspan=2, sticky=tk.NSEW) @@ -497,6 +561,23 @@ class SpeechAIGUI: await ssh.write_text(posixpath.join(work_dir, "transcribe_remote.py"), TRANSCRIBE_SCRIPT) await ssh.write_text(posixpath.join(work_dir, "tts_remote.py"), TTS_SCRIPT) + def _set_upload_progress(self, sent_bytes: int, total_bytes: int): + total = max(total_bytes, 1) + percent = (sent_bytes / total) * 100 + sent_mb = sent_bytes / (1024 * 1024) + total_mb = total_bytes / (1024 * 1024) + self.root.after(0, lambda: self.v_upload_percent.set(percent)) + self.root.after(0, lambda: self.v_upload_status.set(f"Upload {percent:.0f}% ({sent_mb:.1f}/{total_mb:.1f} MB)")) + + def _reset_upload_progress(self, status: str = "Kein Upload aktiv"): + self.root.after(0, lambda: self.v_upload_percent.set(0.0)) + self.root.after(0, lambda: self.v_upload_status.set(status)) + + async def _upload_audio(self, ssh: AsyncSSH, local_path: str, remote_path: str): + self._reset_upload_progress("Upload startet...") + await ssh.upload(local_path, remote_path, self._set_upload_progress) + self.root.after(0, lambda: self.v_upload_status.set("Upload abgeschlossen")) + def _upload_audio_only(self): local_path = self.v_local_audio.get().strip() if not local_path or not os.path.isfile(local_path): @@ -510,9 +591,10 @@ class SpeechAIGUI: await ssh.connect() await self._ensure_remote_ready(ssh) remote_path = self.v_remote_audio.get().strip() - await ssh.upload(local_path, remote_path) + await self._upload_audio(ssh, local_path, remote_path) self._log(f"[ok] uploaded audio to {remote_path}\n", "ok") except Exception as exc: + self._reset_upload_progress("Upload fehlgeschlagen") self._log(f"[error] upload failed: {exc}\n", "error") finally: await ssh.close() @@ -530,42 +612,74 @@ class SpeechAIGUI: await ssh.connect() await self._ensure_remote_ready(ssh) remote_audio = self.v_remote_audio.get().strip() - await ssh.upload(local_path, remote_audio) + await self._upload_audio(ssh, local_path, remote_audio) self._log(f"[info] uploaded audio to {remote_audio}\n", "info") + await self._run_remote_transcription(ssh, remote_audio) + except Exception as exc: + self._reset_upload_progress("Upload fehlgeschlagen") + self._log(f"[error] transcription failed: {exc}\n", "error") + finally: + await ssh.close() - work_dir = self.v_remote_dir.get().strip() - py = self.v_python.get().strip() - output_txt = posixpath.join(work_dir, "transcript.txt") - output_json = posixpath.join(work_dir, "transcript.json") - language = self.v_stt_language.get().strip() or "-" - cmd_parts = [ - shlex.quote(py), - shlex.quote(posixpath.join(work_dir, "transcribe_remote.py")), - shlex.quote(remote_audio), - shlex.quote(output_txt), - shlex.quote(output_json), - shlex.quote(self.v_stt_model.get().strip()), - shlex.quote(language), - shlex.quote(self.v_stt_task.get().strip()), - shlex.quote(self.v_stt_device.get().strip()), - shlex.quote(self.v_stt_compute.get().strip()), - ] - shell_cmd = f"bash -lc {shlex.quote(f'cd {shlex.quote(work_dir)} && ' + ' '.join(cmd_parts))}" - self._log(f"[info] transcribing with model {self.v_stt_model.get().strip()}\n", "info") - exit_code = await ssh.exec_stream(shell_cmd, self._log) - if exit_code != 0: - self._log("[error] transcription failed\n", "error") + def _transcribe_remote_audio(self): + remote_audio = self.v_remote_audio.get().strip() + if not remote_audio: + messagebox.showwarning("Missing file", "Please enter the remote audio file path first.") + return + self._current_task = self._submit(self._async_transcribe_remote_audio()) + + async def _async_transcribe_remote_audio(self): + ssh = self._make_ssh() + try: + await ssh.connect() + await self._ensure_remote_ready(ssh) + remote_audio = self.v_remote_audio.get().strip() + try: + remote_stat = await ssh.stat(remote_audio) + except asyncssh.SFTPError: + self._log(f"[error] remote audio file not found: {remote_audio}\n", "error") return - - transcript = await ssh.read_text(output_txt) - self.latest_transcript_remote = output_txt - self.root.after(0, lambda: self._set_transcript_text(transcript)) - self._log(f"[ok] transcript saved at {output_txt}\n", "ok") + if not stat.S_ISREG(remote_stat.permissions): + self._log(f"[error] remote path is not a file: {remote_audio}\n", "error") + return + self._reset_upload_progress("Kein Upload aktiv") + await self._run_remote_transcription(ssh, remote_audio) except Exception as exc: self._log(f"[error] transcription failed: {exc}\n", "error") finally: await ssh.close() + async def _run_remote_transcription(self, ssh: AsyncSSH, remote_audio: str): + work_dir = self.v_remote_dir.get().strip() + py = self.v_python.get().strip() + output_txt = posixpath.join(work_dir, "transcript.txt") + output_json = posixpath.join(work_dir, "transcript.json") + language = self.v_stt_language.get().strip() or "-" + cmd_parts = [ + shlex.quote(py), + shlex.quote(posixpath.join(work_dir, "transcribe_remote.py")), + shlex.quote(remote_audio), + shlex.quote(output_txt), + shlex.quote(output_json), + shlex.quote(self.v_stt_model.get().strip()), + shlex.quote(language), + shlex.quote(self.v_stt_task.get().strip()), + shlex.quote(self.v_stt_device.get().strip()), + shlex.quote(self.v_stt_compute.get().strip()), + ] + shell_cmd = f"bash -lc {shlex.quote(f'cd {shlex.quote(work_dir)} && ' + ' '.join(cmd_parts))}" + self._log(f"[info] transcribing with model {self.v_stt_model.get().strip()}\n", "info") + exit_code = await ssh.exec_stream(shell_cmd, self._log) + if exit_code != 0: + self._log("[error] transcription failed\n", "error") + return + + transcript = await ssh.read_text(output_txt) + self.latest_transcript_remote = output_txt + self.root.after(0, lambda: self._set_transcript_text(transcript)) + self._log(f"[ok] transcript saved at {output_txt}\n", "ok") + await self._async_refresh_results_tab(work_dir) + def _set_transcript_text(self, text: str): self.transcript_text.delete("1.0", tk.END) self.transcript_text.insert("1.0", text) @@ -618,6 +732,7 @@ class SpeechAIGUI: return self.latest_tts_remote = remote_audio self._log(f"[ok] audio saved at {remote_audio}\n", "ok") + await self._async_refresh_results_tab(work_dir) except Exception as exc: self._log(f"[error] audio generation failed: {exc}\n", "error") finally: @@ -677,6 +792,194 @@ class SpeechAIGUI: pass self.root.after(100, self._poll_log) + def _refresh_results_tab(self): + remote_dir = self.v_remote_dir.get().strip() + self.root.config(cursor="watch") + self.nb.config(cursor="watch") + self.results_tree.config(cursor="watch") + self._submit(self._async_refresh_results_tab(remote_dir)) + + async def _async_refresh_results_tab(self, remote_dir: str): + if not remote_dir: + self.root.after(0, lambda: self._set_results_info("Kein Remote-Verzeichnis gesetzt.")) + self.root.after(0, lambda: self._set_results_items([])) + self.root.after(0, self._clear_wait_cursor) + return + + quoted_dir = shlex.quote(remote_dir) + cmd = ( + f"dir={quoted_dir}; " + "if [ ! -e \"$dir\" ]; then " + "echo 'Remote-Verzeichnis existiert nicht.'; exit 0; " + "fi; " + "find \"$dir\" -maxdepth 2 -type f -printf '%s\t%TY-%Tm-%Td %TH:%TM:%TS\t%P\t%p\\n' | sort" + ) + self.root.after(0, lambda: self._set_results_info(f"Remote-Verzeichnis: {remote_dir}")) + self.root.after(0, lambda: self._set_results_items([])) + + entries: list[dict[str, str]] = [] + + def _capture(line: str): + stripped = line.strip() + if not stripped or stripped.startswith("[process exited"): + return + if "\t" not in stripped: + return + size, modified, rel_path, full_path = stripped.split("\t", 3) + entries.append({ + "name": rel_path or posixpath.basename(full_path), + "size": size, + "modified": modified.split(".")[0], + "remote": full_path, + }) + + try: + ssh = self._make_ssh() + await ssh.connect() + await ssh.exec_stream(cmd, _capture) + await ssh.close() + self.root.after(0, lambda: self._set_results_items(entries)) + except Exception as exc: + self.root.after(0, lambda: self._set_results_info(f"[error] {exc}")) + self.root.after(0, lambda: self._set_results_items([])) + finally: + self.root.after(0, self._clear_wait_cursor) + + def _set_results_info(self, text: str): + self.results_info.config(text=text, foreground="gray") + + def _set_results_items(self, entries: list[dict[str, str]]): + self.results_files = {} + for item in self.results_tree.get_children(): + self.results_tree.delete(item) + for i, entry in enumerate(entries): + item_id = f"result_{i}" + self.results_files[item_id] = entry + self.results_tree.insert( + "", + tk.END, + iid=item_id, + values=( + entry["name"], + self._format_size(entry["size"]), + entry["modified"], + entry["remote"], + ), + ) + self._autosize_results_columns() + + def _autosize_results_columns(self): + body_font = tkfont.nametofont("TkDefaultFont") + head_font = tkfont.nametofont("TkHeadingFont") + padding = 24 + + for col in ("name", "size", "modified", "remote"): + header = self.results_tree.heading(col, "text") or col + width = head_font.measure(header) + padding + for item_id in self.results_tree.get_children(): + value = self.results_tree.set(item_id, col) + width = max(width, body_font.measure(str(value)) + padding) + self.results_tree.column(col, width=width) + + def _clear_wait_cursor(self): + self.root.config(cursor="") + self.nb.config(cursor="") + self.results_tree.config(cursor="") + + def _format_size(self, size_text: str) -> str: + try: + size = int(size_text) + except Exception: + return size_text + units = ["B", "KB", "MB", "GB", "TB"] + value = float(size) + for unit in units: + if value < 1024 or unit == units[-1]: + if unit == "B": + return f"{int(value)} {unit}" + return f"{value:.1f} {unit}" + value /= 1024 + return size_text + + def _download_selected_result(self): + selection = self.results_tree.selection() + if not selection: + messagebox.showwarning("Keine Datei ausgewählt", "Bitte zuerst eine oder mehrere Dateien im Tab 'Remote-Dateien' auswählen.") + return + entries = [] + for item_id in selection: + entry = self.results_files.get(item_id) + if entry: + entries.append(entry) + if not entries: + messagebox.showwarning("Datei fehlt", "Die ausgewählten Dateien konnten nicht gefunden werden.") + return + + source_dir = os.path.dirname(self.v_local_audio.get().strip()) if self.v_local_audio.get().strip() else "" + initial_dir = source_dir if source_dir and os.path.isdir(source_dir) else os.getcwd() + local_dir = filedialog.askdirectory(title="Zielordner für Download auswählen", initialdir=initial_dir) + if not local_dir: + return + + downloads = [] + for entry in entries: + local_path = os.path.normpath(os.path.join(local_dir, os.path.basename(entry["remote"]))) + downloads.append((entry["remote"], local_path)) + self._submit(self._async_download_results(downloads)) + + async def _async_download_results(self, downloads: list[tuple[str, str]]): + try: + ssh = self._make_ssh() + await ssh.connect() + for remote_path, local_path in downloads: + self._log(f"[info] downloading file: {remote_path} -> {local_path}\n", "info") + os.makedirs(os.path.dirname(local_path), exist_ok=True) + await ssh.download(remote_path, local_path) + if not os.path.isfile(local_path): + raise FileNotFoundError(f"Datei nach Download nicht gefunden: {local_path}") + self._log(f"[ok] download completed: {local_path}\n", "ok") + await ssh.close() + except Exception as exc: + self._log(f"[error] download failed: {exc}\n", "error") + + def _delete_remote_dir(self): + remote_dir = self.v_remote_dir.get().strip() + if not remote_dir: + messagebox.showwarning("Remote-Verzeichnis fehlt", "Bitte zuerst ein Remote-Verzeichnis angeben.") + return + if remote_dir == "/": + messagebox.showerror("Ungültiges Verzeichnis", "Das Root-Verzeichnis '/' darf nicht geleert werden.") + return + if not messagebox.askyesno( + "Remote-Inhalt löschen", + f"Soll der gesamte Inhalt dieses Verzeichnisses auf dem Server gelöscht werden?\n\n{remote_dir}\n\nDas Verzeichnis selbst bleibt erhalten." + ): + return + self._submit(self._async_delete_remote_dir(remote_dir)) + + async def _async_delete_remote_dir(self, remote_dir: str): + quoted_dir = shlex.quote(remote_dir) + cmd = ( + f"dir={quoted_dir}; " + "if [ -z \"$dir\" ] || [ \"$dir\" = \"/\" ]; then " + "echo '[error] unsafe remote directory'; exit 1; " + "fi; " + "if [ ! -e \"$dir\" ]; then " + "echo '[info] remote directory does not exist'; exit 0; " + "fi; " + "find \"$dir\" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +; " + "echo \"[ok] remote directory content deleted: $dir\"" + ) + self._log(f"[info] deleting remote directory content: {remote_dir}\n", "info") + try: + ssh = self._make_ssh() + await ssh.connect() + await ssh.exec_stream(cmd, self._log) + await ssh.close() + await self._async_refresh_results_tab(remote_dir) + except Exception as exc: + self._log(f"[error] delete failed: {exc}\n", "error") + def on_close(self): self._loop.call_soon_threadsafe(self._loop.stop) self.root.destroy()