add hotkey support
This commit is contained in:
@@ -63,6 +63,9 @@ To get this script working you will need to install the following on the relevan
|
|||||||
* pip install python-dotenv
|
* pip install python-dotenv
|
||||||
* pip install wave
|
* pip install wave
|
||||||
* pip install pydub
|
* pip install pydub
|
||||||
|
* pip install keyboard
|
||||||
|
* pip install pystray
|
||||||
|
* pip install playsound
|
||||||
|
|
||||||
|
|
||||||
### Mac
|
### Mac
|
||||||
@@ -77,6 +80,7 @@ To get this script working you will need to install the following on the relevan
|
|||||||
* pip3 install wave
|
* pip3 install wave
|
||||||
* pip3 install pydub
|
* pip3 install pydub
|
||||||
|
|
||||||
|
|
||||||
# Making an installer
|
# Making an installer
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|||||||
130
text-to-mic.py
130
text-to-mic.py
@@ -1,12 +1,17 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import platform
|
import platform
|
||||||
from tkinter import ttk, messagebox, simpledialog, Menu
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import pyaudio
|
import pyaudio
|
||||||
import wave
|
import wave
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import json
|
import json
|
||||||
|
import keyboard
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
from pystray import Icon as icon, MenuItem as item, Menu as menu
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
from tkinter import ttk, messagebox, simpledialog, Menu
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -55,13 +60,14 @@ class Application(tk.Tk):
|
|||||||
|
|
||||||
self.create_menu()
|
self.create_menu()
|
||||||
self.initialize_gui()
|
self.initialize_gui()
|
||||||
|
self.setup_hotkeys()
|
||||||
|
|
||||||
def show_version(self):
|
def show_version(self):
|
||||||
instruction_window = tk.Toplevel(self)
|
instruction_window = tk.Toplevel(self)
|
||||||
instruction_window.title("App Version")
|
instruction_window.title("App Version")
|
||||||
instruction_window.geometry("300x150") # Width x Height
|
instruction_window.geometry("300x150") # Width x Height
|
||||||
|
|
||||||
instructions = """Version 1.0.6\n\n App by Scorchsoft.com"""
|
instructions = """Version 1.0.7\n\n App by Scorchsoft.com"""
|
||||||
|
|
||||||
tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=280).pack(padx=10, pady=10)
|
tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=280).pack(padx=10, pady=10)
|
||||||
|
|
||||||
@@ -96,10 +102,50 @@ class Application(tk.Tk):
|
|||||||
help_menu.add_command(label="How to Use", command=self.show_instructions)
|
help_menu.add_command(label="How to Use", command=self.show_instructions)
|
||||||
help_menu.add_command(label="Terms of Use and Licence", command=self.show_terms_of_use)
|
help_menu.add_command(label="Terms of Use and Licence", command=self.show_terms_of_use)
|
||||||
help_menu.add_command(label="Version", command=self.show_version)
|
help_menu.add_command(label="Version", command=self.show_version)
|
||||||
|
help_menu.add_command(label="Hotkey Instructions", command=self.show_hotkey_instructions)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_hotkeys(self):
|
||||||
|
# Register global hotkeys
|
||||||
|
keyboard.add_hotkey('ctrl+shift+r', lambda: self.hotkey_record_trigger())
|
||||||
|
keyboard.add_hotkey('ctrl+shift+s', lambda: self.hotkey_stop_trigger() )
|
||||||
|
keyboard.add_hotkey('ctrl+shift+l', lambda: self.hotkey_play_last_audio_trigger() )
|
||||||
|
|
||||||
|
def hotkey_play_last_audio_trigger(self):
|
||||||
|
if hasattr(self, 'last_audio_file'):
|
||||||
|
self.play_last_audio()
|
||||||
|
else:
|
||||||
|
self.play_sound('assets/no-last-audio.wav')
|
||||||
|
|
||||||
|
|
||||||
|
def hotkey_stop_trigger(self):
|
||||||
|
self.play_sound('assets/wrong-short.wav')
|
||||||
|
if self.recording:
|
||||||
|
self.stop_recording(auto_play=False)
|
||||||
|
|
||||||
|
# Sounds from https://mixkit.co/free-sound-effects/notification/
|
||||||
|
def hotkey_record_trigger(self):
|
||||||
|
|
||||||
|
if self.recording:
|
||||||
|
self.play_sound('assets/mixkit-message-pop-alert-2354.mp3')
|
||||||
|
self.submit_text()
|
||||||
|
else:
|
||||||
|
|
||||||
|
input_device_index = self.input_device_index.get() # Assuming input_device_index is a StringVar
|
||||||
|
input_device_id = self.available_input_devices.get(input_device_index)
|
||||||
|
if input_device_id is None:
|
||||||
|
self.play_sound('assets/please-select-input.wav')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.play_sound('assets/mixkit-message-pop-alert-2354.mp3')
|
||||||
|
self.toggle_recording()
|
||||||
|
|
||||||
|
|
||||||
|
def play_sound(self, sound_file):
|
||||||
|
pygame.init()
|
||||||
|
pygame.mixer.init()
|
||||||
|
sound = pygame.mixer.Sound(sound_file)
|
||||||
|
sound.play()
|
||||||
|
|
||||||
def initialize_gui(self):
|
def initialize_gui(self):
|
||||||
|
|
||||||
@@ -155,8 +201,8 @@ class Application(tk.Tk):
|
|||||||
self.record_button = ttk.Button(main_frame, text="Record Mic", command=self.toggle_recording)
|
self.record_button = ttk.Button(main_frame, text="Record Mic", command=self.toggle_recording)
|
||||||
self.record_button.grid(column=0, row=6, sticky=tk.W + tk.E, pady=(0, 20), padx=(0, 10)) # Left padding to separate buttons
|
self.record_button.grid(column=0, row=6, sticky=tk.W + tk.E, pady=(0, 20), padx=(0, 10)) # Left padding to separate buttons
|
||||||
|
|
||||||
submit_button = ttk.Button(main_frame, text="Play Audio", style="Green.TButton", command=self.submit_text )
|
self.submit_button = ttk.Button(main_frame, text="Play Audio", style="Green.TButton", command=self.submit_text )
|
||||||
submit_button.grid(column=1, row=6, sticky=tk.W + tk.E, pady=(0, 20), padx=(10, 0)) # Right padding to separate buttons
|
self.submit_button.grid(column=1, row=6, sticky=tk.W + tk.E, pady=(0, 20), padx=(10, 0)) # Right padding to separate buttons
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -181,7 +227,26 @@ class Application(tk.Tk):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def show_hotkey_instructions(self):
|
||||||
|
instruction_window = tk.Toplevel(self)
|
||||||
|
instruction_window.title("Hotkey Instructions")
|
||||||
|
instruction_window.geometry("400x300") # Width x Height
|
||||||
|
|
||||||
|
instructions = """How to use Hotkeys
|
||||||
|
ctrl+shift+r
|
||||||
|
This starts a recording, then converts to text and plays when you press this hotkey again.
|
||||||
|
|
||||||
|
ctrl+shift+s
|
||||||
|
If you are recording, you can press this hotkey to stop recording without playing
|
||||||
|
|
||||||
|
ctrl+shift+l
|
||||||
|
This replays the last audio clip played
|
||||||
|
|
||||||
|
"""
|
||||||
|
tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=380).pack(padx=10, pady=10)
|
||||||
|
|
||||||
|
# Add a button to close the window
|
||||||
|
ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0))
|
||||||
|
|
||||||
def show_instructions(self):
|
def show_instructions(self):
|
||||||
instruction_window = tk.Toplevel(self)
|
instruction_window = tk.Toplevel(self)
|
||||||
@@ -343,15 +408,30 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
return Path(filename) # Default to current directory for non-macOS systems
|
return Path(filename) # Default to current directory for non-macOS systems
|
||||||
|
|
||||||
|
|
||||||
def submit_text(self):
|
def submit_text(self, play_text = None):
|
||||||
|
|
||||||
text = self.text_input.get("1.0", tk.END).strip()
|
print(f"submit text self recording: {self.recording}")
|
||||||
selected_voice = self.voice_var.get()
|
if self.recording:
|
||||||
|
print("Stopping recording")
|
||||||
|
self.stop_recording(auto_play = True)
|
||||||
|
else:
|
||||||
|
print("Submitting text")
|
||||||
|
self.submit_text_helper(play_text = play_text)
|
||||||
|
|
||||||
|
def submit_text_helper(self, play_text = None):
|
||||||
|
|
||||||
|
if play_text is None:
|
||||||
|
#Load from GUI if play text not set
|
||||||
|
text = self.text_input.get("1.0", tk.END).strip()
|
||||||
|
else:
|
||||||
|
text = play_text
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
messagebox.showinfo("Error", "Please enter some text to synthesize.")
|
messagebox.showinfo("Error", "Please enter some text to synthesize.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
selected_voice = self.voice_var.get()
|
||||||
|
|
||||||
# Convert device names to indices
|
# Convert device names to indices
|
||||||
primary_index = self.available_devices.get(self.device_index.get(), None)
|
primary_index = self.available_devices.get(self.device_index.get(), None)
|
||||||
secondary_index = self.available_devices.get(self.device_index_2.get(), None) if self.device_index_2.get() != "None" else None
|
secondary_index = self.available_devices.get(self.device_index_2.get(), None) if self.device_index_2.get() != "None" else None
|
||||||
@@ -403,9 +483,6 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
# Open all files and start all streams
|
# Open all files and start all streams
|
||||||
for file_path, device_index in zip(file_paths, device_indices):
|
for file_path, device_index in zip(file_paths, device_indices):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Ensure the file_path is a string when opening the file
|
# Ensure the file_path is a string when opening the file
|
||||||
wf = wave.open(str(file_path), 'rb')
|
wf = wave.open(str(file_path), 'rb')
|
||||||
@@ -532,11 +609,11 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
finally:
|
finally:
|
||||||
p.terminate()
|
p.terminate()
|
||||||
|
|
||||||
def toggle_recording(self):
|
def toggle_recording(self, auto_play=False):
|
||||||
if not self.recording:
|
if not self.recording:
|
||||||
self.start_recording()
|
self.start_recording()
|
||||||
else:
|
else:
|
||||||
self.stop_recording()
|
self.stop_recording(auto_play)
|
||||||
|
|
||||||
def stop_recording_btn_change(self, btn_text):
|
def stop_recording_btn_change(self, btn_text):
|
||||||
self.record_button.config(text=btn_text)
|
self.record_button.config(text=btn_text)
|
||||||
@@ -569,6 +646,8 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
try:
|
try:
|
||||||
self.recording = True
|
self.recording = True
|
||||||
self.record_button.config(text="Stop and Insert", style='Recording.TButton')
|
self.record_button.config(text="Stop and Insert", style='Recording.TButton')
|
||||||
|
self.submit_button.config(text="Stop and Play", style='Recording.TButton')
|
||||||
|
|
||||||
self.frames = []
|
self.frames = []
|
||||||
|
|
||||||
self.p = pyaudio.PyAudio()
|
self.p = pyaudio.PyAudio()
|
||||||
@@ -587,7 +666,7 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
messagebox.showerror("Recording Error", f"Failed to record audio: {str(e)}")
|
messagebox.showerror("Recording Error", f"Failed to record audio: {str(e)}")
|
||||||
self.stop_recording(True)
|
self.stop_recording(True)
|
||||||
|
|
||||||
def stop_recording(self, cancel_save=False):
|
def stop_recording(self, cancel_save=False, auto_play=False):
|
||||||
self.recording = False
|
self.recording = False
|
||||||
if self.record_thread:
|
if self.record_thread:
|
||||||
self.record_thread.join()
|
self.record_thread.join()
|
||||||
@@ -600,12 +679,13 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
self.p.terminate()
|
self.p.terminate()
|
||||||
|
|
||||||
if cancel_save==False:
|
if cancel_save==False:
|
||||||
self.save_recording()
|
self.save_recording(auto_play=auto_play)
|
||||||
|
|
||||||
self.record_button.config(text="Record Mic", style='TButton') # Revert to default style
|
self.record_button.config(text="Record Mic", style='TButton') # Revert to default style
|
||||||
|
self.submit_button.config(text="Play", style='Green.TButton') # Revert to default style
|
||||||
|
|
||||||
|
|
||||||
def save_recording(self):
|
def save_recording(self, auto_play = False):
|
||||||
file_path = "output.wav"
|
file_path = "output.wav"
|
||||||
wf = wave.open(file_path, 'wb')
|
wf = wave.open(file_path, 'wb')
|
||||||
wf.setnchannels(1)
|
wf.setnchannels(1)
|
||||||
@@ -615,12 +695,12 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
wf.close()
|
wf.close()
|
||||||
print("Recording saved.")
|
print("Recording saved.")
|
||||||
|
|
||||||
self.after(0, self.transcribe_audio, file_path)
|
self.after(0, self.transcribe_audio, file_path, auto_play)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def transcribe_audio(self, file_path):
|
def transcribe_audio(self, file_path, auto_play = False):
|
||||||
try:
|
try:
|
||||||
with open(str(file_path), "rb") as audio_file:
|
with open(str(file_path), "rb") as audio_file:
|
||||||
transcription = self.client.audio.transcriptions.create(
|
transcription = self.client.audio.transcriptions.create(
|
||||||
@@ -628,7 +708,6 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
model="whisper-1",
|
model="whisper-1",
|
||||||
response_format="verbose_json"
|
response_format="verbose_json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
settings = self.load_settings()
|
settings = self.load_settings()
|
||||||
|
|
||||||
@@ -641,13 +720,20 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
|
|
||||||
if auto_apply_ai:
|
if auto_apply_ai:
|
||||||
print("applying ai")
|
print("applying ai")
|
||||||
self.apply_ai(transcription.text)
|
play_text = self.apply_ai(transcription.text)
|
||||||
else:
|
else:
|
||||||
print("outputting without ai")
|
print("outputting without ai")
|
||||||
#This prevents issues with trying to upload TK after thread operations
|
#This prevents issues with trying to upload TK after thread operations
|
||||||
#whcih can cause crashes with no error displayed
|
#whcih can cause crashes with no error displayed
|
||||||
self.text_input.delete("1.0", tk.END) # Clear existing text
|
self.text_input.delete("1.0", tk.END) # Clear existing text
|
||||||
self.text_input.insert("1.0", transcription.text) # Insert new text
|
self.text_input.insert("1.0", transcription.text) # Insert new text
|
||||||
|
play_text = transcription.text
|
||||||
|
|
||||||
|
if auto_play:
|
||||||
|
#self.submit_text(play_text = playtext)#
|
||||||
|
print(f"Triggering auto play with: {play_text} ")
|
||||||
|
self.submit_text_helper(play_text = play_text)
|
||||||
|
# TODO: PLAY THE TEXT IMMEDIATELY
|
||||||
|
|
||||||
print("Transcription Complete: The audio has been transcribed and the text has been placed in the input area.")
|
print("Transcription Complete: The audio has been transcribed and the text has been placed in the input area.")
|
||||||
#messagebox.showinfo("Transcription Complete", "The audio has been transcribed and the text has been placed in the input area.")
|
#messagebox.showinfo("Transcription Complete", "The audio has been transcribed and the text has been placed in the input area.")
|
||||||
@@ -762,6 +848,12 @@ https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
|
|||||||
self.text_input.delete("1.0", tk.END)
|
self.text_input.delete("1.0", tk.END)
|
||||||
self.text_input.insert("1.0", response.choices[0].message.content)
|
self.text_input.insert("1.0", response.choices[0].message.content)
|
||||||
|
|
||||||
|
return_text = response.choices[0].message.content
|
||||||
|
else:
|
||||||
|
return_text = text
|
||||||
|
|
||||||
|
return return_text
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = Application()
|
app = Application()
|
||||||
|
|||||||
Reference in New Issue
Block a user