流水线开跑前先问用户用哪个 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 节点
125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
#!/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())
|