From 6f48ee2a7d9b9ad3fc3305f3c59c379d0ff08692 Mon Sep 17 00:00:00 2001 From: poyangyu Date: Mon, 26 May 2025 19:08:06 +0800 Subject: [PATCH 1/7] cache spk weights to reduce the latency --- add_spk.py | 4 ++ single_inference.py | 152 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 add_spk.py diff --git a/add_spk.py b/add_spk.py new file mode 100644 index 0000000..fff2921 --- /dev/null +++ b/add_spk.py @@ -0,0 +1,4 @@ +from single_inference import add_spk + +if __name__ == "__main__": + add_spk() diff --git a/single_inference.py b/single_inference.py index 7e99a2d..5936310 100755 --- a/single_inference.py +++ b/single_inference.py @@ -145,6 +145,25 @@ def frontend_zero_shot_dual(self, tts_text, prompt_text, prompt_speech_16k, flow 'prompt_speech_feat': speech_feat, 'prompt_speech_feat_len': speech_feat_len, 'llm_embedding': embedding, 'flow_embedding': flow_embedding} return model_input + + def frontend_customized(self, tts_text, spk_id): + tts_text_token, tts_text_token_len = self._extract_text_token(tts_text) + prompt_text_token = self.spk2info[spk_id]['prompt_text_token'] + prompt_text_token_len = self.spk2info[spk_id]['prompt_text_token_len'] + speech_feat = self.spk2info[spk_id]['speech_feat'] + speech_feat_len = self.spk2info[spk_id]['speech_feat_len'] + speech_token = self.spk2info[spk_id]['speech_token'] + speech_token_len = self.spk2info[spk_id]['speech_token_len'] + embedding = self.spk2info[spk_id]['embedding'] + + model_input = {'text': tts_text_token, 'text_len': tts_text_token_len, + 'prompt_text': prompt_text_token, 'prompt_text_len': prompt_text_token_len, + 'llm_prompt_speech_token': speech_token, 'llm_prompt_speech_token_len': speech_token_len, + 'flow_prompt_speech_token': speech_token, 'flow_prompt_speech_token_len': speech_token_len, + 'prompt_speech_feat': speech_feat, 'prompt_speech_feat_len': speech_feat_len, + 'llm_embedding': embedding, 'flow_embedding': embedding} + + return model_input ####model class CustomCosyVoiceModel(CosyVoiceModel): @@ -227,6 +246,52 @@ def __init__(self, model_dir): def list_avaliable_spks(self): spks = list(self.frontend.spk2info.keys()) return spks + + def remove_spk(self, spk_id): + # 載入原始的 pt 檔案 + model_name = f"{self.model_dir}/spk2info.pt" + data = torch.load(model_name, map_location=self.device) + + # 確保是字典型別,否則拋出例外 + if not isinstance(data, dict): + raise TypeError(f"The loaded data is of type {type(data)}, expected a dictionary.") + + if spk_id in data: + del data[spk_id] + + # 儲存到新的 pt 檔案 + torch.save(data, model_name) + print(data.keys()) + print(f"新增Speaker成功,儲存為 {spk_id}") + + def add_spk(self, spk_id, spk_info): + # 載入原始的 pt 檔案 + model_name = f"{self.model_dir}/spk2info.pt" + data = torch.load(model_name, map_location=self.device) + + # 確保是字典型別,否則拋出例外 + if not isinstance(data, dict): + raise TypeError(f"The loaded data is of type {type(data)}, expected a dictionary.") + + # 加入新的鍵值對 + data[spk_id] = spk_info + + # 儲存到新的 pt 檔案 + torch.save(data, model_name) + print(data.keys()) + print(f"新增Speaker成功,儲存為 {spk_id}") + + def cal_spk_info(self, audio_path, prompt_text): + prompt_speech_16k = load_wav(audio_path, 16000) + ret = {} + + ret['prompt_text_token'], ret['prompt_text_token_len'] = self.frontend._extract_text_token(prompt_text) + prompt_speech_22050 = torchaudio.transforms.Resample(orig_freq=16000, new_freq=22050)(prompt_speech_16k) + ret['speech_feat'], ret['speech_feat_len'] = self.frontend._extract_speech_feat(prompt_speech_22050) + ret['speech_token'], ret['speech_token_len'] = self.frontend._extract_speech_token(prompt_speech_16k) + ret['embedding'] = self.frontend._extract_spk_embedding(prompt_speech_16k) + + return ret def inference_sft(self, tts_text, spk_id): tts_speeches = [] @@ -274,6 +339,17 @@ def inference_zero_shot_no_normalize(self, tts_text, prompt_text, prompt_speech_ model_output = self.model.inference(**model_input) tts_speeches.append(model_output['tts_speech']) return {'tts_speech': torch.concat(tts_speeches, dim=1)} + + def inference_customized(self, tts_text, spk_id): + tts_speeches = [] + for i in re.split(r'(?<=[?!。.?!])\s*', tts_text): + if not len(i): + continue + print("Synthesizing:",i) + model_input = self.frontend.frontend_customized(i, spk_id) + model_output = self.model.inference(**model_input) + tts_speeches.append(model_output['tts_speech']) + return {'tts_speech': torch.concat(tts_speeches, dim=1)} ####wav2text def transcribe_audio(audio_file): @@ -363,8 +439,6 @@ def single_inference(speaker_prompt_audio_path, content_to_synthesize, output_pa else: speaker_prompt_text_transcription = transcribe_audio(speaker_prompt_audio_path) - - ###normalization speaker_prompt_text_transcription = cosyvoice.frontend.text_normalize_new( speaker_prompt_text_transcription, @@ -388,6 +462,27 @@ def single_inference(speaker_prompt_audio_path, content_to_synthesize, output_pa torchaudio.save(output_path, output['tts_speech'], 22050) print(f"Generated voice saved to {output_path}") +def inference_customized(content_to_synthesize, output_path, cosyvoice, bopomofo_converter, spk_id): + content_to_synthesize = content_to_synthesize + output_path = output_path.strip() + + ###normalization + content_to_synthesize = cosyvoice.frontend.text_normalize_new( + content_to_synthesize, + split=False + ) + + print("Content to be synthesized before bopomofo:",content_to_synthesize) + content_to_synthesize_bopomo = get_bopomofo_rare(content_to_synthesize, bopomofo_converter) + print("Content to be synthesized:",content_to_synthesize) + start = time.time() + output = cosyvoice.inference_customized(content_to_synthesize_bopomo, spk_id) + end = time.time() + print("Elapsed time:",end - start) + print("Generated audio length:", output['tts_speech'].shape[1]/22050, "seconds") + torchaudio.save(output_path, output['tts_speech'], 22050) + print(f"Generated voice saved to {output_path}") + def main(): ####args parser = argparse.ArgumentParser(description="Run BreezyVoice text-to-speech with custom inputs") @@ -410,8 +505,59 @@ def main(): output_path = args.output_path.strip() single_inference(speaker_prompt_audio_path, content_to_synthesize, output_path, cosyvoice, bopomofo_converter, args.speaker_prompt_text_transcription) +def main_customized(): + ####args + parser = argparse.ArgumentParser(description="Run BreezyVoice text-to-speech with custom inputs") + parser.add_argument("--content_to_synthesize", type=str, required=True, help="Specifies the content that will be synthesized into speech.") + parser.add_argument("--output_path", type=str, required=False, default="results/output.wav", help="Specifies the name and path for the output .wav file.") + parser.add_argument("--model_path", type=str, required=False, default = "models",help="Specifies the model used for speech synthesis.") + parser.add_argument("--spk_id", type=str, required=False, default = "test_human",help="spk's name") + args = parser.parse_args() + + + cosyvoice = CustomCosyVoice(args.model_path) + + bopomofo_converter = G2PWConverter() + + content_to_synthesize = args.content_to_synthesize + spk_id = args.spk_id + output_path = args.output_path.strip() + inference_customized(content_to_synthesize, output_path, cosyvoice, bopomofo_converter, spk_id) + +def add_spk(): + ####args + parser = argparse.ArgumentParser(description="Run BreezyVoice text-to-speech with custom inputs") + parser.add_argument("--content_to_synthesize", type=str, required=True, help="Specifies the content that will be synthesized into speech.") + parser.add_argument("--speaker_prompt_audio_path", type=str, required=True, help="Specifies the path to the prompt speech audio file of the speaker.") + parser.add_argument("--speaker_prompt_text_transcription", type=str, required=False, help="Specifies the transcription of the speaker prompt audio (Highly Recommended, if not provided, the system will fall back to transcribing with Whisper.)") + + parser.add_argument("--output_path", type=str, required=False, default="results/output.wav", help="Specifies the name and path for the output .wav file.") + + parser.add_argument("--model_path", type=str, required=False, default = "models",help="Specifies the model used for speech synthesis.") + parser.add_argument("--spk_id", type=str, required=False, default = "test_human",help="spk's name") + args = parser.parse_args() + speaker_prompt_audio_path = args.speaker_prompt_audio_path + content_to_synthesize = args.content_to_synthesize + + if args.speaker_prompt_text_transcription: + speaker_prompt_text_transcription = args.speaker_prompt_text_transcription + else: + speaker_prompt_text_transcription = transcribe_audio(speaker_prompt_audio_path) + + cosyvoice = CustomCosyVoice(args.model_path) + spk_info = cosyvoice.cal_spk_info(speaker_prompt_audio_path, speaker_prompt_text_transcription) + cosyvoice.add_spk(args.spk_id, spk_info) + +def remove_spk(): + ####args + parser = argparse.ArgumentParser(description="Run BreezyVoice text-to-speech with custom inputs") + parser.add_argument("--spk_id", type=str, required=False, default = "test_human",help="spk's name") + args = parser.parse_args() + cosyvoice = CustomCosyVoice(args.model_path) + cosyvoice.remove_spk(args.spk_id) + if __name__ == "__main__": - main() + main_customized() From 543dae4dcec760ec6d18a51894fb02b334a48045 Mon Sep 17 00:00:00 2001 From: poyangyu Date: Mon, 26 May 2025 19:46:28 +0800 Subject: [PATCH 2/7] remove unnecessary args and add comments --- add_spk.py | 7 +++++++ cache_inference.py | 15 +++++++++++++++ single_inference.py | 7 ++----- 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 cache_inference.py diff --git a/add_spk.py b/add_spk.py index fff2921..9aa874a 100644 --- a/add_spk.py +++ b/add_spk.py @@ -1,4 +1,11 @@ from single_inference import add_spk if __name__ == "__main__": + ''' + test command: + + python3 add_spk.py --spk_id 臺灣女 \ + --speaker_prompt_audio_path "data/example.wav" \ + --speaker_prompt_text_transcription "在密碼學中,加密是將明文資訊改變為難以讀取的密文內容,使之不可讀的方法。只有擁有解密方法的對象,經由解密過程,才能將密文還原為正常可讀的內容。" + ''' add_spk() diff --git a/cache_inference.py b/cache_inference.py new file mode 100644 index 0000000..3b72057 --- /dev/null +++ b/cache_inference.py @@ -0,0 +1,15 @@ +from single_inference import main_customized + +if __name__ == "__main__": + + ''' + By default, use the Breezyvoice model located in the ./models directory. + + test command: + + python3 cache_inference.py --spk_id 臺灣女 \ + --content_to_synthesize "歡迎使用聯發創新基地 BreezyVoice 模型。" \ + --output_path results/output.wav + ''' + + main_customized() diff --git a/single_inference.py b/single_inference.py index 5936310..96d7b49 100755 --- a/single_inference.py +++ b/single_inference.py @@ -225,6 +225,7 @@ def __init__(self, model_dir): if not os.path.exists(model_dir): model_dir = snapshot_download(model_dir) print("model", model_dir) + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') self.model_dir = model_dir with open('{}/cosyvoice.yaml'.format(model_dir), 'r') as f: @@ -527,17 +528,13 @@ def main_customized(): def add_spk(): ####args parser = argparse.ArgumentParser(description="Run BreezyVoice text-to-speech with custom inputs") - parser.add_argument("--content_to_synthesize", type=str, required=True, help="Specifies the content that will be synthesized into speech.") parser.add_argument("--speaker_prompt_audio_path", type=str, required=True, help="Specifies the path to the prompt speech audio file of the speaker.") parser.add_argument("--speaker_prompt_text_transcription", type=str, required=False, help="Specifies the transcription of the speaker prompt audio (Highly Recommended, if not provided, the system will fall back to transcribing with Whisper.)") - parser.add_argument("--output_path", type=str, required=False, default="results/output.wav", help="Specifies the name and path for the output .wav file.") - parser.add_argument("--model_path", type=str, required=False, default = "models",help="Specifies the model used for speech synthesis.") parser.add_argument("--spk_id", type=str, required=False, default = "test_human",help="spk's name") args = parser.parse_args() speaker_prompt_audio_path = args.speaker_prompt_audio_path - content_to_synthesize = args.content_to_synthesize if args.speaker_prompt_text_transcription: speaker_prompt_text_transcription = args.speaker_prompt_text_transcription @@ -557,7 +554,7 @@ def remove_spk(): cosyvoice.remove_spk(args.spk_id) if __name__ == "__main__": - main_customized() + main() From f0b2c5a5de6ff67b2b4a03386dabf5ee86ff7eb4 Mon Sep 17 00:00:00 2001 From: poyangyu Date: Tue, 27 May 2025 16:41:03 +0800 Subject: [PATCH 3/7] add TTS api server --- server.py | 83 +++++++++++++++++++++++++++++++++++++++++++++ single_inference.py | 11 +++--- 2 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 server.py diff --git a/server.py b/server.py new file mode 100644 index 0000000..e2dd565 --- /dev/null +++ b/server.py @@ -0,0 +1,83 @@ +# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import sys +import argparse +import logging +logging.getLogger('matplotlib').setLevel(logging.WARNING) +from fastapi import FastAPI, UploadFile, Form, File +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +import uvicorn +import numpy as np +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append('{}/../../..'.format(ROOT_DIR)) +sys.path.append('{}/../../../third_party/Matcha-TTS'.format(ROOT_DIR)) +from single_inference import CustomCosyVoice, get_bopomofo_rare +from cosyvoice.utils.file_utils import load_wav +from g2pw import G2PWConverter + +app = FastAPI() +# set cross region allowance +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"]) + + +def generate_data(model_output): + for i in model_output: + tts_audio = (i['tts_speech'].numpy() * (2 ** 15)).astype(np.int16).tobytes() + yield tts_audio + + +@app.get("/inference_sft") +@app.post("/inference_sft") +async def inference_sft(tts_text: str = Form(), spk_id: str = Form()): + content_to_synthesize = cosyvoice.frontend.text_normalize_new( + tts_text, + split=False + ) + content_to_synthesize_bopomo = get_bopomofo_rare(content_to_synthesize, bopomofo_converter) + model_output = cosyvoice.inference_sft(content_to_synthesize_bopomo, spk_id) + return StreamingResponse(generate_data(model_output)) + + +@app.get("/inference_zero_shot") +@app.post("/inference_zero_shot") +async def inference_zero_shot(tts_text: str = Form(), prompt_text: str = Form(), prompt_wav: UploadFile = File()): + prompt_speech_16k = load_wav(prompt_wav.file, 16000) + model_output = cosyvoice.inference_zero_shot(tts_text, prompt_text, prompt_speech_16k) + return StreamingResponse(generate_data(model_output)) + + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--port', + type=int, + default=50000) + parser.add_argument('--model_dir', + type=str, + default='models', + help='local path or modelscope repo id') + args = parser.parse_args() + try: + bopomofo_converter = G2PWConverter() + cosyvoice = CustomCosyVoice(args.model_dir) + except Exception: + raise TypeError('no valid model_type!') + uvicorn.run(app, host="0.0.0.0", port=args.port) \ No newline at end of file diff --git a/single_inference.py b/single_inference.py index 96d7b49..a5a6a41 100755 --- a/single_inference.py +++ b/single_inference.py @@ -295,12 +295,13 @@ def cal_spk_info(self, audio_path, prompt_text): return ret def inference_sft(self, tts_text, spk_id): - tts_speeches = [] - for i in self.frontend.text_normalize(tts_text, split=True): - model_input = self.frontend.frontend_sft(i, spk_id) + for i in re.split(r'(?<=[?!。.?!])\s*', tts_text): + if not len(i): + continue + print("Synthesizing:",i) + model_input = self.frontend.frontend_customized(i, spk_id) model_output = self.model.inference(**model_input) - tts_speeches.append(model_output['tts_speech']) - return {'tts_speech': torch.concat(tts_speeches, dim=1)} + yield model_output def inference_zero_shot(self, tts_text, prompt_text, prompt_speech_16k): prompt_text = self.frontend.text_normalize(prompt_text, split=False) From 01149d958d4415070b5656d0dc7aa7469a9f6c5f Mon Sep 17 00:00:00 2001 From: poyangyu Date: Mon, 2 Jun 2025 11:21:32 +0800 Subject: [PATCH 4/7] WebUI interface --- server.py | 48 +++++++++- single_inference.py | 21 ++++- static/script.js | 209 +++++++++++++++++++++++++++++++++++++++++++ static/style.css | 109 ++++++++++++++++++++++ templates/index.html | 111 +++++++++++++++++++++++ 5 files changed, 494 insertions(+), 4 deletions(-) create mode 100644 static/script.js create mode 100644 static/style.css create mode 100644 templates/index.html diff --git a/server.py b/server.py index e2dd565..97ff619 100644 --- a/server.py +++ b/server.py @@ -16,8 +16,10 @@ import argparse import logging logging.getLogger('matplotlib').setLevel(logging.WARNING) -from fastapi import FastAPI, UploadFile, Form, File -from fastapi.responses import StreamingResponse +from fastapi import FastAPI, UploadFile, Form, File, Request +from fastapi.responses import StreamingResponse, HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates from fastapi.middleware.cors import CORSMiddleware import uvicorn import numpy as np @@ -37,12 +39,18 @@ allow_methods=["*"], allow_headers=["*"]) +# 掛載靜態文件 +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") def generate_data(model_output): for i in model_output: tts_audio = (i['tts_speech'].numpy() * (2 ** 15)).astype(np.int16).tobytes() yield tts_audio +@app.get("/", response_class=HTMLResponse) +async def root(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) @app.get("/inference_sft") @app.post("/inference_sft") @@ -55,7 +63,6 @@ async def inference_sft(tts_text: str = Form(), spk_id: str = Form()): model_output = cosyvoice.inference_sft(content_to_synthesize_bopomo, spk_id) return StreamingResponse(generate_data(model_output)) - @app.get("/inference_zero_shot") @app.post("/inference_zero_shot") async def inference_zero_shot(tts_text: str = Form(), prompt_text: str = Form(), prompt_wav: UploadFile = File()): @@ -63,7 +70,42 @@ async def inference_zero_shot(tts_text: str = Form(), prompt_text: str = Form(), model_output = cosyvoice.inference_zero_shot(tts_text, prompt_text, prompt_speech_16k) return StreamingResponse(generate_data(model_output)) +@app.post("/add_speaker") +async def add_speaker( + spk_id: str = Form(), + prompt_wav: UploadFile = File(), + prompt_text: str = Form(None) +): + try: + prompt_speech_16k = load_wav(prompt_wav.file, 16000) + + if not prompt_text: + from single_inference import transcribe_audio + prompt_text = transcribe_audio(prompt_wav.file) + + spk_info = cosyvoice.cal_spk_info(prompt_wav.file, prompt_text) + cosyvoice.add_spk(spk_id, spk_info) + + return {"status": "success", "message": f"Speaker {spk_id} added successfully"} + except Exception as e: + return {"status": "error", "message": str(e)} +@app.post("/remove_speaker") +async def remove_speaker(spk_id: str = Form()): + try: + cosyvoice.remove_spk(spk_id) + return {"status": "success", "message": f"Speaker {spk_id} removed successfully"} + except Exception as e: + return {"status": "error", "message": str(e)} + +@app.get("/get_speakers") +async def get_speakers(): + try: + # 這裡需要實現獲取說話者列表的邏輯 + # 暫時返回空列表 + return list(cosyvoice.get_spks()) + except Exception as e: + return {"status": "error", "message": str(e)} if __name__ == '__main__': parser = argparse.ArgumentParser() diff --git a/single_inference.py b/single_inference.py index a5a6a41..702408c 100755 --- a/single_inference.py +++ b/single_inference.py @@ -239,11 +239,20 @@ def __init__(self, model_dir): instruct, configs['allowed_special']) self.model = CosyVoiceModel(configs['llm'], configs['flow'], configs['hift']) + self.spks = self.get_spks() self.model.load('{}/llm.pt'.format(model_dir), '{}/flow.pt'.format(model_dir), '{}/hift.pt'.format(model_dir)) del configs + def get_spks(self): + model_name = f"{self.model_dir}/spk2info.pt" + data = torch.load(model_name, map_location=self.device) + spks = data.keys() + del data # 釋放載入的數據 + torch.cuda.empty_cache() # 清空 GPU 快取 + return spks + def list_avaliable_spks(self): spks = list(self.frontend.spk2info.keys()) return spks @@ -262,8 +271,13 @@ def remove_spk(self, spk_id): # 儲存到新的 pt 檔案 torch.save(data, model_name) + self.spks = data.keys() print(data.keys()) - print(f"新增Speaker成功,儲存為 {spk_id}") + print(f"刪除Speaker成功: {spk_id}") + + # 釋放記憶體 + del data + torch.cuda.empty_cache() def add_spk(self, spk_id, spk_info): # 載入原始的 pt 檔案 @@ -279,8 +293,13 @@ def add_spk(self, spk_id, spk_info): # 儲存到新的 pt 檔案 torch.save(data, model_name) + self.spks = data.keys() print(data.keys()) print(f"新增Speaker成功,儲存為 {spk_id}") + + # 釋放記憶體 + del data + torch.cuda.empty_cache() def cal_spk_info(self, audio_path, prompt_text): prompt_speech_16k = load_wav(audio_path, 16000) diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..f86c8b2 --- /dev/null +++ b/static/script.js @@ -0,0 +1,209 @@ +document.addEventListener('DOMContentLoaded', function() { + const sftForm = document.getElementById('sftForm'); + const addSpeakerForm = document.getElementById('addSpeakerForm'); + const audioPlayer = document.getElementById('audioPlayer'); + const speakerSearch = document.getElementById('speakerSearch'); + let allSpeakers = []; // 存儲所有音色 + + // 將PCM數據轉換為WAV格式 + function pcmToWav(pcmData) { + const wavHeader = new ArrayBuffer(44); + const view = new DataView(wavHeader); + + // RIFF identifier + writeString(view, 0, 'RIFF'); + // RIFF chunk length + view.setUint32(4, 36 + pcmData.byteLength, true); + // RIFF type + writeString(view, 8, 'WAVE'); + // format chunk identifier + writeString(view, 12, 'fmt '); + // format chunk length + view.setUint32(16, 16, true); + // sample format (raw) + view.setUint16(20, 1, true); + // channel count + view.setUint16(22, 1, true); + // sample rate + view.setUint32(24, 22050, true); + // byte rate (sample rate * block align) + view.setUint32(28, 22050 * 2, true); + // block align (channel count * bytes per sample) + view.setUint16(32, 2, true); + // bits per sample + view.setUint16(34, 16, true); + // data chunk identifier + writeString(view, 36, 'data'); + // data chunk length + view.setUint32(40, pcmData.byteLength, true); + + // 合併WAV頭部和PCM數據 + const wavData = new Blob([wavHeader, pcmData], { type: 'audio/wav' }); + return wavData; + } + + // 輔助函數:寫入字符串到DataView + function writeString(view, offset, string) { + for (let i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + } + + // SFT 模式表單提交 + sftForm.addEventListener('submit', async function(e) { + e.preventDefault(); + const form = e.target; + form.classList.add('loading'); + + try { + const formData = new FormData(); + formData.append('tts_text', document.getElementById('sftText').value); + formData.append('spk_id', document.getElementById('sftSpeaker').value); + + const response = await fetch('/inference_sft', { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error('生成失敗'); + + const pcmData = await response.arrayBuffer(); + const wavData = pcmToWav(pcmData); + const audioUrl = URL.createObjectURL(wavData); + audioPlayer.src = audioUrl; + audioPlayer.play(); + } catch (error) { + alert('生成語音時發生錯誤:' + error.message); + } finally { + form.classList.remove('loading'); + } + }); + + // 新增音色表單提交 + addSpeakerForm.addEventListener('submit', async function(e) { + e.preventDefault(); + const form = e.target; + form.classList.add('loading'); + + try { + const formData = new FormData(); + formData.append('spk_id', document.getElementById('speakerId').value); + formData.append('prompt_wav', document.getElementById('speakerAudio').files[0]); + + const speakerText = document.getElementById('speakerText').value; + if (speakerText) { + formData.append('prompt_text', speakerText); + } + + const response = await fetch('/add_speaker', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (result.status === 'success') { + alert('成功新增音色!'); + // 重新載入音色列表 + loadSpeakers(); + } else { + throw new Error(result.message); + } + } catch (error) { + alert('新增音色時發生錯誤:' + error.message); + } finally { + form.classList.remove('loading'); + } + }); + + // 刪除音色 + async function removeSpeaker(spkId) { + if (!confirm(`確定要刪除音色 ${spkId} 嗎?`)) { + return; + } + + try { + const formData = new FormData(); + formData.append('spk_id', spkId); + + const response = await fetch('/remove_speaker', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (result.status === 'success') { + alert('成功刪除音色!'); + // 重新載入音色列表 + loadSpeakers(); + } else { + throw new Error(result.message); + } + } catch (error) { + alert('刪除音色時發生錯誤:' + error.message); + } + } + + // 更新音色列表UI + function updateSpeakerList(speakers) { + const speakerList = document.getElementById('speakerList'); + const speakerSelect = document.getElementById('sftSpeaker'); + + // 清空現有列表 + speakerList.innerHTML = ''; + speakerSelect.innerHTML = ''; + + // 添加新的選項 + speakers.forEach(speaker => { + // 添加到下拉選單 + const option = document.createElement('option'); + option.value = speaker; + option.textContent = speaker; + speakerSelect.appendChild(option); + + // 添加到表格 + const row = document.createElement('tr'); + row.innerHTML = ` + ${speaker} + + + + `; + speakerList.appendChild(row); + }); + } + + // 搜尋音色 + function searchSpeakers(query) { + const filteredSpeakers = allSpeakers.filter(speaker => + speaker.toLowerCase().includes(query.toLowerCase()) + ); + updateSpeakerList(filteredSpeakers); + } + + // 設置搜尋事件監聽器 + speakerSearch.addEventListener('input', function(e) { + searchSpeakers(e.target.value); + }); + + // 載入音色列表 + async function loadSpeakers() { + try { + const response = await fetch('/get_speakers'); + if (!response.ok) throw new Error('獲取音色列表失敗'); + + allSpeakers = await response.json(); + const searchQuery = speakerSearch.value; + searchSpeakers(searchQuery); + } catch (error) { + console.error('載入音色列表失敗:', error); + } + } + + // 將removeSpeaker函數添加到全局作用域 + window.removeSpeaker = removeSpeaker; + + // 初始載入音色列表 + loadSpeakers(); +}); \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..99a13cf --- /dev/null +++ b/static/style.css @@ -0,0 +1,109 @@ +body { + background-color: #f8f9fa; +} + +.card { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + border: none; +} + +.card-header { + background-color: #fff; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +.btn-primary { + background-color: #0d6efd; + border-color: #0d6efd; +} + +.btn-primary:hover { + background-color: #0b5ed7; + border-color: #0a58ca; +} + +.btn-success { + background-color: #198754; + border-color: #198754; +} + +.btn-success:hover { + background-color: #157347; + border-color: #146c43; +} + +.form-control:focus, .form-select:focus { + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +#audioPlayer { + margin-top: 1rem; +} + +.loading { + position: relative; + pointer-events: none; + opacity: 0.7; +} + +.loading::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 1.5rem; + height: 1.5rem; + margin: -0.75rem 0 0 -0.75rem; + border: 0.2rem solid #f3f3f3; + border-top: 0.2rem solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* 摺疊面板樣式 */ +.card-header[data-bs-toggle="collapse"] { + transition: background-color 0.2s; +} + +.card-header[data-bs-toggle="collapse"]:hover { + background-color: #f8f9fa; +} + +.card-header[data-bs-toggle="collapse"] .bi { + transition: transform 0.2s; +} + +.card-header[data-bs-toggle="collapse"][aria-expanded="true"] .bi { + transform: rotate(180deg); +} + +/* 搜尋框樣式 */ +#speakerSearch { + border-radius: 20px; + padding-left: 1rem; + padding-right: 1rem; +} + +#speakerSearch:focus { + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +/* 表格樣式 */ +.table { + margin-bottom: 0; +} + +.table td { + vertical-align: middle; +} + +.btn-danger { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..099ca9c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,111 @@ + + + + + + BreezyVoice TTS + + + + + +
+

BreezyVoice TTS

+ +
+
+
+
+
預訓練模式
+
+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+ +
+
+
音色列表
+ +
+
+
+
+ +
+
+ + + + + + + + + + +
音色ID操作
+
+
+
+
+ +
+
+
新增音色
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ +
+
+
生成的語音
+
+
+ +
+
+
+ + + + + \ No newline at end of file From 2cff61a6bc2329d9cb18f58b7bf192d7a00d4858 Mon Sep 17 00:00:00 2001 From: poyangyu Date: Mon, 2 Jun 2025 12:06:11 +0800 Subject: [PATCH 5/7] remove unnecessary VRAM-occupying code --- server.py | 21 +++++++++++++++++---- single_inference.py | 43 +++++++------------------------------------ 2 files changed, 24 insertions(+), 40 deletions(-) diff --git a/server.py b/server.py index 97ff619..4c5b6aa 100644 --- a/server.py +++ b/server.py @@ -77,17 +77,30 @@ async def add_speaker( prompt_text: str = Form(None) ): try: - prompt_speech_16k = load_wav(prompt_wav.file, 16000) + # Save the uploaded file temporarily + temp_file_path = f"/tmp/{prompt_wav.filename}" + with open(temp_file_path, "wb") as buffer: + content = await prompt_wav.read() + buffer.write(content) + + # Process the saved file + prompt_speech_16k = load_wav(temp_file_path, 16000) if not prompt_text: from single_inference import transcribe_audio - prompt_text = transcribe_audio(prompt_wav.file) + prompt_text = transcribe_audio(temp_file_path) - spk_info = cosyvoice.cal_spk_info(prompt_wav.file, prompt_text) + spk_info = cosyvoice.cal_spk_info(temp_file_path, prompt_text) cosyvoice.add_spk(spk_id, spk_info) + # Clean up the temporary file + os.remove(temp_file_path) + return {"status": "success", "message": f"Speaker {spk_id} added successfully"} except Exception as e: + # Clean up the temporary file in case of error + if os.path.exists(temp_file_path): + os.remove(temp_file_path) return {"status": "error", "message": str(e)} @app.post("/remove_speaker") @@ -103,7 +116,7 @@ async def get_speakers(): try: # 這裡需要實現獲取說話者列表的邏輯 # 暫時返回空列表 - return list(cosyvoice.get_spks()) + return list(cosyvoice.list_avaliable_spks()) except Exception as e: return {"status": "error", "message": str(e)} diff --git a/single_inference.py b/single_inference.py index 702408c..bb0c06f 100755 --- a/single_inference.py +++ b/single_inference.py @@ -239,19 +239,10 @@ def __init__(self, model_dir): instruct, configs['allowed_special']) self.model = CosyVoiceModel(configs['llm'], configs['flow'], configs['hift']) - self.spks = self.get_spks() self.model.load('{}/llm.pt'.format(model_dir), '{}/flow.pt'.format(model_dir), '{}/hift.pt'.format(model_dir)) del configs - - def get_spks(self): - model_name = f"{self.model_dir}/spk2info.pt" - data = torch.load(model_name, map_location=self.device) - spks = data.keys() - del data # 釋放載入的數據 - torch.cuda.empty_cache() # 清空 GPU 快取 - return spks def list_avaliable_spks(self): spks = list(self.frontend.spk2info.keys()) @@ -260,46 +251,26 @@ def list_avaliable_spks(self): def remove_spk(self, spk_id): # 載入原始的 pt 檔案 model_name = f"{self.model_dir}/spk2info.pt" - data = torch.load(model_name, map_location=self.device) - - # 確保是字典型別,否則拋出例外 - if not isinstance(data, dict): - raise TypeError(f"The loaded data is of type {type(data)}, expected a dictionary.") - if spk_id in data: - del data[spk_id] + if spk_id in self.frontend.spk2info: + del self.frontend.spk2info[spk_id] # 儲存到新的 pt 檔案 - torch.save(data, model_name) - self.spks = data.keys() - print(data.keys()) + torch.save(self.frontend.spk2info, model_name) + print(self.frontend.spk2info.keys()) print(f"刪除Speaker成功: {spk_id}") - # 釋放記憶體 - del data - torch.cuda.empty_cache() def add_spk(self, spk_id, spk_info): # 載入原始的 pt 檔案 model_name = f"{self.model_dir}/spk2info.pt" - data = torch.load(model_name, map_location=self.device) - - # 確保是字典型別,否則拋出例外 - if not isinstance(data, dict): - raise TypeError(f"The loaded data is of type {type(data)}, expected a dictionary.") - - # 加入新的鍵值對 - data[spk_id] = spk_info + self.frontend.spk2info[spk_id] = spk_info # 儲存到新的 pt 檔案 - torch.save(data, model_name) - self.spks = data.keys() - print(data.keys()) + torch.save(self.frontend.spk2info, model_name) + print(self.frontend.spk2info.keys()) print(f"新增Speaker成功,儲存為 {spk_id}") - # 釋放記憶體 - del data - torch.cuda.empty_cache() def cal_spk_info(self, audio_path, prompt_text): prompt_speech_16k = load_wav(audio_path, 16000) From 42371ff4c78e2c5d739908065b3cf1d72f97b8b3 Mon Sep 17 00:00:00 2001 From: BrandonYU34 Date: Tue, 1 Jul 2025 06:11:48 +0000 Subject: [PATCH 6/7] remove server and UI --- .gitignore | 2 + server.py | 138 ---------------------------- static/script.js | 209 ------------------------------------------- static/style.css | 109 ---------------------- templates/index.html | 111 ----------------------- 5 files changed, 2 insertions(+), 567 deletions(-) create mode 100644 .gitignore delete mode 100644 server.py delete mode 100644 static/script.js delete mode 100644 static/style.css delete mode 100644 templates/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87559b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +G2PWModel/ \ No newline at end of file diff --git a/server.py b/server.py deleted file mode 100644 index 4c5b6aa..0000000 --- a/server.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu) -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os -import sys -import argparse -import logging -logging.getLogger('matplotlib').setLevel(logging.WARNING) -from fastapi import FastAPI, UploadFile, Form, File, Request -from fastapi.responses import StreamingResponse, HTMLResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from fastapi.middleware.cors import CORSMiddleware -import uvicorn -import numpy as np -ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -sys.path.append('{}/../../..'.format(ROOT_DIR)) -sys.path.append('{}/../../../third_party/Matcha-TTS'.format(ROOT_DIR)) -from single_inference import CustomCosyVoice, get_bopomofo_rare -from cosyvoice.utils.file_utils import load_wav -from g2pw import G2PWConverter - -app = FastAPI() -# set cross region allowance -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"]) - -# 掛載靜態文件 -app.mount("/static", StaticFiles(directory="static"), name="static") -templates = Jinja2Templates(directory="templates") - -def generate_data(model_output): - for i in model_output: - tts_audio = (i['tts_speech'].numpy() * (2 ** 15)).astype(np.int16).tobytes() - yield tts_audio - -@app.get("/", response_class=HTMLResponse) -async def root(request: Request): - return templates.TemplateResponse("index.html", {"request": request}) - -@app.get("/inference_sft") -@app.post("/inference_sft") -async def inference_sft(tts_text: str = Form(), spk_id: str = Form()): - content_to_synthesize = cosyvoice.frontend.text_normalize_new( - tts_text, - split=False - ) - content_to_synthesize_bopomo = get_bopomofo_rare(content_to_synthesize, bopomofo_converter) - model_output = cosyvoice.inference_sft(content_to_synthesize_bopomo, spk_id) - return StreamingResponse(generate_data(model_output)) - -@app.get("/inference_zero_shot") -@app.post("/inference_zero_shot") -async def inference_zero_shot(tts_text: str = Form(), prompt_text: str = Form(), prompt_wav: UploadFile = File()): - prompt_speech_16k = load_wav(prompt_wav.file, 16000) - model_output = cosyvoice.inference_zero_shot(tts_text, prompt_text, prompt_speech_16k) - return StreamingResponse(generate_data(model_output)) - -@app.post("/add_speaker") -async def add_speaker( - spk_id: str = Form(), - prompt_wav: UploadFile = File(), - prompt_text: str = Form(None) -): - try: - # Save the uploaded file temporarily - temp_file_path = f"/tmp/{prompt_wav.filename}" - with open(temp_file_path, "wb") as buffer: - content = await prompt_wav.read() - buffer.write(content) - - # Process the saved file - prompt_speech_16k = load_wav(temp_file_path, 16000) - - if not prompt_text: - from single_inference import transcribe_audio - prompt_text = transcribe_audio(temp_file_path) - - spk_info = cosyvoice.cal_spk_info(temp_file_path, prompt_text) - cosyvoice.add_spk(spk_id, spk_info) - - # Clean up the temporary file - os.remove(temp_file_path) - - return {"status": "success", "message": f"Speaker {spk_id} added successfully"} - except Exception as e: - # Clean up the temporary file in case of error - if os.path.exists(temp_file_path): - os.remove(temp_file_path) - return {"status": "error", "message": str(e)} - -@app.post("/remove_speaker") -async def remove_speaker(spk_id: str = Form()): - try: - cosyvoice.remove_spk(spk_id) - return {"status": "success", "message": f"Speaker {spk_id} removed successfully"} - except Exception as e: - return {"status": "error", "message": str(e)} - -@app.get("/get_speakers") -async def get_speakers(): - try: - # 這裡需要實現獲取說話者列表的邏輯 - # 暫時返回空列表 - return list(cosyvoice.list_avaliable_spks()) - except Exception as e: - return {"status": "error", "message": str(e)} - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('--port', - type=int, - default=50000) - parser.add_argument('--model_dir', - type=str, - default='models', - help='local path or modelscope repo id') - args = parser.parse_args() - try: - bopomofo_converter = G2PWConverter() - cosyvoice = CustomCosyVoice(args.model_dir) - except Exception: - raise TypeError('no valid model_type!') - uvicorn.run(app, host="0.0.0.0", port=args.port) \ No newline at end of file diff --git a/static/script.js b/static/script.js deleted file mode 100644 index f86c8b2..0000000 --- a/static/script.js +++ /dev/null @@ -1,209 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - const sftForm = document.getElementById('sftForm'); - const addSpeakerForm = document.getElementById('addSpeakerForm'); - const audioPlayer = document.getElementById('audioPlayer'); - const speakerSearch = document.getElementById('speakerSearch'); - let allSpeakers = []; // 存儲所有音色 - - // 將PCM數據轉換為WAV格式 - function pcmToWav(pcmData) { - const wavHeader = new ArrayBuffer(44); - const view = new DataView(wavHeader); - - // RIFF identifier - writeString(view, 0, 'RIFF'); - // RIFF chunk length - view.setUint32(4, 36 + pcmData.byteLength, true); - // RIFF type - writeString(view, 8, 'WAVE'); - // format chunk identifier - writeString(view, 12, 'fmt '); - // format chunk length - view.setUint32(16, 16, true); - // sample format (raw) - view.setUint16(20, 1, true); - // channel count - view.setUint16(22, 1, true); - // sample rate - view.setUint32(24, 22050, true); - // byte rate (sample rate * block align) - view.setUint32(28, 22050 * 2, true); - // block align (channel count * bytes per sample) - view.setUint16(32, 2, true); - // bits per sample - view.setUint16(34, 16, true); - // data chunk identifier - writeString(view, 36, 'data'); - // data chunk length - view.setUint32(40, pcmData.byteLength, true); - - // 合併WAV頭部和PCM數據 - const wavData = new Blob([wavHeader, pcmData], { type: 'audio/wav' }); - return wavData; - } - - // 輔助函數:寫入字符串到DataView - function writeString(view, offset, string) { - for (let i = 0; i < string.length; i++) { - view.setUint8(offset + i, string.charCodeAt(i)); - } - } - - // SFT 模式表單提交 - sftForm.addEventListener('submit', async function(e) { - e.preventDefault(); - const form = e.target; - form.classList.add('loading'); - - try { - const formData = new FormData(); - formData.append('tts_text', document.getElementById('sftText').value); - formData.append('spk_id', document.getElementById('sftSpeaker').value); - - const response = await fetch('/inference_sft', { - method: 'POST', - body: formData - }); - - if (!response.ok) throw new Error('生成失敗'); - - const pcmData = await response.arrayBuffer(); - const wavData = pcmToWav(pcmData); - const audioUrl = URL.createObjectURL(wavData); - audioPlayer.src = audioUrl; - audioPlayer.play(); - } catch (error) { - alert('生成語音時發生錯誤:' + error.message); - } finally { - form.classList.remove('loading'); - } - }); - - // 新增音色表單提交 - addSpeakerForm.addEventListener('submit', async function(e) { - e.preventDefault(); - const form = e.target; - form.classList.add('loading'); - - try { - const formData = new FormData(); - formData.append('spk_id', document.getElementById('speakerId').value); - formData.append('prompt_wav', document.getElementById('speakerAudio').files[0]); - - const speakerText = document.getElementById('speakerText').value; - if (speakerText) { - formData.append('prompt_text', speakerText); - } - - const response = await fetch('/add_speaker', { - method: 'POST', - body: formData - }); - - const result = await response.json(); - - if (result.status === 'success') { - alert('成功新增音色!'); - // 重新載入音色列表 - loadSpeakers(); - } else { - throw new Error(result.message); - } - } catch (error) { - alert('新增音色時發生錯誤:' + error.message); - } finally { - form.classList.remove('loading'); - } - }); - - // 刪除音色 - async function removeSpeaker(spkId) { - if (!confirm(`確定要刪除音色 ${spkId} 嗎?`)) { - return; - } - - try { - const formData = new FormData(); - formData.append('spk_id', spkId); - - const response = await fetch('/remove_speaker', { - method: 'POST', - body: formData - }); - - const result = await response.json(); - - if (result.status === 'success') { - alert('成功刪除音色!'); - // 重新載入音色列表 - loadSpeakers(); - } else { - throw new Error(result.message); - } - } catch (error) { - alert('刪除音色時發生錯誤:' + error.message); - } - } - - // 更新音色列表UI - function updateSpeakerList(speakers) { - const speakerList = document.getElementById('speakerList'); - const speakerSelect = document.getElementById('sftSpeaker'); - - // 清空現有列表 - speakerList.innerHTML = ''; - speakerSelect.innerHTML = ''; - - // 添加新的選項 - speakers.forEach(speaker => { - // 添加到下拉選單 - const option = document.createElement('option'); - option.value = speaker; - option.textContent = speaker; - speakerSelect.appendChild(option); - - // 添加到表格 - const row = document.createElement('tr'); - row.innerHTML = ` - ${speaker} - - - - `; - speakerList.appendChild(row); - }); - } - - // 搜尋音色 - function searchSpeakers(query) { - const filteredSpeakers = allSpeakers.filter(speaker => - speaker.toLowerCase().includes(query.toLowerCase()) - ); - updateSpeakerList(filteredSpeakers); - } - - // 設置搜尋事件監聽器 - speakerSearch.addEventListener('input', function(e) { - searchSpeakers(e.target.value); - }); - - // 載入音色列表 - async function loadSpeakers() { - try { - const response = await fetch('/get_speakers'); - if (!response.ok) throw new Error('獲取音色列表失敗'); - - allSpeakers = await response.json(); - const searchQuery = speakerSearch.value; - searchSpeakers(searchQuery); - } catch (error) { - console.error('載入音色列表失敗:', error); - } - } - - // 將removeSpeaker函數添加到全局作用域 - window.removeSpeaker = removeSpeaker; - - // 初始載入音色列表 - loadSpeakers(); -}); \ No newline at end of file diff --git a/static/style.css b/static/style.css deleted file mode 100644 index 99a13cf..0000000 --- a/static/style.css +++ /dev/null @@ -1,109 +0,0 @@ -body { - background-color: #f8f9fa; -} - -.card { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border: none; -} - -.card-header { - background-color: #fff; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); -} - -.btn-primary { - background-color: #0d6efd; - border-color: #0d6efd; -} - -.btn-primary:hover { - background-color: #0b5ed7; - border-color: #0a58ca; -} - -.btn-success { - background-color: #198754; - border-color: #198754; -} - -.btn-success:hover { - background-color: #157347; - border-color: #146c43; -} - -.form-control:focus, .form-select:focus { - border-color: #86b7fe; - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} - -#audioPlayer { - margin-top: 1rem; -} - -.loading { - position: relative; - pointer-events: none; - opacity: 0.7; -} - -.loading::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - width: 1.5rem; - height: 1.5rem; - margin: -0.75rem 0 0 -0.75rem; - border: 0.2rem solid #f3f3f3; - border-top: 0.2rem solid #3498db; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* 摺疊面板樣式 */ -.card-header[data-bs-toggle="collapse"] { - transition: background-color 0.2s; -} - -.card-header[data-bs-toggle="collapse"]:hover { - background-color: #f8f9fa; -} - -.card-header[data-bs-toggle="collapse"] .bi { - transition: transform 0.2s; -} - -.card-header[data-bs-toggle="collapse"][aria-expanded="true"] .bi { - transform: rotate(180deg); -} - -/* 搜尋框樣式 */ -#speakerSearch { - border-radius: 20px; - padding-left: 1rem; - padding-right: 1rem; -} - -#speakerSearch:focus { - box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); -} - -/* 表格樣式 */ -.table { - margin-bottom: 0; -} - -.table td { - vertical-align: middle; -} - -.btn-danger { - padding: 0.25rem 0.5rem; - font-size: 0.875rem; -} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 099ca9c..0000000 --- a/templates/index.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - BreezyVoice TTS - - - - - -
-

