jimmmy

Python-Skript für Vosk Speech to Text

Hi allerseits!

Gleich vorweg. Ich habe keine Ahnung vom Programmieren, versuche aber mit Hilfe vom ollen ChatGPT nen Python-Skript für Vosk zu erstellen. Soweit so gut, für meine Verhältnisse bin ich (und die 'Blechkiste') schon weit gekommen :D

Aber der vorgelesene Text erscheint einfach doppelt und dreifach. Es soll aber ein kompakter Text zum Kopieren erscheinen. Also quasi das, was man vorliest z. B.

Hier mal aus Coelhos Jakobsweg vorgelesen :D

Spracherkennung gestartet...
buch
buch erzählt
buch erzählt paul
buch erzählt paul corallo
buch erzählt paul corallo vorn
buch erzählt paul corallo vorn seiner
buch erzählt paul corallo vorn seiner pilgerreise
buch erzählt paul corallo vorn seiner pilgerreise auf
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg eine
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg eine mittelalterliche
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad der von
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad der von den
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad der von den pyrenäen
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad der von den pyrenäen durch
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad der von den pyrenäen durch nordspanien
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad der von den pyrenäen durch nordspanien nach
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad der von den pyrenäen durch nordspanien nach santiago
buch erzählt paul corallo vorn seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad der von den pyrenäen durch nordspanien nach santiago de
buch erzählt paul corallo von seiner pilgerreise auf dem jakobsweg eine mittelalterlichen wallfahrt pfad der von den pyrenäen durch nordspanien nach santiago de compostela
dafür
führt
a führt

Spracherkennung gestoppt.

Das ist natürlich nicht so der Hit (Spracherkennung mal bei Seite gelassen).

Vielleicht hat jemand bereits was mit Vosk zu tun gehabt und löste bereits das Problem.
Hier der bisherige code:

import queue
import sounddevice as sd
import vosk
import json
import numpy as np
import tkinter as tk
import threading

# Sprach-Modell
model = vosk.Model("/home/user/Test/Vosk/vosk-model-de-0.21/")  

# Audio-Queue
q = queue.Queue()

# Variable zur Steuerung des Aufnahmeprozesses
is_recording = False

# Callback für das Mikrofon
def callback(indata, frames, time, status):
    if status:
        print("Status:", status)  
    if is_recording:
        q.put(indata.copy())

# Funktion, um das Textfeld zu aktualisieren
def update_textbox(text):
    textbox.insert(tk.END, text + '\n')  
    textbox.yview(tk.END)  # Scrollt zum letzten Eintrag

# Funktion für das Stoppen der Aufnahme
def stop_recording():
    global is_recording
    is_recording = False
    update_textbox("Spracherkennung gestoppt.")  

# Funktion für das Starten der Aufnahme
def start_recording():
    global is_recording
    is_recording = True
    update_textbox("Spracherkennung gestartet...")  

    try:
        # Audio streamen und Spracherkennung durchführen
        with sd.InputStream(samplerate=16000, blocksize=8000, dtype='int16', channels=1, callback=callback):  
            rec = vosk.KaldiRecognizer(model, 16000)
            last_full_text = ""  # Speichert den letzten vollständigen Text  
            last_partial_text = ""  # Speichert das letzte Teilergebnis (für Vergleich)  

            while is_recording:
                data = q.get()

                if isinstance(data, np.ndarray):
                    data = data.tobytes()

                # Wenn vollständiges Ergebnis vorhanden ist
                if rec.AcceptWaveform(data):
                    result = json.loads(rec.Result())
                    text = result["text"]  

                    # Verhindert doppelte Ausgaben von Text
                    if text != last_full_text:
                        update_textbox(text)
                        last_full_text = text

                # Wenn nur ein Teilergebnis vorliegt
                else:
                    partial = json.loads(rec.PartialResult())
                    partial_text = partial["partial"]  

                    # Nur den Text anzeigen, wenn er sich vom letzten Teilergebnis unterscheidet
                    if partial_text != last_partial_text:
                        update_textbox(partial_text)
                        last_partial_text = partial_text

                # Fenster aktualisieren
                root.update()

    except Exception as e:
        print("Fehler:", e)  
        stop_recording()

