Files
MohZhuang 0d74a1097d feat(v1.3.0): 新增火山引擎语音合成 2.0 作为可选配音引擎
流水线开跑前先问用户用哪个 TTS。两引擎产出的 audio-manifest.json
结构完全一致,故 timeline_build / subs / 渲染器零改动。

新增
- scripts/tts_volcano.py  接口包,唯一读 tts.env 的地方。零第三方依赖
                          (只用标准库 urllib),HTTP Chunked 单向流式,
                          与 edge-tts「发一次收一串」形态同构
- scripts/tts_setup.py    向导:选方案 → 配密钥 → 测连接 → 选音色,
                          输出 NEXT_ACTION=… 供智能体识别
- tts.env.example         密钥模板(真实 tts.env 已 gitignore)
- references/volcano-tts.md  密钥纪律 / 配置步骤 / 接口规格 / 音色表 / 排查表

安全
- Key 只存 tts.env,只有 tts_volcano.py 读它;异常过脱敏,只抹本次实际
  加载的密钥值与 AKLT… 形态,刻意不用「长字符串即密钥」的宽规则
- check_integrity.py 新增密钥防线检查(含「疑似把模板改名」提示)
- package_skill.py 排除 .env / .key / .pem,修掉一个真实外泄口

修复(真机联调暴露,均只在实跑时复现)
- enable_subtitle 必须显式开启,否则 words 恒为空数组且完全不报错,
  下游会静默退回按字数插值、字幕开始飘
- usage 挂在结束标记那一行,提前 continue 导致计费恒为 0
- 缓存未存首裁量,命中缓存时 lead_cut_sec=0,重跑字幕比首跑晚一帧
- CLI 把中文音色名原样当 speaker 发出,服务端报 resource ID mismatched

文档
- README 中英双语新增「配音:两个引擎」章节,按用户要求不写命令
  (那是智能体的职责),只讲「会问你什么 / 你唯一要动手的地方」
- 疑难排查补 6 条火山症状;lessons 34 → 56 条;流水线图加 tts_setup 节点
2026-09-23 10:04:13 +08:00

125 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
package_skill.py —— 把 html-explainer 打包成可带到任意终端的 zip。
设计取舍:
· **排除 node/node_modules/**(14MB,且平台相关)—— setup_env.sh --install 会重装。
这样 zip 只有几十 KB,纯文本 + 一份 GSAP。
· **保留 assets/gsap.min.js**(72KB,离线必需,体积可接受)。
· **保留 README / LICENSE / THIRD_PARTY_NOTICES.md / licenses/** —— 再分发时必须随包
带上这些文件(Apache-2.0 §4 要求收到衍生作品的人同时收到许可证副本)。
· **排除仓库卫生文件**(.github/、.gitignore、.gitattributes)—— 它们属于 GitHub
仓库,不属于可移植的技能包。
· 排除运行时产物(out/、audio/、*.beats.js、__pycache__、诊断脚本)。
用法:
python package_skill.py # 产出 dist/html-explainer-vX.Y.zip
python package_skill.py --with-deps # 连 node_modules 一起打(完全离线可跑,~14MB)
"""
from __future__ import annotations
import argparse
import os
import re
import sys
import zipfile
from datetime import date
SKILL_ROOT = os.path.dirname(os.path.abspath(__file__))
# 目录名匹配即整棵排除
EXCLUDE_DIRS = {
"node_modules", "__pycache__", "dist", "out", "audio", ".git",
".pytest_cache", "qc_frames",
".github", # 仓库卫生:CI / issue 模板不属于可移植技能包
"render", "research", "script", # 项目级产物(防在技能目录内建过 demo)
}
# 文件名 / 后缀排除
EXCLUDE_SUFFIX = (".pyc", ".pyo", ".beats.js", ".mp4", ".mp3", ".wav", ".log",
".env", ".key", ".pem") # 密钥类:绝不进可分发包
EXCLUDE_NAMES = {
"package-lock.json", ".DS_Store", "Thumbs.db", "qc_sheet.jpg", "qc_report.md",
".gitignore", ".gitattributes",
# ★ 密钥文件。tts.env 是火山 TTS 的 API Key 唯一落点,打包进 zip 等于把
# 用户的密钥随技能一起发出去。模板 tts.env.example 是入库的,不在此列。
"tts.env", "secrets",
}
def version() -> str:
md = os.path.join(SKILL_ROOT, "SKILL.md")
try:
txt = open(md, encoding="utf-8").read()
m = re.search(r"^version:\s*(\S+)", txt, re.M)
if m:
return m.group(1).strip().strip('"\'')
except OSError:
pass
return "1.0.0"
def should_skip(rel: str, with_deps: bool) -> bool:
parts = rel.replace("\\", "/").split("/")
for p in parts[:-1]:
if p in EXCLUDE_DIRS:
if with_deps and p == "node_modules":
continue
return True
name = parts[-1]
if name in EXCLUDE_NAMES:
return True
if name.endswith(EXCLUDE_SUFFIX):
return True
# 临时诊断脚本(下划线开头 .js/.mjs/.py,非技能正式脚本)
if parts[0] == "scripts" and name.startswith("_"):
return True
return False
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--with-deps", action="store_true",
help="把 node/node_modules 一起打包(离线可跑,体积 ~14MB)")
ap.add_argument("--out", default=None)
args = ap.parse_args()
ver = version()
dist = os.path.join(SKILL_ROOT, "dist")
os.makedirs(dist, exist_ok=True)
tag = "-full" if args.with_deps else ""
out = args.out or os.path.join(dist, f"html-explainer-v{ver}{tag}.zip")
count = 0
total = 0
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
for root, dirs, files in os.walk(SKILL_ROOT):
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS or
(args.with_deps and d == "node_modules")]
for f in files:
full = os.path.join(root, f)
rel = os.path.relpath(full, SKILL_ROOT)
if should_skip(rel, args.with_deps):
continue
arc = os.path.join("html-explainer", rel.replace("\\", "/"))
z.write(full, arc)
count += 1
total += os.path.getsize(full)
size = os.path.getsize(out)
print(f"✓ 打包完成:{out}")
print(f" 文件 {count} 个 · 源 {total / 1024:.0f}KB · zip {size / 1024:.0f}KB"
f"(压缩率 {100 - size * 100 / max(total, 1):.0f}%)")
if not args.with_deps:
print(" 注:未含 node/node_modules —— 目标机首次跑 `bash setup_env.sh --install` 装齐")
print()
print(" 带到新终端后:")
print(" 1. 解压到 ~/.workbuddy/skills/ (或任意位置,脚本按自身路径定位)")
print(" 2. bash setup_env.sh --install")
print(" 3. python scripts/new_project.py <name> && 编辑 narration.json …")
return 0
if __name__ == "__main__":
raise SystemExit(main())