Sprachrekorder.

Python PyQt6 Applikation

Zurück

Liebe Leserinnen und Leser, hallo du. Dieses Programm konvertiert den Inhalt einer Audiosprachdatei in Text. Das Programm besteht aus drei Teilen. Teil 1: Audio mit einem Mikrofon aufnehmen. Teil 2: Die aufgenommene Sprache in Text konvertieren und in einem Word-Dokument speichern. Teil 3: Das Word-Dokument prüfen auf Grammatik, Zeichensetzung und Rechtschreibung.


Sprachaufzeichnung


1️⃣ Sprachrekorder: Vor dem Hochfahren der Software muss ein Mikrofon am PC angeschlossen sein. Ansonsten erfolgt eine Fehlermeldung und die Bedienelemente sind gesperrt. Bei mehreren angeschlossenen Mikrofonen gibt es eine Dropdown-Liste zur Auswahl. Im Standard wird die Sprachdatei im gleichen Verzeichnis wie die Applikation plus ein Unterverzeichnis „records“ archiviert. Wenn gewünscht, kann man mit Betätigung der Taste „Durchsuchen“ (Explorer wird geöffnet) einen anderen Speicherort wählen. Vier Taster steuern die Aufnahme. Start: Audiostream wird geöffnet, Pegelanzeige Mikrofon wird scharfgeschaltet. Pause: Audiostream bleibt geöffnet, nimmt aber keine Geräusche auf. Stopp: Audio wird in einer *.wav-Datei gespeichert und der Stream wird geschlossen. Abspielen: Transfer der Audiosignale zum Lautsprecher.

2️⃣ Transkribieren. Umwandeln von Audioaufnahmen in einen geschriebenen Text: Mit Betätigung des Buttons „Sprache in Text konvertieren“ wird die Audio-Sprachdatei (*.wav) in einen Text umgewandelt und anschließend ein Word-Dokument (*.docx) gespeichert. Weiterhin öffnet die Applikation dieses Dokument mit dem Windows-Programm Microsoft Word. Hier können dann schon die ersten Übersetzungsfehler korrigiert werden. Beim Transkribieren wird kein Kontakt zum Internet aufgenommen. Die Umwandlung übernimmt eine Python-Bibliothek „OpenAI Whisper“, der Prozess findet auf deinem PC lokal statt.

3️⃣ Prüfen & Korrigieren: Hier kontaktiert die Applikation das Internet. Man muss diese dritte Funktion nicht ausführen! „Aber man kann“ – „und wenn der Textinhalt keine sensiblen Daten enthält, dann steht einer weiteren Prüfung nichts im Weg“! Oder? Details siehe weiter unten in dieser Beschreibung.

Ein paar Hinweise möchte ich hier noch einfügen: Die Installation der Bibliothek „OpenAI Whisper“ (pip install -U openai-whisper) funktioniert problemlos. Jedoch muss ein zusätzliches Programm für Windows 11 installiert werden: Whisper benötigt zusätzlich FFmpeg auf dem System, um Audiodateien zu verarbeiten. Schau dir dieses Video an, damit hat die Installation von FFmpeg funktioniert. Einige „Unterstützer“ aus dem Internet sind nicht zielführend, musste ich leider feststellen. Weiterhin problemlos ist die Nutzung der Bibliothek „python-docx“ (pip install python-docx).
Der gesamte Text dieser Webseite wurde mit der Sprachrekorder-App erstellt. Das war doch recht aufwendig, aber es hat auch Spaß gemacht. Einige Erfahrungen habe ich gemacht und möchte sie dir mitteilen: Unterteile die Audio-Sprachaufnahme in viele kleine Dateien. Sprich langsam und deutlich. Einige Wörter übersetzt „Whisper“ immer falsch, z.B. das Wort „Python“. Benutze nach jedem Satz die Pausetaste, zum „Luftholen“. Nutze die dritte Funktion „Prüfen & Korrigieren“ nicht, wenn sensible Daten im Text vorkommen. Für die Namensgebung der einzelnen Audio-Dateien nutze nicht den Standard, sondern benutze den Schalter „Durchsuchen“, der als Hilfsmittel den Explorer öffnet.


