v1.0.4
This commit is contained in:
+48
-10
@@ -131,6 +131,12 @@ async function generateWithPython(spec: PresentationSpec, workspacePath: string)
|
||||
try {
|
||||
// Write spec to temp file
|
||||
fs.writeFileSync(tmpSpecPath, JSON.stringify(spec), 'utf-8');
|
||||
console.log(`[pptx] generateWithPython: workspace=${workspacePath} slides=${spec.slides.length} title="${spec.title}"`);
|
||||
for (const [i, s] of spec.slides.entries()) {
|
||||
if (s.image_path || s.image_url) {
|
||||
console.log(`[pptx] generateWithPython: slide ${i + 1} type=${s.type} image_path="${s.image_path}" image_url="${s.image_url?.slice(0, 60) || ''}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await new Promise<{ success: boolean; path?: string; folder?: string; filename?: string; slides?: number; warnings?: string[]; stdout?: string; error?: string; download_url?: string; preview_url?: string }>((resolve, reject) => {
|
||||
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
|
||||
@@ -142,7 +148,7 @@ async function generateWithPython(spec: PresentationSpec, workspacePath: string)
|
||||
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
||||
}, (err, stdout, stderr) => {
|
||||
if (stderr) {
|
||||
console.error(`[pptx] Python stderr: ${stderr.slice(0, 800)}`);
|
||||
console.error(`[pptx] Python stderr: ${stderr.slice(0, 3000)}`);
|
||||
}
|
||||
if (err) {
|
||||
console.error(`[pptx] Python process failed: ${err.message}`);
|
||||
@@ -230,7 +236,8 @@ async function downloadImageByUrl(url: string, dest: string): Promise<boolean> {
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
'Accept': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
|
||||
// Prefer JPEG/PNG over WebP/AVIF — PowerPoint doesn't support WebP/AVIF
|
||||
'Accept': 'image/jpeg,image/png,image/gif,image/*,*/*;q=0.8',
|
||||
'Sec-Fetch-Dest': 'image',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
@@ -239,12 +246,28 @@ async function downloadImageByUrl(url: string, dest: string): Promise<boolean> {
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
if (!resp.ok) return false;
|
||||
if (!resp.ok) {
|
||||
console.log(`[pptx] downloadImageByUrl: HTTP ${resp.status} for ${url.slice(0, 80)}`);
|
||||
return false;
|
||||
}
|
||||
const buffer = Buffer.from(await resp.arrayBuffer());
|
||||
if (buffer.length < 100) return false;
|
||||
if (buffer.length < 100) {
|
||||
console.log(`[pptx] downloadImageByUrl: too small (${buffer.length} bytes) for ${url.slice(0, 80)}`);
|
||||
return false;
|
||||
}
|
||||
const ct = resp.headers.get('content-type') || '';
|
||||
// Detect actual format from magic bytes
|
||||
let actualFmt = 'unknown';
|
||||
if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF) actualFmt = 'jpeg';
|
||||
else if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47) actualFmt = 'png';
|
||||
else if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) actualFmt = 'gif';
|
||||
else if (buffer.length > 8 && buffer.slice(4, 12).toString('ascii') === 'ftypavif') actualFmt = 'avif';
|
||||
else if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46) actualFmt = 'webp';
|
||||
console.log(`[pptx] downloadImageByUrl: ${buffer.length} bytes, ct="${ct}", magic=${actualFmt}, dest=${path.basename(dest)}`);
|
||||
fs.writeFileSync(dest, buffer);
|
||||
return true;
|
||||
} catch {
|
||||
} catch (e: any) {
|
||||
console.log(`[pptx] downloadImageByUrl: failed: ${e.message} for ${url.slice(0, 80)}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -338,6 +361,7 @@ const FREE_IMAGE_APIS = [
|
||||
];
|
||||
|
||||
async function searchAndDownloadImage(query: string, projectDir: string, slideIndex: number, sendSSE?: SSESender): Promise<string | null> {
|
||||
console.log(`[pptx] searchAndDownloadImage: slide ${slideIndex + 1}, query="${query}", dir=${projectDir}`);
|
||||
sendSSE?.('info', { message: `🖼 Slide ${slideIndex + 1}: searching for "${query}"...` });
|
||||
|
||||
// Strategy 1: Browser search (most reliable — handles JS, anti-bot, etc.)
|
||||
@@ -348,10 +372,11 @@ async function searchAndDownloadImage(query: string, projectDir: string, slideIn
|
||||
const dest = path.join(projectDir, fname);
|
||||
sendSSE?.('info', { message: `📥 Slide ${slideIndex + 1}: downloading from browser result...` });
|
||||
if (await downloadImageByUrl(browserUrl, dest)) {
|
||||
console.log(`[pptx] Downloaded browser image for slide ${slideIndex + 1}: ${browserUrl.slice(0, 80)}`);
|
||||
console.log(`[pptx] searchAndDownloadImage: slide ${slideIndex + 1} browser OK, fname=${fname}`);
|
||||
sendSSE?.('info', { message: `✅ Slide ${slideIndex + 1}: image downloaded (browser)` });
|
||||
return fname;
|
||||
}
|
||||
console.log(`[pptx] searchAndDownloadImage: slide ${slideIndex + 1} browser download failed`);
|
||||
sendSSE?.('info', { message: `⚠️ Slide ${slideIndex + 1}: browser download failed, trying API...` });
|
||||
}
|
||||
|
||||
@@ -364,10 +389,11 @@ async function searchAndDownloadImage(query: string, projectDir: string, slideIn
|
||||
const fname = `slide${slideIndex + 1}_image.jpg`;
|
||||
const dest = path.join(projectDir, fname);
|
||||
if (await downloadImageByUrl(url, dest)) {
|
||||
console.log(`[pptx] Downloaded picsum image for slide ${slideIndex + 1}`);
|
||||
console.log(`[pptx] searchAndDownloadImage: slide ${slideIndex + 1} picsum OK`);
|
||||
sendSSE?.('info', { message: `✅ Slide ${slideIndex + 1}: image downloaded (picsum)` });
|
||||
return fname;
|
||||
}
|
||||
console.log(`[pptx] searchAndDownloadImage: slide ${slideIndex + 1} picsum failed`);
|
||||
sendSSE?.('info', { message: `⚠️ Slide ${slideIndex + 1}: picsum download failed` });
|
||||
continue;
|
||||
}
|
||||
@@ -397,10 +423,11 @@ async function searchAndDownloadImage(query: string, projectDir: string, slideIn
|
||||
const dest = path.join(projectDir, fname);
|
||||
sendSSE?.('info', { message: `📥 Slide ${slideIndex + 1}: downloading from ${api.name}...` });
|
||||
if (await downloadImageByUrl(imageUrl, dest)) {
|
||||
console.log(`[pptx] Downloaded ${api.name} image for slide ${slideIndex + 1}`);
|
||||
console.log(`[pptx] searchAndDownloadImage: slide ${slideIndex + 1} ${api.name} OK, fname=${fname}`);
|
||||
sendSSE?.('info', { message: `✅ Slide ${slideIndex + 1}: image downloaded (${api.name})` });
|
||||
return fname;
|
||||
}
|
||||
console.log(`[pptx] searchAndDownloadImage: slide ${slideIndex + 1} ${api.name} download failed`);
|
||||
sendSSE?.('info', { message: `⚠️ Slide ${slideIndex + 1}: ${api.name} download failed` });
|
||||
} catch (e: any) {
|
||||
console.log(`[pptx] ${api.name} search failed for "${query}": ${e.message}`);
|
||||
@@ -438,7 +465,7 @@ interface PresentationSpec {
|
||||
|
||||
export const pptxTool: import('./registry.js').Tool = {
|
||||
name: 'create_presentation',
|
||||
description: 'Generate a PowerPoint (.pptx) file with slides. Use image_search on slides to auto-find images by keyword (e.g. "golden retriever"). Use image_url for direct URLs. Keep slides to 6 or fewer per call. NEVER write Python scripts to create PPTX. Missing images become light-gray placeholders.',
|
||||
description: 'Generate a PowerPoint (.pptx) file with slides. Use image_search on slides to auto-find images by keyword (e.g. "golden retriever"). Use image_url for direct URLs. For very long presentations (10+ slides), consider splitting across multiple calls using edit_presentation. NEVER write Python scripts to create PPTX. Missing images become light-gray placeholders.',
|
||||
schema: {
|
||||
spec: 'JSON object: { filename, title, template, theme, font_sizes, slides: [{ type, title, subtitle, bullets, body, content, font_size, image_path, image_url, image_search, background, template }] }',
|
||||
},
|
||||
@@ -515,6 +542,7 @@ export const pptxTool: import('./registry.js').Tool = {
|
||||
if (slidesNeedingImages.length > 0) {
|
||||
const projectSlug = (spec.title || 'presentation').replace(/[^a-zA-Z0-9가-힣_\-]/g, '_').toLowerCase().slice(0, 60);
|
||||
const projectDir = path.join(workspacePath, projectSlug);
|
||||
console.log(`[pptx] image_search: resolving ${slidesNeedingImages.length} slides, projectDir=${projectDir}`);
|
||||
if (!fs.existsSync(projectDir)) {
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
}
|
||||
@@ -523,20 +551,30 @@ export const pptxTool: import('./registry.js').Tool = {
|
||||
const idx = spec.slides.indexOf(slide);
|
||||
const sendSSE = (args as any)?._sendSSE as SSESender | undefined;
|
||||
const localPath = await searchAndDownloadImage(slide.image_search, projectDir, idx, sendSSE);
|
||||
console.log(`[pptx] image_search: slide ${idx + 1} keyword="${slide.image_search}" -> localPath="${localPath}"`);
|
||||
if (localPath) {
|
||||
const fullPath = path.join(projectDir, localPath);
|
||||
console.log(`[pptx] image_search: file exists=${fs.existsSync(fullPath)} size=${fs.statSync(fullPath).size} at ${fullPath}`);
|
||||
slide.image_path = localPath;
|
||||
}
|
||||
delete slide.image_search;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: log final slide specs for image slides
|
||||
for (const [i, s] of spec.slides.entries()) {
|
||||
if (s.image_path || s.image_url || s.image_search) {
|
||||
console.log(`[pptx] slide ${i + 1}: type=${s.type} image_path="${s.image_path}" image_url="${s.image_url}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return await generateWithPython(spec, workspacePath);
|
||||
},
|
||||
};
|
||||
|
||||
export const editPptxTool: import('./registry.js').Tool = {
|
||||
name: 'edit_presentation',
|
||||
description: 'Append slides to an existing .pptx file. Provide the path and new slides. Use image_search to auto-find images. Keep slides to 6 or fewer per call.',
|
||||
description: 'Append slides to an existing .pptx file. Provide the path and new slides. Use image_search to auto-find images. For very long additions (10+ slides), consider splitting across multiple calls.',
|
||||
schema: {
|
||||
path: 'Path to existing .pptx file (relative to workspace or absolute)',
|
||||
spec: 'JSON object: { slides: [{ type, title, subtitle, bullets, body, content, font_size, image_path, image_url, image_search, background, template }] }',
|
||||
|
||||
Reference in New Issue
Block a user