mirror of
https://github.com/hanzoai/video.git
synced 2026-10-30 18:18:27 +00:00
11 production pipelines, 47 tools, 124 agent skills. Supports cloud APIs (fal.ai, OpenAI, ElevenLabs, Suno, HeyGen, Runway) and free local providers (diffusers, Piper TTS, WAN 2.1, Hunyuan, CogVideo). Co-authored-by: Hanzo Dev <[email protected]>
280 lines
11 KiB
Python
280 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""QA Test 07: Playbook design intelligence — no API calls.
|
|
|
|
Tests contrast validation, color harmony generation, color-blind safety,
|
|
type scale computation, type hierarchy validation, font pairing suggestions,
|
|
and full accessibility audit across all 3 playbooks.
|
|
"""
|
|
|
|
import sys, os, json
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
|
|
|
from styles.playbook_loader import (
|
|
load_playbook,
|
|
validate_playbook,
|
|
list_playbooks,
|
|
validate_contrast,
|
|
check_color_blind_safety,
|
|
validate_palette,
|
|
generate_harmony,
|
|
compute_type_scale,
|
|
validate_type_hierarchy,
|
|
suggest_font_pairing,
|
|
validate_accessibility,
|
|
TYPE_SCALE_RATIOS,
|
|
)
|
|
|
|
PASS = 0
|
|
FAIL = 0
|
|
|
|
def check(name, condition, detail=""):
|
|
global PASS, FAIL
|
|
status = "PASS" if condition else "FAIL"
|
|
if condition:
|
|
PASS += 1
|
|
else:
|
|
FAIL += 1
|
|
suffix = f" — {detail}" if detail else ""
|
|
print(f" [{status}] {name}{suffix}")
|
|
|
|
# ===================================================================
|
|
# Test 1: List and load all playbooks
|
|
# ===================================================================
|
|
print("--- Test 1: List and load playbooks ---")
|
|
playbooks_available = list_playbooks()
|
|
print(f" Found {len(playbooks_available)} playbooks: {playbooks_available}")
|
|
check("At least 3 playbooks exist", len(playbooks_available) >= 3)
|
|
|
|
loaded = {}
|
|
for name in ["clean-professional", "flat-motion-graphics", "minimalist-diagram"]:
|
|
try:
|
|
pb = load_playbook(name)
|
|
loaded[name] = pb
|
|
check(f"Load + validate {name}", True)
|
|
except Exception as e:
|
|
loaded[name] = None
|
|
check(f"Load + validate {name}", False, str(e))
|
|
|
|
# ===================================================================
|
|
# Test 2: Contrast validation (manual spot-checks)
|
|
# ===================================================================
|
|
print("\n--- Test 2: Contrast validation ---")
|
|
|
|
# Known pair: black on white should be 21:1
|
|
r = validate_contrast("#000000", "#FFFFFF")
|
|
check("Black on white ~21:1", abs(r["ratio"] - 21.0) < 0.1, f"ratio={r['ratio']}")
|
|
check("Black on white passes AAA", r["normal_text"]["AAA"])
|
|
|
|
# Known pair: white on white should be 1:1
|
|
r = validate_contrast("#FFFFFF", "#FFFFFF")
|
|
check("White on white = 1:1", abs(r["ratio"] - 1.0) < 0.01, f"ratio={r['ratio']}")
|
|
check("White on white fails AA", not r["normal_text"]["AA"])
|
|
|
|
# Mid-gray on white (~4.5:1 boundary)
|
|
r = validate_contrast("#767676", "#FFFFFF")
|
|
check("#767676 on white passes AA normal", r["normal_text"]["AA"], f"ratio={r['ratio']}")
|
|
|
|
# Just-below threshold
|
|
r = validate_contrast("#777777", "#FFFFFF")
|
|
check("#777777 on white borderline", r["ratio"] >= 4.4, f"ratio={r['ratio']}")
|
|
|
|
# Dark-on-dark: low contrast
|
|
r = validate_contrast("#1A1A1A", "#2B2B2B")
|
|
check("Dark-on-dark fails AA", not r["normal_text"]["AA"], f"ratio={r['ratio']}")
|
|
|
|
# Check each playbook's text-on-bg contrast
|
|
for name, pb in loaded.items():
|
|
if pb is None:
|
|
continue
|
|
palette = pb.get("visual_language", {}).get("color_palette", {})
|
|
text = palette.get("text", "#000000")
|
|
bg = palette.get("background", "#FFFFFF")
|
|
r = validate_contrast(text, bg)
|
|
check(f"{name}: text on bg passes AA", r["normal_text"]["AA"], f"ratio={r['ratio']}")
|
|
|
|
# ===================================================================
|
|
# Test 3: Color harmony generation
|
|
# ===================================================================
|
|
print("\n--- Test 3: Color harmony generation ---")
|
|
|
|
for harmony_type in ["complementary", "analogous", "triadic", "split-complementary"]:
|
|
colors = generate_harmony("#3B82F6", harmony_type)
|
|
check(f"Harmony {harmony_type}", len(colors) >= 2, f"generated {len(colors)} colors: {colors}")
|
|
# First color should match base
|
|
check(f" Base preserved in {harmony_type}", colors[0].upper() == generate_harmony("#3B82F6", harmony_type)[0].upper())
|
|
|
|
# Edge case: pure red
|
|
colors = generate_harmony("#FF0000", "triadic")
|
|
check("Triadic from pure red", len(colors) == 3, f"{colors}")
|
|
|
|
# ===================================================================
|
|
# Test 4: Color-blind safety
|
|
# ===================================================================
|
|
print("\n--- Test 4: Color-blind safety ---")
|
|
|
|
# Safe palette (blue + orange — distinguishable by all CVD types due to lightness diff)
|
|
safe = check_color_blind_safety(["#2563EB", "#F59E0B"])
|
|
print(f" Blue + orange: safe={safe['safe']}, issues={len(safe.get('issues', []))}")
|
|
|
|
# Problematic palette (red + green, similar lightness)
|
|
risky = check_color_blind_safety(["#DC2626", "#16A34A"])
|
|
print(f" Red + green: safe={risky['safe']}, issues={len(risky.get('issues', []))}")
|
|
check("Red+green flagged as risky", not risky["safe"] or len(risky.get("issues", [])) > 0,
|
|
"should flag deuteranopia/protanopia")
|
|
|
|
# Single color = no pairs to check
|
|
single = check_color_blind_safety(["#FF0000"])
|
|
check("Single color is safe", single["safe"])
|
|
|
|
# Grays should be safe (low saturation)
|
|
grays = check_color_blind_safety(["#333333", "#999999", "#CCCCCC"])
|
|
check("Grays are safe", grays["safe"])
|
|
|
|
# ===================================================================
|
|
# Test 5: Full palette validation per playbook
|
|
# ===================================================================
|
|
print("\n--- Test 5: Palette validation (all playbooks) ---")
|
|
|
|
for name, pb in loaded.items():
|
|
if pb is None:
|
|
continue
|
|
issues = validate_palette(pb)
|
|
errors = [i for i in issues if i.get("severity") == "error"]
|
|
warnings = [i for i in issues if i.get("severity") == "warning"]
|
|
print(f" [{name}] {len(errors)} errors, {len(warnings)} warnings, {len(issues)} total issues")
|
|
check(f"{name}: no contrast errors", len(errors) == 0,
|
|
"; ".join(e["message"] for e in errors) if errors else "all clear")
|
|
for issue in issues:
|
|
sev = issue.get("severity", "?")
|
|
print(f" [{sev}] {issue.get('message', '')}")
|
|
|
|
# ===================================================================
|
|
# Test 6: Type scale computation
|
|
# ===================================================================
|
|
print("\n--- Test 6: Type scale computation ---")
|
|
|
|
for ratio_name, ratio_val in TYPE_SCALE_RATIOS.items():
|
|
scale = compute_type_scale(24, ratio_name)
|
|
sizes = scale["sizes"]
|
|
check(f"Scale {ratio_name}: display > heading > subheading > body > caption",
|
|
sizes["display"] > sizes["heading"] > sizes["subheading"] > sizes["body"] > sizes["caption"],
|
|
f"{sizes}")
|
|
check(f" Base preserved", sizes["body"] == 24)
|
|
|
|
# Custom numeric ratio
|
|
scale = compute_type_scale(24, "1.5")
|
|
check("Custom ratio 1.5", scale["ratio_value"] == 1.5, f"sizes={scale['sizes']}")
|
|
|
|
# ===================================================================
|
|
# Test 7: Type hierarchy validation
|
|
# ===================================================================
|
|
print("\n--- Test 7: Type hierarchy validation ---")
|
|
|
|
for name, pb in loaded.items():
|
|
if pb is None:
|
|
continue
|
|
issues = validate_type_hierarchy(pb)
|
|
print(f" [{name}] {len(issues)} type hierarchy issues")
|
|
for issue in issues:
|
|
print(f" [{issue.get('severity')}] {issue.get('message')}")
|
|
# No errors expected in shipped playbooks
|
|
errors = [i for i in issues if i.get("severity") == "error"]
|
|
check(f"{name}: no type hierarchy errors", len(errors) == 0)
|
|
|
|
# Deliberately bad playbook
|
|
bad_typography = {
|
|
"typography": {
|
|
"headings": {"font": "Inter", "weight": 400},
|
|
"body": {"font": "Inter", "weight": 400},
|
|
"stat_card": {"font": "Inter", "size_multiplier": 0.8},
|
|
}
|
|
}
|
|
issues = validate_type_hierarchy(bad_typography)
|
|
check("Bad typography flagged", len(issues) > 0, f"{len(issues)} issues found")
|
|
|
|
# ===================================================================
|
|
# Test 8: Font pairing suggestions
|
|
# ===================================================================
|
|
print("\n--- Test 8: Font pairing suggestions ---")
|
|
|
|
for font in ["Inter", "Space Grotesk", "IBM Plex Sans", "Lora", "JetBrains Mono"]:
|
|
pairings = suggest_font_pairing(font)
|
|
check(f"Pairings for {font}", len(pairings) >= 1, f"{len(pairings)} suggestions")
|
|
for p in pairings:
|
|
print(f" → {p['font']} ({p['category']}): {p['rationale']}")
|
|
|
|
# Unknown font fallback
|
|
pairings = suggest_font_pairing("UnknownFont")
|
|
check("Unknown font gets fallback", len(pairings) >= 1)
|
|
|
|
# ===================================================================
|
|
# Test 9: Full accessibility audit (all playbooks)
|
|
# ===================================================================
|
|
print("\n--- Test 9: Accessibility audit (all playbooks) ---")
|
|
|
|
for name, pb in loaded.items():
|
|
if pb is None:
|
|
continue
|
|
result = validate_accessibility(pb)
|
|
status = "PASS" if result["pass"] else "FAIL"
|
|
print(f"\n [{name}] Overall: {status}"
|
|
f" | Errors: {result['error_count']}"
|
|
f" | Warnings: {result['warning_count']}"
|
|
f" | Total: {result['total_issues']}")
|
|
check(f"{name}: a11y audit passes", result["pass"])
|
|
for issue in result["issues"]:
|
|
cat = issue.get("category", "?")
|
|
sev = issue.get("severity", "?")
|
|
print(f" [{cat}/{sev}] {issue.get('message', '')}")
|
|
|
|
# ===================================================================
|
|
# Test 10: Deliberately low-contrast custom playbook
|
|
# ===================================================================
|
|
print("\n--- Test 10: Low-contrast custom playbook ---")
|
|
|
|
low_contrast_pb = {
|
|
"identity": {"name": "low-contrast-test", "category": "test", "mood": "test", "pace": "moderate", "best_for": ["testing"]},
|
|
"visual_language": {
|
|
"color_palette": {
|
|
"primary": ["#555555"],
|
|
"accent": ["#666666"],
|
|
"background": "#444444",
|
|
"text": "#555555",
|
|
"muted": "#4A4A4A",
|
|
},
|
|
"composition": "centered",
|
|
"texture": "none",
|
|
},
|
|
"typography": {
|
|
"headings": {"font": "Arial", "weight": 700, "size_multiplier": 1.5},
|
|
"body": {"font": "Arial", "weight": 400, "size_multiplier": 1.0},
|
|
"code": {"font": "Courier", "weight": 400, "size_multiplier": 0.9},
|
|
"stat_card": {"font": "Arial", "weight": 700, "size_multiplier": 2.5},
|
|
"scale_system": "major_third",
|
|
"weight_matrix": {"title": 800, "heading": 700, "body": 400, "caption": 300},
|
|
},
|
|
"motion": {"transitions": "cut", "animation_style": "none", "pacing_rules": {}},
|
|
"audio": {"voice_style": "neutral", "music_mood": "none"},
|
|
"asset_generation": {"image_prompt_prefix": "test", "negative_prompt": ""},
|
|
"overlays": {
|
|
"stat_card": {"bg": "#444444", "text": "#555555", "border": "#444444", "radius": 8, "shadow": "none"},
|
|
},
|
|
"quality_rules": [],
|
|
"chart_palette": ["#555555", "#666666", "#777777"],
|
|
}
|
|
|
|
# This should be flagged with errors
|
|
issues = validate_palette(low_contrast_pb)
|
|
errors = [i for i in issues if i.get("severity") == "error"]
|
|
check("Low-contrast playbook has errors", len(errors) > 0, f"{len(errors)} contrast errors")
|
|
for e in errors:
|
|
print(f" [error] {e.get('message')}")
|
|
|
|
# ===================================================================
|
|
# Summary
|
|
# ===================================================================
|
|
print(f"\n{'='*60}")
|
|
print(f"PLAYBOOK INTELLIGENCE TEST COMPLETE: {PASS} passed, {FAIL} failed")
|
|
print(f"{'='*60}")
|