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.d.mts +172 -6
- package/dist/index.d.ts +172 -6
- package/dist/index.js +815 -88
- package/dist/index.mjs +748 -24
- package/package.json +5 -3
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
|
|
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
|
-
|
|
3998
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5861
|
+
var instance6 = null;
|
|
5141
5862
|
function getSlideQualityReviewer() {
|
|
5142
|
-
if (!
|
|
5143
|
-
|
|
5863
|
+
if (!instance6) {
|
|
5864
|
+
instance6 = new SlideQualityReviewer();
|
|
5144
5865
|
}
|
|
5145
|
-
return
|
|
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
|
|
6396
|
+
var instance7 = null;
|
|
5676
6397
|
function getDeckQualityReviewer() {
|
|
5677
|
-
if (!
|
|
5678
|
-
|
|
6398
|
+
if (!instance7) {
|
|
6399
|
+
instance7 = new DeckQualityReviewer();
|
|
5679
6400
|
}
|
|
5680
|
-
return
|
|
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
|
|
6706
|
+
var instance8 = null;
|
|
5986
6707
|
function getRemediator() {
|
|
5987
|
-
if (!
|
|
5988
|
-
|
|
6708
|
+
if (!instance8) {
|
|
6709
|
+
instance8 = new Remediator();
|
|
5989
6710
|
}
|
|
5990
|
-
return
|
|
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
|
|
7683
|
+
var instance9 = null;
|
|
6963
7684
|
function getRenderer() {
|
|
6964
|
-
if (!
|
|
6965
|
-
|
|
7685
|
+
if (!instance9) {
|
|
7686
|
+
instance9 = new Renderer();
|
|
6966
7687
|
}
|
|
6967
|
-
return
|
|
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
|
};
|