Thread, in dem TKinter läuft, friert nach beenden/killen (?) den Main-Thread ein

Fragen zu Tkinter.
Antworten
Fussel132
User
Beiträge: 23
Registriert: Mittwoch 13. März 2019, 13:55

Hallo Leute,
ich habe mal wieder Lust auf ein Python-Projekt bekommen und fand irgendwie die Idee des eigenen Sprachassistenten ganz reizend. Nun habe ich schon einmal die Spracherkennung hinbekommen und das funktioniert auch so weit, aber als ich per Sprachbefehl ein Tkinter-Fenster öffnen und wieder schließen wollte, kamen unerwartet eine Menge Probleme auf: sobald das Fenster offen ist, verhindert der mainloop(), dass die Spracherkennung weiterhin meine Stimme erkennen konnte. Ich habe mir daher überlegt, Tkinter in einen anderen Thread auszulagern und siehe da, zur Hälfte klappts! Das öffnen per Sprachbefehl und das Textfeld im Tkinter-Fenster lassen sich benutzen, allerdings hängt sich der ganze Spaß beim schließen einfach auf. Das Tkinter-Fenster geht noch zu, aber der Main-Thread wird dabei in Mitleidenschaft gezogen. Oder nicht? Es scheint zumindest bei

Code: Alles auswählen

self.root.destroy()
zu liegen, denn selbst, wenn die "Thread-Kill-Maßnahmen" auskommentiert sind, sorgt root.destroy() dafür, dass sich alles aufhängt. Bei auskommentiertem root.destroy() gibt es den Fehler
AttributeError: 'function' object has no attribute 'set'
. Kann mir da irgendwer weiterhelfen? Wie beende ich so einen Thread richtig (habe schon eine Menge Möglichkeiten aus dem Internet gesehen, aber keine hat funktioniert)? Und wie schließe ich mein Tkinter-Fenster, ohne dabei meine Sprachsteuerung zu zerschießen? Ich bin für jede Hilfe dankbar :)

--- CLI.py ---

Code: Alles auswählen

from tkinter import * 

import threading, ctypes

