Skill: fangbb-coder/voice-bridge-light (version 1.0.1)

# Voice Bridge Light Lightweight offline voice bridging service providing OpenAI-compatible STT/TTS HTTP API. ## Features - **TTS Text-to-Speech**: Supports Edge TTS (online) and Piper (local) - **STT Speech Recognition**: Based on Whisper local recognition - **OpenAI Compatible API**: Compatible with OpenAI Audio API - **Lightweight Deployment**: Minimal dependencies, easy to install ## Usage ### Installation ```bash pip install -r requirements.txt ``` ### Start Service Default using Edge TTS: ```bash python api_server.py ``` Using Piper (model required): ```bash TTS_ENGINE=piper PIPER_MODEL=models/piper/zh_CN-huayan-medium.onnx python api_server.py ``` ### API Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `GET /health` | GET | Health check | | `POST /audio/speech` | POST | TTS speech synthesis | | `POST /audio/transcriptions` | POST | STT speech recognition | ### Configuration Environment Variables | Variable | Default | Description | |----------|---------|-------------| | `VOICE_BRIDGE_HOST` | `0.0.0.0` | Listen address | | `VOICE_BRIDGE_PORT` | `18790` | Listen port | | `TTS_ENGINE` | `edge` | TTS engine: `edge` or `piper` | | `EDGE_VOICE` | `zh-CN-XiaoxiaoNeural` | Edge TTS voice | | `PIPER_MODEL` | `models/piper/zh_CN-huayan-medium.onnx` | Piper model path | | `STT_MODEL` | `base` | Whisper model size | ### TTS Request Example ```bash curl -X POST http://localhost:18790/audio/speech \ -H "Content-Type: application/json" \ -d '{ "input": "Hello, world!", "voice": "zh-CN-XiaoxiaoNeural", "response_format": "mp3" }' \ --output speech.mp3 ``` ### STT Request Example ```bash curl -X POST http://localhost:18790/audio/transcriptions \ -F "file=@speech.mp3" \ -H "Content-Type: multipart/form-data" ``` ## OpenClaw Integration Configure in `openclaw.json`: ```json { "tts": { "enabled": true, "provider": "local-piper", "baseUrl": "http://127.0.0.1:18790", "apiKey": "local", "voice": "zh-CN-XiaoxiaoNeural" } } ``` ## Dependencies - Python 3.8+ - edge-tts (Edge TTS) - faster-whisper (Whisper STT) - soundfile (audio processing) - Flask + Flask-CORS (web service) ## Service Management ### systemd Service (Recommended) ```ini [Unit] Description=Voice Bridge Light - STT/TTS HTTP API After=network.target [Service] Type=simple User=root WorkingDirectory=/root/.openclaw/workspace/skills/voice-bridge-light ExecStart=/usr/bin/python3 api_server.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` Enable and start: ```bash systemctl daemon-reload systemctl enable voice-bridge-light.service systemctl start voice-bridge-light.service ``` ## Performance - TTS latency: < 1s (Edge TTS requires network) - STT latency: depends on audio length, real-time CPU - Memory usage: ~300-500MB (mainly from Whisper model) ## Notes - Edge TTS requires internet access to Microsoft services - Piper requires downloading model files (first use) - Whisper model loads slowly on first run, recommend warm-up - Production environment recommended to use systemd management ## License MIT

path: README.md
content: # Voice Bridge Light

轻量级语音桥接服务，提供 OpenAI 兼容的 STT/TTS HTTP API。

## 功能

- **TTS 文本转语音**：支持 Edge TTS（在线）和 Piper（本地）
- **STT 语音识别**：基于 Whisper 本地识别
- **OpenAI 兼容接口**：与 OpenAI Audio API 兼容
- **轻量部署**：依赖少，易于安装

## 使用方法

### 安装

```bash
pip install -r requirements.txt
```

### 启动服务

默认使用 Edge TTS：
```bash
python api_server.py
```

使用 Piper（需下载模型）：
```bash
TTS_ENGINE=piper PIPER_MODEL=models/piper/zh_CN-huayan-medium.onnx python api_server.py
```

### API 端点

| 端点 | 方法 | 说明 |
|------|------|------|
| `GET /health` | GET | 健康检查 |
| `POST /audio/speech` | POST | TTS 语音合成 |
| `POST /audio/transcriptions` | POST | STT 语音识别 |

### 配置环境变量

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `VOICE_BRIDGE_HOST` | `0.0.0.0` | 监听地址 |
| `VOICE_BRIDGE_PORT` | `18790` | 监听端口 |
| `TTS_ENGINE` | `edge` | TTS 引擎：`edge` 或 `piper` |
| `EDGE_VOICE` | `zh-CN-XiaoxiaoNeural` | Edge TTS 音色 |
| `PIPER_MODEL` | `models/piper/zh_CN-huayan-medium.onnx` | Piper 模型路径 |
| `STT_MODEL` | `base` | Whisper 模型大小 |

### TTS 请求示例

```bash
curl -X POST http://localhost:18790/audio/speech \
  -H "Content-Type: application/json" \
  -d '{
    "input": "你好，世界！",
    "voice": "zh-CN-XiaoxiaoNeural",
    "response_format": "mp3"
  }' \
  --output speech.mp3
```

### STT 请求示例

```bash
curl -X POST http://localhost:18790/audio/transcriptions \
  -F "file=@speech.mp3" \
  -H "Content-Type: multipart/form-data"
```

## OpenClaw 集成

在 `openclaw.json` 中配置：

```json
{
  "tts": {
    "enabled": true,
    "provider": "local-piper",
    "baseUrl": "http://127.0.0.1:18790",
    "apiKey": "local",
    "voice": "zh-CN-XiaoxiaoNeural"
  }
}
```

## 依赖

- Python 3.8+
- edge-tts（Edge TTS）
- faster-whisper（Whisper STT）
- soundfile（音频处理）
- Flask + Flask-CORS（Web 服务）

## 服务管理

### systemd 服务（推荐）

```ini
[Unit]
Description=Voice Bridge Light - STT/TTS HTTP API
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/root/.openclaw/workspace/skills/voice-bridge-light
ExecStart=/usr/bin/python3 api_server.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

启用并启动：
```bash
systemctl daemon-reload
systemctl enable voice-bridge-light.service
systemctl start voice-bridge-light.service
```

## 性能

- TTS 延迟：< 1s（Edge TTS 需要网络）
- STT 延迟：依赖音频长度，CPU 实时
- 内存占用：约 300-500MB（主要来自 Whisper 模型）

## 注意事项

- Edge TTS 需要外网访问微软服务
- Piper 需要下载模型文件（首次使用）
- Whisper 模型首次加载较慢，建议预热
- 生产环境建议使用 systemd 管理

## License

MIT


path: api_server.py
content: """
Voice Bridge Light - 简化版 HTTP API 服务
提供 STT (语音识别) 和 TTS (语音合成) 的 OpenAI 兼容接口
支持引擎：
- TTS: Piper (本地), Edge TTS (在线)
- STT: Whisper (本地)
"""

import os
import json
import logging
import asyncio
from flask import Flask, request, Response, jsonify
from flask_cors import CORS
import numpy as np
import soundfile as sf
import io

# TTS 引擎
try:
    from piper import PiperVoice
    PIPER_AVAILABLE = True
except ImportError:
    PIPER_AVAILABLE = False

try:
    import edge_tts
    EDGE_TTS_AVAILABLE = True
except ImportError:
    EDGE_TTS_AVAILABLE = False

# STT 引擎
try:
    from faster_whisper import WhisperModel
    WHISPER_AVAILABLE = True
except ImportError:
    WHISPER_AVAILABLE = False

app = Flask(__name__)
CORS(app)
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

HOST = os.getenv('VOICE_BRIDGE_HOST', '0.0.0.0')
PORT = int(os.getenv('VOICE_BRIDGE_PORT', 18790))
TTS_ENGINE = os.getenv('TTS_ENGINE', 'edge').lower()
PIPER_MODEL = os.getenv('PIPER_MODEL', 'models/piper/zh_CN-huayan-medium.onnx')
EDGE_VOICE = os.getenv('EDGE_VOICE', 'zh-CN-XiaoxiaoNeural')
STT_MODEL = os.getenv('STT_MODEL', 'base')

tts_engine = None
stt_engine = None

def get_piper():
    global tts_engine
    if tts_engine is None and PIPER_AVAILABLE:
        try:
            tts_engine = PiperVoice(PIPER_MODEL)
            logger.info(f"Piper TTS 加载成功: {PIPER_MODEL}")
        except Exception as e:
            logger.error(f"Piper 加载失败: {e}")
    return tts_engine

def get_edge_tts():
    return EDGE_TTS_AVAILABLE

def synthesize_piper(text, voice=None):
    engine = get_piper()
    if engine is None:
        raise RuntimeError("Piper TTS 不可用")
    audio, sample_rate = engine.synthesize(text, speaker_id=0)
    return audio, sample_rate

def synthesize_edge(text, voice=None):
    if not EDGE_TTS_AVAILABLE:
        raise RuntimeError("Edge TTS 不可用")
    voice = voice or EDGE_VOICE
    
    async def _synth():
        communicate = edge_tts.Communicate(text, voice)
        audio_chunks = []
        async for chunk in communicate.stream():
            if chunk["type"] == "audio":
                audio_chunks.append(chunk["data"])
        return b''.join(audio_chunks)
    
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    try:
        audio_mp3 = loop.run_until_complete(_synth())
    finally:
        loop.close()
    return audio_mp3

def get_stt():
    global stt_engine
    if stt_engine is None and WHISPER_AVAILABLE:
        try:
            stt_engine = WhisperModel(STT_MODEL, device="cpu", compute_type="int8")
            logger.info(f"Whisper STT 加载成功: {STT_MODEL}")
        except Exception as e:
            logger.error(f"Whisper 加载失败: {e}")
    return stt_engine

@app.route('/health', methods=['GET'])
def health():
    piper_ready = get_piper() is not None
    edge_ready = EDGE_TTS_AVAILABLE
    stt_ready = get_stt() is not None
    tts_ready = piper_ready if TTS_ENGINE == 'piper' else edge_ready
    return jsonify({
        "status": "healthy",
        "tts_ready": tts_ready,
        "stt_ready": stt_ready,
        "engines": {"piper": piper_ready, "edge": edge_ready, "whisper": stt_ready}
    })

@app.route('/audio/speech', methods=['POST'])
def tts():
    try:
        data = request.get_json(force=True)
        text = data.get('input', '').strip()
        fmt = data.get('response_format', 'wav')
        voice = data.get('voice')
        if not text:
            return jsonify({"error": "empty input"}), 400

        if TTS_ENGINE == 'piper' and PIPER_AVAILABLE:
            audio, sample_rate = synthesize_piper(text, voice)
            buffer = io.BytesIO()
            sf.write(buffer, audio, sample_rate, format='wav')
            audio_bytes = buffer.getvalue()
        else:
            if not EDGE_TTS_AVAILABLE:
                return jsonify({"error": "TTS 引擎不可用"}), 503
            audio_bytes = synthesize_edge(text, voice)
            if fmt == 'wav':
                try:
                    from pydub import AudioSegment
                    mp3 = AudioSegment.from_file(io.BytesIO(audio_bytes), format="mp3")
                    wav_io = io.BytesIO()
                    mp3.export(wav_io, format="wav")
                    audio_bytes = wav_io.getvalue()
                except Exception as e:
                    logger.error(f"MP3 转 WAV 失败: {e}")
                    fmt = 'mp3'

        if fmt == 'pcm':
            import wave
            with io.BytesIO(audio_bytes) as bio:
                with wave.open(bio, 'rb') as wav:
                    pcm_data = wav.readframes(wav.getnframes())
            return Response(pcm_data, mimetype='audio/pcm')
        elif fmt == 'mp3':
            return Response(audio_bytes, mimetype='audio/mpeg')
        else:
            return Response(audio_bytes, mimetype='audio/wav')
    except Exception as e:
        logger.error(f"TTS 错误: {e}", exc_info=True)
        return jsonify({"error": str(e)}), 500

@app.route('/audio/transcriptions', methods=['POST'])
def stt():
    try:
        if 'file' not in request.files:
            return jsonify({"error": "no file provided"}), 400
        file = request.files['file']
        audio_data = file.read()
        import tempfile
        with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
            f.write(audio_data)
            temp_path = f.name
        engine = get_stt()
        if engine is None:
            return jsonify({"error": "STT 不可用"}), 503
        segments, info = engine.transcribe(temp_path, language="zh")
        text = "".join([seg.text for seg in segments])
        os.unlink(temp_path)
        return jsonify({
            "text": text.strip(),
            "task": "transcribe",
            "language": info.language,
            "duration": info.duration
        })
    except Exception as e:
        logger.error(f"STT 错误: {e}", exc_info=True)
        return jsonify({"error": str(e)}), 500

if __name__ == '__main__':
    logger.info(f"Voice Bridge Light 启动: http://{HOST}:{PORT}")
    logger.info(f"TTS 引擎: {TTS_ENGINE} (Piper可用: {PIPER_AVAILABLE}, Edge可用: {EDGE_TTS_AVAILABLE})")
    app.run(host=HOST, port=PORT, debug=False, threaded=True, use_reloader=False)

path: requirements.txt
content: flask>=2.3.0
flask-cors>=4.0.0
numpy>=1.24.0
soundfile>=0.12.0
piper-tts>=1.2.0
faster-whisper>=0.10.0
edge-tts>=6.1.6
pydub>=0.25.0
pyyaml>=6.0.1


path: skill.yaml
content: name: voice-bridge-light
description: 轻量级离线语音桥接，提供 STT/TTS HTTP API
version: 1.0.0
author: OpenClaw

capabilities:
  - stt
  - tts

install: pip install -r requirements.txt
start: python api_server.py
health: http://localhost:18790/health


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)