doco: v2说话人分段模式 — ASR说话人分离+大block拆分+三维动画解说识别
- asr_adapter: 新增roleType=1说话人分离参数,新增parse_order_result_with_speaker(),write_asr_result自动输出asr_v2_timed_spk.txt - fusion_align: 新增speaker-aware alignment v2流程(_annotate_b_lines_with_speakers区间匹配、_detect_speaker_blocks、SYSTEM_PROMPT_SPEAKER_ALIGN大block拆分prompt、_build_broadcast_segments支持block内多段拆分) - cli: 兼容v1/v2 stats字典 - 新增convert_to_md.py(20期融合A稿docx转md+YAML frontmatter) - backup_before_spk/: 修改前代码备份
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
将 20 期融合A稿 (.docx) 转为带 YAML frontmatter 的 .md 文件,
|
||||
供 TPS 主项目知识库 /api/knowledge/upload 批量导入。
|
||||
|
||||
用法:
|
||||
cd E:\tps-dashboard\doco
|
||||
python convert_to_md.py
|
||||
|
||||
产物落在 doco/deliverables/ 目录下,每期一个 .md 文件。
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
|
||||
PROGRAMS_DIR = Path(__file__).parent / "programs"
|
||||
OUTPUT_DIR = Path(__file__).parent / "deliverables"
|
||||
|
||||
# ep001 文件名没有编导和标题信息,需要手工补
|
||||
EP001_OVERRIDE = {
|
||||
"title": "现代防空反导大对决",
|
||||
"author": "", # ← 通哥填
|
||||
"date": "2026-06-12",
|
||||
}
|
||||
|
||||
|
||||
def find_fusion_docx(episode_dir: Path) -> Path | None:
|
||||
"""在 episode 目录里找融合A稿 docx(排除批改稿)。"""
|
||||
candidates = []
|
||||
for f in episode_dir.iterdir():
|
||||
if f.suffix == ".docx" and "融合A稿" in f.name and "批改" not in f.name:
|
||||
candidates.append(f)
|
||||
if not candidates:
|
||||
return None
|
||||
if len(candidates) == 1:
|
||||
return candidates[0]
|
||||
# 优先选带日期前缀的(ep002+ 格式)
|
||||
for c in candidates:
|
||||
if re.match(r"\d{8}", c.stem):
|
||||
return c
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def parse_metadata_from_filename(docx_path: Path, episode_dir_name: str) -> dict:
|
||||
"""
|
||||
从文件名提取元数据。
|
||||
ep002+ 文件名格式:{YYYYMMDD}{标题}_{编导}_融合A稿.docx
|
||||
ep001 特殊处理。
|
||||
"""
|
||||
if episode_dir_name.startswith("ep001"):
|
||||
return EP001_OVERRIDE.copy()
|
||||
|
||||
stem = docx_path.stem # e.g. "20260127潜艇的仿生之路_穆佩弦_融合A稿"
|
||||
|
||||
# 去掉 "_融合A稿" 后缀
|
||||
stem = re.sub(r"_融合A稿$", "", stem)
|
||||
|
||||
# 提取日期 (前8位)
|
||||
date_str = stem[:8]
|
||||
try:
|
||||
dt = datetime.strptime(date_str, "%Y%m%d")
|
||||
date_iso = dt.strftime("%Y-%m-%d")
|
||||
except ValueError:
|
||||
date_iso = ""
|
||||
|
||||
rest = stem[8:] # e.g. "潜艇的仿生之路_穆佩弦"
|
||||
|
||||
# 最后一个 _ 分隔编导
|
||||
parts = rest.rsplit("_", 1)
|
||||
if len(parts) == 2:
|
||||
title, author = parts
|
||||
else:
|
||||
title = rest
|
||||
author = ""
|
||||
|
||||
return {"title": title, "date": date_iso, "author": author}
|
||||
|
||||
|
||||
def docx_to_markdown(docx_path: Path) -> str:
|
||||
"""将融合A稿 docx 转为 markdown 正文。"""
|
||||
doc = Document(str(docx_path))
|
||||
lines = []
|
||||
|
||||
for para in doc.paragraphs:
|
||||
text = para.text.strip()
|
||||
if not text:
|
||||
continue
|
||||
|
||||
# 大标题(第一段,通常带书名号)
|
||||
if not lines and (text.startswith("《") or text.startswith("【") is False):
|
||||
lines.append(f"# {text}")
|
||||
lines.append("")
|
||||
continue
|
||||
|
||||
# 段头标签:【主持人1】【解说3】【专家2】【导视】隔断:【...】
|
||||
if re.match(r"^【.+?】$", text) or re.match(r"^隔断\d*[::]【.+?】", text):
|
||||
lines.append("")
|
||||
lines.append(f"## {text}")
|
||||
lines.append("")
|
||||
continue
|
||||
|
||||
# 普通正文段落
|
||||
lines.append(text)
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines).strip() + "\n"
|
||||
|
||||
|
||||
def build_frontmatter(meta: dict, episode_id: str) -> str:
|
||||
"""生成 YAML frontmatter。"""
|
||||
fm_lines = ["---"]
|
||||
fm_lines.append(f"标题: {meta['title']}")
|
||||
if meta["author"]:
|
||||
fm_lines.append(f"编导: {meta['author']}")
|
||||
if meta["date"]:
|
||||
fm_lines.append(f"播出日期: {meta['date']}")
|
||||
fm_lines.append("类型: 节目文稿")
|
||||
fm_lines.append(f"期号: {episode_id}")
|
||||
fm_lines.append("---")
|
||||
return "\n".join(fm_lines)
|
||||
|
||||
|
||||
def main():
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# 找所有 ep??? 目录(排除 ep002_004 这种非标准的)
|
||||
episode_dirs = sorted(
|
||||
d
|
||||
for d in PROGRAMS_DIR.iterdir()
|
||||
if d.is_dir() and re.match(r"^ep\d{3}_\d{8}_", d.name)
|
||||
)
|
||||
|
||||
print(f"找到 {len(episode_dirs)} 个期目录")
|
||||
success = 0
|
||||
errors = []
|
||||
|
||||
for ep_dir in episode_dirs:
|
||||
episode_id = ep_dir.name.split("_")[0] # ep001, ep002, ...
|
||||
docx_path = find_fusion_docx(ep_dir)
|
||||
|
||||
if docx_path is None:
|
||||
errors.append(f"{ep_dir.name}: 未找到融合A稿")
|
||||
continue
|
||||
|
||||
print(f" {episode_id}: {docx_path.name}")
|
||||
|
||||
meta = parse_metadata_from_filename(docx_path, ep_dir.name)
|
||||
frontmatter = build_frontmatter(meta, episode_id)
|
||||
body = docx_to_markdown(docx_path)
|
||||
|
||||
# 输出文件名:ep001_现代防空反导大对决.md
|
||||
safe_title = meta["title"].replace(":", ":").replace("/", "_")
|
||||
out_name = f"{episode_id}_{safe_title}.md"
|
||||
out_path = OUTPUT_DIR / out_name
|
||||
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(frontmatter + "\n\n" + body)
|
||||
|
||||
success += 1
|
||||
|
||||
print(f"\n完成:{success}/{len(episode_dirs)} 期转换成功")
|
||||
if errors:
|
||||
print("失败:")
|
||||
for e in errors:
|
||||
print(f" {e}")
|
||||
|
||||
print(f"\n产物目录:{OUTPUT_DIR}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user