解决 chattts 无法移动 playlist.m3u8 到 gradio 缓存目录的技术实践
上周把 chattts 语音合成服务接进内部 Demo 站,结果一跑就报错:
chattts cannot move playlist.m3u8 to the gradio cache dir because it was not ...日志截断,看不出“not”后面到底缺了什么。服务重启、换盘、甚至给 C 盘“完全控制”都试过,依旧翻车。折腾两天,终于把坑填平,把过程拆成 6 段,照着做基本能一次过。
1. 问题背景:它为什么突然蹦出来
chattts 生成语音后会把 HLS 切片连同 playlist.m3u8 一起写进临时目录,再整体搬到 Gradio 的缓存路径(默认%TEMP%\gradio或/tmp/gradio)。
如果最后一步“搬过去”失败,前端就永远 404,用户只能对着空白播放器发呆。
典型触发场景:
- Windows 以“非管理员”身份启动服务
- Linux 用 systemd 拉起,ProtectSystem=strict 把 /tmp 锁成只读
- Docker 没挂 volume,容器里 /tmp 用满或 inode 耗尽
- 路径里夹带中文或空格,Gradio 的 shutil.move 没做 quote
一句话:文件其实生成了,就是“挪不动”。
2. 根本原因分析:三条线同时踩雷
权限
Gradio 缓存目录的 ACL 默认继承上级目录,很多云主机把 /tmp 设成 1777,但子目录被 umask 刷成 750,导致 chattts 进程(以低权用户跑)写不进去。路径解析
chattts 内部用pathlib.Path / "playlist.m3u8"拼接,如果缓存目录被配置成相对路径gradio_cache,而启动脚本刚好换了工作目录,拼接结果就飘到奇怪位置,shutil.move 源文件“不存在”。缓存机制
Gradio 3.40+ 默认启用“文件锁”防止并发写,Windows 下如果前一个请求没释放句柄,第二个请求就会报“文件被占用”,同样被吞进同一条日志。
3. 解决方案:五步让文件乖乖就位
下面代码直接塞进 chattts 启动脚本,能热插拔,不改源码。
# cache_guard.py import os import shutil import logging from pathlib import Path # 1. 提前把缓存目录搬到“家目录”下,避开系统 tmp CACHE_ROOT = Path.home() / ".cache" / "chattts_gradio" CACHE_ROOT.mkdir(parents=True, exist_ok=True) # 2. 强制 755,保证无论哪个用户拉起都能写 os.chmod(CACHE_ROOT, 0o755) # 3. 给 Gradio 打环境变量,让它别再去 /tmp os.environ["GRADIO_TEMP_DIR"] = str(CACHE_ROOT) # 4. 重写 shutil.move,带日志+异常兜底 def safe_move(src: str, dst_dir: str, retry: int = 3): src, dst = Path(src), Path(dst_dir) dst.parent.mkdir(parents=True, exist_ok=True) for i in range(retry): try: shutil.move(str(src), str(dst)) logging.info(f"[cache] moved {src.name} -> {dst}") return except PermissionError as e: logging.warning(f"[cache] permission denied, attempt {i+1}: {e}") os.chmod(src, 0o644) # 先给自己提权 os.chmod(dst.parent, 0o755) except FileNotFoundError as e: logging.error(f"[cache] file missing: {e}") break except shutil.Error as e: # 同名文件已存在,直接覆盖 logging.warning(f"[cache] overwrite {dst}") shutil.rmtree(dst, ignore_errors=True) shutil.move(str(src), str(dst)) return logging.exception("[cache] give up moving") # 5. 把 safe_move 注入 chattts 的 post_process 钩子 # 在 chattts 源码里搜 "shutil.move" 那一行,换成: # safe_move(temp_m3u8, gradio_cache_path)启动命令:
# Linux GRADIO_TEMP_DIR=$HOME/.cache/chattts_gradio python app.py # Windows set GRADIO_TEMP_DIR=%USERPROFILE%\.cache\chattts_gradio && python app.py4. 性能优化:别让缓存把磁盘吃光
定时清理:
用 systemd-timer 或 Windows 任务计划,每天凌晨删 3 天前的子目录。find $HOME/.cache/chattts_gradio -type f -mtime +3 -delete软链到高速盘:
如果宿主机有 NVMe,把 CACHE_ROOT 软链过去,HLS 切片写入速度直接翻倍,首包延迟降 30%。并发锁粒度:
Gradio 3.42 起支持share=False时关闭文件锁,若 Demo 只在局域网跑,可加--no-file-locking减少句柄争抢。
5. 避坑指南:别人踩过的坑,你就别再跳
| 错误现象 | 根因 | 一键修复 | |---|---| | 报FileExistsError| 同名目录残留 | 先shutil.rmtree(dst, ignore_errors=True)| | 报No space left on device| inode 满 |df -i查看,换盘或删小文件 | | 报Invalid cross-device link| 跨盘 move | 改用shutil.copy2+os.remove| | 中文路径乱码 | Windows code-页 | 全部用pathlib,禁止字符串拼接 | | 容器重启后丢失 | 未挂 volume | docker-compose 加- gradio_cache:/cache|
6. 生产环境建议:把“能跑”变“稳跑”
用 systemd 跑服务时,加上:
ReadWritePaths=/home/chattts/.cache/chattts_gradio既遵守最小权限,又不怕 ProtectSystem=strict。
日志分级:
把[cache]关键字单独打到 /var/log/chattts_cache.log,方便 ELK 采集。监控:
Prometheus node_exporter 采集磁盘剩余空间,<10% 就告警,比用户 404 投诉早一步。灰度:
先在小流量节点开GRADIO_TEMP_DIR,观察两天无 404 再全量。
延伸思考:下一步还能怎么玩?
- 如果 playlist.m3u8 只读一次,能否直接
BytesIO内存挂载,省掉落盘? - 多节点部署时,用 Redis 把切片索引共享,缓存目录走 NFS 会不会反而拖慢?
- Gradio 4.x 已经支持自定义
FilePreviews,有没有可能把 HLS 切片提前转 WebSocket 流,彻底告别文件系统?
把上面的代码跑到线上,再配个定时清理,chattts 跑了半个月再没报“cannot move”。
下次遇到类似“文件挪不动”的错,别再急着给整个盘开 777,先按“权限→路径→锁”三步查,基本都能定位。祝你排障愉快,404 退散!