Initial commit: Hermes Agent Skills collection

This commit is contained in:
Debian
2026-07-12 19:02:59 +00:00
commit e9cc106625
789 changed files with 233126 additions and 0 deletions
@@ -0,0 +1,445 @@
# Corporate Template Adoption
Workflow for extracting a corporate design system from a `.pptx` template and applying it to a deck built with pptxgenjs.
## 1. Extract Theme Colors & Fonts
python-pptx's theme API (`slide_master.element.find(...)`) can raise `AttributeError`.
Parse the XML directly via `zipfile` instead:
```python
import zipfile, xml.etree.ElementTree as ET
with zipfile.ZipFile('template.pptx', 'r') as z:
theme_files = [f for f in z.namelist() if 'theme' in f and f.endswith('.xml')]
for tf in theme_files:
xml = z.read(tf)
root = ET.fromstring(xml)
ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'}
# Colors
for clr in root.findall('.//a:srgbClr', ns):
print(f"Color: #{clr.get('val')}")
# Fonts
for latin in root.findall('.//a:latin', ns):
print(f"Font: {latin.get('typeface')}")
```
## 2. Extract Embedded Images (Logos, Icons)
```python
import zipfile, os
with zipfile.ZipFile('template.pptx', 'r') as z:
images = [f for f in z.namelist()
if f.startswith('ppt/media/') and f.endswith(('.png', '.jpg', '.jpeg'))]
for img in images:
data = z.read(img)
fname = os.path.basename(img)
with open(f'/tmp/{fname}', 'wb') as f:
f.write(data)
```
Inspect each with `vision_analyze`. Logos may be white-on-transparent and
invisible on white backgrounds — if so, build a text+shape logo instead.
## 3. Render Template Slides for Visual Reference
```bash
python3 ~/.hermes/skills/productivity/powerpoint/scripts/office/soffice.py \
--headless --convert-to pdf template.pptx
mkdir -p tpl_qa
pdftoppm -jpeg -r 100 -f 1 -l 5 template.pdf tpl_qa/tplate
pdftoppm -jpeg -r 100 -f 32 -l 39 template.pdf tpl_qa/tplate
```
Use `vision_analyze` on selected template slides to understand:
- Background color (white, dark, gradient?)
- Card style (dark/light, shadows, rounded/sharp corners)
- Accent bar style (vertical/horizontal, thickness, position)
- Logo placement (usually top right)
- Typography (font family, sizes, weights)
- Decorative elements (hexagons, circles, patterns)
## 4. Define Custom Layout Dimensions
If the template uses non-standard dimensions (e.g., 13.33" × 7.50"), define
a custom layout in pptxgenjs **before adding any slides**:
```javascript
pres.defineLayout({ name: "noris_wide", width: 13.33, height: 7.5 });
pres.layout = "noris_wide"; // Must set AFTER defineLayout
```
Common corporate template dimensions:
- 13.33" × 7.50" — widescreen 16:9 (noris, many corporate templates)
- 10" × 5.625" — LAYOUT_16x9 (pptxgenjs default)
All coordinate values in helper functions (logo position, card widths, etc.)
must be recalculated for the new dimensions — a logo at `x: 8.5` on a 10"-wide
slide needs `x: 11.13` on a 13.33"-wide slide.
## 5. Build Design-System Constants File
Create a `helpers.js` with the extracted colors, fonts, and reusable functions:
```javascript
const C = {
white: "FFFFFF",
bgLight: "F2F2F2", // Card background
petrol: "234D5B", // Primary brand color
accent: "118291", // Accent teal
burgundy: "9E1946", // Red accent
text: "212427", // Body text
textMid: "5F7D86", // Secondary text
greyLt: "D3D3D3",
greyMd: "A6A6A6",
};
const FONT = "Open Sans";
```
Include helpers: `addLogo()`, `slideNumber()`, `card()`, `accentBar()`,
`shieldBadge()`, `title()`, `subtitle()`, `footerBar()`.
## 6. Logo Extraction — Find and Use the REAL Logo
**Do not fall back to a text+shape logo approximation until you have exhausted
all extraction options.** Users expect the actual corporate logo from the
template, not a reconstruction. Only build a text-based logo (section 6c below)
if no usable image exists after trying all techniques in sections 6a6b.
### 6a. Identify the Logo Among Hundreds of Media Files
Templates may contain 100+ images. Find the logo systematically:
**Step 1 — Count image references across slides:**
```python
import zipfile, xml.etree.ElementTree as ET, collections
with zipfile.ZipFile('template.pptx', 'r') as z:
rel_counts = collections.Counter()
slide_rels = sorted([f for f in z.namelist()
if f.startswith('ppt/slides/_rels/') and f.endswith('.xml.rels')])
for sf in slide_rels:
tree = ET.parse(z.open(sf))
ns = {'r': 'http://schemas.openxmlformats.org/package/2006/relationships'}
for rel in tree.findall('.//r:Relationship', ns):
target = rel.get('Target', '')
if 'media/' in target:
rel_counts[target.split('/')[-1]] += 1
for img, cnt in rel_counts.most_common(10):
print(f'{img:30s} appears on {cnt:3d} slides')
```
The logo typically appears on 20+ slides. Decorative elements appear on 1-4.
**Step 2 — Check format and usability of top candidates:**
```python
from PIL import Image
import os
for img_name in top_candidates:
path = f'/tmp/{img_name}'
if img_name.endswith('.emf'):
print(f'{img_name}: EMF format — may not render in LibreOffice/pptxgenjs')
# EMF files are often tiny (100-200 bytes) and unusable
continue
if os.path.exists(path):
im = Image.open(path)
print(f'{img_name}: {im.size} mode={im.mode}')
```
**Pitfall — EMF images:** The most-referenced image may be an EMF (Windows Enhanced
Metafile) that LibreOffice cannot convert and pptxgenjs cannot embed. Check for
PNG/JPG alternatives among the top candidates. In the noris template,
`image1.emf` appeared on 26 slides but was only 188 bytes and unusable; the
actual logo was `image2.png` (2268×336 RGBA), which was NOT the most-referenced
image but was the largest PNG with the right aspect ratio for a logo.
**Step 3 — Inspect candidate images with `vision_analyze`:**
Send each top candidate to `vision_analyze` with:
> "Is this a company logo? Describe the text, colors, and layout. Is it
> suitable for white backgrounds? Dark backgrounds?"
### 6b. Multi-Part Logo Processing
Some logos are single images containing multiple color variants side-by-side
(e.g., "noris" in white-on-dark-box next to "network." in dark-on-transparent).
To use these:
**Find the split point by scanning pixel alpha:**
```python
from PIL import Image
img = Image.open('logo.png')
w, h = img.size
px = img.load()
# Scan a row above the text (y=10) for the transition
split_x = None
found_opaque = False
for x in range(w):
r, g, b, a = px[x, 10]
if a == 255 and r < 100: # opaque dark pixel (bg of left part)
found_opaque = True
elif found_opaque and a == 0: # transition to transparent
split_x = x
break
```
**Crop and save variants:**
```python
# For white slides: use as-is (left=dark box with white text, right=dark text)
img.save('logo_white_bg.png')
# For dark slides: invert the RIGHT part only (dark text → white text)
# Keep the LEFT part as-is (white text on dark box is already visible on dark bg)
dark_ver = img.copy()
dv_px = dark_ver.load()
for x in range(w):
for y in range(h):
r, g, b, a = px[x, y]
if x >= split_x and a > 0:
dv_px[x, y] = (255-r, 255-g, 255-b, a) # invert right part
dark_ver.save('logo_dark_bg.png')
```
**Use in pptxgenjs with correct aspect ratio:**
```javascript
function addLogo(slide, w = 1.8, dark = false) {
const logoPath = dark ? "logo_dark_bg.png" : "logo_white_bg.png";
const h = w / 6.74; // original aspect ratio (width/height)
slide.addImage({
path: logoPath,
x: 13.33 - w - 0.4, y: 0.25, w, h,
});
}
```
### 6c. Text-Based Logo (last resort only)
Only when no usable logo image can be extracted after all attempts above:
```javascript
function addLogo(slide, w = 1.8) {
const h = 0.32;
const x = 13.33 - w - 0.4;
const y = 0.25;
const boxW = w * 0.35;
slide.addShape("roundRect", {
x, y, w: boxW, h,
fill: { color: C.petrol }, line: { type: "none" },
rectRadius: 0.03,
});
slide.addText("noris", {
x, y, w: boxW, h,
fontSize: 13, fontFace: FONT, bold: true,
color: C.white, align: "center", valign: "middle", margin: 0,
});
slide.addText("network.", {
x: x + boxW + 0.05, y, w: w - boxW - 0.05, h,
fontSize: 13, fontFace: FONT, bold: true,
color: C.petrol, align: "left", valign: "middle", margin: 0,
});
}
```
## 7. Non-ASCII Quote Pitfall
German curly quotes (`„…"` / `»…«`) cause `SyntaxError: Unexpected identifier`
in Node.js string literals. The parser treats `"` as a string terminator and
the following text as a bare identifier.
**Wrong:**
```javascript
{ text: "Milliarden von Parametern speichern das „Wissen" des Modells", ... }
```
**Right** — use Unicode escapes:
```javascript
{ text: "Milliarden von Parametern speichern das \u201EWissen\u201C des Modells", ... }
```
The `write_file` linter catches this on save. Always check the lint output
after writing JS files with German text.
## 8. Design-Conformance QA
After rebuilding all slides, verify each slide matches the template's design
language. Use a design-specific QA prompt:
```
Check this slide for: 1) [company] corporate design (white bg, light grey cards,
petrol #XXXXX accents, [Font] font) 2) logo visible top right 3) no text overlaps
4) no layout issues 5) readable text. Reply CLEAN or list ISSUES.
```
Run all slides through this check — a slide can be layout-clean but
design-nonconformant (wrong background, missing logo, wrong card style).
## 9. PowerPoint Compatibility Validation (Post-Generation)
**Critical:** LibreOffice and PowerPoint have different tolerance levels for
malformed OOXML. A PPTX that opens perfectly in LibreOffice (and therefore
passes PDF→JPEG visual QA) may be refused by PowerPoint entirely.
The most common cause is **negative shape dimensions or positions** that
pptxgenjs silently generates:
| Source Pattern | XML Result | Effect |
|---|---|---|
| LINE shape from node A (low y) to node B (high y) | `cy < 0` in `<a:ext>` | PowerPoint refuses to open |
| LINE shape from node A (right) to node B (left) | `cx < 0` in `<a:ext>` | PowerPoint refuses to open |
| LINE with equal source/target y | `cy = 0` in `<a:ext>` | PowerPoint refuses to open |
| OVAL at `x: -2, y: -2` (decorative, partially off-slide) | `x < 0, y < 0` in `<a:off>` | PowerPoint refuses to open |
### Fix in Source Code
**Option A — Fix coordinates (when the shape is meaningful):**
```javascript
// LINE shapes: always use Math.abs/min
const dx = dstX - srcX;
const dy = dstY - srcY;
s.addShape(pres.shapes.LINE, {
x: Math.min(srcX, dstX), y: Math.min(srcY, dstY),
w: Math.abs(dx), h: Math.max(Math.abs(dy), 1), // min 1 EMU
line: { color: "CCCCCC", width: 1 },
});
// Decorative shapes: clamp to non-negative positions
s.addShape(pres.shapes.OVAL, {
x: 0, y: 0, w: 3, h: 3, // was x: -2, y: -2
fill: { color: "F2F2F2" },
});
```
**Option B — Remove purely decorative shapes (often the better choice):**
When decorative shapes (background circles, hexagon patterns, ambient ovals)
cause negative coordinates, consider removing them entirely rather than
repositioning. They serve no informational purpose and their absence improves
compatibility without affecting the design. In the noris deck, decorative
OVALs at `x: -2, y: -2` on the title and closing slides were deleted outright
— the slides looked cleaner without them and the compatibility issue vanished.
```javascript
// Simply delete the decorative shape block — no replacement needed
// s.addShape(pres.shapes.OVAL, { x: -2, y: -2, ... }); // REMOVED
```
### Validate Before Delivery
After `node main.js`, run the validation script:
```bash
python3 ~/.hermes/skills/productivity/visual-qa/scripts/validate_pptx_compat.py \
output.pptx
```
This checks for negative extents/offsets, malformed XML, missing required parts,
broken relationship references, and missing Content_Type overrides. Exit code 0
means clean; 1 means issues found.
**Add this to your standard build pipeline:**
```bash
node main.js && \
python3 scripts/validate_pptx_compat.py AI_101.pptx && \
soffice --headless --convert-to pdf AI_101.pptx && \
pdftoppm -jpeg -r 150 AI_101.pdf qa/slide
```
The validation step takes <1 second and catches the #1 cause of "PowerPoint
can't open the file" complaints before they happen.
## 10. Inserting New Slides into an Existing Deck
When a user asks to add new content slides to an existing pptxgenjs-built deck
(e.g. "ergänze Folien zu Tokens, KV-Cache, aktiven Parametern"), follow this
workflow:
### Step 1: Create a New Module File
Put new slide functions in a separate file (e.g. `slides_extra.js`) to avoid
rewriting existing modules:
```javascript
const { C, FONT, addLogo, slideNumber, card, accentBar, /* ... */ } = require("./helpers");
function slide_tokens(pres) {
const s = pres.addSlide();
s.background = { color: C.white };
addLogo(s, 1.5);
slideNumber(s, 13); // ← set to the FINAL slide number after insertion
title(s, "Was ist ein Token?");
// ...
}
module.exports = { slide_tokens, /* other new slides */ };
```
### Step 2: Update main.js — Insert at the Right Position
```javascript
const se = require("./slides_extra");
// ...
s10.slide11_finetuning(pres);
se.slide_transition_llm(pres); // ← new slide inserted here
se.slide_tokens(pres); // ← new slide
s10.slide12_llm_no_search(pres); // ← existing slide, now shifted
// ...
```
### Step 3: Renumber ALL slideNumber() Calls
Every `slideNumber(s, N)` in EVERY module file must be updated to reflect the
new ordering. A single off-by-one makes the deck look unprofessional.
Track the mapping explicitly:
```
Old 1-11 → unchanged (1-11)
New 12 → slide_transition_llm (NEW)
New 13 → slide_tokens (NEW)
Old 12 → slide12_llm_no_search → now 14
Old 13 → slide13_not_intelligent → now 15
Old 14 → slide14_moe → now 16
New 17 → slide_active_params (NEW)
New 18 → slide_kv_cache (NEW)
Old 15 → slide15_noris → now 19
Old 16 → slide16_summary → now 20
Old 17 → slide17_thanks → now 21 (no slideNumber, it's the closing slide)
```
### Step 4: Update Agenda and Summary Slides
- **Agenda**: add new entries, adjust numbering, reduce font size / spacing
if the list grows beyond 9 items (12 items needs `fontSize: 16`, `y_step: 0.49`)
- **Summary/Takeaways**: add new key points, compress spacing similarly
### Step 5: Rebuild + Validate + QA
```bash
node main.js && \
python3 scripts/validate_pptx_compat.py output.pptx && \
soffice --headless --convert-to pdf output.pptx && \
pdftoppm -jpeg -r 150 output.pdf qa/slide
```
Then run visual QA on at least the new slides + agenda + summary.
### Pitfall: Forgetting to Renumber
The most common mistake is updating `main.js` insertion order but forgetting
to update `slideNumber(s, N)` inside existing slide functions. The slides will
appear in the right order but show wrong page numbers. Always grep for all
`slideNumber` calls after restructuring:
```bash
grep -rn "slideNumber" slides_*.js
```