# Funktion, die das Kopieren des Texts im Textfeld ermöglicht
def copy_text(event=None):
    try:
        # Holt den aktuell markierten Text und kopiert ihn in die Zwischenablage
        root.clipboard_clear()
        root.clipboard_append(textbox.get(tk.SEL_FIRST, tk.SEL_LAST))
        root.update()  # Für den Fall, dass ein event-handler genutzt wird
    except tk.TclError:
        pass  # Fehler, falls kein Text markiert ist

# GUI für die Anzeige
root = tk.Tk()
root.title("Spracherkennung")  

# Fenstergröße anpassen
root.geometry("800x600")  # Setzt die Fenstergröße auf 800x600 Pixel  

# Textfeld und Scrollbar hinzufügen
textbox_frame = tk.Frame(root)
textbox_frame.pack(fill=tk.BOTH, expand=True)

scrollbar = tk.Scrollbar(textbox_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

# Textfeld anpassen, damit der Text kopiert werden kann
textbox = tk.Text(textbox_frame, height=20, width=80, wrap=tk.WORD, font=("Courier", 12), state=tk.NORMAL)  
textbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

# Verbinde die Scrollbar mit dem Textfeld
textbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=textbox.yview)

# Rechtsklick-Menü (Context Menu) für das Textfeld
def show_context_menu(event):
    context_menu.post(event.x_root, event.y_root)

# Kontextmenü erstellen
context_menu = tk.Menu(root, tearoff=0)
context_menu.add_command(label="Kopieren", command=copy_text)  

# Event für den Rechtsklick (Rechtsklick-Menü anzeigen)
textbox.bind("<Button-3>", show_context_menu)  

# Start-Button
start_button = tk.Button(root, text="Start", command=lambda: threading.Thread(target=start_recording, daemon=True).start(), font=("Helvetica", 14))  
start_button.pack(pady=10)

# Stop-Button
stop_button = tk.Button(root, text="Stop", command=stop_recording, font=("Helvetica", 14))  
stop_button.pack(pady=10)

# Beenden-Button
exit_button = tk.Button(root, text="Beenden", command=root.quit, font=("Helvetica", 14))  
exit_button.pack(pady=10)

root.mainloop()

Und bitte nicht erschlagen, weil ich statt Python richtig zu lernen mit dem ChatGPT rumpfusche :D

Grüße
jim
Auf Facebook teilen
Auf X (Twitter) teilen
Auf Reddit teilen
Auf Linkedin teilen

Content-ID: 673925

Url: https://administrator.de/forum/vosk-spracherkennung-python-tkinter-673925.html

Ausgedruckt am: 19.07.2025 um 05:07 Uhr

jimmmy
jimmmy 18.07.2025 um 23:36:15 Uhr
oder gibts bereits ein Projekt, welches das umgesetzt hat?
Ich vermute, aber ich finde die Landschaft um Vosk unglaublich unübersichtlich.
Pjordorf
Pjordorf 18.07.2025 um 23:50:00 Uhr
Hallo,

Zitat von @jimmmy:
Aber der vorgelesene Text erscheint einfach doppelt und dreifach. Es soll aber ein kompakter Text zum Kopieren erscheinen. Also quasi das, was man vorliest z. B.
Also insgesamt 6 mal (1+2+3)
Dann nicht so oft räuspern damit vosk dich auch versteht.

Gruss,
Peter
jimmmy
jimmmy 19.07.2025 um 00:10:29 Uhr
das ist schon mal ein super Tipp... und etwas Ernsthaftes? ;)
firefly
firefly 19.07.2025 aktualisiert um 01:23:11 Uhr
Ich sehe das Problem! Vosk gibt sowohl Teilergebnisse (PartialResult) als auch finale Ergebnisse (Result) aus. Die Teilergebnisse zeigen den fortlaufenden Erkennungsprozess, während das finale Ergebnis den vollständigen erkannten Satz enthält.

Das Problem in deinem Code ist, dass du beide Arten von Ergebnissen anzeigst, was zu den Wiederholungen führt. Hier ist die korrigierte Version:

import queue
import sounddevice as sd
import vosk
import json
import numpy as np
import tkinter as tk
import threading

