diff --git a/README.md b/README.md index 9ebf77b..14aebfc 100644 --- a/README.md +++ b/README.md @@ -63,14 +63,25 @@ Open `outputs\lecture\notes.md`. Images are bundled under `outputs\lecture\notes ## Optional GUI -SlideNote Studio is a Streamlit wrapper around the same CLI pipeline. It lets you upload PPT/PDF files, configure providers, watch progress, and view notes, coverage, usage, and cost reports. +SlideNote Studio is a Streamlit wrapper around the same CLI pipeline. It lets users upload PPT/PDF files, configure API keys in the page, run presets, monitor progress/ETA, inspect token and cost reports, review page-level source traces, and download the generated results. ```powershell python -m pip install -e ".[dev,llm,gui]" streamlit run gui/app.py ``` -The GUI passes API keys through subprocess environment variables, not command-line arguments. See [gui/README_GUI.md](gui/README_GUI.md) for details. +GUI highlights: + +- **API setup without command-line env edits:** API keys entered in the page are passed to the build subprocess through environment variables and are not written to source code or command-line arguments. +- **Speed and cost controls:** exposes `--speed-mode`, global `--concurrency`, `--llm-concurrency`, `--vision-concurrency`, `--ocr-concurrency`, `--figure-concurrency`, shared global cache, OCR/Vision target caps, and page-level `--refresh-pages`. +- **Runtime visibility:** shows readable API states (`Off`, `Missing key`, `Ready`), progress, ETA estimate, stage output, `run_summary.json`, usage files, and token/cost dashboards. +- **Doctor panel:** runs the same environment checks as `slidenote doctor` and shows missing dependencies/API setup hints in the GUI. +- **Review workspace:** provides page explorer views that link original page screenshots, parsed elements, generated notes, page modality, coverage quality, and a manual modality correction manifest. +- **Save and export UX:** supports the default `gui_runs/outputs` workspace or a custom output folder. The sidebar can request `notes.toc.md`, Word `notes.docx`, PDF `notes.pdf`, and LaTeX `notes.tex`, and the results area offers one-click downloads for generated exports and the full result ZIP. Word/LaTeX require Pandoc; PDF is generated from the Word document through LibreOffice for more reliable Chinese/CJK layout. The GUI warns before the run when Pandoc or LibreOffice is missing. + +See [gui/README_GUI.md](gui/README_GUI.md) for details. + +> GUI export note: Markdown TOC export does not need extra tools. Word and LaTeX use Pandoc, while PDF is produced by converting `notes.docx` with LibreOffice; `export_report.json` records success/failure details. PDF export prefers DOCX → LibreOffice PDF instead of Markdown → LaTeX PDF, because this is much more stable for Chinese/CJK notes. LaTeX remains available as a technical source export. ## Features @@ -85,7 +96,7 @@ The GUI passes API keys through subprocess environment variables, not command-li - Produces `content.json` as the source inventory. - Produces `notes.md` with hidden source markers by default, plus optional visible page references. - Produces `coverage.json` / `coverage.md` to flag elements that may be missing from the notes. -- Optional exports can generate `notes.toc.md`, `notes.docx`, `notes.pdf`, and `notes.tex`; Word/PDF/LaTeX require Pandoc. +- Optional exports can generate `notes.toc.md`, `notes.docx`, `notes.pdf`, and `notes.tex`; Word/LaTeX require Pandoc. PDF requires Pandoc plus LibreOffice because it is converted from notes.docx for better CJK layout. - Optional vision extraction writes OCR text and visual summaries back into the structured content. - Optional LLM generation supports OpenAI/ChatGPT, DeepSeek, Qwen, Doubao/Volcengine Ark, GLM, Gemini, and Claude. - Optional `lecture-weave` note strategy first generates detailed per-page explanations, then weaves them into coherent sections. @@ -192,7 +203,8 @@ PowerShell `$env:...="..."` values only apply to the current terminal session. F | Understand diagrams, screenshots, charts, and visual layouts | Configure a vision model API and run with `--vision auto` | | Preserve PPTX full-slide screenshots | Install LibreOffice, or install PowerPoint + `pywin32` on Windows | | Process old `.ppt` files | Recommended: install LibreOffice; fallback: manually export to PDF | -| Export Word, PDF, or LaTeX notes | Install Pandoc and run with `--export docx,pdf,latex` | +| Export Word or LaTeX notes | Install Pandoc and run with `--export docx,latex` | +| Export PDF notes | Install Pandoc + LibreOffice and run with `--export pdf`; PDF is converted from `notes.docx` for better CJK layout | ## Installation @@ -293,7 +305,7 @@ By default, `notes.md` references bundled image copies under `notes.assets/`. If `content_guard.json` is generated by default when `--content-guard auto` is enabled. Without `--use-llm`, it records a local heuristic review. With `--use-llm`, SlideNote first preselects candidate tables, formulas, definitions, conditions, OCR text, visual summaries, and non-decorative figures, then asks the text model to classify page roles and element learning roles. Only high-confidence `must_explain` items count toward `required_visible_coverage` and can trigger one natural repair pass; low-confidence items remain audit information. -Extra exports are opt-in. `--export markdown-toc` writes `notes.toc.md` without Pandoc. `--export docx,pdf,latex` uses Pandoc to write `notes.docx`, `notes.pdf`, and `notes.tex`; conversion status and any Pandoc errors are written to `export_report.json`. +Extra exports are opt-in. `--export markdown-toc` writes `notes.toc.md` without Pandoc. `--export docx,latex` uses Pandoc to write `notes.docx` and `notes.tex`; `--export pdf` first builds `notes.docx` with Pandoc and then converts it to `notes.pdf` with LibreOffice; conversion status and any Pandoc errors are written to `export_report.json`. ## Environment Check diff --git a/README.zh-CN.md b/README.zh-CN.md index 6d345cf..bc90d6e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -63,14 +63,25 @@ python -m slidenote build path\to\lecture.pdf --out outputs\lecture --use-llm -- ## 可选 GUI -SlideNote Studio 是套在同一条 CLI pipeline 外面的 Streamlit 图形界面。它支持上传 PPT/PDF、配置 provider、查看进度,并预览 notes、coverage、usage 和成本报告。 +SlideNote Studio 是套在同一条 CLI pipeline 外面的 Streamlit 图形界面。它支持上传 PPT/PDF、在页面内配置 API key、选择运行预设、查看进度/ETA、查看 token 与成本报告、做逐页溯源检查,并下载生成结果。 ```powershell python -m pip install -e ".[dev,llm,gui]" streamlit run gui/app.py ``` -GUI 会通过子进程环境变量传递 API key,不把 key 放进命令行参数。更多说明见 [gui/README_GUI.zh-CN.md](gui/README_GUI.zh-CN.md)。 +GUI 亮点: + +- **不用命令行配置 API:** 页面中输入的 API key 会通过子进程环境变量传给本次 build,不写入源码,也不放进命令行参数。 +- **速度与成本控制:** 暴露 `--speed-mode`、总并发 `--concurrency`、`--llm-concurrency`、`--vision-concurrency`、`--ocr-concurrency`、`--figure-concurrency`、共享全局缓存、OCR/Vision 目标上限和按页 `--refresh-pages`。 +- **运行可观测:** 用更直观的 `Off` / `Missing key` / `Ready` 显示 API 状态,并展示进度、ETA、阶段日志、`run_summary.json`、usage 文件和 token/cost dashboard。 +- **Doctor 面板:** 在 GUI 内运行与 `slidenote doctor` 同源的环境检查,并展示缺失依赖和 API 配置建议。 +- **审阅工作台:** Page explorer 可以联动查看原页截图、解析元素、生成笔记、页面类型、coverage 质量,并保存页面类型人工修正清单。 +- **保存与导出体验:** 支持默认 `gui_runs/outputs` 工作区,也支持自定义输出目录;可在侧边栏直接勾选导出 `notes.toc.md`、Word `notes.docx`、PDF `notes.pdf` 和 LaTeX `notes.tex`,生成后可一键下载单个文件或完整结果 ZIP。Word/LaTeX 需要 Pandoc;PDF 会优先通过 LibreOffice 将 notes.docx 转成 notes.pdf,以保证中文/CJK 排版更稳定。GUI 会提前提示缺失依赖。 + +更多说明见 [gui/README_GUI.zh-CN.md](gui/README_GUI.zh-CN.md)。 + +> GUI 导出说明:Markdown 目录版不需要额外依赖;Word 和 LaTeX 使用 Pandoc;PDF 会先生成 `notes.docx`,再通过 LibreOffice 转成 `notes.pdf`;`export_report.json` 会记录成功/失败原因。PDF 导出不再默认走 Markdown → LaTeX → PDF,而是优先走 DOCX → LibreOffice PDF,这样中文和图片排版更稳定;LaTeX 仍保留为技术型源码导出。 ## 功能 @@ -86,7 +97,7 @@ GUI 会通过子进程环境变量传递 API key,不把 key 放进命令行参 - 生成 `content.json` 作为原始内容清单。 - 生成 `notes.md`,默认隐藏来源标记,也可选择显示简洁页码或详细元素 ID。 - 生成 `coverage.json` / `coverage.md`,检查哪些元素没有出现在笔记中。 -- 可选导出 `notes.toc.md`、`notes.docx`、`notes.pdf` 和 `notes.tex`;Word/PDF/LaTeX 需要安装 Pandoc。 +- 可选导出 `notes.toc.md`、`notes.docx`、`notes.pdf` 和 `notes.tex`;Word/LaTeX 需要安装 Pandoc;PDF 需要 Pandoc 和 LibreOffice。 - 支持多家 LLM:ChatGPT/OpenAI、DeepSeek、通义千问、豆包、GLM、Gemini、Claude。 - 支持 `lecture-weave` 高质量笔记策略:先逐页深讲,再按章节编织成连贯笔记。 - 支持控制笔记输出语言和术语策略:英文课件可以生成中文或英文笔记,中文笔记可保留英文专业术语。 @@ -294,7 +305,7 @@ outputs/lecture/ `content_guard.json` 默认由 `--content-guard auto` 生成。不开 `--use-llm` 时,它只记录本地启发式审查;开启 `--use-llm` 后,SlideNote 会先本地预筛表格、公式、定义、条件、OCR 关键文本、视觉摘要和非装饰图片,再让文本模型判断页面角色和元素级学习角色。只有高置信 `must_explain` 元素会进入 `required_visible_coverage` 并触发最多一次自然修复;低置信元素只保留在审查报告里。 -额外导出默认关闭。`--export markdown-toc` 不需要 Pandoc,会写出带目录的 `notes.toc.md`。`--export docx,pdf,latex` 会调用 Pandoc 生成 `notes.docx`、`notes.pdf` 和 `notes.tex`;转换结果和错误摘要会写入 `export_report.json`。 +额外导出默认关闭。`--export markdown-toc` 不需要 Pandoc,会写出带目录的 `notes.toc.md`。`--export docx,pdf,latex` 会调用 Pandoc 生成 `notes.docx` 和 `notes.tex`,并用 LibreOffice 将 `notes.docx` 转为 `notes.pdf`;转换结果和错误摘要会写入 `export_report.json`。 ## 环境检测 diff --git a/ROADMAP.zh-CN.md b/ROADMAP.zh-CN.md index b5db6b9..42767cc 100644 --- a/ROADMAP.zh-CN.md +++ b/ROADMAP.zh-CN.md @@ -39,7 +39,7 @@ SlideNote 当前的核心定位仍然是: - 支持 Deck Brief 课程全景图:`--deck-brief auto` 可用 LLM 生成课程主题、核心问题、概念依赖关系和页面角色划分,作为后续笔记生成的全局导航。 - 支持 Figure Grounding 图文锚定:`--figure-grounding local|vision` 将图片锚定到页面内最近的文本或表格元素,让笔记中的图片出现在相关概念附近而非堆积在页尾。输出 `figure_grounding.json`。 - 支持 Content Guard:`--content-guard auto` 识别高置信关键学习内容,并对 required visible coverage 做一次自然修复。 -- 支持额外导出:带目录 Markdown、Word、PDF、LaTeX;Word/PDF/LaTeX 通过可选 Pandoc。 +- 支持额外导出:带目录 Markdown、Word、PDF、LaTeX;Word/LaTeX 通过 Pandoc,PDF 通过 DOCX → LibreOffice 转换以改善中文/CJK 排版。 - 支持基础 SlideNote Studio GUI 和成本报告:上传文件、配置 provider、查看进度、预览输出和 token/cost dashboard。 ## 1. 上下文策略增强 @@ -373,6 +373,9 @@ lecture-weave 第二阶段按 section 编织 基础版已经合入 SlideNote Studio:一个 Streamlit GUI,包装现有 `python -m slidenote build` pipeline,不改核心解析和生成逻辑。它已经支持上传 PPT/PDF、选择预设、填写 provider/API key、配置 OCR/Vision/LLM、设置缓存和并发、查看 `progress.json`、预览 notes/coverage/run summary,并生成 token/cost 报告。 +**2026-05 GUI 导出补齐:** Studio 现在可以在侧边栏直接选择 Markdown TOC、Word、PDF 和 LaTeX 导出,并在结果区提供 Exports 标签页、单文件下载和完整 ZIP 下载。Pandoc 或 LibreOffice 缺失时 GUI 会提前提示,不影响基础 notes.md 生成;转换状态写入 `export_report.json`。 + + 当前基础版已经实现: - 降低 API key 配置门槛。 @@ -1471,18 +1474,24 @@ CLI 对新手不够友好,尤其是多个服务商都要 key。 ### P0:近期最值得做 -已完成基础版,后续增强: +已完成或已具备基础可用能力: -- `slidenote doctor` 环境检测:已实现完整命令行诊断;后续补 GUI 一键诊断和更友好的 API key 配置引导。 -- 运行进度系统:CLI 实时进度、`progress.json`、`run_summary.json` 已完成;后续补 ETA、失败恢复和更细阶段统计。 -- 加速与成本调度:`--speed-mode`、总并发、LLM/Vision/OCR/Figure 细分并发、`--global-cache-dir`、`--refresh-pages` 和临时错误重试已完成;后续补自动限速、只重跑指定小节。 -- SlideNote Studio GUI:基础 Streamlit GUI、上传文件、运行预设、进度预览、成本报告已完成;后续补原页/元素/笔记联动视图、doctor 面板和局部 refresh。 -- 分层生成策略:Lecture-Weave(`--note-strategy lecture-weave`)已是默认策略;后续补质量评分、自动补回遗漏细节。 -- 页面类型检测与处理路由:`page_modalities.json` 已实现;后续补更准的版面分析和 GUI 手动修正。 -- 视觉目标选择:装饰图过滤、figure crop 优先级、图片学习价值排序已实现;后续补内容图分类和更强版面分析。 +- `slidenote doctor` 环境检测:已实现完整命令行诊断;SlideNote Studio 已补 GUI Doctor 面板,可在页面内查看 Python、核心依赖、可选导出工具、LLM/OCR/Vision API key readiness,并给出基础修复建议。 +- 运行进度系统:CLI 实时进度、`progress.json`、`run_summary.json` 已完成;SlideNote Studio 已补 live run 状态、elapsed、ETA 估算、运行日志和结果预览。 +- 加速与成本调度:`--speed-mode`、总并发、LLM/Vision/OCR/Figure 细分并发、`--global-cache-dir`、`--refresh-pages` 和临时错误重试已完成;SlideNote Studio 已暴露 speed mode、总并发、细分并发、cache、refresh pages、OCR/Vision target 限制和 token/cost dashboard。 +- SlideNote Studio GUI:已支持 Streamlit GUI、上传文件、运行预设、页面内 API key 配置、进度预览、成本报告、自定义保存目录、完整结果 ZIP 下载、Doctor 面板、Quality 面板、Page explorer 原页/元素/笔记联动视图,以及 page modality 手动修正 manifest。 +- 分层生成策略:Lecture-Weave(`--note-strategy lecture-weave`)已是默认策略;GUI 已暴露 note strategy、note depth、note context、section detection、deck brief、content guard 等控制项,并在 Quality 面板中展示 coverage score、missing elements 和 repair queue。 +- 页面类型检测与处理路由:`page_modalities.json` 已实现;GUI 已支持查看每页 modality,并可保存 `page_modalities.overrides.json` 作为人工修正清单。 +- 视觉目标选择:装饰图过滤、figure crop 优先级、图片学习价值排序已实现;GUI 已暴露 Vision/OCR/Figure 相关 target、detail、edge、crop 设置,并可结合 Page explorer 查看页面截图与解析元素。 -仍待实现: +仍待实现或进一步增强: +- 自动限速:根据 provider rate limit、错误类型和重试历史自动调整并发。 +- 真正的小节级局部重跑:目前 GUI 可传 `--refresh-pages` 做页级 refresh;后续补 section/chapter-level refresh 与依赖结果复用。 +- 失败恢复增强:目前支持日志、失败提示和复用输出目录局部重跑;后续补断点续跑、失败阶段回滚和任务恢复。 +- 更细阶段统计:后续在 GUI 中展示 parsing / OCR / Vision / LLM / coverage / export 的耗时占比和失败统计。 +- 质量评分自动闭环:目前 GUI 展示 coverage 与 repair queue;后续补自动补回遗漏细节、二次生成和质量评分阈值。 +- 更强版面分析与内容图分类:后续补更准的图表/流程图/表格/装饰图分类,以及 GUI 中人工选择 OCR/Vision 页和目标图。 - 课程工作区基础模型:Course / Source / Chapter。 - 多 PPT / 多 PDF 课程级整合。 - PPT 章节切分与分批输出:`split` 子命令、`--split-by section`、`section_index.json` 等。 diff --git a/gui/README_GUI.md b/gui/README_GUI.md index 6382316..43f48c3 100644 --- a/gui/README_GUI.md +++ b/gui/README_GUI.md @@ -2,26 +2,38 @@ [中文说明](README_GUI.zh-CN.md) -This GUI wraps the existing `python -m slidenote build` pipeline with a Streamlit page. It does not replace or modify the core parser/LLM pipeline. +SlideNote Studio is a Streamlit UI around the existing `python -m slidenote build` pipeline. It does not replace or modify the core parser, OCR, vision, or LLM pipeline. ## What it adds - Upload PPTX / PPT / PDF without touching the command line. -- Paste API keys inside the page for one run; keys are not written to source code. +- Paste API keys inside the page for one run; keys are passed through subprocess environment variables and are not written to source code or command-line arguments. - Choose workflow presets: - Fast API draft - Balanced study notes - Quality detailed notes - Local safe preview -- Control runtime speed: - - concurrency +- Control runtime speed and cost: + - `--speed-mode` + - global `--concurrency` - separate LLM / Vision / OCR / Figure API concurrency - - global cache - - OCR/Vision max target limits - - direct vs lecture-weave note strategy + - shared global cache + - OCR/Vision target limits + - `direct` vs `lecture-weave` - deck brief/content guard switches -- Watch `progress.json` during the run. -- View `notes.md`, `coverage.md`, `run_summary.json`, usage files, and token/cost reports. + - page-level `--refresh-pages` +- Readable runtime cards: `Off`, `Missing key`, or `Ready` for text/vision APIs, plus concurrency and cache state. +- Doctor panel that reuses `slidenote doctor` checks for Python, packages, optional tools, and API key readiness. +- Live progress with ETA estimate, stage detail, and console logs. +- Result workspace with: + - quality/coverage summary + - missing-element repair queue + - page explorer linking screenshot, parsed elements, page modality, and note block + - manual modality correction manifest (`page_modalities.overrides.json`) + - token/cost dashboard + - `notes.md`, `coverage.md`, `run_summary.json`, usage files, and raw outputs +- Save outputs to the default `gui_runs/outputs` workspace or a custom local folder. +- Download `notes.md`, `coverage.md`, `cost_report.md`, or the full result ZIP from the page. ## Install @@ -36,12 +48,29 @@ If you use LLM providers: python -m pip install -e ".[llm]" ``` +Or install everything for development: + +```bash +python -m pip install -e ".[dev,llm,gui]" +``` + ## Run ```bash streamlit run gui/app.py ``` +## First safe test + +Use **Local safe preview** first: + +- Use text LLM: off +- OCR mode: off +- Vision mode: off +- Save: Default workspace or Custom folder + +This checks local parsing and output rendering without API calls or token cost. + ## API keys The GUI passes API keys to one run through subprocess environment variables, not command-line arguments. This avoids asking users to set terminal environment variables manually while keeping keys out of process argv. @@ -50,23 +79,36 @@ Do not commit keys to GitHub. ## Speed tips -For first tests, use **Local safe preview** or **Fast API draft**. +For first API tests, use **Fast API draft** with OCR and Vision off. To reduce runtime: -1. Use `direct` note strategy instead of `lecture-weave`. +1. Use `direct` note strategy for drafts; use `lecture-weave` when you need higher quality connected notes. 2. Use `vision=auto`, not `vision=all`. 3. Use `ocr=auto`, not `ocr=all`. 4. Set `Vision max targets` and `OCR max targets` to small numbers for testing. 5. Keep cache enabled and use the shared global cache. 6. Increase concurrency to 3-6 only when the API provider rate limit allows it. +7. Use `Refresh only these pages` for small reruns instead of rerunning the whole deck. ## Token and cost report -After a successful run, the GUI generates: +After a successful run, the GUI generates and displays: - `cost_report.json` - `cost_report.md` - `cost_dashboard.html` Prices are read from `pricing.template.json`. Keep the prices updated manually using official provider pricing pages. + + +## Export Word / PDF / LaTeX + +The sidebar section **5. Exports** can request optional final artifacts: + +- `notes.toc.md`: Markdown with table of contents; Pandoc is not required. +- `notes.docx`: Word document; requires Pandoc. +- `notes.pdf`: PDF handout; generated by converting `notes.docx` with LibreOffice for better Chinese/CJK layout. Requires Pandoc plus LibreOffice. +- `notes.tex`: LaTeX source; requires Pandoc. + +After a run, the results area shows an **Exports** tab and quick download buttons for available Word / PDF / LaTeX files. Export status is written to `export_report.json`. If Pandoc or LibreOffice is missing from PATH, the GUI warns before the run while still allowing the base `notes.md` build to complete. diff --git a/gui/README_GUI.zh-CN.md b/gui/README_GUI.zh-CN.md index c14854f..3a9d67c 100644 --- a/gui/README_GUI.zh-CN.md +++ b/gui/README_GUI.zh-CN.md @@ -7,21 +7,33 @@ SlideNote Studio 是一个基于 Streamlit 的图形界面。它包装现有的 ## 它能做什么 - 不用写命令行,直接上传 PPTX / PPT / PDF。 -- 在页面里临时填写 API key;key 不会写入源码。 +- 在页面里临时填写 API key;key 会通过子进程环境变量传给本次运行,不写入源码,也不放进命令行参数。 - 选择常用工作流预设: - Fast API draft:快速 API 草稿 - Balanced study notes:平衡版学习笔记 - Quality detailed notes:高质量详细笔记 - Local safe preview:本地安全预览 - 控制运行速度和成本: - - 总并发 + - `--speed-mode` + - 总并发 `--concurrency` - LLM / Vision / OCR / Figure 四类 API 独立并发 - - 全局缓存目录 + - shared global cache - OCR / Vision 最大目标数量 - `direct` 或 `lecture-weave` 笔记策略 - deck brief / content guard 开关 -- 运行时查看 `progress.json` 进度。 -- 在页面里预览 `notes.md`、`coverage.md`、`run_summary.json`、usage 文件和 token/cost 报告。 + - 按页 `--refresh-pages` 局部刷新 +- 更直观的运行状态卡片:Text API / Vision API 会显示 `Off`、`Missing key` 或 `Ready`,不会再显示难懂的 `n...`。 +- Doctor 面板:复用 `slidenote doctor` 的环境检查,展示 Python、依赖包、可选工具和 API key readiness。 +- Live run:显示进度、ETA 估算、当前阶段和控制台日志。 +- 结果审阅工作台: + - quality / coverage 总览 + - missing element 修复队列 + - Page explorer:联动查看原页截图、解析元素、页面类型和对应笔记 + - 页面类型人工修正清单 `page_modalities.overrides.json` + - token/cost dashboard + - `notes.md`、`coverage.md`、`run_summary.json`、usage 文件和原始输出文件 +- 支持默认 `gui_runs/outputs` 工作区,也支持自定义本地输出目录。 +- 页面内一键下载 `notes.md`、`coverage.md`、`cost_report.md` 或完整结果 ZIP。 ## 安装 @@ -50,6 +62,17 @@ streamlit run gui/app.py 启动后在浏览器中打开 Streamlit 给出的本地地址,上传课件并选择运行参数即可。 +## 第一次安全测试 + +建议先用 **Local safe preview**: + +- Use text LLM: off +- OCR mode: off +- Vision mode: off +- Save: Default workspace 或 Custom folder + +这个模式只测试本地解析和输出展示,不会调用 API,也不会产生 token 成本。 + ## API key GUI 会把 API key 通过子进程环境变量传给本次运行,不会把 key 放进命令行参数里。这样用户不需要手动配置终端环境变量,也能避免 key 暴露在进程 argv 中。 @@ -58,7 +81,7 @@ GUI 会把 API key 通过子进程环境变量传给本次运行,不会把 key ## 加速建议 -第一次测试建议使用 **Local safe preview** 或 **Fast API draft**。 +第一次测试 API 建议用 **Fast API draft**,并先关闭 OCR 和 Vision。 想减少运行时间,可以优先这样调: @@ -67,14 +90,27 @@ GUI 会把 API key 通过子进程环境变量传给本次运行,不会把 key 3. 使用 `ocr=auto`,只有扫描版 PDF 或低文本页很多时再考虑 `ocr=all`。 4. 测试时把 `Vision max targets` 和 `OCR max targets` 设小一点。 5. 保持缓存开启,并使用 shared global cache。 -6. API 服务商限流允许时,把并发提高到 3 到 6;如果遇到限流,先降到 2 或 3,不要直接关闭质量阶段。 +6. API 服务商限流允许时,把并发提高到 3 到 6;如果遇到限流,先降到 2 或 3。 +7. 如果只改少数页面,用 `Refresh only these pages` 做局部重跑,不要整份课件重跑。 ## Token 和成本报告 -构建成功后,GUI 会尝试生成: +构建成功后,GUI 会尝试生成并展示: - `cost_report.json` - `cost_report.md` - `cost_dashboard.html` 成本估算会读取 `pricing.template.json`。模板里的价格需要你按官方价格页面手动更新;默认价格可能是 0,所以默认报告更适合作为统计框架,而不是准确账单。 + + +## 导出 Word / PDF / LaTeX + +GUI 侧边栏的 **5. Exports** 可以直接勾选额外导出格式: + +- `notes.toc.md`:带目录 Markdown,不需要 Pandoc。 +- `notes.docx`:Word 文档,需要 Pandoc。 +- `notes.pdf`:PDF 讲义,优先用 LibreOffice 将 `notes.docx` 转成 PDF,中文/CJK 和图片排版更稳定;需要 Pandoc + LibreOffice。 +- `notes.tex`:LaTeX 源码,需要 Pandoc。 + +导出完成后,结果区会出现 **Exports** 标签页,并在顶部下载区显示可用的 Word / PDF / LaTeX 下载按钮。所有导出状态会写入 `export_report.json`。如果 Pandoc 或 LibreOffice 不在 PATH 中,GUI 会在运行前提示安装命令,build 仍可继续生成基础 `notes.md`。 diff --git a/gui/app.py b/gui/app.py index 17055f6..a6ca27e 100644 --- a/gui/app.py +++ b/gui/app.py @@ -1,9 +1,14 @@ from __future__ import annotations +import io import json import os +import re +import shutil import subprocess import time +import zipfile +from datetime import datetime from pathlib import Path from typing import Any @@ -14,6 +19,11 @@ except Exception: # pragma: no cover - GUI fallback only write_cost_report = None +try: + from slidenote.doctor import run_doctor +except Exception: # pragma: no cover - GUI fallback only + run_doctor = None + from gui.studio_core import ( DEFAULT_MODELS, VISION_DEFAULT_MODELS, @@ -22,9 +32,9 @@ build_slidenote_command, command_for_display, discover_outputs, - masked_key_status, performance_tips, progress_percent, + provider_env_key, safe_run_name, ) @@ -102,17 +112,14 @@ }, } +MODALITY_OPTIONS = ["native_text", "mixed", "image_only", "shape_diagram", "decorative", "unknown"] + def main() -> None: st.set_page_config(page_title="SlideNote Studio", page_icon="📝", layout="wide") _style() - st.title("📝 SlideNote Studio") - st.caption("Upload slides, connect API keys in the page, run with concurrency/cache controls, and monitor tokens/costs without touching the command line.") - - RUNS_DIR.mkdir(exist_ok=True) - UPLOADS_DIR.mkdir(parents=True, exist_ok=True) - OUTPUTS_DIR.mkdir(parents=True, exist_ok=True) - GLOBAL_CACHE_DIR.mkdir(parents=True, exist_ok=True) + _ensure_dirs() + _render_hero() with st.sidebar: st.header("1. File") @@ -138,6 +145,34 @@ def main() -> None: vision_model = st.text_input("Vision model", value=VISION_DEFAULT_MODELS.get(vision_provider, "")) vision_api_key = st.text_input("Vision API key", type="password", help="Can be the same as the text provider key if provider is the same.") + st.header("4. Save") + save_mode = st.radio( + "Output location", + ["Default workspace", "Custom folder"], + index=0, + help="Default saves under gui_runs/outputs. Custom lets you choose a parent folder on this computer.", + ) + custom_output_base_text = st.text_input( + "Custom output folder", + value=str(Path.home() / "Desktop" / "SlideNote_outputs"), + disabled=save_mode == "Default workspace", + help="Paste a local folder path, for example C:\\Users\\student\\Desktop\\SlideNote_outputs.", + ) + timestamped_subfolder = st.toggle( + "Create a timestamped subfolder", + value=True, + help="Recommended. Keeps each run separated and prevents old results from being overwritten.", + ) + + st.header("5. Exports") + st.caption("Optional final files generated from notes.md. Word/LaTeX need Pandoc; PDF is generated from Word via LibreOffice for stable Chinese/CJK layout. Markdown TOC works without Pandoc.") + export_markdown_toc = st.checkbox("Markdown with table of contents (.md)", value=True) + export_docx = st.checkbox("Word document (.docx)", value=False) + export_pdf = st.checkbox("PDF handout (.pdf)", value=False) + export_latex = st.checkbox("LaTeX source (.tex)", value=False) + export_options = _selected_export_formats(export_markdown_toc, export_docx, export_pdf, export_latex) + _render_export_readiness(export_options) + col_left, col_right = st.columns([0.95, 1.05], gap="large") with col_left: @@ -156,7 +191,14 @@ def main() -> None: figure_concurrency = st.number_input("Figure", min_value=1, max_value=20, value=int(concurrency), step=1) cache_mode = st.selectbox("LLM cache", ["on", "refresh", "off"], index=0) use_global_cache = st.toggle("Use shared global cache", value=True, help="Keeps OCR/Vision/LLM cache across different GUI runs.") - refresh_pages = st.text_input("Refresh only these pages", value="", placeholder="Example: 3,5-8", help="Bypass cache for selected slide IDs only.") + refresh_default = st.session_state.get("refresh_pages_value", "") + refresh_pages = st.text_input( + "Refresh only these pages", + value=refresh_default, + key="refresh_pages_text", + placeholder="Example: 3,5-8", + help="Bypass cache for selected slide IDs only. Use this for local page refresh after review.", + ) with st.expander("Quality and time controls", expanded=True): c1, c2 = st.columns(2) @@ -189,72 +231,74 @@ def main() -> None: with st.expander("Export and display", expanded=False): source_display = st.selectbox("Source display", ["hidden", "footnote", "inline"], index=0) screenshot_policy = st.selectbox("Screenshot policy", ["fallback", "never", "always"], index=0) - export_options = st.multiselect("Extra exports", ["markdown-toc", "docx", "pdf", "latex"], default=[]) + st.caption("Export formats are configured in the sidebar under '5. Exports'.") + st.write(", ".join(export_options) if export_options else "No extra exports selected") - if use_llm and not api_key: - st.warning("Text LLM is enabled but no API key is entered. You can still run only if the key already exists in your environment.") - if vision != "off" and not vision_api_key and vision_provider != provider: - st.info("Vision provider is different from text provider. Add a vision key or make sure it exists in environment variables.") + preview_config = StudioConfig( + input_path=ROOT / "example.pdf", + output_dir=OUTPUTS_DIR / "preview", + progress_json=OUTPUTS_DIR / "preview" / "progress.json", + speed_mode=str(speed_mode), + concurrency=int(concurrency), + llm_concurrency=int(llm_concurrency), + vision_concurrency=int(vision_concurrency), + ocr_concurrency=int(ocr_concurrency), + figure_concurrency=int(figure_concurrency), + global_cache_dir=GLOBAL_CACHE_DIR if use_global_cache else None, + refresh_pages=refresh_pages.strip() or None, + use_llm=bool(use_llm), + provider=provider, + model=model.strip() or None, + api_key=api_key or None, + base_url=base_url.strip() or None, + max_output_tokens=int(max_output_tokens), + temperature=float(temperature), + content_guard=content_guard, + note_context=note_context, + note_language=note_language, + term_policy=term_policy, + note_strategy=note_strategy, + note_depth=note_depth, + deck_brief=deck_brief, + section_detection=section_detection, + cache=cache_mode, + ocr=ocr, + ocr_provider=ocr_provider, + ocr_api_key=ocr_api_key or None, + ocr_secret_key=ocr_secret_key or None, + ocr_max_targets=int(ocr_max_targets), + ocr_max_edge=int(ocr_max_edge), + vision=vision, + vision_provider=vision_provider, + vision_model=vision_model.strip() or None, + vision_api_key=vision_api_key or (api_key if vision_provider == provider else None) or None, + vision_max_targets=int(vision_max_targets), + vision_max_edge=int(vision_max_edge), + vision_detail=vision_detail, + figure_crop=figure_crop, + figure_max_targets=int(figure_max_targets), + figure_grounding="auto", + source_display=source_display, + screenshot_policy=screenshot_policy, + export=",".join(export_options) if export_options else None, + ) with col_right: st.subheader("Connection and runtime overview") - metric_cols = st.columns(4) - metric_cols[0].metric("Text API", masked_key_status(api_key or os.getenv(f"{provider.upper()}_API_KEY"))) - metric_cols[1].metric("Vision API", masked_key_status(vision_api_key or os.getenv(f"{vision_provider.upper()}_API_KEY"))) - metric_cols[2].metric("Concurrency", concurrency) - metric_cols[3].metric("Cache", cache_mode) - - dummy_input = ROOT / "example.pdf" - dummy_out = OUTPUTS_DIR / "preview" - dummy_progress = dummy_out / "progress.json" - preview_config = StudioConfig( - input_path=dummy_input, - output_dir=dummy_out, - progress_json=dummy_progress, - speed_mode=speed_mode, - concurrency=concurrency, - llm_concurrency=int(llm_concurrency), - vision_concurrency=int(vision_concurrency), - ocr_concurrency=int(ocr_concurrency), - figure_concurrency=int(figure_concurrency), - global_cache_dir=GLOBAL_CACHE_DIR if use_global_cache else None, - refresh_pages=refresh_pages.strip() or None, - use_llm=use_llm, - provider=provider, - model=model.strip() or None, - api_key=api_key or None, - base_url=base_url.strip() or None, - max_output_tokens=int(max_output_tokens), - temperature=float(temperature), - content_guard=content_guard, - note_context=note_context, - note_language=note_language, - term_policy=term_policy, - note_strategy=note_strategy, - note_depth=note_depth, - deck_brief=deck_brief, - section_detection=section_detection, - cache=cache_mode, - ocr=ocr, - ocr_provider=ocr_provider, - ocr_api_key=ocr_api_key or None, - ocr_secret_key=ocr_secret_key or None, - ocr_max_targets=int(ocr_max_targets), - ocr_max_edge=int(ocr_max_edge), - vision=vision, - vision_provider=vision_provider, - vision_model=vision_model.strip() or None, - vision_api_key=vision_api_key or (api_key if vision_provider == provider else None) or None, - vision_max_targets=int(vision_max_targets), - vision_max_edge=int(vision_max_edge), - vision_detail=vision_detail, - figure_crop=figure_crop, - figure_max_targets=int(figure_max_targets), - figure_grounding="auto", - source_display=source_display, - screenshot_policy=screenshot_policy, - export=",".join(export_options) if export_options else None, - ) + text_status = _api_status(use_llm, api_key, provider) + vision_status = _api_status(_needs_vision_api(preview_config), preview_config.vision_api_key, vision_provider) + cache_status = _cache_status(cache_mode, use_global_cache) + concurrency_status = _concurrency_status(concurrency) + grid = st.columns(4) + with grid[0]: + _status_card("Text", *text_status, icon="✦") + with grid[1]: + _status_card("Vision", *vision_status, icon="◈") + with grid[2]: + _status_card("Workers", *concurrency_status, icon="⇄") + with grid[3]: + _status_card("Cache", *cache_status, icon="◎") + tips = performance_tips(preview_config) if tips: st.info("\n".join(f"• {tip}" for tip in tips)) @@ -264,58 +308,24 @@ def main() -> None: with st.expander("Generated command preview", expanded=False): st.code(command_for_display(build_slidenote_command(preview_config)), language="bash") + with st.expander("Doctor panel", expanded=False): + _render_doctor_panel(text_status=text_status, vision_status=vision_status) + run_clicked = st.button("🚀 Run SlideNote build", type="primary", use_container_width=True, disabled=uploaded is None) + if use_llm and not api_key and not os.getenv(provider_env_key(provider)): + st.warning("Text LLM is enabled but no API key is entered or configured in the environment.") + if _needs_vision_api(preview_config) and vision_status[0] == "Missing key": + st.warning("Vision is enabled but no vision API key was entered or found in the environment.") + if run_clicked and uploaded is not None: - input_path, output_dir, progress_json = _prepare_run_paths(uploaded) - config = StudioConfig( - input_path=input_path, - output_dir=output_dir, - progress_json=progress_json, - speed_mode=speed_mode, - concurrency=concurrency, - llm_concurrency=int(llm_concurrency), - vision_concurrency=int(vision_concurrency), - ocr_concurrency=int(ocr_concurrency), - figure_concurrency=int(figure_concurrency), - global_cache_dir=GLOBAL_CACHE_DIR if use_global_cache else None, - refresh_pages=refresh_pages.strip() or None, - use_llm=use_llm, - provider=provider, - model=model.strip() or None, - api_key=api_key or None, - base_url=base_url.strip() or None, - max_output_tokens=int(max_output_tokens), - temperature=float(temperature), - content_guard=content_guard, - note_context=note_context, - note_language=note_language, - term_policy=term_policy, - note_strategy=note_strategy, - note_depth=note_depth, - deck_brief=deck_brief, - section_detection=section_detection, - cache=cache_mode, - ocr=ocr, - ocr_provider=ocr_provider, - ocr_api_key=ocr_api_key or None, - ocr_secret_key=ocr_secret_key or None, - ocr_max_targets=int(ocr_max_targets), - ocr_max_edge=int(ocr_max_edge), - vision=vision, - vision_provider=vision_provider, - vision_model=vision_model.strip() or None, - vision_api_key=vision_api_key or (api_key if vision_provider == provider else None) or None, - vision_max_targets=int(vision_max_targets), - vision_max_edge=int(vision_max_edge), - vision_detail=vision_detail, - figure_crop=figure_crop, - figure_max_targets=int(figure_max_targets), - figure_grounding="auto", - source_display=source_display, - screenshot_policy=screenshot_policy, - export=",".join(export_options) if export_options else None, - ) + output_base = OUTPUTS_DIR if save_mode == "Default workspace" else Path(custom_output_base_text).expanduser() + try: + input_path, output_dir, progress_json = _prepare_run_paths(uploaded, output_base, timestamped_subfolder) + except Exception as exc: + st.error(f"Could not prepare output folder: {exc}") + return + config = _clone_config_for_run(preview_config, input_path=input_path, output_dir=output_dir, progress_json=progress_json) _run_build(config) st.session_state["last_output_dir"] = str(output_dir) @@ -325,11 +335,161 @@ def main() -> None: _render_results(last_output_dir) -def _prepare_run_paths(uploaded) -> tuple[Path, Path, Path]: +def _ensure_dirs() -> None: + RUNS_DIR.mkdir(exist_ok=True) + UPLOADS_DIR.mkdir(parents=True, exist_ok=True) + OUTPUTS_DIR.mkdir(parents=True, exist_ok=True) + GLOBAL_CACHE_DIR.mkdir(parents=True, exist_ok=True) + + +def _render_hero() -> None: + st.markdown( + """ +
Upload slides, connect API keys in the page, run with concurrency/cache controls, and monitor tokens, coverage and cost without touching the command line.
+{output_dir}