AI 提供情绪价值

125 阅读13分钟
  1. 灵感来源-星黎

对话、语气、情绪、打断、声纹识别、记忆、MCP、视觉、幻觉、图灵测试 .....

  1. 开源项目-小智

虾哥开源:https://xiaozhi.me/ https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb

无硬件版:https://github.com/huangjunsen0406/py-xiaozhi?tab=readme-ov-file

华南理工大学-刘思源教授团队主导:<https://github.com/xinnan-tech/xiaozhi-esp32-server>

3. ## 整体架构

`- 🎙 ASR: 使用 FunASR 进行自动语音识别,将用户的语音转换为文本。

  • 🎚 VAD: 使用 silero-vad 进行语音活动检测,以确保只处理有效的语音片段。
  • 🧠 LLM: 使用 deepseek 作为大语言模型来处理用户输入并生成响应,极具性价比。
  • 🔊 TTS: 使用 edge-tts Kokoro-82M ChatTTS MacOS say进行文本到语音的转换,将生成的文本响应转换为自然流畅的语音。`

image.png

  1. 声音的基础知识

声音的本质是一种能量波,由振动而产生的能量波,通过传输介质传输出去。我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要转化为数字信号才能在计算机中使用。 4.1 ### PCM PCM(Pulse Code Modulation)即脉冲编码调制技术。PCM技术就是把声音从模拟信号转化为数字信号的技术,即对声音进行采样、量化的过程,经过PCM处理后的数据,是最原始的音频数据。

image.png

  1. 采样

    1. 采样频率:单位时间内对模拟信号的采样次数,它用赫兹(Hz)来表示。采样频率越高,声音的还原就越真实越自然,当然数据量就越大。采样频率一般共分为22.05KHz44.1KHz48KHz三个等级。
  2. 量化:

    1. 量化精度(位深):用多少个二进制位来表示这个幅度等级。例如:16-bit的量化精度,意味着有 65,536 个可能的幅度等级。
    2. 量化误差:由于四舍五入产生的原始值与量化值之间的微小差异,在回放时会表现为本底噪声。位深越高,量化误差越小,动态范围越广。
  3. 编码

    1. 将离散的幅度值转换为二进制码。
  4. 码率/比特率

    1. 是指每秒传输的比特数,单位是bps(bit per second)。
    2. 在音频编码中,比特率表示编码器每秒产生的比特数。比特率越高,通常音质越好,但文件体积也越大;比特率越低,音质可能越差,但文件体积越小。
    3. 在Opus编码中,我们可以设置一个目标比特率,编码器会尝试以这个比特率来编码音频。Opus编码器支持从6kbps到510kbps的比特率。
  5. 原始PCM大小

    1. PCM数据速率 (bps) = 采样率 × 位深 × 声道数
    2. CD音质举例: 44,100 × 16 × 2 = 1,411,200 bps (约1.4 Mbps)
    3. 一首5分钟的CD品质歌曲,其原始PCM数据体积约为 1.4 Mbps × 300秒 / 8 ≈ 52.9 MB

4.2. ### OPUS

Opus 是一款完全开放、免版税且功能强大的音频编解码器,Opus 可以处理各种音频应用,包括 IP 语音、视频会议、游戏内聊天,甚至远程现场音乐表演。它能够处理从低比特率窄带语音到极高品质立体声音乐的各种音频。

    1. 帧:是 OPUS 编码和解码的基本数据单元。
    2. 帧大小:一帧所代表的时间长度。OPUS 的帧大小非常灵活,可以是 2.5ms, 5ms, 10ms, 20ms, 40ms, 60ms。
  1. 采样点数

    1. 采样点数 = 采样率 * 帧大小(秒)
    2. 例如:在 16kHz 采样率、60ms 帧大小下,一帧包含 16000 * 0.06 = 960 个采样点。
  2. 目标码率

    1. 编码器在压缩音频或视频信号时,期望达到的平均数据速率。单位是bps(bit per second)
    2. 码率越高,压缩的越少,音质损失越少。反之,码率越低,压缩的越多,音质损失越多。
    3. 文件大小 ≈ 目标码率 × 持续时间

