feat: AI 场景路由、ASR 服务及前后端全链路同步

- 新增 AI 场景路由控制器和管理接口
- 新增 ASR 语音识别服务及前后端集成
- 同步 AI Runtime 客户端到 Web/小程序/Life-Script
- 完善 AI 配置测试修复和管理后台路由配置
- 新增数据库迁移脚本

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 13:25:21 +08:00
parent d77090aa5e
commit 89fc42819d
72 changed files with 4584 additions and 383 deletions
+17
View File
@@ -0,0 +1,17 @@
# Emotion Museum ASR Service
Private speech-to-text service for the mini program voice input.
Install on `101.200.208.45`:
```bash
cd /data/programs/emotion-museum/asr-service
python3.11 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
uvicorn app:app --host 127.0.0.1 --port 19120
curl http://127.0.0.1:19120/health
```
The service uses FunASR ONNX Runtime with a local Chinese Paraformer model on CPU and is intended to stay private on localhost. The Java backend exposes the authenticated public API.
+114
View File
@@ -0,0 +1,114 @@
import os
import sys
import tempfile
import time
import types
import importlib.machinery
from pathlib import Path
from threading import Lock
from fastapi import FastAPI, File, UploadFile
app = FastAPI(title="Emotion Museum ASR")
MODEL_NAME = os.getenv("ASR_MODEL", "/data/programs/emotion-museum/asr-service/models/paraformer-zh-onnx")
DEVICE = os.getenv("ASR_DEVICE", "cpu")
WORK_DIR = Path(os.getenv("ASR_WORK_DIR", "/tmp/emotion-museum-asr"))
WORK_DIR.mkdir(parents=True, exist_ok=True)
_model = None
_model_lock = Lock()
def get_model():
global _model
with _model_lock:
if _model is None:
# funasr-onnx imports the optional SenseVoice module from package
# __init__, which imports torch even when we only use Paraformer.
# This service intentionally runs the ONNX path without PyTorch.
if "torch" not in sys.modules:
torch_stub = types.ModuleType("torch")
torch_stub.__spec__ = importlib.machinery.ModuleSpec("torch", loader=None)
torch_stub.Tensor = type("Tensor", (), {})
sys.modules["torch"] = torch_stub
from funasr_onnx import Paraformer
_model = Paraformer(
MODEL_NAME,
batch_size=1,
device_id=-1,
quantize=True,
intra_op_num_threads=2,
)
return _model
def clean_text(text):
if isinstance(text, (list, tuple)):
text = text[0] if text else ""
if not text:
return ""
markers = ["<|zh|>", "<|en|>", "<|yue|>", "<|ja|>", "<|ko|>", "<|nospeech|>", "<|withitn|>", "<|woitn|>"]
for marker in markers:
text = text.replace(marker, "")
return text.strip()
@app.get("/health")
def health():
return {
"status": "ok",
"engine": "funasr-onnx",
"model": MODEL_NAME,
"device": DEVICE,
"loaded": _model is not None,
}
@app.post("/transcribe")
async def transcribe(file: UploadFile = File(...)):
started = time.time()
suffix = Path(file.filename or "audio.wav").suffix or ".wav"
tmp_path = None
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix, dir=WORK_DIR) as tmp:
tmp_path = Path(tmp.name)
while True:
chunk = await file.read(1024 * 1024)
if not chunk:
break
tmp.write(chunk)
model = get_model()
result = model([str(tmp_path)])
first = result[0] if isinstance(result, list) and result else result
text = clean_text(first.get("preds", first.get("text", "")) if isinstance(first, dict) else str(first or ""))
language = first.get("language") if isinstance(first, dict) else None
return {
"success": bool(text),
"text": text,
"language": language,
"durationMs": int((time.time() - started) * 1000),
"engine": "funasr-onnx",
"model": MODEL_NAME,
"errorMessage": None if text else "empty recognition result",
}
except Exception as exc:
return {
"success": False,
"text": "",
"language": None,
"durationMs": int((time.time() - started) * 1000),
"engine": "funasr-onnx",
"model": MODEL_NAME,
"errorMessage": str(exc),
}
finally:
if tmp_path:
try:
tmp_path.unlink(missing_ok=True)
except Exception:
pass
@@ -0,0 +1,16 @@
[Unit]
Description=Emotion Museum ASR Service
After=network.target
[Service]
Type=simple
WorkingDirectory=/data/programs/emotion-museum/asr-service
Environment=ASR_MODEL=/data/programs/emotion-museum/asr-service/models/paraformer-zh-onnx
Environment=ASR_DEVICE=cpu
Environment=ASR_WORK_DIR=/tmp/emotion-museum-asr
ExecStart=/data/programs/emotion-museum/asr-service/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 19120
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,8 @@
fastapi==0.111.0
uvicorn[standard]==0.30.1
pydantic==2.7.4
python-multipart==0.0.20
funasr-onnx==0.4.1
modelscope==1.22.3
onnxruntime==1.26.0
soundfile==0.13.1