chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
import audioop
|
||||
import time
|
||||
import torch
|
||||
import numpy as np
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
from ..constants import ASR_MODEL_PATH
|
||||
from ..utils import ThrottledCallback, is_macos, get_settings
|
||||
from ..audio_input import AudioInputStream
|
||||
|
||||
|
||||
class ASR:
|
||||
def __init__(self,
|
||||
tcp_server=None,
|
||||
# @see https://github.com/SYSTRAN/faster-whisper/blob/master/faster_whisper/transcribe.py
|
||||
# auto, cpu, cuda
|
||||
device='auto',
|
||||
interrupt_leon_speech_callback=None,
|
||||
transcribed_callback=None,
|
||||
end_of_owner_speech_callback=None,
|
||||
active_listening_disabled_callback=None):
|
||||
tic = time.perf_counter()
|
||||
self.log('Loading model...')
|
||||
|
||||
if device == 'auto':
|
||||
if torch.cuda.is_available():
|
||||
device = 'cuda'
|
||||
self.log('Using CUDA (Compute Unified Device Architecture)')
|
||||
|
||||
if 'cuda' in device:
|
||||
assert torch.cuda.is_available()
|
||||
|
||||
self.log(f'Device: {device}')
|
||||
|
||||
compute_type = 'float16'
|
||||
if is_macos():
|
||||
compute_type = 'int8_float32'
|
||||
|
||||
if device == 'cpu':
|
||||
compute_type = 'int8_float32'
|
||||
|
||||
self.tcp_server = tcp_server
|
||||
self.wake_word = None
|
||||
self.compute_type = compute_type
|
||||
self.is_recording = False
|
||||
|
||||
"""
|
||||
Thottle the interrupt Leon's speech callback to avoid sending too many messages to the client
|
||||
"""
|
||||
self.interrupt_leon_speech_callback = ThrottledCallback(
|
||||
interrupt_leon_speech_callback, 0.8
|
||||
)
|
||||
self.transcribed_callback = transcribed_callback
|
||||
self.end_of_owner_speech_callback = end_of_owner_speech_callback
|
||||
self.active_listening_disabled_callback = active_listening_disabled_callback
|
||||
|
||||
self.device = device
|
||||
self.is_voice_activity_detected = False
|
||||
self.silence_start_time = 0
|
||||
self.is_active_listening_enabled = False
|
||||
self.complete_text = ''
|
||||
|
||||
self.buffer = bytearray()
|
||||
self.silence_frames_count = 0
|
||||
self.channels = 1
|
||||
self.rate = 16000
|
||||
self.frames_per_buffer = 1024
|
||||
self.rms_threshold = get_settings('asr')['rms_mic_threshold']
|
||||
# Duration of silence after which the audio data is considered as a new utterance (in seconds)
|
||||
self.silence_duration = get_settings('asr')['silence_duration']
|
||||
"""
|
||||
Duration of silence after which the active listening is stopped (in seconds).
|
||||
Once stopped, the active listening can be resumed by starting a new recording event
|
||||
"""
|
||||
self.base_active_listening_duration = get_settings('asr')['active_listening_duration']
|
||||
self.active_listening_duration = self.base_active_listening_duration
|
||||
|
||||
self.mic_stream = None
|
||||
self.model = None
|
||||
|
||||
model_params = {
|
||||
'model_size_or_path': ASR_MODEL_PATH,
|
||||
'device': self.device,
|
||||
'compute_type': self.compute_type,
|
||||
'local_files_only': True
|
||||
}
|
||||
if self.device == 'cpu':
|
||||
model_params['cpu_threads'] = 4
|
||||
|
||||
self.open_mic_stream()
|
||||
|
||||
self.model = WhisperModel(**model_params)
|
||||
|
||||
self.log('Model loaded')
|
||||
toc = time.perf_counter()
|
||||
|
||||
self.log(f'Time taken to load model: {toc - tic:0.4f} seconds')
|
||||
|
||||
def open_mic_stream(self):
|
||||
try:
|
||||
self.mic_stream = AudioInputStream(
|
||||
channels=self.channels,
|
||||
rate=self.rate,
|
||||
frames_per_buffer=self.frames_per_buffer
|
||||
)
|
||||
self.mic_stream.open()
|
||||
except Exception as e:
|
||||
self.log('Error to open mic stream:', e)
|
||||
|
||||
def start_recording(self):
|
||||
if self.wake_word:
|
||||
# Make sure to stop the wake word detection before recording
|
||||
# otherwise it will loop for the wake word and create conflict
|
||||
# on the audio stream
|
||||
self.wake_word.stop_listening()
|
||||
|
||||
self.is_recording = True
|
||||
# Convert the silence duration to the number of audio frames required to detect the silence
|
||||
silence_threshold = int(self.silence_duration * self.rate / self.frames_per_buffer)
|
||||
|
||||
try:
|
||||
self.log('Recording...')
|
||||
|
||||
while self.is_recording:
|
||||
data = self.mic_stream.read(self.frames_per_buffer, exception_on_overflow=False)
|
||||
rms = audioop.rms(data, 2) # width=2 for signed 16-bit PCM
|
||||
|
||||
if rms >= self.rms_threshold:
|
||||
if not self.is_voice_activity_detected:
|
||||
self.is_active_listening_enabled = True
|
||||
self.is_voice_activity_detected = True
|
||||
|
||||
self.interrupt_leon_speech_callback()
|
||||
|
||||
self.buffer.extend(data)
|
||||
self.silence_frames_count = 0
|
||||
else:
|
||||
if self.is_voice_activity_detected:
|
||||
self.silence_start_time = time.time()
|
||||
self.is_voice_activity_detected = False
|
||||
|
||||
if self.silence_frames_count < silence_threshold:
|
||||
self.silence_frames_count += 1
|
||||
else:
|
||||
if len(self.buffer) > 0:
|
||||
self.log('Silence detected')
|
||||
|
||||
audio_data = np.frombuffer(self.buffer, dtype=np.int16)
|
||||
if self.compute_type == 'int8_float32':
|
||||
audio_data = audio_data.astype(np.float32) / 32768.0
|
||||
transcribe_params = {
|
||||
'beam_size': 5,
|
||||
'language': 'en',
|
||||
'task': 'transcribe',
|
||||
'condition_on_previous_text': False,
|
||||
'hotwords': 'talking to Leon'
|
||||
}
|
||||
if self.device == 'cpu':
|
||||
transcribe_params['temperature'] = 0
|
||||
segments, info = self.model.transcribe(audio_data, **transcribe_params)
|
||||
|
||||
for segment in segments:
|
||||
self.log('[%.2fs -> %.2fs] %s' % (segment.start, segment.end, segment.text))
|
||||
self.complete_text += segment.text
|
||||
|
||||
self.transcribed_callback(self.complete_text)
|
||||
time.sleep(0.1)
|
||||
# Notify the end of the owner's speech
|
||||
self.end_of_owner_speech_callback(self.complete_text)
|
||||
|
||||
self.complete_text = ''
|
||||
self.buffer = bytearray()
|
||||
|
||||
should_stop_active_listening = self.is_active_listening_enabled and time.time() - self.silence_start_time > self.active_listening_duration
|
||||
if should_stop_active_listening:
|
||||
self.is_active_listening_enabled = False
|
||||
self.active_listening_disabled_callback()
|
||||
# Do not add anything after this line because it will be ignored
|
||||
# as it loops for the wake word
|
||||
self.stop_recording()
|
||||
except Exception as e:
|
||||
self.log('Error:', e)
|
||||
|
||||
def stop_recording(self):
|
||||
self.log('Recording stopped')
|
||||
|
||||
if self.wake_word:
|
||||
self.wake_word.reset_model_state()
|
||||
if self.wake_word.is_enabled:
|
||||
# Do not add anything after this line because it will be ignored
|
||||
# as it loops for the wake word
|
||||
self.wake_word.start_listening()
|
||||
else:
|
||||
self.is_recording = False
|
||||
# self.mic_stream.stop_stream()
|
||||
# self.mic_stream.close()
|
||||
# self.log('Stream closed, recording stopped')
|
||||
|
||||
# Make sure to wait for the recording thread to join before starting a new recording.
|
||||
# Only needed when the wake word detection is enabled
|
||||
if (
|
||||
self.wake_word and
|
||||
self.wake_word.is_enabled and
|
||||
self.tcp_server.asr_recording_thread and
|
||||
self.tcp_server.asr_recording_thread.is_alive()
|
||||
):
|
||||
# The thread is only used when received TCP message from the core,
|
||||
# hence it is not used when triggered by the wake word.
|
||||
# If we do not "join" it, it'll duplicate the recording loop
|
||||
self.log('Join recording thread')
|
||||
self.tcp_server.asr_recording_thread.join()
|
||||
|
||||
@staticmethod
|
||||
def log(*args, **kwargs):
|
||||
print('[ASR]', *args, **kwargs)
|
||||
@@ -0,0 +1,57 @@
|
||||
import numpy as np
|
||||
import soundcard as sc
|
||||
|
||||
|
||||
class AudioInputStream:
|
||||
def __init__(self, channels=1, rate=16000, frames_per_buffer=1024):
|
||||
self.channels = channels
|
||||
self.rate = rate
|
||||
self.frames_per_buffer = frames_per_buffer
|
||||
self.sample_width = 2
|
||||
self._microphone = None
|
||||
self._recorder = None
|
||||
self._is_open = False
|
||||
self.device_name = None
|
||||
|
||||
def open(self):
|
||||
if self._is_open:
|
||||
return
|
||||
|
||||
microphones = sc.all_microphones(include_loopback=False)
|
||||
|
||||
try:
|
||||
self._microphone = sc.default_microphone()
|
||||
except Exception as error:
|
||||
if not microphones:
|
||||
raise RuntimeError('No capture device found') from error
|
||||
|
||||
self._microphone = microphones[0]
|
||||
|
||||
self.device_name = str(self._microphone)
|
||||
self._recorder = self._microphone.recorder(
|
||||
samplerate=self.rate,
|
||||
channels=self.channels,
|
||||
blocksize=self.frames_per_buffer
|
||||
)
|
||||
self._recorder.__enter__()
|
||||
self._is_open = True
|
||||
|
||||
def read(self, frames, exception_on_overflow=False):
|
||||
del exception_on_overflow
|
||||
audio_data = self._recorder.record(numframes=frames)
|
||||
audio_data = np.clip(audio_data, -1.0, 1.0)
|
||||
pcm_data = (audio_data * 32767.0).astype(np.int16)
|
||||
|
||||
return pcm_data.tobytes()
|
||||
|
||||
def stop_stream(self):
|
||||
return
|
||||
|
||||
def close(self):
|
||||
self._is_open = False
|
||||
|
||||
if self._recorder:
|
||||
self._recorder.__exit__(None, None, None)
|
||||
self._recorder = None
|
||||
|
||||
self._microphone = None
|
||||
@@ -0,0 +1,52 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
IS_RAN_FROM_BINARY = getattr(sys, 'frozen', False)
|
||||
|
||||
DEFAULT_LEON_PROFILE = 'just-me'
|
||||
|
||||
|
||||
def resolve_leon_codebase_path():
|
||||
configured_codebase_path = os.getenv('LEON_CODEBASE_PATH', '').strip()
|
||||
|
||||
return os.path.abspath(configured_codebase_path or os.getcwd())
|
||||
|
||||
|
||||
def resolve_leon_home():
|
||||
configured_leon_home = os.getenv('LEON_HOME', '').strip()
|
||||
|
||||
if configured_leon_home:
|
||||
return os.path.abspath(configured_leon_home)
|
||||
|
||||
return os.path.join(os.path.expanduser('~'), '.leon')
|
||||
|
||||
|
||||
EXECUTABLE_DIR_PATH = os.path.dirname(sys.executable) if IS_RAN_FROM_BINARY else '.'
|
||||
|
||||
CODEBASE_PATH = resolve_leon_codebase_path()
|
||||
LEON_HOME_PATH = resolve_leon_home()
|
||||
|
||||
LIB_PATH = os.path.join(CODEBASE_PATH, 'tcp_server', 'src', 'lib')
|
||||
if IS_RAN_FROM_BINARY:
|
||||
LIB_PATH = os.path.join(os.path.dirname(sys.executable), 'lib', 'lib')
|
||||
|
||||
PYTHON_VERSION = '3.11'
|
||||
|
||||
TMP_PATH = os.path.join(LIB_PATH, 'tmp')
|
||||
AUDIO_MODELS_PATH = os.path.join(LEON_HOME_PATH, 'models', 'audio')
|
||||
SETTINGS_PATH = os.path.join(CODEBASE_PATH, 'tcp_server', 'settings.json')
|
||||
|
||||
# TTS
|
||||
TTS_MODEL_FOLDER_PATH = os.path.join(AUDIO_MODELS_PATH, 'tts')
|
||||
TTS_BERT_FRENCH_MODEL_DIR_PATH = os.path.join(TTS_MODEL_FOLDER_PATH, 'bert-case-french-europeana-cased')
|
||||
TTS_BERT_BASE_MODEL_DIR_PATH = os.path.join(TTS_MODEL_FOLDER_PATH, 'bert-base-uncased')
|
||||
TTS_MODEL_CONFIG_PATH = os.path.join(TTS_MODEL_FOLDER_PATH, 'config.json')
|
||||
IS_TTS_ENABLED = os.environ.get('LEON_TTS', 'true') == 'true'
|
||||
|
||||
# ASR
|
||||
ASR_MODEL_PATH = os.path.join(AUDIO_MODELS_PATH, 'asr')
|
||||
IS_ASR_ENABLED = os.environ.get('LEON_ASR', 'true') == 'true'
|
||||
|
||||
# Wake word
|
||||
WAKE_WORD_MODEL_FOLDER_PATH = os.path.join(AUDIO_MODELS_PATH, 'wake_word')
|
||||
IS_WAKE_WORD_ENABLED = os.environ.get('LEON_WAKE_WORD', 'true') == 'true'
|
||||
@@ -0,0 +1,254 @@
|
||||
import socket
|
||||
import json
|
||||
import os
|
||||
from typing import Union
|
||||
import time
|
||||
import re
|
||||
import threading
|
||||
|
||||
from .utils import get_settings
|
||||
from .wake_word.api import WakeWord
|
||||
from .asr.api import ASR
|
||||
from .tts.api import TTS
|
||||
from .constants import (
|
||||
TTS_MODEL_CONFIG_PATH,
|
||||
TTS_MODEL_FOLDER_PATH,
|
||||
WAKE_WORD_MODEL_FOLDER_PATH,
|
||||
IS_WAKE_WORD_ENABLED,
|
||||
IS_TTS_ENABLED,
|
||||
TMP_PATH,
|
||||
IS_ASR_ENABLED
|
||||
)
|
||||
|
||||
TTS_MODEL_PATH = os.path.join(TTS_MODEL_FOLDER_PATH, get_settings('tts')['model_file_name'])
|
||||
|
||||
|
||||
class TCPServer:
|
||||
def __init__(self, host: str, port: Union[str, int]):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.conn = None
|
||||
self.addr = None
|
||||
self.wake_word = None
|
||||
self.tts = None
|
||||
self.asr = None
|
||||
self.asr_recording_thread = None
|
||||
|
||||
@staticmethod
|
||||
def log(*args, **kwargs):
|
||||
print('[TCP Server]', *args, **kwargs)
|
||||
|
||||
def send_tcp_message(self, data: dict):
|
||||
if not self.conn:
|
||||
self.log('No client connection found. Cannot send message')
|
||||
return
|
||||
|
||||
self.conn.sendall(json.dumps(data).encode('utf-8'))
|
||||
|
||||
def init_tts(self):
|
||||
if not IS_TTS_ENABLED:
|
||||
self.log('TTS is disabled')
|
||||
return
|
||||
|
||||
if not os.path.exists(TTS_MODEL_CONFIG_PATH):
|
||||
self.log(f'TTS model config not found at {TTS_MODEL_CONFIG_PATH}')
|
||||
return
|
||||
|
||||
if not os.path.exists(TTS_MODEL_PATH):
|
||||
self.log(f'TTS model not found at {TTS_MODEL_PATH}')
|
||||
return
|
||||
|
||||
self.tts = TTS(language='EN',
|
||||
device=get_settings('tts')['device'],
|
||||
config_path=TTS_MODEL_CONFIG_PATH,
|
||||
ckpt_path=TTS_MODEL_PATH)
|
||||
|
||||
def init_asr(self):
|
||||
if not IS_ASR_ENABLED:
|
||||
self.log('ASR is disabled')
|
||||
return
|
||||
|
||||
def transcribed_callback(text):
|
||||
# cleaned_text = clean_up_speech(text)
|
||||
self.log('Cleaned speech:', text)
|
||||
self.send_tcp_message({
|
||||
'topic': 'asr-new-speech',
|
||||
'data': {
|
||||
'text': text
|
||||
}
|
||||
})
|
||||
|
||||
def interrupt_leon_speech_callback():
|
||||
self.log('Interrupting Leon speech because owner started speaking')
|
||||
self.send_tcp_message({
|
||||
'topic': 'asr-interrupt-leon-speech',
|
||||
'data': {}
|
||||
})
|
||||
|
||||
def end_of_owner_speech_callback(utterance):
|
||||
self.log('End of owner speech:', utterance)
|
||||
self.send_tcp_message({
|
||||
'topic': 'asr-end-of-owner-speech-detected',
|
||||
'data': {
|
||||
'utterance': utterance
|
||||
}
|
||||
})
|
||||
|
||||
def active_listening_disabled_callback():
|
||||
self.log('Active listening disabled')
|
||||
self.send_tcp_message({
|
||||
'topic': 'asr-active-listening-disabled',
|
||||
'data': {}
|
||||
})
|
||||
|
||||
self.asr = ASR(tcp_server=self,
|
||||
device=get_settings('asr')['device'],
|
||||
interrupt_leon_speech_callback=interrupt_leon_speech_callback,
|
||||
transcribed_callback=transcribed_callback,
|
||||
end_of_owner_speech_callback=end_of_owner_speech_callback,
|
||||
active_listening_disabled_callback=active_listening_disabled_callback)
|
||||
|
||||
if not IS_WAKE_WORD_ENABLED:
|
||||
self.log('Wake word is disabled')
|
||||
return
|
||||
|
||||
wake_word_model_name = get_settings('wake_word')['model_file_name']
|
||||
wake_word_model_path = os.path.join(WAKE_WORD_MODEL_FOLDER_PATH, wake_word_model_name)
|
||||
|
||||
self.asr.wake_word = WakeWord(
|
||||
asr=self.asr,
|
||||
model_path=wake_word_model_path,
|
||||
device=get_settings('wake_word')['device'],
|
||||
detection_threshold=get_settings('wake_word')['detection_threshold']
|
||||
)
|
||||
# Do not add anything after this line because it will be ignored
|
||||
# as it loops for the wake word
|
||||
self.asr.wake_word.start_listening()
|
||||
|
||||
def init(self):
|
||||
try:
|
||||
# Make sure to establish TCP connection by reusing the address so it does not conflict with port already in use
|
||||
self.tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.tcp_socket.bind((self.host, int(self.port)))
|
||||
self.tcp_socket.listen()
|
||||
except OSError as e:
|
||||
# If the port is already in use, close the connection and retry
|
||||
if 'Address already in use' in str(e):
|
||||
self.log(f'Port {self.port} is already in use. Disconnecting client and retrying...')
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
# Wait for a moment before retrying
|
||||
time.sleep(1)
|
||||
self.init()
|
||||
else:
|
||||
raise
|
||||
|
||||
while True:
|
||||
# Flush buffered output to make it IPC friendly (readable on stdout)
|
||||
self.log('Waiting for connection...', flush=True)
|
||||
|
||||
# Our TCP server only needs to support one connection
|
||||
self.conn, self.addr = self.tcp_socket.accept()
|
||||
|
||||
try:
|
||||
self.log(f'Client connected: {self.addr}')
|
||||
|
||||
while True:
|
||||
# socket_data = self.conn.recv(1024)
|
||||
socket_data = self.conn.recv(8096)
|
||||
|
||||
if not socket_data:
|
||||
break
|
||||
|
||||
data_dict = json.loads(socket_data)
|
||||
|
||||
# Verify the received topic can execute the method
|
||||
method = data_dict['topic'].lower().replace('-', '_')
|
||||
if hasattr(self.__class__, method) and callable(getattr(self.__class__, method)):
|
||||
data = data_dict['data']
|
||||
method = getattr(self, method)
|
||||
res = method(data)
|
||||
|
||||
self.send_tcp_message(res)
|
||||
else:
|
||||
self.log(f'Method "{method}" not found')
|
||||
finally:
|
||||
self.log(f'Client disconnected: {self.addr}')
|
||||
self.conn.close()
|
||||
|
||||
def asr_start_recording(self, extra=None) -> dict:
|
||||
# If ASR is not initialized yet, then wait for 2 seconds before starting recording
|
||||
if not self.asr:
|
||||
self.log('ASR is not initialized yet. Waiting for 2 seconds before starting recording...')
|
||||
time.sleep(2)
|
||||
|
||||
if self.asr.is_recording is False:
|
||||
self.asr_recording_thread = threading.Thread(target=self.asr.start_recording)
|
||||
self.asr_recording_thread.start()
|
||||
|
||||
return {
|
||||
'topic': 'asr-started-recording',
|
||||
'data': {}
|
||||
}
|
||||
|
||||
def tts_synthesize(self, speech: str) -> dict:
|
||||
# If TTS is not initialized yet, then wait for 2 seconds before synthesizing
|
||||
if not self.tts:
|
||||
self.log('TTS is not initialized yet. Waiting for 2 seconds before synthesizing...')
|
||||
time.sleep(2)
|
||||
|
||||
"""
|
||||
TODO:
|
||||
- Implement one speaker per style (joyful, sad, angry, tired, etc.)
|
||||
- Need to train a new model with default voice speaker and other speakers with different styles
|
||||
- EN-Leon-Joyful-V1; EN-Leon-Sad-V1; etc.
|
||||
"""
|
||||
speaker_ids = self.tts.hps.data.spk2id
|
||||
# Random file name to avoid conflicts
|
||||
audio_id = f'{int(time.time())}_{os.urandom(2).hex()}'
|
||||
output_file_name = f'{audio_id}.wav'
|
||||
output_path = os.path.join(TMP_PATH, output_file_name)
|
||||
speed = 1
|
||||
|
||||
formatted_speech = speech.replace(' - ', '.').replace(',', '.').replace(': ', '. ')
|
||||
# Clean up emojis
|
||||
formatted_speech = re.sub(r'[\U00010000-\U0010ffff]', '', formatted_speech)
|
||||
formatted_speech = formatted_speech.strip()
|
||||
# formatted_speech = speech.replace(',', '.').replace('.', '...')
|
||||
|
||||
# TODO: should not wait to finish for streaming support
|
||||
self.tts.tts_to_file(
|
||||
formatted_speech,
|
||||
speaker_ids['EN-Leon-V1_1'],
|
||||
output_path=output_path,
|
||||
speed=speed,
|
||||
quiet=True,
|
||||
format='wav',
|
||||
stream=False
|
||||
)
|
||||
|
||||
return {
|
||||
'topic': 'tts-audio-streaming',
|
||||
'data': {
|
||||
'outputPath': output_path,
|
||||
'audioId': audio_id
|
||||
}
|
||||
}
|
||||
|
||||
def leon_speech_audio_ended(self, audio_duration: float) -> dict:
|
||||
if not self.asr:
|
||||
self.log('ASR is None, cannot update active listening duration')
|
||||
|
||||
if self.asr:
|
||||
if not audio_duration:
|
||||
audio_duration = 0
|
||||
self.asr.active_listening_duration = self.asr.base_active_listening_duration + audio_duration
|
||||
self.log(f'ASR active listening duration increased to {self.asr.active_listening_duration}s')
|
||||
|
||||
return {
|
||||
'topic': 'asr-active-listening-duration-increased',
|
||||
'data': {
|
||||
'activeListeningDuration': self.asr.active_listening_duration
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import re
|
||||
import soundfile
|
||||
import numpy as np
|
||||
import torch.nn as nn
|
||||
from tqdm import tqdm
|
||||
import torch
|
||||
import time
|
||||
import wave
|
||||
import os
|
||||
|
||||
from . import utils
|
||||
from .models import SynthesizerTrn
|
||||
from .split_utils import split_sentence
|
||||
from ..utils import is_macos
|
||||
|
||||
# torch.backends.cudnn.enabled = False
|
||||
|
||||
class TTS(nn.Module):
|
||||
def __init__(self,
|
||||
language,
|
||||
# auto, cpu, cuda, mps
|
||||
device='auto',
|
||||
use_hf=True,
|
||||
config_path=None,
|
||||
ckpt_path=None):
|
||||
super().__init__()
|
||||
|
||||
tic = time.perf_counter()
|
||||
self.log('Loading model...')
|
||||
|
||||
if device == 'auto':
|
||||
device = 'cpu'
|
||||
|
||||
if torch.cuda.is_available():
|
||||
device = 'cuda'
|
||||
self.log('Using CUDA (Compute Unified Device Architecture)')
|
||||
if torch.backends.mps.is_available():
|
||||
device = 'mps'
|
||||
self.log('Using MPS (Metal Performance Shaders)')
|
||||
if 'cuda' in device:
|
||||
assert torch.cuda.is_available()
|
||||
if 'mps' in device:
|
||||
assert torch.backends.mps.is_available()
|
||||
|
||||
if is_macos():
|
||||
"""
|
||||
Temporary fix.
|
||||
Force CPU device for macOS because of the memory leak where cache does not want to clear up on MPS
|
||||
"""
|
||||
device = 'cpu'
|
||||
|
||||
self.log(f'Device: {device}')
|
||||
|
||||
hps = utils.get_hparams_from_file(config_path)
|
||||
|
||||
num_languages = hps.num_languages
|
||||
num_tones = hps.num_tones
|
||||
symbols = hps.symbols
|
||||
|
||||
model = SynthesizerTrn(
|
||||
len(symbols),
|
||||
hps.data.filter_length // 2 + 1,
|
||||
hps.train.segment_size // hps.data.hop_length,
|
||||
n_speakers=hps.data.n_speakers,
|
||||
num_tones=num_tones,
|
||||
num_languages=num_languages,
|
||||
**hps.model,
|
||||
).to(device)
|
||||
|
||||
model.eval()
|
||||
self.model = model
|
||||
self.symbol_to_id = {s: i for i, s in enumerate(symbols)}
|
||||
self.hps = hps
|
||||
self.device = device
|
||||
|
||||
# load state_dict
|
||||
checkpoint_dict = torch.load(ckpt_path, map_location=device)
|
||||
self.model.load_state_dict(checkpoint_dict['model'], strict=True)
|
||||
|
||||
language = language.split('_')[0]
|
||||
self.language = 'ZH_MIX_EN' if language == 'ZH' else language # we support a ZH_MIX_EN model
|
||||
|
||||
self.log('Model loaded')
|
||||
toc = time.perf_counter()
|
||||
|
||||
self.log(f"Time taken to load model: {toc - tic:0.4f} seconds")
|
||||
|
||||
self.log('Warming up model...')
|
||||
speaker_ids = self.hps.data.spk2id
|
||||
self.tts_to_file('This is a test.', speaker_ids['EN-Leon-V1_1'], quiet=True, format='wav')
|
||||
self.log('Model warmed up')
|
||||
|
||||
@staticmethod
|
||||
def audio_numpy_concat(segment_data_list, sr, speed=1.):
|
||||
audio_segments = []
|
||||
for segment_data in segment_data_list:
|
||||
audio_segments += segment_data.reshape(-1).tolist()
|
||||
audio_segments += [0] * int((sr * 0.05) / speed)
|
||||
audio_segments = np.array(audio_segments).astype(np.float32)
|
||||
return audio_segments
|
||||
|
||||
@staticmethod
|
||||
def split_sentences_into_pieces(text, language, quiet=False):
|
||||
texts = split_sentence(text, language_str=language)
|
||||
if not quiet:
|
||||
print(" > Text split to sentences.")
|
||||
print('\n'.join(texts))
|
||||
print(" > ===========================")
|
||||
return texts
|
||||
|
||||
def tts_iter(self, text, speaker_id, sdp_ratio=0.2, noise_scale=0.6, noise_scale_w=0.8, speed=1.0, pbar=None, position=None, quiet=False, stream=False):
|
||||
tic = time.perf_counter()
|
||||
self.log(f"Generating audio for:\n{text}")
|
||||
language = self.language
|
||||
|
||||
texts = self.split_sentences_into_pieces(text, language, quiet)
|
||||
|
||||
if pbar:
|
||||
tx = pbar(texts)
|
||||
else:
|
||||
if position:
|
||||
tx = tqdm(texts, position=position)
|
||||
elif quiet:
|
||||
tx = texts
|
||||
else:
|
||||
tx = tqdm(texts)
|
||||
for t in tx:
|
||||
if language in ['EN', 'ZH_MIX_EN']:
|
||||
t = re.sub(r'([a-z])([A-Z])', r'\1 \2', t)
|
||||
device = self.device
|
||||
bert, ja_bert, phones, tones, lang_ids = utils.get_text_for_tts_infer(t, language, self.hps, device, self.symbol_to_id)
|
||||
with torch.no_grad():
|
||||
x_tst = phones.to(device).unsqueeze(0)
|
||||
tones = tones.to(device).unsqueeze(0)
|
||||
lang_ids = lang_ids.to(device).unsqueeze(0)
|
||||
bert = bert.to(device).unsqueeze(0)
|
||||
ja_bert = ja_bert.to(device).unsqueeze(0)
|
||||
x_tst_lengths = torch.LongTensor([phones.size(0)]).to(device)
|
||||
del phones
|
||||
speakers = torch.LongTensor([speaker_id]).to(device)
|
||||
audio = self.model.infer(
|
||||
x_tst,
|
||||
x_tst_lengths,
|
||||
speakers,
|
||||
tones,
|
||||
lang_ids,
|
||||
bert,
|
||||
ja_bert,
|
||||
sdp_ratio=sdp_ratio,
|
||||
noise_scale=noise_scale,
|
||||
noise_scale_w=noise_scale_w,
|
||||
length_scale=1. / speed,
|
||||
)[0][0, 0].data.cpu().float().numpy()
|
||||
del x_tst, tones, lang_ids, bert, ja_bert, x_tst_lengths, speakers
|
||||
|
||||
audio_segments = []
|
||||
audio_segments += audio.reshape(-1).tolist()
|
||||
audio_segments += [0] * int((self.hps.data.sampling_rate * 0.05) / speed)
|
||||
audio_segments = np.array(audio_segments).astype(np.float32)
|
||||
|
||||
yield audio_segments
|
||||
|
||||
toc = time.perf_counter()
|
||||
self.log(f"Time taken to generate audio: {toc - tic:0.4f} seconds")
|
||||
|
||||
if self.device == 'cuda':
|
||||
torch.cuda.empty_cache()
|
||||
if self.device == 'mps':
|
||||
torch.mps.empty_cache()
|
||||
|
||||
def tts_to_file(self, text, speaker_id, output_path=None, sdp_ratio=0.2, noise_scale=0.6, noise_scale_w=0.8, speed=1.0, pbar=None, format=None, position=None, quiet=False, stream=False):
|
||||
audio_list = []
|
||||
for audio in self.tts_iter(
|
||||
text=text,
|
||||
speaker_id=speaker_id,
|
||||
sdp_ratio=sdp_ratio,
|
||||
noise_scale=noise_scale,
|
||||
noise_scale_w=noise_scale_w,
|
||||
speed=speed,
|
||||
pbar=pbar,
|
||||
position=position,
|
||||
quiet=quiet,
|
||||
stream=stream
|
||||
):
|
||||
audio_list.append(audio)
|
||||
|
||||
audio = np.concatenate(audio_list)
|
||||
|
||||
if output_path is None:
|
||||
return audio
|
||||
else:
|
||||
if format:
|
||||
soundfile.write(output_path, audio, self.hps.data.sampling_rate, format=format)
|
||||
else:
|
||||
soundfile.write(output_path, audio, self.hps.data.sampling_rate)
|
||||
|
||||
@staticmethod
|
||||
def log(*args, **kwargs):
|
||||
print('[TTS]', *args, **kwargs)
|
||||
@@ -0,0 +1,449 @@
|
||||
import math
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from . import commons
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LayerNorm(nn.Module):
|
||||
def __init__(self, channels, eps=1e-5):
|
||||
super().__init__()
|
||||
self.channels = channels
|
||||
self.eps = eps
|
||||
|
||||
self.gamma = nn.Parameter(torch.ones(channels))
|
||||
self.beta = nn.Parameter(torch.zeros(channels))
|
||||
|
||||
def forward(self, x):
|
||||
x = x.transpose(1, -1)
|
||||
x = F.layer_norm(x, (self.channels,), self.gamma, self.beta, self.eps)
|
||||
return x.transpose(1, -1)
|
||||
|
||||
|
||||
class Encoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
hidden_channels,
|
||||
filter_channels,
|
||||
n_heads,
|
||||
n_layers,
|
||||
kernel_size=1,
|
||||
p_dropout=0.0,
|
||||
window_size=4,
|
||||
isflow=True,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__()
|
||||
self.hidden_channels = hidden_channels
|
||||
self.filter_channels = filter_channels
|
||||
self.n_heads = n_heads
|
||||
self.n_layers = n_layers
|
||||
self.kernel_size = kernel_size
|
||||
self.p_dropout = p_dropout
|
||||
self.window_size = window_size
|
||||
|
||||
self.cond_layer_idx = self.n_layers
|
||||
if "gin_channels" in kwargs:
|
||||
self.gin_channels = kwargs["gin_channels"]
|
||||
if self.gin_channels != 0:
|
||||
self.spk_emb_linear = nn.Linear(self.gin_channels, self.hidden_channels)
|
||||
self.cond_layer_idx = (
|
||||
kwargs["cond_layer_idx"] if "cond_layer_idx" in kwargs else 2
|
||||
)
|
||||
assert (
|
||||
self.cond_layer_idx < self.n_layers
|
||||
), "cond_layer_idx should be less than n_layers"
|
||||
self.drop = nn.Dropout(p_dropout)
|
||||
self.attn_layers = nn.ModuleList()
|
||||
self.norm_layers_1 = nn.ModuleList()
|
||||
self.ffn_layers = nn.ModuleList()
|
||||
self.norm_layers_2 = nn.ModuleList()
|
||||
|
||||
for i in range(self.n_layers):
|
||||
self.attn_layers.append(
|
||||
MultiHeadAttention(
|
||||
hidden_channels,
|
||||
hidden_channels,
|
||||
n_heads,
|
||||
p_dropout=p_dropout,
|
||||
window_size=window_size,
|
||||
)
|
||||
)
|
||||
self.norm_layers_1.append(LayerNorm(hidden_channels))
|
||||
self.ffn_layers.append(
|
||||
FFN(
|
||||
hidden_channels,
|
||||
hidden_channels,
|
||||
filter_channels,
|
||||
kernel_size,
|
||||
p_dropout=p_dropout,
|
||||
)
|
||||
)
|
||||
self.norm_layers_2.append(LayerNorm(hidden_channels))
|
||||
|
||||
def forward(self, x, x_mask, g=None):
|
||||
attn_mask = x_mask.unsqueeze(2) * x_mask.unsqueeze(-1)
|
||||
x = x * x_mask
|
||||
for i in range(self.n_layers):
|
||||
if i == self.cond_layer_idx and g is not None:
|
||||
g = self.spk_emb_linear(g.transpose(1, 2))
|
||||
g = g.transpose(1, 2)
|
||||
x = x + g
|
||||
x = x * x_mask
|
||||
y = self.attn_layers[i](x, x, attn_mask)
|
||||
y = self.drop(y)
|
||||
x = self.norm_layers_1[i](x + y)
|
||||
|
||||
y = self.ffn_layers[i](x, x_mask)
|
||||
y = self.drop(y)
|
||||
x = self.norm_layers_2[i](x + y)
|
||||
x = x * x_mask
|
||||
return x
|
||||
|
||||
|
||||
class Decoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
hidden_channels,
|
||||
filter_channels,
|
||||
n_heads,
|
||||
n_layers,
|
||||
kernel_size=1,
|
||||
p_dropout=0.0,
|
||||
proximal_bias=False,
|
||||
proximal_init=True,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__()
|
||||
self.hidden_channels = hidden_channels
|
||||
self.filter_channels = filter_channels
|
||||
self.n_heads = n_heads
|
||||
self.n_layers = n_layers
|
||||
self.kernel_size = kernel_size
|
||||
self.p_dropout = p_dropout
|
||||
self.proximal_bias = proximal_bias
|
||||
self.proximal_init = proximal_init
|
||||
|
||||
self.drop = nn.Dropout(p_dropout)
|
||||
self.self_attn_layers = nn.ModuleList()
|
||||
self.norm_layers_0 = nn.ModuleList()
|
||||
self.encdec_attn_layers = nn.ModuleList()
|
||||
self.norm_layers_1 = nn.ModuleList()
|
||||
self.ffn_layers = nn.ModuleList()
|
||||
self.norm_layers_2 = nn.ModuleList()
|
||||
for i in range(self.n_layers):
|
||||
self.self_attn_layers.append(
|
||||
MultiHeadAttention(
|
||||
hidden_channels,
|
||||
hidden_channels,
|
||||
n_heads,
|
||||
p_dropout=p_dropout,
|
||||
proximal_bias=proximal_bias,
|
||||
proximal_init=proximal_init,
|
||||
)
|
||||
)
|
||||
self.norm_layers_0.append(LayerNorm(hidden_channels))
|
||||
self.encdec_attn_layers.append(
|
||||
MultiHeadAttention(
|
||||
hidden_channels, hidden_channels, n_heads, p_dropout=p_dropout
|
||||
)
|
||||
)
|
||||
self.norm_layers_1.append(LayerNorm(hidden_channels))
|
||||
self.ffn_layers.append(
|
||||
FFN(
|
||||
hidden_channels,
|
||||
hidden_channels,
|
||||
filter_channels,
|
||||
kernel_size,
|
||||
p_dropout=p_dropout,
|
||||
causal=True,
|
||||
)
|
||||
)
|
||||
self.norm_layers_2.append(LayerNorm(hidden_channels))
|
||||
|
||||
def forward(self, x, x_mask, h, h_mask):
|
||||
"""
|
||||
x: decoder input
|
||||
h: encoder output
|
||||
"""
|
||||
self_attn_mask = commons.subsequent_mask(x_mask.size(2)).to(
|
||||
device=x.device, dtype=x.dtype
|
||||
)
|
||||
encdec_attn_mask = h_mask.unsqueeze(2) * x_mask.unsqueeze(-1)
|
||||
x = x * x_mask
|
||||
for i in range(self.n_layers):
|
||||
y = self.self_attn_layers[i](x, x, self_attn_mask)
|
||||
y = self.drop(y)
|
||||
x = self.norm_layers_0[i](x + y)
|
||||
|
||||
y = self.encdec_attn_layers[i](x, h, encdec_attn_mask)
|
||||
y = self.drop(y)
|
||||
x = self.norm_layers_1[i](x + y)
|
||||
|
||||
y = self.ffn_layers[i](x, x_mask)
|
||||
y = self.drop(y)
|
||||
x = self.norm_layers_2[i](x + y)
|
||||
x = x * x_mask
|
||||
return x
|
||||
|
||||
|
||||
class MultiHeadAttention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
channels,
|
||||
out_channels,
|
||||
n_heads,
|
||||
p_dropout=0.0,
|
||||
window_size=None,
|
||||
heads_share=True,
|
||||
block_length=None,
|
||||
proximal_bias=False,
|
||||
proximal_init=False,
|
||||
):
|
||||
super().__init__()
|
||||
assert channels % n_heads == 0
|
||||
|
||||
self.channels = channels
|
||||
self.out_channels = out_channels
|
||||
self.n_heads = n_heads
|
||||
self.p_dropout = p_dropout
|
||||
self.window_size = window_size
|
||||
self.heads_share = heads_share
|
||||
self.block_length = block_length
|
||||
self.proximal_bias = proximal_bias
|
||||
self.proximal_init = proximal_init
|
||||
self.attn = None
|
||||
|
||||
self.k_channels = channels // n_heads
|
||||
self.conv_q = nn.Conv1d(channels, channels, 1)
|
||||
self.conv_k = nn.Conv1d(channels, channels, 1)
|
||||
self.conv_v = nn.Conv1d(channels, channels, 1)
|
||||
self.conv_o = nn.Conv1d(channels, out_channels, 1)
|
||||
self.drop = nn.Dropout(p_dropout)
|
||||
|
||||
if window_size is not None:
|
||||
n_heads_rel = 1 if heads_share else n_heads
|
||||
rel_stddev = self.k_channels**-0.5
|
||||
self.emb_rel_k = nn.Parameter(
|
||||
torch.randn(n_heads_rel, window_size * 2 + 1, self.k_channels)
|
||||
* rel_stddev
|
||||
)
|
||||
self.emb_rel_v = nn.Parameter(
|
||||
torch.randn(n_heads_rel, window_size * 2 + 1, self.k_channels)
|
||||
* rel_stddev
|
||||
)
|
||||
|
||||
nn.init.xavier_uniform_(self.conv_q.weight)
|
||||
nn.init.xavier_uniform_(self.conv_k.weight)
|
||||
nn.init.xavier_uniform_(self.conv_v.weight)
|
||||
if proximal_init:
|
||||
with torch.no_grad():
|
||||
self.conv_k.weight.copy_(self.conv_q.weight)
|
||||
self.conv_k.bias.copy_(self.conv_q.bias)
|
||||
|
||||
def forward(self, x, c, attn_mask=None):
|
||||
q = self.conv_q(x)
|
||||
k = self.conv_k(c)
|
||||
v = self.conv_v(c)
|
||||
|
||||
x, self.attn = self.attention(q, k, v, mask=attn_mask)
|
||||
|
||||
x = self.conv_o(x)
|
||||
return x
|
||||
|
||||
def attention(self, query, key, value, mask=None):
|
||||
# reshape [b, d, t] -> [b, n_h, t, d_k]
|
||||
b, d, t_s, t_t = (*key.size(), query.size(2))
|
||||
query = query.view(b, self.n_heads, self.k_channels, t_t).transpose(2, 3)
|
||||
key = key.view(b, self.n_heads, self.k_channels, t_s).transpose(2, 3)
|
||||
value = value.view(b, self.n_heads, self.k_channels, t_s).transpose(2, 3)
|
||||
|
||||
scores = torch.matmul(query / math.sqrt(self.k_channels), key.transpose(-2, -1))
|
||||
if self.window_size is not None:
|
||||
assert (
|
||||
t_s == t_t
|
||||
), "Relative attention is only available for self-attention."
|
||||
key_relative_embeddings = self._get_relative_embeddings(self.emb_rel_k, t_s)
|
||||
rel_logits = self._matmul_with_relative_keys(
|
||||
query / math.sqrt(self.k_channels), key_relative_embeddings
|
||||
)
|
||||
scores_local = self._relative_position_to_absolute_position(rel_logits)
|
||||
scores = scores + scores_local
|
||||
if self.proximal_bias:
|
||||
assert t_s == t_t, "Proximal bias is only available for self-attention."
|
||||
scores = scores + self._attention_bias_proximal(t_s).to(
|
||||
device=scores.device, dtype=scores.dtype
|
||||
)
|
||||
if mask is not None:
|
||||
scores = scores.masked_fill(mask == 0, -1e4)
|
||||
if self.block_length is not None:
|
||||
assert (
|
||||
t_s == t_t
|
||||
), "Local attention is only available for self-attention."
|
||||
block_mask = (
|
||||
torch.ones_like(scores)
|
||||
.triu(-self.block_length)
|
||||
.tril(self.block_length)
|
||||
)
|
||||
scores = scores.masked_fill(block_mask == 0, -1e4)
|
||||
p_attn = F.softmax(scores, dim=-1) # [b, n_h, t_t, t_s]
|
||||
p_attn = self.drop(p_attn)
|
||||
output = torch.matmul(p_attn, value)
|
||||
if self.window_size is not None:
|
||||
relative_weights = self._absolute_position_to_relative_position(p_attn)
|
||||
value_relative_embeddings = self._get_relative_embeddings(
|
||||
self.emb_rel_v, t_s
|
||||
)
|
||||
output = output + self._matmul_with_relative_values(
|
||||
relative_weights, value_relative_embeddings
|
||||
)
|
||||
output = (
|
||||
output.transpose(2, 3).contiguous().view(b, d, t_t)
|
||||
) # [b, n_h, t_t, d_k] -> [b, d, t_t]
|
||||
return output, p_attn
|
||||
|
||||
def _matmul_with_relative_values(self, x, y):
|
||||
"""
|
||||
x: [b, h, l, m]
|
||||
y: [h or 1, m, d]
|
||||
ret: [b, h, l, d]
|
||||
"""
|
||||
ret = torch.matmul(x, y.unsqueeze(0))
|
||||
return ret
|
||||
|
||||
def _matmul_with_relative_keys(self, x, y):
|
||||
"""
|
||||
x: [b, h, l, d]
|
||||
y: [h or 1, m, d]
|
||||
ret: [b, h, l, m]
|
||||
"""
|
||||
ret = torch.matmul(x, y.unsqueeze(0).transpose(-2, -1))
|
||||
return ret
|
||||
|
||||
def _get_relative_embeddings(self, relative_embeddings, length):
|
||||
2 * self.window_size + 1
|
||||
# Pad first before slice to avoid using cond ops.
|
||||
pad_length = max(length - (self.window_size + 1), 0)
|
||||
slice_start_position = max((self.window_size + 1) - length, 0)
|
||||
slice_end_position = slice_start_position + 2 * length - 1
|
||||
if pad_length > 0:
|
||||
padded_relative_embeddings = F.pad(
|
||||
relative_embeddings,
|
||||
commons.convert_pad_shape([[0, 0], [pad_length, pad_length], [0, 0]]),
|
||||
)
|
||||
else:
|
||||
padded_relative_embeddings = relative_embeddings
|
||||
used_relative_embeddings = padded_relative_embeddings[
|
||||
:, slice_start_position:slice_end_position
|
||||
]
|
||||
return used_relative_embeddings
|
||||
|
||||
def _relative_position_to_absolute_position(self, x):
|
||||
"""
|
||||
x: [b, h, l, 2*l-1]
|
||||
ret: [b, h, l, l]
|
||||
"""
|
||||
batch, heads, length, _ = x.size()
|
||||
# Concat columns of pad to shift from relative to absolute indexing.
|
||||
x = F.pad(x, commons.convert_pad_shape([[0, 0], [0, 0], [0, 0], [0, 1]]))
|
||||
|
||||
# Concat extra elements so to add up to shape (len+1, 2*len-1).
|
||||
x_flat = x.view([batch, heads, length * 2 * length])
|
||||
x_flat = F.pad(
|
||||
x_flat, commons.convert_pad_shape([[0, 0], [0, 0], [0, length - 1]])
|
||||
)
|
||||
|
||||
# Reshape and slice out the padded elements.
|
||||
x_final = x_flat.view([batch, heads, length + 1, 2 * length - 1])[
|
||||
:, :, :length, length - 1 :
|
||||
]
|
||||
return x_final
|
||||
|
||||
def _absolute_position_to_relative_position(self, x):
|
||||
"""
|
||||
x: [b, h, l, l]
|
||||
ret: [b, h, l, 2*l-1]
|
||||
"""
|
||||
batch, heads, length, _ = x.size()
|
||||
# pad along column
|
||||
x = F.pad(
|
||||
x, commons.convert_pad_shape([[0, 0], [0, 0], [0, 0], [0, length - 1]])
|
||||
)
|
||||
x_flat = x.view([batch, heads, length**2 + length * (length - 1)])
|
||||
# add 0's in the beginning that will skew the elements after reshape
|
||||
x_flat = F.pad(x_flat, commons.convert_pad_shape([[0, 0], [0, 0], [length, 0]]))
|
||||
x_final = x_flat.view([batch, heads, length, 2 * length])[:, :, :, 1:]
|
||||
return x_final
|
||||
|
||||
def _attention_bias_proximal(self, length):
|
||||
"""Bias for self-attention to encourage attention to close positions.
|
||||
Args:
|
||||
length: an integer scalar.
|
||||
Returns:
|
||||
a Tensor with shape [1, 1, length, length]
|
||||
"""
|
||||
r = torch.arange(length, dtype=torch.float32)
|
||||
diff = torch.unsqueeze(r, 0) - torch.unsqueeze(r, 1)
|
||||
return torch.unsqueeze(torch.unsqueeze(-torch.log1p(torch.abs(diff)), 0), 0)
|
||||
|
||||
|
||||
class FFN(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
filter_channels,
|
||||
kernel_size,
|
||||
p_dropout=0.0,
|
||||
activation=None,
|
||||
causal=False,
|
||||
):
|
||||
super().__init__()
|
||||
self.in_channels = in_channels
|
||||
self.out_channels = out_channels
|
||||
self.filter_channels = filter_channels
|
||||
self.kernel_size = kernel_size
|
||||
self.p_dropout = p_dropout
|
||||
self.activation = activation
|
||||
self.causal = causal
|
||||
|
||||
if causal:
|
||||
self.padding = self._causal_padding
|
||||
else:
|
||||
self.padding = self._same_padding
|
||||
|
||||
self.conv_1 = nn.Conv1d(in_channels, filter_channels, kernel_size)
|
||||
self.conv_2 = nn.Conv1d(filter_channels, out_channels, kernel_size)
|
||||
self.drop = nn.Dropout(p_dropout)
|
||||
|
||||
def forward(self, x, x_mask):
|
||||
x = self.conv_1(self.padding(x * x_mask))
|
||||
if self.activation == "gelu":
|
||||
x = x * torch.sigmoid(1.702 * x)
|
||||
else:
|
||||
x = torch.relu(x)
|
||||
x = self.drop(x)
|
||||
x = self.conv_2(self.padding(x * x_mask))
|
||||
return x * x_mask
|
||||
|
||||
def _causal_padding(self, x):
|
||||
if self.kernel_size == 1:
|
||||
return x
|
||||
pad_l = self.kernel_size - 1
|
||||
pad_r = 0
|
||||
padding = [[0, 0], [0, 0], [pad_l, pad_r]]
|
||||
x = F.pad(x, commons.convert_pad_shape(padding))
|
||||
return x
|
||||
|
||||
def _same_padding(self, x):
|
||||
if self.kernel_size == 1:
|
||||
return x
|
||||
pad_l = (self.kernel_size - 1) // 2
|
||||
pad_r = self.kernel_size // 2
|
||||
padding = [[0, 0], [0, 0], [pad_l, pad_r]]
|
||||
x = F.pad(x, commons.convert_pad_shape(padding))
|
||||
return x
|
||||
@@ -0,0 +1,150 @@
|
||||
import math
|
||||
import torch
|
||||
from torch.nn import functional as F
|
||||
|
||||
|
||||
def init_weights(m, mean=0.0, std=0.01):
|
||||
classname = m.__class__.__name__
|
||||
if classname.find("Conv") != -1:
|
||||
m.weight.data.normal_(mean, std)
|
||||
|
||||
|
||||
def get_padding(kernel_size, dilation=1):
|
||||
return int((kernel_size * dilation - dilation) / 2)
|
||||
|
||||
|
||||
def convert_pad_shape(pad_shape):
|
||||
layer = pad_shape[::-1]
|
||||
pad_shape = [item for sublist in layer for item in sublist]
|
||||
return pad_shape
|
||||
|
||||
|
||||
def intersperse(lst, item):
|
||||
result = [item] * (len(lst) * 2 + 1)
|
||||
result[1::2] = lst
|
||||
return result
|
||||
|
||||
|
||||
def kl_divergence(m_p, logs_p, m_q, logs_q):
|
||||
"""KL(P||Q)"""
|
||||
kl = (logs_q - logs_p) - 0.5
|
||||
kl += (
|
||||
0.5 * (torch.exp(2.0 * logs_p) + ((m_p - m_q) ** 2)) * torch.exp(-2.0 * logs_q)
|
||||
)
|
||||
return kl
|
||||
|
||||
|
||||
def rand_gumbel(shape):
|
||||
"""Sample from the Gumbel distribution, protect from overflows."""
|
||||
uniform_samples = torch.rand(shape) * 0.99998 + 0.00001
|
||||
return -torch.log(-torch.log(uniform_samples))
|
||||
|
||||
|
||||
def rand_gumbel_like(x):
|
||||
g = rand_gumbel(x.size()).to(dtype=x.dtype, device=x.device)
|
||||
return g
|
||||
|
||||
|
||||
def slice_segments(x, ids_str, segment_size=4):
|
||||
ret = torch.zeros_like(x[:, :, :segment_size])
|
||||
for i in range(x.size(0)):
|
||||
idx_str = ids_str[i]
|
||||
idx_end = idx_str + segment_size
|
||||
ret[i] = x[i, :, idx_str:idx_end]
|
||||
return ret
|
||||
|
||||
|
||||
def rand_slice_segments(x, x_lengths=None, segment_size=4):
|
||||
b, d, t = x.size()
|
||||
if x_lengths is None:
|
||||
x_lengths = t
|
||||
ids_str_max = x_lengths - segment_size + 1
|
||||
ids_str = (torch.rand([b]).to(device=x.device) * ids_str_max).to(dtype=torch.long)
|
||||
ret = slice_segments(x, ids_str, segment_size)
|
||||
return ret, ids_str
|
||||
|
||||
|
||||
def get_timing_signal_1d(length, channels, min_timescale=1.0, max_timescale=1.0e4):
|
||||
position = torch.arange(length, dtype=torch.float)
|
||||
num_timescales = channels // 2
|
||||
log_timescale_increment = math.log(float(max_timescale) / float(min_timescale)) / (
|
||||
num_timescales - 1
|
||||
)
|
||||
inv_timescales = min_timescale * torch.exp(
|
||||
torch.arange(num_timescales, dtype=torch.float) * -log_timescale_increment
|
||||
)
|
||||
scaled_time = position.unsqueeze(0) * inv_timescales.unsqueeze(1)
|
||||
signal = torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], 0)
|
||||
signal = F.pad(signal, [0, 0, 0, channels % 2])
|
||||
signal = signal.view(1, channels, length)
|
||||
return signal
|
||||
|
||||
|
||||
def add_timing_signal_1d(x, min_timescale=1.0, max_timescale=1.0e4):
|
||||
b, channels, length = x.size()
|
||||
signal = get_timing_signal_1d(length, channels, min_timescale, max_timescale)
|
||||
return x + signal.to(dtype=x.dtype, device=x.device)
|
||||
|
||||
|
||||
def cat_timing_signal_1d(x, min_timescale=1.0, max_timescale=1.0e4, axis=1):
|
||||
b, channels, length = x.size()
|
||||
signal = get_timing_signal_1d(length, channels, min_timescale, max_timescale)
|
||||
return torch.cat([x, signal.to(dtype=x.dtype, device=x.device)], axis)
|
||||
|
||||
|
||||
def subsequent_mask(length):
|
||||
mask = torch.tril(torch.ones(length, length)).unsqueeze(0).unsqueeze(0)
|
||||
return mask
|
||||
|
||||
|
||||
def convert_pad_shape(pad_shape):
|
||||
layer = pad_shape[::-1]
|
||||
pad_shape = [item for sublist in layer for item in sublist]
|
||||
return pad_shape
|
||||
|
||||
|
||||
def shift_1d(x):
|
||||
x = F.pad(x, convert_pad_shape([[0, 0], [0, 0], [1, 0]]))[:, :, :-1]
|
||||
return x
|
||||
|
||||
|
||||
def sequence_mask(length, max_length=None):
|
||||
if max_length is None:
|
||||
max_length = length.max()
|
||||
x = torch.arange(max_length, dtype=length.dtype, device=length.device)
|
||||
return x.unsqueeze(0) < length.unsqueeze(1)
|
||||
|
||||
|
||||
def generate_path(duration, mask):
|
||||
"""
|
||||
duration: [b, 1, t_x]
|
||||
mask: [b, 1, t_y, t_x]
|
||||
"""
|
||||
|
||||
b, _, t_y, t_x = mask.shape
|
||||
cum_duration = torch.cumsum(duration, -1)
|
||||
|
||||
cum_duration_flat = cum_duration.view(b * t_x)
|
||||
path = sequence_mask(cum_duration_flat, t_y).to(mask.dtype)
|
||||
path = path.view(b, t_x, t_y)
|
||||
path = path - F.pad(path, convert_pad_shape([[0, 0], [1, 0], [0, 0]]))[:, :-1]
|
||||
path = path.unsqueeze(1).transpose(2, 3) * mask
|
||||
return path
|
||||
|
||||
|
||||
def clip_grad_value_(parameters, clip_value, norm_type=2):
|
||||
if isinstance(parameters, torch.Tensor):
|
||||
parameters = [parameters]
|
||||
parameters = list(filter(lambda p: p.grad is not None, parameters))
|
||||
norm_type = float(norm_type)
|
||||
if clip_value is not None:
|
||||
clip_value = float(clip_value)
|
||||
|
||||
total_norm = 0
|
||||
for p in parameters:
|
||||
param_norm = p.grad.data.norm(norm_type)
|
||||
total_norm += param_norm.item() ** norm_type
|
||||
if clip_value is not None:
|
||||
p.grad.data.clamp_(min=-clip_value, max=clip_value)
|
||||
total_norm = total_norm ** (1.0 / norm_type)
|
||||
return total_norm
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,571 @@
|
||||
import math
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from torch.nn import Conv1d
|
||||
from torch.nn.utils import weight_norm, remove_weight_norm
|
||||
|
||||
from . import commons
|
||||
from .commons import init_weights, get_padding
|
||||
from .transforms import piecewise_rational_quadratic_transform
|
||||
from .attentions import Encoder
|
||||
|
||||
LRELU_SLOPE = 0.1
|
||||
|
||||
|
||||
class LayerNorm(nn.Module):
|
||||
def __init__(self, channels, eps=1e-5):
|
||||
super().__init__()
|
||||
self.channels = channels
|
||||
self.eps = eps
|
||||
|
||||
self.gamma = nn.Parameter(torch.ones(channels))
|
||||
self.beta = nn.Parameter(torch.zeros(channels))
|
||||
|
||||
def forward(self, x):
|
||||
x = x.transpose(1, -1)
|
||||
x = F.layer_norm(x, (self.channels,), self.gamma, self.beta, self.eps)
|
||||
return x.transpose(1, -1)
|
||||
|
||||
|
||||
class ConvReluNorm(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels,
|
||||
hidden_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
n_layers,
|
||||
p_dropout,
|
||||
):
|
||||
super().__init__()
|
||||
self.in_channels = in_channels
|
||||
self.hidden_channels = hidden_channels
|
||||
self.out_channels = out_channels
|
||||
self.kernel_size = kernel_size
|
||||
self.n_layers = n_layers
|
||||
self.p_dropout = p_dropout
|
||||
assert n_layers > 1, "Number of layers should be larger than 0."
|
||||
|
||||
self.conv_layers = nn.ModuleList()
|
||||
self.norm_layers = nn.ModuleList()
|
||||
self.conv_layers.append(
|
||||
nn.Conv1d(
|
||||
in_channels, hidden_channels, kernel_size, padding=kernel_size // 2
|
||||
)
|
||||
)
|
||||
self.norm_layers.append(LayerNorm(hidden_channels))
|
||||
self.relu_drop = nn.Sequential(nn.ReLU(), nn.Dropout(p_dropout))
|
||||
for _ in range(n_layers - 1):
|
||||
self.conv_layers.append(
|
||||
nn.Conv1d(
|
||||
hidden_channels,
|
||||
hidden_channels,
|
||||
kernel_size,
|
||||
padding=kernel_size // 2,
|
||||
)
|
||||
)
|
||||
self.norm_layers.append(LayerNorm(hidden_channels))
|
||||
self.proj = nn.Conv1d(hidden_channels, out_channels, 1)
|
||||
self.proj.weight.data.zero_()
|
||||
self.proj.bias.data.zero_()
|
||||
|
||||
def forward(self, x, x_mask):
|
||||
x_org = x
|
||||
for i in range(self.n_layers):
|
||||
x = self.conv_layers[i](x * x_mask)
|
||||
x = self.norm_layers[i](x)
|
||||
x = self.relu_drop(x)
|
||||
x = x_org + self.proj(x)
|
||||
return x * x_mask
|
||||
|
||||
|
||||
class DDSConv(nn.Module):
|
||||
"""
|
||||
Dialted and Depth-Separable Convolution
|
||||
"""
|
||||
|
||||
def __init__(self, channels, kernel_size, n_layers, p_dropout=0.0):
|
||||
super().__init__()
|
||||
self.channels = channels
|
||||
self.kernel_size = kernel_size
|
||||
self.n_layers = n_layers
|
||||
self.p_dropout = p_dropout
|
||||
|
||||
self.drop = nn.Dropout(p_dropout)
|
||||
self.convs_sep = nn.ModuleList()
|
||||
self.convs_1x1 = nn.ModuleList()
|
||||
self.norms_1 = nn.ModuleList()
|
||||
self.norms_2 = nn.ModuleList()
|
||||
for i in range(n_layers):
|
||||
dilation = kernel_size**i
|
||||
padding = (kernel_size * dilation - dilation) // 2
|
||||
self.convs_sep.append(
|
||||
nn.Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
groups=channels,
|
||||
dilation=dilation,
|
||||
padding=padding,
|
||||
)
|
||||
)
|
||||
self.convs_1x1.append(nn.Conv1d(channels, channels, 1))
|
||||
self.norms_1.append(LayerNorm(channels))
|
||||
self.norms_2.append(LayerNorm(channels))
|
||||
|
||||
def forward(self, x, x_mask, g=None):
|
||||
if g is not None:
|
||||
x = x + g
|
||||
for i in range(self.n_layers):
|
||||
y = self.convs_sep[i](x * x_mask)
|
||||
y = self.norms_1[i](y)
|
||||
y = F.gelu(y)
|
||||
y = self.convs_1x1[i](y)
|
||||
y = self.norms_2[i](y)
|
||||
y = F.gelu(y)
|
||||
y = self.drop(y)
|
||||
x = x + y
|
||||
return x * x_mask
|
||||
|
||||
|
||||
class WN(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
hidden_channels,
|
||||
kernel_size,
|
||||
dilation_rate,
|
||||
n_layers,
|
||||
gin_channels=0,
|
||||
p_dropout=0,
|
||||
):
|
||||
super(WN, self).__init__()
|
||||
assert kernel_size % 2 == 1
|
||||
self.hidden_channels = hidden_channels
|
||||
self.kernel_size = (kernel_size,)
|
||||
self.dilation_rate = dilation_rate
|
||||
self.n_layers = n_layers
|
||||
self.gin_channels = gin_channels
|
||||
self.p_dropout = p_dropout
|
||||
|
||||
self.in_layers = torch.nn.ModuleList()
|
||||
self.res_skip_layers = torch.nn.ModuleList()
|
||||
self.drop = nn.Dropout(p_dropout)
|
||||
|
||||
if gin_channels != 0:
|
||||
cond_layer = torch.nn.Conv1d(
|
||||
gin_channels, 2 * hidden_channels * n_layers, 1
|
||||
)
|
||||
self.cond_layer = torch.nn.utils.weight_norm(cond_layer, name="weight")
|
||||
|
||||
for i in range(n_layers):
|
||||
dilation = dilation_rate**i
|
||||
padding = int((kernel_size * dilation - dilation) / 2)
|
||||
in_layer = torch.nn.Conv1d(
|
||||
hidden_channels,
|
||||
2 * hidden_channels,
|
||||
kernel_size,
|
||||
dilation=dilation,
|
||||
padding=padding,
|
||||
)
|
||||
in_layer = torch.nn.utils.weight_norm(in_layer, name="weight")
|
||||
self.in_layers.append(in_layer)
|
||||
|
||||
# last one is not necessary
|
||||
if i < n_layers - 1:
|
||||
res_skip_channels = 2 * hidden_channels
|
||||
else:
|
||||
res_skip_channels = hidden_channels
|
||||
|
||||
res_skip_layer = torch.nn.Conv1d(hidden_channels, res_skip_channels, 1)
|
||||
res_skip_layer = torch.nn.utils.weight_norm(res_skip_layer, name="weight")
|
||||
self.res_skip_layers.append(res_skip_layer)
|
||||
|
||||
def remove_weight_norm(self):
|
||||
if self.gin_channels != 0:
|
||||
torch.nn.utils.remove_weight_norm(self.cond_layer)
|
||||
for l in self.in_layers:
|
||||
torch.nn.utils.remove_weight_norm(l)
|
||||
for l in self.res_skip_layers:
|
||||
torch.nn.utils.remove_weight_norm(l)
|
||||
|
||||
|
||||
class ResBlock1(torch.nn.Module):
|
||||
def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)):
|
||||
super(ResBlock1, self).__init__()
|
||||
self.convs1 = nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[0],
|
||||
padding=get_padding(kernel_size, dilation[0]),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[1],
|
||||
padding=get_padding(kernel_size, dilation[1]),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[2],
|
||||
padding=get_padding(kernel_size, dilation[2]),
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
self.convs1.apply(init_weights)
|
||||
|
||||
self.convs2 = nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=1,
|
||||
padding=get_padding(kernel_size, 1),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=1,
|
||||
padding=get_padding(kernel_size, 1),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=1,
|
||||
padding=get_padding(kernel_size, 1),
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
self.convs2.apply(init_weights)
|
||||
|
||||
def forward(self, x, x_mask=None):
|
||||
for c1, c2 in zip(self.convs1, self.convs2):
|
||||
xt = F.leaky_relu(x, LRELU_SLOPE)
|
||||
if x_mask is not None:
|
||||
xt = xt * x_mask
|
||||
xt = c1(xt)
|
||||
xt = F.leaky_relu(xt, LRELU_SLOPE)
|
||||
if x_mask is not None:
|
||||
xt = xt * x_mask
|
||||
xt = c2(xt)
|
||||
x = xt + x
|
||||
if x_mask is not None:
|
||||
x = x * x_mask
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
for l in self.convs1:
|
||||
remove_weight_norm(l)
|
||||
for l in self.convs2:
|
||||
remove_weight_norm(l)
|
||||
|
||||
|
||||
class ResBlock2(torch.nn.Module):
|
||||
def __init__(self, channels, kernel_size=3, dilation=(1, 3)):
|
||||
super(ResBlock2, self).__init__()
|
||||
self.convs = nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[0],
|
||||
padding=get_padding(kernel_size, dilation[0]),
|
||||
)
|
||||
),
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
1,
|
||||
dilation=dilation[1],
|
||||
padding=get_padding(kernel_size, dilation[1]),
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
self.convs.apply(init_weights)
|
||||
|
||||
def forward(self, x, x_mask=None):
|
||||
for c in self.convs:
|
||||
xt = F.leaky_relu(x, LRELU_SLOPE)
|
||||
if x_mask is not None:
|
||||
xt = xt * x_mask
|
||||
xt = c(xt)
|
||||
x = xt + x
|
||||
if x_mask is not None:
|
||||
x = x * x_mask
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
for l in self.convs:
|
||||
remove_weight_norm(l)
|
||||
|
||||
|
||||
class Log(nn.Module):
|
||||
def forward(self, x, x_mask, reverse=False, **kwargs):
|
||||
if not reverse:
|
||||
y = torch.log(torch.clamp_min(x, 1e-5)) * x_mask
|
||||
logdet = torch.sum(-y, [1, 2])
|
||||
return y, logdet
|
||||
else:
|
||||
x = torch.exp(x) * x_mask
|
||||
return x
|
||||
|
||||
|
||||
class Flip(nn.Module):
|
||||
def forward(self, x, *args, reverse=False, **kwargs):
|
||||
x = torch.flip(x, [1])
|
||||
if not reverse:
|
||||
logdet = torch.zeros(x.size(0)).to(dtype=x.dtype, device=x.device)
|
||||
return x, logdet
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
class ElementwiseAffine(nn.Module):
|
||||
def __init__(self, channels):
|
||||
super().__init__()
|
||||
self.channels = channels
|
||||
self.m = nn.Parameter(torch.zeros(channels, 1))
|
||||
self.logs = nn.Parameter(torch.zeros(channels, 1))
|
||||
|
||||
def forward(self, x, x_mask, reverse=False, **kwargs):
|
||||
if not reverse:
|
||||
y = self.m + torch.exp(self.logs) * x
|
||||
y = y * x_mask
|
||||
logdet = torch.sum(self.logs * x_mask, [1, 2])
|
||||
return y, logdet
|
||||
else:
|
||||
x = (x - self.m) * torch.exp(-self.logs) * x_mask
|
||||
return x
|
||||
|
||||
|
||||
class ResidualCouplingLayer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
channels,
|
||||
hidden_channels,
|
||||
kernel_size,
|
||||
dilation_rate,
|
||||
n_layers,
|
||||
p_dropout=0,
|
||||
gin_channels=0,
|
||||
mean_only=False,
|
||||
):
|
||||
assert channels % 2 == 0, "channels should be divisible by 2"
|
||||
super().__init__()
|
||||
self.channels = channels
|
||||
self.hidden_channels = hidden_channels
|
||||
self.kernel_size = kernel_size
|
||||
self.dilation_rate = dilation_rate
|
||||
self.n_layers = n_layers
|
||||
self.half_channels = channels // 2
|
||||
self.mean_only = mean_only
|
||||
|
||||
self.pre = nn.Conv1d(self.half_channels, hidden_channels, 1)
|
||||
self.enc = WN(
|
||||
hidden_channels,
|
||||
kernel_size,
|
||||
dilation_rate,
|
||||
n_layers,
|
||||
p_dropout=p_dropout,
|
||||
gin_channels=gin_channels,
|
||||
)
|
||||
self.post = nn.Conv1d(hidden_channels, self.half_channels * (2 - mean_only), 1)
|
||||
self.post.weight.data.zero_()
|
||||
self.post.bias.data.zero_()
|
||||
|
||||
def forward(self, x, x_mask, g=None, reverse=False):
|
||||
x0, x1 = torch.split(x, [self.half_channels] * 2, 1)
|
||||
h = self.pre(x0) * x_mask
|
||||
h = self.enc(h, x_mask, g=g)
|
||||
stats = self.post(h) * x_mask
|
||||
if not self.mean_only:
|
||||
m, logs = torch.split(stats, [self.half_channels] * 2, 1)
|
||||
else:
|
||||
m = stats
|
||||
logs = torch.zeros_like(m)
|
||||
|
||||
if not reverse:
|
||||
x1 = m + x1 * torch.exp(logs) * x_mask
|
||||
x = torch.cat([x0, x1], 1)
|
||||
logdet = torch.sum(logs, [1, 2])
|
||||
return x, logdet
|
||||
else:
|
||||
x1 = (x1 - m) * torch.exp(-logs) * x_mask
|
||||
x = torch.cat([x0, x1], 1)
|
||||
return x
|
||||
|
||||
|
||||
class ConvFlow(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels,
|
||||
filter_channels,
|
||||
kernel_size,
|
||||
n_layers,
|
||||
num_bins=10,
|
||||
tail_bound=5.0,
|
||||
):
|
||||
super().__init__()
|
||||
self.in_channels = in_channels
|
||||
self.filter_channels = filter_channels
|
||||
self.kernel_size = kernel_size
|
||||
self.n_layers = n_layers
|
||||
self.num_bins = num_bins
|
||||
self.tail_bound = tail_bound
|
||||
self.half_channels = in_channels // 2
|
||||
|
||||
self.pre = nn.Conv1d(self.half_channels, filter_channels, 1)
|
||||
self.convs = DDSConv(filter_channels, kernel_size, n_layers, p_dropout=0.0)
|
||||
self.proj = nn.Conv1d(
|
||||
filter_channels, self.half_channels * (num_bins * 3 - 1), 1
|
||||
)
|
||||
self.proj.weight.data.zero_()
|
||||
self.proj.bias.data.zero_()
|
||||
|
||||
def forward(self, x, x_mask, g=None, reverse=False):
|
||||
x0, x1 = torch.split(x, [self.half_channels] * 2, 1)
|
||||
h = self.pre(x0)
|
||||
h = self.convs(h, x_mask, g=g)
|
||||
h = self.proj(h) * x_mask
|
||||
|
||||
b, c, t = x0.shape
|
||||
h = h.reshape(b, c, -1, t).permute(0, 1, 3, 2) # [b, cx?, t] -> [b, c, t, ?]
|
||||
|
||||
unnormalized_widths = h[..., : self.num_bins] / math.sqrt(self.filter_channels)
|
||||
unnormalized_heights = h[..., self.num_bins : 2 * self.num_bins] / math.sqrt(
|
||||
self.filter_channels
|
||||
)
|
||||
unnormalized_derivatives = h[..., 2 * self.num_bins :]
|
||||
|
||||
x1, logabsdet = piecewise_rational_quadratic_transform(
|
||||
x1,
|
||||
unnormalized_widths,
|
||||
unnormalized_heights,
|
||||
unnormalized_derivatives,
|
||||
inverse=reverse,
|
||||
tails="linear",
|
||||
tail_bound=self.tail_bound,
|
||||
)
|
||||
|
||||
x = torch.cat([x0, x1], 1) * x_mask
|
||||
logdet = torch.sum(logabsdet * x_mask, [1, 2])
|
||||
if not reverse:
|
||||
return x, logdet
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
class TransformerCouplingLayer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
channels,
|
||||
hidden_channels,
|
||||
kernel_size,
|
||||
n_layers,
|
||||
n_heads,
|
||||
p_dropout=0,
|
||||
filter_channels=0,
|
||||
mean_only=False,
|
||||
wn_sharing_parameter=None,
|
||||
gin_channels=0,
|
||||
):
|
||||
assert n_layers == 3, n_layers
|
||||
assert channels % 2 == 0, "channels should be divisible by 2"
|
||||
super().__init__()
|
||||
self.channels = channels
|
||||
self.hidden_channels = hidden_channels
|
||||
self.kernel_size = kernel_size
|
||||
self.n_layers = n_layers
|
||||
self.half_channels = channels // 2
|
||||
self.mean_only = mean_only
|
||||
|
||||
self.pre = nn.Conv1d(self.half_channels, hidden_channels, 1)
|
||||
self.enc = (
|
||||
Encoder(
|
||||
hidden_channels,
|
||||
filter_channels,
|
||||
n_heads,
|
||||
n_layers,
|
||||
kernel_size,
|
||||
p_dropout,
|
||||
isflow=True,
|
||||
gin_channels=gin_channels,
|
||||
)
|
||||
if wn_sharing_parameter is None
|
||||
else wn_sharing_parameter
|
||||
)
|
||||
self.post = nn.Conv1d(hidden_channels, self.half_channels * (2 - mean_only), 1)
|
||||
self.post.weight.data.zero_()
|
||||
self.post.bias.data.zero_()
|
||||
|
||||
def forward(self, x, x_mask, g=None, reverse=False):
|
||||
x0, x1 = torch.split(x, [self.half_channels] * 2, 1)
|
||||
h = self.pre(x0) * x_mask
|
||||
h = self.enc(h, x_mask, g=g)
|
||||
stats = self.post(h) * x_mask
|
||||
if not self.mean_only:
|
||||
m, logs = torch.split(stats, [self.half_channels] * 2, 1)
|
||||
else:
|
||||
m = stats
|
||||
logs = torch.zeros_like(m)
|
||||
|
||||
if not reverse:
|
||||
x1 = m + x1 * torch.exp(logs) * x_mask
|
||||
x = torch.cat([x0, x1], 1)
|
||||
logdet = torch.sum(logs, [1, 2])
|
||||
return x, logdet
|
||||
else:
|
||||
x1 = (x1 - m) * torch.exp(-logs) * x_mask
|
||||
x = torch.cat([x0, x1], 1)
|
||||
return x
|
||||
|
||||
x1, logabsdet = piecewise_rational_quadratic_transform(
|
||||
x1,
|
||||
unnormalized_widths,
|
||||
unnormalized_heights,
|
||||
unnormalized_derivatives,
|
||||
inverse=reverse,
|
||||
tails="linear",
|
||||
tail_bound=self.tail_bound,
|
||||
)
|
||||
|
||||
x = torch.cat([x0, x1], 1) * x_mask
|
||||
logdet = torch.sum(logabsdet * x_mask, [1, 2])
|
||||
if not reverse:
|
||||
return x, logdet
|
||||
else:
|
||||
return x
|
||||
@@ -0,0 +1,16 @@
|
||||
from numpy import zeros, int32, float32
|
||||
from torch import from_numpy
|
||||
|
||||
from .core import maximum_path_jit
|
||||
|
||||
|
||||
def maximum_path(neg_cent, mask):
|
||||
device = neg_cent.device
|
||||
dtype = neg_cent.dtype
|
||||
neg_cent = neg_cent.data.cpu().numpy().astype(float32)
|
||||
path = zeros(neg_cent.shape, dtype=int32)
|
||||
|
||||
t_t_max = mask.sum(1)[:, 0].data.cpu().numpy().astype(int32)
|
||||
t_s_max = mask.sum(2)[:, 0].data.cpu().numpy().astype(int32)
|
||||
maximum_path_jit(path, neg_cent, t_t_max, t_s_max)
|
||||
return from_numpy(path).to(device=device, dtype=dtype)
|
||||
@@ -0,0 +1,46 @@
|
||||
import numba
|
||||
|
||||
|
||||
@numba.jit(
|
||||
numba.void(
|
||||
numba.int32[:, :, ::1],
|
||||
numba.float32[:, :, ::1],
|
||||
numba.int32[::1],
|
||||
numba.int32[::1],
|
||||
),
|
||||
nopython=True,
|
||||
nogil=True,
|
||||
)
|
||||
def maximum_path_jit(paths, values, t_ys, t_xs):
|
||||
b = paths.shape[0]
|
||||
max_neg_val = -1e9
|
||||
for i in range(int(b)):
|
||||
path = paths[i]
|
||||
value = values[i]
|
||||
t_y = t_ys[i]
|
||||
t_x = t_xs[i]
|
||||
|
||||
v_prev = v_cur = 0.0
|
||||
index = t_x - 1
|
||||
|
||||
for y in range(t_y):
|
||||
for x in range(max(0, t_x + y - t_y), min(t_x, y + 1)):
|
||||
if x == y:
|
||||
v_cur = max_neg_val
|
||||
else:
|
||||
v_cur = value[y - 1, x]
|
||||
if x == 0:
|
||||
if y == 0:
|
||||
v_prev = 0.0
|
||||
else:
|
||||
v_prev = max_neg_val
|
||||
else:
|
||||
v_prev = value[y - 1, x - 1]
|
||||
value[y, x] += max(v_prev, v_cur)
|
||||
|
||||
for y in range(t_y - 1, -1, -1):
|
||||
path[y, index] = 1
|
||||
if index != 0 and (
|
||||
index == y or value[y - 1, index] < value[y - 1, index - 1]
|
||||
):
|
||||
index = index - 1
|
||||
@@ -0,0 +1,168 @@
|
||||
import re
|
||||
|
||||
def split_sentence(text, min_len=10, language_str='EN'):
|
||||
if language_str in ['EN', 'FR', 'ES', 'SP']:
|
||||
sentences = split_sentences_latin(text, min_len=min_len)
|
||||
else:
|
||||
sentences = split_sentences_zh(text, min_len=min_len)
|
||||
return sentences
|
||||
|
||||
|
||||
def split_sentences_latin(text, min_len=10):
|
||||
text = re.sub('[。!?;]', '.', text)
|
||||
text = re.sub('[,]', ',', text)
|
||||
text = re.sub('[“”]', '"', text)
|
||||
text = re.sub('[‘’]', "'", text)
|
||||
text = re.sub(r"[\<\>\(\)\[\]\"\«\»]+", "", text)
|
||||
return [item.strip() for item in txtsplit(text, 256, 512) if item.strip()]
|
||||
|
||||
|
||||
def split_sentences_zh(text, min_len=10):
|
||||
text = re.sub('[。!?;]', '.', text)
|
||||
text = re.sub('[,]', ',', text)
|
||||
# 将文本中的换行符、空格和制表符替换为空格
|
||||
text = re.sub('[\n\t ]+', ' ', text)
|
||||
# 在标点符号后添加一个空格
|
||||
text = re.sub('([,.!?;])', r'\1 $#!', text)
|
||||
# 分隔句子并去除前后空格
|
||||
# sentences = [s.strip() for s in re.split('(。|!|?|;)', text)]
|
||||
sentences = [s.strip() for s in text.split('$#!')]
|
||||
if len(sentences[-1]) == 0: del sentences[-1]
|
||||
|
||||
new_sentences = []
|
||||
new_sent = []
|
||||
count_len = 0
|
||||
for ind, sent in enumerate(sentences):
|
||||
new_sent.append(sent)
|
||||
count_len += len(sent)
|
||||
if count_len > min_len or ind == len(sentences) - 1:
|
||||
count_len = 0
|
||||
new_sentences.append(' '.join(new_sent))
|
||||
new_sent = []
|
||||
return merge_short_sentences_zh(new_sentences)
|
||||
|
||||
|
||||
def merge_short_sentences_en(sens):
|
||||
"""Avoid short sentences by merging them with the following sentence.
|
||||
|
||||
Args:
|
||||
List[str]: list of input sentences.
|
||||
|
||||
Returns:
|
||||
List[str]: list of output sentences.
|
||||
"""
|
||||
sens_out = []
|
||||
for s in sens:
|
||||
# If the previous sentense is too short, merge them with
|
||||
# the current sentence.
|
||||
if len(sens_out) > 0 and len(sens_out[-1].split(" ")) <= 2:
|
||||
sens_out[-1] = sens_out[-1] + " " + s
|
||||
else:
|
||||
sens_out.append(s)
|
||||
try:
|
||||
if len(sens_out[-1].split(" ")) <= 2:
|
||||
sens_out[-2] = sens_out[-2] + " " + sens_out[-1]
|
||||
sens_out.pop(-1)
|
||||
except:
|
||||
pass
|
||||
return sens_out
|
||||
|
||||
|
||||
def merge_short_sentences_zh(sens):
|
||||
# return sens
|
||||
"""Avoid short sentences by merging them with the following sentence.
|
||||
|
||||
Args:
|
||||
List[str]: list of input sentences.
|
||||
|
||||
Returns:
|
||||
List[str]: list of output sentences.
|
||||
"""
|
||||
sens_out = []
|
||||
for s in sens:
|
||||
# If the previous sentense is too short, merge them with
|
||||
# the current sentence.
|
||||
if len(sens_out) > 0 and len(sens_out[-1]) <= 2:
|
||||
sens_out[-1] = sens_out[-1] + " " + s
|
||||
else:
|
||||
sens_out.append(s)
|
||||
try:
|
||||
if len(sens_out[-1]) <= 2:
|
||||
sens_out[-2] = sens_out[-2] + " " + sens_out[-1]
|
||||
sens_out.pop(-1)
|
||||
except:
|
||||
pass
|
||||
return sens_out
|
||||
|
||||
|
||||
|
||||
def txtsplit(text, desired_length=100, max_length=200):
|
||||
"""Split text it into chunks of a desired length trying to keep sentences intact."""
|
||||
text = re.sub(r'\n\n+', '\n', text)
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
text = re.sub(r'[""]', '"', text)
|
||||
text = re.sub(r'([,.?!])', r'\1 ', text)
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
|
||||
rv = []
|
||||
in_quote = False
|
||||
current = ""
|
||||
split_pos = []
|
||||
pos = -1
|
||||
end_pos = len(text) - 1
|
||||
def seek(delta):
|
||||
nonlocal pos, in_quote, current
|
||||
is_neg = delta < 0
|
||||
for _ in range(abs(delta)):
|
||||
if is_neg:
|
||||
pos -= 1
|
||||
current = current[:-1]
|
||||
else:
|
||||
pos += 1
|
||||
current += text[pos]
|
||||
if text[pos] == '"':
|
||||
in_quote = not in_quote
|
||||
return text[pos]
|
||||
def peek(delta):
|
||||
p = pos + delta
|
||||
return text[p] if p < end_pos and p >= 0 else ""
|
||||
def commit():
|
||||
nonlocal rv, current, split_pos
|
||||
rv.append(current)
|
||||
current = ""
|
||||
split_pos = []
|
||||
while pos < end_pos:
|
||||
c = seek(1)
|
||||
if len(current) >= max_length:
|
||||
if len(split_pos) > 0 and len(current) > (desired_length / 2):
|
||||
d = pos - split_pos[-1]
|
||||
seek(-d)
|
||||
else:
|
||||
while c not in '!?.\n ' and pos > 0 and len(current) > desired_length:
|
||||
c = seek(-1)
|
||||
commit()
|
||||
elif not in_quote and (c in '!?\n' or (c in '.,' and peek(1) in '\n ')):
|
||||
while pos < len(text) - 1 and len(current) < max_length and peek(1) in '!?.':
|
||||
c = seek(1)
|
||||
split_pos.append(pos)
|
||||
if len(current) >= desired_length:
|
||||
commit()
|
||||
elif in_quote and peek(1) == '"' and peek(2) in '\n ':
|
||||
seek(2)
|
||||
split_pos.append(pos)
|
||||
rv.append(current)
|
||||
rv = [s.strip() for s in rv]
|
||||
rv = [s for s in rv if len(s) > 0 and not re.match(r'^[\s\.,;:!?]*$', s)]
|
||||
return rv
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
zh_text = "好的,我来给你讲一个故事吧。从前有一个小姑娘,她叫做小红。小红非常喜欢在森林里玩耍,她经常会和她的小伙伴们一起去探险。有一天,小红和她的小伙伴们走到了森林深处,突然遇到了一只凶猛的野兽。小红的小伙伴们都吓得不敢动弹,但是小红并没有被吓倒,她勇敢地走向野兽,用她的智慧和勇气成功地制服了野兽,保护了她的小伙伴们。从那以后,小红变得更加勇敢和自信,成为了她小伙伴们心中的英雄。"
|
||||
en_text = "I didn’t know what to do. I said please kill her because it would be better than being kidnapped,” Ben, whose surname CNN is not using for security concerns, said on Wednesday. “It’s a nightmare. I said ‘please kill her, don’t take her there.’"
|
||||
sp_text = "¡Claro! ¿En qué tema te gustaría que te hable en español? Puedo proporcionarte información o conversar contigo sobre una amplia variedad de temas, desde cultura y comida hasta viajes y tecnología. ¿Tienes alguna preferencia en particular?"
|
||||
fr_text = "Bien sûr ! En quelle matière voudriez-vous que je vous parle en français ? Je peux vous fournir des informations ou discuter avec vous sur une grande variété de sujets, que ce soit la culture, la nourriture, les voyages ou la technologie. Avez-vous une préférence particulière ?"
|
||||
|
||||
print(split_sentence(zh_text, language_str='ZH'))
|
||||
print(split_sentence(en_text, language_str='EN'))
|
||||
print(split_sentence(sp_text, language_str='SP'))
|
||||
print(split_sentence(fr_text, language_str='FR'))
|
||||
@@ -0,0 +1,29 @@
|
||||
from .symbols import *
|
||||
|
||||
|
||||
_symbol_to_id = {s: i for i, s in enumerate(symbols)}
|
||||
|
||||
|
||||
def cleaned_text_to_sequence(cleaned_text, tones, language, symbol_to_id=None):
|
||||
"""Converts a string of text to a sequence of IDs corresponding to the symbols in the text.
|
||||
Args:
|
||||
text: string to convert to a sequence
|
||||
Returns:
|
||||
List of integers corresponding to the symbols in the text
|
||||
"""
|
||||
symbol_to_id_map = symbol_to_id if symbol_to_id else _symbol_to_id
|
||||
phones = [symbol_to_id_map[symbol] for symbol in cleaned_text]
|
||||
tone_start = language_tone_start_map[language]
|
||||
tones = [i + tone_start for i in tones]
|
||||
lang_id = language_id_map[language]
|
||||
lang_ids = [lang_id for i in phones]
|
||||
return phones, tones, lang_ids
|
||||
|
||||
|
||||
def get_bert(norm_text, word2ph, language, device):
|
||||
from .english_bert import get_bert_feature as en_bert
|
||||
# from .french_bert import get_bert_feature as fr_bert
|
||||
|
||||
lang_bert_func_map = {"EN": en_bert}
|
||||
bert = lang_bert_func_map[language](norm_text, word2ph, device)
|
||||
return bert
|
||||
@@ -0,0 +1,37 @@
|
||||
from . import english
|
||||
from . import cleaned_text_to_sequence
|
||||
import copy
|
||||
|
||||
# language_module_map = {"EN": english,
|
||||
# 'FR': french}
|
||||
language_module_map = {"EN": english}
|
||||
|
||||
|
||||
def clean_text(text, language):
|
||||
language_module = language_module_map[language]
|
||||
norm_text = language_module.text_normalize(text)
|
||||
phones, tones, word2ph = language_module.g2p(norm_text)
|
||||
return norm_text, phones, tones, word2ph
|
||||
|
||||
|
||||
def clean_text_bert(text, language, device=None):
|
||||
language_module = language_module_map[language]
|
||||
norm_text = language_module.text_normalize(text)
|
||||
phones, tones, word2ph = language_module.g2p(norm_text)
|
||||
|
||||
word2ph_bak = copy.deepcopy(word2ph)
|
||||
for i in range(len(word2ph)):
|
||||
word2ph[i] = word2ph[i] * 2
|
||||
word2ph[0] += 1
|
||||
bert = language_module.get_bert_feature(norm_text, word2ph, device=device)
|
||||
|
||||
return norm_text, phones, tones, word2ph_bak, bert
|
||||
|
||||
|
||||
def text_to_sequence(text, language):
|
||||
norm_text, phones, tones, word2ph = clean_text(text, language)
|
||||
return cleaned_text_to_sequence(phones, tones, language)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Set of default text cleaners"""
|
||||
# TODO: pick the cleaner for languages dynamically
|
||||
|
||||
import re
|
||||
|
||||
# Regular expression matching whitespace:
|
||||
_whitespace_re = re.compile(r"\s+")
|
||||
|
||||
rep_map = {
|
||||
":": ",",
|
||||
";": ",",
|
||||
",": ",",
|
||||
"。": ".",
|
||||
"!": "!",
|
||||
"?": "?",
|
||||
"\n": ".",
|
||||
"·": ",",
|
||||
"、": ",",
|
||||
"...": ".",
|
||||
"…": ".",
|
||||
"$": ".",
|
||||
"“": "'",
|
||||
"”": "'",
|
||||
"‘": "'",
|
||||
"’": "'",
|
||||
"(": "'",
|
||||
")": "'",
|
||||
"(": "'",
|
||||
")": "'",
|
||||
"《": "'",
|
||||
"》": "'",
|
||||
"【": "'",
|
||||
"】": "'",
|
||||
"[": "'",
|
||||
"]": "'",
|
||||
"—": "",
|
||||
"~": "-",
|
||||
"~": "-",
|
||||
"「": "'",
|
||||
"」": "'",
|
||||
}
|
||||
|
||||
def replace_punctuation(text):
|
||||
pattern = re.compile("|".join(re.escape(p) for p in rep_map.keys()))
|
||||
replaced_text = pattern.sub(lambda x: rep_map[x.group()], text)
|
||||
return replaced_text
|
||||
|
||||
def lowercase(text):
|
||||
return text.lower()
|
||||
|
||||
|
||||
def collapse_whitespace(text):
|
||||
return re.sub(_whitespace_re, " ", text).strip()
|
||||
|
||||
def remove_punctuation_at_begin(text):
|
||||
return re.sub(r'^[,.!?]+', '', text)
|
||||
|
||||
def remove_aux_symbols(text):
|
||||
text = re.sub(r"[\<\>\(\)\[\]\"\«\»\']+", "", text)
|
||||
return text
|
||||
|
||||
|
||||
def replace_symbols(text, lang="en"):
|
||||
"""Replace symbols based on the lenguage tag.
|
||||
|
||||
Args:
|
||||
text:
|
||||
Input text.
|
||||
lang:
|
||||
Lenguage identifier. ex: "en", "fr", "pt", "ca".
|
||||
|
||||
Returns:
|
||||
The modified text
|
||||
example:
|
||||
input args:
|
||||
text: "si l'avi cau, diguem-ho"
|
||||
lang: "ca"
|
||||
Output:
|
||||
text: "si lavi cau, diguemho"
|
||||
"""
|
||||
text = text.replace(";", ",")
|
||||
text = text.replace("-", " ") if lang != "ca" else text.replace("-", "")
|
||||
text = text.replace(":", ",")
|
||||
if lang == "en":
|
||||
text = text.replace("&", " and ")
|
||||
elif lang == "fr":
|
||||
text = text.replace("&", " et ")
|
||||
elif lang == "pt":
|
||||
text = text.replace("&", " e ")
|
||||
elif lang == "ca":
|
||||
text = text.replace("&", " i ")
|
||||
text = text.replace("'", "")
|
||||
elif lang== "es":
|
||||
text=text.replace("&","y")
|
||||
text = text.replace("'", "")
|
||||
return text
|
||||
|
||||
def unicleaners(text, cased=False, lang='en'):
|
||||
"""Basic pipeline for Portuguese text. There is no need to expand abbreviation and
|
||||
numbers, phonemizer already does that"""
|
||||
if not cased:
|
||||
text = lowercase(text)
|
||||
text = replace_punctuation(text)
|
||||
text = replace_symbols(text, lang=lang)
|
||||
text = remove_aux_symbols(text)
|
||||
text = remove_punctuation_at_begin(text)
|
||||
text = collapse_whitespace(text)
|
||||
text = re.sub(r'([^\.,!\?\-…])$', r'\1.', text)
|
||||
return text
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,298 @@
|
||||
import pickle
|
||||
import os
|
||||
import re
|
||||
from g2p_en import G2p
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
|
||||
from lib.constants import TTS_BERT_BASE_MODEL_DIR_PATH
|
||||
|
||||
from . import symbols
|
||||
|
||||
from .english_utils.abbreviations import expand_abbreviations
|
||||
from .english_utils.time_norm import expand_time_english
|
||||
from .english_utils.number_norm import normalize_numbers
|
||||
|
||||
|
||||
current_file_path = os.path.dirname(__file__)
|
||||
CMU_DICT_PATH = os.path.join(current_file_path, "cmudict.rep")
|
||||
CACHE_PATH = os.path.join(current_file_path, "cmudict_cache.pickle")
|
||||
_g2p = G2p()
|
||||
|
||||
arpa = {
|
||||
"AH0",
|
||||
"S",
|
||||
"AH1",
|
||||
"EY2",
|
||||
"AE2",
|
||||
"EH0",
|
||||
"OW2",
|
||||
"UH0",
|
||||
"NG",
|
||||
"B",
|
||||
"G",
|
||||
"AY0",
|
||||
"M",
|
||||
"AA0",
|
||||
"F",
|
||||
"AO0",
|
||||
"ER2",
|
||||
"UH1",
|
||||
"IY1",
|
||||
"AH2",
|
||||
"DH",
|
||||
"IY0",
|
||||
"EY1",
|
||||
"IH0",
|
||||
"K",
|
||||
"N",
|
||||
"W",
|
||||
"IY2",
|
||||
"T",
|
||||
"AA1",
|
||||
"ER1",
|
||||
"EH2",
|
||||
"OY0",
|
||||
"UH2",
|
||||
"UW1",
|
||||
"Z",
|
||||
"AW2",
|
||||
"AW1",
|
||||
"V",
|
||||
"UW2",
|
||||
"AA2",
|
||||
"ER",
|
||||
"AW0",
|
||||
"UW0",
|
||||
"R",
|
||||
"OW1",
|
||||
"EH1",
|
||||
"ZH",
|
||||
"AE0",
|
||||
"IH2",
|
||||
"IH",
|
||||
"Y",
|
||||
"JH",
|
||||
"P",
|
||||
"AY1",
|
||||
"EY0",
|
||||
"OY2",
|
||||
"TH",
|
||||
"HH",
|
||||
"D",
|
||||
"ER0",
|
||||
"CH",
|
||||
"AO1",
|
||||
"AE1",
|
||||
"AO2",
|
||||
"OY1",
|
||||
"AY2",
|
||||
"IH1",
|
||||
"OW0",
|
||||
"L",
|
||||
"SH",
|
||||
}
|
||||
|
||||
|
||||
def distribute_phone(n_phone, n_word):
|
||||
phones_per_word = [0] * n_word
|
||||
for task in range(n_phone):
|
||||
min_tasks = min(phones_per_word)
|
||||
min_index = phones_per_word.index(min_tasks)
|
||||
phones_per_word[min_index] += 1
|
||||
return phones_per_word
|
||||
|
||||
def post_replace_ph(ph):
|
||||
rep_map = {
|
||||
":": ",",
|
||||
";": ",",
|
||||
",": ",",
|
||||
"。": ".",
|
||||
"!": "!",
|
||||
"?": "?",
|
||||
"\n": ".",
|
||||
"·": ",",
|
||||
"、": ",",
|
||||
"...": "…",
|
||||
"v": "V",
|
||||
}
|
||||
if ph in rep_map.keys():
|
||||
ph = rep_map[ph]
|
||||
if ph in symbols:
|
||||
return ph
|
||||
if ph not in symbols:
|
||||
ph = "UNK"
|
||||
return ph
|
||||
|
||||
|
||||
def read_dict():
|
||||
g2p_dict = {}
|
||||
start_line = 49
|
||||
with open(CMU_DICT_PATH) as f:
|
||||
line = f.readline()
|
||||
line_index = 1
|
||||
while line:
|
||||
if line_index >= start_line:
|
||||
line = line.strip()
|
||||
word_split = line.split(" ")
|
||||
word = word_split[0]
|
||||
|
||||
syllable_split = word_split[1].split(" - ")
|
||||
g2p_dict[word] = []
|
||||
for syllable in syllable_split:
|
||||
phone_split = syllable.split(" ")
|
||||
g2p_dict[word].append(phone_split)
|
||||
|
||||
line_index = line_index + 1
|
||||
line = f.readline()
|
||||
|
||||
return g2p_dict
|
||||
|
||||
|
||||
def cache_dict(g2p_dict, file_path):
|
||||
with open(file_path, "wb") as pickle_file:
|
||||
pickle.dump(g2p_dict, pickle_file)
|
||||
|
||||
|
||||
def get_dict():
|
||||
if os.path.exists(CACHE_PATH):
|
||||
with open(CACHE_PATH, "rb") as pickle_file:
|
||||
g2p_dict = pickle.load(pickle_file)
|
||||
else:
|
||||
g2p_dict = read_dict()
|
||||
cache_dict(g2p_dict, CACHE_PATH)
|
||||
|
||||
return g2p_dict
|
||||
|
||||
|
||||
eng_dict = get_dict()
|
||||
|
||||
|
||||
def refine_ph(phn):
|
||||
tone = 0
|
||||
if re.search(r"\d$", phn):
|
||||
tone = int(phn[-1]) + 1
|
||||
phn = phn[:-1]
|
||||
return phn.lower(), tone
|
||||
|
||||
|
||||
def refine_syllables(syllables):
|
||||
tones = []
|
||||
phonemes = []
|
||||
for phn_list in syllables:
|
||||
for i in range(len(phn_list)):
|
||||
phn = phn_list[i]
|
||||
phn, tone = refine_ph(phn)
|
||||
phonemes.append(phn)
|
||||
tones.append(tone)
|
||||
return phonemes, tones
|
||||
|
||||
|
||||
def text_normalize(text):
|
||||
text = text.lower()
|
||||
text = expand_time_english(text)
|
||||
text = normalize_numbers(text)
|
||||
text = expand_abbreviations(text)
|
||||
return text
|
||||
|
||||
load_model_params = {
|
||||
"pretrained_model_name_or_path": TTS_BERT_BASE_MODEL_DIR_PATH,
|
||||
"local_files_only": True
|
||||
}
|
||||
tokenizer = AutoTokenizer.from_pretrained(**load_model_params)
|
||||
|
||||
def g2p_old(text):
|
||||
tokenized = tokenizer.tokenize(text)
|
||||
# import pdb; pdb.set_trace()
|
||||
phones = []
|
||||
tones = []
|
||||
words = re.split(r"([,;.\-\?\!\s+])", text)
|
||||
for w in words:
|
||||
if w.upper() in eng_dict:
|
||||
phns, tns = refine_syllables(eng_dict[w.upper()])
|
||||
phones += phns
|
||||
tones += tns
|
||||
else:
|
||||
phone_list = list(filter(lambda p: p != " ", _g2p(w)))
|
||||
for ph in phone_list:
|
||||
if ph in arpa:
|
||||
ph, tn = refine_ph(ph)
|
||||
phones.append(ph)
|
||||
tones.append(tn)
|
||||
else:
|
||||
phones.append(ph)
|
||||
tones.append(0)
|
||||
# todo: implement word2ph
|
||||
word2ph = [1 for i in phones]
|
||||
|
||||
phones = [post_replace_ph(i) for i in phones]
|
||||
return phones, tones, word2ph
|
||||
|
||||
def g2p(text, pad_start_end=True, tokenized=None):
|
||||
if tokenized is None:
|
||||
tokenized = tokenizer.tokenize(text)
|
||||
# import pdb; pdb.set_trace()
|
||||
phs = []
|
||||
ph_groups = []
|
||||
for t in tokenized:
|
||||
if not t.startswith("#"):
|
||||
ph_groups.append([t])
|
||||
else:
|
||||
ph_groups[-1].append(t.replace("#", ""))
|
||||
|
||||
phones = []
|
||||
tones = []
|
||||
word2ph = []
|
||||
for group in ph_groups:
|
||||
w = "".join(group)
|
||||
phone_len = 0
|
||||
word_len = len(group)
|
||||
if w.upper() in eng_dict:
|
||||
phns, tns = refine_syllables(eng_dict[w.upper()])
|
||||
phones += phns
|
||||
tones += tns
|
||||
phone_len += len(phns)
|
||||
else:
|
||||
phone_list = list(filter(lambda p: p != " ", _g2p(w)))
|
||||
for ph in phone_list:
|
||||
if ph in arpa:
|
||||
ph, tn = refine_ph(ph)
|
||||
phones.append(ph)
|
||||
tones.append(tn)
|
||||
else:
|
||||
phones.append(ph)
|
||||
tones.append(0)
|
||||
phone_len += 1
|
||||
aaa = distribute_phone(phone_len, word_len)
|
||||
word2ph += aaa
|
||||
phones = [post_replace_ph(i) for i in phones]
|
||||
|
||||
if pad_start_end:
|
||||
phones = ["_"] + phones + ["_"]
|
||||
tones = [0] + tones + [0]
|
||||
word2ph = [1] + word2ph + [1]
|
||||
return phones, tones, word2ph
|
||||
|
||||
def get_bert_feature(text, word2ph, device=None):
|
||||
from text import english_bert
|
||||
|
||||
return english_bert.get_bert_feature(text, word2ph, device=device)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# print(get_dict())
|
||||
# print(eng_word_to_phoneme("hello"))
|
||||
from text.english_bert import get_bert_feature
|
||||
text = "In this paper, we propose 1 DSPGAN, a N-F-T GAN-based universal vocoder."
|
||||
text = text_normalize(text)
|
||||
phones, tones, word2ph = g2p(text)
|
||||
import pdb; pdb.set_trace()
|
||||
bert = get_bert_feature(text, word2ph)
|
||||
|
||||
print(phones, tones, word2ph, bert.shape)
|
||||
|
||||
# all_phones = set()
|
||||
# for k, syllables in eng_dict.items():
|
||||
# for group in syllables:
|
||||
# for ph in group:
|
||||
# all_phones.add(ph)
|
||||
# print(all_phones)
|
||||
@@ -0,0 +1,44 @@
|
||||
import torch
|
||||
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
||||
import sys
|
||||
|
||||
from lib.constants import TTS_BERT_BASE_MODEL_DIR_PATH
|
||||
|
||||
load_model_params = {
|
||||
"pretrained_model_name_or_path": TTS_BERT_BASE_MODEL_DIR_PATH,
|
||||
"local_files_only": True
|
||||
}
|
||||
tokenizer = AutoTokenizer.from_pretrained(**load_model_params)
|
||||
model = None
|
||||
|
||||
def get_bert_feature(text, word2ph, device=None):
|
||||
global model
|
||||
if (
|
||||
sys.platform == "darwin"
|
||||
and torch.backends.mps.is_available()
|
||||
and device == "cpu"
|
||||
):
|
||||
device = "mps"
|
||||
if not device:
|
||||
device = "cuda"
|
||||
if model is None:
|
||||
model = AutoModelForMaskedLM.from_pretrained(**load_model_params).to(
|
||||
device
|
||||
)
|
||||
with torch.no_grad():
|
||||
inputs = tokenizer(text, return_tensors="pt")
|
||||
for i in inputs:
|
||||
inputs[i] = inputs[i].to(device)
|
||||
res = model(**inputs, output_hidden_states=True)
|
||||
res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu()
|
||||
|
||||
assert inputs["input_ids"].shape[-1] == len(word2ph)
|
||||
word2phone = word2ph
|
||||
phone_level_feature = []
|
||||
for i in range(len(word2phone)):
|
||||
repeat_feature = res[i].repeat(word2phone[i], 1)
|
||||
phone_level_feature.append(repeat_feature)
|
||||
|
||||
phone_level_feature = torch.cat(phone_level_feature, dim=0)
|
||||
|
||||
return phone_level_feature.T
|
||||
@@ -0,0 +1,35 @@
|
||||
import re
|
||||
|
||||
# List of (regular expression, replacement) pairs for abbreviations in english:
|
||||
abbreviations_en = [
|
||||
(re.compile("\\b%s\\." % x[0], re.IGNORECASE), x[1])
|
||||
for x in [
|
||||
("mrs", "misess"),
|
||||
("mr", "mister"),
|
||||
("dr", "doctor"),
|
||||
("st", "saint"),
|
||||
("co", "company"),
|
||||
("jr", "junior"),
|
||||
("maj", "major"),
|
||||
("gen", "general"),
|
||||
("drs", "doctors"),
|
||||
("rev", "reverend"),
|
||||
("lt", "lieutenant"),
|
||||
("hon", "honorable"),
|
||||
("sgt", "sergeant"),
|
||||
("capt", "captain"),
|
||||
("esq", "esquire"),
|
||||
("ltd", "limited"),
|
||||
("col", "colonel"),
|
||||
("ft", "fort"),
|
||||
]
|
||||
]
|
||||
|
||||
def expand_abbreviations(text, lang="en"):
|
||||
if lang == "en":
|
||||
_abbreviations = abbreviations_en
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
for regex, replacement in _abbreviations:
|
||||
text = re.sub(regex, replacement, text)
|
||||
return text
|
||||
@@ -0,0 +1,97 @@
|
||||
""" from https://github.com/keithito/tacotron """
|
||||
|
||||
import re
|
||||
from typing import Dict
|
||||
|
||||
import inflect
|
||||
|
||||
_inflect = inflect.engine()
|
||||
_comma_number_re = re.compile(r"([0-9][0-9\,]+[0-9])")
|
||||
_decimal_number_re = re.compile(r"([0-9]+\.[0-9]+)")
|
||||
_currency_re = re.compile(r"(£|\$|¥)([0-9\,\.]*[0-9]+)")
|
||||
_ordinal_re = re.compile(r"[0-9]+(st|nd|rd|th)")
|
||||
_number_re = re.compile(r"-?[0-9]+")
|
||||
|
||||
|
||||
def _remove_commas(m):
|
||||
return m.group(1).replace(",", "")
|
||||
|
||||
|
||||
def _expand_decimal_point(m):
|
||||
return m.group(1).replace(".", " point ")
|
||||
|
||||
|
||||
def __expand_currency(value: str, inflection: Dict[float, str]) -> str:
|
||||
parts = value.replace(",", "").split(".")
|
||||
if len(parts) > 2:
|
||||
return f"{value} {inflection[2]}" # Unexpected format
|
||||
text = []
|
||||
integer = int(parts[0]) if parts[0] else 0
|
||||
if integer > 0:
|
||||
integer_unit = inflection.get(integer, inflection[2])
|
||||
text.append(f"{integer} {integer_unit}")
|
||||
fraction = int(parts[1]) if len(parts) > 1 and parts[1] else 0
|
||||
if fraction > 0:
|
||||
fraction_unit = inflection.get(fraction / 100, inflection[0.02])
|
||||
text.append(f"{fraction} {fraction_unit}")
|
||||
if len(text) == 0:
|
||||
return f"zero {inflection[2]}"
|
||||
return " ".join(text)
|
||||
|
||||
|
||||
def _expand_currency(m: "re.Match") -> str:
|
||||
currencies = {
|
||||
"$": {
|
||||
0.01: "cent",
|
||||
0.02: "cents",
|
||||
1: "dollar",
|
||||
2: "dollars",
|
||||
},
|
||||
"€": {
|
||||
0.01: "cent",
|
||||
0.02: "cents",
|
||||
1: "euro",
|
||||
2: "euros",
|
||||
},
|
||||
"£": {
|
||||
0.01: "penny",
|
||||
0.02: "pence",
|
||||
1: "pound sterling",
|
||||
2: "pounds sterling",
|
||||
},
|
||||
"¥": {
|
||||
# TODO rin
|
||||
0.02: "sen",
|
||||
2: "yen",
|
||||
},
|
||||
}
|
||||
unit = m.group(1)
|
||||
currency = currencies[unit]
|
||||
value = m.group(2)
|
||||
return __expand_currency(value, currency)
|
||||
|
||||
|
||||
def _expand_ordinal(m):
|
||||
return _inflect.number_to_words(m.group(0))
|
||||
|
||||
|
||||
def _expand_number(m):
|
||||
num = int(m.group(0))
|
||||
if 1000 < num < 3000:
|
||||
if num == 2000:
|
||||
return "two thousand"
|
||||
if 2000 < num < 2010:
|
||||
return "two thousand " + _inflect.number_to_words(num % 100)
|
||||
if num % 100 == 0:
|
||||
return _inflect.number_to_words(num // 100) + " hundred"
|
||||
return _inflect.number_to_words(num, andword="", zero="oh", group=2).replace(", ", " ")
|
||||
return _inflect.number_to_words(num, andword="")
|
||||
|
||||
|
||||
def normalize_numbers(text):
|
||||
text = re.sub(_comma_number_re, _remove_commas, text)
|
||||
text = re.sub(_currency_re, _expand_currency, text)
|
||||
text = re.sub(_decimal_number_re, _expand_decimal_point, text)
|
||||
text = re.sub(_ordinal_re, _expand_ordinal, text)
|
||||
text = re.sub(_number_re, _expand_number, text)
|
||||
return text
|
||||
@@ -0,0 +1,47 @@
|
||||
import re
|
||||
|
||||
import inflect
|
||||
|
||||
_inflect = inflect.engine()
|
||||
|
||||
_time_re = re.compile(
|
||||
r"""\b
|
||||
((0?[0-9])|(1[0-1])|(1[2-9])|(2[0-3])) # hours
|
||||
:
|
||||
([0-5][0-9]) # minutes
|
||||
\s*(a\\.m\\.|am|pm|p\\.m\\.|a\\.m|p\\.m)? # am/pm
|
||||
\b""",
|
||||
re.IGNORECASE | re.X,
|
||||
)
|
||||
|
||||
|
||||
def _expand_num(n: int) -> str:
|
||||
return _inflect.number_to_words(n)
|
||||
|
||||
|
||||
def _expand_time_english(match: "re.Match") -> str:
|
||||
hour = int(match.group(1))
|
||||
past_noon = hour >= 12
|
||||
time = []
|
||||
if hour > 12:
|
||||
hour -= 12
|
||||
elif hour == 0:
|
||||
hour = 12
|
||||
past_noon = True
|
||||
time.append(_expand_num(hour))
|
||||
|
||||
minute = int(match.group(6))
|
||||
if minute > 0:
|
||||
if minute < 10:
|
||||
time.append("oh")
|
||||
time.append(_expand_num(minute))
|
||||
am_pm = match.group(7)
|
||||
if am_pm is None:
|
||||
time.append("p m" if past_noon else "a m")
|
||||
else:
|
||||
time.extend(list(am_pm.replace(".", "")))
|
||||
return " ".join(time)
|
||||
|
||||
|
||||
def expand_time_english(text: str) -> str:
|
||||
return re.sub(_time_re, _expand_time_english, text)
|
||||
@@ -0,0 +1,140 @@
|
||||
import abc
|
||||
from typing import List, Tuple
|
||||
|
||||
from .punctuation import Punctuation
|
||||
|
||||
|
||||
class BasePhonemizer(abc.ABC):
|
||||
"""Base phonemizer class
|
||||
|
||||
Phonemization follows the following steps:
|
||||
1. Preprocessing:
|
||||
- remove empty lines
|
||||
- remove punctuation
|
||||
- keep track of punctuation marks
|
||||
|
||||
2. Phonemization:
|
||||
- convert text to phonemes
|
||||
|
||||
3. Postprocessing:
|
||||
- join phonemes
|
||||
- restore punctuation marks
|
||||
|
||||
Args:
|
||||
language (str):
|
||||
Language used by the phonemizer.
|
||||
|
||||
punctuations (List[str]):
|
||||
List of punctuation marks to be preserved.
|
||||
|
||||
keep_puncs (bool):
|
||||
Whether to preserve punctuation marks or not.
|
||||
"""
|
||||
|
||||
def __init__(self, language, punctuations=Punctuation.default_puncs(), keep_puncs=False):
|
||||
# ensure the backend is installed on the system
|
||||
if not self.is_available():
|
||||
raise RuntimeError("{} not installed on your system".format(self.name())) # pragma: nocover
|
||||
|
||||
# ensure the backend support the requested language
|
||||
self._language = self._init_language(language)
|
||||
|
||||
# setup punctuation processing
|
||||
self._keep_puncs = keep_puncs
|
||||
self._punctuator = Punctuation(punctuations)
|
||||
|
||||
def _init_language(self, language):
|
||||
"""Language initialization
|
||||
|
||||
This method may be overloaded in child classes (see Segments backend)
|
||||
|
||||
"""
|
||||
if not self.is_supported_language(language):
|
||||
raise RuntimeError(f'language "{language}" is not supported by the ' f"{self.name()} backend")
|
||||
return language
|
||||
|
||||
@property
|
||||
def language(self):
|
||||
"""The language code configured to be used for phonemization"""
|
||||
return self._language
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def name():
|
||||
"""The name of the backend"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def is_available(cls):
|
||||
"""Returns True if the backend is installed, False otherwise"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def version(cls):
|
||||
"""Return the backend version as a tuple (major, minor, patch)"""
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def supported_languages():
|
||||
"""Return a dict of language codes -> name supported by the backend"""
|
||||
...
|
||||
|
||||
def is_supported_language(self, language):
|
||||
"""Returns True if `language` is supported by the backend"""
|
||||
return language in self.supported_languages()
|
||||
|
||||
@abc.abstractmethod
|
||||
def _phonemize(self, text, separator):
|
||||
"""The main phonemization method"""
|
||||
|
||||
def _phonemize_preprocess(self, text) -> Tuple[List[str], List]:
|
||||
"""Preprocess the text before phonemization
|
||||
|
||||
1. remove spaces
|
||||
2. remove punctuation
|
||||
|
||||
Override this if you need a different behaviour
|
||||
"""
|
||||
text = text.strip()
|
||||
if self._keep_puncs:
|
||||
# a tuple (text, punctuation marks)
|
||||
return self._punctuator.strip_to_restore(text)
|
||||
return [self._punctuator.strip(text)], []
|
||||
|
||||
def _phonemize_postprocess(self, phonemized, punctuations) -> str:
|
||||
"""Postprocess the raw phonemized output
|
||||
|
||||
Override this if you need a different behaviour
|
||||
"""
|
||||
if self._keep_puncs:
|
||||
return self._punctuator.restore(phonemized, punctuations)[0]
|
||||
return phonemized[0]
|
||||
|
||||
def phonemize(self, text: str, separator="|", language: str = None) -> str: # pylint: disable=unused-argument
|
||||
"""Returns the `text` phonemized for the given language
|
||||
|
||||
Args:
|
||||
text (str):
|
||||
Text to be phonemized.
|
||||
|
||||
separator (str):
|
||||
string separator used between phonemes. Default to '_'.
|
||||
|
||||
Returns:
|
||||
(str): Phonemized text
|
||||
"""
|
||||
text, punctuations = self._phonemize_preprocess(text)
|
||||
phonemized = []
|
||||
for t in text:
|
||||
p = self._phonemize(t, separator)
|
||||
phonemized.append(p)
|
||||
phonemized = self._phonemize_postprocess(phonemized, punctuations)
|
||||
return phonemized
|
||||
|
||||
def print_logs(self, level: int = 0):
|
||||
indent = "\t" * level
|
||||
print(f"{indent}| > phoneme language: {self.language}")
|
||||
print(f"{indent}| > phoneme backend: {self.name()}")
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Set of default text cleaners"""
|
||||
# TODO: pick the cleaner for languages dynamically
|
||||
|
||||
import re
|
||||
from .french_abbreviations import abbreviations_fr
|
||||
|
||||
# Regular expression matching whitespace:
|
||||
_whitespace_re = re.compile(r"\s+")
|
||||
|
||||
|
||||
rep_map = {
|
||||
":": ",",
|
||||
";": ",",
|
||||
",": ",",
|
||||
"。": ".",
|
||||
"!": "!",
|
||||
"?": "?",
|
||||
"\n": ".",
|
||||
"·": ",",
|
||||
"、": ",",
|
||||
"...": ".",
|
||||
"…": ".",
|
||||
"$": ".",
|
||||
"“": "",
|
||||
"”": "",
|
||||
"‘": "",
|
||||
"’": "",
|
||||
"(": "",
|
||||
")": "",
|
||||
"(": "",
|
||||
")": "",
|
||||
"《": "",
|
||||
"》": "",
|
||||
"【": "",
|
||||
"】": "",
|
||||
"[": "",
|
||||
"]": "",
|
||||
"—": "",
|
||||
"~": "-",
|
||||
"~": "-",
|
||||
"「": "",
|
||||
"」": "",
|
||||
"¿" : "",
|
||||
"¡" : ""
|
||||
}
|
||||
|
||||
|
||||
def replace_punctuation(text):
|
||||
pattern = re.compile("|".join(re.escape(p) for p in rep_map.keys()))
|
||||
replaced_text = pattern.sub(lambda x: rep_map[x.group()], text)
|
||||
return replaced_text
|
||||
|
||||
def expand_abbreviations(text, lang="fr"):
|
||||
if lang == "fr":
|
||||
_abbreviations = abbreviations_fr
|
||||
for regex, replacement in _abbreviations:
|
||||
text = re.sub(regex, replacement, text)
|
||||
return text
|
||||
|
||||
|
||||
def lowercase(text):
|
||||
return text.lower()
|
||||
|
||||
|
||||
def collapse_whitespace(text):
|
||||
return re.sub(_whitespace_re, " ", text).strip()
|
||||
|
||||
def remove_punctuation_at_begin(text):
|
||||
return re.sub(r'^[,.!?]+', '', text)
|
||||
|
||||
def remove_aux_symbols(text):
|
||||
text = re.sub(r"[\<\>\(\)\[\]\"\«\»]+", "", text)
|
||||
return text
|
||||
|
||||
|
||||
def replace_symbols(text, lang="en"):
|
||||
"""Replace symbols based on the lenguage tag.
|
||||
|
||||
Args:
|
||||
text:
|
||||
Input text.
|
||||
lang:
|
||||
Lenguage identifier. ex: "en", "fr", "pt", "ca".
|
||||
|
||||
Returns:
|
||||
The modified text
|
||||
example:
|
||||
input args:
|
||||
text: "si l'avi cau, diguem-ho"
|
||||
lang: "ca"
|
||||
Output:
|
||||
text: "si lavi cau, diguemho"
|
||||
"""
|
||||
text = text.replace(";", ",")
|
||||
text = text.replace("-", " ") if lang != "ca" else text.replace("-", "")
|
||||
text = text.replace(":", ",")
|
||||
if lang == "en":
|
||||
text = text.replace("&", " and ")
|
||||
elif lang == "fr":
|
||||
text = text.replace("&", " et ")
|
||||
elif lang == "pt":
|
||||
text = text.replace("&", " e ")
|
||||
elif lang == "ca":
|
||||
text = text.replace("&", " i ")
|
||||
text = text.replace("'", "")
|
||||
elif lang== "es":
|
||||
text=text.replace("&","y")
|
||||
text = text.replace("'", "")
|
||||
return text
|
||||
|
||||
def french_cleaners(text):
|
||||
"""Pipeline for French text. There is no need to expand numbers, phonemizer already does that"""
|
||||
text = expand_abbreviations(text, lang="fr")
|
||||
# text = lowercase(text) # as we use the cased bert
|
||||
text = replace_punctuation(text)
|
||||
text = replace_symbols(text, lang="fr")
|
||||
text = remove_aux_symbols(text)
|
||||
text = remove_punctuation_at_begin(text)
|
||||
text = collapse_whitespace(text)
|
||||
text = re.sub(r'([^\.,!\?\-…])$', r'\1.', text)
|
||||
return text
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"symbols": [
|
||||
"_",
|
||||
",",
|
||||
".",
|
||||
"!",
|
||||
"?",
|
||||
"-",
|
||||
"~",
|
||||
"\u2026",
|
||||
"N",
|
||||
"Q",
|
||||
"a",
|
||||
"b",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"\u0251",
|
||||
"\u00e6",
|
||||
"\u0283",
|
||||
"\u0291",
|
||||
"\u00e7",
|
||||
"\u026f",
|
||||
"\u026a",
|
||||
"\u0254",
|
||||
"\u025b",
|
||||
"\u0279",
|
||||
"\u00f0",
|
||||
"\u0259",
|
||||
"\u026b",
|
||||
"\u0265",
|
||||
"\u0278",
|
||||
"\u028a",
|
||||
"\u027e",
|
||||
"\u0292",
|
||||
"\u03b8",
|
||||
"\u03b2",
|
||||
"\u014b",
|
||||
"\u0266",
|
||||
"\u207c",
|
||||
"\u02b0",
|
||||
"`",
|
||||
"^",
|
||||
"#",
|
||||
"*",
|
||||
"=",
|
||||
"\u02c8",
|
||||
"\u02cc",
|
||||
"\u2192",
|
||||
"\u2193",
|
||||
"\u2191",
|
||||
" ",
|
||||
"ɣ",
|
||||
"ɡ",
|
||||
"r",
|
||||
"ɲ",
|
||||
"ʝ",
|
||||
"ʎ",
|
||||
"ː"
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"symbols": [
|
||||
"_",
|
||||
",",
|
||||
".",
|
||||
"!",
|
||||
"?",
|
||||
"-",
|
||||
"~",
|
||||
"\u2026",
|
||||
"N",
|
||||
"Q",
|
||||
"a",
|
||||
"b",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"\u0251",
|
||||
"\u00e6",
|
||||
"\u0283",
|
||||
"\u0291",
|
||||
"\u00e7",
|
||||
"\u026f",
|
||||
"\u026a",
|
||||
"\u0254",
|
||||
"\u025b",
|
||||
"\u0279",
|
||||
"\u00f0",
|
||||
"\u0259",
|
||||
"\u026b",
|
||||
"\u0265",
|
||||
"\u0278",
|
||||
"\u028a",
|
||||
"\u027e",
|
||||
"\u0292",
|
||||
"\u03b8",
|
||||
"\u03b2",
|
||||
"\u014b",
|
||||
"\u0266",
|
||||
"\u207c",
|
||||
"\u02b0",
|
||||
"`",
|
||||
"^",
|
||||
"#",
|
||||
"*",
|
||||
"=",
|
||||
"\u02c8",
|
||||
"\u02cc",
|
||||
"\u2192",
|
||||
"\u2193",
|
||||
"\u2191",
|
||||
" ",
|
||||
"\u0263",
|
||||
"\u0261",
|
||||
"r",
|
||||
"\u0272",
|
||||
"\u029d",
|
||||
"\u028e",
|
||||
"\u02d0",
|
||||
|
||||
"\u0303",
|
||||
"\u0153",
|
||||
"\u00f8",
|
||||
"\u0281",
|
||||
"\u0252",
|
||||
"\u028c",
|
||||
"\u2014",
|
||||
"\u025c",
|
||||
"\u0250"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
from .cleaner import french_cleaners
|
||||
from .gruut_wrapper import Gruut
|
||||
|
||||
|
||||
def remove_consecutive_t(input_str):
|
||||
result = []
|
||||
count = 0
|
||||
|
||||
for char in input_str:
|
||||
if char == 't':
|
||||
count += 1
|
||||
else:
|
||||
if count < 3:
|
||||
result.extend(['t'] * count)
|
||||
count = 0
|
||||
result.append(char)
|
||||
|
||||
if count < 3:
|
||||
result.extend(['t'] * count)
|
||||
|
||||
return ''.join(result)
|
||||
|
||||
def fr2ipa(text):
|
||||
e = Gruut(language="fr-fr", keep_puncs=True, keep_stress=True, use_espeak_phonemes=True)
|
||||
# text = french_cleaners(text)
|
||||
phonemes = e.phonemize(text, separator="")
|
||||
# print(phonemes)
|
||||
phonemes = remove_consecutive_t(phonemes)
|
||||
# print(phonemes)
|
||||
return phonemes
|
||||
@@ -0,0 +1,48 @@
|
||||
import re
|
||||
|
||||
# List of (regular expression, replacement) pairs for abbreviations in french:
|
||||
abbreviations_fr = [
|
||||
(re.compile("\\b%s\\." % x[0], re.IGNORECASE), x[1])
|
||||
for x in [
|
||||
("M", "monsieur"),
|
||||
("Mlle", "mademoiselle"),
|
||||
("Mlles", "mesdemoiselles"),
|
||||
("Mme", "Madame"),
|
||||
("Mmes", "Mesdames"),
|
||||
("N.B", "nota bene"),
|
||||
("M", "monsieur"),
|
||||
("p.c.q", "parce que"),
|
||||
("Pr", "professeur"),
|
||||
("qqch", "quelque chose"),
|
||||
("rdv", "rendez-vous"),
|
||||
("max", "maximum"),
|
||||
("min", "minimum"),
|
||||
("no", "numéro"),
|
||||
("adr", "adresse"),
|
||||
("dr", "docteur"),
|
||||
("st", "saint"),
|
||||
("co", "companie"),
|
||||
("jr", "junior"),
|
||||
("sgt", "sergent"),
|
||||
("capt", "capitain"),
|
||||
("col", "colonel"),
|
||||
("av", "avenue"),
|
||||
("av. J.-C", "avant Jésus-Christ"),
|
||||
("apr. J.-C", "après Jésus-Christ"),
|
||||
("art", "article"),
|
||||
("boul", "boulevard"),
|
||||
("c.-à-d", "c’est-à-dire"),
|
||||
("etc", "et cetera"),
|
||||
("ex", "exemple"),
|
||||
("excl", "exclusivement"),
|
||||
("boul", "boulevard"),
|
||||
]
|
||||
] + [
|
||||
(re.compile("\\b%s" % x[0]), x[1])
|
||||
for x in [
|
||||
("Mlle", "mademoiselle"),
|
||||
("Mlles", "mesdemoiselles"),
|
||||
("Mme", "Madame"),
|
||||
("Mmes", "Mesdames"),
|
||||
]
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
_,.!?-~…NQabdefghijklmnopstuvwxyzɑæʃʑçɯɪɔɛɹðəɫɥɸʊɾʒθβŋɦ⁼ʰ`^#*=ˈˌ→↓↑ ɣɡrɲʝʎː̃œøʁɒʌ—ɜɐ
|
||||
@@ -0,0 +1,258 @@
|
||||
import importlib
|
||||
from typing import List
|
||||
|
||||
import gruut
|
||||
from gruut_ipa import IPA # pip install gruut_ipa
|
||||
|
||||
from .base import BasePhonemizer
|
||||
from .punctuation import Punctuation
|
||||
|
||||
# Table for str.translate to fix gruut/TTS phoneme mismatch
|
||||
GRUUT_TRANS_TABLE = str.maketrans("g", "ɡ")
|
||||
|
||||
|
||||
class Gruut(BasePhonemizer):
|
||||
"""Gruut wrapper for G2P
|
||||
|
||||
Args:
|
||||
language (str):
|
||||
Valid language code for the used backend.
|
||||
|
||||
punctuations (str):
|
||||
Characters to be treated as punctuation. Defaults to `Punctuation.default_puncs()`.
|
||||
|
||||
keep_puncs (bool):
|
||||
If true, keep the punctuations after phonemization. Defaults to True.
|
||||
|
||||
use_espeak_phonemes (bool):
|
||||
If true, use espeak lexicons instead of default Gruut lexicons. Defaults to False.
|
||||
|
||||
keep_stress (bool):
|
||||
If true, keep the stress characters after phonemization. Defaults to False.
|
||||
|
||||
Example:
|
||||
|
||||
>>> from TTS.tts.utils.text.phonemizers.gruut_wrapper import Gruut
|
||||
>>> phonemizer = Gruut('en-us')
|
||||
>>> phonemizer.phonemize("Be a voice, not an! echo?", separator="|")
|
||||
'b|i| ə| v|ɔ|ɪ|s, n|ɑ|t| ə|n! ɛ|k|o|ʊ?'
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
language: str,
|
||||
punctuations=Punctuation.default_puncs(),
|
||||
keep_puncs=True,
|
||||
use_espeak_phonemes=False,
|
||||
keep_stress=False,
|
||||
):
|
||||
super().__init__(language, punctuations=punctuations, keep_puncs=keep_puncs)
|
||||
self.use_espeak_phonemes = use_espeak_phonemes
|
||||
self.keep_stress = keep_stress
|
||||
|
||||
@staticmethod
|
||||
def name():
|
||||
return "gruut"
|
||||
|
||||
def phonemize_gruut(self, text: str, separator: str = "|", tie=False) -> str: # pylint: disable=unused-argument
|
||||
"""Convert input text to phonemes.
|
||||
|
||||
Gruut phonemizes the given `str` by seperating each phoneme character with `separator`, even for characters
|
||||
that constitude a single sound.
|
||||
|
||||
It doesn't affect 🐸TTS since it individually converts each character to token IDs.
|
||||
|
||||
Examples::
|
||||
"hello how are you today?" -> `h|ɛ|l|o|ʊ| h|a|ʊ| ɑ|ɹ| j|u| t|ə|d|e|ɪ`
|
||||
|
||||
Args:
|
||||
text (str):
|
||||
Text to be converted to phonemes.
|
||||
|
||||
tie (bool, optional) : When True use a '͡' character between
|
||||
consecutive characters of a single phoneme. Else separate phoneme
|
||||
with '_'. This option requires espeak>=1.49. Default to False.
|
||||
"""
|
||||
ph_list = []
|
||||
for sentence in gruut.sentences(text, lang=self.language, espeak=self.use_espeak_phonemes):
|
||||
for word in sentence:
|
||||
if word.is_break:
|
||||
# Use actual character for break phoneme (e.g., comma)
|
||||
if ph_list:
|
||||
# Join with previous word
|
||||
ph_list[-1].append(word.text)
|
||||
else:
|
||||
# First word is punctuation
|
||||
ph_list.append([word.text])
|
||||
elif word.phonemes:
|
||||
# Add phonemes for word
|
||||
word_phonemes = []
|
||||
|
||||
for word_phoneme in word.phonemes:
|
||||
if not self.keep_stress:
|
||||
# Remove primary/secondary stress
|
||||
word_phoneme = IPA.without_stress(word_phoneme)
|
||||
|
||||
word_phoneme = word_phoneme.translate(GRUUT_TRANS_TABLE)
|
||||
|
||||
if word_phoneme:
|
||||
# Flatten phonemes
|
||||
word_phonemes.extend(word_phoneme)
|
||||
|
||||
if word_phonemes:
|
||||
ph_list.append(word_phonemes)
|
||||
|
||||
ph_words = [separator.join(word_phonemes) for word_phonemes in ph_list]
|
||||
ph = f"{separator} ".join(ph_words)
|
||||
return ph
|
||||
|
||||
def _phonemize(self, text, separator):
|
||||
return self.phonemize_gruut(text, separator, tie=False)
|
||||
|
||||
def is_supported_language(self, language):
|
||||
"""Returns True if `language` is supported by the backend"""
|
||||
return gruut.is_language_supported(language)
|
||||
|
||||
@staticmethod
|
||||
def supported_languages() -> List:
|
||||
"""Get a dictionary of supported languages.
|
||||
|
||||
Returns:
|
||||
List: List of language codes.
|
||||
"""
|
||||
return list(gruut.get_supported_languages())
|
||||
|
||||
def version(self):
|
||||
"""Get the version of the used backend.
|
||||
|
||||
Returns:
|
||||
str: Version of the used backend.
|
||||
"""
|
||||
return gruut.__version__
|
||||
|
||||
@classmethod
|
||||
def is_available(cls):
|
||||
"""Return true if ESpeak is available else false"""
|
||||
return importlib.util.find_spec("gruut") is not None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from cleaner import french_cleaners
|
||||
import json
|
||||
|
||||
e = Gruut(language="fr-fr", keep_puncs=True, keep_stress=True, use_espeak_phonemes=True)
|
||||
symbols = [ # en + sp
|
||||
"_",
|
||||
",",
|
||||
".",
|
||||
"!",
|
||||
"?",
|
||||
"-",
|
||||
"~",
|
||||
"\u2026",
|
||||
"N",
|
||||
"Q",
|
||||
"a",
|
||||
"b",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"\u0251",
|
||||
"\u00e6",
|
||||
"\u0283",
|
||||
"\u0291",
|
||||
"\u00e7",
|
||||
"\u026f",
|
||||
"\u026a",
|
||||
"\u0254",
|
||||
"\u025b",
|
||||
"\u0279",
|
||||
"\u00f0",
|
||||
"\u0259",
|
||||
"\u026b",
|
||||
"\u0265",
|
||||
"\u0278",
|
||||
"\u028a",
|
||||
"\u027e",
|
||||
"\u0292",
|
||||
"\u03b8",
|
||||
"\u03b2",
|
||||
"\u014b",
|
||||
"\u0266",
|
||||
"\u207c",
|
||||
"\u02b0",
|
||||
"`",
|
||||
"^",
|
||||
"#",
|
||||
"*",
|
||||
"=",
|
||||
"\u02c8",
|
||||
"\u02cc",
|
||||
"\u2192",
|
||||
"\u2193",
|
||||
"\u2191",
|
||||
" ",
|
||||
"ɣ",
|
||||
"ɡ",
|
||||
"r",
|
||||
"ɲ",
|
||||
"ʝ",
|
||||
"ʎ",
|
||||
"ː"
|
||||
]
|
||||
with open('/home/xumin/workspace/VITS-Training-Multiling/230715_fr/metadata.txt', 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
|
||||
used_sym = []
|
||||
not_existed_sym = []
|
||||
phonemes = []
|
||||
|
||||
for line in lines:
|
||||
text = line.split('|')[-1].strip()
|
||||
text = french_cleaners(text)
|
||||
ipa = e.phonemize(text, separator="")
|
||||
phonemes.append(ipa)
|
||||
for s in ipa:
|
||||
if s not in symbols:
|
||||
if s not in not_existed_sym:
|
||||
print(f'not_existed char: {s}')
|
||||
not_existed_sym.append(s)
|
||||
else:
|
||||
if s not in used_sym:
|
||||
# print(f'used char: {s}')
|
||||
used_sym.append(s)
|
||||
|
||||
print(used_sym)
|
||||
print(not_existed_sym)
|
||||
|
||||
|
||||
with open('./text/fr_phonemizer/french_symbols.txt', 'w') as g:
|
||||
g.writelines(symbols + not_existed_sym)
|
||||
|
||||
with open('./text/fr_phonemizer/example_ipa.txt', 'w') as g:
|
||||
g.writelines(phonemes)
|
||||
|
||||
data = {'symbols': symbols + not_existed_sym}
|
||||
|
||||
with open('./text/fr_phonemizer/fr_symbols.json', 'w') as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import collections
|
||||
import re
|
||||
from enum import Enum
|
||||
|
||||
import six
|
||||
|
||||
_DEF_PUNCS = ';:,.!?¡¿—…"«»“”'
|
||||
|
||||
_PUNC_IDX = collections.namedtuple("_punc_index", ["punc", "position"])
|
||||
|
||||
|
||||
class PuncPosition(Enum):
|
||||
"""Enum for the punctuations positions"""
|
||||
|
||||
BEGIN = 0
|
||||
END = 1
|
||||
MIDDLE = 2
|
||||
ALONE = 3
|
||||
|
||||
|
||||
class Punctuation:
|
||||
"""Handle punctuations in text.
|
||||
|
||||
Just strip punctuations from text or strip and restore them later.
|
||||
|
||||
Args:
|
||||
puncs (str): The punctuations to be processed. Defaults to `_DEF_PUNCS`.
|
||||
|
||||
Example:
|
||||
>>> punc = Punctuation()
|
||||
>>> punc.strip("This is. example !")
|
||||
'This is example'
|
||||
|
||||
>>> text_striped, punc_map = punc.strip_to_restore("This is. example !")
|
||||
>>> ' '.join(text_striped)
|
||||
'This is example'
|
||||
|
||||
>>> text_restored = punc.restore(text_striped, punc_map)
|
||||
>>> text_restored[0]
|
||||
'This is. example !'
|
||||
"""
|
||||
|
||||
def __init__(self, puncs: str = _DEF_PUNCS):
|
||||
self.puncs = puncs
|
||||
|
||||
@staticmethod
|
||||
def default_puncs():
|
||||
"""Return default set of punctuations."""
|
||||
return _DEF_PUNCS
|
||||
|
||||
@property
|
||||
def puncs(self):
|
||||
return self._puncs
|
||||
|
||||
@puncs.setter
|
||||
def puncs(self, value):
|
||||
if not isinstance(value, six.string_types):
|
||||
raise ValueError("[!] Punctuations must be of type str.")
|
||||
self._puncs = "".join(list(dict.fromkeys(list(value)))) # remove duplicates without changing the oreder
|
||||
self.puncs_regular_exp = re.compile(rf"(\s*[{re.escape(self._puncs)}]+\s*)+")
|
||||
|
||||
def strip(self, text):
|
||||
"""Remove all the punctuations by replacing with `space`.
|
||||
|
||||
Args:
|
||||
text (str): The text to be processed.
|
||||
|
||||
Example::
|
||||
|
||||
"This is. example !" -> "This is example "
|
||||
"""
|
||||
return re.sub(self.puncs_regular_exp, " ", text).rstrip().lstrip()
|
||||
|
||||
def strip_to_restore(self, text):
|
||||
"""Remove punctuations from text to restore them later.
|
||||
|
||||
Args:
|
||||
text (str): The text to be processed.
|
||||
|
||||
Examples ::
|
||||
|
||||
"This is. example !" -> [["This is", "example"], [".", "!"]]
|
||||
|
||||
"""
|
||||
text, puncs = self._strip_to_restore(text)
|
||||
return text, puncs
|
||||
|
||||
def _strip_to_restore(self, text):
|
||||
"""Auxiliary method for Punctuation.preserve()"""
|
||||
matches = list(re.finditer(self.puncs_regular_exp, text))
|
||||
if not matches:
|
||||
return [text], []
|
||||
# the text is only punctuations
|
||||
if len(matches) == 1 and matches[0].group() == text:
|
||||
return [], [_PUNC_IDX(text, PuncPosition.ALONE)]
|
||||
# build a punctuation map to be used later to restore punctuations
|
||||
puncs = []
|
||||
for match in matches:
|
||||
position = PuncPosition.MIDDLE
|
||||
if match == matches[0] and text.startswith(match.group()):
|
||||
position = PuncPosition.BEGIN
|
||||
elif match == matches[-1] and text.endswith(match.group()):
|
||||
position = PuncPosition.END
|
||||
puncs.append(_PUNC_IDX(match.group(), position))
|
||||
# convert str text to a List[str], each item is separated by a punctuation
|
||||
splitted_text = []
|
||||
for idx, punc in enumerate(puncs):
|
||||
split = text.split(punc.punc)
|
||||
prefix, suffix = split[0], punc.punc.join(split[1:])
|
||||
splitted_text.append(prefix)
|
||||
# if the text does not end with a punctuation, add it to the last item
|
||||
if idx == len(puncs) - 1 and len(suffix) > 0:
|
||||
splitted_text.append(suffix)
|
||||
text = suffix
|
||||
return splitted_text, puncs
|
||||
|
||||
@classmethod
|
||||
def restore(cls, text, puncs):
|
||||
"""Restore punctuation in a text.
|
||||
|
||||
Args:
|
||||
text (str): The text to be processed.
|
||||
puncs (List[str]): The list of punctuations map to be used for restoring.
|
||||
|
||||
Examples ::
|
||||
|
||||
['This is', 'example'], ['.', '!'] -> "This is. example!"
|
||||
|
||||
"""
|
||||
return cls._restore(text, puncs, 0)
|
||||
|
||||
@classmethod
|
||||
def _restore(cls, text, puncs, num): # pylint: disable=too-many-return-statements
|
||||
"""Auxiliary method for Punctuation.restore()"""
|
||||
if not puncs:
|
||||
return text
|
||||
|
||||
# nothing have been phonemized, returns the puncs alone
|
||||
if not text:
|
||||
return ["".join(m.punc for m in puncs)]
|
||||
|
||||
current = puncs[0]
|
||||
|
||||
if current.position == PuncPosition.BEGIN:
|
||||
return cls._restore([current.punc + text[0]] + text[1:], puncs[1:], num)
|
||||
|
||||
if current.position == PuncPosition.END:
|
||||
return [text[0] + current.punc] + cls._restore(text[1:], puncs[1:], num + 1)
|
||||
|
||||
if current.position == PuncPosition.ALONE:
|
||||
return [current.mark] + cls._restore(text, puncs[1:], num + 1)
|
||||
|
||||
# POSITION == MIDDLE
|
||||
if len(text) == 1: # pragma: nocover
|
||||
# a corner case where the final part of an intermediate
|
||||
# mark (I) has not been phonemized
|
||||
return cls._restore([text[0] + current.punc], puncs[1:], num)
|
||||
|
||||
return cls._restore([text[0] + current.punc + text[1]] + text[2:], puncs[1:], num)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# punc = Punctuation()
|
||||
# text = "This is. This is, example!"
|
||||
|
||||
# print(punc.strip(text))
|
||||
|
||||
# split_text, puncs = punc.strip_to_restore(text)
|
||||
# print(split_text, " ---- ", puncs)
|
||||
|
||||
# restored_text = punc.restore(split_text, puncs)
|
||||
# print(restored_text)
|
||||
@@ -0,0 +1,95 @@
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from lib.constants import TTS_BERT_FRENCH_MODEL_DIR_PATH
|
||||
|
||||
from .fr_phonemizer import cleaner as fr_cleaner
|
||||
from .fr_phonemizer import fr_to_ipa
|
||||
|
||||
|
||||
def distribute_phone(n_phone, n_word):
|
||||
phones_per_word = [0] * n_word
|
||||
for task in range(n_phone):
|
||||
min_tasks = min(phones_per_word)
|
||||
min_index = phones_per_word.index(min_tasks)
|
||||
phones_per_word[min_index] += 1
|
||||
return phones_per_word
|
||||
|
||||
def text_normalize(text):
|
||||
text = fr_cleaner.french_cleaners(text)
|
||||
return text
|
||||
|
||||
load_model_params = {
|
||||
"pretrained_model_name_or_path": 'dbmdz/bert-base-french-europeana-cased',
|
||||
"local_files_only": True
|
||||
}
|
||||
tokenizer = AutoTokenizer.from_pretrained(**load_model_params)
|
||||
|
||||
def g2p(text, pad_start_end=True, tokenized=None):
|
||||
if tokenized is None:
|
||||
tokenized = tokenizer.tokenize(text)
|
||||
# import pdb; pdb.set_trace()
|
||||
phs = []
|
||||
ph_groups = []
|
||||
for t in tokenized:
|
||||
if not t.startswith("#"):
|
||||
ph_groups.append([t])
|
||||
else:
|
||||
ph_groups[-1].append(t.replace("#", ""))
|
||||
|
||||
phones = []
|
||||
tones = []
|
||||
word2ph = []
|
||||
# print(ph_groups)
|
||||
for group in ph_groups:
|
||||
w = "".join(group)
|
||||
phone_len = 0
|
||||
word_len = len(group)
|
||||
if w == '[UNK]':
|
||||
phone_list = ['UNK']
|
||||
else:
|
||||
phone_list = list(filter(lambda p: p != " ", fr_to_ipa.fr2ipa(w)))
|
||||
|
||||
for ph in phone_list:
|
||||
phones.append(ph)
|
||||
tones.append(0)
|
||||
phone_len += 1
|
||||
aaa = distribute_phone(phone_len, word_len)
|
||||
word2ph += aaa
|
||||
# print(phone_list, aaa)
|
||||
# print('=' * 10)
|
||||
|
||||
if pad_start_end:
|
||||
phones = ["_"] + phones + ["_"]
|
||||
tones = [0] + tones + [0]
|
||||
word2ph = [1] + word2ph + [1]
|
||||
return phones, tones, word2ph
|
||||
|
||||
def get_bert_feature(text, word2ph, device=None):
|
||||
from text import french_bert
|
||||
return french_bert.get_bert_feature(text, word2ph, device=device)
|
||||
|
||||
if __name__ == "__main__":
|
||||
ori_text = 'Ce service gratuit est“”"" 【disponible》 en chinois 【simplifié] et autres 123'
|
||||
# ori_text = "Ils essayaient vainement de faire comprendre à ma mère qu'avec les cent mille francs que m'avait laissé mon père,"
|
||||
# print(ori_text)
|
||||
text = text_normalize(ori_text)
|
||||
print(text)
|
||||
phoneme = fr_to_ipa.fr2ipa(text)
|
||||
print(phoneme)
|
||||
|
||||
|
||||
from TTS.tts.utils.text.phonemizers.multi_phonemizer import MultiPhonemizer
|
||||
from text.cleaner_multiling import unicleaners
|
||||
|
||||
def text_normalize(text):
|
||||
text = unicleaners(text, cased=True, lang='fr')
|
||||
return text
|
||||
|
||||
# print(ori_text)
|
||||
text = text_normalize(ori_text)
|
||||
print(text)
|
||||
phonemizer = MultiPhonemizer({"fr-fr": "espeak"})
|
||||
# phonemizer.lang_to_phonemizer['fr'].keep_stress = True
|
||||
# phonemizer.lang_to_phonemizer['fr'].use_espeak_phonemes = True
|
||||
phoneme = phonemizer.phonemize(text, separator="", language='fr-fr')
|
||||
print(phoneme)
|
||||
@@ -0,0 +1,44 @@
|
||||
import torch
|
||||
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
||||
import sys
|
||||
|
||||
from lib.constants import TTS_BERT_FRENCH_MODEL_DIR_PATH
|
||||
|
||||
load_model_params = {
|
||||
"pretrained_model_name_or_path": TTS_BERT_FRENCH_MODEL_DIR_PATH,
|
||||
"local_files_only": True
|
||||
}
|
||||
tokenizer = AutoTokenizer.from_pretrained(**load_model_params)
|
||||
model = None
|
||||
|
||||
def get_bert_feature(text, word2ph, device=None):
|
||||
global model
|
||||
if (
|
||||
sys.platform == "darwin"
|
||||
and torch.backends.mps.is_available()
|
||||
and device == "cpu"
|
||||
):
|
||||
device = "mps"
|
||||
if not device:
|
||||
device = "cuda"
|
||||
if model is None:
|
||||
model = AutoModelForMaskedLM.from_pretrained(**load_model_params).to(
|
||||
device
|
||||
)
|
||||
with torch.no_grad():
|
||||
inputs = tokenizer(text, return_tensors="pt")
|
||||
for i in inputs:
|
||||
inputs[i] = inputs[i].to(device)
|
||||
res = model(**inputs, output_hidden_states=True)
|
||||
res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu()
|
||||
|
||||
assert inputs["input_ids"].shape[-1] == len(word2ph)
|
||||
word2phone = word2ph
|
||||
phone_level_feature = []
|
||||
for i in range(len(word2phone)):
|
||||
repeat_feature = res[i].repeat(word2phone[i], 1)
|
||||
phone_level_feature.append(repeat_feature)
|
||||
|
||||
phone_level_feature = torch.cat(phone_level_feature, dim=0)
|
||||
|
||||
return phone_level_feature.T
|
||||
@@ -0,0 +1,429 @@
|
||||
a AA a
|
||||
ai AA ai
|
||||
an AA an
|
||||
ang AA ang
|
||||
ao AA ao
|
||||
ba b a
|
||||
bai b ai
|
||||
ban b an
|
||||
bang b ang
|
||||
bao b ao
|
||||
bei b ei
|
||||
ben b en
|
||||
beng b eng
|
||||
bi b i
|
||||
bian b ian
|
||||
biao b iao
|
||||
bie b ie
|
||||
bin b in
|
||||
bing b ing
|
||||
bo b o
|
||||
bu b u
|
||||
ca c a
|
||||
cai c ai
|
||||
can c an
|
||||
cang c ang
|
||||
cao c ao
|
||||
ce c e
|
||||
cei c ei
|
||||
cen c en
|
||||
ceng c eng
|
||||
cha ch a
|
||||
chai ch ai
|
||||
chan ch an
|
||||
chang ch ang
|
||||
chao ch ao
|
||||
che ch e
|
||||
chen ch en
|
||||
cheng ch eng
|
||||
chi ch ir
|
||||
chong ch ong
|
||||
chou ch ou
|
||||
chu ch u
|
||||
chua ch ua
|
||||
chuai ch uai
|
||||
chuan ch uan
|
||||
chuang ch uang
|
||||
chui ch ui
|
||||
chun ch un
|
||||
chuo ch uo
|
||||
ci c i0
|
||||
cong c ong
|
||||
cou c ou
|
||||
cu c u
|
||||
cuan c uan
|
||||
cui c ui
|
||||
cun c un
|
||||
cuo c uo
|
||||
da d a
|
||||
dai d ai
|
||||
dan d an
|
||||
dang d ang
|
||||
dao d ao
|
||||
de d e
|
||||
dei d ei
|
||||
den d en
|
||||
deng d eng
|
||||
di d i
|
||||
dia d ia
|
||||
dian d ian
|
||||
diao d iao
|
||||
die d ie
|
||||
ding d ing
|
||||
diu d iu
|
||||
dong d ong
|
||||
dou d ou
|
||||
du d u
|
||||
duan d uan
|
||||
dui d ui
|
||||
dun d un
|
||||
duo d uo
|
||||
e EE e
|
||||
ei EE ei
|
||||
en EE en
|
||||
eng EE eng
|
||||
er EE er
|
||||
fa f a
|
||||
fan f an
|
||||
fang f ang
|
||||
fei f ei
|
||||
fen f en
|
||||
feng f eng
|
||||
fo f o
|
||||
fou f ou
|
||||
fu f u
|
||||
ga g a
|
||||
gai g ai
|
||||
gan g an
|
||||
gang g ang
|
||||
gao g ao
|
||||
ge g e
|
||||
gei g ei
|
||||
gen g en
|
||||
geng g eng
|
||||
gong g ong
|
||||
gou g ou
|
||||
gu g u
|
||||
gua g ua
|
||||
guai g uai
|
||||
guan g uan
|
||||
guang g uang
|
||||
gui g ui
|
||||
gun g un
|
||||
guo g uo
|
||||
ha h a
|
||||
hai h ai
|
||||
han h an
|
||||
hang h ang
|
||||
hao h ao
|
||||
he h e
|
||||
hei h ei
|
||||
hen h en
|
||||
heng h eng
|
||||
hong h ong
|
||||
hou h ou
|
||||
hu h u
|
||||
hua h ua
|
||||
huai h uai
|
||||
huan h uan
|
||||
huang h uang
|
||||
hui h ui
|
||||
hun h un
|
||||
huo h uo
|
||||
ji j i
|
||||
jia j ia
|
||||
jian j ian
|
||||
jiang j iang
|
||||
jiao j iao
|
||||
jie j ie
|
||||
jin j in
|
||||
jing j ing
|
||||
jiong j iong
|
||||
jiu j iu
|
||||
ju j v
|
||||
jv j v
|
||||
juan j van
|
||||
jvan j van
|
||||
jue j ve
|
||||
jve j ve
|
||||
jun j vn
|
||||
jvn j vn
|
||||
ka k a
|
||||
kai k ai
|
||||
kan k an
|
||||
kang k ang
|
||||
kao k ao
|
||||
ke k e
|
||||
kei k ei
|
||||
ken k en
|
||||
keng k eng
|
||||
kong k ong
|
||||
kou k ou
|
||||
ku k u
|
||||
kua k ua
|
||||
kuai k uai
|
||||
kuan k uan
|
||||
kuang k uang
|
||||
kui k ui
|
||||
kun k un
|
||||
kuo k uo
|
||||
la l a
|
||||
lai l ai
|
||||
lan l an
|
||||
lang l ang
|
||||
lao l ao
|
||||
le l e
|
||||
lei l ei
|
||||
leng l eng
|
||||
li l i
|
||||
lia l ia
|
||||
lian l ian
|
||||
liang l iang
|
||||
liao l iao
|
||||
lie l ie
|
||||
lin l in
|
||||
ling l ing
|
||||
liu l iu
|
||||
lo l o
|
||||
long l ong
|
||||
lou l ou
|
||||
lu l u
|
||||
luan l uan
|
||||
lun l un
|
||||
luo l uo
|
||||
lv l v
|
||||
lve l ve
|
||||
ma m a
|
||||
mai m ai
|
||||
man m an
|
||||
mang m ang
|
||||
mao m ao
|
||||
me m e
|
||||
mei m ei
|
||||
men m en
|
||||
meng m eng
|
||||
mi m i
|
||||
mian m ian
|
||||
miao m iao
|
||||
mie m ie
|
||||
min m in
|
||||
ming m ing
|
||||
miu m iu
|
||||
mo m o
|
||||
mou m ou
|
||||
mu m u
|
||||
na n a
|
||||
nai n ai
|
||||
nan n an
|
||||
nang n ang
|
||||
nao n ao
|
||||
ne n e
|
||||
nei n ei
|
||||
nen n en
|
||||
neng n eng
|
||||
ni n i
|
||||
nian n ian
|
||||
niang n iang
|
||||
niao n iao
|
||||
nie n ie
|
||||
nin n in
|
||||
ning n ing
|
||||
niu n iu
|
||||
nong n ong
|
||||
nou n ou
|
||||
nu n u
|
||||
nuan n uan
|
||||
nun n un
|
||||
nuo n uo
|
||||
nv n v
|
||||
nve n ve
|
||||
o OO o
|
||||
ou OO ou
|
||||
pa p a
|
||||
pai p ai
|
||||
pan p an
|
||||
pang p ang
|
||||
pao p ao
|
||||
pei p ei
|
||||
pen p en
|
||||
peng p eng
|
||||
pi p i
|
||||
pian p ian
|
||||
piao p iao
|
||||
pie p ie
|
||||
pin p in
|
||||
ping p ing
|
||||
po p o
|
||||
pou p ou
|
||||
pu p u
|
||||
qi q i
|
||||
qia q ia
|
||||
qian q ian
|
||||
qiang q iang
|
||||
qiao q iao
|
||||
qie q ie
|
||||
qin q in
|
||||
qing q ing
|
||||
qiong q iong
|
||||
qiu q iu
|
||||
qu q v
|
||||
qv q v
|
||||
quan q van
|
||||
qvan q van
|
||||
que q ve
|
||||
qve q ve
|
||||
qun q vn
|
||||
qvn q vn
|
||||
ran r an
|
||||
rang r ang
|
||||
rao r ao
|
||||
re r e
|
||||
ren r en
|
||||
reng r eng
|
||||
ri r ir
|
||||
rong r ong
|
||||
rou r ou
|
||||
ru r u
|
||||
rua r ua
|
||||
ruan r uan
|
||||
rui r ui
|
||||
run r un
|
||||
ruo r uo
|
||||
sa s a
|
||||
sai s ai
|
||||
san s an
|
||||
sang s ang
|
||||
sao s ao
|
||||
se s e
|
||||
sen s en
|
||||
seng s eng
|
||||
sha sh a
|
||||
shai sh ai
|
||||
shan sh an
|
||||
shang sh ang
|
||||
shao sh ao
|
||||
she sh e
|
||||
shei sh ei
|
||||
shen sh en
|
||||
sheng sh eng
|
||||
shi sh ir
|
||||
shou sh ou
|
||||
shu sh u
|
||||
shua sh ua
|
||||
shuai sh uai
|
||||
shuan sh uan
|
||||
shuang sh uang
|
||||
shui sh ui
|
||||
shun sh un
|
||||
shuo sh uo
|
||||
si s i0
|
||||
song s ong
|
||||
sou s ou
|
||||
su s u
|
||||
suan s uan
|
||||
sui s ui
|
||||
sun s un
|
||||
suo s uo
|
||||
ta t a
|
||||
tai t ai
|
||||
tan t an
|
||||
tang t ang
|
||||
tao t ao
|
||||
te t e
|
||||
tei t ei
|
||||
teng t eng
|
||||
ti t i
|
||||
tian t ian
|
||||
tiao t iao
|
||||
tie t ie
|
||||
ting t ing
|
||||
tong t ong
|
||||
tou t ou
|
||||
tu t u
|
||||
tuan t uan
|
||||
tui t ui
|
||||
tun t un
|
||||
tuo t uo
|
||||
wa w a
|
||||
wai w ai
|
||||
wan w an
|
||||
wang w ang
|
||||
wei w ei
|
||||
wen w en
|
||||
weng w eng
|
||||
wo w o
|
||||
wu w u
|
||||
xi x i
|
||||
xia x ia
|
||||
xian x ian
|
||||
xiang x iang
|
||||
xiao x iao
|
||||
xie x ie
|
||||
xin x in
|
||||
xing x ing
|
||||
xiong x iong
|
||||
xiu x iu
|
||||
xu x v
|
||||
xv x v
|
||||
xuan x van
|
||||
xvan x van
|
||||
xue x ve
|
||||
xve x ve
|
||||
xun x vn
|
||||
xvn x vn
|
||||
ya y a
|
||||
yan y En
|
||||
yang y ang
|
||||
yao y ao
|
||||
ye y E
|
||||
yi y i
|
||||
yin y in
|
||||
ying y ing
|
||||
yo y o
|
||||
yong y ong
|
||||
you y ou
|
||||
yu y v
|
||||
yv y v
|
||||
yuan y van
|
||||
yvan y van
|
||||
yue y ve
|
||||
yve y ve
|
||||
yun y vn
|
||||
yvn y vn
|
||||
za z a
|
||||
zai z ai
|
||||
zan z an
|
||||
zang z ang
|
||||
zao z ao
|
||||
ze z e
|
||||
zei z ei
|
||||
zen z en
|
||||
zeng z eng
|
||||
zha zh a
|
||||
zhai zh ai
|
||||
zhan zh an
|
||||
zhang zh ang
|
||||
zhao zh ao
|
||||
zhe zh e
|
||||
zhei zh ei
|
||||
zhen zh en
|
||||
zheng zh eng
|
||||
zhi zh ir
|
||||
zhong zh ong
|
||||
zhou zh ou
|
||||
zhu zh u
|
||||
zhua zh ua
|
||||
zhuai zh uai
|
||||
zhuan zh uan
|
||||
zhuang zh uang
|
||||
zhui zh ui
|
||||
zhun zh un
|
||||
zhuo zh uo
|
||||
zi z i0
|
||||
zong z ong
|
||||
zou z ou
|
||||
zu z u
|
||||
zuan z uan
|
||||
zui z ui
|
||||
zun z un
|
||||
zuo z uo
|
||||
@@ -0,0 +1,290 @@
|
||||
# punctuation = ["!", "?", "…", ",", ".", "'", "-"]
|
||||
punctuation = ["!", "?", "…", ",", ".", "'", "-", "¿", "¡"]
|
||||
pu_symbols = punctuation + ["SP", "UNK"]
|
||||
pad = "_"
|
||||
|
||||
# chinese
|
||||
zh_symbols = [
|
||||
"E",
|
||||
"En",
|
||||
"a",
|
||||
"ai",
|
||||
"an",
|
||||
"ang",
|
||||
"ao",
|
||||
"b",
|
||||
"c",
|
||||
"ch",
|
||||
"d",
|
||||
"e",
|
||||
"ei",
|
||||
"en",
|
||||
"eng",
|
||||
"er",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"i0",
|
||||
"ia",
|
||||
"ian",
|
||||
"iang",
|
||||
"iao",
|
||||
"ie",
|
||||
"in",
|
||||
"ing",
|
||||
"iong",
|
||||
"ir",
|
||||
"iu",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"ong",
|
||||
"ou",
|
||||
"p",
|
||||
"q",
|
||||
"r",
|
||||
"s",
|
||||
"sh",
|
||||
"t",
|
||||
"u",
|
||||
"ua",
|
||||
"uai",
|
||||
"uan",
|
||||
"uang",
|
||||
"ui",
|
||||
"un",
|
||||
"uo",
|
||||
"v",
|
||||
"van",
|
||||
"ve",
|
||||
"vn",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"zh",
|
||||
"AA",
|
||||
"EE",
|
||||
"OO",
|
||||
]
|
||||
num_zh_tones = 6
|
||||
|
||||
# japanese
|
||||
ja_symbols = [
|
||||
"N",
|
||||
"a",
|
||||
"a:",
|
||||
"b",
|
||||
"by",
|
||||
"ch",
|
||||
"d",
|
||||
"dy",
|
||||
"e",
|
||||
"e:",
|
||||
"f",
|
||||
"g",
|
||||
"gy",
|
||||
"h",
|
||||
"hy",
|
||||
"i",
|
||||
"i:",
|
||||
"j",
|
||||
"k",
|
||||
"ky",
|
||||
"m",
|
||||
"my",
|
||||
"n",
|
||||
"ny",
|
||||
"o",
|
||||
"o:",
|
||||
"p",
|
||||
"py",
|
||||
"q",
|
||||
"r",
|
||||
"ry",
|
||||
"s",
|
||||
"sh",
|
||||
"t",
|
||||
"ts",
|
||||
"ty",
|
||||
"u",
|
||||
"u:",
|
||||
"w",
|
||||
"y",
|
||||
"z",
|
||||
"zy",
|
||||
]
|
||||
num_ja_tones = 1
|
||||
|
||||
# English
|
||||
en_symbols = [
|
||||
"aa",
|
||||
"ae",
|
||||
"ah",
|
||||
"ao",
|
||||
"aw",
|
||||
"ay",
|
||||
"b",
|
||||
"ch",
|
||||
"d",
|
||||
"dh",
|
||||
"eh",
|
||||
"er",
|
||||
"ey",
|
||||
"f",
|
||||
"g",
|
||||
"hh",
|
||||
"ih",
|
||||
"iy",
|
||||
"jh",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"ng",
|
||||
"ow",
|
||||
"oy",
|
||||
"p",
|
||||
"r",
|
||||
"s",
|
||||
"sh",
|
||||
"t",
|
||||
"th",
|
||||
"uh",
|
||||
"uw",
|
||||
"V",
|
||||
"w",
|
||||
"y",
|
||||
"z",
|
||||
"zh",
|
||||
]
|
||||
num_en_tones = 4
|
||||
|
||||
# Korean
|
||||
kr_symbols = ['ᄌ', 'ᅥ', 'ᆫ', 'ᅦ', 'ᄋ', 'ᅵ', 'ᄅ', 'ᅴ', 'ᄀ', 'ᅡ', 'ᄎ', 'ᅪ', 'ᄑ', 'ᅩ', 'ᄐ', 'ᄃ', 'ᅢ', 'ᅮ', 'ᆼ', 'ᅳ', 'ᄒ', 'ᄆ', 'ᆯ', 'ᆷ', 'ᄂ', 'ᄇ', 'ᄉ', 'ᆮ', 'ᄁ', 'ᅬ', 'ᅣ', 'ᄄ', 'ᆨ', 'ᄍ', 'ᅧ', 'ᄏ', 'ᆸ', 'ᅭ', '(', 'ᄊ', ')', 'ᅲ', 'ᅨ', 'ᄈ', 'ᅱ', 'ᅯ', 'ᅫ', 'ᅰ', 'ᅤ', '~', '\\', '[', ']', '/', '^', ':', 'ㄸ', '*']
|
||||
num_kr_tones = 1
|
||||
|
||||
# Spanish
|
||||
es_symbols = [
|
||||
"N",
|
||||
"Q",
|
||||
"a",
|
||||
"b",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"ɑ",
|
||||
"æ",
|
||||
"ʃ",
|
||||
"ʑ",
|
||||
"ç",
|
||||
"ɯ",
|
||||
"ɪ",
|
||||
"ɔ",
|
||||
"ɛ",
|
||||
"ɹ",
|
||||
"ð",
|
||||
"ə",
|
||||
"ɫ",
|
||||
"ɥ",
|
||||
"ɸ",
|
||||
"ʊ",
|
||||
"ɾ",
|
||||
"ʒ",
|
||||
"θ",
|
||||
"β",
|
||||
"ŋ",
|
||||
"ɦ",
|
||||
"ɡ",
|
||||
"r",
|
||||
"ɲ",
|
||||
"ʝ",
|
||||
"ɣ",
|
||||
"ʎ",
|
||||
"ˈ",
|
||||
"ˌ",
|
||||
"ː"
|
||||
]
|
||||
num_es_tones = 1
|
||||
|
||||
# French
|
||||
fr_symbols = [
|
||||
"\u0303",
|
||||
"œ",
|
||||
"ø",
|
||||
"ʁ",
|
||||
"ɒ",
|
||||
"ʌ",
|
||||
"ɜ",
|
||||
"ɐ"
|
||||
]
|
||||
num_fr_tones = 1
|
||||
|
||||
# German
|
||||
de_symbols = [
|
||||
"ʏ",
|
||||
"̩"
|
||||
]
|
||||
num_de_tones = 1
|
||||
|
||||
# Russian
|
||||
ru_symbols = [
|
||||
"ɭ",
|
||||
"ʲ",
|
||||
"ɕ",
|
||||
"\"",
|
||||
"ɵ",
|
||||
"^",
|
||||
"ɬ"
|
||||
]
|
||||
num_ru_tones = 1
|
||||
|
||||
# combine all symbols
|
||||
normal_symbols = sorted(set(zh_symbols + ja_symbols + en_symbols + kr_symbols + es_symbols + fr_symbols + de_symbols + ru_symbols))
|
||||
symbols = [pad] + normal_symbols + pu_symbols
|
||||
sil_phonemes_ids = [symbols.index(i) for i in pu_symbols]
|
||||
|
||||
# combine all tones
|
||||
num_tones = num_zh_tones + num_ja_tones + num_en_tones + num_kr_tones + num_es_tones + num_fr_tones + num_de_tones + num_ru_tones
|
||||
|
||||
# language maps
|
||||
language_id_map = {"ZH": 0, "JP": 1, "EN": 2, "ZH_MIX_EN": 3, 'KR': 4, 'ES': 5, 'SP': 5 ,'FR': 6}
|
||||
num_languages = len(language_id_map.keys())
|
||||
|
||||
language_tone_start_map = {
|
||||
"ZH": 0,
|
||||
"ZH_MIX_EN": 0,
|
||||
"JP": num_zh_tones,
|
||||
"EN": num_zh_tones + num_ja_tones,
|
||||
'KR': num_zh_tones + num_ja_tones + num_en_tones,
|
||||
"ES": num_zh_tones + num_ja_tones + num_en_tones + num_kr_tones,
|
||||
"SP": num_zh_tones + num_ja_tones + num_en_tones + num_kr_tones,
|
||||
"FR": num_zh_tones + num_ja_tones + num_en_tones + num_kr_tones + num_es_tones,
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = set(zh_symbols)
|
||||
b = set(en_symbols)
|
||||
print(sorted(a & b))
|
||||
@@ -0,0 +1,209 @@
|
||||
import torch
|
||||
from torch.nn import functional as F
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
DEFAULT_MIN_BIN_WIDTH = 1e-3
|
||||
DEFAULT_MIN_BIN_HEIGHT = 1e-3
|
||||
DEFAULT_MIN_DERIVATIVE = 1e-3
|
||||
|
||||
|
||||
def piecewise_rational_quadratic_transform(
|
||||
inputs,
|
||||
unnormalized_widths,
|
||||
unnormalized_heights,
|
||||
unnormalized_derivatives,
|
||||
inverse=False,
|
||||
tails=None,
|
||||
tail_bound=1.0,
|
||||
min_bin_width=DEFAULT_MIN_BIN_WIDTH,
|
||||
min_bin_height=DEFAULT_MIN_BIN_HEIGHT,
|
||||
min_derivative=DEFAULT_MIN_DERIVATIVE,
|
||||
):
|
||||
if tails is None:
|
||||
spline_fn = rational_quadratic_spline
|
||||
spline_kwargs = {}
|
||||
else:
|
||||
spline_fn = unconstrained_rational_quadratic_spline
|
||||
spline_kwargs = {"tails": tails, "tail_bound": tail_bound}
|
||||
|
||||
outputs, logabsdet = spline_fn(
|
||||
inputs=inputs,
|
||||
unnormalized_widths=unnormalized_widths,
|
||||
unnormalized_heights=unnormalized_heights,
|
||||
unnormalized_derivatives=unnormalized_derivatives,
|
||||
inverse=inverse,
|
||||
min_bin_width=min_bin_width,
|
||||
min_bin_height=min_bin_height,
|
||||
min_derivative=min_derivative,
|
||||
**spline_kwargs
|
||||
)
|
||||
return outputs, logabsdet
|
||||
|
||||
|
||||
def searchsorted(bin_locations, inputs, eps=1e-6):
|
||||
bin_locations[..., -1] += eps
|
||||
return torch.sum(inputs[..., None] >= bin_locations, dim=-1) - 1
|
||||
|
||||
|
||||
def unconstrained_rational_quadratic_spline(
|
||||
inputs,
|
||||
unnormalized_widths,
|
||||
unnormalized_heights,
|
||||
unnormalized_derivatives,
|
||||
inverse=False,
|
||||
tails="linear",
|
||||
tail_bound=1.0,
|
||||
min_bin_width=DEFAULT_MIN_BIN_WIDTH,
|
||||
min_bin_height=DEFAULT_MIN_BIN_HEIGHT,
|
||||
min_derivative=DEFAULT_MIN_DERIVATIVE,
|
||||
):
|
||||
inside_interval_mask = (inputs >= -tail_bound) & (inputs <= tail_bound)
|
||||
outside_interval_mask = ~inside_interval_mask
|
||||
|
||||
outputs = torch.zeros_like(inputs)
|
||||
logabsdet = torch.zeros_like(inputs)
|
||||
|
||||
if tails == "linear":
|
||||
unnormalized_derivatives = F.pad(unnormalized_derivatives, pad=(1, 1))
|
||||
constant = np.log(np.exp(1 - min_derivative) - 1)
|
||||
unnormalized_derivatives[..., 0] = constant
|
||||
unnormalized_derivatives[..., -1] = constant
|
||||
|
||||
outputs[outside_interval_mask] = inputs[outside_interval_mask]
|
||||
logabsdet[outside_interval_mask] = 0
|
||||
else:
|
||||
raise RuntimeError("{} tails are not implemented.".format(tails))
|
||||
|
||||
(
|
||||
outputs[inside_interval_mask],
|
||||
logabsdet[inside_interval_mask],
|
||||
) = rational_quadratic_spline(
|
||||
inputs=inputs[inside_interval_mask],
|
||||
unnormalized_widths=unnormalized_widths[inside_interval_mask, :],
|
||||
unnormalized_heights=unnormalized_heights[inside_interval_mask, :],
|
||||
unnormalized_derivatives=unnormalized_derivatives[inside_interval_mask, :],
|
||||
inverse=inverse,
|
||||
left=-tail_bound,
|
||||
right=tail_bound,
|
||||
bottom=-tail_bound,
|
||||
top=tail_bound,
|
||||
min_bin_width=min_bin_width,
|
||||
min_bin_height=min_bin_height,
|
||||
min_derivative=min_derivative,
|
||||
)
|
||||
|
||||
return outputs, logabsdet
|
||||
|
||||
|
||||
def rational_quadratic_spline(
|
||||
inputs,
|
||||
unnormalized_widths,
|
||||
unnormalized_heights,
|
||||
unnormalized_derivatives,
|
||||
inverse=False,
|
||||
left=0.0,
|
||||
right=1.0,
|
||||
bottom=0.0,
|
||||
top=1.0,
|
||||
min_bin_width=DEFAULT_MIN_BIN_WIDTH,
|
||||
min_bin_height=DEFAULT_MIN_BIN_HEIGHT,
|
||||
min_derivative=DEFAULT_MIN_DERIVATIVE,
|
||||
):
|
||||
if torch.min(inputs) < left or torch.max(inputs) > right:
|
||||
raise ValueError("Input to a transform is not within its domain")
|
||||
|
||||
num_bins = unnormalized_widths.shape[-1]
|
||||
|
||||
if min_bin_width * num_bins > 1.0:
|
||||
raise ValueError("Minimal bin width too large for the number of bins")
|
||||
if min_bin_height * num_bins > 1.0:
|
||||
raise ValueError("Minimal bin height too large for the number of bins")
|
||||
|
||||
widths = F.softmax(unnormalized_widths, dim=-1)
|
||||
widths = min_bin_width + (1 - min_bin_width * num_bins) * widths
|
||||
cumwidths = torch.cumsum(widths, dim=-1)
|
||||
cumwidths = F.pad(cumwidths, pad=(1, 0), mode="constant", value=0.0)
|
||||
cumwidths = (right - left) * cumwidths + left
|
||||
cumwidths[..., 0] = left
|
||||
cumwidths[..., -1] = right
|
||||
widths = cumwidths[..., 1:] - cumwidths[..., :-1]
|
||||
|
||||
derivatives = min_derivative + F.softplus(unnormalized_derivatives)
|
||||
|
||||
heights = F.softmax(unnormalized_heights, dim=-1)
|
||||
heights = min_bin_height + (1 - min_bin_height * num_bins) * heights
|
||||
cumheights = torch.cumsum(heights, dim=-1)
|
||||
cumheights = F.pad(cumheights, pad=(1, 0), mode="constant", value=0.0)
|
||||
cumheights = (top - bottom) * cumheights + bottom
|
||||
cumheights[..., 0] = bottom
|
||||
cumheights[..., -1] = top
|
||||
heights = cumheights[..., 1:] - cumheights[..., :-1]
|
||||
|
||||
if inverse:
|
||||
bin_idx = searchsorted(cumheights, inputs)[..., None]
|
||||
else:
|
||||
bin_idx = searchsorted(cumwidths, inputs)[..., None]
|
||||
|
||||
input_cumwidths = cumwidths.gather(-1, bin_idx)[..., 0]
|
||||
input_bin_widths = widths.gather(-1, bin_idx)[..., 0]
|
||||
|
||||
input_cumheights = cumheights.gather(-1, bin_idx)[..., 0]
|
||||
delta = heights / widths
|
||||
input_delta = delta.gather(-1, bin_idx)[..., 0]
|
||||
|
||||
input_derivatives = derivatives.gather(-1, bin_idx)[..., 0]
|
||||
input_derivatives_plus_one = derivatives[..., 1:].gather(-1, bin_idx)[..., 0]
|
||||
|
||||
input_heights = heights.gather(-1, bin_idx)[..., 0]
|
||||
|
||||
if inverse:
|
||||
a = (inputs - input_cumheights) * (
|
||||
input_derivatives + input_derivatives_plus_one - 2 * input_delta
|
||||
) + input_heights * (input_delta - input_derivatives)
|
||||
b = input_heights * input_derivatives - (inputs - input_cumheights) * (
|
||||
input_derivatives + input_derivatives_plus_one - 2 * input_delta
|
||||
)
|
||||
c = -input_delta * (inputs - input_cumheights)
|
||||
|
||||
discriminant = b.pow(2) - 4 * a * c
|
||||
assert (discriminant >= 0).all()
|
||||
|
||||
root = (2 * c) / (-b - torch.sqrt(discriminant))
|
||||
outputs = root * input_bin_widths + input_cumwidths
|
||||
|
||||
theta_one_minus_theta = root * (1 - root)
|
||||
denominator = input_delta + (
|
||||
(input_derivatives + input_derivatives_plus_one - 2 * input_delta)
|
||||
* theta_one_minus_theta
|
||||
)
|
||||
derivative_numerator = input_delta.pow(2) * (
|
||||
input_derivatives_plus_one * root.pow(2)
|
||||
+ 2 * input_delta * theta_one_minus_theta
|
||||
+ input_derivatives * (1 - root).pow(2)
|
||||
)
|
||||
logabsdet = torch.log(derivative_numerator) - 2 * torch.log(denominator)
|
||||
|
||||
return outputs, -logabsdet
|
||||
else:
|
||||
theta = (inputs - input_cumwidths) / input_bin_widths
|
||||
theta_one_minus_theta = theta * (1 - theta)
|
||||
|
||||
numerator = input_heights * (
|
||||
input_delta * theta.pow(2) + input_derivatives * theta_one_minus_theta
|
||||
)
|
||||
denominator = input_delta + (
|
||||
(input_derivatives + input_derivatives_plus_one - 2 * input_delta)
|
||||
* theta_one_minus_theta
|
||||
)
|
||||
outputs = input_cumheights + numerator / denominator
|
||||
|
||||
derivative_numerator = input_delta.pow(2) * (
|
||||
input_derivatives_plus_one * theta.pow(2)
|
||||
+ 2 * input_delta * theta_one_minus_theta
|
||||
+ input_derivatives * (1 - theta).pow(2)
|
||||
)
|
||||
logabsdet = torch.log(derivative_numerator) - 2 * torch.log(denominator)
|
||||
|
||||
return outputs, logabsdet
|
||||
@@ -0,0 +1,404 @@
|
||||
import os
|
||||
import glob
|
||||
import argparse
|
||||
import logging
|
||||
import json
|
||||
import subprocess
|
||||
import torch
|
||||
from lib.tts.text import cleaned_text_to_sequence, get_bert
|
||||
from lib.tts.text.cleaner import clean_text
|
||||
from lib.tts import commons
|
||||
|
||||
MATPLOTLIB_FLAG = False
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
def get_text_for_tts_infer(text, language_str, hps, device, symbol_to_id=None):
|
||||
norm_text, phone, tone, word2ph = clean_text(text, language_str)
|
||||
phone, tone, language = cleaned_text_to_sequence(phone, tone, language_str, symbol_to_id)
|
||||
|
||||
if hps.data.add_blank:
|
||||
phone = commons.intersperse(phone, 0)
|
||||
tone = commons.intersperse(tone, 0)
|
||||
language = commons.intersperse(language, 0)
|
||||
for i in range(len(word2ph)):
|
||||
word2ph[i] = word2ph[i] * 2
|
||||
word2ph[0] += 1
|
||||
|
||||
if getattr(hps.data, "disable_bert", False):
|
||||
bert = torch.zeros(1024, len(phone))
|
||||
ja_bert = torch.zeros(768, len(phone))
|
||||
else:
|
||||
bert = get_bert(norm_text, word2ph, language_str, device)
|
||||
del word2ph
|
||||
assert bert.shape[-1] == len(phone), phone
|
||||
|
||||
if language_str == "ZH":
|
||||
bert = bert
|
||||
ja_bert = torch.zeros(768, len(phone))
|
||||
elif language_str in ["JP", "EN", "ZH_MIX_EN", 'KR', 'SP', 'ES', 'FR', 'DE', 'RU']:
|
||||
ja_bert = bert
|
||||
bert = torch.zeros(1024, len(phone))
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
assert bert.shape[-1] == len(
|
||||
phone
|
||||
), f"Bert seq len {bert.shape[-1]} != {len(phone)}"
|
||||
|
||||
phone = torch.LongTensor(phone)
|
||||
tone = torch.LongTensor(tone)
|
||||
language = torch.LongTensor(language)
|
||||
return bert, ja_bert, phone, tone, language
|
||||
|
||||
def load_checkpoint(checkpoint_path, model, optimizer=None, skip_optimizer=False):
|
||||
assert os.path.isfile(checkpoint_path)
|
||||
checkpoint_dict = torch.load(checkpoint_path, map_location="cpu")
|
||||
iteration = checkpoint_dict.get("iteration", 0)
|
||||
learning_rate = checkpoint_dict.get("learning_rate", 0.)
|
||||
if (
|
||||
optimizer is not None
|
||||
and not skip_optimizer
|
||||
and checkpoint_dict["optimizer"] is not None
|
||||
):
|
||||
optimizer.load_state_dict(checkpoint_dict["optimizer"])
|
||||
elif optimizer is None and not skip_optimizer:
|
||||
# else: Disable this line if Infer and resume checkpoint,then enable the line upper
|
||||
new_opt_dict = optimizer.state_dict()
|
||||
new_opt_dict_params = new_opt_dict["param_groups"][0]["params"]
|
||||
new_opt_dict["param_groups"] = checkpoint_dict["optimizer"]["param_groups"]
|
||||
new_opt_dict["param_groups"][0]["params"] = new_opt_dict_params
|
||||
optimizer.load_state_dict(new_opt_dict)
|
||||
|
||||
saved_state_dict = checkpoint_dict["model"]
|
||||
if hasattr(model, "module"):
|
||||
state_dict = model.module.state_dict()
|
||||
else:
|
||||
state_dict = model.state_dict()
|
||||
|
||||
new_state_dict = {}
|
||||
for k, v in state_dict.items():
|
||||
try:
|
||||
# assert "emb_g" not in k
|
||||
new_state_dict[k] = saved_state_dict[k]
|
||||
assert saved_state_dict[k].shape == v.shape, (
|
||||
saved_state_dict[k].shape,
|
||||
v.shape,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
# For upgrading from the old version
|
||||
if "ja_bert_proj" in k:
|
||||
v = torch.zeros_like(v)
|
||||
logger.warn(
|
||||
f"Seems you are using the old version of the model, the {k} is automatically set to zero for backward compatibility"
|
||||
)
|
||||
else:
|
||||
logger.error(f"{k} is not in the checkpoint")
|
||||
|
||||
new_state_dict[k] = v
|
||||
|
||||
if hasattr(model, "module"):
|
||||
model.module.load_state_dict(new_state_dict, strict=False)
|
||||
else:
|
||||
model.load_state_dict(new_state_dict, strict=False)
|
||||
|
||||
logger.info(
|
||||
"Loaded checkpoint '{}' (iteration {})".format(checkpoint_path, iteration)
|
||||
)
|
||||
|
||||
return model, optimizer, learning_rate, iteration
|
||||
|
||||
|
||||
def save_checkpoint(model, optimizer, learning_rate, iteration, checkpoint_path):
|
||||
logger.info(
|
||||
"Saving model and optimizer state at iteration {} to {}".format(
|
||||
iteration, checkpoint_path
|
||||
)
|
||||
)
|
||||
if hasattr(model, "module"):
|
||||
state_dict = model.module.state_dict()
|
||||
else:
|
||||
state_dict = model.state_dict()
|
||||
torch.save(
|
||||
{
|
||||
"model": state_dict,
|
||||
"iteration": iteration,
|
||||
"optimizer": optimizer.state_dict(),
|
||||
"learning_rate": learning_rate,
|
||||
},
|
||||
checkpoint_path,
|
||||
)
|
||||
|
||||
|
||||
def summarize(
|
||||
writer,
|
||||
global_step,
|
||||
scalars={},
|
||||
histograms={},
|
||||
images={},
|
||||
audios={},
|
||||
audio_sampling_rate=22050,
|
||||
):
|
||||
for k, v in scalars.items():
|
||||
writer.add_scalar(k, v, global_step)
|
||||
for k, v in histograms.items():
|
||||
writer.add_histogram(k, v, global_step)
|
||||
for k, v in images.items():
|
||||
writer.add_image(k, v, global_step, dataformats="HWC")
|
||||
for k, v in audios.items():
|
||||
writer.add_audio(k, v, global_step, audio_sampling_rate)
|
||||
|
||||
|
||||
def latest_checkpoint_path(dir_path, regex="G_*.pth"):
|
||||
f_list = glob.glob(os.path.join(dir_path, regex))
|
||||
f_list.sort(key=lambda f: int("".join(filter(str.isdigit, f))))
|
||||
x = f_list[-1]
|
||||
return x
|
||||
|
||||
|
||||
def plot_spectrogram_to_numpy(spectrogram):
|
||||
global MATPLOTLIB_FLAG
|
||||
if not MATPLOTLIB_FLAG:
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
MATPLOTLIB_FLAG = True
|
||||
mpl_logger = logging.getLogger("matplotlib")
|
||||
mpl_logger.setLevel(logging.WARNING)
|
||||
import matplotlib.pylab as plt
|
||||
import numpy as np
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 2))
|
||||
im = ax.imshow(spectrogram, aspect="auto", origin="lower", interpolation="none")
|
||||
plt.colorbar(im, ax=ax)
|
||||
plt.xlabel("Frames")
|
||||
plt.ylabel("Channels")
|
||||
plt.tight_layout()
|
||||
|
||||
fig.canvas.draw()
|
||||
data = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep="")
|
||||
data = data.reshape(fig.canvas.get_width_height()[::-1] + (3,))
|
||||
plt.close()
|
||||
return data
|
||||
|
||||
|
||||
def plot_alignment_to_numpy(alignment, info=None):
|
||||
global MATPLOTLIB_FLAG
|
||||
if not MATPLOTLIB_FLAG:
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
MATPLOTLIB_FLAG = True
|
||||
mpl_logger = logging.getLogger("matplotlib")
|
||||
mpl_logger.setLevel(logging.WARNING)
|
||||
import matplotlib.pylab as plt
|
||||
import numpy as np
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6, 4))
|
||||
im = ax.imshow(
|
||||
alignment.transpose(), aspect="auto", origin="lower", interpolation="none"
|
||||
)
|
||||
fig.colorbar(im, ax=ax)
|
||||
xlabel = "Decoder timestep"
|
||||
if info is not None:
|
||||
xlabel += "\n\n" + info
|
||||
plt.xlabel(xlabel)
|
||||
plt.ylabel("Encoder timestep")
|
||||
plt.tight_layout()
|
||||
|
||||
fig.canvas.draw()
|
||||
data = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep="")
|
||||
data = data.reshape(fig.canvas.get_width_height()[::-1] + (3,))
|
||||
plt.close()
|
||||
return data
|
||||
|
||||
def load_filepaths_and_text(filename, split="|"):
|
||||
with open(filename, encoding="utf-8") as f:
|
||||
filepaths_and_text = [line.strip().split(split) for line in f]
|
||||
return filepaths_and_text
|
||||
|
||||
|
||||
def get_hparams(init=True):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
type=str,
|
||||
default="./configs/base.json",
|
||||
help="JSON file for configuration",
|
||||
)
|
||||
parser.add_argument('--local_rank', type=int, default=0)
|
||||
parser.add_argument('--world-size', type=int, default=1)
|
||||
parser.add_argument('--port', type=int, default=10000)
|
||||
parser.add_argument("-m", "--model", type=str, required=True, help="Model name")
|
||||
parser.add_argument('--pretrain_G', type=str, default=None,
|
||||
help='pretrain model')
|
||||
parser.add_argument('--pretrain_D', type=str, default=None,
|
||||
help='pretrain model D')
|
||||
parser.add_argument('--pretrain_dur', type=str, default=None,
|
||||
help='pretrain model duration')
|
||||
|
||||
args = parser.parse_args()
|
||||
model_dir = os.path.join("./logs", args.model)
|
||||
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
|
||||
config_path = args.config
|
||||
config_save_path = os.path.join(model_dir, "config.json")
|
||||
if init:
|
||||
with open(config_path, "r") as f:
|
||||
data = f.read()
|
||||
with open(config_save_path, "w") as f:
|
||||
f.write(data)
|
||||
else:
|
||||
with open(config_save_path, "r") as f:
|
||||
data = f.read()
|
||||
config = json.loads(data)
|
||||
|
||||
hparams = HParams(**config)
|
||||
hparams.model_dir = model_dir
|
||||
hparams.pretrain_G = args.pretrain_G
|
||||
hparams.pretrain_D = args.pretrain_D
|
||||
hparams.pretrain_dur = args.pretrain_dur
|
||||
hparams.port = args.port
|
||||
return hparams
|
||||
|
||||
|
||||
def clean_checkpoints(path_to_models="logs/44k/", n_ckpts_to_keep=2, sort_by_time=True):
|
||||
"""Freeing up space by deleting saved ckpts
|
||||
|
||||
Arguments:
|
||||
path_to_models -- Path to the model directory
|
||||
n_ckpts_to_keep -- Number of ckpts to keep, excluding G_0.pth and D_0.pth
|
||||
sort_by_time -- True -> chronologically delete ckpts
|
||||
False -> lexicographically delete ckpts
|
||||
"""
|
||||
import re
|
||||
|
||||
ckpts_files = [
|
||||
f
|
||||
for f in os.listdir(path_to_models)
|
||||
if os.path.isfile(os.path.join(path_to_models, f))
|
||||
]
|
||||
|
||||
def name_key(_f):
|
||||
return int(re.compile("._(\\d+)\\.pth").match(_f).group(1))
|
||||
|
||||
def time_key(_f):
|
||||
return os.path.getmtime(os.path.join(path_to_models, _f))
|
||||
|
||||
sort_key = time_key if sort_by_time else name_key
|
||||
|
||||
def x_sorted(_x):
|
||||
return sorted(
|
||||
[f for f in ckpts_files if f.startswith(_x) and not f.endswith("_0.pth")],
|
||||
key=sort_key,
|
||||
)
|
||||
|
||||
to_del = [
|
||||
os.path.join(path_to_models, fn)
|
||||
for fn in (x_sorted("G")[:-n_ckpts_to_keep] + x_sorted("D")[:-n_ckpts_to_keep])
|
||||
]
|
||||
|
||||
def del_info(fn):
|
||||
return logger.info(f".. Free up space by deleting ckpt {fn}")
|
||||
|
||||
def del_routine(x):
|
||||
return [os.remove(x), del_info(x)]
|
||||
|
||||
[del_routine(fn) for fn in to_del]
|
||||
|
||||
|
||||
def get_hparams_from_dir(model_dir):
|
||||
config_save_path = os.path.join(model_dir, "config.json")
|
||||
with open(config_save_path, "r", encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
config = json.loads(data)
|
||||
|
||||
hparams = HParams(**config)
|
||||
hparams.model_dir = model_dir
|
||||
return hparams
|
||||
|
||||
|
||||
def get_hparams_from_file(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
config = json.loads(data)
|
||||
|
||||
hparams = HParams(**config)
|
||||
return hparams
|
||||
|
||||
|
||||
def check_git_hash(model_dir):
|
||||
source_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
if not os.path.exists(os.path.join(source_dir, ".git")):
|
||||
logger.warn(
|
||||
"{} is not a git repository, therefore hash value comparison will be ignored.".format(
|
||||
source_dir
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
cur_hash = subprocess.getoutput("git rev-parse HEAD")
|
||||
|
||||
path = os.path.join(model_dir, "githash")
|
||||
if os.path.exists(path):
|
||||
saved_hash = open(path).read()
|
||||
if saved_hash != cur_hash:
|
||||
logger.warn(
|
||||
"git hash values are different. {}(saved) != {}(current)".format(
|
||||
saved_hash[:8], cur_hash[:8]
|
||||
)
|
||||
)
|
||||
else:
|
||||
open(path, "w").write(cur_hash)
|
||||
|
||||
|
||||
def get_logger(model_dir, filename="train.log"):
|
||||
global logger
|
||||
logger = logging.getLogger(os.path.basename(model_dir))
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s")
|
||||
if not os.path.exists(model_dir):
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
h = logging.FileHandler(os.path.join(model_dir, filename))
|
||||
h.setLevel(logging.DEBUG)
|
||||
h.setFormatter(formatter)
|
||||
logger.addHandler(h)
|
||||
return logger
|
||||
|
||||
|
||||
class HParams:
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
if type(v) == dict:
|
||||
v = HParams(**v)
|
||||
self[k] = v
|
||||
|
||||
def keys(self):
|
||||
return self.__dict__.keys()
|
||||
|
||||
def items(self):
|
||||
return self.__dict__.items()
|
||||
|
||||
def values(self):
|
||||
return self.__dict__.values()
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__dict__)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
return setattr(self, key, value)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.__dict__
|
||||
|
||||
def __repr__(self):
|
||||
return self.__dict__.__repr__()
|
||||
@@ -0,0 +1,36 @@
|
||||
import time
|
||||
import sys
|
||||
import json
|
||||
from .constants import SETTINGS_PATH
|
||||
|
||||
|
||||
class ThrottledCallback:
|
||||
def __init__(self, callback, min_interval):
|
||||
self.callback = callback
|
||||
self.min_interval = min_interval
|
||||
self.last_call = 0
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
current_time = time.time()
|
||||
if current_time - self.last_call > self.min_interval:
|
||||
self.callback(*args, **kwargs)
|
||||
self.last_call = current_time
|
||||
|
||||
|
||||
def is_macos():
|
||||
return sys.platform == 'darwin'
|
||||
|
||||
|
||||
def is_windows():
|
||||
return sys.platform == 'win32'
|
||||
|
||||
|
||||
def is_linux():
|
||||
return sys.platform == 'linux'
|
||||
|
||||
|
||||
def get_settings(key):
|
||||
with open(SETTINGS_PATH) as f:
|
||||
settings = json.load(f)
|
||||
|
||||
return settings[key]
|
||||
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import time
|
||||
import numpy as np
|
||||
from openwakeword.model import Model as WakeWordModel
|
||||
|
||||
from ..constants import WAKE_WORD_MODEL_FOLDER_PATH
|
||||
|
||||
class WakeWord:
|
||||
def __init__(self, asr, model_path, device='cpu', detection_threshold=0.5):
|
||||
tic = time.perf_counter()
|
||||
self.log('Loading model...')
|
||||
|
||||
self.log(f'Device: {device}')
|
||||
|
||||
self.asr = asr
|
||||
self.model_path = model_path
|
||||
self.device = device
|
||||
self.detection_threshold = detection_threshold
|
||||
self.chunk_size = 1280
|
||||
self.audio = None
|
||||
self.is_listening = False
|
||||
self.is_enabled = False
|
||||
|
||||
if not os.path.exists(model_path):
|
||||
self.log(f'Wake word model not found at {model_path}')
|
||||
return
|
||||
|
||||
# @see https://github.com/dscripka/openWakeWord/blob/main/openwakeword/model.py#L38
|
||||
# @see https://github.com/dscripka/openWakeWord/blob/main/openwakeword/utils.py#L38
|
||||
self.model = WakeWordModel(
|
||||
device=self.device,
|
||||
wakeword_models=[self.model_path],
|
||||
melspec_model_path=os.path.join(WAKE_WORD_MODEL_FOLDER_PATH, 'melspectrogram.onnx'),
|
||||
embedding_model_path=os.path.join(WAKE_WORD_MODEL_FOLDER_PATH, 'embedding.onnx'),
|
||||
ncpu=1,
|
||||
inference_framework='onnx'
|
||||
)
|
||||
|
||||
self.log('Model loaded')
|
||||
toc = time.perf_counter()
|
||||
|
||||
self.log(f'Time taken to load model: {toc - tic:0.4f} seconds')
|
||||
|
||||
self.is_enabled = True
|
||||
|
||||
def reset_model_state(self):
|
||||
"""
|
||||
Reset the wake word model's prediction buffer to avoid false triggers
|
||||
"""
|
||||
for mdl in self.model.prediction_buffer.keys():
|
||||
self.model.prediction_buffer[mdl] = []
|
||||
|
||||
def start_listening(self):
|
||||
if self.is_enabled:
|
||||
self.asr.is_recording = False
|
||||
self.is_listening = True
|
||||
self.audio = None
|
||||
|
||||
self.reset_model_state()
|
||||
|
||||
try:
|
||||
self.log('Listening...')
|
||||
|
||||
while self.is_listening:
|
||||
# Get audio
|
||||
# Reuse the shared mic audio stream with ASR
|
||||
self.audio = np.frombuffer(self.asr.mic_stream.read(self.chunk_size), dtype=np.int16)
|
||||
|
||||
# Feed to openWakeWord model
|
||||
prediction = self.model.predict(self.audio)
|
||||
|
||||
for mdl in self.model.prediction_buffer.keys():
|
||||
scores = list(self.model.prediction_buffer[mdl])
|
||||
|
||||
if scores[-1] > self.detection_threshold:
|
||||
self.log(f'Wakeword Detected! ({mdl})')
|
||||
self.stop_listening()
|
||||
self.asr.transcribed_callback('')
|
||||
self.asr.start_recording()
|
||||
except Exception as e:
|
||||
self.stop_listening()
|
||||
self.log('Error:', e)
|
||||
|
||||
def stop_listening(self):
|
||||
if self.is_enabled:
|
||||
self.is_listening = False
|
||||
self.log('Stopped listening')
|
||||
|
||||
@staticmethod
|
||||
def log(*args, **kwargs):
|
||||
print('[Wake word]', *args, **kwargs)
|
||||
@@ -0,0 +1,172 @@
|
||||
import argparse
|
||||
import ctypes
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from os.path import join
|
||||
from dotenv import load_dotenv
|
||||
|
||||
DEFAULT_LEON_PROFILE = "just-me"
|
||||
DEFAULT_TCP_SERVER_HOST = "127.0.0.1"
|
||||
DEFAULT_TCP_SERVER_PORT = 5_367
|
||||
|
||||
|
||||
def resolve_leon_home() -> str:
|
||||
configured_leon_home = os.getenv("LEON_HOME", "").strip()
|
||||
|
||||
if configured_leon_home:
|
||||
return os.path.abspath(configured_leon_home)
|
||||
|
||||
return os.path.join(os.path.expanduser("~"), ".leon")
|
||||
|
||||
|
||||
def resolve_leon_profile() -> str:
|
||||
return os.getenv("LEON_PROFILE", "").strip() or DEFAULT_LEON_PROFILE
|
||||
|
||||
|
||||
def resolve_leon_profile_path() -> str:
|
||||
configured_profile_path = os.getenv("LEON_PROFILE_PATH", "").strip()
|
||||
|
||||
if configured_profile_path:
|
||||
return os.path.abspath(configured_profile_path)
|
||||
|
||||
return os.path.join(resolve_leon_home(), "profiles", resolve_leon_profile())
|
||||
|
||||
|
||||
def _resolve_torch_root(pytorch_path: str) -> str | None:
|
||||
normalized_path = os.path.abspath(pytorch_path)
|
||||
if os.path.basename(normalized_path) == "torch" and os.path.isfile(
|
||||
os.path.join(normalized_path, "__init__.py")
|
||||
):
|
||||
return normalized_path
|
||||
|
||||
torch_candidate = os.path.join(normalized_path, "torch")
|
||||
if os.path.isfile(os.path.join(torch_candidate, "__init__.py")):
|
||||
return torch_candidate
|
||||
|
||||
torch_nested_candidate = os.path.join(normalized_path, "torch", "torch")
|
||||
if os.path.isfile(os.path.join(torch_nested_candidate, "__init__.py")):
|
||||
return torch_nested_candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _add_pytorch_path(pytorch_path: str | None) -> str | None:
|
||||
if not pytorch_path:
|
||||
return None
|
||||
|
||||
torch_root = _resolve_torch_root(pytorch_path)
|
||||
if torch_root:
|
||||
sys.path.insert(0, os.path.dirname(torch_root))
|
||||
return torch_root
|
||||
|
||||
sys.path.insert(0, os.path.abspath(pytorch_path))
|
||||
return None
|
||||
|
||||
|
||||
def _set_library_paths(paths: list[str]) -> None:
|
||||
if not paths:
|
||||
return
|
||||
|
||||
existing_path = ""
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
add_dll_directory = getattr(os, "add_dll_directory", None)
|
||||
for path in paths:
|
||||
if os.path.isdir(path) and add_dll_directory:
|
||||
add_dll_directory(path)
|
||||
existing_path = os.environ.get("PATH", "")
|
||||
os.environ["PATH"] = (
|
||||
os.pathsep.join([*paths, existing_path])
|
||||
if existing_path
|
||||
else os.pathsep.join(paths)
|
||||
)
|
||||
return
|
||||
|
||||
if sys.platform == "darwin":
|
||||
existing_path = os.environ.get("DYLD_LIBRARY_PATH", "")
|
||||
os.environ["DYLD_LIBRARY_PATH"] = (
|
||||
os.pathsep.join([*paths, existing_path])
|
||||
if existing_path
|
||||
else os.pathsep.join(paths)
|
||||
)
|
||||
return
|
||||
|
||||
existing_path = os.environ.get("LD_LIBRARY_PATH", "")
|
||||
os.environ["LD_LIBRARY_PATH"] = (
|
||||
os.pathsep.join([*paths, existing_path])
|
||||
if existing_path
|
||||
else os.pathsep.join(paths)
|
||||
)
|
||||
|
||||
|
||||
def _configure_external_libraries(
|
||||
pytorch_path: str | None, nvidia_path: str | None
|
||||
) -> None:
|
||||
lib_paths = []
|
||||
torch_root = _add_pytorch_path(pytorch_path)
|
||||
|
||||
if torch_root:
|
||||
torch_lib_path = os.path.join(torch_root, "lib")
|
||||
if os.path.isdir(torch_lib_path):
|
||||
lib_paths.append(torch_lib_path)
|
||||
|
||||
if nvidia_path:
|
||||
nvidia_root = os.path.abspath(nvidia_path)
|
||||
nvjitlink_pattern = os.path.join(
|
||||
nvidia_root, "nvjitlink", "lib", "libnvJitLink.so.*"
|
||||
)
|
||||
for library in [
|
||||
"cublas",
|
||||
"cudnn",
|
||||
"cusparse",
|
||||
"cusparse_full",
|
||||
"nccl",
|
||||
"nvshmem",
|
||||
"nvjitlink",
|
||||
]:
|
||||
candidate = os.path.join(nvidia_root, library, "lib")
|
||||
if os.path.isdir(candidate):
|
||||
lib_paths.append(candidate)
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
nvjitlink_candidates = sorted(glob.glob(nvjitlink_pattern), reverse=True)
|
||||
if nvjitlink_candidates:
|
||||
ctypes.CDLL(nvjitlink_candidates[0], mode=ctypes.RTLD_GLOBAL)
|
||||
|
||||
_set_library_paths(lib_paths)
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Leon TCP server")
|
||||
parser.add_argument(
|
||||
"lang", nargs="?", default="en", help="Language code (e.g. en, fr)"
|
||||
)
|
||||
parser.add_argument("--pytorch-path", dest="pytorch_path", type=str, default=None)
|
||||
parser.add_argument("--nvidia-path", dest="nvidia_path", type=str, default=None)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
args = _parse_args()
|
||||
os.environ["LEON_PY_TCP_SERVER_LANG"] = args.lang
|
||||
_configure_external_libraries(args.pytorch_path, args.nvidia_path)
|
||||
|
||||
dotenv_path = join(resolve_leon_profile_path(), ".env")
|
||||
load_dotenv(dotenv_path)
|
||||
|
||||
from lib.tcp_server import TCPServer
|
||||
|
||||
tcp_server_host = os.environ.get("LEON_PY_TCP_SERVER_HOST", DEFAULT_TCP_SERVER_HOST)
|
||||
tcp_server_port = os.environ.get("LEON_PY_TCP_SERVER_PORT", DEFAULT_TCP_SERVER_PORT)
|
||||
|
||||
tcp_server = TCPServer(tcp_server_host, tcp_server_port)
|
||||
|
||||
# Use thread as ASR starts recording audio and it blocks the main thread
|
||||
asr_thread = threading.Thread(target=tcp_server.init_asr)
|
||||
asr_thread.start()
|
||||
|
||||
tcp_server.init_tts()
|
||||
|
||||
tcp_server_thread = threading.Thread(target=tcp_server.init)
|
||||
tcp_server_thread.start()
|
||||
@@ -0,0 +1,23 @@
|
||||
[project]
|
||||
name = "leon-tcp-server"
|
||||
# Keep this metadata version for uv project compatibility only.
|
||||
# Leon runtime versioning still comes from `version.py`.
|
||||
version = "1.0.0"
|
||||
requires-python = "==3.11.9"
|
||||
dependencies = [
|
||||
"python-dotenv==0.19.2",
|
||||
"transformers==4.27.4",
|
||||
"g2p-en==2.1.0",
|
||||
"gruut[de,es,fr]==2.2.3",
|
||||
"inflect==7.0.0",
|
||||
"tqdm==4.66.4",
|
||||
"soundfile==0.12.1",
|
||||
"numba==0.59.1",
|
||||
"faster-whisper==1.1.1",
|
||||
"numpy==1.26.4",
|
||||
"openwakeword==0.6.0",
|
||||
"soundcard==0.4.5",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = '2.0.0'
|
||||
Reference in New Issue
Block a user