Initial commit: Hermes Agent Skills collection
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Batch visual QA of rendered document pages/slides via direct vLLM API calls.
|
||||
|
||||
Bypasses the vision_analyze tool — useful when:
|
||||
- vision_analyze fails due to config caching / model resolution issues
|
||||
- You need to QA many images efficiently in one pass
|
||||
- The active chat model is text-only but a multimodal model is on the same endpoint
|
||||
|
||||
Usage:
|
||||
python batch_vision_qa.py /path/to/images/ <num_images> [options]
|
||||
|
||||
Environment variables:
|
||||
NORIS_API_KEY - API key for the vLLM endpoint (required)
|
||||
VISION_BASE_URL - Full chat completions URL (default: https://ai.noris.de/v1/chat/completions)
|
||||
VISION_MODEL - Model name (default: vllm/release/gemma-4-31b-it)
|
||||
|
||||
Example:
|
||||
NORIS_API_KEY=sk-bf-xxx python batch_vision_qa.py /home/debian/ai101/qa/ 17
|
||||
|
||||
Expects files named slide-01.jpg, slide-02.jpg, ..., slide-NN.jpg.
|
||||
Outputs per-slide status and a summary of any issues found.
|
||||
|
||||
Before using: verify the model accepts images (see references/model-capabilities.md).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
API_KEY = os.environ.get("NORIS_API_KEY", "")
|
||||
BASE_URL = os.environ.get(
|
||||
"VISION_BASE_URL", "https://ai.noris.de/v1/chat/completions"
|
||||
)
|
||||
MODEL = os.environ.get("VISION_MODEL", "vllm/release/gemma-4-31b-it")
|
||||
|
||||
# Expanded 6-category prompt: overlap, overflow, contrast, layout, content, symbols
|
||||
QA_PROMPT_TEMPLATE = """You are a visual QA reviewer for a professional presentation slide.
|
||||
Analyze this slide carefully for ALL of these issues:
|
||||
1. TEXT OVERLAP: Any text overlapping other text or graphics?
|
||||
2. TEXT OVERFLOW: Any text extending beyond edges or getting cut off?
|
||||
3. CONTRAST: Any low contrast between text and background making text hard to read?
|
||||
4. LAYOUT: Misalignment, elements too close, insufficient margins, awkward spacing?
|
||||
5. CONTENT: Unclear messaging, missing information, placeholder text, spelling errors, logical inconsistencies?
|
||||
6. SYMBOLS: Any broken/missing Unicode characters showing as boxes or empty circles?
|
||||
|
||||
Respond in this exact JSON format:
|
||||
{{"slide": {slide_num}, "overlap": "none|<description>", "overflow": "none|<description>", "contrast": "ok|<description>", "layout": "ok|<description>", "content": "ok|<description>", "symbols": "ok|<description>", "overall": "clean|issues found"}}
|
||||
|
||||
Be precise and thorough. If no issues, say "clean". If issues exist, describe them specifically."""
|
||||
|
||||
|
||||
def qa_single_image(image_path: str, slide_num: int) -> dict:
|
||||
"""Send one image to the vision model and return parsed QA result."""
|
||||
with open(image_path, "rb") as f:
|
||||
img_b64 = base64.b64encode(f.read()).decode()
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": QA_PROMPT_TEMPLATE.format(slide_num=slide_num),
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/jpeg;base64,{img_b64}"
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
"max_tokens": 500,
|
||||
}
|
||||
|
||||
req = urllib.request.Request(
|
||||
BASE_URL,
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={
|
||||
"Authorization": f"Bearer {API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
resp = urllib.request.urlopen(req, timeout=60)
|
||||
data = json.loads(resp.read())
|
||||
content = data["choices"][0]["message"].get("content", "")
|
||||
|
||||
# Strip markdown code fences if present
|
||||
content = content.strip()
|
||||
if content.startswith("```"):
|
||||
content = content.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
|
||||
|
||||
try:
|
||||
return json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
"slide": slide_num,
|
||||
"overlap": "parse error",
|
||||
"overflow": "parse error",
|
||||
"contrast": "parse error",
|
||||
"layout": "parse error",
|
||||
"content": "parse error",
|
||||
"symbols": "parse error",
|
||||
"overall": content[:200],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(f"Usage: {sys.argv[0]} <images_dir> <num_images>")
|
||||
print(f"Example: {sys.argv[0]} /home/debian/ai101/qa/ 17")
|
||||
sys.exit(1)
|
||||
|
||||
images_dir = sys.argv[1]
|
||||
num_slides = int(sys.argv[2])
|
||||
|
||||
if not API_KEY:
|
||||
print("ERROR: Set NORIS_API_KEY environment variable.")
|
||||
sys.exit(1)
|
||||
|
||||
results = []
|
||||
issues_found = []
|
||||
|
||||
for i in range(1, num_slides + 1):
|
||||
slide_path = os.path.join(images_dir, f"slide-{i:02d}.jpg")
|
||||
if not os.path.exists(slide_path):
|
||||
print(f"Slide {i}: file not found — {slide_path}")
|
||||
continue
|
||||
|
||||
try:
|
||||
result = qa_single_image(slide_path, i)
|
||||
results.append(result)
|
||||
status = result.get("overall", "?")
|
||||
marker = "⚠️" if "issue" in status.lower() else "✅"
|
||||
print(f"Slide {i}/{num_slides} {marker} {status}")
|
||||
if "issue" in status.lower():
|
||||
issues_found.append(result)
|
||||
except Exception as e:
|
||||
print(f"Slide {i} FAILED: {e}")
|
||||
results.append({"slide": i, "error": str(e)})
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
if issues_found:
|
||||
print(f"ISSUES ON {len(issues_found)} SLIDE(S):")
|
||||
for r in issues_found:
|
||||
print(f" Slide {r['slide']}:")
|
||||
for key in ["overlap", "overflow", "contrast", "layout", "content", "symbols"]:
|
||||
val = r.get(key, "")
|
||||
if val and val not in ("none", "ok", ""):
|
||||
print(f" {key}: {val}")
|
||||
else:
|
||||
print("ALL SLIDES CLEAN ✅")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user