fix some ui issues, add scrolling to presets area, update readme instructions, other small QOL improvements

This commit is contained in:
Andrew Ward
2024-10-29 10:54:57 +00:00
parent 4280469c52
commit f4ae2a9e39
6 changed files with 669 additions and 29 deletions

View File

@@ -4,7 +4,7 @@
**Text to Mic** is a powerful, user-friendly application that seamlessly converts written text into natural-sounding speech, playing it over a virtual microphone. This tool is perfect for situations where you want to automate voice output — whether it's for presentations, online meetings, voiceovers, or accessibility purposes.
![Example Image 1](images/app-screenshot-v1-0-0.png)
![Example Image 1](images/app-screenshot-v1-0-9.png)
More info about Text to Mic including a changelog can be found here:
https://www.scorchsoft.com/blog/text-to-mic-for-meetings/
@@ -50,6 +50,35 @@ https://platform.openai.com/docs/quickstart/account-setup
This tool was brought to you by Scorchsoft - We build custom apps to your requirements. Please contact us if you have a requirement for a custom app project.
## Advanced Tips
### 1. ChatGPT AI Manipulation
If you go to "Settings > ChatGPT Manipulation" then you can turn this on and pick which model to use.
![ChatGPT Manipulation](images/chatgpt-manipulation.png)
If enabled (both enabled and "auto apply to recorded transcript"), this will run your transcript through AI with the desired prompt each time you record your voice and convert it to text.
If you've enabled but not turned on auto apply, then you can manually trigger this action to any text you've input into "text to Read" via the context menu "Input > Apply AI manipulation to text input". This will only work if you've turned it on and added your API key
### 2. Hotkeys
You can use a hotkey combination to quickly trigger recording and playing of recorded text. By default the hot keys are "ctrl+shift+0" to start the recording, then press it again to stop, transcribe, and submit. "Ctrl+shift+9" stops the recording without playing it. "Ctrl+shift+8" replays the last transcribed or written text.
"Settings > Hotkey Settings" allows you to customise the hotkey combinations used to trigger the above actions.
### 3. Presets
Click the presets button at the bottom of the app to open the presets area.
Once loaded for the first time, presets are stored in "config/presets.json", this means if you close the app you can edit these and add categories etc via notepad. If you do this, please make sure you don't break or invalidate the json structure.
You can also edit presets from within the app, but this is limtied to saving new presets to an existing category, favouriting presets, and deleting them. Any other edits have to be completed by editing the json file.
You can add a new preset by writing it into the "Text to Read" area, then at the top right of the area select the category you wish to add it to, and hit save.
## Running the CLI Version
run the executable or "python text-to-mic.py"

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

559
presets.example.json Normal file
View File

