Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<link rel="stylesheet" href="js/ui/styles.css">
<link rel="stylesheet" href="js/ui/stylesScreensaver.css">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script defer src="polliLib/polliLib-web.global.js"></script>
<script defer src="js/polliLib/polliLib-web.global.js"></script>
<script defer>
// Configure polliLib default client and expose an explicit client for MCP helpers
(function(){
Expand Down
294 changes: 294 additions & 0 deletions js/polliLib/polliLib-web.global.js

Large diffs are not rendered by default.

298 changes: 298 additions & 0 deletions js/polliLib/polliLib-web.global.js.bak

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions js/polliLib/src/audio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { getDefaultClient } from './client.js';

export async function tts(text, { voice, model = 'openai-audio', referrer } = {}, client = getDefaultClient()) {
const url = `${client.textBase}/${encodeURIComponent(text)}`;
const params = { model };
if (voice) params.voice = voice;
if (referrer) params.referrer = referrer;
const r = await client.get(url, { params });
if (!r.ok) throw new Error(`tts error ${r.status}`);
return await r.blob();
}

export async function stt({ file, data, format, question }, client = getDefaultClient()) {
if (!file && !data) throw new Error("Provide either 'file' or 'data'");
if (!format && file) {
if (file.type && file.type.startsWith('audio/')) format = file.type.split('/')[1];
else if (file.name && file.name.includes('.')) format = file.name.split('.').pop().toLowerCase();
}
if (!format) throw new Error("Audio 'format' is required (e.g., 'mp3' or 'wav')");
const bytes = file ? await file.arrayBuffer() : (data instanceof ArrayBuffer ? data : (data?.buffer ?? data));
const b64 = base64FromArrayBuffer(bytes);
const body = {
model: 'openai-audio',
messages: [{
role: 'user',
content: [
{ type: 'text', text: question ?? 'Transcribe this audio' },
{ type: 'input_audio', input_audio: { data: b64, format } },
],
}],
};
const r = await client.postJson(`${client.textBase}/openai`, body);
if (!r.ok) throw new Error(`stt error ${r.status}`);
return await r.json();
}

function base64FromArrayBuffer(ab) {
const bytes = new Uint8Array(ab);
let binary = '';
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
const sub = bytes.subarray(i, i + chunk);
binary += String.fromCharCode.apply(null, sub);
}
return btoa(binary);
}

64 changes: 64 additions & 0 deletions js/polliLib/src/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Browsers do not allow setting a custom `User-Agent` header, so
// PolliClientWeb relies on the default browser user agent.
export class PolliClientWeb {
constructor({
referrer = inferReferrer(),
imageBase = 'https://image.pollinations.ai',
textBase = 'https://text.pollinations.ai',
timeoutMs = 60_000,
} = {}) {
this.referrer = referrer;
this.imageBase = stripTrail(imageBase);
this.textBase = stripTrail(textBase);
this.timeoutMs = timeoutMs;
}

_addReferrer(u, params) {
const hasRefParam = params && Object.prototype.hasOwnProperty.call(params, 'referrer');
if (!hasRefParam && this.referrer) {
u.searchParams.set('referrer', this.referrer);
} else if (hasRefParam && params.referrer) {
u.searchParams.set('referrer', params.referrer);
}
}

async get(url, { params = {}, headers = {}, stream = false } = {}) {
const u = new URL(url);
for (const [k, v] of Object.entries(params)) {
if (v != null && k !== 'referrer') u.searchParams.set(k, String(v));
}
this._addReferrer(u, params);
const final = u.toString();
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), this.timeoutMs);
try {
return await fetch(final, { method: 'GET', headers, signal: controller.signal });
} finally { clearTimeout(id); }
}

async postJson(url, body, { headers = {}, stream = false } = {}) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), this.timeoutMs);
const hdrs = { 'Content-Type': 'application/json' };
Object.assign(hdrs, headers);
const payload = { ...(body || {}) };
if (this.referrer && payload.referrer == null) payload.referrer = this.referrer;
try {
return await fetch(url, { method: 'POST', headers: hdrs, body: JSON.stringify(payload), signal: controller.signal });
} finally { clearTimeout(id); }
}
}

function inferReferrer() {
try {
if (typeof window !== 'undefined' && window.location && window.location.origin) return window.location.origin;
} catch {}
return null;
}

function stripTrail(s) { return s.endsWith('/') ? s.slice(0, -1) : s; }

export let defaultClient = null;
export function getDefaultClient() { return defaultClient ??= new PolliClientWeb(); }
export function setDefaultClient(c) { defaultClient = c; }

29 changes: 29 additions & 0 deletions js/polliLib/src/feeds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getDefaultClient } from './client.js';
import { sseEvents } from './sse.js';

export async function* imageFeed({ limit } = {}, client = getDefaultClient()) {
const r = await client.get(`${client.imageBase}/feed`, { headers: { 'Accept': 'text/event-stream' } });
if (!r.ok) throw new Error(`imageFeed error ${r.status}`);
let count = 0;
for await (const data of sseEvents(r)) {
try {
const obj = JSON.parse(data);
yield obj;
if (limit != null && ++count >= limit) break;
} catch { /* ignore */ }
}
}

