claude-presentation-master 4.2.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/dist/index.mjs CHANGED
@@ -3382,6 +3382,7 @@ ${content}
3382
3382
  --accent-red: #dc3545;
3383
3383
  --border-color: ${isLight ? "#dee2e6" : "rgba(255,255,255,0.1)"};
3384
3384
  --header-bar: ${p.primary};
3385
+ --mckinsey-blue: ${p.accent};
3385
3386
  }`;
3386
3387
  }
3387
3388
  /**
@@ -3890,8 +3891,615 @@ function createRendererV2(presentationType = "consulting_deck", kb) {
3890
3891
  return new RendererV2(presentationType, kb);
3891
3892
  }
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;
4499
+ }
4500
+
3893
4501
  // src/core/PresentationEngineV2.ts
3894
- import { writeFileSync as writeFileSync2 } from "fs";
4502
+ import { writeFileSync as writeFileSync3 } from "fs";
3895
4503
  var PALETTE_TO_THEME = {
3896
4504
  dark_executive: "dark",
3897
4505
  modern_business: "light",
@@ -3952,6 +4560,80 @@ function getQualityCriteria(specs, type) {
3952
4560
  requireDataSources: specs.sourcesRequired
3953
4561
  };
3954
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
+ }
3955
4637
  var PresentationEngineV2 = class {
3956
4638
  options;
3957
4639
  kb = null;
@@ -3962,7 +4644,10 @@ var PresentationEngineV2 = class {
3962
4644
  imageApiKey: options.imageApiKey || process.env.GEMINI_API_KEY || process.env.OPENAI_API_KEY || "",
3963
4645
  minScore: options.minScore ?? 95,
3964
4646
  verbose: options.verbose ?? false,
3965
- 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
3966
4651
  };
3967
4652
  }
3968
4653
  log(message) {
@@ -3994,8 +4679,21 @@ var PresentationEngineV2 = class {
3994
4679
  this.log(` Words/slide: ${designSpecs.wordsPerSlide.min}-${designSpecs.wordsPerSlide.max}`);
3995
4680
  this.log(` Experts: ${designSpecs.experts.slice(0, 2).join(", ")}...`);
3996
4681
  this.log("Step 3: Generating slides...");
3997
- const generator = createSlideGeneratorV2(presentationType);
3998
- let slides = generator.generate(markdown, title);
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
+ }
3999
4697
  this.log(` Generated ${slides.length} slides`);
4000
4698
  if (!this.options.imageApiKey) {
4001
4699
  const shouldUseImages = this.shouldRecommendImages(slides, presentationType);
@@ -4047,10 +4745,29 @@ Summary: ${review.summary}`
4047
4745
  this.log("Step 7: Rendering output...");
4048
4746
  const renderer = createRendererV2(presentationType, this.kb);
4049
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
+ }
4050
4767
  this.log(`
4051
4768
  SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
4052
4769
  this.log(`Flow: ${review.flow}`);
4053
- return {
4770
+ const result = {
4054
4771
  slides,
4055
4772
  html,
4056
4773
  presentationType,
@@ -4058,6 +4775,10 @@ SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
4058
4775
  review,
4059
4776
  warnings
4060
4777
  };
4778
+ if (visualQA) {
4779
+ result.visualQA = visualQA;
4780
+ }
4781
+ return result;
4061
4782
  }
4062
4783
  /**
4063
4784
  * Detect presentation type from content analysis.
@@ -4152,7 +4873,7 @@ SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
4152
4873
  const issues = [];
4153
4874
  const suggestions = [];
4154
4875
  let score = 100;
4155
- if (slide.type === "title" || slide.type === "thank_you") {
4876
+ if (slide.type === "title" || slide.type === "thank_you" || slide.type === "section") {
4156
4877
  return {
4157
4878
  slideIndex: index,
4158
4879
  title: slide.title,
@@ -4210,7 +4931,7 @@ SUCCESS: Generated ${slides.length} slides scoring ${review.overallScore}/100`);
4210
4931
  score -= 10;
4211
4932
  }
4212
4933
  }
4213
- 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;
4214
4935
  if (!hasContent) {
4215
4936
  issues.push("Slide has no content");
4216
4937
  score -= 30;
@@ -4393,7 +5114,7 @@ async function generatePresentation(markdown, options) {
4393
5114
  const result = await engine.generate(markdown, options?.title);
4394
5115
  if (options?.outputPath) {
4395
5116
  const htmlPath = options.outputPath.replace(/\.[^.]+$/, "") + ".html";
4396
- writeFileSync2(htmlPath, result.html);
5117
+ writeFileSync3(htmlPath, result.html);
4397
5118
  console.log(`
4398
5119
  Output: ${htmlPath}`);
4399
5120
  }
@@ -5137,12 +5858,12 @@ var SlideQualityReviewer = class {
5137
5858
  };
5138
5859
  }
5139
5860
  };
5140
- var instance5 = null;
5861
+ var instance6 = null;
5141
5862
  function getSlideQualityReviewer() {
5142
- if (!instance5) {
5143
- instance5 = new SlideQualityReviewer();
5863
+ if (!instance6) {
5864
+ instance6 = new SlideQualityReviewer();
5144
5865
  }
5145
- return instance5;
5866
+ return instance6;
5146
5867
  }
5147
5868
  async function initSlideQualityReviewer() {
5148
5869
  const reviewer = getSlideQualityReviewer();
@@ -5672,12 +6393,12 @@ var DeckQualityReviewer = class {
5672
6393
  return Math.max(0, 100 - deduction);
5673
6394
  }
5674
6395
  };
5675
- var instance6 = null;
6396
+ var instance7 = null;
5676
6397
  function getDeckQualityReviewer() {
5677
- if (!instance6) {
5678
- instance6 = new DeckQualityReviewer();
6398
+ if (!instance7) {
6399
+ instance7 = new DeckQualityReviewer();
5679
6400
  }
5680
- return instance6;
6401
+ return instance7;
5681
6402
  }
5682
6403
  async function initDeckQualityReviewer() {
5683
6404
  const reviewer = getDeckQualityReviewer();
@@ -5982,12 +6703,12 @@ var Remediator = class {
5982
6703
  return text.split(/\s+/).filter((w) => w.length > 0).length;
5983
6704
  }
5984
6705
  };
5985
- var instance7 = null;
6706
+ var instance8 = null;
5986
6707
  function getRemediator() {
5987
- if (!instance7) {
5988
- instance7 = new Remediator();
6708
+ if (!instance8) {
6709
+ instance8 = new Remediator();
5989
6710
  }
5990
- return instance7;
6711
+ return instance8;
5991
6712
  }
5992
6713
  async function initRemediator() {
5993
6714
  const remediator = getRemediator();
@@ -6959,12 +7680,12 @@ ${content}
6959
7680
  return html;
6960
7681
  }
6961
7682
  };
6962
- var instance8 = null;
7683
+ var instance9 = null;
6963
7684
  function getRenderer() {
6964
- if (!instance8) {
6965
- instance8 = new Renderer();
7685
+ if (!instance9) {
7686
+ instance9 = new Renderer();
6966
7687
  }
6967
- return instance8;
7688
+ return instance9;
6968
7689
  }
6969
7690
  async function initRenderer() {
6970
7691
  const renderer = getRenderer();
@@ -8021,6 +8742,7 @@ export {
8021
8742
  SlideQualityReviewer,
8022
8743
  VERSION,
8023
8744
  VisualDesignSystem,
8745
+ VisualQAEngine,
8024
8746
  createNanoBananaProvider,
8025
8747
  createPresentationEngineV2,
8026
8748
  createRendererV2,
@@ -8036,6 +8758,7 @@ export {
8036
8758
  getSlideGenerator,
8037
8759
  getSlideQualityReviewer,
8038
8760
  getVisualDesignSystem,
8761
+ getVisualQAEngine,
8039
8762
  initContentAnalyzer,
8040
8763
  initDeckQualityReviewer,
8041
8764
  initKB,
@@ -8044,6 +8767,7 @@ export {
8044
8767
  initSlideGenerator,
8045
8768
  initSlideQualityReviewer,
8046
8769
  initVisualDesignSystem,
8770
+ initVisualQAEngine,
8047
8771
  runCodeQualityCheck,
8048
8772
  validateCodeQuality
8049
8773
  };