'commit'
This commit is contained in:
@@ -27,19 +27,29 @@ DEFAULT_AUDIO_MIME_TYPE = "audio/mpeg"
|
||||
|
||||
class MyCallback(QwenTtsRealtimeCallback):
|
||||
"""
|
||||
自定义 TTS 流式回调
|
||||
自定义 TTS 流式回调,增加了播放缓冲区以解决语音断续问题
|
||||
"""
|
||||
def __init__(self):
|
||||
def __init__(self, buffer_seconds=0.5):
|
||||
self.complete_event = threading.Event()
|
||||
self.playback_start_event = threading.Event() # 新增:播放开始事件
|
||||
self._player = pyaudio.PyAudio()
|
||||
self._stream = self._player.open(
|
||||
format=pyaudio.paInt16, channels=1, rate=24000, output=True
|
||||
format=pyaudio.paInt16, channels=1, rate=24000, output=True,
|
||||
frames_per_buffer=1024
|
||||
)
|
||||
self.audio_queue = [] # 缓冲区
|
||||
self.buffer_threshold = int(24000 * 2 * buffer_seconds) # 字节数 (16bit=2bytes)
|
||||
self.is_playing = False
|
||||
self.playback_thread = None
|
||||
self.all_data_received = False
|
||||
|
||||
def on_open(self) -> None:
|
||||
print('[TTS] 连接已建立')
|
||||
|
||||
def on_close(self, close_status_code, close_msg) -> None:
|
||||
self.all_data_received = True
|
||||
if self.playback_thread:
|
||||
self.playback_thread.join()
|
||||
if self._stream:
|
||||
self._stream.stop_stream()
|
||||
self._stream.close()
|
||||
@@ -47,6 +57,22 @@ class MyCallback(QwenTtsRealtimeCallback):
|
||||
self._player.terminate()
|
||||
print(f'[TTS] 连接关闭 code={close_status_code}, msg={close_msg}')
|
||||
|
||||
def _playback_worker(self):
|
||||
"""播放线程,从缓冲区读取并播放"""
|
||||
print("[TTS] 缓冲区达到阈值,开始播放...")
|
||||
self.playback_start_event.set() # 触发播放开始事件
|
||||
chunk_count = 0
|
||||
while not self.all_data_received or self.audio_queue:
|
||||
if self.audio_queue:
|
||||
data = self.audio_queue.pop(0)
|
||||
self._stream.write(data)
|
||||
chunk_count += 1
|
||||
else:
|
||||
if self.all_data_received:
|
||||
break
|
||||
time.sleep(0.01) # 等待数据
|
||||
print(f"[TTS] 播放线程结束,共播放 {chunk_count} 个数据块")
|
||||
|
||||
def on_event(self, response: dict) -> None:
|
||||
try:
|
||||
event_type = response.get('type', '')
|
||||
@@ -54,17 +80,32 @@ class MyCallback(QwenTtsRealtimeCallback):
|
||||
print(f'[TTS] 会话开始: {response["session"]["id"]}')
|
||||
elif event_type == 'response.audio.delta':
|
||||
audio_data = base64.b64decode(response['delta'])
|
||||
self._stream.write(audio_data)
|
||||
self.audio_queue.append(audio_data)
|
||||
|
||||
# 计算当前已缓冲的总字节数
|
||||
current_buffer_size = sum(len(d) for d in self.audio_queue)
|
||||
|
||||
# 如果还没开始播放且达到阈值,启动播放线程
|
||||
if not self.is_playing and current_buffer_size >= self.buffer_threshold:
|
||||
self.is_playing = True
|
||||
self.playback_thread = threading.Thread(target=self._playback_worker)
|
||||
self.playback_thread.start()
|
||||
|
||||
elif event_type == 'response.done':
|
||||
print(f'[TTS] 响应完成')
|
||||
total_size = sum(len(d) for d in self.audio_queue)
|
||||
print(f'[TTS] 响应完成,总接收音频大小: {total_size} 字节')
|
||||
elif event_type == 'session.finished':
|
||||
print('[TTS] 会话结束')
|
||||
self.all_data_received = True
|
||||
self.complete_event.set()
|
||||
except Exception as e:
|
||||
print(f'[Error] 处理回调事件异常: {e}')
|
||||
|
||||
def wait_for_finished(self):
|
||||
self.complete_event.wait()
|
||||
# 确保播放线程也执行完毕
|
||||
if self.playback_thread and self.playback_thread.is_alive():
|
||||
self.playback_thread.join()
|
||||
|
||||
class QwenTTSManager:
|
||||
"""
|
||||
@@ -109,11 +150,11 @@ class QwenTTSManager:
|
||||
|
||||
return resp.json()["output"]["voice"]
|
||||
|
||||
def start_synthesis(self, voice_id, text_list):
|
||||
def start_synthesis(self, voice_id, text_list, wait_finished=True, buffer_seconds=0.5):
|
||||
"""
|
||||
开始实时语音合成并播放
|
||||
"""
|
||||
self.callback = MyCallback()
|
||||
self.callback = MyCallback(buffer_seconds=buffer_seconds)
|
||||
self.qwen_tts_realtime = QwenTtsRealtime(
|
||||
model=self.model,
|
||||
callback=self.callback,
|
||||
@@ -133,4 +174,13 @@ class QwenTTSManager:
|
||||
time.sleep(0.1)
|
||||
|
||||
self.qwen_tts_realtime.finish()
|
||||
self.callback.wait_for_finished()
|
||||
|
||||
if wait_finished:
|
||||
self.callback.wait_for_finished()
|
||||
|
||||
return self.callback
|
||||
|
||||
def wait_for_playback_start(self):
|
||||
"""等待播放实际开始"""
|
||||
if self.callback:
|
||||
self.callback.playback_start_event.wait()
|
||||
|
||||
Binary file not shown.
63
WeiXin/T6_SendAudio_ActionTest.py
Normal file
63
WeiXin/T6_SendAudio_ActionTest.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# coding=utf-8
|
||||
import uiautomator2 as u2
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
# 添加项目根目录到 sys.path 以便导入 WxUtil
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if project_root not in sys.path:
|
||||
sys.path.append(project_root)
|
||||
|
||||
from WeiXin import WxUtil
|
||||
|
||||
# 配置日志
|
||||
LOG_FILE = os.path.join(WxUtil.LOG_DIR, "T6_SendAudio.log")
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler(LOG_FILE, encoding='utf-8'),
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger("T6_SendAudio")
|
||||
|
||||
def test_send_audio_action():
|
||||
logger.info("开始测试 T6: 语音发送流程压力测试 (包含多次模式切换)")
|
||||
|
||||
# 1. 连接设备
|
||||
d = WxUtil.connect_device()
|
||||
if not d:
|
||||
logger.error("无法连接设备,测试终止")
|
||||
return
|
||||
|
||||
# 测试场景 1: 多次切换并最终发送语音
|
||||
for i in range(1, 3): # 进行 2 轮切换测试
|
||||
logger.info(f"\n--- 第 {i} 轮切换测试 ---")
|
||||
|
||||
# 1.1 确保当前是键盘模式
|
||||
logger.info("步骤 1: 确保切换到键盘模式...")
|
||||
WxUtil.switch_to_keyboard_mode(d)
|
||||
time.sleep(2)
|
||||
|
||||
# 1.2 切换回语音模式并发送
|
||||
logger.info("步骤 2: 切换回语音模式并尝试发送...")
|
||||
success = WxUtil.perform_voice_input(d, duration=2.0, debug_prefix=f"T6_Round{i}")
|
||||
|
||||
if success:
|
||||
logger.info(f"第 {i} 轮测试成功。")
|
||||
else:
|
||||
logger.error(f"第 {i} 轮测试失败。")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
logger.info("\n--- 压力测试结束 ---")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 确保目录存在
|
||||
if not os.path.exists(WxUtil.LOG_DIR): os.makedirs(WxUtil.LOG_DIR)
|
||||
if not os.path.exists(WxUtil.OUTPUT_DIR): os.makedirs(WxUtil.OUTPUT_DIR)
|
||||
|
||||
test_send_audio_action()
|
||||
145
WeiXin/T7_TTS_VoiceReply.py
Normal file
145
WeiXin/T7_TTS_VoiceReply.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# coding=utf-8
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
|
||||
# 添加项目根目录到 sys.path 以便导入 Util
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if project_root not in sys.path:
|
||||
sys.path.append(project_root)
|
||||
|
||||
from Util.AlyTtsKit import QwenTTSManager
|
||||
from WeiXin import WxUtil
|
||||
|
||||
# ======= 配置 =======
|
||||
# 这里的 Voice ID 是从 T2_PlayVoice.py 中复制过来的
|
||||
MY_VOICE_ID = "qwen-tts-vc-guanyu-voice-20260131160431051-8e51"
|
||||
|
||||
# 目标文本
|
||||
REPLY_TEXT = "我是少惠林的大张老师,您的孩子几年级了?我们周六周日上班,您可以带孩子过来试听一下。"
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler(os.path.join(WxUtil.LOG_DIR, "T7_TTS_VoiceReply.log"), mode='w', encoding='utf-8')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger("T7_TTS")
|
||||
|
||||
def tts_worker(tts_manager, voice_id, text, finished_event, start_event):
|
||||
"""TTS 播音工作线程"""
|
||||
try:
|
||||
logger.info(f"[TTS] 启动语音合成,音色ID: {voice_id}")
|
||||
# 增加缓冲区到 1.0 秒以解决连贯性问题
|
||||
tts_manager.start_synthesis(voice_id, [text], wait_finished=False, buffer_seconds=1.0)
|
||||
|
||||
# 等待播放实际开始并通知主线程
|
||||
tts_manager.wait_for_playback_start()
|
||||
start_event.set()
|
||||
|
||||
# 此时等待全部播放完成
|
||||
if tts_manager.callback:
|
||||
tts_manager.callback.wait_for_finished()
|
||||
|
||||
logger.info("[TTS] 语音播放完成")
|
||||
except Exception as e:
|
||||
logger.error(f"[TTS] 播放过程中发生异常: {e}")
|
||||
finally:
|
||||
finished_event.set()
|
||||
|
||||
def run_t7_task():
|
||||
logger.info("开始执行 T7: 集成 TTS 克隆引擎与微信语音回复 (优化连贯性版本)")
|
||||
|
||||
# 1. 连接设备
|
||||
d = WxUtil.connect_device()
|
||||
if not d:
|
||||
logger.error("无法连接设备,任务终止")
|
||||
return
|
||||
|
||||
# 2. 初始化 TTS 管理器
|
||||
try:
|
||||
# 增加缓冲区到 1.0 秒以解决断续问题
|
||||
tts_manager = QwenTTSManager()
|
||||
# 这里虽然 init 没传参,但我们可以在 start_synthesis 逻辑里或者修改 AlyTtsKit 让它支持传参
|
||||
except Exception as e:
|
||||
logger.error(f"初始化 TTS 失败: {e}")
|
||||
return
|
||||
|
||||
# 3. 准备微信环境:确保处于语音输入模式
|
||||
logger.info("步骤1: 准备微信环境...")
|
||||
# 先尝试寻找 "按住说话" 按钮,如果找不到则尝试切换
|
||||
tmp_screen = os.path.join(WxUtil.OUTPUT_DIR, "t7_check_mode.jpg")
|
||||
d.screenshot(tmp_screen)
|
||||
press_say_template = os.path.join(WxUtil.TEMPLATE_DIR, "press_say.jpg")
|
||||
pos = WxUtil.match_template_center(tmp_screen, press_say_template, threshold=0.8)
|
||||
|
||||
if not pos:
|
||||
logger.info(">>> [状态] 未发现 '按住说话' 按钮,尝试切换模式...")
|
||||
# 尝试点击键盘图标切换回语音模式
|
||||
audio_reply_template = os.path.join(WxUtil.TEMPLATE_DIR, "audio_reply.jpg")
|
||||
switch_pos = WxUtil.match_template_center(tmp_screen, audio_reply_template, threshold=0.8)
|
||||
if switch_pos:
|
||||
logger.info(f">>> [切换] 找到切换按钮 {switch_pos},点击切换...")
|
||||
d.click(switch_pos[0], switch_pos[1])
|
||||
time.sleep(1.5)
|
||||
d.screenshot(tmp_screen)
|
||||
pos = WxUtil.match_template_center(tmp_screen, press_say_template, threshold=0.8)
|
||||
|
||||
if not pos:
|
||||
logger.error("无法定位到 '按住说话' 按钮,请检查手机界面")
|
||||
return
|
||||
|
||||
logger.info(f">>> [定位] 成功定位到语音按钮中心: {pos}")
|
||||
|
||||
# 4. 同步执行:开始长按 + 启动 TTS
|
||||
finished_event = threading.Event()
|
||||
playback_start_event = threading.Event()
|
||||
tts_thread = threading.Thread(target=tts_worker, args=(tts_manager, MY_VOICE_ID, REPLY_TEXT, finished_event, playback_start_event))
|
||||
|
||||
logger.info("步骤2: 开始同步录音与播放 (使用 Jitter Buffer 优化)...")
|
||||
|
||||
# 启动 TTS 线程
|
||||
tts_thread.start()
|
||||
|
||||
# 等待 TTS 真正开始发出声音
|
||||
logger.info(">>> [等待] 等待音频缓冲区填充并开始播放...")
|
||||
if not playback_start_event.wait(timeout=10):
|
||||
logger.error("等待音频播放超时")
|
||||
return
|
||||
|
||||
# 按住按钮
|
||||
logger.info(">>> [发送] 检测到音频播放开始,立即按住发送语音按钮...")
|
||||
d.touch.down(pos[0], pos[1])
|
||||
|
||||
# 模拟计时
|
||||
start_time = time.time()
|
||||
last_second = 0
|
||||
while not finished_event.is_set():
|
||||
elapsed = int(time.time() - start_time)
|
||||
if elapsed > last_second:
|
||||
logger.info(f">>> [计时] {elapsed}")
|
||||
last_second = elapsed
|
||||
time.sleep(0.1)
|
||||
if elapsed > 30:
|
||||
logger.warning("录音时间超过 30 秒,强制结束")
|
||||
break
|
||||
|
||||
# 释放按钮
|
||||
d.touch.up(pos[0], pos[1])
|
||||
total_duration = time.time() - start_time
|
||||
logger.info(f">>> [完成] 录音完成,总时长约 {total_duration:.2f}s")
|
||||
|
||||
tts_thread.join()
|
||||
|
||||
# 5. 截图保存结果
|
||||
final_screen = os.path.join(WxUtil.OUTPUT_DIR, "T7_Final_Result_Optimized.jpg")
|
||||
d.screenshot(final_screen)
|
||||
logger.info(f"任务结束,结果截图已保存至: {final_screen}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_t7_task()
|
||||
966
WeiXin/TTS.md
Normal file
966
WeiXin/TTS.md
Normal file
@@ -0,0 +1,966 @@
|
||||
Qwen-TTS声音复刻API参考
|
||||
更新时间:2026-01-17 00:25:11
|
||||
复制为 MD 格式
|
||||
产品详情
|
||||
我的收藏
|
||||
声音复刻依托大模型进行特征提取,无需训练即可复刻声音。仅需提供 10~20 秒的音频,即可生成高度相似且听感自然的定制音色。声音复刻与语音合成是前后关联的两个步骤。本文档聚焦于介绍声音复刻的参数和接口细节,语音合成请参见实时语音合成-通义千问。
|
||||
|
||||
用户指南:关于模型介绍和选型建议请参见实时语音合成-通义千问。
|
||||
|
||||
音频要求
|
||||
高质量的输入音频是获得优质复刻效果的基础。
|
||||
|
||||
|
||||
|
||||
项目
|
||||
|
||||
要求
|
||||
|
||||
支持格式
|
||||
|
||||
WAV (16bit)、MP3、M4A
|
||||
|
||||
音频时长
|
||||
|
||||
推荐10~20秒,最长不得超过60秒
|
||||
|
||||
文件大小
|
||||
|
||||
< 10 MB
|
||||
|
||||
采样率
|
||||
|
||||
≥ 24 kHz
|
||||
|
||||
声道
|
||||
|
||||
单声道
|
||||
|
||||
内容
|
||||
|
||||
音频必须包含至少3秒连续清晰朗读(无背景音),其余部分仅允许短暂停顿(≤2秒);整段音频应避免背景音乐、噪音或其他人声,确保核心朗读内容质量;请使用正常说话音频作为输入,不要上传歌曲或唱歌音频,以确保复刻效果准确和可用
|
||||
|
||||
语言
|
||||
|
||||
中文(zh)、英文(en)、德语(de)、意大利语(it)、葡萄牙语(pt)、西班牙语(es)、日语(ja)、韩语(ko)、法语(fr)、俄语(ru)
|
||||
|
||||
快速开始:从复刻到合成
|
||||
image
|
||||
1. 工作流程
|
||||
声音复刻与语音合成是紧密关联的两个独立步骤,遵循“先创建,后使用”的流程:
|
||||
|
||||
创建音色
|
||||
|
||||
调用创建音色接口,上传一段音频。系统会分析该音频,创建一个专属的复刻音色。此步骤必须指定target_model,声明创建的音色将由哪个语音合成模型驱动。
|
||||
|
||||
若已有创建好的音色(调用查询音色列表接口查看),可跳过这一步直接进行下一步。
|
||||
|
||||
使用音色进行语音合成
|
||||
|
||||
调用语音合成接口,传入上一步获得的音色。此步骤指定的语音合成模型必须和上一步的target_model一致。
|
||||
|
||||
2. 模型配置与准备工作
|
||||
选择合适的模型并完成准备工作。
|
||||
|
||||
模型配置
|
||||
声音复刻时需要指定以下两个模型:
|
||||
|
||||
声音复刻模型:qwen-voice-enrollment
|
||||
|
||||
驱动音色的语音合成模型:
|
||||
|
||||
qwen3-tts-vc-realtime-2026-01-15
|
||||
|
||||
qwen3-tts-vc-realtime-2025-11-27
|
||||
|
||||
准备工作
|
||||
获取API Key:获取API Key,为安全起见,推荐将API Key配置到环境变量。
|
||||
|
||||
安装SDK:确保已安装最新版DashScope SDK。
|
||||
|
||||
准备待复刻音频:音频需符合音频要求。
|
||||
|
||||
3. 端到端示例
|
||||
以下示例演示了如何在语音合成中使用声音复刻生成的专属音色,实现与原音高度相似的输出效果。
|
||||
|
||||
关键原则:声音复刻时,target_model(驱动音色的语音合成模型)必须与后续调用语音合成接口时指定的语音合成模型一致,否则会合成失败。
|
||||
|
||||
示例使用本地音频文件 voice.mp3 进行声音复刻,运行代码时,请注意替换。
|
||||
|
||||
PythonJava
|
||||
|
||||
# coding=utf-8
|
||||
# Installation instructions for pyaudio:
|
||||
# APPLE Mac OS X
|
||||
# brew install portaudio
|
||||
# pip install pyaudio
|
||||
# Debian/Ubuntu
|
||||
# sudo apt-get install python-pyaudio python3-pyaudio
|
||||
# or
|
||||
# pip install pyaudio
|
||||
# CentOS
|
||||
# sudo yum install -y portaudio portaudio-devel && pip install pyaudio
|
||||
# Microsoft Windows
|
||||
# python -m pip install pyaudio
|
||||
|
||||
import pyaudio
|
||||
import os
|
||||
import requests
|
||||
import base64
|
||||
import pathlib
|
||||
import threading
|
||||
import time
|
||||
import dashscope # DashScope Python SDK 版本需要不低于1.23.9
|
||||
from dashscope.audio.qwen_tts_realtime import QwenTtsRealtime, QwenTtsRealtimeCallback, AudioFormat
|
||||
|
||||
# ======= 常量配置 =======
|
||||
DEFAULT_TARGET_MODEL = "qwen3-tts-vc-realtime-2026-01-15" # 声音复刻、语音合成要使用相同的模型
|
||||
DEFAULT_PREFERRED_NAME = "guanyu"
|
||||
DEFAULT_AUDIO_MIME_TYPE = "audio/mpeg"
|
||||
VOICE_FILE_PATH = "voice.mp3" # 用于声音复刻的本地音频文件的相对路径
|
||||
|
||||
TEXT_TO_SYNTHESIZE = [
|
||||
'对吧~我就特别喜欢这种超市,',
|
||||
'尤其是过年的时候',
|
||||
'去逛超市',
|
||||
'就会觉得',
|
||||
'超级超级开心!',
|
||||
'想买好多好多的东西呢!'
|
||||
]
|
||||
|
||||
def create_voice(file_path: str,
|
||||
target_model: str = DEFAULT_TARGET_MODEL,
|
||||
preferred_name: str = DEFAULT_PREFERRED_NAME,
|
||||
audio_mime_type: str = DEFAULT_AUDIO_MIME_TYPE) -> str:
|
||||
"""
|
||||
创建音色,并返回 voice 参数
|
||||
"""
|
||||
# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
|
||||
# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key = "sk-xxx"
|
||||
api_key = os.getenv("DASHSCOPE_API_KEY")
|
||||
|
||||
file_path_obj = pathlib.Path(file_path)
|
||||
if not file_path_obj.exists():
|
||||
raise FileNotFoundError(f"音频文件不存在: {file_path}")
|
||||
|
||||
base64_str = base64.b64encode(file_path_obj.read_bytes()).decode()
|
||||
data_uri = f"data:{audio_mime_type};base64,{base64_str}"
|
||||
|
||||
# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
url = "https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization"
|
||||
payload = {
|
||||
"model": "qwen-voice-enrollment", # 不要修改该值
|
||||
"input": {
|
||||
"action": "create",
|
||||
"target_model": target_model,
|
||||
"preferred_name": preferred_name,
|
||||
"audio": {"data": data_uri}
|
||||
}
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
resp = requests.post(url, json=payload, headers=headers)
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"创建 voice 失败: {resp.status_code}, {resp.text}")
|
||||
|
||||
try:
|
||||
return resp.json()["output"]["voice"]
|
||||
except (KeyError, ValueError) as e:
|
||||
raise RuntimeError(f"解析 voice 响应失败: {e}")
|
||||
|
||||
def init_dashscope_api_key():
|
||||
"""
|
||||
初始化 dashscope SDK 的 API key
|
||||
"""
|
||||
# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
|
||||
# 若没有配置环境变量,请用百炼API Key将下行替换为:dashscope.api_key = "sk-xxx"
|
||||
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
|
||||
|
||||
# ======= 回调类 =======
|
||||
class MyCallback(QwenTtsRealtimeCallback):
|
||||
"""
|
||||
自定义 TTS 流式回调
|
||||
"""
|
||||
def __init__(self):
|
||||
self.complete_event = threading.Event()
|
||||
self._player = pyaudio.PyAudio()
|
||||
self._stream = self._player.open(
|
||||
format=pyaudio.paInt16, channels=1, rate=24000, output=True
|
||||
)
|
||||
|
||||
def on_open(self) -> None:
|
||||
print('[TTS] 连接已建立')
|
||||
|
||||
def on_close(self, close_status_code, close_msg) -> None:
|
||||
self._stream.stop_stream()
|
||||
self._stream.close()
|
||||
self._player.terminate()
|
||||
print(f'[TTS] 连接关闭 code={close_status_code}, msg={close_msg}')
|
||||
|
||||
def on_event(self, response: dict) -> None:
|
||||
try:
|
||||
event_type = response.get('type', '')
|
||||
if event_type == 'session.created':
|
||||
print(f'[TTS] 会话开始: {response["session"]["id"]}')
|
||||
elif event_type == 'response.audio.delta':
|
||||
audio_data = base64.b64decode(response['delta'])
|
||||
self._stream.write(audio_data)
|
||||
elif event_type == 'response.done':
|
||||
print(f'[TTS] 响应完成, Response ID: {qwen_tts_realtime.get_last_response_id()}')
|
||||
elif event_type == 'session.finished':
|
||||
print('[TTS] 会话结束')
|
||||
self.complete_event.set()
|
||||
except Exception as e:
|
||||
print(f'[Error] 处理回调事件异常: {e}')
|
||||
|
||||
def wait_for_finished(self):
|
||||
self.complete_event.wait()
|
||||
|
||||
# ======= 主执行逻辑 =======
|
||||
if __name__ == '__main__':
|
||||
init_dashscope_api_key()
|
||||
print('[系统] 初始化 Qwen TTS Realtime ...')
|
||||
|
||||
callback = MyCallback()
|
||||
qwen_tts_realtime = QwenTtsRealtime(
|
||||
model=DEFAULT_TARGET_MODEL,
|
||||
callback=callback,
|
||||
# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime
|
||||
url='wss://dashscope.aliyuncs.com/api-ws/v1/realtime'
|
||||
)
|
||||
qwen_tts_realtime.connect()
|
||||
|
||||
qwen_tts_realtime.update_session(
|
||||
voice=create_voice(VOICE_FILE_PATH), # 将voice参数替换为复刻生成的专属音色
|
||||
response_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
|
||||
mode='server_commit'
|
||||
)
|
||||
|
||||
for text_chunk in TEXT_TO_SYNTHESIZE:
|
||||
print(f'[发送文本]: {text_chunk}')
|
||||
qwen_tts_realtime.append_text(text_chunk)
|
||||
time.sleep(0.1)
|
||||
|
||||
qwen_tts_realtime.finish()
|
||||
callback.wait_for_finished()
|
||||
|
||||
print(f'[Metric] session_id={qwen_tts_realtime.get_session_id()}, '
|
||||
f'first_audio_delay={qwen_tts_realtime.get_first_audio_delay()}s')
|
||||
API参考
|
||||
使用不同 API 时,请确保使用同一账号进行操作。
|
||||
|
||||
创建音色
|
||||
上传用于复刻的音频,创建自定义音色。
|
||||
|
||||
URL
|
||||
|
||||
中国内地:
|
||||
|
||||
|
||||
POST https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
国际:
|
||||
|
||||
|
||||
POST https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
请求头
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
参数
|
||||
|
||||
类型
|
||||
|
||||
是否必须
|
||||
|
||||
说明
|
||||
|
||||
Authorization
|
||||
|
||||
string
|
||||
|
||||
支持
|
||||
|
||||
鉴权令牌,格式为Bearer <your_api_key>,使用时,将“<your_api_key>”替换为实际的API Key。
|
||||
|
||||
Content-Type
|
||||
|
||||
string
|
||||
|
||||
支持
|
||||
|
||||
请求体中传输的数据的媒体类型。固定为application/json。
|
||||
|
||||
消息体
|
||||
|
||||
包含所有请求参数的消息体如下,对于可选字段,在实际业务中可根据需求省略。
|
||||
|
||||
重要
|
||||
注意区分如下参数:
|
||||
|
||||
model:声音复刻模型,固定为qwen-voice-enrollment
|
||||
|
||||
target_model:驱动音色的语音合成模型,须和后续调用语音合成接口时使用的语音合成模型一致,否则合成会失败
|
||||
|
||||
|
||||
{
|
||||
"model": "qwen-voice-enrollment",
|
||||
"input": {
|
||||
"action": "create",
|
||||
"target_model": "qwen3-tts-vc-realtime-2026-01-15",
|
||||
"preferred_name": "guanyu",
|
||||
"audio": {
|
||||
"data": "https://xxx.wav"
|
||||
},
|
||||
"text": "可选项,填入audio.data对应的文本",
|
||||
"language": "可选项,填入audio.data对应的语种,如zh"
|
||||
}
|
||||
}
|
||||
请求参数
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
参数
|
||||
|
||||
类型
|
||||
|
||||
默认值
|
||||
|
||||
是否必须
|
||||
|
||||
说明
|
||||
|
||||
model
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
声音复刻模型,固定为qwen-voice-enrollment。
|
||||
|
||||
action
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
操作类型,固定为create。
|
||||
|
||||
target_model
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
驱动音色的语音合成模型,支持的模型有:
|
||||
|
||||
qwen3-tts-vc-realtime-2026-01-15
|
||||
|
||||
qwen3-tts-vc-realtime-2025-11-27
|
||||
|
||||
必须与后续调用语音合成接口时使用的语音合成模型一致,否则合成会失败。
|
||||
|
||||
preferred_name
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
为音色指定一个便于识别的名称(仅允许数字、大小写字母和下划线,不超过16个字符)。建议选用与角色、场景相关的标识。
|
||||
|
||||
该关键字会在复刻的音色名中出现,例如关键字为“guanyu”,最终音色名为“qwen-tts-vc-guanyu-voice-20250812105009984-838b”
|
||||
audio.data
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
用于复刻的音频(录制时需遵循录音操作指南,音频需满足音频要求)。
|
||||
|
||||
可通过以下两种方式提交音频数据:
|
||||
|
||||
Data URL
|
||||
|
||||
格式:data:<mediatype>;base64,<data>
|
||||
|
||||
<mediatype>:MIME类型
|
||||
|
||||
WAV:audio/wav
|
||||
|
||||
MP3:audio/mpeg
|
||||
|
||||
M4A:audio/mp4
|
||||
|
||||
<data>:音频转成的Base64编码的字符串
|
||||
|
||||
Base64编码会增大体积,请控制原文件大小,确保编码后仍小于10MB
|
||||
|
||||
示例:data:audio/wav;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjI5LjEwMAAAAAAAAAAAAAAA//PAxABQ/BXRbMPe4IQAhl9
|
||||
|
||||
点击查看示例代码
|
||||
|
||||
音频URL(推荐将音频上传至OSS)
|
||||
|
||||
文件大小不超过10MB
|
||||
|
||||
URL必须公网可访问且无需鉴权
|
||||
|
||||
text
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
不支持
|
||||
|
||||
与audio.data音频内容相匹配的文本。
|
||||
|
||||
传入该参数后,服务端会对比音频与该文本的差异,若差异过大,将返回Audio.PreprocessError。
|
||||
|
||||
language
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
不支持
|
||||
|
||||
audio.data音频对应的语种。
|
||||
|
||||
支持zh(中文)、en(英文)、de(德语)、it(意大利语)、pt(葡萄牙语)、es(西班牙语)、ja(日语)、ko(韩语)、fr(法语)、ru(俄语)。
|
||||
|
||||
若使用该参数,设置的语种要和实际用于复刻的音频的语种一致。
|
||||
|
||||
响应参数
|
||||
|
||||
点击查看响应示例
|
||||
|
||||
需关注的参数如下:
|
||||
|
||||
|
||||
|
||||
|
||||
参数
|
||||
|
||||
类型
|
||||
|
||||
说明
|
||||
|
||||
voice
|
||||
|
||||
string
|
||||
|
||||
音色名称,可直接用于语音合成接口的voice参数。
|
||||
|
||||
target_model
|
||||
|
||||
string
|
||||
|
||||
驱动音色的语音合成模型,支持的模型有:
|
||||
|
||||
qwen3-tts-vc-realtime-2026-01-15
|
||||
|
||||
qwen3-tts-vc-realtime-2025-11-27
|
||||
|
||||
必须与后续调用语音合成接口时使用的语音合成模型一致,否则合成会失败。
|
||||
|
||||
request_id
|
||||
|
||||
string
|
||||
|
||||
Request ID。
|
||||
|
||||
count
|
||||
|
||||
integer
|
||||
|
||||
本次请求实际计入费用的“创建音色”次数,本次请求的费用为
|
||||
count×0.01
|
||||
元。
|
||||
|
||||
创建音色时,count恒为1。
|
||||
|
||||
示例代码
|
||||
|
||||
重要
|
||||
注意区分如下参数:
|
||||
|
||||
model:声音复刻模型,固定为qwen-voice-enrollment
|
||||
|
||||
target_model:驱动音色的语音合成模型,须和后续调用语音合成接口时使用的语音合成模型一致,否则合成会失败
|
||||
|
||||
cURLPythonJava
|
||||
若未将API Key配置到环境变量,需将示例中的$DASHSCOPE_API_KEY替换为实际的API Key。
|
||||
|
||||
|
||||
# ======= 重要提示 =======
|
||||
# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
# 新加坡地域和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
|
||||
# === 执行时请删除该注释 ===
|
||||
|
||||
curl -X POST https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization \
|
||||
-H "Authorization: Bearer $DASHSCOPE_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "qwen-voice-enrollment",
|
||||
"input": {
|
||||
"action": "create",
|
||||
"target_model": "qwen3-tts-vc-realtime-2026-01-15",
|
||||
"preferred_name": "guanyu",
|
||||
"audio": {
|
||||
"data": "https://xxx.wav"
|
||||
}
|
||||
}
|
||||
}'
|
||||
查询音色列表
|
||||
分页查询已创建的音色列表。
|
||||
|
||||
URL
|
||||
|
||||
中国内地:
|
||||
|
||||
|
||||
POST https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
国际:
|
||||
|
||||
|
||||
POST https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
请求头
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
参数
|
||||
|
||||
类型
|
||||
|
||||
是否必须
|
||||
|
||||
说明
|
||||
|
||||
Authorization
|
||||
|
||||
string
|
||||
|
||||
支持
|
||||
|
||||
鉴权令牌,格式为Bearer <your_api_key>,使用时,将“<your_api_key>”替换为实际的API Key。
|
||||
|
||||
Content-Type
|
||||
|
||||
string
|
||||
|
||||
支持
|
||||
|
||||
请求体中传输的数据的媒体类型。固定为application/json。
|
||||
|
||||
消息体
|
||||
|
||||
包含所有请求参数的消息体如下,对于可选字段,在实际业务中可根据需求省略。
|
||||
|
||||
重要
|
||||
model:声音复刻模型,固定为qwen-voice-enrollment,请勿修改。
|
||||
|
||||
|
||||
{
|
||||
"model": "qwen-voice-enrollment",
|
||||
"input": {
|
||||
"action": "list",
|
||||
"page_size": 2,
|
||||
"page_index": 0
|
||||
}
|
||||
}
|
||||
请求参数
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
参数
|
||||
|
||||
类型
|
||||
|
||||
默认值
|
||||
|
||||
是否必须
|
||||
|
||||
说明
|
||||
|
||||
model
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
声音复刻模型,固定为qwen-voice-enrollment。
|
||||
|
||||
action
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
操作类型,固定为list。
|
||||
|
||||
page_index
|
||||
|
||||
integer
|
||||
|
||||
0
|
||||
|
||||
不支持
|
||||
|
||||
页码索引。取值范围:[0, 1000000]。
|
||||
|
||||
page_size
|
||||
|
||||
integer
|
||||
|
||||
10
|
||||
|
||||
不支持
|
||||
|
||||
每页包含数据条数。取值范围:[0, 1000000]。
|
||||
|
||||
响应参数
|
||||
|
||||
点击查看响应示例
|
||||
|
||||
需关注的参数如下:
|
||||
|
||||
|
||||
|
||||
|
||||
参数
|
||||
|
||||
类型
|
||||
|
||||
说明
|
||||
|
||||
voice
|
||||
|
||||
string
|
||||
|
||||
音色名称,可直接用于语音合成接口的voice参数。
|
||||
|
||||
gmt_create
|
||||
|
||||
string
|
||||
|
||||
创建音色的时间。
|
||||
|
||||
target_model
|
||||
|
||||
string
|
||||
|
||||
驱动音色的语音合成模型,支持的模型有:
|
||||
|
||||
qwen3-tts-vc-realtime-2026-01-15
|
||||
|
||||
qwen3-tts-vc-realtime-2025-11-27
|
||||
|
||||
必须与后续调用语音合成接口时使用的语音合成模型一致,否则合成会失败。
|
||||
|
||||
request_id
|
||||
|
||||
string
|
||||
|
||||
Request ID。
|
||||
|
||||
count
|
||||
|
||||
integer
|
||||
|
||||
本次请求实际计入费用的“创建音色”次数,本次请求的费用为
|
||||
count×0.01
|
||||
元。
|
||||
|
||||
查询音色不计费,因此count恒为0。
|
||||
|
||||
示例代码
|
||||
|
||||
重要
|
||||
model:声音复刻模型,固定为qwen-voice-enrollment,请勿修改。
|
||||
|
||||
cURLPythonJava
|
||||
若未将API Key配置到环境变量,需将示例中的$DASHSCOPE_API_KEY替换为实际的API Key。
|
||||
|
||||
|
||||
# ======= 重要提示 =======
|
||||
# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
# 新加坡地域和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
|
||||
# === 执行时请删除该注释 ===
|
||||
|
||||
curl --location --request POST 'https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization' \
|
||||
--header 'Authorization: Bearer $DASHSCOPE_API_KEY' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"model": "qwen-voice-enrollment",
|
||||
"input": {
|
||||
"action": "list",
|
||||
"page_size": 10,
|
||||
"page_index": 0
|
||||
}
|
||||
}'
|
||||
删除音色
|
||||
删除指定音色,释放对应额度。
|
||||
|
||||
URL
|
||||
|
||||
中国内地:
|
||||
|
||||
|
||||
POST https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
国际:
|
||||
|
||||
|
||||
POST https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
请求头
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
参数
|
||||
|
||||
类型
|
||||
|
||||
是否必须
|
||||
|
||||
说明
|
||||
|
||||
Authorization
|
||||
|
||||
string
|
||||
|
||||
支持
|
||||
|
||||
鉴权令牌,格式为Bearer <your_api_key>,使用时,将“<your_api_key>”替换为实际的API Key。
|
||||
|
||||
Content-Type
|
||||
|
||||
string
|
||||
|
||||
支持
|
||||
|
||||
请求体中传输的数据的媒体类型。固定为application/json。
|
||||
|
||||
消息体
|
||||
|
||||
包含所有请求参数的消息体如下,对于可选字段,在实际业务中可根据需求省略:
|
||||
|
||||
重要
|
||||
model:声音复刻模型,固定为qwen-voice-enrollment,请勿修改。
|
||||
|
||||
|
||||
{
|
||||
"model": "qwen-voice-enrollment",
|
||||
"input": {
|
||||
"action": "delete",
|
||||
"voice": "yourVoice"
|
||||
}
|
||||
}
|
||||
请求参数
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
参数
|
||||
|
||||
类型
|
||||
|
||||
默认值
|
||||
|
||||
是否必须
|
||||
|
||||
说明
|
||||
|
||||
model
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
声音复刻模型,固定为qwen-voice-enrollment。
|
||||
|
||||
action
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
操作类型,固定为delete。
|
||||
|
||||
voice
|
||||
|
||||
string
|
||||
|
||||
-
|
||||
|
||||
支持
|
||||
|
||||
待删除的音色。
|
||||
|
||||
响应参数
|
||||
|
||||
点击查看响应示例
|
||||
|
||||
需关注的参数如下:
|
||||
|
||||
|
||||
|
||||
|
||||
参数
|
||||
|
||||
类型
|
||||
|
||||
说明
|
||||
|
||||
request_id
|
||||
|
||||
string
|
||||
|
||||
Request ID。
|
||||
|
||||
count
|
||||
|
||||
integer
|
||||
|
||||
本次请求实际计入费用的“创建音色”次数,本次请求的费用为
|
||||
count×0.01
|
||||
元。
|
||||
|
||||
删除音色不计费,因此count恒为0。
|
||||
|
||||
示例代码
|
||||
|
||||
重要
|
||||
model:声音复刻模型,固定为qwen-voice-enrollment,请勿修改。
|
||||
|
||||
cURLPythonJava
|
||||
若未将API Key配置到环境变量,需将示例中的$DASHSCOPE_API_KEY替换为实际的API Key。
|
||||
|
||||
|
||||
# ======= 重要提示 =======
|
||||
# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization
|
||||
# 新加坡地域和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
|
||||
# === 执行时请删除该注释 ===
|
||||
|
||||
curl --location --request POST 'https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization' \
|
||||
--header 'Authorization: Bearer $DASHSCOPE_API_KEY' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"model": "qwen-voice-enrollment",
|
||||
"input": {
|
||||
"action": "delete",
|
||||
"voice": "yourVoice"
|
||||
}
|
||||
}'
|
||||
语音合成
|
||||
如何使用声音复刻生成的专属音色合成个性化的声音,请参见快速开始:从复刻到合成。
|
||||
|
||||
用于声音复刻的语音合成模型(如 qwen3-tts-vc-realtime-2026-01-15)为专用模型,仅支持使用复刻生成的音色,不支持Chelsie、Serena、Ethan、Cherry等公共预设音色。
|
||||
|
||||
音色配额与自动清理规则
|
||||
总数限制:1000个音色/账号
|
||||
|
||||
当前接口不提供音色数量查询功能,可通过调用查询音色列表接口自行统计音色数目
|
||||
自动清理:若单个音色在过去一年内未被用于任何语音合成请求,系统将自动将其删除
|
||||
|
||||
计费说明
|
||||
声音复刻和语音合成分开计费:
|
||||
|
||||
声音复刻:创建音色按0.01 元/个计费,创建失败不计费
|
||||
|
||||
说明
|
||||
免费额度说明(仅中国站北京地域和国际站新加坡地域有免费额度):
|
||||
|
||||
阿里云百炼开通后90天内,可享1000次免费音色创建机会。
|
||||
|
||||
创建失败不占用免费次数。
|
||||
|
||||
删除音色不会恢复免费次数。
|
||||
|
||||
免费额度用完或超出 90 天有效期后,创建音色将按0.01 元/个的价格计费。
|
||||
|
||||
使用复刻生成的专属音色进行语音合成:按量(文本字符数)计费,详情请参见实时语音合成-通义千问
|
||||
|
||||
版权与合法性
|
||||
您需对所提供声音的所有权及合法使用权负责,请注意阅读服务协议。
|
||||
|
||||
录音操作指南
|
||||
录音设备
|
||||
推荐使用具备降噪功能的麦克风,或在安静环境下使用手机近距离录音,以保证音源纯净。
|
||||
|
||||
录音环境
|
||||
场地
|
||||
建议在 10 平方米以内的小型封闭空间录音。
|
||||
|
||||
优先选择配有吸音材料(如吸音棉、地毯、窗帘)的房间。
|
||||
|
||||
避免空旷大厅、会议室、教室等高混响场所。
|
||||
|
||||
噪音控制
|
||||
室外噪音:关闭门窗,避免交通、施工等干扰。
|
||||
|
||||
室内噪音:关闭空调、风扇、日光灯镇流器等设备;可通过手机录制环境音并放大播放,识别潜在噪音源。
|
||||
|
||||
混响控制
|
||||
混响会导致声音模糊、清晰度下降。
|
||||
|
||||
减少光滑表面反射:拉上窗帘、打开衣柜门、铺放衣物或床单覆盖桌面/柜面。
|
||||
|
||||
利用不规则物体(如书架、软包家具)实现声波漫反射。
|
||||
|
||||
录音文案
|
||||
文案内容灵活,建议与目标应用场景一致(例如,若用于客服场景,文案应为客服对话风格),但必须确保不包含任何敏感或非法词汇(如政治、色情、暴力相关内容),否则会导致复刻失败。
|
||||
|
||||
避免短句(如“你好”、“是的”),应使用完整句子。
|
||||
|
||||
保持语义连贯,朗读时避免频繁停顿(建议至少连续 3 秒无中断)。
|
||||
|
||||
可带入目标情绪(如亲切、严肃),但需避免过度夸张的戏剧化朗读,保持语调自然。
|
||||
|
||||
操作建议
|
||||
以普通卧室为例:
|
||||
|
||||
关闭门窗,隔绝外部噪音。
|
||||
|
||||
关闭空调、电扇等电器。
|
||||
|
||||
拉上窗帘,减少玻璃反射。
|
||||
|
||||
在桌面铺放衣物或毛毯,降低桌面反射。
|
||||
|
||||
提前熟悉文案,设定角色语气,自然演绎。
|
||||
|
||||
与录音设备保持约 10 厘米距离,避免喷麦或信号过弱。
|
||||
BIN
WeiXin/Templates/audio_reply.jpg
Normal file
BIN
WeiXin/Templates/audio_reply.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
WeiXin/Templates/keyboard.jpg
Normal file
BIN
WeiXin/Templates/keyboard.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
WeiXin/Templates/press_say.jpg
Normal file
BIN
WeiXin/Templates/press_say.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
128
WeiXin/WxUtil.py
128
WeiXin/WxUtil.py
@@ -1391,6 +1391,134 @@ def perform_input_action(d, center_point, text, auto_send=True, debug_prefix=Non
|
||||
logger.error(f"perform_input_action error: {e}")
|
||||
return False
|
||||
|
||||
def perform_voice_input(d, duration=3.0, debug_prefix=None):
|
||||
"""
|
||||
执行语音输入操作 (长按说话)
|
||||
1. 检查是否在语音模式 (寻找 press_say.jpg)
|
||||
2. 如果不在,尝试点击 keyboard.jpg 或 audio_reply.jpg 切换
|
||||
3. 长按 press_say.jpg,并进行计时记录
|
||||
"""
|
||||
try:
|
||||
def save_debug_shot(name):
|
||||
if debug_prefix:
|
||||
shot_path = os.path.join(OUTPUT_DIR, f"{debug_prefix}_{name}.jpg")
|
||||
d.screenshot(shot_path)
|
||||
logger.info(f"保存中间过程截图: {shot_path}")
|
||||
|
||||
save_debug_shot("voice_1_check")
|
||||
|
||||
# 模板路径
|
||||
press_say_template = os.path.join(TEMPLATE_DIR, "press_say.jpg")
|
||||
keyboard_template = os.path.join(TEMPLATE_DIR, "keyboard.jpg")
|
||||
audio_reply_template = os.path.join(TEMPLATE_DIR, "audio_reply.jpg")
|
||||
|
||||
# 1. 检查当前模式
|
||||
tmp_screen = os.path.join(OUTPUT_DIR, "temp_voice_check.jpg")
|
||||
d.screenshot(tmp_screen)
|
||||
|
||||
press_say_pos = find_template_match(tmp_screen, press_say_template, threshold=0.8)
|
||||
|
||||
if press_say_pos:
|
||||
logger.info(">>> [状态] 当前已是语音模式 (找到 '按住说话' 按钮)")
|
||||
need_switch = False
|
||||
else:
|
||||
logger.info(">>> [状态] 当前可能是键盘模式 (未找到 '按住说话' 按钮)")
|
||||
need_switch = True
|
||||
|
||||
if need_switch:
|
||||
logger.info(">>> [切换] 需要进行模式切换...")
|
||||
|
||||
# 2. 尝试点击键盘图标或音频图标切换模式
|
||||
switch_pos = find_template_match(tmp_screen, keyboard_template, threshold=0.8)
|
||||
if not switch_pos:
|
||||
switch_pos = find_template_match(tmp_screen, audio_reply_template, threshold=0.8)
|
||||
|
||||
if switch_pos:
|
||||
logger.info(f">>> [切换] 找到切换按钮 {switch_pos},正在点击切换...")
|
||||
d.click(switch_pos[0], switch_pos[1])
|
||||
time.sleep(1.5) # 稍微增加等待时间确保切换完成
|
||||
|
||||
# 验证切换是否成功
|
||||
d.screenshot(tmp_screen)
|
||||
press_say_pos = find_template_match(tmp_screen, press_say_template, threshold=0.8)
|
||||
if press_say_pos:
|
||||
logger.info(">>> [切换] 成功完成切换,进入语音模式")
|
||||
else:
|
||||
logger.error(">>> [切换] 切换操作已执行,但仍未找到 '按住说话' 按钮,切换可能失败")
|
||||
else:
|
||||
logger.warning(">>> [切换] 未找到切换按钮 (keyboard.jpg/audio_reply.jpg),无法切换")
|
||||
|
||||
# 3. 执行长按
|
||||
if press_say_pos:
|
||||
x, y = press_say_pos
|
||||
logger.info(f">>> [发送] 开始按住发送语音按钮 ({x}, {y})")
|
||||
|
||||
save_debug_shot("voice_2_before_hold")
|
||||
|
||||
# 开始计时长按
|
||||
d.touch.down(x, y)
|
||||
|
||||
start_time = time.time()
|
||||
last_second = 0
|
||||
while time.time() - start_time < duration:
|
||||
elapsed = int(time.time() - start_time) + 1
|
||||
if elapsed > last_second and elapsed <= duration:
|
||||
logger.info(f">>> [计时] {elapsed}")
|
||||
last_second = elapsed
|
||||
time.sleep(0.1)
|
||||
|
||||
d.touch.up(x, y)
|
||||
|
||||
logger.info(f">>> [完成] 已完成指定时长 ({duration}s) 的按住按钮发送语音")
|
||||
save_debug_shot("voice_3_after_hold")
|
||||
|
||||
if os.path.exists(tmp_screen):
|
||||
try: os.remove(tmp_screen)
|
||||
except: pass
|
||||
return True
|
||||
else:
|
||||
logger.error(">>> [失败] 最终未能定位到发送语音按钮")
|
||||
if os.path.exists(tmp_screen):
|
||||
try: os.remove(tmp_screen)
|
||||
except: pass
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"perform_voice_input error: {e}")
|
||||
return False
|
||||
|
||||
def switch_to_keyboard_mode(d):
|
||||
"""
|
||||
强制切换到键盘/文本模式
|
||||
"""
|
||||
try:
|
||||
logger.info(">>> [模式] 尝试切换到键盘模式...")
|
||||
# 1. 尝试 UI 树
|
||||
voice_mode_btn = d(description="切换到键盘")
|
||||
if voice_mode_btn.exists:
|
||||
logger.info(">>> [模式] 检测到语音模式按钮,点击切换到键盘...")
|
||||
voice_mode_btn.click()
|
||||
time.sleep(1.0)
|
||||
return True
|
||||
|
||||
# 2. 尝试图像匹配 (wen_zi_input.jpg)
|
||||
tmp_screen = os.path.join(OUTPUT_DIR, "temp_switch_kb.jpg")
|
||||
d.screenshot(tmp_screen)
|
||||
wen_zi_template = os.path.join(TEMPLATE_DIR, "wen_zi_input.jpg")
|
||||
wen_zi_pos = find_template_match(tmp_screen, wen_zi_template, threshold=0.8)
|
||||
|
||||
if wen_zi_pos:
|
||||
logger.info(f">>> [模式] 找到切换文字图标 {wen_zi_pos},点击切换...")
|
||||
d.click(wen_zi_pos[0], wen_zi_pos[1])
|
||||
time.sleep(1.0)
|
||||
return True
|
||||
|
||||
logger.info(">>> [模式] 当前可能已经是键盘模式,或未找到切换按钮")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"switch_to_keyboard_mode error: {e}")
|
||||
return False
|
||||
|
||||
def match_template_center(image_path, template_path, threshold=0.8):
|
||||
"""
|
||||
使用 OpenCV 模板匹配寻找目标图片中心坐标
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user