Skip to content

Commit

Permalink
feat:autoconfigure language (#158)
Browse files Browse the repository at this point in the history
* feat:autoconfigure language

* help text

* error logs

* split prefs from stt into base config

* remove comments

* remove comments

* remove extra newlines

* remove extra newlines

* print time_format

* do not require ovos-utils

* ovos-plugin-manager validation

* more voices

* more voices

* en-au

* Update ovos_config/__main__.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* granularity in unit preferences

* better OPM validation

* remove bad comments

* remove bad comments

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
JarbasAl and coderabbitai[bot] authored Sep 25, 2024
1 parent fef4bee commit ede6243
Show file tree
Hide file tree
Showing 79 changed files with 875 additions and 29 deletions.
169 changes: 151 additions & 18 deletions ovos_config/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#!/bin/env python3
import json
import os.path
from typing import Any, Tuple

import rich_click as click
from rich import print_json
from rich.console import Console
from rich.prompt import Prompt
from rich.table import Table
Expand All @@ -19,19 +22,18 @@

def drawTable(dic: dict, table: Table, level: int = 0) -> None:
for key, value in dic.items():
s = f'{key:>{4*level+len(key)}}'
s = f'{key:>{4 * level + len(key)}}'
if not isinstance(value, dict):
table.add_row(s, str(value))
else:
if level == 0:
table.add_section()
table.add_row(f'[red]{s}[/red]')
drawTable(value, table, level+1)
drawTable(value, table, level + 1)
return


def dictDepth(dic: dict, level: int = 1) -> int:

if not isinstance(dic, dict) or not dic:
return level
return max(dictDepth(dic[key], level + 1)
Expand All @@ -47,18 +49,18 @@ def walkDict(dic: dict,
if key.lower() in k.lower():
found = True
if not full_path:
yield path+(k,), dic[k]
yield path + (k,), dic[k]
found = False

# endpoint
if type(dic[k]) != dict:
if found:
yield path+(k,), dic[k]
yield path + (k,), dic[k]
else:
yield from walkDict(dic[k],
key,
full_path,
path+(k,),
path + (k,),
found)
found = False

Expand Down Expand Up @@ -115,6 +117,7 @@ def pathSet(dic: dict, path: str, value: Any) -> None:

console = Console()


@click.group()
def config():
"""\b
Expand All @@ -126,11 +129,140 @@ def config():
pass


@config.command()
@click.option("--lang", "-l", required=True, help="the language code")
@click.option("--online", "-on", is_flag=True, help="set default online STT plugin")
@click.option("--offline", "-off", is_flag=True, help="set default offline STT plugin")
@click.option("--male", "-m", is_flag=True, help="set default male voice for TTS")
@click.option("--female", "-f", is_flag=True, help="set default female voice for TTS")
def autoconfigure(lang, online, offline, male, female):
"""
Automatically configures the language, STT, and TTS settings based on user input.
sets up configurations for language, online or offline speech-to-text, and male or female text-to-speech voice options.
ensures that only one of the mutually exclusive options (online/offline and male/female) is selected, and merges the appropriate configuration files for the selected options.
Notes:
- If neither `online` nor `offline` are provided, defaults to `online`.
- If neither `male` nor `female` are provided, TTS configuration is skipped.
- The function merges configuration files based on the specified options and stores the final configuration in the user's config file.
"""
if not online and not offline:
console.print("[red]Defaulting to online public servers[/red]")
online = True

if online and offline:
raise click.UsageError("Pass either --online or --offline, not both")
if male and female:
raise click.UsageError("Pass either --male or --female, not both")

if not male and not female:
console.print("[red]Skipping TTS configuration, pass '--male' or '--female' to set language defaults[/red]")

try:
from ovos_utils.lang import standardize_lang_tag
stdlang = standardize_lang_tag(lang, macro=True)
console.print(f"[blue]Standardized lang-code:[/blue] {stdlang}")
except ImportError:
stdlang = lang
console.print(f"[red]ERROR: Failed to standardize lang tag, please install latest 'ovos-utils' package[/red]")

config = LocalConf(USER_CONFIG)
config["tts"] = {"ovos-tts-plugin-server": {}}
config["stt"] = {"ovos-stt-plugin-server": {}}

def do_merge(folder):
l2 = stdlang.split("-")[0]
recs_path = f"{os.path.dirname(__file__)}/recommends"
path = f"{recs_path}/{folder}/{lang.lower()}.conf"
if not os.path.isfile(path):
paths = [f"{recs_path}/{folder}/{f}"
for f in os.listdir(f"{recs_path}/{folder}") if f.startswith(l2)]
if paths:
path = paths[0]

if not os.path.isfile(path):
console.print(f"[red]ERROR: {folder} not available for {stdlang}[/red]")
return

c = LocalConf(path)
config.merge(c)
console.print(f"Merged config: {c.path}")

do_merge("base")
if offline:
do_merge("offline_stt")
if male:
do_merge("offline_male")
elif female:
do_merge("offline_female")

elif online:
config["tts"]["module"] = "ovos-tts-plugin-server"
config["stt"]["module"] = "ovos-stt-plugin-server"
do_merge("online_stt")
if male:
do_merge("online_male")
elif female:
do_merge("online_female")

config["lang"] = stdlang

try:
from ovos_plugin_manager.stt import find_stt_plugins
from ovos_plugin_manager.tts import find_tts_plugins

available_stt = list(find_stt_plugins().keys())
available_tts = list(find_tts_plugins().keys())

console.print("[blue]Available STT plugins:[/blue]")
for plugin in available_stt:
console.print(f" - '{plugin}'")
console.print("[blue]Available TTS plugins:[/blue]")
for plugin in available_tts:
console.print(f" - '{plugin}'")

missing_plugins = []
if config["stt"]["module"] not in available_stt:
missing_plugins.append(f"STT plugin '{config['stt']['module']}'")
if config["tts"]["module"] not in available_tts:
missing_plugins.append(f"TTS plugin '{config['tts']['module']}'")

if missing_plugins:
console.print("[yellow]WARNING: The following plugins are missing:[/yellow]")
for plugin in missing_plugins:
console.print(f" - {plugin}")
console.print("Please install the missing plugins using 'pip install <plugin_name>'")
except ImportError:
console.print("[yellow]WARNING: 'ovos-plugin-manager' not installed. Skipping plugin validation.[/yellow]")
console.print(
"To enable plugin validation, install 'ovos-plugin-manager' using 'pip install ovos-plugin-manager'")

config.store()
console.print(f"Config updated: {config.path}")

print_json(json.dumps({k: v for k, v in config.items()
if k in ["lang",
"tts", "stt",
"system_unit",
"temperature_unit",
"windspeed_unit",
"precipitation_unit",
"date_format",
"time_format",
"spoken_time_format"]}))


@config.command()
@click.option("--user", "-u", is_flag=True, help="User Configuration")
@click.option("--system", "-s", is_flag=True, help="System Configuration")
@click.option("--remote", "-r", is_flag=True, help="Remote Configuration")
@click.option("--section", default="", show_default=False, help="Choose a specific section from the underlying configuration")
@click.option("--section", default="", show_default=False,
help="Choose a specific section from the underlying configuration")
@click.option("--list-sections", "-l", is_flag=True, help="List the sections based on the underlying configuration")
def show(user, system, remote, section, list_sections):
"""\b
Expand All @@ -155,7 +287,6 @@ def show(user, system, remote, section, list_sections):
name, config = CONFIGS[2]
elif remote:
name, config = CONFIGS[3]


# based on chosen configuration
if name != "Joined":
Expand All @@ -165,7 +296,7 @@ def show(user, system, remote, section, list_sections):
else:
_sections = SECTIONS

if list_sections:
if list_sections:
console.print(f"Available sections ({name} config): " + " ".join(_sections))
exit()

Expand All @@ -178,7 +309,7 @@ def show(user, system, remote, section, list_sections):
# based on chosen configuration
elif section not in _sections:
found_in = [f"`{_name}`" for _name, _config in CONFIGS
if section in _config and _name != name]
if section in _config and _name != name]
console.print(f"The section `{section}` doesn't exist in the {name} "
f"Configuration. It is part of the {'/'.join(found_in)} "
"Configuration though")
Expand All @@ -190,17 +321,18 @@ def show(user, system, remote, section, list_sections):
else:
# sorted dict based on depth
_config = {key: value for key, value in
sorted(config.items(), key=lambda item: dictDepth(item[1]))}
sorted(config.items(), key=lambda item: dictDepth(item[1]))}

section_info = f", Section: {section}" if section else ""
additional_info = f"(Configuration: {name}{section_info})"

table = Table(show_header=True, header_style="bold red")
table.add_column(f"Configuration keys {additional_info}", min_width=60)
table.add_column("Value", min_width=20)

drawTable(_config, table)
console.print(table)
console.print(table)


@config.command()
@click.option("--key", "-k", required=True, help="the key (or parts thereof) which should be searched")
Expand Down Expand Up @@ -258,7 +390,7 @@ def set(key, value):
tuples = list(walkDict(CONFIG, key, full_path=True))
values = [tup[1] for tup in tuples]
paths = ["/".join(tup[0]) for tup in tuples]

if len(paths) > 1:
table = Table(show_header=True, header_style="bold red")
table.add_column("#")
Expand All @@ -271,7 +403,7 @@ def set(key, value):

exit_ = str(len(paths))
choice = Prompt.ask(f"Which value should be changed? ({exit_}='Exit')",
choices=[str(i) for i in range(0, len(paths)+1)])
choices=[str(i) for i in range(0, len(paths) + 1)])
if choice == exit_:
console.print("User exit", style="red")
exit()
Expand All @@ -287,11 +419,11 @@ def set(key, value):
# to not irritate the use to suggest typing `["xyz"]`
if selected_type == "list":
selected_type = "str"

if not value:
value = Prompt.ask(("Please enter the value to be stored "
f"(type: [red]{selected_type}[/red]) "))
value = value.replace('"','').replace("'","").replace("`","")
value = value.replace('"', '').replace("'", "").replace("`", "")

local_conf = CONFIGS[2][1]
_value = None
Expand All @@ -313,7 +445,7 @@ def set(key, value):
_value = list()
console.print(("Note: defining lists in the user config "
"will override subsequent list configurations"),
style="grey53")
style="grey53")
_value.append(value)
elif isinstance(selected_value, int):
_value = int(value)
Expand All @@ -326,5 +458,6 @@ def set(key, value):
pathSet(local_conf, selected_path, _value)
local_conf.store()


if __name__ == "__main__":
config()
18 changes: 7 additions & 11 deletions ovos_config/mycroft.conf
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,22 @@
"secondary_langs": [],

// Measurement units, either 'metric' or 'imperial'
// Override: REMOTE
"system_unit": "metric",
// Temperature units, either 'celsius' or 'fahrenheit'
"temperature_unit": "celsius",
// Windspeed units, either 'km/h', 'm/s', 'mph' or 'kn' (knots)
"windspeed_unit": "m/s",
// Precipitation units, either 'mm' or 'inch'
"precipitation_unit": "mm",

// Time format, either 'half' (e.g. "11:37 pm") or 'full' (e.g. "23:37")
// Override: REMOTE
"time_format": "half",
"spoken_time_format": "full",

// Date format, either 'MDY' (e.g. "11-29-1978") or 'DMY' (e.g. "29-11-1978")
// Override: REMOTE
"date_format": "MDY",

// Whether to opt in to data collection
// Override: REMOTE
"opt_in": false,

// Play a beep when system begins to listen?
Expand All @@ -55,23 +58,19 @@

// Mechanism used to play WAV audio files
// by default ovos-utils will try to detect best player
// Override: SYSTEM
//"play_wav_cmdline": "paplay %1 --stream-name=mycroft-voice",

// Mechanism used to play MP3 audio files
// by default ovos-utils will try to detect best player
// Override: SYSTEM
//"play_mp3_cmdline": "mpg123 %1",

// Mechanism used to play OGG audio files
// by default ovos-utils will try to detect best player
// Override: SYSTEM
//"play_ogg_cmdline": "ogg123 -q %1",

// Location where the system resides
// NOTE: Although this is set here, an Enclosure can override the value.
// For example a mycroft-core running in a car could use the GPS.
// Override: REMOTE
"location": {
"city": {
"code": "Lawrence",
Expand Down Expand Up @@ -447,7 +446,6 @@
},

// Settings used by the wake-up-word listener
// Override: REMOTE
"listener": {
"sample_rate": 16000,

Expand Down Expand Up @@ -693,7 +691,6 @@
},

// Speech to Text parameters
// Override: REMOTE
"stt": {
// select a STT plugin as described in the respective readme
// ovos-stt-plugin-server default plugin has a bundled list of public whisper instances
Expand All @@ -703,7 +700,6 @@
},

// Text to Speech parameters
// Override: REMOTE
"tts": {
"pulse_duck": false,
// ovos tts server piper public servers by default, alan pope voice
Expand Down
9 changes: 9 additions & 0 deletions ovos_config/recommends/base/ca-es.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"system_unit": "metric",
"temperature_unit": "celsius",
"windspeed_unit": "km/h",
"precipitation_unit": "mm",
"time_format": "full",
"spoken_time_format": "half",
"date_format": "DMY"
}
9 changes: 9 additions & 0 deletions ovos_config/recommends/base/de-de.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"system_unit": "metric",
"temperature_unit": "celsius",
"windspeed_unit": "km/h",
"precipitation_unit": "mm",
"time_format": "full",
"spoken_time_format": "full",
"date_format": "DMY"
}
Loading

0 comments on commit ede6243

Please sign in to comment.