@ztffn/presentation-generator-plugin 1.6.1 → 1.6.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/.claude-plugin/plugin.json +1 -1
- package/agents/presentation-style.md +2 -2
- package/package.json +1 -1
- package/scripts/validate_content_brief.py +203 -0
- package/skills/presentation-generator/SKILL.md +8 -2
- package/skills/slide-content/SKILL.md +2 -0
- package/skills/slide-recipes/SKILL.md +17 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "presentation-generator",
|
|
3
3
|
"description": "Generate complete graph-based presentations from natural language briefs and project documents. Pipeline: content extraction, narrative design, deterministic graph generation, and visual styling.",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Huma"
|
|
7
7
|
},
|
|
@@ -30,7 +30,7 @@ These define the visual intent mapping, slide recipes, allowed data fields, and
|
|
|
30
30
|
|
|
31
31
|
### Step 3 — Classify each slide
|
|
32
32
|
|
|
33
|
-
Walk through every node and assign a visual intent. Use the outline's `**
|
|
33
|
+
Walk through every node and assign a visual intent. Use the outline's `**Visual intent:**` annotation if present. Otherwise infer:
|
|
34
34
|
|
|
35
35
|
| Position / Content Signal | Visual Intent | Treatment |
|
|
36
36
|
|---|---|---|
|
|
@@ -40,7 +40,7 @@ Walk through every node and assign a visual intent. Use the outline's `**Slide t
|
|
|
40
40
|
| Spine node with a single bold claim | `impact` | Centered, no bullets, whitespace-forward |
|
|
41
41
|
| Spine or drill-down with 4+ bullet points | `workhorse` | Left-aligned, `centered: false`, single or two-column |
|
|
42
42
|
| Slide with comparison data (before/after, us/them, old/new) | `evidence` | Two-column layout, split on `---` |
|
|
43
|
-
| Slide with numeric
|
|
43
|
+
| Slide with 2+ numeric values of the same type, or named items scored across options | `evidence` | **Synthesize a chart** — do not leave this as bullets. Add `charts` dict, embed `[chart:name]` in content, remove raw data from bullet form. Use chart-trigger signals in your slide-recipes skill to select chart type. |
|
|
44
44
|
| Slide after 2+ consecutive bullet-heavy slides | `breathing-room` | Centered, background image, minimal text |
|
|
45
45
|
| Drill-down with detailed specs or numbers | `workhorse` or `evidence` | Depends on whether data is comparative or sequential |
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
validate_content_brief.py — Validates the content brief JSON produced by Phase 2.
|
|
4
|
+
Checks required fields, detects placeholder/weak values, and warns on thin content.
|
|
5
|
+
Exit 0 if brief is usable; exit 1 if critically malformed.
|
|
6
|
+
Usage: python3 validate_content_brief.py <path-to-content-brief.json>
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
VALID_GOALS = {"pitch", "demo", "update", "internal", "exploratory"}
|
|
15
|
+
|
|
16
|
+
# Sentinel strings the content agent writes when data is absent in source material.
|
|
17
|
+
NOT_FOUND_RE = re.compile(r"NOT FOUND IN SOURCE", re.IGNORECASE)
|
|
18
|
+
|
|
19
|
+
# Generic/weak phrases that should not appear as values — adapted from content-signals SKILL.md.
|
|
20
|
+
WEAK_VALUE_RE = re.compile(
|
|
21
|
+
r"\b(significant|various|many|some|multiple|considerable|substantial|good|better|great|saves time|easy to use|improves efficiency)\b",
|
|
22
|
+
re.IGNORECASE,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# A specific number means the string contains at least one digit sequence.
|
|
26
|
+
HAS_NUMBER_RE = re.compile(r"\d")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_not_found(value):
|
|
30
|
+
if isinstance(value, str):
|
|
31
|
+
return bool(NOT_FOUND_RE.search(value))
|
|
32
|
+
if isinstance(value, list):
|
|
33
|
+
return all(isinstance(v, str) and NOT_FOUND_RE.search(v) for v in value) and len(value) > 0
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_string(field, value, errors, warnings):
|
|
38
|
+
"""Validate a required top-level string field."""
|
|
39
|
+
if not isinstance(value, str) or not value.strip():
|
|
40
|
+
errors.append(f"{field}: missing or empty")
|
|
41
|
+
return
|
|
42
|
+
if is_not_found(value):
|
|
43
|
+
warnings.append(f"{field}: contains 'NOT FOUND IN SOURCE' placeholder — brief is incomplete")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def validate(path):
|
|
47
|
+
errors = []
|
|
48
|
+
warnings = []
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
raw = Path(path).read_text()
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
print(f"ERROR File not found: {path}")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
brief = json.loads(raw)
|
|
58
|
+
except json.JSONDecodeError as e:
|
|
59
|
+
print(f"ERROR Invalid JSON: {e}")
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
if not isinstance(brief, dict):
|
|
63
|
+
print("ERROR Content brief must be a JSON object")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
# --- product ---
|
|
67
|
+
product = brief.get("product")
|
|
68
|
+
if not isinstance(product, dict):
|
|
69
|
+
errors.append("product: missing or not an object")
|
|
70
|
+
else:
|
|
71
|
+
if not str(product.get("name", "")).strip():
|
|
72
|
+
errors.append("product.name: missing or empty")
|
|
73
|
+
if not str(product.get("description", "")).strip():
|
|
74
|
+
errors.append("product.description: missing or empty")
|
|
75
|
+
|
|
76
|
+
# --- audience ---
|
|
77
|
+
audience = brief.get("audience")
|
|
78
|
+
if not isinstance(audience, dict):
|
|
79
|
+
errors.append("audience: missing or not an object")
|
|
80
|
+
else:
|
|
81
|
+
for sub in ("who", "background", "cares_about"):
|
|
82
|
+
val = audience.get(sub, "")
|
|
83
|
+
if not isinstance(val, str) or not val.strip():
|
|
84
|
+
errors.append(f"audience.{sub}: missing or empty")
|
|
85
|
+
elif is_not_found(val):
|
|
86
|
+
warnings.append(f"audience.{sub}: 'NOT FOUND IN SOURCE' placeholder — narrative will lack audience specificity")
|
|
87
|
+
|
|
88
|
+
# --- goal ---
|
|
89
|
+
goal = brief.get("goal", "")
|
|
90
|
+
if not isinstance(goal, str) or not goal.strip():
|
|
91
|
+
errors.append("goal: missing or empty — must be one of: pitch, demo, update, internal, exploratory")
|
|
92
|
+
elif goal not in VALID_GOALS:
|
|
93
|
+
if is_not_found(goal):
|
|
94
|
+
warnings.append("goal: 'NOT FOUND IN SOURCE' placeholder — framework selection will be generic")
|
|
95
|
+
else:
|
|
96
|
+
errors.append(f"goal: '{goal}' is not a valid value — must be one of: {', '.join(sorted(VALID_GOALS))}")
|
|
97
|
+
|
|
98
|
+
# --- keyMessages ---
|
|
99
|
+
key_messages = brief.get("keyMessages")
|
|
100
|
+
if not isinstance(key_messages, list) or len(key_messages) == 0:
|
|
101
|
+
errors.append("keyMessages: missing or empty array — at least one declarative sentence required")
|
|
102
|
+
else:
|
|
103
|
+
real = [m for m in key_messages if isinstance(m, str) and not NOT_FOUND_RE.search(m) and m.strip()]
|
|
104
|
+
if len(real) == 0:
|
|
105
|
+
errors.append("keyMessages: all entries are placeholders — no real key messages extracted")
|
|
106
|
+
elif len(real) < 3:
|
|
107
|
+
warnings.append(f"keyMessages: only {len(real)} non-placeholder entry(ies) — schema requires 3–5 declarative sentences")
|
|
108
|
+
elif len(real) > 5:
|
|
109
|
+
warnings.append(f"keyMessages: {len(real)} entries — schema recommends 3–5")
|
|
110
|
+
for i, msg in enumerate(real):
|
|
111
|
+
if WEAK_VALUE_RE.search(msg):
|
|
112
|
+
warnings.append(f"keyMessages[{i}]: weak/generic language detected — '{msg[:80]}...' — make it specific")
|
|
113
|
+
|
|
114
|
+
# --- valueProps ---
|
|
115
|
+
value_props = brief.get("valueProps")
|
|
116
|
+
if not isinstance(value_props, list) or len(value_props) == 0:
|
|
117
|
+
warnings.append("valueProps: missing or empty — narrative may lack audience-specific benefit framing")
|
|
118
|
+
elif is_not_found(value_props):
|
|
119
|
+
warnings.append("valueProps: all entries are placeholders — will produce generic benefit statements")
|
|
120
|
+
|
|
121
|
+
# --- dataPoints ---
|
|
122
|
+
data_points = brief.get("dataPoints")
|
|
123
|
+
if not isinstance(data_points, list) or len(data_points) == 0:
|
|
124
|
+
errors.append("dataPoints: missing or empty — at least one specific metric is required")
|
|
125
|
+
else:
|
|
126
|
+
has_specific_number = False
|
|
127
|
+
for i, dp in enumerate(data_points):
|
|
128
|
+
if not isinstance(dp, dict):
|
|
129
|
+
errors.append(f"dataPoints[{i}]: must be an object with label, value, source")
|
|
130
|
+
continue
|
|
131
|
+
val = str(dp.get("value", ""))
|
|
132
|
+
if HAS_NUMBER_RE.search(val) and not NOT_FOUND_RE.search(val):
|
|
133
|
+
has_specific_number = True
|
|
134
|
+
else:
|
|
135
|
+
warnings.append(
|
|
136
|
+
f"dataPoints[{i}] ('{dp.get('label', '?')}'): value '{val}' has no specific number — "
|
|
137
|
+
f"weak metric (schema requires a concrete figure)"
|
|
138
|
+
)
|
|
139
|
+
if not dp.get("source", "").strip():
|
|
140
|
+
warnings.append(f"dataPoints[{i}] ('{dp.get('label', '?')}'): missing source attribution")
|
|
141
|
+
if not has_specific_number:
|
|
142
|
+
errors.append("dataPoints: no entry has a specific numeric value — brief cannot support evidence slides")
|
|
143
|
+
|
|
144
|
+
# --- proofPoints ---
|
|
145
|
+
proof_points = brief.get("proofPoints")
|
|
146
|
+
if not isinstance(proof_points, list) or len(proof_points) == 0:
|
|
147
|
+
warnings.append("proofPoints: missing or empty — narrative will lack named evidence")
|
|
148
|
+
elif is_not_found(proof_points):
|
|
149
|
+
warnings.append("proofPoints: all entries are placeholders")
|
|
150
|
+
|
|
151
|
+
# --- objections ---
|
|
152
|
+
objections = brief.get("objections")
|
|
153
|
+
if not isinstance(objections, list) or len(objections) == 0:
|
|
154
|
+
warnings.append("objections: missing or empty — narrative cannot preempt audience concerns")
|
|
155
|
+
elif is_not_found(objections):
|
|
156
|
+
warnings.append("objections: all entries are placeholders")
|
|
157
|
+
|
|
158
|
+
# --- callToAction ---
|
|
159
|
+
cta = brief.get("callToAction", "")
|
|
160
|
+
check_string("callToAction", cta, errors, warnings)
|
|
161
|
+
if isinstance(cta, str) and cta.strip() and not is_not_found(cta):
|
|
162
|
+
# Heuristic: a real CTA contains an action verb and a concrete outcome
|
|
163
|
+
weak_cta_patterns = re.compile(
|
|
164
|
+
r"^(let'?s|we should|continue|follow up|stay in touch|keep talking)",
|
|
165
|
+
re.IGNORECASE,
|
|
166
|
+
)
|
|
167
|
+
if weak_cta_patterns.match(cta.strip()):
|
|
168
|
+
warnings.append(f"callToAction: '{cta}' — too vague; should name a specific action and outcome")
|
|
169
|
+
|
|
170
|
+
# --- context ---
|
|
171
|
+
context = brief.get("context", "")
|
|
172
|
+
if not isinstance(context, str) or not context.strip():
|
|
173
|
+
warnings.append("context: missing or empty — narrative agent has no competitive or timing signals")
|
|
174
|
+
elif is_not_found(context):
|
|
175
|
+
warnings.append("context: 'NOT FOUND IN SOURCE' placeholder")
|
|
176
|
+
|
|
177
|
+
# --- Report ---
|
|
178
|
+
if warnings:
|
|
179
|
+
print(f"WARN {len(warnings)} warning(s):")
|
|
180
|
+
for w in warnings:
|
|
181
|
+
print(f" WARN {w}")
|
|
182
|
+
print()
|
|
183
|
+
|
|
184
|
+
if errors:
|
|
185
|
+
for e in errors:
|
|
186
|
+
print(f"ERROR {e}")
|
|
187
|
+
print()
|
|
188
|
+
print(f"FAILED {len(errors)} critical error(s) — content brief is not usable; re-run Phase 2")
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
if not warnings:
|
|
192
|
+
print("OK Content brief is valid")
|
|
193
|
+
else:
|
|
194
|
+
print(f"OK (with warnings) Content brief is usable — {len(warnings)} weak field(s) noted above")
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
if len(sys.argv) != 2:
|
|
200
|
+
print("Usage: python3 validate_content_brief.py <path-to-content-brief.json>")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
ok = validate(sys.argv[1])
|
|
203
|
+
sys.exit(0 if ok else 1)
|
|
@@ -69,9 +69,15 @@ Write a JSON content brief to _temp/presentation-content-brief.json
|
|
|
69
69
|
Report back what you extracted.
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
When the agent finishes,
|
|
72
|
+
When the agent finishes (or after you write the brief yourself), validate the brief before continuing:
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
```bash
|
|
75
|
+
python3 "{PLUGIN_ROOT}/scripts/validate_content_brief.py" _temp/presentation-content-brief.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If exit code is non-zero, stop and show the errors to the user — do not proceed to Phase 3.
|
|
79
|
+
|
|
80
|
+
If no documents were provided, write _temp/presentation-content-brief.json yourself from the user's prompt, then run the validation above.
|
|
75
81
|
|
|
76
82
|
---
|
|
77
83
|
|
|
@@ -261,4 +261,6 @@ Example annotation in an outline (use `### SPINE N:` header format — the parse
|
|
|
261
261
|
|
|
262
262
|
5. **Breathing room after sustained density.** If the outline has a run of `workhorse` and `evidence` slides (detailed argument, data, comparison), follow it with a `breathing-room` or `impact` slide before continuing. The audience needs a reset.
|
|
263
263
|
|
|
264
|
+
6. **Chart rhythm.** One `evidence` slide per major section is a signal. Two in a row is a pattern the audience can follow. Three or more consecutive `evidence` slides is a data dump — it exhausts attention and flattens the argument. If the content demands multiple data points, prefer one chart slide with a strong claim followed by a drill-down for the supporting data, rather than stacking chart slides on the spine. Equally, a deck with no `evidence` at any point reads as assertion without proof — aim for at least one per major argument.
|
|
265
|
+
|
|
264
266
|
These annotations describe intent only. How the design agent translates them into layout, background, and typography is not the narrative agent's concern.
|
|
@@ -112,6 +112,23 @@ Set `showBranding: false` for immersive slides (R3F, full-bleed video).
|
|
|
112
112
|
|
|
113
113
|
When a signal is present: synthesise the `charts` dict from the outline figures, embed `[chart:name]` in the content markdown, and remove the raw data from bullet form. The agent must extract values from the outline text — do not leave chart-ready data as bullets.
|
|
114
114
|
|
|
115
|
+
**Data extraction and creation rules:**
|
|
116
|
+
|
|
117
|
+
The outline rarely contains a tidy data table. More often, values are embedded in prose or bullets. The agent must parse them out and construct the data array. When exact values are missing or incomplete, synthesise plausible values consistent with the narrative — a chart that conveys the right order of magnitude and relative relationship is more useful than no chart.
|
|
118
|
+
|
|
119
|
+
| Outline text | What to do |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `- Iron oxide: 3,000 kJ/kg` / `- Lithium-ion: 576 kJ/kg` / `- Molten salt: 360 kJ/kg` | Extract label + value pairs → `[{material, energy_density}]`, `xKey: "material"`, `yKeys: ["energy_density"]`, chart type: `bar` |
|
|
122
|
+
| `Response time down 60%, cost down 40% over 6 months` | Construct time-series or before/after → `[{period: "Before", response_ms: 100, cost: 100}, {period: "After", response_ms: 40, cost: 60}]`, chart type: `bar` |
|
|
123
|
+
| `Outperforms competitors on flexibility, integration speed, and reliability` | Named items across multiple axes → `[{axis: "Flexibility", ours: 90, competitor: 35}, ...]`, chart type: `radar` |
|
|
124
|
+
| `Revenue grew 40% QoQ` — no breakdown given | Create a minimal 2-point bar: `[{quarter: "Q2", revenue: 100}, {quarter: "Q3", revenue: 140}]`. Label it clearly. |
|
|
125
|
+
|
|
126
|
+
Key principles:
|
|
127
|
+
- Use short, readable axis labels — `"Q3"` not `"Third Quarter of Fiscal Year"`
|
|
128
|
+
- `yKeys` values become legend labels — use the words the audience would say: `"ours"` / `"competitor"`, `"before"` / `"after"`, `"target"` / `"actual"`
|
|
129
|
+
- Normalise to a common base (e.g., index to 100) when absolute values would mislead
|
|
130
|
+
- When data is qualitative (e.g. "best-in-class"), skip the chart — use an impact or workhorse slide instead
|
|
131
|
+
|
|
115
132
|
Chart placement:
|
|
116
133
|
- Data IS the primary message of the slide → `type: "chart"` (full-viewport)
|
|
117
134
|
- Data supports a text argument → `[chart:name]` inline in a `two-column` layout
|