From d550af2235467bd8bc98e72d786f2a6a10b62b47 Mon Sep 17 00:00:00 2001 From: LEO690201 <3409961762@qq.com> Date: Sun, 16 Aug 2026 21:10:34 +0800 Subject: [PATCH 1/2] fix: honor --vision off and add --ocr flag to build `--vision off` only skipped the explicit vision stage; semantic_layout still ran in auto mode and issued vision API calls, hanging on providers without a reachable image endpoint. Cascade off to semantic_layout local mode. Also expose `--ocr {off,auto,all}` on `build` so lecture builds on digital text PDFs can skip the Baidu OCR requirement without switching to the local preset. Co-Authored-By: Claude Opus 4.7 --- slidenote/build/stages.py | 5 ++++- slidenote/cli.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/slidenote/build/stages.py b/slidenote/build/stages.py index a41811a..a9467ee 100644 --- a/slidenote/build/stages.py +++ b/slidenote/build/stages.py @@ -74,6 +74,9 @@ def _stage_table_understanding(state: BuildState) -> None: def _stage_semantic_layout(state: BuildState) -> None: args = state.args deck = _require_deck(state) + mode = args.semantic_layout + if args.vision == "off" and mode == "auto": + mode = "local" state.semantic_layout_report = _run_json_stage( deck, state.build_context, @@ -86,7 +89,7 @@ def _stage_semantic_layout(state: BuildState) -> None: runner=lambda stage_deck: enrich_deck_with_semantic_layout( stage_deck, output_root=state.output_root, - mode=args.semantic_layout, + mode=mode, provider=args.vision_provider, model=args.vision_model, api_key=args.vision_api_key, diff --git a/slidenote/cli.py b/slidenote/cli.py index de08552..b0d2c28 100644 --- a/slidenote/cli.py +++ b/slidenote/cli.py @@ -73,6 +73,12 @@ def _build_parser() -> argparse.ArgumentParser: default="auto", help="Vision extraction mode. auto is the quality default; off skips visual API calls.", ) + build.add_argument( + "--ocr", + choices=["off", "auto", "all"], + default=None, + help="OCR mode. auto only OCRs low-text/scanned pages (default); off skips OCR; all OCRs every page.", + ) doctor = subparsers.add_parser("doctor", help="Check local dependencies, optional tools, and API key environment variables.") doctor.add_argument("--json", type=Path, default=None, help="Write the doctor report as JSON to this path.") From a04be6478c76e72c7eb0d4f65766c43b5832129f Mon Sep 17 00:00:00 2001 From: Blizzard <206320063@qq.com> Date: Sun, 16 Aug 2026 23:30:11 +0800 Subject: [PATCH 2/2] fix:normalize-semantic-layout-mode --- slidenote/build/config.py | 2 ++ slidenote/build/stages.py | 5 +-- tests/test_cli.py | 64 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/slidenote/build/config.py b/slidenote/build/config.py index ec192ec..8a2b08a 100644 --- a/slidenote/build/config.py +++ b/slidenote/build/config.py @@ -232,6 +232,8 @@ def _apply_build_preset_defaults(args: argparse.Namespace) -> None: for name, value in preset_defaults.items(): if preset == "local" or name not in explicit_options: setattr(args, name, value) + if args.vision == "off" and args.semantic_layout == "auto": + args.semantic_layout = "local" def _resolve_api_concurrency(args: argparse.Namespace) -> dict[str, int]: diff --git a/slidenote/build/stages.py b/slidenote/build/stages.py index a9467ee..a41811a 100644 --- a/slidenote/build/stages.py +++ b/slidenote/build/stages.py @@ -74,9 +74,6 @@ def _stage_table_understanding(state: BuildState) -> None: def _stage_semantic_layout(state: BuildState) -> None: args = state.args deck = _require_deck(state) - mode = args.semantic_layout - if args.vision == "off" and mode == "auto": - mode = "local" state.semantic_layout_report = _run_json_stage( deck, state.build_context, @@ -89,7 +86,7 @@ def _stage_semantic_layout(state: BuildState) -> None: runner=lambda stage_deck: enrich_deck_with_semantic_layout( stage_deck, output_root=state.output_root, - mode=mode, + mode=args.semantic_layout, provider=args.vision_provider, model=args.vision_model, api_key=args.vision_api_key, diff --git a/tests/test_cli.py b/tests/test_cli.py index 5fc0208..0a4b04e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -214,6 +214,59 @@ def test_local_preset_does_not_call_figure_crop_api(tmp_path): assert run_summary["figure_crop"] is None +def test_vision_off_uses_and_reports_local_semantic_layout(tmp_path, monkeypatch): + source = tmp_path / "lecture.pdf" + doc = fitz.open() + page = doc.new_page() + page.insert_text((72, 72), "Transport Layer") + doc.save(source) + doc.close() + out = tmp_path / "out" + + class UnexpectedVisionClient: + def __init__(self, **kwargs): + raise AssertionError("vision client should not be constructed") + + def unexpected_ocr(*args, **kwargs): + raise AssertionError("OCR should remain disabled") + + monkeypatch.setattr("slidenote.semantic_layout.LLMClient", UnexpectedVisionClient) + monkeypatch.setattr("slidenote.build.stages.enrich_deck_with_ocr", unexpected_ocr) + monkeypatch.setattr("slidenote.build.stages.build_section_plan", lambda *args, **kwargs: {"summary": {"sections_total": 1}}) + monkeypatch.setattr("slidenote.build.stages.build_deck_brief", lambda *args, **kwargs: {"summary": {"llm_call": False}}) + monkeypatch.setattr("slidenote.build.stages.render_deck_brief_markdown", lambda report: "# Deck brief\n") + monkeypatch.setattr("slidenote.build.stages.build_content_guard", lambda *args, **kwargs: None) + monkeypatch.setattr( + "slidenote.build.stages.generate_notes_result", + lambda *args, **kwargs: NoteGenerationResult(markdown="Transport Layer. "), + ) + + exit_code = main( + [ + "build", + str(source), + "--out", + str(out), + "--quiet", + "--provider", + "openai", + "--vision", + "off", + "--ocr", + "off", + ] + ) + + assert exit_code == 0 + semantic_layout = json.loads((out / "semantic_layout.json").read_text(encoding="utf-8")) + run_summary = json.loads((out / "run_summary.json").read_text(encoding="utf-8")) + assert semantic_layout["mode"] == "local" + assert semantic_layout["summary"]["vision_calls"] == 0 + assert run_summary["run"]["semantic_layout"] == "local" + assert not (out / "vision_usage.json").exists() + assert not (out / "ocr_usage.json").exists() + + def test_missing_default_vision_key_prints_text_mode_hint(tmp_path, monkeypatch, capsys): monkeypatch.delenv("QWEN_API_KEY", raising=False) monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) @@ -376,6 +429,17 @@ def test_lecture_preset_maps_to_teacher_style_pipeline(): assert args.source_display == "hidden" +def test_explicit_ocr_off_survives_lecture_preset_defaults(): + from slidenote.cli import _build_parser + + argv = ["build", "lecture.pdf", "--ocr", "off"] + args = _build_parser().parse_args(argv) + args._explicit_options = _explicit_cli_options(argv) + _apply_build_preset_defaults(args) + + assert args.ocr == "off" + + def test_build_rejects_removed_lower_level_options(): from slidenote.cli import _build_parser