文件预览

generate_ch04_ch07.py

查看 lobster-novel 技能包中的文件内容。

文件内容

scripts/generate_ch04_ch07.py

#!/usr/bin/env python3
"""
Generate volume_02 chapters 4-7 to bridge the gap.
Continuity from existing Ch03_初战告捷.md to Ch08_决赛前夕.md.
"""
import json, os, re, sys, time, urllib.request
from pathlib import Path

DEEPSEEK_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
API_URL = "https://api.deepseek.com/v1/chat/completions"
MODEL = "deepseek-chat"
NOVEL_DIR = Path(os.environ.get("NOVEL_DIR", "."))
CHAPTER_DIR = NOVEL_DIR / "chapters" / "volume_02"
PLAN_PATH = NOVEL_DIR / "plans" / "volume_02_plan.json"
BIBLE_PATH = NOVEL_DIR / "bible.json"
QC_SCRIPT = NOVEL_DIR / "review" / "quality_check.py"

plan_data = json.loads(PLAN_PATH.read_text("utf-8"))
bible_data = json.loads(BIBLE_PATH.read_text("utf-8"))
CHAPTERS_INFO = {ch["number"]: ch for ch in plan_data["chapters"]}

SYSTEM_PROMPT = """你是一个优秀的长篇中文网文作者,擅长「蓝晶翻译体」风格的西幻写作。

## 核心风格要求:
1. **翻译体语感**:大量使用破折号(——)做句中停顿和场景过渡,营造翻译体特有的叙事韵律
2. **纯中文写作**:不使用任何英文词汇
3. **对话格式**:对话不使用引号,用破折号+直角引号——「对话内容」——这样写
4. **第三人称有限视角**:主要从主角「理查德」的视角出发
5. **段落控制**:段落不宜太长,多用短段和空行分隔不同场景/动作/情绪
6. **暖丽细腻的场景描写 + 幽默诙谐的对话 + 人物鲜活的动作细节**
7. **章节字数**:每章4000-5000汉字
8. **章末钩子**:每章结尾必须留悬念(问句/意外事件/紧急状况)

## 角色性格:
- 理查德(嘴硬乐观/天生蛮力/有正义感也有私心/18岁/灰港镇出身)
- 梅丽安(外冷内热/博学冷静/38岁/帝国魔法学院叛逃学者)
- 托德(朴实忠诚/爱喝酒/23岁/理查德发小)
- 卡斯特(铁冠城佣兵公会执事/观察力敏锐)
- 艾德温·灰石勋爵(铁冠城贵族/对龙血有企图)
- 莉安娜·灰石(勋爵之女/对理查德异常热情)
- 铁牙(北地佣兵/行踪可疑)

## 已有章节内容回顾:
第1-3章已经写完:理查德抵达铁冠城→注册佣兵→完成第一个战斗任务(变异野猪)→完成多件D级任务→升至C级佣兵→接取B级任务(北山矿场盗匪+商道狼群)→建立名声→报名参加铁冠城竞技场"""


def call_deepseek(messages, temp=0.75, max_tokens=8192, timeout=300):
    payload = json.dumps({
        "model": MODEL,
        "messages": messages,
        "temperature": temp,
        "max_tokens": max_tokens,
    }).encode("utf-8")
    req = urllib.request.Request(
        API_URL, data=payload,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {DEEPSEEK_KEY}",
        })
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        data = json.loads(resp.read().decode("utf-8"))
    return data["choices"][0]["message"]["content"]


def count_chinese(text):
    return len([c for c in text if '\u4e00' <= c <= '\u9fff'])


def get_existing_chapter(num):
    for f in CHAPTER_DIR.glob(f"Ch{num:02d}_*.md"):
        return f.read_text("utf-8")
    return ""


def run_qc(path):
    import subprocess
    r = subprocess.run(["python3", str(QC_SCRIPT), str(path), "--json"],
                       capture_output=True, text=True, timeout=30)
    if r.returncode == 0 and r.stdout.strip():
        return json.loads(r.stdout)
    return None


