Skip to content

AI PPT 工作台:模型配置、逐页生成编辑与导出#1

Merged
LRriver merged 15 commits into
mainfrom
dev
May 31, 2026
Merged

AI PPT 工作台:模型配置、逐页生成编辑与导出#1
LRriver merged 15 commits into
mainfrom
dev

Conversation

@LRriver

@LRriver LRriver commented May 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • 新增多格式文档解析、三角色模型配置、模型路由和图像结果规范化,支持 URL/base64/data URL/Markdown 图片链接等输出。
  • 重构 AI PPT 工作台前端,支持模型配置折叠、PPT 内容设置、生成流程、大纲确认、逐页图片编辑、版本历史和 PDF/PPTX 导出。
  • 更新默认 demo 文档为仓库内 doc/L9.md,补充中英文 README、API 文档和高清演示视频。

Test Plan

  • npm run lint
  • npm run test
  • npm run build
  • /tmp/aippt-test-20260531-demo/bin/python -m pytest tests
  • 浏览器冒烟:使用 doc/L9.md 走上传、多页生成、单页编辑、PDF/PPTX 导出和演示录制流程。

Notes

  • config.yaml 仍作为本地配置文件被忽略;仓库提供 config.example.yaml 作为模板。
  • 第一版支持整页图像编辑,框选局部编辑保留为后续扩展方向。

Copilot AI review requested due to automatic review settings May 31, 2026 09:22
@LRriver LRriver merged commit 617e52f into main May 31, 2026
1 check failed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a structured multi-step PPT generation workflow that allows users to generate, edit, and confirm a design outline and page-by-page designs before generating slide images. It also adds support for parsing multiple document formats, configures three distinct model roles, and implements dark mode and localization. The review feedback highlights several important issues, including a critical bug where the temporary directory is cleaned up before the exported presentation file is served, a usability issue in the outline editor that prevents typing newlines, potential validation failures when slide prompts are received out of order, a possible IndexError on empty model choices, and unhandled exceptions during document parsing.