@@ -0,0 +1,559 @@
{
"presets": [
{
"category": "Greetings",
"phrases": [
{
"text": "Hello, how are you?",
"isFavourite": false
},
{
"text": "Good morning!",
"isFavourite": true
},
{
"text": "Good evening!",
"isFavourite": false
},
{
"text": "Nice to meet you.",
"isFavourite": false
},
{
"text": "Hi there!",
"isFavourite": false
},
{
"text": "Welcome back!",
"isFavourite": false
}
]
},
{
"category": "Questions",
"phrases": [
{
"text": "Could you please repeat that?",
"isFavourite": false
},
{
"text": "Where is the nearest restroom?",
"isFavourite": true
},
{
"text": "What time is it?",
"isFavourite": true
},
{
"text": "Can I help you with something?",
"isFavourite": false
},
{
"text": "Testing 123",
"isFavourite": true
},
{
"text": "How does this work?",
"isFavourite": false
},
{
"text": "Can you explain that further?",
"isFavourite": false
}
]
},
{
"category": "Responses",
"phrases": [
{
"text": "Thank you!",
"isFavourite": false
},
{
"text": "You're welcome.",
"isFavourite": true
},
{
"text": "No problem.",
"isFavourite": false
},
{
"text": "I understand.",
"isFavourite": false
},
{
"text": "Absolutely!",
"isFavourite": false
},
{
"text": "I agree.",
"isFavourite": false
}
]
},
{
"category": "Gaming",
"phrases": [
{
"text": "Watch out, behind you!",
"isFavourite": true
},
{
"text": "Need backup here.",
"isFavourite": false
},
{
"text": "Let's stick together.",
"isFavourite": false
},
{
"text": "GG (Good Game)!",
"isFavourite": false
},
{
"text": "Enemy spotted!",
"isFavourite": false
},
{
"text": "Level up!",
"isFavourite": false
}
]
},
{
"category": "Directions",
"phrases": [
{
"text": "Go straight ahead.",
"isFavourite": false
},
{
"text": "Turn left at the next corner.",
"isFavourite": false
},
{
"text": "It's on your right.",
"isFavourite": false
},
{
"text": "Follow me, please.",
"isFavourite": true
},
{
"text": "Take the second exit.",
"isFavourite": false
},
{
"text": "You will see it on the left.",
"isFavourite": false
}
]
},
{
"category": "Requests",
"phrases": [
{
"text": "Could you assist me with this?",
"isFavourite": false
},
{
"text": "Please let me know if I can help.",
"isFavourite": false
},
{
"text": "May I have some assistance?",
"isFavourite": true
},
{
"text": "Could you lower your voice?",
"isFavourite": false
},
{
"text": "Can you pass the salt?",
"isFavourite": false
},
{
"text": "Please wait a moment.",
"isFavourite": false
}
]
},
{
"category": "Social",
"phrases": [
{
"text": "How was your day?",
"isFavourite": false
},
{
"text": "It's nice to see you!",
"isFavourite": false
},
{
"text": "I hope you're doing well.",
"isFavourite": true
},
{
"text": "Take care.",
"isFavourite": false
},
{
"text": "What's new with you?",
"isFavourite": false
},
{
"text": "Long time no see!",
"isFavourite": false
}
]
},
{
"category": "Apologies",
"phrases": [
{
"text": "I'm sorry.",
"isFavourite": true
},
{
"text": "Excuse me, please.",
"isFavourite": false
},
{
"text": "I didn't mean to.",
"isFavourite": false
},
{
"text": "Pardon me.",
"isFavourite": false
},
{
"text": "My apologies.",
"isFavourite": false
},
{
"text": "Sorry for the inconvenience.",
"isFavourite": false
}
]
},
{
"category": "Encouragement",
"phrases": [
{
"text": "Great job!",
"isFavourite": false
},
{
"text": "You can do it!",
"isFavourite": true
},
{
"text": "Keep going!",
"isFavourite": false
},
{
"text": "Well done!",
"isFavourite": false
},
{
"text": "Don't give up!",
"isFavourite": false
},
{
"text": "You're making progress.",
"isFavourite": false
}
]
},
{
"category": "Emergency",
"phrases": [
{
"text": "I need help immediately!",
"isFavourite": true
},
{
"text": "Call 999!",
"isFavourite": false
},
{
"text": "I'm not feeling well.",
"isFavourite": false
},
{
"text": "Please get help.",
"isFavourite": false
},
{
"text": "There's been an accident.",
"isFavourite": false
},
{
"text": "I require urgent assistance.",
"isFavourite": false
}
]
},
{
"category": "Farewells",
"phrases": [
{
"text": "Goodbye!",
"isFavourite": false
},
{
"text": "Take care!",
"isFavourite": true
},
{
"text": "See you later.",
"isFavourite": false
},
{
"text": "Have a great day!",
"isFavourite": false
},
{
"text": "Farewell!",
"isFavourite": false
},
{
"text": "Until next time.",
"isFavourite": false
}
]
},
{
"category": "Business",
"phrases": [
{
"text": "Let's schedule a meeting.",
"isFavourite": false
},
{
"text": "I will get back to you on that.",
"isFavourite": true
},
{
"text": "Could you send me the report?",
"isFavourite": false
},
{
"text": "Thank you for your cooperation.",
"isFavourite": false
},
{
"text": "Let's discuss this further.",
"isFavourite": false
},
{
"text": "Please review the attached document.",
"isFavourite": false
}
]
},
{
"category": "Travel",
"phrases": [
{
"text": "Where is the nearest bus stop?",
"isFavourite": true
},
{
"text": "I need a taxi.",
"isFavourite": false
},
{
"text": "What is the flight status?",
"isFavourite": false
},
{
"text": "Can you help me with my luggage?",
"isFavourite": false
},
{
"text": "Where can I buy a ticket?",
"isFavourite": false
},
{
"text": "Is this seat taken?",
"isFavourite": false
}
]
},
{
"category": "Healthcare",
"phrases": [
{
"text": "I need to see a doctor.",
"isFavourite": true
},
{
"text": "I have a headache.",
"isFavourite": false
},
{
"text": "Where is the nearest hospital?",
"isFavourite": false
},
{
"text": "I need medication.",
"isFavourite": false
},
{
"text": "I'm feeling dizzy.",
"isFavourite": false
},
{
"text": "I need an ambulance.",
"isFavourite": false
}
]
},
{
"category": "Education",
"phrases": [
{
"text": "Can you help me with my homework?",
"isFavourite": true
},
{
"text": "What is the assignment due date?",
"isFavourite": false
},
{
"text": "I need tutoring in math.",
"isFavourite": false
},
{
"text": "Where is the library?",
"isFavourite": false
},
{
"text": "Can I join the study group?",
"isFavourite": false
},
{
"text": "When is the next exam?",
"isFavourite": false
}
]
},
{
"category": "Shopping",
"phrases": [
{
"text": "How much does this cost?",
"isFavourite": true
},
{
"text": "Do you have this in a different size?",
"isFavourite": false
},
{
"text": "I'd like to return this item.",
"isFavourite": false
},
{
"text": "Can I pay with a credit card?",
"isFavourite": false
},
{
"text": "Where is the fitting room?",
"isFavourite": false
},
{
"text": "Do you offer a warranty?",
"isFavourite": false
}
]
},
{
"category": "Technology",
"phrases": [
{
"text": "How do I reset my password?",
"isFavourite": true
},
{
"text": "My device is not working.",
"isFavourite": false
},
{
"text": "Can you help me install this software?",
"isFavourite": false
},
{
"text": "Where can I find the user manual?",
"isFavourite": false
},
{
"text": "The screen is frozen.",
"isFavourite": false
},
{
"text": "I forgot my username.",
"isFavourite": false
}
]
},
{
"category": "Personal",
"phrases": [
{
"text": "I need to buy groceries.",
"isFavourite": true
},
{
"text": "I'm going to the gym.",
"isFavourite": false
},
{
"text": "Can we meet up later?",
"isFavourite": false
},
{
"text": "I need some alone time.",
"isFavourite": false
},
{
"text": "I'm cooking dinner.",
"isFavourite": false
},
{
"text": "I have a doctor's appointment.",
"isFavourite": false
}
]
},
{
"category": "Entertainment",
"phrases": [
{
"text": "Let's watch a movie.",
"isFavourite": true
},
{
"text": "I'm listening to music.",
"isFavourite": false
},
{
"text": "Do you want to play a game?",
"isFavourite": false
},
{
"text": "I'm reading a book.",
"isFavourite": false
},
{
"text": "Let's go to a concert.",
"isFavourite": false
},
{
"text": "I'm streaming a new series.",
"isFavourite": false
}
]
}
]
}