BreezyVoice TTS

- -
-
-
-
-
預訓練模式
-
-
-
-
- - -
-
- - -
- -
-
-
-
-
- -
-
-
音色列表
- -
-
-
-
- -
-
- - - - - - - - - - -
音色ID操作
-
-
-
-
- -
-
-
新增音色
-
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- -
-
-
- -
-
-
生成的語音
-
-
- -
-
-
- - - - - \ No newline at end of file From 0b77c0d260f6b6c8ad719b81825a288b8e84cf43 Mon Sep 17 00:00:00 2001 From: BrandonYU34 Date: Tue, 1 Jul 2025 06:30:19 +0000 Subject: [PATCH 7/7] refactor & rename --- cache_inference.py | 6 ++---- single_inference.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/cache_inference.py b/cache_inference.py index 3b72057..8d1bd6a 100644 --- a/cache_inference.py +++ b/cache_inference.py @@ -1,10 +1,8 @@ -from single_inference import main_customized +from single_inference import main_cached if __name__ == "__main__": ''' - By default, use the Breezyvoice model located in the ./models directory. - test command: python3 cache_inference.py --spk_id 臺灣女 \ @@ -12,4 +10,4 @@ --output_path results/output.wav ''' - main_customized() + main_cached() diff --git a/single_inference.py b/single_inference.py index bb0c06f..2e0b8dc 100755 --- a/single_inference.py +++ b/single_inference.py @@ -146,7 +146,7 @@ def frontend_zero_shot_dual(self, tts_text, prompt_text, prompt_speech_16k, flow 'llm_embedding': embedding, 'flow_embedding': flow_embedding} return model_input - def frontend_customized(self, tts_text, spk_id): + def frontend_cached(self, tts_text, spk_id): tts_text_token, tts_text_token_len = self._extract_text_token(tts_text) prompt_text_token = self.spk2info[spk_id]['prompt_text_token'] prompt_text_token_len = self.spk2info[spk_id]['prompt_text_token_len'] @@ -289,7 +289,7 @@ def inference_sft(self, tts_text, spk_id): if not len(i): continue print("Synthesizing:",i) - model_input = self.frontend.frontend_customized(i, spk_id) + model_input = self.frontend.frontend_cached(i, spk_id) model_output = self.model.inference(**model_input) yield model_output @@ -332,13 +332,13 @@ def inference_zero_shot_no_normalize(self, tts_text, prompt_text, prompt_speech_ tts_speeches.append(model_output['tts_speech']) return {'tts_speech': torch.concat(tts_speeches, dim=1)} - def inference_customized(self, tts_text, spk_id): + def inference_cached(self, tts_text, spk_id): tts_speeches = [] for i in re.split(r'(?<=[?!。.?!])\s*', tts_text): if not len(i): continue print("Synthesizing:",i) - model_input = self.frontend.frontend_customized(i, spk_id) + model_input = self.frontend.frontend_cached(i, spk_id) model_output = self.model.inference(**model_input) tts_speeches.append(model_output['tts_speech']) return {'tts_speech': torch.concat(tts_speeches, dim=1)} @@ -454,7 +454,7 @@ def single_inference(speaker_prompt_audio_path, content_to_synthesize, output_pa torchaudio.save(output_path, output['tts_speech'], 22050) print(f"Generated voice saved to {output_path}") -def inference_customized(content_to_synthesize, output_path, cosyvoice, bopomofo_converter, spk_id): +def inference_cached(content_to_synthesize, output_path, cosyvoice, bopomofo_converter, spk_id): content_to_synthesize = content_to_synthesize output_path = output_path.strip() @@ -468,7 +468,7 @@ def inference_customized(content_to_synthesize, output_path, cosyvoice, bopomofo content_to_synthesize_bopomo = get_bopomofo_rare(content_to_synthesize, bopomofo_converter) print("Content to be synthesized:",content_to_synthesize) start = time.time() - output = cosyvoice.inference_customized(content_to_synthesize_bopomo, spk_id) + output = cosyvoice.inference_cached(content_to_synthesize_bopomo, spk_id) end = time.time() print("Elapsed time:",end - start) print("Generated audio length:", output['tts_speech'].shape[1]/22050, "seconds") @@ -497,12 +497,12 @@ def main(): output_path = args.output_path.strip() single_inference(speaker_prompt_audio_path, content_to_synthesize, output_path, cosyvoice, bopomofo_converter, args.speaker_prompt_text_transcription) -def main_customized(): +def main_cached(): ####args parser = argparse.ArgumentParser(description="Run BreezyVoice text-to-speech with custom inputs") parser.add_argument("--content_to_synthesize", type=str, required=True, help="Specifies the content that will be synthesized into speech.") parser.add_argument("--output_path", type=str, required=False, default="results/output.wav", help="Specifies the name and path for the output .wav file.") - parser.add_argument("--model_path", type=str, required=False, default = "models",help="Specifies the model used for speech synthesis.") + parser.add_argument("--model_path", type=str, required=False, default = "MediaTek-Research/BreezyVoice-300M",help="Specifies the model used for speech synthesis.") parser.add_argument("--spk_id", type=str, required=False, default = "test_human",help="spk's name") args = parser.parse_args() @@ -514,7 +514,7 @@ def main_customized(): content_to_synthesize = args.content_to_synthesize spk_id = args.spk_id output_path = args.output_path.strip() - inference_customized(content_to_synthesize, output_path, cosyvoice, bopomofo_converter, spk_id) + inference_cached(content_to_synthesize, output_path, cosyvoice, bopomofo_converter, spk_id) def add_spk(): ####args @@ -522,7 +522,7 @@ def add_spk(): parser.add_argument("--speaker_prompt_audio_path", type=str, required=True, help="Specifies the path to the prompt speech audio file of the speaker.") parser.add_argument("--speaker_prompt_text_transcription", type=str, required=False, help="Specifies the transcription of the speaker prompt audio (Highly Recommended, if not provided, the system will fall back to transcribing with Whisper.)") - parser.add_argument("--model_path", type=str, required=False, default = "models",help="Specifies the model used for speech synthesis.") + parser.add_argument("--model_path", type=str, required=False, default = "MediaTek-Research/BreezyVoice-300M",help="Specifies the model used for speech synthesis.") parser.add_argument("--spk_id", type=str, required=False, default = "test_human",help="spk's name") args = parser.parse_args() speaker_prompt_audio_path = args.speaker_prompt_audio_path