# Sprach-Modell
model = vosk.Model("/home/user/Test/Vosk/vosk-model-de-0.21/")  

# Audio-Queue
q = queue.Queue()

# Variable zur Steuerung des Aufnahmeprozesses
is_recording = False

# Callback für das Mikrofon
def callback(indata, frames, time, status):
    if status:
        print("Status:", status)  
    if is_recording:
        q.put(indata.copy())

# Funktion, um das Textfeld zu aktualisieren
def update_textbox(text):
    textbox.config(state=tk.NORMAL)
    textbox.insert(tk.END, text + ' ')  # Fügt Text mit Leerzeichen hinzu  
    textbox.yview(tk.END)  # Scrollt zum letzten Eintrag
    textbox.config(state=tk.NORMAL)

# Funktion für das Stoppen der Aufnahme
def stop_recording():
    global is_recording
    is_recording = False
    textbox.config(state=tk.NORMAL)
    textbox.insert(tk.END, "\n\nSpracherkennung gestoppt.")  
    textbox.config(state=tk.NORMAL)

# Funktion für das Starten der Aufnahme
def start_recording():
    global is_recording
    is_recording = True
    textbox.config(state=tk.NORMAL)
    textbox.insert(tk.END, "Spracherkennung gestartet...\n\n")  
    textbox.config(state=tk.NORMAL)

    try:
        # Audio streamen und Spracherkennung durchführen
        with sd.InputStream(samplerate=16000, blocksize=8000, dtype='int16', channels=1, callback=callback):  
            rec = vosk.KaldiRecognizer(model, 16000)
            
            while is_recording:
                data = q.get()

                if isinstance(data, np.ndarray):
                    data = data.tobytes()

                # Nur finale Ergebnisse verarbeiten
                if rec.AcceptWaveform(data):
                    result = json.loads(rec.Result())
                    text = result["text"]  
                    
                    # Nur nicht-leere Texte hinzufügen
                    if text.strip():
                        update_textbox(text)
                
                # Teilergebnisse ignorieren wir jetzt komplett
                # Das verhindert die Wiederholungen
                
                # Fenster aktualisieren
                root.update()

    except Exception as e:
        print("Fehler:", e)  
        stop_recording()

# Funktion für das Löschen des Textfelds
def clear_text():
    textbox.config(state=tk.NORMAL)
    textbox.delete(1.0, tk.END)
    textbox.config(state=tk.NORMAL)

# Funktion, die das Kopieren des Texts im Textfeld ermöglicht
def copy_text(event=None):
    try:
        # Holt den aktuell markierten Text und kopiert ihn in die Zwischenablage
        root.clipboard_clear()
        root.clipboard_append(textbox.get(tk.SEL_FIRST, tk.SEL_LAST))
        root.update()
    except tk.TclError:
        # Falls kein Text markiert ist, kopiere den gesamten Text
        root.clipboard_clear()
        root.clipboard_append(textbox.get(1.0, tk.END))
        root.update()

# GUI für die Anzeige
root = tk.Tk()
root.title("Spracherkennung")  

# Fenstergröße anpassen
root.geometry("800x600")  

# Textfeld und Scrollbar hinzufügen
textbox_frame = tk.Frame(root)
textbox_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

scrollbar = tk.Scrollbar(textbox_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

# Textfeld anpassen
textbox = tk.Text(textbox_frame, height=20, width=80, wrap=tk.WORD, 
                  font=("Courier", 12), state=tk.NORMAL, bg="white", fg="black")  
textbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

# Verbinde die Scrollbar mit dem Textfeld
textbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=textbox.yview)

# Rechtsklick-Menü (Context Menu) für das Textfeld
def show_context_menu(event):
    context_menu.post(event.x_root, event.y_root)

# Kontextmenü erstellen
context_menu = tk.Menu(root, tearoff=0)
context_menu.add_command(label="Kopieren", command=copy_text)  
context_menu.add_command(label="Alles auswählen", command=lambda: textbox.tag_add(tk.SEL, "1.0", tk.END))  
context_menu.add_separator()
context_menu.add_command(label="Löschen", command=clear_text)  

# Event für den Rechtsklick
textbox.bind("<Button-3>", show_context_menu)  

