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.")
|
messagebox.showinfo("Settings Updated", "Your hotkey settings have been saved successfully.")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_hotkey_instructions(app):
|
def show_hotkey_instructions(parent):
|
||||||
"""Show hotkey instructions."""
|
"""Show instructions for using hotkeys."""
|
||||||
instruction_window = tk.Toplevel(app)
|
instruction_window = tk.Toplevel(parent)
|
||||||
instruction_window.title("Hotkey Instructions")
|
instruction_window.title("Hotkey Instructions")
|
||||||
instruction_window.geometry("400x300") # Width x Height
|
instruction_window.geometry("600x400") # Width x Height
|
||||||
|
|
||||||
instructions = """How to use Hotkeys
|
settings = parent.load_settings()
|
||||||
ctrl+shift+0
|
record_shortcut = "+".join(filter(None, settings["hotkeys"]["record_start_stop"]))
|
||||||
This starts a recording, then converts to text and plays when you press this hotkey again.
|
play_shortcut = "+".join(filter(None, settings["hotkeys"]["play_last_audio"]))
|
||||||
|
stop_shortcut = "+".join(filter(None, settings["hotkeys"]["stop_recording"]))
|
||||||
|
|
||||||
ctrl+shift+9
|
instructions = f"""Available Hotkeys:
|
||||||
If you are recording, you can press this hotkey to stop recording without playing
|
|
||||||
|
|
||||||
ctrl+shift+8
|
1. {record_shortcut} - Start/Stop Recording
|
||||||
This replays the last audio clip played
|
This hotkey toggles recording from your selected input device.
|
||||||
|
|
||||||
|
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=380).pack(padx=10, pady=10)
|
|
||||||
|
tk.Label(instruction_window, text=instructions, justify=tk.LEFT, wraplength=580).pack(padx=10, pady=10)
|
||||||
|
|
||||||
# Add a button to close the window
|
# Add a button to close the window
|
||||||
ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0))
|
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.api_key_manager import APIKeyManager
|
||||||
from utils.hotkey_manager import HotkeyManager
|
from utils.hotkey_manager import HotkeyManager
|
||||||
from utils.resource_utils import ResourceUtils
|
from utils.resource_utils import ResourceUtils
|
||||||
|
from utils.tone_presets_manager import TonePresetsManager
|
||||||
|
|
||||||
# Modify the load environment variables to load from config/.env
|
# Modify the load environment variables to load from config/.env
|
||||||
def load_env_file():
|
def load_env_file():
|
||||||
@@ -33,7 +34,8 @@ class TextToMic(tk.Tk):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.title("Scorchsoft Text to Mic")
|
self.title("Scorchsoft Text to Mic")
|
||||||
self.default_geometry = "590x750"
|
self.default_geometry = "590x770"
|
||||||
|
self.untoggled_geometry ="590x490"
|
||||||
self.geometry(self.default_geometry)
|
self.geometry(self.default_geometry)
|
||||||
|
|
||||||
self.available_models = ["gpt-4o-mini", "gpt-4o", "gpt-4-turbo"]
|
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_devices = self.get_audio_devices() # Load audio devices
|
||||||
self.available_input_devices = self.get_input_devices() # Load input 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.create_menu()
|
||||||
self.initialize_gui()
|
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="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="ChatGPT Manipulation", command=self.chat_gpt_settings)
|
||||||
settings_menu.add_command(label="Hotkey Settings", command=self.show_hotkey_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
|
||||||
playback_menu = Menu(self.menubar, tearoff=0)
|
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 = ttk.OptionMenu(main_frame, self.voice_var,'fable', *voices)
|
||||||
voice_menu.grid(column=1, row=0, sticky=tk.W)
|
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
|
# 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 = 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
|
# 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 = 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
|
# 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 = 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 = 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
|
# 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
|
# Create a frame to contain the dropdown and save button
|
||||||
save_frame = ttk.Frame(main_frame)
|
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
|
# Preset Category dropdown
|
||||||
self.category_var = tk.StringVar(value="Select Category")
|
self.category_var = tk.StringVar(value="Select Category")
|
||||||
@@ -278,11 +292,11 @@ class TextToMic(tk.Tk):
|
|||||||
bg_color = self.style.lookup('TFrame', 'background')
|
bg_color = self.style.lookup('TFrame', 'background')
|
||||||
text_color = self.style.lookup('TLabel', 'foreground')
|
text_color = self.style.lookup('TLabel', 'foreground')
|
||||||
self.text_input.configure(bg=bg_color, fg=text_color, insertbackground=text_color)
|
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
|
# Create a frame for the buttons to allow for better styling
|
||||||
button_frame = ttk.Frame(main_frame)
|
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(0, weight=1)
|
||||||
button_frame.columnconfigure(1, weight=1)
|
button_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
@@ -328,9 +342,12 @@ class TextToMic(tk.Tk):
|
|||||||
|
|
||||||
#Credits
|
#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 = 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())
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def create_presets_section(self):
|
def create_presets_section(self):
|
||||||
@@ -441,7 +458,7 @@ class TextToMic(tk.Tk):
|
|||||||
def show_instructions(self):
|
def show_instructions(self):
|
||||||
instruction_window = tk.Toplevel(self)
|
instruction_window = tk.Toplevel(self)
|
||||||
instruction_window.title("How to Use")
|
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:
|
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.
|
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.
|
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()
|
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
|
# 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
|
||||||
@@ -658,13 +687,15 @@ Please also make sure you read the Terms of use and licence statement before usi
|
|||||||
return
|
return
|
||||||
|
|
||||||
print(f"Primary Index: {primary_index}, Secondary Index: {secondary_index}")
|
print(f"Primary Index: {primary_index}, Secondary Index: {secondary_index}")
|
||||||
|
print(f"Selected Tone: {selected_tone_name}")
|
||||||
|
print(f"Tone Instructions: {tone_instructions}")
|
||||||
try:
|
try:
|
||||||
|
|
||||||
response = self.client.audio.speech.create(
|
response = self.client.audio.speech.create(
|
||||||
model="tts-1",
|
model="gpt-4o-mini-tts",
|
||||||
voice=selected_voice,
|
voice=selected_voice,
|
||||||
input=text,
|
input=text,
|
||||||
|
instructions=tone_instructions,
|
||||||
response_format='wav'
|
response_format='wav'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1086,7 +1117,7 @@ Please also make sure you read the Terms of use and licence statement before usi
|
|||||||
else:
|
else:
|
||||||
self.presets_frame.grid_remove()
|
self.presets_frame.grid_remove()
|
||||||
self.presets_button.config(text="▶ Presets")
|
self.presets_button.config(text="▶ Presets")
|
||||||
self.geometry("590x460")
|
self.geometry(self.untoggled_geometry)
|
||||||
self.presets_collapsed = not self.presets_collapsed
|
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,
|
"model": self.default_model,
|
||||||
"prompt": "",
|
"prompt": "",
|
||||||
"auto_apply_ai_to_recording": False,
|
"auto_apply_ai_to_recording": False,
|
||||||
|
"current_tone": "None",
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"record_start_stop": ["ctrl", "shift", "0"],
|
"record_start_stop": ["ctrl", "shift", "0"],
|
||||||
"stop_recording": ["ctrl", "shift", "9"],
|
"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:
|
else:
|
||||||
return filename # Default to current directory for non-macOS systems
|
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