diff --git a/xiaomusic/config.py b/xiaomusic/config.py
index c85ed96..02d259d 100644
--- a/xiaomusic/config.py
+++ b/xiaomusic/config.py
@@ -228,6 +228,12 @@ class Config:
web_music_proxy: bool = (
os.getenv("XIAOMUSIC_WEB_MUSIC_PROXY", "true").lower() == "true"
)
+ # edge-tts 语音角色
+ edge_tts_voice: str = os.getenv("XIAOMUSIC_EDGE_TTS_VOICE", "")
+ # 是否启用定时清理临时文件
+ enable_auto_clean_temp: bool = (
+ os.getenv("XIAOMUSIC_ENABLE_AUTO_CLEAN_TEMP", "true").lower() == "true"
+ )
def append_keyword(self, keys, action):
for key in keys.split(","):
diff --git a/xiaomusic/crontab.py b/xiaomusic/crontab.py
index 7cf2027..21c03f6 100644
--- a/xiaomusic/crontab.py
+++ b/xiaomusic/crontab.py
@@ -1,4 +1,5 @@
import json
+import os
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.base import BaseTrigger
@@ -218,3 +219,34 @@ class Crontab:
self.log.info("crontab reload_config ok")
except Exception as e:
self.log.exception(f"Execption {e}")
+
+ # 添加定时清理临时文件任务
+ if xiaomusic.config.enable_auto_clean_temp:
+
+ async def clean_temp_job():
+ try:
+ temp_dir = xiaomusic.config.temp_dir
+ if not os.path.exists(temp_dir):
+ self.log.info(f"临时目录不存在: {temp_dir}")
+ return
+
+ files = os.listdir(temp_dir)
+ deleted_count = 0
+ for filename in files:
+ try:
+ file_path = os.path.join(temp_dir, filename)
+ if os.path.isfile(file_path):
+ os.remove(file_path)
+ deleted_count += 1
+ self.log.debug(f"已删除临时文件: {file_path}")
+ except Exception as e:
+ self.log.warning(f"删除文件失败 {file_path}: {e}")
+
+ self.log.info(
+ f"定时清理临时文件完成,共删除 {deleted_count} 个文件"
+ )
+ except Exception as e:
+ self.log.exception(f"清理临时文件异常: {e}")
+
+ self.add_job("0 3 * * *", clean_temp_job)
+ self.log.info("已添加每日凌晨3点定时清理临时文件任务")
diff --git a/xiaomusic/device_player.py b/xiaomusic/device_player.py
index 6d7be5e..81bce4a 100644
--- a/xiaomusic/device_player.py
+++ b/xiaomusic/device_player.py
@@ -77,6 +77,8 @@ class XiaoMusicDevice:
# 添加歌曲定时器
self._add_song_timer = None
+ # TTS 播放定时器
+ self._tts_timer = None
@property
def did(self):
@@ -599,24 +601,82 @@ class XiaoMusicDevice:
async def text_to_speech(self, value):
"""文字转语音"""
try:
- # 有 tts command 优先使用 tts command 说话
- if self.hardware in TTS_COMMAND:
- tts_cmd = TTS_COMMAND[self.hardware]
- self.log.info("Call MiIOService tts.")
- value = value.replace(" ", ",") # 不能有空格
- await miio_command(
- self.auth_manager.miio_service,
- self.did,
- f"{tts_cmd} {value}",
- )
+ # 检查是否配置了 edge-tts 语音角色
+ if self.config.edge_tts_voice:
+ await self._text_to_speech_edge_tts(value)
else:
- self.log.debug("Call MiNAService tts.")
- await self.auth_manager.mina_service.text_to_speech(
- self.device_id, value
- )
+ # 使用原有的 TTS 逻辑
+ # 有 tts command 优先使用 tts command 说话
+ if self.hardware in TTS_COMMAND:
+ tts_cmd = TTS_COMMAND[self.hardware]
+ self.log.info("Call MiIOService tts.")
+ value = value.replace(" ", ",") # 不能有空格
+ await miio_command(
+ self.auth_manager.miio_service,
+ self.did,
+ f"{tts_cmd} {value}",
+ )
+ else:
+ self.log.debug("Call MiNAService tts.")
+ await self.auth_manager.mina_service.text_to_speech(
+ self.device_id, value
+ )
except Exception as e:
self.log.exception(f"Execption {e}")
+ async def _text_to_speech_edge_tts(self, value):
+ """使用 edge-tts 进行文字转语音"""
+ from xiaomusic.utils.music_utils import get_local_music_duration
+ from xiaomusic.utils.network_utils import text_to_mp3
+
+ try:
+ # 取消之前的 TTS 定时器
+ if self._tts_timer:
+ self._tts_timer.cancel()
+ self._tts_timer = None
+ self.log.info("已取消之前的 TTS 定时器")
+
+ # 使用 edge-tts 生成 MP3 文件
+ self.log.info(
+ f"使用 edge-tts 生成语音: {value}, voice: {self.config.edge_tts_voice}"
+ )
+ mp3_path = await text_to_mp3(
+ text=value,
+ save_dir=self.config.temp_dir,
+ voice=self.config.edge_tts_voice,
+ )
+ self.log.info(f"edge-tts 生成的文件路径: {mp3_path}")
+
+ # 生成播放 URL
+ url = self.xiaomusic.music_url_handler._get_file_url(mp3_path)
+ self.log.info(f"TTS 播放 URL: {url}")
+
+ # 播放 TTS 音频
+ await self.group_player_play(url)
+
+ # 获取 MP3 时长
+ duration = await get_local_music_duration(mp3_path, self.config)
+ self.log.info(f"TTS 音频时长: {duration} 秒")
+
+ # 创建定时器,时长到后停止
+ if duration > 0:
+
+ async def _tts_timeout():
+ await asyncio.sleep(duration)
+ try:
+ self.log.info("TTS 播放定时器时间到")
+ if self._tts_timer:
+ self._tts_timer = None
+ await self.stop(arg1="notts")
+ except Exception as e:
+ self.log.error(f"TTS 定时器异常: {e}")
+
+ self._tts_timer = asyncio.create_task(_tts_timeout())
+ self.log.info(f"已设置 TTS 定时器,{duration} 秒后停止")
+
+ except Exception as e:
+ self.log.exception(f"edge-tts 播放失败: {e}")
+
async def group_player_play(self, url, name=""):
"""同一组设备播放"""
device_id_list = self.xiaomusic.get_group_device_id_list(self.group_name)
@@ -841,6 +901,11 @@ class XiaoMusicDevice:
self._stop_timer = None
self.log.info("cancel_all_timer _stop_timer.cancel")
+ if self._tts_timer:
+ self._tts_timer.cancel()
+ self._tts_timer = None
+ self.log.info("cancel_all_timer _tts_timer.cancel")
+
@classmethod
def dict_clear(cls, d):
"""清空设备字典并取消所有定时器"""
diff --git a/xiaomusic/music_url.py b/xiaomusic/music_url.py
index af201c0..d53a793 100644
--- a/xiaomusic/music_url.py
+++ b/xiaomusic/music_url.py
@@ -138,6 +138,21 @@ class MusicUrlHandler:
str: 本地音乐播放URL
"""
filename = self.music_library.get_filename(name)
+ self.log.info(
+ f"_get_local_music_url local music. name:{name}, filename:{filename}"
+ )
+ return self._get_file_url(filename)
+
+ def _get_file_url(self, filepath):
+ """根据文件路径生成可访问的URL
+
+ Args:
+ filepath: 文件的完整路径
+
+ Returns:
+ str: 文件访问URL
+ """
+ filename = filepath
# 处理文件路径
if filename.startswith(self.config.music_path):
@@ -146,9 +161,7 @@ class MusicUrlHandler:
if filename.startswith("/"):
filename = filename[1:]
- self.log.info(
- f"_get_local_music_url local music. name:{name}, filename:{filename}"
- )
+ self.log.info(f"_get_file_url filepath:{filepath}, filename:{filename}")
# 构造URL
encoded_name = urllib.parse.quote(filename)
diff --git a/xiaomusic/static/default/setting.html b/xiaomusic/static/default/setting.html
index 4c75cc2..c551d88 100644
--- a/xiaomusic/static/default/setting.html
+++ b/xiaomusic/static/default/setting.html
@@ -101,6 +101,31 @@ var vConsole = new window.VConsole();
+
+
+
+
+
+