@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.
Files changed (2) hide show
  1. package/build_deck.py +225 -19
  2. 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 == "image_triptych":
1223
+ elif layout == "ingredient_split":
1172
1224
  items = page["items"]
1173
- top = 2.06
1174
- scrim = {"direction": "bottom", "color": "1A0E08",
1175
- "start": .42, "end": .98, "opacity": .88}
1176
- self.artwork(slide, items[0]["image"], .6, top, 7.55, 4.38,
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.text(slide, f"01 {items[0]['title']}", .95, 5.58, 6.85, .34,
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.text(slide, f"02 {items[1]['title']}", 8.62, 3.14, 3.88, .28,
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, f"03 {items[2]['title']}", 8.62, 5.58, 3.88, .28,
1190
- 13, "FFFFFF", True)
1191
- self.text(slide, items[2]["body"], 8.62, 5.88, 3.88, .20, 10.5, "FFFFFF")
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.1",
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,