@yujp_vc/ppt-assistant 1.5.1 → 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.
- package/build_deck.py +225 -19
- package/package.json +1 -1
package/build_deck.py
CHANGED
|
@@ -32,7 +32,7 @@ from grasp_evidence import (candidate_projections, depth_colors, depth_pair,
|
|
|
32
32
|
gated_candidate_projections, gripper_projection)
|
|
33
33
|
|
|
34
34
|
LAYOUTS = {"cover", "cards", "process", "comparison", "table", "chart",
|
|
35
|
-
"image_split", "image_grid", "image_triptych", "heat_bars", "chart_dashboard", "closing",
|
|
35
|
+
"image_split", "image_grid", "image_triptych", "ingredient_board", "ingredient_split", "state_triptych", "state_band", "state_gallery", "prep_stove", "heat_bars", "chart_dashboard", "closing",
|
|
36
36
|
"image_hero", "image_overlay", "image_wall",
|
|
37
37
|
"section_number", "image_statement",
|
|
38
38
|
"split_diagonal", "glass_overlay", "quote_stage", "kpi_showcase",
|
|
@@ -118,6 +118,58 @@ def validate(spec: dict, base: Path) -> None:
|
|
|
118
118
|
raise ValueError(f"slide {index}: heat_bars fire must be 大火/中火/小火/微火")
|
|
119
119
|
if not isinstance(item.get("minutes"), (int, float)) or float(item["minutes"]) <= 0:
|
|
120
120
|
raise ValueError(f"slide {index}: heat_bars minutes must be positive")
|
|
121
|
+
if layout == "ingredient_split":
|
|
122
|
+
if not slide.get("image"):
|
|
123
|
+
raise ValueError(f"slide {index}: ingredient_split requires an image")
|
|
124
|
+
items = slide.get("items")
|
|
125
|
+
if not isinstance(items, list) or not 3 <= len(items) <= 6:
|
|
126
|
+
raise ValueError(f"slide {index}: ingredient_split requires 3–6 items")
|
|
127
|
+
for item in items:
|
|
128
|
+
if not isinstance(item, dict) or not item.get("label") or not item.get("value"):
|
|
129
|
+
raise ValueError(f"slide {index}: every ingredient item requires label and value")
|
|
130
|
+
assets = [slide]
|
|
131
|
+
if layout == "state_gallery":
|
|
132
|
+
items = slide.get("items")
|
|
133
|
+
if not isinstance(items, list) or len(items) != 3:
|
|
134
|
+
raise ValueError(f"slide {index}: state_gallery requires exactly 3 items")
|
|
135
|
+
if any(not isinstance(item, dict) or not item.get("image") for item in items):
|
|
136
|
+
raise ValueError(f"slide {index}: every state item requires an image")
|
|
137
|
+
assets = items
|
|
138
|
+
if layout == "state_band":
|
|
139
|
+
items = slide.get("items")
|
|
140
|
+
if not isinstance(items, list) or len(items) != 3:
|
|
141
|
+
raise ValueError(f"slide {index}: state_band requires exactly 3 items")
|
|
142
|
+
if any(not isinstance(item, dict) or not item.get("image") for item in items):
|
|
143
|
+
raise ValueError(f"slide {index}: every state item requires an image")
|
|
144
|
+
assets = items
|
|
145
|
+
if layout == "state_triptych":
|
|
146
|
+
items = slide.get("items")
|
|
147
|
+
if not isinstance(items, list) or len(items) != 3:
|
|
148
|
+
raise ValueError(f"slide {index}: state_triptych requires exactly 3 items")
|
|
149
|
+
if any(not isinstance(item, dict) or not item.get("image") for item in items):
|
|
150
|
+
raise ValueError(f"slide {index}: every state item requires an image")
|
|
151
|
+
assets = items
|
|
152
|
+
if layout == "ingredient_board":
|
|
153
|
+
if not slide.get("image"):
|
|
154
|
+
raise ValueError(f"slide {index}: ingredient_board requires an image")
|
|
155
|
+
items = slide.get("items")
|
|
156
|
+
if not isinstance(items, list) or not 3 <= len(items) <= 6:
|
|
157
|
+
raise ValueError(f"slide {index}: ingredient_board requires 3–6 items")
|
|
158
|
+
for item in items:
|
|
159
|
+
if not isinstance(item, dict) or not item.get("label") or not item.get("value"):
|
|
160
|
+
raise ValueError(f"slide {index}: every ingredient item requires label and value")
|
|
161
|
+
assets = [slide]
|
|
162
|
+
if layout == "prep_stove":
|
|
163
|
+
items = slide.get("items")
|
|
164
|
+
if not isinstance(items, list) or not 3 <= len(items) <= 6:
|
|
165
|
+
raise ValueError(f"slide {index}: prep_stove requires 3–6 items")
|
|
166
|
+
for item in items:
|
|
167
|
+
if not isinstance(item, dict) or item.get("lane") not in {"prep", "stove"}:
|
|
168
|
+
raise ValueError(f"slide {index}: prep_stove lane must be prep or stove")
|
|
169
|
+
if not isinstance(item.get("minutes"), (int, float)) or float(item["minutes"]) <= 0:
|
|
170
|
+
raise ValueError(f"slide {index}: prep_stove minutes must be positive")
|
|
171
|
+
if not item.get("title") or not item.get("body"):
|
|
172
|
+
raise ValueError(f"slide {index}: prep_stove item requires title and body")
|
|
121
173
|
if layout == "image_triptych":
|
|
122
174
|
items = slide.get("items")
|
|
123
175
|
if not isinstance(items, list) or len(items) != 3:
|
|
@@ -1168,27 +1220,180 @@ class DeckBuilder:
|
|
|
1168
1220
|
self.text(slide, item["body"], body_x, y + .43, body_w, .26, 10, "muted")
|
|
1169
1221
|
if page.get("source"):
|
|
1170
1222
|
self.text(slide, page["source"], .6, 6.70, 11.8, .20, 9, "muted")
|
|
1171
|
-
elif layout == "
|
|
1223
|
+
elif layout == "ingredient_split":
|
|
1172
1224
|
items = page["items"]
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
self.
|
|
1225
|
+
self.artwork(slide, page["image"], 5.55, 0, 7.78, 7.5, radius=0.0)
|
|
1226
|
+
self.box(slide, 0, 0, 5.55, 7.5, "140C08")
|
|
1227
|
+
self.box(slide, 5.54, 0, .012, 7.5, "B98A63")
|
|
1228
|
+
self.text(slide, page.get("kicker", "备料 / MISE EN PLACE"), .65, .55,
|
|
1229
|
+
4.5, .22, 9, "B98A63", True)
|
|
1230
|
+
self.text(slide, "800", .60, 1.00, 2.6, 1.16, 64, "FFFFFF", True,
|
|
1231
|
+
font=self.head_font)
|
|
1232
|
+
self.text(slide, "g", 3.08, 1.48, .5, .4, 20, "B98A63", True)
|
|
1233
|
+
self.text(slide, page.get("fish_label", "草鱼(或黑鱼 750 g)"), .65, 2.18,
|
|
1234
|
+
4.35, .25, 9.5, "E8D5C4")
|
|
1235
|
+
self.text(slide, "300", .60, 2.54, 2.6, 1.16, 64, "FFFFFF", True,
|
|
1236
|
+
font=self.head_font)
|
|
1237
|
+
self.text(slide, "g", 3.08, 3.02, .5, .4, 20, "B98A63", True)
|
|
1238
|
+
self.text(slide, page.get("acid_label", "老坛酸菜"), .65, 3.72,
|
|
1239
|
+
4.35, .25, 9.5, "E8D5C4")
|
|
1240
|
+
self.text(slide, "8 : 3", 4.15, 1.10, 1.1, .34, 18, "B98A63", True,
|
|
1241
|
+
align=PP_ALIGN.RIGHT)
|
|
1242
|
+
self.text(slide, "鱼 : 酸菜", 4.15, 1.42, 1.1, .20, 8, "E8D5C4",
|
|
1243
|
+
align=PP_ALIGN.RIGHT)
|
|
1244
|
+
for i, item in enumerate(items):
|
|
1245
|
+
y = 4.05 + i * .38
|
|
1246
|
+
self.text(slide, item["label"], .65, y, .95, .22, 8, "B98A63", True)
|
|
1247
|
+
self.text(slide, item["value"], 1.65, y - .02, 3.55, .30, 9.5, "F4E9DC")
|
|
1248
|
+
if page.get("source"):
|
|
1249
|
+
self.text(slide, page["source"], .65, 6.52, 4.55, .52, 8.5, "C9B3A0")
|
|
1250
|
+
elif layout == "state_gallery":
|
|
1251
|
+
items = page["items"]
|
|
1252
|
+
self.box(slide, .60, 2.00, 7.15, 4.45, "B04A2A")
|
|
1253
|
+
self.artwork(slide, items[0]["image"], .70, 2.10, 6.95, 4.25, radius=0.0)
|
|
1254
|
+
self.text(slide, "01", .60, 6.48, .7, .40, 22, "accent", True)
|
|
1255
|
+
self.text(slide, items[0]["title"], 1.28, 6.50, 6.4, .32, 17, "text", True)
|
|
1256
|
+
self.text(slide, items[0]["body"], 1.28, 6.78, 6.4, .22, 11, "muted")
|
|
1257
|
+
for idx, (y, item) in enumerate(((2.00, items[1]), (4.52, items[2])), start=1):
|
|
1258
|
+
self.box(slide, 8.00, y, 4.70, 2.00, "B04A2A")
|
|
1259
|
+
self.artwork(slide, item["image"], 8.06, y + .06, 4.58, 1.88, radius=0.0)
|
|
1260
|
+
self.text(slide, f"0{idx+1}", 8.00, y + 2.04, .55, .28, 13, "accent", True)
|
|
1261
|
+
self.text(slide, item["title"], 8.52, y + 2.02, 4.0, .26, 12.5, "text", True)
|
|
1262
|
+
self.text(slide, item["body"], 8.52, y + 2.26, 4.0, .18, 8.8, "muted")
|
|
1263
|
+
elif layout == "state_band":
|
|
1264
|
+
items = page["items"]
|
|
1265
|
+
self.box(slide, 0, 0, 13.333, 1.18, "FBF6EE")
|
|
1266
|
+
self.text(slide, page["title"], .6, .28, 12.1, .52, 26, "text", True,
|
|
1267
|
+
font=self.head_font)
|
|
1268
|
+
if page.get("subtitle"):
|
|
1269
|
+
self.text(slide, page["subtitle"], .6, .82, 12.1, .26, 13, "muted")
|
|
1270
|
+
top, bottom = 1.18, 7.02
|
|
1271
|
+
h2 = (bottom - top - .04) / 2
|
|
1272
|
+
self.artwork(slide, items[0]["image"], 0, top, 8.2, bottom - top, radius=0.0)
|
|
1273
|
+
self.artwork(slide, items[1]["image"], 8.2, top, 5.133, h2, radius=0.0)
|
|
1274
|
+
self.artwork(slide, items[2]["image"], 8.2, top + h2 + .04, 5.133, h2, radius=0.0)
|
|
1275
|
+
# left caption band
|
|
1276
|
+
self.box(slide, 0, 6.02, 8.2, 1.0, "FBF6EE")
|
|
1277
|
+
self.box(slide, 0, 6.02, 8.2, .018, "B04A2A")
|
|
1278
|
+
self.text(slide, "01", .55, 6.12, .7, .38, 20, "accent", True)
|
|
1279
|
+
self.text(slide, items[0]["title"], 1.28, 6.12, 6.5, .34, 18, "text", True)
|
|
1280
|
+
self.text(slide, items[0]["body"], 1.28, 6.46, 6.5, .26, 11.5, "muted")
|
|
1281
|
+
# right top band
|
|
1282
|
+
self.box(slide, 8.2, 3.30, 5.133, .68, "FBF6EE")
|
|
1283
|
+
self.box(slide, 8.2, 3.30, 5.133, .016, "B04A2A")
|
|
1284
|
+
self.text(slide, "02", 8.42, 3.38, .55, .28, 13, "accent", True)
|
|
1285
|
+
self.text(slide, items[1]["title"], 9.02, 3.36, 4.0, .26, 12.5, "text", True)
|
|
1286
|
+
self.text(slide, items[1]["body"], 9.02, 3.60, 4.0, .22, 9.5, "muted")
|
|
1287
|
+
# right bottom band
|
|
1288
|
+
self.box(slide, 8.2, 6.34, 5.133, .68, "FBF6EE")
|
|
1289
|
+
self.box(slide, 8.2, 6.34, 5.133, .016, "B04A2A")
|
|
1290
|
+
self.text(slide, "03", 8.42, 6.42, .55, .28, 13, "accent", True)
|
|
1291
|
+
self.text(slide, items[2]["title"], 9.02, 6.40, 4.0, .26, 12.5, "text", True)
|
|
1292
|
+
self.text(slide, items[2]["body"], 9.02, 6.64, 4.0, .22, 9.5, "muted")
|
|
1293
|
+
elif layout == "state_triptych":
|
|
1294
|
+
items = page["items"]
|
|
1295
|
+
self.box(slide, 0, 0, 13.333, 1.32, "FBF6EE")
|
|
1296
|
+
self.text(slide, page["title"], .6, .36, 12.1, .55, 28, "text", True,
|
|
1297
|
+
font=self.head_font)
|
|
1298
|
+
if page.get("subtitle"):
|
|
1299
|
+
self.text(slide, page["subtitle"], .6, .94, 12.1, .28, 14, "muted")
|
|
1300
|
+
top = 1.32
|
|
1301
|
+
bottom = 7.02
|
|
1302
|
+
h = (bottom - top) / 2 - .02
|
|
1303
|
+
scrim = {"direction": "bottom", "color": "120B08", "start": .42,
|
|
1304
|
+
"end": .98, "opacity": .88}
|
|
1305
|
+
self.artwork(slide, items[0]["image"], 0, top, 8.2, bottom - top,
|
|
1177
1306
|
radius=0.0, scrim=scrim)
|
|
1178
|
-
self.
|
|
1179
|
-
18, "FFFFFF", True)
|
|
1180
|
-
self.text(slide, items[0]["body"], .95, 5.96, 6.85, .24, 12, "FFF6EC")
|
|
1181
|
-
self.box(slide, 8.24, top, .012, 4.38, "D8C6B4")
|
|
1182
|
-
self.artwork(slide, items[1]["image"], 8.40, top, 4.30, 1.98,
|
|
1307
|
+
self.artwork(slide, items[1]["image"], 8.2, top, 5.133, h,
|
|
1183
1308
|
radius=0.0, scrim=scrim)
|
|
1184
|
-
self.
|
|
1185
|
-
13, "FFFFFF", True)
|
|
1186
|
-
self.text(slide, items[1]["body"], 8.62, 3.44, 3.88, .20, 10.5, "FFFFFF")
|
|
1187
|
-
self.artwork(slide, items[2]["image"], 8.40, 4.60, 4.30, 1.84,
|
|
1309
|
+
self.artwork(slide, items[2]["image"], 8.2, top + h + .04, 5.133, h,
|
|
1188
1310
|
radius=0.0, scrim=scrim)
|
|
1189
|
-
self.text(slide,
|
|
1190
|
-
|
|
1191
|
-
self.text(slide, items[
|
|
1311
|
+
self.text(slide, "01", .55, top + .28, .8, .44, 24, "FFFFFF", True)
|
|
1312
|
+
self.text(slide, items[0]["title"], .55, bottom - 1.00, 7.0, .38, 20, "FFFFFF", True)
|
|
1313
|
+
self.text(slide, items[0]["body"], .55, bottom - .58, 7.0, .24, 12, "FFF6EC")
|
|
1314
|
+
self.text(slide, "02", 8.65, top + .26, .8, .36, 20, "FFFFFF", True)
|
|
1315
|
+
self.text(slide, items[1]["title"], 8.65, top + h - .60, 4.3, .28, 14, "FFFFFF", True)
|
|
1316
|
+
self.text(slide, items[1]["body"], 8.65, top + h - .32, 4.3, .22, 10.5, "FFF6EC")
|
|
1317
|
+
y2 = top + h + .04
|
|
1318
|
+
self.text(slide, "03", 8.65, y2 + .26, .8, .36, 20, "FFFFFF", True)
|
|
1319
|
+
self.text(slide, items[2]["title"], 8.65, bottom - .66, 4.3, .28, 14, "FFFFFF", True)
|
|
1320
|
+
self.text(slide, items[2]["body"], 8.65, bottom - .38, 4.3, .22, 10.5, "FFF6EC")
|
|
1321
|
+
elif layout == "ingredient_board":
|
|
1322
|
+
items = page["items"]
|
|
1323
|
+
top = 2.02
|
|
1324
|
+
self.artwork(slide, page["image"], .6, top, 7.2, 4.86, radius=0.0)
|
|
1325
|
+
self.text(slide, page.get("fish_value", "800 g"), 8.0, 1.98, 2.25, .55,
|
|
1326
|
+
30, "accent", True, font=self.head_font)
|
|
1327
|
+
self.text(slide, page.get("fish_label", "草鱼"), 8.0, 2.52, 2.2, .22,
|
|
1328
|
+
10.5, "muted")
|
|
1329
|
+
self.text(slide, page.get("acid_value", "300 g"), 10.3, 1.98, 2.25, .55,
|
|
1330
|
+
30, "accent", True, font=self.head_font)
|
|
1331
|
+
self.text(slide, page.get("acid_label", "酸菜"), 10.3, 2.52, 2.2, .22,
|
|
1332
|
+
10.5, "muted")
|
|
1333
|
+
self.box(slide, 8.0, 2.82, 4.65, .012, "D8C4B2")
|
|
1334
|
+
for i, item in enumerate(items):
|
|
1335
|
+
y = 2.98 + i * .54
|
|
1336
|
+
self.text(slide, item["label"], 8.0, y, 1.0, .22, 9.5, "muted", True)
|
|
1337
|
+
self.text(slide, item["value"], 9.05, y - .03, 3.55, .42, 11.5, "text")
|
|
1338
|
+
if i < len(items) - 1:
|
|
1339
|
+
self.box(slide, 8.0, y + .46, 4.65, .008, "E7D8C8")
|
|
1340
|
+
if page.get("source"):
|
|
1341
|
+
self.text(slide, page["source"], 8.0, 6.25, 4.65, .55, 9.5, "muted")
|
|
1342
|
+
elif layout == "prep_stove":
|
|
1343
|
+
items = page["items"]
|
|
1344
|
+
total = float(page.get("total_minutes", 40))
|
|
1345
|
+
x0, x1 = 2.85, 12.55
|
|
1346
|
+
span = x1 - x0
|
|
1347
|
+
for tick in (0, 10, 20, 30, 40):
|
|
1348
|
+
x = x0 + span * tick / total
|
|
1349
|
+
self.box(slide, x, 2.16, .008, 3.45, "E6D5C5")
|
|
1350
|
+
self.text(slide, str(tick), x - .14, 1.92, .28, .18, 8, "muted",
|
|
1351
|
+
align=PP_ALIGN.CENTER)
|
|
1352
|
+
self.text(slide, "分钟", x1 + .02, 1.92, .4, .18, 8, "muted")
|
|
1353
|
+
for lane, y, label, hint, color in (
|
|
1354
|
+
("prep", 2.42, "提前备料", "可提前做", "B98A63"),
|
|
1355
|
+
("stove", 4.05, "灶前", "连续操作", "B04A2A")):
|
|
1356
|
+
entries = [item for item in items if item.get("lane") == lane]
|
|
1357
|
+
self.text(slide, label, .6, y - .04, 2.1, .30, 14, "text", True)
|
|
1358
|
+
self.text(slide, hint, .6, y + .28, 2.1, .20, 9.5, "muted")
|
|
1359
|
+
offset = 16.0 if lane == "stove" else 0.0
|
|
1360
|
+
cursor = 0.0
|
|
1361
|
+
for item in entries:
|
|
1362
|
+
minutes = float(item["minutes"])
|
|
1363
|
+
bx = x0 + span * (offset + cursor) / total
|
|
1364
|
+
bw = max(.55, span * minutes / total - .05)
|
|
1365
|
+
self.box(slide, bx + .025, y, bw, .92, color, rounded=True)
|
|
1366
|
+
title_size = 11 if bw > 1.15 else (9.5 if bw > .8 else 9)
|
|
1367
|
+
self.text(slide, item["title"], bx + .10, y + .08, bw - .16, .26,
|
|
1368
|
+
title_size, "FFFFFF", True)
|
|
1369
|
+
self.text(slide, f"{minutes:g} 分钟", bx + .10, y + .32, bw - .16,
|
|
1370
|
+
.20, 8.5, "FFF6EC")
|
|
1371
|
+
self.text(slide, item["body"], bx + .10, y + .52, bw - .16, .36,
|
|
1372
|
+
8.2, "FFF6EC")
|
|
1373
|
+
cursor += minutes
|
|
1374
|
+
xh = x0 + span * 16 / total
|
|
1375
|
+
self.box(slide, xh, 2.16, .012, 3.45, "B04A2A")
|
|
1376
|
+
self.text(slide, "开火", xh - .18, 1.92, .5, .18, 8.5, "accent", True,
|
|
1377
|
+
align=PP_ALIGN.CENTER)
|
|
1378
|
+
self.text(slide, "16", .6, 5.62, .75, .55, 28, "accent", True)
|
|
1379
|
+
self.text(slide, "分钟 · 备料", 1.35, 5.76, 1.8, .22, 10, "muted")
|
|
1380
|
+
self.text(slide, "24", 3.4, 5.62, .75, .55, 28, "accent", True)
|
|
1381
|
+
self.text(slide, "分钟 · 灶前", 4.15, 5.76, 1.8, .22, 10, "muted")
|
|
1382
|
+
self.text(slide, "40", 6.2, 5.62, .75, .55, 28, "accent", True)
|
|
1383
|
+
self.text(slide, "分钟 · 上桌", 6.95, 5.76, 1.8, .22, 10, "muted")
|
|
1384
|
+
elif layout == "image_triptych":
|
|
1385
|
+
items = page["items"]
|
|
1386
|
+
top = 2.06
|
|
1387
|
+
self.artwork(slide, items[0]["image"], .6, top, 7.55, 4.38, radius=0.0)
|
|
1388
|
+
self.artwork(slide, items[1]["image"], 8.40, top, 4.30, 1.98, radius=0.0)
|
|
1389
|
+
self.artwork(slide, items[2]["image"], 8.40, 4.60, 4.30, 1.84, radius=0.0)
|
|
1390
|
+
self.box(slide, 8.24, top, .012, 4.38, "D8C6B4")
|
|
1391
|
+
self.text(slide, f"01 {items[0]['title']}", .6, 6.46, 7.55, .31, 17, "accent", True)
|
|
1392
|
+
self.text(slide, items[0]["body"], .6, 6.80, 7.55, .22, 12, "muted")
|
|
1393
|
+
self.text(slide, f"02 {items[1]['title']}", 8.40, 4.08, 4.30, .26, 14, "accent", True)
|
|
1394
|
+
self.text(slide, items[1]["body"], 8.40, 4.32, 4.30, .20, 11, "muted")
|
|
1395
|
+
self.text(slide, f"03 {items[2]['title']}", 8.40, 6.48, 4.30, .26, 14, "accent", True)
|
|
1396
|
+
self.text(slide, items[2]["body"], 8.40, 6.74, 4.30, .20, 11, "muted")
|
|
1192
1397
|
elif layout in {"chart", "chart_dashboard"}:
|
|
1193
1398
|
top = 2.05 if page.get("subtitle") else 1.65
|
|
1194
1399
|
self.chart_page(slide, page, top=top, height=6.6 - top)
|
|
@@ -1216,7 +1421,8 @@ class DeckBuilder:
|
|
|
1216
1421
|
for run in paragraph.runs:
|
|
1217
1422
|
run._r.get_or_add_rPr().append(run._r.makeelement(qn("a:ea"), {"typeface": self.font}))
|
|
1218
1423
|
if page.get("source") and layout not in {"glass_overlay", "kpi_showcase",
|
|
1219
|
-
"image_annotated"
|
|
1424
|
+
"image_annotated", "ingredient_board",
|
|
1425
|
+
"ingredient_split"}:
|
|
1220
1426
|
self.text(slide, page["source"], .6, 6.7, 12.1, .27, 11, "muted")
|
|
1221
1427
|
|
|
1222
1428
|
def chart_page(self, slide, page, top=1.5, height=5.15):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yujp_vc/ppt-assistant",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "PPT 助手 agent preset for DeepSeek Harness: a visual-director workflow with six style recipes, plan-first editable decks, GPT imagery/vision QC, showcase layouts, and a verified scoped-edit ledger.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|