This commit is contained in:
2026-04-24 16:02:16 +08:00
commit 1d6f0cc370
66 changed files with 9990 additions and 0 deletions

733
test_pipeline.py Normal file
View File

@@ -0,0 +1,733 @@
"""
故事提取流程 v4 - 逐步测试脚本
用法python test_pipeline.py <docx文件路径> [步骤编号]
步骤编号1=预处理, 2=判断故事, 3=切片提取, 4=重组, 5=生成故事, all=全部执行
"""
import sys
import os
import json
import asyncio
import time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "backend"))
from app.services.docx_parser import parse_transcript
from app.services.llm_client import llm_client, _parse_json
TEST_DIR = os.path.join(os.path.dirname(__file__), "backend", "uploads", "test")
def _save_json(filepath, data):
"""保存 JSON 到文件"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _save_text(filepath, text):
"""保存文本到文件"""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, "w", encoding="utf-8") as f:
f.write(text)
# ============================================================
# 第一步代码预处理v3时间范围截取允许重叠
# ============================================================
def step1_preprocess(lines, output_dir, save=True):
"""代码预处理:识别老师 → 过滤杂音 → 按时间范围截取每个学员的全部内容"""
print("\n" + "=" * 60)
print("第一步代码预处理v3时间范围截取")
print("=" * 60)
# 1.1 统计每个说话人
print(f"总行数: {len(lines)}")
speaker_stats = {}
for i, line in enumerate(lines):
speaker = line.get("speaker", "")
if not speaker:
continue
if speaker not in speaker_stats:
speaker_stats[speaker] = {"lines": 0, "chars": 0, "first": i, "last": i}
speaker_stats[speaker]["lines"] += 1
speaker_stats[speaker]["chars"] += len(line.get("content", ""))
speaker_stats[speaker]["last"] = i
print(f"\n说话人统计:")
for s, stats in sorted(speaker_stats.items(), key=lambda x: -x[1]["lines"]):
print(f" {s}: {stats['lines']}行, {stats['chars']}字, 行范围[{stats['first']}-{stats['last']}]")
# 1.2 识别老师(发言最多的人)
teacher = max(speaker_stats, key=lambda s: speaker_stats[s]["lines"])
print(f"\n识别老师: {teacher} ({speaker_stats[teacher]['lines']}行)")
# 1.3 过滤杂音(< 20句的说话人+ 排除老师
students = []
noise_speakers = []
for s, stats in speaker_stats.items():
if s == teacher:
continue
if stats["lines"] < 20:
noise_speakers.append(f"{s} ({stats['lines']}句)")
continue
students.append(s)
if noise_speakers:
print(f"\n杂音过滤: {', '.join(noise_speakers)}")
print(f"\n有效学员: {students}")
# 1.4 按时间范围截取:每个学员从第一句到最后一句,中间所有行全部截取
segments = []
for speaker in students:
stats = speaker_stats[speaker]
first_idx = stats["first"]
last_idx = stats["last"]
seg = lines[first_idx:last_idx + 1]
# 统计该学员在此范围内的发言
student_lines = [l for l in seg if l.get("speaker", "") == speaker]
student_chars = sum(len(l.get("content", "")) for l in student_lines)
segments.append((speaker, seg))
print(f" {speaker}: 截取行[{first_idx}-{last_idx}], 共{len(seg)}行, 学员{len(student_lines)}句/{student_chars}")
# 1.5 过滤:短段落 + 当事人占比过低(非主讲述人)
final_segments = []
filtered = []
for speaker, seg in segments:
student_lines = [l for l in seg if l.get("speaker", "") == speaker]
if len(student_lines) < 15:
filtered.append(f"{speaker} ({len(student_lines)}句<15)")
continue
student_chars = sum(len(l.get("content", "")) for l in student_lines)
if student_chars < 80:
filtered.append(f"{speaker} ({student_chars}字<80)")
continue
# 当事人说话占比 < 1/10 → 非主讲述人,丢弃
ratio = len(student_lines) / len(seg) if seg else 0
if ratio < 0.1:
filtered.append(f"{speaker} (占比{ratio:.1%}<10%)")
continue
final_segments.append((speaker, seg))
if filtered:
print(f"\n过滤: {', '.join(filtered)}")
print(f"\n最终: {len(final_segments)} 个学员段落")
for i, (speaker, seg) in enumerate(final_segments):
student_lines = [l for l in seg if l.get("speaker", "") == speaker]
student_chars = sum(len(l.get("content", "")) for l in student_lines)
ratio = len(student_lines) / len(seg) if seg else 0
print(f" 段落{i}: {speaker}, 截取{len(seg)}行, 学员{len(student_lines)}句/{student_chars}字, 占比{ratio:.1%}")
# 保存结果
if save:
step1_dir = os.path.join(output_dir, "step1_预处理")
# 保存每个学员段落
for i, (speaker, seg) in enumerate(final_segments):
text = _segment_to_text(seg)
safe_name = speaker.replace("/", "_")
_save_text(os.path.join(step1_dir, f"segment_{i}_{safe_name}.txt"), text)
print(f" 保存: segment_{i}_{safe_name}.txt ({len(text)}字)")
# 保存统计摘要
summary = {
"total_lines": len(lines),
"teacher": teacher,
"noise_filtered": noise_speakers,
"final_filtered": filtered,
"segments": [
{
"speaker": s,
"total_lines": len(seg),
"student_lines": len([l for l in seg if l.get("speaker", "") == s]),
"student_chars": sum(len(l.get("content", "")) for l in seg if l.get("speaker", "") == s),
"ratio": round(len([l for l in seg if l.get("speaker", "") == s]) / len(seg), 3) if seg else 0,
}
for s, seg in final_segments
]
}
_save_json(os.path.join(step1_dir, "summary.json"), summary)
print(f" 保存: summary.json")
return final_segments, teacher
def _get_main_speaker(student_lines):
if not student_lines:
return "unknown"
counts = {}
for l in student_lines:
s = l.get("speaker", "")
counts[s] = counts.get(s, 0) + 1
return max(counts, key=counts.get)
def _segment_to_text(seg):
parts = []
for line in seg:
speaker = line.get("speaker", "")
content = line.get("content", "")
if speaker:
parts.append(f"{speaker}: {content}")
else:
parts.append(content)
return "\n".join(parts)
# ============================================================
# 第二步:判断是否包含故事 + 提取事实(短段落合并处理)
# ============================================================
async def step2_identify(segments, output_dir, save=True):
"""短段落直接提取事实,长段落先判断是否有故事再提取"""
print("\n" + "=" * 60)
print("第二步:判断故事 + 提取事实")
print("=" * 60)
SHORT_THRESHOLD = 4000 # 短段落阈值(字数)
all_facts = []
step2_results = [] # 记录每个段落的处理结果
step2_t0 = time.time()
# 构建所有段落的异步任务,并行执行
async def process_segment(i, speaker, seg):
text = _segment_to_text(seg)
safe_name = speaker.replace("/", "_")
prefix = f"seg{i}_{safe_name}_"
print(f"\n--- 段落 {i}: {speaker} ({len(text)} 字) ---")
result_record = {
"index": i,
"speaker": speaker,
"text_length": len(text),
"is_short": len(text) <= SHORT_THRESHOLD,
}
if len(text) <= SHORT_THRESHOLD:
print(f" 短段落,跳过判断,直接提取事实")
facts = await _extract_facts_from_chunk(text, "", output_dir, prefix)
if facts:
hints = facts.get("story_hints", "")
if not hints:
hints = f"{speaker}的故事"
print(f" 提取完成: hints={hints}")
result_record["status"] = "extracted"
result_record["hints"] = hints
return (speaker, seg, hints, [facts]), result_record
else:
print(f" 提取失败,跳过")
result_record["status"] = "extract_failed"
return None, result_record
else:
is_story, hints = await _identify_story(text, output_dir, prefix)
print(f" 判断结果: is_story={is_story}, hints={hints}")
result_record["is_story"] = is_story
result_record["hints"] = hints
if is_story:
story_segments_for_step3 = [(speaker, seg, hints)]
step3_result = await step3_extract_facts(story_segments_for_step3, output_dir, save)
result_record["status"] = "has_story"
return step3_result, result_record
else:
print(f" 无故事,跳过")
result_record["status"] = "no_story"
return None, result_record
tasks = [process_segment(i, speaker, seg) for i, (speaker, seg) in enumerate(segments)]
results = await asyncio.gather(*tasks)
step2_elapsed = round(time.time() - step2_t0, 2)
print(f"\n⏱ 第二步总耗时: {step2_elapsed}s")
# 收集结果
for i, (r, record) in enumerate(results):
if r is not None:
if isinstance(r, list):
all_facts.extend(r)
else:
all_facts.append(r)
step2_results.append(record)
print(f"\n{len(all_facts)} 个故事段落完成事实提取")
# 保存结果
if save:
step2_dir = os.path.join(output_dir, "step2_判断提取")
_save_json(os.path.join(step2_dir, "results.json"), step2_results)
print(f" 保存: results.json")
return all_facts
async def _identify_story(text, output_dir=None, log_prefix="", do_save=True):
prompt = """判断这段对话是否包含学员的个人故事。直接输出JSON不要分析。
个人故事特征满足至少2条学员讲述具体经历、包含时间地点人物细节、表达深层情感、老师深入引导疗愈、揭示心理或家庭问题。
非故事内容:纯课堂讲解、简单问答寒暄、课堂管理、纯疗愈引导词。
直接输出JSON
{"is_story": true或false, "confidence": 0.0到1.0, "story_hints": "一句话描述故事主题和讲述者"}
对话内容:
```
""" + text[:4000] + """
```"""
try:
detail = await llm_client.chat_detail([{"role": "user", "content": prompt}], temperature=0.2)
api_log = {
"type": "identify_story",
"prompt": prompt,
"content": detail.get("content", ""),
"reasoning_content": detail.get("reasoning_content", ""),
"finish_reason": detail.get("finish_reason", ""),
"usage": detail.get("usage", {}),
"elapsed": detail.get("elapsed", 0),
}
print(f" {log_prefix}API耗时: {api_log['elapsed']}s, finish_reason={api_log['finish_reason']}, "
f"tokens: {api_log['usage'].get('total_tokens', '?')} "
f"(reasoning={api_log['usage'].get('reasoning_tokens', 0)})")
if output_dir and do_save:
_save_json(os.path.join(output_dir, "api_logs", f"{log_prefix}identify.json"), api_log)
result = _parse_json(detail["content"])
if result:
return bool(result.get("is_story", False)) and result.get("confidence", 0) >= 0.5, result.get("story_hints", "")
except Exception as e:
print(f" {log_prefix}错误: {e}")
return False, ""
# ============================================================
# 第三步切片提取事实Map
# ============================================================
async def step3_extract_facts(story_segments, output_dir, save=True):
print("\n" + "=" * 60)
print("第三步切片提取事实Map")
print("=" * 60)
all_facts = []
step3_t0 = time.time()
for i, (speaker, seg, hints) in enumerate(story_segments):
text = _segment_to_text(seg)
safe_name = speaker.replace("/", "_")
print(f"\n--- 故事 {i}: {speaker} - {hints[:50]}... ({len(text)} 字) ---")
chunks = _chunk_text_by_paragraphs(text, max_chars=3000, overlap_paragraphs=5)
print(f" 切成 {len(chunks)}")
# 保存切片信息
chunk_records = []
for j, chunk in enumerate(chunks):
chunk_records.append({"chunk_index": j, "chars": len(chunk), "preview": chunk[:100] + "..."})
tasks = [_extract_facts_from_chunk(chunk, hints, output_dir, f"story{i}_{safe_name}_chunk{j}_") for j, chunk in enumerate(chunks)]
results = await asyncio.gather(*tasks, return_exceptions=True)
facts = []
for j, r in enumerate(results):
if isinstance(r, Exception):
print(f"{j} 错误: {r}")
chunk_records[j]["status"] = "error"
chunk_records[j]["error"] = str(r)
else:
print(f"{j}: {json.dumps(r, ensure_ascii=False)[:200]}")
facts.append(r)
chunk_records[j]["status"] = "ok"
chunk_records[j]["facts"] = r
all_facts.append((speaker, seg, hints, facts))
# 保存切片和提取结果
if save:
step3_dir = os.path.join(output_dir, "step3_切片提取", f"story_{i}_{safe_name}")
for j, chunk in enumerate(chunks):
_save_text(os.path.join(step3_dir, f"chunk_{j}.txt"), chunk)
_save_json(os.path.join(step3_dir, "chunks_info.json"), chunk_records)
print(f" 保存: {len(chunks)} 个切片文件 + chunks_info.json")
step3_elapsed = round(time.time() - step3_t0, 2)
print(f"\n⏱ 第三步总耗时: {step3_elapsed}s")
return all_facts
def _chunk_text_by_paragraphs(text, max_chars=3000, overlap_paragraphs=5):
"""按自然段切分,在 max_chars 附近找段落结尾切,重叠 overlap_paragraphs 个自然段"""
paragraphs = [p for p in text.split("\n") if p.strip()]
if not paragraphs:
return [text]
if len(text) <= max_chars:
return [text]
chunks = []
i = 0
while i < len(paragraphs):
# 从当前位置开始,累加段落直到超过 max_chars
current = []
current_len = 0
j = i
while j < len(paragraphs):
para = paragraphs[j]
# 超过 max_chars 且已有内容 → 在这个段落前切断(段落结尾切)
if current_len + len(para) > max_chars and current:
break
current.append(para)
current_len += len(para)
j += 1
chunks.append("\n".join(current))
# 到末尾了就结束
if j >= len(paragraphs):
break
# 下一次起点:当前切到的位置 - 重叠段落数
next_i = j - overlap_paragraphs
# 防止死循环确保至少前进1个段落
if next_i <= i:
next_i = i + 1
i = next_i
return chunks
async def _extract_facts_from_chunk(text, hints, output_dir=None, log_prefix="", do_save=True):
prompt = f"""从这段对话中提取关键信息。只输出JSON。
故事主题:{hints}
提取以下内容:
- events: 发生的事件(尽量详细,保留具体细节)
- emotions: 表达的情感
- quotes: 讲述者的原话(保留原话,不要改写,尽量多提取;完全相同的重复语句只保留一条)
- key_people: 提到的人物
{{"events":["事件1","事件2"],"emotions":["情感1","情感2"],"quotes":["原话1","原话2","原话3"],"key_people":["人物1"]}}
对话内容:
```
{text}
```"""
detail = await llm_client.chat_detail([{"role": "user", "content": prompt}], temperature=0.1)
api_log = {
"type": "extract_facts",
"prompt": prompt,
"content": detail.get("content", ""),
"reasoning_content": detail.get("reasoning_content", ""),
"finish_reason": detail.get("finish_reason", ""),
"usage": detail.get("usage", {}),
"elapsed": detail.get("elapsed", 0),
}
print(f" {log_prefix}API耗时: {api_log['elapsed']}s, finish_reason={api_log['finish_reason']}, "
f"tokens: {api_log['usage'].get('total_tokens', '?')} "
f"(reasoning={api_log['usage'].get('reasoning_tokens', 0)})")
if output_dir and do_save:
_save_json(os.path.join(output_dir, "api_logs", f"{log_prefix}extract.json"), api_log)
result = _parse_json(detail["content"])
return result
# ============================================================
# 模糊去重工具函数
# ============================================================
def _fuzzy_dedup_quotes(quotes: list[str]) -> list[str]:
"""
模糊去重:对高度相似的语句只保留第一条。
策略:提取每条语句的核心部分(去掉常见前缀后缀),相似度超过阈值的视为重复。
"""
import re
def normalize(s: str) -> str:
s = s.strip()
if s.startswith("") and s.endswith(""):
s = s[1:-1]
for prefix in ["", "", "", "就是", "然后", "那个", "所以"]:
if s.startswith(prefix):
s = s[len(prefix):].strip()
for suffix in ["", "", "", "", "", ""]:
if s.endswith(suffix) and len(s) > 4:
s = s[:-1]
s = re.sub(r'(.)\1{2,}', r'\1', s)
return s
def similarity(a: str, b: str) -> float:
if not a or not b:
return 0.0
a_bigrams = set(a[i:i+2] for i in range(len(a)-1))
b_bigrams = set(b[i:i+2] for i in range(len(b)-1))
if not a_bigrams or not b_bigrams:
return 0.0
intersection = a_bigrams & b_bigrams
union = a_bigrams | b_bigrams
return len(intersection) / len(union)
kept = []
kept_normalized = []
for q in quotes:
nq = normalize(q)
is_dup = False
for kn in kept_normalized:
threshold = 0.8 if len(nq) < 15 else 0.7
if similarity(nq, kn) >= threshold:
is_dup = True
break
if not is_dup:
kept.append(q)
kept_normalized.append(nq)
removed = len(quotes) - len(kept)
if removed > 0:
print(f" 模糊去重: {len(quotes)} -> {len(kept)} (去除 {removed} 条相似重复)")
return kept
# ============================================================
# 第四步重组Reduce
# ============================================================
def step4_merge(all_facts, output_dir, save=True):
print("\n" + "=" * 60)
print("第四步重组Reduce")
print("=" * 60)
merged = []
for i, (speaker, seg, hints, facts_list) in enumerate(all_facts):
print(f"\n--- 故事 {i}: {speaker} ---")
all_events, all_emotions, all_quotes, all_people = [], [], [], []
for facts in facts_list:
if isinstance(facts, dict):
all_events.extend(facts.get("events", []))
all_emotions.extend(facts.get("emotions", []))
all_quotes.extend(facts.get("quotes", []))
all_people.extend(facts.get("key_people", []))
all_emotions = list(dict.fromkeys(all_emotions))
all_people = list(dict.fromkeys(all_people))
seen = set()
unique_quotes = [q for q in all_quotes if q not in seen and not seen.add(q)]
# 模糊去重:去除相似重复语句(如多次释放语句只保留一条)
unique_quotes = _fuzzy_dedup_quotes(unique_quotes)
material = {
"events": all_events,
"emotions": all_emotions,
"quotes": unique_quotes,
"key_people": all_people,
"story_hints": hints,
}
print(f" 事件: {len(all_events)}, 情感: {len(all_emotions)}, 原话: {len(unique_quotes)}, 人物: {len(all_people)}")
print(f" 素材: {json.dumps(material, ensure_ascii=False)[:500]}")
merged.append((speaker, seg, material))
# 保存结果
if save:
step4_dir = os.path.join(output_dir, "step4_重组")
for i, (speaker, seg, material) in enumerate(merged):
safe_name = speaker.replace("/", "_")
_save_json(os.path.join(step4_dir, f"material_{i}_{safe_name}.json"), material)
print(f" 保存: material_{i}_{safe_name}.json")
return merged
# ============================================================
# 第五步:生成最终故事
# ============================================================
async def step5_generate(merged, output_dir, save=True):
print("\n" + "=" * 60)
print("第五步:生成最终故事(并行)")
print("=" * 60)
step5_t0 = time.time()
async def generate_one(i, speaker, seg, material):
print(f"\n--- 生成故事 {i}: {speaker} ---")
prompt = f"""基于素材生成第三人称故事。只输出JSON不要其他内容。
素材:
主题:{material['story_hints']}
事件:{json.dumps(material['events'], ensure_ascii=False)}
情感:{json.dumps(material['emotions'], ensure_ascii=False)}
原话:{json.dumps(material['quotes'], ensure_ascii=False)}
人物:{json.dumps(material['key_people'], ensure_ascii=False)}
写作手法(非常重要):
【双时空结构】
故事有两个时空层:
- "当下时空":学员在课堂上与卢慧老师的对话——这是故事的框架,用对话体呈现,保留师生互动的完整脉络
- "过往时空":学员讲述的过去经历——这是故事的核心,用"场景重建"手法呈现,让读者穿越到那个时空
【场景重建手法(用于"过往的故事"部分)】
当学员讲述过去的经历时,不要写成"她说她小时候…"这种转述体,而是直接把读者拉进那个时空:
- 直接切入场景,用过去时态叙述。比如不要写"文晔回忆说她的儿子15岁去了美国",而要写"十五岁那年,文晔的儿子独自登上了飞往美国的航班"
- 用五感描写构建画面:她看到了什么、听到了什么、身体感受到了什么(比如"手心出汗""脚发凉"这些身体反应要融入场景中)
- 用环境细节暗示情绪:不要直接说"她很痛苦",而是通过场景让读者感受到痛苦
- 关键情感爆发处用直接引语(「」包裹原话),其他地方用间接引语和叙述自然融合
- 场景之间要有过渡,比如"然而,命运的转折发生在前年冬天——"
【对话体手法(用于"当下的困境""转变"部分)】
- 保留师生对话的现场感,写出谁说了什么、对方如何回应
- 在对话中穿插学员的内心活动、身体反应、情绪变化
- 老师的引导语要完整保留,这是疗愈过程的关键
【通用要求】
- 每个事件都要展开写,写出具体过程和细节,不要一句话概括
- 事件之间要有过渡和因果逻辑,不能跳跃
- 情感变化要写出过程:从什么情感转变到什么情感,中间经历了什么
- 不编造素材中没有的内容,但可以根据素材合理补充场景细节(如环境、氛围)
- 每个部分充分展开,不要限制长度,越详细越好
- 原文金句数组中的每条金句必须用英文双引号包裹,如 ["金句1", "金句2"],不要用「」替代双引号
禁止事项:
- 禁止用一句话概括多个事件(如"她倾诉了挣扎""她经历了疗愈过程"
- 禁止省略素材中的具体细节
- 禁止省略老师的引导和回应
- 禁止事件之间跳跃,要保持时间顺序和因果逻辑
- 禁止过度总结或抽象化描述
- 禁止在"过往的故事"部分全程使用转述体("她说…她回忆…"),必须用场景重建手法让读者身临其境
- 禁止为了简洁而牺牲内容的丰富性
输出格式:{{"title":"标题","summary":"80字摘要","tags":["标签"],"speaker_nickname":"昵称","content":{{"讲述者":"...","当下的困境":"...","过往的故事":"...","转变":"...","原文金句":["原话1","原话2","原话3"]}}}}"""
try:
detail = await llm_client.chat_detail([{"role": "user", "content": prompt}], temperature=0.7, max_tokens=16384)
raw = detail["content"]
api_log = {
"type": "generate_story",
"speaker": speaker,
"prompt": prompt,
"content": raw,
"reasoning_content": detail.get("reasoning_content", ""),
"finish_reason": detail.get("finish_reason", ""),
"usage": detail.get("usage", {}),
"elapsed": detail.get("elapsed", 0),
}
print(f" API耗时: {api_log['elapsed']}s, finish_reason={api_log['finish_reason']}, "
f"tokens: {api_log['usage'].get('total_tokens', '?')} "
f"(reasoning={api_log['usage'].get('reasoning_tokens', 0)})")
print(f" 原始输出长度: {len(raw)}")
if save:
step5_dir = os.path.join(output_dir, "step5_生成故事")
os.makedirs(step5_dir, exist_ok=True)
_save_text(os.path.join(step5_dir, f"raw_{i}_{speaker}.txt"), raw)
_save_json(os.path.join(output_dir, "api_logs", f"story{i}_{speaker}_generate.json"), api_log)
result = _parse_json(raw)
except Exception as e:
print(f" 生成失败: {e}")
result = None
if result:
print(f" 标题: {result.get('title', '')}")
print(f" 摘要: {result.get('summary', '')[:100]}")
content = result.get('content', {})
if isinstance(content, dict):
for key, val in content.items():
print(f" {key}: {str(val)[:80]}...")
print(f"\n 完整JSON:")
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(" 生成失败!")
return result
tasks = [generate_one(i, speaker, seg, material) for i, (speaker, seg, material) in enumerate(merged)]
results = await asyncio.gather(*tasks)
stories = [r for r in results if r is not None]
step5_elapsed = round(time.time() - step5_t0, 2)
print(f"\n⏱ 第五步总耗时: {step5_elapsed}s")
# 保存结果
if save:
step5_dir = os.path.join(output_dir, "step5_生成故事")
for i, story in enumerate(stories):
safe_name = story.get("speaker_nickname", f"story_{i}").replace("/", "_")
_save_json(os.path.join(step5_dir, f"story_{i}_{safe_name}.json"), story)
print(f" 保存: story_{i}_{safe_name}.json")
return stories
# ============================================================
# 主函数
# ============================================================
async def main():
if len(sys.argv) < 2:
print("用法: python test_pipeline.py <docx文件路径> [步骤编号]")
print(" 步骤编号: 1=预处理, 2=判断故事, 3=切片提取, 4=重组, 5=生成故事, all=全部")
sys.exit(1)
file_path = sys.argv[1]
step = sys.argv[2] if len(sys.argv) > 2 else "all"
if not os.path.exists(file_path):
print(f"文件不存在: {file_path}")
sys.exit(1)
# 根据文件名创建输出目录
basename = os.path.splitext(os.path.basename(file_path))[0]
output_dir = os.path.join(TEST_DIR, basename)
os.makedirs(output_dir, exist_ok=True)
print(f"文件: {file_path}")
print(f"输出目录: {output_dir}")
total_t0 = time.time()
lines = parse_transcript(file_path)
print(f"解析到 {len(lines)}")
# 如果只跑第5步从 step4 产物恢复
if step == "5":
step4_dir = os.path.join(output_dir, "step4_重组")
if os.path.exists(step4_dir):
print("从 step4 产物恢复...")
merged = []
for f in sorted(os.listdir(step4_dir)):
if f.endswith(".json"):
with open(os.path.join(step4_dir, f), "r", encoding="utf-8") as fp:
material = json.load(fp)
parts = f.replace("material_", "").replace(".json", "").split("_", 1)
speaker = parts[1] if len(parts) > 1 else f
merged.append((speaker, [], material))
print(f"恢复到 {len(merged)} 个故事")
stories = await step5_generate(merged, output_dir)
total_elapsed = round(time.time() - total_t0, 2)
print(f"\n{'=' * 60}")
print(f"✅ 全流程完成!总耗时: {total_elapsed}s")
print(f" 生成故事: {len(stories)}")
print(f"{'=' * 60}")
return
else:
print("step4 产物不存在请先跑前4步")
return
segments, teacher = step1_preprocess(lines, output_dir)
if step == "1":
return
all_facts = await step2_identify(segments, output_dir)
if step == "2":
return
if not all_facts:
print("没有发现故事段落,结束。")
return
merged = step4_merge(all_facts, output_dir)
if step == "4":
return
stories = await step5_generate(merged, output_dir)
if __name__ == "__main__":
asyncio.run(main())