1. 环境安装需要 ffmpeg
  2. py 3.8+
  3. pip install edge-tts
  4. 安装这个 edge-tts
  5. 还需要系统环境 有 lame wget nodejs

    tts_local_edg.zip文件下载

    服务根目录的 tts.js 换成这个,把之前的tts.js 科大讯飞版本 换成这个 edg py版本就 换了 文字 转语音的引擎
const CryptoJS = require('crypto-js')
const WebSocket = require('ws')
var log = require('log4node')
var fs = require('fs')
const { exec } = require('child_process');
const path = require('path');

// 强制切换到当前文件目录(你已确认正确)
process.chdir(__dirname);
console.log("✅ 当前工作目录:", process.cwd());
//如果你的 edge-tts  不支持这么多语音包你就把对应的删除掉
//pip install --upgrade edge-tts --break-system-packages 更新命令
//edge-tts --list-voices 查看语音包的命令
const CHINESE_VOICES = {
    "xiaoxiao": "zh-CN-XiaoxiaoNeural",
    "yunxi": "zh-CN-YunxiNeural",
    "yunjian": "zh-CN-YunjianNeural",
    "xiaoyi": "zh-CN-XiaoyiNeural",
    "yunxia": "zh-CN-YunxiaNeural",
    "xiaochen": "zh-CN-XiaochenNeural",
    "xiaohan": "zh-CN-XiaohanNeural",
    "xiaomeng": "zh-CN-XiaomengNeural",
    "xiaomo": "zh-CN-XiaomoNeural",
    "xiaoqiu": "zh-CN-XiaoqiuNeural",
    "xiaorui": "zh-CN-XiaoruiNeural",
    "xiaoshuang": "zh-CN-XiaoshuangNeural",
    "xiaoxuan": "zh-CN-XiaoxuanNeural",
    "xiaoyan": "zh-CN-XiaoyanNeural",
    "xiaoyou": "zh-CN-XiaoyouNeural",
    "yunfeng": "zh-CN-YunfengNeural",
    "yunhao": "zh-CN-YunhaoNeural",
    "yunxiang": "zh-CN-YunxiangNeural",
    "yunyang": "zh-CN-YunyangNeural",
};

var arg = process.argv.splice(2);
let volume = arg[0]
let speed = arg[1]
let sex = arg[2]
let path_output = arg[3]
let textpath = arg[4]

// 音色判断
let finalVoiceKey = CHINESE_VOICES.hasOwnProperty(sex) ? sex : "xiaoxiao";
let realVoice = CHINESE_VOICES[finalVoiceKey];

// ====================== 🔥 修复开机自启失败 ======================
// 用【绝对路径】运行 python3,开机自启100%能找到!
var cmd = `/usr/bin/python3 xiaoboshu.py tts ${textpath} ${path_output} --voice=${realVoice}`;
// =================================================================

console.log("执行命令:", cmd);

exec(cmd , (error, stdout, stderr) => {
  if (error) {
    console.error('执行失败:', error);
    console.error('错误输出:', stderr);
    return;
  }
  console.log('生成成功:', stdout);
});

在同一个目录创建一个 xiaoboshu.py 然后就可以了。


#!/usr/bin/env python3
import sys
import os
import json
import tempfile
import subprocess
import asyncio
from pathlib import Path
import random
import string
from typing import Optional

# -------------------------- 常量定义 --------------------------
# TTS 缓存目录(脚本所在目录下的 tts 文件夹)
TTS_DIR = Path(__file__).parent / "tty"
# FFmpeg 转换参数
FFMPEG_BITRATE = "128k"
FFMPEG_SAMPLERATE = "22050"
# 中文语音选项 (Edge TTS)
CHINESE_VOICES = {
    "xiaoxiao": "zh-CN-XiaoxiaoNeural",      # 晓晓 - 女声,自然亲切
    "yunxi": "zh-CN-YunxiNeural",            # 云希 - 男声,年轻活力
    "yunjian": "zh-CN-YunjianNeural",        # 云健 - 男声,成熟稳重
    "xiaoyi": "zh-CN-XiaoyiNeural",          # 晓伊 - 女声,温柔甜美
    "yunxia": "zh-CN-YunxiaNeural",          # 云夏 - 男童声
    "xiaochen": "zh-CN-XiaochenNeural",      # 晓辰 - 女声,新闻播报风格
    "xiaohan": "zh-CN-XiaohanNeural",        # 晓涵 - 女声,温暖
    "xiaomeng": "zh-CN-XiaomengNeural",      # 晓梦 - 女声,活泼
    "xiaomo": "zh-CN-XiaomoNeural",          # 晓墨 - 女声,知性
    "xiaoqiu": "zh-CN-XiaoqiuNeural",        # 晓秋 - 女声,温和
    "xiaorui": "zh-CN-XiaoruiNeural",        # 晓睿 - 女童声
    "xiaoshuang": "zh-CN-XiaoshuangNeural",  # 晓双 - 女童声
    "xiaoxuan": "zh-CN-XiaoxuanNeural",      # 晓萱 - 女声
    "xiaoyan": "zh-CN-XiaoyanNeural",        # 晓妍 - 女声
    "xiaoyou": "zh-CN-XiaoyouNeural",        # 悠悠 - 女童声
    "yunfeng": "zh-CN-YunfengNeural",        # 云枫 - 男声
    "yunhao": "zh-CN-YunhaoNeural",          # 云皓 - 男声
    "yunxiang": "zh-CN-YunxiangNeural",      # 云翔 - 男声
    "yunyang": "zh-CN-YunyangNeural",        # 云扬 - 男声
}
DEFAULT_VOICE = "xiaoxiao"

