Skip to content

Commit 8ede775

Browse files
nodeeeeeeclaude
andcommitted
Generate notes in English first, then translate as post-step
Removed the separate Chinese prompt set entirely. All note generation now uses English prompts for maximum quality (image placement, detail, structure). When a non-English language is selected, each generated section is translated via a separate LLM call (_translate function) that preserves all Markdown formatting, image references, LaTeX, and code blocks while translating only the prose text. This approach ensures: - Same number of images in Chinese and English notes - Correct image-text alignment (images placed by English prompts which the LLM handles best) - Full translation quality (dedicated translation prompt, not mixed generation+translation) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9b5d00e commit 8ede775

2 files changed

Lines changed: 65 additions & 120 deletions

File tree

note_generation.py

Lines changed: 54 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -82,112 +82,6 @@
8282

8383
_PROMPTS: dict[str, dict] = {
8484

85-
"zh": dict(
86-
system="""\
87-
你是一名顶尖大学计算机科学课程的助教,负责根据讲义和课堂录音为学生撰写高质量的中文学习笔记。
88-
89-
写作规范:
90-
1. 使用中文写作,专业技术术语保留英文并在括号内标注(如:进程 (Process))。
91-
2. **严禁使用第三人称叙述视角。** 不得出现「老师说」「教授指出」「老师要我们」「本课教授强调」等以讲师为主语的句式。
92-
笔记聚焦于**知识本身**,直接陈述概念、原理和结论,例如:
93-
- ✗「老师把问题定得很清楚:…」 → ✓「本课聚焦于…」
94-
- ✗「教授用了一个例子…」 → ✓「以下例子说明…」
95-
3. 内容以「概念→原理→示例→考试重点」逻辑展开,写成流畅的说明性段落,不要逐条罗列幻灯片。
96-
4. 数学公式使用 LaTeX:行内 $...$,单独公式 $$...$$。
97-
5. 代码示例必须是**可编译/可运行**的完整片段(包含必要的 #include、函数签名、main 等),使用正确的语法高亮(```c, ```cpp, ```python 等)。
98-
伪代码仅在真正没有对应真实代码时使用,并标注语言为 ```pseudo。
99-
6. 考试重点用:
100-
> [!IMPORTANT]
101-
> 内容
102-
7. 有趣类比或助记技巧用斜体。
103-
8. 图片插入规则(严格遵守):
104-
- **插入且仅插入含视觉元素的图片**:图表、流程图、架构图、代码截图、数学推导、数据可视化等。如果是与课程无关的行政或其他元素(如课程信息、投票二维码、签到提示等),即使是图片也不要插入。
105-
- 纯文字要点、定义、标题幻灯片不需要插入——笔记本身已用文字表达更好。
106-
- **每张图片必须内联放置,紧跟解释该图片概念的段落之后。** 绝对不要将多张图片堆叠在一起。每张图片的上方和下方都应有解释性文字。如果一个片段有 5 张相关图片,它们应分散在文本的 5 个不同位置,各自紧邻相关解释。
107-
- 幻灯片格式:`![Slide N](images/LXX/slide_NNN.png) *(一句话描述)*`
108-
- 屏幕录制帧格式:`![Frame N](images/LXX/frame_NNN.png) *(一句话描述)*`
109-
(LXX 由调用方提供,禁止自行修改)。
110-
9. 绝对禁止捏造原始材料中不存在的技术细节。
111-
""",
112-
chunk="""\
113-
请为以下课程片段({course_name} Lecture {lec_num}: {lec_title})撰写学习笔记。
114-
115-
## 本片段幻灯片列表
116-
{slide_outline}
117-
118-
## 教授录音逐字稿(按幻灯片顺序)
119-
{transcript_block}
120-
121-
## 可用图片(含图表/代码截图的幻灯片)
122-
{image_hints}
123-
124-
---
125-
126-
要求:
127-
- 本片段对应笔记的二级标题为 `### {lec_num}.{chunk_idx} {chunk_title}`(**不要输出此行**,由调用方添加)
128-
- 详细度:{detail}/10。{detail_instruction}
129-
- 图片插入:**插入所有含视觉元素的图片**(图表、流程图、架构图、代码截图、数学推导、数据可视化等),跳过纯文字截图和与课程无关的行政元素。
130-
路径必须完全照抄上方「可用图片」列表中给出的路径(含 images/L** 子目录),禁止自造路径。
131-
**关键:每张图片必须内联放在解释该概念的段落旁边,绝对不要将多张图片堆在一起。每张图片的上下都应有解释性文字。**
132-
格式:`![Slide N](path) *(说明)*` 或 `![Frame N](path) *(说明)*`
133-
- 代码示例写完整可编译片段(含必要 include/imports),用正确的语言标签(```c, ```cpp, ```python)。
134-
- 只写本片段内容,不要引入其他讲座的内容
135-
""",
136-
slide_only="""\
137-
请根据以下幻灯片内容,撰写 {course_name} Lecture {lec_num}: {lec_title} 的学习笔记。
138-
(该讲座没有录音,请结合你的 CS 知识展开解释。)
139-
140-
## 幻灯片内容
141-
{slide_outline}
142-
143-
## 可用图片
144-
{image_hints}
145-
146-
---
147-
148-
要求:
149-
- 本片段对应笔记的二级标题为 `### {lec_num}.{chunk_idx} {chunk_title}`(**不要输出此行**)
150-
- 详细度:{detail}/10。{detail_instruction}
151-
- 图片插入:**插入所有含视觉元素的图片**(图表、流程图、架构图、代码截图、数学推导、数据可视化等),跳过纯文字截图和与课程无关的行政元素。
152-
路径必须完全照抄上方「可用图片」列表中给出的路径(含 images/L** 子目录),禁止自造路径。
153-
**关键:每张图片必须内联放在解释该概念的段落旁边,绝对不要将多张图片堆在一起。每张图片的上下都应有解释性文字。**
154-
格式:`![Slide N](path) *(说明)*` 或 `![Frame N](path) *(说明)*`
155-
- 代码示例写完整可编译片段,用正确的语言标签(```c, ```cpp, ```python)。
156-
""",
157-
verify="""\
158-
请检查以下笔记片段中的技术术语是否与幻灯片一致,以及是否存在明显的事实错误。
159-
160-
**参考术语表(来自幻灯片):**
161-
{term_list}
162-
163-
**笔记片段:**
164-
{draft}
165-
166-
如果没有问题,直接回复 APPROVED(仅此一词)。
167-
如果有术语错误或事实错误,返回修正后的完整笔记片段(不加任何说明)。
168-
""",
169-
exam="""\
170-
以下是 {course_name} 的全部讲座笔记摘要。请在最后汇总一个考试速记章节。
171-
172-
格式要求:
173-
- 标题:`## Exam Notes`
174-
- 每条格式:`N. **考点名**:一句话说明`
175-
- 不超过 30 条,覆盖各讲座核心考点、公式、算法步骤、常见混淆点
176-
177-
笔记摘要:
178-
{summary}
179-
""",
180-
no_transcript="(本片段无录音逐字稿)",
181-
detail_instructions=[
182-
(range(0, 3), "极简要点:每个概念仅一行,不展开,每张幻灯片最多3条。"),
183-
(range(3, 6), "有层次的要点结构:每个主要概念一条一级bullet(`-`),"
184-
"其下最多2条二级bullet(` -`)补充关键细节。"
185-
"每张幻灯片合计不超过5条,禁止写连续段落。"),
186-
(range(6, 9), "详细段落:概念、原理、教授示例与类比全部包含。"),
187-
(range(9, 11), "最高详细度:包含所有细节、边界情况、与其他章节的联系及考点标注。"),
188-
],
189-
), # end zh
190-
19185
"en": dict(
19286
system="""\
19387
You are a teaching assistant at a top university, writing high-quality study notes for computer science courses based on lecture slides and audio transcripts.
@@ -207,7 +101,7 @@
207101
8. Image insertion rules (strictly follow):
208102
- **Insert all and only images that contain visual elements**: diagrams, flowcharts, architecture drawings, code screenshots, mathematical derivations, data visualizations, tables with meaningful structure, annotated figures, or any non-trivial visual illustration. Do NOT insert administrative or non-course elements (course info slides, polling QR codes, attendance prompts, etc.) even if they contain images.
209103
- Pure text slides (bullet points, definitions, titles) do not need images — the notes express text better than a screenshot.
210-
- **Each image MUST be placed inline, immediately after the paragraph that discusses the concept it illustrates.** NEVER group multiple images together in a cluster. Each image should have explanatory text both above and below it. If there are 5 relevant images in a segment, they should be spread across 5 different locations in the text, each adjacent to the related explanation.
104+
- **Each image MUST be placed inline, immediately after the paragraph that directly discusses the concept shown in that image.** Only insert an image when the paragraph above it is actually explaining the same concept the image depicts. If an image does not match any specific paragraph, skip it or move it to a more appropriate location. NEVER group multiple images together.
211105
- Format for slide images: `![Slide N](images/LXX/slide_NNN.png) *(one-sentence description)*`
212106
- Format for screen-capture frames: `![Frame N](images/LXX/frame_NNN.png) *(one-sentence description)*`
213107
(LXX is provided by the caller — do not modify it; the caption must be in parentheses wrapped in asterisks exactly as shown).
@@ -232,7 +126,7 @@
232126
- Detail level: {detail}/10. {detail_instruction}
233127
- Images: **insert all images that contain visual elements** (diagrams, charts, graphs, code screenshots, architecture drawings, data visualizations, mathematical derivations, etc.). Skip images of pure text, bullet points, or administrative/non-course elements.
234128
Copy the exact path from the "Available images" list above (including the images/L** subdirectory). Do not invent paths.
235-
**CRITICAL: place each image inline, immediately after the paragraph that explains the concept it illustrates — NEVER cluster multiple images together. Each image must have explanatory text both above and below it.**
129+
**CRITICAL: only insert an image directly after a paragraph that discusses the SAME concept the image depicts. The caption must describe what the image shows and connect to the paragraph above. If an image doesn't match any paragraph, skip it. NEVER cluster multiple images together.**
236130
Format: `![Slide N](path) *(caption)*` or `![Frame N](path) *(caption)*`
237131
- Code examples must be complete and compilable (with necessary includes/imports), using the correct language tag (```c, ```cpp, ```python).
238132
- Only cover the content in this segment; do not introduce material from other lectures.
@@ -254,7 +148,7 @@
254148
- Detail level: {detail}/10. {detail_instruction}
255149
- Images: **insert all images that contain visual elements** (diagrams, charts, graphs, code screenshots, architecture drawings, data visualizations, mathematical derivations, etc.). Skip images of pure text, bullet points, or administrative/non-course elements.
256150
Copy the exact path from the "Available images" list above (including the images/L** subdirectory). Do not invent paths.
257-
**CRITICAL: place each image inline, immediately after the paragraph that explains the concept it illustrates — NEVER cluster multiple images together. Each image must have explanatory text both above and below it.**
151+
**CRITICAL: only insert an image directly after a paragraph that discusses the SAME concept the image depicts. If an image doesn't match any paragraph, skip it. NEVER cluster multiple images together.**
258152
Format: `![Slide N](path) *(caption)*` or `![Frame N](path) *(caption)*`
259153
- Code examples must be complete and compilable, using the correct language tag (```c, ```cpp, ```python).
260154
""",
@@ -295,13 +189,20 @@
295189
} # end _PROMPTS
296190

297191

192+
# Language names for the translation instruction
193+
_LANG_NAMES = {"en": "English", "zh": "Chinese", "ja": "Japanese", "ko": "Korean"}
194+
195+
298196
def _P(key: str) -> str:
299-
"""Return the prompt string for the current NOTE_LANGUAGE, falling back to English."""
300-
return _PROMPTS.get(NOTE_LANGUAGE, _PROMPTS["en"])[key]
197+
"""Return the English prompt, with a translation instruction appended when
198+
NOTE_LANGUAGE is not English. All intermediate processing (system prompt,
199+
chunk prompt, verification) stays in English for best quality; only the
200+
final note output is translated word-by-word."""
201+
return _PROMPTS["en"][key]
301202

302203

303204
def _detail_instr(level: int) -> str:
304-
instrs = _PROMPTS.get(NOTE_LANGUAGE, _PROMPTS["en"])["detail_instructions"]
205+
instrs = _PROMPTS["en"]["detail_instructions"]
305206
for rng, txt in instrs:
306207
if level in rng:
307208
return txt
@@ -580,6 +481,39 @@ def _get_client_for(model: str):
580481
return _client_cache[p]
581482

582483

484+
def _translate(text: str, lang: str) -> str:
485+
"""Translate note text to the target language, preserving all Markdown
486+
formatting, image references, LaTeX formulas, and code blocks verbatim.
487+
Only prose text is translated; technical terms keep English with
488+
translation in parentheses on first use."""
489+
system = (
490+
f"You are a professional translator. Translate English study notes "
491+
f"into {lang}. You MUST translate every English sentence into {lang}. "
492+
f"Do NOT leave any prose in English. The output must read naturally "
493+
f"as {lang} text, not as English with annotations."
494+
)
495+
prompt = (
496+
f"Translate the following study notes entirely into {lang}.\n\n"
497+
f"Rules:\n"
498+
f"1. Every English sentence must become a {lang} sentence. Do NOT "
499+
f"leave English prose — the reader should be able to read the entire "
500+
f"note in {lang} without knowing English.\n"
501+
f"2. Technical terms: write the {lang} term first, then the English "
502+
f"in parentheses on first use. Example for Chinese: 子网掩码 (subnet mask).\n"
503+
f"3. Keep EXACTLY as-is without any modification:\n"
504+
f" - Image lines: ![Slide N](path) *(caption)* — translate ONLY "
505+
f"the caption text inside *(...)*, keep the path unchanged\n"
506+
f" - LaTeX: $...$ and $$...$$\n"
507+
f" - Code blocks: ```...```\n"
508+
f" - Callout markers: > [!IMPORTANT]\n"
509+
f" - Markdown formatting: ###, **, *, ---, etc.\n"
510+
f"4. Do NOT shorten, summarize, or omit any content.\n"
511+
f"5. Output ONLY the translated text.\n\n"
512+
f"---\n\n{text}"
513+
)
514+
return _call(NOTE_MODEL, system, prompt, len(text) * 3)
515+
516+
583517
def _call(model: str, system: str, user: str, max_tokens: int) -> str:
584518
"""Call any supported LLM (OpenAI, Gemini, Anthropic) with a text prompt."""
585519
client = _get_client_for(model)
@@ -895,6 +829,14 @@ def generate_section(
895829
else:
896830
tqdm.write(f" [warn] Verifier suspicious response, keeping draft")
897831

832+
# Translate to target language if not English
833+
if NOTE_LANGUAGE != "en" and draft:
834+
lang = _LANG_NAMES.get(NOTE_LANGUAGE, NOTE_LANGUAGE)
835+
tqdm.write(f" translating to {lang}…")
836+
_tt = _time.monotonic()
837+
draft = _translate(draft, lang)
838+
tqdm.write(f" ✓ translated ({_time.monotonic()-_tt:.0f}s)")
839+
898840
heading = f"### {lec_num}.{ci} {chunk_title}"
899841
content = f"{heading}\n\n{draft}"
900842
sec_file.write_text(content, encoding="utf-8")

test/test_language_and_skip.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,22 +170,25 @@ def test_language_en_not_printed_when_default(self):
170170
f"Language line should not appear without --language flag:\n{r.stdout}"
171171
)
172172

173-
def test_prompt_selection_respects_language(self):
174-
"""Verify _P() returns Chinese prompts when NOTE_LANGUAGE is 'zh'."""
173+
def test_prompts_always_english_translate_is_separate(self):
174+
"""Verify _P() always returns English prompts (translation is post-step)."""
175175
import note_generation
176-
# Default: English
177176
original = note_generation.NOTE_LANGUAGE
178177
try:
179178
note_generation.NOTE_LANGUAGE = 'en'
180179
en_sys = note_generation._P('system')
181-
assert 'note' in en_sys.lower(), (
182-
'English system prompt should contain English text')
180+
assert 'note' in en_sys.lower()
183181

184182
note_generation.NOTE_LANGUAGE = 'zh'
185183
zh_sys = note_generation._P('system')
186-
assert _CJK_RE.search(zh_sys), (
187-
'Chinese system prompt should contain CJK characters')
188-
assert en_sys != zh_sys, 'English and Chinese prompts should differ'
184+
# Prompts should be identical — language is handled by _translate()
185+
assert en_sys == zh_sys, (
186+
'Prompts should be the same regardless of language; '
187+
'translation is a separate post-generation step')
188+
189+
# _translate function should exist
190+
assert hasattr(note_generation, '_translate'), (
191+
'_translate function must exist for post-generation translation')
189192
finally:
190193
note_generation.NOTE_LANGUAGE = original
191194

0 commit comments

Comments
 (0)