hanseol-dev 5.0.2-dev.151 → 5.0.2-dev.152

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.
@@ -1 +1 @@
1
- {"version":3,"file":"powerpoint-create-agent.d.ts","sourceRoot":"","sources":["../../../src/agents/office/powerpoint-create-agent.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,YAAY,EAAc,MAAM,sBAAsB,CAAC;AAwrDhE,wBAAgB,iCAAiC,IAAI,YAAY,CAkChE"}
1
+ {"version":3,"file":"powerpoint-create-agent.d.ts","sourceRoot":"","sources":["../../../src/agents/office/powerpoint-create-agent.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,YAAY,EAAc,MAAM,sBAAsB,CAAC;AA6vChE,wBAAgB,iCAAiC,IAAI,YAAY,CAkChE"}
@@ -4,22 +4,7 @@ import { powerpointClient } from '../../tools/office/powerpoint-client.js';
4
4
  import { getSubAgentPhaseLogger, getSubAgentToolCallLogger } from '../common/sub-agent.js';
5
5
  import { logger } from '../../utils/logger.js';
6
6
  import { getPlatform } from '../../utils/platform-utils.js';
7
- import { PPT_CREATE_ENHANCEMENT_PROMPT, PPT_STRUCTURED_PLANNING_PROMPT, buildSlideHtmlPrompt, extractLayoutHint, checkLayoutCompliance, postProcessSlideHtml, buildLayoutSetPrompt, parseLayoutSet, buildAllSkeletonsPrompt, parseSkeletons, buildFillPrompt, } from './powerpoint-create-prompts.js';
8
- function getUnderfillFixByLayout(layoutType) {
9
- const fixes = {
10
- cards: 'ADD MORE CONTENT to cards:\n- Each card needs 4-5 bullet points (not 2-3)\n- Add a 2-sentence descriptive paragraph under each heading\n- Add a metric/stat at the bottom of each card\n- Use generous padding: 40px inside cards, 20px between items',
11
- progress_bars: 'ADD MORE BARS and detail:\n- Increase to 5-6 bars (not 3-4)\n- Add a 1-line detail text below each bar explaining context\n- Add an insight summary box at the very bottom\n- Use larger bar height (48px) and label font (28px)',
12
- bar_chart: 'ADD MORE to the chart:\n- Add more bars (5-6 total)\n- Make bars taller (tallest 85%)\n- Add a 2-3 line insight section at the bottom\n- Increase font sizes: values 32px, labels 28px',
13
- donut_chart: 'ADD MORE to donut and legend:\n- Add 4-5 segments with detailed labels\n- Each legend item: name + absolute value + percentage\n- Add a 2-sentence summary paragraph below the legend\n- Make donut larger (450px)',
14
- table: 'ADD MORE ROWS and content:\n- Increase to 6-8 data rows\n- Fill ALL cells with real data\n- Add a summary bar below the table with key insight\n- Use larger padding on td (28-32px)',
15
- timeline: 'ADD MORE DETAIL to milestones:\n- Each milestone: 3-4 sentence description (not 1 line)\n- Add a KPI metric at the bottom of each milestone card\n- Use 3-4 milestone cards (not 2)\n- Add date labels and context',
16
- process_flow: 'ADD MORE DETAIL to steps:\n- Each step: 2-3 sentence description (not 1 line)\n- Add time/metric at the bottom of each step\n- Use step numbers + titles\n- Increase padding for readability',
17
- big_numbers: 'ADD MORE CONTEXT:\n- Each metric: number + unit + label + 2-3 sentence context + trend indicator\n- Ensure 3 metric cards minimum\n- Add supporting text below each number\n- Make numbers larger (80px)',
18
- two_col_split: 'ADD MORE CONTENT:\n- Left column: primary visual with supporting context\n- Right column: 4-5 detailed bullet points\n- Both columns must be content-rich\n- Use generous font sizes (28-30px)',
19
- hero_stat: 'ADD MORE CONTEXT:\n- Central number must be 128px\n- Add 2-3 sentence context paragraph below\n- Add row of 2-3 supporting metrics\n- Include trend indicators and labels',
20
- };
21
- return fixes[layoutType] || fixes['cards'];
22
- }
7
+ import { PPT_CREATE_ENHANCEMENT_PROMPT, PPT_STRUCTURED_PLANNING_PROMPT, buildSlideHtmlPrompt, extractLayoutHint, buildContentFillJsonPrompt, parseContentFillJson, buildContentSlideHtml, } from './powerpoint-create-prompts.js';
23
8
  const DEFAULT_DESIGN = {
24
9
  primary_color: '#1B2A4A',
25
10
  accent_color: '#00D4AA',
@@ -266,64 +251,6 @@ function injectViewportCss(html, backgroundColor) {
266
251
  }
267
252
  return result;
268
253
  }
269
- function enforceMinFontSize(html) {
270
- const MIN_PX = 26;
271
- let result = html.replace(/font-size:\s*(\d+(?:\.\d+)?)px/g, (_match, size) => {
272
- const px = parseFloat(size);
273
- return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
274
- });
275
- result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)pt/g, (_match, size) => {
276
- const pt = parseFloat(size);
277
- const px = pt * 1.333;
278
- return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
279
- });
280
- result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)r?em/g, (_match, size) => {
281
- const em = parseFloat(size);
282
- const px = em * 16;
283
- return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
284
- });
285
- result = result.replace(/font-size:\s*(\d+(?:\.\d+)?)%/g, (_match, size) => {
286
- const pct = parseFloat(size);
287
- const px = pct / 100 * 16;
288
- return (px > 12 && px < MIN_PX) ? `font-size:${MIN_PX}px` : _match;
289
- });
290
- return result;
291
- }
292
- function ensureSafeBodyPadding(html) {
293
- const safeStyle = `<style>body{padding-top:20px!important;padding-bottom:40px!important;min-height:auto!important}body>div:last-child,body>section:last-child{margin-bottom:0!important;padding-bottom:0!important}</style>`;
294
- if (html.includes('</head>')) {
295
- return html.replace('</head>', `${safeStyle}</head>`);
296
- }
297
- return html;
298
- }
299
- function stripGradientTextEffects(html) {
300
- let result = html.replace(/-webkit-text-fill-color:\s*transparent/gi, '-webkit-text-fill-color:initial');
301
- result = result.replace(/background-clip:\s*text/gi, 'background-clip:initial');
302
- result = result.replace(/-webkit-background-clip:\s*text/gi, '-webkit-background-clip:initial');
303
- return result;
304
- }
305
- function removeAbsolutePositioning(html) {
306
- let result = html.replace(/style="([^"]*)position:\s*absolute([^"]*)"/gi, (match, before, after) => {
307
- if (/(?:width|height):\s*[1-5]?\dpx/i.test(before + after) || /opacity:\s*0\.[0-4]/i.test(before + after)) {
308
- return match;
309
- }
310
- let cleaned = `${before}position:static${after}`;
311
- cleaned = cleaned.replace(/(?:^|;)\s*(?:top|left|right|bottom):\s*[^;]+/gi, '');
312
- return `style="${cleaned}"`;
313
- });
314
- result = result.replace(/(<style[^>]*>)([\s\S]*?)(<\/style>)/gi, (_m, open, css, close) => {
315
- let fixed = css.replace(/position:\s*absolute/gi, 'position:static');
316
- fixed = fixed.replace(/position:\s*static([^}]*)/gi, (ruleMatch) => {
317
- return ruleMatch.replace(/(?:^|;)\s*(?:top|left|right|bottom):\s*[^;{}]+/gi, '');
318
- });
319
- return open + fixed + close;
320
- });
321
- result = result.replace(/transform:\s*translate[^;)]*\)/gi, 'transform:none');
322
- result = result.replace(/transform:\s*scale\(\s*0\.\d+\s*\)/gi, 'transform:none');
323
- result = result.replace(/transform:\s*scale\(\s*0\.\d+\s*,\s*0\.\d+\s*\)/gi, 'transform:none');
324
- result = result.replace(/zoom:\s*0\.\d+/gi, 'zoom:1');
325
- return result;
326
- }
327
254
  function injectTitleContrastFix(html, designTextColor) {
328
255
  const safeColor = designTextColor.replace(/'/g, "\\'");
329
256
  const script = `<script>(function(){` +
@@ -350,61 +277,6 @@ function injectTitleContrastFix(html, designTextColor) {
350
277
  }
351
278
  return html + script;
352
279
  }
353
- function injectMeasureCss(html) {
354
- let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
355
- const measureCss = `<style>*{box-sizing:border-box;word-break:keep-all;overflow-wrap:break-word;overflow:visible!important;max-height:none!important}html{width:2040px!important;height:auto!important;min-height:auto!important;margin:0!important}body{width:1920px!important;min-width:1920px!important;height:auto!important;min-height:auto!important;overflow:visible!important;margin:0!important}</style>`;
356
- const injection = measureCss;
357
- if (result.includes('</head>')) {
358
- result = result.replace('</head>', `${injection}</head>`);
359
- }
360
- else if (result.includes('<head>')) {
361
- result = result.replace('<head>', `<head>${injection}`);
362
- }
363
- else if (result.includes('<html')) {
364
- result = result.replace(/<html[^>]*>/, (match) => `${match}<head>${injection}</head>`);
365
- }
366
- else {
367
- result = injection + result;
368
- }
369
- const measureScript = `<script>document.title='SH:'+document.documentElement.scrollHeight</script>`;
370
- if (result.includes('</body>')) {
371
- result = result.replace('</body>', `${measureScript}</body>`);
372
- }
373
- else if (result.includes('</html>')) {
374
- result = result.replace('</html>', `<body>${measureScript}</body></html>`);
375
- }
376
- else {
377
- result += `</style></head><body>${measureScript}</body></html>`;
378
- }
379
- return result;
380
- }
381
- function injectFillMeasureCss(html) {
382
- let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
383
- const fillCss = `<style>*{box-sizing:border-box;word-break:keep-all;overflow-wrap:break-word;min-height:auto!important;height:auto!important}html{width:2040px!important;margin:0!important;height:auto!important}body{width:1920px!important;min-width:1920px!important;margin:0!important;display:block!important;height:auto!important;min-height:auto!important;overflow:visible!important}body>*{flex:none!important}body *{flex-grow:0!important;flex-shrink:1!important;flex-basis:auto!important}</style>`;
384
- const measureScript = `<script>document.title='SH:'+document.documentElement.scrollHeight</script>`;
385
- if (result.includes('</head>')) {
386
- result = result.replace('</head>', `${fillCss}</head>`);
387
- }
388
- else if (result.includes('<head>')) {
389
- result = result.replace('<head>', `<head>${fillCss}`);
390
- }
391
- else if (result.includes('<html')) {
392
- result = result.replace(/<html[^>]*>/, (match) => `${match}<head>${fillCss}</head>`);
393
- }
394
- else {
395
- result = fillCss + result;
396
- }
397
- if (result.includes('</body>')) {
398
- result = result.replace('</body>', `${measureScript}</body>`);
399
- }
400
- else if (result.includes('</html>')) {
401
- result = result.replace('</html>', `<body>${measureScript}</body></html>`);
402
- }
403
- else {
404
- result += `</style></head><body>${measureScript}</body></html>`;
405
- }
406
- return result;
407
- }
408
280
  function escapeHtml(text) {
409
281
  return text
410
282
  .replace(/&/g, '&amp;')
@@ -871,56 +743,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
871
743
  }
872
744
  }
873
745
  }
874
- let layoutTemplates = [];
875
- let slideSkeletons = new Map();
876
- if (plan.design) {
877
- try {
878
- if (phaseLogger)
879
- phaseLogger('powerpoint-create', 'layout-set', 'Generating custom layout components...');
880
- const layoutSetPrompt = buildLayoutSetPrompt(plan.design);
881
- const layoutRes = await llmClient.chatCompletion({
882
- messages: [
883
- { role: 'system', content: layoutSetPrompt },
884
- { role: 'user', content: 'Generate the layout components now. Start with ---LAYOUT: and end each with ---END---.' },
885
- ],
886
- temperature: 0.7,
887
- max_tokens: 6000,
888
- }, { maxRetries: 2 });
889
- const layoutRaw = layoutRes.choices[0]?.message;
890
- const layoutText = layoutRaw ? extractContent(layoutRaw) : '';
891
- layoutTemplates = parseLayoutSet(layoutText);
892
- logger.info(`Layout Set: generated ${layoutTemplates.length} templates: ${layoutTemplates.map(l => l.name).join(', ')}`);
893
- if (phaseLogger)
894
- phaseLogger('powerpoint-create', 'layout-set', `Generated ${layoutTemplates.length} layout templates`);
895
- }
896
- catch (e) {
897
- logger.warn(`Layout Set generation failed, will use fallback: ${e}`);
898
- }
899
- }
900
- if (layoutTemplates.length >= 4) {
901
- try {
902
- if (phaseLogger)
903
- phaseLogger('powerpoint-create', 'skeleton', 'Generating slide skeletons...');
904
- const skeletonPrompt = buildAllSkeletonsPrompt(plan.slides, layoutTemplates);
905
- const skeletonRes = await llmClient.chatCompletion({
906
- messages: [
907
- { role: 'system', content: skeletonPrompt },
908
- { role: 'user', content: 'Generate skeletons for all content slides. Use ===SLIDE:N=== format.' },
909
- ],
910
- temperature: 0.3,
911
- max_tokens: 8000,
912
- }, { maxRetries: 2 });
913
- const skeletonRaw = skeletonRes.choices[0]?.message;
914
- const skeletonText = skeletonRaw ? extractContent(skeletonRaw) : '';
915
- slideSkeletons = parseSkeletons(skeletonText);
916
- logger.info(`Skeletons: generated ${slideSkeletons.size} slide skeletons`);
917
- if (phaseLogger)
918
- phaseLogger('powerpoint-create', 'skeleton', `Generated ${slideSkeletons.size} slide skeletons`);
919
- }
920
- catch (e) {
921
- logger.warn(`Skeleton generation failed, will use fallback: ${e}`);
922
- }
923
- }
924
746
  if (phaseLogger)
925
747
  phaseLogger('powerpoint-create', 'execution', 'Starting HTML rendering pipeline...');
926
748
  const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
@@ -995,7 +817,6 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
995
817
  if (phaseLogger)
996
818
  phaseLogger('powerpoint-create', 'execution', `Rendering slide ${slideNum}/${plan.slides.length}: ${slidePlan.title}`);
997
819
  let html = null;
998
- let htmlPrompt = null;
999
820
  if (slidePlan.type === 'title') {
1000
821
  html = buildTitleSlideHtml(plan.design, companyName, titleSubtitle, kstDate, slideNum);
1001
822
  }
@@ -1011,254 +832,73 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
1011
832
  logger.info(`Slide ${slideNum}: Using code-generated overview template (${overviewItems.length} items)`);
1012
833
  }
1013
834
  }
1014
- let slideLayoutType = 'cards';
1015
- let usedSkeleton = false;
1016
835
  if (!html && slidePlan.type !== 'title' && slidePlan.type !== 'closing') {
1017
836
  const cleanedDirection = (slidePlan.content_direction || '')
1018
837
  .replace(/\s*Layout\s*:\s*[^\n]*/gi, '')
1019
838
  .trim();
1020
- slideLayoutType = extractLayoutHint(slidePlan.content_direction || '');
1021
- const skeletonInfo = slideSkeletons.get(slideNum);
1022
- const matchedLayout = skeletonInfo
1023
- ? layoutTemplates.find(l => l.name === skeletonInfo.layout)
1024
- : null;
1025
- if (skeletonInfo && matchedLayout) {
1026
- usedSkeleton = true;
1027
- logger.info(`Slide ${slideNum}: Using skeleton "${skeletonInfo.layout}" from Phase 4 (layoutType: ${slideLayoutType})`);
1028
- htmlPrompt = buildFillPrompt(skeletonInfo.skeleton, matchedLayout.css, cleanedDirection, slidePlan.title, plan.design, i, plan.slides.length, language, slideLayoutType);
1029
- }
1030
- else {
1031
- logger.info(`Slide ${slideNum}: Fallback to layout-specific prompt → "${slideLayoutType}"`);
1032
- htmlPrompt = buildSlideHtmlPrompt(slidePlan.title, cleanedDirection, plan.design, i, plan.slides.length, language, slideLayoutType);
1033
- }
839
+ const slideLayoutType = extractLayoutHint(slidePlan.content_direction || '');
840
+ const jsonPrompt = buildContentFillJsonPrompt(slidePlan.title, cleanedDirection, slideLayoutType, language);
841
+ let slideData = null;
1034
842
  try {
1035
- const htmlRes = await llmClient.chatCompletion({
843
+ const jsonRes = await llmClient.chatCompletion({
1036
844
  messages: [
1037
- { role: 'system', content: htmlPrompt },
1038
- { role: 'user', content: 'Generate the HTML slide now.' },
845
+ { role: 'system', content: jsonPrompt },
846
+ { role: 'user', content: 'Output the JSON now.' },
1039
847
  ],
1040
848
  temperature: 0.3,
1041
- max_tokens: 8000,
849
+ max_tokens: 2000,
1042
850
  });
1043
- const htmlMsg = htmlRes.choices[0]?.message;
1044
- const rawHtml = htmlMsg ? extractContent(htmlMsg) : '';
1045
- html = extractHtml(rawHtml);
1046
- if (!html && rawHtml.length > 100) {
1047
- logger.warn(`Slide ${slideNum}: HTML extraction failed, retrying`);
851
+ const jsonMsg = jsonRes.choices[0]?.message;
852
+ const jsonRaw = jsonMsg ? extractContent(jsonMsg) : '';
853
+ slideData = parseContentFillJson(jsonRaw, slideLayoutType);
854
+ if (!slideData) {
855
+ logger.warn(`Slide ${slideNum}: JSON parse failed, retrying`);
1048
856
  const retryRes = await llmClient.chatCompletion({
1049
857
  messages: [
1050
- { role: 'system', content: htmlPrompt },
1051
- { role: 'user', content: 'Generate the complete HTML document. Start with <!DOCTYPE html> and end with </html>. No markdown fences.' },
858
+ { role: 'system', content: jsonPrompt },
859
+ { role: 'user', content: 'Output ONLY valid JSON. No markdown fences, no explanation. Start with { and end with }.' },
1052
860
  ],
1053
861
  temperature: 0.2,
1054
- max_tokens: 4000,
862
+ max_tokens: 2000,
1055
863
  });
1056
864
  const retryMsg = retryRes.choices[0]?.message;
1057
865
  const retryRaw = retryMsg ? extractContent(retryMsg) : '';
1058
- html = extractHtml(retryRaw);
866
+ slideData = parseContentFillJson(retryRaw, slideLayoutType);
1059
867
  }
1060
868
  }
1061
869
  catch (e) {
1062
- logger.warn(`Slide ${slideNum}: LLM call failed: ${e}`);
870
+ logger.warn(`Slide ${slideNum}: JSON fill LLM call failed: ${e}`);
1063
871
  }
1064
- }
1065
- if (!html) {
1066
- logger.warn(`Slide ${slideNum}: Failed to generate HTML, skipping`);
1067
- failCount++;
1068
- continue;
1069
- }
1070
- const OVERFLOW_THRESHOLD = 1080;
1071
- if (htmlPrompt) {
1072
- const MAX_REGEN_ATTEMPTS = 2;
1073
- for (let attempt = 0; attempt < MAX_REGEN_ATTEMPTS; attempt++) {
1074
- const measureFileName = `hanseol_measure_${slideNum}_${timestamp}_${attempt}.html`;
1075
- const measureWritePath = path.join(tempWritePath, measureFileName);
1076
- const measureWinPath = `${tempWinPath}\\${measureFileName}`;
1077
- try {
1078
- fs.writeFileSync(measureWritePath, injectMeasureCss(html), 'utf-8');
1079
- const contentHeight = await powerpointClient.measureHtmlHeight(measureWinPath);
1080
- try {
1081
- fs.unlinkSync(measureWritePath);
1082
- }
1083
- catch { }
1084
- if (contentHeight <= OVERFLOW_THRESHOLD) {
1085
- if (attempt > 0)
1086
- logger.info(`Slide ${slideNum}: Fits after ${attempt} regeneration(s) (${contentHeight}px)`);
1087
- break;
1088
- }
1089
- logger.warn(`Slide ${slideNum}: Content overflows (${contentHeight}px > ${OVERFLOW_THRESHOLD}), attempt ${attempt + 1}/${MAX_REGEN_ATTEMPTS}`);
1090
- if (phaseLogger)
1091
- phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: overflow (${contentHeight}px), regen ${attempt + 1}/${MAX_REGEN_ATTEMPTS}...`);
1092
- try {
1093
- const regenRes = await llmClient.chatCompletion({
1094
- messages: [
1095
- { role: 'system', content: htmlPrompt },
1096
- { role: 'user', content: `Generate the HTML slide now. Your previous version was ${contentHeight}px tall — it MUST fit in 1080px (with 60px top+bottom padding = 960px usable). Content was clipped by ${contentHeight - 1080}px.\n\nFix:\n- Reduce content: remove 2-3 least important bullets or rows from each section\n- Compact padding: cards 24px, gaps 12-16px\n- Keep fonts: title 40-48px, body 26px min\n- If 4+ cards/sections → reduce to 3\n- Target: 850-960px content height` },
1097
- ],
1098
- temperature: 0.2,
1099
- max_tokens: 8000,
1100
- });
1101
- const regenMsg = regenRes.choices[0]?.message;
1102
- const regenRaw = regenMsg ? extractContent(regenMsg) : '';
1103
- const regenHtml = extractHtml(regenRaw);
1104
- if (regenHtml) {
1105
- html = regenHtml;
1106
- logger.info(`Slide ${slideNum}: Regenerated HTML (attempt ${attempt + 1})`);
1107
- }
1108
- else {
1109
- break;
1110
- }
1111
- }
1112
- catch (e) {
1113
- logger.warn(`Slide ${slideNum}: Overflow regeneration attempt ${attempt + 1} failed: ${e}`);
1114
- break;
1115
- }
1116
- }
1117
- catch {
1118
- break;
1119
- }
872
+ if (slideData) {
873
+ html = buildContentSlideHtml(plan.design, slidePlan.title, slideLayoutType, slideData, slideNum, i);
874
+ logger.info(`Slide ${slideNum}: Code template "${slideLayoutType}" generated`);
1120
875
  }
1121
- }
1122
- const MIN_FILL_HEIGHT = 850;
1123
- const MAX_FILL_ATTEMPTS = 3;
1124
- if (htmlPrompt && slidePlan.type === 'content') {
1125
- if (phaseLogger)
1126
- phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: starting fill measurement...`);
1127
- for (let fillAttempt = 0; fillAttempt < MAX_FILL_ATTEMPTS; fillAttempt++) {
1128
- const fillFileName = `hanseol_fill_${slideNum}_${timestamp}_${fillAttempt}.html`;
1129
- const fillWritePath = path.join(tempWritePath, fillFileName);
1130
- const fillWinPath = `${tempWinPath}\\${fillFileName}`;
1131
- try {
1132
- fs.writeFileSync(fillWritePath, injectFillMeasureCss(html), 'utf-8');
1133
- const naturalHeight = await powerpointClient.measureHtmlHeight(fillWinPath);
1134
- if (naturalHeight > 0) {
1135
- try {
1136
- fs.unlinkSync(fillWritePath);
1137
- }
1138
- catch { }
1139
- }
1140
- if (phaseLogger)
1141
- phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: fill=${naturalHeight}px (${Math.round(naturalHeight / 1080 * 100)}%), threshold=${MIN_FILL_HEIGHT}px`);
1142
- if (naturalHeight > 0 && naturalHeight < MIN_FILL_HEIGHT) {
1143
- const fillPct = Math.round(naturalHeight / 1080 * 100);
1144
- logger.warn(`Slide ${slideNum}: Underfill — natural height ${naturalHeight}px (${fillPct}%). Attempt ${fillAttempt + 1}/${MAX_FILL_ATTEMPTS}...`);
1145
- if (phaseLogger)
1146
- phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: underfill (${fillPct}%), regen attempt ${fillAttempt + 1}...`);
1147
- try {
1148
- const htmlExcerpt = html.length > 2500 ? html.slice(0, 2500) + '\n...(truncated)' : html;
1149
- const isCatastrophic = fillPct < 50;
1150
- const fillRes = await llmClient.chatCompletion({
1151
- messages: [
1152
- { role: 'system', content: htmlPrompt },
1153
- { role: 'user', content: `${isCatastrophic ? '⚠⚠⚠ CATASTROPHIC FAILURE — COMPLETE REGENERATION REQUIRED ⚠⚠⚠\n\n' : ''}REJECTED HTML (fills only ${fillPct}% — ${naturalHeight}px of 1080px):\n\`\`\`html\n${htmlExcerpt}\n\`\`\`\n\nThis slide was REJECTED because the content is too sparse. ${isCatastrophic ? 'It fills LESS THAN HALF the slide — this is unacceptable.' : ''}\n\nYou MUST regenerate from scratch with SIGNIFICANTLY MORE content:\n1. Each card/section: 4-5 bullet points with specific numbers (not 2 sparse lines)\n2. ALL content elements MUST be DIRECT children of .content — NO wrapper divs\n3. Tables: 6-8 data rows. Progress bars: 5-6 bars. Timeline: 4-5 detailed milestones.\n4. Target content height: ${Math.round(1080 * 0.85)}-${Math.round(1080 * 0.95)}px\n\n${getUnderfillFixByLayout(slideLayoutType)}\n\nDo NOT repeat the same sparse structure. Add REAL detailed content.\nOutput the COMPLETE HTML from <!DOCTYPE html> to </html>.` },
1154
- ],
1155
- temperature: isCatastrophic ? 0.5 : 0.3,
1156
- max_tokens: 8000,
1157
- });
1158
- const fillMsg = fillRes.choices[0]?.message;
1159
- const fillRaw = fillMsg ? extractContent(fillMsg) : '';
1160
- const fillHtml = extractHtml(fillRaw);
1161
- if (fillHtml) {
1162
- const overflowCheckFile = `hanseol_ocheck_${slideNum}_${timestamp}_${fillAttempt}.html`;
1163
- const overflowCheckPath = path.join(tempWritePath, overflowCheckFile);
1164
- const overflowCheckWin = `${tempWinPath}\\${overflowCheckFile}`;
1165
- let overflowOk = true;
1166
- try {
1167
- fs.writeFileSync(overflowCheckPath, injectMeasureCss(fillHtml), 'utf-8');
1168
- const regenHeight = await powerpointClient.measureHtmlHeight(overflowCheckWin);
1169
- try {
1170
- fs.unlinkSync(overflowCheckPath);
1171
- }
1172
- catch { }
1173
- if (regenHeight > OVERFLOW_THRESHOLD) {
1174
- logger.warn(`Slide ${slideNum}: Fill regen overflows (${regenHeight}px), keeping original`);
1175
- overflowOk = false;
1176
- }
1177
- }
1178
- catch {
1179
- try {
1180
- fs.unlinkSync(overflowCheckPath);
1181
- }
1182
- catch { }
1183
- }
1184
- if (!overflowOk)
1185
- break;
1186
- html = fillHtml;
1187
- logger.info(`Slide ${slideNum}: Regenerated with more content (was ${fillPct}% fill, attempt ${fillAttempt + 1})`);
1188
- continue;
1189
- }
1190
- }
1191
- catch (e) {
1192
- logger.warn(`Slide ${slideNum}: Fill regeneration failed: ${e}`);
1193
- }
1194
- break;
1195
- }
1196
- else if (naturalHeight > 0) {
1197
- const fillPct = Math.round(naturalHeight / 1080 * 100);
1198
- logger.info(`Slide ${slideNum}: Fill OK — ${naturalHeight}px (${fillPct}%)`);
1199
- break;
1200
- }
1201
- else {
1202
- if (phaseLogger)
1203
- phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: fill measure returned 0, skipping`);
1204
- break;
1205
- }
1206
- }
1207
- catch (fillErr) {
1208
- if (phaseLogger)
1209
- phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: fill measure FAILED: ${fillErr}`);
1210
- break;
1211
- }
1212
- }
1213
- }
1214
- if (html && slidePlan.type === 'content') {
1215
- html = postProcessSlideHtml(html);
1216
- if (slideNum <= 3) {
1217
- try {
1218
- fs.writeFileSync(path.join(tempWritePath, `hanseol_debug_slide_${slideNum}.html`), html, 'utf-8');
1219
- }
1220
- catch { }
1221
- }
1222
- }
1223
- if (htmlPrompt && html && slidePlan.type === 'content' && !usedSkeleton) {
1224
- const MAX_LAYOUT_REGEN = 2;
1225
- for (let layoutAttempt = 0; layoutAttempt < MAX_LAYOUT_REGEN; layoutAttempt++) {
1226
- const complianceError = checkLayoutCompliance(html, slideLayoutType);
1227
- if (!complianceError) {
1228
- if (layoutAttempt > 0) {
1229
- logger.info(`Slide ${slideNum}: Layout compliance OK after ${layoutAttempt} regen(s)`);
1230
- }
1231
- break;
1232
- }
1233
- logger.warn(`Slide ${slideNum}: Layout compliance FAILED — ${complianceError}`);
1234
- if (phaseLogger)
1235
- phaseLogger('powerpoint-create', 'execution', `Slide ${slideNum}: wrong layout, regen ${layoutAttempt + 1}/${MAX_LAYOUT_REGEN}...`);
876
+ else {
877
+ logger.warn(`Slide ${slideNum}: JSON fill failed, falling back to LLM HTML generation`);
878
+ const fallbackPrompt = buildSlideHtmlPrompt(slidePlan.title, cleanedDirection, plan.design, i, plan.slides.length, language, slideLayoutType);
1236
879
  try {
1237
- const regenRes = await llmClient.chatCompletion({
880
+ const htmlRes = await llmClient.chatCompletion({
1238
881
  messages: [
1239
- { role: 'system', content: htmlPrompt },
1240
- { role: 'user', content: `Generate the HTML slide now.\n\n⚠⚠⚠ CRITICAL ERROR: ${complianceError}\n\nYou MUST use the "${slideLayoutType}" layout as specified in the REQUIRED LAYOUT section above. Re-read the CSS STRUCTURE section carefully and follow it EXACTLY. Do NOT use cards or any other layout.` },
882
+ { role: 'system', content: fallbackPrompt },
883
+ { role: 'user', content: 'Generate the HTML slide now.' },
1241
884
  ],
1242
- temperature: 0.2,
885
+ temperature: 0.3,
1243
886
  max_tokens: 8000,
1244
887
  });
1245
- const regenMsg = regenRes.choices[0]?.message;
1246
- const regenRaw = regenMsg ? extractContent(regenMsg) : '';
1247
- const regenHtml = extractHtml(regenRaw);
1248
- if (regenHtml) {
1249
- html = regenHtml;
1250
- logger.info(`Slide ${slideNum}: Regenerated for layout compliance (attempt ${layoutAttempt + 1})`);
1251
- }
1252
- else {
1253
- break;
1254
- }
888
+ const htmlMsg = htmlRes.choices[0]?.message;
889
+ const rawHtml = htmlMsg ? extractContent(htmlMsg) : '';
890
+ html = extractHtml(rawHtml);
1255
891
  }
1256
892
  catch (e) {
1257
- logger.warn(`Slide ${slideNum}: Layout regen failed: ${e}`);
1258
- break;
893
+ logger.warn(`Slide ${slideNum}: LLM HTML fallback failed: ${e}`);
1259
894
  }
1260
895
  }
1261
896
  }
897
+ if (!html) {
898
+ logger.warn(`Slide ${slideNum}: Failed to generate HTML, skipping`);
899
+ failCount++;
900
+ continue;
901
+ }
1262
902
  const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
1263
903
  const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
1264
904
  const htmlWritePath = path.join(tempWritePath, htmlFileName);
@@ -1266,7 +906,7 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
1266
906
  const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
1267
907
  const pngWinPath = `${tempWinPath}\\${pngFileName}`;
1268
908
  try {
1269
- const processed = injectTitleContrastFix(ensureSafeBodyPadding(removeAbsolutePositioning(stripGradientTextEffects(enforceMinFontSize(injectViewportCss(html, plan.design.background_color))))), plan.design.text_color);
909
+ const processed = injectTitleContrastFix(injectViewportCss(html, plan.design.background_color), plan.design.text_color);
1270
910
  fs.writeFileSync(htmlWritePath, processed, 'utf-8');
1271
911
  tempFiles.push(htmlWritePath);
1272
912
  }