ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+467
View File
@@ -0,0 +1,467 @@
"""Canonical real-API paper-to-lecture-video campaign for Experiment 5-5."""
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import os
import re
import shutil
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from dotenv import load_dotenv
from openai import OpenAI
HERE = Path(__file__).resolve().parent
PROTOCOL_PATH = HERE / "experiment_protocol.json"
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def sha256_bytes(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def sha256_file(path: Path) -> str:
return sha256_bytes(path.read_bytes())
def atomic_json(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
temporary.replace(path)
def run(command: list[str]) -> str:
process = subprocess.run(command, text=True, capture_output=True)
if process.returncode:
raise RuntimeError(f"command failed ({process.returncode}): {' '.join(command)}\n{process.stderr}")
return process.stdout
def probe(path: Path) -> dict[str, Any]:
return json.loads(run([
"ffprobe", "-v", "error", "-show_streams", "-show_format", "-of", "json", str(path)
]))
def duration(probe_result: dict[str, Any]) -> float:
return float(probe_result["format"]["duration"])
def parse_json_content(content: str) -> dict[str, Any]:
text = content.strip()
if text.startswith("```"):
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text, flags=re.S)
start, end = text.find("{"), text.rfind("}")
if start < 0 or end < start:
raise ValueError("provider response did not contain a JSON object")
value = json.loads(text[start:end + 1])
if not isinstance(value, dict):
raise ValueError("provider response was not a JSON object")
return value
def cached_json_call(
path: Path,
client: OpenAI,
*,
provider: str,
model: str,
messages: list[dict[str, Any]],
max_tokens: int,
) -> tuple[dict[str, Any], dict[str, Any]]:
signature = sha256_bytes(json.dumps({
"provider": provider, "model": model, "messages": messages,
}, ensure_ascii=False, sort_keys=True).encode())
if path.exists():
cached = json.loads(path.read_text(encoding="utf-8"))
if cached.get("signature") != signature:
raise RuntimeError(f"cached provider-call signature mismatch: {path}")
return cached["parsed"], cached["receipt"]
failed_attempts: list[dict[str, Any]] = []
for attempt in range(1, 4):
started = time.perf_counter()
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=1,
max_tokens=max_tokens,
response_format={"type": "json_object"},
)
raw = response.model_dump(mode="json")
receipt = {
"provider": provider,
"request": {"model": model, "messages": messages, "temperature": 1, "max_tokens": max_tokens, "response_format": {"type": "json_object"}},
"response": raw,
"elapsed_ms": round((time.perf_counter() - started) * 1000, 3),
"json_attempt": attempt,
}
try:
parsed = parse_json_content(response.choices[0].message.content or "")
except (ValueError, json.JSONDecodeError) as exc:
# Providers occasionally ignore response_format or truncate an
# object. Preserve the real failed receipt and retry the API; do
# not fabricate a narration or visual judgment to fill the page.
failed_attempts.append({"error": str(exc), "receipt": receipt})
atomic_json(
path.with_name(path.stem + ".failed-attempts.json"),
{"signature": signature, "attempts": failed_attempts},
)
if attempt == 3:
raise RuntimeError(
f"provider returned malformed JSON three times: {path}"
) from exc
continue
atomic_json(path, {"signature": signature, "parsed": parsed, "receipt": receipt})
return parsed, receipt
raise AssertionError("unreachable JSON retry loop")
def slide_sections(markdown: str) -> list[str]:
parts = markdown.split("\n---\n")
return [part.strip() for part in parts if "#" in part and "theme:" not in part]
def image_data_url(path: Path) -> str:
encoded = base64.b64encode(path.read_bytes()).decode()
return f"data:image/png;base64,{encoded}"
def provider_receipt_valid(receipt: dict[str, Any], expected_model: str) -> bool:
response = receipt.get("response") or {}
usage = response.get("usage") or {}
return bool(response.get("id") and response.get("model") == expected_model and usage.get("total_tokens") is not None)
def narration_prompt(section: str, page_index: int, total: int, feedback: str | None) -> str:
return f"""你是一位严谨而自然的中文技术讲师,正在讲解论文 Attention Is All You Need。
这是第 {page_index}/{total} 页。下面给出该页真实 Slidev 源码:
<slide>\n{section}\n</slide>
请写一段 180–240 个中文字符左右的口语讲解。必须满足:
1. 用引导性叙事解释“为什么”和“它与前后页的关系”,不能逐条照读幻灯片;
2. 明确指向当前屏幕上的可见元素(标题、公式、表格、数值或图片);有图片/表格/公式时必须说出观众该看哪里;
3. 不虚构源码中没有的数值或结论;开头/结尾与页序匹配;
4. 只返回 JSON{{"narration":"...","visual_references":["..."]}}
{('上一轮独立视觉审核反馈:' + feedback + '。请修正。') if feedback else ''}"""
def judge_prompt(narration: str, section: str) -> str:
return f"""你是独立的讲解视频视觉审核员。图片是当前真实幻灯片;源码和旁白如下。
<slide_source>\n{section}\n</slide_source>
<narration>\n{narration}\n</narration>
请只按图片中实际可见内容审核:
- visual_alignment: 15,旁白是否准确呼应可见标题/公式/表格/图片;
- guiding_narrative: 15,是否是讲解而非逐条复述;
- factual_fidelity: 15,是否无虚构;
- visible_elements_referenced: 列出旁白确实提到的可见元素;
- issues: 具体问题列表。
只返回 JSON 对象。"""
def synthesize_fish(text: str, reference_id: str, output: Path) -> dict[str, Any]:
from fish_audio_sdk import Session, TTSRequest
started = time.perf_counter()
request = TTSRequest(text=text, reference_id=reference_id, format="mp3")
audio = b"".join(Session(os.environ["FISH_API_KEY"]).tts(request, backend="s1"))
if len(audio) < 1024:
raise RuntimeError("Fish Audio returned empty or implausibly small audio")
output.write_bytes(audio)
return {
"provider": "Fish Audio",
"model": "s1",
"request": {
"text_sha256": sha256_bytes(text.encode()),
"text_characters": len(text),
"reference_id_sha256": sha256_bytes(reference_id.encode()),
"format": "mp3",
},
"response_artifact": {
"path": output.name,
"sha256": sha256_file(output),
"bytes": output.stat().st_size,
},
"elapsed_ms": round((time.perf_counter() - started) * 1000, 3),
}
def make_segment(slide: Path, audio: Path, output: Path, audio_duration: float) -> None:
run([
"ffmpeg", "-y", "-loglevel", "error", "-loop", "1", "-i", str(slide),
"-i", str(audio), "-c:v", "libx264", "-tune", "stillimage", "-pix_fmt", "yuv420p",
"-r", "30", "-vf", "scale=1280:720", "-c:a", "aac", "-b:a", "192k",
"-t", f"{audio_duration:.6f}", "-movflags", "+faststart", str(output),
])
def process_page(
run_dir: Path,
narration_client: OpenAI,
vision_client: OpenAI,
protocol: dict[str, Any],
page_number: int,
source_page: int,
slide: Path,
section: str,
reference_id: str,
) -> dict[str, Any]:
cache = run_dir / "provider_calls"
cache.mkdir(exist_ok=True)
max_rounds = int(protocol["acceptance"]["maximum_generation_review_rounds"])
narration_model = protocol["providers"]["narration"]["model"]
vision_model = protocol["providers"]["independent_visual_reviewer"]["model"]
attempts = []
feedback = None
selected = None
for attempt in range(1, max_rounds + 1):
messages = [{"role": "user", "content": narration_prompt(section, page_number, protocol["acceptance"]["pages"], feedback)}]
generated, narration_receipt = cached_json_call(
cache / f"page-{page_number:02d}-narration-{attempt}.json", narration_client,
provider="Moonshot", model=narration_model, messages=messages, max_tokens=1200,
)
narration = str(generated.get("narration") or "").strip()
if not narration:
raise RuntimeError(f"empty narration on page {page_number}, attempt {attempt}")
judge_messages = [{
"role": "user",
"content": [
{"type": "text", "text": judge_prompt(narration, section)},
{"type": "image_url", "image_url": {"url": image_data_url(slide)}},
],
}]
judged, judge_receipt = cached_json_call(
cache / f"page-{page_number:02d}-vision-{attempt}.json", vision_client,
provider="DashScope", model=vision_model, messages=judge_messages, max_tokens=1200,
)
scores = {
name: int(judged.get(name, 0) or 0)
for name in ("visual_alignment", "guiding_narrative", "factual_fidelity")
}
current = {
"attempt": attempt,
"narration": narration,
"visual_references": generated.get("visual_references") or [],
"judge": judged,
"scores": scores,
"narration_receipt": narration_receipt,
"vision_receipt": judge_receipt,
}
attempts.append(current)
if min(scores.values()) >= int(protocol["acceptance"]["minimum_visual_alignment_score"]):
selected = current
break
feedback = json.dumps(judged.get("issues") or judged, ensure_ascii=False)
if selected is None:
selected = max(attempts, key=lambda item: min(item["scores"].values()))
audio = run_dir / "audio" / f"page-{page_number:02d}.mp3"
audio.parent.mkdir(exist_ok=True)
tts_receipt_path = run_dir / "provider_calls" / f"page-{page_number:02d}-fish-tts.json"
expected_text_hash = sha256_bytes(selected["narration"].encode())
if tts_receipt_path.exists() and audio.exists():
tts_receipt = json.loads(tts_receipt_path.read_text(encoding="utf-8"))
if (
tts_receipt["request"]["text_sha256"] != expected_text_hash
or tts_receipt["response_artifact"]["sha256"] != sha256_file(audio)
):
raise RuntimeError(f"Fish TTS checkpoint mismatch for page {page_number}")
else:
tts_receipt = synthesize_fish(selected["narration"], reference_id, audio)
atomic_json(tts_receipt_path, tts_receipt)
audio_probe = probe(audio)
audio_duration = duration(audio_probe)
segment = run_dir / "segments" / f"page-{page_number:02d}.mp4"
segment.parent.mkdir(exist_ok=True)
make_segment(slide, audio, segment, audio_duration)
segment_probe = probe(segment)
segment_duration = duration(segment_probe)
return {
"page": page_number,
"source_page": source_page,
"slide": {"path": str(slide.relative_to(run_dir)), "sha256": sha256_file(slide), "bytes": slide.stat().st_size},
"section_sha256": sha256_bytes(section.encode()),
"selected_attempt": selected["attempt"],
"narration": selected["narration"],
"narration_characters": len(selected["narration"]),
"visual_references": selected["visual_references"],
"review": selected["judge"],
"scores": selected["scores"],
"attempts": attempts,
"tts_receipt": tts_receipt,
"audio": {"path": str(audio.relative_to(run_dir)), "sha256": sha256_file(audio), "bytes": audio.stat().st_size, "seconds": audio_duration, "probe": audio_probe},
"segment": {"path": str(segment.relative_to(run_dir)), "sha256": sha256_file(segment), "bytes": segment.stat().st_size, "seconds": segment_duration, "probe": segment_probe},
"av_delta_seconds": abs(segment_duration - audio_duration),
}
def concat_segments(run_dir: Path, pages: list[dict[str, Any]], output: Path) -> None:
listing = run_dir / "segments" / "concat.txt"
listing.write_text("".join(
f"file '{(run_dir / page['segment']['path']).resolve()}'\n" for page in pages
), encoding="utf-8")
run([
"ffmpeg", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", str(listing),
"-c", "copy", "-movflags", "+faststart", str(output),
])
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path)
parser.add_argument("--workers", type=int, default=3)
args = parser.parse_args()
load_dotenv(HERE / ".env")
required = ("MOONSHOT_API_KEY", "DASHSCOPE_API_KEY", "FISH_API_KEY")
missing = [name for name in required if not os.getenv(name)]
if missing:
raise RuntimeError(f"missing required real-provider credentials: {missing}")
if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
raise RuntimeError("ffmpeg and ffprobe are required")
protocol_bytes = PROTOCOL_PATH.read_bytes()
protocol = json.loads(protocol_bytes)
protocol_hash = sha256_bytes(protocol_bytes)
run_dir = (args.output or HERE / "validation" / "runs" / f"exp5-5-real-{datetime.now().strftime('%Y%m%d-%H%M%S')}").resolve()
run_dir.mkdir(parents=True, exist_ok=True)
protocol_copy = run_dir / "experiment_protocol.json"
if protocol_copy.exists() and protocol_copy.read_bytes() != protocol_bytes:
raise RuntimeError("run directory has a different frozen protocol")
protocol_copy.write_bytes(protocol_bytes)
source_spec = protocol["source"]
paper = (HERE / source_spec["paper"]).resolve()
markdown_path = (HERE / source_spec["slide_markdown"]).resolve()
rendered = (HERE / source_spec["rendered_slides"]).resolve()
for path in (paper, markdown_path, rendered):
if not path.exists():
raise RuntimeError(f"required Experiment 5-4 source is missing: {path}")
sections = slide_sections(markdown_path.read_text(encoding="utf-8"))
selected_pages = [int(value) for value in source_spec["selected_pages"]]
if len(selected_pages) != int(protocol["acceptance"]["pages"]):
raise RuntimeError("selected page count differs from protocol")
slides_dir = run_dir / "slides"
slides_dir.mkdir(exist_ok=True)
page_inputs = []
for page_number, source_page in enumerate(selected_pages, 1):
source_image = rendered / f"{source_page}.png"
copied = slides_dir / f"page-{page_number:02d}-source-{source_page:02d}.png"
if not copied.exists():
shutil.copy2(source_image, copied)
elif sha256_file(copied) != sha256_file(source_image):
raise RuntimeError(f"copied slide mutated: {copied}")
page_inputs.append((page_number, source_page, copied, sections[source_page - 1]))
voice_manifest_path = (HERE / protocol["providers"]["tts"]["authorized_reference_manifest"]).resolve()
voice_manifest = json.loads(voice_manifest_path.read_text(encoding="utf-8"))
reference_id = str(voice_manifest.get("source_reference_id") or "")
if not reference_id:
raise RuntimeError("authorized Fish voice manifest has no source_reference_id")
narration_provider = protocol["providers"]["narration"]
vision_provider = protocol["providers"]["independent_visual_reviewer"]
narration_client = OpenAI(api_key=os.environ["MOONSHOT_API_KEY"], base_url=narration_provider["endpoint"], timeout=180, max_retries=3)
vision_client = OpenAI(api_key=os.environ["DASHSCOPE_API_KEY"], base_url=vision_provider["endpoint"], timeout=180, max_retries=3)
page_results = []
with ThreadPoolExecutor(max_workers=args.workers) as pool:
futures = {
pool.submit(
process_page, run_dir, narration_client, vision_client, protocol,
page_number, source_page, slide, section, reference_id,
): page_number
for page_number, source_page, slide, section in page_inputs
}
for future in as_completed(futures):
page = futures[future]
result = future.result()
page_results.append(result)
print(f"page {page}/12 complete: {result['audio']['seconds']:.2f}s", flush=True)
page_results.sort(key=lambda item: item["page"])
final_video = run_dir / "lecture.mp4"
concat_segments(run_dir, page_results, final_video)
video_probe = probe(final_video)
video_duration = duration(video_probe)
audio_total = sum(page["audio"]["seconds"] for page in page_results)
streams = video_probe["streams"]
video_streams = [stream for stream in streams if stream.get("codec_type") == "video"]
audio_streams = [stream for stream in streams if stream.get("codec_type") == "audio"]
min_score = int(protocol["acceptance"]["minimum_visual_alignment_score"])
acceptance = {
"real_paper_and_experiment_5_4_slides": len(paper.read_bytes()) > 1_000_000 and len(page_results) == 12,
"twelve_selected_real_rendered_pages": len({page["slide"]["sha256"] for page in page_results}) == 12,
"live_narration_provider_receipts": all(
provider_receipt_valid(attempt["narration_receipt"], narration_provider["model"])
for page in page_results for attempt in page["attempts"]
),
"independent_live_vision_review_receipts": all(
provider_receipt_valid(attempt["vision_receipt"], vision_provider["model"])
for page in page_results for attempt in page["attempts"]
),
"visual_narrative_alignment": all(min(page["scores"].values()) >= min_score for page in page_results),
"real_fish_s1_audio_each_page": all(
page["tts_receipt"]["provider"] == "Fish Audio"
and page["tts_receipt"]["model"] == "s1"
and page["audio"]["bytes"] > 1024
for page in page_results
),
"per_page_display_matches_audio": all(
page["av_delta_seconds"] <= float(protocol["acceptance"]["per_page_av_alignment_tolerance_seconds"])
for page in page_results
),
"duration_5_to_15_minutes": float(protocol["acceptance"]["duration_seconds_min"]) <= video_duration <= float(protocol["acceptance"]["duration_seconds_max"]),
"final_duration_matches_page_audio": abs(video_duration - audio_total) <= float(protocol["acceptance"]["final_duration_tolerance_seconds"]),
"h264_video_and_aac_audio": bool(video_streams and audio_streams) and video_streams[0]["codec_name"] == "h264" and audio_streams[0]["codec_name"] == "aac",
"raw_artifact_hashes": all(page["audio"]["sha256"] and page["segment"]["sha256"] and page["slide"]["sha256"] for page in page_results),
}
acceptance["passed"] = all(acceptance.values())
evidence = {
"experiment_id": "5-5",
"status": "passed" if acceptance["passed"] else "partial",
"created_at": utc_now(),
"protocol_sha256": protocol_hash,
"source": {
"paper": str(paper), "paper_sha256": sha256_file(paper), "paper_bytes": paper.stat().st_size,
"slide_markdown": str(markdown_path), "slide_markdown_sha256": sha256_file(markdown_path),
"selected_source_pages": selected_pages,
"voice_manifest_sha256": sha256_file(voice_manifest_path),
"authorized_reference_id_sha256": sha256_bytes(reference_id.encode()),
},
"scope": {"pages": len(page_results), "narration_and_review_attempts": sum(len(page["attempts"]) for page in page_results)},
"summary": {"audio_seconds_sum": audio_total, "video_seconds": video_duration, "minutes": video_duration / 60, "max_av_delta_seconds": max(page["av_delta_seconds"] for page in page_results)},
"acceptance": acceptance,
"pages": page_results,
"final_video": {"path": final_video.name, "sha256": sha256_file(final_video), "bytes": final_video.stat().st_size, "probe": video_probe},
}
comparison = run_dir / "comparison.json"
atomic_json(comparison, evidence)
artifacts = {
str(path.relative_to(run_dir)): {"sha256": sha256_file(path), "bytes": path.stat().st_size}
for path in sorted(item for item in run_dir.rglob("*") if item.is_file() and item.name != "manifest.json")
}
manifest = {
"experiment_id": "5-5", "status": evidence["status"], "acceptance": acceptance,
"comparison_sha256": sha256_file(comparison), "artifacts": artifacts,
}
manifest_path = run_dir / "manifest.json"
atomic_json(manifest_path, manifest)
atomic_json(HERE / "validation" / "latest.json", {
"experiment_id": "5-5", "status": evidence["status"],
"run_dir": str(run_dir.relative_to(HERE)), "manifest_sha256": sha256_file(manifest_path),
})
print(json.dumps({"status": evidence["status"], "summary": evidence["summary"], "acceptance": acceptance}, ensure_ascii=False, indent=2))
return 0 if acceptance["passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())