mirror of
https://github.com/TaterTotterson/microWakeWord-Trainer-Nvidia-Docker.git
synced 2026-08-12 16:05:34 -06:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13f229e451 | ||
|
|
68c4227cb7 |
@@ -1,2 +1 @@
|
||||
- Prevented unreadable audio files from freezing sample preparation by adding a bounded FFmpeg watchdog, safe cleanup, and visible normalization progress.
|
||||
- Kept primary and guided STT results visible after captures are automatically sorted into positive or negative training samples.
|
||||
- Added English accent emphasis for Mixed English, Australian, American, British, Canadian, Irish, Scottish, New Zealand, Indian, and South African voices. Qwen shapes the selected accent and MOSS carries it into cloned references, with the setting available in both manual and automatic training.
|
||||
|
||||
@@ -12,6 +12,7 @@ DEFAULT_SAMPLES=50000
|
||||
DEFAULT_BATCH_SIZE=100
|
||||
DEFAULT_TRAINING_STEPS=40000
|
||||
DEFAULT_LANGUAGE=en
|
||||
DEFAULT_ENGLISH_ACCENT=mixed
|
||||
DEFAULT_TTS_MODE=hybrid
|
||||
DEFAULT_TTS_VOICE_COUNT=128
|
||||
|
||||
@@ -21,6 +22,7 @@ DEFAULT_TTS_VOICE_COUNT=128
|
||||
: "${BATCH_SIZE:=${DEFAULT_BATCH_SIZE}}"
|
||||
: "${TRAINING_STEPS:=${DEFAULT_TRAINING_STEPS}}"
|
||||
: "${LANGUAGE:=${DEFAULT_LANGUAGE}}"
|
||||
: "${ENGLISH_ACCENT:=${DEFAULT_ENGLISH_ACCENT}}"
|
||||
: "${TTS_MODE:=${DEFAULT_TTS_MODE}}"
|
||||
: "${TTS_VOICE_COUNT:=${DEFAULT_TTS_VOICE_COUNT}}"
|
||||
: "${CLEANUP_WORK_DIR:=false}"
|
||||
|
||||
@@ -31,20 +31,24 @@ if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from tts_config import ( # noqa: E402
|
||||
DEFAULT_ENGLISH_ACCENT,
|
||||
DEFAULT_TTS_MODE,
|
||||
ENGLISH_ACCENTS,
|
||||
ENGINE_MOSS,
|
||||
ENGINE_OMNIVOICE,
|
||||
ENGINE_PIPER,
|
||||
ENGINE_QWEN3,
|
||||
MIXED_ENGLISH_ACCENTS,
|
||||
QWEN_LANGUAGE_NAMES,
|
||||
distribute_samples,
|
||||
engines_for_language,
|
||||
language_for_engine,
|
||||
normalize_english_accent,
|
||||
normalize_tts_mode,
|
||||
)
|
||||
|
||||
|
||||
GENERATOR_VERSION = "modern-tts-v15-four-provider-direct-corpus-safe-limits"
|
||||
GENERATOR_VERSION = "modern-tts-v17-four-provider-direct-corpus-safe-limits-english-accent-emphasis"
|
||||
VOICE_BANK_VERSION = "modern-tts-voice-bank-v1-native-random-qualified-single-utterance"
|
||||
COMPATIBLE_VOICE_BANK_VERSIONS = {
|
||||
VOICE_BANK_VERSION,
|
||||
@@ -199,7 +203,11 @@ def stable_prompt_text(phrase: str, language: str = "en") -> str:
|
||||
return clean + "."
|
||||
|
||||
|
||||
def qwen_descriptions(language_name: str, count: int) -> list[str]:
|
||||
def qwen_descriptions(
|
||||
language_name: str,
|
||||
count: int,
|
||||
english_accent: str = DEFAULT_ENGLISH_ACCENT,
|
||||
) -> list[str]:
|
||||
genders = ("female", "male")
|
||||
ages = ("child", "teenager", "young adult", "middle-aged adult", "elderly adult")
|
||||
pitches = ("low pitch", "medium pitch", "high pitch")
|
||||
@@ -215,16 +223,30 @@ def qwen_descriptions(language_name: str, count: int) -> list[str]:
|
||||
weights = ("light", "balanced", "compact", "full-bodied", "resonant")
|
||||
combinations = list(product(genders, ages, pitches, deliveries, textures, paces, weights))
|
||||
descriptions = []
|
||||
accent_cycle: tuple[str, ...] = ()
|
||||
if language_name == "English":
|
||||
selected_accent = normalize_english_accent(english_accent, "en")
|
||||
accent_cycle = (
|
||||
MIXED_ENGLISH_ACCENTS
|
||||
if selected_accent == DEFAULT_ENGLISH_ACCENT
|
||||
else (selected_accent,)
|
||||
)
|
||||
# Walking the Cartesian product sequentially clusters the leading traits
|
||||
# (the first 375 combinations are all female). A coprime stride retains a
|
||||
# deterministic, non-repeating order while balancing every trait early.
|
||||
for index in range(count):
|
||||
combination_index = (index * VOICE_PROFILE_STRIDE) % len(combinations)
|
||||
gender, age, pitch, delivery, texture, pace, weight = combinations[combination_index]
|
||||
language_style = f"native {language_name}"
|
||||
if accent_cycle:
|
||||
selected_accent = accent_cycle[index % len(accent_cycle)]
|
||||
language_style = (
|
||||
f"English with a natural {ENGLISH_ACCENTS[selected_accent]} accent"
|
||||
)
|
||||
descriptions.append(
|
||||
f"A distinct {age} {gender} speaker with a {texture} timbre, "
|
||||
f"{pitch}, {weight} vocal weight, and {delivery}, speaking native "
|
||||
f"{language_name} at a {pace} pace. Say only the supplied text once."
|
||||
f"{pitch}, {weight} vocal weight, and {delivery}, speaking "
|
||||
f"{language_style} at a {pace} pace. Say only the supplied text once."
|
||||
)
|
||||
return descriptions
|
||||
|
||||
@@ -283,6 +305,10 @@ def valid_sample(path: Path) -> bool:
|
||||
class Generator:
|
||||
def __init__(self, args: argparse.Namespace):
|
||||
self.args = args
|
||||
self.english_accent = normalize_english_accent(
|
||||
getattr(args, "english_accent", DEFAULT_ENGLISH_ACCENT),
|
||||
args.language,
|
||||
)
|
||||
self.spoken_phrase = args.phrase.replace("_", " ").strip()
|
||||
self.data_dir = args.data_dir.resolve()
|
||||
self.output_dir = args.output_dir.resolve()
|
||||
@@ -339,6 +365,7 @@ class Generator:
|
||||
"generator_version": GENERATOR_VERSION,
|
||||
"phrase": self.args.phrase,
|
||||
"language": self.args.language,
|
||||
"english_accent": self.english_accent,
|
||||
"tts_mode": self.args.tts_mode,
|
||||
"samples": self.args.samples,
|
||||
"engines": engines,
|
||||
@@ -1064,7 +1091,11 @@ class Generator:
|
||||
self.direct_attempt[engine] += count
|
||||
rng = random.Random(24051984 + start + sum(ord(ch) for ch in engine + prefix))
|
||||
descriptions = (
|
||||
qwen_descriptions(QWEN_LANGUAGE_NAMES[self.args.language], start + count)[start:]
|
||||
qwen_descriptions(
|
||||
QWEN_LANGUAGE_NAMES[self.args.language],
|
||||
start + count,
|
||||
self.english_accent,
|
||||
)[start:]
|
||||
if engine == ENGINE_QWEN3
|
||||
else []
|
||||
)
|
||||
@@ -1360,6 +1391,8 @@ class Generator:
|
||||
self.final_dir.mkdir(parents=True, exist_ok=True)
|
||||
plan = distribute_samples(self.args.samples, engines)
|
||||
log(f"===== Direct TTS corpus plan ({self.args.tts_mode}, {self.args.language}) =====")
|
||||
if self.args.language == "en" and ENGINE_QWEN3 in plan:
|
||||
log(f" English accent emphasis: {self.english_accent}")
|
||||
for engine, count in plan.items():
|
||||
log(f" {engine}: {count} sample(s)")
|
||||
log(
|
||||
@@ -1447,6 +1480,7 @@ class Generator:
|
||||
"reusable_profile_bank": False,
|
||||
"moss_unique_accepted_carriers": True,
|
||||
"piper_all_model_speakers": True,
|
||||
"english_accent_emphasis": self.english_accent,
|
||||
},
|
||||
"qa": {
|
||||
"audio_format": "16 kHz mono PCM16 WAV",
|
||||
@@ -1475,6 +1509,10 @@ def parser() -> argparse.ArgumentParser:
|
||||
result = argparse.ArgumentParser()
|
||||
result.add_argument("phrase")
|
||||
result.add_argument("--language", default="en")
|
||||
result.add_argument(
|
||||
"--english-accent",
|
||||
default=os.environ.get("MWW_ENGLISH_ACCENT", DEFAULT_ENGLISH_ACCENT),
|
||||
)
|
||||
result.add_argument("--tts-mode", default=DEFAULT_TTS_MODE)
|
||||
result.add_argument("--samples", type=int, default=50000)
|
||||
result.add_argument("--batch-size", type=int, default=8)
|
||||
@@ -1494,6 +1532,7 @@ def parser() -> argparse.ArgumentParser:
|
||||
def main() -> int:
|
||||
args = parser().parse_args()
|
||||
args.language = args.language.strip().lower().replace("-", "_")
|
||||
args.english_accent = normalize_english_accent(args.english_accent, args.language)
|
||||
args.tts_mode = normalize_tts_mode(args.tts_mode)
|
||||
if args.samples < 1:
|
||||
raise SystemExit("--samples must be positive")
|
||||
|
||||
@@ -69,7 +69,6 @@ def main() -> int:
|
||||
text=str(item["text"]),
|
||||
output_audio_path=str(output_path),
|
||||
mode="voice_clone",
|
||||
prompt_text=str(item["ref_text"]),
|
||||
prompt_audio_path=str(item["ref_audio"]),
|
||||
reference_audio_path=None,
|
||||
text_tokenizer_path=None,
|
||||
|
||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
||||
PROGPATH="$(realpath "$0")"
|
||||
PROGDIR="$(dirname "${PROGPATH}")"
|
||||
|
||||
KNOWN_ARGS=( samples batch-size data-dir language tts-mode tts-voice-count )
|
||||
KNOWN_ARGS=( samples batch-size data-dir language english-accent tts-mode tts-voice-count )
|
||||
# shellcheck source=/dev/null
|
||||
source "${PROGDIR}/shell.functions"
|
||||
WAKE_WORD="${POSITIONAL_ARGS[0]:-}"
|
||||
@@ -17,12 +17,14 @@ fi
|
||||
if [ "${HELP}" == "true" ] || [ -z "${WAKE_WORD}" ] ; then
|
||||
cat <<EOF >&2
|
||||
Usage: $0 [ --samples=<samples> ] [ --batch-size=<batch_size> ]
|
||||
[ --language=<lang> ] [ --tts-mode=<modern|hybrid|piper> ]
|
||||
[ --language=<lang> ] [ --english-accent=<accent> ]
|
||||
[ --tts-mode=<modern|hybrid|piper> ]
|
||||
[ --tts-voice-count=<voices> ] <wake_word>
|
||||
|
||||
--samples: Number of samples to generate. Default: ${DEFAULT_SAMPLES}
|
||||
--batch-size: Generation batch size. Default: ${DEFAULT_BATCH_SIZE}
|
||||
--language: TTS language code. Default: ${DEFAULT_LANGUAGE}
|
||||
--english-accent: English accent emphasis. Default: ${DEFAULT_ENGLISH_ACCENT}
|
||||
--tts-mode: modern, hybrid, or piper. Default: ${DEFAULT_TTS_MODE}
|
||||
--tts-voice-count: Deprecated compatibility option; direct generation ignores it.
|
||||
<wake_word> Required phrase to synthesize.
|
||||
@@ -38,17 +40,31 @@ case "${TTS_MODE}" in
|
||||
;;
|
||||
esac
|
||||
|
||||
LANGUAGE="$(echo "${LANGUAGE}" | tr '[:upper:]' '[:lower:]')"
|
||||
ENGLISH_ACCENT="$(echo "${ENGLISH_ACCENT}" | tr '[:upper:] -' '[:lower:]__')"
|
||||
if [ "${LANGUAGE}" != "en" ]; then
|
||||
ENGLISH_ACCENT="mixed"
|
||||
fi
|
||||
case "${ENGLISH_ACCENT}" in
|
||||
mixed|australian|american|british|canadian|irish|scottish|new_zealand|indian|south_african) ;;
|
||||
*)
|
||||
echo "ERROR: unsupported --english-accent '${ENGLISH_ACCENT}'." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
WORK_DIR="${DATA_DIR}/work"
|
||||
SAMPLES_DIR="${WORK_DIR}/wake_word_samples"
|
||||
mkdir -p "${WORK_DIR}"
|
||||
|
||||
START_TS=$EPOCHSECONDS
|
||||
echo "===== Generating ${SAMPLES} wake-word samples (language=${LANGUAGE}, tts=${TTS_MODE}) ====="
|
||||
echo "===== Generating ${SAMPLES} wake-word samples (language=${LANGUAGE}, accent=${ENGLISH_ACCENT}, tts=${TTS_MODE}) ====="
|
||||
|
||||
python3 "${PROGDIR}/tts_generate_samples.py" "${WAKE_WORD}" \
|
||||
--samples="${SAMPLES}" \
|
||||
--batch-size="${BATCH_SIZE}" \
|
||||
--language="${LANGUAGE}" \
|
||||
--english-accent="${ENGLISH_ACCENT}" \
|
||||
--tts-mode="${TTS_MODE}" \
|
||||
--voice-count="${TTS_VOICE_COUNT}" \
|
||||
--data-dir="${DATA_DIR}" \
|
||||
|
||||
@@ -70,7 +70,7 @@ const dataCategories = computed(() => {
|
||||
return Array.from(groups, ([name, items]) => ({ name, items }));
|
||||
});
|
||||
|
||||
watch(() => trainer.language, ensureSupportedTtsMode);
|
||||
watch([() => trainer.language, () => trainer.ttsMode], ensureSupportedTtsMode);
|
||||
watch(() => trainer.toast.serial, () => window.setTimeout(() => { trainer.toast.message = ""; }, 4500));
|
||||
watch(consoleLines, async () => {
|
||||
if (!consoleFollowing.value) return;
|
||||
@@ -197,6 +197,7 @@ function consoleTone(line: string): string {
|
||||
<div class="form-grid phrase-form">
|
||||
<label class="field wide"><span>Wake phrase</span><input v-model="trainer.phrase" type="text" placeholder='e.g. "hey tater"' :disabled="Boolean(trainer.session.safe_word) || isBusy('session')" @keyup.enter="startSession" /></label>
|
||||
<label class="field"><span>Language</span><select v-model="trainer.language" :disabled="Boolean(trainer.session.safe_word) || isBusy('session')"><option v-for="item in trainer.languages" :key="item.code" :value="item.code">{{ item.label }}</option></select><small>{{ ttsRoute }}</small></label>
|
||||
<label v-if="trainer.language === 'en' && trainer.ttsMode !== 'piper'" class="field"><span>English accent emphasis</span><select v-model="trainer.englishAccent" :disabled="Boolean(trainer.session.safe_word) || isBusy('session')"><option v-for="accent in trainer.englishAccents" :key="accent.code" :value="accent.code">{{ accent.label }}</option></select><small>Qwen emphasizes this accent; MOSS carries it through accepted references. OmniVoice and Piper keep broad English coverage.</small></label>
|
||||
<label class="field"><span>TTS source</span><select v-model="trainer.ttsMode" :disabled="Boolean(trainer.session.safe_word) || isBusy('session')">
|
||||
<option value="hybrid" :disabled="!trainer.languages.find((item) => item.code === trainer.language)?.engines?.includes('piper')">Four-provider ensemble · recommended</option>
|
||||
<option value="modern" :disabled="!trainer.languages.find((item) => item.code === trainer.language)?.engines?.some((engine) => engine !== 'piper')">Modern only · no Piper</option>
|
||||
@@ -224,6 +225,7 @@ function consoleTone(line: string): string {
|
||||
<div class="form-grid">
|
||||
<label class="field"><span>Wake phrase</span><input v-model="trainer.autoForm.wake_phrase" type="text" /></label>
|
||||
<label class="field"><span>STT language</span><input v-model="trainer.autoForm.language" type="text" /></label>
|
||||
<label v-if="String(trainer.autoForm.language).toLowerCase().startsWith('en')" class="field"><span>English accent emphasis</span><select v-model="trainer.autoForm.english_accent"><option v-for="accent in trainer.englishAccents" :key="accent.code" :value="accent.code">{{ accent.label }}</option></select><small>Used when Auto Training needs to regenerate English TTS.</small></label>
|
||||
<label class="field wide"><span>STT engine</span><select v-model="trainer.autoForm.stt_engine"><option v-for="engine in sttEngines" :key="engine.id || engine.value" :value="engine.id || engine.value">{{ engine.label || engine.name || engine.id }}</option></select><small>{{ sttEngines.find((row) => (row.id || row.value) === trainer.autoForm.stt_engine)?.description || "Runs locally on this trainer." }}</small></label>
|
||||
<label class="field"><span>Minimum transcript characters</span><input v-model.number="trainer.autoForm.minimum_transcript_chars" min="1" max="100" type="number" /></label>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed, reactive } from "vue";
|
||||
import { getJson, postJson, putJson, request, type JsonRecord } from "./api";
|
||||
import type {
|
||||
AccentOption,
|
||||
AudioItem,
|
||||
AutoTrainForm,
|
||||
AutoTrainPayload,
|
||||
@@ -26,6 +27,7 @@ const defaultAutoForm = (): AutoTrainForm => ({
|
||||
enabled: false,
|
||||
wake_phrase: "",
|
||||
language: "en",
|
||||
english_accent: "mixed",
|
||||
stt_engine: "faster_whisper",
|
||||
minimum_transcript_chars: 2,
|
||||
delete_confirmed_wakes: false,
|
||||
@@ -43,8 +45,21 @@ export const trainer = reactive({
|
||||
busy: new Set<string>(),
|
||||
phrase: "",
|
||||
language: "en",
|
||||
englishAccent: "mixed",
|
||||
ttsMode: "hybrid",
|
||||
languages: [{ code: "en", label: "English (en)", engines: ["omnivoice"] }] as LanguageOption[],
|
||||
englishAccents: [
|
||||
{ code: "mixed", label: "Mixed English" },
|
||||
{ code: "australian", label: "Australian" },
|
||||
{ code: "american", label: "American" },
|
||||
{ code: "british", label: "British" },
|
||||
{ code: "canadian", label: "Canadian" },
|
||||
{ code: "irish", label: "Irish" },
|
||||
{ code: "scottish", label: "Scottish" },
|
||||
{ code: "new_zealand", label: "New Zealand" },
|
||||
{ code: "indian", label: "Indian" },
|
||||
{ code: "south_african", label: "South African" },
|
||||
] as AccentOption[],
|
||||
session: {} as SessionPayload,
|
||||
samples: emptySamples(),
|
||||
captured: emptyCaptured(),
|
||||
@@ -123,8 +138,12 @@ function applySession(payload: SessionPayload): void {
|
||||
if (Array.isArray(payload.available_languages) && payload.available_languages.length) {
|
||||
trainer.languages = payload.available_languages;
|
||||
}
|
||||
if (Array.isArray(payload.available_english_accents) && payload.available_english_accents.length) {
|
||||
trainer.englishAccents = payload.available_english_accents;
|
||||
}
|
||||
if (payload.raw_phrase) trainer.phrase = payload.raw_phrase;
|
||||
if (payload.language) trainer.language = payload.language;
|
||||
if (payload.english_accent) trainer.englishAccent = payload.english_accent;
|
||||
if (payload.tts_mode) trainer.ttsMode = payload.tts_mode;
|
||||
if (payload.training) trainer.training = payload.training;
|
||||
}
|
||||
@@ -145,6 +164,7 @@ export async function startSession(): Promise<void> {
|
||||
const payload = await postJson<SessionPayload>("/api/start_session", {
|
||||
phrase: trainer.phrase.trim(),
|
||||
language: trainer.language,
|
||||
english_accent: trainer.englishAccent,
|
||||
tts_mode: trainer.ttsMode,
|
||||
});
|
||||
applySession(payload);
|
||||
@@ -193,6 +213,7 @@ export function ensureSupportedTtsMode(): void {
|
||||
if (trainer.ttsMode === "modern" && !modern) trainer.ttsMode = "piper";
|
||||
if (trainer.ttsMode === "hybrid" && !(modern && piper)) trainer.ttsMode = modern ? "modern" : "piper";
|
||||
if (trainer.ttsMode === "piper" && !piper) trainer.ttsMode = "modern";
|
||||
if (trainer.language !== "en" || trainer.ttsMode === "piper") trainer.englishAccent = "mixed";
|
||||
}
|
||||
|
||||
export async function refreshSamples(quiet = false): Promise<SamplesPayload> {
|
||||
@@ -340,6 +361,8 @@ function applyAuto(payload: AutoTrainPayload, populate: boolean): void {
|
||||
trainer.autoForm = { ...defaultAutoForm(), ...(payload.config || {}) };
|
||||
if (!trainer.autoForm.wake_phrase) trainer.autoForm.wake_phrase = trainer.session.raw_phrase || "";
|
||||
if (!trainer.autoForm.language) trainer.autoForm.language = trainer.session.language || "en";
|
||||
if (!trainer.autoForm.english_accent) trainer.autoForm.english_accent = trainer.session.english_accent || "mixed";
|
||||
if (!String(trainer.autoForm.language).toLowerCase().startsWith("en")) trainer.autoForm.english_accent = "mixed";
|
||||
}
|
||||
|
||||
export async function refreshAuto(populate = false): Promise<AutoTrainPayload> {
|
||||
|
||||
@@ -10,6 +10,11 @@ export interface LanguageOption extends JsonRecord {
|
||||
quality?: string;
|
||||
}
|
||||
|
||||
export interface AccentOption extends JsonRecord {
|
||||
code: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface TrainingState extends JsonRecord {
|
||||
running: boolean;
|
||||
exit_code: number | null;
|
||||
@@ -20,9 +25,11 @@ export interface SessionPayload extends JsonRecord {
|
||||
safe_word?: string;
|
||||
raw_phrase?: string;
|
||||
language?: string;
|
||||
english_accent?: string;
|
||||
tts_mode?: string;
|
||||
takes_received?: number;
|
||||
available_languages?: LanguageOption[];
|
||||
available_english_accents?: AccentOption[];
|
||||
training?: TrainingState;
|
||||
}
|
||||
|
||||
@@ -56,6 +63,7 @@ export interface AutoTrainForm extends JsonRecord {
|
||||
enabled: boolean;
|
||||
wake_phrase: string;
|
||||
language: string;
|
||||
english_accent: string;
|
||||
stt_engine: string;
|
||||
minimum_transcript_chars: number;
|
||||
delete_confirmed_wakes: boolean;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
@@ -70,6 +71,44 @@ class ModernTtsTests(unittest.TestCase):
|
||||
["--position_temperature", "5.0", "--class_temperature", "0.0"],
|
||||
)
|
||||
|
||||
def test_qwen_accent_emphasis_supports_specific_and_mixed_english(self) -> None:
|
||||
australian = generator_module.qwen_descriptions("English", 4, "australian")
|
||||
self.assertTrue(all("natural Australian accent" in row for row in australian))
|
||||
|
||||
mixed = generator_module.qwen_descriptions("English", 9, "mixed")
|
||||
for label in (
|
||||
"Australian",
|
||||
"American",
|
||||
"British",
|
||||
"Canadian",
|
||||
"Irish",
|
||||
"Scottish",
|
||||
"New Zealand",
|
||||
"Indian",
|
||||
"South African",
|
||||
):
|
||||
self.assertTrue(any(f"natural {label} accent" in row for row in mixed))
|
||||
|
||||
german = generator_module.qwen_descriptions("German", 1, "australian")
|
||||
self.assertIn("speaking native German", german[0])
|
||||
self.assertNotIn("accent", german[0])
|
||||
|
||||
def test_moss_voice_clone_uses_audio_without_disallowed_prompt_text(self) -> None:
|
||||
worker_path = REPO_ROOT / "cli" / "tts_moss_worker.py"
|
||||
tree = ast.parse(worker_path.read_text(encoding="utf-8"))
|
||||
inference_calls = [
|
||||
node
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.Call)
|
||||
and isinstance(node.func, ast.Attribute)
|
||||
and node.func.attr == "inference"
|
||||
]
|
||||
|
||||
self.assertEqual(len(inference_calls), 1)
|
||||
keywords = {keyword.arg for keyword in inference_calls[0].keywords}
|
||||
self.assertIn("prompt_audio_path", keywords)
|
||||
self.assertNotIn("prompt_text", keywords)
|
||||
|
||||
def test_omnivoice_uses_a_hidden_stable_prompt_before_short_clone(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
data_dir = Path(temp_dir)
|
||||
|
||||
@@ -10,6 +10,7 @@ from tts_config import (
|
||||
distribute_samples,
|
||||
engines_for_language,
|
||||
language_for_engine,
|
||||
normalize_english_accent,
|
||||
normalize_tts_mode,
|
||||
quality_for_engines,
|
||||
)
|
||||
@@ -51,6 +52,12 @@ class TtsConfigTests(unittest.TestCase):
|
||||
def test_invalid_mode_falls_back_to_four_provider_route(self) -> None:
|
||||
self.assertEqual(normalize_tts_mode("unknown"), "hybrid")
|
||||
|
||||
def test_english_accent_aliases_and_non_english_fallback(self) -> None:
|
||||
self.assertEqual(normalize_english_accent("Australia", "en"), "australian")
|
||||
self.assertEqual(normalize_english_accent("new-zealand", "en_US"), "new_zealand")
|
||||
self.assertEqual(normalize_english_accent("unknown", "en"), "mixed")
|
||||
self.assertEqual(normalize_english_accent("australian", "fr"), "mixed")
|
||||
|
||||
def test_common_language_aliases_use_model_catalog_ids(self) -> None:
|
||||
self.assertEqual(language_for_engine(ENGINE_OMNIVOICE, "ar"), "arb")
|
||||
self.assertEqual(language_for_engine(ENGINE_OMNIVOICE, "ne"), "npi")
|
||||
|
||||
@@ -5,7 +5,7 @@ PROGPATH=$(realpath "$0")
|
||||
PROGDIR=$(dirname "${PROGPATH}")
|
||||
CLIDIR="${PROGDIR}/cli"
|
||||
|
||||
KNOWN_ARGS=( samples batch-size training-steps data-dir cleanup-work-dir language tts-mode tts-voice-count )
|
||||
KNOWN_ARGS=( samples batch-size training-steps data-dir cleanup-work-dir language english-accent tts-mode tts-voice-count )
|
||||
source "${CLIDIR}/shell.functions"
|
||||
WAKE_WORD=${POSITIONAL_ARGS[0]}
|
||||
|
||||
@@ -19,6 +19,7 @@ if [ "${HELP}" == "true" ] || [ -z "${WAKE_WORD}" ] ; then
|
||||
Usage: train_wake_word [ --samples=<samples> ] [ --batch-size=<batch_size> ]
|
||||
[ --training-steps=<steps> ] [ --cleanup-work-dir ]
|
||||
[ --language=<lang> ]
|
||||
[ --english-accent=<accent> ]
|
||||
[ --tts-mode=<modern|hybrid|piper> ]
|
||||
[ --tts-voice-count=<voices> ]
|
||||
<wake_word> [ <wake_word_title> ]
|
||||
@@ -41,6 +42,8 @@ Options:
|
||||
--language: Language for TTS voice selection (e.g. "en", "nl").
|
||||
Default: ${DEFAULT_LANGUAGE}
|
||||
|
||||
--english-accent: English accent emphasis. Default: ${DEFAULT_ENGLISH_ACCENT}
|
||||
|
||||
--tts-mode: TTS source: modern (OmniVoice plus Qwen3/MOSS where
|
||||
supported), hybrid (modern plus Piper), or piper.
|
||||
Default: ${DEFAULT_TTS_MODE}
|
||||
@@ -124,6 +127,7 @@ export GRPC_VERBOSITY=ERROR
|
||||
--samples=${SAMPLES} \
|
||||
--batch-size=${BATCH_SIZE} \
|
||||
--language="${LANGUAGE}" \
|
||||
--english-accent="${ENGLISH_ACCENT}" \
|
||||
--tts-mode="${TTS_MODE}" \
|
||||
--tts-voice-count="${TTS_VOICE_COUNT}" \
|
||||
--data-dir="${DATA_DIR}" "${WAKE_WORD}"
|
||||
|
||||
@@ -39,6 +39,7 @@ ROOT_DIR = Path(__file__).resolve().parent
|
||||
|
||||
from tts_config import (
|
||||
COMMON_OMNIVOICE_LANGUAGES,
|
||||
DEFAULT_ENGLISH_ACCENT,
|
||||
DEFAULT_TTS_MODE,
|
||||
ENGINE_MOSS,
|
||||
ENGINE_OMNIVOICE,
|
||||
@@ -47,6 +48,8 @@ from tts_config import (
|
||||
MOSS_LANGUAGES,
|
||||
OMNIVOICE_LANGUAGE_ALIASES,
|
||||
QWEN_LANGUAGES,
|
||||
english_accent_options,
|
||||
normalize_english_accent,
|
||||
normalize_tts_mode,
|
||||
parse_omnivoice_catalog,
|
||||
quality_for_engines,
|
||||
@@ -115,6 +118,10 @@ TRAIN_CMD = os.environ.get(
|
||||
)
|
||||
DEFAULT_LANGUAGE = os.environ.get("MWW_LANGUAGE", "en")
|
||||
DEFAULT_SERVER_TTS_MODE = normalize_tts_mode(os.environ.get("MWW_TTS_MODE", DEFAULT_TTS_MODE))
|
||||
DEFAULT_SERVER_ENGLISH_ACCENT = normalize_english_accent(
|
||||
os.environ.get("MWW_ENGLISH_ACCENT", DEFAULT_ENGLISH_ACCENT),
|
||||
DEFAULT_LANGUAGE,
|
||||
)
|
||||
|
||||
TAKES_PER_SPEAKER_DEFAULT = int(os.environ.get("REC_TAKES_PER_SPEAKER", "10"))
|
||||
SPEAKERS_TOTAL_DEFAULT = int(os.environ.get("REC_SPEAKERS_TOTAL", "1"))
|
||||
@@ -157,6 +164,7 @@ AUTO_TRAIN_DEFAULT_CONFIG: Dict[str, Any] = {
|
||||
"enabled": False,
|
||||
"wake_phrase": "",
|
||||
"language": DEFAULT_LANGUAGE,
|
||||
"english_accent": DEFAULT_SERVER_ENGLISH_ACCENT,
|
||||
"stt_engine": DEFAULT_STT_ENGINE,
|
||||
"minimum_transcript_chars": 2,
|
||||
"delete_confirmed_wakes": False,
|
||||
@@ -213,6 +221,7 @@ STATE: Dict[str, Any] = {
|
||||
"raw_phrase": None,
|
||||
"safe_word": None,
|
||||
"language": DEFAULT_LANGUAGE,
|
||||
"english_accent": DEFAULT_SERVER_ENGLISH_ACCENT,
|
||||
"tts_mode": DEFAULT_SERVER_TTS_MODE,
|
||||
|
||||
# multi-speaker
|
||||
@@ -787,6 +796,9 @@ def _normalize_auto_train_config(values: Dict[str, Any] | None, *, base: Dict[st
|
||||
"enabled": _config_bool(source.get("enabled")),
|
||||
"wake_phrase": str(source.get("wake_phrase") or "").strip(),
|
||||
"language": language,
|
||||
"english_accent": normalize_english_accent(
|
||||
source.get("english_accent"), language
|
||||
),
|
||||
"stt_engine": _normalize_stt_engine(source.get("stt_engine")),
|
||||
"minimum_transcript_chars": _bounded_int(source.get("minimum_transcript_chars"), 2, 1, 100),
|
||||
"delete_confirmed_wakes": _config_bool(source.get("delete_confirmed_wakes")),
|
||||
@@ -1725,6 +1737,9 @@ def _start_auto_training() -> Dict[str, Any]:
|
||||
safe_word = safe_name(wake_phrase)
|
||||
available_languages = _available_languages()
|
||||
language = _normalize_language(str(config.get("language") or DEFAULT_LANGUAGE))
|
||||
english_accent = normalize_english_accent(
|
||||
config.get("english_accent"), language
|
||||
)
|
||||
tts_mode = _resolve_tts_mode_for_language(
|
||||
DEFAULT_SERVER_TTS_MODE,
|
||||
language,
|
||||
@@ -1737,6 +1752,7 @@ def _start_auto_training() -> Dict[str, Any]:
|
||||
STATE["raw_phrase"] = wake_phrase
|
||||
STATE["safe_word"] = safe_word
|
||||
STATE["language"] = language
|
||||
STATE["english_accent"] = english_accent
|
||||
STATE["tts_mode"] = tts_mode
|
||||
STATE["training"]["running"] = True
|
||||
with AUTO_TRAIN_LOCK:
|
||||
@@ -1744,7 +1760,9 @@ def _start_auto_training() -> Dict[str, Any]:
|
||||
AUTO_TRAIN_RUNTIME["training_pending_consumed"] = int(AUTO_TRAIN_STATE.get("pending_negative_count") or 0)
|
||||
_save_auto_train_state_locked()
|
||||
try:
|
||||
_start_training_thread(safe_word, language, True, True, tts_mode)
|
||||
_start_training_thread(
|
||||
safe_word, language, True, True, tts_mode, english_accent
|
||||
)
|
||||
except Exception as exc:
|
||||
with STATE_LOCK:
|
||||
STATE["training"]["running"] = False
|
||||
@@ -1754,6 +1772,7 @@ def _start_auto_training() -> Dict[str, Any]:
|
||||
"started": True,
|
||||
"safe_word": safe_word,
|
||||
"language": language,
|
||||
"english_accent": english_accent,
|
||||
"tts_mode": tts_mode,
|
||||
}
|
||||
|
||||
@@ -3073,11 +3092,19 @@ def _start_training_thread(
|
||||
allow_no_personal: bool,
|
||||
auto_run: bool,
|
||||
tts_mode: str,
|
||||
english_accent: str = DEFAULT_SERVER_ENGLISH_ACCENT,
|
||||
) -> threading.Thread:
|
||||
global TRAINING_THREAD
|
||||
thread = threading.Thread(
|
||||
target=_run_training_background,
|
||||
args=(safe_word, language, allow_no_personal, auto_run, tts_mode),
|
||||
args=(
|
||||
safe_word,
|
||||
language,
|
||||
allow_no_personal,
|
||||
auto_run,
|
||||
tts_mode,
|
||||
english_accent,
|
||||
),
|
||||
daemon=True,
|
||||
name="wake-word-training",
|
||||
)
|
||||
@@ -3121,10 +3148,12 @@ def _run_training_background(
|
||||
allow_no_personal: bool,
|
||||
auto_run: bool = False,
|
||||
tts_mode: str = DEFAULT_SERVER_TTS_MODE,
|
||||
english_accent: str = DEFAULT_SERVER_ENGLISH_ACCENT,
|
||||
):
|
||||
global TRAINING_PROCESS, TRAINING_THREAD
|
||||
language = (language or DEFAULT_LANGUAGE).strip().lower() or DEFAULT_LANGUAGE
|
||||
tts_mode = normalize_tts_mode(tts_mode)
|
||||
english_accent = normalize_english_accent(english_accent, language)
|
||||
rc = 999
|
||||
proc: subprocess.Popen | None = None
|
||||
with STATE_LOCK:
|
||||
@@ -3170,7 +3199,12 @@ def _run_training_background(
|
||||
except Exception as error:
|
||||
_append_train_log(f"⚠️ Piper is unavailable for hybrid mode; using modern TTS only: {error}")
|
||||
|
||||
command_args = [f"--language={language}", f"--tts-mode={tts_mode}", safe_word]
|
||||
command_args = [
|
||||
f"--language={language}",
|
||||
f"--english-accent={english_accent}",
|
||||
f"--tts-mode={tts_mode}",
|
||||
safe_word,
|
||||
]
|
||||
if wake_word_title:
|
||||
command_args.append(wake_word_title)
|
||||
cmd_str = f"{TRAIN_CMD} " + " ".join(shlex.quote(argument) for argument in command_args)
|
||||
@@ -3180,6 +3214,8 @@ def _run_training_background(
|
||||
|
||||
_append_train_log("===== Training (train_wake_word) =====")
|
||||
_append_train_log(f"→ Running: {cmd_str}")
|
||||
if language == "en":
|
||||
_append_train_log(f"→ English accent emphasis: {english_accent}")
|
||||
|
||||
with open(log_path, "a", encoding="utf-8") as lf:
|
||||
proc = subprocess.Popen(
|
||||
@@ -3411,6 +3447,9 @@ def start_session(payload: Dict[str, Any]):
|
||||
language,
|
||||
available_languages,
|
||||
)
|
||||
english_accent = normalize_english_accent(
|
||||
payload.get("english_accent", DEFAULT_SERVER_ENGLISH_ACCENT), language
|
||||
)
|
||||
|
||||
speakers_total = max(1, min(10, speakers_total))
|
||||
takes_per_speaker = max(1, min(50, takes_per_speaker))
|
||||
@@ -3419,6 +3458,7 @@ def start_session(payload: Dict[str, Any]):
|
||||
STATE["raw_phrase"] = raw
|
||||
STATE["safe_word"] = safe
|
||||
STATE["language"] = language
|
||||
STATE["english_accent"] = english_accent
|
||||
STATE["tts_mode"] = tts_mode
|
||||
STATE["speakers_total"] = speakers_total
|
||||
STATE["takes_per_speaker"] = takes_per_speaker
|
||||
@@ -3432,6 +3472,7 @@ def start_session(payload: Dict[str, Any]):
|
||||
"raw_phrase": raw,
|
||||
"safe_word": safe,
|
||||
"language": language,
|
||||
"english_accent": english_accent,
|
||||
"tts_mode": tts_mode,
|
||||
"speakers_total": speakers_total,
|
||||
"takes_per_speaker": takes_per_speaker,
|
||||
@@ -3439,6 +3480,7 @@ def start_session(payload: Dict[str, Any]):
|
||||
"takes_received": len(takes),
|
||||
"takes": takes,
|
||||
"available_languages": available_languages,
|
||||
"available_english_accents": english_accent_options(),
|
||||
"personal_dir": str(PERSONAL_DIR),
|
||||
"data_dir": str(DATA_DIR),
|
||||
}
|
||||
@@ -3468,6 +3510,9 @@ def stop_session():
|
||||
STATE["training"]["safe_word"] = None
|
||||
training = dict(STATE["training"])
|
||||
language = _normalize_language(STATE["language"])
|
||||
english_accent = normalize_english_accent(
|
||||
STATE.get("english_accent"), language
|
||||
)
|
||||
tts_mode = normalize_tts_mode(STATE.get("tts_mode"))
|
||||
return {
|
||||
"ok": True,
|
||||
@@ -3476,11 +3521,13 @@ def stop_session():
|
||||
"raw_phrase": None,
|
||||
"safe_word": None,
|
||||
"language": language,
|
||||
"english_accent": english_accent,
|
||||
"tts_mode": tts_mode,
|
||||
"takes_received": len(takes),
|
||||
"takes": list(takes),
|
||||
"training": training,
|
||||
"available_languages": available_languages,
|
||||
"available_english_accents": english_accent_options(),
|
||||
}
|
||||
|
||||
|
||||
@@ -3491,13 +3538,18 @@ def get_session():
|
||||
with STATE_LOCK:
|
||||
current_language = _normalize_language(STATE["language"])
|
||||
current_tts_mode = normalize_tts_mode(STATE.get("tts_mode"))
|
||||
current_english_accent = normalize_english_accent(
|
||||
STATE.get("english_accent"), current_language
|
||||
)
|
||||
STATE["language"] = current_language
|
||||
STATE["english_accent"] = current_english_accent
|
||||
STATE["tts_mode"] = current_tts_mode
|
||||
return {
|
||||
"ok": True,
|
||||
"raw_phrase": STATE["raw_phrase"],
|
||||
"safe_word": STATE["safe_word"],
|
||||
"language": current_language,
|
||||
"english_accent": current_english_accent,
|
||||
"tts_mode": current_tts_mode,
|
||||
"speakers_total": STATE["speakers_total"],
|
||||
"takes_per_speaker": STATE["takes_per_speaker"],
|
||||
@@ -3505,6 +3557,7 @@ def get_session():
|
||||
"takes": list(takes),
|
||||
"training": dict(STATE["training"]),
|
||||
"available_languages": available_languages,
|
||||
"available_english_accents": english_accent_options(),
|
||||
}
|
||||
|
||||
|
||||
@@ -4017,6 +4070,9 @@ def train_now(payload: Dict[str, Any] = None):
|
||||
with STATE_LOCK:
|
||||
safe_word = STATE["safe_word"]
|
||||
language = (STATE.get("language") or DEFAULT_LANGUAGE)
|
||||
english_accent = normalize_english_accent(
|
||||
STATE.get("english_accent"), language
|
||||
)
|
||||
tts_mode = normalize_tts_mode(STATE.get("tts_mode"))
|
||||
takes_received = int(STATE["takes_received"])
|
||||
speakers_total = int(STATE["speakers_total"])
|
||||
@@ -4046,7 +4102,14 @@ def train_now(payload: Dict[str, Any] = None):
|
||||
with STATE_LOCK:
|
||||
STATE["training"]["running"] = True
|
||||
try:
|
||||
_start_training_thread(safe_word, language, allow_no_personal, False, tts_mode)
|
||||
_start_training_thread(
|
||||
safe_word,
|
||||
language,
|
||||
allow_no_personal,
|
||||
False,
|
||||
tts_mode,
|
||||
english_accent,
|
||||
)
|
||||
except Exception as exc:
|
||||
with STATE_LOCK:
|
||||
STATE["training"]["running"] = False
|
||||
@@ -4060,6 +4123,7 @@ def train_now(payload: Dict[str, Any] = None):
|
||||
"started": True,
|
||||
"safe_word": safe_word,
|
||||
"language": language,
|
||||
"english_accent": english_accent,
|
||||
"tts_mode": tts_mode,
|
||||
"personal_samples_used": takes_received > 0,
|
||||
"allow_no_personal": allow_no_personal,
|
||||
|
||||
@@ -16,6 +16,26 @@ TTS_MODE_PIPER = "piper"
|
||||
TTS_MODES = (TTS_MODE_MODERN, TTS_MODE_HYBRID, TTS_MODE_PIPER)
|
||||
DEFAULT_TTS_MODE = TTS_MODE_HYBRID
|
||||
|
||||
# English is one TTS language, while these values control the accent mix used
|
||||
# by providers that can follow a style instruction or clone a reference. The
|
||||
# remaining providers continue contributing their available English voices.
|
||||
DEFAULT_ENGLISH_ACCENT = "mixed"
|
||||
ENGLISH_ACCENTS = {
|
||||
"mixed": "Mixed English",
|
||||
"australian": "Australian",
|
||||
"american": "American",
|
||||
"british": "British",
|
||||
"canadian": "Canadian",
|
||||
"irish": "Irish",
|
||||
"scottish": "Scottish",
|
||||
"new_zealand": "New Zealand",
|
||||
"indian": "Indian",
|
||||
"south_african": "South African",
|
||||
}
|
||||
MIXED_ENGLISH_ACCENTS = tuple(
|
||||
code for code in ENGLISH_ACCENTS if code != DEFAULT_ENGLISH_ACCENT
|
||||
)
|
||||
|
||||
ENGINE_OMNIVOICE = "omnivoice"
|
||||
ENGINE_QWEN3 = "qwen3"
|
||||
ENGINE_MOSS = "moss"
|
||||
@@ -157,6 +177,39 @@ def normalize_tts_mode(value: object) -> str:
|
||||
return token if token in TTS_MODES else DEFAULT_TTS_MODE
|
||||
|
||||
|
||||
def normalize_english_accent(value: object, language: object = "en") -> str:
|
||||
"""Return a supported English accent emphasis or the mixed default."""
|
||||
|
||||
language_code = str(language or "en").strip().lower().replace("-", "_")
|
||||
if language_code.split("_", 1)[0] != "en":
|
||||
return DEFAULT_ENGLISH_ACCENT
|
||||
|
||||
token = str(value or DEFAULT_ENGLISH_ACCENT).strip().lower().replace("-", "_").replace(" ", "_")
|
||||
aliases = {
|
||||
"all": "mixed",
|
||||
"none": "mixed",
|
||||
"us": "american",
|
||||
"usa": "american",
|
||||
"uk": "british",
|
||||
"gb": "british",
|
||||
"australia": "australian",
|
||||
"canada": "canadian",
|
||||
"ireland": "irish",
|
||||
"scotland": "scottish",
|
||||
"new_zealand_english": "new_zealand",
|
||||
"south_africa": "south_african",
|
||||
}
|
||||
token = aliases.get(token, token)
|
||||
return token if token in ENGLISH_ACCENTS else DEFAULT_ENGLISH_ACCENT
|
||||
|
||||
|
||||
def english_accent_options() -> list[dict[str, str]]:
|
||||
return [
|
||||
{"code": code, "label": label}
|
||||
for code, label in ENGLISH_ACCENTS.items()
|
||||
]
|
||||
|
||||
|
||||
def language_for_engine(engine: str, language: str) -> str:
|
||||
code = str(language or "en").strip().lower().replace("-", "_")
|
||||
if engine == ENGINE_OMNIVOICE:
|
||||
|
||||
Reference in New Issue
Block a user