hanseol-dev 5.0.3-dev.2 → 5.0.3-dev.21
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/agents/office/powerpoint-create-agent.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-agent.js +342 -144
- package/dist/agents/office/powerpoint-create-agent.js.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.d.ts +1 -1
- package/dist/agents/office/powerpoint-create-prompts.d.ts.map +1 -1
- package/dist/agents/office/powerpoint-create-prompts.js +118 -92
- package/dist/agents/office/powerpoint-create-prompts.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -1
- package/dist/constants.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"powerpoint-create-agent.d.ts","sourceRoot":"","sources":["../../../src/agents/office/powerpoint-create-agent.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,YAAY,EAAc,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"powerpoint-create-agent.d.ts","sourceRoot":"","sources":["../../../src/agents/office/powerpoint-create-agent.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,YAAY,EAAc,MAAM,sBAAsB,CAAC;AAwgDhE,wBAAgB,iCAAiC,IAAI,YAAY,CAkChE"}
|
|
@@ -4,7 +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_DESIGN_PROMPT,
|
|
7
|
+
import { PPT_DESIGN_PROMPT, validateSlideHtml, extractLayoutHint, buildContentFillJsonPrompt, parseContentFillJson, buildContentSlideHtml, } from './powerpoint-create-prompts.js';
|
|
8
8
|
const DEFAULT_DESIGN = {
|
|
9
9
|
primary_color: '#1B2A4A',
|
|
10
10
|
accent_color: '#00D4AA',
|
|
@@ -17,7 +17,7 @@ const DEFAULT_DESIGN = {
|
|
|
17
17
|
mood: 'modern-minimal',
|
|
18
18
|
design_notes: 'Clean gradients, card-based layouts',
|
|
19
19
|
};
|
|
20
|
-
const MAX_CONCURRENT =
|
|
20
|
+
const MAX_CONCURRENT = 5;
|
|
21
21
|
function extractContent(msg) {
|
|
22
22
|
const content = msg['content'];
|
|
23
23
|
if (content && content.trim())
|
|
@@ -228,18 +228,6 @@ function parseJsonPlan(raw) {
|
|
|
228
228
|
return null;
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
-
function extractHtml(raw) {
|
|
232
|
-
const trimmed = raw.trim();
|
|
233
|
-
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html'))
|
|
234
|
-
return trimmed;
|
|
235
|
-
const fenceMatch = trimmed.match(/```(?:html)?\s*\n([\s\S]*?)\n```/);
|
|
236
|
-
if (fenceMatch?.[1])
|
|
237
|
-
return fenceMatch[1].trim();
|
|
238
|
-
const docMatch = trimmed.match(/(<!DOCTYPE[\s\S]*<\/html>)/i);
|
|
239
|
-
if (docMatch?.[1])
|
|
240
|
-
return docMatch[1].trim();
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
231
|
function injectEdgeSizing(html, backgroundColor) {
|
|
244
232
|
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
245
233
|
const bgColor = backgroundColor || '#000000';
|
|
@@ -258,24 +246,6 @@ function injectEdgeSizing(html, backgroundColor) {
|
|
|
258
246
|
}
|
|
259
247
|
return result;
|
|
260
248
|
}
|
|
261
|
-
function injectViewportCss(html, backgroundColor) {
|
|
262
|
-
let result = html.replace(/<meta\s+name=["']viewport["'][^>]*>/gi, '');
|
|
263
|
-
const bgColor = backgroundColor || '#000000';
|
|
264
|
-
const overrideCss = `<style>*{box-sizing:border-box;word-break:keep-all;overflow-wrap:break-word}html{width:2040px!important;height:1200px!important;overflow:hidden!important;margin:0!important;background-color:${bgColor}!important;zoom:1!important}body{width:1920px!important;height:1080px!important;min-width:1920px!important;min-height:1080px!important;overflow:hidden!important;margin:0!important;display:flex!important;flex-direction:column!important;zoom:1!important;font-size:26px!important}body>div,body>main,body>section,body>article,body>header,body>footer{max-width:none!important;zoom:1!important}body>*>div,body>*>main,body>*>section,body>*>article{max-width:none!important;zoom:1!important}body>*:only-child{display:flex!important;flex-direction:column!important;flex:1!important;min-height:1080px!important}body>*:only-child>*:nth-child(2),body>*:only-child>*:nth-child(3),body>*:only-child>*:last-child:not(:first-child){flex:1!important;display:flex!important;align-items:stretch!important}body>.content,body>*:nth-child(2),body>*:nth-child(3),body *>.content,body [class*=content]{flex:1!important;align-items:stretch!important;align-content:stretch!important;grid-auto-rows:1fr!important}[class*=card],[class*=item],[class*=step],[class*=milestone],[class*=feature]{justify-content:flex-start!important;gap:20px!important}</style>`;
|
|
265
|
-
if (result.includes('</head>')) {
|
|
266
|
-
result = result.replace('</head>', `${overrideCss}</head>`);
|
|
267
|
-
}
|
|
268
|
-
else if (result.includes('<head>')) {
|
|
269
|
-
result = result.replace('<head>', `<head>${overrideCss}`);
|
|
270
|
-
}
|
|
271
|
-
else if (result.includes('<html')) {
|
|
272
|
-
result = result.replace(/<html[^>]*>/, (m) => `${m}<head>${overrideCss}</head>`);
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
result = overrideCss + result;
|
|
276
|
-
}
|
|
277
|
-
return result;
|
|
278
|
-
}
|
|
279
249
|
function injectTitleContrastFix(html, designTextColor) {
|
|
280
250
|
const safeColor = designTextColor.replace(/'/g, "\\'");
|
|
281
251
|
const script = `<script>(function(){` +
|
|
@@ -589,6 +559,74 @@ body {
|
|
|
589
559
|
</body>
|
|
590
560
|
</html>`;
|
|
591
561
|
}
|
|
562
|
+
function buildFallbackSlideHtml(design, title, contentDirection, slideNum) {
|
|
563
|
+
const items = [];
|
|
564
|
+
const parts = contentDirection.split(/\(\d+\)\s*|•\s*|\n-\s*|\n\d+[.)]\s*|Step\s*\d+[.:]\s*/i);
|
|
565
|
+
for (const part of parts) {
|
|
566
|
+
const cleaned = part.replace(/Layout:.*$/i, '').trim();
|
|
567
|
+
if (cleaned.length > 5) {
|
|
568
|
+
const sentence = cleaned.split(/[.。!]\s/)[0] || cleaned;
|
|
569
|
+
items.push(sentence.slice(0, 80));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (items.length < 3) {
|
|
573
|
+
const commaParts = contentDirection.split(/[,;,;]\s*/);
|
|
574
|
+
for (const part of commaParts) {
|
|
575
|
+
const cleaned = part.replace(/Layout:.*$/i, '').trim();
|
|
576
|
+
if (cleaned.length > 8 && !items.includes(cleaned.slice(0, 80))) {
|
|
577
|
+
items.push(cleaned.slice(0, 80));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (items.length < 3) {
|
|
582
|
+
const sentences = contentDirection.split(/[.。!?]\s+/);
|
|
583
|
+
for (const s of sentences) {
|
|
584
|
+
const cleaned = s.replace(/Layout:.*$/i, '').trim();
|
|
585
|
+
if (cleaned.length > 10 && !items.some(i => i.startsWith(cleaned.slice(0, 20)))) {
|
|
586
|
+
items.push(cleaned.slice(0, 80));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const bulletItems = items.slice(0, 6);
|
|
591
|
+
const useGrid = bulletItems.length >= 4;
|
|
592
|
+
const gridCols = useGrid ? 'grid-template-columns:1fr 1fr' : 'grid-template-columns:1fr';
|
|
593
|
+
const pointsHtml = bulletItems.map((item, i) => `<div class="point"><div class="point-num">${i + 1}</div><div class="point-text">${escapeHtml(item)}</div></div>`).join('\n ');
|
|
594
|
+
return `<!DOCTYPE html>
|
|
595
|
+
<html lang="ko">
|
|
596
|
+
<head>
|
|
597
|
+
<meta charset="UTF-8">
|
|
598
|
+
<style>
|
|
599
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
600
|
+
html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
601
|
+
body {
|
|
602
|
+
background: ${design.background_color};
|
|
603
|
+
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
604
|
+
padding: 80px 100px;
|
|
605
|
+
display: flex;
|
|
606
|
+
flex-direction: column;
|
|
607
|
+
}
|
|
608
|
+
.header { margin-bottom: 48px; }
|
|
609
|
+
.slide-num { font-size: 14px; color: ${design.accent_color}; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 12px; }
|
|
610
|
+
h1 { font-size: 52px; font-weight: 700; color: ${design.text_color}; font-family: "${design.font_title}", "Segoe UI", sans-serif; line-height: 1.2; }
|
|
611
|
+
.accent-bar { width: 80px; height: 4px; background: ${design.accent_color}; margin-top: 20px; border-radius: 2px; }
|
|
612
|
+
.content { flex: 1; display: grid; ${gridCols}; gap: 24px; align-content: center; }
|
|
613
|
+
.point { display: flex; align-items: flex-start; gap: 20px; padding: 28px 32px; background: #fff; border-radius: 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.06); }
|
|
614
|
+
.point-num { width: 48px; height: 48px; border-radius: 50%; background: ${design.accent_color}; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 700; flex-shrink: 0; }
|
|
615
|
+
.point-text { font-size: 26px; line-height: 1.5; color: ${design.text_color}; flex: 1; }
|
|
616
|
+
</style>
|
|
617
|
+
</head>
|
|
618
|
+
<body>
|
|
619
|
+
<div class="header">
|
|
620
|
+
<div class="slide-num">SLIDE ${slideNum}</div>
|
|
621
|
+
<h1>${escapeHtml(title)}</h1>
|
|
622
|
+
<div class="accent-bar"></div>
|
|
623
|
+
</div>
|
|
624
|
+
<div class="content">
|
|
625
|
+
${pointsHtml}
|
|
626
|
+
</div>
|
|
627
|
+
</body>
|
|
628
|
+
</html>`;
|
|
629
|
+
}
|
|
592
630
|
function getTempDir() {
|
|
593
631
|
const platform = getPlatform();
|
|
594
632
|
if (platform === 'wsl') {
|
|
@@ -683,34 +721,64 @@ async function runDesignPhase(llmClient, instruction, phaseLogger) {
|
|
|
683
721
|
}
|
|
684
722
|
return plan;
|
|
685
723
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
724
|
+
const LAYOUT_ALTERNATIVES = {
|
|
725
|
+
cards: ['progress_bars', 'hero_stat', 'donut_chart', 'two_col_split', 'process_flow', 'timeline'],
|
|
726
|
+
};
|
|
727
|
+
const CARDS_KEEP_KEYWORDS = /team|팀|case|사례|success|성과|partner|파트너|고객|customer|member|멤버/i;
|
|
728
|
+
function rebalanceLayouts(plan) {
|
|
729
|
+
const contentSlides = plan.slides.filter(s => s.type === 'content');
|
|
730
|
+
const counts = new Map();
|
|
731
|
+
const slidesByLayout = new Map();
|
|
732
|
+
for (const slide of contentSlides) {
|
|
733
|
+
const layout = extractLayoutHint(slide.content_direction || '');
|
|
734
|
+
counts.set(layout, (counts.get(layout) || 0) + 1);
|
|
735
|
+
if (!slidesByLayout.has(layout))
|
|
736
|
+
slidesByLayout.set(layout, []);
|
|
737
|
+
slidesByLayout.get(layout).push(slide);
|
|
738
|
+
}
|
|
739
|
+
for (const [layout, count] of counts) {
|
|
740
|
+
if (count <= 2)
|
|
741
|
+
continue;
|
|
742
|
+
const slides = slidesByLayout.get(layout);
|
|
743
|
+
const keepSlides = slides.filter(s => CARDS_KEEP_KEYWORDS.test(s.title));
|
|
744
|
+
const reassignCandidates = slides.filter(s => !CARDS_KEEP_KEYWORDS.test(s.title));
|
|
745
|
+
const keepCount = Math.max(2, keepSlides.length);
|
|
746
|
+
const excess = reassignCandidates.slice(Math.max(0, keepCount - keepSlides.length));
|
|
747
|
+
if (excess.length === 0)
|
|
748
|
+
continue;
|
|
749
|
+
const alternatives = LAYOUT_ALTERNATIVES[layout] || ['progress_bars', 'hero_stat', 'donut_chart', 'two_col_split'];
|
|
750
|
+
const usedAlts = new Set();
|
|
751
|
+
for (const slide of excess) {
|
|
752
|
+
let chosen = null;
|
|
753
|
+
for (const alt of alternatives) {
|
|
754
|
+
const altCount = (counts.get(alt) || 0) + (usedAlts.has(alt) ? 1 : 0);
|
|
755
|
+
if (altCount < 2) {
|
|
756
|
+
chosen = alt;
|
|
757
|
+
usedAlts.add(alt);
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
706
760
|
}
|
|
707
|
-
|
|
761
|
+
if (!chosen)
|
|
762
|
+
chosen = alternatives[0];
|
|
763
|
+
const cd = slide.content_direction || '';
|
|
764
|
+
const hasLayoutHint = /Layout:\s*.+$/im.test(cd);
|
|
765
|
+
const layoutName = chosen.replace(/_/g, ' ');
|
|
766
|
+
if (hasLayoutHint) {
|
|
767
|
+
slide.content_direction = cd.replace(/Layout:\s*.+$/im, `Layout: ${layoutName}`);
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
slide.content_direction = cd + `\nLayout: ${layoutName}`;
|
|
771
|
+
}
|
|
772
|
+
counts.set(chosen, (counts.get(chosen) || 0) + 1);
|
|
773
|
+
counts.set(layout, counts.get(layout) - 1);
|
|
774
|
+
logger.info(`Rebalance: "${slide.title}" layout changed from ${layout} → ${chosen}`);
|
|
708
775
|
}
|
|
709
776
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
777
|
+
}
|
|
778
|
+
async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, _totalSlides, language) {
|
|
779
|
+
const cleanedDirection = (slide.content_direction || '').replace(/\s*Layout\s*:\s*[^\n]*/gi, '').trim();
|
|
780
|
+
const layoutType = extractLayoutHint(slide.content_direction || '');
|
|
781
|
+
logger.info(`Slide ${slideIndex + 1}: Generating code template "${layoutType}"`);
|
|
714
782
|
const jsonPrompt = buildContentFillJsonPrompt(slide.title, cleanedDirection, layoutType, language);
|
|
715
783
|
try {
|
|
716
784
|
const jsonRes = await llmClient.chatCompletion({
|
|
@@ -725,6 +793,7 @@ async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, tot
|
|
|
725
793
|
const jsonRaw = jsonMsg ? extractContent(jsonMsg) : '';
|
|
726
794
|
let slideData = parseContentFillJson(jsonRaw, layoutType);
|
|
727
795
|
if (!slideData) {
|
|
796
|
+
logger.warn(`Slide ${slideIndex + 1}: JSON fill parse failed (attempt 1). Layout: ${layoutType}. Raw length: ${jsonRaw.length}. First 200 chars: ${jsonRaw.slice(0, 200)}`);
|
|
728
797
|
const retryRes = await llmClient.chatCompletion({
|
|
729
798
|
messages: [
|
|
730
799
|
{ role: 'system', content: jsonPrompt },
|
|
@@ -736,6 +805,9 @@ async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, tot
|
|
|
736
805
|
const retryMsg = retryRes.choices[0]?.message;
|
|
737
806
|
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
738
807
|
slideData = parseContentFillJson(retryRaw, layoutType);
|
|
808
|
+
if (!slideData) {
|
|
809
|
+
logger.warn(`Slide ${slideIndex + 1}: JSON fill parse failed (attempt 2). Raw length: ${retryRaw.length}. First 200 chars: ${retryRaw.slice(0, 200)}`);
|
|
810
|
+
}
|
|
739
811
|
}
|
|
740
812
|
if (slideData) {
|
|
741
813
|
const html = buildContentSlideHtml(design, slide.title, layoutType, slideData, slideIndex + 1, slideIndex);
|
|
@@ -797,6 +869,12 @@ async function generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstD
|
|
|
797
869
|
if (result) {
|
|
798
870
|
results.set(index, { index, html: result.html, isCodeTemplate: result.isCodeTemplate });
|
|
799
871
|
}
|
|
872
|
+
else {
|
|
873
|
+
const slide = plan.slides[index];
|
|
874
|
+
const fallbackHtml = buildFallbackSlideHtml(plan.design, slide.title, slide.content_direction || '', index + 1);
|
|
875
|
+
results.set(index, { index, html: fallbackHtml, isCodeTemplate: true });
|
|
876
|
+
logger.warn(`Slide ${index + 1}: Using fallback HTML for "${slide.title}"`);
|
|
877
|
+
}
|
|
800
878
|
}
|
|
801
879
|
const done = Math.min(batch + MAX_CONCURRENT, contentIndices.length);
|
|
802
880
|
if (phaseLogger)
|
|
@@ -823,38 +901,55 @@ async function validateAndRegenerate(llmClient, htmlResults, plan, language, pha
|
|
|
823
901
|
return htmlResults;
|
|
824
902
|
}
|
|
825
903
|
if (phaseLogger)
|
|
826
|
-
phaseLogger('powerpoint-create', 'validation', `${failedIndices.length} slides failed validation, regenerating...`);
|
|
827
|
-
for (
|
|
828
|
-
const
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
904
|
+
phaseLogger('powerpoint-create', 'validation', `${failedIndices.length} slides failed validation, regenerating in parallel...`);
|
|
905
|
+
for (let batch = 0; batch < failedIndices.length; batch += MAX_CONCURRENT) {
|
|
906
|
+
const chunk = failedIndices.slice(batch, batch + MAX_CONCURRENT);
|
|
907
|
+
const promises = chunk.map(async (index) => {
|
|
908
|
+
const slide = plan.slides[index];
|
|
909
|
+
const original = htmlResults.get(index);
|
|
910
|
+
try {
|
|
911
|
+
const result = await generateSingleSlideHtml(llmClient, slide, plan.design, index, plan.slides.length, language);
|
|
912
|
+
if (result) {
|
|
913
|
+
const regenHasPlaceholder = hasPlaceholderText(result.html);
|
|
914
|
+
const origHasPlaceholder = hasPlaceholderText(original.html);
|
|
915
|
+
if (!regenHasPlaceholder || origHasPlaceholder) {
|
|
916
|
+
return { index, result: { index, html: result.html, isCodeTemplate: result.isCodeTemplate } };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
catch (e) {
|
|
921
|
+
logger.warn(`Slide ${index + 1}: Regen error: ${e}`);
|
|
922
|
+
}
|
|
923
|
+
return { index, result: null };
|
|
924
|
+
});
|
|
925
|
+
const chunkResults = await Promise.all(promises);
|
|
926
|
+
for (const { index, result } of chunkResults) {
|
|
927
|
+
if (result) {
|
|
928
|
+
htmlResults.set(index, result);
|
|
836
929
|
}
|
|
837
930
|
}
|
|
838
931
|
}
|
|
839
932
|
return htmlResults;
|
|
840
933
|
}
|
|
841
|
-
|
|
934
|
+
const MAX_SCREENSHOT_CONCURRENT = 4;
|
|
935
|
+
async function assemblePresentation(htmlResults, plan, timestamp, savePath, companyName, language, phaseLogger, toolCallLogger) {
|
|
842
936
|
const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
|
|
843
937
|
ensureTempDir(tempWritePath);
|
|
938
|
+
const totalSlides = htmlResults.size;
|
|
939
|
+
const sortedEntries = [...htmlResults.entries()].sort((a, b) => a[0] - b[0]);
|
|
844
940
|
const builtSlides = [];
|
|
845
|
-
let failCount = 0;
|
|
846
941
|
let totalToolCalls = 0;
|
|
847
942
|
const tempFiles = [];
|
|
848
|
-
|
|
943
|
+
if (phaseLogger)
|
|
944
|
+
phaseLogger('powerpoint-create', 'assembly', `Pre-creating ${totalSlides} slides...`);
|
|
945
|
+
for (let i = 1; i < totalSlides; i++) {
|
|
946
|
+
await powerpointClient.powerpointAddSlide(7);
|
|
947
|
+
totalToolCalls++;
|
|
948
|
+
}
|
|
949
|
+
const slideFiles = new Map();
|
|
950
|
+
let slideNum = 0;
|
|
849
951
|
for (const [index, result] of sortedEntries) {
|
|
850
|
-
|
|
851
|
-
const slideNum = builtSlides.length + 1;
|
|
852
|
-
if (failCount >= 3) {
|
|
853
|
-
logger.warn('Too many slide failures, stopping');
|
|
854
|
-
break;
|
|
855
|
-
}
|
|
856
|
-
if (phaseLogger)
|
|
857
|
-
phaseLogger('powerpoint-create', 'assembly', `Rendering slide ${slideNum}: ${slidePlan.title}`);
|
|
952
|
+
slideNum++;
|
|
858
953
|
const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
|
|
859
954
|
const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
|
|
860
955
|
const htmlWritePath = path.join(tempWritePath, htmlFileName);
|
|
@@ -862,101 +957,202 @@ async function assemblePresentation(htmlResults, plan, timestamp, savePath, phas
|
|
|
862
957
|
const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
|
|
863
958
|
const pngWinPath = `${tempWinPath}\\${pngFileName}`;
|
|
864
959
|
try {
|
|
865
|
-
const viewportHtml = result.
|
|
866
|
-
? injectEdgeSizing(result.html, plan.design.background_color)
|
|
867
|
-
: injectViewportCss(result.html, plan.design.background_color);
|
|
960
|
+
const viewportHtml = injectEdgeSizing(result.html, plan.design.background_color);
|
|
868
961
|
const processed = injectTitleContrastFix(viewportHtml, plan.design.text_color);
|
|
869
962
|
fs.writeFileSync(htmlWritePath, processed, 'utf-8');
|
|
870
963
|
tempFiles.push(htmlWritePath);
|
|
964
|
+
slideFiles.set(index, { slideNum, htmlWritePath, pngWritePath, htmlWinPath, pngWinPath });
|
|
871
965
|
}
|
|
872
966
|
catch (e) {
|
|
873
|
-
logger.warn(`Slide ${slideNum}: Failed to write HTML
|
|
874
|
-
failCount++;
|
|
875
|
-
continue;
|
|
876
|
-
}
|
|
877
|
-
let renderSuccess = false;
|
|
878
|
-
try {
|
|
879
|
-
const renderResult = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
|
|
880
|
-
totalToolCalls++;
|
|
881
|
-
renderSuccess = renderResult.success;
|
|
882
|
-
if (!renderSuccess) {
|
|
883
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
884
|
-
const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
|
|
885
|
-
totalToolCalls++;
|
|
886
|
-
renderSuccess = retryRender.success;
|
|
887
|
-
}
|
|
967
|
+
logger.warn(`Slide ${slideNum}: Failed to write HTML: ${e}`);
|
|
888
968
|
}
|
|
889
|
-
|
|
890
|
-
|
|
969
|
+
}
|
|
970
|
+
if (phaseLogger)
|
|
971
|
+
phaseLogger('powerpoint-create', 'assembly', `Rendering ${slideFiles.size} screenshots in parallel (batch ${MAX_SCREENSHOT_CONCURRENT})...`);
|
|
972
|
+
const screenshotEntries = [...slideFiles.entries()].sort((a, b) => a[0] - b[0]);
|
|
973
|
+
const screenshotSuccess = new Set();
|
|
974
|
+
for (let batch = 0; batch < screenshotEntries.length; batch += MAX_SCREENSHOT_CONCURRENT) {
|
|
975
|
+
const chunk = screenshotEntries.slice(batch, batch + MAX_SCREENSHOT_CONCURRENT);
|
|
976
|
+
const batchResults = await Promise.all(chunk.map(async ([index, files]) => {
|
|
977
|
+
let success = false;
|
|
891
978
|
try {
|
|
892
|
-
|
|
893
|
-
const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
|
|
979
|
+
const result = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
|
|
894
980
|
totalToolCalls++;
|
|
895
|
-
|
|
981
|
+
success = result.success;
|
|
982
|
+
if (!success) {
|
|
983
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
984
|
+
const retry = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
|
|
985
|
+
totalToolCalls++;
|
|
986
|
+
success = retry.success;
|
|
987
|
+
}
|
|
896
988
|
}
|
|
897
989
|
catch {
|
|
898
|
-
|
|
990
|
+
try {
|
|
991
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
992
|
+
const retry = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
|
|
993
|
+
totalToolCalls++;
|
|
994
|
+
success = retry.success;
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
success = false;
|
|
998
|
+
}
|
|
899
999
|
}
|
|
1000
|
+
try {
|
|
1001
|
+
fs.unlinkSync(files.htmlWritePath);
|
|
1002
|
+
}
|
|
1003
|
+
catch { }
|
|
1004
|
+
if (success) {
|
|
1005
|
+
try {
|
|
1006
|
+
const stat = fs.statSync(files.pngWritePath);
|
|
1007
|
+
if (stat.size < 15000) {
|
|
1008
|
+
logger.warn(`Slide ${files.slideNum}: Screenshot too small (${stat.size} bytes)`);
|
|
1009
|
+
success = false;
|
|
1010
|
+
try {
|
|
1011
|
+
fs.unlinkSync(files.pngWritePath);
|
|
1012
|
+
}
|
|
1013
|
+
catch { }
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
catch { }
|
|
1017
|
+
}
|
|
1018
|
+
if (success)
|
|
1019
|
+
tempFiles.push(files.pngWritePath);
|
|
1020
|
+
return { index, success };
|
|
1021
|
+
}));
|
|
1022
|
+
for (const { index, success } of batchResults) {
|
|
1023
|
+
if (success)
|
|
1024
|
+
screenshotSuccess.add(index);
|
|
900
1025
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1026
|
+
const done = Math.min(batch + MAX_SCREENSHOT_CONCURRENT, screenshotEntries.length);
|
|
1027
|
+
if (phaseLogger)
|
|
1028
|
+
phaseLogger('powerpoint-create', 'assembly', `Screenshots: ${done}/${screenshotEntries.length} done`);
|
|
1029
|
+
}
|
|
1030
|
+
if (phaseLogger)
|
|
1031
|
+
phaseLogger('powerpoint-create', 'assembly', `Filling ${totalSlides} slides...`);
|
|
1032
|
+
const unfilledSlideNums = [];
|
|
1033
|
+
for (const [index] of sortedEntries) {
|
|
1034
|
+
const files = slideFiles.get(index);
|
|
1035
|
+
const slidePlan = plan.slides[index];
|
|
1036
|
+
const htmlResult = htmlResults.get(index);
|
|
1037
|
+
if (!files) {
|
|
1038
|
+
const posInSorted = sortedEntries.findIndex(([idx]) => idx === index);
|
|
1039
|
+
const inferredSlideNum = posInSorted >= 0 ? posInSorted + 1 : -1;
|
|
1040
|
+
logger.warn(`Slide index ${index} (slideNum ${inferredSlideNum}): No file info, marking for deletion`);
|
|
1041
|
+
if (inferredSlideNum > 0)
|
|
1042
|
+
unfilledSlideNums.push(inferredSlideNum);
|
|
908
1043
|
continue;
|
|
909
1044
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1045
|
+
let filled = false;
|
|
1046
|
+
if (screenshotSuccess.has(index)) {
|
|
1047
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(files.slideNum, files.pngWinPath);
|
|
1048
|
+
totalToolCalls++;
|
|
1049
|
+
if (toolCallLogger)
|
|
1050
|
+
toolCallLogger('powerpoint-create', 'addFullSlideImage', { slideNum: files.slideNum, imagePath: files.pngWinPath }, bgResult.success ? 'OK' : 'Failed', bgResult.success, files.slideNum, totalToolCalls);
|
|
1051
|
+
filled = bgResult.success;
|
|
1052
|
+
}
|
|
1053
|
+
if (!filled) {
|
|
1054
|
+
logger.info(`Slide ${files.slideNum}: Primary screenshot failed, trying fallback rendering...`);
|
|
1055
|
+
const slide = plan.slides[index];
|
|
1056
|
+
const fallbackHtml = buildFallbackSlideHtml(plan.design, slide.title, slide.content_direction || '', files.slideNum);
|
|
1057
|
+
const fbHtmlName = `hanseol_fb_${files.slideNum}_${timestamp}.html`;
|
|
1058
|
+
const fbPngName = `hanseol_fb_${files.slideNum}_${timestamp}.png`;
|
|
1059
|
+
const fbHtmlWrite = path.join(tempWritePath, fbHtmlName);
|
|
1060
|
+
const fbPngWrite = path.join(tempWritePath, fbPngName);
|
|
1061
|
+
const fbHtmlWin = `${tempWinPath}\\${fbHtmlName}`;
|
|
1062
|
+
const fbPngWin = `${tempWinPath}\\${fbPngName}`;
|
|
1063
|
+
try {
|
|
1064
|
+
const viewportHtml = injectEdgeSizing(fallbackHtml, plan.design.background_color);
|
|
1065
|
+
fs.writeFileSync(fbHtmlWrite, viewportHtml, 'utf-8');
|
|
1066
|
+
const fbResult = await powerpointClient.renderHtmlToImage(fbHtmlWin, fbPngWin);
|
|
1067
|
+
totalToolCalls++;
|
|
915
1068
|
try {
|
|
916
|
-
fs.unlinkSync(
|
|
1069
|
+
fs.unlinkSync(fbHtmlWrite);
|
|
1070
|
+
}
|
|
1071
|
+
catch { }
|
|
1072
|
+
if (fbResult.success) {
|
|
1073
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(files.slideNum, fbPngWin);
|
|
1074
|
+
totalToolCalls++;
|
|
1075
|
+
filled = bgResult.success;
|
|
1076
|
+
try {
|
|
1077
|
+
fs.unlinkSync(fbPngWrite);
|
|
1078
|
+
}
|
|
1079
|
+
catch { }
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
catch (e) {
|
|
1083
|
+
logger.warn(`Slide ${files.slideNum}: Fallback rendering also failed: ${e}`);
|
|
1084
|
+
try {
|
|
1085
|
+
fs.unlinkSync(fbHtmlWrite);
|
|
917
1086
|
}
|
|
918
1087
|
catch { }
|
|
919
|
-
continue;
|
|
920
1088
|
}
|
|
921
1089
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const addResult = await powerpointClient.powerpointAddSlide(7);
|
|
925
|
-
totalToolCalls++;
|
|
926
|
-
if (!addResult.success) {
|
|
927
|
-
logger.warn(`Slide ${slideNum}: Failed to add blank slide`);
|
|
928
|
-
failCount++;
|
|
929
|
-
continue;
|
|
930
|
-
}
|
|
931
|
-
const bgResult = await powerpointClient.powerpointAddFullSlideImage(slideNum, pngWinPath);
|
|
932
|
-
totalToolCalls++;
|
|
933
|
-
if (toolCallLogger)
|
|
934
|
-
toolCallLogger('powerpoint-create', 'addFullSlideImage', { slideNum, imagePath: pngWinPath }, bgResult.success ? 'OK' : 'Failed', bgResult.success, slideNum, totalToolCalls);
|
|
935
|
-
if (bgResult.success) {
|
|
936
|
-
builtSlides.push(`Slide ${slideNum}: ${slidePlan.title} (${slidePlan.type})`);
|
|
1090
|
+
if (filled) {
|
|
1091
|
+
builtSlides.push(`Slide ${files.slideNum}: ${slidePlan.title} (${slidePlan.type})`);
|
|
937
1092
|
try {
|
|
938
|
-
await powerpointClient.powerpointAddNote(slideNum,
|
|
1093
|
+
await powerpointClient.powerpointAddNote(files.slideNum, htmlResult.html);
|
|
939
1094
|
}
|
|
940
1095
|
catch { }
|
|
941
1096
|
}
|
|
942
1097
|
else {
|
|
943
|
-
logger.warn(`Slide ${slideNum}:
|
|
944
|
-
|
|
1098
|
+
logger.warn(`Slide ${files.slideNum}: All rendering attempts failed, marking for deletion`);
|
|
1099
|
+
unfilledSlideNums.push(files.slideNum);
|
|
945
1100
|
}
|
|
946
1101
|
}
|
|
947
|
-
if (
|
|
1102
|
+
if (unfilledSlideNums.length > 0) {
|
|
1103
|
+
const sortedDesc = [...unfilledSlideNums].sort((a, b) => b - a);
|
|
1104
|
+
for (const slideNum of sortedDesc) {
|
|
1105
|
+
try {
|
|
1106
|
+
await powerpointClient.powerpointDeleteSlide(slideNum);
|
|
1107
|
+
totalToolCalls++;
|
|
1108
|
+
logger.info(`Deleted unfilled slide ${slideNum}`);
|
|
1109
|
+
}
|
|
1110
|
+
catch (e) {
|
|
1111
|
+
logger.warn(`Failed to delete unfilled slide ${slideNum}: ${e}`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
const hasClosingInPlan = plan.slides.some(s => s.type === 'closing');
|
|
1116
|
+
const hasClosingBuilt = builtSlides.some(s => s.includes('(closing)'));
|
|
1117
|
+
if (hasClosingInPlan && !hasClosingBuilt && builtSlides.length > 0) {
|
|
1118
|
+
logger.info('Closing slide was lost during assembly — adding it now');
|
|
948
1119
|
try {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1120
|
+
await powerpointClient.powerpointAddSlide(7);
|
|
1121
|
+
totalToolCalls++;
|
|
1122
|
+
const slideCountRes = await powerpointClient.powerpointGetSlideCount();
|
|
1123
|
+
const newSlideNum = slideCountRes['slide_count'] || builtSlides.length + 1;
|
|
1124
|
+
const closingPlan = plan.slides.find(s => s.type === 'closing');
|
|
1125
|
+
const closingTagline = (closingPlan.content_direction || '').replace(/감사합니다|thank\s*you/gi, '').trim() || undefined;
|
|
1126
|
+
const closingHtml = buildClosingSlideHtml(plan.design, companyName, newSlideNum, language, closingTagline);
|
|
1127
|
+
const clHtmlName = `hanseol_closing_${timestamp}.html`;
|
|
1128
|
+
const clPngName = `hanseol_closing_${timestamp}.png`;
|
|
1129
|
+
const clHtmlWrite = path.join(tempWritePath, clHtmlName);
|
|
1130
|
+
const clPngWrite = path.join(tempWritePath, clPngName);
|
|
1131
|
+
const clHtmlWin = `${tempWinPath}\\${clHtmlName}`;
|
|
1132
|
+
const clPngWin = `${tempWinPath}\\${clPngName}`;
|
|
1133
|
+
const viewportHtml = injectEdgeSizing(closingHtml, plan.design.background_color);
|
|
1134
|
+
fs.writeFileSync(clHtmlWrite, viewportHtml, 'utf-8');
|
|
1135
|
+
const ssResult = await powerpointClient.renderHtmlToImage(clHtmlWin, clPngWin);
|
|
1136
|
+
totalToolCalls++;
|
|
1137
|
+
try {
|
|
1138
|
+
fs.unlinkSync(clHtmlWrite);
|
|
1139
|
+
}
|
|
1140
|
+
catch { }
|
|
1141
|
+
if (ssResult.success) {
|
|
1142
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(newSlideNum, clPngWin);
|
|
1143
|
+
totalToolCalls++;
|
|
1144
|
+
if (bgResult.success) {
|
|
1145
|
+
builtSlides.push(`Slide ${newSlideNum}: ${closingPlan.title} (closing)`);
|
|
1146
|
+
logger.info('Closing slide added successfully');
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
fs.unlinkSync(clPngWrite);
|
|
955
1150
|
}
|
|
1151
|
+
catch { }
|
|
956
1152
|
}
|
|
957
1153
|
}
|
|
958
1154
|
catch (e) {
|
|
959
|
-
logger.warn(`Failed to
|
|
1155
|
+
logger.warn(`Failed to add closing slide: ${e}`);
|
|
960
1156
|
}
|
|
961
1157
|
}
|
|
962
1158
|
if (builtSlides.length > 0) {
|
|
@@ -1069,7 +1265,8 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1069
1265
|
if (/로고|슬로건|연락처|contact|logo|placeholder/i.test(titleSubtitle)) {
|
|
1070
1266
|
titleSubtitle = '';
|
|
1071
1267
|
}
|
|
1072
|
-
const
|
|
1268
|
+
const cleanInstruction = instruction.replace(/\*\*/g, '');
|
|
1269
|
+
const companyMatch = cleanInstruction.match(/회사명\s*[::]?\s*([^\s,,、]+)/);
|
|
1073
1270
|
if (companyMatch && companyMatch[1]) {
|
|
1074
1271
|
const companyName_ = companyMatch[1];
|
|
1075
1272
|
if (titleSlidePlan && titleSlidePlan.title.trim() !== companyName_) {
|
|
@@ -1083,13 +1280,14 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1083
1280
|
}
|
|
1084
1281
|
}
|
|
1085
1282
|
}
|
|
1283
|
+
rebalanceLayouts(plan);
|
|
1086
1284
|
if (phaseLogger)
|
|
1087
1285
|
phaseLogger('powerpoint-create', 'html-generation', 'Starting parallel HTML generation...');
|
|
1088
1286
|
const htmlResults = await generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstDate, language, phaseLogger);
|
|
1089
1287
|
const validatedResults = await validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger);
|
|
1090
1288
|
if (phaseLogger)
|
|
1091
1289
|
phaseLogger('powerpoint-create', 'assembly', `Assembling ${validatedResults.size} slides into PowerPoint...`);
|
|
1092
|
-
const { builtSlides, totalToolCalls } = await assemblePresentation(validatedResults, plan, timestamp, savePath, phaseLogger, toolCallLogger);
|
|
1290
|
+
const { builtSlides, totalToolCalls } = await assemblePresentation(validatedResults, plan, timestamp, savePath, companyName, language, phaseLogger, toolCallLogger);
|
|
1093
1291
|
const duration = Date.now() - startTime;
|
|
1094
1292
|
const slideList = builtSlides.join('\n');
|
|
1095
1293
|
const summary = `Presentation COMPLETE — ${builtSlides.length} slides created and saved successfully.\nAll requested topics are covered across these slides. Do NOT add more slides or call powerpoint_modify_agent.\n\nSlides:\n${slideList}`;
|