Skip to content

Commit

Permalink
Merge pull request #27 from HackXIt/feature/elevenlabs
Browse files Browse the repository at this point in the history
Feature/elevenlabs - Shrinked MVP
  • Loading branch information
HackXIt committed Jan 18, 2024
2 parents e7a586c + bf163f5 commit 9d94842
Show file tree
Hide file tree
Showing 31 changed files with 1,724 additions and 273 deletions.
2 changes: 1 addition & 1 deletion .github/static/SpeechJokey.spec
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ from kivymd import hooks_path as kivymd_hooks_path

block_cipher = None
app_name = 'SpeechJokey'
#win_icon = '.\icon.ico'
win_icon = '.\assets\speech-jokey.png'

a = Analysis(
['src\\main.py'],
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# VSCode environment
.vscode/.history

tmp
app_settings.json

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: SpeechJokey",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/main.py",
"console": "integratedTerminal",
"justMyCode": true
}
]
}
3 changes: 3 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Relevant links
- (SSML support in voice platforms)[http://ssml.guru/]
- (Speech Markdown Docs)[https://www.speechmarkdown.org/basics/what/]
3 changes: 1 addition & 2 deletions natasa-speech-synthesis.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"Gruntfuggly.todo-tree",
"zeshuaro.vscode-python-poetry"
"Gruntfuggly.todo-tree"
]
}
}
507 changes: 506 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ six = "^1.16.0"
buildozer = "^1.5.0"
pyinstaller = "^5.6.2"
kivymd = "^1.1.1"
elevenlabs = "^0.2.26"
pyaudio = "^0.2.14"

[build-system]
requires = ["poetry-core"]
Expand Down
61 changes: 33 additions & 28 deletions src/SpeechJokey.kv
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<MainScreen>:
orientation: 'vertical'
text_input: text_input

BoxLayout:
orientation: 'vertical'
Expand All @@ -17,45 +18,49 @@
text: 'Load File'
on_press: root.load_file()

Button:
text: 'Save File'
on_press: root.save_file()

Button:
text: 'Listen'
on_press: root.play_audio()

Button:
text: 'Save'
on_press: root.save_file()
text: 'Generate'
on_press: root.generate_audio()

Button:
text: 'Settings'
on_press: root.open_settings()

# Adjustable Split View for the text input and additional functionalities
Splitter:
sizable_from: 'top'
min_size: 100
max_size: 500
orientation: 'vertical'

TextInput:
id: text_input
multiline: True
hint_text: 'Enter your text here or load from a file...'
# on_text: root.adjust_split_view()

# SSML tag editing tools within the split view
BoxLayout:
orientation: 'vertical'

TextInput:
id: text_input
multiline: True
hint_text: 'Enter your text here or load from a file...'
on_text: root.adjust_split_view()

# SSML tag editing tools within the split view
BoxLayout:
size_hint_y: None
height: '50dp'

Button:
text: 'Add Break'
on_press: root.insert_ssml_tag('break')

Button:
text: 'Change Pitch'
on_press: root.insert_ssml_tag('pitch')

Button:
text: 'Emphasize'
on_press: root.insert_ssml_tag('emphasis')
size_hint_y: None
height: '50dp'

Button:
text: 'Add Break'
on_press: root.insert_ssml_tag('break')

Button:
text: 'Change Pitch'
on_press: root.insert_ssml_tag('pitch')

Button:
text: 'Emphasize'
on_press: root.insert_ssml_tag('emphasis')

# Playback controls
BoxLayout:
Expand Down
101 changes: 101 additions & 0 deletions src/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Supported APIs

This directory contains the client wrappers for the supported APIs.

A client wrapper is a class that implements the API calls and provides a settings widget for the application to use for configuration.
Any settings that are required for the API shall be stored in the settings class.

New APIs can be added by following the instructions below and **must** follow the described structure.

For a reference implementation, see the [example API](exampleapi/).

## Structure

An API module **must** be stored in a directory under `src/api/`, using `<api_name>` as the directory name.

API modules **must** consist of three classes, stored in the `<api_name>.py` file:
- `<ApiName>Widget`: The widget for the API to view and edit settings in the application settings popup. This class should use CamelCase naming.
- `<ApiName>Settings`: The settings class for the API, which **must** inherit from `BaseApiSettings`. This class should use CamelCase naming.
- `<ApiName>`: The API implementation class. This class should use CamelCase naming.

Example for the naming scheme:
`<api_name>`: `exampleapi`
`<ApiName>`: `ExampleApi` (should be a CamelCase version of `<api_name>`)
`<ApiName>Widget`: `ExampleApiWidget`
`<ApiName>Settings`: `ExampleApiSettings`