View File

@@ -30,10 +30,11 @@ class TextToMic(tk.Tk):
super().__init__()
self.title("Scorchsoft Text to Mic")
self.default_geometry = "590x800"
self.default_geometry = "590x750"
self.geometry(self.default_geometry)
self.available_models = ["gpt-4o-mini", "gpt-4o", "gpt-4-turbo"]
self.default_model = "gpt-4o-mini"
self.style = ttk.Style(self)
if self.tk.call('tk', 'windowingsystem') == 'aqua':
@@ -88,7 +89,7 @@ class TextToMic(tk.Tk):
instruction_window.title("App Version")
instruction_window.geometry("300x150") # Width x Height
instructions = """Version 1.1.0\n\n App by Scorchsoft.com"""
instructions = """Version 1.2.0\n\n App by Scorchsoft.com"""
tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=280).pack(padx=10, pady=10)
@@ -324,7 +325,7 @@ class TextToMic(tk.Tk):
save_button.grid(column=1, row=0, sticky=tk.E) # Align to the right
# Specify the text to read
self.text_input = tk.Text(main_frame, height=10, width=68)
self.text_input = tk.Text(main_frame, height=5, width=68)
self.text_input.grid(column=0, row=6, columnspan=2, pady=(0, 20), sticky="nsew") # Fill available width
@@ -380,7 +381,7 @@ class TextToMic(tk.Tk):
self.right_arrow.pack(side=tk.RIGHT, padx=1) # Reduced padding
# Presets display area with a fixed height and vertical scrollbar
self.presets_canvas = Canvas(self.presets_frame, height=200, width=self.presets_frame.winfo_width())
self.presets_canvas = Canvas(self.presets_frame, height=250, width=self.presets_frame.winfo_width())
self.presets_scrollbar = Scrollbar(self.presets_frame, orient="vertical", command=self.presets_canvas.yview)
self.presets_canvas.configure(yscrollcommand=self.presets_scrollbar.set)
@@ -400,6 +401,8 @@ class TextToMic(tk.Tk):
self.refresh_presets_display()
self.toggle_presets()
self.toggle_presets()
self.enable_mouse_wheel_scrolling()
def scroll_left(self):
@@ -409,8 +412,36 @@ class TextToMic(tk.Tk):
self.tabs_canvas.xview_scroll(5, "units")
def enable_mouse_wheel_scrolling(self):
"""Enable conditional mouse wheel scrolling for the presets canvas and category tabs canvas."""
def on_vertical_scroll(event):
# Scroll the presets_canvas vertically
if event.num == 4: # macOS scroll up
self.presets_canvas.yview_scroll(-1, "units")
elif event.num == 5: # macOS scroll down
self.presets_canvas.yview_scroll(1, "units")
else: # Windows and Linux
self.presets_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
def on_horizontal_scroll(event):
# Scroll the tabs_canvas horizontally
if event.num == 4: # macOS scroll left
self.tabs_canvas.xview_scroll(-1, "units")
elif event.num == 5: # macOS scroll right
self.tabs_canvas.xview_scroll(1, "units")
else: # Windows and Linux
self.tabs_canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")
# Bind scroll events when mouse enters or leaves the presets canvas area
self.presets_canvas.bind("<Enter>", lambda e: self.presets_canvas.bind_all("<MouseWheel>", on_vertical_scroll))
self.presets_canvas.bind("<Leave>", lambda e: self.presets_canvas.unbind_all("<MouseWheel>"))
# Bind scroll events when mouse enters or leaves the tabs canvas area
self.tabs_canvas.bind("<Enter>", lambda e: self.tabs_canvas.bind_all("<MouseWheel>", on_horizontal_scroll))
self.tabs_canvas.bind("<Leave>", lambda e: self.tabs_canvas.unbind_all("<MouseWheel>"))
def open_scorchsoft(self, event=None):
webbrowser.open('https://www.scorchsoft.com')
@@ -975,7 +1006,7 @@ Please also make sure you read the Terms of use and licence statement before usi
# Default settings
settings = {
"chat_gpt_completion": False,
"model": "gpt-4o-mini",
"model": self.default_model,
"prompt": "",
"auto_apply_ai_to_recording": False,
"hotkeys": {
@@ -1016,10 +1047,13 @@ Please also make sure you read the Terms of use and licence statement before usi
enable_completion = tk.BooleanVar(value=settings.get("chat_gpt_completion", False))
ttk.Checkbutton(main_frame, text="Enable ChatGPT Completion", variable=enable_completion).grid(row=0, column=1, sticky=tk.W, pady=2)
# Model selection
model_var = tk.StringVar(value=settings.get("model", "gpt-3.5-turbo"))
# Model selection#
model_var = tk.StringVar(value=settings.get("model", self.default_model))
ttk.Label(main_frame, text="Model:").grid(row=1, column=0, sticky=tk.W, pady=2)
ttk.OptionMenu(main_frame, model_var, "gpt-4o-mini", "gpt-4o", "gpt-4-turbo").grid(row=1, column=1, sticky=tk.W, pady=2)
model_menu = ttk.OptionMenu(main_frame, model_var, model_var.get(), *self.available_models)
model_menu.grid(row=1, column=1, sticky=tk.W, pady=2)
# Max Tokens selection
max_tokens_var = tk.IntVar(value=settings.get("max_tokens", 750))
@@ -1173,29 +1207,29 @@ Please also make sure you read the Terms of use and licence statement before usi
bottom_frame = ttk.Frame(frame)
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=2)
self.style.configure("Flat.TButton",
borderwidth=0,
highlightthickness=0,
font=("Arial", 12), # Adjust font and size
anchor="center") # Center text
# Favourite button
fav_icon = "❤️" if phrase["isFavourite"] else ""
fav_btn = ttk.Button(bottom_frame, text=fav_icon, command=lambda p=phrase: self.toggle_favourite(p), width=3, style="Flat.TButton")
fav_btn = ttk.Button(bottom_frame, text=fav_icon, command=lambda p=phrase: self.toggle_favourite(p), width=2, style="Flat.TButton")
fav_btn.pack(side=tk.RIGHT, padx=2)
#fav_btn.config(borderwidth=0, highlightthickness=0) # Remove border
# Delete button
del_btn = ttk.Button(bottom_frame, text="🗑️", command=lambda t=phrase["text"]: self.delete_preset(self.current_category, t), width=3, style="Flat.TButton")
del_btn = ttk.Button(bottom_frame, text="🗑️", command=lambda t=phrase["text"]: self.delete_preset(self.current_category, t), width=2, style="Flat.TButton")
del_btn.pack(side=tk.RIGHT, padx=2)
#del_btn.config(borderwidth=0, highlightthickness=0) # Remove border
# Update scroll region after populating all items
self.presets_canvas.configure(scrollregion=self.presets_canvas.bbox("all"))
def wrap_text(self, text, max_lines=3, max_chars_per_line=20):
"""Wrap text to fit within a limited number of lines and characters."""
words = text.split()
@@ -1246,7 +1280,7 @@ Please also make sure you read the Terms of use and licence statement before usi
else:
self.presets_frame.grid_remove()
self.presets_button.config(text="▶ Presets")
self.geometry("590x520")
self.geometry("590x460")
self.presets_collapsed = not self.presets_collapsed
@@ -1263,14 +1297,32 @@ Please also make sure you read the Terms of use and licence statement before usi
def load_presets(self):
"""Load presets from the JSON file."""
"""Load presets from the JSON file, copying from example if necessary."""
presets_path = Path("config/presets.json")
example_path = self.resource_path("presets.example.json") # Path for the example file
# Check if presets.json exists, and if not, copy presets.example.json to config
if not presets_path.exists():
try:
# Ensure config directory exists
presets_path.parent.mkdir(parents=True, exist_ok=True)
# Copy example presets to config directory
with open(example_path, "r", encoding="utf-8") as example_file:
with open(presets_path, "w", encoding="utf-8") as config_file:
config_file.write(example_file.read())
except Exception as e:
messagebox.showerror("Error", f"Failed to copy example presets: {e}")
return [] # Return empty if unable to load or copy presets
# Load presets.json as usual
try:
with open("config/presets.json", "r") as f:
with open(presets_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("presets", [])
except (FileNotFoundError, json.JSONDecodeError):
# Create a default structure if the file doesn't exist or is corrupted
return []
except (FileNotFoundError, json.JSONDecodeError) as e:
messagebox.showerror("Error", f"Error loading presets: {e}")
return [] # Default to empty if load fails
def save_presets(self):
"""Save presets to the JSON file."""