---
name: visual-qa
description: "Automated visual QA of rendered documents (slides, PDFs, diagrams, screenshots). Detect overlap, overflow, contrast issues by sending images to a vision-capable model. Trigger on 'QA', 'review slides', 'check presentation', 'visual inspection', or when converting documents to images for review."
---

# Visual QA Skill

Automated visual quality assurance of rendered documents — presentations, PDFs, diagrams, screenshots. Sends images to a vision-capable LLM and gets structured feedback on layout issues.

## When to Use

- After generating or editing a .pptx deck (pair with the `powerpoint` skill)
- After rendering PDFs, diagrams, or HTML artifacts to images
- Before delivering visual content to a user
- Any time you need "fresh eyes" on rendered output

## Workflow

1. **Render to images** (if not already done):
   ```bash
   # PPTX → PDF → JPEGs
   soffice --headless --convert-to pdf input.pptx
   pdftoppm -jpeg -r 150 input.pdf slide
   # Produces slide-01.jpg, slide-02.jpg, etc.
   ```

2. **Verify a vision-capable model exists** on your endpoint (see [references/model-capabilities.md](references/model-capabilities.md)):
   ```bash
   curl -s <base_url>/models -H "Authorization: Bearer <key>" | python3 -m json.tool | grep -i model
   ```
   Not all models accept images — test with one image first.

3. **Run batch QA** — use the batch script for efficiency:
   ```bash
   NORIS_API_KEY=<key> python scripts/batch_vision_qa.py /path/to/slides/ <num_slides>
   ```

4. **Fix issues found**, re-render affected slides, re-verify.

## Key Techniques

### Direct API Calls for Batch Processing

For QA of many images, calling the vision model API directly is far more efficient than individual `vision_analyze` tool calls. A 17-slide deck processes in ~22 seconds via batch API vs. 17 sequential tool calls.

Pattern (Python with `urllib.request`):

```python
import urllib.request, json, base64

with open(slide_path, "rb") as f:
    img_b64 = base64.b64encode(f.read()).decode()

payload = {
    "model": "<vision_model>",
    "messages": [{"role": "user", "content": [
        {"type": "text", "text": "<QA prompt>"},
        {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}
    ]}],
    "max_tokens": 300
}

req = urllib.request.Request(
    "<base_url>/chat/completions",
    data=json.dumps(payload).encode(),
    headers={"Authorization": "Bearer <key>", "Content-Type": "application/json"}
)
resp = urllib.request.urlopen(req, timeout=60)
result = json.loads(resp.read())["choices"][0]["message"]["content"]
```

### Structured QA Prompt

Use a prompt that forces structured JSON output for easy parsing. The base prompt checks overlap, overflow, and contrast:

```
Analyze this slide for: 1) TEXT OVERLAP, 2) TEXT OVERFLOW, 3) CONTRAST issues.
Respond as JSON: {"slide": N, "overlap": "none|<desc>", "overflow": "none|<desc>", "contrast": "ok|<desc>", "overall": "clean|issues found"}
```

For comprehensive QA, expand the prompt to also check symbols and content consistency:

```
Analyze this slide for: 1) TEXT OVERLAP, 2) TEXT OVERFLOW, 3) CONTRAST, 4) LAYOUT (misalignment, spacing, margins),
5) CONTENT (unclear messaging, placeholder text, spelling, logical inconsistencies), 6) SYMBOLS (broken/missing
Unicode characters showing as boxes or empty circles).
Respond as JSON: {"slide": N, "overlap": "...", "overflow": "...", "contrast": "...", "layout": "...",
"content": "...", "symbols": "...", "overall": "clean|issues found"}
```

### Two-Tier QA Strategy

A single batch pass with the base prompt catches obvious layout issues but **misses rendering defects and content problems** that require closer inspection. Use a two-tier approach:

1. **Tier 1 — Batch scan** (breadth): Send all slides through the batch script with the expanded prompt above. Identifies which slides have potential issues.

2. **Tier 2 — Targeted deep-dive** (depth): For slides flagged in Tier 1 (or slides with complex elements like diagrams, badges, special characters), use individual `vision_analyze` calls with **element-specific questions**:
   - "Do the Unicode symbols ∅, ≡, ⚙ inside the circles render correctly or show as boxes?"
   - "Are the connecting lines between neurons visible? What color are they?"
   - "Does the example value '42' match the description 'Temperatur = 23°C'?"
   - "Is there adequate padding between the text 'API' and the circle edge?"

