493浏览
查看: 493|回复: 0

[M10项目] 行空板之云天智能音箱

[复制链接]
本帖最后由 云天 于 2024-10-8 11:49 编辑

【项目背景】

        随着智能家居和物联网技术的发展,语音交互已经成为人与设备沟通的重要方式之一。为了提供更加自然和便捷的用户体验,本项目旨在开发一个基于行空板的智能语音交互系统。该系统将集成先进的语音唤醒、人声检测、语音识别、对话处理和语音合成技术,以实现流畅的语音交互体验。

行空板之云天智能音箱图1

【项目设计】

  • 语音唤醒:利用Snowboy库实现低功耗的语音唤醒功能,用户可以通过特定的唤醒词激活设备,而不需要手动操作。

  • 人声检测:通过WebRTCVAD(Voice Activity Detection)技术进行人声检测,确保系统只在有人说话时开始录音,提高录音效率和准确性。

  • 录音与暂停:当检测到人声时,系统开始录音;当语音停顿超过2秒时,系统自动停止录音,以减少无效录音。

  • 语音识别:将录音文件发送给讯飞语音识别服务,将语音转换为文本,为后续的对话处理提供基础。

  • 对话处理:将识别出的文本发送给Kimi进行对话处理,Kimi将根据文本内容生成合适的回复。

  • 语音合成:将Kimi生成的文本回复发送给讯飞进行语音合成,转换成语音信号。

  • 语音播放:利用行空板连接的蓝牙音箱播放合成的语音,为用户提供听觉反馈。


【技术亮点】

  • 低功耗语音唤醒:Snowboy库提供了高效的离线语音唤醒功能,减少了设备的能耗。
  • 实时人声检测:WebRTCVAD能够实时检测人声活动,确保录音的准确性。
  • 智能对话处理:Kimi的智能对话系统能够理解用户意图并生成合适的回复。
  • 高质量的语音合成:讯飞的语音合成技术能够生成自然流畅的语音输出。
  • 无线音频输出:通过蓝牙音箱播放语音,提供了便捷的无线音频解决方案。
【语音唤醒】
1.windows系统上安装“snowboy”库:pip install snowboy
2.snowboy官网已停止运营了,可使用第三方:https://snowboy.hahack.com/录制自己的唤醒词,并下载训练好的模型文件。
行空板之云天智能音箱图2

3.行空板系统上安装“snowboy”库

        (1)获取Snowboy源码:
        可以从GitHub上的Snowboy仓库克隆源代码:
  1. git clone https://github.com/Kitt-AI/snowboy.git
复制代码

        (2)编译Snowboy:

进入源码目录并编译Python wrapper:
  1. cd snowboy/swig/Python
  2. make
复制代码


        (3)这将生成_snowboydetect.so文件和Python wrapper snowboydetect.py。

        测试Snowboy:

进入示例目录并运行demo:

  1. cd snowboy/examples/Python
  2. python demo.py resources/models/snowboy.umdl
复制代码

        按照提示说话,看是否能检测到唤醒词。
【唤醒词测试】
1.Mind+使用“终端“连接行空板,进入”行空板中的文件“——”snowboy“——”examples“——”Python3“,修改”demo.py“文件,并将下载的唤醒词文件yuntian.pmdl,上传至行空板当前目录。
行空板之云天智能音箱图3
  1. import snowboydecoder
  2. import sys
  3. import signal
  4. interrupted = False
  5. def signal_handler(signal, frame):
  6.    global interrupted
  7.    interrupted = True
  8. def interrupt_callback():
  9.    global interrupted
  10.    return interrupted
  11. #if len(sys.argv) == 1:
  12. #    print("Error: need to specify model name")
  13. #    print("Usage: python demo.py your.model")
  14. #    sys.exit(-1)
  15. #model = sys.argv[1]
  16. #model="./resources/models/snowboy.umdl"
  17. model="yuntian.pmdl"
  18. # capture SIGINT signal, e.g., Ctrl+C
  19. signal.signal(signal.SIGINT, signal_handler)
  20. detector = snowboydecoder.HotwordDetector(model, sensitivity=0.5)
  21. print('Listening... Press Ctrl+C to exit')
  22. # main loop
  23. detector.start(detected_callback=snowboydecoder.play_audio_file,
  24.                interrupt_check=interrupt_callback,
  25.                sleep_time=0.03)
  26. detector.terminate()
