@@ -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" | |||
@@ -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): # 线程异常结束时的回调函数 | |||
@@ -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: | |||