chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:44:08 +08:00
commit 983960e2dd
1244 changed files with 281996 additions and 0 deletions
+215
View File
@@ -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)
+57
View File
@@ -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
+52
View File
@@ -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'
+254
View File
@@ -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
}
}
View File
+199
View File
@@ -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)
+449
View File
@@ -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
+150
View File
@@ -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
+571
View File
@@ -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
+168
View File
@@ -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 didnt 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. “Its a nightmare. I said please kill her, dont 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'))
+29
View File
@@ -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
+37
View File
@@ -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.
+298
View File
@@ -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", "cest-à-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)
+95
View File
@@ -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
+290
View File
@@ -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))
+209
View File
@@ -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
+404
View File
@@ -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__()
+36
View File
@@ -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]
+91
View File
@@ -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)
+172
View File
@@ -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()
+23
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
__version__ = '2.0.0'