opencode-pollinations-plugin 6.0.0 → 6.1.0-beta.10
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/README.md +140 -87
- package/dist/index.js +33 -154
- package/dist/server/commands.d.ts +2 -0
- package/dist/server/commands.js +84 -25
- package/dist/server/config.d.ts +6 -0
- package/dist/server/config.js +4 -1
- package/dist/server/generate-config.d.ts +3 -30
- package/dist/server/generate-config.js +172 -100
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +124 -149
- package/dist/server/pollinations-api.d.ts +11 -0
- package/dist/server/pollinations-api.js +20 -0
- package/dist/server/proxy.js +158 -72
- package/dist/server/quota.d.ts +8 -0
- package/dist/server/quota.js +106 -61
- package/dist/server/toast.d.ts +3 -0
- package/dist/server/toast.js +16 -0
- package/dist/tools/design/gen_diagram.d.ts +2 -0
- package/dist/tools/design/gen_diagram.js +94 -0
- package/dist/tools/design/gen_palette.d.ts +2 -0
- package/dist/tools/design/gen_palette.js +182 -0
- package/dist/tools/design/gen_qrcode.d.ts +2 -0
- package/dist/tools/design/gen_qrcode.js +50 -0
- package/dist/tools/index.d.ts +22 -0
- package/dist/tools/index.js +81 -0
- package/dist/tools/pollinations/deepsearch.d.ts +7 -0
- package/dist/tools/pollinations/deepsearch.js +80 -0
- package/dist/tools/pollinations/gen_audio.d.ts +18 -0
- package/dist/tools/pollinations/gen_audio.js +204 -0
- package/dist/tools/pollinations/gen_image.d.ts +13 -0
- package/dist/tools/pollinations/gen_image.js +239 -0
- package/dist/tools/pollinations/gen_music.d.ts +14 -0
- package/dist/tools/pollinations/gen_music.js +139 -0
- package/dist/tools/pollinations/gen_video.d.ts +16 -0
- package/dist/tools/pollinations/gen_video.js +222 -0
- package/dist/tools/pollinations/search_crawl_scrape.d.ts +7 -0
- package/dist/tools/pollinations/search_crawl_scrape.js +85 -0
- package/dist/tools/pollinations/shared.d.ts +170 -0
- package/dist/tools/pollinations/shared.js +454 -0
- package/dist/tools/pollinations/transcribe_audio.d.ts +17 -0
- package/dist/tools/pollinations/transcribe_audio.js +235 -0
- package/dist/tools/power/extract_audio.d.ts +2 -0
- package/dist/tools/power/extract_audio.js +180 -0
- package/dist/tools/power/extract_frames.d.ts +2 -0
- package/dist/tools/power/extract_frames.js +240 -0
- package/dist/tools/power/file_to_url.d.ts +2 -0
- package/dist/tools/power/file_to_url.js +217 -0
- package/dist/tools/power/remove_background.d.ts +2 -0
- package/dist/tools/power/remove_background.js +365 -0
- package/dist/tools/power/rmbg_keys.d.ts +2 -0
- package/dist/tools/power/rmbg_keys.js +78 -0
- package/dist/tools/shared.d.ts +30 -0
- package/dist/tools/shared.js +74 -0
- package/package.json +9 -3
- package/dist/server/models-seed.d.ts +0 -18
- package/dist/server/models-seed.js +0 -55
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen_audio Tool - Pollinations Text-to-Speech
|
|
3
|
+
*
|
|
4
|
+
* Updated: 2026-02-12 - Verified API Reference
|
|
5
|
+
*
|
|
6
|
+
* Two TTS options:
|
|
7
|
+
* 1. openai-audio (DEFAULT): GPT-4o Audio Preview - uses /v1/chat/completions with modalities
|
|
8
|
+
* - Supports both TTS and STT (Speech-to-Text)
|
|
9
|
+
* - Least expensive option
|
|
10
|
+
* - Voices: alloy, echo, fable, onyx, nova, shimmer
|
|
11
|
+
* - Formats: mp3, wav, pcm16
|
|
12
|
+
*
|
|
13
|
+
* 2. elevenlabs: ElevenLabs v3 TTS - uses /audio/{text}
|
|
14
|
+
* - 34 expressive voices
|
|
15
|
+
* - Higher quality but more expensive
|
|
16
|
+
*/
|
|
17
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { getApiKey, httpsPost, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateTtsCost, isCostEstimatorEnabled, AUDIO_MODELS, } from './shared.js';
|
|
21
|
+
// ─── TTS Configuration ────────────────────────────────────────────────────
|
|
22
|
+
const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
|
|
23
|
+
const ELEVENLABS_VOICES = [
|
|
24
|
+
'rachel', 'domi', 'bella', 'elli', 'charlotte', 'dorothy',
|
|
25
|
+
'sarah', 'emily', 'lily', 'matilda',
|
|
26
|
+
'adam', 'antoni', 'arnold', 'josh', 'sam', 'daniel',
|
|
27
|
+
'charlie', 'james', 'fin', 'callum', 'liam', 'george', 'brian', 'bill',
|
|
28
|
+
'ash', 'ballad', 'coral', 'sage', 'verse',
|
|
29
|
+
];
|
|
30
|
+
const DEFAULT_VOICE = 'alloy';
|
|
31
|
+
const DEFAULT_MODEL = 'openai-audio'; // Changed: openai-audio is now default (least expensive)
|
|
32
|
+
const DEFAULT_FORMAT = 'mp3';
|
|
33
|
+
// ─── Tool Definition ──────────────────────────────────────────────────────
|
|
34
|
+
export const genAudioTool = tool({
|
|
35
|
+
description: `Convert text to speech using Pollinations AI.
|
|
36
|
+
|
|
37
|
+
**🔊 Models:**
|
|
38
|
+
|
|
39
|
+
| Model | Type | Voices | Format | Cost | Notes |
|
|
40
|
+
|-------|------|--------|--------|------|-------|
|
|
41
|
+
| openai-audio | TTS + STT | 6 | mp3, wav, pcm16 | Lowest | **DEFAULT** - GPT-4o Audio |
|
|
42
|
+
| elevenlabs | TTS | 34 | mp3, wav, etc. | Higher | Expressive voices |
|
|
43
|
+
|
|
44
|
+
**🎵 OpenAI Audio (Default, Recommended):**
|
|
45
|
+
- Voices: \`alloy\`, \`echo\`, \`fable\`, \`onyx\`, \`nova\`, \`shimmer\`
|
|
46
|
+
- Formats: \`mp3\` (default), \`wav\`, \`pcm16\`
|
|
47
|
+
- Uses GPT-4o Audio Preview modalities endpoint
|
|
48
|
+
- Lowest cost option
|
|
49
|
+
|
|
50
|
+
**🎤 ElevenLabs:**
|
|
51
|
+
- 34 expressive voices including: rachel, domi, bella, adam, etc.
|
|
52
|
+
- Higher quality natural-sounding speech
|
|
53
|
+
- More expensive but more expressive
|
|
54
|
+
|
|
55
|
+
**💡 Tips:**
|
|
56
|
+
- Use \`openai-audio\` for cost-effective TTS
|
|
57
|
+
- Use \`elevenlabs\` for more expressive/character voices
|
|
58
|
+
- For STT (transcription), use the \`transcribe_audio\` tool`,
|
|
59
|
+
args: {
|
|
60
|
+
text: tool.schema.string().describe('Text to convert to speech'),
|
|
61
|
+
voice: tool.schema.string().optional().describe(`Voice to use (default: ${DEFAULT_VOICE})`),
|
|
62
|
+
model: tool.schema.string().optional().describe(`TTS model (default: ${DEFAULT_MODEL})`),
|
|
63
|
+
format: tool.schema.enum(['mp3', 'wav', 'pcm16']).optional().describe('Audio format (default: mp3, openai-audio only)'),
|
|
64
|
+
save_to: tool.schema.string().optional().describe('Custom output directory'),
|
|
65
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension)'),
|
|
66
|
+
},
|
|
67
|
+
async execute(args, context) {
|
|
68
|
+
const apiKey = getApiKey();
|
|
69
|
+
if (!apiKey) {
|
|
70
|
+
return `❌ Le TTS nécessite une clé API Pollinations.
|
|
71
|
+
🔧 Connectez votre clé avec /pollinations connect`;
|
|
72
|
+
}
|
|
73
|
+
const text = args.text;
|
|
74
|
+
const model = args.model || DEFAULT_MODEL;
|
|
75
|
+
const voice = args.voice || DEFAULT_VOICE;
|
|
76
|
+
const format = args.format || DEFAULT_FORMAT;
|
|
77
|
+
// Validate model
|
|
78
|
+
const modelInfo = AUDIO_MODELS[model];
|
|
79
|
+
if (!modelInfo) {
|
|
80
|
+
return `❌ Modèle inconnu: ${model}
|
|
81
|
+
💡 Modèles disponibles: ${Object.keys(AUDIO_MODELS).filter(m => AUDIO_MODELS[m].type === 'tts' || AUDIO_MODELS[m].type === 'both').join(', ')}`;
|
|
82
|
+
}
|
|
83
|
+
// Validate voice for selected model
|
|
84
|
+
if (model === 'openai-audio' && !OPENAI_VOICES.includes(voice)) {
|
|
85
|
+
return `⚠️ Voix "${voice}" non supportée par openai-audio.
|
|
86
|
+
💡 Voix OpenAI: ${OPENAI_VOICES.join(', ')}`;
|
|
87
|
+
}
|
|
88
|
+
if (model === 'elevenlabs' && !ELEVENLABS_VOICES.includes(voice)) {
|
|
89
|
+
return `⚠️ Voix "${voice}" non reconnue pour elevenlabs.
|
|
90
|
+
💡 Voix ElevenLabs populaires: rachel, domi, bella, adam, josh...
|
|
91
|
+
📋 Total: ${ELEVENLABS_VOICES.length} voix disponibles`;
|
|
92
|
+
}
|
|
93
|
+
// Estimate cost
|
|
94
|
+
const estimatedCost = estimateTtsCost(text.length);
|
|
95
|
+
// Metadata
|
|
96
|
+
context.metadata({ title: `🔊 TTS: ${voice} (${text.length} chars)` });
|
|
97
|
+
try {
|
|
98
|
+
let audioData;
|
|
99
|
+
let responseHeaders = {};
|
|
100
|
+
let actualFormat = format;
|
|
101
|
+
if (model === 'openai-audio') {
|
|
102
|
+
// === OpenAI Audio: Use modalities endpoint ===
|
|
103
|
+
// POST /v1/chat/completions with audio modalities
|
|
104
|
+
const response = await httpsPost('https://gen.pollinations.ai/v1/chat/completions', {
|
|
105
|
+
model: 'openai-audio',
|
|
106
|
+
modalities: ['text', 'audio'],
|
|
107
|
+
audio: {
|
|
108
|
+
voice: voice,
|
|
109
|
+
format: format,
|
|
110
|
+
},
|
|
111
|
+
messages: [
|
|
112
|
+
{
|
|
113
|
+
role: 'user',
|
|
114
|
+
content: text
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
}, {
|
|
118
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
119
|
+
});
|
|
120
|
+
const data = JSON.parse(response.data.toString());
|
|
121
|
+
// Extract audio from response
|
|
122
|
+
const audioBase64 = data.choices?.[0]?.message?.audio?.data;
|
|
123
|
+
if (!audioBase64) {
|
|
124
|
+
throw new Error('No audio data in response');
|
|
125
|
+
}
|
|
126
|
+
audioData = Buffer.from(audioBase64, 'base64');
|
|
127
|
+
responseHeaders = response.headers;
|
|
128
|
+
}
|
|
129
|
+
else if (model === 'elevenlabs') {
|
|
130
|
+
// === ElevenLabs: Use audio endpoint ===
|
|
131
|
+
// GET/POST /audio/{text}
|
|
132
|
+
const promptEncoded = encodeURIComponent(text);
|
|
133
|
+
const url = `https://gen.pollinations.ai/audio/${promptEncoded}?model=elevenlabs&voice=${voice}`;
|
|
134
|
+
// For elevenlabs, we might need a different approach
|
|
135
|
+
// Let's use POST with JSON body
|
|
136
|
+
const response = await httpsPost('https://gen.pollinations.ai/v1/audio/speech', {
|
|
137
|
+
model: 'elevenlabs',
|
|
138
|
+
input: text,
|
|
139
|
+
voice: voice,
|
|
140
|
+
}, {
|
|
141
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
142
|
+
});
|
|
143
|
+
// Check if response is JSON (error) or binary (audio)
|
|
144
|
+
const contentType = response.headers['content-type'] || '';
|
|
145
|
+
if (contentType.includes('application/json')) {
|
|
146
|
+
const data = JSON.parse(response.data.toString());
|
|
147
|
+
throw new Error(data.error?.message || 'Unknown error');
|
|
148
|
+
}
|
|
149
|
+
audioData = response.data;
|
|
150
|
+
responseHeaders = response.headers;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Fallback to OpenAI-compatible endpoint
|
|
154
|
+
const response = await httpsPost('https://gen.pollinations.ai/v1/audio/speech', {
|
|
155
|
+
model: model,
|
|
156
|
+
input: text,
|
|
157
|
+
voice: voice,
|
|
158
|
+
}, {
|
|
159
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
160
|
+
});
|
|
161
|
+
audioData = response.data;
|
|
162
|
+
responseHeaders = response.headers;
|
|
163
|
+
}
|
|
164
|
+
// Save audio
|
|
165
|
+
const outputDir = args.save_to || getDefaultOutputDir('audio');
|
|
166
|
+
ensureDir(outputDir);
|
|
167
|
+
const filename = args.filename || generateFilename('tts', `${model}_${voice}`, actualFormat);
|
|
168
|
+
const filePath = path.join(outputDir, filename.endsWith(`.${actualFormat}`) ? filename : `${filename}.${actualFormat}`);
|
|
169
|
+
fs.writeFileSync(filePath, audioData);
|
|
170
|
+
const fileSize = fs.statSync(filePath).size;
|
|
171
|
+
// Estimate duration (approx 15 chars per second for speech)
|
|
172
|
+
const estimatedDuration = Math.ceil(text.length / 15);
|
|
173
|
+
// Build result
|
|
174
|
+
const lines = [
|
|
175
|
+
`🔊 Audio Généré (TTS)`,
|
|
176
|
+
`━━━━━━━━━━━━━━━━━━`,
|
|
177
|
+
`Texte: "${text.substring(0, 60)}${text.length > 60 ? '...' : ''}"`,
|
|
178
|
+
`Modèle: ${model}${model === 'openai-audio' ? ' (recommandé)' : ''}`,
|
|
179
|
+
`Voix: ${voice}`,
|
|
180
|
+
`Format: ${actualFormat}`,
|
|
181
|
+
`Durée estimée: ~${estimatedDuration}s`,
|
|
182
|
+
`Fichier: ${filePath}`,
|
|
183
|
+
`Taille: ${formatFileSize(fileSize)}`,
|
|
184
|
+
];
|
|
185
|
+
// Cost info
|
|
186
|
+
if (isCostEstimatorEnabled()) {
|
|
187
|
+
lines.push(`Coût estimé: ${formatCost(estimatedCost)}`);
|
|
188
|
+
}
|
|
189
|
+
if (responseHeaders['x-request-id']) {
|
|
190
|
+
lines.push(`Request ID: ${responseHeaders['x-request-id']}`);
|
|
191
|
+
}
|
|
192
|
+
return lines.join('\n');
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (err.message?.includes('402') || err.message?.includes('Payment')) {
|
|
196
|
+
return `❌ Crédits insuffisants.`;
|
|
197
|
+
}
|
|
198
|
+
if (err.message?.includes('401') || err.message?.includes('403')) {
|
|
199
|
+
return `❌ Clé API invalide ou non autorisée.`;
|
|
200
|
+
}
|
|
201
|
+
return `❌ Erreur TTS: ${err.message}`;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen_image Tool - Pollinations Image Generation
|
|
3
|
+
*
|
|
4
|
+
* Updated: 2026-02-12 - Verified API Reference
|
|
5
|
+
*
|
|
6
|
+
* Supports:
|
|
7
|
+
* - FREE models: sana, zimage (flux REMOVED, turbo BROKEN)
|
|
8
|
+
* - ENTER models: flux, kontext, seedream, klein, gptimage, imagen-4, etc.
|
|
9
|
+
* - Image-to-Image (I2I): klein, klein-large, kontext, seedream, seedream-pro, nanobanana, nanobanana-pro
|
|
10
|
+
* - Multi-image I2I: seedream-pro (comma-separated URLs)
|
|
11
|
+
*/
|
|
12
|
+
import { type ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
13
|
+
export declare const genImageTool: ToolDefinition;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen_image Tool - Pollinations Image Generation
|
|
3
|
+
*
|
|
4
|
+
* Updated: 2026-02-12 - Verified API Reference
|
|
5
|
+
*
|
|
6
|
+
* Supports:
|
|
7
|
+
* - FREE models: sana, zimage (flux REMOVED, turbo BROKEN)
|
|
8
|
+
* - ENTER models: flux, kontext, seedream, klein, gptimage, imagen-4, etc.
|
|
9
|
+
* - Image-to-Image (I2I): klein, klein-large, kontext, seedream, seedream-pro, nanobanana, nanobanana-pro
|
|
10
|
+
* - Multi-image I2I: seedream-pro (comma-separated URLs)
|
|
11
|
+
*/
|
|
12
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { getApiKey, hasApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateImageCost, isCostEstimatorEnabled, supportsI2I, FREE_IMAGE_MODELS, PAID_IMAGE_MODELS, } from './shared.js';
|
|
16
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* FREE models that work reliably (2026-02-12)
|
|
19
|
+
* WARNING: flux removed from free, turbo shows deprecated notice
|
|
20
|
+
*/
|
|
21
|
+
const RELIABLE_FREE_MODELS = ['sana', 'zimage'];
|
|
22
|
+
const DEFAULT_MODEL = 'flux';
|
|
23
|
+
const FREE_DEFAULT_MODEL = 'sana';
|
|
24
|
+
// ─── Tool Definition ──────────────────────────────────────────────────────
|
|
25
|
+
export const genImageTool = tool({
|
|
26
|
+
description: `Generate an image from a text prompt using Pollinations AI.
|
|
27
|
+
|
|
28
|
+
**🆓 FREE Models** (no API key, no cost):
|
|
29
|
+
- \`sana\`: Default free model (~60KB, reliable)
|
|
30
|
+
- \`zimage\`: Low quality alias (~35KB)
|
|
31
|
+
- ⚠️ \`turbo\`: BROKEN - shows deprecated notice
|
|
32
|
+
- ⚠️ \`flux\`: REMOVED from free tier!
|
|
33
|
+
|
|
34
|
+
**💎 ENTER Models** (requires API key):
|
|
35
|
+
| Model | Cost | T2I | I2I | Notes |
|
|
36
|
+
|-------|------|-----|-----|-------|
|
|
37
|
+
| flux | 0.0002 🌻 | ✅ | ❌ | Fast high-quality |
|
|
38
|
+
| zimage | 0.0002 🌻 | ✅ | ❌ | 6B Flux with 2x upscaling |
|
|
39
|
+
| imagen-4 | 0.0025 🌻 | ✅ | ❌ | Google high fidelity |
|
|
40
|
+
| klein | 0.008 🌻 | ✅ | ✅ | FLUX.2 Klein 4B |
|
|
41
|
+
| klein-large | 0.012 🌻 | ✅ | ✅ | FLUX.2 Klein 9B |
|
|
42
|
+
| kontext | 0.04 🌻 | ✅ | ✅ | In-Context Editing |
|
|
43
|
+
| seedream | 0.03 🌻 | ✅ | ✅ | ByteDance ARK quality |
|
|
44
|
+
| seedream-pro | 0.04 🌻 | ✅ | ✅ | 4K, Multi-Image support |
|
|
45
|
+
| gptimage | tokens | ✅ | ❌ | OpenAI GPT Image Mini |
|
|
46
|
+
| gptimage-large | tokens | ✅ | ❌ | OpenAI GPT Image 1.5 |
|
|
47
|
+
| nanobanana | tokens | ✅ | ✅ | Gemini 2.5 Flash |
|
|
48
|
+
| nanobanana-pro | tokens | ✅ | ✅ | Gemini 3 Pro Thinking |
|
|
49
|
+
|
|
50
|
+
**🖼️ Image-to-Image (I2I)**:
|
|
51
|
+
Models with I2I support can transform existing images.
|
|
52
|
+
- Use \`reference_image\` parameter with URL or local path
|
|
53
|
+
- \`seedream-pro\` supports multiple images (comma-separated URLs)
|
|
54
|
+
- \`kontext\` specializes in in-context editing
|
|
55
|
+
|
|
56
|
+
**⚙️ Per-Model Parameters**:
|
|
57
|
+
- \`width/height\`: All models (default: 1024x1024)
|
|
58
|
+
- \`quality\`: gptimage only (low/med/high)
|
|
59
|
+
- \`transparent\`: gptimage only (true/false)
|
|
60
|
+
- \`seed\`: Reproducibility (-1 for random)`,
|
|
61
|
+
args: {
|
|
62
|
+
prompt: tool.schema.string().describe('Description of the image to generate'),
|
|
63
|
+
model: tool.schema.string().optional().describe('Model to use (default: flux or sana if no key)'),
|
|
64
|
+
width: tool.schema.number().min(256).max(4096).optional().describe('Image width (default: 1024)'),
|
|
65
|
+
height: tool.schema.number().min(256).max(4096).optional().describe('Image height (default: 1024)'),
|
|
66
|
+
reference_image: tool.schema.string().optional().describe('URL(s) for image-to-image editing (comma-separated for multi-image models)'),
|
|
67
|
+
seed: tool.schema.number().optional().describe('Seed for reproducibility (-1 for random)'),
|
|
68
|
+
quality: tool.schema.enum(['low', 'med', 'high']).optional().describe('Quality for gptimage models only'),
|
|
69
|
+
transparent: tool.schema.boolean().optional().describe('Transparent background for gptimage models only'),
|
|
70
|
+
save_to: tool.schema.string().optional().describe('Custom output directory'),
|
|
71
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension)'),
|
|
72
|
+
},
|
|
73
|
+
async execute(args, context) {
|
|
74
|
+
const apiKey = getApiKey();
|
|
75
|
+
const hasKey = hasApiKey();
|
|
76
|
+
// Determine model based on key presence
|
|
77
|
+
let model = args.model || (hasKey ? DEFAULT_MODEL : FREE_DEFAULT_MODEL);
|
|
78
|
+
const width = args.width || 1024;
|
|
79
|
+
const height = args.height || 1024;
|
|
80
|
+
// Check if it's a free model
|
|
81
|
+
const isFreeModel = Object.keys(FREE_IMAGE_MODELS).includes(model);
|
|
82
|
+
const isReliableFree = RELIABLE_FREE_MODELS.includes(model);
|
|
83
|
+
// Check if it's a paid model
|
|
84
|
+
const paidModelInfo = PAID_IMAGE_MODELS[model];
|
|
85
|
+
const isPaidModel = !!paidModelInfo;
|
|
86
|
+
// Validate model selection
|
|
87
|
+
if (isFreeModel && !isReliableFree) {
|
|
88
|
+
return `⚠️ Le modèle "${model}" n'est plus fiable.
|
|
89
|
+
${model === 'turbo' ? '`turbo` affiche une notice de dépréciation.' : ''}
|
|
90
|
+
💡 Modèles gratuits recommandés: ${RELIABLE_FREE_MODELS.join(', ')}
|
|
91
|
+
${hasKey ? `💎 Ou utilisez un modèle payant: flux, kontext, seedream...` : ''}`;
|
|
92
|
+
}
|
|
93
|
+
if (isPaidModel && !hasKey) {
|
|
94
|
+
return `❌ Le modèle "${model}" nécessite une clé API.
|
|
95
|
+
💡 Utilisez un modèle gratuit: ${RELIABLE_FREE_MODELS.join(', ')}
|
|
96
|
+
🔧 Ou connectez votre clé avec /pollinations connect`;
|
|
97
|
+
}
|
|
98
|
+
// Validate I2I support
|
|
99
|
+
if (args.reference_image && isPaidModel && !supportsI2I(model)) {
|
|
100
|
+
return `⚠️ Le modèle "${model}" ne supporte pas l'Image-to-Image.
|
|
101
|
+
💡 Modèles I2I supportés: ${Object.entries(PAID_IMAGE_MODELS)
|
|
102
|
+
.filter(([, info]) => info.i2i)
|
|
103
|
+
.map(([name]) => name)
|
|
104
|
+
.join(', ')}`;
|
|
105
|
+
}
|
|
106
|
+
// Check if model exists
|
|
107
|
+
if (!isFreeModel && !isPaidModel) {
|
|
108
|
+
return `❌ Modèle inconnu: ${model}
|
|
109
|
+
💡 Modèles gratuits: ${RELIABLE_FREE_MODELS.join(', ')}
|
|
110
|
+
💎 Modèles payants: ${Object.keys(PAID_IMAGE_MODELS).slice(0, 5).join(', ')}...`;
|
|
111
|
+
}
|
|
112
|
+
// Estimate cost
|
|
113
|
+
const estimatedCost = isPaidModel ? estimateImageCost(model) : 0;
|
|
114
|
+
// Set metadata
|
|
115
|
+
context.metadata({ title: `🎨 Image: ${model}` });
|
|
116
|
+
try {
|
|
117
|
+
let imageData;
|
|
118
|
+
let responseHeaders = {};
|
|
119
|
+
let usedModel = model;
|
|
120
|
+
if (isFreeModel && !hasKey) {
|
|
121
|
+
// === FREE endpoint (image.pollinations.ai) ===
|
|
122
|
+
const params = new URLSearchParams({
|
|
123
|
+
nologo: 'true',
|
|
124
|
+
private: 'true',
|
|
125
|
+
});
|
|
126
|
+
if (model !== 'sana') {
|
|
127
|
+
params.set('model', model);
|
|
128
|
+
}
|
|
129
|
+
if (args.seed !== undefined) {
|
|
130
|
+
params.set('seed', String(args.seed));
|
|
131
|
+
}
|
|
132
|
+
const promptEncoded = encodeURIComponent(args.prompt);
|
|
133
|
+
const url = `https://image.pollinations.ai/${promptEncoded}?${params}`;
|
|
134
|
+
const result = await httpsGet(url);
|
|
135
|
+
imageData = result.data;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// === ENTER endpoint (gen.pollinations.ai) ===
|
|
139
|
+
const params = new URLSearchParams({
|
|
140
|
+
nologo: 'true',
|
|
141
|
+
private: 'true',
|
|
142
|
+
width: String(width),
|
|
143
|
+
height: String(height),
|
|
144
|
+
});
|
|
145
|
+
// Model parameter
|
|
146
|
+
params.set('model', model);
|
|
147
|
+
// Seed
|
|
148
|
+
if (args.seed !== undefined) {
|
|
149
|
+
params.set('seed', String(args.seed));
|
|
150
|
+
}
|
|
151
|
+
// I2I: reference image(s)
|
|
152
|
+
if (args.reference_image) {
|
|
153
|
+
// Check if it's a local file path
|
|
154
|
+
let imageUrl = args.reference_image;
|
|
155
|
+
if (!args.reference_image.startsWith('http')) {
|
|
156
|
+
// For local files, we'd need to upload first
|
|
157
|
+
// For now, require URL
|
|
158
|
+
return `❌ Les fichiers locaux nécessitent d'être uploadés d'abord.
|
|
159
|
+
💡 Utilisez l'outil \`file_to_url\` pour obtenir une URL publique.`;
|
|
160
|
+
}
|
|
161
|
+
params.set('image', imageUrl);
|
|
162
|
+
}
|
|
163
|
+
// Quality (gptimage only)
|
|
164
|
+
if (args.quality && model.startsWith('gptimage')) {
|
|
165
|
+
params.set('quality', args.quality);
|
|
166
|
+
}
|
|
167
|
+
// Transparent (gptimage only)
|
|
168
|
+
if (args.transparent !== undefined && model.startsWith('gptimage')) {
|
|
169
|
+
params.set('transparent', String(args.transparent));
|
|
170
|
+
}
|
|
171
|
+
const promptEncoded = encodeURIComponent(args.prompt);
|
|
172
|
+
const url = `https://gen.pollinations.ai/image/${promptEncoded}?${params}`;
|
|
173
|
+
const headers = {};
|
|
174
|
+
if (apiKey)
|
|
175
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
176
|
+
const result = await httpsGet(url, headers);
|
|
177
|
+
imageData = result.data;
|
|
178
|
+
responseHeaders = result.headers;
|
|
179
|
+
// Update used model from response if available
|
|
180
|
+
if (responseHeaders['x-model-used']) {
|
|
181
|
+
usedModel = responseHeaders['x-model-used'];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Save the image
|
|
185
|
+
const outputDir = args.save_to || getDefaultOutputDir('images');
|
|
186
|
+
ensureDir(outputDir);
|
|
187
|
+
const filename = args.filename || generateFilename('image', usedModel, 'png');
|
|
188
|
+
const filePath = path.join(outputDir, filename.endsWith('.png') ? filename : `${filename}.png`);
|
|
189
|
+
fs.writeFileSync(filePath, imageData);
|
|
190
|
+
const fileSize = fs.statSync(filePath).size;
|
|
191
|
+
// Extract actual cost from headers if available
|
|
192
|
+
let actualCost = estimatedCost;
|
|
193
|
+
if (isCostEstimatorEnabled() && responseHeaders['x-usage-completion-image-tokens']) {
|
|
194
|
+
const tokens = parseFloat(responseHeaders['x-usage-completion-image-tokens']);
|
|
195
|
+
// Token-based cost calculation would go here
|
|
196
|
+
}
|
|
197
|
+
// Build result
|
|
198
|
+
const lines = [
|
|
199
|
+
`🎨 Image Générée`,
|
|
200
|
+
`━━━━━━━━━━━━━━━━━━`,
|
|
201
|
+
`Prompt: ${args.prompt.substring(0, 100)}${args.prompt.length > 100 ? '...' : ''}`,
|
|
202
|
+
`Modèle: ${usedModel}${isFreeModel ? ' (GRATUIT)' : ''}`,
|
|
203
|
+
`Résolution: ${width}×${height}`,
|
|
204
|
+
];
|
|
205
|
+
// Add I2I info if used
|
|
206
|
+
if (args.reference_image) {
|
|
207
|
+
lines.push(`I2I Source: ${args.reference_image.substring(0, 50)}...`);
|
|
208
|
+
}
|
|
209
|
+
lines.push(`Fichier: ${filePath}`);
|
|
210
|
+
lines.push(`Taille: ${formatFileSize(fileSize)}`);
|
|
211
|
+
// Cost info
|
|
212
|
+
if (isFreeModel) {
|
|
213
|
+
lines.push(`Coût: GRATUIT`);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
lines.push(`Coût estimé: ${formatCost(actualCost)}`);
|
|
217
|
+
if (responseHeaders['x-request-id']) {
|
|
218
|
+
lines.push(`Request ID: ${responseHeaders['x-request-id']}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (err.message?.includes('402') || err.message?.includes('Payment')) {
|
|
225
|
+
return `❌ Crédits insuffisants pour le modèle "${model}".
|
|
226
|
+
💡 Essayez un modèle gratuit: ${RELIABLE_FREE_MODELS.join(', ')}`;
|
|
227
|
+
}
|
|
228
|
+
if (err.message?.includes('401') || err.message?.includes('403')) {
|
|
229
|
+
return `❌ Clé API invalide ou non autorisée.
|
|
230
|
+
🔧 Vérifiez votre clé avec /pollinations connect`;
|
|
231
|
+
}
|
|
232
|
+
if (err.message?.includes('400')) {
|
|
233
|
+
return `❌ Paramètres invalides: ${err.message}
|
|
234
|
+
💡 Vérifiez que le modèle supporte les paramètres fournis.`;
|
|
235
|
+
}
|
|
236
|
+
return `❌ Erreur génération image: ${err.message}`;
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen_music Tool - Pollinations Music Generation
|
|
3
|
+
*
|
|
4
|
+
* Updated: 2026-02-12 - Verified API Reference
|
|
5
|
+
*
|
|
6
|
+
* Model: elevenmusic (ElevenLabs Music)
|
|
7
|
+
* Endpoint: gen.pollinations.ai/audio/{text}
|
|
8
|
+
*
|
|
9
|
+
* Parameters:
|
|
10
|
+
* - duration: 3-300 seconds
|
|
11
|
+
* - instrumental: boolean (vocals or instrumental only)
|
|
12
|
+
*/
|
|
13
|
+
import { type ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
14
|
+
export declare const genMusicTool: ToolDefinition;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen_music Tool - Pollinations Music Generation
|
|
3
|
+
*
|
|
4
|
+
* Updated: 2026-02-12 - Verified API Reference
|
|
5
|
+
*
|
|
6
|
+
* Model: elevenmusic (ElevenLabs Music)
|
|
7
|
+
* Endpoint: gen.pollinations.ai/audio/{text}
|
|
8
|
+
*
|
|
9
|
+
* Parameters:
|
|
10
|
+
* - duration: 3-300 seconds
|
|
11
|
+
* - instrumental: boolean (vocals or instrumental only)
|
|
12
|
+
*/
|
|
13
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { getApiKey, httpsGet, ensureDir, generateFilename, getDefaultOutputDir, formatCost, formatFileSize, estimateMusicCost, isCostEstimatorEnabled, } from './shared.js';
|
|
17
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
18
|
+
const MIN_DURATION = 3;
|
|
19
|
+
const MAX_DURATION = 300; // 5 minutes
|
|
20
|
+
const DEFAULT_DURATION = 10;
|
|
21
|
+
const MODEL_NAME = 'elevenmusic';
|
|
22
|
+
// ─── Tool Definition ──────────────────────────────────────────────────────
|
|
23
|
+
export const genMusicTool = tool({
|
|
24
|
+
description: `Generate music from a text description using Pollinations AI.
|
|
25
|
+
|
|
26
|
+
**🎵 Model:** elevenmusic (ElevenLabs Music)
|
|
27
|
+
|
|
28
|
+
**📝 Parameters:**
|
|
29
|
+
- \`duration\`: 3-300 seconds (default: 10s)
|
|
30
|
+
- \`instrumental\`: true = no vocals, false = vocals allowed
|
|
31
|
+
|
|
32
|
+
**💡 Example Prompts:**
|
|
33
|
+
- "upbeat jazz with saxophone solo"
|
|
34
|
+
- "ambient electronic for meditation"
|
|
35
|
+
- "epic orchestral film score with dramatic strings"
|
|
36
|
+
- "lo-fi hip hop beats with piano"
|
|
37
|
+
- "acoustic guitar ballad with soft vocals"
|
|
38
|
+
- "electronic dance music with heavy bass drop"
|
|
39
|
+
|
|
40
|
+
**💰 Cost:** ~0.005 🌻 per second
|
|
41
|
+
- 10 seconds ≈ 0.05 🌻
|
|
42
|
+
- 30 seconds ≈ 0.15 🌻
|
|
43
|
+
- 60 seconds ≈ 0.30 🌻
|
|
44
|
+
|
|
45
|
+
**⚠️ Notes:**
|
|
46
|
+
- Generation time scales with duration (~1s per second of audio)
|
|
47
|
+
- Longer tracks (60s+) may take 1-2 minutes
|
|
48
|
+
- Instrumental mode produces cleaner results for background music`,
|
|
49
|
+
args: {
|
|
50
|
+
prompt: tool.schema.string().describe('Description of the music to generate'),
|
|
51
|
+
duration: tool.schema.number().min(MIN_DURATION).max(MAX_DURATION).optional()
|
|
52
|
+
.describe(`Duration in seconds (default: ${DEFAULT_DURATION}, max: ${MAX_DURATION})`),
|
|
53
|
+
instrumental: tool.schema.boolean().optional().describe('Instrumental only - no vocals (default: false)'),
|
|
54
|
+
seed: tool.schema.number().optional().describe('Seed for reproducibility (-1 for random)'),
|
|
55
|
+
save_to: tool.schema.string().optional().describe('Custom output directory'),
|
|
56
|
+
filename: tool.schema.string().optional().describe('Custom filename (without extension)'),
|
|
57
|
+
},
|
|
58
|
+
async execute(args, context) {
|
|
59
|
+
const apiKey = getApiKey();
|
|
60
|
+
if (!apiKey) {
|
|
61
|
+
return `❌ La génération musicale nécessite une clé API Pollinations.
|
|
62
|
+
🔧 Connectez votre clé avec /pollinations connect`;
|
|
63
|
+
}
|
|
64
|
+
const duration = Math.min(args.duration || DEFAULT_DURATION, MAX_DURATION);
|
|
65
|
+
const instrumental = args.instrumental || false;
|
|
66
|
+
// Estimate cost
|
|
67
|
+
const estimatedCost = estimateMusicCost(duration);
|
|
68
|
+
// Estimate generation time
|
|
69
|
+
const genTimeSeconds = Math.ceil(duration * 1.2); // ~1.2s per second of audio
|
|
70
|
+
// Metadata
|
|
71
|
+
context.metadata({ title: `🎵 Music: ${duration}s (~${genTimeSeconds}s gen time)` });
|
|
72
|
+
try {
|
|
73
|
+
// Build URL
|
|
74
|
+
const params = new URLSearchParams({
|
|
75
|
+
model: MODEL_NAME,
|
|
76
|
+
nologo: 'true',
|
|
77
|
+
private: 'true',
|
|
78
|
+
duration: String(duration),
|
|
79
|
+
});
|
|
80
|
+
if (instrumental) {
|
|
81
|
+
params.set('instrumental', 'true');
|
|
82
|
+
}
|
|
83
|
+
// Seed for reproducibility
|
|
84
|
+
if (args.seed !== undefined) {
|
|
85
|
+
params.set('seed', String(args.seed));
|
|
86
|
+
}
|
|
87
|
+
const promptEncoded = encodeURIComponent(args.prompt);
|
|
88
|
+
const url = `https://gen.pollinations.ai/audio/${promptEncoded}?${params}`;
|
|
89
|
+
const headers = {
|
|
90
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
91
|
+
};
|
|
92
|
+
// Music generation takes time
|
|
93
|
+
const result = await httpsGet(url, headers);
|
|
94
|
+
const audioData = result.data;
|
|
95
|
+
const responseHeaders = result.headers;
|
|
96
|
+
// Save audio
|
|
97
|
+
const outputDir = args.save_to || getDefaultOutputDir('music');
|
|
98
|
+
ensureDir(outputDir);
|
|
99
|
+
const filename = args.filename || generateFilename('music', MODEL_NAME, 'mp3');
|
|
100
|
+
const filePath = path.join(outputDir, filename.endsWith('.mp3') ? filename : `${filename}.mp3`);
|
|
101
|
+
fs.writeFileSync(filePath, audioData);
|
|
102
|
+
const fileSize = fs.statSync(filePath).size;
|
|
103
|
+
// Build result
|
|
104
|
+
const lines = [
|
|
105
|
+
`🎵 Musique Générée`,
|
|
106
|
+
`━━━━━━━━━━━━━━━━━━`,
|
|
107
|
+
`Prompt: ${args.prompt}`,
|
|
108
|
+
`Durée: ~${duration}s`,
|
|
109
|
+
`Mode: ${instrumental ? 'Instrumental (sans voix)' : 'Avec voix possible'}`,
|
|
110
|
+
`Fichier: ${filePath}`,
|
|
111
|
+
`Taille: ${formatFileSize(fileSize)}`,
|
|
112
|
+
];
|
|
113
|
+
// Cost info
|
|
114
|
+
if (isCostEstimatorEnabled()) {
|
|
115
|
+
lines.push(`Coût estimé: ${formatCost(estimatedCost)}`);
|
|
116
|
+
}
|
|
117
|
+
if (responseHeaders['x-model-used']) {
|
|
118
|
+
lines.push(`Modèle utilisé: ${responseHeaders['x-model-used']}`);
|
|
119
|
+
}
|
|
120
|
+
if (responseHeaders['x-request-id']) {
|
|
121
|
+
lines.push(`Request ID: ${responseHeaders['x-request-id']}`);
|
|
122
|
+
}
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (err.message?.includes('402') || err.message?.includes('Payment')) {
|
|
127
|
+
return `❌ Crédits insuffisants.`;
|
|
128
|
+
}
|
|
129
|
+
if (err.message?.includes('401') || err.message?.includes('403')) {
|
|
130
|
+
return `❌ Clé API invalide ou non autorisée.`;
|
|
131
|
+
}
|
|
132
|
+
if (err.message?.includes('Timeout')) {
|
|
133
|
+
return `❌ Timeout - La génération musicale a pris trop de temps.
|
|
134
|
+
💡 Essayez une durée plus courte.`;
|
|
135
|
+
}
|
|
136
|
+
return `❌ Erreur génération musicale: ${err.message}`;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gen_video Tool - Pollinations Video Generation
|
|
3
|
+
*
|
|
4
|
+
* Updated: 2026-02-12 - Verified API Reference
|
|
5
|
+
*
|
|
6
|
+
* Video models with different capabilities:
|
|
7
|
+
* - T2V (Text-to-Video): grok-video, ltx-2, veo, seedance, seedance-pro
|
|
8
|
+
* - I2V (Image-to-Video): wan (I2V ONLY!), veo, seedance, seedance-pro
|
|
9
|
+
* - Veo Interpolation: Uses image=url1,url2 for transitions
|
|
10
|
+
*
|
|
11
|
+
* Response headers for cost tracking:
|
|
12
|
+
* - x-usage-completion-video-seconds (grok, ltx-2, veo, wan)
|
|
13
|
+
* - x-usage-completion-video-tokens (seedance, seedance-pro)
|
|
14
|
+
*/
|
|
15
|
+
import { type ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
16
|
+
export declare const genVideoTool: ToolDefinition;
|