The `BaseApiSettings` class implements the required Singleton pattern for the settings class. It ensures proper dynamic loading of the API during application startup by declaring methods that **must** be implemented by the settings class.

Additionally, the API module may contain a `<api_name>.kv` file, which contains the kivy rules for the settings view of the specific API.

The widget for the settings holds the reference to the singleton instance of the settings class for the API.
Whenever settings are changed in the settings widget, the settings class should be updated accordingly.
Whenever settings are used, they shall be retrieved via the settings widget or via the settings class.

The settings class **must** implement the following methods:
- `isSupported()`: Returns a boolean indicating whether the API is functionally supported by the current environment. Setting this to false will ignore the API during the application startup.
- `get_settings_widget()`: Returns an instance of the settings widget for the API.
- `load_settings()`: Loads the settings from the global settings instance into the internal state of the API settings. Internally this shall call `global_settings.get_setting(api_name, setting_name)` for each setting that is required by your API.
- `save_settings()`: Saves the internal state of the API settings into the global settings instance. Internally this shall call `global_settings.update_setting(api_name, setting_name, value)` for each setting that is required to be stored for your API.

## How to add a new API

1. Create a new directory for the API under `src/api/`.
2. Add these files into the created directory `__init__.py`, `<api_name>.py`, `<api_name>.kv`.
3. Update `<api_name>.py` with the following content:

```python
from kivy.app import App
from ..base_settings import BaseApiSettings

class <ApiName>Widget(<LayoutForSettings>):
# Add the properties for the settings widget here
# Example: example_setting = StringProperty('')
settings = ObjectProperty(None)

def __init__(self, **kwargs):
super(<ApiName>Widget, self).__init__(**kwargs)
self.add_widget(Label(text="Hello World!"))
self.settings = <ApiName>Settings()
self.settings.load_settings()

class <ApiName>Settings(BaseApiSettings):
api_name = '<ApiName>'
# Add relevant settings for the API here
# Example: example_setting = 'Foo'

def __init__(self, **kwargs):
super(ExampleAPISettings, self).__init__(**kwargs)
self.load_settings()

@classmethod
def isSupported(cls):
return False # Set to true once the API is functionally supported

@classmethod
def get_settings_widget(cls):
return <ApiName>Widget()

def load_settings(self):
app_instance = App.get_running_app()
# Update the internal settings state for the API here
# Example: token = app_instance.global_settings.get_setting(self.api_name, "example_setting")

def save_settings(self):
app_instance = App.get_running_app()
# Save the internal settings state from the API here
# Example: app_instance.global_settings.update_setting(self.api_name, "example_setting", self.example_setting)

class <ApiName>():
# The API implementation goes here
```

4. Update `<api_name>.kv` with the following content:

```kivy
<ApiName>Widget:
# Add the settings view for your specific API here
# Example: TextInput:
# text: root.example_setting
# on_text_validate: root.save_settings()
```
Empty file added src/api/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions src/api/base_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from abc import ABC, abstractmethod
from kivy.event import EventDispatcher
from kivy.clock import Clock

class BaseApiSettings(ABC, EventDispatcher):
_instance = None

@classmethod
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(BaseApiSettings, cls).__new__(cls)
return cls._instance

def __init__(self, **kwargs):
super(BaseApiSettings, self).__init__(**kwargs)
Clock.schedule_once(lambda dt: self.load_settings, 1.5) # Do an initial load of settings

@classmethod
@abstractmethod
def isSupported(cls):
"""
This property must be overridden in derived classes.
It should return a boolean indicating if the API is functionally supported yet.
"""
pass

@classmethod
@abstractmethod
def get_settings_widget(cls):
"""
This method must be overridden in derived classes.
It should return the widget that will be displayed in the settings popup.
"""
pass

@abstractmethod
def load_settings(self):
"""
This method must be overridden in derived classes.
It should load the API specific settings into the application.
"""
pass

@abstractmethod
def save_settings(self):
"""
This method must be overridden in derived classes.
It should return the settings from the API in JSON format.
"""
pass
Empty file.
26 changes: 26 additions & 0 deletions src/api/elevenlabsapi/elevenlabsapi.kv
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<ElevenLabsWidget>:
orientation: 'vertical'
api_key_input: api_key_input
voice_selection: voice_name_spinner
model_selection: model_spinner

Label:
text: 'ElevenLabs API Key:'
TextInput:
id: api_key_input
text: ''
multiline: False

Label:
text: 'Voice Name:'
Spinner:
id: voice_name_spinner
text: root.voice_names[0] if root.voice_names else 'No voices available'
values: root.voice_names

Label:
text: 'Model:'
Spinner:
id: model_spinner
text: root.model_names[0] if root.model_names else 'No models available'
values: root.model_names
Loading

0 comments on commit 9d94842

Please sign in to comment.