- 环境安装需要 ffmpeg
- py 3.8+
- pip install edge-tts
- 安装这个 edge-tts
- 还需要系统环境 有 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
最后编辑:oxiaom 更新时间:2026-06-16 00:57