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
Signed| @@ -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> |
| @@ -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 | ) { |
| @@ -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 | }; |
| 511 | 512 | ||
| 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 | } |
| 531 | 532 | ||
| 532 | // Custom API doesn't need additional checks | 533 | // 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 | } |
| @@ -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 | } |
| 283 | 287 | ||
| 284 | /** | 288 | /** |
| @@ -1955,6 +1955,8 @@ async function loadXAIModels() { | |||
| 1955 | } | 1955 | } |
| 1956 | 1956 | ||
| 1957 | async function loadPollinationsModels() { | 1957 | async 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) { | |||
| 3312 | 3314 | ||
| 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 () => { | |||
| 5514 | 5516 | ||
| 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 | }); |
| 5528 | 5534 | ||
| @@ -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" /> |
| @@ -2707,10 +2707,6 @@ export async function createGenerationParameters(settings, model, type, messages | |||
| 2707 | } | 2707 | } |
| 2708 | } | 2708 | } |
| 2709 | 2709 | ||
| 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/completions | 2710 | // 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 | }; |
| 5612 | 5609 | ||
| 5613 | // Vertex AI Express version - use API key | 5610 | // 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: |
| @@ -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 | }; |
| 77 | 78 | ||
| 78 | const FRIENDLY_NAMES = { | 79 | const 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 | }; |
| 138 | 140 | ||
| 139 | const INPUT_MAP = { | 141 | const 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 | }; |
| 181 | 184 | ||
| 182 | const getLabel = () => moment().format('L LT'); | 185 | const getLabel = () => moment().format('L LT'); |
| @@ -79,7 +79,7 @@ const API_NANOGPT = 'https://nano-gpt.com/api/v1'; | |||
| 79 | const API_DEEPSEEK = 'https://api.deepseek.com/beta'; | 79 | const API_DEEPSEEK = 'https://api.deepseek.com/beta'; |
| 80 | const API_XAI = 'https://api.x.ai/v1'; | 80 | const API_XAI = 'https://api.x.ai/v1'; |
| 81 | const API_AIMLAPI = 'https://api.aimlapi.com/v1'; | 81 | const API_AIMLAPI = 'https://api.aimlapi.com/v1'; |
| 82 | const API_POLLINATIONS = 'https://text.pollinations.ai/openai'; | 82 | const API_POLLINATIONS = 'https://gen.pollinations.ai/v1'; |
| 83 | const API_MOONSHOT = 'https://api.moonshot.ai/v1'; | 83 | const API_MOONSHOT = 'https://api.moonshot.ai/v1'; |
| 84 | const API_FIREWORKS = 'https://api.fireworks.ai/inference/v1'; | 84 | const API_FIREWORKS = 'https://api.fireworks.ai/inference/v1'; |
| 85 | const API_COMETAPI = 'https://api.cometapi.com/v1'; | 85 | const 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(); | |||
| 2434 | 2435 | ||
| 2435 | multimodalModels.post('/pollinations', async (_req, res) => { | 2436 | multimodalModels.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'); |
| 2438 | 2439 | ||
| 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 | } |
| 2449 | 2450 | ||
| 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); |
| @@ -97,7 +97,12 @@ router.post('/caption-image', async (request, response) => { | |||
| 97 | bodyParams.max_tokens = 4096; // default is 1024 | 97 | bodyParams.max_tokens = 4096; // default is 1024 |
| 98 | } | 98 | } |
| 99 | 99 | ||
| 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 | } |
| 174 | 179 | ||
| 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 | } |
| 179 | 183 | ||
| 180 | if (request.body.api === 'moonshot' && !request.body.reverse_proxy) { | 184 | if (request.body.api === 'moonshot' && !request.body.reverse_proxy) { |
| @@ -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 | }; |
| 70 | 71 | ||
| 71 | /** | 72 | /** |
| @@ -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'; |
| 91 | 91 | ||
| 92 | const response = await fetch('https://text.pollinations.ai/models'); | 92 | const response = await fetch('https://gen.pollinations.ai/text/models'); |
| 93 | 93 | ||
| 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) => { | |||
| 116 | 116 | ||
| 117 | pollinations.post('/generate', async (req, res) => { | 117 | pollinations.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'; |
| 122 | 128 | ||
| 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()); | ||
| 128 | 130 | ||
| 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 | }); | ||
| 130 | 152 | ||
| 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 | } |
| 135 | 157 | ||
| 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); |
| @@ -8,6 +8,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic'; | |||
| 8 | import FormData from 'form-data'; | 8 | import FormData from 'form-data'; |
| 9 | import urlJoin from 'url-join'; | 9 | import urlJoin from 'url-join'; |
| 10 | import _ from 'lodash'; | 10 | import _ from 'lodash'; |
| 11 | import mime from 'mime-types'; | ||
| 11 | 12 | ||
| 12 | import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js'; | 13 | import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js'; |
| 13 | import { readSecret, SECRET_KEYS } from './secrets.js'; | 14 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| @@ -898,7 +899,7 @@ const pollinations = express.Router(); | |||
| 898 | 899 | ||
| 899 | pollinations.post('/models', async (_request, response) => { | 900 | pollinations.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); |
| 903 | 904 | ||
| 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 | } |
| 915 | 916 | ||
| 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) => { | |||
| 923 | 924 | ||
| 924 | pollinations.post('/generate', async (request, response) => { | 925 | pollinations.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) => { | |||
| 942 | 945 | ||
| 943 | console.info('Pollinations request URL:', promptUrl.toString()); | 946 | console.info('Pollinations request URL:', promptUrl.toString()); |
| 944 | 947 | ||
| 945 | const result = await fetch(promptUrl); | 948 | const result = await fetch(promptUrl, { |
| 949 | method: 'GET', | ||
| 950 | headers: { | ||
| 951 | 'Authorization': `Bearer ${key}`, | ||
| 952 | }, | ||
| 953 | }); | ||
| 946 | 954 | ||
| 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 | } |
| 952 | 960 | ||
| 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); |