第 1 卷(人物主线):第 1–3 集 + 人物表
主角线:Guspu Frillyknots / Ral Fastenhatchets / Alath Blottedmine 由线索挖掘器自动选定,按因果链切集,经 5 维度自评闸门(≥12 分)后入库。
This commit is contained in:
+154
-2
@@ -16,9 +16,26 @@ import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from dfannals import chronicle, factcheck, legends, publish, timeline
|
||||
from dfannals import (
|
||||
cast,
|
||||
chronicle,
|
||||
episodes,
|
||||
factcheck,
|
||||
legends,
|
||||
publish,
|
||||
threads,
|
||||
timeline,
|
||||
)
|
||||
from dfannals import slice as chapter_slice
|
||||
from dfannals.config import EXPORT_DIR, GITEA_WEB, chapter_dir, ensure_dirs, volume_dir
|
||||
from dfannals import volume as volume_mod
|
||||
from dfannals.config import (
|
||||
DATA_DIR,
|
||||
EXPORT_DIR,
|
||||
GITEA_WEB,
|
||||
chapter_dir,
|
||||
ensure_dirs,
|
||||
volume_dir,
|
||||
)
|
||||
|
||||
|
||||
def find_exports(export_dir: Path) -> list[Path]:
|
||||
@@ -142,6 +159,109 @@ def cmd_next(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_threads(args: argparse.Namespace) -> int:
|
||||
ensure_dirs()
|
||||
world = load_world(EXPORT_DIR)
|
||||
found = threads.find_threads(
|
||||
world,
|
||||
min_interactions=args.min_interactions,
|
||||
min_span=args.min_span,
|
||||
limit=args.top,
|
||||
)
|
||||
print(threads.render_report(world, found, per_thread=args.samples))
|
||||
path = threads.save_json(world, found, DATA_DIR / "threads.json")
|
||||
print(f"共 {len(found)} 条候选;明细已写入 {path}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_episodes(args: argparse.Namespace) -> int:
|
||||
ensure_dirs()
|
||||
world = load_world(EXPORT_DIR)
|
||||
found = threads.find_threads(
|
||||
world, min_interactions=args.min_interactions, min_span=args.min_span
|
||||
)
|
||||
if not found:
|
||||
print("没有候选线索")
|
||||
return 1
|
||||
if not 1 <= args.thread <= len(found):
|
||||
print(f"候选只有 {len(found)} 条,--thread 超出范围")
|
||||
return 1
|
||||
thread = found[args.thread - 1]
|
||||
_, planned = volume_mod.extended_plan(world, thread)
|
||||
print(episodes.render_plan(world, thread, planned))
|
||||
print()
|
||||
print(episodes.budget_report(planned))
|
||||
if args.samples:
|
||||
print("\n=== 前两集事件样例 ===")
|
||||
for ep in planned[:2]:
|
||||
print(f" 第 {ep.index} 集({ep.span})")
|
||||
for e in ep.events[: args.samples]:
|
||||
print(f" · {e.year}年 {e.type} " + " · ".join(world.render_refs(e)))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_cast(args: argparse.Namespace) -> int:
|
||||
ensure_dirs()
|
||||
world = load_world(EXPORT_DIR)
|
||||
found = threads.find_threads(
|
||||
world, min_interactions=args.min_interactions, min_span=args.min_span
|
||||
)
|
||||
if not found:
|
||||
print("没有候选线索")
|
||||
return 1
|
||||
if not 1 <= args.thread <= len(found):
|
||||
print(f"候选只有 {len(found)} 条,--thread 超出范围")
|
||||
return 1
|
||||
|
||||
thread = found[args.thread - 1]
|
||||
_, planned = volume_mod.extended_plan(world, thread)
|
||||
text, rows, suspicions = cast.build_cast(
|
||||
world, thread, planned, with_bios=not args.no_bios
|
||||
)
|
||||
if args.dry_run:
|
||||
print(text[:2000])
|
||||
return 0
|
||||
|
||||
state = chronicle.State.load()
|
||||
volume = state.volume if state.world == world.name else 1
|
||||
path = cast.write_cast(volume_dir(volume) / "cast.md", text)
|
||||
print(f"人物表已写入 {path}\n涵盖 {len(rows)} 人:{', '.join(r.name for r in rows)}")
|
||||
print(factcheck.report(suspicions) if suspicions else "专名校验:小传里的专名全部能在史料中找到。")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_episode(args: argparse.Namespace) -> int:
|
||||
ensure_dirs()
|
||||
world = load_world(EXPORT_DIR)
|
||||
found = threads.find_threads(
|
||||
world, min_interactions=args.min_interactions, min_span=args.min_span
|
||||
)
|
||||
if not found:
|
||||
print("没有候选线索")
|
||||
return 1
|
||||
|
||||
out_dir = None if args.dry_run else chapter_dir(1)
|
||||
produced = volume_mod.produce_episode(
|
||||
world, found, episode_index=args.index, out_dir=out_dir
|
||||
)
|
||||
|
||||
print("选择依据:" + produced.selection.reason)
|
||||
print(f"本集字数:{chronicle.cjk_length(produced.text)}")
|
||||
if args.dry_run:
|
||||
print("\n--- 试运行,不落盘 ---\n")
|
||||
print(produced.text[:1600])
|
||||
return 0
|
||||
|
||||
print(f"已写入 {produced.path}")
|
||||
if not args.no_push:
|
||||
publish.ensure_repo()
|
||||
head = publish.commit_and_push(
|
||||
f"第 1 卷第 {produced.episode.index} 集(人物主线):{produced.episode.span}"
|
||||
)
|
||||
print(f"已推送:{head or '(无改动)'} → {GITEA_WEB}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_run(args: argparse.Namespace) -> int:
|
||||
rc = cmd_index(args)
|
||||
if rc:
|
||||
@@ -163,6 +283,38 @@ def main(argv: list[str] | None = None) -> int:
|
||||
p = sub.add_parser("plan", help="显示切章方案")
|
||||
p.set_defaults(func=cmd_plan)
|
||||
|
||||
p = sub.add_parser("threads", help="挖掘人物线索候选")
|
||||
p.add_argument("--min-interactions", type=int, default=threads.DEFAULT_MIN_INTERACTIONS,
|
||||
help="强边阈值:一对人物至少反复互动多少次(默认 5)")
|
||||
p.add_argument("--min-span", type=int, default=threads.DEFAULT_MIN_SPAN,
|
||||
help="线索最小跨度年数(默认 30)")
|
||||
p.add_argument("--top", type=int, default=0, help="只显示前 N 条(0 为全部)")
|
||||
p.add_argument("--samples", type=int, default=3, help="每条线索展示几条事件样例")
|
||||
p.set_defaults(func=cmd_threads)
|
||||
|
||||
p = sub.add_parser("episodes", help="按人物线索切出剧集")
|
||||
p.add_argument("--thread", type=int, default=1, help="用第几条候选线索(默认 1)")
|
||||
p.add_argument("--min-interactions", type=int, default=threads.DEFAULT_MIN_INTERACTIONS)
|
||||
p.add_argument("--min-span", type=int, default=threads.DEFAULT_MIN_SPAN)
|
||||
p.add_argument("--samples", type=int, default=0, help="额外打印每集前 N 条事件")
|
||||
p.set_defaults(func=cmd_episodes)
|
||||
|
||||
p = sub.add_parser("cast", help="生成每卷人物表(表格 + 小传)")
|
||||
p.add_argument("--thread", type=int, default=1, help="用第几条候选线索(默认 1)")
|
||||
p.add_argument("--min-interactions", type=int, default=threads.DEFAULT_MIN_INTERACTIONS)
|
||||
p.add_argument("--min-span", type=int, default=threads.DEFAULT_MIN_SPAN)
|
||||
p.add_argument("--no-bios", action="store_true", help="只出表格,不让模型写小传")
|
||||
p.add_argument("--dry-run", action="store_true", help="只打印不落盘")
|
||||
p.set_defaults(func=cmd_cast)
|
||||
|
||||
p = sub.add_parser("episode", help="按人物主线产出下一集")
|
||||
p.add_argument("--index", type=int, default=1, help="写该线索的第几集(默认 1)")
|
||||
p.add_argument("--min-interactions", type=int, default=threads.DEFAULT_MIN_INTERACTIONS)
|
||||
p.add_argument("--min-span", type=int, default=threads.DEFAULT_MIN_SPAN)
|
||||
p.add_argument("--dry-run", action="store_true", help="只生成不落盘、不推送")
|
||||
p.add_argument("--no-push", action="store_true", help="落盘但不推送")
|
||||
p.set_defaults(func=cmd_episode)
|
||||
|
||||
p = sub.add_parser("next", help="生成下一章")
|
||||
p.add_argument("--dry-run", action="store_true", help="只生成不落盘、不推送")
|
||||
p.set_defaults(func=cmd_next)
|
||||
|
||||
Reference in New Issue
Block a user