opencode-pollinations-plugin 6.1.0-beta.1 → 6.1.0-beta.11
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 +106 -60
- package/dist/server/config.d.ts +27 -23
- package/dist/server/config.js +24 -50
- 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 +187 -149
- 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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Registry — Conditional Injection System
|
|
3
|
+
*
|
|
4
|
+
* Free Universe (no key): 8 tools always available
|
|
5
|
+
* Enter Universe (with key): +6 Pollinations tools
|
|
6
|
+
*
|
|
7
|
+
* Tools are injected ONCE at plugin init. Restart needed after /poll connect.
|
|
8
|
+
*/
|
|
9
|
+
import { loadConfig } from '../server/config.js';
|
|
10
|
+
// === FREE TOOLS (Always available) ===
|
|
11
|
+
import { genQrcodeTool } from './design/gen_qrcode.js';
|
|
12
|
+
import { genDiagramTool } from './design/gen_diagram.js';
|
|
13
|
+
import { genPaletteTool } from './design/gen_palette.js';
|
|
14
|
+
import { fileToUrlTool } from './power/file_to_url.js';
|
|
15
|
+
import { removeBackgroundTool } from './power/remove_background.js';
|
|
16
|
+
import { extractFramesTool } from './power/extract_frames.js';
|
|
17
|
+
import { extractAudioTool } from './power/extract_audio.js';
|
|
18
|
+
import { rmbgKeysTool } from './power/rmbg_keys.js';
|
|
19
|
+
// === ENTER TOOLS (Require API key) ===
|
|
20
|
+
import { genImageTool } from './pollinations/gen_image.js';
|
|
21
|
+
import { genVideoTool } from './pollinations/gen_video.js';
|
|
22
|
+
import { genAudioTool } from './pollinations/gen_audio.js';
|
|
23
|
+
import { transcribeAudioTool } from './pollinations/transcribe_audio.js';
|
|
24
|
+
import { genMusicTool } from './pollinations/gen_music.js';
|
|
25
|
+
import { deepsearchTool } from './pollinations/deepsearch.js';
|
|
26
|
+
import { searchCrawlScrapeTool } from './pollinations/search_crawl_scrape.js';
|
|
27
|
+
import * as fs from 'fs';
|
|
28
|
+
const LOG_FILE = '/tmp/opencode_pollinations_v4.log';
|
|
29
|
+
function log(msg) {
|
|
30
|
+
try {
|
|
31
|
+
fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] [Tools] ${msg}\n`);
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Detect if a valid API key is present
|
|
37
|
+
*/
|
|
38
|
+
function hasValidKey() {
|
|
39
|
+
const config = loadConfig();
|
|
40
|
+
return !!(config.apiKey && config.apiKey.length > 5 && config.apiKey !== 'dummy');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the tool registry based on user's access level
|
|
44
|
+
*
|
|
45
|
+
* @returns Record<string, Tool> to be spread into the plugin's tool: {} property
|
|
46
|
+
*/
|
|
47
|
+
export function createToolRegistry() {
|
|
48
|
+
const tools = {};
|
|
49
|
+
const keyPresent = hasValidKey();
|
|
50
|
+
// === FREE UNIVERSE: Always injected (8 tools) ===
|
|
51
|
+
// Design tools (3)
|
|
52
|
+
tools['gen_qrcode'] = genQrcodeTool;
|
|
53
|
+
tools['gen_diagram'] = genDiagramTool;
|
|
54
|
+
tools['gen_palette'] = genPaletteTool;
|
|
55
|
+
// Power tools (5)
|
|
56
|
+
tools['file_to_url'] = fileToUrlTool;
|
|
57
|
+
tools['remove_background'] = removeBackgroundTool;
|
|
58
|
+
tools['extract_frames'] = extractFramesTool;
|
|
59
|
+
tools['extract_audio'] = extractAudioTool;
|
|
60
|
+
tools['rmbg_keys'] = rmbgKeysTool;
|
|
61
|
+
log(`Free tools injected: ${Object.keys(tools).length}`);
|
|
62
|
+
// === ENTER UNIVERSE: Only with valid API key (+7 tools) ===
|
|
63
|
+
if (keyPresent) {
|
|
64
|
+
// Pollinations media tools
|
|
65
|
+
tools['gen_image'] = genImageTool;
|
|
66
|
+
tools['gen_video'] = genVideoTool;
|
|
67
|
+
tools['gen_audio'] = genAudioTool;
|
|
68
|
+
tools['transcribe_audio'] = transcribeAudioTool;
|
|
69
|
+
tools['gen_music'] = genMusicTool;
|
|
70
|
+
// Search tools
|
|
71
|
+
tools['deepsearch'] = deepsearchTool;
|
|
72
|
+
tools['search_crawl_scrape'] = searchCrawlScrapeTool;
|
|
73
|
+
log(`Enter tools injected (key detected). Total: ${Object.keys(tools).length}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
log(`Enter tools SKIPPED (no key). Total: ${Object.keys(tools).length}`);
|
|
77
|
+
}
|
|
78
|
+
return tools;
|
|
79
|
+
}
|
|
80
|
+
// Re-export for convenience
|
|
81
|
+
export { genImageTool, genVideoTool, genAudioTool, transcribeAudioTool, genMusicTool, deepsearchTool, searchCrawlScrapeTool, };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deepsearch Tool - Deep Research with AI
|
|
3
|
+
*
|
|
4
|
+
* Uses perplexity-reasoning for in-depth research and analysis
|
|
5
|
+
*/
|
|
6
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
7
|
+
import { getApiKey, httpsPost, } from './shared.js';
|
|
8
|
+
// ─── Tool Definition ──────────────────────────────────────────────────────
|
|
9
|
+
export const deepsearchTool = tool({
|
|
10
|
+
description: `Perform deep research and analysis on a topic using AI reasoning.
|
|
11
|
+
|
|
12
|
+
**Model:** perplexity-reasoning
|
|
13
|
+
|
|
14
|
+
This tool provides comprehensive research with:
|
|
15
|
+
- Multi-step reasoning
|
|
16
|
+
- Source citations
|
|
17
|
+
- In-depth analysis
|
|
18
|
+
- Fact verification
|
|
19
|
+
|
|
20
|
+
**Use for:**
|
|
21
|
+
- Complex research questions
|
|
22
|
+
- Technical analysis
|
|
23
|
+
- Fact-checking
|
|
24
|
+
- Comparative studies
|
|
25
|
+
|
|
26
|
+
**Cost:** ~0.000002-0.000008 🌻 per token (very affordable)`,
|
|
27
|
+
args: {
|
|
28
|
+
query: tool.schema.string().describe('Research query or question to investigate'),
|
|
29
|
+
depth: tool.schema.enum(['quick', 'standard', 'thorough']).optional()
|
|
30
|
+
.describe('Research depth (default: standard)'),
|
|
31
|
+
},
|
|
32
|
+
async execute(args, context) {
|
|
33
|
+
const apiKey = getApiKey();
|
|
34
|
+
if (!apiKey) {
|
|
35
|
+
return `❌ Deep Search nécessite une clé API Pollinations.
|
|
36
|
+
🔧 Connectez votre clé avec /pollinations connect`;
|
|
37
|
+
}
|
|
38
|
+
const model = 'perplexity-reasoning';
|
|
39
|
+
const depth = args.depth || 'standard';
|
|
40
|
+
// Metadata
|
|
41
|
+
context.metadata({ title: `🔍 Deep Search: ${args.query.substring(0, 50)}...` });
|
|
42
|
+
try {
|
|
43
|
+
// Build system prompt based on depth
|
|
44
|
+
const systemPrompts = {
|
|
45
|
+
quick: 'Provide a concise but thorough answer with key sources. Be efficient.',
|
|
46
|
+
standard: 'Provide comprehensive research with analysis, sources, and reasoning steps.',
|
|
47
|
+
thorough: 'Provide exhaustive research with multiple perspectives, detailed analysis, all relevant sources, and thorough fact-checking. Consider edge cases and alternative viewpoints.',
|
|
48
|
+
};
|
|
49
|
+
const { data } = await httpsPost('https://gen.pollinations.ai/v1/chat/completions', {
|
|
50
|
+
model: model,
|
|
51
|
+
messages: [
|
|
52
|
+
{ role: 'system', content: systemPrompts[depth] },
|
|
53
|
+
{ role: 'user', content: args.query },
|
|
54
|
+
],
|
|
55
|
+
max_tokens: depth === 'thorough' ? 8000 : depth === 'standard' ? 4000 : 2000,
|
|
56
|
+
}, {
|
|
57
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
58
|
+
});
|
|
59
|
+
const jsonData = JSON.parse(data.toString());
|
|
60
|
+
const content = jsonData.choices?.[0]?.message?.content || 'No response';
|
|
61
|
+
// Format result
|
|
62
|
+
const lines = [
|
|
63
|
+
`🔍 Deep Search Results`,
|
|
64
|
+
`━━━━━━━━━━━━━━━━━━`,
|
|
65
|
+
`Query: ${args.query}`,
|
|
66
|
+
`Depth: ${depth}`,
|
|
67
|
+
`Model: ${model}`,
|
|
68
|
+
``,
|
|
69
|
+
content,
|
|
70
|
+
];
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err.message?.includes('402') || err.message?.includes('Payment')) {
|
|
75
|
+
return `❌ Crédits insuffisants.`;
|
|
76
|
+
}
|
|
77
|
+
return `❌ Erreur Deep Search: ${err.message}`;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
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 { type ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
18
|
+
export declare const genAudioTool: ToolDefinition;
|
|
@@ -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;
|