Select the types of activity you want to include in your feed.
A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
···4747 switch (provider) {
4848 case 'gemini':
4949 // apiKey is guaranteed by check above
5050- return await callGemini(apiKey!, model || 'models/gemini-2.5-flash', buffer, mimeType, prompt);
5050+ return normalizeAltTextOutput(
5151+ await callGemini(apiKey!, model || 'models/gemini-2.5-flash', buffer, mimeType, prompt),
5252+ );
5153 case 'openai':
5254 case 'custom':
5353- return await callOpenAICompatible(apiKey, model || 'gpt-4o', baseUrl, buffer, mimeType, prompt);
5555+ return normalizeAltTextOutput(
5656+ await callOpenAICompatible(apiKey, model || 'gpt-4o', baseUrl, buffer, mimeType, prompt),
5757+ );
5458 case 'anthropic':
5559 // apiKey is guaranteed by check above
5656- return await callAnthropic(
5757- apiKey!,
5858- model || 'claude-3-5-sonnet-20241022',
5959- baseUrl,
6060- buffer,
6161- mimeType,
6262- prompt,
6060+ return normalizeAltTextOutput(
6161+ await callAnthropic(
6262+ apiKey!,
6363+ model || 'claude-3-5-sonnet-20241022',
6464+ baseUrl,
6565+ buffer,
6666+ mimeType,
6767+ prompt,
6868+ ),
6369 );
6470 default:
6571 console.warn(`[AI] ⚠️ Unknown provider: ${provider}`);
···8490 'Write one alt text description (1-2 sentences).',
8591 'Describe only what is visible.',
8692 'Use context to identify people/places/objects if relevant for search.',
9393+ 'Describe only this image; ignore other images in the post.',
8794 'Return only the alt text with no labels, quotes, or options.',
8895 'No hashtags or emojis.',
8996 `Context: "${trimmed}"`,
9097 ].join(' ');
9898+}
9999+100100+function normalizeAltTextOutput(output: string | undefined): string | undefined {
101101+ if (!output) return undefined;
102102+103103+ let cleaned = output.trim();
104104+ if (!cleaned) return undefined;
105105+106106+ cleaned = cleaned.replace(/^["'“”]+|["'“”]+$/g, '').trim();
107107+ cleaned = cleaned.replace(/^(alt\s*text|description)\s*[:\-]\s*/i, '').trim();
108108+109109+ const lines = cleaned.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
110110+ if (lines.length > 0) cleaned = lines[0];
111111+112112+ cleaned = cleaned.replace(/^option\s*\d+\s*[:\-]\s*/i, '').trim();
113113+ cleaned = cleaned.replace(/^[\-\*\d\.\)]+\s*/g, '').trim();
114114+ cleaned = cleaned.replace(/\s+/g, ' ').trim();
115115+116116+ return cleaned || undefined;
91117}
9211893119async function callGemini(
+1-2
src/index.ts
···1474147414751475 // Removed early dryRun continue to allow verifying logic
1476147614771477- const altTextContext = buildAltTextContext(tweet, tweetText, tweetMap);
14781478-14791477 let text = tweetText
14801478 .replace(/&/g, '&')
14811479 .replace(/</g, '<')
···15701568 if (!altText) {
15711569 console.log(`[${twitterUsername}] 🤖 Generating alt text via Gemini...`);
15721570 // Use original tweet text for context, not the modified/cleaned one
15711571+ const altTextContext = buildAltTextContext(tweet, tweetText, tweetMap);
15731572 altText = await generateAltText(buffer, mimeType, altTextContext);
15741573 if (altText) console.log(`[${twitterUsername}] ✅ Alt text generated: ${altText.substring(0, 50)}...`);
15751574 }