Просмотр исходного кода

Merge Purll Request #920 into wechatmp

lanvent 3 лет назад
Родитель
Сommit
350633b69b
3 измененных файлов с 106 добавлено и 71 удалено
  1. 45 26
      channel/wechatmp/passive_reply.py
  2. 28 35
      channel/wechatmp/wechatmp_channel.py
  3. 33 10
      voice/pytts/pytts_voice.py

+ 45 - 26
channel/wechatmp/passive_reply.py

@@ -25,21 +25,9 @@ class Query:
             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 @@ class Query:
                         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 @@ class Query:
                 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 @@ class Query:
 
                 # 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 @@ class Query:
                     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"

+ 28 - 35
channel/wechatmp/wechatmp_channel.py

@@ -4,22 +4,20 @@ import os
 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 @@ class WechatMPChannel(ChatChannel):
             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 @@ class WechatMPChannel(ChatChannel):
         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 @@ class WechatMPChannel(ChatChannel):
                     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 @@ class WechatMPChannel(ChatChannel):
                     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 @@ class WechatMPChannel(ChatChannel):
                         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 @@ class WechatMPChannel(ChatChannel):
                 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 @@ class WechatMPChannel(ChatChannel):
                 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):  # 线程异常结束时的回调函数

+ 33 - 10
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.log import logger
 from common.tmp_dir import TmpDir
 from voice.voice import Voice
 
-
 class PyttsVoice(Voice):
     engine = pyttsx3.init()
 
@@ -20,19 +20,42 @@ class PyttsVoice(Voice):
         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: