Skip to content

Commit

Permalink
Merge pull request #10 from jason-kane/beta
Browse files Browse the repository at this point in the history
Beta -> Release
  • Loading branch information
jason-kane authored Jun 9, 2024
2 parents fd6fccd + c061307 commit 6b0c157
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 66 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies = [
"google-cloud-texttospeech",
"matplotlib",
"pyfiglet",
"pyfeather",
"pypiwin32",
"setuptools",
"sqlalchemy-utils",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ prompt-toolkit==3.0.43
proto-plus==1.23.0
protobuf==4.25.3
pure-eval==0.2.2
tkfeather==1.0.0
pyasn1-modules==0.3.0
pyasn1==0.5.1
pycparser==2.21
Expand Down
83 changes: 50 additions & 33 deletions src/coh_npc_voices/effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pedalboard
import voicebox
from sqlalchemy import select
from tkfeather import Feather

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -144,6 +145,7 @@ def __init__(self, parent, *args, **kwargs):
self.effect_id = tk.IntVar()
self.parameters = []
self.traces = {}
self.trashcan = Feather("trash-2", size=24)

topbar = ttk.Frame(self)
ttk.Label(
Expand All @@ -164,7 +166,7 @@ def __init__(self, parent, *args, **kwargs):

ttk.Button(
topbar,
text="X",
image=self.trashcan.icon,
style="CloseFrame.TButton",
command=self.remove_effect
).pack(side="right")
Expand Down Expand Up @@ -207,15 +209,15 @@ def reconfig(self, varname, lindex, operation):
"""
log.info(f'reconfig triggered by {varname}/{lindex}/{operation}')
effect_id = self.effect_id.get()
key = varname.split('_')[-1] # I know, I feel dirty.
# key = "_".join(varname.split('_')[1:]) # I know, I feel dirty.

with models.Session(models.engine) as session:
# fragile, varname is what is coming off the trace trigger
log.info(f'Reading effects settings when {effect_id=} and key={key}')
log.info(f'Reading effects settings when {effect_id=}')
effect_settings = session.scalars(
select(models.EffectSetting).where(
models.EffectSetting.effect_id==effect_id,
models.EffectSetting.key==key
models.EffectSetting.effect_id==effect_id
# models.EffectSetting.key==key
)
).all()

Expand All @@ -238,9 +240,11 @@ def reconfig(self, varname, lindex, operation):
else:
log.info(f'Value for {effect_setting.key} has not changed')

log.info(found)
log.info(f"{found=}")
change = False
for effect_setting_key in self.traces:
if effect_setting_key not in found:
change = True
log.info(f'Expected key {effect_setting_key} does not exist in the database')
value = self.traces[effect_setting_key].get()
log.info(f'Creating new EffectSetting({effect_id}, key={effect_setting_key}, value={value})')
Expand All @@ -250,43 +254,56 @@ def reconfig(self, varname, lindex, operation):
value=value
)
session.add(new_setting)
log.info('commiting db session')
session.commit()

if change:
log.info('commiting db session')
session.commit()


def load(self, session=None):
def load(self):
"""
reflect the current db values for each effect setting
to the tk.Variable tied to the widget for that
setting.
"""
if session is None:
session = models.Session(models.engine)
#if session is None:
# session = models.Session(models.engine)

effect_id = self.effect_id.get()
log.info(f'Loading {effect_id=}')

effect_settings = session.scalars(
select(models.EffectSetting).where(
models.EffectSetting.effect_id == effect_id
)
).all()
with models.db() as session:
effect_settings = session.scalars(
select(models.EffectSetting).where(
models.EffectSetting.effect_id == effect_id
)
).all()

for setting in effect_settings:
log.info(f'Working on {setting}')
found = set()
for setting in effect_settings:
log.info(f'Working on {setting}')

if setting.key in found:
log.info(f'Duplicate setting for {setting.key=} where {effect_id=}')
session.delete(setting)
continue

tkvar = getattr(self, setting.key, None)
found.add(setting.key)

if setting.key not in self.traces:
if tkvar:
tkvar.set(setting.value)
else:
log.error(
f'Invalid configuration. '
f'{setting.key} is not available for '
f'{self}')

tkvar.trace_add("write", self.reconfig)
self.traces[setting.key] = tkvar
tkvar = getattr(self, setting.key, None)

if setting.key not in self.traces:
if tkvar:
tkvar.set(setting.value)
else:
log.error(
f'Invalid configuration. '
f'{setting.key} is not available for '
f'{self}')

tkvar.trace_add("write", self.reconfig)
self.traces[setting.key] = tkvar
session.commit()

# scipy iir filters
IIR_FILTERS = ['butter', 'cheby1', 'cheby2', 'ellip', 'bessel']
Expand Down Expand Up @@ -609,9 +626,9 @@ def __init__(self, parent, *args, **kwargs):
pname="max_amplitude",
label='Max-Amplitude',
desc="Maximum amplitude in Hz",
default=1.0,
from_=0,
to=3,
default=0.0,
from_=-1,
to=1,
digits=2,
resolution=0.1
).pack(side='top', fill='x', expand=True)
Expand Down
45 changes: 25 additions & 20 deletions src/coh_npc_voices/engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import os
import sys
import tempfile
import numpy as np
import time
from io import BytesIO
import tkinter as tk
from dataclasses import dataclass, field
from tkinter import ttk
Expand Down Expand Up @@ -48,26 +50,29 @@ def get_speech(self, text: StrOrSSML) -> Audio:
voice.set_rate(self.rate)
voice.set_voice(self.voice)

with tempfile.NamedTemporaryFile() as tmp:
# just need the safe filename
tmp.close()
# this can:
# File "C:\Users\jason\Desktop\coh_npc_voices\venv\Lib\site-packages\tts\sapi.py", line 93, in say
# self.voice.Speak(message, flag)
# _ctypes.COMError: (-2147200958, None, ('XML parser error', None, None, 0, None))

success = False
while not success:
try:
# create a temporary wave file
voice.create_recording(tmp.name, text)
success = True
except Exception as err:
log.error(err)
log.error("Text was: %s", text)
time.sleep(0.1)
stream = tts.sapi.comtypes.client.CreateObject('SAPI.SpMemoryStream')

# save the original output stream
temp_stream = voice.voice.AudioOutputStream

# hijack it
voice.voice.AudioOutputStream = stream

voice.say(text)

# restore it
voice.voice.AudioOutputStream = temp_stream

samples = np.frombuffer(
bytes(stream.GetData()),
dtype=np.int16
)

audio = voicebox.tts.utils.get_audio_from_samples(
samples,
22050
)

audio = voicebox.tts.utils.get_audio_from_wav_file(tmp.name)
return audio


Expand Down Expand Up @@ -462,7 +467,7 @@ class GoogleCloud(TTSEngine):
config = (
('Language Code', 'language_code', "StringVar", 'en-US', {}, "get_language_codes"),
('Voice Name', 'voice_name', "StringVar", "<unconfigured>", {}, "get_voice_names"),
('Speaking Rate', 'speaking_rate', "DoubleVar", 1, {'min': 0.25, 'max': 2.75, 'digits': 3, 'resolution': 0.25}, None),
('Speakin Rate', 'speaking_rate', "DoubleVar", 1, {'min': 0.5, 'max': 1.75, 'digits': 3, 'resolution': 0.25}, None),
('Voice Pitch', 'voice_pitch', "DoubleVar", 1, {'min': -10, 'max': 10, 'resolution': 0.5}, None)
)

Expand Down
92 changes: 79 additions & 13 deletions src/coh_npc_voices/voice_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pedalboard.io import AudioFile
from scipy.io import wavfile
from sqlalchemy import delete, desc, select, update
from tkfeather import Feather
from voicebox.sinks import Distributor, SoundDevice, WaveFile
from voicebox.tts.utils import get_audio_from_wav_file

Expand Down Expand Up @@ -560,7 +561,7 @@ def load_effects(self):

category, name = raw_name.split(maxsplit=1)

with models.Session(models.engine) as session:
with models.db() as session:
character = models.get_character(name, category, session)

if character is None:
Expand Down Expand Up @@ -597,7 +598,7 @@ def load_effects(self):
effect_config_frame.effect_id.set(effect.id)
self.effects.append(effect_config_frame)

effect_config_frame.load(session=session)
effect_config_frame.load()

if not has_effects:
self.buffer = ttk.Frame(self, width=1, height=1).pack(side="top")
Expand Down Expand Up @@ -763,23 +764,39 @@ class DetailSide(ttk.Frame):
"""
def __init__(self, parent, selected_character, *args, **kwargs):
super().__init__(parent, *args, **kwargs)

self.parent = parent
self.selected_character = selected_character
self.listside = None
self.trashcan = Feather("trash-2", size=24)

self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.frame = ttk.Frame(self.canvas)
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb = tk.Scrollbar(self, orient="vertical")
self.vsb.pack(side="right", fill="y", expand=False)

self.vsb.pack(side="right", fill="y")
self.canvas = tk.Canvas(
self,
borderwidth=0,
background="#ffffff",
yscrollcommand=self.vsb.set
)
self.canvas.pack(side="left", fill="both", expand=True)

# drag the scrollbar, see the canvas slide
self.vsb.config(command=self.canvas.yview)

self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)

# this is the scrollable thing
self.frame = ttk.Frame(self.canvas)
self.frame_id = self.canvas.create_window(
(0, 0), window=self.frame, anchor="nw", tags="self.frame"
(0, 0), window=self.frame, anchor="nw",
tags="self.frame"
)

self.frame.bind("<Configure>", self.onFrameConfigure)
# self.frame.pack(side='top', fill='x')
self.canvas.bind("<Configure>", self.onCanvasConfigure)
###

name_frame = ttk.Frame(self.frame)

Expand All @@ -798,7 +815,7 @@ def __init__(self, parent, selected_character, *args, **kwargs):

ttk.Button(
name_frame,
text="X",
image=self.trashcan.icon,
style="RemoveCharacter.TButton",
command=self.remove_character
).pack(side="right")
Expand Down Expand Up @@ -849,13 +866,40 @@ def __init__(self, parent, selected_character, *args, **kwargs):
self.effect_list = EffectList(self.frame, selected_character)
self.effect_list.pack(side="top", fill="x", expand=True)

self.bind('<Enter>', self._bound_to_mousewheel)
self.bind('<Leave>', self._unbound_to_mousewheel)

def _bound_to_mousewheel(self, event):
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)

