@yujp_vc/ppt-assistant 0.0.0-stage → 1.5.2

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/asset_catalog.py +159 -0
  4. package/assets/base_template.pptx +0 -0
  5. package/assets/cover_deco.png +0 -0
  6. package/assets/logo.png +0 -0
  7. package/assets/slide2_right.png +0 -0
  8. package/assets/theme_apricot.jpg +0 -0
  9. package/assets/theme_blush.jpg +0 -0
  10. package/assets/theme_coral.jpg +0 -0
  11. package/assets/theme_glow.jpg +0 -0
  12. package/assets/theme_mauve.jpg +0 -0
  13. package/assets/theme_peach.jpg +0 -0
  14. package/assets/theme_rose.jpg +0 -0
  15. package/assets/theme_sunset.jpg +0 -0
  16. package/assets/topright_deco.png +0 -0
  17. package/build_deck.py +2288 -0
  18. package/charts.py +329 -0
  19. package/check.py +236 -0
  20. package/config.json +5 -0
  21. package/contact_sheet.py +88 -0
  22. package/cordis.patch.yml +457 -0
  23. package/deck_baseline.py +335 -0
  24. package/delivery.py +143 -0
  25. package/density-recipe.md +110 -0
  26. package/example.json +26 -0
  27. package/formula.py +125 -0
  28. package/gen_image.py +395 -0
  29. package/generate_consensus.py +892 -0
  30. package/generate_ppt.py +1356 -0
  31. package/grasp_evidence.py +150 -0
  32. package/image_check.py +148 -0
  33. package/image_fetch.py +253 -0
  34. package/ingest.py +93 -0
  35. package/media.py +470 -0
  36. package/package.json +68 -4
  37. package/preset.yml +2 -0
  38. package/quality_check.py +196 -0
  39. package/render_ppt.py +91 -0
  40. package/requirements.txt +7 -0
  41. package/restyle.py +80 -0
  42. package/run_pptfast.sh +27 -0
  43. package/run_py.sh +43 -0
  44. package/slide_ops.py +160 -0
  45. package/slide_patch.py +153 -0
  46. package/speaker_notes.py +245 -0
  47. package/style-recipes.md +166 -0
  48. package/video_gif.py +164 -0
  49. package/vision_check.py +79 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,132 @@
1
- # Temporary Holding Version
1
+ # PPT 助手
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DeepSeek Harness 的独立 PPT 助手预设:需求分析、大纲、可编辑 PowerPoint 生成和修改、自主 GPT 生图、满幅/渐变蒙版图片排版、结构质检、可选的联网配图与渲染审查。安装只插入 `ppt-assistant` 预设,不修改标准模式、模型路由、权限或现有会话。
4
+
5
+ ## 安装
6
+
7
+ 两个通道,内容同源;内网优先用私库,公网用 GitHub。
8
+
9
+ - **公网(GitHub)**
10
+
11
+ ```bash
12
+ dsh plugin --profile web add https://github.com/YujpVc/ppt-assistant/releases/download/v1.5.0/ppt-assistant-1.5.0.tgz
13
+ ```
14
+
15
+ - **内网镜像(ZhaoWenhui/ppt-assistant,私有库,需要 Gitea 登录)**
16
+
17
+ ```bash
18
+ dsh plugin --profile web add http://172.18.24.36:3060/ZhaoWenhui/ppt-assistant/releases/download/v1.5.0/ppt-assistant-1.5.0.tgz
19
+ ```
20
+
21
+ 装完新建会话并选择 **PPT 助手**。包名为 `@yujp_vc/ppt-assistant`,通过 GitHub 与内网 Gitea 双通道分发,尚未发布到 npm。社区市场收录申请见 [PR #10056](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/10056),审核合并前无法在市场目录搜索到。
22
+
23
+ 此包保留原有 PPT 助手提示词和工具组合,同时附带 Python 生成引擎和通用主题资源;不复制用户配置、API 密钥、机构模板、机构 Logo、历史会话或生成的文件。提示词沿用原来的引擎发现规则:`deepseek-harness/ppt-assistant`、`~/ppt-assistant` 或安装后的 `node_modules/@yujp_vc/ppt-assistant`。
24
+
25
+ ## 运行环境
26
+
27
+ 使用 Python 3.10+。在引擎目录运行 `bash run_py.sh --help` 可确认解释器;`run_py.sh` 会挑选真正装有 python-pptx/Pillow 的 Python(DSH 非登录 shell 里的 `python3` 可能只是运行时)。依赖安装:`python3 -m pip install -r requirements.txt`,不会在插件安装时自动执行。`run_pptfast.sh` 可选使用已安装的 `@liustack/pptfast@0.20.0`,缺少时使用内置 `build_deck.py`;本包不自动下载 pptfast。
28
+
29
+ 生图客户端 `gen_image.py` 只用标准库,不需要额外依赖。它的配置按顺序从 `--config`、`$PPT_IMAGE_CONFIG`、引擎目录 `image-config.json`、`~/.config/ppt-assistant/image.json` 读取,格式为 `{"api_base": "https://uuapi.io/v1", "api_key": "..."}`;也接受 `UUAPI_API_KEY` / `UUAPI_BASE_URL`。密钥不写入本仓库、不写进成图、不打印。
30
+
31
+ 渲染需要 LibreOffice 和 `pdftoppm`,扫描文档 OCR 需要 Tesseract。联网取图可通过 `PEXELS_API_KEY` 设置个人密钥,视觉审查需要 `DEEPSEEK_API_KEY` 和可用的视觉模型权限。旧提示词提到的 dsh-office、mindmap 等补充插件需另行安装,本包不自动安装或宣称提供这些插件的工具。
32
+
33
+ ## 使用
34
+
35
+ ```bash
36
+ bash run_py.sh build_deck.py spec.json output.pptx
37
+ bash run_py.sh quality_check.py output.pptx
38
+ bash run_py.sh build_deck.py template-spec.json output.pptx --template template.pptx
39
+ bash run_py.sh quality_check.py output.pptx --template template.pptx
40
+ ```
41
+
42
+ 内置默认生成器支持原生可编辑文本、表格与图表。`quality_check.py` 会拒绝「整页图片、没有可编辑文字」的稿——一键 AI 工具导出的图片 PPTX 不能作为交付。模板、Logo 由使用者显式提供,本包提供通用默认模板;旧版 `generate_ppt.py` 可用 `--template` 指定用户模板,不包含本机机构素材。生成质量取决于输入素材和逐页检查;没有运行视觉检查时不得声称已通过。
43
+
44
+ ## 自主生图
45
+
46
+ ```bash
47
+ bash run_py.sh gen_image.py --prompt-file hero.txt --out assets/hero.png \
48
+ --size landscape --model gpt-image-2.5-flare --retries 1
49
+ ```
50
+
51
+ - 尺寸:`1024x1024`、`1536x1024`(landscape,满幅 16:9 用这个再裁)、`1024x1536`。
52
+ - `--ref a.png --ref b.png` 走参考图编辑,最多 4 张;`--dry-run` 先校验配置与端点。
53
+ - 每次生成会写同名 `.json` 记录提示词、模型、耗时与哈希;密钥不在记录里。
54
+ - 单张约 30 秒到 4 分钟,慢是上游排队、不是卡死。整套 deck 建议 3–6 张,同一组光线/色温词保证风格统一。
55
+ - `--transparent` 生成透明底 PNG,用于 `cutout_statement` 的破格主体。
56
+ - 生成后跑 `image_check.py <图> --intent "..." --min-score 7`,有文字/水印/主体不符就重生成;透明抠图加 `--transparent`(预览会压成白底,别误判)。
57
+ - 出口被挡时可走 SSH 隧道:`ssh -D 1081 <host>`,再设 `PPT_IMAGE_PROXY=socks5h://127.0.0.1:1081`。
58
+
59
+ ## 图片版式
60
+
61
+ `build_deck.py` 在原有版式外新增三种图片关系,全部由引擎完成裁切、渐变和阴影,不需要手工修图:
62
+
63
+ | 版式 | 用途 | 关键字段 |
64
+ | --- | --- | --- |
65
+ | `image_hero` | 封面/章节/收尾:满幅出血 + 渐变蒙版 + 大标题 | `image` `kicker` `title` `body` `scrim` `ink` |
66
+ | `image_overlay` | 满幅图 + 单侧渐变面板,放正文、要点、数字 | `image` `side` `title` `body` `bullets` `stats` |
67
+ | `image_wall` | 2–4 张瀑布流错位,圆角 + 细阴影 | `items[{image,title,body}]` |
68
+ | `image_triptych` | 三图一主两辅:左大图 + 右双叠 | `items[3]{image,title,body}` |
69
+
70
+ `image_split` / `image_grid` 也升级为满格裁切 + 圆角 + 软阴影。所有图片一律 cover 裁切、不留白边,蒙版颜色由图片暗部采样,文字区按页型自动避让。
71
+
72
+ ## 炫技版式
73
+
74
+ | 版式 | 用途 | 关键字段 |
75
+ | --- | --- | --- |
76
+ | `section_number` | 章节页:巨型数字 + 网格底 + 进度点 | `number` `title` `items` `sections` `section_no` `art` |
77
+ | `image_statement` | 满幅图 + 巨型字艺 + 结论标题 | `image` `art_text` `art` `title` |
78
+ | `split_diagonal` | 两张图斜切对照 | `images:[a,b]` `seam` `labels` |
79
+ | `glass_overlay` | 满幅图 + 毛玻璃面板 + 数字 | `image` `glass_opacity` `stats` |
80
+ | `heat_bars` | 甘特时间轴:阶段按时长累加,颜色映射火力 | `items[{title,body,fire,level,minutes,time}]` |
81
+ | `quote_stage` | 金句页:大引号 + 网格底 | `quote` `by` |
82
+ | `kpi_showcase` | 大数字墙 + 进度条 | `stats[{value,label,note,share}]` |
83
+ | `cutout_statement` | 透明抠图主体压住版面/标题 | `image` `side` `height` `flip` |
84
+ | `image_annotated` | 原生圆点 + 引线 + 标签 | `points[{x,y,text}]` |
85
+ | `image_arch` | 拱形裁切 + 侧栏文字 | `image` `items` |
86
+
87
+ - `art` 可选 `gradient`(主色渐变)、`outline`(空心描边)、`image`(用图片填字,需配 `image`,自动加主色细描边);字艺是装饰位图,正文仍是真文本。
88
+ - `**关键词**` 在主色里高亮;`meta.transition: fade|push|morph` 加页面切换;`treatment: mono|duotone|halftone|glitch` 做印刷/故障质感。
89
+ - 一套 deck 最多 2–3 页炫技,相邻页不连续堆特效。
90
+ - 路线选择:默认输出可编辑 PPTX;已有母版走模板模式;明确要 Google Slides 时说明本环境无原生导出;浏览器动画/交互是核心时才走单文件 HTML(需自备 layouts/styles,HTML 不在 quality_check 覆盖范围)。
91
+
92
+ ## 视觉总监与风格配方
93
+
94
+ - 先明确受众、要促成的判断、内容类型、场景、情绪;再写七行风格简报:三个形容词 / 一个设计张力 / 密度 / 配色逻辑 / 字体逻辑 / 图片行为 / 空间节奏。
95
+ - 从 `<engine-root>/style-recipes.md` 选一套主配方:`consulting` `formal` `civic` `artfair` `editorial` `history`;需要时最多加一套辅助,声明主/辅。把 `meta.recipe` 记进 spec。
96
+ - 配方 tokens 落进 `meta`:`theme` `accent` `colors` `font` `titleFont`。`titleFont` 可让标题单独用衬线(如 `Noto Serif CJK SC`),正文仍用 `font`。
97
+ - 评审顺序:先 `contact_sheet.py` 出缩略图总览,看节奏/密度/重复,再抽封面、最密页、特殊媒体页、结束页全尺寸看。
98
+
99
+ ```bash
100
+ bash run_py.sh contact_sheet.py <工作区>/.render --out contact.png --cols 4 --labels
101
+ ```
102
+
103
+ ## 页计划与风格
104
+
105
+ - 动笔前先写四行 style brief:用途与受众、气质、主色(只选一个)、图像风格。主色写进 `meta.accent`;需要换底色、面板或文字色时用 `meta.colors` 覆盖 `bg/panel/text/muted/accent`(6 位 hex)。
106
+ - 每个正文页的标题必须是一句话结论(「执行率 88.5%,预算仍有余量」),不是名词短语。写 spec 前先列页计划:`页 / 结论句 / 证据 / 视觉类型 / 来源`。
107
+ - 配色按 HSL 想:邻近色相做区分,同色相拉明度做层级,降饱和+提亮=莫兰迪;一页只留一个高饱和重点色。
108
+
109
+ ## 改稿合同(scoped edit)
110
+
111
+ 改现有 PPT 时不要直接动手,先立基线:
112
+
113
+ ```bash
114
+ bash run_py.sh deck_baseline.py snapshot 原稿.pptx baseline.json
115
+ # 修改工作副本 → 重建
116
+ bash run_py.sh deck_baseline.py compare baseline.json 新稿.pptx \
117
+ --protect P01,P02,P05 --expect P03 --out ledger.md
118
+ ```
119
+
120
+ - 稳定页名 `P01…` 写在 slide name 里,换序也不丢身份;台账按页列出标题、文字、数字、图片的变化。
121
+ - 保护页被改、越权修改、缺页都会让命令以 exit 2 结束;交付时附上 `ledger.md`。
122
+ - 数字逐位核对,不为了排版改精度;截图与交付文件版本必须一致。
123
+
124
+ ## 打包
125
+
126
+ ```bash
127
+ npm pack # 生成 @yujp_vc/ppt-assistant-1.5.0.tgz
128
+ ```
129
+
130
+ 插件不包含模型服务或账号,遵循会话已有权限。安装需要 DSH `0.2.1-rc.3` 或兼容的 `0.2.x`,已在 Linux Desktop 0.2.1-rc.3 的插件管理入口验证安装;激活验证结果以发行说明为准。
131
+
132
+ 引擎与预设提取自本机 DeepSeek Harness PPT 助手,保留 MIT 许可及上游署名。
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ """Create and verify a deterministic inventory of presentation source assets."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import mimetypes
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+
13
+ KIND_BY_SUFFIX = {
14
+ ".pptx": "presentation", ".ppt": "presentation", ".pdf": "document",
15
+ ".docx": "document", ".xlsx": "data", ".csv": "data", ".json": "data",
16
+ ".md": "text", ".txt": "text", ".html": "text", ".css": "text",
17
+ ".png": "image", ".jpg": "image", ".jpeg": "image", ".gif": "image",
18
+ ".webp": "image", ".mp4": "video", ".mov": "video", ".avi": "video",
19
+ ".mkv": "video", ".wav": "audio", ".mp3": "audio",
20
+ }
21
+
22
+
23
+ def digest(path: Path) -> str:
24
+ """Return the SHA256 of a regular file."""
25
+ hasher = hashlib.sha256()
26
+ with path.open("rb") as stream:
27
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
28
+ hasher.update(chunk)
29
+ return hasher.hexdigest()
30
+
31
+
32
+ def relative_name(value) -> str:
33
+ """Validate a canonical, non-hidden POSIX path from a JSON manifest."""
34
+ if (not isinstance(value, str) or not value or "\\" in value
35
+ or Path(value).is_absolute() or Path(value).as_posix() != value
36
+ or any(part.startswith(".") for part in value.split("/"))):
37
+ raise ValueError("manifest contains an invalid relative path")
38
+ return value
39
+
40
+
41
+ def _entry(root: Path, path: Path) -> dict:
42
+ relative = path.relative_to(root).as_posix()
43
+ suffix = path.suffix.lower()
44
+ return {
45
+ "path": relative,
46
+ "bytes": path.stat().st_size,
47
+ "sha256": digest(path),
48
+ "kind": KIND_BY_SUFFIX.get(suffix, "other"),
49
+ "mime": mimetypes.guess_type(path.name)[0] or "application/octet-stream",
50
+ }
51
+
52
+
53
+ def scan(root: Path, *, exclude: set[str] | None = None) -> dict:
54
+ """Inventory regular files below ``root`` in stable relative-path order."""
55
+ root = root.resolve()
56
+ if not root.is_dir():
57
+ raise ValueError("asset root must be a directory")
58
+ excluded = {relative_name(item) for item in (exclude or set())}
59
+ files = []
60
+ for path in sorted(root.rglob("*")):
61
+ if not path.is_file() or path.is_symlink():
62
+ continue
63
+ relative = path.relative_to(root).as_posix()
64
+ if relative in excluded or any(part.startswith(".") for part in path.relative_to(root).parts):
65
+ continue
66
+ files.append(_entry(root, path))
67
+ return {"version": 1, "files": files}
68
+
69
+
70
+ def write_manifest(root: Path, output: Path, markdown: Path | None = None) -> dict:
71
+ """Write JSON and optional Markdown inventory without including the outputs."""
72
+ root = root.resolve()
73
+ output = output.resolve()
74
+ if markdown and markdown.resolve() == output:
75
+ raise ValueError("JSON and Markdown outputs must be separate")
76
+ excluded = {output.relative_to(root).as_posix()} if output.is_relative_to(root) else set()
77
+ if markdown and markdown.resolve().is_relative_to(root):
78
+ excluded.add(markdown.resolve().relative_to(root).as_posix())
79
+ manifest = scan(root, exclude=excluded)
80
+ manifest["excluded"] = sorted(excluded)
81
+ output.parent.mkdir(parents=True, exist_ok=True)
82
+ with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=output.parent, delete=False) as stream:
83
+ temporary = Path(stream.name)
84
+ json.dump(manifest, stream, ensure_ascii=False, indent=2)
85
+ stream.write("\n")
86
+ temporary.replace(output)
87
+ if markdown:
88
+ groups: dict[str, int] = {}
89
+ for item in manifest["files"]:
90
+ groups[item["kind"]] = groups.get(item["kind"], 0) + 1
91
+ lines = ["# Asset inventory", "", f"Files: {len(manifest['files'])}", "",
92
+ "| Kind | Count |", "| --- | ---: |"]
93
+ lines.extend(f"| {kind} | {groups[kind]} |" for kind in sorted(groups))
94
+ lines.extend(["", "| Path | Kind | Bytes | SHA256 |", "| --- | --- | ---: | --- |"])
95
+ for item in manifest["files"]:
96
+ name = item["path"].replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
97
+ name = name.replace("|", "&#124;").replace("`", "&#96;").replace("\n", "&#10;").replace("\r", "&#13;")
98
+ lines.append(f"| {name} | {item['kind']} | {item['bytes']} | `{item['sha256']}` |")
99
+ markdown.parent.mkdir(parents=True, exist_ok=True)
100
+ markdown.write_text("\n".join(lines) + "\n", encoding="utf-8")
101
+ return manifest
102
+
103
+
104
+ def verify(root: Path, manifest_path: Path, *, strict: bool = False) -> dict:
105
+ """Verify listed paths and hashes; strict mode also rejects unlisted files."""
106
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
107
+ if not isinstance(manifest, dict) or manifest.get("version") != 1 or not isinstance(manifest.get("files"), list):
108
+ raise ValueError("unsupported asset manifest")
109
+ root = root.resolve()
110
+ if not root.is_dir():
111
+ raise ValueError("asset root must be a directory")
112
+ excluded_values = manifest.get("excluded", [])
113
+ if not isinstance(excluded_values, list):
114
+ raise ValueError("excluded must be a list of relative paths")
115
+ excluded = {relative_name(value) for value in excluded_values}
116
+ listed = set()
117
+ for item in manifest["files"]:
118
+ if not isinstance(item, dict):
119
+ raise ValueError("asset entry must be an object")
120
+ path = relative_name(item.get("path"))
121
+ if path in listed or path in excluded:
122
+ raise ValueError("manifest contains an invalid or duplicate path")
123
+ listed.add(path)
124
+ unresolved = root / path
125
+ candidate = unresolved.resolve()
126
+ if (not candidate.is_relative_to(root) or not candidate.is_file()
127
+ or any(parent.is_symlink() for parent in [unresolved, *unresolved.parents] if parent != root)):
128
+ raise ValueError(f"asset is missing or leaves root: {path}")
129
+ if digest(candidate) != item.get("sha256"):
130
+ raise ValueError(f"SHA256 mismatch: {path}")
131
+ if strict:
132
+ if manifest_path.resolve().is_relative_to(root):
133
+ excluded.add(manifest_path.resolve().relative_to(root).as_posix())
134
+ actual = {item["path"] for item in scan(root, exclude=excluded)["files"]}
135
+ if actual != listed:
136
+ raise ValueError("strict inventory differs from the asset root")
137
+ return {"verified": len(listed), "strict": strict}
138
+
139
+
140
+ def main() -> int:
141
+ parser = argparse.ArgumentParser(description=__doc__)
142
+ commands = parser.add_subparsers(dest="command", required=True)
143
+ writer = commands.add_parser("scan")
144
+ writer.add_argument("root", type=Path)
145
+ writer.add_argument("--output", type=Path, required=True)
146
+ writer.add_argument("--markdown", type=Path)
147
+ checker = commands.add_parser("verify")
148
+ checker.add_argument("root", type=Path)
149
+ checker.add_argument("manifest", type=Path)
150
+ checker.add_argument("--strict", action="store_true")
151
+ args = parser.parse_args()
152
+ result = write_manifest(args.root, args.output, args.markdown) if args.command == "scan" \
153
+ else verify(args.root, args.manifest, strict=args.strict)
154
+ print(json.dumps(result, ensure_ascii=False, indent=2))
155
+ return 0
156
+
157
+
158
+ if __name__ == "__main__":
159
+ raise SystemExit(main())
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file