-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebpost-haiku-images-module.js
More file actions
391 lines (337 loc) Β· 11.9 KB
/
Copy pathwebpost-haiku-images-module.js
File metadata and controls
391 lines (337 loc) Β· 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
/**
* WEBPOST-HAIKU-IMAGES MODULE
*
* Simple + Complete: Brave search β Haiku post β Haiku image prompts β SVG/HTML rendering
*
* Generates images using Haiku itself (no external image APIs):
* 1. Haiku creates compelling post
* 2. Haiku generates image descriptions (prompts)
* 3. Render as SVG/HTML carousel frames (deterministic)
*
* Usage:
* runWebPostHaikuImages("Chile tech startups")
* β Brave search β Haiku post β Haiku image prompts β SVG carousel
*/
const axios = require('axios');
const BRAVE_API_KEY = process.env.BRAVE_SEARCH_API_KEY;
const HAIKU_API_KEY = process.env.API_KEY;
class WebPostHaikuImages {
constructor() {
this.model = 'claude-haiku-4-5'; // Fastest Claude
}
/**
* Search articles with Brave API
*/
async searchBrave(query, count = 5) {
if (!BRAVE_API_KEY) {
console.log('β οΈ BRAVE_SEARCH_API_KEY not set, using mock search');
return this.mockSearch(query);
}
try {
const response = await axios.get('https://api.search.brave.com/res/v1/web/search', {
params: {
q: query,
count: Math.min(count, 10),
},
headers: {
'Accept': 'application/json',
'X-Subscription-Token': BRAVE_API_KEY,
},
});
const results = response.data.web || [];
return results.map(r => ({
title: r.title,
description: r.description,
url: r.url,
}));
} catch (error) {
console.error('β Brave search error:', error.message);
return this.mockSearch(query);
}
}
/**
* Mock search for testing
*/
mockSearch(query) {
return [
{
title: `${query} - Latest Developments`,
description: `Recent updates and innovations in ${query}. Industry leaders share insights.`,
url: `https://example.com/${query.replace(/\s+/g, '-')}`,
},
{
title: `Market Analysis: ${query} Trends 2026`,
description: `Deep dive into current market dynamics affecting ${query}.`,
url: `https://example.com/analysis`,
},
{
title: `${query}: What's Next?`,
description: `Expert predictions on future direction and innovation.`,
url: `https://example.com/predictions`,
},
];
}
/**
* Generate post + image prompts with Claude Haiku
* Single API call for both
*/
async generatePostAndImagePrompts(query, articles) {
if (!HAIKU_API_KEY) {
throw new Error('β ANTHROPIC_API_KEY not set');
}
const articlesToUse = articles.slice(0, 3);
const sourcesText = articlesToUse
.map((a, i) => `${i + 1}. "${a.title}" - ${a.description}`)
.join('\n');
const prompt = `Create an Instagram post + 3 image descriptions for: "${query}"
Based on these articles:
${sourcesText}
OUTPUT FORMAT (use exactly this format):
POST:
[Write a compelling Instagram post with hook, 3 insights, CTA, and 2-3 emojis. Max 250 words]
IMAGE-PROMPT-1:
[Description for first carousel slide image (vivid, visual, one sentence)]
IMAGE-PROMPT-2:
[Description for second carousel slide image (vivid, visual, one sentence)]
IMAGE-PROMPT-3:
[Description for third carousel slide image (vivid, visual, one sentence)]
Requirements:
- Post: Professional, conversational tone
- Image prompts: Visual, descriptive, single sentence each
- Style: Modern, professional, suitable for Instagram`;
try {
const startTime = Date.now();
const response = await axios.post(
'https://api.anthropic.com/v1/messages',
{
model: this.model,
max_tokens: 800,
messages: [
{
role: 'user',
content: prompt,
},
],
},
{
headers: {
'x-api-key': HAIKU_API_KEY,
'anthropic-version': '2023-06-01',
},
}
);
const elapsed = Date.now() - startTime;
const fullContent = response.data.content?.[0]?.text || '';
const tokensUsed = response.data.usage?.input_tokens + response.data.usage?.output_tokens;
// Parse post and image prompts from response
const parsed = this.parsePostAndPrompts(fullContent);
console.log(`β
Haiku post + image prompts generated (${tokensUsed} tokens, ${elapsed}ms)`);
return {
post: parsed.post,
imagePrompts: parsed.imagePrompts,
tokensUsed,
model: this.model,
elapsed,
fullContent,
};
} catch (error) {
console.error('β Haiku error:', error.response?.data || error.message);
throw error;
}
}
/**
* Parse Haiku response into post + image prompts
*/
parsePostAndPrompts(content) {
const postMatch = content.match(/POST:\s*\n([\s\S]*?)(?=\n\nIMAGE-PROMPT|$)/);
const prompt1Match = content.match(/IMAGE-PROMPT-1:\s*\n([\s\S]*?)(?=\n\nIMAGE-PROMPT-2|$)/);
const prompt2Match = content.match(/IMAGE-PROMPT-2:\s*\n([\s\S]*?)(?=\n\nIMAGE-PROMPT-3|$)/);
const prompt3Match = content.match(/IMAGE-PROMPT-3:\s*\n([\s\S]*?)(?=\n\n|$)/);
return {
post: postMatch ? postMatch[1].trim() : content,
imagePrompts: [
prompt1Match ? prompt1Match[1].trim() : 'Modern professional design',
prompt2Match ? prompt2Match[1].trim() : 'Contemporary business concept',
prompt3Match ? prompt3Match[1].trim() : 'Innovation and growth visualization',
],
};
}
/**
* Create SVG carousel frames from image prompts
* Deterministic rendering (no external APIs needed)
*/
createSvgFrames(imagePrompts, postTitle) {
return imagePrompts.map((prompt, idx) => {
// Create a colorful SVG frame with text (deterministic, no API calls)
const colors = [
{ bg: '#1e293b', accent: '#3b82f6' }, // dark blue
{ bg: '#1e1b4b', accent: '#8b5cf6' }, // dark purple
{ bg: '#0f172a', accent: '#ec4899' }, // dark pink
];
const color = colors[idx];
return {
id: `frame-${idx + 1}`,
svgData: `<?xml version="1.0" encoding="UTF-8"?>
<svg width="1080" height="1920" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad${idx}" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:${color.bg};stop-opacity:1" />
<stop offset="100%" style="stop-color:${color.accent};stop-opacity:0.1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="1080" height="1920" fill="url(#grad${idx})" />
<!-- Title -->
<text x="540" y="200" font-size="56" font-weight="bold" fill="white" text-anchor="middle" font-family="Arial">
${postTitle.substring(0, 30)}...
</text>
<!-- Image description (visual placeholder) -->
<circle cx="540" cy="900" r="300" fill="${color.accent}" opacity="0.3" />
<circle cx="540" cy="900" r="280" fill="none" stroke="${color.accent}" stroke-width="2" opacity="0.6" />
<!-- Content text -->
<text x="540" y="1200" font-size="32" fill="white" text-anchor="middle" font-family="Arial" font-weight="500">
${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}
</text>
<!-- Counter -->
<text x="540" y="1800" font-size="24" fill="${color.accent}" text-anchor="middle" font-family="Arial">
${idx + 1} / 3
</text>
</svg>`,
prompt,
};
});
}
/**
* Generate HTML carousel (alternative to SVG, more stylish)
*/
createHtmlCarouselFrames(imagePrompts, postTitle) {
return imagePrompts.map((prompt, idx) => {
const gradients = [
'from-blue-600 to-purple-600',
'from-purple-600 to-pink-600',
'from-pink-600 to-orange-600',
];
return {
id: `carousel-${idx + 1}`,
html: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { margin: 0; padding: 0; }
.carousel-frame { width: 1080px; height: 1920px; }
</style>
</head>
<body class="bg-gradient-to-br ${gradients[idx]}">
<div class="carousel-frame flex flex-col items-center justify-center text-white p-8">
<!-- Title -->
<h1 class="text-6xl font-bold mb-12 text-center leading-tight">
${postTitle.substring(0, 25)}
</h1>
<!-- Visual Area -->
<div class="w-80 h-80 rounded-full border-4 border-white/30 flex items-center justify-center mb-12 bg-white/5">
<div class="text-center px-6">
<p class="text-2xl font-semibold">${prompt.substring(0, 40)}</p>
<p class="text-lg mt-2 opacity-75">${prompt.substring(40, 80)}</p>
</div>
</div>
<!-- Counter -->
<div class="mt-auto bg-white/10 px-8 py-4 rounded-full">
<p class="text-2xl font-light">${idx + 1} / 3</p>
</div>
</div>
</body>
</html>`,
prompt,
};
});
}
/**
* Main command: /webpost-haiku-images <query>
*/
async run(query, options = {}) {
const {
imageFormat = 'html', // 'html' or 'svg'
imageCount = 3,
} = options;
if (!query || query.trim().length === 0) {
return {
success: false,
error: 'Usage: /webpost-haiku-images <topic>\nExample: /webpost-haiku-images Chile tech startups',
};
}
const startTime = Date.now();
const result = {
query,
search: null,
post: null,
imagePrompts: null,
images: null,
tokens: {},
elapsed: 0,
};
try {
console.log(`\nπ /webpost-haiku-images "${query}"`);
// Step 1: Search (0 tokens)
console.log('π‘ Searching with Brave API...');
result.search = await this.searchBrave(query, 5);
console.log(`β
Found ${result.search.length} articles`);
// Step 2: Generate post + image prompts (Haiku tokens)
console.log('βοΈ Generating post + image prompts with Claude Haiku...');
result.post = await this.generatePostAndImagePrompts(query, result.search);
result.imagePrompts = result.post.imagePrompts;
result.tokens.post = result.post.tokensUsed;
// Step 3: Create carousel images (deterministic rendering, no API calls)
console.log(`π¨ Generating ${imageCount} carousel frames...`);
if (imageFormat === 'html') {
result.images = this.createHtmlCarouselFrames(result.imagePrompts, query);
} else {
result.images = this.createSvgFrames(result.imagePrompts, query);
}
console.log(`β
Generated ${result.images.length} frames`);
// Summary
result.elapsed = Date.now() - startTime;
result.success = true;
result.tokens.total = Object.values(result.tokens).reduce((a, b) => a + b, 0);
console.log(`\nβ
Complete in ${result.elapsed}ms`);
console.log(`π Tokens: ${result.tokens.total} (Haiku only)`);
return result;
} catch (error) {
result.success = false;
result.error = error.message;
console.error('β Error:', error.message);
return result;
}
}
/**
* Format for Telegram output
*/
formatForTelegram(result) {
if (!result.success) {
return `β Error: ${result.error}`;
}
let text = `π± *WebPost (Haiku + Images): ${result.query}*\n\n`;
// Post content
text += `βοΈ *Post:*\n${result.post.post}\n\n`;
// Sources
text += `π *Sources:*\n`;
result.search.slice(0, 2).forEach((article, i) => {
text += `${i + 1}. [${article.title}](${article.url})\n`;
});
// Token info
text += `\nπ *Token Usage (Haiku ONLY):*\n`;
text += `Haiku: ${result.post.tokensUsed} tokens\n`;
text += `β±οΈ Generated in ${(result.elapsed / 1000).toFixed(1)}s\n`;
// Image info
if (result.images?.length > 0) {
text += `\nπ¨ *Carousel:* ${result.images.length} HTML frames (deterministic)\n`;
text += `β’ Frame 1: ${result.imagePrompts[0].substring(0, 40)}...\n`;
text += `β’ Frame 2: ${result.imagePrompts[1].substring(0, 40)}...\n`;
text += `β’ Frame 3: ${result.imagePrompts[2].substring(0, 40)}...\n`;
}
return text;
}
}
module.exports = WebPostHaikuImages;