Comment thread api/routes/export.py
Comment on lines 64 to 65
return FileResponse(
path=str(output_path),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

由于 temp_dir 是通过 with tempfile.TemporaryDirectory() 创建的,当路由函数返回时,with 代码块会立即退出并删除该临时目录及其下的所有文件(包括 presentation.pptx)。而 FileResponse 是延迟(异步)读取文件的,此时文件已被删除,会导致 FileNotFoundError 或 500 错误。建议在 with 块内将文件读取到内存中,并使用 StreamingResponse 返回,或者使用 FastAPI 的 BackgroundTasks 在响应发送后延迟清理临时目录。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in efd576c. Exported files now use a BackgroundTask to remove the temporary directory only after FileResponse finishes sending, and error paths clean up the directory immediately.

Comment on lines +155 to +169
const updateSlideKeyPoints = useCallback((page: number, value: string) => {
markOutlineDirty()
const keyPoints = value
.split('\n')
.map((item) => item.trim())
.filter(Boolean)
setOutline((current) => current
? {
...current,
slides: current.slides.map((slide) => (
slide.page === page ? { ...slide, key_points: keyPoints } : slide
))
}
: current)
}, [markOutlineDirty])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

updateSlideKeyPoints 中,每次输入(onChange)都会通过 .filter(Boolean) 过滤掉空行。当用户在 textarea 中按下回车键准备换行时,输入值末尾会产生一个空行,该空行会被立即过滤掉,导致用户无法正常换行输入多个要点。建议在输入时不进行过滤(保留空行),而是在提交大纲(handleGeneratePrompts)时再统一过滤空行。

  const updateSlideKeyPoints = useCallback((page: number, value: string) => {
    markOutlineDirty()
    const keyPoints = value.split('\n')
    setOutline((current) => current
      ? {
          ...current,
          slides: current.slides.map((slide) => (
            slide.page === page ? { ...slide, key_points: keyPoints } : slide
          ))
        }
      : current)
  }, [markOutlineDirty])

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in efd576c. The outline key-points textarea now preserves blank trailing lines while editing, so users can press Enter to create a new item naturally.

Comment on lines +104 to +107
const parsedOutline = outline
if (!Array.isArray(parsedOutline.slides) || parsedOutline.slides.length !== generationConfig.pageCount) {
throw new Error(t('workflow.pageCountMismatch', { count: generationConfig.pageCount }))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

配合 updateSlideKeyPoints 的修改,建议在提交大纲生成逐页设计时,在此处对 key_points 进行统一的过滤和去空格处理,以确保发送给后端的请求数据干净且不包含空行。

      const cleanedSlides = outline.slides.map((slide) => ({
        ...slide,
        key_points: slide.key_points.map((item) => item.trim()).filter(Boolean)
      }))
      const parsedOutline = { ...outline, slides: cleanedSlides }
      if (!Array.isArray(parsedOutline.slides) || parsedOutline.slides.length !== generationConfig.pageCount) {
        throw new Error(t('workflow.pageCountMismatch', { count: generationConfig.pageCount }))
      }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in efd576c. Key points are now trimmed and empty entries are removed only when submitting the outline to generate page designs.

Comment thread api/routes/generate.py
Comment on lines +55 to +61
slide_prompts = request.slide_prompts or []
if len(slide_prompts) != ppt_config.num_pages:
raise ValueError(f"确认后的逐页设计数量不匹配: 期望{ppt_config.num_pages}页,实际{len(slide_prompts)}页")

pages = [item.page for item in slide_prompts]
expected = list(range(1, ppt_config.num_pages + 1))
if pages != expected:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

如果前端传入的 slide_prompts 顺序被打乱(例如 [2, 1, 3]),直接对比 pages != expected 会导致校验失败并抛出 ValueError。建议在校验前先对 slide_prompts 按页码进行排序,这样不仅能提高校验的容错性,还能确保后续生成的 PromptData 顺序是正确的。

Suggested change
slide_prompts = request.slide_prompts or []
if len(slide_prompts) != ppt_config.num_pages:
raise ValueError(f"确认后的逐页设计数量不匹配: 期望{ppt_config.num_pages}页,实际{len(slide_prompts)}页")
pages = [item.page for item in slide_prompts]
expected = list(range(1, ppt_config.num_pages + 1))
if pages != expected:
slide_prompts = sorted(request.slide_prompts or [], key=lambda x: x.page)
if len(slide_prompts) != ppt_config.num_pages:
raise ValueError(f"确认后的逐页设计数量不匹配: 期望{ppt_config.num_pages}页,实际{len(slide_prompts)}页")
pages = [item.page for item in slide_prompts]
expected = list(range(1, ppt_config.num_pages + 1))
if pages != expected:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in efd576c. Confirmed slide prompts are sorted by page before validation and PromptData construction, so shuffled input is accepted when the page set is valid.

Comment thread src/model_router.py
Comment on lines +33 to +34
response = client.chat.completions.create(model=profile.model, messages=messages)
return response.choices[0].message.content or ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在调用大模型生成文本时,如果由于内容过滤或其他异常导致返回的 response.choices 为空,直接访问 response.choices[0] 会抛出 IndexError。建议在访问前先进行非空校验,以提高代码的健壮性。

        response = client.chat.completions.create(model=profile.model, messages=messages)
        if not response.choices:
            return ""
        return response.choices[0].message.content or ""

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in efd576c. Text generation now returns an empty string safely when the provider response contains no choices instead of raising IndexError.

Comment thread api/routes/upload.py
temp_path = Path(temp_file.name)

try:
content_str = DocumentParser().parse(temp_path).normalized_markdown

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

如果 DocumentParser().parse 抛出 ValueError(不支持的文件格式)或 RuntimeError(未安装 docling),这些异常不会被外层的 except UnicodeDecodeError 捕获,从而导致 FastAPI 返回通用的 500 Internal Server Error。建议在外层 try 块中添加对 Exception 的捕获,或者在此处显式捕获并抛出适当的 HTTPException(例如 400 或 500),以提供更友好的错误提示。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in efd576c. Upload parsing now maps unsupported parser errors to 400 responses and missing parser/runtime failures to explicit 500 responses with clearer messages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants