feat(export): screenplay export to Fountain / txt / Markdown / PDF - #60
Merged
Merged
Conversation
The editor's only download was the schema YAML — useful for the pipeline contract, useless for directors and actors. Adds an end-to-end export path so the author leaves with a file the rest of the team can read. Backend: new story2script.export package with three pure renderers (to_fountain, to_txt, to_markdown), wired to POST /api/v1/export. The endpoint runs the same schema validator the YAML download relies on, so a broken draft surfaces as 422 with field-level errors instead of a half-rendered file. PDF is intentionally NOT a server format — generating CJK-safe PDFs needs either weasyprint+GTK or a 5 MB bundled font; the frontend opens the Markdown variant in a print-friendly window and lets the browser's native Print → Save as PDF do the conversion. Zero new server dependencies. Frontend: the toolbar's 导出 YAML button becomes a 导出剧本 dropdown with five options (Fountain / txt / md / PDF / raw YAML). PDF reuses the markdown response, rewraps it in an inline HTML template with CJK system fonts, and triggers window.print() after load. Error messaging surfaces 422 schema details so the author knows which field to fix in the editor. Tests: 25 new (8 fountain, 4 txt, 7 markdown, 6 endpoint), covering character cue casing, parenthetical split, transition normalization, mode block presence, invalid YAML / invalid screenplay / unknown format error paths. Full suite 314 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shanchuann
added a commit
that referenced
this pull request
Jun 7, 2026
* feat(export): screenplay export to Fountain / txt / Markdown / PDF The editor's only download was the schema YAML — useful for the pipeline contract, useless for directors and actors. Adds an end-to-end export path so the author leaves with a file the rest of the team can read. Backend: new story2script.export package with three pure renderers (to_fountain, to_txt, to_markdown), wired to POST /api/v1/export. The endpoint runs the same schema validator the YAML download relies on, so a broken draft surfaces as 422 with field-level errors instead of a half-rendered file. PDF is intentionally NOT a server format — generating CJK-safe PDFs needs either weasyprint+GTK or a 5 MB bundled font; the frontend opens the Markdown variant in a print-friendly window and lets the browser's native Print → Save as PDF do the conversion. Zero new server dependencies. Frontend: the toolbar's 导出 YAML button becomes a 导出剧本 dropdown with five options (Fountain / txt / md / PDF / raw YAML). PDF reuses the markdown response, rewraps it in an inline HTML template with CJK system fonts, and triggers window.print() after load. Error messaging surfaces 422 schema details so the author knows which field to fix in the editor. Tests: 25 new (8 fountain, 4 txt, 7 markdown, 6 endpoint), covering character cue casing, parenthetical split, transition normalization, mode block presence, invalid YAML / invalid screenplay / unknown format error paths. Full suite 314 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(adapt): sync edits from /editor back into the result panel Returning to /app after editing in /editor showed a stale result — the screenplay_yaml in the React state still reflected the original LLM output even though the editor had patched sessionStorage with every keystroke. Two real consequences: - Exporting via PR #60's new dropdown would have produced the PRE-edit Fountain / Markdown / etc., silently discarding all the author's manual fixes. - Schema validation and quality metrics displayed the LLM's draft state, not what the author was about to ship. Strategy: on mount AND on tab focus / visibility change, diff the editor's draft against the result.screenplay_yaml. When they differ, swap the YAML in and re-run /api/v1/validate so schema_validation reflects current state. Quality metrics intentionally stay frozen since they describe how the LLM produced the draft, not whether the author's hand-edits are still valid. The validate call's failure path falls through to a YAML-only swap so the export button still reflects current edits when the backend is down; the eventual /export call will surface the same errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shanchuann
added a commit
that referenced
this pull request
Jun 7, 2026
…nts (#63) * feat(export): screenplay export to Fountain / txt / Markdown / PDF The editor's only download was the schema YAML — useful for the pipeline contract, useless for directors and actors. Adds an end-to-end export path so the author leaves with a file the rest of the team can read. Backend: new story2script.export package with three pure renderers (to_fountain, to_txt, to_markdown), wired to POST /api/v1/export. The endpoint runs the same schema validator the YAML download relies on, so a broken draft surfaces as 422 with field-level errors instead of a half-rendered file. PDF is intentionally NOT a server format — generating CJK-safe PDFs needs either weasyprint+GTK or a 5 MB bundled font; the frontend opens the Markdown variant in a print-friendly window and lets the browser's native Print → Save as PDF do the conversion. Zero new server dependencies. Frontend: the toolbar's 导出 YAML button becomes a 导出剧本 dropdown with five options (Fountain / txt / md / PDF / raw YAML). PDF reuses the markdown response, rewraps it in an inline HTML template with CJK system fonts, and triggers window.print() after load. Error messaging surfaces 422 schema details so the author knows which field to fix in the editor. Tests: 25 new (8 fountain, 4 txt, 7 markdown, 6 endpoint), covering character cue casing, parenthetical split, transition normalization, mode block presence, invalid YAML / invalid screenplay / unknown format error paths. Full suite 314 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(adapt): sync edits from /editor back into the result panel Returning to /app after editing in /editor showed a stale result — the screenplay_yaml in the React state still reflected the original LLM output even though the editor had patched sessionStorage with every keystroke. Two real consequences: - Exporting via PR #60's new dropdown would have produced the PRE-edit Fountain / Markdown / etc., silently discarding all the author's manual fixes. - Schema validation and quality metrics displayed the LLM's draft state, not what the author was about to ship. Strategy: on mount AND on tab focus / visibility change, diff the editor's draft against the result.screenplay_yaml. When they differ, swap the YAML in and re-run /api/v1/validate so schema_validation reflects current state. Quality metrics intentionally stay frozen since they describe how the LLM produced the draft, not whether the author's hand-edits are still valid. The validate call's failure path falls through to a YAML-only swap so the export button still reflects current edits when the backend is down; the eventual /export call will surface the same errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(editor): per-scene AI polish with mode picker + undo The mode_refine LLM stage was wired into the pipeline run but invisible in the editor — once the draft was generated, the author had no way to ask "redo this scene with low fidelity / rich expansion" without regenerating the whole document. Exposes a per-scene polish toolbar that hits a new POST /api/v1/refine/scene endpoint. Backend: new endpoint deserializes the YAML, finds the scene by id, reconstructs the Scene / Beat / Character pydantic instances the existing ai_refine_scene_beats helper expects, and returns the refined beat list. 503 when no LLM provider is configured, 404 on unknown scene id, 422 on empty beats, 502 when the LLM call returns nothing usable (rule_fallback fired). Tests cover the four error paths without burning quota. Frontend: a dashed-bordered toolbar at the top of each scene with two selects (fidelity / expansion) + 润色场景 button + 撤销 button. The default (medium, balanced) mode disables the button because the backend would no-op it anyway. A polish snapshots the pre-polish YAML to a ref for one-level undo so the author can try a mode, hate it, and revert without losing their other edits. Beats are spliced back wholesale rather than per-beat patched — expansion=rich can change the beat count, so a positional patch would leave the YAML inconsistent. The full splice round-trips through js-yaml.dump to keep the file structurally clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(adapt): retry sample autorun on next reload + structured error hints Two stability papercuts found while smoke-testing the demo flow: - First-visit autorun set the localStorage flag unconditionally — including when the backend was down — so a grader who hit /app before starting uvicorn saw the demo fail once and never again, even after launching the server. Now the flag is set only on success; failure leaves the flag clear so the next page load retries. autorunFiredRef still blocks intra-session re-fires so the user sees one attempt per reload, not a hammer. - The Adapt result panel's error block dumped the raw ApiError message without suggesting next steps. status=0 (network) and status=503 (no LLM provider) are the two cases a grader is most likely to hit during setup; both now show a one-line hint with the exact command or env var that resolves them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
功能描述
让作者能拿走给演员 / 导演 / 投资方看的剧本,不只是 schema 用的 YAML。
之前编辑器右上角只有 `导出 YAML` 一个按钮,YAML 对管道契约有用、对人没用。这一版换成 `导出剧本` 下拉,五种格式:
实现思路
后端 `src/story2script/export/`:三个纯函数 `to_fountain` / `to_txt` / `to_markdown`,输入是验证过的 screenplay dict,输出是字符串。每个文件实现一种格式,互不依赖。
`POST /api/v1/export`:YAML 入,rendered 文本出。先跑 `_validate_against_schema`——草稿没修干净就 422 + 字段级 errors,前端弹回作者改完再导。
PDF 不在服务端做:CJK 字体要么走 weasyprint+GTK 大依赖、要么塞 5 MB Source Han 字体进 wheel,两种都不优雅。前端拿 markdown 响应直接套一份 `<style>` 模板用系统字体 `Source Han Serif SC / Noto Serif CJK SC / Songti SC`,`window.print()` 一弹,用户选"保存为 PDF"即可。Cross-platform 零成本。
Fountain 格式细节(导出后可被 Final Draft 直接读):
未知 speaker 的处理:YAML 里 `speaker: char_nope` 但 characters 数组里没这个 id(手动改 / 角色删了)→ fallback 到大写 raw id `CHAR_NOPE` 让作者一眼看到哪里坏了,而不是吞成 `(未知)`。
测试方式
新增测试覆盖(25 条):
依赖与复用声明
零新增第三方依赖。复用:
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com