Python Code
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ***************************************************************************** # Sprachrekorder & Audio zu Text & Prüfung: Rechtschreibung, Satzzeichen und # Grammatik. Diese Python-Sprachrekorder-App verwandelt deinen PC in ein # digitales Diktiergerät. Sie nimmt gesprochene Worte, Gespräche oder Geräusche # über das Mikrofon auf, speichert die Audiodateien ab und erlaubt es dir, # diese Sprachdatei in Text zu konvertieren. Voraussetzungen installieren: # pip install PyQt6 sounddevice scipy numpy. pip install -U openai-whisper. # Zusätzlich musst du das Audio-Tool "FFmpeg" auf deinem System installieren, # da Whisper dieses zwingend benötigt, um Audiodateien zu verarbeiten. # pip install python-docx. (c) Hans Busche Datum: 24.08.2026 # ***************************************************************************** import sys import os import datetime import subprocess import requests import numpy as np import scipy.io.wavfile as wav import sounddevice as sd from docx import Document import whisper from PyQt6.QtCore import Qt, QTimer, QRectF, QThread, pyqtSignal from PyQt6.QtGui import QPainter, QColor, QBrush, QPen, QIcon from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit, QLabel, QFileDialog, QComboBox, QTabWidget, QTextEdit ) # Verzeichnis dieser Datei, damit das Icon unabhängig vom Startort gefunden wird BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ICON_PATH = os.path.join(BASE_DIR, "Schaefer.ico") # ***************************************************************************** # Dark Mode Stylesheet inkl. TabBar Styling (QSS) # ***************************************************************************** DARK_STYLE = """ QMainWindow, QWidget { background-color: #121212; color: #E0E0E0; font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; } QTabWidget::pane { border: 1px solid #333333; background-color: #121212; top: -1px; } QTabBar::tab { background: #1E1E1E; color: #AAA; padding: 10px 20px; border-top-left-radius: 6px; border-top-right-radius: 6px; border: 1px solid #333333; margin-right: 2px; font-weight: bold; } QTabBar::tab:selected { background: #2D2D2D; color: #0D6EFD; border-bottom: 2px solid #0D6EFD; } QTabBar::tab:hover { background: #282828; color: #FFF; } QLineEdit, QComboBox, QTextEdit { background-color: #1E1E1E; border: 1px solid #333333; border-radius: 5px; padding: 6px 10px; color: #FFFFFF; } QComboBox QAbstractItemView { background-color: #1E1E1E; color: #FFFFFF; selection-background-color: #0D6EFD; } QLineEdit:focus, QComboBox:focus, QTextEdit:focus { border: 1px solid #0D6EFD; } QPushButton { background-color: #2D2D2D; border: 1px solid #444444; border-radius: 5px; padding: 8px 16px; color: #FFFFFF; font-weight: bold; } QPushButton:hover { background-color: #3D3D3D; } QPushButton:disabled { background-color: #1A1A1A; color: #555555; border: 1px solid #222222; } QPushButton#recordBtn { background-color: #198754; border: none; } QPushButton#recordBtn:hover { background-color: #157347; } QPushButton#pauseBtn { background-color: #FFC107; color: #000000; border: none; } QPushButton#pauseBtn:hover { background-color: #FFCA2C; } QPushButton#stopBtn { background-color: #DC3545; border: none; } QPushButton#stopBtn:hover { background-color: #BB2D3B; } QPushButton#playBtn, QPushButton#actionBtn { background-color: #0D6EFD; border: none; } QPushButton#playBtn:hover, QPushButton#actionBtn:hover { background-color: #0B5ED7; } """ # ***************************************************************************** # Pegelanzeige Mikrofon # ***************************************************************************** class LEDVuMeter(QWidget): def __init__(self, num_leds=18, parent=None): super().__init__(parent) self.num_leds = num_leds self.level = 0 self.setFixedHeight(18) def setLevel(self, level): self.level = max(0, min(100, level)) self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.setBrush(QBrush(QColor("#1E1E1E"))) painter.setPen(QPen(QColor("#333333"), 1)) painter.drawRoundedRect(0, 0, self.width() - 1, self.height() - 1, 4, 4) margin = 3 spacing = 2 available_width = self.width() - (2 * margin) - ((self.num_leds - 1) * spacing) led_width = available_width / self.num_leds led_height = self.height() - (2 * margin) active_leds = int((self.level / 100.0) * self.num_leds) for i in range(self.num_leds): x = margin + i * (led_width + spacing) y = margin if i < self.num_leds * 0.6: color_on, color_off = QColor("#198754"), QColor("#0A3622") elif i < self.num_leds * 0.85: color_on, color_off = QColor("#FFC107"), QColor("#5A4500") else: color_on, color_off = QColor("#DC3545"), QColor("#58151C") if i < active_leds: painter.setBrush(QBrush(color_on)) else: painter.setBrush(QBrush(color_off)) painter.setPen(Qt.PenStyle.NoPen) rect = QRectF(x, y, led_width, led_height) painter.drawRoundedRect(rect, 2, 2) # ***************************************************************************** # Thread! Whisper. Audio Text Konverter # ***************************************************************************** class WhisperWorker(QThread): finished = pyqtSignal(str) error = pyqtSignal(str) def __init__(self, audio_path): super().__init__() self.audio_path = audio_path def run(self): try: model = whisper.load_model("base") result = model.transcribe(self.audio_path) text = result.get("text", "").strip() self.finished.emit(text) except Exception as e: self.error.emit(str(e)) # ***************************************************************************** # Thread! Kontaktiert das Internet. API: languagetool.org # ***************************************************************************** class LanguageToolWorker(QThread): finished = pyqtSignal(str, int, str) error = pyqtSignal(str) def __init__(self, docx_path, lang='de-DE'): super().__init__() self.docx_path = docx_path self.lang = lang def run(self): try: doc = Document(self.docx_path) full_text = "\n".join([p.text for p in doc.paragraphs if p.text.strip()]) if not full_text.strip(): self.finished.emit(self.docx_path, 0, "Dokument enthält keinen Text.") return url = "https://api.languagetool.org/v2/check" data = {'text': full_text, 'language': self.lang} response = requests.post(url, data=data, timeout=10) if response.status_code == 429: self.error.emit("Rate-Limit erreicht! Bitte warte 1-2 Minuten.") return elif response.status_code != 200: self.error.emit(f"API Server Fehler: HTTP {response.status_code}") return result = response.json() matches = result.get('matches', []) total_matches = len(matches) corrected_text = full_text for match in sorted(matches, key=lambda x: x['offset'], reverse=True): replacements = match.get('replacements', []) if replacements: replacement = replacements[0]['value'] offset = match['offset'] length = match['length'] corrected_text = corrected_text[:offset] + replacement + corrected_text[offset + length:] new_doc = Document() new_doc.add_heading("Korrigierte Transkription", level=1) for paragraph_text in corrected_text.split("\n"): if paragraph_text.strip(): new_doc.add_paragraph(paragraph_text) corrected_path = os.path.splitext(self.docx_path)[0] + "_korrigiert.docx" new_doc.save(corrected_path) self.finished.emit(corrected_path, total_matches, corrected_text) except Exception as e: self.error.emit(str(e)) # ***************************************************************************** # Sprachrekorder (Audio) # ***************************************************************************** class RecorderTab(QWidget): def __init__(self, default_path): super().__init__() self.default_path = default_path self.sample_rate = 44100 self.channels = 1 self.audio_data = [] self.is_recording = False self.is_paused = False self.elapsed_seconds = 0 self.current_volume = 0 self.devices_map = {} self.stream = None self.timer = QTimer() self.timer.timeout.connect(self.update_ui_counters) self.init_ui() self.load_input_devices() def init_ui(self): layout = QVBoxLayout(self) layout.setSpacing(12) layout.setContentsMargins(15, 15, 15, 15) device_layout = QHBoxLayout() device_label = QLabel("Mikrofon:") device_label.setFixedWidth(70) self.device_combo = QComboBox() device_layout.addWidget(device_label) device_layout.addWidget(self.device_combo) path_layout = QHBoxLayout() path_label = QLabel("Pfad:") path_label.setFixedWidth(70) self.path_input = QLineEdit() self.path_input.setText(self.default_path) self.browse_btn = QPushButton("Durchsuchen...") self.browse_btn.clicked.connect(self.browse_file) path_layout.addWidget(path_label) path_layout.addWidget(self.path_input) path_layout.addWidget(self.browse_btn) info_layout = QHBoxLayout() self.time_label = QLabel("00:00:00") self.time_label.setStyleSheet("font-size: 24px; font-weight: bold; color: #0D6EFD;") self.vu_meter = LEDVuMeter(num_leds=18) info_layout.addWidget(self.time_label) info_layout.addWidget(self.vu_meter, stretch=1) # Tastenleiste btn_layout = QHBoxLayout() self.start_btn = QPushButton("Start") self.start_btn.setObjectName("recordBtn") self.start_btn.clicked.connect(self.start_recording) self.pause_btn = QPushButton("Pause") self.pause_btn.setObjectName("pauseBtn") self.pause_btn.setEnabled(False) self.pause_btn.clicked.connect(self.pause_recording) self.stop_btn = QPushButton("Stopp") self.stop_btn.setObjectName("stopBtn") self.stop_btn.setEnabled(False) self.stop_btn.clicked.connect(self.stop_recording) self.play_btn = QPushButton("Abspielen") self.play_btn.setObjectName("playBtn") self.play_btn.clicked.connect(self.play_audio) btn_layout.addWidget(self.start_btn) btn_layout.addWidget(self.pause_btn) btn_layout.addWidget(self.stop_btn) btn_layout.addWidget(self.play_btn) self.status_label = QLabel("Bereit") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.status_label.setStyleSheet("color: #AAA; font-style: italic;") layout.addLayout(device_layout) layout.addLayout(path_layout) layout.addLayout(info_layout) layout.addLayout(btn_layout) layout.addWidget(self.status_label) def load_input_devices(self): try: devices = sd.query_devices() self.device_combo.clear() self.devices_map.clear() default_device = sd.query_devices(kind='input') default_index = default_device['index'] if default_device else None for i, dev in enumerate(devices): if dev['max_input_channels'] > 0: name = f"{dev['name']}" self.devices_map[name] = i self.device_combo.addItem(name) if i == default_index: self.device_combo.setCurrentText(name) except Exception as e: self.status_label.setText(f"Fehler beim Laden der Geräte: {str(e)}") def browse_file(self): file_path, _ = QFileDialog.getSaveFileName( self, "WAV-Datei speichern", self.path_input.text(), "WAV Datei (*.wav)" ) if file_path: if not file_path.lower().endswith(".wav"): file_path += ".wav" self.path_input.setText(file_path) def audio_callback(self, indata, frames, time_info, status): # Nur aufzeichnen, wenn Aufnahme läuft UND nicht pausiert ist if self.is_recording and not self.is_paused: self.audio_data.append(indata.copy()) rms = np.sqrt(np.mean(indata**2)) self.current_volume = int(min(1.0, rms * 5) * 100) else: self.current_volume = 0 def start_recording(self): save_path = self.path_input.text().strip() if not save_path: self.status_label.setText("Fehler: Kein Dateipfad angegeben!") self.status_label.setStyleSheet("color: #DC3545;") return # Fall A: Aus Pause fortsetzen if self.is_recording and self.is_paused: self.is_paused = False self.start_btn.setText("Start") self.start_btn.setEnabled(False) self.pause_btn.setEnabled(True) self.status_label.setText("Aufnahme fortgesetzt...") self.status_label.setStyleSheet("color: #198754;") self.timer.start(100) return # Fall B: Ganz neue Aufnahme starten device_id = self.devices_map.get(self.device_combo.currentText()) sd.stop() try: self.audio_data = [] self.is_recording = True self.is_paused = False self.elapsed_seconds = 0 self.stream = sd.InputStream( samplerate=self.sample_rate, channels=self.channels, device=device_id, callback=self.audio_callback ) self.stream.start() self.start_btn.setText("Start") self.start_btn.setEnabled(False) self.pause_btn.setEnabled(True) self.stop_btn.setEnabled(True) self.play_btn.setEnabled(False) self.browse_btn.setEnabled(False) self.path_input.setEnabled(False) self.device_combo.setEnabled(False) self.status_label.setText("Aufnahme läuft...") self.status_label.setStyleSheet("color: #198754;") self.timer.start(100) except Exception as e: self.status_label.setText(f"Fehler beim Starten: {str(e)}") self.status_label.setStyleSheet("color: #DC3545;") self.is_recording = False def pause_recording(self): if self.is_recording and not self.is_paused: self.is_paused = True self.timer.stop() self.vu_meter.setLevel(0) self.start_btn.setText("Fortsetzen") self.start_btn.setEnabled(True) self.pause_btn.setEnabled(False) self.status_label.setText("Aufnahme pausiert") self.status_label.setStyleSheet("color: #FFC107;") def stop_recording(self): if not self.is_recording: return self.is_recording = False self.is_paused = False self.timer.stop() try: if self.stream: self.stream.stop() self.stream.close() if self.audio_data: full_audio = np.concatenate(self.audio_data, axis=0) wav.write(self.path_input.text(), self.sample_rate, full_audio) self.status_label.setText(f"Gespeichert: {os.path.basename(self.path_input.text())}") self.status_label.setStyleSheet("color: #0D6EFD;") else: self.status_label.setText("Keine Audiodaten vorhanden.") except Exception as e: self.status_label.setText(f"Fehler beim Speichern: {str(e)}") self.status_label.setStyleSheet("color: #DC3545;") self.start_btn.setText("Start") self.start_btn.setEnabled(True) self.pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.play_btn.setEnabled(True) self.browse_btn.setEnabled(True) self.path_input.setEnabled(True) self.device_combo.setEnabled(True) self.vu_meter.setLevel(0) def play_audio(self): file_path = self.path_input.text().strip() if not os.path.exists(file_path): self.status_label.setText("Fehler: Datei existiert nicht!") self.status_label.setStyleSheet("color: #DC3545;") return try: sr, data = wav.read(file_path) sd.play(data, sr) self.status_label.setText(f"Wiedergabe: {os.path.basename(file_path)}") self.status_label.setStyleSheet("color: #0D6EFD;") except Exception as e: self.status_label.setText(f"Fehler beim Abspielen: {str(e)}") self.status_label.setStyleSheet("color: #DC3545;") def update_ui_counters(self): if not self.is_paused: self.elapsed_seconds += 0.1 formatted_time = str(datetime.timedelta(seconds=int(self.elapsed_seconds))) self.time_label.setText(formatted_time) self.vu_meter.setLevel(self.current_volume) # ***************************************************************************** # Sprachdatei (Audio) transkribieren. Von Audio zu Text. # ***************************************************************************** class TranscriberTab(QWidget): def __init__(self, default_path): super().__init__() self.default_path = default_path self.worker = None self.init_ui() def init_ui(self): layout = QVBoxLayout(self) layout.setSpacing(15) layout.setContentsMargins(15, 15, 15, 15) path_layout = QHBoxLayout() path_label = QLabel("Datei:") path_label.setFixedWidth(50) self.path_input = QLineEdit() self.path_input.setText(self.default_path) self.browse_btn = QPushButton("Durchsuchen...") self.browse_btn.clicked.connect(self.browse_file) path_layout.addWidget(path_label) path_layout.addWidget(self.path_input) path_layout.addWidget(self.browse_btn) self.convert_btn = QPushButton("Sprachdatei in Text konvertieren") self.convert_btn.setObjectName("actionBtn") self.convert_btn.clicked.connect(self.start_conversion) self.status_label = QLabel("Bereit") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.status_label.setStyleSheet("color: #AAA; font-style: italic;") layout.addLayout(path_layout) layout.addWidget(self.convert_btn) layout.addWidget(self.status_label) def browse_file(self): file_path, _ = QFileDialog.getOpenFileName( self, "Sprachdatei auswählen", self.path_input.text(), "Audio Dateien (*.wav *.mp3 *.m4a)" ) if file_path: self.path_input.setText(file_path) def start_conversion(self): audio_path = self.path_input.text().strip() if not os.path.exists(audio_path): self.status_label.setText("Fehler: Ausgewählte Datei existiert nicht!") self.status_label.setStyleSheet("color: #DC3545;") return self.convert_btn.setEnabled(False) self.browse_btn.setEnabled(False) self.path_input.setEnabled(False) self.status_label.setText("Konvertiere Sprache in Text (Whisper läuft)...") self.status_label.setStyleSheet("color: #0D6EFD;") self.worker = WhisperWorker(audio_path) self.worker.finished.connect(self.on_transcription_finished) self.worker.error.connect(self.on_transcription_error) self.worker.start() def on_transcription_finished(self, text): if not text: self.status_label.setText("Hinweis: Es konnte kein Sprachtext erkannt werden.") self.status_label.setStyleSheet("color: #FFC107;") self.reset_ui() return try: doc = Document() doc.add_heading("Transkription", level=1) doc.add_paragraph(text) audio_path = self.path_input.text().strip() docx_path = os.path.splitext(audio_path)[0] + ".docx" doc.save(docx_path) self.status_label.setText("Erfolgreich! Öffne Word-Dokument...") self.status_label.setStyleSheet("color: #198754;") self.open_document(docx_path) except Exception as e: self.status_label.setText(f"Fehler beim Erstellen der DOCX-Datei: {str(e)}") self.status_label.setStyleSheet("color: #DC3545;") self.reset_ui() def on_transcription_error(self, error_msg): self.status_label.setText(f"Whisper-Fehler: {error_msg}") self.status_label.setStyleSheet("color: #DC3545;") self.reset_ui() def open_document(self, file_path): if sys.platform == "win32": os.startfile(file_path) elif sys.platform == "darwin": subprocess.run(["open", file_path]) else: subprocess.run(["xdg-open", file_path]) def reset_ui(self): self.convert_btn.setEnabled(True) self.browse_btn.setEnabled(True) self.path_input.setEnabled(True) # ***************************************************************************** # Word-Dokument mit Languaetool prüfen # ***************************************************************************** class CheckerTab(QWidget): def __init__(self, default_path): super().__init__() self.default_path = os.path.splitext(default_path)[0] + ".docx" self.worker = None self.init_ui() def init_ui(self): layout = QVBoxLayout(self) layout.setSpacing(10) layout.setContentsMargins(15, 15, 15, 15) path_layout = QHBoxLayout() path_label = QLabel("Datei:") path_label.setFixedWidth(50) self.path_input = QLineEdit() self.path_input.setText(self.default_path) self.browse_btn = QPushButton("Durchsuchen...") self.browse_btn.clicked.connect(self.browse_file) path_layout.addWidget(path_label) path_layout.addWidget(self.path_input) path_layout.addWidget(self.browse_btn) self.check_btn = QPushButton("aufzeichnung.docx öffnen & prüfen") self.check_btn.setObjectName("actionBtn") self.check_btn.clicked.connect(self.start_checking) self.text_preview = QTextEdit() self.text_preview.setReadOnly(True) self.text_preview.setPlaceholderText("Ergebnis der Korrektur erscheint hier...") self.status_label = QLabel("Bereit") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.status_label.setStyleSheet("color: #AAA; font-style: italic;") layout.addLayout(path_layout) layout.addWidget(self.check_btn) layout.addWidget(self.text_preview) layout.addWidget(self.status_label) def browse_file(self): file_path, _ = QFileDialog.getOpenFileName( self, "Word-Dokument auswählen", self.path_input.text(), "Word Dokumente (*.docx)" ) if file_path: self.path_input.setText(file_path) def start_checking(self): docx_path = self.path_input.text().strip() if not os.path.exists(docx_path): self.status_label.setText("Fehler: Datei existiert nicht!") self.status_label.setStyleSheet("color: #DC3545;") return self.check_btn.setEnabled(False) self.browse_btn.setEnabled(False) self.path_input.setEnabled(False) self.status_label.setText("Verbinde mit LanguageTool & prüfe Text...") self.status_label.setStyleSheet("color: #0D6EFD;") self.worker = LanguageToolWorker(docx_path) self.worker.finished.connect(self.on_check_finished) self.worker.error.connect(self.on_check_error) self.worker.start() def on_check_finished(self, corrected_path, match_count, corrected_text): self.status_label.setText( f"Fertig! {match_count} Korrektur(en) vorgenommen. Korrigierte Datei gespeichert." ) self.status_label.setStyleSheet("color: #198754;") self.text_preview.setText(corrected_text) self.open_document(corrected_path) self.reset_ui() def on_check_error(self, error_msg): self.status_label.setText(f"Prüffehler: {error_msg}") self.status_label.setStyleSheet("color: #DC3545;") self.reset_ui() def open_document(self, file_path): if sys.platform == "win32": os.startfile(file_path) elif sys.platform == "darwin": subprocess.run(["open", file_path]) else: subprocess.run(["xdg-open", file_path]) def reset_ui(self): self.check_btn.setEnabled(True) self.browse_btn.setEnabled(True) self.path_input.setEnabled(True) # ***************************************************************************** # Hauptfenster mit Tab-Navigation # ***************************************************************************** class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Sprachrekorder, Transkribierer & Prüfer") # Icon if os.path.exists(ICON_PATH): self.setWindowIcon(QIcon(ICON_PATH)) self.resize(620, 480) self.setStyleSheet(DARK_STYLE) app_dir = os.path.dirname(os.path.abspath(__file__)) records_dir = os.path.join(app_dir, "records") os.makedirs(records_dir, exist_ok=True) default_file = os.path.join(records_dir, "aufzeichnung.wav") self.tabs = QTabWidget() self.tabs.addTab(RecorderTab(default_file), "🎙️ Aufnehmen") self.tabs.addTab(TranscriberTab(default_file), "📝 Transkribieren") self.tabs.addTab(CheckerTab(default_file), "🔍 Prüfen & Korrigieren") self.setCentralWidget(self.tabs) # ***************************************************************************** # Applikation starten # ***************************************************************************** if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) # ***************************************************************************** # Ende # *****************************************************************************


