#!/usr/bin/env python3 """修正 episodes.json 的廉价格式问题(会先备份原文件)。 只做安全、可逆性强的就地修正,**不臆造数据、不改正文语义**: 1. 重算统计字段,使其与实际一致: meta.totalEpisodes / meta.totalActs / meta.audioActs(新增) episode.totalLines / episode.hasAudio 2. 弯引号统一为直引号(“ ”→",‘ ’→'),便于点词/分词匹配 3. 去除 en 首尾空白 **刻意不做**(需人工/补料后另行处理): - 不剥离 ep18/19 的说话人前缀(来源不一,待补音频后统一) - 不删除任何 act/line,不修改时间戳数值,不为无音频集补造音频 用法: python3 scripts/fix_episodes.py # 就地修正,自动备份 python3 scripts/fix_episodes.py --dry-run # 只报告将改什么,不写 python3 scripts/fix_episodes.py --in X --out Y # 指定输入/输出 python3 scripts/fix_episodes.py --no-backup # 跳过备份 """ import argparse import datetime import json import os import shutil HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.dirname(HERE) DEFAULT_JSON = os.path.join(REPO, "family-album-usa", "episodes.json") QUOTE_MAP = {"“": '"', "”": '"', "‘": "'", "’": "'"} def normalize_quotes(s): for a, b in QUOTE_MAP.items(): s = s.replace(a, b) return s def fix(data): """就地修正 data,返回改动计数字典。""" changes = {"quotes": 0, "stripped": 0, "stats": 0} eps = data["episodes"] # 1. 正文:引号与空白 for e in eps: for a in e.get("acts", []): for l in a.get("lines", []): en = l.get("en", "") new = normalize_quotes(en) if new != en: changes["quotes"] += 1 en = new stripped = en.strip() if stripped != en: changes["stripped"] += 1 en = stripped l["en"] = en # 2. 统计字段重算 acts = [a for e in eps for a in e.get("acts", [])] meta = data.setdefault("meta", {}) def set_if_diff(d, k, v): if d.get(k) != v: d[k] = v changes["stats"] += 1 set_if_diff(meta, "totalEpisodes", len(eps)) set_if_diff(meta, "totalActs", len(acts)) set_if_diff(meta, "audioActs", sum(1 for a in acts if a.get("hasAudio"))) for e in eps: set_if_diff(e, "totalLines", sum(len(a.get("lines", [])) for a in e.get("acts", []))) set_if_diff(e, "hasAudio", any(a.get("hasAudio") for a in e.get("acts", []))) return changes def main(): p = argparse.ArgumentParser(description="修正 episodes.json 的格式问题") p.add_argument("--in", dest="src", default=DEFAULT_JSON, help="输入文件") p.add_argument("--out", dest="dst", default=None, help="输出文件(默认覆盖输入)") p.add_argument("--dry-run", action="store_true", help="只报告,不写文件") p.add_argument("--no-backup", action="store_true", help="不生成 .bak 备份") args = p.parse_args() dst = args.dst or args.src with open(args.src, encoding="utf-8") as f: data = json.load(f) changes = fix(data) print("将进行的改动:") print(f" 弯引号 -> 直引号 :{changes['quotes']} 处") print(f" 去首尾空白 :{changes['stripped']} 处") print(f" 重算统计字段 :{changes['stats']} 个") if args.dry_run: print("\n[dry-run] 未写入任何文件。") return # 备份(仅覆盖写时) if dst == args.src and not args.no_backup: stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") bak = f"{args.src}.{stamp}.bak" shutil.copy2(args.src, bak) print(f"\n已备份原文件 -> {bak}") with open(dst, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) f.write("\n") print(f"已写入 -> {dst}") print("建议接着运行:python3 scripts/validate_episodes.py") if __name__ == "__main__": main()