image.png

  1. 语音活动检测

  1. VAD

  2. 官方文档:github.com/snakers4/si…

  3. 模型耗时:单个音频块(30毫秒以上)在单个CPU线程上处理时间不到1毫秒

  4. speech_prob的含义

    1. 数值范围:0.0 到 1.0 之间的浮点数
    2. 数值含义:值越接近1.0,表示该音频片段包含语音的可能性越大;值越接近0.0,表示该音频片段更可能是静音或噪声
  5. 声音时长:512 / 16000 = 0.032秒 = 32毫秒

  6. 张量Tensor:类型转换、归一化、自动微分`

  7. 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话

image.png

def is_vad(self, conn, opus_packet):
    try:
        pcm_frame = self.decoder.decode(opus_packet, 960)
        conn.client_audio_buffer.extend(pcm_frame)  # 将新数据加入缓冲区

        # 处理缓冲区中的完整帧(每次处理512采样点)
        client_have_voice = False
        while len(conn.client_audio_buffer) >= 512 * 2:
            # 提取前512个采样点(1024字节)
            chunk = conn.client_audio_buffer[: 512 * 2]
            conn.client_audio_buffer = conn.client_audio_buffer[512 * 2 :]

            # 转换为模型需要的张量格式
            audio_int16 = np.frombuffer(chunk, dtype=np.int16)
            audio_float32 = audio_int16.astype(np.float32) / 32768.0
            audio_tensor = torch.from_numpy(audio_float32)

            # 检测语音活动,在推理阶段,不需要计算梯度
            with torch.no_grad():
                speech_prob = self.model(audio_tensor, 16000).item()

            # 双阈值判断
            if speech_prob >= self.vad_threshold:
                is_voice = True
            elif speech_prob <= self.vad_threshold_low:
                is_voice = False
            else:
                is_voice = conn.last_is_voice

            # 声音没低于最低值则延续前一个状态,判断为有声音
            conn.last_is_voice = is_voice

            # 更新滑动窗口
            conn.client_voice_window.append(is_voice)
            client_have_voice = (
                conn.client_voice_window.count(True) >= self.frame_window_threshold
            )

            # 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话
            if conn.client_have_voice and not client_have_voice:
                stop_duration = time.time() * 1000 - conn.last_activity_time
                if stop_duration >= self.silence_threshold_ms:
                    conn.client_voice_stop = True
            if client_have_voice:
                conn.client_have_voice = True
                conn.last_activity_time = time.time() * 1000

        return client_have_voice
    except opuslib_next.OpusError as e:
        logger.bind(tag=TAG).info(f"解码错误: {e}")
    except Exception as e:
        logger.bind(tag=TAG).error(f"Error processing audio packet: {e}")

6. ## 声纹识别

6.1. ### 3D-Speaker

  1. 官方文档:github.com/modelscope/…
  2. 本地部署:github.com/xinnan-tech… github.com/xinnan-tech…

image.png

def identify_voiceprint(
    self, speaker_ids: List[str], audio_bytes: bytes
) -> Tuple[str, float]:
    """
识别声纹

Args:
speaker_ids: 候选说话人ID列表
audio_bytes: 音频字节数据

Returns:
Tuple[str, float]: (识别出的说话人ID, 相似度分数)
"""
start_time = time.time()
    logger.info(f"开始声纹识别流程,候选说话人数量: {len(speaker_ids)}")

    audio_path = None
    try:
        # 简化音频验证
        if len(audio_bytes) < 1000:
            logger.warning("音频文件过小")
            return "", 0.0

        # 处理音频文件
        audio_process_start = time.time()
        audio_path = audio_processor.ensure_16k_wav(audio_bytes)
        audio_process_time = time.time() - audio_process_start
        logger.debug(f"音频文件处理完成,耗时: {audio_process_time:.3f}秒")

        # 提取声纹特征
        extract_start = time.time()
        logger.debug("开始提取声纹特征...")
        test_emb = self.extract_voiceprint(audio_path)
        extract_time = time.time() - extract_start
        logger.debug(f"声纹特征提取完成,耗时: {extract_time:.3f}秒")

        # 获取候选声纹特征
        db_query_start = time.time()
        logger.debug("开始查询数据库获取候选声纹特征...")
        voiceprints = voiceprint_db.get_voiceprints(speaker_ids)
        db_query_time = time.time() - db_query_start
        logger.debug(
            f"数据库查询完成,获取到{len(voiceprints)}个声纹特征,耗时: {db_query_time:.3f}秒"
        )

        if not voiceprints:
            logger.info("未找到候选说话人声纹")
            return "", 0.0

        # 计算相似度
        similarity_start = time.time()
        logger.debug("开始计算相似度...")
        similarities = {}
        for name, emb in voiceprints.items():
            similarity = self.calculate_similarity(test_emb, emb)
            similarities[name] = similarity
        similarity_time = time.time() - similarity_start
        logger.debug(
            f"相似度计算完成,共计算{len(similarities)}个,耗时: {similarity_time:.3f}秒"
        )

        # 找到最佳匹配
        if not similarities:
            return "", 0.0

        match_name = max(similarities, key=similarities.get)
        match_score = similarities[match_name]

        # 检查是否超过阈值
        if match_score < self.similarity_threshold:
            logger.info(
                f"未识别到说话人,最高分: {match_score:.4f},阈值: {self.similarity_threshold}"
            )
            total_time = time.time() - start_time
            logger.info(f"声纹识别流程完成,总耗时: {total_time:.3f}秒")
            return "", match_score

        total_time = time.time() - start_time
        logger.info(
            f"识别到说话人: {match_name}, 分数: {match_score:.4f}, 总耗时: {total_time:.3f}秒"
        )
        return match_name, match_score

    except Exception as e:
        total_time = time.time() - start_time
        logger.error(f"声纹识别异常,总耗时: {total_time:.3f}秒,错误: {e}")
        return "", 0.0
    finally:
        # 清理临时文件
        cleanup_start = time.time()
        if audio_path:
            audio_processor.cleanup_temp_file(audio_path)
        cleanup_time = time.time() - cleanup_start
        logger.debug(f"临时文件清理完成,耗时: {cleanup_time:.3f}秒")

7. ## 语音识别

官方文档:github.com/modelscope/…

  1. 记忆

8.1. ### 短期记忆

  • 上下文记忆

  • 本地记忆

    • 根据对话内容生成总结

8.2. ### 长期记忆

8.3. ### mem0

官方文档:github.com/mem0ai/mem0 docs.mem0.ai/quickstart

在线聊天:app.mem0.ai/playground?…

记忆日志:app.mem0.ai/dashboard/r…

MCP工具:server.smithery.ai/@big-omega/…

其他记忆:github.com/letta-ai/le… github.com/NevaMind-AI…

image.png

向量信息存储在本地/tmp/qdrant
//原始输入
result = m.add("我的名字叫张三,我有一个朋友叫李四,我们都在小米工作,李四的学长王五21年毕业于清华大学,最近学长刚提了一台小米su7", user_id="test")
print(result)
//信息提取-提示词
'{"facts" : ["名字叫张三", "有一个朋友叫李四", "都在小米工作", "李四的学长王五21年毕业于清华大学", "学长刚提了一台小米su7"]}'
//结合旧的记忆,增删改记忆-LLM
{
  "memory" : [ {
    "id" : "0",
    "text" : "名字叫张三",
    "event" : "ADD"
  }, {
    "id" : "1",
    "text" : "有一个朋友叫李四",
    "event" : "ADD"
  }, {
    "id" : "2",
    "text" : "都在小米工作",
    "event" : "ADD"
  }, {
    "id" : "3",
    "text" : "李四的学长王五21年毕业于清华大学",
    "event" : "ADD"
  }, {
    "id" : "4",
    "text" : "学长刚提了一台小米su7",
    "event" : "ADD"
  } ]
}

图数据库neo4j

官方平台:neo4j.com/

官方文档:neo4j.ac.cn/docs/

cypher语法:neo4j.com/docs/cypher…

本地部署:brew install neo4j brew services start neo4j

服务启动: neo4j restart --verbose

本地可视化: http://localhost:7474 用户名: neo4j 密码:12345678

image.png

image.png

//原始输入
result = m.add("我的名字叫张三,我有一个朋友叫李四,我们都在小米工作,李四的学长王五21年毕业于清华大学,最近学长刚提了一台小米su7", user_id="test")
print(result)
//提取实体-llm
{'entities': [{'entity': '张三', 'entity_type': 'Person'}, 
{'entity': '李四', 'entity_type': 'Person'}, 
{'entity': '小米', 'entity_type': 'Organization'},
 {'entity': '王五', 'entity_type': 'Person'}, 
 {'entity': '清华大学', 'entity_type': 'Organization'}, 
 {'entity': '小米su7', 'entity_type': 'Product'}]}
 //构建关系-llm
 {'content': None, 'tool_calls': [{'arguments': {'entities': [
 {'destination': '张三', 'relationship': 'name', 'source': 'user_id: test'}, 
 {'destination': '李四', 'relationship': 'friend', 'source': 'user_id: test'}, 
 {'destination': '小米', 'relationship': 'work_at', 'source': 'user_id: test'}, {'destination': '小米', 'relationship': 'work_at', 'source': '李四'}, 
 {'destination': '清华大学', 'relationship': 'graduate_of', 'source': '王五'}, {'destination': '小米su7', 'relationship': 'purchase', 'source': '王五'}]}, 
 'name': 'establish_relationships'}]}
with concurrent.futures.ThreadPoolExecutor() as executor:
    future1 = executor.submit(self._add_to_vector_store, messages, processed_metadata, effective_filters, infer)
    future2 = executor.submit(self._add_to_graph, messages, effective_filters)

    concurrent.futures.wait([future1, future2])

    vector_store_result = future1.result()
    graph_result = future2.result()

if self.enable_graph:
    return {
        "results": vector_store_result,
        "relations": graph_result,
    }

return {"results": vector_store_result}

8.4. ### 相关提示词

  1. MCP

  2. mcp工具:smithery.ai/

  3. 官方文档:modelcontextprotocol.io/introductio…

  4. MCP,全称是Model Context Protocol,模型上下文协议,由Claude母公司Anthropic于2024年11月正式 提出。MCP是一种技术协议,一种智能 体Agent开发过程中共同约定的一种规范,它标准化了应用程序向 LLM 提供上下文的方式。MCP 提供了一种将 AI 模型连接到不同数据源和工具的标准化方式。

  5. MCP解决的最大痛点就是Agent开发中调用外部工具的技术门槛过高、外部函数重复编写的问题。

  6. MCP也是通过Function calling来实现的,如果模型本身没有Function calling功能很难实现MCP。(可通过系统提示词间接实现)

  7. 大语言模型

deepseek

api接口:api-docs.deepseek.com/

流式输出:当检测到标点符号的时候截断进行文本转语音

def _get_segment_text(self):
    # 合并当前全部文本并处理未分割部分
    full_text = "".join(self.tts_text_buff)
    current_text = full_text[self.processed_chars :]  # 从未处理的位置开始
    last_punct_pos = -1

    # 根据是否是第一句话选择不同的标点符号集合
    punctuations_to_use = (
        self.first_sentence_punctuations
        if self.is_first_sentence
        else self.punctuations
    )

    for punct in punctuations_to_use:
        pos = current_text.rfind(punct)
        if (pos != -1 and last_punct_pos == -1) or (
            pos != -1 and pos < last_punct_pos
        ):
            last_punct_pos = pos

    if last_punct_pos != -1:
        segment_text_raw = current_text[: last_punct_pos + 1]
        segment_text = textUtils.get_string_no_punctuation_or_emoji(
            segment_text_raw
        )
        self.processed_chars += len(segment_text_raw)  # 更新已处理字符位置

        # 如果是第一句话,在找到第一个逗号后,将标志设置为False
        if self.is_first_sentence:
            self.is_first_sentence = False

        return segment_text
    elif self.tts_stop_request and current_text:
        segment_text = current_text
        self.is_first_sentence = True  # 重置标志
        return segment_text
    else:
        return None

11. ## 语音合成

EdgeTTS

官方:github.com/rany2/edge-…

特点:免费、本地部署

minimax

体验平台:www.minimaxi.com/audio/text-…

接入文档:platform.minimaxi.com/docs/guides…

在线调试:platform.minimaxi.com/examination…

火山

官方平台: www.volcengine.com/

开通模型: console.volcengine.com/speech/serv…

资源ID固定为:volc.service_type.10029(大模型语音合成及混音)

  1. 视觉模型

智普

官方:bigmodel.cn/usercenter/…

视觉:底层调用摄像头拍照,图片分析/视频分析

def capture_photo(camera_index=0, output_path="captured_image.jpg", warmup_frames=5, delay=0.1):
    """
拍摄照片并保存

Args:
camera_index: 摄像头索引
output_path: 输出文件路径
warmup_frames: 预热帧数,用于摄像头自动曝光调整
delay: 帧间延迟时间
"""
cap = cv2.VideoCapture(camera_index)
    if not cap.isOpened():
        logger.error(f"无法打开摄像头 {camera_index}")
        return False

    try:
        # 预热摄像头,让自动曝光和白平衡调整
        for i in range(warmup_frames):
            ret, frame = cap.read()
            if not ret:
                logger.warning(f"预热帧 {i + 1} 读取失败")
            time.sleep(delay)  # 给摄像头一点时间调整

        # 实际拍摄
        ret, frame = cap.read()

        if ret:
            cv2.imwrite(output_path, frame)
            logger.info(f"照片已保存至 {output_path}")
            return True
        else:
            logger.error("拍照失败,无法读取画面")
            return False
    except Exception as e:
        logger.error(f"拍照过程出错: {e}")
        return False
    finally:
        cap.release()

image.png

  1. 服务部署

部署流程https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97
mysql用户名root密码123456
本地启动mysql:brew services start mysql
建立连接:jdbc:mysql://localhost:3306/xiaozhi_esp32_server
本地连接:mysql -u root -p

本地启动redis:brew services start redis
redis 无密码登录

小智登录账号fangfang 密码Yff19980221
服务器密钥e3d942c8-5ce6-44ee-9586-6d62f228cdfd


启动java后端项目manger-api,jdk21


启动前端项目 manger-web

npm install
npm run serve

  App running at:
  - Local:   http://localhost:8001/ 
  - Network: http://10.189.87.253:8001/



启动python项目xiaozhi-server python app.py


启动测试页面 test.html


http://10.189.87.253:8002/xiaozhi/ota/

ws://10.189.87.253:8000/xiaozhi/v1/

http://10.189.87.253/

启动mcp服务-mcp-endpoint-server
250826 15:04:47[0.0.6][src.server]-INFO-智控台MCP参数配置: http://10.189.87.253:8004/mcp_endpoint/health?key=d0971a409005449cb407f8c97ebd2d39
250826 15:04:47[0.0.6][src.server]-INFO-单模块部署MCP接入点: ws://10.189.87.253:8004/mcp_endpoint/mcp/?token=Z9C/GBN7RMXqY62Oa9BSFoxbWGDKVkvyjHUOYb/Ueik%3D

启动mcp工具-mcp-calculator
export MCP_ENDPOINT=ws://192.168.1.25:8004/mcp_endpoint/mcp/?token=abc
python mcp_pipe.py calculator.py


启动声纹识别服务
voiceprint-api
250827 11:34:48[0.0.4][app.core.logger]-INFO-日志系统初始化完成,级别: INFO
250827 11:34:48[0.0.4][app.core.logger]-INFO-🚀 开始: 开发环境服务启动,监听地址: 0.0.0.0:8005
250827 11:34:48[0.0.4][app.core.logger]-INFO-API文档: http://0.0.0.0:8005/voiceprint/docs
250827 11:34:48[0.0.4][app.core.logger]-INFO-============================================================
250827 11:34:48[0.0.4][app.core.logger]-INFO-声纹接口地址: http://10.189.87.253:8005/voiceprint/health?key=31ab25cd-fbf0-4168-adca-10bc6ccdcf39
250827 11:34:48[0.0.4][app.core.logger]-INFO-============================================================

启动视觉模块
250827 14:58:01[0.7.5-00000000000000][__main__]-INFO-视觉分析接口是        http://10.189.87.253:8003/mcp/vision/explain
250827 14:58:01[0.7.5-00000000000000][__main__]-INFO-Websocket地址是        ws://10.189.87.253:8000/xiaozhi/v1/
250827 14:58:01[0.7.5-00000000000000][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
250827 14:58:01[0.7.5-00000000000000][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html


xiaozhi-py

# 使用 Homebrew 安装
brew install portaudio opus ffmpeg gfortran
brew upgrade tcl-tk

# 安装 Xcode 命令行工具(如未安装)
xcode-select --install

启动图数据库neo4j
sudo chown -R $(whoami) /opt/homebrew/var/neo4j
sudo chmod -R 755 /opt/homebrew/var/neo4j

 neo4j restart --verbose