Das Skript ist eine vollständige Desktop-Anwendung mit grafischer Benutzeroberfläche (GUI), entwickelt in Python. Es kombiniert drei leistungsfähige Werkzeuge in einer Anwendung mit Tabs (Reitern). Hier ist der Überblick, wie das Programm im Detail aufgebaut ist und wie die einzelnen Teile zusammenarbeiten:

Das optische Grundgerüst & Design!
PyQt6 (GUI-Framework): Steuert die gesamte Benutzeroberfläche (Fenster, Knöpfe, Eingabefelder, Tabs). Dark Mode (DARK_STYLE): Ein zentraler CSS/QSS-Designblock sorgt dafür, dass die App im modernen, dunklen Design mit farbig hervorgehobenen Tasten (Grün für Start, Gelb für Pause, Rot für Stopp, Blau für Aktionen) erscheint. LEDVuMeter (Eigenes Widget): Ein selbstgezeichnetes Element (paintEvent), dass das Mikrofonsignal in 18 farbigen Segmenten (Grün, Gelb, Rot) wie an einem Mischpult visualisiert.

Die drei Hauptfunktionen (Tabs)!
Tab 1: 🎙️ Aufnehmen (RecorderTab). Aufnahmesteuerung: Über die Bibliothek sounddevice greift das Programm auf dein Mikrofon zu und nimmt den Ton mit 44,1 kHz in Mono auf. Pause-Logik: Bei einer Pause stoppt lediglich das Erfassen der Audiodaten und die Zeitzählung. Der Audio-Stream bleibt offen. Bei „Fortsetzen“ hängt das Programm neue Audiodaten einfach an das bestehende Array an (np.concatenate). Speichern & Abspielen: Bei Klick auf „Stopp“ wandelt scipy.io.wavfile die gesammelten Audiodaten in eine fertige .wav-Datei um. Über play_audio kann die Aufnahme sofort gegengehört werden.