export async function* textFeed({ limit } = {}, client = getDefaultClient()) {
const r = await client.get(`${client.textBase}/feed`, { headers: { 'Accept': 'text/event-stream' } });
if (!r.ok) throw new Error(`textFeed error ${r.status}`);
let count = 0;
for await (const data of sseEvents(r)) {
try {
const obj = JSON.parse(data);
yield obj;
if (limit != null && ++count >= limit) break;
} catch { /* ignore */ }
}
}

31 changes: 31 additions & 0 deletions js/polliLib/src/image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { getDefaultClient } from './client.js';

const bool = v => (v == null ? undefined : (v ? 'true' : 'false'));

export async function image(prompt, {
model, seed, width, height, image, nologo, private: priv, enhance, safe, referrer,
} = {}, client = getDefaultClient()) {
const url = `${client.imageBase}/prompt/${encodeURIComponent(prompt)}`;
const params = {};
if (model) params.model = model;
if (seed != null) params.seed = seed;
if (width != null) params.width = width;
if (height != null) params.height = height;
if (image) params.image = image;
if (nologo != null) params.nologo = bool(nologo);
if (priv != null) params.private = bool(priv);
if (enhance != null) params.enhance = bool(enhance);
if (safe != null) params.safe = bool(safe);
if (referrer) params.referrer = referrer;

const r = await client.get(url, { params });
if (!r.ok) throw new Error(`image error ${r.status}`);
return await r.blob();
}

export async function imageModels(client = getDefaultClient()) {
const r = await client.get(`${client.imageBase}/models`);
if (!r.ok) throw new Error(`imageModels error ${r.status}`);
return await r.json();
}

61 changes: 61 additions & 0 deletions js/polliLib/src/mcp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { image as imageGen } from './image.js';
import { imageModels as listImage } from './image.js';
import { textModels as listText } from './text.js';

export function serverName() { return 'pollinations-multimodal-api'; }

export function toolDefinitions() {
return {
name: serverName(),
tools: [
{ name: 'generateImageUrl', description: 'Generate an image and return its URL', parameters: {
type: 'object', properties: {
prompt: { type: 'string' }, model: { type: 'string' }, seed: { type: 'integer' }, width: { type: 'integer' }, height: { type: 'integer' }, nologo: { type: 'boolean' }, private: { type: 'boolean' }
}, required: ['prompt'] } },
{ name: 'generateImage', description: 'Generate an image and return base64', parameters: {
type: 'object', properties: { prompt: { type: 'string' }, model: { type: 'string' }, seed: { type: 'integer' }, width: { type: 'integer' }, height: { type: 'integer' } }, required: ['prompt'] } },
{ name: 'listImageModels', description: 'List available image models', parameters: { type: 'object', properties: {} } },
{ name: 'respondAudio', description: 'Generate TTS audio (voice) from text', parameters: { type: 'object', properties: { text: { type: 'string' }, voice: { type: 'string' } }, required: ['text'] } },
{ name: 'sayText', description: 'Speak the provided text', parameters: { type: 'object', properties: { text: { type: 'string' }, voice: { type: 'string' } }, required: ['text'] } },
{ name: 'listAudioVoices', description: 'List available voices', parameters: { type: 'object', properties: {} } },
{ name: 'listTextModels', description: 'List text & multimodal models', parameters: { type: 'object', properties: {} } },
{ name: 'listModels', description: 'List models by kind', parameters: { type: 'object', properties: { kind: { type: 'string', enum: ['image','text','audio'] } } } },
]
};
}

export function generateImageUrl(client, params) {
const { prompt, ...rest } = params;
const u = new URL(`${client.imageBase}/prompt/${encodeURIComponent(prompt)}`);
for (const [k, v] of Object.entries(rest)) if (v != null) u.searchParams.set(k, String(v));
if (client.referrer && !u.searchParams.has('referrer')) u.searchParams.set('referrer', client.referrer);
return u.toString();
}

export async function generateImageBase64(client, params) {
const blob = await imageGen(params.prompt, params, client);
const base64 = await blobToBase64(blob);
return base64;
}

export async function listImageModels(client) { return await listImage(client); }
export async function listTextModels(client) { return await listText(client); }

export async function listAudioVoices(client) {
const models = await listText(client);
const voices = models?.['openai-audio']?.voices ?? [];
return voices;
}

async function blobToBase64(blob) {
const ab = await blob.arrayBuffer();
const bytes = new Uint8Array(ab);
let binary = '';
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
const sub = bytes.subarray(i, i + chunk);
binary += String.fromCharCode.apply(null, sub);
}
return btoa(binary);
}

36 changes: 36 additions & 0 deletions js/polliLib/src/pipeline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { text as textGet } from './text.js';
import { image as imageGen } from './image.js';
import { tts as ttsGen } from './audio.js';
import { vision as visionAnalyze } from './vision.js';

export class Context extends Map {}