# -------------------------- 工具函数 --------------------------
def create_random_filename(prefix: str = "file_", suffix: str = "", length: int = 6) -> str:
    """
    生成指定长度的随机数字文件名

    Args:
        prefix: 文件名前缀
        suffix: 文件后缀(无需带点,如 "webm" 而非 ".webm")
        length: 随机数字长度

    Returns:
        拼接后的文件名(如 "file_123456.webm")
    """
    random_num = ''.join(random.choices(string.digits, k=length))
    suffix = f".{suffix}" if suffix and not suffix.startswith(".") else suffix
    return f"{prefix}{random_num}{suffix}"

def ensure_dir_exists(dir_path: Path) -> None:
    """确保目录存在,不存在则创建"""
    if not dir_path.exists():
        dir_path.mkdir(parents=True, exist_ok=True)

# -------------------------- TTS 核心逻辑 --------------------------
async def generate_tts(text: str, voice_name: str, output_path: Path) -> None:
    """
    使用 Edge TTS 生成语音

    Args:
        text: 要转换的文本内容
        voice_name: 语音名称(如 "xiaoxiao")
        output_path: 生成的 webm 文件路径
    """
    try:
        import edge_tts
    except ImportError:
        raise ImportError("请安装 edge-tts:pip install edge-tts")

    voice_id = CHINESE_VOICES.get(voice_name, CHINESE_VOICES[DEFAULT_VOICE])
    communicate = edge_tts.Communicate(text, voice_id)
    await communicate.save(str(output_path))

def convert_to_mp3(input_path: Path, output_path: Path) -> bool:
    """
    使用 ffmpeg 转换为 MP3

    Args:
        input_path: 输入 webm 文件路径
        output_path: 输出 mp3 文件路径

    Returns:
        转换成功返回 True,失败返回 False
    """
    try:
        cmd = [
            "ffmpeg", "-y", "-i", str(input_path),
            "-acodec", "libmp3lame",
            "-ab", FFMPEG_BITRATE,
            "-ar", FFMPEG_SAMPLERATE,
            str(output_path)
        ]
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            print(f"FFmpeg 转换失败:{result.stderr}")
            return False
        return True
    except FileNotFoundError:
        print("错误:未找到 ffmpeg,请先安装 ffmpeg 并添加到系统环境变量")
        return False

def text_to_speech(text_path: str, mp3_filename: str, voice: Optional[str] = None) -> bool:
    """
    从文本文件生成 MP3 语音(自动保存到 tts 目录)

    Args:
        text_path: 文本文件路径(UTF-8 编码)
        mp3_filename: 输出 MP3 文件名(如 test.mp3)
        voice: 语音名称(可选,默认 "xiaoxiao")

    Returns:
        生成成功返回 True,失败返回 False
    """
    # 1. 初始化参数
    voice_name = voice or DEFAULT_VOICE
    if voice_name not in CHINESE_VOICES:
        print(f"警告: 未知语音 '{voice_name}',使用默认语音 '{DEFAULT_VOICE}'")
        voice_name = DEFAULT_VOICE

    # 2. 确保 TTS 目录存在
    ensure_dir_exists(TTS_DIR)

    # 3. 处理路径:文本文件路径校验 + MP3 路径拼接(强制放入 TTS_DIR)
    text_path_obj = Path(text_path).resolve()
    if not text_path_obj.exists():
        print(f"错误:文本文件不存在 - {text_path_obj}")
        return False

    # 强制将 MP3 文件放入 tts 目录
    mp3_output_path = TTS_DIR / mp3_filename
    # 确保 MP3 文件名带 .mp3 后缀
    if mp3_output_path.suffix.lower() != ".mp3":
        mp3_output_path = mp3_output_path.with_suffix(".mp3")
        print(f"提示:自动补充 MP3 后缀,最终文件名:{mp3_output_path.name}")

    # 4. 读取文本
    try:
        with open(text_path_obj, "r", encoding="utf-8") as f:
            text = f.read().strip()
        if not text:
            print("错误:文本文件为空")
            return False
        print(f"生成语音: {text[:50]}{'...' if len(text) > 50 else ''}")
        print(f"语音: {voice_name}")
    except Exception as e:
        print(f"错误:读取文本文件失败 - {e}")
        return False

    # 5. 生成 WebM 临时文件(放在 tts 目录下)
    webm_filename = create_random_filename(prefix="tts_temp_", suffix="webm")
    webm_path = TTS_DIR / webm_filename
    try:
        asyncio.run(generate_tts(text, voice_name, webm_path))
        if not webm_path.exists():
            print("错误:TTS 生成 WebM 文件失败")
            return False
    except Exception as e:
        print(f"错误:生成 TTS 失败 - {e}")
        return False

    # 6. 转换为 MP3(输出到 tts 目录)
    print("转换为 MP3...")
    if not convert_to_mp3(webm_path, mp3_output_path):
        webm_path.unlink(missing_ok=True)  # 清理临时文件
        return False

    # 7. 清理临时文件
    webm_path.unlink(missing_ok=True)
    print(f"生成成功: {mp3_output_path.absolute()}")
    return True

# -------------------------- 命令行逻辑 --------------------------
def login(host: str, username: str, password: str) -> None:
    """
    登录小播鼠系统(占位函数,需补充实际逻辑)

    Args:
        host: 服务器地址
        username: 用户名
        password: 密码
    """
    # 需补充实际的登录逻辑(如请求 API、保存 token 等)
    print(f"登录 {host} - 用户名: {username}(密码已隐藏)")
    # 示例:保存登录信息到配置文件
    config_path = Path.home() / ".xiaoboshu.json"
    config = {"host": host, "username": username, "token": "fake_token_123"}  # 替换为实际 token
    with open(config_path, "w", encoding="utf-8") as f:
        json.dump(config, f, ensure_ascii=False, indent=2)
    print(f"登录成功,配置已保存到 {config_path}")

def list_voices() -> None:
    """列出所有支持的语音"""
    print("支持的语音列表:")
    for name, voice_id in CHINESE_VOICES.items():
        print(f"  {name}: {voice_id}")

def main() -> None:
    """主命令行入口"""
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(0)

    cmd = sys.argv[1].lower()

    # 登录命令
    if cmd == "login":
        if len(sys.argv) < 5:
            print("用法: python xiaoboshu.py login <host> <username> <password>")
            sys.exit(1)
        login(sys.argv[2], sys.argv[3], sys.argv[4])

    # TTS 命令
    elif cmd == "tts":
        # 解析参数:tts <text_path> <mp3_output_filename> [--voice=xxx]
        if len(sys.argv) < 4:
            print("用法: python xiaoboshu.py tts <text_path> <mp3_output_filename> [--voice=<voice>]")
            print("示例: python xiaoboshu.py tts ./text.txt my_voice.mp3 --voice=yunxi")
            sys.exit(1)

        text_path = sys.argv[2]
        mp3_filename = sys.argv[3]
        voice = None
        # 解析可选的 --voice 参数
        for arg in sys.argv[4:]:
            if arg.startswith("--voice="):
                voice = arg.split("=", 1)[1]
                break

        if not Path(text_path).exists():
            print(f"错误:文本文件不存在 - {text_path}")
            sys.exit(1)

        success = text_to_speech(text_path, mp3_filename, voice)
        sys.exit(0 if success else 1)

    # 列出语音命令
    elif cmd == "voices":
        list_voices()

    # 占位命令(需补充实际逻辑)
    elif cmd in ["devices", "files", "play", "stop", "volume", "tasks"]:
        print(f"命令 '{cmd}' 暂未实现,需补充 API 调用逻辑")
        sys.exit(1)

    # 未知命令
    else:
        print(f"未知命令: {cmd}")
        print(__doc__)
        sys.exit(1)

# -------------------------- 入口 --------------------------
if __name__ == "__main__":
    main()
作者:oxiaom  创建时间:2026-03-12 15:42
最后编辑:oxiaom  更新时间:2026-06-16 00:57