- 등록 모델 실행 프로파일을 검증 게이트, 문서 fallback, post-tool verification까지 확장 적용 - Cowork/Code 진행 카드에 계획/도구/검증/압축/폴백/재시도 단계 메타를 추가해 대기 상태 가시성 강화 - OpenAI/vLLM tool 요청에 병렬 도구 호출 힌트를 추가하고 회귀 프롬프트 문서를 프로파일 기준으로 전면 정리 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
133 lines
4.7 KiB
JavaScript
133 lines
4.7 KiB
JavaScript
const JSZip = require('jszip');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// EMU to inches: 1 inch = 914400 EMU
|
|
// Slide: 9144000 x 5143500 EMU = 10" x 5.625"
|
|
const EMU = 914400;
|
|
const SLIDE_W = 9144000;
|
|
const SLIDE_H = 5143500;
|
|
const SCALE = 960 / 10; // 96px per inch (at 100%)
|
|
|
|
function emuToPx(emu) {
|
|
return (emu / EMU) * SCALE;
|
|
}
|
|
|
|
function hexToRgb(hex) {
|
|
if (!hex || hex.length < 6) return '#888888';
|
|
return '#' + hex.slice(-6);
|
|
}
|
|
|
|
async function renderSlideHtml(xml, slideNum) {
|
|
// Extract background color
|
|
const bgMatch = xml.match(/p:bg>.*?<a:srgbClr val="([A-Fa-f0-9]{6})"/s);
|
|
const bgColor = bgMatch ? '#' + bgMatch[1] : '#F5F7FA';
|
|
|
|
// Parse all shapes and text boxes
|
|
const shapes = [];
|
|
|
|
// Find all spTree child elements (sp = shape/textbox, pic = image)
|
|
const spPattern = /<p:sp>(.+?)<\/p:sp>/gs;
|
|
let spMatch;
|
|
while ((spMatch = spPattern.exec(xml)) !== null) {
|
|
const spXml = spMatch[1];
|
|
|
|
// Get position
|
|
const offMatch = spXml.match(/<a:off x="(-?\d+)" y="(-?\d+)"/);
|
|
const extMatch = spXml.match(/<a:ext cx="(\d+)" cy="(\d+)"/);
|
|
if (!offMatch || !extMatch) continue;
|
|
|
|
const x = emuToPx(parseInt(offMatch[1]));
|
|
const y = emuToPx(parseInt(offMatch[2]));
|
|
const w = emuToPx(parseInt(extMatch[1]));
|
|
const h = emuToPx(parseInt(extMatch[2]));
|
|
|
|
// Get fill color
|
|
const fillMatch = spXml.match(/p:spPr[\s\S]*?<a:srgbClr val="([A-Fa-f0-9]{6})"/);
|
|
const fillColor = fillMatch ? '#' + fillMatch[1] : null;
|
|
|
|
// Get text content
|
|
const texts = [];
|
|
const paraPattern = /<a:p>([\s\S]*?)<\/a:p>/g;
|
|
let paraMatch;
|
|
while ((paraMatch = paraPattern.exec(spXml)) !== null) {
|
|
const paraXml = paraMatch[1];
|
|
const textMatches = [...paraXml.matchAll(/<a:t[^>]*>([^<]*)<\/a:t>/g)];
|
|
const paraText = textMatches.map(m => m[1]).join('');
|
|
if (paraText.trim()) texts.push(paraText.trim());
|
|
}
|
|
|
|
// Get font size
|
|
const szMatch = spXml.match(/sz="(\d+)"/);
|
|
const fontSize = szMatch ? parseInt(szMatch[1]) / 100 : 12;
|
|
|
|
// Get text color
|
|
const txtColorMatch = spXml.match(/<a:t>[\s\S]*?<a:srgbClr val="([A-Fa-f0-9]{6})"/);
|
|
const txtColor = txtColorMatch ? '#' + txtColorMatch[1] : '#FFFFFF';
|
|
|
|
shapes.push({ x, y, w, h, fillColor, texts, fontSize, txtColor });
|
|
}
|
|
|
|
// Build HTML
|
|
let shapesHtml = '';
|
|
for (const s of shapes) {
|
|
const bgStyle = s.fillColor ? `background-color: ${s.fillColor};` : '';
|
|
const textContent = s.texts.join('<br>');
|
|
shapesHtml += `<div style="position:absolute;left:${s.x.toFixed(1)}px;top:${s.y.toFixed(1)}px;width:${s.w.toFixed(1)}px;height:${s.h.toFixed(1)}px;${bgStyle}overflow:hidden;box-sizing:border-box;padding:2px 4px;">
|
|
<div style="font-size:${Math.min(s.fontSize, 20)}px;color:${s.txtColor};font-family:Arial,sans-serif;overflow:hidden;">${textContent}</div>
|
|
</div>`;
|
|
}
|
|
|
|
return `<div style="position:relative;width:960px;height:540px;background-color:${bgColor};overflow:hidden;border:1px solid #333;flex-shrink:0;">
|
|
<div style="position:absolute;top:2px;right:4px;font-size:10px;color:rgba(128,128,128,0.5);z-index:999">${slideNum}</div>
|
|
${shapesHtml}
|
|
</div>`;
|
|
}
|
|
|
|
async function createPreview(pptxPath, outputPath) {
|
|
const data = fs.readFileSync(pptxPath);
|
|
const zip = await JSZip.loadAsync(data);
|
|
|
|
const slideFiles = Object.keys(zip.files)
|
|
.filter(f => f.match(/^ppt\/slides\/slide\d+\.xml$/))
|
|
.sort((a, b) => {
|
|
const na = parseInt(a.match(/slide(\d+)/)[1]);
|
|
const nb = parseInt(b.match(/slide(\d+)/)[1]);
|
|
return na - nb;
|
|
});
|
|
|
|
let slidesHtml = '';
|
|
for (const sf of slideFiles) {
|
|
const slideNum = parseInt(sf.match(/slide(\d+)/)[1]);
|
|
const xml = await zip.file(sf).async('string');
|
|
slidesHtml += await renderSlideHtml(xml, slideNum);
|
|
}
|
|
|
|
const html = `<!DOCTYPE html>
|
|
<html><head><meta charset="utf-8">
|
|
<title>${path.basename(pptxPath)}</title>
|
|
<style>
|
|
body { background: #1a1a1a; font-family: Arial; margin: 0; padding: 20px; }
|
|
h2 { color: #fff; font-size: 14px; margin-bottom: 10px; }
|
|
.slides { display: flex; flex-direction: column; gap: 16px; align-items: flex-start; }
|
|
</style>
|
|
</head><body>
|
|
<h2>${path.basename(pptxPath)}</h2>
|
|
<div class="slides">${slidesHtml}</div>
|
|
</body></html>`;
|
|
|
|
fs.writeFileSync(outputPath, html);
|
|
console.log(`Preview: ${outputPath} (${slideFiles.length} slides)`);
|
|
}
|
|
|
|
(async () => {
|
|
await createPreview(
|
|
'E:/test/삼성디스플레이 vs LG디스플레이 비교 분석 보고서_20260407_1958.pptx',
|
|
'C:/Users/admin/AppData/Local/Temp/pptx_gen/preview1.html'
|
|
);
|
|
await createPreview(
|
|
'E:/test/삼성디스플레이 사업 영역 및 제품 강점 분석 보고서_20260407_1956.pptx',
|
|
'C:/Users/admin/AppData/Local/Temp/pptx_gen/preview2.html'
|
|
);
|
|
})();
|