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.
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.
#!/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.
PyQt6 Sprachrekorder. Ohne Gewähr © 2026 Hans Busche