def _unbound_to_mousewheel(self, event):
self.canvas.unbind_all("<MouseWheel>")

def _on_mousewheel(self, event):
self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")

def remove_character(self):
if self.listside:
self.listside.delete_selected_character()

def onFrameConfigure(self, event):
"""Reset the scroll region to encompass the inner frame"""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
# Update the scrollbars to match the size of the inner frame.
size = (self.frame.winfo_reqwidth(), self.frame.winfo_reqheight())
self.canvas.config(scrollregion="0 0 %s %s" % size)
if self.frame.winfo_reqwidth() != self.canvas.winfo_width():
# Update the canvas's width to fit the inner frame.
self.canvas.config(width=self.frame.winfo_reqwidth())

# self.canvas.configure(scrollregion=self.canvas.bbox("all"))

def onCanvasConfigure(self, event):
if self.frame.winfo_reqwidth() != self.canvas.winfo_width():
# Update the frame width to fill the canvas.
self.canvas.itemconfigure(
self.frame_id,
width=self.canvas.winfo_width()
)

def load_character(self, raw_name):
"""
Expand Down Expand Up @@ -1021,8 +1065,21 @@ def __init__(self, parent, detailside, *args, **kwargs):
self.list_items = tk.Variable(value=[])
self.refresh_character_list()

self.listbox = tk.Listbox(self, height=10, listvariable=self.list_items)
self.listbox.pack(side="top", expand=True, fill=tk.BOTH)
listarea = ttk.Frame(self)
self.listbox = tk.Listbox(listarea, height=10, listvariable=self.list_items)
self.listbox.pack(side="left", expand=True, fill=tk.BOTH)
vsb = tk.Scrollbar(
listarea,
orient='vertical',
command=self.listbox.yview
)
self.listbox.configure(yscrollcommand=vsb.set)

self.bind('<Enter>', self._bound_to_mousewheel)
self.bind('<Leave>', self._unbound_to_mousewheel)

vsb.pack(side='right', fill='y')
listarea.pack(side="top", expand=True, fill=tk.BOTH)

action_frame = ttk.Frame(self)
ttk.Button(
Expand All @@ -1037,6 +1094,15 @@ def __init__(self, parent, detailside, *args, **kwargs):
self.listbox.select_set(0)
self.listbox.bind("<<ListboxSelect>>", self.character_selected)

def _bound_to_mousewheel(self, event):
self.listbox.bind_all("<MouseWheel>", self._on_mousewheel)

def _unbound_to_mousewheel(self, event):
self.listbox.unbind_all("<MouseWheel>")

def _on_mousewheel(self, event):
self.listbox.yview_scroll(int(-1*(event.delta/120)), "units")

def apply_list_filter(self, a, b, c):
self.refresh_character_list()

Expand Down

0 comments on commit 6b0c157

Please sign in to comment.