export class Pipeline {
constructor() { this.steps = []; }
step(s) { this.steps.push(s); return this; }
async execute({ client, context = new Context() } = {}) {
for (const s of this.steps) await s.run({ client, context });
return context;
}
}

export class TextGetStep {
constructor({ prompt, outKey, params = {} }) { this.prompt = prompt; this.outKey = outKey; this.params = params; }
async run({ client, context }) { const v = await textGet(this.prompt, this.params, client); context.set(this.outKey, v); }
}

export class ImageStep {
constructor({ prompt, outKey, params = {} }) { this.prompt = prompt; this.outKey = outKey; this.params = params; }
async run({ client, context }) { const blob = await imageGen(this.prompt, this.params, client); context.set(this.outKey, { blob }); }
}

export class TtsStep {
constructor({ text, outKey, params = {} }) { this.text = text; this.outKey = outKey; this.params = params; }
async run({ client, context }) { const blob = await ttsGen(this.text, this.params, client); context.set(this.outKey, { blob }); }
}

export class VisionUrlStep {
constructor({ imageUrl, outKey, question, params = {} }) { this.imageUrl = imageUrl; this.outKey = outKey; this.params = { question, ...params }; }
async run({ client, context }) { const v = await visionAnalyze({ imageUrl: this.imageUrl, ...this.params }, client); context.set(this.outKey, v); }
}

37 changes: 37 additions & 0 deletions js/polliLib/src/sse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Minimal SSE parser for fetch(Response).body (ReadableStream)
export async function* sseEvents(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let eventLines = [];
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let idx;
while ((idx = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, idx).replace(/\r$/, '');
buffer = buffer.slice(idx + 1);
if (line === '') {
if (eventLines.length) {
const data = eventLines
.filter(l => l.startsWith('data:'))
.map(l => l.slice(5).trimStart())
.join('\n');
eventLines = [];
if (data) yield data;
}
} else {
eventLines.push(line);
}
}
}
if (eventLines.length) {
const data = eventLines
.filter(l => l.startsWith('data:'))
.map(l => l.slice(5).trimStart())
.join('\n');
if (data) yield data;
}
}

77 changes: 77 additions & 0 deletions js/polliLib/src/text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { getDefaultClient } from './client.js';
import { sseEvents } from './sse.js';

export async function text(prompt, {
model, seed, temperature, top_p, presence_penalty, frequency_penalty, json, system, stream, private: priv, referrer,
} = {}, client = getDefaultClient()) {
const url = `${client.textBase}/${encodeURIComponent(prompt)}`;
const params = {};
if (model) params.model = model;
if (seed != null) params.seed = seed;
if (temperature != null) params.temperature = temperature;
if (top_p != null) params.top_p = top_p;
if (presence_penalty != null) params.presence_penalty = presence_penalty;
if (frequency_penalty != null) params.frequency_penalty = frequency_penalty;
if (json) params.json = 'true';
if (system) params.system = system;
if (priv != null) params.private = !!priv;
if (referrer) params.referrer = referrer;

if (stream) {
params.stream = 'true';
const r = await client.get(url, { params, headers: { 'Accept': 'text/event-stream' } });
if (!r.ok) throw new Error(`text(stream) error ${r.status}`);
return (async function* () {
for await (const data of sseEvents(r)) {
if (String(data).trim() === '[DONE]') break;
yield data;
}
})();
} else {
const r = await client.get(url, { params });
if (!r.ok) throw new Error(`text error ${r.status}`);
return await r.text();
}
}

export async function chat({ model, messages, seed, temperature, top_p, presence_penalty, frequency_penalty, max_tokens, stream, private: priv, tools, tool_choice, referrer }, client = getDefaultClient()) {
const url = `${client.textBase}/openai`;
const body = { model, messages };
if (seed != null) body.seed = seed;
if (temperature != null) body.temperature = temperature;
if (top_p != null) body.top_p = top_p;
if (presence_penalty != null) body.presence_penalty = presence_penalty;
if (frequency_penalty != null) body.frequency_penalty = frequency_penalty;
if (max_tokens != null) body.max_tokens = max_tokens;
if (priv != null) body.private = !!priv;
if (tools) body.tools = tools;
if (tool_choice) body.tool_choice = tool_choice;
if (referrer) body.referrer = referrer;

if (stream) {
body.stream = true;
const r = await client.postJson(url, body, { headers: { 'Accept': 'text/event-stream' } });
if (!r.ok) throw new Error(`chat(stream) error ${r.status}`);
return (async function* () {
for await (const data of sseEvents(r)) {
if (String(data).trim() === '[DONE]') break;
yield JSON.parse(data);
}
})();
} else {
const r = await client.postJson(url, body);
if (!r.ok) throw new Error(`chat error ${r.status}`);
return await r.json();
}
}

export async function textModels(client = getDefaultClient()) {
const r = await client.get(`${client.textBase}/models`);
if (!r.ok) throw new Error(`textModels error ${r.status}`);
return await r.json();
}

export async function search(query, model = 'searchgpt', client = getDefaultClient()) {
return await text(query, { model }, client);
}

Loading
Loading