Tab 2: 📝 Transkribieren (TranscriberTab), Spracherkennung (KI): Verwendet OpenAI Whisper (whisper.load_model("base")). Die KI analysiert die Wavedatei lokal auf deinem PC und wandelt die gesprochene Sprache in Text um. Word-Export: Die Bibliothek python-docx nimmt den erkannten Text und speichert ihn automatisch als sauber formatierte Word-Datei (.docx) ab. Anschließend wird das Dokument direkt auf deinem PC geöffnet.

Tab 3: 🔍 Prüfen & Korrigieren (CheckerTab). Grammatik- & Rechtschreibprüfung: Liest die zuvor erstellte Word-Datei ein und sendet den Text in einer einzigen, optimierten Anforderung an die API von LanguageTool. Rückwärts-Korrektur: Gefundene Fehler werden von hinten nach vorne im Text ersetzt (damit sich die Zeichen-Offsets bei Textänderungen nicht verschieben). Ausgabe: Das Ergebnis wird direkt im Programmfenster in einer Vorschau angezeigt und als neue Datei mit dem Suffix _korrigiert.docx abgespeichert und geöffnet.

Technische Highlights im Hintergrund!
Multi-Threading (QThread): Sowohl das Transkribieren (Whisper) als auch die Online-Prüfung (LanguageTool) laufen in eigenen Hintergrund-Prozessen (WhisperWorker, LanguageToolWorker). Dadurch friert die Benutzeroberfläche niemals ein, während die KI rechnet oder Daten aus dem Internet lädt. Intelligenter Ordnerpfad: Das Skript erstellt beim Start automatisch einen Ordner namens records im selben Verzeichnis, in dem das Skript liegt, um Verfasserdateien geordnet abzulegen.


Zurück