Update Pollinations API (#5060) * Upgrade Pollinations API Done: text, caption To do: TTS, image Fixes #5020 * Update Pollinations TTS to new API * Update Pollinations API for images

5a7875ba282a0dc52378b997baa5730283a15b51

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
13 files changed, +129 -63Ignore whitespace
public/index.html+12 -9
@@ -3772,19 +3772,22 @@
3772 </select>3772 </select>
3773 </div>3773 </div>
3774 <div id="pollinations_form" data-source="pollinations">3774 <div id="pollinations_form" data-source="pollinations">
3775 <h4>
3776 <a href="https://enter.pollinations.ai/" target="_blank" rel="noopener noreferrer" data-i18n="Pollinations API Key">
3777 Pollinations API Key
3778 </a>
3779 </h4>
3780 <div class="flex-container">
3781 <input id="api_key_pollinations" name="api_key_pollinations" class="text_pole flex1" value="" type="text" autocomplete="off">
3782 <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_pollinations"></div>
3783 </div>
3784 <div data-for="api_key_pollinations" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3785 For privacy reasons, your API key will be hidden after you click 'Connect'.
3786 </div>
3775 <h4 data-i18n="Pollinations Model">Pollinations Model</h4>3787 <h4 data-i18n="Pollinations Model">Pollinations Model</h4>
3776 <select id="model_pollinations_select">3788 <select id="model_pollinations_select">
3777 <!-- Populated by JavaScript -->3789 <!-- Populated by JavaScript -->
3778 </select>3790 </select>
3779 <div class="info-block hint">
3780 <a href="https://pollinations.ai/" target="_blank" rel="noopener noreferrer" data-i18n="Provided free of charge by Pollinations.AI">
3781 Provided free of charge by Pollinations.AI
3782 </a>
3783 <br>
3784 <span data-i18n="Avoid sending sensitive information. Provider's outputs may include ads.">
3785 Avoid sending sensitive information. Provider's outputs may include ads.
3786 </span>
3787 </div>
3788 </div>3791 </div>
3789 <div id="moonshot_form" data-source="moonshot">3792 <div id="moonshot_form" data-source="moonshot">
3790 <h4>3793 <h4>
public/scripts/RossAscends-mods.js+1 -1
@@ -408,7 +408,7 @@ function RA_autoconnect(PrevApi) {
408 || (secret_state[SECRET_KEYS.FIREWORKS] && oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS)408 || (secret_state[SECRET_KEYS.FIREWORKS] && oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS)
409 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)409 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)
410 || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)410 || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)
411 || (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)411 || (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
412 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)412 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
413 || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)413 || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)
414 ) {414 ) {
public/scripts/extensions/caption/index.js+2 -1
@@ -507,6 +507,7 @@ jQuery(async function () {
507 'nanogpt': SECRET_KEYS.NANOGPT,507 'nanogpt': SECRET_KEYS.NANOGPT,
508 'chutes': SECRET_KEYS.CHUTES,508 'chutes': SECRET_KEYS.CHUTES,
509 'electronhub': SECRET_KEYS.ELECTRONHUB,509 'electronhub': SECRET_KEYS.ELECTRONHUB,
510 'pollinations': SECRET_KEYS.POLLINATIONS,
510 };511 };
511512
512 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {513 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
@@ -530,7 +531,7 @@ jQuery(async function () {
530 }531 }
531532
532 // Custom API doesn't need additional checks533 // Custom API doesn't need additional checks
533 if (api === 'custom' || api === 'pollinations') {534 if (api === 'custom') {
534 return true;535 return true;
535 }536 }
536 }537 }
public/scripts/extensions/shared.js+4 -0
@@ -279,6 +279,10 @@ function throwIfInvalidModel(useReverseProxy) {
279 if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {279 if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {
280 throw new Error('Z.AI API key is not set.');280 throw new Error('Z.AI API key is not set.');
281 }281 }
282
283 if (multimodalApi === 'pollinations' && !secret_state[SECRET_KEYS.POLLINATIONS]) {
284 throw new Error('Pollinations API key is not set.');
285 }
282}286}
283287
284/**288/**
public/scripts/extensions/stable-diffusion/index.js+16 -10
@@ -1955,6 +1955,8 @@ async function loadXAIModels() {
1955}1955}
19561956
1957async function loadPollinationsModels() {1957async function loadPollinationsModels() {
1958 $('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]);
1959
1958 const result = await fetch('/api/sd/pollinations/models', {1960 const result = await fetch('/api/sd/pollinations/models', {
1959 method: 'POST',1961 method: 'POST',
1960 headers: getRequestHeaders({ omitContentType: true }),1962 headers: getRequestHeaders({ omitContentType: true }),
@@ -3312,7 +3314,7 @@ async function generatePollinationsImage(prompt, negativePrompt, signal) {
33123314
3313 if (result.ok) {3315 if (result.ok) {
3314 const data = await result.json();3316 const data = await result.json();
3315 return { format: 'jpg', data: data?.image };3317 return { format: data?.format, data: data?.image };
3316 } else {3318 } else {
3317 const text = await result.text();3319 const text = await result.text();
3318 throw new Error(text);3320 throw new Error(text);
@@ -4757,7 +4759,7 @@ function isValidState() {
4757 case sources.togetherai:4759 case sources.togetherai:
4758 return secret_state[SECRET_KEYS.TOGETHERAI];4760 return secret_state[SECRET_KEYS.TOGETHERAI];
4759 case sources.pollinations:4761 case sources.pollinations:
4760 return true;4762 return secret_state[SECRET_KEYS.POLLINATIONS];
4761 case sources.stability:4763 case sources.stability:
4762 return secret_state[SECRET_KEYS.STABILITY];4764 return secret_state[SECRET_KEYS.STABILITY];
4763 case sources.huggingface:4765 case sources.huggingface:
@@ -5514,15 +5516,19 @@ jQuery(async () => {
55145516
5515 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {5517 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
5516 eventSource.on(event, async (/** @type {string} */ key) => {5518 eventSource.on(event, async (/** @type {string} */ key) => {
5517 switch (key) {5519 const keySourceMap = {
5518 case SECRET_KEYS.BFL:5520 [sources.bfl]: SECRET_KEYS.BFL,
5519 case SECRET_KEYS.FALAI:5521 [sources.falai]: SECRET_KEYS.FALAI,
5520 case SECRET_KEYS.STABILITY:5522 [sources.stability]: SECRET_KEYS.STABILITY,
5521 case SECRET_KEYS.AIMLAPI:5523 [sources.aimlapi]: SECRET_KEYS.AIMLAPI,
5522 case SECRET_KEYS.COMFY_RUNPOD:5524 [sources.comfy]: SECRET_KEYS.COMFY_RUNPOD,
5523 await loadSettingOptions();5525 [sources.pollinations]: SECRET_KEYS.POLLINATIONS,
5524 break;5526 };
5527 const shouldReloadOptions = Object.entries(keySourceMap).some(([k, v]) => k === extension_settings.sd.source && v === key);
5528 if (!shouldReloadOptions) {
5529 return;
5525 }5530 }
5531 await loadSettingOptions();
5526 });5532 });
5527 });5533 });
55285534
public/scripts/extensions/stable-diffusion/settings.html+8 -3
@@ -263,9 +263,14 @@
263 </div>263 </div>
264 </div>264 </div>
265 <div data-sd-source="pollinations">265 <div data-sd-source="pollinations">
266 <p>266 <a href="https://enter.pollinations.ai">Pollinations.ai</a>
267 <a href="https://pollinations.ai">Pollinations.ai</a>267 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
268 </p>268 <strong class="flex1" data-i18n="API Key">API Key</strong>
269 <div id="sd_pollinations_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_pollinations">
270 <i class="fa-fw fa-solid fa-key"></i>
271 <span data-i18n="Click to set">Click to set</span>
272 </div>
273 </div>
269 <div class="flex-container">274 <div class="flex-container">
270 <label class="flex1 checkbox_label" for="sd_pollinations_enhance" data-i18n="[title]Enables prompt enhancing (passes prompts through an LLM to add detail)." title="Enables prompt enhancing (passes prompts through an LLM to add detail).">275 <label class="flex1 checkbox_label" for="sd_pollinations_enhance" data-i18n="[title]Enables prompt enhancing (passes prompts through an LLM to add detail)." title="Enables prompt enhancing (passes prompts through an LLM to add detail).">
271 <input id="sd_pollinations_enhance" type="checkbox" />276 <input id="sd_pollinations_enhance" type="checkbox" />
public/scripts/openai.js+2 -5
@@ -2707,10 +2707,6 @@ export async function createGenerationParameters(settings, model, type, messages
2707 }2707 }
2708 }2708 }
27092709
2710 if (settings.chat_completion_source === chat_completion_sources.POLLINATIONS) {
2711 delete generate_data.max_tokens;
2712 }
2713
2714 // https://docs.electronhub.ai/api-reference/chat/completions2710 // https://docs.electronhub.ai/api-reference/chat/completions
2715 if (settings.chat_completion_source === chat_completion_sources.ELECTRONHUB) {2711 if (settings.chat_completion_source === chat_completion_sources.ELECTRONHUB) {
2716 generate_data.top_k = Number(settings.top_k_openai);2712 generate_data.top_k = Number(settings.top_k_openai);
@@ -5608,6 +5604,7 @@ async function onConnectButtonClick(e) {
5608 [chat_completion_sources.AZURE_OPENAI]: { key: SECRET_KEYS.AZURE_OPENAI, selector: '#api_key_azure_openai', proxy: false },5604 [chat_completion_sources.AZURE_OPENAI]: { key: SECRET_KEYS.AZURE_OPENAI, selector: '#api_key_azure_openai', proxy: false },
5609 [chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: true },5605 [chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: true },
5610 [chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },5606 [chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },
5607 [chat_completion_sources.POLLINATIONS]: { key: SECRET_KEYS.POLLINATIONS, selector: '#api_key_pollinations', proxy: false },
5611 };5608 };
56125609
5613 // Vertex AI Express version - use API key5610 // Vertex AI Express version - use API key
@@ -5887,7 +5884,7 @@ export function isImageInliningSupported() {
5887 case chat_completion_sources.ELECTRONHUB:5884 case chat_completion_sources.ELECTRONHUB:
5888 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.electronhub_model)?.metadata?.vision);5885 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.electronhub_model)?.metadata?.vision);
5889 case chat_completion_sources.POLLINATIONS:5886 case chat_completion_sources.POLLINATIONS:
5890 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision);5887 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.input_modalities?.includes('image'));
5891 case chat_completion_sources.COMETAPI:5888 case chat_completion_sources.COMETAPI:
5892 return true;5889 return true;
5893 case chat_completion_sources.MOONSHOT:5890 case chat_completion_sources.MOONSHOT:
public/scripts/secrets.js+3 -0
@@ -73,6 +73,7 @@ export const SECRET_KEYS = {
73 ZAI: 'api_key_zai',73 ZAI: 'api_key_zai',
74 SILICONFLOW: 'api_key_siliconflow',74 SILICONFLOW: 'api_key_siliconflow',
75 ELEVENLABS: 'api_key_elevenlabs',75 ELEVENLABS: 'api_key_elevenlabs',
76 POLLINATIONS: 'api_key_pollinations',
76};77};
7778
78const FRIENDLY_NAMES = {79const FRIENDLY_NAMES = {
@@ -134,6 +135,7 @@ const FRIENDLY_NAMES = {
134 [SECRET_KEYS.ZAI]: 'Z.AI',135 [SECRET_KEYS.ZAI]: 'Z.AI',
135 [SECRET_KEYS.SILICONFLOW]: 'SiliconFlow',136 [SECRET_KEYS.SILICONFLOW]: 'SiliconFlow',
136 [SECRET_KEYS.ELEVENLABS]: 'ElevenLabs TTS',137 [SECRET_KEYS.ELEVENLABS]: 'ElevenLabs TTS',
138 [SECRET_KEYS.POLLINATIONS]: 'Pollinations',
137};139};
138140
139const INPUT_MAP = {141const INPUT_MAP = {
@@ -177,6 +179,7 @@ const INPUT_MAP = {
177 [SECRET_KEYS.ZAI]: '#api_key_zai',179 [SECRET_KEYS.ZAI]: '#api_key_zai',
178 [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',180 [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',
179 [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod',181 [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod',
182 [SECRET_KEYS.POLLINATIONS]: '#api_key_pollinations',
180};183};
181184
182const getLabel = () => moment().format('L LT');185const getLabel = () => moment().format('L LT');
src/endpoints/backends/chat-completions.js+16 -12
@@ -79,7 +79,7 @@ const API_NANOGPT = 'https://nano-gpt.com/api/v1';
79const API_DEEPSEEK = 'https://api.deepseek.com/beta';79const API_DEEPSEEK = 'https://api.deepseek.com/beta';
80const API_XAI = 'https://api.x.ai/v1';80const API_XAI = 'https://api.x.ai/v1';
81const API_AIMLAPI = 'https://api.aimlapi.com/v1';81const API_AIMLAPI = 'https://api.aimlapi.com/v1';
82const API_POLLINATIONS = 'https://text.pollinations.ai/openai';82const API_POLLINATIONS = 'https://gen.pollinations.ai/v1';
83const API_MOONSHOT = 'https://api.moonshot.ai/v1';83const API_MOONSHOT = 'https://api.moonshot.ai/v1';
84const API_FIREWORKS = 'https://api.fireworks.ai/inference/v1';84const API_FIREWORKS = 'https://api.fireworks.ai/inference/v1';
85const API_COMETAPI = 'https://api.cometapi.com/v1';85const API_COMETAPI = 'https://api.cometapi.com/v1';
@@ -1690,8 +1690,8 @@ router.post('/status', async function (request, statusResponse) {
1690 apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);1690 apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
1691 headers = { ...AIMLAPI_HEADERS };1691 headers = { ...AIMLAPI_HEADERS };
1692 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {1692 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
1693 apiUrl = 'https://text.pollinations.ai';1693 apiUrl = 'https://gen.pollinations.ai/text';
1694 apiKey = 'NONE';1694 apiKey = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
1695 headers = {};1695 headers = {};
1696 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {1696 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
1697 apiUrl = API_GROQ;1697 apiUrl = API_GROQ;
@@ -2244,18 +2244,19 @@ router.post('/generate', async function (request, response) {
2244 }2244 }
2245 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {2245 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
2246 apiUrl = API_POLLINATIONS;2246 apiUrl = API_POLLINATIONS;
2247 apiKey = 'NONE';2247 apiKey = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
2248 headers = {2248 headers = {};
2249 'Authorization': '',
2250 };
2251 bodyParams = {2249 bodyParams = {
2252 reasoning_effort: request.body.reasoning_effort,2250 reasoning_effort: request.body.reasoning_effort,
2253 private: true,
2254 referrer: 'sillytavern',
2255 seed: request.body.seed ?? Math.floor(Math.random() * 99999999),2251 seed: request.body.seed ?? Math.floor(Math.random() * 99999999),
2256 };2252 };
2257 if (request.body.json_schema) {2253 if (request.body.json_schema) {
2258 setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);2254 bodyParams['response_format'] = {
2255 type: 'json_schema',
2256 json_schema: {
2257 schema: request.body.json_schema.value,
2258 },
2259 };
2259 }2260 }
2260 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {2261 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {
2261 apiUrl = new URL(request.body.reverse_proxy || API_MOONSHOT).toString();2262 apiUrl = new URL(request.body.reverse_proxy || API_MOONSHOT).toString();
@@ -2434,7 +2435,7 @@ const multimodalModels = express.Router();
24342435
2435multimodalModels.post('/pollinations', async (_req, res) => {2436multimodalModels.post('/pollinations', async (_req, res) => {
2436 try {2437 try {
2437 const response = await fetch('https://text.pollinations.ai/models');2438 const response = await fetch('https://gen.pollinations.ai/models');
24382439
2439 if (!response.ok) {2440 if (!response.ok) {
2440 return res.json([]);2441 return res.json([]);
@@ -2447,7 +2448,10 @@ multimodalModels.post('/pollinations', async (_req, res) => {
2447 return res.json([]);2448 return res.json([]);
2448 }2449 }
24492450
2450 const multimodalModels = data.filter(m => m?.vision).map(m => m.name);2451 const multimodalModels = data
2452 .filter(m => Array.isArray(m?.input_modalities))
2453 .filter(m => m.input_modalities.includes('image'))
2454 .map(m => m.name);
2451 return res.json(multimodalModels);2455 return res.json(multimodalModels);
2452 } catch (error) {2456 } catch (error) {
2453 console.error(error);2457 console.error(error);
src/endpoints/openai.js+7 -3
@@ -97,7 +97,12 @@ router.post('/caption-image', async (request, response) => {
97 bodyParams.max_tokens = 4096; // default is 102497 bodyParams.max_tokens = 4096; // default is 1024
98 }98 }
9999
100 const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations'];100 if (request.body.api === 'pollinations') {
101 key = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
102 bodyParams.seed = Math.floor(Math.random() * Math.pow(2, 32));
103 }
104
105 const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp'];
101 if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) {106 if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) {
102 console.warn('No key found for API', request.body.api);107 console.warn('No key found for API', request.body.api);
103 return response.sendStatus(400);108 return response.sendStatus(400);
@@ -173,8 +178,7 @@ router.post('/caption-image', async (request, response) => {
173 }178 }
174179
175 if (request.body.api === 'pollinations') {180 if (request.body.api === 'pollinations') {
176 headers = { Authorization: '' };181 apiUrl = 'https://gen.pollinations.ai/v1/chat/completions';
177 apiUrl = 'https://text.pollinations.ai/openai/chat/completions';
178 }182 }
179183
180 if (request.body.api === 'moonshot' && !request.body.reverse_proxy) {184 if (request.body.api === 'moonshot' && !request.body.reverse_proxy) {
src/endpoints/secrets.js+1 -0
@@ -66,6 +66,7 @@ export const SECRET_KEYS = {
66 ZAI: 'api_key_zai',66 ZAI: 'api_key_zai',
67 SILICONFLOW: 'api_key_siliconflow',67 SILICONFLOW: 'api_key_siliconflow',
68 ELEVENLABS: 'api_key_elevenlabs',68 ELEVENLABS: 'api_key_elevenlabs',
69 POLLINATIONS: 'api_key_pollinations',
69};70};
7071
71/**72/**
src/endpoints/speech.js+39 -8
@@ -89,7 +89,7 @@ pollinations.post('/voices', async (req, res) => {
89 try {89 try {
90 const model = req.body.model || 'openai-audio';90 const model = req.body.model || 'openai-audio';
9191
92 const response = await fetch('https://text.pollinations.ai/models');92 const response = await fetch('https://gen.pollinations.ai/text/models');
9393
94 if (!response.ok) {94 if (!response.ok) {
95 throw new Error('Failed to fetch Pollinations models');95 throw new Error('Failed to fetch Pollinations models');
@@ -116,25 +116,56 @@ pollinations.post('/voices', async (req, res) => {
116116
117pollinations.post('/generate', async (req, res) => {117pollinations.post('/generate', async (req, res) => {
118 try {118 try {
119 const key = readSecret(req.user.directories, SECRET_KEYS.POLLINATIONS);
120 if (!key) {
121 console.warn('No API key saved for Pollinations TTS.');
122 return res.sendStatus(400);
123 }
124
119 const text = req.body.text;125 const text = req.body.text;
120 const model = req.body.model || 'openai-audio';126 const model = req.body.model || 'openai-audio';
121 const voice = req.body.voice || 'alloy';127 const voice = req.body.voice || 'alloy';
122128
123 const url = new URL(`https://text.pollinations.ai/generate/${encodeURIComponent(text)}`);129 console.debug('Pollinations TTS request', { text, model, voice });
124 url.searchParams.append('model', model);
125 url.searchParams.append('voice', voice);
126 url.searchParams.append('referrer', 'sillytavern');
127 console.info('Pollinations request URL:', url.toString());
128130
129 const response = await fetch(url);131 const response = await fetch('https://gen.pollinations.ai/v1/chat/completions', {
132 method: 'POST',
133 headers: {
134 'Authorization': `Bearer ${key}`,
135 'Content-Type': 'application/json',
136 },
137 body: JSON.stringify({
138 model: model,
139 stream: false,
140 modalities: ['text', 'audio'],
141 seed: Math.floor(Math.random() * Math.pow(2, 32)),
142 audio: {
143 format: 'mp3',
144 voice: voice,
145 },
146 messages: [{
147 role: 'user',
148 content: text,
149 }],
150 }),
151 });
130152
131 if (!response.ok) {153 if (!response.ok) {
132 const text = await response.text();154 const text = await response.text();
133 throw new Error(`Failed to generate audio from Pollinations: ${text}`);155 throw new Error(`Failed to generate audio from Pollinations: ${text}`);
134 }156 }
135157
158 /** @type {any} */
159 const data = await response.json();
160 const audioData = data?.choices?.[0]?.message?.audio?.data;
161
162 if (!audioData) {
163 console.warn('Pollinations TTS audio data is missing from the response');
164 return res.sendStatus(500);
165 }
166
136 res.set('Content-Type', 'audio/mpeg');167 res.set('Content-Type', 'audio/mpeg');
137 forwardFetchResponse(response, res);168 return res.send(Buffer.from(audioData, 'base64'));
138 } catch (error) {169 } catch (error) {
139 console.error(error);170 console.error(error);
140 return res.sendStatus(500);171 return res.sendStatus(500);
src/endpoints/stable-diffusion.js+18 -11
@@ -8,6 +8,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
8import FormData from 'form-data';8import FormData from 'form-data';
9import urlJoin from 'url-join';9import urlJoin from 'url-join';
10import _ from 'lodash';10import _ from 'lodash';
11import mime from 'mime-types';
1112
12import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js';13import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js';
13import { readSecret, SECRET_KEYS } from './secrets.js';14import { readSecret, SECRET_KEYS } from './secrets.js';
@@ -898,7 +899,7 @@ const pollinations = express.Router();
898899
899pollinations.post('/models', async (_request, response) => {900pollinations.post('/models', async (_request, response) => {
900 try {901 try {
901 const modelsUrl = new URL('https://image.pollinations.ai/models');902 const modelsUrl = new URL('https://gen.pollinations.ai/image/models');
902 const result = await fetch(modelsUrl);903 const result = await fetch(modelsUrl);
903904
904 if (!result.ok) {905 if (!result.ok) {
@@ -913,7 +914,7 @@ pollinations.post('/models', async (_request, response) => {
913 throw new Error('Pollinations request failed.');914 throw new Error('Pollinations request failed.');
914 }915 }
915916
916 const models = data.map(x => ({ value: x, text: x }));917 const models = data.map(x => ({ value: x.name, text: x.name }));
917 return response.send(models);918 return response.send(models);
918 } catch (error) {919 } catch (error) {
919 console.error(error);920 console.error(error);
@@ -923,17 +924,19 @@ pollinations.post('/models', async (_request, response) => {
923924
924pollinations.post('/generate', async (request, response) => {925pollinations.post('/generate', async (request, response) => {
925 try {926 try {
926 const promptUrl = new URL(`https://image.pollinations.ai/prompt/${encodeURIComponent(request.body.prompt)}`);927 const key = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
928 if (!key) {
929 console.warn('Pollinations API key not found.');
930 return response.sendStatus(400);
931 }
932
933 const promptUrl = new URL(`https://gen.pollinations.ai/image/${encodeURIComponent(request.body.prompt)}`);
927 const params = new URLSearchParams({934 const params = new URLSearchParams({
928 model: String(request.body.model),935 model: String(request.body.model),
929 negative_prompt: String(request.body.negative_prompt),936 negative_prompt: String(request.body.negative_prompt),
930 seed: String(request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000)),937 seed: String(request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000)),
931 width: String(request.body.width ?? 1024),938 width: String(request.body.width ?? 1024),
932 height: String(request.body.height ?? 1024),939 height: String(request.body.height ?? 1024),
933 nologo: String(true),
934 nofeed: String(true),
935 private: String(true),
936 referrer: 'sillytavern',
937 });940 });
938 if (request.body.enhance) {941 if (request.body.enhance) {
939 params.set('enhance', String(true));942 params.set('enhance', String(true));
@@ -942,7 +945,12 @@ pollinations.post('/generate', async (request, response) => {
942945
943 console.info('Pollinations request URL:', promptUrl.toString());946 console.info('Pollinations request URL:', promptUrl.toString());
944947
945 const result = await fetch(promptUrl);948 const result = await fetch(promptUrl, {
949 method: 'GET',
950 headers: {
951 'Authorization': `Bearer ${key}`,
952 },
953 });
946954
947 if (!result.ok) {955 if (!result.ok) {
948 const text = await result.text();956 const text = await result.text();
@@ -950,10 +958,9 @@ pollinations.post('/generate', async (request, response) => {
950 throw new Error('Pollinations request failed.');958 throw new Error('Pollinations request failed.');
951 }959 }
952960
961 const format = result.headers.get('Content-Type')?.toString() || 'image/jpeg';
953 const buffer = await result.arrayBuffer();962 const buffer = await result.arrayBuffer();
954 const base64 = Buffer.from(buffer).toString('base64');963 return response.send({ image: Buffer.from(buffer).toString('base64'), format: mime.extension(format) || 'jpg' });
955
956 return response.send({ image: base64 });
957 } catch (error) {964 } catch (error) {
958 console.error(error);965 console.error(error);
959 return response.sendStatus(500);966 return response.sendStatus(500);