diff --git a/cache_data/input.wav b/cache_data/input.wav index 79f0c22..62a3d53 100644 Binary files a/cache_data/input.wav and b/cache_data/input.wav differ diff --git a/config.json b/config.json index 9e1d5d0..716ad4c 100644 --- a/config.json +++ b/config.json @@ -29,7 +29,7 @@ "automatic_player_status": true, "automatic_player_url": "http://127.0.0.1:6000", "liveRoom": { - "enabled": true, + "enabled": false, "url": "" }, "record": { diff --git a/core/recorder.py b/core/recorder.py index a6a56a2..e8088c4 100644 --- a/core/recorder.py +++ b/core/recorder.py @@ -16,8 +16,11 @@ import tempfile import wave from core import fay_core from core import interact + +import re + # 启动时间 (秒) -_ATTACK = 0.2 +_ATTACK = 0.05 # 更快进入拾音,减少短唤醒词被漏掉的概率 # 释放时间 (秒) _RELEASE = 0.7 @@ -85,6 +88,31 @@ class Recorder: with fay_core.auto_play_lock: fay_core.can_auto_play = True + def _norm_asr_text(self, s: str) -> str: + """ + 大厅场景:ASR 结果常见问题 + - 句首有空格/标点/语气词(嗯、啊、这个...) + - 唤醒词后跟逗号、冒号等 + 所以需要做最小必要的规范化,避免 front 模式误判为“待唤醒” + """ + if not s: + return "" + s = s.strip() + # 去掉句首标点/空白 + s = re.sub(r'^[\s,,。.!!?::、~~]+', '', s) + # 去掉句首常见语气词(按需可继续加) + s = re.sub(r'^(嗯|啊|呃|额|哦|唔|这个|那个)\s*', '', s) + return s + + def _strip_wake_prefix(self, full_text: str, wake_word: str) -> str: + """ + 去除前置唤醒词,只把“真正的问题”交给对话系统 + 例如:'小F,播放音乐' -> '播放音乐' + """ + rest = full_text[len(wake_word):] + # 吞掉唤醒词后的空白/标点 + return rest.lstrip(" \t,,。.!!?::、~~") + def __waitingResult(self, iat: asrclient, audio_data): self.processing = True t = time.time() @@ -141,34 +169,83 @@ class Recorder: self.timer.cancel() # 取消之前的计时器任务 self.timer = threading.Timer(60, self.reset_wakeup_status) # 重设计时器为60秒 self.timer.start() - - #前置唤醒词模式 - elif cfg.config['source']['wake_word_type'] == 'front': - wake_word = cfg.config['source']['wake_word'] - wake_word_list = wake_word.split(',') + + # 前置唤醒词模式(大厅优化版) + elif cfg.config['source']['wake_word_type'] == 'front': + wake_word = cfg.config['source']['wake_word'] + wake_word_list = [w.strip() for w in wake_word.split(',') if w.strip()] + + raw_text = text + text2 = self._norm_asr_text(raw_text) + wake_up = False - for word in wake_word_list: - if text.startswith(word): - wake_up_word = word + matched_word = None + + # 1) 规范化后做“严格句首匹配” + for w in wake_word_list: + w2 = self._norm_asr_text(w) + if w2 and text2.startswith(w2): wake_up = True + matched_word = w2 break + + # 2) 容错:允许唤醒词出现在句首很短范围内(防止语气词未完全清掉) + # 注意:范围要小,避免大厅误唤醒 + if not wake_up: + N = 4 # 建议 3~6,越大越容易误触发 + head = text2[:N] + for w in wake_word_list: + w2 = self._norm_asr_text(w) + if w2 and w2 in head: + wake_up = True + matched_word = w2 + # 从唤醒词出现的位置截断,确保 strip 正确 + idx = text2.find(w2) + text2 = text2[idx:] + break + if wake_up: - util.printInfo(1, self.username, "唤醒成功!") + util.printInfo(1, self.username, f"唤醒成功!(front:{matched_word})") if wsa_server.get_web_instance().is_connected(self.username): - wsa_server.get_web_instance().add_cmd({"panelMsg": "唤醒成功!", "Username" : self.username , 'robot': f'http://{cfg.fay_url}:5000/robot/Listening.jpg'}) + wsa_server.get_web_instance().add_cmd({ + "panelMsg": "唤醒成功!", + "Username": self.username, + 'robot': f'http://{cfg.fay_url}:5000/robot/Listening.jpg' + }) if wsa_server.get_instance().is_connected(self.username): - content = {'Topic': 'Unreal', 'Data': {'Key': 'log', 'Value': "唤醒成功!"}, 'Username' : self.username, 'robot': f'http://{cfg.fay_url}:5000/robot/Listening.jpg'} + content = { + 'Topic': 'Unreal', + 'Data': {'Key': 'log', 'Value': "唤醒成功!"}, + 'Username': self.username, + 'robot': f'http://{cfg.fay_url}:5000/robot/Listening.jpg' + } wsa_server.get_instance().add_cmd(content) - #去除唤醒词后语句 - question = text#[len(wake_up_word):].lstrip() + + # ✅ 关键:剥离唤醒词,把真正问题交给对话系统 + question = self._strip_wake_prefix(text2, matched_word) + + # 如果只说了唤醒词(或后面太短),给一句提示 + if not question: + question = "在呢,你说?" + self.on_speaking(question) self.processing = False else: - util.printInfo(1, self.username, "[!] 待唤醒!") + # ✅ 关键:打印原始识别和规范化后文本,现场好定位为何没匹配上 + util.printInfo(1, self.username, f"[!] 待唤醒!(front) ASR='{raw_text}' norm='{text2}'") if wsa_server.get_web_instance().is_connected(self.username): - wsa_server.get_web_instance().add_cmd({"panelMsg": "[!] 待唤醒!", "Username" : self.username , 'robot': f'http://{cfg.fay_url}:5000/robot/Normal.jpg'}) + wsa_server.get_web_instance().add_cmd({ + "panelMsg": "[!] 待唤醒!", + "Username": self.username, + 'robot': f'http://{cfg.fay_url}:5000/robot/Normal.jpg' + }) if wsa_server.get_instance().is_connected(self.username): - content = {'Topic': 'Unreal', 'Data': {'Key': 'log', 'Value': "[!] 待唤醒!"}, 'Username' : self.username, 'robot': f'http://{cfg.fay_url}:5000/robot/Normal.jpg'} + content = { + 'Topic': 'Unreal', + 'Data': {'Key': 'log', 'Value': "[!] 待唤醒!"}, + 'Username': self.username, + 'robot': f'http://{cfg.fay_url}:5000/robot/Normal.jpg' + } wsa_server.get_instance().add_cmd(content) #非唤醒模式 @@ -234,7 +311,10 @@ class Recorder: #计算音量是否满足激活拾音 level = audioop.rms(data, 2) - if len(self.__history_data) >= 10:#保存激活前的音频,以免信息掉失 + + # 把激活前缓存拉长,避免“唤醒词”在触发拾音前被漏掉 + # 1024帧@16kHz≈64ms/块,30块≈1.9秒 + if len(self.__history_data) >= 30: # 保存激活前的音频,以免信息掉失 self.__history_data.pop(0) if len(self.__history_level) >= 500: self.__history_level.pop(0) diff --git a/fay.db b/fay.db index 8b3a6a4..0830d50 100644 Binary files a/fay.db and b/fay.db differ diff --git a/gui/static/js/chat1.js b/gui/static/js/chat1.js index 90588e9..0d41f26 100644 --- a/gui/static/js/chat1.js +++ b/gui/static/js/chat1.js @@ -150,6 +150,19 @@ class FayInterface { handleIncomingMessage(data) { const vueInstance = this.vueInstance; + + if (data.panelReply !== undefined) { + vueInstance.panelReply = data.panelReply.content; + + // 发送消息给父窗口,并指定目标 origin(必须是父组件的域名) + if (window.parent) { + window.parent.postMessage( + {type: 'panelReply', data: data.panelReply.content}, + '*' // 父组件的域名 + ); + } + } + // console.log('Incoming message:', data); if (data.liveState !== undefined) { vueInstance.liveState = data.liveState; diff --git a/qa.csv b/qa.csv index 2fbd4bb..b824f0e 100644 --- a/qa.csv +++ b/qa.csv @@ -1,4 +1,4 @@ -你好,你好,我是小橄榄!有什么我可以帮助你的吗 +你好,你好,我是小橄榄!有什么我可以帮助你的吗 我们现在在哪里,我们现在在冕宁元升农业的展览厅,可以参观游览我们先进的油橄榄产业园区哦! 介绍一下基地,元升集团油橄榄种植基地是中国目前最大的油橄榄种植庄园,目前整个庄园面积已接近30000亩。 介绍一下元升集团,2011年,冕宁元升农业董事长林春福跟随周恩来总理的脚步,在冕宁地区开启了油橄榄庄园打造之路。经过十年的发展,冕宁元升农业目前已成为国家林业局示范基地、国家林业重点龙头企业、四川省第一种植庄园、四川省脱贫标杆企业,获得各种荣誉奖项200余项。 diff --git a/system.conf b/system.conf index c1fbc84..47a38db 100644 --- a/system.conf +++ b/system.conf @@ -52,11 +52,11 @@ lingju_api_authcode= #gpt 服务密钥(NLP多选1) https://openai.com/ #免费key只支持gpt 3.5 ,若想使用其他model,可到 https://api.zyai.online/register/?aff_code=MyCI 下购买申请。 -gpt_api_key=sk-4Spva89SGSikpacz3a70Dd081cA84c9a8dEd345f19C9BdFc +gpt_api_key=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 #gpt base url 如:https://api.openai.com/v1、https://rwkv.ai-creator.net/chntuned/v1、https://api.fastgpt.in/api/v1、https://api.moonshot.cn/v1 -gpt_base_url=https://api.zyai.online/v1 +gpt_base_url=https://openrouter.ai/api/v1 #gpt model engine 如:gpt-3.5-turbo、moonshot-v1-8k -gpt_model_engine=gpt-3.5-turbo +gpt_model_engine=qwen/qwen3-4b:free #gpt(fastgpt)代理(可为空,填写例子:127.0.0.1:7890) proxy_config=