mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-03-19 13:28:11 +08:00
Merge Purll Request #920 into wechatmp
This commit is contained in:
@@ -25,21 +25,9 @@ class Query:
|
|||||||
message = web.data() # todo crypto
|
message = web.data() # todo crypto
|
||||||
msg = parse_message(message)
|
msg = parse_message(message)
|
||||||
logger.debug("[wechatmp] Receive post data:\n" + message.decode("utf-8"))
|
logger.debug("[wechatmp] Receive post data:\n" + message.decode("utf-8"))
|
||||||
if msg.type == "event":
|
|
||||||
logger.info(
|
if msg.type in ["text", "voice", "image"]:
|
||||||
"[wechatmp] Event {} from {}".format(
|
wechatmp_msg = WeChatMPMessage(msg, client=channel.client)
|
||||||
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]:
|
|
||||||
from_user = wechatmp_msg.from_user_id
|
from_user = wechatmp_msg.from_user_id
|
||||||
content = wechatmp_msg.content
|
content = wechatmp_msg.content
|
||||||
message_id = wechatmp_msg.msg_id
|
message_id = wechatmp_msg.msg_id
|
||||||
@@ -76,7 +64,7 @@ class Query:
|
|||||||
channel.running.add(from_user)
|
channel.running.add(from_user)
|
||||||
channel.produce(context)
|
channel.produce(context)
|
||||||
else:
|
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 or not supported:
|
||||||
if trigger_prefix:
|
if trigger_prefix:
|
||||||
reply_text = textwrap.dedent(
|
reply_text = textwrap.dedent(
|
||||||
@@ -107,13 +95,13 @@ class Query:
|
|||||||
request_cnt = channel.request_cnt.get(message_id, 0) + 1
|
request_cnt = channel.request_cnt.get(message_id, 0) + 1
|
||||||
channel.request_cnt[message_id] = request_cnt
|
channel.request_cnt[message_id] = request_cnt
|
||||||
logger.info(
|
logger.info(
|
||||||
"[wechatmp] Request {} from {} {}\n{}\n{}:{}".format(
|
"[wechatmp] Request {} from {} {} {}:{}\n{}".format(
|
||||||
request_cnt,
|
request_cnt,
|
||||||
from_user,
|
from_user,
|
||||||
message_id,
|
message_id,
|
||||||
content,
|
|
||||||
web.ctx.env.get("REMOTE_ADDR"),
|
web.ctx.env.get("REMOTE_ADDR"),
|
||||||
web.ctx.env.get("REMOTE_PORT"),
|
web.ctx.env.get("REMOTE_PORT"),
|
||||||
|
content
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -151,23 +139,23 @@ class Query:
|
|||||||
|
|
||||||
# Only one request can access to the cached data
|
# Only one request can access to the cached data
|
||||||
try:
|
try:
|
||||||
(reply_type, content) = channel.cache_dict.pop(from_user)
|
(reply_type, reply_content) = channel.cache_dict.pop(from_user)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
if (reply_type == "text"):
|
if (reply_type == "text"):
|
||||||
if len(content.encode("utf8")) <= MAX_UTF8_LEN:
|
if len(reply_content.encode("utf8")) <= MAX_UTF8_LEN:
|
||||||
reply_text = content
|
reply_text = reply_content
|
||||||
else:
|
else:
|
||||||
continue_text = "\n【未完待续,回复任意文字以继续】"
|
continue_text = "\n【未完待续,回复任意文字以继续】"
|
||||||
splits = split_string_by_utf8_length(
|
splits = split_string_by_utf8_length(
|
||||||
content,
|
reply_content,
|
||||||
MAX_UTF8_LEN - len(continue_text.encode("utf-8")),
|
MAX_UTF8_LEN - len(continue_text.encode("utf-8")),
|
||||||
max_split=1,
|
max_split=1,
|
||||||
)
|
)
|
||||||
reply_text = splits[0] + continue_text
|
reply_text = splits[0] + continue_text
|
||||||
channel.cache_dict[from_user] = ("text", splits[1])
|
channel.cache_dict[from_user] = ("text", splits[1])
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[wechatmp] Request {} do send to {} {}: {}\n{}".format(
|
"[wechatmp] Request {} do send to {} {}: {}\n{}".format(
|
||||||
request_cnt,
|
request_cnt,
|
||||||
@@ -180,20 +168,51 @@ class Query:
|
|||||||
replyPost = create_reply(reply_text, msg)
|
replyPost = create_reply(reply_text, msg)
|
||||||
return replyPost.render()
|
return replyPost.render()
|
||||||
|
|
||||||
|
|
||||||
elif (reply_type == "voice"):
|
elif (reply_type == "voice"):
|
||||||
media_id = content
|
media_id = reply_content
|
||||||
asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
|
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 = VoiceReply(message=msg)
|
||||||
replyPost.media_id = media_id
|
replyPost.media_id = media_id
|
||||||
return replyPost.render()
|
return replyPost.render()
|
||||||
|
|
||||||
elif (reply_type == "image"):
|
elif (reply_type == "image"):
|
||||||
media_id = content
|
media_id = reply_content
|
||||||
asyncio.run_coroutine_threadsafe(channel.delete_media(media_id), channel.delete_media_loop)
|
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 = ImageReply(message=msg)
|
||||||
replyPost.media_id = media_id
|
replyPost.media_id = media_id
|
||||||
return replyPost.render()
|
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:
|
else:
|
||||||
logger.info("暂且不处理")
|
logger.info("暂且不处理")
|
||||||
return "success"
|
return "success"
|
||||||
|
|||||||
@@ -4,22 +4,20 @@ import os
|
|||||||
import time
|
import time
|
||||||
import imghdr
|
import imghdr
|
||||||
import requests
|
import requests
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
from config import conf
|
||||||
from bridge.context import *
|
from bridge.context import *
|
||||||
from bridge.reply 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.log import logger
|
||||||
from common.singleton import singleton
|
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
|
from wechatpy.exceptions import WeChatClientException
|
||||||
import asyncio
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
import web
|
import web
|
||||||
|
|
||||||
from voice.audio_convert import any_to_mp3
|
|
||||||
# If using SSL, uncomment the following lines, and modify the certificate path.
|
# If using SSL, uncomment the following lines, and modify the certificate path.
|
||||||
# from cheroot.server import HTTPServer
|
# from cheroot.server import HTTPServer
|
||||||
# from cheroot.ssl.builtin import BuiltinSSLAdapter
|
# from cheroot.ssl.builtin import BuiltinSSLAdapter
|
||||||
@@ -46,7 +44,7 @@ class WechatMPChannel(ChatChannel):
|
|||||||
self.request_cnt = dict()
|
self.request_cnt = dict()
|
||||||
# The permanent media need to be deleted to avoid media number limit
|
# The permanent media need to be deleted to avoid media number limit
|
||||||
self.delete_media_loop = asyncio.new_event_loop()
|
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.setDaemon(True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
@@ -75,20 +73,26 @@ class WechatMPChannel(ChatChannel):
|
|||||||
if self.passive_reply:
|
if self.passive_reply:
|
||||||
if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
|
if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
|
||||||
reply_text = reply.content
|
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)
|
self.cache_dict[receiver] = ("text", reply_text)
|
||||||
elif reply.type == ReplyType.VOICE:
|
elif reply.type == ReplyType.VOICE:
|
||||||
try:
|
try:
|
||||||
file_path = reply.content
|
voice_file_path = reply.content
|
||||||
response = self.client.material.add("voice", open(file_path, "rb"))
|
with open(voice_file_path, 'rb') as f:
|
||||||
logger.debug("[wechatmp] upload voice response: {}".format(response))
|
# 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:
|
except WeChatClientException as e:
|
||||||
logger.error("[wechatmp] upload voice failed: {}".format(e))
|
logger.error("[wechatmp] upload voice failed: {}".format(e))
|
||||||
return
|
return
|
||||||
time.sleep(3) # todo check media_id
|
|
||||||
media_id = response["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)
|
self.cache_dict[receiver] = ("voice", media_id)
|
||||||
|
|
||||||
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
elif reply.type == ReplyType.IMAGE_URL: # 从网络下载图片
|
||||||
img_url = reply.content
|
img_url = reply.content
|
||||||
pic_res = requests.get(img_url, stream=True)
|
pic_res = requests.get(img_url, stream=True)
|
||||||
@@ -106,9 +110,7 @@ class WechatMPChannel(ChatChannel):
|
|||||||
logger.error("[wechatmp] upload image failed: {}".format(e))
|
logger.error("[wechatmp] upload image failed: {}".format(e))
|
||||||
return
|
return
|
||||||
media_id = response["media_id"]
|
media_id = response["media_id"]
|
||||||
logger.info(
|
logger.info("[wechatmp] image uploaded, receiver {}, media_id {}".format(receiver, media_id))
|
||||||
"[wechatmp] image reply url={}, receiver={}".format(img_url, receiver)
|
|
||||||
)
|
|
||||||
self.cache_dict[receiver] = ("image", media_id)
|
self.cache_dict[receiver] = ("image", media_id)
|
||||||
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
|
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
|
||||||
image_storage = reply.content
|
image_storage = reply.content
|
||||||
@@ -123,15 +125,13 @@ class WechatMPChannel(ChatChannel):
|
|||||||
logger.error("[wechatmp] upload image failed: {}".format(e))
|
logger.error("[wechatmp] upload image failed: {}".format(e))
|
||||||
return
|
return
|
||||||
media_id = response["media_id"]
|
media_id = response["media_id"]
|
||||||
logger.info(
|
logger.info("[wechatmp] image uploaded, receiver {}, media_id {}".format(receiver, media_id))
|
||||||
"[wechatmp] image reply url={}, receiver={}".format(img_url, receiver)
|
|
||||||
)
|
|
||||||
self.cache_dict[receiver] = ("image", media_id)
|
self.cache_dict[receiver] = ("image", media_id)
|
||||||
else:
|
else:
|
||||||
if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
|
if reply.type == ReplyType.TEXT or reply.type == ReplyType.INFO or reply.type == ReplyType.ERROR:
|
||||||
reply_text = reply.content
|
reply_text = reply.content
|
||||||
self.client.message.send_text(receiver, reply_text)
|
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:
|
elif reply.type == ReplyType.VOICE:
|
||||||
try:
|
try:
|
||||||
file_path = reply.content
|
file_path = reply.content
|
||||||
@@ -148,6 +148,7 @@ class WechatMPChannel(ChatChannel):
|
|||||||
file_name = os.path.basename(file_path)
|
file_name = os.path.basename(file_path)
|
||||||
file_type = "audio/mpeg"
|
file_type = "audio/mpeg"
|
||||||
logger.info("[wechatmp] file_name: {}, file_type: {} ".format(file_name, file_type))
|
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))
|
response = self.client.media.upload("voice", (file_name, open(file_path, "rb"), file_type))
|
||||||
logger.debug("[wechatmp] upload voice response: {}".format(response))
|
logger.debug("[wechatmp] upload voice response: {}".format(response))
|
||||||
except WeChatClientException as e:
|
except WeChatClientException as e:
|
||||||
@@ -171,12 +172,8 @@ class WechatMPChannel(ChatChannel):
|
|||||||
except WeChatClientException as e:
|
except WeChatClientException as e:
|
||||||
logger.error("[wechatmp] upload image failed: {}".format(e))
|
logger.error("[wechatmp] upload image failed: {}".format(e))
|
||||||
return
|
return
|
||||||
self.client.message.send_image(
|
self.client.message.send_image(receiver, response["media_id"])
|
||||||
receiver, response["media_id"]
|
logger.info("[wechatmp] Do send image to {}".format(receiver))
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
"[wechatmp] sendImage url={}, receiver={}".format(img_url, receiver)
|
|
||||||
)
|
|
||||||
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
|
elif reply.type == ReplyType.IMAGE: # 从文件读取图片
|
||||||
image_storage = reply.content
|
image_storage = reply.content
|
||||||
image_storage.seek(0)
|
image_storage.seek(0)
|
||||||
@@ -189,12 +186,8 @@ class WechatMPChannel(ChatChannel):
|
|||||||
except WeChatClientException as e:
|
except WeChatClientException as e:
|
||||||
logger.error("[wechatmp] upload image failed: {}".format(e))
|
logger.error("[wechatmp] upload image failed: {}".format(e))
|
||||||
return
|
return
|
||||||
self.client.message.send_image(
|
self.client.message.send_image(receiver, response["media_id"])
|
||||||
receiver, response["media_id"]
|
logger.info("[wechatmp] Do send image to {}".format(receiver))
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
"[wechatmp] sendImage, receiver={}".format(receiver)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数
|
def _success_callback(self, session_id, context, **kwargs): # 线程异常结束时的回调函数
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
pytts voice service (offline)
|
pytts voice service (offline)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pyttsx3
|
import pyttsx3
|
||||||
|
|
||||||
from bridge.reply import Reply, ReplyType
|
from bridge.reply import Reply, ReplyType
|
||||||
@@ -11,7 +12,6 @@ from common.log import logger
|
|||||||
from common.tmp_dir import TmpDir
|
from common.tmp_dir import TmpDir
|
||||||
from voice.voice import Voice
|
from voice.voice import Voice
|
||||||
|
|
||||||
|
|
||||||
class PyttsVoice(Voice):
|
class PyttsVoice(Voice):
|
||||||
engine = pyttsx3.init()
|
engine = pyttsx3.init()
|
||||||
|
|
||||||
@@ -20,19 +20,42 @@ class PyttsVoice(Voice):
|
|||||||
self.engine.setProperty("rate", 125)
|
self.engine.setProperty("rate", 125)
|
||||||
# 音量
|
# 音量
|
||||||
self.engine.setProperty("volume", 1.0)
|
self.engine.setProperty("volume", 1.0)
|
||||||
for voice in self.engine.getProperty("voices"):
|
if sys.platform == 'win32':
|
||||||
if "Chinese" in voice.name:
|
for voice in self.engine.getProperty("voices"):
|
||||||
self.engine.setProperty("voice", voice.id)
|
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):
|
def textToVoice(self, text):
|
||||||
try:
|
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.save_to_file(text, wavFile)
|
||||||
self.engine.runAndWait()
|
|
||||||
logger.info(
|
if sys.platform == 'win32':
|
||||||
"[Pytts] textToVoice text={} voice file name={}".format(text, wavFile)
|
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)
|
reply = Reply(ReplyType.VOICE, wavFile)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
reply = Reply(ReplyType.ERROR, str(e))
|
reply = Reply(ReplyType.ERROR, str(e))
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
Reference in New Issue
Block a user