Add speaker names to web interface
This commit is contained in:
parent
50d4c2a6c9
commit
d4ef4744c7
5 changed files with 63 additions and 29 deletions
|
|
@ -21,6 +21,7 @@ import io
|
||||||
import wave
|
import wave
|
||||||
import tempfile
|
import tempfile
|
||||||
import typing
|
import typing
|
||||||
|
import dataclasses
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
@ -62,9 +63,7 @@ parser.add_argument(
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--port", type=int, default=59125, help="Port of HTTP server (default: 59125)"
|
"--port", type=int, default=59125, help="Port of HTTP server (default: 59125)"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument("--speaker", type=int, help="Default speaker to use (name or id)")
|
||||||
"--speaker-id", type=int, default=0, help="Default speaker id to use"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--length-scale", type=float, default=1.0, help="Speed of speech (> 1 is slower)"
|
"--length-scale", type=float, default=1.0, help="Speed of speech (> 1 is slower)"
|
||||||
)
|
)
|
||||||
|
|
@ -114,7 +113,6 @@ _LOGGER.debug(args)
|
||||||
class TextToWavParams:
|
class TextToWavParams:
|
||||||
text: str
|
text: str
|
||||||
voice: str = args.voice
|
voice: str = args.voice
|
||||||
speaker_id: int = args.speaker_id
|
|
||||||
noise_scale: float = args.noise_scale
|
noise_scale: float = args.noise_scale
|
||||||
noise_w: float = args.noise_w
|
noise_w: float = args.noise_w
|
||||||
length_scale: float = args.length_scale
|
length_scale: float = args.length_scale
|
||||||
|
|
@ -139,7 +137,7 @@ _WAV_CACHE: typing.Dict[TextToWavParams, Path] = {}
|
||||||
mimic3 = Mimic3TextToSpeechSystem(
|
mimic3 = Mimic3TextToSpeechSystem(
|
||||||
Mimic3Settings(
|
Mimic3Settings(
|
||||||
voice=args.voice,
|
voice=args.voice,
|
||||||
speaker_id=args.speaker_id,
|
speaker=args.speaker,
|
||||||
length_scale=args.length_scale,
|
length_scale=args.length_scale,
|
||||||
noise_scale=args.noise_scale,
|
noise_scale=args.noise_scale,
|
||||||
noise_w=args.noise_w,
|
noise_w=args.noise_w,
|
||||||
|
|
@ -160,7 +158,6 @@ def text_to_wav(params: TextToWavParams, no_cache: bool = False) -> bytes:
|
||||||
return wav_bytes
|
return wav_bytes
|
||||||
|
|
||||||
mimic3.voice = params.voice
|
mimic3.voice = params.voice
|
||||||
mimic3.speaker_id = params.speaker_id
|
|
||||||
|
|
||||||
mimic3.settings.length_scale = params.length_scale
|
mimic3.settings.length_scale = params.length_scale
|
||||||
mimic3.settings.noise_scale = params.noise_scale
|
mimic3.settings.noise_scale = params.noise_scale
|
||||||
|
|
@ -242,10 +239,6 @@ async def app_tts() -> Response:
|
||||||
if voice is not None:
|
if voice is not None:
|
||||||
tts_args["voice"] = str(voice)
|
tts_args["voice"] = str(voice)
|
||||||
|
|
||||||
speaker_id = request.args.get("speakerId")
|
|
||||||
if speaker_id is not None:
|
|
||||||
tts_args["speaker_id"] = int(speaker_id)
|
|
||||||
|
|
||||||
# TTS settings
|
# TTS settings
|
||||||
noise_scale = request.args.get("noiseScale")
|
noise_scale = request.args.get("noiseScale")
|
||||||
if noise_scale is not None:
|
if noise_scale is not None:
|
||||||
|
|
@ -286,9 +279,7 @@ async def app_tts() -> Response:
|
||||||
|
|
||||||
@app.route("/api/voices", methods=["GET"])
|
@app.route("/api/voices", methods=["GET"])
|
||||||
async def api_voices():
|
async def api_voices():
|
||||||
voices = mimic3.get_voices()
|
return jsonify([dataclasses.asdict(v) for v in mimic3.get_voices()])
|
||||||
voice_ids = sorted([v.name for v in voices])
|
|
||||||
return jsonify(voice_ids)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/process", methods=["GET", "POST"])
|
@app.route("/process", methods=["GET", "POST"])
|
||||||
|
|
@ -308,20 +299,14 @@ async def api_process():
|
||||||
|
|
||||||
voice = voice or args.voice
|
voice = voice or args.voice
|
||||||
|
|
||||||
speaker_id = args.speaker_id
|
|
||||||
if "#" in voice:
|
|
||||||
voice, speaker_id_str = voice.split("#", maxsplit=1)
|
|
||||||
speaker_id = int(speaker_id_str)
|
|
||||||
|
|
||||||
# Assume SSML if text begins with an angle bracket
|
# Assume SSML if text begins with an angle bracket
|
||||||
ssml = text.strip().startswith("<")
|
ssml = text.strip().startswith("<")
|
||||||
|
|
||||||
_LOGGER.debug("Speaking with voice '%s (speaker=%s)': %s", voice, speaker_id, text)
|
_LOGGER.debug("Speaking with voice '%s': %s", voice, text)
|
||||||
wav_bytes = text_to_wav(
|
wav_bytes = text_to_wav(
|
||||||
TextToWavParams(
|
TextToWavParams(
|
||||||
text=text,
|
text=text,
|
||||||
voice=voice,
|
voice=voice,
|
||||||
speaker_id=speaker_id,
|
|
||||||
ssml=ssml,
|
ssml=ssml,
|
||||||
length_scale=args.length_scale,
|
length_scale=args.length_scale,
|
||||||
noise_scale=args.noise_scale,
|
noise_scale=args.noise_scale,
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,9 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="speaker-id" title="Index of speaker">Speaker:</label>
|
<label for="speaker" title="Name of speaker">Speaker:</label>
|
||||||
<input type="number" id="speaker-id" name="speaker_id" size="5" min="0" value="0">
|
<select id="speaker-list" name="speaker">
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="audio-message" class="row mt-3" hidden>
|
<div id="audio-message" class="row mt-3" hidden>
|
||||||
|
|
@ -122,7 +123,13 @@
|
||||||
var noiseScale = q('#noise-scale').value || '0.333'
|
var noiseScale = q('#noise-scale').value || '0.333'
|
||||||
var noiseW = q('#noise-w').value || '1.0'
|
var noiseW = q('#noise-w').value || '1.0'
|
||||||
var lengthScale = q('#length-scale').value || '1.0'
|
var lengthScale = q('#length-scale').value || '1.0'
|
||||||
var speakerId = q('#speaker-id').value || '0'
|
|
||||||
|
var speakerList = q('#speaker-list')
|
||||||
|
var speaker = speakerList.options[speakerList.selectedIndex].value
|
||||||
|
if (speaker.length > 0) {
|
||||||
|
voice = voice + "#" + speaker
|
||||||
|
}
|
||||||
|
|
||||||
var textLanguage = q('#text-language').value || ''
|
var textLanguage = q('#text-language').value || ''
|
||||||
|
|
||||||
q('#audio-message').hidden = false
|
q('#audio-message').hidden = false
|
||||||
|
|
@ -135,8 +142,7 @@
|
||||||
'&noiseScale=' + encodeURIComponent(noiseScale) +
|
'&noiseScale=' + encodeURIComponent(noiseScale) +
|
||||||
'&noiseW=' + encodeURIComponent(noiseW) +
|
'&noiseW=' + encodeURIComponent(noiseW) +
|
||||||
'&lengthScale=' + encodeURIComponent(lengthScale) +
|
'&lengthScale=' + encodeURIComponent(lengthScale) +
|
||||||
'&textLanguage=' + encodeURIComponent(textLanguage) +
|
'&textLanguage=' + encodeURIComponent(textLanguage),
|
||||||
'&speakerId=' + encodeURIComponent(speakerId),
|
|
||||||
{cache: 'no-cache'})
|
{cache: 'no-cache'})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
@ -162,6 +168,29 @@
|
||||||
q('#message').textContent = ''
|
q('#message').textContent = ''
|
||||||
q('#audio').hidden = true
|
q('#audio').hidden = true
|
||||||
q('#audio').autoplay = true
|
q('#audio').autoplay = true
|
||||||
|
|
||||||
|
// Reset speakers
|
||||||
|
var speakerList = q('#speaker-list')
|
||||||
|
for (var i = speakerList.options.length - 1; i >= 0; i--) {
|
||||||
|
speakerList.options[i].remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
var voiceKey = voiceList.options[voiceList.selectedIndex].value
|
||||||
|
var voice = voicesInfo[voiceKey]
|
||||||
|
|
||||||
|
if (voice.speakers) {
|
||||||
|
voice.speakers.forEach(function(speaker) {
|
||||||
|
speakerList.insertAdjacentHTML(
|
||||||
|
'beforeend', '<option value="' + speaker + '">' + speaker + '</option>'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Add default speaker
|
||||||
|
speakerList.insertAdjacentHTML(
|
||||||
|
'beforeend', '<option value="">default</option>'
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
q('#voice-list').addEventListener('change', voiceChanged)
|
q('#voice-list').addEventListener('change', voiceChanged)
|
||||||
|
|
@ -180,14 +209,15 @@
|
||||||
if (!res.ok) throw Error(res.statusText)
|
if (!res.ok) throw Error(res.statusText)
|
||||||
return res.json()
|
return res.json()
|
||||||
}).then(function(voices) {
|
}).then(function(voices) {
|
||||||
voicesInfo = voices
|
voicesInfo = {}
|
||||||
|
|
||||||
// Populate select
|
// Populate select
|
||||||
var indexToSelect = -1
|
var indexToSelect = -1
|
||||||
|
|
||||||
voices.forEach(function(voice) {
|
voices.forEach(function(voice) {
|
||||||
|
voicesInfo[voice.key] = voice
|
||||||
voiceList.insertAdjacentHTML(
|
voiceList.insertAdjacentHTML(
|
||||||
'beforeend', '<option value="' + voice + '">' + voice + '</option>'
|
'beforeend', '<option value="' + voice.key + '">' + voice.language + '/' + voice.name + '</option>'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,12 +135,25 @@ class Mimic3TextToSpeechSystem(TextToSpeechSystem):
|
||||||
|
|
||||||
voice_lang = lang_dir.name
|
voice_lang = lang_dir.name
|
||||||
voice_name = voice_dir.name
|
voice_name = voice_dir.name
|
||||||
|
speakers: typing.Optional[typing.Sequence[str]] = None
|
||||||
|
|
||||||
|
speakers_path = voice_dir / "speakers.txt"
|
||||||
|
if speakers_path.is_file():
|
||||||
|
speakers = []
|
||||||
|
with open(
|
||||||
|
speakers_path, "r", encoding="utf-8"
|
||||||
|
) as speakers_file:
|
||||||
|
for line in speakers_file:
|
||||||
|
line = line.strip()
|
||||||
|
if line:
|
||||||
|
speakers.append(line)
|
||||||
|
|
||||||
yield Voice(
|
yield Voice(
|
||||||
key=str(voice_dir.absolute()),
|
key=str(voice_dir.absolute()),
|
||||||
name=voice_name,
|
name=voice_name,
|
||||||
language=voice_lang,
|
language=voice_lang,
|
||||||
description="",
|
description="",
|
||||||
|
speakers=speakers,
|
||||||
)
|
)
|
||||||
|
|
||||||
def begin_utterance(self):
|
def begin_utterance(self):
|
||||||
|
|
|
||||||
|
|
@ -212,17 +212,18 @@ class Mimic3Voice(metaclass=ABCMeta):
|
||||||
with open(phoneme_map_path, "r", encoding="utf-8") as map_file:
|
with open(phoneme_map_path, "r", encoding="utf-8") as map_file:
|
||||||
phoneme_map = phonemes2ids.utils.load_phoneme_map(map_file)
|
phoneme_map = phonemes2ids.utils.load_phoneme_map(map_file)
|
||||||
|
|
||||||
# id -> speaker | alias | alias ...
|
# id -> speaker
|
||||||
speaker_map: typing.Optional[SPEAKER_MAP_TYPE] = None
|
speaker_map: typing.Optional[SPEAKER_MAP_TYPE] = None
|
||||||
speaker_map_path = voice_dir / "speaker_map.csv"
|
speaker_map_path = voice_dir / "speaker_map.csv"
|
||||||
if speaker_map_path.is_file():
|
if speaker_map_path.is_file():
|
||||||
_LOGGER.debug("Loading speaker map from %s", speaker_map_path)
|
_LOGGER.debug("Loading speaker map from %s", speaker_map_path)
|
||||||
with open(speaker_map_path, "r", encoding="utf-8") as map_file:
|
with open(speaker_map_path, "r", encoding="utf-8") as map_file:
|
||||||
|
# id | dataset | name | [alias] | [alias] ...
|
||||||
reader = csv.reader(map_file, delimiter="|")
|
reader = csv.reader(map_file, delimiter="|")
|
||||||
speaker_map = {}
|
speaker_map = {}
|
||||||
for row in reader:
|
for row in reader:
|
||||||
speaker_id = int(row[0])
|
speaker_id = int(row[0])
|
||||||
for alias in row[1:]:
|
for alias in row[2:]:
|
||||||
speaker_map[alias] = speaker_id
|
speaker_map[alias] = speaker_id
|
||||||
|
|
||||||
if config.phonemizer == Phonemizer.GRUUT:
|
if config.phonemizer == Phonemizer.GRUUT:
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,13 @@ class Voice:
|
||||||
name: str
|
name: str
|
||||||
language: str
|
language: str
|
||||||
description: str
|
description: str
|
||||||
|
speakers: typing.Optional[typing.Sequence[str]] = None
|
||||||
properties: typing.Optional[typing.Mapping[str, typing.Any]] = None
|
properties: typing.Optional[typing.Mapping[str, typing.Any]] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_multispeaker(self) -> bool:
|
||||||
|
return (self.speakers is not None) and (len(self.speakers) > 1)
|
||||||
|
|
||||||
|
|
||||||
# @dataclass
|
# @dataclass
|
||||||
# class LexiconEntry:
|
# class LexiconEntry:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue