Compare commits

..

37 Commits
1.0.5 ... 1.0.6

Author SHA1 Message Date
zhayujie
a660aa2133 Merge pull request #549 from a5225662/master
明确config.py中config.json的查找目录为当前目录 #547
2023-03-23 01:08:41 +08:00
a5225662
5e48dd50ac 明确config.py中config.json的查找目录为当前目录 #547 2023-03-23 00:36:41 +08:00
zhayujie
2d3ffa1738 Merge pull request #539 from lichengzhe/master
增加限速配置参数文档说明
2023-03-21 23:00:04 +08:00
李成喆
663967680a Merge branch 'zhayujie:master' into master 2023-03-21 22:57:52 +08:00
lichengzhe
b190db73dc -增加限速配置参数文档说明 2023-03-21 22:56:30 +08:00
zhayujie
475d2f7911 Merge pull request #520 from B1gM8c/master
支持Wechaty的自定义前缀+关键词生成AI图片的功能
2023-03-21 22:52:39 +08:00
zhayujie
a1323c9de8 Merge pull request #527 from goldfishh/master
feature(rate-limit): 新增令牌桶类,用于主动限制调用gpt3.5, dalle接口频率
2023-03-21 22:36:22 +08:00
zhayujie
260c374a56 Merge pull request #537 from lichengzhe/master
如启用hot_reload,不处理1分钟前的历史消息避免重复提交
2023-03-21 22:34:37 +08:00
lichengzhe
3d264207a8 如启用hot_reload,不处理1分钟前的历史消息避免重复提交 2023-03-21 22:12:06 +08:00
goldfishh
3f889ab75f feature(rate-limit): 新增令牌桶类,用于主动限制调用gpt3.5, dalle接口频率 2023-03-20 22:18:10 +08:00
B1gM8c
852adb72a2 支持Wechaty的自定义前缀+关键词生成AI图片的功能
Wechaty判断is_at为True,返回的内容是过滤掉@之后的内容;而is_at为False,则会返回完整的内容

故判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容,用于实现类似自定义+前缀触发生成AI图片的功能
2023-03-20 01:17:29 +08:00
zhayujie
cfd423c991 Merge pull request #511 from lichengzhe/master
itchat增加hot_reload特性开关,默认关闭。配置文档增加可选参数说明。
2023-03-19 10:42:25 +08:00
lichengzhe
021ee2312e 恢复默认config-template.json 2023-03-19 09:11:36 +08:00
李成喆
0f830f2317 Merge branch 'zhayujie:master' into master 2023-03-19 08:35:26 +08:00
lichengzhe
3ef7855384 itchat增加hot_reload特性开关,默认关闭。配置文档增加可选参数说明。 2023-03-19 08:29:25 +08:00
zhayujie
d760b045d5 fix: close hot reload because of repeat msg 2023-03-19 01:26:53 +08:00
zhayujie
53cc1df369 Merge pull request #507 from lichengzhe/master
清除记忆命令和API调用参数改为config.json配置项
2023-03-19 01:13:36 +08:00
lichengzhe
9b2da6c431 清除记忆命令和API调用参数改为config.json配置项 2023-03-19 01:10:27 +08:00
zhayujie
b3e1f56fb9 feat: itchat login hot reload 2023-03-19 01:09:36 +08:00
zhayujie
1aa2382843 docs: update issue template 2023-03-16 22:28:32 +08:00
zhayujie
3c04325aae feat: add config for model selection #471 2023-03-15 23:27:51 +08:00
zhayujie
b404e2c51f docs: update README.md 2023-03-15 22:26:32 +08:00
zhayujie
5b0f0e8b6c Merge pull request #476 from Chiaki-Chan/master
1.新增wechaty方案的语音识别、语音回复功能;2.更新README;
2023-03-15 19:44:46 +08:00
Chiaki
f9b0ad7697 1.新增wechaty方案的语音识别、语音回复功能;2.更新README; 2023-03-15 13:56:23 +08:00
zhayujie
224ee6bd89 fix: openai_base_url load 2023-03-15 12:57:34 +08:00
zhayujie
1dc39af423 Merge pull request #465 from B1gM8c/master
支持自定义openai_api_base
2023-03-15 00:24:04 +08:00
B1gM8c
2c8da59b47 支持自定义openai_api_base
支持自定义openai_api_base

解决国内API被墙的问题,可以自定义使用自己的中转API
2023-03-15 00:14:39 +08:00
zhayujie
9e3a5395c7 Merge pull request #452 from limccn/feature/docker-support-voice-reply
feat: container support voice reply
2023-03-14 00:20:51 +08:00
zhayujie
54290f7e5d Merge pull request #451 from limccn/feature/docker-support-voice-recognition
feat: container support speech recognition
2023-03-14 00:20:19 +08:00
limccn
1bb5c6dc0d feat: container support voice reply 2023-03-13 16:17:54 +08:00
limccn
b204d305a1 feat: container support speech recognition 2023-03-13 16:07:19 +08:00
zhayujie
8fa4041fc2 fix: variable name compatibility modification #415 2023-03-10 09:25:56 +08:00
zhayujie
8107165792 fix: variable name compatibility modification 2023-03-10 09:23:58 +08:00
zhayujie
fc4912c640 docs: update README.md 2023-03-10 00:57:00 +08:00
zhayujie
36ed9d02b7 Merge pull request #417 from goldfishh/master
feature: 消息控制配置热更新
2023-03-10 00:16:59 +08:00
goldfishh
d6c92e1fd5 feature: 消息控制配置热更新 2023-03-09 23:13:53 +08:00
zhayujie
4ccad86010 docs: temporarily remove the config in template
It will be described later in the document as an optional configuration
2023-03-09 02:01:22 +08:00
24 changed files with 469 additions and 55 deletions

