diff --git a/channel/wechatmp/passive_reply.py b/channel/wechatmp/passive_reply.py index 2025d03a4..3407198cc 100644 --- a/channel/wechatmp/passive_reply.py +++ b/channel/wechatmp/passive_reply.py @@ -25,21 +25,9 @@ def POST(self): message = web.data() # todo crypto msg = parse_message(message) logger.debug("[wechatmp] Receive post data:\n" + message.decode("utf-8")) - if msg.type == "event": - logger.info( - "[wechatmp] Event {} from {}".format( - msg.event, msg.source - ) - ) - if msg.event in ["subscribe", "subscribe_scan"]: - reply_text = subscribe_msg() - replyPost = create_reply(reply_text, msg) - return replyPost.render() - else: - return "success" - - wechatmp_msg = WeChatMPMessage(msg, client=channel.client) - if wechatmp_msg.ctype in [ContextType.TEXT, ContextType.IMAGE, ContextType.VOICE]: + + if msg.type in ["text", "voice", "image"]: + wechatmp_msg = WeChatMPMessage(msg, client=channel.client) from_user = wechatmp_msg.from_user_id content = wechatmp_msg.content message_id = wechatmp_msg.msg_id @@ -76,7 +64,7 @@ def POST(self): channel.running.add(from_user) channel.produce(context) else: - trigger_prefix = conf().get("single_chat_prefix", [""]) + trigger_prefix = conf().get("single_chat_prefix", [""])[0] if trigger_prefix or not supported: if trigger_prefix: reply_text = textwrap.dedent( @@ -107,13 +95,13 @@ def POST(self): request_cnt = channel.request_cnt.get(message_id, 0) + 1 channel.request_cnt[message_id] = request_cnt logger.info( - "[wechatmp] Request {} from {} {}\n{}\n{}:{}".format( + "[wechatmp] Request {} from {} {} {}:{}\n{}".format( request_cnt, from_user, message_id, - content, web.ctx.env.get("REMOTE_ADDR"), web.ctx.env.get("REMOTE_PORT"), + content ) ) @@ -151,23 +139,23 @@ def POST(self): # Only one request can access to the cached data try: - (reply_type, content) = channel.cache_dict.pop(from_user) + (reply_type, reply_content) = channel.cache_dict.pop(from_user) except KeyError: return "success" if (reply_type == "text"): - if len(content.encode("utf8")) <= MAX_UTF8_LEN: - reply_text = content + if len(reply_content.encode("utf8")) <= MAX_UTF8_LEN: + reply_text = reply_content else: continue_text = "\n【未完待续,回复任意文字以继续】" splits = split_string_by_utf8_length( - content, + reply_content, MAX_UTF8_LEN - len(continue_text.encode("utf-8")), max_split=1, ) reply_text = splits[0] + continue_text channel.cache_dict[from_user] = ("text", splits[1]) - + logger.info( "[wechatmp] Request {} do send to {} {}: {}\n{}".format( request_cnt, @@ -180,20 +168,51 @@ def POST(self): replyPost = create_reply(reply_text, msg) return replyPost.render() - elif (reply_type == "voice"): - media_id = content + media_id = reply_content asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop) + logger.info( + "[wechatmp] Request {} do send to {} {}: {} voice media_id {}".format( + request_cnt, + from_user, + message_id, + content, + media_id, + ) + ) replyPost = VoiceReply(message=msg) replyPost.media_id = media_id return replyPost.render() elif (reply_type == "image"): - media_id = content + media_id = reply_content asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop) + logger.info( + "[wechatmp] Request {} do send to {} {}: {} image media_id {}".format( + request_cnt, + from_user, + message_id, + content, + media_id, + ) + ) replyPost = ImageReply(message=msg) replyPost.media_id = media_id return replyPost.render() + + elif msg.type == "event": + logger.info( + "[wechatmp] Event {} from {}".format( + msg.event, msg.source + ) + ) + if msg.event in ["subscribe", "subscribe_scan"]: + reply_text = subscribe_msg() + replyPost = create_reply(reply_text, msg) + return replyPost.render() + else: + return "success" + else: logger.info("暂且不处理") return "success" diff --git a/channel/wechatmp/wechatmp_channel.py b/channel/wechatmp/wechatmp_channel.py index afa5ac5f5..1768475a5 100644 --- a/channel/wechatmp/wechatmp_channel.py +++ b/channel/wechatmp/wechatmp_channel.py @@ -4,22 +4,20 @@ import time import imghdr import requests +import asyncio +import threading +from config import conf from bridge.context import * from bridge.reply import * -from channel.chat_channel import ChatChannel -from channel.wechatmp.wechatmp_client import WechatMPClient -from channel.wechatmp.common import * from common.log import logger from common.singleton import singleton -from config import conf - +from voice.audio_convert import any_to_mp3 +from channel.chat_channel import ChatChannel +from channel.wechatmp.common import * +from channel.wechatmp.wechatmp_client import WechatMPClient from wechatpy.exceptions import WeChatClientException -import asyncio -from threading import Thread import web - -from voice.audio_convert import any_to_mp3 # If using SSL, uncomment the following lines, and modify the certificate path. # from cheroot.server import HTTPServer # from cheroot.ssl.builtin import BuiltinSSLAdapter @@ -46,7 +44,7 @@ def __init__(self, passive_reply=True): self.request_cnt = dict() # The permanent media need to be deleted to avoid media number limit self.delete_media_loop = asyncio.new_event_loop() - t = Thread(target=self.start_loop, args=(self.delete_media_loop,)) + t = threading.Thread(target=self.start_loop, args=(self.delete_media_loop,)) t.setDaemon(True) t.start() @@ -75,20 +73,26 @@ def send(self, reply: Reply, context: Context): if self.passive_reply: if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR: reply_text = reply.content - logger.info("[wechatmp] reply to {} cached:\n{}".format(receiver, reply_text)) + logger.info("[wechatmp] text cached, receiver {}\n{}".format(receiver, reply_text)) self.cache_dict[receiver] = ("text", reply_text) elif reply.type == ReplyType.VOICE: try: - file_path = reply.content - response = self.client.material.add("voice", open(file_path, "rb")) - logger.debug("[wechatmp] upload voice response: {}".format(response)) + voice_file_path = reply.content + with open(voice_file_path, 'rb') as f: + # support: <2M, <60s, mp3/wma/wav/amr + response = self.client.material.add("voice", f) + logger.debug("[wechatmp] upload voice response: {}".format(response)) + # 根据文件大小估计一个微信自动审核的时间,审核结束前返回将会导致语音无法播放,这个估计有待验证 + f_size = os.fstat(f.fileno()).st_size + time.sleep(1.0 + 2 * f_size / 1024 / 1024) + # todo check media_id except WeChatClientException as e: logger.error("[wechatmp] upload voice failed: {}".format(e)) return - time.sleep(3) # todo check media_id media_id = response["media_id"] - logger.info("[wechatmp] voice reply to {} uploaded: {}".format(receiver, media_id)) + logger.info("[wechatmp] voice uploaded, receiver {}, media_id {}".format(receiver, media_id)) self.cache_dict[receiver] = ("voice", media_id) + elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片 img_url = reply.content pic_res = requests.get(img_url, stream=True) @@ -106,9 +110,7 @@ def send(self, reply: Reply, context: Context): logger.error("[wechatmp] upload image failed: {}".format(e)) return media_id = response["media_id"] - logger.info( - "[wechatmp] image reply url={}, receiver={}".format(img_url, receiver) - ) + logger.info("[wechatmp] image uploaded, receiver {}, media_id {}".format(receiver, media_id)) self.cache_dict[receiver] = ("image", media_id) elif reply.type == ReplyType.IMAGE: # 从文件读取图片 image_storage = reply.content @@ -123,15 +125,13 @@ def send(self, reply: Reply, context: Context): logger.error("[wechatmp] upload image failed: {}".format(e)) return media_id = response["media_id"] - logger.info( - "[wechatmp] image reply url={}, receiver={}".format(img_url, receiver) - ) + logger.info("[wechatmp] image uploaded, receiver {}, media_id {}".format(receiver, media_id)) self.cache_dict[receiver] = ("image", media_id) else: if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR: reply_text = reply.content self.client.message.send_text(receiver, reply_text) - logger.info("[wechatmp] Do send to {}: {}".format(receiver, reply_text)) + logger.info("[wechatmp] Do send text to {}: {}".format(receiver, reply_text)) elif reply.type == ReplyType.VOICE: try: file_path = reply.content @@ -148,6 +148,7 @@ def send(self, reply: Reply, context: Context): file_name = os.path.basename(file_path) file_type = "audio/mpeg" logger.info("[wechatmp] file_name: {}, file_type: {} ".format(file_name, file_type)) + # support: <2M, <60s, AMR\MP3 response = self.client.media.upload("voice", (file_name, open(file_path, "rb"), file_type)) logger.debug("[wechatmp] upload voice response: {}".format(response)) except WeChatClientException as e: @@ -171,12 +172,8 @@ def send(self, reply: Reply, context: Context): except WeChatClientException as e: logger.error("[wechatmp] upload image failed: {}".format(e)) return - self.client.message.send_image( - receiver, response["media_id"] - ) - logger.info( - "[wechatmp] sendImage url={}, receiver={}".format(img_url, receiver) - ) + self.client.message.send_image(receiver, response["media_id"]) + logger.info("[wechatmp] Do send image to {}".format(receiver)) elif reply.type == ReplyType.IMAGE: # 从文件读取图片 image_storage = reply.content image_storage.seek(0) @@ -189,12 +186,8 @@ def send(self, reply: Reply, context: Context): except WeChatClientException as e: logger.error("[wechatmp] upload image failed: {}".format(e)) return - self.client.message.send_image( - receiver, response["media_id"] - ) - logger.info( - "[wechatmp] sendImage, receiver={}".format(receiver) - ) + self.client.message.send_image(receiver, response["media_id"]) + logger.info("[wechatmp] Do send image to {}".format(receiver)) return def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数 diff --git a/voice/pytts/pytts_voice.py b/voice/pytts/pytts_voice.py index 072e28b41..17cd6ff16 100644 --- a/voice/pytts/pytts_voice.py +++ b/voice/pytts/pytts_voice.py @@ -2,8 +2,9 @@ pytts voice service (offline) """ +import os +import sys import time - import pyttsx3 from bridge.reply import Reply, ReplyType @@ -11,7 +12,6 @@ from common.tmp_dir import TmpDir from voice.voice import Voice - class PyttsVoice(Voice): engine = pyttsx3.init() @@ -20,19 +20,42 @@ def __init__(self): self.engine.setProperty("rate", 125) # 音量 self.engine.setProperty("volume", 1.0) - for voice in self.engine.getProperty("voices"): - if "Chinese" in voice.name: - self.engine.setProperty("voice", voice.id) + if sys.platform == 'win32': + for voice in self.engine.getProperty("voices"): + if "Chinese" in voice.name: + self.engine.setProperty("voice", voice.id) + else: + self.engine.setProperty("voice", "zh") + # If the problem of espeak is fixed, using runAndWait() and remove this startLoop() + # TODO: check if this is work on win32 + self.engine.startLoop(useDriverLoop=False) def textToVoice(self, text): try: - wavFile = TmpDir().path() + "reply-" + str(int(time.time())) + ".wav" + # avoid the same filename + wavFileName = "reply-" + str(int(time.time())) + "-" + str(hash(text) & 0x7fffffff) + ".wav" + wavFile = TmpDir().path() + wavFileName + logger.info("[Pytts] textToVoice text={} voice file name={}".format(text, wavFile)) + self.engine.save_to_file(text, wavFile) - self.engine.runAndWait() - logger.info( - "[Pytts] textToVoice text={} voice file name={}".format(text, wavFile) - ) + + if sys.platform == 'win32': + self.engine.runAndWait() + else: + # In ubuntu, runAndWait do not really wait until the file created. + # It will return once the task queue is empty, but the task is still running in coroutine. + # And if you call runAndWait() and time.sleep() twice, it will stuck, so do not use this. + # If you want to fix this, add self._proxy.setBusy(True) in line 127 in espeak.py, at the beginning of the function save_to_file. + # self.engine.runAndWait() + + # Before espeak fix this problem, we iterate the generator and control the waiting by ourself. + # But this is not the canonical way to use it, for example if the file already exists it also cannot wait. + self.engine.iterate() + while self.engine.isBusy() or wavFileName not in os.listdir(TmpDir().path()): + time.sleep(0.1) + reply = Reply(ReplyType.VOICE, wavFile) + except Exception as e: reply = Reply(ReplyType.ERROR, str(e)) finally: