media_to_doc 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. media_to_doc-1.0.0/.github/workflows/ci.yml +92 -0
  2. media_to_doc-1.0.0/.gitignore +80 -0
  3. media_to_doc-1.0.0/.learnings/ERRORS.md +42 -0
  4. media_to_doc-1.0.0/.learnings/LEARNINGS.md +300 -0
  5. media_to_doc-1.0.0/CHANGELOG.md +71 -0
  6. media_to_doc-1.0.0/CLAUDE.md +329 -0
  7. media_to_doc-1.0.0/LICENSE +21 -0
  8. media_to_doc-1.0.0/PKG-INFO +448 -0
  9. media_to_doc-1.0.0/PRD.md +396 -0
  10. media_to_doc-1.0.0/README.md +343 -0
  11. media_to_doc-1.0.0/ROADMAP.md +594 -0
  12. media_to_doc-1.0.0/TDD.md +1164 -0
  13. media_to_doc-1.0.0/docs/MCP_INTEGRATION.md +268 -0
  14. media_to_doc-1.0.0/docs/RELEASE_NOTES_v1.0.0.md +198 -0
  15. media_to_doc-1.0.0/docs/installation.md +355 -0
  16. media_to_doc-1.0.0/examples/cross_project_demo.py +214 -0
  17. media_to_doc-1.0.0/handoff-docs-api-re-export-2026-07-19.md +430 -0
  18. media_to_doc-1.0.0/handoff-le-design-2026-07-18.md +301 -0
  19. media_to_doc-1.0.0/handoff-le-wiring-2026-07-19.md +409 -0
  20. media_to_doc-1.0.0/handoff-pipeline-w1-2026-07-18.md +361 -0
  21. media_to_doc-1.0.0/handoff-pipeline-w10-llm-health-2026-07-19.md +343 -0
  22. media_to_doc-1.0.0/handoff-pipeline-w10-real-LE-verify-2026-07-19.md +291 -0
  23. media_to_doc-1.0.0/handoff-pipeline-w11-gatekeeper-2026-07-19.md +202 -0
  24. media_to_doc-1.0.0/handoff-pipeline-w2-2026-07-18.md +393 -0
  25. media_to_doc-1.0.0/handoff-pipeline-w3-2026-07-18.md +419 -0
  26. media_to_doc-1.0.0/handoff-pipeline-w4-2026-07-18.md +459 -0
  27. media_to_doc-1.0.0/handoff-pipeline-w5-smoke-2026-07-18.md +257 -0
  28. media_to_doc-1.0.0/handoff-pipeline-w5-smoke-2026-07-19.md +244 -0
  29. media_to_doc-1.0.0/handoff-pipeline-w6-cli-2026-07-19.md +153 -0
  30. media_to_doc-1.0.0/handoff-pipeline-w7-mcp-2026-07-19.md +421 -0
  31. media_to_doc-1.0.0/handoff-research-2026-07-17.md +238 -0
  32. media_to_doc-1.0.0/handoff-skeleton-bootstrap-2026-07-18.md +349 -0
  33. media_to_doc-1.0.0/handoff-template.md +165 -0
  34. media_to_doc-1.0.0/pyproject.toml +159 -0
  35. media_to_doc-1.0.0/scripts/_w10a_check.py +59 -0
  36. media_to_doc-1.0.0/scripts/_w10a_poll.py +50 -0
  37. media_to_doc-1.0.0/scripts/_w10a_verify.py +68 -0
  38. media_to_doc-1.0.0/scripts/_w11a_consistency.py +121 -0
  39. media_to_doc-1.0.0/scripts/run_smoke.py +251 -0
  40. media_to_doc-1.0.0/src/media_to_doc/__init__.py +232 -0
  41. media_to_doc-1.0.0/src/media_to_doc/cli.py +697 -0
  42. media_to_doc-1.0.0/src/media_to_doc/config.py +147 -0
  43. media_to_doc-1.0.0/src/media_to_doc/llm/__init__.py +139 -0
  44. media_to_doc-1.0.0/src/media_to_doc/llm/anthropic.py +132 -0
  45. media_to_doc-1.0.0/src/media_to_doc/llm/base.py +265 -0
  46. media_to_doc-1.0.0/src/media_to_doc/llm/health.py +278 -0
  47. media_to_doc-1.0.0/src/media_to_doc/llm/ollama.py +150 -0
  48. media_to_doc-1.0.0/src/media_to_doc/llm/openai_compat.py +250 -0
  49. media_to_doc-1.0.0/src/media_to_doc/logger/__init__.py +75 -0
  50. media_to_doc-1.0.0/src/media_to_doc/logger/gatekeeper.py +221 -0
  51. media_to_doc-1.0.0/src/media_to_doc/logger/learnings.py +249 -0
  52. media_to_doc-1.0.0/src/media_to_doc/logger/pipeline_logger.py +292 -0
  53. media_to_doc-1.0.0/src/media_to_doc/mcp_server.py +930 -0
  54. media_to_doc-1.0.0/src/media_to_doc/paths.py +120 -0
  55. media_to_doc-1.0.0/src/media_to_doc/pipeline/__init__.py +90 -0
  56. media_to_doc-1.0.0/src/media_to_doc/pipeline/asr.py +237 -0
  57. media_to_doc-1.0.0/src/media_to_doc/pipeline/asr_correct.py +322 -0
  58. media_to_doc-1.0.0/src/media_to_doc/pipeline/audio.py +144 -0
  59. media_to_doc-1.0.0/src/media_to_doc/pipeline/chapters.py +443 -0
  60. media_to_doc-1.0.0/src/media_to_doc/pipeline/draft.py +449 -0
  61. media_to_doc-1.0.0/src/media_to_doc/pipeline/frames.py +286 -0
  62. media_to_doc-1.0.0/src/media_to_doc/pipeline/imagegen.py +347 -0
  63. media_to_doc-1.0.0/src/media_to_doc/pipeline/longdoc.py +728 -0
  64. media_to_doc-1.0.0/src/media_to_doc/pipeline/ocr.py +267 -0
  65. media_to_doc-1.0.0/src/media_to_doc/pipeline/render.py +449 -0
  66. media_to_doc-1.0.0/src/media_to_doc/pipeline/runner.py +660 -0
  67. media_to_doc-1.0.0/src/media_to_doc/pipeline/verify.py +500 -0
  68. media_to_doc-1.0.0/src/media_to_doc/state.py +173 -0
  69. media_to_doc-1.0.0/src/media_to_doc/utils/__init__.py +19 -0
  70. media_to_doc-1.0.0/src/media_to_doc/utils/ffmpeg_utils.py +214 -0
  71. media_to_doc-1.0.0/src/media_to_doc/utils/hash_utils.py +109 -0
  72. media_to_doc-1.0.0/src/media_to_doc/utils/progress.py +110 -0
  73. media_to_doc-1.0.0/task.md +608 -0
  74. media_to_doc-1.0.0/tests/__init__.py +13 -0
  75. media_to_doc-1.0.0/tests/conftest.py +19 -0
  76. media_to_doc-1.0.0/tests/test_cli.py +523 -0
  77. media_to_doc-1.0.0/tests/test_init.py +329 -0
  78. media_to_doc-1.0.0/tests/test_llm/__init__.py +0 -0
  79. media_to_doc-1.0.0/tests/test_llm/test_anthropic.py +205 -0
  80. media_to_doc-1.0.0/tests/test_llm/test_base.py +215 -0
  81. media_to_doc-1.0.0/tests/test_llm/test_health.py +295 -0
  82. media_to_doc-1.0.0/tests/test_llm/test_ollama.py +227 -0
  83. media_to_doc-1.0.0/tests/test_llm/test_openai_compat.py +388 -0
  84. media_to_doc-1.0.0/tests/test_logger/__init__.py +0 -0
  85. media_to_doc-1.0.0/tests/test_logger/test_gatekeeper.py +495 -0
  86. media_to_doc-1.0.0/tests/test_logger/test_learnings.py +390 -0
  87. media_to_doc-1.0.0/tests/test_logger/test_pipeline_logger.py +438 -0
  88. media_to_doc-1.0.0/tests/test_mcp_server.py +669 -0
  89. media_to_doc-1.0.0/tests/test_pipeline/__init__.py +3 -0
  90. media_to_doc-1.0.0/tests/test_pipeline/test_asr.py +198 -0
  91. media_to_doc-1.0.0/tests/test_pipeline/test_asr_correct.py +334 -0
  92. media_to_doc-1.0.0/tests/test_pipeline/test_audio.py +201 -0
  93. media_to_doc-1.0.0/tests/test_pipeline/test_chapters.py +434 -0
  94. media_to_doc-1.0.0/tests/test_pipeline/test_draft.py +450 -0
  95. media_to_doc-1.0.0/tests/test_pipeline/test_frames.py +259 -0
  96. media_to_doc-1.0.0/tests/test_pipeline/test_imagegen.py +313 -0
  97. media_to_doc-1.0.0/tests/test_pipeline/test_longdoc.py +426 -0
  98. media_to_doc-1.0.0/tests/test_pipeline/test_ocr.py +314 -0
  99. media_to_doc-1.0.0/tests/test_pipeline/test_render.py +390 -0
  100. media_to_doc-1.0.0/tests/test_pipeline/test_runner.py +724 -0
  101. media_to_doc-1.0.0/tests/test_pipeline/test_verify.py +402 -0
  102. media_to_doc-1.0.0/tests/test_smoke.py +192 -0
  103. media_to_doc-1.0.0/tests/test_utils/__init__.py +3 -0
  104. media_to_doc-1.0.0/tests/test_utils/test_ffmpeg_utils.py +151 -0
  105. media_to_doc-1.0.0/tests/test_utils/test_hash_utils.py +136 -0
  106. media_to_doc-1.0.0/uv.lock +3286 -0
  107. media_to_doc-1.0.0/workspace/inbox/.gitkeep +3 -0
  108. media_to_doc-1.0.0/workspace/work/.gitkeep +4 -0
@@ -0,0 +1,92 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master, feat/**, fix/**]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ # 取消同分支前序运行(节省 CI 资源)
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ # ───────────────────────────────────────────────────────────
16
+ # Python 后端测试与代码质量(Phase 0 启用)
17
+ # ───────────────────────────────────────────────────────────
18
+ python:
19
+ name: Python (windows-latest)
20
+ runs-on: windows-latest
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ python-version: ["3.11", "3.12"]
25
+
26
+ steps:
27
+ - name: Checkout
28
+ uses: actions/checkout@v4
29
+
30
+ - name: Install uv
31
+ uses: astral-sh/setup-uv@v3
32
+ with:
33
+ version: "0.11.x"
34
+ enable-cache: true
35
+ cache-dependency-glob: "uv.lock"
36
+
37
+ - name: Set up Python ${{ matrix.python-version }}
38
+ run: uv python install ${{ matrix.python-version }}
39
+
40
+ - name: Sync dependencies (with dev extras)
41
+ run: uv sync --extra dev --python ${{ matrix.python-version }}
42
+
43
+ - name: Lint (ruff)
44
+ run: uv run --python ${{ matrix.python-version }} ruff check src/ tests/
45
+
46
+ - name: Format check (ruff)
47
+ run: uv run --python ${{ matrix.python-version }} ruff format --check src/ tests/
48
+
49
+ - name: Type check (mypy, lenient)
50
+ run: uv run --python ${{ matrix.python-version }} mypy src/media_to_doc/
51
+ continue-on-error: true # Phase 0 类型注解宽松,逐步收紧
52
+
53
+ - name: Test (pytest)
54
+ run: uv run --python ${{ matrix.python-version }} pytest tests/ -v --maxfail=1
55
+
56
+ - name: Test coverage (pytest-cov,optional)
57
+ if: matrix.python-version == '3.12'
58
+ run: uv run --python ${{ matrix.python-version }} pytest tests/ --cov=media_to_doc --cov-report=term-missing
59
+ continue-on-error: true
60
+
61
+ # ───────────────────────────────────────────────────────────
62
+ # UI 构建(Phase 2+ 启用,Phase 0 注释掉)
63
+ # ───────────────────────────────────────────────────────────
64
+ # ui:
65
+ # name: UI (windows-latest)
66
+ # runs-on: windows-latest
67
+ # steps:
68
+ # - uses: actions/checkout@v4
69
+ # - uses: actions/setup-node@v4
70
+ # with:
71
+ # node-version: "20"
72
+ # - run: npm ci
73
+ # working-directory: ui
74
+ # - run: npm run lint
75
+ # working-directory: ui
76
+ # - run: npm run test
77
+ # working-directory: ui
78
+ # - run: npm run build
79
+ # working-directory: ui
80
+
81
+ # ───────────────────────────────────────────────────────────
82
+ # NSIS 安装器打包(Phase 3+ 启用)
83
+ # ───────────────────────────────────────────────────────────
84
+ # installer:
85
+ # name: NSIS Installer
86
+ # runs-on: windows-latest
87
+ # steps:
88
+ # - uses: actions/checkout@v4
89
+ # - name: Build installer
90
+ # run: |
91
+ # # NSIS 安装器构建脚本(Phase 3 实施时填实)
92
+ # echo "NSIS build placeholder"
@@ -0,0 +1,80 @@
1
+ # ─────────────────────────────────────────────────────────────
2
+ # media-to-doc .gitignore
3
+ # 遵循 CLAUDE.md §7 + ROADMAP Phase 0 + _research/ 不进 git
4
+ # ─────────────────────────────────────────────────────────────
5
+
6
+ # ── Python ──────────────────────────────────────────────────
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ *.so
11
+ .Python
12
+ *.egg-info/
13
+ *.egg
14
+ dist/
15
+ build/
16
+ .eggs/
17
+ .installed.cfg
18
+ *.egg-info
19
+
20
+ # ── 虚拟环境 ─────────────────────────────────────────────────
21
+ .venv/
22
+ venv/
23
+ env/
24
+ ENV/
25
+
26
+ # ── 测试与覆盖率 ────────────────────────────────────────────
27
+ .pytest_cache/
28
+ .coverage
29
+ .coverage.*
30
+ htmlcov/
31
+ coverage.xml
32
+ .tox/
33
+ .cache
34
+
35
+ # ── 类型检查 ─────────────────────────────────────────────────
36
+ .mypy_cache/
37
+ .ruff_cache/
38
+ .pyright_cache/
39
+
40
+ # ── 编辑器 / IDE ─────────────────────────────────────────────
41
+ .vscode/
42
+ .idea/
43
+ *.swp
44
+ *.swo
45
+ *~
46
+ .DS_Store
47
+ Thumbs.db
48
+
49
+ # ── 项目专属:研究材料不入 git(Phase 5 迁 LE 原型到 src/ 再纳入) ──
50
+ # _research/ 已纳入屏蔽(CLAUDE.md §4 + ROADMAP §0)
51
+ _research/
52
+
53
+ # ── 项目专属:运行时工作目录 ──────────────────────────────────
54
+ # workspace/ 运行时数据(inbox 输入 + work 中间产物 + raw 产出)
55
+ workspace/inbox/*
56
+ workspace/work/*
57
+ !workspace/inbox/.gitkeep
58
+ !workspace/work/.gitkeep
59
+
60
+ # ── 环境变量与密钥(严禁入 git,见 CLAUDE.md 安全红线) ──
61
+ .env
62
+ .env.*
63
+ !.env.example
64
+ *.pem
65
+ *.key
66
+
67
+ # ── 日志 ─────────────────────────────────────────────────────
68
+ *.log
69
+ logs/
70
+
71
+ # ── 模型与缓存(不进 git,用户本地保留) ───────────────────────
72
+ models/
73
+ *.safetensors
74
+ *.bin
75
+ *.pt
76
+ *.pth
77
+
78
+ # ── 构建产物 ─────────────────────────────────────────────────
79
+ *.spec
80
+ .mount/
@@ -0,0 +1,42 @@
1
+ # media-to-doc 项目级错误库 — ERRORS
2
+
3
+ > **目的**:追踪重复出现的错误模式(Pattern-Key),由 LE L4 进化层自动晋升。
4
+ > **晋升规则**:同一 Pattern-Key 在 `work/<course>/ERRORS.md` 中出现 ≥ 3 次时,自动写入本文件。
5
+ > **幂等**:已存在的 Pattern-Key 不会重复写入(参见 `_research/le_prototype/learnings.py:escalate_recurring_errors`)。
6
+ > **使用**:`PipelineLogger` 启动时读本文件,命中已知模式时注入警告或前置校验。
7
+
8
+ ---
9
+
10
+ ## 晋升条目(自动写入)
11
+
12
+ <!--
13
+ Phase 5 时 LE L4 落地后,自动追加形如:
14
+ ## [Connection:ollama]
15
+ **First promoted**: 2026-07-18 09:30:00
16
+ **Occurrences**: 3 (across 5 shown)
17
+ **Threshold**: 3
18
+ **Auto-detected**: True
19
+ **Examples**:
20
+ - `course1/ERRORS.md`
21
+ - `course2/ERRORS.md`
22
+ - `course3/ERRORS.md`
23
+
24
+ **Recommended action**: review / write rule / patch code
25
+ -->
26
+
27
+ (Phase 0 尚无条目,Phase 5 LE L4 落地后由 `post_pipeline_hook` 自动追加)
28
+
29
+ ---
30
+
31
+ ## 手动录入条目(可选)
32
+
33
+ ```markdown
34
+ ## [Custom:keyword]
35
+
36
+ **First seen**: YYYY-MM-DD HH:MM:SS
37
+ **Occurrences**: N
38
+ **Description**: <错误描述>
39
+ **Root cause**: <根因>
40
+ **Fix**: <修复方法>
41
+ **Affected code**: <src/.../file.py:line>
42
+ ```
@@ -0,0 +1,300 @@
1
+ # media-to-doc 项目级学习库 — LEARNINGS
2
+
3
+ > **目的**:积累项目开发过程中沉淀的最佳实践(best_practice),供未来会话/开发者复用。
4
+ > **格式**:每条 LP-YYYYMMDD-NNN 条目,包含 标题 / 上下文 / 做法 / 启示 四部分。
5
+ > **写入**:Claude 在每个里程碑 commit 时主动追加;也可人工编辑。
6
+ > **关联**:`../_research/le_prototype/`(LE 原型)、`../PRD.md` §4.1.G、`../TDD.md` §4.5。
7
+ > **W9 实装**(首批 LP 条目):W1-W8 关键 best_practice 沉淀。
8
+
9
+ ---
10
+
11
+ ## 模板
12
+
13
+ ```markdown
14
+ ### LP-YYYYMMDD-NNN — <标题>
15
+
16
+ **上下文**: <何时何地遇到的问题 / 学到的经验>
17
+ **做法**: <具体如何解决 / 实践>
18
+ **启示**: <对未来开发者的建议 / 通用原则>
19
+ **相关文件**: <src/.../file.py:line>、<docs/...>
20
+ **作者**: <Claude 自动 / 用户 / 协作者>
21
+ ```
22
+
23
+ ---
24
+
25
+ ## 条目
26
+
27
+ ### LP-20260718-001 — 11 stage 函数签名统一 `(work: Path, config: WorkflowConfig)`
28
+
29
+ **上下文**:W1 把 11 stage 从占位转成实装,每个 stage 都需要拿到 work 目录 + config,
30
+ 签名不统一会让 runner `_invoke_stage` 写大量 `if/else` 分发。
31
+
32
+ **做法**:
33
+ - 全部 stage 函数签名:`(work: Path, config: WorkflowConfig) -> <StageResult>`
34
+ - runner 用统一 `_invoke_stage(stage, func, ctx)` 包装,ctx 注入 `inbox / state / logger`
35
+ - chapters / draft / longdoc 三个 LLM 阶段走 wrapper(`_chapters_wrapper` 等)从 config 派生 LLM provider
36
+
37
+ **启示**:流水线 stage 签名统一 = runner 极简,新增 stage 只需在 `STAGE_FUNCS` 注册一行。
38
+
39
+ **相关文件**:`src/media_to_doc/pipeline/runner.py:50-90`(STAGE_FUNCS / _chapters_wrapper),
40
+ `src/media_to_doc/pipeline/runner.py:287-407`(_invoke_stage 三分支)
41
+ **作者**:Claude W1
42
+
43
+ ---
44
+
45
+ ### LP-20260718-002 — LLM provider 用 ABC + lazy import,缺库时给清晰 ImportError
46
+
47
+ **上下文**:W2 接 3 个 LLM provider(ollama / anthropic / openai_compat),用户可能没装全部 SDK。
48
+
49
+ **做法**:
50
+ - 基类 `BaseLLMProvider`(`llm/base.py`)用 `abc.ABC`,子类只实现 `_chat_impl`
51
+ - 实际 SDK(ollama / anthropic / openai)在子类 `__init__` 内 lazy import
52
+ - 缺库时 `raise ImportError("请 pip install media-to-doc[llm_anthropic]")`
53
+ - `llm/__init__.py` 顶层只暴露基类 + 工厂函数,3 个具体 provider 通过 `_register_defaults()` 延迟注册
54
+
55
+ **启示**:ABC 强约束接口 + lazy import 强隔离重依赖,测试时可只装 ollama。
56
+
57
+ **相关文件**:`src/media_to_doc/llm/base.py`、`src/media_to_doc/llm/anthropic.py`、`src/media_to_doc/llm/ollama.py`
58
+ **作者**:Claude W2
59
+
60
+ ---
61
+
62
+ ### LP-20260718-003 — chapters JSON 解析做宽松适配(围栏 / 前缀文字 / `[` 到 `]` 切片)
63
+
64
+ **上下文**:W2 chapters 阶段用 LLM 输出章节列表,LLM 输出经常不规范(带 ```json 围栏 / 前缀文字 / 解释行)。
65
+
66
+ **做法**:`chapters.py:_parse_chapters_response` 三重容错:
67
+
68
+ 1. 先 strip markdown 围栏(```json ... ```)
69
+ 2. 正则找第一个 `[` 到最后一个 `]` 切片
70
+ 3. 解析失败时用 json.JSONDecoder.raw_decode 从 `[` 开始逐字符尝试
71
+
72
+ **启示**:LLM 输出永远不要相信,用最宽松的解析策略 + 友好错误信息。
73
+
74
+ **相关文件**:`src/media_to_doc/pipeline/chapters.py`(W2 实装)
75
+ **作者**:Claude W2
76
+
77
+ ---
78
+
79
+ ### LP-20260718-004 — imagegen ABC + Protocol 双轨(产品代码 ABC,测试 duck-typed)
80
+
81
+ **上下文**:W3 imagegen 阶段要支持 2 种 provider(local_sdxl / skip)+ 未来插件扩展。
82
+
83
+ **做法**:
84
+ - 产品代码用 `abc.ABC` 强制 `generate(prompt) -> Path` 接口
85
+ - 测试用 `typing.Protocol` duck-typed mock(无需继承 ABC),让测试 fixture 更轻量
86
+ - `SkipProvider.generate()` 直接返回 None,render 阶段检测到 None 时退化警告文字
87
+
88
+ **启示**:ABC 用于运行时多态,Protocol 用于测试替身 — 双轨互不干扰。
89
+
90
+ **相关文件**:`src/media_to_doc/pipeline/imagegen.py`(W3)
91
+ **作者**:Claude W3
92
+
93
+ ---
94
+
95
+ ### LP-20260718-005 — render 阶段缺失图自动退化为警告文字
96
+
97
+ **上下文**:W3 render 时 chapters.json 引用 `gen_001.png` 等图,但 imagegen skip 时图不存在。
98
+
99
+ **做法**:`render.py` 拼装 HTML 时:
100
+
101
+ 1. 先扫 `images/` 子目录,把存在的图存进 `_existing_images: set[str]`
102
+ 2. 渲染时若引用图不在 set,输出 `<p><em>[图片缺失: gen_001.png]</em></p>` 而非坏链接
103
+ 3. verify 阶段检查 image_refs 与实际文件一致
104
+
105
+ **启示**:缺失外部资源永远不要 raise,降级显示比中断流水线更有价值。
106
+
107
+ **相关文件**:`src/media_to_doc/pipeline/render.py:150-220`(W3)
108
+ **作者**:Claude W3
109
+
110
+ ---
111
+
112
+ ### LP-20260718-006 — draft 默认输出到 `<work>/chapters/raw/<stem>/`,后续可注入 inbox
113
+
114
+ **上下文**:W3 draft 阶段产物应放哪里?work 中间产物 vs inbox 最终产物?
115
+
116
+ **做法**:
117
+ - 默认:`<work>/chapters/raw/<stem>/chapter_NN.md`(中间产物,可清空重跑)
118
+ - runner 接受 `output_dir` 参数,W5 smoke 跑通时把它注入到 `inbox/raw/<stem>/`
119
+ - `render` 阶段从 `_resolve_drafts_dir(work)` 派生路径,与 default 一致
120
+
121
+ **启示**:中间产物与最终产物物理隔离,方便 resume / 重跑 / 归档。
122
+
123
+ **相关文件**:`src/media_to_doc/pipeline/runner.py:407-419`(_resolve_drafts_dir)、`src/media_to_doc/pipeline/draft.py`
124
+ **作者**:Claude W3
125
+
126
+ ---
127
+
128
+ ### LP-20260718-007 — Ollama `num_ctx` 默认 65536,长 transcript 调 LLM 必备
129
+
130
+ **上下文**:W5 smoke 跑真实课程(50816 tokens transcript)时 Ollama 报"exceeds context size"。
131
+
132
+ **根因**:Ollama `num_ctx` 默认 4096,Qwen3-14B 原生 32k(可 RoPE 扩展到 65k)。
133
+
134
+ **做法**:`LLMConfig.num_ctx = 65536`(默认),`OllamaProvider` 创建时显式传 `num_ctx`。
135
+ chapters 阶段 prompt 也做 30000 chars 截断(留 system prompt + 输出空间)。
136
+
137
+ **启示**:本地 LLM 调长 prompt 时,context window 是隐性瓶颈,必须显式声明。
138
+
139
+ **相关文件**:`src/media_to_doc/config.py:43`、`src/media_to_doc/llm/ollama.py`
140
+ **作者**:Claude W5
141
+
142
+ ---
143
+
144
+ ### LP-20260718-008 — longdoc 默认 `provider="skip"`,规则清理兜底
145
+
146
+ **上下文**:W4 longdoc 阶段要不要默认调 LLM?
147
+
148
+ **决策**:**默认 skip,只跑规则清理**(去时间戳 / 合并空行)。LLM 净化是可选项。
149
+
150
+ **做法**:
151
+ - `PipelineConfig.longdoc_llm_provider = "skip"`(默认)
152
+ - `process_long_doc` 检测 `provider=None` 时走纯规则分支
153
+ - CI 离线环境(无 GPU)可全跑通过,真用时再 `--longdoc-llm anthropic`
154
+
155
+ **启示**:默认行为应该是最保守 / 最兼容的(零 GPU 依赖),高级功能用 opt-in flag 开启。
156
+
157
+ **相关文件**:`src/media_to_doc/config.py:80`、`src/media_to_doc/pipeline/longdoc.py`
158
+ **作者**:Claude W4
159
+
160
+ ---
161
+
162
+ ### LP-20260719-009 — Gatekeeper image_refs 候选路径 3 重试(md-link / wiki-link / images 子目录)
163
+
164
+ **上下文**:W8 gatekeeper 检查 lecture.md 引用的图片是否存在。原型只查 basename,W3-W5 后
165
+ 产物布局变了(图可能在 `images/` 子目录,引用可能是 wiki-link `![[foo.png]]` 或 md-link `![alt](images/foo.png)`)。
166
+
167
+ **做法**:对每个 image_ref,候选路径 = 3 个:
168
+
169
+ ```python
170
+ candidates = [
171
+ lecture_dir / ref, # 原路径
172
+ lecture_dir / basename, # 同目录(wiki-link)
173
+ lecture_dir / "images" / basename, # images 子目录(W3 render 默认)
174
+ ]
175
+ ```
176
+
177
+ 任一存在即 OK,3 个都不存在才报 missing。
178
+
179
+ **启示**:产物布局会演化,文件存在性检查永远做候选路径重试,不要假设单一布局。
180
+
181
+ **相关文件**:`src/media_to_doc/logger/gatekeeper.py`(W8)
182
+ **作者**:Claude W8
183
+
184
+ ---
185
+
186
+ ### LP-20260719-010 — PipelineLogger 三层 try/except 异常隔离,LE 失败不破坏 run_pipeline
187
+
188
+ **上下文**:W8 LE 接入 runner 末尾:`gatekeeper_check` / `logger.finalize` / `post_pipeline_hook`
189
+ 任何一项失败(磁盘满 / 权限不够),都不应破坏 `run_pipeline` 的 return 值。
190
+
191
+ **做法**:`run_pipeline` 末尾三层 try/except,每个 catch 后只 `print(..., file=sys.stderr)`:
192
+
193
+ ```python
194
+ try:
195
+ ... # 主流水线
196
+ finally:
197
+ try: gatekeeper = gatekeeper_check(work)
198
+ except Exception as exc: print(f"[le] gatekeeper failed: {exc}", file=sys.stderr)
199
+ try: pipeline_run = logger.finalize(...)
200
+ except Exception as exc: print(f"[le] logger.finalize failed: {exc}", file=sys.stderr)
201
+ try: post_pipeline_hook(work)
202
+ except Exception as exc: print(f"[le] post_pipeline_hook failed: {exc}", file=sys.stderr)
203
+ ```
204
+
205
+ **启示**:LE 是辅助 / 沉淀层,不是调度真相。state.json(主真相)始终先 save,LE 失败可观察但不致命。
206
+
207
+ **相关文件**:`src/media_to_doc/pipeline/runner.py:540-585`(W8)
208
+ **作者**:Claude W8
209
+
210
+ ---
211
+
212
+ ### LP-20260719-011 — `assess_llm_health.total_runs` 只计成功解析的 run_file
213
+
214
+ **上下文**:W8 health 评估跨 run 的 LLM 失败率。原 `assess_llm_health` 把损坏的 JSON run_file 也计入 `total_runs`,污染统计。
215
+
216
+ **做法**:引入 `parsed_runs` 单独计数,只在 `json.loads` 成功时 +1;`total_runs` 字段 = 成功解析数。
217
+ 损坏文件只 warn,不计入分母。
218
+
219
+ **启示**:跨 run 聚合统计要严格区分"目录数" vs "有效数据数",分母必须可信。
220
+
221
+ **相关文件**:`src/media_to_doc/logger/learnings.py:129-198`(W8)
222
+ **作者**:Claude W8
223
+
224
+ ---
225
+
226
+ ### LP-20260719-012 — PEP 562 `__getattr__` 实现 lazy import,重依赖按需加载
227
+
228
+ **上下文**:W9 让 `from media_to_doc import run_pipeline` 跨项目可用,但 `import media_to_doc` 不应触发
229
+ faster-whisper / diffusers / anthropic 等重依赖。
230
+
231
+ **做法**:
232
+ - `__init__.py` 用 PEP 562 模块级 `__getattr__(name)`,从 `_LAZY_EXPORTS: dict[str, str]` 查目标模块路径
233
+ - 用户访问 `media_to_doc.run_pipeline` 时才 `importlib.import_module("media_to_doc.pipeline.runner")`
234
+ - 首次访问后缓存到 `globals()[name]`,后续访问走正常属性查找
235
+ - `__dir__()` 列出所有公开符号,IDE 自动补全可用
236
+
237
+ **启示**:Python 3.7+ 的 PEP 562 是包顶层 re-export 的最佳实践,比 `import *` 更可控。
238
+
239
+ **相关文件**:`src/media_to_doc/__init__.py`(W9)、`tests/test_init.py`(26 用例)
240
+ **作者**:Claude W9
241
+
242
+ ---
243
+
244
+ ### LP-20260719-013 — 测试不要真跑 11 stage,monkeypatch 是已验证模式
245
+
246
+ **上下文**:11 stage 涉及 ffmpeg / faster-whisper / SDXL,CI 环境无法真跑。
247
+
248
+ **做法**:
249
+ - `tests/test_pipeline/test_runner.py` 用 monkeypatch 把 `STAGE_FUNCS[stage]` 替换为 mock 函数
250
+ - mock 函数只写产物文件,不真调重依赖
251
+ - 验证:`state.stages[stage].status == "completed"` + 文件存在
252
+ - 同样模式适用 `test_llm/*`(mock provider)、`test_imagegen/*`(Protocol duck-typed)
253
+
254
+ **启示**:单测 / 集成测中,重依赖全部 mock。E2E 测单独跑(本项目 `scripts/run_smoke.py`)。
255
+
256
+ **相关文件**:`tests/test_pipeline/test_runner.py`(W4-W8 共用模式)
257
+ **作者**:Claude W1-W8
258
+
259
+ ---
260
+
261
+ ### LP-20260719-014 — stdout 留给 JSON,所有日志走 stderr
262
+
263
+ **上下文**:CLI 的 `--json` 输出与 MCP 的 stdio JSON-RPC 都依赖 stdout 纯净。调试 log 走 stdout 会破坏输出。
264
+
265
+ **做法**:
266
+ - CLI:`sys.stdout.write(json.dumps(...))`,不用 `console.print`(Rich 把 `[...]` 当 markup)
267
+ - MCP server:handler 内 `print(..., file=sys.stderr)`,stdout 留给 JSON-RPC 帧
268
+ - logger 配置:Python `logging` 默认走 stderr
269
+
270
+ **启示**:stdout = 数据契约,stderr = 人类观察。混用就是埋雷。
271
+
272
+ **相关文件**:`src/media_to_doc/cli.py`(eprint helper)、`src/media_to_doc/mcp_server.py`(_log helper)
273
+ **作者**:Claude W6 + W7
274
+
275
+ ---
276
+
277
+ ### LP-20260719-015 — `StageContext.metrics` 做跨 stage 累积容器,wrapper 注册 + 末尾聚合
278
+
279
+ **上下文**:流水线中多个 stage 需要累积运行时指标(LLM 调用统计、计时、外部 SDK 句柄等),传统做法是 module-level 全局变量 / 参数透传,但前者多 run 冲突,后者改 STAGE_FUNCS 接口。
280
+
281
+ **做法**:
282
+ - `StageContext` 加 `metrics: dict[str, Any]` 字段,默认 `default_factory=lambda: {"llm_providers": {}}`
283
+ - 3 个 LLM wrapper(`_chapters_wrapper` / `_draft_wrapper` / `_longdoc_wrapper`)签名从 `(work, config)` 改为 `(ctx)`,创建 provider 后注册 `ctx.metrics["llm_providers"][stage_name] = provider`
284
+ - `run_pipeline` 把 `ctx = StageContext(...)` 创建移到 `for` loop 外,让 metrics 跨 stage 累积
285
+ - run_pipeline 末尾(LE finally 块)调聚合 helper:`_aggregate_llm_health(ctx.metrics)` → 喂给 `logger.finalize(llm_health=...)`
286
+ - 聚合 key 用 `{stage_name}_{provider.name}`(跨 stage 不冲突 + `assess_llm_health` 仍 sum)
287
+
288
+ **启示**:跨 stage 累积 = ctx 设计模式。ctx 已经存在(共享 inbox/work/config),直接扩 fields 不引入新概念。配合 wrapper 接受 ctx + 聚合 helper,模块化和测试都不破坏。**通用模式**:任何需要在多步骤流水线末尾聚合运行时数据的场景(Pipeline / Builder / Saga 模式),都适合 "ctx.metrics + 注册 + 末尾聚合" 三件套。
289
+
290
+ **相关文件**:`src/media_to_doc/pipeline/runner.py`(StageContext / 3 wrapper / `_aggregate_llm_health`)
291
+ **作者**:Claude W10-C
292
+
293
+ ---
294
+
295
+ ## 沉淀规则
296
+
297
+ - 每条 LP 条目必须有 **上下文 / 做法 / 启示** 三段,缺一不收
298
+ - "启示"段必须可复用(其它项目也适用),不能只描述本项目特定 case
299
+ - W+ 数字 = W1/W2/.../W9 的开发会话标识,新增条目按 `LP-YYYYMMDD-NNN` 自增编号
300
+ - 重复出现的 ERRORS.md Pattern-Key 由 LE L4 进化层自动晋升到本文件(参见 `.learnings/ERRORS.md`)
@@ -0,0 +1,71 @@
1
+ # Changelog
2
+
3
+ All notable changes to `media-to-doc` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ---
9
+
10
+ ## [1.0.0] - 2026-07-20
11
+
12
+ ### 🎉 First stable release
13
+
14
+ 11 阶段流水线全跑通,3 种调用方式(CLI / Python API / MCP server),Loop Engineering 五层闭环端到端接入。
15
+ W10-A 真端到端验证:107 分钟中文培训视频 3 小时 57 分钟跑完,529 测试 0 失败。
16
+
17
+ ### Added
18
+
19
+ - **11 阶段流水线** (W1-W4):`audio → asr → frames → ocr → asr_correct → chapters → draft → imagegen → render → longdoc → verify`
20
+ - **可插拔 LLM**:Ollama(默认) / Anthropic / OpenAI-compatible(MiniMax、DeepSeek、智谱、Moonshot、混元、OpenRouter 等 7 个 preset)
21
+ - **可跳过重模块**:`imagegen_provider=skip` 让 Claude 自己做配图,`longdoc_llm_provider=skip` 用规则清理代替 LLM 净化
22
+ - **3 种调用方式**(W6 / W7 / W9):
23
+ - **CLI** `mtd run | resume | status | list | doctor | config | mcp`
24
+ - **Python API** PEP 562 `__getattr__` 顶层 re-export,52 个公开符号 lazy import,启动 < 100ms
25
+ - **MCP server** 8 工具(W7=6 + W8=2),stdio JSON-RPC,Claude Desktop / Codex / Cline 原生集成
26
+ - **Loop Engineering 五层闭环**(W8,见 `.learnings/` LEARNINGS.md / ERRORS.md):
27
+ - L1 执行层:`timed_stage(logger, stage)` 包裹每 stage
28
+ - L2 审核层:`gatekeeper_check(work)` 4 项机器可验证
29
+ - L3 沉淀层:`pipeline_run.json` 写盘,含 `llm_health` / `gatekeeper_passed` / `quality` / `errors`
30
+ - L4 进化层:`post_pipeline_hook` 扫 Pattern-Key 自动晋升到 `.learnings/ERRORS.md`
31
+ - L5 健康度:`assess_llm_health` 失败率 > 10% → `switch_provider` 建议,> 20% → `reduce_chunk` 建议
32
+ - **跨 run 健康度查询** (W8):`get_run_metrics(work)` / `list_runs(workspace)`,Python API + MCP 工具等价
33
+ - **状态持久化与断点续跑** (W4 / W6):`state.json` 11 stage 调度真相 + `pipeline_run.json` LE 沉淀双轨;`mtd resume <work>` 自动从 state 派生 inbox
34
+ - **可分发的产物**:图片一律相对路径(`<stem>/images/gen_*.png`),产物目录整盘复制到任何电脑/上传网盘/丢知识库路径不失效
35
+ - **LEARNINGS 系统**:14 条 LP-YYYYMMDD-NNN best_practice 条目(W1-W8 沉淀)
36
+
37
+ ### Changed
38
+
39
+ - **产物布局**(W3 / W4):render 输出从 `<drafts_dir>/<stem>.md` 移到 `<drafts_dir>/<stem>/<stem>.md`,longdoc 写 `<drafts_dir>/<stem>_cleaned.md` + `<drafts_dir>/<stem>_final.html`(W5 兼容旧布局)
40
+ - **Ollama 默认 num_ctx** = 65536(W5 long transcript 支持 qwen3:14b 32K RoPE 扩展)
41
+ - **chapters prompt transcript 截断** = 30000 chars(W5 适配 32K context 留 system 余量)
42
+
43
+ ### Fixed
44
+
45
+ - **OCR 输出路径不一致**(W5):runner ocr 阶段不传 output_dir → OCR 写 `inbox/img/ocr/`,asr_correct 读 `work/ocr/` 不匹配 → JSONDecodeError;统一写到 `work/ocr/`
46
+ - **Ollama 上下文超长**(W5):`num_ctx=None` → Ollama 默认 4096 → 50K tokens 超 32K max → 4 stage 全部失败;`num_ctx=65536` 修
47
+ - **transcript 截断缺失**(W5):`_load_transcript` 不限长度 → chapters prompt 50K tokens → 默认 → 显式截 30000 chars
48
+ - **longdoc/verify 路径布局兼容**(W5):`db92ac9` `_check_outputs_exist` 兼容新旧两布局
49
+ - **W8 llm_health 聚合自动**(W10-C `bddc387`):`StageContext.metrics` 跨 stage 累积,`_aggregate_llm_health` helper 替换原 `{}` TODO
50
+ - **Gatekeeper vs Verify 不一致**(W11-A `d2b39d3`):gatekeeper resolver 写死 W4 原型路径,verify 迁 W5 新布局后两者对同一份数据给相反结论;新布局优先 + 旧布局回退,image_refs 加 `<stem>/images/` 候选
51
+
52
+ ### Tested
53
+
54
+ - **529 pytest 用例 / 0 跳过**:W10-C 519 → W11-A +10 → 529。涵盖 11 stage 单元测试 + LE 闭环 + CLI + MCP server + 一致性回归
55
+ - **W5 真实端到端冒烟**:1.3GB / 112min 中文培训视频 CPU 模式 ASR + frames + OCR + LLM 全跑通
56
+ - **W10-A 真端到端验收**:395MB / 107min 中文培训视频 CPU 模式 3h57min,llm_health 真聚合 chapters_ollama(1 calls) + draft_ollama(6 calls),0 failures
57
+
58
+ ---
59
+
60
+ ## [0.1.0-dev] - 2026-07-18
61
+
62
+ 开发起点。骨架 + 5 个占位模块(14 测试)。
63
+
64
+ ### Added
65
+
66
+ - 项目骨架:uv init + pyproject.toml + src/ + tests/ + workspace/ + .learnings/ + .github/workflows/
67
+ - 5 个占位模块:`__init__` / `cli` / `paths` / `config` / `state` + `logger/__init__`
68
+ - 14 测试 (`tests/test_smoke.py`) + ruff + mypy + pytest CI
69
+
70
+ [1.0.0]: https://github.com/media-to-doc/media-to-doc/releases/tag/v1.0.0
71
+ [0.1.0-dev]: https://github.com/media-to-doc/media-to-doc/releases/tag/v0.1.0-dev