- scripts/validate_episodes.py:结构/字段/时间戳/音频对账等校验 - scripts/fix_episodes.py:重算统计字段、引号归一、去空白(自动备份) - scripts/README.md:用法与已知数据缺口说明 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
117 lines
4.1 KiB
Python
117 lines
4.1 KiB
Python
#!/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()
|