复制代码
【完整程序】
        修改”snowboydecoder.py“文件,实现语音唤醒、人声检测、语音识别、对话处理和语音合成技术,以流畅的语音交互体验。
  1. #!/usr/bin/env python
  2. import collections
  3. import pyaudio
  4. import snowboydetect
  5. import time
  6. import wave
  7. import os
  8. import logging
  9. from ctypes import *
  10. from contextlib import contextmanager
  11. import sys
  12. sys.path.append("/root/mindplus/.lib/thirdExtension/liliang-xunfeiyuyin-thirdex")
  13. sys.path.append("/root/mindplus/.lib/thirdExtension/mengchangfeng-kimi-thirdex")
  14. import xunfeiasr
  15. import openai
  16. import json
  17. from unihiker import Audio
  18. from df_xfyun_speech import XfTts
  19. from unihiker import GUI
  20. import record
  21. u_gui=GUI()
  22. 显示=u_gui.draw_text(text="Hi 云天",x=25,y=60,font_size=40, color="#0000FF")
  23. appId = "5c7a6af2"    #填写控制台中获取的 APPID 信息
  24. apiSecret = "YTYwZjMwMDYwNDVjYTU0OTFhY2RmNjEx"   #填写控制台中获取的 APISecret 信息
  25. apiKey ="94932090baf7bb1eae2200ace714f424"    #填写控制台中获取的 APIKey 信息
  26. u_audio = Audio()
  27. options = {}
  28. tts = XfTts(appId, apiKey, apiSecret, options)
  29. xunfeiasr.xunfeiasr_set(APPID=appId,APISecret=apiSecret,APIKey=apiKey)
  30. client = openai.OpenAI(api_key="sk-7EuCue2dQIFOWzaBpeavzSNjxrTi0KXbKVKKbDiN7n1vR8Mz", base_url="https://api.moonshot.cn/v1")
  31. kimi_model = "moonshot-v1-8k"
  32. kimi_temperature = 0.3
  33. kimi_history = [
  34.     {"role": "system", "content": """你是 Kimi,由 Moonshot AI 提供的人工智能助手,
  35.     你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。
  36.     回答问题的时候尽量精简词语,尽量将回答控制在100字以内。
  37.     也不需要在回答中添加关于时效性或者是请注意之类的额外说明"""}
  38. ]
  39. def kimi_chat(query, kimi_history, kimi_model, kimi_temperature):
  40.     kimi_history.append({
  41.         "role": "user",
  42.         "content": query
  43.     })
  44.     completion = client.chat.completions.create(
  45.         model=kimi_model,
  46.         messages=kimi_history,
  47.         temperature=kimi_temperature,
  48.     )
  49.     result = completion.choices[0].message.content
  50.     kimi_history.append({
  51.         "role": "assistant",
  52.         "content": result
  53.     })
  54.     return result
  55. interrupted = False
  56. logging.basicConfig()
  57. logger = logging.getLogger("snowboy")
  58. logger.setLevel(logging.INFO)
  59. TOP_DIR = os.path.dirname(os.path.abspath(__file__))
  60. RESOURCE_FILE = os.path.join(TOP_DIR, "resources/common.res")
  61. DETECT_DING = os.path.join(TOP_DIR, "resources/wzn.wav")
  62. DETECT_DONG = os.path.join(TOP_DIR, "resources/dong.wav")
  63. def py_error_handler(filename, line, function, err, fmt):
  64.     pass
  65. ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p)
  66. c_error_handler = ERROR_HANDLER_FUNC(py_error_handler)
  67. @contextmanager
  68. def no_alsa_error():
  69.     try:
  70.         asound = cdll.LoadLibrary('libasound.so')
  71.         asound.snd_lib_error_set_handler(c_error_handler)
  72.         yield
  73.         asound.snd_lib_error_set_handler(None)
  74.     except:
  75.         yield
  76.         pass
  77. class RingBuffer(object):
  78.     """Ring buffer to hold audio from PortAudio"""
  79.     def __init__(self, size=4096):
  80.         self._buf = collections.deque(maxlen=size)
  81.     def extend(self, data):
  82.         """Adds data to the end of buffer"""
  83.         self._buf.extend(data)
  84.     def get(self):
  85.         """Retrieves data from the beginning of buffer and clears it"""
  86.         tmp = bytes(bytearray(self._buf))
  87.         self._buf.clear()
  88.         return tmp
  89. def play_audio_file(fname=DETECT_DING):
  90.     """Simple callback function to play a wave file. By default it plays
  91.     a Ding sound.
  92.     :param str fname: wave file name
  93.     :return: None
  94.     """
  95.     ding_wav = wave.open(fname, 'rb')
  96.     ding_data = ding_wav.readframes(ding_wav.getnframes())
  97.     with no_alsa_error():
  98.         audio = pyaudio.PyAudio()
  99.     stream_out = audio.open(
  100.         format=audio.get_format_from_width(ding_wav.getsampwidth()),
  101.         channels=ding_wav.getnchannels(),
  102.         rate=ding_wav.getframerate(), input=False, output=True)
  103.     stream_out.start_stream()
  104.     stream_out.write(ding_data)
  105.     time.sleep(0.2)
  106.     stream_out.stop_stream()
  107.     stream_out.close()
  108.     audio.terminate()
  109. class HotwordDetector(object):
  110.     """
  111.     Snowboy decoder to detect whether a keyword specified by `decoder_model`
  112.     exists in a microphone input stream.
  113.     :param decoder_model: decoder model file path, a string or a list of strings
  114.     :param resource: resource file path.
  115.     :param sensitivity: decoder sensitivity, a float of a list of floats.
  116.                               The bigger the value, the more senstive the
  117.                               decoder. If an empty list is provided, then the
  118.                               default sensitivity in the model will be used.
  119.     :param audio_gain: multiply input volume by this factor.
  120.     :param apply_frontend: applies the frontend processing algorithm if True.
  121.     """
  122.     def __init__(self, decoder_model,
  123.                  resource=RESOURCE_FILE,
  124.                  sensitivity=[],
  125.                  audio_gain=1,
  126.                  apply_frontend=False):
  127.         tm = type(decoder_model)
  128.         ts = type(sensitivity)
  129.         if tm is not list:
  130.             decoder_model = [decoder_model]
  131.         if ts is not list:
  132.             sensitivity = [sensitivity]
  133.         model_str = ",".join(decoder_model)
  134.         self.detector = snowboydetect.SnowboyDetect(
  135.             resource_filename=resource.encode(), model_str=model_str.encode())
  136.         self.detector.SetAudioGain(audio_gain)
  137.         self.detector.ApplyFrontend(apply_frontend)
  138.         self.num_hotwords = self.detector.NumHotwords()
  139.         if len(decoder_model) > 1 and len(sensitivity) == 1:
  140.             sensitivity = sensitivity * self.num_hotwords
  141.         if len(sensitivity) != 0:
  142.             assert self.num_hotwords == len(sensitivity), \
  143.                 "number of hotwords in decoder_model (%d) and sensitivity " \
  144.                 "(%d) does not match" % (self.num_hotwords, len(sensitivity))
  145.         sensitivity_str = ",".join([str(t) for t in sensitivity])
  146.         if len(sensitivity) != 0:
  147.             self.detector.SetSensitivity(sensitivity_str.encode())
  148.         self.ring_buffer = RingBuffer(
  149.             self.detector.NumChannels() * self.detector.SampleRate() * 5)
  150.     def start(self, detected_callback=play_audio_file,
  151.               interrupt_check=lambda: False,
  152.               sleep_time=0.03,
  153.               audio_recorder_callback=None,
  154.               silent_count_threshold=15,
  155.               recording_timeout=100):
  156.         """
  157.         Start the voice detector. For every `sleep_time` second it checks the
  158.         audio buffer for triggering keywords. If detected, then call
  159.         corresponding function in `detected_callback`, which can be a single
  160.         function (single model) or a list of callback functions (multiple
  161.         models). Every loop it also calls `interrupt_check` -- if it returns
  162.         True, then breaks from the loop and return.
  163.         :param detected_callback: a function or list of functions. The number of
  164.                                   items must match the number of models in
  165.                                   `decoder_model`.
  166.         :param interrupt_check: a function that returns True if the main loop
  167.                                 needs to stop.
  168.         :param float sleep_time: how much time in second every loop waits.
  169.         :param audio_recorder_callback: if specified, this will be called after
  170.                                         a keyword has been spoken and after the
  171.                                         phrase immediately after the keyword has
  172.                                         been recorded. The function will be
  173.                                         passed the name of the file where the
  174.                                         phrase was recorded.
  175.         :param silent_count_threshold: indicates how long silence must be heard
  176.                                        to mark the end of a phrase that is
  177.                                        being recorded.
  178.         :param recording_timeout: limits the maximum length of a recording.
  179.         :return: None
  180.         """
  181.         self._running = True
  182.         def audio_callback(in_data, frame_count, time_info, status):
  183.             self.ring_buffer.extend(in_data)
  184.             play_data = chr(0) * len(in_data)
  185.             return play_data, pyaudio.paContinue
  186.         with no_alsa_error():
  187.             self.audio = pyaudio.PyAudio()
  188.         self.stream_in = self.audio.open(
  189.             input=True, output=False,
  190.             format=self.audio.get_format_from_width(
  191.                 self.detector.BitsPerSample() / 8),
  192.             channels=self.detector.NumChannels(),
  193.             rate=self.detector.SampleRate(),
  194.             frames_per_buffer=2048,
  195.             stream_callback=audio_callback)
  196.         if interrupt_check():
  197.             logger.debug("detect voice return")
  198.             return
  199.         tc = type(detected_callback)
  200.         if tc is not list:
  201.             detected_callback = [detected_callback]
  202.         if len(detected_callback) == 1 and self.num_hotwords > 1:
  203.             detected_callback *= self.num_hotwords
  204.         assert self.num_hotwords == len(detected_callback), \
  205.             "Error: hotwords in your models (%d) do not match the number of " \
  206.             "callbacks (%d)" % (self.num_hotwords, len(detected_callback))
  207.         logger.debug("detecting...")
  208.         state = "PASSIVE"
  209.         while self._running is True:
  210.             if interrupt_check():
  211.                 logger.debug("detect voice break")
  212.                 break
  213.             data = self.ring_buffer.get()
  214.             if len(data) == 0:
  215.                 time.sleep(sleep_time)
  216.                 continue
  217.             status = self.detector.RunDetection(data)
  218.             if status == -1:
  219.                 logger.warning("Error initializing streams or reading audio data")
  220.             #small state machine to handle recording of phrase after keyword
  221.             if state == "PASSIVE":
  222.                 if status > 0: #key word found
  223.                     self.recordedData = []
  224.                     self.recordedData.append(data)
  225.                     silentCount = 0
  226.                     recordingCount = 0
  227.                     
  228.                     message = "Keyword " + str(status) + " detected at time: "
  229.                     message += time.strftime("%Y-%m-%d %H:%M:%S",
  230.                                          time.localtime(time.time()))
  231.                     logger.info(message)
  232.                     callback = detected_callback[status-1]
  233.                     if callback is not None:
  234.                         callback()
  235.                         显示.config(text="听你说")
  236.                         record.record_audio()
  237.                         #u_audio.record("record.wav",6)
  238.                         
  239.                         text=xunfeiasr.xunfeiasr(r"record.wav")
  240.                         
  241.                         print(text)
  242.                         texts=""
  243.                         if(len(text)>7):
  244.                             num_lines = (len(text) + 6) // 7
  245.                             for i in range(num_lines):
  246.                                    texts+=text[i*7:(i+1)*7]+"\n"
  247.                             显示.config(text="你说:\n"+texts)
  248.                         else:
  249.                             显示.config(text="你说:\n"+text)
  250.                         显示.config(font_size=20)
  251.                         
  252.                         
  253.                         if(text):
  254.                           text=kimi_chat(text,kimi_history, kimi_model, kimi_temperature)
  255.                           显示.config(text="思考中")
  256.                           显示.config(font_size=40)
  257.                           tts.synthesis(text+"呢", "speech.wav")
  258.                           显示.config(text="回答中")
  259.                           u_audio.play("speech.wav")
  260.                         显示.config(text="HI 云天")
  261.                     if audio_recorder_callback is not None:
  262.                         state = "ACTIVE"
  263.                     continue
  264.             elif state == "ACTIVE":
  265.                 stopRecording = False
  266.                 if recordingCount > recording_timeout:
  267.                     stopRecording = True
  268.                 elif status == -2: #silence found
  269.                     if silentCount > silent_count_threshold:
  270.                         stopRecording = True
  271.                     else:
  272.                         silentCount = silentCount + 1
  273.                 elif status == 0: #voice found
  274.                     silentCount = 0
  275.                 if stopRecording == True:
  276.                     fname = self.saveMessage()
  277.                     audio_recorder_callback(fname)
  278.                     state = "PASSIVE"
  279.                     continue
  280.                 recordingCount = recordingCount + 1
  281.                 self.recordedData.append(data)
  282.         logger.debug("finished.")
  283.     def saveMessage(self):
  284.         """
  285.         Save the message stored in self.recordedData to a timestamped file.
  286.         """
  287.         filename = 'output' + str(int(time.time())) + '.wav'
  288.         data = b''.join(self.recordedData)
  289.         #use wave to save data
  290.         wf = wave.open(filename, 'wb')
  291.         wf.setnchannels(1)
  292.         wf.setsampwidth(self.audio.get_sample_size(
  293.             self.audio.get_format_from_width(
  294.                 self.detector.BitsPerSample() / 8)))
  295.         wf.setframerate(self.detector.SampleRate())
  296.         wf.writeframes(data)
  297.         wf.close()
  298.         logger.debug("finished saving: " + filename)
  299.         return filename
  300.     def terminate(self):
  301.         """
  302.         Terminate audio stream. Users can call start() again to detect.
  303.         :return: None
  304.         """
  305.         self.stream_in.stop_stream()
  306.         self.stream_in.close()
  307.         self.audio.terminate()
  308.         self._running = False
复制代码
【视频演示】

【应用场景】


        本项目适用于家庭、办公室、服务机器人等多种场景,可以作为智能助手、语音控制中心或信息查询工具,为用户提供便捷的语音交互服务。


本项目的实施将推动语音交互技术在智能家居和物联网领域的应用,提高用户的操作便利性和体验满意度,同时也为未来智能设备的发展提供了新的方向。



您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail