hanseol-dev 5.0.3-dev.2 → 5.0.3-dev.20
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 +339 -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 +107 -82
- 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;AAmgDhE,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,71 @@ 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 bulletsHtml = bulletItems.map(item => `<div class="bullet"><span class="dot"></span><span>${escapeHtml(item)}</span></div>`).join('\n ');
|
|
592
|
+
return `<!DOCTYPE html>
|
|
593
|
+
<html lang="ko">
|
|
594
|
+
<head>
|
|
595
|
+
<meta charset="UTF-8">
|
|
596
|
+
<style>
|
|
597
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
598
|
+
html, body { width: 1920px; height: 1080px; overflow: hidden; }
|
|
599
|
+
body {
|
|
600
|
+
background: ${design.background_color};
|
|
601
|
+
font-family: "${design.font_body}", "Malgun Gothic", sans-serif;
|
|
602
|
+
padding: 80px 100px;
|
|
603
|
+
display: flex;
|
|
604
|
+
flex-direction: column;
|
|
605
|
+
}
|
|
606
|
+
.header { margin-bottom: 48px; }
|
|
607
|
+
.slide-num { font-size: 14px; color: ${design.accent_color}; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 12px; }
|
|
608
|
+
h1 { font-size: 52px; font-weight: 700; color: ${design.text_color}; font-family: "${design.font_title}", "Segoe UI", sans-serif; line-height: 1.2; }
|
|
609
|
+
.accent-bar { width: 80px; height: 4px; background: ${design.accent_color}; margin-top: 20px; border-radius: 2px; }
|
|
610
|
+
.content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 20px; }
|
|
611
|
+
.bullet { display: flex; align-items: flex-start; gap: 16px; font-size: 26px; color: ${design.text_color}; line-height: 1.5; }
|
|
612
|
+
.dot { width: 10px; height: 10px; border-radius: 50%; background: ${design.accent_color}; flex-shrink: 0; margin-top: 10px; }
|
|
613
|
+
</style>
|
|
614
|
+
</head>
|
|
615
|
+
<body>
|
|
616
|
+
<div class="header">
|
|
617
|
+
<div class="slide-num">SLIDE ${slideNum}</div>
|
|
618
|
+
<h1>${escapeHtml(title)}</h1>
|
|
619
|
+
<div class="accent-bar"></div>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="content">
|
|
622
|
+
${bulletsHtml}
|
|
623
|
+
</div>
|
|
624
|
+
</body>
|
|
625
|
+
</html>`;
|
|
626
|
+
}
|
|
592
627
|
function getTempDir() {
|
|
593
628
|
const platform = getPlatform();
|
|
594
629
|
if (platform === 'wsl') {
|
|
@@ -683,34 +718,64 @@ async function runDesignPhase(llmClient, instruction, phaseLogger) {
|
|
|
683
718
|
}
|
|
684
719
|
return plan;
|
|
685
720
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
721
|
+
const LAYOUT_ALTERNATIVES = {
|
|
722
|
+
cards: ['progress_bars', 'hero_stat', 'donut_chart', 'two_col_split', 'process_flow', 'timeline'],
|
|
723
|
+
};
|
|
724
|
+
const CARDS_KEEP_KEYWORDS = /team|팀|case|사례|success|성과|partner|파트너|고객|customer|member|멤버/i;
|
|
725
|
+
function rebalanceLayouts(plan) {
|
|
726
|
+
const contentSlides = plan.slides.filter(s => s.type === 'content');
|
|
727
|
+
const counts = new Map();
|
|
728
|
+
const slidesByLayout = new Map();
|
|
729
|
+
for (const slide of contentSlides) {
|
|
730
|
+
const layout = extractLayoutHint(slide.content_direction || '');
|
|
731
|
+
counts.set(layout, (counts.get(layout) || 0) + 1);
|
|
732
|
+
if (!slidesByLayout.has(layout))
|
|
733
|
+
slidesByLayout.set(layout, []);
|
|
734
|
+
slidesByLayout.get(layout).push(slide);
|
|
735
|
+
}
|
|
736
|
+
for (const [layout, count] of counts) {
|
|
737
|
+
if (count <= 2)
|
|
738
|
+
continue;
|
|
739
|
+
const slides = slidesByLayout.get(layout);
|
|
740
|
+
const keepSlides = slides.filter(s => CARDS_KEEP_KEYWORDS.test(s.title));
|
|
741
|
+
const reassignCandidates = slides.filter(s => !CARDS_KEEP_KEYWORDS.test(s.title));
|
|
742
|
+
const keepCount = Math.max(2, keepSlides.length);
|
|
743
|
+
const excess = reassignCandidates.slice(Math.max(0, keepCount - keepSlides.length));
|
|
744
|
+
if (excess.length === 0)
|
|
745
|
+
continue;
|
|
746
|
+
const alternatives = LAYOUT_ALTERNATIVES[layout] || ['progress_bars', 'hero_stat', 'donut_chart', 'two_col_split'];
|
|
747
|
+
const usedAlts = new Set();
|
|
748
|
+
for (const slide of excess) {
|
|
749
|
+
let chosen = null;
|
|
750
|
+
for (const alt of alternatives) {
|
|
751
|
+
const altCount = (counts.get(alt) || 0) + (usedAlts.has(alt) ? 1 : 0);
|
|
752
|
+
if (altCount < 2) {
|
|
753
|
+
chosen = alt;
|
|
754
|
+
usedAlts.add(alt);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
706
757
|
}
|
|
707
|
-
|
|
758
|
+
if (!chosen)
|
|
759
|
+
chosen = alternatives[0];
|
|
760
|
+
const cd = slide.content_direction || '';
|
|
761
|
+
const hasLayoutHint = /Layout:\s*.+$/im.test(cd);
|
|
762
|
+
const layoutName = chosen.replace(/_/g, ' ');
|
|
763
|
+
if (hasLayoutHint) {
|
|
764
|
+
slide.content_direction = cd.replace(/Layout:\s*.+$/im, `Layout: ${layoutName}`);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
slide.content_direction = cd + `\nLayout: ${layoutName}`;
|
|
768
|
+
}
|
|
769
|
+
counts.set(chosen, (counts.get(chosen) || 0) + 1);
|
|
770
|
+
counts.set(layout, counts.get(layout) - 1);
|
|
771
|
+
logger.info(`Rebalance: "${slide.title}" layout changed from ${layout} → ${chosen}`);
|
|
708
772
|
}
|
|
709
773
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
774
|
+
}
|
|
775
|
+
async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, _totalSlides, language) {
|
|
776
|
+
const cleanedDirection = (slide.content_direction || '').replace(/\s*Layout\s*:\s*[^\n]*/gi, '').trim();
|
|
777
|
+
const layoutType = extractLayoutHint(slide.content_direction || '');
|
|
778
|
+
logger.info(`Slide ${slideIndex + 1}: Generating code template "${layoutType}"`);
|
|
714
779
|
const jsonPrompt = buildContentFillJsonPrompt(slide.title, cleanedDirection, layoutType, language);
|
|
715
780
|
try {
|
|
716
781
|
const jsonRes = await llmClient.chatCompletion({
|
|
@@ -725,6 +790,7 @@ async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, tot
|
|
|
725
790
|
const jsonRaw = jsonMsg ? extractContent(jsonMsg) : '';
|
|
726
791
|
let slideData = parseContentFillJson(jsonRaw, layoutType);
|
|
727
792
|
if (!slideData) {
|
|
793
|
+
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
794
|
const retryRes = await llmClient.chatCompletion({
|
|
729
795
|
messages: [
|
|
730
796
|
{ role: 'system', content: jsonPrompt },
|
|
@@ -736,6 +802,9 @@ async function generateSingleSlideHtml(llmClient, slide, design, slideIndex, tot
|
|
|
736
802
|
const retryMsg = retryRes.choices[0]?.message;
|
|
737
803
|
const retryRaw = retryMsg ? extractContent(retryMsg) : '';
|
|
738
804
|
slideData = parseContentFillJson(retryRaw, layoutType);
|
|
805
|
+
if (!slideData) {
|
|
806
|
+
logger.warn(`Slide ${slideIndex + 1}: JSON fill parse failed (attempt 2). Raw length: ${retryRaw.length}. First 200 chars: ${retryRaw.slice(0, 200)}`);
|
|
807
|
+
}
|
|
739
808
|
}
|
|
740
809
|
if (slideData) {
|
|
741
810
|
const html = buildContentSlideHtml(design, slide.title, layoutType, slideData, slideIndex + 1, slideIndex);
|
|
@@ -797,6 +866,12 @@ async function generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstD
|
|
|
797
866
|
if (result) {
|
|
798
867
|
results.set(index, { index, html: result.html, isCodeTemplate: result.isCodeTemplate });
|
|
799
868
|
}
|
|
869
|
+
else {
|
|
870
|
+
const slide = plan.slides[index];
|
|
871
|
+
const fallbackHtml = buildFallbackSlideHtml(plan.design, slide.title, slide.content_direction || '', index + 1);
|
|
872
|
+
results.set(index, { index, html: fallbackHtml, isCodeTemplate: true });
|
|
873
|
+
logger.warn(`Slide ${index + 1}: Using fallback HTML for "${slide.title}"`);
|
|
874
|
+
}
|
|
800
875
|
}
|
|
801
876
|
const done = Math.min(batch + MAX_CONCURRENT, contentIndices.length);
|
|
802
877
|
if (phaseLogger)
|
|
@@ -823,38 +898,55 @@ async function validateAndRegenerate(llmClient, htmlResults, plan, language, pha
|
|
|
823
898
|
return htmlResults;
|
|
824
899
|
}
|
|
825
900
|
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
|
-
|
|
901
|
+
phaseLogger('powerpoint-create', 'validation', `${failedIndices.length} slides failed validation, regenerating in parallel...`);
|
|
902
|
+
for (let batch = 0; batch < failedIndices.length; batch += MAX_CONCURRENT) {
|
|
903
|
+
const chunk = failedIndices.slice(batch, batch + MAX_CONCURRENT);
|
|
904
|
+
const promises = chunk.map(async (index) => {
|
|
905
|
+
const slide = plan.slides[index];
|
|
906
|
+
const original = htmlResults.get(index);
|
|
907
|
+
try {
|
|
908
|
+
const result = await generateSingleSlideHtml(llmClient, slide, plan.design, index, plan.slides.length, language);
|
|
909
|
+
if (result) {
|
|
910
|
+
const regenHasPlaceholder = hasPlaceholderText(result.html);
|
|
911
|
+
const origHasPlaceholder = hasPlaceholderText(original.html);
|
|
912
|
+
if (!regenHasPlaceholder || origHasPlaceholder) {
|
|
913
|
+
return { index, result: { index, html: result.html, isCodeTemplate: result.isCodeTemplate } };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
catch (e) {
|
|
918
|
+
logger.warn(`Slide ${index + 1}: Regen error: ${e}`);
|
|
919
|
+
}
|
|
920
|
+
return { index, result: null };
|
|
921
|
+
});
|
|
922
|
+
const chunkResults = await Promise.all(promises);
|
|
923
|
+
for (const { index, result } of chunkResults) {
|
|
924
|
+
if (result) {
|
|
925
|
+
htmlResults.set(index, result);
|
|
836
926
|
}
|
|
837
927
|
}
|
|
838
928
|
}
|
|
839
929
|
return htmlResults;
|
|
840
930
|
}
|
|
841
|
-
|
|
931
|
+
const MAX_SCREENSHOT_CONCURRENT = 4;
|
|
932
|
+
async function assemblePresentation(htmlResults, plan, timestamp, savePath, companyName, language, phaseLogger, toolCallLogger) {
|
|
842
933
|
const { writePath: tempWritePath, winPath: tempWinPath } = getTempDir();
|
|
843
934
|
ensureTempDir(tempWritePath);
|
|
935
|
+
const totalSlides = htmlResults.size;
|
|
936
|
+
const sortedEntries = [...htmlResults.entries()].sort((a, b) => a[0] - b[0]);
|
|
844
937
|
const builtSlides = [];
|
|
845
|
-
let failCount = 0;
|
|
846
938
|
let totalToolCalls = 0;
|
|
847
939
|
const tempFiles = [];
|
|
848
|
-
|
|
940
|
+
if (phaseLogger)
|
|
941
|
+
phaseLogger('powerpoint-create', 'assembly', `Pre-creating ${totalSlides} slides...`);
|
|
942
|
+
for (let i = 1; i < totalSlides; i++) {
|
|
943
|
+
await powerpointClient.powerpointAddSlide(7);
|
|
944
|
+
totalToolCalls++;
|
|
945
|
+
}
|
|
946
|
+
const slideFiles = new Map();
|
|
947
|
+
let slideNum = 0;
|
|
849
948
|
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}`);
|
|
949
|
+
slideNum++;
|
|
858
950
|
const htmlFileName = `hanseol_slide_${slideNum}_${timestamp}.html`;
|
|
859
951
|
const pngFileName = `hanseol_slide_${slideNum}_${timestamp}.png`;
|
|
860
952
|
const htmlWritePath = path.join(tempWritePath, htmlFileName);
|
|
@@ -862,101 +954,202 @@ async function assemblePresentation(htmlResults, plan, timestamp, savePath, phas
|
|
|
862
954
|
const htmlWinPath = `${tempWinPath}\\${htmlFileName}`;
|
|
863
955
|
const pngWinPath = `${tempWinPath}\\${pngFileName}`;
|
|
864
956
|
try {
|
|
865
|
-
const viewportHtml = result.
|
|
866
|
-
? injectEdgeSizing(result.html, plan.design.background_color)
|
|
867
|
-
: injectViewportCss(result.html, plan.design.background_color);
|
|
957
|
+
const viewportHtml = injectEdgeSizing(result.html, plan.design.background_color);
|
|
868
958
|
const processed = injectTitleContrastFix(viewportHtml, plan.design.text_color);
|
|
869
959
|
fs.writeFileSync(htmlWritePath, processed, 'utf-8');
|
|
870
960
|
tempFiles.push(htmlWritePath);
|
|
961
|
+
slideFiles.set(index, { slideNum, htmlWritePath, pngWritePath, htmlWinPath, pngWinPath });
|
|
871
962
|
}
|
|
872
963
|
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
|
-
}
|
|
964
|
+
logger.warn(`Slide ${slideNum}: Failed to write HTML: ${e}`);
|
|
888
965
|
}
|
|
889
|
-
|
|
890
|
-
|
|
966
|
+
}
|
|
967
|
+
if (phaseLogger)
|
|
968
|
+
phaseLogger('powerpoint-create', 'assembly', `Rendering ${slideFiles.size} screenshots in parallel (batch ${MAX_SCREENSHOT_CONCURRENT})...`);
|
|
969
|
+
const screenshotEntries = [...slideFiles.entries()].sort((a, b) => a[0] - b[0]);
|
|
970
|
+
const screenshotSuccess = new Set();
|
|
971
|
+
for (let batch = 0; batch < screenshotEntries.length; batch += MAX_SCREENSHOT_CONCURRENT) {
|
|
972
|
+
const chunk = screenshotEntries.slice(batch, batch + MAX_SCREENSHOT_CONCURRENT);
|
|
973
|
+
const batchResults = await Promise.all(chunk.map(async ([index, files]) => {
|
|
974
|
+
let success = false;
|
|
891
975
|
try {
|
|
892
|
-
|
|
893
|
-
const retryRender = await powerpointClient.renderHtmlToImage(htmlWinPath, pngWinPath);
|
|
976
|
+
const result = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
|
|
894
977
|
totalToolCalls++;
|
|
895
|
-
|
|
978
|
+
success = result.success;
|
|
979
|
+
if (!success) {
|
|
980
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
981
|
+
const retry = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
|
|
982
|
+
totalToolCalls++;
|
|
983
|
+
success = retry.success;
|
|
984
|
+
}
|
|
896
985
|
}
|
|
897
986
|
catch {
|
|
898
|
-
|
|
987
|
+
try {
|
|
988
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
989
|
+
const retry = await powerpointClient.renderHtmlToImage(files.htmlWinPath, files.pngWinPath);
|
|
990
|
+
totalToolCalls++;
|
|
991
|
+
success = retry.success;
|
|
992
|
+
}
|
|
993
|
+
catch {
|
|
994
|
+
success = false;
|
|
995
|
+
}
|
|
899
996
|
}
|
|
997
|
+
try {
|
|
998
|
+
fs.unlinkSync(files.htmlWritePath);
|
|
999
|
+
}
|
|
1000
|
+
catch { }
|
|
1001
|
+
if (success) {
|
|
1002
|
+
try {
|
|
1003
|
+
const stat = fs.statSync(files.pngWritePath);
|
|
1004
|
+
if (stat.size < 15000) {
|
|
1005
|
+
logger.warn(`Slide ${files.slideNum}: Screenshot too small (${stat.size} bytes)`);
|
|
1006
|
+
success = false;
|
|
1007
|
+
try {
|
|
1008
|
+
fs.unlinkSync(files.pngWritePath);
|
|
1009
|
+
}
|
|
1010
|
+
catch { }
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
catch { }
|
|
1014
|
+
}
|
|
1015
|
+
if (success)
|
|
1016
|
+
tempFiles.push(files.pngWritePath);
|
|
1017
|
+
return { index, success };
|
|
1018
|
+
}));
|
|
1019
|
+
for (const { index, success } of batchResults) {
|
|
1020
|
+
if (success)
|
|
1021
|
+
screenshotSuccess.add(index);
|
|
900
1022
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1023
|
+
const done = Math.min(batch + MAX_SCREENSHOT_CONCURRENT, screenshotEntries.length);
|
|
1024
|
+
if (phaseLogger)
|
|
1025
|
+
phaseLogger('powerpoint-create', 'assembly', `Screenshots: ${done}/${screenshotEntries.length} done`);
|
|
1026
|
+
}
|
|
1027
|
+
if (phaseLogger)
|
|
1028
|
+
phaseLogger('powerpoint-create', 'assembly', `Filling ${totalSlides} slides...`);
|
|
1029
|
+
const unfilledSlideNums = [];
|
|
1030
|
+
for (const [index] of sortedEntries) {
|
|
1031
|
+
const files = slideFiles.get(index);
|
|
1032
|
+
const slidePlan = plan.slides[index];
|
|
1033
|
+
const htmlResult = htmlResults.get(index);
|
|
1034
|
+
if (!files) {
|
|
1035
|
+
const posInSorted = sortedEntries.findIndex(([idx]) => idx === index);
|
|
1036
|
+
const inferredSlideNum = posInSorted >= 0 ? posInSorted + 1 : -1;
|
|
1037
|
+
logger.warn(`Slide index ${index} (slideNum ${inferredSlideNum}): No file info, marking for deletion`);
|
|
1038
|
+
if (inferredSlideNum > 0)
|
|
1039
|
+
unfilledSlideNums.push(inferredSlideNum);
|
|
908
1040
|
continue;
|
|
909
1041
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1042
|
+
let filled = false;
|
|
1043
|
+
if (screenshotSuccess.has(index)) {
|
|
1044
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(files.slideNum, files.pngWinPath);
|
|
1045
|
+
totalToolCalls++;
|
|
1046
|
+
if (toolCallLogger)
|
|
1047
|
+
toolCallLogger('powerpoint-create', 'addFullSlideImage', { slideNum: files.slideNum, imagePath: files.pngWinPath }, bgResult.success ? 'OK' : 'Failed', bgResult.success, files.slideNum, totalToolCalls);
|
|
1048
|
+
filled = bgResult.success;
|
|
1049
|
+
}
|
|
1050
|
+
if (!filled) {
|
|
1051
|
+
logger.info(`Slide ${files.slideNum}: Primary screenshot failed, trying fallback rendering...`);
|
|
1052
|
+
const slide = plan.slides[index];
|
|
1053
|
+
const fallbackHtml = buildFallbackSlideHtml(plan.design, slide.title, slide.content_direction || '', files.slideNum);
|
|
1054
|
+
const fbHtmlName = `hanseol_fb_${files.slideNum}_${timestamp}.html`;
|
|
1055
|
+
const fbPngName = `hanseol_fb_${files.slideNum}_${timestamp}.png`;
|
|
1056
|
+
const fbHtmlWrite = path.join(tempWritePath, fbHtmlName);
|
|
1057
|
+
const fbPngWrite = path.join(tempWritePath, fbPngName);
|
|
1058
|
+
const fbHtmlWin = `${tempWinPath}\\${fbHtmlName}`;
|
|
1059
|
+
const fbPngWin = `${tempWinPath}\\${fbPngName}`;
|
|
1060
|
+
try {
|
|
1061
|
+
const viewportHtml = injectEdgeSizing(fallbackHtml, plan.design.background_color);
|
|
1062
|
+
fs.writeFileSync(fbHtmlWrite, viewportHtml, 'utf-8');
|
|
1063
|
+
const fbResult = await powerpointClient.renderHtmlToImage(fbHtmlWin, fbPngWin);
|
|
1064
|
+
totalToolCalls++;
|
|
915
1065
|
try {
|
|
916
|
-
fs.unlinkSync(
|
|
1066
|
+
fs.unlinkSync(fbHtmlWrite);
|
|
1067
|
+
}
|
|
1068
|
+
catch { }
|
|
1069
|
+
if (fbResult.success) {
|
|
1070
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(files.slideNum, fbPngWin);
|
|
1071
|
+
totalToolCalls++;
|
|
1072
|
+
filled = bgResult.success;
|
|
1073
|
+
try {
|
|
1074
|
+
fs.unlinkSync(fbPngWrite);
|
|
1075
|
+
}
|
|
1076
|
+
catch { }
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
catch (e) {
|
|
1080
|
+
logger.warn(`Slide ${files.slideNum}: Fallback rendering also failed: ${e}`);
|
|
1081
|
+
try {
|
|
1082
|
+
fs.unlinkSync(fbHtmlWrite);
|
|
917
1083
|
}
|
|
918
1084
|
catch { }
|
|
919
|
-
continue;
|
|
920
1085
|
}
|
|
921
1086
|
}
|
|
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})`);
|
|
1087
|
+
if (filled) {
|
|
1088
|
+
builtSlides.push(`Slide ${files.slideNum}: ${slidePlan.title} (${slidePlan.type})`);
|
|
937
1089
|
try {
|
|
938
|
-
await powerpointClient.powerpointAddNote(slideNum,
|
|
1090
|
+
await powerpointClient.powerpointAddNote(files.slideNum, htmlResult.html);
|
|
939
1091
|
}
|
|
940
1092
|
catch { }
|
|
941
1093
|
}
|
|
942
1094
|
else {
|
|
943
|
-
logger.warn(`Slide ${slideNum}:
|
|
944
|
-
|
|
1095
|
+
logger.warn(`Slide ${files.slideNum}: All rendering attempts failed, marking for deletion`);
|
|
1096
|
+
unfilledSlideNums.push(files.slideNum);
|
|
945
1097
|
}
|
|
946
1098
|
}
|
|
947
|
-
if (
|
|
1099
|
+
if (unfilledSlideNums.length > 0) {
|
|
1100
|
+
const sortedDesc = [...unfilledSlideNums].sort((a, b) => b - a);
|
|
1101
|
+
for (const slideNum of sortedDesc) {
|
|
1102
|
+
try {
|
|
1103
|
+
await powerpointClient.powerpointDeleteSlide(slideNum);
|
|
1104
|
+
totalToolCalls++;
|
|
1105
|
+
logger.info(`Deleted unfilled slide ${slideNum}`);
|
|
1106
|
+
}
|
|
1107
|
+
catch (e) {
|
|
1108
|
+
logger.warn(`Failed to delete unfilled slide ${slideNum}: ${e}`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
const hasClosingInPlan = plan.slides.some(s => s.type === 'closing');
|
|
1113
|
+
const hasClosingBuilt = builtSlides.some(s => s.includes('(closing)'));
|
|
1114
|
+
if (hasClosingInPlan && !hasClosingBuilt && builtSlides.length > 0) {
|
|
1115
|
+
logger.info('Closing slide was lost during assembly — adding it now');
|
|
948
1116
|
try {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1117
|
+
await powerpointClient.powerpointAddSlide(7);
|
|
1118
|
+
totalToolCalls++;
|
|
1119
|
+
const slideCountRes = await powerpointClient.powerpointGetSlideCount();
|
|
1120
|
+
const newSlideNum = slideCountRes['slide_count'] || builtSlides.length + 1;
|
|
1121
|
+
const closingPlan = plan.slides.find(s => s.type === 'closing');
|
|
1122
|
+
const closingTagline = (closingPlan.content_direction || '').replace(/감사합니다|thank\s*you/gi, '').trim() || undefined;
|
|
1123
|
+
const closingHtml = buildClosingSlideHtml(plan.design, companyName, newSlideNum, language, closingTagline);
|
|
1124
|
+
const clHtmlName = `hanseol_closing_${timestamp}.html`;
|
|
1125
|
+
const clPngName = `hanseol_closing_${timestamp}.png`;
|
|
1126
|
+
const clHtmlWrite = path.join(tempWritePath, clHtmlName);
|
|
1127
|
+
const clPngWrite = path.join(tempWritePath, clPngName);
|
|
1128
|
+
const clHtmlWin = `${tempWinPath}\\${clHtmlName}`;
|
|
1129
|
+
const clPngWin = `${tempWinPath}\\${clPngName}`;
|
|
1130
|
+
const viewportHtml = injectEdgeSizing(closingHtml, plan.design.background_color);
|
|
1131
|
+
fs.writeFileSync(clHtmlWrite, viewportHtml, 'utf-8');
|
|
1132
|
+
const ssResult = await powerpointClient.renderHtmlToImage(clHtmlWin, clPngWin);
|
|
1133
|
+
totalToolCalls++;
|
|
1134
|
+
try {
|
|
1135
|
+
fs.unlinkSync(clHtmlWrite);
|
|
1136
|
+
}
|
|
1137
|
+
catch { }
|
|
1138
|
+
if (ssResult.success) {
|
|
1139
|
+
const bgResult = await powerpointClient.powerpointAddFullSlideImage(newSlideNum, clPngWin);
|
|
1140
|
+
totalToolCalls++;
|
|
1141
|
+
if (bgResult.success) {
|
|
1142
|
+
builtSlides.push(`Slide ${newSlideNum}: ${closingPlan.title} (closing)`);
|
|
1143
|
+
logger.info('Closing slide added successfully');
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
fs.unlinkSync(clPngWrite);
|
|
955
1147
|
}
|
|
1148
|
+
catch { }
|
|
956
1149
|
}
|
|
957
1150
|
}
|
|
958
1151
|
catch (e) {
|
|
959
|
-
logger.warn(`Failed to
|
|
1152
|
+
logger.warn(`Failed to add closing slide: ${e}`);
|
|
960
1153
|
}
|
|
961
1154
|
}
|
|
962
1155
|
if (builtSlides.length > 0) {
|
|
@@ -1069,7 +1262,8 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1069
1262
|
if (/로고|슬로건|연락처|contact|logo|placeholder/i.test(titleSubtitle)) {
|
|
1070
1263
|
titleSubtitle = '';
|
|
1071
1264
|
}
|
|
1072
|
-
const
|
|
1265
|
+
const cleanInstruction = instruction.replace(/\*\*/g, '');
|
|
1266
|
+
const companyMatch = cleanInstruction.match(/회사명\s*[::]?\s*([^\s,,、]+)/);
|
|
1073
1267
|
if (companyMatch && companyMatch[1]) {
|
|
1074
1268
|
const companyName_ = companyMatch[1];
|
|
1075
1269
|
if (titleSlidePlan && titleSlidePlan.title.trim() !== companyName_) {
|
|
@@ -1083,13 +1277,14 @@ async function runStructured(llmClient, instruction, explicitSavePath) {
|
|
|
1083
1277
|
}
|
|
1084
1278
|
}
|
|
1085
1279
|
}
|
|
1280
|
+
rebalanceLayouts(plan);
|
|
1086
1281
|
if (phaseLogger)
|
|
1087
1282
|
phaseLogger('powerpoint-create', 'html-generation', 'Starting parallel HTML generation...');
|
|
1088
1283
|
const htmlResults = await generateAllHtml(llmClient, plan, companyName, titleSubtitle, kstDate, language, phaseLogger);
|
|
1089
1284
|
const validatedResults = await validateAndRegenerate(llmClient, htmlResults, plan, language, phaseLogger);
|
|
1090
1285
|
if (phaseLogger)
|
|
1091
1286
|
phaseLogger('powerpoint-create', 'assembly', `Assembling ${validatedResults.size} slides into PowerPoint...`);
|
|
1092
|
-
const { builtSlides, totalToolCalls } = await assemblePresentation(validatedResults, plan, timestamp, savePath, phaseLogger, toolCallLogger);
|
|
1287
|
+
const { builtSlides, totalToolCalls } = await assemblePresentation(validatedResults, plan, timestamp, savePath, companyName, language, phaseLogger, toolCallLogger);
|
|
1093
1288
|
const duration = Date.now() - startTime;
|
|
1094
1289
|
const slideList = builtSlides.join('\n');
|
|
1095
1290
|
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}`;
|