From 1a90562805f883fa4860ded1fe1946b44c7b1800 Mon Sep 17 00:00:00 2001 From: yuyun2000 <15515722313yxw@gmail.com> Date: Wed, 30 Apr 2025 10:30:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96g2p=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E4=BB=A5=E5=A4=84=E7=90=86=E5=A4=9A=E9=9F=B3?= =?UTF-8?q?=E5=AD=97=EF=BC=8C=E4=B8=AD=E8=8B=B1=E6=B7=B7=E5=90=88=E7=9A=84?= =?UTF-8?q?=E6=83=85=E5=86=B5=E7=AD=89=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main_melotts/src/runner/Lexicon.hpp | 326 ++++++++++---- .../tools/test_tools/test-melo.py | 242 ++++++++++ .../tools/test_tools/test-tts.py | 412 ++++++++++++++++++ .../llm_framework/tools/test_tools/test.py | 145 ++++++ 4 files changed, 1039 insertions(+), 86 deletions(-) create mode 100644 projects/llm_framework/tools/test_tools/test-melo.py create mode 100644 projects/llm_framework/tools/test_tools/test-tts.py create mode 100644 projects/llm_framework/tools/test_tools/test.py diff --git a/projects/llm_framework/main_melotts/src/runner/Lexicon.hpp b/projects/llm_framework/main_melotts/src/runner/Lexicon.hpp index 8b6255d..2d55a47 100644 --- a/projects/llm_framework/main_melotts/src/runner/Lexicon.hpp +++ b/projects/llm_framework/main_melotts/src/runner/Lexicon.hpp @@ -4,14 +4,20 @@ #include #include #include -#include +#include +#include +#include +#include // 用于日志输出 -std::vector split (const std::string &s, char delim) { +// 使用引用传参优化split函数,避免不必要的拷贝 +std::vector split(const std::string &s, char delim) { std::vector result; - std::stringstream ss (s); + std::stringstream ss(s); std::string item; - while (getline (ss, item, delim)) { - result.push_back (item); + while (getline(ss, item, delim)) { + if (!item.empty()) { // 避免添加空字符串 + result.push_back(item); + } } return result; } @@ -19,134 +25,282 @@ std::vector split (const std::string &s, char delim) { class Lexicon { private: std::unordered_map, std::vector>> lexicon; + size_t max_phrase_length; // 追踪词典中最长的词组长度 + std::pair, std::vector> unknown_token; // '_'的发音作为未知词的默认值 + std::unordered_map reverse_tokens; // 用于将音素ID转回音素符号,用于日志 public: - Lexicon(const std::string& lexicon_filename, const std::string& tokens_filename) { + Lexicon(const std::string& lexicon_filename, const std::string& tokens_filename) : max_phrase_length(0) { std::unordered_map tokens; + + // 加载tokens std::ifstream ifs(tokens_filename); assert(ifs.is_open()); std::string line; - while ( std::getline(ifs, line) ) { + while (std::getline(ifs, line)) { auto splitted_line = split(line, ' '); - tokens.insert({splitted_line[0], std::stoi(splitted_line[1])}); + if (splitted_line.size() >= 2) { + int token_id = std::stoi(splitted_line[1]); + tokens.insert({splitted_line[0], token_id}); + reverse_tokens[token_id] = splitted_line[0]; // 建立反向映射 + } } ifs.close(); + // 加载lexicon ifs.open(lexicon_filename); assert(ifs.is_open()); - while ( std::getline(ifs, line) ) { + while (std::getline(ifs, line)) { auto splitted_line = split(line, ' '); + if (splitted_line.empty()) continue; + std::string word_or_phrase = splitted_line[0]; + + // 更新最长词组长度 + auto chars = splitEachChar(word_or_phrase); + max_phrase_length = std::max(max_phrase_length, chars.size()); + size_t phone_tone_len = splitted_line.size() - 1; size_t half_len = phone_tone_len / 2; std::vector phones, tones; + for (size_t i = 0; i < phone_tone_len; i++) { auto phone_or_tone = splitted_line[i + 1]; if (i < half_len) { - phones.push_back(tokens[phone_or_tone]); + if (tokens.find(phone_or_tone) != tokens.end()) { + phones.push_back(tokens[phone_or_tone]); + } } else { tones.push_back(std::stoi(phone_or_tone)); } } - lexicon.insert({word_or_phrase, std::make_pair(phones, tones)}); + lexicon[word_or_phrase] = std::make_pair(phones, tones); } + // 添加特殊映射 lexicon["呣"] = lexicon["母"]; lexicon["嗯"] = lexicon["恩"]; + // 添加标点符号 const std::vector punctuation{"!", "?", "…", ",", ".", "'", "-"}; - for (auto p : punctuation) { - int i = tokens[p]; - int tone = 0; - lexicon[p] = std::make_pair(std::vector{i}, std::vector{tone}); + for (const auto& p : punctuation) { + if (tokens.find(p) != tokens.end()) { + int i = tokens[p]; + lexicon[p] = std::make_pair(std::vector{i}, std::vector{0}); + } } - lexicon[" "] = std::make_pair(std::vector{tokens["_"]}, std::vector{0}); + + // 设置'_'作为未知词的发音 + assert(tokens.find("_") != tokens.end()); // 确保tokens中包含"_" + unknown_token = std::make_pair(std::vector{tokens["_"]}, std::vector{0}); + + // 空格映射到'_'的发音 + lexicon[" "] = unknown_token; + + // 中文标点转换映射 + lexicon[","] = lexicon[","]; + lexicon["。"] = lexicon["."]; + lexicon["!"] = lexicon["!"]; + lexicon["?"] = lexicon["?"]; + + // 输出词典信息 + std::cout << "词典加载完成,包含 " << lexicon.size() << " 个条目,最长词组长度: " << max_phrase_length << std::endl; } - std::vector splitEachChar(const std::string& text) - { + std::vector splitEachChar(const std::string& text) { std::vector words; - std::string input(text); - int len = input.length(); + int len = text.length(); int i = 0; while (i < len) { - int next = 1; - if ((input[i] & 0x80) == 0x00) { - // std::cout << "one character: " << input[i] << std::endl; - } else if ((input[i] & 0xE0) == 0xC0) { - next = 2; - // std::cout << "two character: " << input.substr(i, next) << std::endl; - } else if ((input[i] & 0xF0) == 0xE0) { - next = 3; - // std::cout << "three character: " << input.substr(i, next) << std::endl; - } else if ((input[i] & 0xF8) == 0xF0) { - next = 4; - // std::cout << "four character: " << input.substr(i, next) << std::endl; - } - words.push_back(input.substr(i, next)); - i += next; + int next = 1; + if ((text[i] & 0x80) == 0x00) { + // ASCII + } else if ((text[i] & 0xE0) == 0xC0) { + next = 2; // 2字节UTF-8 + } else if ((text[i] & 0xF0) == 0xE0) { + next = 3; // 3字节UTF-8 + } else if ((text[i] & 0xF8) == 0xF0) { + next = 4; // 4字节UTF-8 + } + words.push_back(text.substr(i, next)); + i += next; } return words; } - bool is_english(std::string s) { - if (s.size() == 1) - return (s[0] >= 'A' && s[0] <= 'Z') || (s[0] >= 'a' && s[0] <= 'z'); - else - return false; - } - - std::vector merge_english(const std::vector& splitted_text) { - std::vector words; - int i = 0; - while (i < splitted_text.size()) { - std::string s; - if (is_english(splitted_text[i])) { - while (i < splitted_text.size()) { - if (!is_english(splitted_text[i])) { - break; - } - s += splitted_text[i]; - i++; - } - // to lowercase - std::transform(s.begin(), s.end(), s.begin(), - [](unsigned char c){ return std::tolower(c); }); - words.push_back(s); - if (i >= splitted_text.size()) - break; - } - else { - words.push_back(splitted_text[i]); - i++; - } - } - return words; + bool is_english(const std::string& s) { + return s.size() == 1 && ((s[0] >= 'A' && s[0] <= 'Z') || (s[0] >= 'a' && s[0] <= 'z')); } + // 根据词典中的内容,使用最长匹配算法处理输入文本 void convert(const std::string& text, std::vector& phones, std::vector& tones) { - auto splitted_text = splitEachChar(text); - auto zh_mix_en = merge_english(splitted_text); - for (auto c : zh_mix_en) { - std::string s{c}; - if (s == ",") - s = ","; - else if (s == "。") - s = "."; - else if (s == "!") - s = "!"; - else if (s == "?") - s = "?"; + std::cout << "\n开始处理文本: \"" << text << "\"" << std::endl; + std::cout << "=======匹配结果=======" << std::endl; + std::cout << "单元\t|\t音素\t|\t声调" << std::endl; + std::cout << "-----------------------------" << std::endl; + + // 在开头添加'_'边界标记 + phones.insert(phones.end(), unknown_token.first.begin(), unknown_token.first.end()); + tones.insert(tones.end(), unknown_token.second.begin(), unknown_token.second.end()); + std::cout << "\t|\t" << phonesToString(unknown_token.first) << "\t|\t" + << tonesToString(unknown_token.second) << std::endl; + + auto chars = splitEachChar(text); + int i = 0; + + while (i < chars.size()) { + // 处理英文单词 + if (is_english(chars[i])) { + std::string eng_word; + int start = i; + while (i < chars.size() && is_english(chars[i])) { + eng_word += chars[i++]; + } + + // 英文转小写 + std::string orig_word = eng_word; // 保留原始单词用于日志 + std::transform(eng_word.begin(), eng_word.end(), eng_word.begin(), + [](unsigned char c){ return std::tolower(c); }); + + // 如果词典中有这个英文单词,使用它;否则使用'_'的发音 + if (lexicon.find(eng_word) != lexicon.end()) { + auto& [eng_phones, eng_tones] = lexicon[eng_word]; + phones.insert(phones.end(), eng_phones.begin(), eng_phones.end()); + tones.insert(tones.end(), eng_tones.begin(), eng_tones.end()); + + // 打印匹配信息 + std::cout << orig_word << "\t|\t" << phonesToString(eng_phones) << "\t|\t" + << tonesToString(eng_tones) << std::endl; + } else { + // 未找到单词,使用'_'的发音 + phones.insert(phones.end(), unknown_token.first.begin(), unknown_token.first.end()); + tones.insert(tones.end(), unknown_token.second.begin(), unknown_token.second.end()); + + // 打印未匹配信息 + std::cout << orig_word << "\t|\t" << phonesToString(unknown_token.first) << " (未匹配)\t|\t" + << tonesToString(unknown_token.second) << std::endl; + } + continue; + } + // 处理非英文字符(如空格、标点) + std::string c = chars[i++]; + if (c == " ") continue; // 跳过空格 + // 回退一步,用于最长匹配 + i--; - auto phones_and_tones = lexicon[" "]; - if (lexicon.find(s) != lexicon.end()) { - phones_and_tones = lexicon[s]; + + // 最长匹配算法处理中文/日文 + bool matched = false; + // 尝试从最长的词组开始匹配 + for (size_t len = std::min(max_phrase_length, chars.size() - i); len > 0 && !matched; --len) { + std::string phrase; + for (size_t j = 0; j < len; ++j) { + phrase += chars[i + j]; + } + + if (lexicon.find(phrase) != lexicon.end()) { + auto& [phrase_phones, phrase_tones] = lexicon[phrase]; + phones.insert(phones.end(), phrase_phones.begin(), phrase_phones.end()); + tones.insert(tones.end(), phrase_tones.begin(), phrase_tones.end()); + + // 打印匹配信息 + std::cout << phrase << "\t|\t" << phonesToString(phrase_phones) << "\t|\t" + << tonesToString(phrase_tones) << std::endl; + + i += len; + matched = true; + break; + } + } + + // 如果没有匹配到任何词组,使用'_'的发音 + if (!matched) { + std::string c = chars[i++]; + std::string s = c; + + // 中文标点符号转换 + std::string orig_char = s; // 保留原始字符用于日志 + if (s == ",") s = ","; + else if (s == "。") s = "."; + else if (s == "!") s = "!"; + else if (s == "?") s = "?"; + + // 如果词典中找不到,则使用'_'的发音 + if (lexicon.find(s) != lexicon.end()) { + auto& [char_phones, char_tones] = lexicon[s]; + phones.insert(phones.end(), char_phones.begin(), char_phones.end()); + tones.insert(tones.end(), char_tones.begin(), char_tones.end()); + + // 打印匹配信息 + std::cout << orig_char << "\t|\t" << phonesToString(char_phones) << "\t|\t" + << tonesToString(char_tones) << std::endl; + } else { + phones.insert(phones.end(), unknown_token.first.begin(), unknown_token.first.end()); + tones.insert(tones.end(), unknown_token.second.begin(), unknown_token.second.end()); + + // 打印未匹配信息 + std::cout << orig_char << "\t|\t" << phonesToString(unknown_token.first) << " (未匹配)\t|\t" + << tonesToString(unknown_token.second) << std::endl; + } } - phones.insert(phones.end(), phones_and_tones.first.begin(), phones_and_tones.first.end()); - tones.insert(tones.end(), phones_and_tones.second.begin(), phones_and_tones.second.end()); } + + // 在末尾添加'_'边界标记 + phones.insert(phones.end(), unknown_token.first.begin(), unknown_token.first.end()); + tones.insert(tones.end(), unknown_token.second.begin(), unknown_token.second.end()); + std::cout << "\t|\t" << phonesToString(unknown_token.first) << "\t|\t" + << tonesToString(unknown_token.second) << std::endl; + + // 汇总打印最终结果 + std::cout << "\n处理结果汇总:" << std::endl; + std::cout << "原文: " << text << std::endl; + std::cout << "音素: " << phonesToString(phones) << std::endl; + std::cout << "声调: " << tonesToString(tones) << std::endl; + std::cout << "====================" << std::endl; } -}; \ No newline at end of file + +private: + // 处理单个字符 + void processChar(const std::string& c, std::vector& phones, std::vector& tones) { + std::string s = c; + + // 中文标点符号转换 + if (s == ",") s = ","; + else if (s == "。") s = "."; + else if (s == "!") s = "!"; + else if (s == "?") s = "?"; + + // 如果词典中找不到,则使用'_'的发音 + auto& phones_and_tones = (lexicon.find(s) != lexicon.end()) ? lexicon[s] : unknown_token; + + phones.insert(phones.end(), phones_and_tones.first.begin(), phones_and_tones.first.end()); + tones.insert(tones.end(), phones_and_tones.second.begin(), phones_and_tones.second.end()); + } + + // 将音素ID数组转换为字符串用于日志输出 + std::string phonesToString(const std::vector& phones) { + std::string result; + for (auto id : phones) { + if (!result.empty()) result += " "; + if (reverse_tokens.find(id) != reverse_tokens.end()) { + result += reverse_tokens[id]; + } else { + result += "<" + std::to_string(id) + ">"; + } + } + return result; + } + + // 将声调数组转换为字符串用于日志输出 + std::string tonesToString(const std::vector& tones) { + std::string result; + for (auto tone : tones) { + if (!result.empty()) result += " "; + result += std::to_string(tone); + } + return result; + } +}; diff --git a/projects/llm_framework/tools/test_tools/test-melo.py b/projects/llm_framework/tools/test_tools/test-melo.py new file mode 100644 index 0000000..b257f2f --- /dev/null +++ b/projects/llm_framework/tools/test_tools/test-melo.py @@ -0,0 +1,242 @@ +import socket +import json +import argparse +import uuid +import time + +def create_tcp_connection(host, port): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + return sock + +def send_json(sock, data): + json_data = json.dumps(data, ensure_ascii=False) + '\n' + print(f"Sending: {json_data}") + sock.sendall(json_data.encode('utf-8')) + +def receive_response(sock, timeout=None): + """接收响应,带可选的超时设置""" + old_timeout = sock.gettimeout() + try: + if timeout is not None: + sock.settimeout(timeout) + response = '' + while True: + part = sock.recv(4096).decode('utf-8') + if not part: # 连接已关闭 + return response.strip() + response += part + if '\n' in response: + break + return response.strip() + except socket.timeout: + return None + finally: + sock.settimeout(old_timeout) + +def close_connection(sock): + if sock: + sock.close() + +def create_melotts_setup_data(request_id="melotts_setup"): + return { + "request_id": request_id, + "work_id": "melotts", + "action": "setup", + "object": "melotts.setup", + "data": { + "model": "melotts_zh-cn", + "response_format": "sys.pcm", + "input": "tts.utf-8", + "enoutput": False + } + } + +def list_available_tasks(sock): + """获取可用的任务列表""" + request_id = str(uuid.uuid4()) + send_json(sock, { + "request_id": request_id, + "work_id": "melotts", + "action": "taskinfo" + }) + + response = receive_response(sock) + if not response: + return {"error": "No response received"} + try: + return json.loads(response) + except: + return {"error": "Failed to parse response"} + +def parse_setup_response(response_data, sent_request_id): + error = response_data.get('error') + request_id = response_data.get('request_id') + + if request_id != sent_request_id: + print(f"Request ID mismatch: sent {sent_request_id}, received {request_id}") + return None + if error and error.get('code') != 0: + print(f"Error Code: {error['code']}, Message: {error['message']}") + return None + return response_data.get('work_id') + +def setup(sock, setup_data): + sent_request_id = setup_data['request_id'] + send_json(sock, setup_data) + response = receive_response(sock) + if not response: + print("No response received during setup") + return None + try: + response_data = json.loads(response) + return parse_setup_response(response_data, sent_request_id) + except json.JSONDecodeError: + print(f"Invalid JSON response: {response}") + return None + +def melotts_tts_inference(sock, melotts_work_id, text, use_stream=False): + request_id = str(uuid.uuid4()) + + # 根据文档,选择流式或非流式请求格式 + if use_stream: + send_json(sock, { + "request_id": request_id, + "work_id": melotts_work_id, + "action": "inference", + "object": "melotts.utf-8.stream", + "data": { + "delta": text, + "index": 0, + "finish": True + } + }) + else: + # 非流式请求 + send_json(sock, { + "request_id": request_id, + "work_id": melotts_work_id, + "action": "inference", + "object": "melotts.utf-8", + "data": text + }) + + # 关键更改:不等待响应或设置更长的超时时间 + # 由于使用sys.pcm格式,音频会直接播放,可能不会立即返回响应 + print("语音合成请求已发送,正在播放...") + + # 可选:设置一个较短的超时来检查是否有响应,但不要因为没响应就认为失败 + response = receive_response(sock, timeout=0.5) # 设置短超时,只是尝试看有没有响应 + if response: + try: + response_data = json.loads(response) + error = response_data.get('error') + if error and error.get('code') != 0: + print(f"收到错误响应: Code={error['code']}, Message={error['message']}") + return False + print("收到成功响应") + except: + print(f"收到非JSON响应: {response[:100]}...") + else: + # 不收到响应也视为成功,因为服务器可能正忙于播放音频 + print("未收到响应,但这不一定表示失败(服务器可能正忙于处理音频)") + + # 这里给TTS处理一些时间 + # 根据文本长度估计播放时间 + estimated_time = len(text) * 0.1 # 假设每个字符需要0.1秒 + estimated_time = max(1.0, min(estimated_time, 10.0)) # 至少1秒,最多10秒 + print(f"等待大约 {estimated_time:.1f} 秒让音频播放完...") + time.sleep(estimated_time) + + return True + +def exit_session(sock, melotts_work_id): + send_json(sock, { + "request_id": "melotts_exit", + "work_id": melotts_work_id, + "action": "exit" + }) + response = receive_response(sock, timeout=2.0) + if not response: + print("退出命令已发送,但未收到响应") + return True # 假设成功 + try: + response_data = json.loads(response) + print("Exit Response:", response_data) + return response_data.get('error', {}).get('code', -1) == 0 + except: + print("Failed to parse exit response") + return False + +def get_task_info(sock, work_id): + """获取任务的详细信息""" + request_id = str(uuid.uuid4()) + send_json(sock, { + "request_id": request_id, + "work_id": work_id, + "action": "taskinfo" + }) + + response = receive_response(sock) + if not response: + return {"error": "No response received"} + try: + return json.loads(response) + except: + return {"error": "Failed to parse response"} + +def main(host, port): + sock = create_tcp_connection(host, port) + try: + print("Setting up MeloTTS...") + setup_data = create_melotts_setup_data() + melotts_work_id = setup(sock, setup_data) + + if not melotts_work_id: + print("Setup failed. Checking available tasks...") + task_list = list_available_tasks(sock) + print("Available tasks:", task_list) + return + + print(f"MeloTTS SETUP finished, work_id: {melotts_work_id}") + + # 获取并显示任务详细信息 + task_info = get_task_info(sock, melotts_work_id) + print("Task info:", task_info) + + # 选择流式或非流式模式 + use_stream = input("是否使用流式输入? (y/n, 默认n): ").lower() == 'y' + + while True: + text = input("请输入你要合成语音的中文文本(输入exit退出):") + if text.lower() == 'exit': + break + + print("正在合成语音...", flush=True) + success = melotts_tts_inference(sock, melotts_work_id, text, use_stream) + + if success: + print("语音合成处理完成") + else: + print("语音合成处理失败") + + # 每次请求间隔 + time.sleep(1) + + # 退出会话 + if exit_session(sock, melotts_work_id): + print("成功退出会话") + else: + print("退出会话可能有问题") + + except Exception as e: + print(f"程序异常: {e}") + finally: + close_connection(sock) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='TCP Client for MeloTTS Unit.') + parser.add_argument('--host', type=str, default='localhost', help='Server hostname (default: localhost)') + parser.add_argument('--port', type=int, default=10001, help='Server port (default: 10001)') + args = parser.parse_args() + main(args.host, args.port) diff --git a/projects/llm_framework/tools/test_tools/test-tts.py b/projects/llm_framework/tools/test_tools/test-tts.py new file mode 100644 index 0000000..7368140 --- /dev/null +++ b/projects/llm_framework/tools/test_tools/test-tts.py @@ -0,0 +1,412 @@ +import socket +import json +import argparse +import uuid +import time +import sys + +def create_tcp_connection(host, port): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + return sock + +def send_json(sock, data): + json_data = json.dumps(data, ensure_ascii=False) + '\n' + print(f"Sending: {json_data}") + sock.sendall(json_data.encode('utf-8')) + +def receive_response(sock, timeout=None): + """接收响应,带可选的超时设置""" + old_timeout = sock.gettimeout() + try: + if timeout is not None: + sock.settimeout(timeout) + response = '' + while True: + part = sock.recv(4096).decode('utf-8') + if not part: # 连接已关闭 + return response.strip() + response += part + if '\n' in response: + break + return response.strip() + except socket.timeout: + return None + finally: + sock.settimeout(old_timeout) + +def close_connection(sock): + if sock: + sock.close() + +def create_tts_setup_data(request_id=None, link_with=None): + if request_id is None: + request_id = str(uuid.uuid4()) + + # 基本设置 + data = { + "model": "single_speaker_fast", + "response_format": "sys.pcm", + "input": "tts.utf-8", + "enoutput": False + } + + # 如果需要链接其他单元 + if link_with: + if isinstance(link_with, list): + inputs = ["tts.utf-8"] + link_with + data["input"] = inputs + else: + inputs = ["tts.utf-8", link_with] + data["input"] = inputs + + return { + "request_id": request_id, + "work_id": "tts", + "action": "setup", + "object": "tts.setup", + "data": data + } + +def list_available_tasks(sock, work_id="tts"): + """获取可用的任务列表""" + request_id = str(uuid.uuid4()) + send_json(sock, { + "request_id": request_id, + "work_id": work_id, + "action": "taskinfo" + }) + + response = receive_response(sock) + if not response: + return {"error": "No response received"} + try: + return json.loads(response) + except: + return {"error": "Failed to parse response"} + +def parse_setup_response(response_data, sent_request_id): + error = response_data.get('error') + request_id = response_data.get('request_id') + + if request_id != sent_request_id: + print(f"Request ID mismatch: sent {sent_request_id}, received {request_id}") + return None + if error and error.get('code') != 0: + print(f"Error Code: {error['code']}, Message: {error['message']}") + return None + return response_data.get('work_id') + +def setup(sock, setup_data): + sent_request_id = setup_data['request_id'] + send_json(sock, setup_data) + response = receive_response(sock) + if not response: + print("No response received during setup") + return None + try: + response_data = json.loads(response) + return parse_setup_response(response_data, sent_request_id) + except json.JSONDecodeError: + print(f"Invalid JSON response: {response}") + return None + +def link_units(sock, tts_work_id, target_work_id): + """链接TTS单元与其他单元""" + request_id = str(uuid.uuid4()) + send_json(sock, { + "request_id": request_id, + "work_id": tts_work_id, + "action": "link", + "object": "work_id", + "data": target_work_id + }) + + response = receive_response(sock) + if not response: + print("No response received for link request") + return False + + try: + response_data = json.loads(response) + error = response_data.get('error', {}) + if error.get('code') == 0: + print(f"成功链接 {tts_work_id} 与 {target_work_id}") + return True + else: + print(f"链接失败: {error.get('message', '未知错误')}") + return False + except: + print(f"Failed to parse link response: {response}") + return False + +def unlink_units(sock, tts_work_id, target_work_id): + """取消TTS单元与其他单元的链接""" + request_id = str(uuid.uuid4()) + send_json(sock, { + "request_id": request_id, + "work_id": tts_work_id, + "action": "unlink", + "object": "work_id", + "data": target_work_id + }) + + response = receive_response(sock) + if not response: + print("No response received for unlink request") + return False + + try: + response_data = json.loads(response) + error = response_data.get('error', {}) + if error.get('code') == 0: + print(f"成功取消链接 {tts_work_id} 与 {target_work_id}") + return True + else: + print(f"取消链接失败: {error.get('message', '未知错误')}") + return False + except: + print(f"Failed to parse unlink response: {response}") + return False + +def pause_unit(sock, tts_work_id): + """暂停TTS单元工作""" + request_id = str(uuid.uuid4()) + send_json(sock, { + "request_id": request_id, + "work_id": tts_work_id, + "action": "pause" + }) + + response = receive_response(sock) + if not response: + print("No response received for pause request") + return False + + try: + response_data = json.loads(response) + error = response_data.get('error', {}) + if error.get('code') == 0: + print(f"成功暂停 {tts_work_id}") + return True + else: + print(f"暂停失败: {error.get('message', '未知错误')}") + return False + except: + print(f"Failed to parse pause response: {response}") + return False + +def resume_unit(sock, tts_work_id): + """恢复TTS单元工作""" + request_id = str(uuid.uuid4()) + send_json(sock, { + "request_id": request_id, + "work_id": tts_work_id, + "action": "work" + }) + + response = receive_response(sock) + if not response: + print("No response received for resume request") + return False + + try: + response_data = json.loads(response) + error = response_data.get('error', {}) + if error.get('code') == 0: + print(f"成功恢复 {tts_work_id}") + return True + else: + print(f"恢复失败: {error.get('message', '未知错误')}") + return False + except: + print(f"Failed to parse resume response: {response}") + return False + +def tts_inference(sock, tts_work_id, text): + request_id = str(uuid.uuid4()) + + # 非流式请求 + send_json(sock, { + "request_id": request_id, + "work_id": tts_work_id, + "action": "inference", + "object": "tts.utf-8", + "data": text + }) + + print("语音合成请求已发送,正在播放...") + + # 可选:设置一个较短的超时来检查是否有响应,但不要因为没响应就认为失败 + response = receive_response(sock, timeout=0.5) # 设置短超时,只是尝试看有没有响应 + if response: + try: + response_data = json.loads(response) + error = response_data.get('error') + if error and error.get('code') != 0: + print(f"收到错误响应: Code={error['code']}, Message={error['message']}") + return False + print("收到成功响应") + except: + print(f"收到非JSON响应: {response[:100]}...") + else: + # 不收到响应也视为成功,因为服务器可能正忙于播放音频 + print("未收到响应,但这不一定表示失败(服务器可能正忙于处理音频)") + + # 这里给TTS处理一些时间 + # 根据文本长度估计播放时间 + estimated_time = len(text) * 0.1 # 假设每个字符需要0.1秒 + estimated_time = max(1.0, min(estimated_time, 10.0)) # 至少1秒,最多10秒 + print(f"等待大约 {estimated_time:.1f} 秒让音频播放完...") + time.sleep(estimated_time) + + return True + +def exit_session(sock, tts_work_id): + request_id = str(uuid.uuid4()) + send_json(sock, { + "request_id": request_id, + "work_id": tts_work_id, + "action": "exit" + }) + response = receive_response(sock, timeout=2.0) + if not response: + print("退出命令已发送,但未收到响应") + return True # 假设成功 + try: + response_data = json.loads(response) + error = response_data.get('error', {}) + if error.get('code') == 0: + print(f"成功退出 {tts_work_id}") + return True + else: + print(f"退出失败: {error.get('message', '未知错误')}") + return False + except: + print("Failed to parse exit response") + return False + +def get_task_info(sock, work_id): + """获取任务的详细信息""" + request_id = str(uuid.uuid4()) + send_json(sock, { + "request_id": request_id, + "work_id": work_id, + "action": "taskinfo" + }) + + response = receive_response(sock) + if not response: + return {"error": "No response received"} + try: + return json.loads(response) + except: + return {"error": "Failed to parse response"} + +def print_menu(): + print("\n===== TTS控制菜单 =====") + print("1. 合成语音") + print("2. 链接到其他单元") + print("3. 取消链接") + print("4. 暂停TTS单元") + print("5. 恢复TTS单元") + print("6. 获取任务信息") + print("7. 退出TTS单元") + print("0. 退出程序") + print("======================") + +def main(host, port): + sock = create_tcp_connection(host, port) + try: + print("Setting up TTS...") + setup_data = create_tts_setup_data() + tts_work_id = setup(sock, setup_data) + + if not tts_work_id: + print("Setup failed. Checking available tasks...") + task_list = list_available_tasks(sock) + print("Available tasks:", task_list) + if task_list.get('data') and isinstance(task_list.get('data'), list) and len(task_list.get('data')) > 0: + tts_work_id = task_list.get('data')[0] + print(f"使用已存在的TTS任务: {tts_work_id}") + else: + print("找不到可用的TTS任务,程序退出") + return + + print(f"TTS SETUP finished, work_id: {tts_work_id}") + + # 获取并显示任务详细信息 + task_info = get_task_info(sock, tts_work_id) + print("Task info:", task_info) + + while True: + print_menu() + choice = input("请选择操作 (0-7): ") + + if choice == '0': + print("程序退出") + break + + elif choice == '1': + text = input("请输入要合成语音的文本: ") + if text: + print("正在合成语音...", flush=True) + success = tts_inference(sock, tts_work_id, text) + if success: + print("语音合成处理完成") + else: + print("语音合成处理失败") + else: + print("文本为空,取消合成") + + elif choice == '2': + target_id = input("请输入要链接的单元ID (例如 kws.1000): ") + if target_id: + link_units(sock, tts_work_id, target_id) + else: + print("单元ID为空,取消链接操作") + + elif choice == '3': + target_id = input("请输入要取消链接的单元ID (例如 kws.1000): ") + if target_id: + unlink_units(sock, tts_work_id, target_id) + else: + print("单元ID为空,取消操作") + + elif choice == '4': + pause_unit(sock, tts_work_id) + + elif choice == '5': + resume_unit(sock, tts_work_id) + + elif choice == '6': + task_info = get_task_info(sock, tts_work_id) + print("Task info:", json.dumps(task_info, indent=2, ensure_ascii=False)) + + elif choice == '7': + if exit_session(sock, tts_work_id): + print("TTS单元已退出") + # 重新检查可用任务 + task_list = list_available_tasks(sock) + print("Available tasks:", task_list) + else: + print("TTS单元退出失败") + + else: + print("无效的选择,请重试") + + # 每次操作间隔 + time.sleep(0.5) + + except KeyboardInterrupt: + print("\n程序被用户中断") + except Exception as e: + print(f"程序异常: {e}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='TCP Client for MeloTTS Unit.') + parser.add_argument('--host', type=str, default='localhost', help='Server hostname (default: localhost)') + parser.add_argument('--port', type=int, default=10001, help='Server port (default: 10001)') + args = parser.parse_args() + main(args.host, args.port) \ No newline at end of file diff --git a/projects/llm_framework/tools/test_tools/test.py b/projects/llm_framework/tools/test_tools/test.py new file mode 100644 index 0000000..9ea712b --- /dev/null +++ b/projects/llm_framework/tools/test_tools/test.py @@ -0,0 +1,145 @@ +import socket +import json +import argparse + + +def create_tcp_connection(host, port): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + return sock + + +def send_json(sock, data): + json_data = json.dumps(data, ensure_ascii=False) + '\n' + sock.sendall(json_data.encode('utf-8')) + + +def receive_response(sock): + response = '' + while True: + part = sock.recv(4096).decode('utf-8') + response += part + if '\n' in response: + break + return response.strip() + + +def close_connection(sock): + if sock: + sock.close() + + +def create_init_data(): + return { + "request_id": "llm_001", + "work_id": "llm", + "action": "setup", + "object": "llm.setup", + "data": { + "model": "qwen2.5-0.5B-prefill-20e", + "response_format": "llm.utf-8.stream", + "input": "llm.utf-8.stream", + "enoutput": True, + "max_token_len": 1023, + "prompt": "You are a knowledgeable assistant capable of answering various questions and providing information." + } + } + + +def parse_setup_response(response_data, sent_request_id): + error = response_data.get('error') + request_id = response_data.get('request_id') + + if request_id != sent_request_id: + print(f"Request ID mismatch: sent {sent_request_id}, received {request_id}") + return None + + if error and error.get('code') != 0: + print(f"Error Code: {error['code']}, Message: {error['message']}") + return None + + return response_data.get('work_id') + + +def setup(sock, init_data): + sent_request_id = init_data['request_id'] + send_json(sock, init_data) + response = receive_response(sock) + response_data = json.loads(response) + return parse_setup_response(response_data, sent_request_id) + + +def exit_session(sock, deinit_data): + send_json(sock, deinit_data) + response = receive_response(sock) + response_data = json.loads(response) + print("Exit Response:", response_data) + + +def parse_inference_response(response_data): + error = response_data.get('error') + if error and error.get('code') != 0: + print(f"Error Code: {error['code']}, Message: {error['message']}") + return None + + return response_data.get('data') + + +def main(host, port): + sock = create_tcp_connection(host, port) + + try: + print("Setup LLM...") + init_data = create_init_data() + llm_work_id = setup(sock, init_data) + print("Setup LLM finished.") + + while True: + user_input = input("Enter your message (or 'exit' to quit): ") + if user_input.lower() == 'exit': + break + + send_json(sock, { + "request_id": "llm_001", + "work_id": llm_work_id, + "action": "inference", + "object": "llm.utf-8.stream", + "data": { + "delta": user_input, + "index": 0, + "finish": True + } + }) + + while True: + response = receive_response(sock) + response_data = json.loads(response) + + data = parse_inference_response(response_data) + if data is None: + break + + delta = data.get('delta') + finish = data.get('finish') + print(delta, end='', flush=True) + + if finish: + print() + break + + exit_session(sock, { + "request_id": "llm_exit", + "work_id": llm_work_id, + "action": "exit" + }) + finally: + close_connection(sock) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='TCP Client to send JSON data.') + parser.add_argument('--host', type=str, default='localhost', help='Server hostname (default: localhost)') + parser.add_argument('--port', type=int, default=10001, help='Server port (default: 10001)') + + args = parser.parse_args() + main(args.host, args.port)