def generate_chapter(chapter_num, prev_text):
    """Generate a chapter with continuity from prev_text."""
    info = CHAPTERS_INFO.get(chapter_num, {})
    title = info.get("title", "")
    summary = info.get("summary", "")
    scenes = info.get("scenes", 3)
    location = info.get("location", "")
    char_focus = info.get("character_focus", [])
    
    chars = bible_data.get("characters", {})
    char_summary = "\n".join([
        f"- {k}: {'、'.join(v.get('traits',[]))}"
        for k, v in chars.items()
    ])
    
    prompt = (
        f"# 第{chapter_num}章「{title}」\n\n"
        f"## 本章概要\n{summary}\n\n"
        f"## 场景数\n{scenes}个场景\n\n"
        f"## 地点\n{location}\n\n"
        f"## 重点角色\n{', '.join(char_focus)}\n\n"
        f"## 上一章结尾(关键连续性)\n{prev_text}\n\n"
        f"## 角色设定\n{char_summary}\n\n"
        f"---\n"
        f"请写出完整的第{chapter_num}章「{title}」文本。章末留悬念钩子。"
    )
    
    print(f"\n{'='*60}")
    print(f"Generating Ch{chapter_num:02d}: {title}...")
    sys.stdout.flush()
    
    text = call_deepseek([
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": prompt},
    ])
    
    if not text.startswith("# 第"):
        text = f"# 第{chapter_num}章 {title}\n\n---\n\n{text}"
    
    cn_count = count_chinese(text)
    text += f"\n\n【字数:约{cn_count}字】"
    
    safe_title = re.sub(r'[\\/:*?"<>|]', '', title)
    filename = f"Ch{chapter_num:02d}_{safe_title}.md"
    filepath = CHAPTER_DIR / filename
    filepath.write_text(text, "utf-8")
    print(f"  ✓ Saved: {filename} ({cn_count} Chinese chars)")
    sys.stdout.flush()
    
    # Quality check
    time.sleep(2)
    try:
        report = run_qc(filepath)
        if report:
            issues = report.get("issues", [])
            p0 = [i for i in issues if i.get("severity") == "P0"]
            p1 = [i for i in issues if i.get("severity") == "P1"]
            p2 = [i for i in issues if i.get("severity") == "P2"]
            print(f"  QC: P0={len(p0)}, P1={len(p1)}, P2={len(p2)}")
    except Exception as e:
        print(f"  QC error: {e}")
    
    return filepath


def main():
    # Continuity: read Ch03 ending
    ch03 = get_existing_chapter(3)
    prev_text = ch03[-1500:] if ch03 else "理查德完成了竞技场报名,准备迎接挑战。"
    
    chapters = list(range(4, 8))  # Ch04~Ch07
    results = {}
    
    for cn in chapters:
        try:
            path = generate_chapter(cn, prev_text)
            results[cn] = {"status": "ok", "path": str(path)}
            # Update prev_text for next chapter
            if path.exists():
                prev_text = path.read_text("utf-8")[-1500:]
        except Exception as e:
            print(f"  ✗ Ch{cn:02d} FAILED: {e}")
            import traceback; traceback.print_exc()
            results[cn] = {"status": "error", "error": str(e)}
        time.sleep(3)
    
    # Summary
    print(f"\n{'='*60}")
    print("COMPLETION REPORT (Ch04-Ch07):")
    total = 0
    for cn in chapters:
        p = Path(results.get(cn, {}).get("path", ""))
        if p.exists():
            t = p.read_text("utf-8")
            c = count_chinese(t)
            total += c
            print(f"  Ch{cn:02d}: {p.name} — {c}字")
        else:
            print(f"  Ch{cn:02d}: NOT WRITTEN")
    print(f"  Subtotal: {total} Chinese chars")
    print("Done.")

if __name__ == "__main__":
    main()