update to enable tone of voice playback
This commit is contained in:
23
assets/tone_presets.example.json
Normal file
23
assets/tone_presets.example.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"tone_presets": {
|
||||
"Cheerful": "Voice Affect: Bright, uplifted, and energetic; project warmth and positivity in every word.\n\nTone: Light-hearted, friendly, and optimistic—maintain an upbeat quality that feels genuine and engaging.\n\nPacing: Moderately quick and buoyant; keep a lively rhythm that conveys enthusiasm without rushing.\n\nEmotion: Authentic happiness and joy; express delight through your voice, especially when emphasizing positive aspects.\n\nPronunciation: Clear with slightly emphasized inflections on positive words ('wonderful', 'great', 'exciting') to enhance the cheerful mood.\n\nPauses: Short and bouncy pauses that maintain energy while allowing the cheerful tone to resonate.",
|
||||
|
||||
"Angry": "Voice Affect: Tense, sharp, and forceful; project intensity and frustration clearly.\n\nTone: Stern, irritated, and confrontational—allow controlled aggression to color your words.\n\nPacing: Somewhat rapid with emphasis on certain words; use quick bursts followed by deliberate slowdowns for effect.\n\nEmotion: Genuine frustration and indignation; channel controlled anger especially when emphasizing points of contention.\n\nPronunciation: Hard consonants and emphasized words, particularly on words expressing dissatisfaction ('unacceptable', 'wrong', 'failure').\n\nPauses: Abrupt, tension-filled pauses that heighten the sense of frustration and allow for dramatic emphasis.",
|
||||
|
||||
"Bedtime Story": "Voice Affect: Soft, gentle, and nurturing; project a calming and protective presence.\n\nTone: Warm, soothing, and melodic—maintain a comforting quality that induces relaxation and sleepiness.\n\nPacing: Slow and rhythmic; allow each word to gently flow into the next like a lullaby.\n\nEmotion: Tender affection and tranquility; express gentle care through your voice, especially during descriptive passages.\n\nPronunciation: Soft consonants and elongated vowels, creating a melodic flow that's easy to listen to with slightly lowered volume.\n\nPauses: Long, peaceful pauses between phrases and sentences, allowing the listener to drift toward sleep.",
|
||||
|
||||
"News Anchor": "Voice Affect: Authoritative, composed, and professional; project credibility and competence.\n\nTone: Clear, formal, and objective—maintain an impartial quality while delivering information with confidence.\n\nPacing: Measured and deliberate; maintain consistent timing with appropriate emphasis on key information.\n\nEmotion: Controlled neutrality with appropriate gravity; express seriousness for important matters while maintaining professional distance.\n\nPronunciation: Crisp and precise articulation of each word, especially names, locations, and technical terms, with perfect diction.\n\nPauses: Strategic pauses between topics and key points to allow information to register while maintaining audience attention.",
|
||||
|
||||
"Excited": "Voice Affect: Highly energetic, animated, and dynamic; project unbridled enthusiasm and passion.\n\nTone: Exuberant, thrilled, and intensely positive—allow genuine excitement to amplify your vocal delivery.\n\nPacing: Quick and varied; use rapid delivery with dramatic variations in speed to convey heightened emotions.\n\nEmotion: Intense enthusiasm and amazement; express wonder and delight through vocal projection and emphasis.\n\nPronunciation: Emphatic with exaggerated inflection on exciting words ('amazing', 'incredible', 'unbelievable') with volume peaks on key points.\n\nPauses: Quick, anticipatory pauses before revealing exciting information, creating tension and release patterns.",
|
||||
|
||||
"Whisper": "Voice Affect: Hushed, intimate, and secretive; project closeness and confidentiality.\n\nTone: Quiet, breathy, and slightly tense—maintain an air of secrecy and importance in each word.\n\nPacing: Slow and deliberate; give each word weight while maintaining the hushed quality throughout.\n\nEmotion: Conspiratorial intimacy and subtle urgency; express the importance of discretion through vocal restraint.\n\nPronunciation: Soft consonants with minimal mouth movement, creating the authentic sensation of whispering with slightly obscured clarity.\n\nPauses: Frequent, tension-filled pauses that enhance the secretive nature and build anticipation.",
|
||||
|
||||
"Robotic": "Voice Affect: Mechanical, processed, and artificial; project a complete absence of human emotion.\n\nTone: Flat, monotonous, and computerized—maintain consistent pitch and eliminate natural vocal variations.\n\nPacing: Perfectly metered and rhythmic; deliver each syllable with machine-like precision and evenness.\n\nEmotion: Complete emotional neutrality; avoid any trace of human feeling or expression in your delivery.\n\nPronunciation: Precise and clipped articulation with equal stress on all syllables, avoiding contractions and using formal language patterns.\n\nPauses: Programmatic, equally-timed pauses between sentences and phrases that ignore natural speech patterns.",
|
||||
|
||||
"ASMR": "Voice Affect: Extremely soft, gentle, and sensory-focused; project an intimate, calming presence.\n\nTone: Whispered, soothing, and hypnotic—maintain a delicate quality that triggers relaxation responses.\n\nPacing: Very slow and deliberate; allow ample time between words to emphasize mouth sounds and breathing.\n\nEmotion: Nurturing tranquility and attention to detail; express care through subtle vocal textures and breathing patterns.\n\nPronunciation: Crisp but gentle articulation that highlights mouth sounds, with attention to the auditory texture of consonants like 'k', 's', and 't'.\n\nPauses: Long, intentional pauses filled with gentle breathing sounds that create a rhythmic, relaxing pattern.",
|
||||
|
||||
"Urgent": "Voice Affect: Alert, insistent, and high-energy; project immediacy and critical importance.\n\nTone: Pressing, serious, and attention-demanding—convey that immediate action is required.\n\nPacing: Rapid and emphatic; deliver information quickly while emphasizing critical points with intensity.\n\nEmotion: Controlled anxiety and heightened alertness; express the gravity of the situation through vocal tension.\n\nPronunciation: Sharp and distinct articulation with emphasis on action words ('now', 'immediately', 'crucial') and slightly raised volume.\n\nPauses: Minimal, tense pauses used only for dramatic emphasis on the most critical information.",
|
||||
|
||||
"Storyteller": "Voice Affect: Engaging, dramatic, and captivating; project a sense of wonder and narrative mastery.\n\nTone: Dynamic, expressive, and varied—shift between mysterious, exciting, tender, or tense as the narrative requires.\n\nPacing: Varied and intentional; slow down for suspense or emotional moments, quicken for action or excitement.\n\nEmotion: Rich emotional range; express a full spectrum of feelings through vocal coloring appropriate to each part of the story.\n\nPronunciation: Vivid and theatrical articulation with character voices and sound effects where appropriate, using volume variation for emphasis.\n\nPauses: Strategic, dramatic pauses at key plot points, revelations, or transitions that build suspense and maintain audience engagement."
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/tone_presets_manager.cpython-312.pyc
Normal file
BIN
utils/__pycache__/tone_presets_manager.cpython-312.pyc
Normal file
Binary file not shown.
@@ -110,24 +110,35 @@ class HotkeyManager:
|
||||
messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.")
|
||||
|
||||
@staticmethod
|
||||
def show_hotkey_instructions(app):
|
||||
"""Show hotkey instructions."""
|
||||
instruction_window = tk.Toplevel(app)
|
||||
def show_hotkey_instructions(parent):
|
||||
"""Show instructions for using hotkeys."""
|
||||
instruction_window = tk.Toplevel(parent)
|
||||
instruction_window.title("Hotkey Instructions")
|
||||
instruction_window.geometry("400x300") # Width x Height
|
||||
instruction_window.geometry("600x400") # Width x Height
|
||||
|
||||
instructions = """How to use Hotkeys
|
||||
ctrl+shift+0
|
||||
This starts a recording, then converts to text and plays when you press this hotkey again.
|
||||
settings = parent.load_settings()
|
||||
record_shortcut = "+".join(filter(None, settings["hotkeys"]["record_start_stop"]))
|
||||
play_shortcut = "+".join(filter(None, settings["hotkeys"]["play_last_audio"]))
|
||||
stop_shortcut = "+".join(filter(None, settings["hotkeys"]["stop_recording"]))
|
||||
|
||||
ctrl+shift+9
|
||||
If you are recording, you can press this hotkey to stop recording without playing
|
||||
instructions = f"""Available Hotkeys:
|
||||
|
||||
ctrl+shift+8
|
||||
This replays the last audio clip played
|
||||
1. {record_shortcut} - Start/Stop Recording
|
||||
This hotkey toggles recording from your selected input device.
|
||||
|
||||
"""
|
||||
tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=380).pack(padx=10, pady=10)
|
||||
2. {play_shortcut} - Play Last Audio
|
||||
Play the most recently generated audio.
|
||||
|
||||
3. {stop_shortcut} - Stop Recording
|
||||
Immediately stops any active recording.
|
||||
|
||||
These hotkeys work globally across your system, even when the app is minimized.
|
||||
You can customize these hotkeys in Settings → Hotkey Settings.
|
||||
|
||||
You can also access tone presets from Settings → Manage Tone Presets to modify how your text is spoken.
|
||||
"""
|
||||
|
||||
tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=580).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))
|
||||
@@ -21,6 +21,7 @@ from pydub import AudioSegment
|
||||
from utils.api_key_manager import APIKeyManager
|
||||
from utils.hotkey_manager import HotkeyManager
|
||||
from utils.resource_utils import ResourceUtils
|
||||
from utils.tone_presets_manager import TonePresetsManager
|
||||
|
||||
# Modify the load environment variables to load from config/.env
|
||||
def load_env_file():
|
||||
@@ -33,7 +34,8 @@ class TextToMic(tk.Tk):
|
||||
super().__init__()
|
||||
|
||||
self.title("Scorchsoft Text to Mic")
|
||||
self.default_geometry = "590x750"
|
||||
self.default_geometry = "590x770"
|
||||
self.untoggled_geometry ="590x490"
|
||||
self.geometry(self.default_geometry)
|
||||
|
||||
self.available_models = ["gpt-4o-mini", "gpt-4o", "gpt-4-turbo"]
|
||||
@@ -95,6 +97,10 @@ class TextToMic(tk.Tk):
|
||||
self.available_devices = self.get_audio_devices() # Load audio devices
|
||||
self.available_input_devices = self.get_input_devices() # Load input devices
|
||||
|
||||
# Load tone presets
|
||||
self.tone_presets = TonePresetsManager.load_tone_presets(self)
|
||||
self.current_tone_name = self.load_current_tone_from_settings()
|
||||
|
||||
self.create_menu()
|
||||
self.initialize_gui()
|
||||
|
||||
@@ -128,7 +134,7 @@ class TextToMic(tk.Tk):
|
||||
settings_menu.add_command(label="Change API Key", command=self.change_api_key)
|
||||
settings_menu.add_command(label="ChatGPT Manipulation", command=self.chat_gpt_settings)
|
||||
settings_menu.add_command(label="Hotkey Settings", command=self.show_hotkey_settings)
|
||||
|
||||
settings_menu.add_command(label="Manage Tone Presets", command=self.show_tone_presets_manager)
|
||||
|
||||
# Playback menu
|
||||
playback_menu = Menu(self.menubar, tearoff=0)
|
||||
@@ -234,32 +240,40 @@ class TextToMic(tk.Tk):
|
||||
voice_menu = ttk.OptionMenu(main_frame, self.voice_var,'fable', *voices)
|
||||
voice_menu.grid(column=1, row=0, sticky=tk.W)
|
||||
|
||||
# Tone Preset Selection
|
||||
self.tone_var = tk.StringVar(value=self.current_tone_name)
|
||||
ttk.Label(main_frame, text="Tone Preset").grid(column=0, row=1, sticky=tk.W, pady=(5, 10))
|
||||
tone_options = ["None"] + list(self.tone_presets.keys())
|
||||
self.tone_menu = ttk.OptionMenu(main_frame, self.tone_var, self.tone_var.get(), *tone_options,
|
||||
command=self.on_tone_change)
|
||||
self.tone_menu.grid(column=1, row=1, sticky=tk.W)
|
||||
|
||||
# Microphone Selection Setup
|
||||
ttk.Label(main_frame, text="Input Device (optional):").grid(column=0, row=1, sticky=tk.W, pady=(5, 10)) # Padding added
|
||||
ttk.Label(main_frame, text="Input Device (optional):").grid(column=0, row=2, sticky=tk.W, pady=(5, 10)) # Padding added
|
||||
input_device_menu = ttk.OptionMenu(main_frame, self.input_device_index, "None", *self.available_input_devices.keys())
|
||||
input_device_menu.grid(column=1, row=1, sticky=tk.W)
|
||||
input_device_menu.grid(column=1, row=2, sticky=tk.W)
|
||||
|
||||
# Select Primary audio device
|
||||
ttk.Label(main_frame, text="Primary Playback Device:").grid(column=0, row=2, sticky=tk.W, pady=(10, 10)) # Padding added
|
||||
ttk.Label(main_frame, text="Primary Playback Device:").grid(column=0, row=3, sticky=tk.W, pady=(10, 10)) # Padding added
|
||||
primary_device_menu = ttk.OptionMenu(main_frame, self.device_index, *self.available_devices.keys())
|
||||
primary_device_menu.grid(column=1, row=2, sticky=tk.W)
|
||||
primary_device_menu.grid(column=1, row=3, sticky=tk.W)
|
||||
|
||||
# Select Secondary audio device
|
||||
ttk.Label(main_frame, text="Secondary Playback Device (optional):").grid(column=0, row=3, sticky=tk.W, pady=(5, 10)) # Padding added
|
||||
ttk.Label(main_frame, text="Secondary Playback Device (optional):").grid(column=0, row=4, sticky=tk.W, pady=(5, 10)) # Padding added
|
||||
secondary_device_menu = ttk.OptionMenu(main_frame, self.device_index_2, "None", *self.available_devices.keys())
|
||||
secondary_device_menu.grid(column=1, row=3, sticky=tk.W)
|
||||
secondary_device_menu.grid(column=1, row=4, sticky=tk.W)
|
||||
|
||||
|
||||
spacer = ttk.Frame(main_frame, height=20) # Adjust height as needed
|
||||
spacer.grid(column=0, row=4, columnspan=2) # Place spacer in the grid
|
||||
spacer.grid(column=0, row=5, columnspan=2) # Place spacer in the grid
|
||||
|
||||
|
||||
# Text to Read Label
|
||||
ttk.Label(main_frame, text="Text to Read:").grid(column=0, row=5, sticky=tk.W, pady=(10, 0))
|
||||
ttk.Label(main_frame, text="Text to Read:").grid(column=0, row=6, sticky=tk.W, pady=(10, 0))
|
||||
|
||||
# Create a frame to contain the dropdown and save button
|
||||
save_frame = ttk.Frame(main_frame)
|
||||
save_frame.grid(column=1, row=5, sticky=tk.E, padx=(5, 0), pady=(5, 0)) # Align to the top right
|
||||
save_frame.grid(column=1, row=6, sticky=tk.E, padx=(5, 0), pady=(5, 0)) # Align to the top right
|
||||
|
||||
# Preset Category dropdown
|
||||
self.category_var = tk.StringVar(value="Select Category")
|
||||
@@ -278,11 +292,11 @@ class TextToMic(tk.Tk):
|
||||
bg_color = self.style.lookup('TFrame', 'background')
|
||||
text_color = self.style.lookup('TLabel', 'foreground')
|
||||
self.text_input.configure(bg=bg_color, fg=text_color, insertbackground=text_color)
|
||||
self.text_input.grid(column=0, row=6, columnspan=2, pady=(0, 20), sticky="nsew") # Fill available width
|
||||
self.text_input.grid(column=0, row=7, columnspan=2, pady=(0, 20), sticky="nsew") # Fill available width
|
||||
|
||||
# Create a frame for the buttons to allow for better styling
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.grid(column=0, row=7, columnspan=2, sticky="ew", pady=(0, 20))
|
||||
button_frame.grid(column=0, row=8, columnspan=2, sticky="ew", pady=(0, 20))
|
||||
button_frame.columnconfigure(0, weight=1)
|
||||
button_frame.columnconfigure(1, weight=1)
|
||||
|
||||
@@ -328,8 +342,11 @@ class TextToMic(tk.Tk):
|
||||
|
||||
#Credits
|
||||
info_label = tk.Label(main_frame, text="Enjoying this free tool? Thank us by referring a business friend to Scorchsoft.com", fg="blue", cursor="hand2")
|
||||
info_label.grid(column=0, row=10, columnspan=2, pady=(0, 0))
|
||||
info_label.grid(column=0, row=9, columnspan=2, pady=(0, 0))
|
||||
info_label.bind("<Button-1>", lambda e: self.open_scorchsoft())
|
||||
|
||||
desc_label = tk.Label(main_frame, text="We build Apps, AI agents, voice/speech apps, and autonomous tools that work with or without human interaction", wraplength=500)
|
||||
desc_label.grid(column=0, row=10, columnspan=2, pady=(5, 0))
|
||||
|
||||
|
||||
|
||||
@@ -441,7 +458,7 @@ class TextToMic(tk.Tk):
|
||||
def show_instructions(self):
|
||||
instruction_window = tk.Toplevel(self)
|
||||
instruction_window.title("How to Use")
|
||||
instruction_window.geometry("600x680") # Width x Height
|
||||
instruction_window.geometry("600x720") # Width x Height
|
||||
|
||||
instructions = """How to Use Scorchsoft Text to Mic:
|
||||
|
||||
@@ -459,15 +476,17 @@ OpenAI pricing: openai.com/pricing
|
||||
|
||||
3. Choose a voice that you prefer for the speech synthesis.
|
||||
|
||||
4. Select a playback device. I recommend you select one device to be your headphones, and the other the virtuall microphone installed above (Which is usually labelled "Cable Input (VB-Audio))"
|
||||
4. (Optional) Select a Tone Preset to modify how the text is spoken. You can use the built-in presets or create your own under 'Settings > Manage Tone Presets'. Tone presets add special instructions to make the voice sound cheerful, angry, like a bedtime story, etc.
|
||||
|
||||
3. Enter the text in the provided text area that you want to convert to speech.
|
||||
5. Select a playback device. I recommend you select one device to be your headphones, and the other the virtuall microphone installed above (Which is usually labelled "Cable Input (VB-Audio))"
|
||||
|
||||
4. Click 'Submit' to hear the spoken version of your text.
|
||||
6. Enter the text in the provided text area that you want to convert to speech.
|
||||
|
||||
5. The 'Play Last Audio' button can be used to replay the last generated speech output.
|
||||
7. Click 'Play Audio' to hear the spoken version of your text.
|
||||
|
||||
6. You can change the API key at any time under the 'Settings' menu.
|
||||
8. The 'Record Mic' button can be used to record from your microphone and transcribe it to text, which can then be played back.
|
||||
|
||||
9. You can change the API key at any time under the 'Settings' menu.
|
||||
|
||||
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.
|
||||
|
||||
@@ -649,6 +668,16 @@ Please also make sure you read the Terms of use and licence statement before usi
|
||||
|
||||
selected_voice = self.voice_var.get()
|
||||
|
||||
# Check if a tone preset is selected and add it to the text
|
||||
selected_tone_name = self.tone_var.get()
|
||||
|
||||
# Get the actual tone instructions from the tone_presets dictionary
|
||||
tone_instructions = None
|
||||
if selected_tone_name != "None" and selected_tone_name in self.tone_presets:
|
||||
tone_instructions = self.tone_presets[selected_tone_name]
|
||||
else:
|
||||
tone_instructions = "" # Empty string if "None" or not found
|
||||
|
||||
# Convert device names to indices
|
||||
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
|
||||
@@ -658,13 +687,15 @@ Please also make sure you read the Terms of use and licence statement before usi
|
||||
return
|
||||
|
||||
print(f"Primary Index: {primary_index}, Secondary Index: {secondary_index}")
|
||||
|
||||
print(f"Selected Tone: {selected_tone_name}")
|
||||
print(f"Tone Instructions: {tone_instructions}")
|
||||
try:
|
||||
|
||||
response = self.client.audio.speech.create(
|
||||
model="tts-1",
|
||||
model="gpt-4o-mini-tts",
|
||||
voice=selected_voice,
|
||||
input=text,
|
||||
instructions=tone_instructions,
|
||||
response_format='wav'
|
||||
)
|
||||
|
||||
@@ -1086,7 +1117,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("590x460")
|
||||
self.geometry(self.untoggled_geometry)
|
||||
self.presets_collapsed = not self.presets_collapsed
|
||||
|
||||
|
||||
@@ -1316,6 +1347,7 @@ Please also make sure you read the Terms of use and licence statement before usi
|
||||
"model": self.default_model,
|
||||
"prompt": "",
|
||||
"auto_apply_ai_to_recording": False,
|
||||
"current_tone": "None",
|
||||
"hotkeys": {
|
||||
"record_start_stop": ["ctrl", "shift", "0"],
|
||||
"stop_recording": ["ctrl", "shift", "9"],
|
||||
@@ -1338,4 +1370,43 @@ Please also make sure you read the Terms of use and licence statement before usi
|
||||
else:
|
||||
return filename # Default to current directory for non-macOS systems
|
||||
|
||||
# Methods for tone preset management
|
||||
def show_tone_presets_manager(self):
|
||||
"""Show the tone presets manager dialog."""
|
||||
TonePresetsManager(self)
|
||||
|
||||
def load_current_tone_from_settings(self):
|
||||
"""Load the current tone preset from settings."""
|
||||
settings = self.load_settings()
|
||||
return settings.get("current_tone", "None")
|
||||
|
||||
def save_current_tone_to_settings(self):
|
||||
"""Save the current tone preset to settings."""
|
||||
settings = self.load_settings()
|
||||
settings["current_tone"] = self.current_tone_name
|
||||
self.save_settings_to_JSON(settings)
|
||||
|
||||
def on_tone_change(self, event):
|
||||
"""Handle tone selection change in the dropdown."""
|
||||
self.current_tone_name = self.tone_var.get()
|
||||
self.save_current_tone_to_settings()
|
||||
|
||||
def update_tone_selection(self):
|
||||
"""Update the tone selection dropdown with current presets."""
|
||||
# Update the variable
|
||||
self.tone_var.set(self.current_tone_name)
|
||||
|
||||
# Rebuild the dropdown menu
|
||||
menu = self.tone_menu["menu"]
|
||||
menu.delete(0, "end")
|
||||
|
||||
tone_options = ["None"] + list(self.tone_presets.keys())
|
||||
for tone in tone_options:
|
||||
menu.add_command(label=tone,
|
||||
command=lambda value=tone: self.tone_var.set(value))
|
||||
|
||||
def save_tone_presets(self, tone_presets):
|
||||
"""Save tone presets using the TonePresetsManager."""
|
||||
return TonePresetsManager.save_tone_presets(self, tone_presets)
|
||||
|
||||
|
||||
|
||||
391
utils/tone_presets_manager.py
Normal file
391
utils/tone_presets_manager.py
Normal file
@@ -0,0 +1,391 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import json
|
||||
from pathlib import Path
|
||||
import customtkinter as ctk
|
||||
import os
|
||||
|
||||
class TonePresetsManager:
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title("Spoken Tone Presets")
|
||||
self.dialog.geometry("800x650")
|
||||
self.dialog.resizable(False, False)
|
||||
self.dialog.transient(parent)
|
||||
self.dialog.grab_set()
|
||||
|
||||
# Center the dialog on the screen
|
||||
self.center_dialog()
|
||||
|
||||
# Add a variable to track the currently selected tone
|
||||
self.current_selected_tone = None
|
||||
|
||||
self.create_dialog()
|
||||
self.parent.after(100, self.update_content)
|
||||
|
||||
def center_dialog(self):
|
||||
# Get the parent window position and dimensions
|
||||
parent_x = self.parent.winfo_x()
|
||||
parent_y = self.parent.winfo_y()
|
||||
parent_width = self.parent.winfo_width()
|
||||
parent_height = self.parent.winfo_height()
|
||||
|
||||
# Calculate position for the dialog
|
||||
dialog_width = 800 # Width from geometry
|
||||
dialog_height = 650 # Height from geometry
|
||||
position_x = parent_x + (parent_width - dialog_width) // 2
|
||||
position_y = parent_y + (parent_height - dialog_height) // 2
|
||||
|
||||
# Set the position
|
||||
self.dialog.geometry(f"{dialog_width}x{dialog_height}+{position_x}+{position_y}")
|
||||
|
||||
def create_dialog(self):
|
||||
# Main container frame with padding
|
||||
main_frame = ttk.Frame(self.dialog, padding="10")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Create left panel (tone selection)
|
||||
self.create_left_panel(main_frame)
|
||||
|
||||
# Create right panel (tone content)
|
||||
self.create_right_panel(main_frame)
|
||||
|
||||
# Create bottom frame for main action button
|
||||
self.create_bottom_frame()
|
||||
|
||||
def create_left_panel(self, main_frame):
|
||||
left_panel = ttk.Frame(main_frame, width=200)
|
||||
left_panel.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
|
||||
left_panel.pack_propagate(False)
|
||||
|
||||
select_frame = ttk.LabelFrame(left_panel, text="Tone Selection", padding="5")
|
||||
select_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Listbox for tones with scrollbar
|
||||
self.tone_list = tk.Listbox(select_frame, height=8, selectmode=tk.BROWSE)
|
||||
tone_scrollbar = ttk.Scrollbar(select_frame, orient=tk.VERTICAL, command=self.tone_list.yview)
|
||||
self.tone_list.config(yscrollcommand=tone_scrollbar.set)
|
||||
|
||||
self.tone_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, pady=5)
|
||||
tone_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
|
||||
|
||||
# Populate listbox with "None" option plus tone presets
|
||||
all_tones = ["None"] + list(self.parent.tone_presets.keys())
|
||||
for tone in all_tones:
|
||||
self.tone_list.insert(tk.END, tone)
|
||||
if tone == self.parent.current_tone_name:
|
||||
self.tone_list.selection_set(all_tones.index(tone))
|
||||
|
||||
# Button frame
|
||||
button_frame = ttk.Frame(left_panel)
|
||||
button_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
# Action buttons
|
||||
self.new_button = ttk.Button(button_frame, text="New Tone", command=self.create_new_tone)
|
||||
self.delete_button = ttk.Button(button_frame, text="Delete", command=self.delete_current_tone)
|
||||
|
||||
self.new_button.pack(side=tk.LEFT, padx=2)
|
||||
self.delete_button.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Bind selection event
|
||||
self.tone_list.bind('<<ListboxSelect>>', self.on_tone_select)
|
||||
|
||||
def on_tone_select(self, event):
|
||||
"""Handle tone selection and update the current selection."""
|
||||
selected_indices = self.tone_list.curselection()
|
||||
if selected_indices:
|
||||
self.current_selected_tone = self.tone_list.get(selected_indices[0])
|
||||
self.update_content()
|
||||
|
||||
def create_right_panel(self, main_frame):
|
||||
self.right_panel = ttk.Frame(main_frame)
|
||||
self.right_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
content_frame = ttk.LabelFrame(self.right_panel, text="Tone Description", padding="5")
|
||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.edit_status_label = ttk.Label(content_frame, foreground="blue")
|
||||
self.edit_status_label.pack(anchor=tk.W, padx=5, pady=(5,0))
|
||||
|
||||
# Create a frame to contain the text widget and scrollbar
|
||||
text_frame = ttk.Frame(content_frame)
|
||||
text_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||
|
||||
# Create vertical scrollbar only
|
||||
v_scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL)
|
||||
v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# Create the text widget with word wrap and vertical scrollbar
|
||||
self.content_text = tk.Text(text_frame, wrap=tk.WORD,
|
||||
yscrollcommand=v_scrollbar.set)
|
||||
self.content_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# Configure the scrollbar
|
||||
v_scrollbar.config(command=self.content_text.yview)
|
||||
|
||||
self.save_changes_button = ttk.Button(self.right_panel, text="Save Changes",
|
||||
command=self.save_content_changes, state='disabled')
|
||||
self.save_changes_button.pack(pady=(5, 0), anchor=tk.E)
|
||||
|
||||
def create_bottom_frame(self):
|
||||
bottom_frame = ttk.Frame(self.dialog)
|
||||
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10, padx=10)
|
||||
|
||||
save_button = ctk.CTkButton(
|
||||
bottom_frame,
|
||||
text="Save Selection and Exit",
|
||||
corner_radius=20,
|
||||
height=35,
|
||||
fg_color="#058705",
|
||||
hover_color="#046a38",
|
||||
font=("Arial", 13, "bold"),
|
||||
command=self.save_and_exit
|
||||
)
|
||||
save_button.pack(side=tk.BOTTOM, fill=tk.X)
|
||||
|
||||
def update_content(self):
|
||||
"""Update the content area with the selected tone."""
|
||||
if not self.current_selected_tone:
|
||||
selected_indices = self.tone_list.curselection()
|
||||
if selected_indices:
|
||||
self.current_selected_tone = self.tone_list.get(selected_indices[0])
|
||||
else:
|
||||
return
|
||||
|
||||
# Enable content_text before any operations
|
||||
self.content_text.config(state='normal')
|
||||
self.content_text.delete('1.0', tk.END)
|
||||
|
||||
if self.current_selected_tone == "None":
|
||||
self.content_text.insert('1.0', "No tone modifier will be applied to the speech.")
|
||||
self.content_text.config(state='disabled')
|
||||
self.save_changes_button.config(state='disabled')
|
||||
self.delete_button.config(state='disabled')
|
||||
self.edit_status_label.config(
|
||||
text="(No tone modifier)",
|
||||
foreground="gray"
|
||||
)
|
||||
else:
|
||||
self.content_text.insert('1.0', self.parent.tone_presets[self.current_selected_tone])
|
||||
self.content_text.config(state='normal')
|
||||
self.save_changes_button.config(state='normal')
|
||||
self.delete_button.config(state='normal')
|
||||
self.edit_status_label.config(
|
||||
text=f"{self.current_selected_tone}",
|
||||
foreground="blue"
|
||||
)
|
||||
|
||||
def save_content_changes(self):
|
||||
"""Save changes to the current tone content."""
|
||||
if not self.current_selected_tone or self.current_selected_tone == "None":
|
||||
return
|
||||
|
||||
new_content = self.content_text.get("1.0", tk.END).strip()
|
||||
if not new_content:
|
||||
messagebox.showerror("Error", "Tone description cannot be empty")
|
||||
return
|
||||
|
||||
self.parent.tone_presets[self.current_selected_tone] = new_content
|
||||
self.parent.save_tone_presets(self.parent.tone_presets)
|
||||
messagebox.showinfo("Success", "Tone changes saved successfully")
|
||||
|
||||
def create_new_tone(self):
|
||||
"""Open dialog for creating a new tone preset."""
|
||||
tone_dialog = tk.Toplevel(self.dialog)
|
||||
tone_dialog.title("Create New Tone Preset")
|
||||
tone_dialog.geometry("600x400")
|
||||
tone_dialog.transient(self.dialog)
|
||||
tone_dialog.grab_set()
|
||||
|
||||
# Center the new tone dialog
|
||||
dialog_width = 600
|
||||
dialog_height = 400
|
||||
position_x = self.dialog.winfo_x() + (self.dialog.winfo_width() - dialog_width) // 2
|
||||
position_y = self.dialog.winfo_y() + (self.dialog.winfo_height() - dialog_height) // 2
|
||||
tone_dialog.geometry(f"{dialog_width}x{dialog_height}+{position_x}+{position_y}")
|
||||
|
||||
# Name entry
|
||||
name_frame = ttk.Frame(tone_dialog, padding="10")
|
||||
name_frame.pack(fill=tk.X)
|
||||
ttk.Label(name_frame, text="Tone Name:").pack(side=tk.LEFT)
|
||||
name_entry = ttk.Entry(name_frame)
|
||||
name_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 0))
|
||||
|
||||
# Tone description
|
||||
desc_frame = ttk.LabelFrame(tone_dialog, text="Tone Description", padding="10")
|
||||
desc_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||
|
||||
# Create a frame for the text widget and scrollbar
|
||||
text_frame = ttk.Frame(desc_frame)
|
||||
text_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# Create vertical scrollbar only
|
||||
v_scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL)
|
||||
v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# Create the text widget with word wrap and vertical scrollbar
|
||||
new_tone_text = tk.Text(text_frame, wrap=tk.WORD, height=15,
|
||||
yscrollcommand=v_scrollbar.set)
|
||||
new_tone_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
# Configure the scrollbar
|
||||
v_scrollbar.config(command=new_tone_text.yview)
|
||||
|
||||
def save_new_tone():
|
||||
new_name = name_entry.get().strip()
|
||||
if not new_name:
|
||||
messagebox.showerror("Error", "Please enter a tone name")
|
||||
return
|
||||
if len(new_name) > 25:
|
||||
messagebox.showerror("Error", "Tone name must be 25 characters or less")
|
||||
return
|
||||
|
||||
new_content = new_tone_text.get("1.0", tk.END).strip()
|
||||
if not new_content:
|
||||
messagebox.showerror("Error", "Please enter a tone description")
|
||||
return
|
||||
|
||||
# Save tone
|
||||
self.parent.tone_presets[new_name] = new_content
|
||||
self.parent.save_tone_presets(self.parent.tone_presets)
|
||||
|
||||
# Update listbox and select the new tone
|
||||
self.tone_list.delete(0, tk.END)
|
||||
all_tones = ["None"] + list(self.parent.tone_presets.keys())
|
||||
for t in all_tones:
|
||||
self.tone_list.insert(tk.END, t)
|
||||
|
||||
# Find and select the new tone
|
||||
new_tone_index = all_tones.index(new_name)
|
||||
self.tone_list.selection_clear(0, tk.END)
|
||||
self.tone_list.selection_set(new_tone_index)
|
||||
self.tone_list.see(new_tone_index)
|
||||
|
||||
# Set the current selected tone and update content
|
||||
self.current_selected_tone = new_name
|
||||
self.update_content()
|
||||
|
||||
tone_dialog.destroy()
|
||||
|
||||
# Buttons
|
||||
button_frame = ttk.Frame(tone_dialog)
|
||||
button_frame.pack(fill=tk.X, pady=10, padx=10)
|
||||
ttk.Button(button_frame, text="Save",
|
||||
command=save_new_tone).pack(side=tk.RIGHT, padx=5)
|
||||
ttk.Button(button_frame, text="Cancel",
|
||||
command=tone_dialog.destroy).pack(side=tk.RIGHT)
|
||||
|
||||
def delete_current_tone(self):
|
||||
"""Delete the currently selected tone."""
|
||||
selected_indices = self.tone_list.curselection()
|
||||
if not selected_indices:
|
||||
return
|
||||
|
||||
tone_name = self.tone_list.get(selected_indices[0])
|
||||
if tone_name == "None":
|
||||
return
|
||||
|
||||
if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete '{tone_name}'?"):
|
||||
if tone_name == self.parent.current_tone_name:
|
||||
self.parent.current_tone_name = "None"
|
||||
self.parent.save_current_tone_to_settings()
|
||||
self.parent.update_tone_selection()
|
||||
|
||||
del self.parent.tone_presets[tone_name]
|
||||
self.parent.save_tone_presets(self.parent.tone_presets)
|
||||
|
||||
# Update listbox
|
||||
self.tone_list.delete(0, tk.END)
|
||||
for t in ["None"] + list(self.parent.tone_presets.keys()):
|
||||
self.tone_list.insert(tk.END, t)
|
||||
|
||||
# Set current_selected_tone to "None" before selecting it in the listbox
|
||||
self.current_selected_tone = "None"
|
||||
self.tone_list.selection_set(0)
|
||||
self.update_content()
|
||||
|
||||
def save_and_exit(self):
|
||||
"""Save the selected tone and close the dialog."""
|
||||
if not self.current_selected_tone:
|
||||
messagebox.showwarning("No Selection", "Please select a tone before saving")
|
||||
return
|
||||
|
||||
tone_name = self.current_selected_tone
|
||||
|
||||
# First save any content changes if it's not "None"
|
||||
if tone_name != "None":
|
||||
new_content = self.content_text.get("1.0", tk.END).strip()
|
||||
if new_content:
|
||||
self.parent.tone_presets[tone_name] = new_content
|
||||
self.parent.save_tone_presets(self.parent.tone_presets)
|
||||
|
||||
# Then save the selection
|
||||
self.parent.current_tone_name = tone_name
|
||||
self.parent.save_current_tone_to_settings()
|
||||
self.parent.update_tone_selection()
|
||||
self.dialog.destroy()
|
||||
messagebox.showinfo("Success", f"Now using tone: {tone_name}")
|
||||
|
||||
@staticmethod
|
||||
def load_tone_presets(app_instance):
|
||||
"""Load tone presets from JSON file, creating from template if needed."""
|
||||
# Create config directory if it doesn't exist
|
||||
config_dir = Path("config")
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tone_presets_file = config_dir / "tone_presets.json"
|
||||
example_path = app_instance.resource_path("assets/tone_presets.example.json")
|
||||
|
||||
# If tone presets file doesn't exist, copy from example
|
||||
if not tone_presets_file.exists():
|
||||
try:
|
||||
# Check if example file exists
|
||||
if not os.path.exists(example_path):
|
||||
# Create default tone presets
|
||||
default_presets = {
|
||||
"Cheerful": "Speak in a cheerful, upbeat tone with enthusiasm.",
|
||||
"Angry": "Sound like you're angry and frustrated.",
|
||||
"Bedtime Story": "Say it like you're reading a bedtime story to a child, soft and soothing.",
|
||||
"News Anchor": "Speak in a professional news anchor tone, clear and formal.",
|
||||
"Excited": "Sound extremely excited and enthusiastic."
|
||||
}
|
||||
|
||||
# Create and save default presets
|
||||
with open(tone_presets_file, "w", encoding="utf-8") as f:
|
||||
json.dump({"tone_presets": default_presets}, f, indent=2)
|
||||
else:
|
||||
# Copy example presets to config directory
|
||||
with open(example_path, "r", encoding="utf-8") as example_file:
|
||||
with open(tone_presets_file, "w", encoding="utf-8") as config_file:
|
||||
config_file.write(example_file.read())
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to create tone presets file: {e}")
|
||||
return {}
|
||||
|
||||
# Load presets from file
|
||||
try:
|
||||
with open(tone_presets_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data.get("tone_presets", {})
|
||||
except Exception as e:
|
||||
print(f"Error loading tone presets: {e}")
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def save_tone_presets(app_instance, tone_presets):
|
||||
"""Save tone presets to JSON file."""
|
||||
config_dir = Path("config")
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tone_presets_file = config_dir / "tone_presets.json"
|
||||
data = {"tone_presets": tone_presets}
|
||||
|
||||
try:
|
||||
with open(tone_presets_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error saving tone presets: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user