""" PregoPal 全局工具 — 全双工语音助手 ==================================== 集成本地 llama-server 全双工能力 """ import os import io import json import time import base64 import logging import numpy as np import requests as req from pathlib import Path logger = logging.getLogger("prego_voice") # ── 配置 ────────────────────────────────────────────── BASE_DIR = Path(__file__).resolve().parents[1] LLAMA_SERVER_URL = os.environ.get("LLAMA_SERVER_URL", "http://127.0.0.1:8081") OMNI_OUTPUT_DIR = os.environ.get("OMNI_OUTPUT_DIR", str(BASE_DIR / "omni_output")) API_BASE = os.environ.get("MINICPM_API_BASE", LLAMA_SERVER_URL) def chat_text(messages: list, max_tokens: int = 300, temperature: float = 0.7) -> str: """ 文本对话(直接调用 llama-server) Args: messages: [{"role": "user/system", "content": "..."}] max_tokens: 最大输出 token 数 temperature: 生成温度 Returns: str: AI 回复文本 """ body = { "messages": messages, "max_tokens": max_tokens, "temperature": temperature, "stream": False, } try: url = f"{LLAMA_SERVER_URL}/v1/chat/completions" resp = req.post(url, json=body, timeout=120) if resp.status_code == 200: data = resp.json() return data["choices"][0]["message"]["content"] else: logger.error(f"chat_text 失败: {resp.status_code} {resp.text[:200]}") return f"[API错误] {resp.status_code}" except Exception as e: logger.error(f"chat_text 异常: {e}") return f"[连接错误] {e}" def chat_voice(audio_path: str) -> dict: """ 语音对话(调用 PregoAPI 后端) Args: audio_path: WAV 音频文件路径 Returns: dict: { "text": str, # AI 回复文本 "audio_base64": str, # TTS 音频 base64 "success": bool, } """ try: # 读取音频并转 base64 import soundfile as sf audio_data, sr = sf.read(audio_path, dtype='float32') if sr != 16000: try: import librosa audio_data = librosa.resample(audio_data, orig_sr=sr, target_sr=16000) except ImportError: pass buf = io.BytesIO() sf.write(buf, audio_data, 16000, format='WAV', subtype='PCM_16') audio_b64 = base64.b64encode(buf.getvalue()).decode('utf-8') if not audio_b64: return {"success": False, "error": "音频为空", "text": "", "audio_base64": ""} # 调用后端 body = { "audio_base64": audio_b64, "sample_rate": 16000, "max_tokens": 300, } url = f"{API_BASE}/v1/omni/voice_chat" resp = req.post(url, json=body, timeout=180) if resp.status_code == 200: data = resp.json() return { "success": data.get("success", False), "text": data.get("text", ""), "audio_base64": data.get("audio_base64", ""), "round": data.get("round", 0), "audio_files": data.get("audio_files", 0), } else: # fallback: 直接用文本对话(避免服务中断) logger.warning(f"voice_chat API 失败 ({resp.status_code}),回退到文本对话") return { "success": True, "text": "(语音识别未启用,已切换文字模式)", "audio_base64": "", "fallback": True, } except Exception as e: logger.error(f"chat_voice 异常: {e}") return {"success": False, "error": str(e), "text": "", "audio_base64": ""} def omni_status() -> dict: """检查全双工后端状态""" try: resp = req.get(f"{API_BASE}/health", timeout=5) if resp.status_code == 200: return resp.json() return {"status": "error", "message": f"HTTP {resp.status_code}"} except Exception as e: return {"status": "error", "message": str(e)}