Targeted prompts catch what generic prompts miss — the vision model focuses on the specific element rather than scanning the whole slide.

### Model Capability Detection

Before batch QA, verify the target model accepts image input. Some models return `400: At most 0 image(s)` or claim "I cannot see images" even with proper `image_url` format. Test with a single image first.

See [references/model-capabilities.md](references/model-capabilities.md) for tested models.

## Config: vision_analyze Tool

The `vision_analyze` tool reads model config from TWO places in `~/.hermes/config.yaml`:

- `auxiliary.vision.*` — provider, model, base_url, api_key
- Top-level `vision.*` — model, base_url, api_key (overrides auxiliary)

Both must have the correct model name (including any `release/` prefix) and non-empty base_url + api_key. Use `hermes config set` to update:

```bash
hermes config set auxiliary.vision.model "vllm/release/<model>"
hermes config set auxiliary.vision.base_url "https://<endpoint>/v1"
hermes config set auxiliary.vision.api_key "<key>"
hermes config set vision.model "vllm/release/<model>"
hermes config set vision.base_url "https://<endpoint>/v1"
hermes config set vision.api_key "<key>"
```

Changes require a session restart to take effect in the `vision_analyze` tool. For immediate use without restart, call the API directly (see above).

## Corporate Template Adoption QA

When a user provides a `.pptx` template and asks to rebuild a deck to match its design, the QA phase must verify **design conformance** in addition to layout correctness:

1. **Template extraction QA** — render template slides to JPEGs and inspect with `vision_analyze` to understand: background color, card style (dark/light, shadows, corners), accent bar style, logo placement, typography, decorative elements.
2. **Theme extraction** — parse `ppt/theme/theme1.xml` via `zipfile` + `xml.etree.ElementTree` (not python-pptx's theme API, which can raise `AttributeError`). Extract `<a:srgbClr>` values and `<a:latin typeface="...">` font names.
3. **Embedded image inspection** — extract images from `ppt/media/` via `zipfile`, inspect each with `vision_analyze`. Logos may be white-on-transparent (invisible on white backgrounds). If unusable, build a text+shape logo instead.
4. **Post-rebuild QA prompt** — add design-conformance checks to the standard QA prompt:
   ```
   Check this slide for: 1) noris corporate design (white bg, light grey cards #F2F2F2, 
   petrol #234D5B accents, Open Sans font) 2) logo visible top right 3) no text overlaps 
   4) no layout issues 5) readable text. Reply CLEAN or list ISSUES.
   ```
5. **Non-ASCII quote pitfall in source code** — German curly quotes (`„…"` / `»…«`) cause `SyntaxError: Unexpected identifier` in Node.js string literals. Use `\u201E` (left double) and `\u201C` (right double) Unicode escapes instead. The `write_file` linter catches this but only if the quotes land inside a JS string.
6. **Logo extraction — use the REAL logo, not a text approximation** — When a user provides a `.pptx` template and asks to use its design, they expect the actual corporate logo image, not a reconstructed text+shape approximation. Identify the logo by counting image references across slide `_rels/*.xml.rels` files (logo appears on 20+ slides). **Pitfall:** the most-referenced image may be an EMF (unusable in LibreOffice/pptxgenjs) — check for PNG/JPG alternatives. Multi-part logos (two color variants in one image) require pixel-alpha scanning to find the split point and selective inversion for dark-background variants. See [references/template-adoption.md](references/template-adoption.md) §6 for the full workflow.

See [references/template-adoption.md](references/template-adoption.md) for the full extraction workflow with code snippets, including §10 (inserting new slides into an existing deck — module organization, renumbering, agenda/summary updates).

## Pitfalls

- **Model name prefixes matter**: `vllm/gemma-4-31b-it` ≠ `vllm/release/gemma-4-31b-it`. Wrong prefix → "no keys found that support model" error.
- **Empty base_url/api_key in auxiliary.vision**: Even if top-level `vision.*` is correct, empty auxiliary fields can cause failures. Set both sections.
- **Not all LLMs are multimodal**: A model serving text completions may reject image inputs. Always test one image first.
- **vision_analyze caches config**: After `hermes config set`, the running session may still use old values. Direct API calls bypass this.
- **Rate limiting**: Add 0.5s sleep between batch calls to avoid throttling.
- **Unicode symbols silently fail in LibreOffice PDF rendering**: Characters like ∅, ≡, ⚙, ★, ✓, Σ, Ω, → may look fine in PowerPoint but render as blank circles or broken boxes when exported to PDF via `soffice --headless`. Since visual QA relies on PDF→JPEG conversion, these defects are visible in QA images but the **generic batch prompt may not flag them** — it sees "a circle with something in it" and passes it. Always use targeted Tier 2 prompts that explicitly ask "do the symbols inside the circles render correctly?" for slides with badges or icon placeholders.
- **Content consistency requires targeted prompts**: A batch prompt won't catch logical inconsistencies like "example shows 42 but description says 23°C" unless the prompt explicitly asks about content consistency. Add content checks to the expanded prompt and follow up with targeted questions.
- **PowerPoint rejects negative shape dimensions/positions — LibreOffice doesn't**: pptxgenjs silently emits negative `cx`/`cy` (extent) or `x`/`y` (offset) values in the OOXML when LINE shapes connect nodes bottom-to-top or right-to-left, or when decorative OVALs are placed partially off-slide (`x: -2, y: -2`). LibreOffice opens these files fine and the PDF→JPEG render looks correct, so visual QA passes — but PowerPoint refuses to open the file. After generating a PPTX, always run `scripts/validate_pptx_compat.py <file.pptx>` to catch negative extents/offsets before delivery. Fix in source: use `Math.abs()` for dimensions, `Math.min()` for positions, and clamp decorative shapes to non-negative coordinates. See [references/template-adoption.md](references/template-adoption.md) §9.

## Narrative Structure Review

After creating or substantially expanding a deck, review the **narrative flow** ("roter Faden") before delivery. A deck can be visually clean but structurally confused.

### Consolidation Rules

- **Max 8 agenda points** — beyond that, the audience loses the thread. Group related slides under one agenda item.
- **Max 5 takeaways** — distill into the fewest memorable points. Merge related ones (e.g., "ML learns from data" + "parameters store knowledge" → one takeaway).
- **One concept per slide** — if a slide teaches two things, split it or merge into a progression.

### Flow Audit Steps

1. **List all slides** with titles and the concept they teach.
2. **Identify redundancy**: slides repeating the same idea at different depths (e.g., "KI ist nicht intelligent" as both slide 3 and slide 17).
3. **Identify interruptions**: deep-dives that break the narrative arc (e.g., tensor dimensionality between neural networks and LLMs).
4. **Check proximity of linked concepts**: Attention-Mechanismus and KV-Cache are technically coupled — 7 slides apart breaks the connection. Keep linked concepts adjacent.
5. **Propose merges**: combine slides teaching the same concept (e.g., MoE architecture + Dense-vs-MoE comparison → one slide).
6. **Propose cuts**: slides that don't advance the narrative.

### Output Format

Present as a table mapping new agenda items to current slides with actions (Merge/Consolidate/Keep/Cut), then list ≤5 proposed takeaways showing which current points they unify.

### Pitfall: Expanding Without Consolidating

Adding content slides to an existing deck is common. Each addition shifts slide numbers, grows the agenda, and lengthens the summary. After expansion, always run a flow audit and propose consolidation — a 23-slide deck with 14 agenda points and 12 takeaways is harder to follow than 18 slides with 8 and 5.

## Files

| File | Purpose |
|------|---------|
| `scripts/batch_vision_qa.py` | Batch QA script — sends all slides to vision model, reports issues |
| `scripts/validate_pptx_compat.py` | PPTX structural validation — catches negative dimensions/positions that LibreOffice tolerates but PowerPoint rejects |
| `references/model-capabilities.md` | Tested model vision capabilities (vLLM/Noris) |
| `references/iterative-fix-verify-loop.md` | Multi-cycle QA pattern: fix → re-render → re-QA until zero defects |
| `references/template-adoption.md` | Corporate template extraction: theme colors, fonts, **logo identification & multi-part processing** (§6), design-system rebuild |

## Related Skills

- `powerpoint` — PPTX creation/editing; this skill handles the visual QA step of its workflow