View File

@@ -1,6 +1,6 @@
### 前置确认
1. 网络能够访问openai接口 [#351](https://github.com/zhayujie/chatgpt-on-wechat/issues/351)
1. 网络能够访问openai接口
2. python 已安装:版本在 3.7 ~ 3.10 之间,依赖已安装
3. 在已有 issue 中未搜索到类似问题
4. [FAQS](https://github.com/zhayujie/chatgpt-on-wechat/wiki/FAQs) 中无类似问题

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.DS_Store
.idea
.wechaty/
__pycache__/
venv*
*.pyc

View File

@@ -15,11 +15,11 @@
# 更新日志
>**2023.03.09** 基于 `whisper API` 实现对微信语音消息的解析和回复,添加配置项 `"speech_recognition":true` 即可启用。(contributed by [wanggang1987](https://github.com/wanggang1987) in [#385](https://github.com/zhayujie/chatgpt-on-wechat/pull/385))
>**2023.03.09** 基于 `whisper API` 实现对微信语音消息的解析和回复,添加配置项 `"speech_recognition":true` 即可启用,使用参考 [#415](https://github.com/zhayujie/chatgpt-on-wechat/issues/415)。(contributed by [wanggang1987](https://github.com/wanggang1987) in [#385](https://github.com/zhayujie/chatgpt-on-wechat/pull/385))
>**2023.03.02** 接入[ChatGPT API](https://platform.openai.com/docs/guides/chat) (gpt-3.5-turbo)默认使用该模型进行对话需升级openai依赖 (`pip3 install --upgrade openai`)。网络问题参考 [#351](https://github.com/zhayujie/chatgpt-on-wechat/issues/351)
>**2023.02.20** 增加 [python-wechaty](https://github.com/wechaty/python-wechaty) 作为可选渠道使用Pad协议相对稳定但Token收费 (使用参考[#244](https://github.com/zhayujie/chatgpt-on-wechat/pull/244)contributed by [ZQ7](https://github.com/ZQ7))
>**2023.02.20** 增加 [python-wechaty](https://github.com/wechaty/python-wechaty) 作为可选渠道使用Pad协议但Token收费 (使用参考[#244](https://github.com/zhayujie/chatgpt-on-wechat/pull/244)contributed by [ZQ7](https://github.com/ZQ7))
>**2023.02.09** 扫码登录存在封号风险,请谨慎使用,参考[#58](https://github.com/AutumnWhj/ChatGPT-wechat-bot/issues/158)
@@ -62,15 +62,14 @@
支持 Linux、MacOS、Windows 系统可在Linux服务器上长期运行),同时需安装 `Python`
> 建议Python版本在 3.7.1~3.9.X 之间3.10及以上版本在 MacOS 可用,其他系统上不确定能否正常运行。
1.克隆项目代码:
**(1) 克隆项目代码:**
```bash
git clone https://github.com/zhayujie/chatgpt-on-wechat
cd chatgpt-on-wechat/
```
2.安装所需核心依赖:
**(2) 安装核心依赖 (必选)**
```bash
pip3 install itchat-uos==1.5.0.dev0
@@ -78,13 +77,17 @@ pip3 install --upgrade openai
```
注:`itchat-uos`使用指定版本1.5.0.dev0`openai`使用最新版本需高于0.27.0。
**(3) 拓展依赖 (可选)**
语音识别及语音回复相关依赖:[#415](https://github.com/zhayujie/chatgpt-on-wechat/issues/415)。
## 配置
配置文件的模板在根目录的`config-template.json`中,需复制该模板创建最终生效的 `config.json` 文件:
```bash
cp config-template.json config.json
cp config-template.json config.json
```
然后在`config.json`中填入配置,以下是对默认配置的说明,可根据需要进行自定义修改:
@@ -93,6 +96,7 @@ cp config-template.json config.json
# config.json文件内容示例
{
"open_ai_api_key": "YOUR API KEY", # 填入上面创建的 OpenAI API KEY
"model": "gpt-3.5-turbo", # 模型名称
"proxy": "127.0.0.1:7890", # 代理客户端的ip和端口
"single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复
"single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人
@@ -100,7 +104,8 @@ cp config-template.json config.json
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"], # 开启自动回复的群名称列表
"image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀
"conversation_max_tokens": 1000, # 支持上下文记忆的最多字符数
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。" # 人格描述
"speech_recognition": false, # 是否开启语音识别
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", # 人格描述
}
```
**配置说明:**
@@ -117,15 +122,21 @@ cp config-template.json config.json
+ 可选配置: `group_name_keyword_white_list`配置项支持模糊匹配群名称,`group_chat_keyword`配置项则支持模糊匹配群消息内容用法与上述两个配置项相同。Contributed by [evolay](https://github.com/evolay))
**3.语音识别**
+ 添加 `"speech_recognition": true` 将开启语音识别默认使用openai的whisper模型识别为文字同时以文字回复目前只支持私聊 (注意由于语音消息无法匹配前缀,一旦开启将对所有语音自动回复)
+ 添加 `"voice_reply_voice": true` 将开启语音回复语音但是需要配置对应语音合成平台的key由于itchat协议的限制只能发送语音mp3文件若使用wechaty则回复的是微信语音。
**4.其他配置**
+ `model`: 模型名称,目前支持 `gpt-3.5-turbo`, `text-davinci-003`, `gpt-4`, `gpt-4-32k` (其中gpt-4 api暂未开放)
+ `temperature`,`frequency_penalty`,`presence_penalty`: Chat API接口参数详情参考[OpenAI官方文档。](https://platform.openai.com/docs/api-reference/chat)
+ `proxy`:由于目前 `openai` 接口国内无法访问,需配置代理客户端的地址,详情参考 [#351](https://github.com/zhayujie/chatgpt-on-wechat/issues/351)
+ 对于图像生成,在满足个人或群组触发条件外,还需要额外的关键词前缀来触发,对应配置 `image_create_prefix `
+ 关于OpenAI对话及图片接口的参数配置内容自由度、回复字数限制、图片大小等可以参考 [对话接口](https://beta.openai.com/docs/api-reference/completions) 和 [图像接口](https://beta.openai.com/docs/api-reference/completions) 文档直接在 [代码](https://github.com/zhayujie/chatgpt-on-wechat/blob/master/bot/openai/open_ai_bot.py) `bot/openai/open_ai_bot.py` 中进行调整。
+ `conversation_max_tokens`:表示能够记忆的上下文最大字数(一问一答为一组对话,如果累积的对话字数超出限制,就会优先移除最早的一组对话)
+ `rate_limit_chatgpt``rate_limit_dalle`:每分钟最高问答速率、画图速率,超速后排队按序处理。
+ `clear_memory_commands`: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。
+ `hot_reload`: 程序退出后,暂存微信扫码状态,默认关闭。
+ `character_desc` 配置中保存着你对机器人说的一段话,他会记住这段话并作为他的设定,你可以为他定制任何人格 (关于会话上下文的更多内容参考该 [issue](https://github.com/zhayujie/chatgpt-on-wechat/issues/43))
@@ -149,8 +160,7 @@ python3 app.py
touch nohup.out # 首次运行需要新建日志文件
nohup python3 app.py & tail -f nohup.out # 在后台运行程序并通过日志输出二维码
```
扫码登录后程序即可运行于服务器后台,此时可通过 `ctrl+c` 关闭日志,不会影响后台程序的运行。使用 `ps -ef | grep app.py | grep -v grep` 命令可查看运行于后台的进程,如果想要重新启动程序可以先 `kill` 掉对应的进程。日志关闭后如果想要再次打开只需输入 `tail -f nohup.out`
scripts/目录有相应的脚本可以调用
扫码登录后程序即可运行于服务器后台,此时可通过 `ctrl+c` 关闭日志,不会影响后台程序的运行。使用 `ps -ef | grep app.py | grep -v grep` 命令可查看运行于后台的进程,如果想要重新启动程序可以先 `kill` 掉对应的进程。日志关闭后如果想要再次打开只需输入 `tail -f nohup.out`此外,`scripts` 目录下有一键运行、关闭程序的脚本供使用。
> **注意:** 如果 扫码后手机提示登录验证需要等待5s而终端的二维码再次刷新并提示 `Log in time out, reloading QR code`,此时需参考此 [issue](https://github.com/zhayujie/chatgpt-on-wechat/issues/8) 修改一行代码即可解决。

View File

@@ -1,6 +1,7 @@
"""
channel factory
"""
from common import const
def create_bot(bot_type):
@@ -9,17 +10,17 @@ def create_bot(bot_type):
:param channel_type: channel type code
:return: channel instance
"""
if bot_type == 'baidu':
if bot_type == const.BAIDU:
# Baidu Unit对话接口
from bot.baidu.baidu_unit_bot import BaiduUnitBot
return BaiduUnitBot()
elif bot_type == 'chatGPT':
elif bot_type == const.CHATGPT:
# ChatGPT 网页端web接口
from bot.chatgpt.chat_gpt_bot import ChatGPTBot
return ChatGPTBot()
elif bot_type == 'openAI':
elif bot_type == const.OPEN_AI:
# OpenAI 官方对话模型API
from bot.openai.open_ai_bot import OpenAIBot
return OpenAIBot()

View File

@@ -1,8 +1,9 @@
# encoding:utf-8
from bot.bot import Bot
from config import conf
from config import conf, load_config
from common.log import logger
from common.token_bucket import TokenBucket
from common.expired_dict import ExpiredDict
import openai
import time
@@ -16,21 +17,31 @@ else:
class ChatGPTBot(Bot):
def __init__(self):
openai.api_key = conf().get('open_ai_api_key')
if conf().get('open_ai_api_base'):
openai.api_base = conf().get('open_ai_api_base')
proxy = conf().get('proxy')
if proxy:
openai.proxy = proxy
if conf().get('rate_limit_chatgpt'):
self.tb4chatgpt = TokenBucket(conf().get('rate_limit_chatgpt', 20))
if conf().get('rate_limit_dalle'):
self.tb4dalle = TokenBucket(conf().get('rate_limit_dalle', 50))
def reply(self, query, context=None):
# acquire reply content
if not context or not context.get('type') or context.get('type') == 'TEXT':
logger.info("[OPEN_AI] query={}".format(query))
session_id = context['session_id']
if query == '#清除记忆':
session_id = context.get('session_id') or context.get('from_user_id')
clear_memory_commands = conf().get('clear_memory_commands', ['#清除记忆'])
if query in clear_memory_commands:
Session.clear_session(session_id)
return '记忆已清除'
elif query == '#清除所有':
Session.clear_all_session()
return '所有人记忆已清除'
return '所有人记忆已清除'
elif query == '#更新配置':
load_config()
return '配置已更新'
session = Session.build_session_query(query, session_id)
logger.debug("[OPEN_AI] session query={}".format(session))
@@ -57,14 +68,16 @@ class ChatGPTBot(Bot):
:return: {}
'''
try:
if conf().get('rate_limit_chatgpt') and not self.tb4chatgpt.get_token():
return {"completion_tokens": 0, "content": "提问太快啦,请休息一下再问我吧"}
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo", # 对话模型的名称
model= conf().get("model") or "gpt-3.5-turbo", # 对话模型的名称
messages=session,
temperature=0.9, # 值在[0,1]之间,越大表示回复越具有不确定性
temperature=conf().get('temperature', 0.9), # 值在[0,1]之间,越大表示回复越具有不确定性
#max_tokens=4096, # 回复最大的字符数
top_p=1,
frequency_penalty=0.0, # [-2,2]之间,该值越大则更倾向于产生不同的内容
presence_penalty=0.0, # [-2,2]之间,该值越大则更倾向于产生不同的内容
frequency_penalty=conf().get('frequency_penalty', 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容
presence_penalty=conf().get('presence_penalty', 0.0), # [-2,2]之间,该值越大则更倾向于产生不同的内容
)
# logger.info("[ChatGPT] reply={}, total_tokens={}".format(response.choices[0]['message']['content'], response["usage"]["total_tokens"]))
return {"total_tokens": response["usage"]["total_tokens"],
@@ -96,6 +109,8 @@ class ChatGPTBot(Bot):
def create_img(self, query, retry_count=0):
try:
if conf().get('rate_limit_dalle') and not self.tb4dalle.get_token():
return "请求太快了,请休息一下再问我吧"
logger.info("[OPEN_AI] image_query={}".format(query))
response = openai.Image.create(
prompt=query, #图片描述
@@ -112,7 +127,7 @@ class ChatGPTBot(Bot):
logger.warn("[OPEN_AI] ImgCreate RateLimit exceed, 第{}次重试".format(retry_count+1))
return self.create_img(query, retry_count+1)
else:
return "提问太快啦,请休息一下再问我吧"
return "请求太快啦,请休息一下再问我吧"
except Exception as e:
logger.exception(e)
return None

View File

@@ -12,13 +12,17 @@ user_session = dict()
class OpenAIBot(Bot):
def __init__(self):
openai.api_key = conf().get('open_ai_api_key')
if conf().get('open_ai_api_base'):
openai.api_base = conf().get('open_ai_api_base')
proxy = conf().get('proxy')
if proxy:
openai.proxy = proxy
def reply(self, query, context=None):
# acquire reply content
if not context or not context.get('type') or context.get('type') == 'TEXT':
logger.info("[OPEN_AI] query={}".format(query))
from_user_id = context['from_user_id']
from_user_id = context.get('from_user_id') or context.get('session_id')
if query == '#清除记忆':
Session.clear_session(from_user_id)
return '记忆已清除'
@@ -41,7 +45,7 @@ class OpenAIBot(Bot):
def reply_text(self, query, user_id, retry_count=0):
try:
response = openai.Completion.create(
model="text-davinci-003", # 对话模型的名称
model= conf().get("model") or "text-davinci-003", # 对话模型的名称
prompt=query,
temperature=0.9, # 值在[0,1]之间,越大表示回复越具有不确定性
max_tokens=1200, # 回复最大的字符数
@@ -163,4 +167,4 @@ class Session(object):
@staticmethod
def clear_all_session():
user_session.clear()
user_session.clear()

View File

@@ -1,5 +1,7 @@
from bot import bot_factory
from voice import voice_factory
from config import conf
from common import const
class Bridge(object):
@@ -7,7 +9,13 @@ class Bridge(object):
pass
def fetch_reply_content(self, query, context):
return bot_factory.create_bot("chatGPT").reply(query, context)
bot_type = const.CHATGPT
model_type = conf().get("model")
if model_type in ["gpt-3.5-turbo", "gpt-4", "gpt-4-32k"]:
bot_type = const.CHATGPT
elif model_type in ["text-davinci-003"]:
bot_type = const.OPEN_AI
return bot_factory.create_bot(bot_type).reply(query, context)
def fetch_voice_to_text(self, voiceFile):
return voice_factory.create_voice("openai").voiceToText(voiceFile)

View File

@@ -14,4 +14,7 @@ def create_channel(channel_type):
elif channel_type == 'wxy':
from channel.wechat.wechaty_channel import WechatyChannel
return WechatyChannel()
elif channel_type == 'terminal':
from channel.terminal.terminal_channel import TerminalChannel
return TerminalChannel()
raise RuntimeError

View File

@@ -0,0 +1,29 @@
from channel.channel import Channel
import sys
class TerminalChannel(Channel):
def startup(self):
context = {"from_user_id": "User"}
print("\nPlease input your question")
while True:
try:
prompt = self.get_input("User:\n")
except KeyboardInterrupt:
print("\nExiting...")
sys.exit()
print("Bot:")
sys.stdout.flush()
for res in super().build_reply_content(prompt, context):
print(res, end="")
sys.stdout.flush()
print("\n")
def get_input(self, prompt):
"""
Multi-line input function
"""
print(prompt, end="")
line = input()
return line

View File

@@ -14,6 +14,7 @@ from common.tmp_dir import TmpDir
from config import conf
import requests
import io
import time
thread_pool = ThreadPoolExecutor(max_workers=8)
@@ -42,7 +43,7 @@ class WechatChannel(Channel):
def startup(self):
# login by scan QRCode
itchat.auto_login(enableCmdQR=2)
itchat.auto_login(enableCmdQR=2, hotReload=conf().get('hot_reload', False))
# start message listener
itchat.run()
@@ -74,7 +75,11 @@ class WechatChannel(Channel):
from_user_id = msg['FromUserName']
to_user_id = msg['ToUserName'] # 接收人id
other_user_id = msg['User']['UserName'] # 对手方id
create_time = msg['CreateTime'] # 消息时间
match_prefix = self.check_prefix(content, conf().get('single_chat_prefix'))
if conf().get('hot_reload') == True and int(create_time) < int(time.time()) - 60: #跳过1分钟前的历史消息
logger.debug("[WX]history message skipped")
return
if "\n- - - - - - - - - - - - - - -" in content:
logger.debug("[WX]reference query skipped")
return
@@ -108,6 +113,10 @@ class WechatChannel(Channel):
logger.debug("[WX]receive group msg: " + json.dumps(msg, ensure_ascii=False))
group_name = msg['User'].get('NickName', None)
group_id = msg['User'].get('UserName', None)
create_time = msg['CreateTime'] # 消息时间
if conf().get('hot_reload') == True and int(create_time) < int(time.time()) - 60: #跳过1分钟前的历史消息
logger.debug("[WX]history group message skipped")
return
if not group_name:
return ""
origin_content = msg['Content']

View File

@@ -10,12 +10,16 @@ import json
import time
import asyncio
import requests
import pysilk
import wave
from pydub import AudioSegment
from typing import Optional, Union
from wechaty_puppet import MessageType, FileBox, ScanStatus # type: ignore
from wechaty import Wechaty, Contact
from wechaty.user import Message, Room, MiniProgram, UrlLink
from channel.channel import Channel
from common.log import logger
from common.tmp_dir import TmpDir
from config import conf
@@ -89,6 +93,48 @@ class WechatyChannel(Channel):
await self._do_send_img(content, to_user_id)
else:
await self._do_send(content, to_user_id)
elif room is None and msg.type() == MessageType.MESSAGE_TYPE_AUDIO:
if not msg.is_self(): # 接收语音消息
# 下载语音文件
voice_file = await msg.to_file_box()
silk_file = TmpDir().path() + voice_file.name
await voice_file.to_file(silk_file)
logger.info("[WX]receive voice file: " + silk_file)
# 将文件转成wav格式音频
wav_file = silk_file.replace(".slk", ".wav")
with open(silk_file, 'rb') as f:
silk_data = f.read()
pcm_data = pysilk.decode(silk_data)
with wave.open(wav_file, 'wb') as wav_data:
wav_data.setnchannels(1)
wav_data.setsampwidth(2)
wav_data.setframerate(24000)
wav_data.writeframes(pcm_data)
if os.path.exists(wav_file):
converter_state = "true" # 转换wav成功
else:
converter_state = "false" # 转换wav失败
logger.info("[WX]receive voice converter: " + converter_state)
# 语音识别为文本
query = super().build_voice_to_text(wav_file)
# 交验关键字
match_prefix = self.check_prefix(query, conf().get('single_chat_prefix'))
if match_prefix is not None:
if match_prefix != '':
str_list = query.split(match_prefix, 1)
if len(str_list) == 2:
query = str_list[1].strip()
# 返回消息
if conf().get('voice_reply_voice'):
await self._do_send_voice(query, from_user_id)
else:
await self._do_send(query, from_user_id)
else:
logger.info("[WX]receive voice check prefix: " + 'False')
# 清除缓存文件
os.remove(wav_file)
os.remove(silk_file)
elif room and msg.type() == MessageType.MESSAGE_TYPE_TEXT:
# 群组&文本消息
room_id = room.room_id
@@ -101,6 +147,13 @@ class WechatyChannel(Channel):
match_prefix = (is_at and not config.get("group_at_off", False)) \
or self.check_prefix(content, config.get('group_chat_prefix')) \
or self.check_contain(content, config.get('group_chat_keyword'))
# Wechaty判断is_at为True返回的内容是过滤掉@之后的内容而is_at为False则会返回完整的内容
# 故判断如果匹配到自定义前缀,则返回过滤掉前缀+空格后的内容,用于实现类似自定义+前缀触发生成AI图片的功能
prefixes = config.get('group_chat_prefix')
for prefix in prefixes:
if content.startswith(prefix):
content = content.replace(prefix, '', 1).strip()
break
if ('ALL_GROUP' in config.get('group_name_white_list') or room_name in config.get(
'group_name_white_list') or self.check_contain(room_name, config.get(
'group_name_keyword_white_list'))) and match_prefix:
@@ -135,6 +188,39 @@ class WechatyChannel(Channel):
except Exception as e:
logger.exception(e)
async def _do_send_voice(self, query, reply_user_id):
try:
if not query:
return
context = dict()
context['session_id'] = reply_user_id
reply_text = super().build_reply_content(query, context)
if reply_text:
# 转换 mp3 文件为 silk 格式
mp3_file = super().build_text_to_voice(reply_text)
silk_file = mp3_file.replace(".mp3", ".silk")
# Load the MP3 file
audio = AudioSegment.from_file(mp3_file, format="mp3")
# Convert to WAV format
audio = audio.set_frame_rate(24000).set_channels(1)
wav_data = audio.raw_data
sample_width = audio.sample_width
# Encode to SILK format
silk_data = pysilk.encode(wav_data, 24000)
# Save the silk file
with open(silk_file, "wb") as f:
f.write(silk_data)
# 发送语音
t = int(time.time())
file_box = FileBox.from_file(silk_file, name=str(t) + '.silk')
await self.send(file_box, reply_user_id)
# 清除缓存文件
os.remove(mp3_file)
os.remove(silk_file)
except Exception as e:
logger.exception(e)
async def _do_send_img(self, query, reply_user_id):
try:
if not query:

4
common/const.py Normal file
View File

@@ -0,0 +1,4 @@
# bot_type
OPEN_AI = "openAI"
CHATGPT = "chatGPT"
BAIDU = "baidu"

45
common/token_bucket.py Normal file
View File

@@ -0,0 +1,45 @@
import threading
import time
class TokenBucket:
def __init__(self, tpm, timeout=None):
self.capacity = int(tpm) # 令牌桶容量
self.tokens = 0 # 初始令牌数为0
self.rate = int(tpm) / 60 # 令牌每秒生成速率
self.timeout = timeout # 等待令牌超时时间
self.cond = threading.Condition() # 条件变量
self.is_running = True
# 开启令牌生成线程
threading.Thread(target=self._generate_tokens).start()
def _generate_tokens(self):
"""生成令牌"""
while self.is_running:
with self.cond:
if self.tokens < self.capacity:
self.tokens += 1
self.cond.notify() # 通知获取令牌的线程
time.sleep(1 / self.rate)
def get_token(self):
"""获取令牌"""
with self.cond:
while self.tokens <= 0:
flag = self.cond.wait(self.timeout)
if not flag: # 超时
return False
self.tokens -= 1
return True
def close(self):
self.is_running = False
if __name__ == "__main__":
token_bucket = TokenBucket(20, None) # 创建一个每分钟生产20个tokens的令牌桶
# token_bucket = TokenBucket(20, 0.1)
for i in range(3):
if token_bucket.get_token():
print(f"{i+1}次请求成功")
token_bucket.close()

View File

@@ -1,14 +1,16 @@
{
"open_ai_api_key": "YOUR API KEY",
"proxy": "",
"single_chat_prefix": ["bot", "@bot"],
"single_chat_reply_prefix": "[bot] ",
"group_chat_prefix": ["@bot"],
"group_chat_in_one_session": ["ChatGPT测试群"],
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"],
"image_create_prefix": ["画", "看", "找"],
"conversation_max_tokens": 1000,
"speech_recognition": false,
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。",
"expires_in_seconds": 3600
}
{
"open_ai_api_key": "YOUR API KEY",
"model": "gpt-3.5-turbo",
"proxy": "",
"single_chat_prefix": ["bot", "@bot"],
"single_chat_reply_prefix": "[bot] ",
"group_chat_prefix": ["@bot"],
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"],
"image_create_prefix": ["画", "看", "找"],
"speech_recognition": false,
"voice_reply_voice": false,
"conversation_max_tokens": 1000,
"expires_in_seconds": 3600,
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。"
}

View File

@@ -9,7 +9,7 @@ config = {}
def load_config():
global config
config_path = "config.json"
config_path = "./config.json"
if not os.path.exists(config_path):
raise Exception('配置文件不存在请根据config-template.json模板创建config.json文件')

View File

@@ -11,5 +11,6 @@ docker build -f Dockerfile.alpine \
-t zhayujie/chatgpt-on-wechat .
# tag image
docker tag zhayujie/chatgpt-on-wechat zhayujie/chatgpt-on-wechat:$CHATGPT_ON_WECHAT_TAG-alpine
docker tag zhayujie/chatgpt-on-wechat zhayujie/chatgpt-on-wechat:alpine
docker tag zhayujie/chatgpt-on-wechat zhayujie/chatgpt-on-wechat:$CHATGPT_ON_WECHAT_TAG-alpine

View File

@@ -11,4 +11,5 @@ docker build -f Dockerfile.debian \
-t zhayujie/chatgpt-on-wechat .
# tag image
docker tag zhayujie/chatgpt-on-wechat zhayujie/chatgpt-on-wechat:debian
docker tag zhayujie/chatgpt-on-wechat zhayujie/chatgpt-on-wechat:$CHATGPT_ON_WECHAT_TAG-debian

View File

@@ -0,0 +1,23 @@
FROM zhayujie/chatgpt-on-wechat:alpine
LABEL maintainer="foo@bar.com"
ARG TZ='Asia/Shanghai'
USER root
RUN apk add --no-cache \
ffmpeg \
espeak \
&& pip install --no-cache \
baidu-aip \
chardet \
SpeechRecognition
# replace entrypoint
ADD ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
USER noroot
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,24 @@
FROM zhayujie/chatgpt-on-wechat:debian
LABEL maintainer="foo@bar.com"
ARG TZ='Asia/Shanghai'
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
espeak \
&& pip install --no-cache \
baidu-aip \
chardet \
SpeechRecognition
# replace entrypoint
ADD ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
USER noroot
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,24 @@
version: '2.0'
services:
chatgpt-on-wechat:
build:
context: ./
dockerfile: Dockerfile.alpine
image: zhayujie/chatgpt-on-wechat-voice-reply
container_name: chatgpt-on-wechat-voice-reply
environment:
OPEN_AI_API_KEY: 'YOUR API KEY'
OPEN_AI_PROXY: ''
SINGLE_CHAT_PREFIX: '["bot", "@bot"]'
SINGLE_CHAT_REPLY_PREFIX: '"[bot] "'
GROUP_CHAT_PREFIX: '["@bot"]'
GROUP_NAME_WHITE_LIST: '["ChatGPT测试群", "ChatGPT测试群2"]'
IMAGE_CREATE_PREFIX: '["画", "看", "找"]'
CONVERSATION_MAX_TOKENS: 1000
SPEECH_RECOGNITION: 'true'
CHARACTER_DESC: '你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。'
EXPIRES_IN_SECONDS: 3600
VOICE_REPLY_VOICE: 'true'
BAIDU_APP_ID: 'YOUR BAIDU APP ID'
BAIDU_API_KEY: 'YOUR BAIDU API KEY'
BAIDU_SECRET_KEY: 'YOUR BAIDU SERVICE KEY'

View File

@@ -0,0 +1,117 @@
#!/bin/bash
set -e
# build prefix
CHATGPT_ON_WECHAT_PREFIX=${CHATGPT_ON_WECHAT_PREFIX:-""}
# path to config.json
CHATGPT_ON_WECHAT_CONFIG_PATH=${CHATGPT_ON_WECHAT_CONFIG_PATH:-""}
# execution command line
CHATGPT_ON_WECHAT_EXEC=${CHATGPT_ON_WECHAT_EXEC:-""}
OPEN_AI_API_KEY=${OPEN_AI_API_KEY:-""}
OPEN_AI_PROXY=${OPEN_AI_PROXY:-""}
SINGLE_CHAT_PREFIX=${SINGLE_CHAT_PREFIX:-""}
SINGLE_CHAT_REPLY_PREFIX=${SINGLE_CHAT_REPLY_PREFIX:-""}
GROUP_CHAT_PREFIX=${GROUP_CHAT_PREFIX:-""}
GROUP_NAME_WHITE_LIST=${GROUP_NAME_WHITE_LIST:-""}
IMAGE_CREATE_PREFIX=${IMAGE_CREATE_PREFIX:-""}
CONVERSATION_MAX_TOKENS=${CONVERSATION_MAX_TOKENS:-""}
SPEECH_RECOGNITION=${SPEECH_RECOGNITION:-""}
CHARACTER_DESC=${CHARACTER_DESC:-""}
EXPIRES_IN_SECONDS=${EXPIRES_IN_SECONDS:-""}
VOICE_REPLY_VOICE=${VOICE_REPLY_VOICE:-""}
BAIDU_APP_ID=${BAIDU_APP_ID:-""}
BAIDU_API_KEY=${BAIDU_API_KEY:-""}
BAIDU_SECRET_KEY=${BAIDU_SECRET_KEY:-""}
# CHATGPT_ON_WECHAT_PREFIX is empty, use /app
if [ "$CHATGPT_ON_WECHAT_PREFIX" == "" ] ; then
CHATGPT_ON_WECHAT_PREFIX=/app
fi
# CHATGPT_ON_WECHAT_CONFIG_PATH is empty, use '/app/config.json'
if [ "$CHATGPT_ON_WECHAT_CONFIG_PATH" == "" ] ; then
CHATGPT_ON_WECHAT_CONFIG_PATH=$CHATGPT_ON_WECHAT_PREFIX/config.json
fi
# CHATGPT_ON_WECHAT_EXEC is empty, use python app.py
if [ "$CHATGPT_ON_WECHAT_EXEC" == "" ] ; then
CHATGPT_ON_WECHAT_EXEC="python app.py"
fi
# modify content in config.json
if [ "$OPEN_AI_API_KEY" != "" ] ; then
sed -i "s/\"open_ai_api_key\".*,$/\"open_ai_api_key\": \"$OPEN_AI_API_KEY\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
else
echo -e "\033[31m[Warning] You need to set OPEN_AI_API_KEY before running!\033[0m"
fi
# use http_proxy as default
if [ "$HTTP_PROXY" != "" ] ; then
sed -i "s/\"proxy\".*,$/\"proxy\": \"$HTTP_PROXY\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$OPEN_AI_PROXY" != "" ] ; then
sed -i "s/\"proxy\".*,$/\"proxy\": \"$OPEN_AI_PROXY\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$SINGLE_CHAT_PREFIX" != "" ] ; then
sed -i "s/\"single_chat_prefix\".*,$/\"single_chat_prefix\": $SINGLE_CHAT_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$SINGLE_CHAT_REPLY_PREFIX" != "" ] ; then
sed -i "s/\"single_chat_reply_prefix\".*,$/\"single_chat_reply_prefix\": $SINGLE_CHAT_REPLY_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$GROUP_CHAT_PREFIX" != "" ] ; then
sed -i "s/\"group_chat_prefix\".*,$/\"group_chat_prefix\": $GROUP_CHAT_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$GROUP_NAME_WHITE_LIST" != "" ] ; then
sed -i "s/\"group_name_white_list\".*,$/\"group_name_white_list\": $GROUP_NAME_WHITE_LIST,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$IMAGE_CREATE_PREFIX" != "" ] ; then
sed -i "s/\"image_create_prefix\".*,$/\"image_create_prefix\": $IMAGE_CREATE_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$CONVERSATION_MAX_TOKENS" != "" ] ; then
sed -i "s/\"conversation_max_tokens\".*,$/\"conversation_max_tokens\": $CONVERSATION_MAX_TOKENS,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$SPEECH_RECOGNITION" != "" ] ; then
sed -i "s/\"speech_recognition\".*,$/\"speech_recognition\": $SPEECH_RECOGNITION,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$CHARACTER_DESC" != "" ] ; then
sed -i "s/\"character_desc\".*,$/\"character_desc\": \"$CHARACTER_DESC\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$EXPIRES_IN_SECONDS" != "" ] ; then
sed -i "s/\"expires_in_seconds\".*$/\"expires_in_seconds\": $EXPIRES_IN_SECONDS/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
# append
if [ "$BAIDU_SECRET_KEY" != "" ] ; then
sed -i "1a \ \ \"baidu_secret_key\": \"$BAIDU_SECRET_KEY\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$BAIDU_API_KEY" != "" ] ; then
sed -i "1a \ \ \"baidu_api_key\": \"$BAIDU_API_KEY\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$BAIDU_APP_ID" != "" ] ; then
sed -i "1a \ \ \"baidu_app_id\": \"$BAIDU_APP_ID\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$VOICE_REPLY_VOICE" != "" ] ; then
sed -i "1a \ \ \"voice_reply_voice\": $VOICE_REPLY_VOICE," $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
# go to prefix dir
cd $CHATGPT_ON_WECHAT_PREFIX
# excute
$CHATGPT_ON_WECHAT_EXEC

View File

@@ -15,5 +15,6 @@ services:
GROUP_NAME_WHITE_LIST: '["ChatGPT测试群", "ChatGPT测试群2"]'
IMAGE_CREATE_PREFIX: '["画", "看", "找"]'
CONVERSATION_MAX_TOKENS: 1000
SPEECH_RECOGNITION: 'false'
CHARACTER_DESC: '你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。'
EXPIRES_IN_SECONDS: 3600

View File

@@ -16,6 +16,7 @@ GROUP_CHAT_PREFIX=${GROUP_CHAT_PREFIX:-""}
GROUP_NAME_WHITE_LIST=${GROUP_NAME_WHITE_LIST:-""}
IMAGE_CREATE_PREFIX=${IMAGE_CREATE_PREFIX:-""}
CONVERSATION_MAX_TOKENS=${CONVERSATION_MAX_TOKENS:-""}
SPEECH_RECOGNITION=${SPEECH_RECOGNITION:-""}
CHARACTER_DESC=${CHARACTER_DESC:-""}
EXPIRES_IN_SECONDS=${EXPIRES_IN_SECONDS:-""}
@@ -36,50 +37,54 @@ fi
# modify content in config.json
if [ "$OPEN_AI_API_KEY" != "" ] ; then
sed -i "2c \"open_ai_api_key\": \"$OPEN_AI_API_KEY\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"open_ai_api_key\".*,$/\"open_ai_api_key\": \"$OPEN_AI_API_KEY\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
else
echo -e "\033[31m[Warning] You need to set OPEN_AI_API_KEY before running!\033[0m"
fi
# use http_proxy as default
if [ "$HTTP_PROXY" != "" ] ; then
sed -i "3c \"proxy\": \"$HTTP_PROXY\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"proxy\".*,$/\"proxy\": \"$HTTP_PROXY\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$OPEN_AI_PROXY" != "" ] ; then
sed -i "3c \"proxy\": \"$OPEN_AI_PROXY\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"proxy\".*,$/\"proxy\": \"$OPEN_AI_PROXY\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$SINGLE_CHAT_PREFIX" != "" ] ; then
sed -i "4c \"single_chat_prefix\": $SINGLE_CHAT_PREFIX," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"single_chat_prefix\".*,$/\"single_chat_prefix\": $SINGLE_CHAT_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$SINGLE_CHAT_REPLY_PREFIX" != "" ] ; then
sed -i "5c \"single_chat_reply_prefix\": $SINGLE_CHAT_REPLY_PREFIX," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"single_chat_reply_prefix\".*,$/\"single_chat_reply_prefix\": $SINGLE_CHAT_REPLY_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$GROUP_CHAT_PREFIX" != "" ] ; then
sed -i "6c \"group_chat_prefix\": $GROUP_CHAT_PREFIX," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"group_chat_prefix\".*,$/\"group_chat_prefix\": $GROUP_CHAT_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$GROUP_NAME_WHITE_LIST" != "" ] ; then
sed -i "7c \"group_name_white_list\": $GROUP_NAME_WHITE_LIST," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"group_name_white_list\".*,$/\"group_name_white_list\": $GROUP_NAME_WHITE_LIST,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$IMAGE_CREATE_PREFIX" != "" ] ; then
sed -i "8c \"image_create_prefix\": $IMAGE_CREATE_PREFIX," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"image_create_prefix\".*,$/\"image_create_prefix\": $IMAGE_CREATE_PREFIX,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$CONVERSATION_MAX_TOKENS" != "" ] ; then
sed -i "9c \"conversation_max_tokens\": $CONVERSATION_MAX_TOKENS," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"conversation_max_tokens\".*,$/\"conversation_max_tokens\": $CONVERSATION_MAX_TOKENS,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$SPEECH_RECOGNITION" != "" ] ; then
sed -i "s/\"speech_recognition\".*,$/\"speech_recognition\": $SPEECH_RECOGNITION,/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$CHARACTER_DESC" != "" ] ; then
sed -i "10c \"character_desc\": \"$CHARACTER_DESC\"," $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"character_desc\".*,$/\"character_desc\": \"$CHARACTER_DESC\",/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
if [ "$EXPIRES_IN_SECONDS" != "" ] ; then
sed -i "11c \"expires_in_seconds\": $EXPIRES_IN_SECONDS" $CHATGPT_ON_WECHAT_CONFIG_PATH
sed -i "s/\"expires_in_seconds\".*$/\"expires_in_seconds\": $EXPIRES_IN_SECONDS/" $CHATGPT_ON_WECHAT_CONFIG_PATH
fi
# go to prefix dir

View File

@@ -6,6 +6,7 @@ GROUP_CHAT_PREFIX=["@bot"]
GROUP_NAME_WHITE_LIST=["ChatGPT测试群", "ChatGPT测试群2"]
IMAGE_CREATE_PREFIX=["画", "看", "找"]
CONVERSATION_MAX_TOKENS=1000
SPEECH_RECOGNITION=false
CHARACTER_DESC=你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。
EXPIRES_IN_SECONDS=3600