# Button-Frame
button_frame = tk.Frame(root)
button_frame.pack(pady=10)

# Start-Button
start_button = tk.Button(button_frame, text="Start",   
                        command=lambda: threading.Thread(target=start_recording, daemon=True).start(), 
                        font=("Helvetica", 14), bg="green", fg="white", width=10)  
start_button.pack(side=tk.LEFT, padx=5)

# Stop-Button
stop_button = tk.Button(button_frame, text="Stop", command=stop_recording,   
                       font=("Helvetica", 14), bg="red", fg="white", width=10)  
stop_button.pack(side=tk.LEFT, padx=5)

# Löschen-Button
clear_button = tk.Button(button_frame, text="Löschen", command=clear_text,   
                        font=("Helvetica", 14), bg="blue", fg="white", width=10)  
clear_button.pack(side=tk.LEFT, padx=5)

# Beenden-Button
exit_button = tk.Button(button_frame, text="Beenden", command=root.quit,   
                       font=("Helvetica", 14), bg="gray", fg="white", width=10)  
exit_button.pack(side=tk.LEFT, padx=5)

# Tastenkombinationen
root.bind('<Control-c>', copy_text)  
root.bind('<Control-a>', lambda e: textbox.tag_add(tk.SEL, "1.0", tk.END))  

root.mainloop()

Der Text sollte jetzt so aussehen:

Spracherkennung gestartet...

das buch erzählt paul corallo von seiner pilgerreise auf dem jakobsweg einem mittelalterlichen wallfahrt pfad der von den pyrenäen durch nordspanien nach santiago de compostela führt

Spracherkennung gestoppt.


Hier ist eine erweiterte Version deines Codes mit:

  • Queue-Leerung beim Stoppen: Beim Stoppen wird die Warteschlange geleert, damit keine alten Audiodaten verarbeitet werden.
  • Timeout bei „queue.get()”: Durch einen Timeout von 0,1 Sekunden reagiert das Programm schneller auf den Stop-Button.
  • Mehrfachstart-Schutz: Der Start-Button startet nur einen neuen Thread, wenn nicht bereits eine Aufnahme läuft.

Dein Anliegen, die Wiederholungen loszuwerden, sollte mit dem oberen Code bereits gelöst sein. Mit den folgenden kleinen Änderungen wird das Programm noch stabiler.

import queue
import sounddevice as sd
import vosk
import json
import numpy as np
import tkinter as tk
import threading

# Sprach-Modell
model = vosk.Model("/home/user/Test/Vosk/vosk-model-de-0.21/")  

# Audio-Queue
q = queue.Queue()

# Variable zur Steuerung des Aufnahmeprozesses
is_recording = False

# Callback für das Mikrofon
def callback(indata, frames, time, status):
    if status:
        print("Status:", status)  
    if is_recording:
        q.put(indata.copy())

# Funktion, um das Textfeld zu aktualisieren
def update_textbox(text):
    textbox.config(state=tk.NORMAL)
    textbox.insert(tk.END, text + ' ')  # Fügt Text mit Leerzeichen hinzu  
    textbox.yview(tk.END)  # Scrollt zum letzten Eintrag
    textbox.config(state=tk.NORMAL)

# Funktion für das Stoppen der Aufnahme
def stop_recording():
    global is_recording
    is_recording = False
    # Queue leeren
    while not q.empty():
        try:
            q.get_nowait()
        except queue.Empty:
            break
    textbox.config(state=tk.NORMAL)
    textbox.insert(tk.END, "\n\nSpracherkennung gestoppt.")  
    textbox.config(state=tk.NORMAL)

