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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webbrain",
"version": "39.1.1",
"version": "39.1.3",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"private": true,
"type": "module",
Expand Down Expand Up @@ -70,7 +70,8 @@
"test:compass-quota": "node --test test/compass-quota.mjs",
"test:compass-quota:ui": "node test/compass-quota-ui.mjs",
"test:generative-media": "node --test test/generative-media.mjs",
"test:generative-media:ui": "node test/generative-media-ui.mjs"
"test:generative-media:ui": "node test/generative-media-ui.mjs",
"test:generative-media:extension": "node test/generative-media-extension.mjs"
},
"devDependencies": {
"playwright": "^1.48.0",
Expand Down
2 changes: 1 addition & 1 deletion src/chrome/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WebBrain Chrome/Edge Extension — Architecture

> Version 39.1.1 · Manifest V3 · Service Worker background
> Version 39.1.3 · Manifest V3 · Service Worker background

## High-Level Overview

Expand Down
2 changes: 1 addition & 1 deletion src/chrome/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "WebBrain",
"version": "39.1.1",
"version": "39.1.3",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"permissions": [
"sidePanel",
Expand Down
12 changes: 2 additions & 10 deletions src/chrome/src/agent/fal-media.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,5 @@ export async function runFalGeneration({
}
}

// Legacy entry points retained for callers importing fal-media.js.
export async function generateImage(args, options = {}) {
const media = await import('./generative-media.js');
return media.generateImage(args, options);
}

export async function testImageGenProvider(fetchImpl = fetch) {
const media = await import('./generative-media.js');
return media.testImageGenProvider(fetchImpl);
}
// Legacy entry points must use static exports: MV3 service workers forbid import().
export { generateImage, testImageGenProvider } from './generative-media.js';
47 changes: 37 additions & 10 deletions src/chrome/src/agent/generative-media.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,21 @@ async function mediaResponse(response, label) {
return url;
}

export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, signal, timeoutMs = GENERATION_TIMEOUT_MS, pollIntervalMs = 2000 }) {
async function openRouterMediaType(model, headers, request) {
for (const kind of ['images', 'videos']) {
const catalog = await jsonResponse(await request(`https://openrouter.ai/api/v1/${kind}/models`, { headers }), `OpenRouter ${kind} models`);
if (catalog.data?.some(item => item.id === model)) return kind;
}
throw new Error('The selected OpenRouter model is not in the image or video generation catalog.');
}

export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, signal, timeoutMs = GENERATION_TIMEOUT_MS, pollIntervalMs }) {
const provider = validateMediaConfig(config);
const text = String(prompt || '').trim();
if (!text) throw new Error('prompt is required.');
if (provider === 'fal') return runFalGeneration({ prompt: text, config, fetchImpl, signal, timeoutMs });
const operation = operationSignal(signal, timeoutMs);
const pollMs = pollIntervalMs ?? (provider === 'openrouter' ? 30000 : 2000);
const model = provider === 'comfyui' ? 'ComfyUI workflow' : normalizeMediaModel(config.model);
const request = (url, init = {}) => {
operation.signal.throwIfAborted();
Expand All @@ -115,11 +124,30 @@ export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, si
let completed = false;
try {
if (provider === 'openrouter') {
const res = await request('https://openrouter.ai/api/v1/images', {
method: 'POST', headers: { Authorization: `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' },
const headers = { Authorization: `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' };
const kind = await openRouterMediaType(model, headers, request);
const base = `https://openrouter.ai/api/v1/${kind}`;
const res = await request(base, {
method: 'POST', headers,
body: JSON.stringify({ model, prompt: text }),
});
return { url: await mediaResponse(res, 'OpenRouter generation'), model };
if (kind === 'images') return { url: await mediaResponse(res, 'OpenRouter generation'), model };
const queued = await jsonResponse(res, 'OpenRouter video submit');
if (!/^[A-Za-z0-9_-]+$/.test(queued.id || '')) throw new Error('OpenRouter returned an invalid video job ID.');
// Construct authenticated paths ourselves; returned URLs cannot select a key recipient.
const jobUrl = `${base}/${queued.id}`;
let waitMs = pollDelay(res, pollMs);
while (true) {
await delay(waitMs, operation.signal);
const statusRes = await request(jobUrl, { headers });
const status = await jsonResponse(statusRes, 'OpenRouter video status');
waitMs = pollDelay(statusRes, pollMs);
Comment on lines +142 to +144
if (status.status === 'completed') {
const result = await request(`${jobUrl}/content?index=0`, { headers });
return { url: await mediaResponse(result, 'OpenRouter video download'), model };
}
if (!['pending', 'in_progress'].includes(status.status)) throw new Error(`OpenRouter video generation ${status.status || 'returned an unknown status'}.`);
}
}
if (provider === 'comfyrouter') {
const base = `https://api.comfy.org/v2/models/${model}/requests`;
Expand All @@ -134,15 +162,15 @@ export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, si
// Build authenticated URLs ourselves; never send the key to returned URLs.
const resultUrl = `${base}/${queued.request_id}`;
cancelUrl = `${resultUrl}/cancel`; cancelInit = { method: 'PUT', headers };
let waitMs = pollIntervalMs;
let waitMs = pollMs;
while (true) {
await delay(waitMs, operation.signal);
const res = await request(`${resultUrl}/status`, { headers });
const status = await jsonResponse(res, 'Comfy Router status');
waitMs = pollDelay(res, pollIntervalMs);
waitMs = pollDelay(res, pollMs);
if (status.status === 'COMPLETED') {
const result = await request(resultUrl, { headers });
if (result.status === 202) { waitMs = pollDelay(result, pollIntervalMs); continue; }
if (result.status === 202) { waitMs = pollDelay(result, pollMs); continue; }
const url = await mediaResponse(result, 'Comfy Router generation');
completed = true;
return { url, model };
Expand All @@ -160,7 +188,7 @@ export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, si
// Delete this queued prompt only. /interrupt could stop someone else's running workflow.
cancelUrl = `${base}/queue`; cancelInit = { method: 'POST', headers, body: JSON.stringify({ delete: [queued.prompt_id] }) };
while (true) {
await delay(pollIntervalMs, operation.signal);
await delay(pollMs, operation.signal);
const history = await jsonResponse(await request(`${base}/history/${queued.prompt_id}`), 'ComfyUI history');
const item = history[queued.prompt_id];
if (!item) continue;
Expand Down Expand Up @@ -217,8 +245,7 @@ export async function testImageGenProvider(fetchImpl = fetch) {
if (!res.ok) throw new Error(`${MEDIA_PROVIDERS[provider].label} connection failed (HTTP ${res.status}).`);
if (provider === 'openrouter') {
await jsonResponse(res, 'OpenRouter key');
const catalog = await jsonResponse(await fetchImpl('https://openrouter.ai/api/v1/images/models', { headers, signal: operation.signal, redirect: 'error', credentials: 'omit' }), 'OpenRouter image models');
if (!catalog.data?.some(item => item.id === model)) throw new Error('The selected OpenRouter model is not in the image generation catalog.');
await openRouterMediaType(model, headers, (url, init) => fetchImpl(url, { ...init, signal: operation.signal, redirect: 'error', credentials: 'omit' }));
} else if (provider === 'comfyui') {
const stats = await jsonResponse(res, 'ComfyUI');
if (!stats.system || !Array.isArray(stats.devices)) throw new Error('The endpoint did not return ComfyUI system information.');
Expand Down
4 changes: 2 additions & 2 deletions src/chrome/src/ui/locales/generative-media-copy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const english = {
'st.imagegen.parameters': 'Model input JSON (optional)',
'st.imagegen.setup': 'Setup guide ↗',
'st.imagegen.hint.fal': 'Enter your fal.ai key and model ID. Test Connection checks authentication without generating media.',
'st.imagegen.hint.openrouter': 'Enter an OpenRouter key and an image generation model ID. Test Connection checks the key and image catalog without generating an image.',
'st.imagegen.hint.openrouter': 'Enter an OpenRouter key and an image or video generation model ID. Test Connection checks the key and model catalog without generating media.',
'st.imagegen.hint.comfyrouter': 'Enter a Comfy API key and provider/model ID. For models with other prompt fields, paste their input JSON using {{prompt}} where the text belongs. Test Connection reads model metadata.',
'st.imagegen.hint.comfyui': 'Start ComfyUI locally, export the workflow in API format, and replace its positive prompt text with {{prompt}}. Include a Save Image or media output node. No API key is needed. Test Connection checks the local server; the workflow runs only when you request generation.',
'st.imagegen.output': 'Generated media',
Expand All @@ -19,7 +19,7 @@ const turkish = {
'st.imagegen.parameters': 'Model girdisi JSON (isteğe bağlı)',
'st.imagegen.setup': 'Kurulum rehberi ↗',
'st.imagegen.hint.fal': 'fal.ai anahtarınızı ve model kimliğini girin. Bağlantı testi medya üretmeden kimlik doğrulamayı kontrol eder.',
'st.imagegen.hint.openrouter': 'OpenRouter anahtarını ve görsel üreten bir model kimliğini girin. Bağlantı testi görsel üretmeden anahtarı ve görsel kataloğunu kontrol eder.',
'st.imagegen.hint.openrouter': 'OpenRouter anahtarını ve görsel veya video üreten bir model kimliğini girin. Bağlantı testi medya üretmeden anahtarı ve model kataloğunu kontrol eder.',
'st.imagegen.hint.comfyrouter': 'Comfy API anahtarını ve sağlayıcı/model kimliğini girin. Farklı istem alanları kullanan modeller için girdi JSON dosyasını yapıştırıp metnin yerine {{prompt}} yazın. Bağlantı testi model bilgilerini okur.',
'st.imagegen.hint.comfyui': 'ComfyUI yerel sunucusunu başlatın, iş akışını API biçiminde dışa aktarın ve olumlu istem metnini {{prompt}} ile değiştirin. Save Image veya medya çıktı düğümü ekleyin. API anahtarı gerekmez. Bağlantı testi yerel sunucuyu kontrol eder; iş akışı yalnızca medya üretimi istediğinizde çalışır.',
'st.imagegen.output': 'Üretilen medya',
Expand Down
2 changes: 1 addition & 1 deletion src/chrome/src/ui/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const SUBSCRIPTION_GUIDE_PRODUCTS = Object.freeze({

// Version shown in the subtitle. Kept here so it only needs one update per
// release; the subtitle string itself is translated.
const EXT_VERSION = '39.1.1';
const EXT_VERSION = '39.1.3';

const providersContainer = document.getElementById('providers');
const displaySettings = document.getElementById('display-settings');
Expand Down
2 changes: 1 addition & 1 deletion src/firefox/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WebBrain Firefox Extension — Architecture

> Version 39.1.1 · Manifest V2 · Background Page
> Version 39.1.3 · Manifest V2 · Background Page

## How Firefox Differs from Chrome

Expand Down
2 changes: 1 addition & 1 deletion src/firefox/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "WebBrain",
"version": "39.1.1",
"version": "39.1.3",
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
"permissions": [
"activeTab",
Expand Down
12 changes: 2 additions & 10 deletions src/firefox/src/agent/fal-media.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,5 @@ export async function runFalGeneration({
}
}

// Legacy entry points retained for callers importing fal-media.js.
export async function generateImage(args, options = {}) {
const media = await import('./generative-media.js');
return media.generateImage(args, options);
}

export async function testImageGenProvider(fetchImpl = fetch) {
const media = await import('./generative-media.js');
return media.testImageGenProvider(fetchImpl);
}
// Legacy entry points must use static exports: MV3 service workers forbid import().
export { generateImage, testImageGenProvider } from './generative-media.js';
47 changes: 37 additions & 10 deletions src/firefox/src/agent/generative-media.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,21 @@ async function mediaResponse(response, label) {
return url;
}

export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, signal, timeoutMs = GENERATION_TIMEOUT_MS, pollIntervalMs = 2000 }) {
async function openRouterMediaType(model, headers, request) {
for (const kind of ['images', 'videos']) {
const catalog = await jsonResponse(await request(`https://openrouter.ai/api/v1/${kind}/models`, { headers }), `OpenRouter ${kind} models`);
if (catalog.data?.some(item => item.id === model)) return kind;
}
throw new Error('The selected OpenRouter model is not in the image or video generation catalog.');
}

export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, signal, timeoutMs = GENERATION_TIMEOUT_MS, pollIntervalMs }) {
const provider = validateMediaConfig(config);
const text = String(prompt || '').trim();
if (!text) throw new Error('prompt is required.');
if (provider === 'fal') return runFalGeneration({ prompt: text, config, fetchImpl, signal, timeoutMs });
const operation = operationSignal(signal, timeoutMs);
const pollMs = pollIntervalMs ?? (provider === 'openrouter' ? 30000 : 2000);
const model = provider === 'comfyui' ? 'ComfyUI workflow' : normalizeMediaModel(config.model);
const request = (url, init = {}) => {
operation.signal.throwIfAborted();
Expand All @@ -115,11 +124,30 @@ export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, si
let completed = false;
try {
if (provider === 'openrouter') {
const res = await request('https://openrouter.ai/api/v1/images', {
method: 'POST', headers: { Authorization: `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' },
const headers = { Authorization: `Bearer ${config.apiKey}`, 'Content-Type': 'application/json' };
const kind = await openRouterMediaType(model, headers, request);
const base = `https://openrouter.ai/api/v1/${kind}`;
const res = await request(base, {
method: 'POST', headers,
body: JSON.stringify({ model, prompt: text }),
});
return { url: await mediaResponse(res, 'OpenRouter generation'), model };
if (kind === 'images') return { url: await mediaResponse(res, 'OpenRouter generation'), model };
const queued = await jsonResponse(res, 'OpenRouter video submit');
if (!/^[A-Za-z0-9_-]+$/.test(queued.id || '')) throw new Error('OpenRouter returned an invalid video job ID.');
// Construct authenticated paths ourselves; returned URLs cannot select a key recipient.
const jobUrl = `${base}/${queued.id}`;
let waitMs = pollDelay(res, pollMs);
while (true) {
await delay(waitMs, operation.signal);
const statusRes = await request(jobUrl, { headers });
const status = await jsonResponse(statusRes, 'OpenRouter video status');
waitMs = pollDelay(statusRes, pollMs);
Comment on lines +142 to +144
if (status.status === 'completed') {
const result = await request(`${jobUrl}/content?index=0`, { headers });
return { url: await mediaResponse(result, 'OpenRouter video download'), model };
}
if (!['pending', 'in_progress'].includes(status.status)) throw new Error(`OpenRouter video generation ${status.status || 'returned an unknown status'}.`);
}
}
if (provider === 'comfyrouter') {
const base = `https://api.comfy.org/v2/models/${model}/requests`;
Expand All @@ -134,15 +162,15 @@ export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, si
// Build authenticated URLs ourselves; never send the key to returned URLs.
const resultUrl = `${base}/${queued.request_id}`;
cancelUrl = `${resultUrl}/cancel`; cancelInit = { method: 'PUT', headers };
let waitMs = pollIntervalMs;
let waitMs = pollMs;
while (true) {
await delay(waitMs, operation.signal);
const res = await request(`${resultUrl}/status`, { headers });
const status = await jsonResponse(res, 'Comfy Router status');
waitMs = pollDelay(res, pollIntervalMs);
waitMs = pollDelay(res, pollMs);
if (status.status === 'COMPLETED') {
const result = await request(resultUrl, { headers });
if (result.status === 202) { waitMs = pollDelay(result, pollIntervalMs); continue; }
if (result.status === 202) { waitMs = pollDelay(result, pollMs); continue; }
const url = await mediaResponse(result, 'Comfy Router generation');
completed = true;
return { url, model };
Expand All @@ -160,7 +188,7 @@ export async function runMediaGeneration({ prompt, config, fetchImpl = fetch, si
// Delete this queued prompt only. /interrupt could stop someone else's running workflow.
cancelUrl = `${base}/queue`; cancelInit = { method: 'POST', headers, body: JSON.stringify({ delete: [queued.prompt_id] }) };
while (true) {
await delay(pollIntervalMs, operation.signal);
await delay(pollMs, operation.signal);
const history = await jsonResponse(await request(`${base}/history/${queued.prompt_id}`), 'ComfyUI history');
const item = history[queued.prompt_id];
if (!item) continue;
Expand Down Expand Up @@ -217,8 +245,7 @@ export async function testImageGenProvider(fetchImpl = fetch) {
if (!res.ok) throw new Error(`${MEDIA_PROVIDERS[provider].label} connection failed (HTTP ${res.status}).`);
if (provider === 'openrouter') {
await jsonResponse(res, 'OpenRouter key');
const catalog = await jsonResponse(await fetchImpl('https://openrouter.ai/api/v1/images/models', { headers, signal: operation.signal, redirect: 'error', credentials: 'omit' }), 'OpenRouter image models');
if (!catalog.data?.some(item => item.id === model)) throw new Error('The selected OpenRouter model is not in the image generation catalog.');
await openRouterMediaType(model, headers, (url, init) => fetchImpl(url, { ...init, signal: operation.signal, redirect: 'error', credentials: 'omit' }));
} else if (provider === 'comfyui') {
const stats = await jsonResponse(res, 'ComfyUI');
if (!stats.system || !Array.isArray(stats.devices)) throw new Error('The endpoint did not return ComfyUI system information.');
Expand Down
Loading
Loading