class CLI(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        print("Cli sollt erzeugt worden sein")
        #self.start()

    def stop(self):
        #self.root.destroy()
        self._stop.set()

    def stopped(self):
        return self._stop.isSet()
        
        
    def run(self):
        self.root=Tk()
        self.root.wm_title("C//_>")
        self.root.iconbitmap('Images/commandPromptImage.ico')
        green,black,blue='#39ff14','#000000','#0099ff'
        foreground_color=blue
        text_area = Text(self.root)
        text_area.grid(row=0, column=0, columnspan=4, sticky=N+S+W+E)
        text_area.configure(background=black,foreground=foreground_color)
        self.root.grid_columnconfigure(0, weight=1)
        self.root.grid_rowconfigure(0, weight=1)
        self.root.mainloop()
--- Commands.py ---

Code: Alles auswählen

from tkinter import * 

import threading, ctypes

class CLI(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        print("Cli sollt erzeugt worden sein")
        #self.start()

    def stop(self):
        #self.root.destroy()
        self._stop.set()

    def stopped(self):
        return self._stop.isSet()
        
        
    def run(self):
        self.root=Tk()
        self.root.wm_title("C//_>")
        self.root.iconbitmap('Images/commandPromptImage.ico')
        green,black,blue='#39ff14','#000000','#0099ff'
        foreground_color=blue
        text_area = Text(self.root)
        text_area.grid(row=0, column=0, columnspan=4, sticky=N+S+W+E)
        text_area.configure(background=black,foreground=foreground_color)
        self.root.grid_columnconfigure(0, weight=1)
        self.root.grid_rowconfigure(0, weight=1)
        self.root.mainloop()
--- SpeechToText.py ---

Code: Alles auswählen

from tkinter import * 

import threading, ctypes

class CLI(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        print("Cli sollt erzeugt worden sein")
        #self.start()

    def stop(self):
        #self.root.destroy()
        self._stop.set()

    def stopped(self):
        return self._stop.isSet()
        
        
    def run(self):
        self.root=Tk()
        self.root.wm_title("C//_>")
        self.root.iconbitmap('Images/commandPromptImage.ico')
        green,black,blue='#39ff14','#000000','#0099ff'
        foreground_color=blue
        text_area = Text(self.root)
        text_area.grid(row=0, column=0, columnspan=4, sticky=N+S+W+E)
        text_area.configure(background=black,foreground=foreground_color)
        self.root.grid_columnconfigure(0, weight=1)
        self.root.grid_rowconfigure(0, weight=1)
        self.root.mainloop()

Falls ich irgendetwas nicht gut genug beschrieben habe, ich versuchs auch gerne nochmal und damit ihr die ganzen Pakete, Sprachmodelle etc. beim eventuellen ausprobieren nicht herunterladen müsst, mach ich das auch gerne. Vielen vielen Dank im Voraus,
Fussel 132
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Fussel132: Tk muss im Hauptthread laufen.

Das erben von Thread macht hier keinen Sinn und das überschreiben von nicht-öffentlichen Methoden und abfragen von nicht-öffentlichen Zustand ist nicht gut bis falsch.

Warum postest Du dreimal den selben Quelltext unter unterschiedlichen Dateinamen?

Die GUI darf nur von dem Thread aus geändert werden, in der auch die `mainloop()` läuft, also im Hauptthread. Den Code kennen wir ja nicht, aber da steht ja ziemlich sicher so etwas drin, sonst wäre die GUI etwas sinnlos.

Der Spracherkennungscode muss in einem Thread laufen und beispielsweise mittels `after()`-Methode auf Widgets und Queues mit der GUI kommunizieren.

Sternchen-Importe sind Böse™. Da holt man sich gerade bei `tkinter` fast 200 Namen ins Modul von denen nur ein kleiner Bruchteil verwendet wird. Auch Namen die gar nicht in `tkinter` definiert werden, sondern ihrerseits von woanders importiert werden. Das macht Programme unnötig unübersichtlicher und fehleranfälliger und es besteht die Gefahr von Namenskollisionen.

`ctypes` wird importiert aber nicht verwendet.

Code: Alles auswählen

import tkinter as tk
from threading import Thread


def _run_cli():
    root = tk.Tk()
    root.title("C//_>")
    root.iconbitmap("Images/commandPromptImage.ico")
    text_area = tk.Text(root, background="black", foreground="#0099ff")
    text_area.grid(row=0, column=0, sticky=tk.NSEW)
    root.grid_columnconfigure(0, weight=1)
    root.grid_rowconfigure(0, weight=1)
    root.mainloop()


def run_cli():
    #
    # FIXME Must not start GUI in another than the main thread!
    #
    Thread(target=_run_cli, daemon=True).start()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Fussel132
User
Beiträge: 23
Registriert: Mittwoch 13. März 2019, 13:55

Hallo __blackjack__,
erstmal danke für die schnelle Antwort. Dass alle drei Codes gleich sind ist mir gar nicht aufgefallen, ich hänge die richtigen Programme dieser Nachricht an (vollständigkeitshalber) und entschuldige mich für das durcheinander. Ich übernehme aber deinen obigen Code und führe die drei Dateien zu einer oder zwei zusammen, ich versuche SpeechToText.py zu behalten, da es wirklich nur das gesprochene erkennt und die Wörter an die nach der Bearbeitung neu entstandene Hauptdatei übergeben soll. Wenn ich ein ganzes .py-Script direkt im extra Thread ausführen möchte, schreibe ich das Programm mit Erbung von Thread oder gibt es da eine bessere Möglichkeit (vielleicht ähnlich zu deiner letzten Zeile, nur dass target dann ein Script ist und nicht eine Funktion)?
Aber vielen Dank erstmal, ich probiere mal ein wenig herum das hinzubiegen :)


--- CLI.py ---
(Siehe die obigen drei :/)

--- Commands.py ---

Code: Alles auswählen

#in this command class all keywords as well as the functions they excecute are listed

from CLI import *
import sys
sys.path.append('..')
import Video.MP3player

global my_cli
my_cli = CLI()

#class Commands:
#    
#    def __init__(self, input):
#        print("Commands wurde gerufen und der constructor ausgeführt")
#        myDict.get(input, lambda:'Invalid')()
        
#alle funktionen die durch befehle executed werden müssen
#root=Tk()
def eins(): 
    print ("das war 1")
def zwei(): 
    print ("das war 2")
def drei(): 
    print ("das war 3")
def start_cli():
    print('öffne CLI....')
    my_cli.start()

def undefined():
    print("Entschuldigung Sir, ich konnte sie nicht verstehen")

    
def destroy_cli():
    print('schliesse cli...')
    my_cli.stop()
    my_cli.join()
def resize_all():
    resize_all
def convertVideo():
    print('Du muss VideoConversion noch importieren und integrieren')
def play_music():
    print('du musst musik integration noch hinzufügen')
    

#hier werden die keywords den zugehörigen funktionen zugeordnet
myDict={
        'eins': eins,
        'zwei': zwei,
        'drei': drei,
        'das system starten':start_cli,
        'beende steuerung':destroy_cli,
        'pass video an':resize_all,
        'video':convertVideo,
        'mach mal Stimmung':play_music
        
    }
def recieve_comand(input):
    myDict.get(input,undefined)()
--- SpeechToText.py ---

Code: Alles auswählen

#!/usr/bin/env python3
import threading

from Commands import *
import argparse
import os
import queue
import sounddevice as sd
import vosk
import sys
from vosk import SetLogLevel
import json
SetLogLevel(-1)

q = queue.Queue()

def int_or_str(text):
    """Helper function for argument parsing."""
    try:
        return int(text)
    except ValueError:
        return text

def callback(indata, frames, time, status):
    """This is called (from a separate thread) for each audio block."""
    if status:
        print(status, file=sys.stderr)
    q.put(bytes(indata))

parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
    '-l', '--list-devices', action='store_true',
    help='show list of audio devices and exit')
args, remaining = parser.parse_known_args()
if args.list_devices:
    print(sd.query_devices())
    parser.exit(0)
parser = argparse.ArgumentParser(
    description=__doc__,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    parents=[parser])
parser.add_argument(
    '-f', '--filename', type=str, metavar='FILENAME',
    help='audio file to store recording to')
parser.add_argument(
    '-m', '--model', type=str, metavar='MODEL_PATH',
    help='Path to the model')
parser.add_argument(
    '-d', '--device', type=int_or_str,
    help='input device (numeric ID or substring)')
parser.add_argument(
    '-r', '--samplerate', type=int, help='sampling rate')
args = parser.parse_args(remaining)


try:
    if args.model is None:
        args.model = "model"
    if not os.path.exists(args.model):
        print ("Please download a model for your language from https://alphacephei.com/vosk/models")
        print ("and unpack as 'model' in the current folder.")
        parser.exit(0)
    if args.samplerate is None:
        device_info = sd.query_devices(args.device, 'input')
        # soundfile expects an int, sounddevice provides a float:
        args.samplerate = int(device_info['default_samplerate'])

    model = vosk.Model(args.model)

    if args.filename:
        dump_fn = open(args.filename, "wb")
    else:
        dump_fn = None

    with sd.RawInputStream(samplerate=args.samplerate, blocksize = 8000, device=args.device, dtype='int16',
                            channels=1, callback=callback):
            print('#' * 80)
            print('Press Ctrl+C to stop the recording')
            print('#' * 80)

            rec = vosk.KaldiRecognizer(model, args.samplerate)
            while True:
                data = q.get()
                if rec.AcceptWaveform(data):
                    recResults=json.loads(rec.Result())['text']
                    print(recResults)
                    recieve_comand(recResults)
                    print("commands wurde gerufen")
                else:
                    "print(rec.PartialResult())"
                if dump_fn is not None:
                    dump_fn.write(data)

except KeyboardInterrupt:
    print('\nDone')
    parser.exit(0)
except Exception as e:
    parser.exit(type(e).__name__ + ': ' + str(e))
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Fussel132: Es macht keinen Sinn das auf drei Module aufzuteilen wenn dann zwei Module Sternchen-Importe verwenden und das alles wieder in einem Namensraum landet. Bei der Reihenfolge wird auch deutlich wie blöd *-Importe sind, weil `SpeechToText` per Sternchen-Import alles aus `Commands` holt, was seinerseits per *-Import alles aus `CLI` holt. Da hat man letztlich Null Kontrolle und Übersicht was man sich da alles mit einfängt.

An `sys.path` etwas über dem aktuellen Arbeitsverzeichnis anzuhängen ist falsch. Da sind dann Module über verschiedene Wege importierbar, was leicht zu Verwirrung und Chaos führen kann.

``global`` solltest Du ganz schnell wieder vergessen. Auf Modulebene hat das auch überhaupt gar keinen Effekt. Da sollte aber auch keine Variable stehen, und auch das Hauptprogramm gehört da nicht einfach so hin. Da sollte nur Code stehen, der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Funktionen und Methoden bekommen alles was sie ausser Konstanten benötigen, als Argument(e) übergeben.

`my_` ist eine unsinnige Vorsilbe bei Namen wenn es den gleichen Namen nicht auch mit `our_`, `their_` oder ähnlich gibt. Einfach nur `my_` enthält keinerlei sinnvolle Information für den Leser.

Zweimal hintereinander einen `ArgumentParser` zu erstellen und abzuarbeiten ist schräg. Das sollte nur einer sein.

Argumenten kann man einen Defaultwert geben, dann muss man das nicht selbst mit extra Code lösen.

Was soll `dump_fn` bedeuten? `fn` kenne ich als Abkürzung für „function” und „filename“, aber dieses Objekt ist weder noch. Darum sind kryptische Abkürzungen in Namen keine gute Idee.

`rec` ist auch nicht gut weil man das im Zusammenhang mit Audio auch als Abkürzung für `recording` missverstehen kann, es ist aber ein `recognizer`.

Wenn ein Problem aufgetreten ist, sollte ein Programm nicht mit einem Rückgabecode von 0 beendet werden, denn der bedeutet normalerweise problemloser Programmablauf.

Bei GUI-Code ist es in der Regel am einfachsten wenn man den zum Hauptprogramm macht und alles notwendige in die Hauptschleife/Ereignisverwaltung vom GUI-Rahmenwerk integriert. Hier könnte man beispielsweise den Körper der ``while``-Schleife in eine Funktion stecken und die vom GUI-Rahmenwerk regelmässig aufrufen lassen.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import argparse
import json
import os
import queue
import sys
import tkinter as tk
from functools import partial

import sounddevice as sd
import vosk


def eins():
    print("das war 1")


def zwei():
    print("das war 2")


def drei():
    print("das war 3")


def start_cli(cli):
    print("öffne CLI....")
    cli.deiconify()


def destroy_cli(cli):
    print("schliesse cli...")
    cli.withdraw()


def undefined():
    print("Entschuldigung Sir, ich konnte sie nicht verstehen")


def resize_all():
    ...


def convert_video():
    print("Du musst VideoConversion noch importieren und integrieren")


def play_music():
    print("du musst musik integration noch hinzufügen")


def recieve_command(command_to_action, text):
    command_to_action.get(text, undefined)()


def int_or_str(text):
    """Helper function for argument parsing."""
    try:
        return int(text)
    except ValueError:
        return text


def on_audio_block(audio_blocks, indata, _frames, _time, status):
    """
    This is called (from a separate thread) for each audio block.
    """
    if status:
        print(status, file=sys.stderr)
    audio_blocks.put(bytes(indata))


def process_blocks(widget, audio_blocks, file, recognizer, command_to_action):
    try:
        data = audio_blocks.get_nowait()
    except queue.Empty:
        pass
    else:
        if recognizer.AcceptWaveform(data):
            text = json.loads(recognizer.Result())["text"]
            print(text)
            recieve_command(command_to_action, text)
            print("command wurde gerufen")

        file.write(data)

    widget.after(
        100,
        process_blocks,
        widget,
        audio_blocks,
        file,
        recognizer,
        command_to_action,
    )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-l",
        "--list-devices",
        action="store_true",
        help="show list of audio devices and exit",
    )
    parser.add_argument(
        "-f",
        "--filename",
        type=str,
        default=os.devnull,
        metavar="FILENAME",
        help="audio file to store recording to",
    )
    parser.add_argument(
        "-m",
        "--model",
        type=str,
        default="model",
        metavar="MODEL_PATH",
        help="Path to the model",
    )
    parser.add_argument(
        "-d",
        "--device",
        type=int_or_str,
        help="input device (numeric ID or substring)",
    )
    parser.add_argument("-r", "--samplerate", type=int, help="sampling rate")
    args = parser.parse_args()

    if args.list_devices:
        print(sd.query_devices())
    else:
        try:
            if not os.path.exists(args.model):
                sys.exit(
                    "Please download a model for your language"
                    " from https://alphacephei.com/vosk/models\n"
                    "and unpack as 'model' in the current folder."
                )

            if args.samplerate is None:
                # soundfile expects an int, sounddevice provides a float:
                args.samplerate = int(
                    sd.query_devices(args.device, "input")[
                        "default_samplerate"
                    ]
                )

            root = tk.Tk()
            root.title("C//_>")
            root.iconbitmap("Images/commandPromptImage.ico")
            text_area = tk.Text(root, background="black", foreground="#0099ff")
            text_area.grid(row=0, column=0, sticky=tk.NSEW)
            root.grid_columnconfigure(0, weight=1)
            root.grid_rowconfigure(0, weight=1)
            root.withdraw()

            command_to_action = {
                "eins": eins,
                "zwei": zwei,
                "drei": drei,
                "das system starten": partial(start_cli, root),
                "beende steuerung": partial(destroy_cli, root),
                "pass video an": resize_all,
                "video": convert_video,
                "mach mal Stimmung": play_music,
            }

            vosk.SetLogLevel(-1)
            model = vosk.Model(args.model)
            with open(args.filename, "wb") as file:
                audio_blocks = queue.Queue()
                with sd.RawInputStream(
                    samplerate=args.samplerate,
                    blocksize=8000,
                    device=args.device,
                    dtype="int16",
                    channels=1,
                    callback=partial(on_audio_block, audio_blocks),
                ):
                    print("#" * 80)
                    print("Press Ctrl+C to stop the recording")
                    print("#" * 80)
                    recognizer = vosk.KaldiRecognizer(model, args.samplerate)
                    process_blocks(
                        root, audio_blocks, file, recognizer, command_to_action
                    )
                    root.mainloop()

        except KeyboardInterrupt:
            print("\nDone")


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Fussel132
User
Beiträge: 23
Registriert: Mittwoch 13. März 2019, 13:55

Hi __blackjack__,
vielen Dank nochmal, ich sehe ein, mehrere Dateien zu benutzen ist in dem Fall nicht wirklich die beste Idee und ich hatte eigentlich vor, am Ende alles nochmal neu zu schreiben, da Teile mehr oder weniger Learning by doing entstanden sind. Ich habe jetzt alle deine Tipps in mein Programm übernommen und auch den Projektordner umstrukturiert, dass man nicht mehr in das Parent Directory wechseln muss sondern ein einfacher Import reicht. Mit GUI hatte ich noch nicht so viel am Hut und hab daher ein wenig mit Threads rumgespielt und ausprobiert aber alles an einer Stelle zu haben ist wirklich die beste Idee, danke :).
In dem Sinne hat sich für mich erstmal alles erklärt und ich stürze mich mal kopfüber in das baldige nächste Problem.
Danke und einen schönen Abend,
Fussel132
Antworten