# Funktion für das Starten der Aufnahme
def start_recording():
    global is_recording
    is_recording = True
    textbox.config(state=tk.NORMAL)
    textbox.insert(tk.END, "Spracherkennung gestartet...\n\n")  
    textbox.config(state=tk.NORMAL)

    try:
        # Audio streamen und Spracherkennung durchführen
        with sd.InputStream(samplerate=16000, blocksize=8000, dtype='int16', channels=1, callback=callback):  
            rec = vosk.KaldiRecognizer(model, 16000)
            
            while is_recording:
                try:
                    data = q.get(timeout=0.1)  # Timeout hinzugefügt
                except queue.Empty:
                    continue

                if isinstance(data, np.ndarray):
                    data = data.tobytes()

                # Nur finale Ergebnisse verarbeiten
                if rec.AcceptWaveform(data):
                    result = json.loads(rec.Result())
                    text = result["text"]  
                    
                    # Nur nicht-leere Texte hinzufügen
                    if text.strip():
                        update_textbox(text)
                
                # Teilergebnisse ignorieren wir jetzt komplett
                # Das verhindert die Wiederholungen
                
                # Fenster aktualisieren
                root.update()

    except Exception as e:
        print("Fehler:", e)  
        stop_recording()

# Funktion für das Löschen des Textfelds
def clear_text():
    textbox.config(state=tk.NORMAL)
    textbox.delete(1.0, tk.END)
    textbox.config(state=tk.NORMAL)

# Funktion, die das Kopieren des Texts im Textfeld ermöglicht
def copy_text(event=None):
    try:
        # Holt den aktuell markierten Text und kopiert ihn in die Zwischenablage
        root.clipboard_clear()
        root.clipboard_append(textbox.get(tk.SEL_FIRST, tk.SEL_LAST))
        root.update()
    except tk.TclError:
        # Falls kein Text markiert ist, kopiere den gesamten Text
        root.clipboard_clear()
        root.clipboard_append(textbox.get(1.0, tk.END))
        root.update()

# GUI für die Anzeige
root = tk.Tk()
root.title("Spracherkennung")  

# Fenstergröße anpassen
root.geometry("800x600")  

# Textfeld und Scrollbar hinzufügen
textbox_frame = tk.Frame(root)
textbox_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

scrollbar = tk.Scrollbar(textbox_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

# Textfeld anpassen
textbox = tk.Text(textbox_frame, height=20, width=80, wrap=tk.WORD, 
                  font=("Courier", 12), state=tk.NORMAL, bg="white", fg="black")  
textbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

# Verbinde die Scrollbar mit dem Textfeld
textbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=textbox.yview)

# Rechtsklick-Menü (Context Menu) für das Textfeld
def show_context_menu(event):
    context_menu.post(event.x_root, event.y_root)

# Kontextmenü erstellen
context_menu = tk.Menu(root, tearoff=0)
context_menu.add_command(label="Kopieren", command=copy_text)  
context_menu.add_command(label="Alles auswählen", command=lambda: textbox.tag_add(tk.SEL, "1.0", tk.END))  
context_menu.add_separator()
context_menu.add_command(label="Löschen", command=clear_text)  

# Event für den Rechtsklick
textbox.bind("<Button-3>", show_context_menu)  

# Button-Frame
button_frame = tk.Frame(root)
button_frame.pack(pady=10)

# Start-Button
start_button = tk.Button(button_frame, text="Start",   
                        command=lambda: threading.Thread(target=start_recording, daemon=True).start() if not is_recording else None, 
                        font=("Helvetica", 14), bg="green", fg="white", width=10)  
start_button.pack(side=tk.LEFT, padx=5)

# Stop-Button
stop_button = tk.Button(button_frame, text="Stop", command=stop_recording,   
                       font=("Helvetica", 14), bg="red", fg="white", width=10)  
stop_button.pack(side=tk.LEFT, padx=5)

# Löschen-Button
clear_button = tk.Button(button_frame, text="Löschen", command=clear_text,   
                        font=("Helvetica", 14), bg="blue", fg="white", width=10)  
clear_button.pack(side=tk.LEFT, padx=5)

# Beenden-Button
exit_button = tk.Button(button_frame, text="Beenden", command=root.quit,   
                       font=("Helvetica", 14), bg="gray", fg="white", width=10)  
exit_button.pack(side=tk.LEFT, padx=5)

# Tastenkombinationen
root.bind('<Control-c>', copy_text)  
root.bind('<Control-a>', lambda e: textbox.tag_add(tk.SEL, "1.0", tk.END))  

root.mainloop()


Sag mir bitte Bescheid, ob es funktioniert hat face-smile

P.S: Keine Sorge wegen ChatGPT - jeder fängt mal an, und wenn es funktioniert, ist das doch super!

@firefly