claude-presentation-master 4.1.0 → 4.3.0
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/bin/cli.js +2 -2
- package/dist/index.d.mts +207 -22
- package/dist/index.d.ts +207 -22
- package/dist/index.js +968 -185
- package/dist/index.mjs +901 -121
- package/package.json +5 -3
package/dist/index.mjs
CHANGED
|
@@ -2489,69 +2489,112 @@ function createSlideGeneratorV2(type = "consulting_deck") {
|
|
|
2489
2489
|
|
|
2490
2490
|
// src/output/RendererV2.ts
|
|
2491
2491
|
import { writeFileSync } from "fs";
|
|
2492
|
-
var
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2492
|
+
var FALLBACK_PALETTES = {
|
|
2493
|
+
consulting_classic: {
|
|
2494
|
+
background: "#FAFAF9",
|
|
2495
|
+
primary: "#0F172A",
|
|
2496
|
+
secondary: "#475569",
|
|
2497
|
+
accent: "#0369A1",
|
|
2498
|
+
text: "#18181B",
|
|
2499
|
+
name: "Consulting Classic"
|
|
2499
2500
|
},
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2501
|
+
dark_executive: {
|
|
2502
|
+
background: "#18181B",
|
|
2503
|
+
primary: "#FAFAFA",
|
|
2504
|
+
secondary: "#A1A1AA",
|
|
2505
|
+
accent: "#F59E0B",
|
|
2506
|
+
text: "#F4F4F5",
|
|
2507
|
+
name: "Dark Executive"
|
|
2505
2508
|
},
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2509
|
+
modern_business: {
|
|
2510
|
+
background: "#F8FAFC",
|
|
2511
|
+
primary: "#1E293B",
|
|
2512
|
+
secondary: "#64748B",
|
|
2513
|
+
accent: "#0891B2",
|
|
2514
|
+
text: "#0F172A",
|
|
2515
|
+
name: "Modern Business"
|
|
2511
2516
|
},
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2517
|
+
executive_professional: {
|
|
2518
|
+
background: "#F5F5F4",
|
|
2519
|
+
primary: "#1E3A5F",
|
|
2520
|
+
secondary: "#64748B",
|
|
2521
|
+
accent: "#D97706",
|
|
2522
|
+
text: "#1F2937",
|
|
2523
|
+
name: "Executive Professional"
|
|
2518
2524
|
},
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2525
|
+
strategy_growth: {
|
|
2526
|
+
background: "#FAF9F7",
|
|
2527
|
+
primary: "#292524",
|
|
2528
|
+
secondary: "#78716C",
|
|
2529
|
+
accent: "#059669",
|
|
2530
|
+
text: "#1C1917",
|
|
2531
|
+
name: "Strategy Growth"
|
|
2524
2532
|
}
|
|
2525
2533
|
};
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
// Sales → Dark dramatic style (persuasion, impact)
|
|
2530
|
-
sales_pitch: "dark",
|
|
2531
|
-
// Consulting/Analysis decks → McKinsey white style (data-heavy, professional)
|
|
2532
|
-
consulting_deck: "mckinsey",
|
|
2533
|
-
// Investment Banking → McKinsey style (financial, dense data)
|
|
2534
|
-
investment_banking: "mckinsey",
|
|
2535
|
-
// Investor Pitch → Startup dark style (modern, VC audiences)
|
|
2536
|
-
investor_pitch: "startup",
|
|
2537
|
-
// Technical → Dark style (engineering audiences like dark mode)
|
|
2538
|
-
technical_presentation: "dark",
|
|
2539
|
-
// All Hands → Minimal clean style (readable, accessible)
|
|
2540
|
-
all_hands: "minimal"
|
|
2541
|
-
};
|
|
2534
|
+
function getPaletteStyle(paletteName) {
|
|
2535
|
+
return paletteName === "dark_executive" ? "dark" : "light";
|
|
2536
|
+
}
|
|
2542
2537
|
var RendererV2 = class {
|
|
2543
2538
|
theme;
|
|
2544
2539
|
presentationType;
|
|
2540
|
+
kb;
|
|
2545
2541
|
/**
|
|
2546
|
-
* Create renderer with theme
|
|
2547
|
-
* @param presentationType - The type of deck being created (determines
|
|
2548
|
-
* @param
|
|
2542
|
+
* Create renderer with theme loaded from Knowledge Base.
|
|
2543
|
+
* @param presentationType - The type of deck being created (determines palette)
|
|
2544
|
+
* @param kb - Knowledge Base gateway (optional, will use global if not provided)
|
|
2549
2545
|
*/
|
|
2550
|
-
constructor(presentationType = "consulting_deck",
|
|
2546
|
+
constructor(presentationType = "consulting_deck", kb) {
|
|
2551
2547
|
this.presentationType = presentationType;
|
|
2552
|
-
|
|
2553
|
-
this.theme =
|
|
2554
|
-
console.log(`[RendererV2] Using "${this.theme.
|
|
2548
|
+
this.kb = kb || getKB();
|
|
2549
|
+
this.theme = this.loadThemeFromKB(presentationType);
|
|
2550
|
+
console.log(`[RendererV2] Using "${this.theme.paletteName}" palette (${this.theme.style} style) for "${presentationType}" deck`);
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Load theme configuration from Knowledge Base.
|
|
2554
|
+
* Falls back to hardcoded values only if KB fails.
|
|
2555
|
+
*/
|
|
2556
|
+
loadThemeFromKB(type) {
|
|
2557
|
+
try {
|
|
2558
|
+
const paletteName = this.kb.queryOptional(`presentation_types.${type}.color_palette`).value || this.getDefaultPaletteName(type);
|
|
2559
|
+
const kbPalette = this.kb.queryOptional(`color_palettes.${paletteName}`).value;
|
|
2560
|
+
if (kbPalette) {
|
|
2561
|
+
console.log(`[RendererV2] Loaded "${paletteName}" palette from KB: bg=${kbPalette.background}, accent=${kbPalette.accent}`);
|
|
2562
|
+
return {
|
|
2563
|
+
style: getPaletteStyle(paletteName),
|
|
2564
|
+
palette: kbPalette,
|
|
2565
|
+
paletteName
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
const fallback = FALLBACK_PALETTES[paletteName] ?? FALLBACK_PALETTES.consulting_classic;
|
|
2569
|
+
console.log(`[RendererV2] Using fallback "${paletteName}" palette`);
|
|
2570
|
+
return {
|
|
2571
|
+
style: getPaletteStyle(paletteName),
|
|
2572
|
+
palette: fallback,
|
|
2573
|
+
paletteName
|
|
2574
|
+
};
|
|
2575
|
+
} catch {
|
|
2576
|
+
const defaultPalette = FALLBACK_PALETTES.consulting_classic;
|
|
2577
|
+
return {
|
|
2578
|
+
style: "light",
|
|
2579
|
+
palette: defaultPalette,
|
|
2580
|
+
paletteName: "consulting_classic"
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
2585
|
+
* Default palette mapping per presentation type (used if KB doesn't specify).
|
|
2586
|
+
*/
|
|
2587
|
+
getDefaultPaletteName(type) {
|
|
2588
|
+
const defaults = {
|
|
2589
|
+
ted_keynote: "dark_executive",
|
|
2590
|
+
sales_pitch: "modern_business",
|
|
2591
|
+
consulting_deck: "consulting_classic",
|
|
2592
|
+
investment_banking: "executive_professional",
|
|
2593
|
+
investor_pitch: "modern_business",
|
|
2594
|
+
technical_presentation: "dark_executive",
|
|
2595
|
+
all_hands: "strategy_growth"
|
|
2596
|
+
};
|
|
2597
|
+
return defaults[type] || "consulting_classic";
|
|
2555
2598
|
}
|
|
2556
2599
|
/**
|
|
2557
2600
|
* Render slides to complete HTML document.
|
|
@@ -2641,15 +2684,15 @@ ${slidesHtml}
|
|
|
2641
2684
|
const content = this.renderSlideContent(slide);
|
|
2642
2685
|
return `<div class="slide slide-${slide.type}">${content}</div>`;
|
|
2643
2686
|
}).join("\n");
|
|
2644
|
-
if (this.theme.style === "
|
|
2645
|
-
return this.
|
|
2687
|
+
if (this.theme.style === "light") {
|
|
2688
|
+
return this.getLightPrintHTML(title, slidesHtml);
|
|
2646
2689
|
}
|
|
2647
2690
|
return this.getDarkPrintHTML(title, slidesHtml);
|
|
2648
2691
|
}
|
|
2649
2692
|
/**
|
|
2650
|
-
*
|
|
2693
|
+
* Light theme print HTML (professional, white background).
|
|
2651
2694
|
*/
|
|
2652
|
-
|
|
2695
|
+
getLightPrintHTML(title, slidesHtml) {
|
|
2653
2696
|
return `<!DOCTYPE html>
|
|
2654
2697
|
<html lang="en">
|
|
2655
2698
|
<head>
|
|
@@ -3305,34 +3348,60 @@ ${content}
|
|
|
3305
3348
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3306
3349
|
}
|
|
3307
3350
|
/**
|
|
3308
|
-
*
|
|
3351
|
+
* Generate CSS with colors from Knowledge Base.
|
|
3309
3352
|
*/
|
|
3310
3353
|
getCSS() {
|
|
3311
|
-
if (this.theme.style === "
|
|
3312
|
-
return this.
|
|
3354
|
+
if (this.theme.style === "light") {
|
|
3355
|
+
return this.getLightThemeCSS();
|
|
3313
3356
|
}
|
|
3314
|
-
return this.
|
|
3357
|
+
return this.getDarkThemeCSS();
|
|
3315
3358
|
}
|
|
3316
3359
|
/**
|
|
3317
|
-
*
|
|
3360
|
+
* Generate CSS variables from KB palette.
|
|
3318
3361
|
*/
|
|
3319
|
-
|
|
3362
|
+
getCSSVariables() {
|
|
3363
|
+
const p = this.theme.palette;
|
|
3364
|
+
const isLight = this.theme.style === "light";
|
|
3365
|
+
const bgSecondary = isLight ? this.lightenColor(p.background, -5) : this.lightenColor(p.background, 10);
|
|
3366
|
+
const bgAccent = isLight ? this.lightenColor(p.background, -10) : this.lightenColor(p.background, 15);
|
|
3367
|
+
const textMuted = isLight ? this.lightenColor(p.text, 40) : this.lightenColor(p.text, -30);
|
|
3320
3368
|
return `
|
|
3321
|
-
/*
|
|
3369
|
+
/* ${this.theme.paletteName} Theme - Loaded from Knowledge Base */
|
|
3322
3370
|
:root {
|
|
3323
|
-
--
|
|
3324
|
-
--bg-
|
|
3325
|
-
--bg-
|
|
3326
|
-
--
|
|
3327
|
-
--text-
|
|
3328
|
-
--text-
|
|
3329
|
-
--
|
|
3330
|
-
--
|
|
3371
|
+
--kb-palette: "${this.theme.paletteName}";
|
|
3372
|
+
--bg-primary: ${p.background};
|
|
3373
|
+
--bg-secondary: ${bgSecondary};
|
|
3374
|
+
--bg-accent: ${bgAccent};
|
|
3375
|
+
--text-primary: ${p.text};
|
|
3376
|
+
--text-secondary: ${p.secondary};
|
|
3377
|
+
--text-muted: ${textMuted};
|
|
3378
|
+
--color-primary: ${p.primary};
|
|
3379
|
+
--color-accent: ${p.accent};
|
|
3380
|
+
--accent-blue: ${p.accent};
|
|
3331
3381
|
--accent-green: #28a745;
|
|
3332
3382
|
--accent-red: #dc3545;
|
|
3333
|
-
--border-color: #dee2e6;
|
|
3334
|
-
--header-bar:
|
|
3335
|
-
|
|
3383
|
+
--border-color: ${isLight ? "#dee2e6" : "rgba(255,255,255,0.1)"};
|
|
3384
|
+
--header-bar: ${p.primary};
|
|
3385
|
+
--mckinsey-blue: ${p.accent};
|
|
3386
|
+
}`;
|
|
3387
|
+
}
|
|
3388
|
+
/**
|
|
3389
|
+
* Lighten or darken a hex color.
|
|
3390
|
+
*/
|
|
3391
|
+
lightenColor(hex, percent) {
|
|
3392
|
+
const num = parseInt(hex.replace("#", ""), 16);
|
|
3393
|
+
const amt = Math.round(2.55 * percent);
|
|
3394
|
+
const R = Math.max(0, Math.min(255, (num >> 16) + amt));
|
|
3395
|
+
const G = Math.max(0, Math.min(255, (num >> 8 & 255) + amt));
|
|
3396
|
+
const B = Math.max(0, Math.min(255, (num & 255) + amt));
|
|
3397
|
+
return `#${(16777216 + R * 65536 + G * 256 + B).toString(16).slice(1)}`;
|
|
3398
|
+
}
|
|
3399
|
+
/**
|
|
3400
|
+
* Light theme CSS (consulting style) with KB palette colors.
|
|
3401
|
+
*/
|
|
3402
|
+
getLightThemeCSS() {
|
|
3403
|
+
return `
|
|
3404
|
+
${this.getCSSVariables()}
|
|
3336
3405
|
|
|
3337
3406
|
.reveal {
|
|
3338
3407
|
font-family: 'Georgia', 'Times New Roman', serif;
|
|
@@ -3603,23 +3672,11 @@ ${content}
|
|
|
3603
3672
|
`;
|
|
3604
3673
|
}
|
|
3605
3674
|
/**
|
|
3606
|
-
* Dark theme CSS
|
|
3675
|
+
* Dark theme CSS with KB palette colors.
|
|
3607
3676
|
*/
|
|
3608
|
-
|
|
3677
|
+
getDarkThemeCSS() {
|
|
3609
3678
|
return `
|
|
3610
|
-
|
|
3611
|
-
:root {
|
|
3612
|
-
--bg-primary: #1a1a2e;
|
|
3613
|
-
--bg-secondary: #16213e;
|
|
3614
|
-
--bg-accent: #0f3460;
|
|
3615
|
-
--text-primary: #ffffff;
|
|
3616
|
-
--text-secondary: rgba(255, 255, 255, 0.85);
|
|
3617
|
-
--text-muted: rgba(255, 255, 255, 0.6);
|
|
3618
|
-
--accent-blue: #4a9eff;
|
|
3619
|
-
--accent-green: #00d4aa;
|
|
3620
|
-
--accent-orange: #ff9f43;
|
|
3621
|
-
--border-color: rgba(255, 255, 255, 0.1);
|
|
3622
|
-
}
|
|
3679
|
+
${this.getCSSVariables()}
|
|
3623
3680
|
|
|
3624
3681
|
.reveal {
|
|
3625
3682
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
@@ -3830,18 +3887,625 @@ ${content}
|
|
|
3830
3887
|
`;
|
|
3831
3888
|
}
|
|
3832
3889
|
};
|
|
3833
|
-
function createRendererV2(presentationType = "consulting_deck",
|
|
3834
|
-
return new RendererV2(presentationType,
|
|
3890
|
+
function createRendererV2(presentationType = "consulting_deck", kb) {
|
|
3891
|
+
return new RendererV2(presentationType, kb);
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
// src/qa/VisualQAEngine.ts
|
|
3895
|
+
import { writeFileSync as writeFileSync2, unlinkSync, mkdtempSync } from "fs";
|
|
3896
|
+
import { join as join2 } from "path";
|
|
3897
|
+
import { tmpdir } from "os";
|
|
3898
|
+
var VisualQAEngine = class {
|
|
3899
|
+
kb;
|
|
3900
|
+
browser = null;
|
|
3901
|
+
initialized = false;
|
|
3902
|
+
// KB-loaded thresholds
|
|
3903
|
+
minWhitespace = 0.35;
|
|
3904
|
+
minContrast = 4.5;
|
|
3905
|
+
// WCAG AA
|
|
3906
|
+
maxWordsPerSlide = 50;
|
|
3907
|
+
targetScore = 95;
|
|
3908
|
+
constructor() {
|
|
3909
|
+
this.kb = getKB();
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3912
|
+
* Initialize Playwright browser.
|
|
3913
|
+
*/
|
|
3914
|
+
async initialize() {
|
|
3915
|
+
if (this.initialized) return;
|
|
3916
|
+
const playwright = await import("playwright");
|
|
3917
|
+
this.browser = await playwright.chromium.launch({
|
|
3918
|
+
headless: true,
|
|
3919
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
3920
|
+
});
|
|
3921
|
+
await this.loadKBThresholds();
|
|
3922
|
+
this.initialized = true;
|
|
3923
|
+
console.log("[VisualQA] Initialized with Playwright");
|
|
3924
|
+
}
|
|
3925
|
+
/**
|
|
3926
|
+
* Load quality thresholds from Knowledge Base.
|
|
3927
|
+
*/
|
|
3928
|
+
async loadKBThresholds() {
|
|
3929
|
+
try {
|
|
3930
|
+
const whitespaceResult = this.kb.queryOptional("design_system.whitespace_rules");
|
|
3931
|
+
if (whitespaceResult.value?.min) {
|
|
3932
|
+
this.minWhitespace = whitespaceResult.value.min;
|
|
3933
|
+
}
|
|
3934
|
+
} catch {
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
/**
|
|
3938
|
+
* Capture screenshots of all slides in a presentation.
|
|
3939
|
+
*/
|
|
3940
|
+
async captureSlides(html) {
|
|
3941
|
+
await this.initialize();
|
|
3942
|
+
if (!this.browser) throw new Error("Browser not initialized");
|
|
3943
|
+
const tempDir = mkdtempSync(join2(tmpdir(), "visual-qa-"));
|
|
3944
|
+
const tempFile = join2(tempDir, "presentation.html");
|
|
3945
|
+
const modifiedHtml = html.replace(
|
|
3946
|
+
/hash:\s*true/g,
|
|
3947
|
+
"hash: false"
|
|
3948
|
+
);
|
|
3949
|
+
writeFileSync2(tempFile, modifiedHtml);
|
|
3950
|
+
const page = await this.browser.newPage();
|
|
3951
|
+
await page.setViewportSize({ width: 1920, height: 1080 });
|
|
3952
|
+
await page.goto(`file://${tempFile}`, { waitUntil: "networkidle" });
|
|
3953
|
+
await page.waitForTimeout(1500);
|
|
3954
|
+
const slideCount = await page.evaluate(() => {
|
|
3955
|
+
const reveal = window.Reveal;
|
|
3956
|
+
if (reveal) {
|
|
3957
|
+
return reveal.getTotalSlides();
|
|
3958
|
+
}
|
|
3959
|
+
return document.querySelectorAll(".slides > section").length;
|
|
3960
|
+
});
|
|
3961
|
+
console.log(`[VisualQA] Capturing ${slideCount} slides...`);
|
|
3962
|
+
const screenshots = [];
|
|
3963
|
+
for (let i = 0; i < slideCount; i++) {
|
|
3964
|
+
await page.evaluate((index) => {
|
|
3965
|
+
const reveal = window.Reveal;
|
|
3966
|
+
if (reveal) {
|
|
3967
|
+
reveal.slide(index, 0, 0);
|
|
3968
|
+
}
|
|
3969
|
+
}, i);
|
|
3970
|
+
await page.waitForTimeout(400);
|
|
3971
|
+
const screenshot = await page.screenshot({
|
|
3972
|
+
type: "png",
|
|
3973
|
+
fullPage: false
|
|
3974
|
+
});
|
|
3975
|
+
screenshots.push(screenshot);
|
|
3976
|
+
console.log(`[VisualQA] Captured slide ${i + 1}/${slideCount}`);
|
|
3977
|
+
}
|
|
3978
|
+
await page.close();
|
|
3979
|
+
try {
|
|
3980
|
+
unlinkSync(tempFile);
|
|
3981
|
+
} catch {
|
|
3982
|
+
}
|
|
3983
|
+
return screenshots;
|
|
3984
|
+
}
|
|
3985
|
+
/**
|
|
3986
|
+
* Analyze a single slide screenshot.
|
|
3987
|
+
*/
|
|
3988
|
+
async analyzeSlide(screenshot, slideIndex, slideTitle, presentationType) {
|
|
3989
|
+
const issues = [];
|
|
3990
|
+
let score = 100;
|
|
3991
|
+
const typeThresholds = this.getTypeThresholds(presentationType);
|
|
3992
|
+
const whitespacePercentage = await this.measureWhitespace(screenshot);
|
|
3993
|
+
if (whitespacePercentage < typeThresholds.minWhitespace) {
|
|
3994
|
+
const penalty = Math.round((typeThresholds.minWhitespace - whitespacePercentage) * 100);
|
|
3995
|
+
score -= penalty;
|
|
3996
|
+
issues.push({
|
|
3997
|
+
severity: penalty > 15 ? "critical" : "major",
|
|
3998
|
+
category: "whitespace",
|
|
3999
|
+
issue: `Whitespace is ${(whitespacePercentage * 100).toFixed(1)}% (need ${typeThresholds.minWhitespace * 100}%+)`,
|
|
4000
|
+
measurement: whitespacePercentage,
|
|
4001
|
+
threshold: typeThresholds.minWhitespace,
|
|
4002
|
+
fix: "Reduce content or increase margins"
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
4005
|
+
const colorContrast = await this.measureContrast(screenshot);
|
|
4006
|
+
if (colorContrast < this.minContrast) {
|
|
4007
|
+
score -= 20;
|
|
4008
|
+
issues.push({
|
|
4009
|
+
severity: "critical",
|
|
4010
|
+
category: "contrast",
|
|
4011
|
+
issue: `Contrast ratio is ${colorContrast.toFixed(1)}:1 (WCAG AA requires 4.5:1)`,
|
|
4012
|
+
measurement: colorContrast,
|
|
4013
|
+
threshold: this.minContrast,
|
|
4014
|
+
fix: "Increase contrast between text and background"
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
4017
|
+
const textDensity = await this.measureTextDensity(screenshot);
|
|
4018
|
+
const maxDensity = typeThresholds.maxWordsPerSlide / 100;
|
|
4019
|
+
if (textDensity > maxDensity) {
|
|
4020
|
+
score -= 15;
|
|
4021
|
+
issues.push({
|
|
4022
|
+
severity: "major",
|
|
4023
|
+
category: "density",
|
|
4024
|
+
issue: `Text is too dense for ${presentationType}`,
|
|
4025
|
+
measurement: textDensity,
|
|
4026
|
+
threshold: maxDensity,
|
|
4027
|
+
fix: "Split content across multiple slides"
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
const visualBalance = await this.measureBalance(screenshot);
|
|
4031
|
+
const balanceThreshold = ["ted_keynote", "sales_pitch"].includes(presentationType) ? 20 : 60;
|
|
4032
|
+
if (visualBalance < balanceThreshold) {
|
|
4033
|
+
score -= 10;
|
|
4034
|
+
issues.push({
|
|
4035
|
+
severity: "minor",
|
|
4036
|
+
category: "balance",
|
|
4037
|
+
issue: `Visual balance score is ${visualBalance}/100`,
|
|
4038
|
+
measurement: visualBalance,
|
|
4039
|
+
threshold: balanceThreshold,
|
|
4040
|
+
fix: "Redistribute content for better visual balance"
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
const hierarchyClear = await this.checkHierarchy(screenshot);
|
|
4044
|
+
if (!hierarchyClear) {
|
|
4045
|
+
score -= 10;
|
|
4046
|
+
issues.push({
|
|
4047
|
+
severity: "major",
|
|
4048
|
+
category: "hierarchy",
|
|
4049
|
+
issue: "No clear visual hierarchy detected",
|
|
4050
|
+
fix: "Add distinct title styling or increase heading size"
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
const glanceTestPassed = textDensity < 0.3 && hierarchyClear;
|
|
4054
|
+
if (!glanceTestPassed && presentationType !== "investment_banking") {
|
|
4055
|
+
score -= 10;
|
|
4056
|
+
issues.push({
|
|
4057
|
+
severity: "major",
|
|
4058
|
+
category: "density",
|
|
4059
|
+
issue: "Fails 3-second glance test",
|
|
4060
|
+
fix: "Simplify to one clear message per slide"
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
const minBalanceForPro = ["ted_keynote", "sales_pitch"].includes(presentationType) ? 20 : 50;
|
|
4064
|
+
const professionalLook = whitespacePercentage >= typeThresholds.minWhitespace * 0.9 && colorContrast >= this.minContrast * 0.9 && visualBalance >= minBalanceForPro;
|
|
4065
|
+
if (!professionalLook) {
|
|
4066
|
+
score -= 5;
|
|
4067
|
+
issues.push({
|
|
4068
|
+
severity: "minor",
|
|
4069
|
+
category: "professional",
|
|
4070
|
+
issue: "Slide lacks professional polish",
|
|
4071
|
+
fix: "Review spacing, colors, and alignment"
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
return {
|
|
4075
|
+
slideIndex,
|
|
4076
|
+
title: slideTitle,
|
|
4077
|
+
screenshot,
|
|
4078
|
+
whitespacePercentage,
|
|
4079
|
+
textDensity,
|
|
4080
|
+
colorContrast,
|
|
4081
|
+
visualBalance,
|
|
4082
|
+
glanceTestPassed,
|
|
4083
|
+
hierarchyClear,
|
|
4084
|
+
professionalLook,
|
|
4085
|
+
score: Math.max(0, Math.min(100, score)),
|
|
4086
|
+
issues,
|
|
4087
|
+
passed: score >= this.targetScore
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
/**
|
|
4091
|
+
* Analyze entire deck visually.
|
|
4092
|
+
*/
|
|
4093
|
+
async analyzeDeck(html, slides, presentationType) {
|
|
4094
|
+
console.log(`[VisualQA] Starting visual analysis of ${slides.length} slides...`);
|
|
4095
|
+
const screenshots = await this.captureSlides(html);
|
|
4096
|
+
const slideAnalyses = [];
|
|
4097
|
+
for (let i = 0; i < screenshots.length; i++) {
|
|
4098
|
+
const slide = slides[i];
|
|
4099
|
+
const analysis = await this.analyzeSlide(
|
|
4100
|
+
screenshots[i],
|
|
4101
|
+
i,
|
|
4102
|
+
slide?.title || `Slide ${i + 1}`,
|
|
4103
|
+
presentationType
|
|
4104
|
+
);
|
|
4105
|
+
slideAnalyses.push(analysis);
|
|
4106
|
+
console.log(`[VisualQA] Slide ${i + 1}: ${analysis.score}/100 ${analysis.passed ? "\u2713" : "\u2717"}`);
|
|
4107
|
+
}
|
|
4108
|
+
const consistency = this.calculateConsistency(slideAnalyses);
|
|
4109
|
+
const deckIssues = [];
|
|
4110
|
+
const failingSlides = slideAnalyses.filter((s) => !s.passed);
|
|
4111
|
+
if (failingSlides.length > 0) {
|
|
4112
|
+
deckIssues.push(`${failingSlides.length} slides below quality threshold`);
|
|
4113
|
+
}
|
|
4114
|
+
if (consistency < 80) {
|
|
4115
|
+
deckIssues.push(`Visual inconsistency across slides (${consistency}/100)`);
|
|
4116
|
+
}
|
|
4117
|
+
const avgScore = slideAnalyses.reduce((sum, s) => sum + s.score, 0) / slideAnalyses.length;
|
|
4118
|
+
const consistencyBonus = consistency >= 90 ? 2 : consistency >= 80 ? 0 : -3;
|
|
4119
|
+
const overallScore = Math.round(Math.min(100, Math.max(0, avgScore + consistencyBonus)));
|
|
4120
|
+
const passed = overallScore >= this.targetScore && failingSlides.length === 0;
|
|
4121
|
+
const summary = passed ? `\u2705 Visual QA PASSED: ${overallScore}/100 with ${consistency}% consistency` : `\u274C Visual QA FAILED: ${overallScore}/100, ${failingSlides.length} slides need work`;
|
|
4122
|
+
console.log(`[VisualQA] ${summary}`);
|
|
4123
|
+
return {
|
|
4124
|
+
slides: slideAnalyses,
|
|
4125
|
+
overallScore,
|
|
4126
|
+
passed,
|
|
4127
|
+
consistency,
|
|
4128
|
+
deckIssues,
|
|
4129
|
+
summary
|
|
4130
|
+
};
|
|
4131
|
+
}
|
|
4132
|
+
/**
|
|
4133
|
+
* Generate remediation feedback for failing slides.
|
|
4134
|
+
*/
|
|
4135
|
+
generateRemediationFeedback(analysis) {
|
|
4136
|
+
const feedback = [];
|
|
4137
|
+
for (const slide of analysis.slides) {
|
|
4138
|
+
if (!slide.passed) {
|
|
4139
|
+
const fixes = [];
|
|
4140
|
+
const problems = [];
|
|
4141
|
+
for (const issue of slide.issues) {
|
|
4142
|
+
problems.push(issue.issue);
|
|
4143
|
+
fixes.push(issue.fix);
|
|
4144
|
+
}
|
|
4145
|
+
feedback.push({
|
|
4146
|
+
slideIndex: slide.slideIndex,
|
|
4147
|
+
currentScore: slide.score,
|
|
4148
|
+
targetScore: this.targetScore,
|
|
4149
|
+
specificFixes: fixes,
|
|
4150
|
+
visualProblems: problems
|
|
4151
|
+
});
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
return feedback;
|
|
4155
|
+
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Run the full QA loop until passing or max iterations.
|
|
4158
|
+
*/
|
|
4159
|
+
async runQALoop(generateFn, remediateFn, presentationType, maxIterations = 3) {
|
|
4160
|
+
console.log(`[VisualQA] Starting QA loop (max ${maxIterations} iterations)...`);
|
|
4161
|
+
let iteration = 0;
|
|
4162
|
+
let currentAnalysis = null;
|
|
4163
|
+
let initialScore = 0;
|
|
4164
|
+
const improvements = [];
|
|
4165
|
+
while (iteration < maxIterations) {
|
|
4166
|
+
iteration++;
|
|
4167
|
+
console.log(`
|
|
4168
|
+
[VisualQA] === Iteration ${iteration} ===`);
|
|
4169
|
+
const { html, slides } = await generateFn();
|
|
4170
|
+
currentAnalysis = await this.analyzeDeck(html, slides, presentationType);
|
|
4171
|
+
if (iteration === 1) {
|
|
4172
|
+
initialScore = currentAnalysis.overallScore;
|
|
4173
|
+
}
|
|
4174
|
+
if (currentAnalysis.passed) {
|
|
4175
|
+
console.log(`[VisualQA] \u2705 PASSED on iteration ${iteration}!`);
|
|
4176
|
+
break;
|
|
4177
|
+
}
|
|
4178
|
+
const feedback = this.generateRemediationFeedback(currentAnalysis);
|
|
4179
|
+
console.log(`[VisualQA] ${feedback.length} slides need remediation`);
|
|
4180
|
+
for (const fb of feedback) {
|
|
4181
|
+
console.log(` Slide ${fb.slideIndex + 1}: ${fb.currentScore}\u2192${fb.targetScore}`);
|
|
4182
|
+
for (const fix of fb.specificFixes.slice(0, 2)) {
|
|
4183
|
+
console.log(` - ${fix}`);
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
improvements.push(`Iteration ${iteration}: ${currentAnalysis.overallScore}/100`);
|
|
4187
|
+
if (iteration < maxIterations) {
|
|
4188
|
+
await remediateFn(feedback);
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
if (!currentAnalysis) {
|
|
4192
|
+
throw new Error("No analysis performed");
|
|
4193
|
+
}
|
|
4194
|
+
return {
|
|
4195
|
+
finalScore: currentAnalysis.overallScore,
|
|
4196
|
+
passed: currentAnalysis.passed,
|
|
4197
|
+
iterations: iteration,
|
|
4198
|
+
initialScore,
|
|
4199
|
+
improvements,
|
|
4200
|
+
finalAnalysis: currentAnalysis
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
/**
|
|
4204
|
+
* Close browser and cleanup.
|
|
4205
|
+
*/
|
|
4206
|
+
async cleanup() {
|
|
4207
|
+
if (this.browser) {
|
|
4208
|
+
await this.browser.close();
|
|
4209
|
+
this.browser = null;
|
|
4210
|
+
this.initialized = false;
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
// =============================================================================
|
|
4214
|
+
// PIXEL ANALYSIS METHODS
|
|
4215
|
+
// =============================================================================
|
|
4216
|
+
/**
|
|
4217
|
+
* Measure whitespace percentage from screenshot.
|
|
4218
|
+
* Uses actual pixel analysis.
|
|
4219
|
+
*/
|
|
4220
|
+
async measureWhitespace(screenshot) {
|
|
4221
|
+
try {
|
|
4222
|
+
const sharp = await import("sharp");
|
|
4223
|
+
const image = sharp.default(screenshot);
|
|
4224
|
+
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
|
4225
|
+
const totalPixels = info.width * info.height;
|
|
4226
|
+
let backgroundPixels = 0;
|
|
4227
|
+
const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
|
|
4228
|
+
for (let i = 0; i < data.length; i += info.channels) {
|
|
4229
|
+
const r = data[i];
|
|
4230
|
+
const g = data[i + 1];
|
|
4231
|
+
const b = data[i + 2];
|
|
4232
|
+
if (this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 30)) {
|
|
4233
|
+
backgroundPixels++;
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
return backgroundPixels / totalPixels;
|
|
4237
|
+
} catch {
|
|
4238
|
+
return 0.4;
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
/**
|
|
4242
|
+
* Measure color contrast ratio.
|
|
4243
|
+
*/
|
|
4244
|
+
async measureContrast(screenshot) {
|
|
4245
|
+
try {
|
|
4246
|
+
const sharp = await import("sharp");
|
|
4247
|
+
const image = sharp.default(screenshot);
|
|
4248
|
+
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
|
4249
|
+
const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
|
|
4250
|
+
const textColor = this.detectTextColor(data, info.width, info.height, info.channels, bgColor);
|
|
4251
|
+
const bgLuminance = this.relativeLuminance(bgColor.r, bgColor.g, bgColor.b);
|
|
4252
|
+
const textLuminance = this.relativeLuminance(textColor.r, textColor.g, textColor.b);
|
|
4253
|
+
const lighter = Math.max(bgLuminance, textLuminance);
|
|
4254
|
+
const darker = Math.min(bgLuminance, textLuminance);
|
|
4255
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
4256
|
+
} catch {
|
|
4257
|
+
return 7;
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
/**
|
|
4261
|
+
* Measure text density (approximation based on non-background pixels).
|
|
4262
|
+
*/
|
|
4263
|
+
async measureTextDensity(screenshot) {
|
|
4264
|
+
try {
|
|
4265
|
+
const sharp = await import("sharp");
|
|
4266
|
+
const image = sharp.default(screenshot);
|
|
4267
|
+
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
|
4268
|
+
const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
|
|
4269
|
+
let contentPixels = 0;
|
|
4270
|
+
const totalPixels = info.width * info.height;
|
|
4271
|
+
for (let i = 0; i < data.length; i += info.channels) {
|
|
4272
|
+
const r = data[i];
|
|
4273
|
+
const g = data[i + 1];
|
|
4274
|
+
const b = data[i + 2];
|
|
4275
|
+
if (!this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 30)) {
|
|
4276
|
+
contentPixels++;
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
return contentPixels / totalPixels;
|
|
4280
|
+
} catch {
|
|
4281
|
+
return 0.2;
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
/**
|
|
4285
|
+
* Measure visual balance (how evenly distributed is the content).
|
|
4286
|
+
* Uses a 3x3 grid to properly detect centered layouts (common in keynotes).
|
|
4287
|
+
*/
|
|
4288
|
+
async measureBalance(screenshot) {
|
|
4289
|
+
try {
|
|
4290
|
+
const sharp = await import("sharp");
|
|
4291
|
+
const image = sharp.default(screenshot);
|
|
4292
|
+
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
|
4293
|
+
const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
|
|
4294
|
+
const grid = [0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
4295
|
+
const thirdW = Math.floor(info.width / 3);
|
|
4296
|
+
const thirdH = Math.floor(info.height / 3);
|
|
4297
|
+
for (let y = 0; y < info.height; y++) {
|
|
4298
|
+
for (let x = 0; x < info.width; x++) {
|
|
4299
|
+
const i = (y * info.width + x) * info.channels;
|
|
4300
|
+
const r = data[i];
|
|
4301
|
+
const g = data[i + 1];
|
|
4302
|
+
const b = data[i + 2];
|
|
4303
|
+
if (!this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 30)) {
|
|
4304
|
+
const col = x < thirdW ? 0 : x < thirdW * 2 ? 1 : 2;
|
|
4305
|
+
const row = y < thirdH ? 0 : y < thirdH * 2 ? 1 : 2;
|
|
4306
|
+
const cellIndex = row * 3 + col;
|
|
4307
|
+
grid[cellIndex] = (grid[cellIndex] ?? 0) + 1;
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
const total = grid.reduce((a, b) => a + b, 0);
|
|
4312
|
+
if (total === 0) return 100;
|
|
4313
|
+
const centerColumn = (grid[1] ?? 0) + (grid[4] ?? 0) + (grid[7] ?? 0);
|
|
4314
|
+
const leftColumn = (grid[0] ?? 0) + (grid[3] ?? 0) + (grid[6] ?? 0);
|
|
4315
|
+
const rightColumn = (grid[2] ?? 0) + (grid[5] ?? 0) + (grid[8] ?? 0);
|
|
4316
|
+
const centerRatio = centerColumn / total;
|
|
4317
|
+
if (centerRatio > 0.6) {
|
|
4318
|
+
const sideBalance = leftColumn > 0 && rightColumn > 0 ? Math.min(leftColumn, rightColumn) / Math.max(leftColumn, rightColumn) : 1;
|
|
4319
|
+
return Math.round(70 + sideBalance * 30);
|
|
4320
|
+
}
|
|
4321
|
+
const leftTotal = leftColumn + (grid[1] ?? 0) / 2 + (grid[4] ?? 0) / 2 + (grid[7] ?? 0) / 2;
|
|
4322
|
+
const rightTotal = rightColumn + (grid[1] ?? 0) / 2 + (grid[4] ?? 0) / 2 + (grid[7] ?? 0) / 2;
|
|
4323
|
+
const topRow = (grid[0] ?? 0) + (grid[1] ?? 0) + (grid[2] ?? 0);
|
|
4324
|
+
const bottomRow = (grid[6] ?? 0) + (grid[7] ?? 0) + (grid[8] ?? 0);
|
|
4325
|
+
const hBalance = leftTotal > 0 && rightTotal > 0 ? Math.min(leftTotal, rightTotal) / Math.max(leftTotal, rightTotal) : 0.5;
|
|
4326
|
+
const vBalance = topRow > 0 && bottomRow > 0 ? Math.min(topRow, bottomRow) / Math.max(topRow, bottomRow) : 0.5;
|
|
4327
|
+
const score = (hBalance * 0.6 + vBalance * 0.4) * 100;
|
|
4328
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
4329
|
+
} catch {
|
|
4330
|
+
return 75;
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Check if there's clear visual hierarchy.
|
|
4335
|
+
*/
|
|
4336
|
+
async checkHierarchy(screenshot) {
|
|
4337
|
+
try {
|
|
4338
|
+
const sharp = await import("sharp");
|
|
4339
|
+
const image = sharp.default(screenshot);
|
|
4340
|
+
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
|
4341
|
+
const bgColor = this.detectBackgroundColor(data, info.width, info.height, info.channels);
|
|
4342
|
+
const topRegionHeight = Math.floor(info.height * 0.2);
|
|
4343
|
+
let topContentPixels = 0;
|
|
4344
|
+
let bottomContentPixels = 0;
|
|
4345
|
+
for (let y = 0; y < info.height; y++) {
|
|
4346
|
+
for (let x = 0; x < info.width; x++) {
|
|
4347
|
+
const i = (y * info.width + x) * info.channels;
|
|
4348
|
+
const r = data[i];
|
|
4349
|
+
const g = data[i + 1];
|
|
4350
|
+
const b = data[i + 2];
|
|
4351
|
+
if (!this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 30)) {
|
|
4352
|
+
if (y < topRegionHeight) {
|
|
4353
|
+
topContentPixels++;
|
|
4354
|
+
} else {
|
|
4355
|
+
bottomContentPixels++;
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
const topDensity = topContentPixels / (info.width * topRegionHeight);
|
|
4361
|
+
const bottomDensity = bottomContentPixels / (info.width * (info.height - topRegionHeight));
|
|
4362
|
+
return topDensity > 0.01 && topDensity < 0.3;
|
|
4363
|
+
} catch {
|
|
4364
|
+
return true;
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
// =============================================================================
|
|
4368
|
+
// HELPER METHODS
|
|
4369
|
+
// =============================================================================
|
|
4370
|
+
getTypeThresholds(type) {
|
|
4371
|
+
const thresholds = {
|
|
4372
|
+
ted_keynote: { minWhitespace: 0.6, maxWordsPerSlide: 15 },
|
|
4373
|
+
sales_pitch: { minWhitespace: 0.5, maxWordsPerSlide: 30 },
|
|
4374
|
+
investor_pitch: { minWhitespace: 0.5, maxWordsPerSlide: 35 },
|
|
4375
|
+
consulting_deck: { minWhitespace: 0.35, maxWordsPerSlide: 70 },
|
|
4376
|
+
investment_banking: { minWhitespace: 0.3, maxWordsPerSlide: 100 },
|
|
4377
|
+
technical_presentation: { minWhitespace: 0.45, maxWordsPerSlide: 45 },
|
|
4378
|
+
all_hands: { minWhitespace: 0.4, maxWordsPerSlide: 50 }
|
|
4379
|
+
};
|
|
4380
|
+
return thresholds[type] || { minWhitespace: 0.4, maxWordsPerSlide: 50 };
|
|
4381
|
+
}
|
|
4382
|
+
detectBackgroundColor(data, width, height, channels) {
|
|
4383
|
+
const colorBuckets = /* @__PURE__ */ new Map();
|
|
4384
|
+
for (let y = 0; y < height; y += 4) {
|
|
4385
|
+
for (let x = 0; x < width; x += 4) {
|
|
4386
|
+
const i = (y * width + x) * channels;
|
|
4387
|
+
const r = data[i];
|
|
4388
|
+
const g = data[i + 1];
|
|
4389
|
+
const b = data[i + 2];
|
|
4390
|
+
const key = `${Math.floor(r / 16)},${Math.floor(g / 16)},${Math.floor(b / 16)}`;
|
|
4391
|
+
const existing = colorBuckets.get(key);
|
|
4392
|
+
if (existing) {
|
|
4393
|
+
existing.count++;
|
|
4394
|
+
} else {
|
|
4395
|
+
colorBuckets.set(key, { r, g, b, count: 1 });
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
let maxCount = 0;
|
|
4400
|
+
let dominantColor = { r: 240, g: 240, b: 240 };
|
|
4401
|
+
for (const color of colorBuckets.values()) {
|
|
4402
|
+
if (color.count > maxCount) {
|
|
4403
|
+
maxCount = color.count;
|
|
4404
|
+
dominantColor = { r: color.r, g: color.g, b: color.b };
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
return dominantColor;
|
|
4408
|
+
}
|
|
4409
|
+
detectTextColor(data, width, height, channels, bgColor) {
|
|
4410
|
+
const bgLuminance = this.relativeLuminance(bgColor.r, bgColor.g, bgColor.b);
|
|
4411
|
+
const isDarkBg = bgLuminance < 0.5;
|
|
4412
|
+
const luminanceBuckets = /* @__PURE__ */ new Map();
|
|
4413
|
+
const startY = Math.floor(height * 0.05);
|
|
4414
|
+
const endY = Math.floor(height * 0.95);
|
|
4415
|
+
const startX = Math.floor(width * 0.05);
|
|
4416
|
+
const endX = Math.floor(width * 0.95);
|
|
4417
|
+
for (let y = startY; y < endY; y += 2) {
|
|
4418
|
+
for (let x = startX; x < endX; x += 2) {
|
|
4419
|
+
const i = (y * width + x) * channels;
|
|
4420
|
+
const r = data[i];
|
|
4421
|
+
const g = data[i + 1];
|
|
4422
|
+
const b = data[i + 2];
|
|
4423
|
+
if (this.isSimilarColor(r, g, b, bgColor.r, bgColor.g, bgColor.b, 25)) {
|
|
4424
|
+
continue;
|
|
4425
|
+
}
|
|
4426
|
+
const lum = this.relativeLuminance(r, g, b);
|
|
4427
|
+
const bucket = Math.floor(lum * 10);
|
|
4428
|
+
const existing = luminanceBuckets.get(bucket);
|
|
4429
|
+
if (existing) {
|
|
4430
|
+
const total = existing.count + 1;
|
|
4431
|
+
existing.r = Math.round((existing.r * existing.count + r) / total);
|
|
4432
|
+
existing.g = Math.round((existing.g * existing.count + g) / total);
|
|
4433
|
+
existing.b = Math.round((existing.b * existing.count + b) / total);
|
|
4434
|
+
existing.count = total;
|
|
4435
|
+
} else {
|
|
4436
|
+
luminanceBuckets.set(bucket, { r, g, b, count: 1 });
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
const totalPixels = Array.from(luminanceBuckets.values()).reduce((sum, b) => sum + b.count, 0);
|
|
4441
|
+
const minCount = Math.max(50, totalPixels * 5e-3);
|
|
4442
|
+
let textColor = isDarkBg ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 };
|
|
4443
|
+
if (isDarkBg) {
|
|
4444
|
+
for (let bucket = 10; bucket >= 0; bucket--) {
|
|
4445
|
+
const colors = luminanceBuckets.get(bucket);
|
|
4446
|
+
if (colors && colors.count >= minCount) {
|
|
4447
|
+
textColor = { r: colors.r, g: colors.g, b: colors.b };
|
|
4448
|
+
break;
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
} else {
|
|
4452
|
+
for (let bucket = 0; bucket <= 10; bucket++) {
|
|
4453
|
+
const colors = luminanceBuckets.get(bucket);
|
|
4454
|
+
if (colors && colors.count >= minCount) {
|
|
4455
|
+
textColor = { r: colors.r, g: colors.g, b: colors.b };
|
|
4456
|
+
break;
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
return textColor;
|
|
4461
|
+
}
|
|
4462
|
+
isSimilarColor(r1, g1, b1, r2, g2, b2, threshold) {
|
|
4463
|
+
return Math.abs(r1 - r2) < threshold && Math.abs(g1 - g2) < threshold && Math.abs(b1 - b2) < threshold;
|
|
4464
|
+
}
|
|
4465
|
+
relativeLuminance(r, g, b) {
|
|
4466
|
+
const sRGB = [r, g, b].map((c) => {
|
|
4467
|
+
c = c / 255;
|
|
4468
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
4469
|
+
});
|
|
4470
|
+
return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];
|
|
4471
|
+
}
|
|
4472
|
+
calculateConsistency(slides) {
|
|
4473
|
+
if (slides.length < 2) return 100;
|
|
4474
|
+
const whitespaces = slides.map((s) => s.whitespacePercentage);
|
|
4475
|
+
const densities = slides.map((s) => s.textDensity);
|
|
4476
|
+
const wsVariance = this.variance(whitespaces);
|
|
4477
|
+
const densityVariance = this.variance(densities);
|
|
4478
|
+
const maxVariance = 0.1;
|
|
4479
|
+
const wsScore = Math.max(0, 100 - wsVariance / maxVariance * 50);
|
|
4480
|
+
const densityScore = Math.max(0, 100 - densityVariance / maxVariance * 50);
|
|
4481
|
+
return Math.round((wsScore + densityScore) / 2);
|
|
4482
|
+
}
|
|
4483
|
+
variance(values) {
|
|
4484
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
4485
|
+
return values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / values.length;
|
|
4486
|
+
}
|
|
4487
|
+
};
|
|
4488
|
+
var instance5 = null;
|
|
4489
|
+
function getVisualQAEngine() {
|
|
4490
|
+
if (!instance5) {
|
|
4491
|
+
instance5 = new VisualQAEngine();
|
|
4492
|
+
}
|
|
4493
|
+
return instance5;
|
|
4494
|
+
}
|
|
4495
|
+
async function initVisualQAEngine() {
|
|
4496
|
+
const engine = getVisualQAEngine();
|
|
4497
|
+
await engine.initialize();
|
|
4498
|
+
return engine;
|
|
3835
4499
|
}
|
|
3836
4500
|
|
|
3837
4501
|
// src/core/PresentationEngineV2.ts
|
|
3838
|
-
import { writeFileSync as
|
|
4502
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
3839
4503
|
var PALETTE_TO_THEME = {
|
|
3840
4504
|
dark_executive: "dark",
|
|
3841
|
-
modern_business: "
|
|
3842
|
-
consulting_classic: "
|
|
3843
|
-
executive_professional: "
|
|
3844
|
-
strategy_growth: "
|
|
4505
|
+
modern_business: "light",
|
|
4506
|
+
consulting_classic: "light",
|
|
4507
|
+
executive_professional: "light",
|
|
4508
|
+
strategy_growth: "light"
|
|
3845
4509
|
};
|
|
3846
4510
|
async function loadDesignSpecsFromKB(kb, type) {
|
|
3847
4511
|
const typeConfig = kb.queryRequired(`presentation_types.${type}`);
|
|
@@ -3852,7 +4516,7 @@ async function loadDesignSpecsFromKB(kb, type) {
|
|
|
3852
4516
|
const typography = typeConfig.value.typography;
|
|
3853
4517
|
const primaryExperts = typeConfig.value.primary_experts;
|
|
3854
4518
|
const colorPalette = typeConfig.value.color_palette || "consulting_classic";
|
|
3855
|
-
const theme = PALETTE_TO_THEME[colorPalette] || "
|
|
4519
|
+
const theme = PALETTE_TO_THEME[colorPalette] || "light";
|
|
3856
4520
|
const scoringWeights = typeConfig.value.scoring_weights;
|
|
3857
4521
|
let structure = "General presentation structure";
|
|
3858
4522
|
if (primaryExperts.some((e) => e.includes("Minto") || e.includes("McKinsey"))) {
|
|
@@ -3896,6 +4560,80 @@ function getQualityCriteria(specs, type) {
|
|
|
3896
4560
|
requireDataSources: specs.sourcesRequired
|
|
3897
4561
|
};
|
|
3898
4562
|
}
|
|
4563
|
+
function convertV1SlideToV2(slide) {
|
|
4564
|
+
const data = slide.data;
|
|
4565
|
+
const typeMap = {
|
|
4566
|
+
title: "title",
|
|
4567
|
+
title_impact: "title",
|
|
4568
|
+
agenda: "bullets",
|
|
4569
|
+
section_divider: "section",
|
|
4570
|
+
thank_you: "thank_you",
|
|
4571
|
+
single_statement: "statement",
|
|
4572
|
+
big_idea: "statement",
|
|
4573
|
+
big_number: "metrics",
|
|
4574
|
+
quote: "statement",
|
|
4575
|
+
star_moment: "statement",
|
|
4576
|
+
call_to_action: "call_to_action",
|
|
4577
|
+
three_points: "bullets",
|
|
4578
|
+
bullet_points: "bullets",
|
|
4579
|
+
two_column: "bullets",
|
|
4580
|
+
comparison: "bullets",
|
|
4581
|
+
metrics_grid: "metrics",
|
|
4582
|
+
data_insight: "metrics",
|
|
4583
|
+
problem_statement: "statement",
|
|
4584
|
+
solution_overview: "bullets",
|
|
4585
|
+
recommendation: "bullets",
|
|
4586
|
+
next_steps: "bullets"
|
|
4587
|
+
};
|
|
4588
|
+
const v2Type = typeMap[slide.type] || "bullets";
|
|
4589
|
+
const content = {};
|
|
4590
|
+
if (data.bullets && data.bullets.length > 0) {
|
|
4591
|
+
content.bullets = data.bullets;
|
|
4592
|
+
}
|
|
4593
|
+
if (data.metrics && data.metrics.length > 0) {
|
|
4594
|
+
content.metrics = data.metrics.map((m) => {
|
|
4595
|
+
const metric = {
|
|
4596
|
+
value: String(m.value),
|
|
4597
|
+
label: m.label
|
|
4598
|
+
};
|
|
4599
|
+
if (m.trend) {
|
|
4600
|
+
metric.trend = m.trend;
|
|
4601
|
+
}
|
|
4602
|
+
return metric;
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
if (v2Type === "statement") {
|
|
4606
|
+
const statement = data.keyMessage || data.quote || data.body || "";
|
|
4607
|
+
if (statement) {
|
|
4608
|
+
content.statement = statement;
|
|
4609
|
+
}
|
|
4610
|
+
if (data.attribution) {
|
|
4611
|
+
content.subtext = data.attribution;
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
if (data.body && v2Type !== "statement") {
|
|
4615
|
+
content.body = data.body;
|
|
4616
|
+
}
|
|
4617
|
+
if (data.source) {
|
|
4618
|
+
content.source = data.source;
|
|
4619
|
+
}
|
|
4620
|
+
if (data.subtitle) {
|
|
4621
|
+
content.subtext = data.subtitle;
|
|
4622
|
+
}
|
|
4623
|
+
const v2Slide = {
|
|
4624
|
+
index: slide.index,
|
|
4625
|
+
type: v2Type,
|
|
4626
|
+
title: data.title || "",
|
|
4627
|
+
content
|
|
4628
|
+
};
|
|
4629
|
+
if (slide.notes) {
|
|
4630
|
+
v2Slide.notes = slide.notes;
|
|
4631
|
+
}
|
|
4632
|
+
return v2Slide;
|
|
4633
|
+
}
|
|
4634
|
+
function convertV1SlidesToV2(slides) {
|
|
4635
|
+
return slides.map(convertV1SlideToV2);
|
|
4636
|
+
}
|
|
3899
4637
|
var PresentationEngineV2 = class {
|
|
3900
4638
|
options;
|
|
3901
4639
|
kb = null;
|
|
@@ -3906,7 +4644,10 @@ var PresentationEngineV2 = class {
|
|
|
3906
4644
|
imageApiKey: options.imageApiKey || process.env.GEMINI_API_KEY || process.env.OPENAI_API_KEY || "",
|
|
3907
4645
|
minScore: options.minScore ?? 95,
|
|
3908
4646
|
verbose: options.verbose ?? false,
|
|
3909
|
-
maxRemediationAttempts: options.maxRemediationAttempts ?? 3
|
|
4647
|
+
maxRemediationAttempts: options.maxRemediationAttempts ?? 3,
|
|
4648
|
+
runVisualQA: options.runVisualQA ?? false,
|
|
4649
|
+
useRichPipeline: options.useRichPipeline ?? true
|
|
4650
|
+
// Default to V1's rich pipeline
|
|
3910
4651
|
};
|
|
3911
4652
|
}
|
|
3912
4653
|
log(message) {
|
|
@@ -3938,8 +4679,21 @@ var PresentationEngineV2 = class {
|
|
|
3938
4679
|
this.log(` Words/slide: ${designSpecs.wordsPerSlide.min}-${designSpecs.wordsPerSlide.max}`);
|
|
3939
4680
|
this.log(` Experts: ${designSpecs.experts.slice(0, 2).join(", ")}...`);
|
|
3940
4681
|
this.log("Step 3: Generating slides...");
|
|
3941
|
-
|
|
3942
|
-
|
|
4682
|
+
let slides;
|
|
4683
|
+
if (this.options.useRichPipeline) {
|
|
4684
|
+
this.log(" Using V1 rich pipeline...");
|
|
4685
|
+
const contentAnalyzer = await initContentAnalyzer();
|
|
4686
|
+
const slideGenerator = await initSlideGenerator();
|
|
4687
|
+
const analysis = await contentAnalyzer.analyze(markdown, "markdown");
|
|
4688
|
+
this.log(` Content analysis: ${analysis.sections.length} sections, ${analysis.dataPoints.length} data points`);
|
|
4689
|
+
const v1Slides = await slideGenerator.generate(analysis, presentationType);
|
|
4690
|
+
this.log(` V1 generated ${v1Slides.length} slides`);
|
|
4691
|
+
slides = convertV1SlidesToV2(v1Slides);
|
|
4692
|
+
} else {
|
|
4693
|
+
this.log(" Using V2 minimalist pipeline...");
|
|
4694
|
+
const generator = createSlideGeneratorV2(presentationType);
|
|
4695
|
+
slides = generator.generate(markdown, title);
|
|
4696
|
+
}
|
|
3943
4697
|
this.log(` Generated ${slides.length} slides`);
|
|
3944
4698
|
if (!this.options.imageApiKey) {
|
|
3945
4699
|
const shouldUseImages = this.shouldRecommendImages(slides, presentationType);
|
|
@@ -3989,12 +4743,31 @@ Summary: ${review.summary}`
|
|
|
3989
4743
|
);
|
|
3990
4744
|
}
|
|
3991
4745
|
this.log("Step 7: Rendering output...");
|
|
3992
|
-
const renderer = createRendererV2(presentationType, this.
|
|
4746
|
+
const renderer = createRendererV2(presentationType, this.kb);
|
|
3993
4747
|
const html = renderer.render(slides, title || "Presentation");
|
|
4748
|
+
let visualQA;
|
|
4749
|
+
if (this.options.runVisualQA) {
|
|
4750
|
+
this.log("Step 8: Running Visual QA analysis...");
|
|
4751
|
+
try {
|
|
4752
|
+
const visualQAEngine = await initVisualQAEngine();
|
|
4753
|
+
visualQA = await visualQAEngine.analyzeDeck(html, slides, presentationType);
|
|
4754
|
+
await visualQAEngine.cleanup();
|
|
4755
|
+
if (!visualQA.passed) {
|
|
4756
|
+
warnings.push(`Visual QA detected issues: ${visualQA.summary}`);
|
|
4757
|
+
this.log(`Visual QA: ${visualQA.overallScore}/100 - ${visualQA.deckIssues.length} issues found`);
|
|
4758
|
+
} else {
|
|
4759
|
+
this.log(`Visual QA PASSED: ${visualQA.overallScore}/100`);
|
|
4760
|
+
}
|
|
4761
|
+
} catch (error) {
|
|
4762
|
+
const errMsg = error instanceof Error ? error.message : "Unknown error";
|
|
4763
|
+
warnings.push(`Visual QA failed: ${errMsg}`);
|
|
4764
|
+
this.log(`Visual QA error: ${errMsg}`);
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
3994
4767
|
this.log(`
|
|
3995
4768
|
SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
|
|
3996
4769
|
this.log(`Flow: ${review.flow}`);
|
|
3997
|
-
|
|
4770
|
+
const result = {
|
|
3998
4771
|
slides,
|
|
3999
4772
|
html,
|
|
4000
4773
|
presentationType,
|
|
@@ -4002,6 +4775,10 @@ SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
|
|
|
4002
4775
|
review,
|
|
4003
4776
|
warnings
|
|
4004
4777
|
};
|
|
4778
|
+
if (visualQA) {
|
|
4779
|
+
result.visualQA = visualQA;
|
|
4780
|
+
}
|
|
4781
|
+
return result;
|
|
4005
4782
|
}
|
|
4006
4783
|
/**
|
|
4007
4784
|
* Detect presentation type from content analysis.
|
|
@@ -4096,7 +4873,7 @@ SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
|
|
|
4096
4873
|
const issues = [];
|
|
4097
4874
|
const suggestions = [];
|
|
4098
4875
|
let score = 100;
|
|
4099
|
-
if (slide.type === "title" || slide.type === "thank_you") {
|
|
4876
|
+
if (slide.type === "title" || slide.type === "thank_you" || slide.type === "section") {
|
|
4100
4877
|
return {
|
|
4101
4878
|
slideIndex: index,
|
|
4102
4879
|
title: slide.title,
|
|
@@ -4154,7 +4931,7 @@ SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
|
|
|
4154
4931
|
score -= 10;
|
|
4155
4932
|
}
|
|
4156
4933
|
}
|
|
4157
|
-
const hasContent = slide.content.bullets && slide.content.bullets.length > 0 || slide.content.table || slide.content.statement || slide.content.body;
|
|
4934
|
+
const hasContent = slide.content.bullets && slide.content.bullets.length > 0 || slide.content.table || slide.content.metrics && slide.content.metrics.length > 0 || slide.content.statement || slide.content.body;
|
|
4158
4935
|
if (!hasContent) {
|
|
4159
4936
|
issues.push("Slide has no content");
|
|
4160
4937
|
score -= 30;
|
|
@@ -4337,7 +5114,7 @@ async function generatePresentation(markdown, options) {
|
|
|
4337
5114
|
const result = await engine.generate(markdown, options?.title);
|
|
4338
5115
|
if (options?.outputPath) {
|
|
4339
5116
|
const htmlPath = options.outputPath.replace(/\.[^.]+$/, "") + ".html";
|
|
4340
|
-
|
|
5117
|
+
writeFileSync3(htmlPath, result.html);
|
|
4341
5118
|
console.log(`
|
|
4342
5119
|
Output: ${htmlPath}`);
|
|
4343
5120
|
}
|
|
@@ -5081,12 +5858,12 @@ var SlideQualityReviewer = class {
|
|
|
5081
5858
|
};
|
|
5082
5859
|
}
|
|
5083
5860
|
};
|
|
5084
|
-
var
|
|
5861
|
+
var instance6 = null;
|
|
5085
5862
|
function getSlideQualityReviewer() {
|
|
5086
|
-
if (!
|
|
5087
|
-
|
|
5863
|
+
if (!instance6) {
|
|
5864
|
+
instance6 = new SlideQualityReviewer();
|
|
5088
5865
|
}
|
|
5089
|
-
return
|
|
5866
|
+
return instance6;
|
|
5090
5867
|
}
|
|
5091
5868
|
async function initSlideQualityReviewer() {
|
|
5092
5869
|
const reviewer = getSlideQualityReviewer();
|
|
@@ -5616,12 +6393,12 @@ var DeckQualityReviewer = class {
|
|
|
5616
6393
|
return Math.max(0, 100 - deduction);
|
|
5617
6394
|
}
|
|
5618
6395
|
};
|
|
5619
|
-
var
|
|
6396
|
+
var instance7 = null;
|
|
5620
6397
|
function getDeckQualityReviewer() {
|
|
5621
|
-
if (!
|
|
5622
|
-
|
|
6398
|
+
if (!instance7) {
|
|
6399
|
+
instance7 = new DeckQualityReviewer();
|
|
5623
6400
|
}
|
|
5624
|
-
return
|
|
6401
|
+
return instance7;
|
|
5625
6402
|
}
|
|
5626
6403
|
async function initDeckQualityReviewer() {
|
|
5627
6404
|
const reviewer = getDeckQualityReviewer();
|
|
@@ -5926,12 +6703,12 @@ var Remediator = class {
|
|
|
5926
6703
|
return text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
5927
6704
|
}
|
|
5928
6705
|
};
|
|
5929
|
-
var
|
|
6706
|
+
var instance8 = null;
|
|
5930
6707
|
function getRemediator() {
|
|
5931
|
-
if (!
|
|
5932
|
-
|
|
6708
|
+
if (!instance8) {
|
|
6709
|
+
instance8 = new Remediator();
|
|
5933
6710
|
}
|
|
5934
|
-
return
|
|
6711
|
+
return instance8;
|
|
5935
6712
|
}
|
|
5936
6713
|
async function initRemediator() {
|
|
5937
6714
|
const remediator = getRemediator();
|
|
@@ -6903,12 +7680,12 @@ ${content}
|
|
|
6903
7680
|
return html;
|
|
6904
7681
|
}
|
|
6905
7682
|
};
|
|
6906
|
-
var
|
|
7683
|
+
var instance9 = null;
|
|
6907
7684
|
function getRenderer() {
|
|
6908
|
-
if (!
|
|
6909
|
-
|
|
7685
|
+
if (!instance9) {
|
|
7686
|
+
instance9 = new Renderer();
|
|
6910
7687
|
}
|
|
6911
|
-
return
|
|
7688
|
+
return instance9;
|
|
6912
7689
|
}
|
|
6913
7690
|
async function initRenderer() {
|
|
6914
7691
|
const renderer = getRenderer();
|
|
@@ -7778,7 +8555,7 @@ if (typeof process !== "undefined" && process.argv[1]?.includes("CodeQualityVali
|
|
|
7778
8555
|
}
|
|
7779
8556
|
|
|
7780
8557
|
// src/index.ts
|
|
7781
|
-
var VERSION = "4.
|
|
8558
|
+
var VERSION = "4.2.0";
|
|
7782
8559
|
async function generate(options) {
|
|
7783
8560
|
const {
|
|
7784
8561
|
content,
|
|
@@ -7965,6 +8742,7 @@ export {
|
|
|
7965
8742
|
SlideQualityReviewer,
|
|
7966
8743
|
VERSION,
|
|
7967
8744
|
VisualDesignSystem,
|
|
8745
|
+
VisualQAEngine,
|
|
7968
8746
|
createNanoBananaProvider,
|
|
7969
8747
|
createPresentationEngineV2,
|
|
7970
8748
|
createRendererV2,
|
|
@@ -7980,6 +8758,7 @@ export {
|
|
|
7980
8758
|
getSlideGenerator,
|
|
7981
8759
|
getSlideQualityReviewer,
|
|
7982
8760
|
getVisualDesignSystem,
|
|
8761
|
+
getVisualQAEngine,
|
|
7983
8762
|
initContentAnalyzer,
|
|
7984
8763
|
initDeckQualityReviewer,
|
|
7985
8764
|
initKB,
|
|
@@ -7988,6 +8767,7 @@ export {
|
|
|
7988
8767
|
initSlideGenerator,
|
|
7989
8768
|
initSlideQualityReviewer,
|
|
7990
8769
|
initVisualDesignSystem,
|
|
8770
|
+
initVisualQAEngine,
|
|
7991
8771
|
runCodeQualityCheck,
|
|
7992
8772
|
validateCodeQuality
|
|
7993
8773
|
};
|