Browse Source

Merge Purll Request #920 into wechatmp

develop
lanvent 1 year ago
parent
commit
350633b69b
3 changed files with 106 additions and 71 deletions
  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 View File

@@ -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 View File

@@ -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 View File

@@ -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:


Loading…
Cancel
Save