opencode-pollinations-plugin 6.4.5 → 6.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/locales/de.json +24 -9
- package/dist/locales/en.json +24 -9
- package/dist/locales/es.json +24 -9
- package/dist/locales/fr.json +23 -8
- package/dist/locales/it.json +24 -9
- package/dist/locales/zh.json +24 -9
- package/dist/server/commands.js +17 -10
- package/dist/server/connect-response.js +15 -9
- package/dist/server/quota.js +10 -10
- package/dist/server/toast.d.ts +1 -0
- package/dist/server/toast.js +7 -2
- package/dist/tools/index.js +8 -0
- package/dist/tools/pollinations/gen_edit_image_free.js +4 -3
- package/dist/tools/pollinations/gen_video_free.js +3 -2
- package/dist/tools/pollinations/image_enhancer.d.ts +2 -0
- package/dist/tools/pollinations/image_enhancer.js +62 -0
- package/dist/tools/pollinations/image_upscaler.d.ts +2 -0
- package/dist/tools/pollinations/image_upscaler.js +61 -0
- package/dist/tools/pollinations/imgtools/clients.d.ts +11 -0
- package/dist/tools/pollinations/imgtools/clients.js +182 -0
- package/dist/tools/pollinations/imgtools/config.d.ts +42 -0
- package/dist/tools/pollinations/imgtools/config.js +81 -0
- package/dist/tools/pollinations/imgtools/crypto.d.ts +4 -0
- package/dist/tools/pollinations/imgtools/crypto.js +34 -0
- package/dist/tools/pollinations/imgtools/helpers.d.ts +24 -0
- package/dist/tools/pollinations/imgtools/helpers.js +111 -0
- package/dist/tools/pollinations/object_remover.d.ts +2 -0
- package/dist/tools/pollinations/object_remover.js +62 -0
- package/dist/tools/power/remove_background.js +16 -60
- package/package.json +1 -1
package/dist/server/toast.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare function setGlobalClient(client: any): void;
|
|
|
2
2
|
export declare function emitLogToast(type: 'info' | 'warning' | 'error' | 'success', message: string, title?: string): void;
|
|
3
3
|
export declare function emitStatusToast(type: 'info' | 'warning' | 'error' | 'success', message: string, title?: string, metadata?: {
|
|
4
4
|
filePath?: string;
|
|
5
|
+
freeTool?: boolean;
|
|
5
6
|
params?: Record<string, any>;
|
|
6
7
|
}): void;
|
|
7
8
|
export declare function createToastHooks(client: any): {
|
package/dist/server/toast.js
CHANGED
|
@@ -31,9 +31,14 @@ export function emitStatusToast(type, message, title, metadata) {
|
|
|
31
31
|
finalMessage += `\n📁 ${metadata.filePath}`;
|
|
32
32
|
}
|
|
33
33
|
if (type === 'success' || type === 'error') {
|
|
34
|
-
//
|
|
34
|
+
// Skip quota display for non-polli tools (free tools)
|
|
35
|
+
if (metadata?.freeTool === true) {
|
|
36
|
+
dispatchToast('status', type, finalMessage, title || 'Pollinations Status');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// En arrière-plan, récupère le quota EN LIVE (pas de cache)
|
|
35
40
|
import('./quota.js').then(({ getQuotaStatus, formatQuotaForToast }) => {
|
|
36
|
-
getQuotaStatus(
|
|
41
|
+
getQuotaStatus(true).then(quota => {
|
|
37
42
|
const quotaMsg = formatQuotaForToast
|
|
38
43
|
? formatQuotaForToast(quota)
|
|
39
44
|
: `🌻 Freetier: ${quota.tierRemaining.toFixed(2)}/${quota.tierLimit} | Wallet: $${quota.walletBalance.toFixed(2)}`;
|
package/dist/tools/index.js
CHANGED
|
@@ -20,6 +20,10 @@ import { rmbgKeysTool } from './power/rmbg_keys.js';
|
|
|
20
20
|
import { genEditImageFreeTool } from './pollinations/gen_edit_image_free.js';
|
|
21
21
|
import { genVideoFreeTool } from './pollinations/gen_video_free.js';
|
|
22
22
|
import { polliLoginTool } from './pollinations/polli_login.js';
|
|
23
|
+
// === FREE BONUS: standalone image processing tools (no key, no API, direct calls) ===
|
|
24
|
+
import { objectRemoverTool } from './pollinations/object_remover.js';
|
|
25
|
+
import { imageUpscalerTool } from './pollinations/image_upscaler.js';
|
|
26
|
+
import { imageEnhancerTool } from './pollinations/image_enhancer.js';
|
|
23
27
|
// === ENTER TOOLS (Require API key) ===
|
|
24
28
|
import { polliGenImageTool } from './pollinations/gen_image.js';
|
|
25
29
|
import { polliGenVideoTool } from './pollinations/gen_video.js';
|
|
@@ -65,6 +69,10 @@ export function createToolRegistry() {
|
|
|
65
69
|
tools['gen_video_free'] = genVideoFreeTool;
|
|
66
70
|
// Login tool: device-flow login, callable by any model (no key needed to run)
|
|
67
71
|
tools['polli_login'] = polliLoginTool;
|
|
72
|
+
// Standalone image processing tools (no key, no API, direct calls from user IP)
|
|
73
|
+
tools['object_remover'] = objectRemoverTool;
|
|
74
|
+
tools['image_upscaler'] = imageUpscalerTool;
|
|
75
|
+
tools['image_enhancer'] = imageEnhancerTool;
|
|
68
76
|
log(`Free tools injected: ${Object.keys(tools).length}`);
|
|
69
77
|
// === ENTER UNIVERSE: Only with valid API key (+6 tools) ===
|
|
70
78
|
if (keyPresent) {
|
|
@@ -99,7 +99,7 @@ export const genEditImageFreeTool = tool({
|
|
|
99
99
|
}
|
|
100
100
|
if (!apiResp || !apiResp.success || !apiResp.imageUrl) {
|
|
101
101
|
const reason = apiResp?.error || 'unknown';
|
|
102
|
-
emitStatusToast('error', t('tools.gen_edit_image_free.failed', { error: String(reason).substring(0, 60) }), '🆓 gen_edit_image_free');
|
|
102
|
+
emitStatusToast('error', t('tools.gen_edit_image_free.failed', { error: String(reason).substring(0, 60) }), '🆓 gen_edit_image_free', { freeTool: true });
|
|
103
103
|
return t('tools.gen_edit_image_free.api_error', { error: String(reason) });
|
|
104
104
|
}
|
|
105
105
|
// 3. Download the produced image and save it locally
|
|
@@ -133,12 +133,13 @@ export const genEditImageFreeTool = tool({
|
|
|
133
133
|
}
|
|
134
134
|
lines.push('');
|
|
135
135
|
lines.push(t('tools.gen_edit_image_free.res_note'));
|
|
136
|
-
|
|
136
|
+
const quotaMsg = after ? ` | ${after.remaining}/${after.max}/j` : '';
|
|
137
|
+
emitStatusToast('success', t('tools.gen_edit_image_free.success', { mode }) + quotaMsg, '🆓 gen_edit_image_free', { filePath, freeTool: true });
|
|
137
138
|
return lines.join('\n');
|
|
138
139
|
}
|
|
139
140
|
catch (err) {
|
|
140
141
|
const msg = err.message || String(err);
|
|
141
|
-
emitStatusToast('error', t('tools.gen_edit_image_free.failed', { error: msg.substring(0, 60) }), '🆓 gen_edit_image_free');
|
|
142
|
+
emitStatusToast('error', t('tools.gen_edit_image_free.failed', { error: msg.substring(0, 60) }), '🆓 gen_edit_image_free', { freeTool: true });
|
|
142
143
|
// Graceful degradation: explicitly point to Pollinations models
|
|
143
144
|
return t('tools.gen_edit_image_free.degraded', { error: msg.substring(0, 150) });
|
|
144
145
|
}
|
|
@@ -234,12 +234,13 @@ export const genVideoFreeTool = tool({
|
|
|
234
234
|
lines.push(t('tools.gen_video_free.res_quota', { remaining: after.remaining, max: after.max }));
|
|
235
235
|
lines.push('');
|
|
236
236
|
lines.push(t('tools.gen_video_free.res_note'));
|
|
237
|
-
|
|
237
|
+
const quotaMsg = after ? ` | ${after.remaining}/${after.max}/j` : '';
|
|
238
|
+
emitStatusToast('success', t('tools.gen_video_free.success') + quotaMsg, '🆓 gen_video_free', { filePath, freeTool: true });
|
|
238
239
|
return lines.join('\n');
|
|
239
240
|
}
|
|
240
241
|
catch (err) {
|
|
241
242
|
const msg = err.message || String(err);
|
|
242
|
-
emitStatusToast('error', t('tools.gen_video_free.failed', { error: msg.substring(0, 60) }), '🆓 gen_video_free');
|
|
243
|
+
emitStatusToast('error', t('tools.gen_video_free.failed', { error: msg.substring(0, 60) }), '🆓 gen_video_free', { freeTool: true });
|
|
243
244
|
return t('tools.gen_video_free.degraded', { error: msg.substring(0, 150) });
|
|
244
245
|
}
|
|
245
246
|
},
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// image_enhancer — Amélioration IA (débruitage, netteté) via imgupscaler.com
|
|
2
|
+
// Standalone : appel direct depuis l'IP utilisateur, pas d'API, pas de clé
|
|
3
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { ensureDir, generateFilename, getDefaultOutputDir, formatFileSize, sanitizeFilename, httpsGet, } from './shared.js';
|
|
7
|
+
import { emitStatusToast } from '../../server/toast.js';
|
|
8
|
+
import { t } from '../../locales/index.js';
|
|
9
|
+
import { processTool } from './imgtools/clients.js';
|
|
10
|
+
export const imageEnhancerTool = tool({
|
|
11
|
+
description: `Améliore la qualité d'une image : réduction du bruit, netteté, restauration des détails (gratuit, appel direct).
|
|
12
|
+
Résolutions : 1K (1024px), 2K (2048px), 4K (4096px). Défaut : 2K.
|
|
13
|
+
Temps de traitement : 30-120s. Pas de clé requise.`,
|
|
14
|
+
args: {
|
|
15
|
+
file: tool.schema.string().describe('Chemin local de l\'image à améliorer'),
|
|
16
|
+
target_longest_side: tool.schema.enum(['1024', '2048', '4096']).optional().describe('Résolution cible (1K=1024, 2K=2048, 4K=4096). Défaut: 2048'),
|
|
17
|
+
save_to: tool.schema.string().optional().describe('Dossier de sortie'),
|
|
18
|
+
filename: tool.schema.string().optional().describe('Nom du fichier de sortie (sans extension)'),
|
|
19
|
+
},
|
|
20
|
+
async execute(args, context) {
|
|
21
|
+
const imagePath = args.file;
|
|
22
|
+
if (!fs.existsSync(imagePath)) {
|
|
23
|
+
return t('tools.image_enhancer.file_not_found', { path: imagePath }) || `❌ Fichier introuvable : ${imagePath}`;
|
|
24
|
+
}
|
|
25
|
+
const ext = path.extname(imagePath).toLowerCase();
|
|
26
|
+
const mimeType = ext === '.png' ? 'image/png' : 'image/jpeg';
|
|
27
|
+
const imageData = fs.readFileSync(imagePath);
|
|
28
|
+
const targetLongestSide = parseInt(args.target_longest_side || '2048');
|
|
29
|
+
const resLabel = targetLongestSide >= 4096 ? '4K' : targetLongestSide >= 2048 ? '2K' : '1K';
|
|
30
|
+
context.metadata({ title: '✨ image_enhancer', metadata: { type: 'info', message: `Amélioration ${resLabel}...` } });
|
|
31
|
+
try {
|
|
32
|
+
const result = await processTool('enhance', {
|
|
33
|
+
data: imageData,
|
|
34
|
+
contentType: mimeType,
|
|
35
|
+
filename: path.basename(imagePath),
|
|
36
|
+
options: { targetLongestSide },
|
|
37
|
+
});
|
|
38
|
+
if (!result.imageUrl) {
|
|
39
|
+
return t('tools.image_enhancer.no_result') || '❌ Aucun résultat reçu.';
|
|
40
|
+
}
|
|
41
|
+
const dl = await httpsGet(result.imageUrl);
|
|
42
|
+
const outputDir = args.save_to ? args.save_to : getDefaultOutputDir('image_enhancer');
|
|
43
|
+
const outputFilename = (args.filename ? sanitizeFilename(args.filename) : generateFilename('enhance', 'imgupscaler', 'png'));
|
|
44
|
+
const filePath = path.join(outputDir, outputFilename.includes('.') ? outputFilename : `${outputFilename}.png`);
|
|
45
|
+
ensureDir(outputDir);
|
|
46
|
+
fs.writeFileSync(filePath, dl.data);
|
|
47
|
+
emitStatusToast("success", "✨ Enhancement terminé", "image_enhancer", { filePath, freeTool: true });
|
|
48
|
+
const fileSize = fs.statSync(filePath).size;
|
|
49
|
+
const lines = [];
|
|
50
|
+
lines.push('✨ **Image Améliorée**');
|
|
51
|
+
lines.push('━━━━━━━━━━━━━━━━━━');
|
|
52
|
+
lines.push(`Fichier : \`${filePath}\``);
|
|
53
|
+
lines.push(`Taille : ${formatFileSize(fileSize)}`);
|
|
54
|
+
lines.push(`Résolution : ${resLabel}`);
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
emitStatusToast("warning", "❌ " + (err.message?.substring(0, 80) || ""), "image_enhancer", { freeTool: true });
|
|
59
|
+
return "❌ Erreur : " + err.message;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// image_upscaler — Agrandissement 2x/4x via imgupscaler.com
|
|
2
|
+
// Standalone : appel direct depuis l'IP utilisateur, pas d'API, pas de clé
|
|
3
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { ensureDir, generateFilename, getDefaultOutputDir, formatFileSize, sanitizeFilename, httpsGet, } from './shared.js';
|
|
7
|
+
import { emitStatusToast } from '../../server/toast.js';
|
|
8
|
+
import { t } from '../../locales/index.js';
|
|
9
|
+
import { processTool } from './imgtools/clients.js';
|
|
10
|
+
export const imageUpscalerTool = tool({
|
|
11
|
+
description: `Agrandit une image 2x ou 4x (gratuit, appel direct).
|
|
12
|
+
Temps de traitement : 30-120s. Pas de clé requise.`,
|
|
13
|
+
args: {
|
|
14
|
+
file: tool.schema.string().describe('Chemin local de l\'image à agrandir'),
|
|
15
|
+
ratio: tool.schema.enum(['2', '4']).optional().describe('Facteur d\'agrandissement (2=2x, 4=4x). Défaut: 2'),
|
|
16
|
+
save_to: tool.schema.string().optional().describe('Dossier de sortie'),
|
|
17
|
+
filename: tool.schema.string().optional().describe('Nom du fichier de sortie (sans extension)'),
|
|
18
|
+
},
|
|
19
|
+
async execute(args, context) {
|
|
20
|
+
const imagePath = args.file;
|
|
21
|
+
if (!fs.existsSync(imagePath)) {
|
|
22
|
+
return t('tools.image_upscaler.file_not_found', { path: imagePath }) || `❌ Fichier introuvable : ${imagePath}`;
|
|
23
|
+
}
|
|
24
|
+
const ext = path.extname(imagePath).toLowerCase();
|
|
25
|
+
const mimeType = ext === '.png' ? 'image/png' : 'image/jpeg';
|
|
26
|
+
const imageData = fs.readFileSync(imagePath);
|
|
27
|
+
const ratio = args.ratio === '4' ? '400' : '200';
|
|
28
|
+
const ratioLabel = args.ratio === '4' ? '4x' : '2x';
|
|
29
|
+
context.metadata({ title: '📐 image_upscaler', metadata: { type: 'info', message: `Agrandissement ${ratioLabel}...` } });
|
|
30
|
+
try {
|
|
31
|
+
const result = await processTool('upscale', {
|
|
32
|
+
data: imageData,
|
|
33
|
+
contentType: mimeType,
|
|
34
|
+
filename: path.basename(imagePath),
|
|
35
|
+
options: { ratio },
|
|
36
|
+
});
|
|
37
|
+
if (!result.imageUrl) {
|
|
38
|
+
return t('tools.image_upscaler.no_result') || '❌ Aucun résultat reçu.';
|
|
39
|
+
}
|
|
40
|
+
const dl = await httpsGet(result.imageUrl);
|
|
41
|
+
const outputDir = args.save_to ? args.save_to : getDefaultOutputDir('image_upscaler');
|
|
42
|
+
const outputFilename = (args.filename ? sanitizeFilename(args.filename) : generateFilename('upscale', 'imgupscaler', 'png'));
|
|
43
|
+
const filePath = path.join(outputDir, outputFilename.includes('.') ? outputFilename : `${outputFilename}.png`);
|
|
44
|
+
ensureDir(outputDir);
|
|
45
|
+
fs.writeFileSync(filePath, dl.data);
|
|
46
|
+
emitStatusToast("success", "📐 Upscale terminé", "image_upscaler", { filePath, freeTool: true });
|
|
47
|
+
const fileSize = fs.statSync(filePath).size;
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push('📐 **Image Agrandie**');
|
|
50
|
+
lines.push('━━━━━━━━━━━━━━━━━━');
|
|
51
|
+
lines.push(`Fichier : \`${filePath}\``);
|
|
52
|
+
lines.push(`Taille : ${formatFileSize(fileSize)}`);
|
|
53
|
+
lines.push(`Ratio : ${ratioLabel}`);
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
emitStatusToast("warning", "❌ " + (err.message?.substring(0, 80) || ""), "image_upscaler", { freeTool: true });
|
|
58
|
+
return "❌ Erreur : " + err.message;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ToolInput {
|
|
2
|
+
data: Buffer;
|
|
3
|
+
contentType: string;
|
|
4
|
+
filename: string;
|
|
5
|
+
options?: Record<string, string | number>;
|
|
6
|
+
}
|
|
7
|
+
export interface ToolResult {
|
|
8
|
+
imageUrl: string;
|
|
9
|
+
downloadUrls: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function processTool(toolName: string, input: ToolInput): Promise<ToolResult>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// imgtools/clients.ts — orchestration upload → poll → download pour les 4 outils
|
|
2
|
+
// Adapté depuis /home/fkomp/Bureau/oracle/dev-serveur/iamges-tools-api/modules/clients.js
|
|
3
|
+
// Mode standalone : appels directs depuis l'IP utilisateur, pas de queue, pas d'API
|
|
4
|
+
import { TOOLS, UA } from './config.js';
|
|
5
|
+
import { getKey, encrypt, decrypt } from './crypto.js';
|
|
6
|
+
import { buildMultipart, httpsPost, getDims, aspectRatio } from './helpers.js';
|
|
7
|
+
const BASE_HEADERS = {
|
|
8
|
+
'User-Agent': UA,
|
|
9
|
+
'Accept': '*/*',
|
|
10
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
11
|
+
};
|
|
12
|
+
function headersFor(t, contentType) {
|
|
13
|
+
return {
|
|
14
|
+
...BASE_HEADERS,
|
|
15
|
+
'Origin': `https://${t.host}`,
|
|
16
|
+
'Referer': t.page,
|
|
17
|
+
...(contentType ? { 'Content-Type': contentType } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function normalizeUrls(dUrls) {
|
|
21
|
+
if (Array.isArray(dUrls))
|
|
22
|
+
return dUrls.filter(u => u && String(u).trim()).map(u => String(u).trim());
|
|
23
|
+
if (dUrls && typeof dUrls === 'object') {
|
|
24
|
+
return Object.values(dUrls).filter(u => u && String(u).trim()).map(u => String(u).trim());
|
|
25
|
+
}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
function hasUrls(dUrls) {
|
|
29
|
+
return normalizeUrls(dUrls).length > 0;
|
|
30
|
+
}
|
|
31
|
+
function resolveUrls(t, urls) {
|
|
32
|
+
const resolved = urls.map(u => {
|
|
33
|
+
if (String(u).startsWith('http'))
|
|
34
|
+
return String(u);
|
|
35
|
+
if (t.resultBase) {
|
|
36
|
+
const fn = String(u).split('/').pop() || '';
|
|
37
|
+
return `${t.resultBase}/${fn}`;
|
|
38
|
+
}
|
|
39
|
+
return String(u);
|
|
40
|
+
});
|
|
41
|
+
return { imageUrl: resolved[0] || '', downloadUrls: resolved };
|
|
42
|
+
}
|
|
43
|
+
// ─── Legacy (rmbg, upscale) ──────────────────────────────────────────
|
|
44
|
+
async function uploadLegacy(t, file) {
|
|
45
|
+
const mpFields = [
|
|
46
|
+
{ name: 'file', value: file.data, filename: file.filename || 'image.jpg', contentType: file.contentType || 'image/jpeg' },
|
|
47
|
+
];
|
|
48
|
+
if (t.uploadFields) {
|
|
49
|
+
for (const [k, v] of Object.entries(t.uploadFields)) {
|
|
50
|
+
let val = v;
|
|
51
|
+
if (k === 'ratio' && file.options?.ratio)
|
|
52
|
+
val = String(file.options.ratio);
|
|
53
|
+
mpFields.push({ name: k, value: val });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const { boundary, body } = buildMultipart(mpFields);
|
|
57
|
+
const hdrs = headersFor(t, `multipart/form-data; boundary=${boundary}`);
|
|
58
|
+
const res = await httpsPost(t.host, t.uploadPath, hdrs, body);
|
|
59
|
+
if (res.status !== 200)
|
|
60
|
+
throw new Error(`Upload ${t.name} failed: ${res.status} ${res.body.toString('utf8').slice(0, 200)}`);
|
|
61
|
+
let json;
|
|
62
|
+
try {
|
|
63
|
+
json = JSON.parse(res.body.toString('utf8'));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error(`Upload ${t.name} non-JSON: ${res.body.toString('utf8').slice(0, 200)}`);
|
|
67
|
+
}
|
|
68
|
+
if (json.downloadUrls && hasUrls(json.downloadUrls)) {
|
|
69
|
+
return { code: null, directUrls: normalizeUrls(json.downloadUrls) };
|
|
70
|
+
}
|
|
71
|
+
const code = json.code || json.taskId || json.taskCode || (json.data && (json.data.code || json.data.taskId));
|
|
72
|
+
if (!code)
|
|
73
|
+
throw new Error(`Upload ${t.name}: no code in ${JSON.stringify(json).slice(0, 300)}`);
|
|
74
|
+
return { code: String(code), directUrls: [] };
|
|
75
|
+
}
|
|
76
|
+
async function pollLegacy(t, code) {
|
|
77
|
+
const codes = Array.isArray(code) ? code : [code];
|
|
78
|
+
const bodyObj = t.statusBodyBuilder ? t.statusBodyBuilder(codes) : { type: t.statusType, [t.statusField]: codes };
|
|
79
|
+
const body = JSON.stringify(bodyObj);
|
|
80
|
+
const hdrs = headersFor(t, 'application/json');
|
|
81
|
+
const res = await httpsPost(t.host, t.statusPath, hdrs, body);
|
|
82
|
+
if (res.status !== 200)
|
|
83
|
+
throw new Error(`Status ${t.name} failed: ${res.status}`);
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(res.body.toString('utf8'));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error(`Status ${t.name} non-JSON`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// ─── Encrypted (ruo, enhance) ────────────────────────────────────────
|
|
92
|
+
function buildUploadParams(t, file, dims, extra) {
|
|
93
|
+
const ratio = aspectRatio(dims.width, dims.height);
|
|
94
|
+
if (t.name === 'ruo') {
|
|
95
|
+
const p = String(extra.prompt || 'remove unwanted objects');
|
|
96
|
+
return {
|
|
97
|
+
type: t.statusType, selected_model: t.selectedModel, model_name: t.modelName,
|
|
98
|
+
user_id: 'anonymous', tool: t.tool,
|
|
99
|
+
positive_prompts: p, resolved_prompt: p, raw_prompt: p,
|
|
100
|
+
aspect_ratio: ratio, prompt_image_refs: ['primary'], image_order_map: ['primary'],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (t.name === 'enhance') {
|
|
104
|
+
const prompt = t.prompt;
|
|
105
|
+
const tls = Number(extra.targetLongestSide || 1024);
|
|
106
|
+
return {
|
|
107
|
+
positive_prompts: prompt, resolved_prompt: prompt, aspect_ratio: ratio,
|
|
108
|
+
selected_model: t.selectedModel, model_name: t.modelName,
|
|
109
|
+
user_id: 'anonymous', type: t.statusType, target_longest_side: tls,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Unknown encrypted tool: ${t.name}`);
|
|
113
|
+
}
|
|
114
|
+
async function uploadEncrypted(t, file) {
|
|
115
|
+
const key = getKey(t.crypto.salt);
|
|
116
|
+
const dims = getDims(file.data) || { width: 0, height: 0 };
|
|
117
|
+
const params = buildUploadParams(t, file, dims, file.options || {});
|
|
118
|
+
const enc = encrypt(params, key);
|
|
119
|
+
const { boundary, body } = buildMultipart([
|
|
120
|
+
{ name: 'params', value: enc },
|
|
121
|
+
{ name: 'file', value: file.data, filename: file.filename || 'image.jpg', contentType: file.contentType || 'image/jpeg' },
|
|
122
|
+
{ name: 'selected_model', value: t.selectedModel },
|
|
123
|
+
]);
|
|
124
|
+
const hdrs = headersFor(t, `multipart/form-data; boundary=${boundary}`);
|
|
125
|
+
const res = await httpsPost(t.host, t.uploadPath, hdrs, body);
|
|
126
|
+
if (res.status !== 200)
|
|
127
|
+
throw new Error(`Upload ${t.name} failed: ${res.status}`);
|
|
128
|
+
const json = JSON.parse(res.body.toString('utf8'));
|
|
129
|
+
if (!json.data_enc)
|
|
130
|
+
throw new Error(`Upload ${t.name}: no data_enc in ${JSON.stringify(json).slice(0, 200)}`);
|
|
131
|
+
const dec = decrypt(json.data_enc, key);
|
|
132
|
+
if (!dec.code)
|
|
133
|
+
throw new Error(`Upload ${t.name}: no code in decrypted ${JSON.stringify(dec).slice(0, 200)}`);
|
|
134
|
+
return { code: String(dec.code) };
|
|
135
|
+
}
|
|
136
|
+
async function pollEncrypted(t, code) {
|
|
137
|
+
const key = getKey(t.crypto.salt);
|
|
138
|
+
const statusPayload = { type: t.statusType, code, user_id: 'anonymous' };
|
|
139
|
+
const enc = encrypt(statusPayload, key);
|
|
140
|
+
const body = JSON.stringify({ params: enc });
|
|
141
|
+
const hdrs = headersFor(t, 'application/json');
|
|
142
|
+
const res = await httpsPost(t.host, t.statusPath, hdrs, body);
|
|
143
|
+
if (res.status !== 200)
|
|
144
|
+
throw new Error(`Status ${t.name} failed: ${res.status}`);
|
|
145
|
+
const json = JSON.parse(res.body.toString('utf8'));
|
|
146
|
+
if (!json.data_enc)
|
|
147
|
+
throw new Error(`Status ${t.name}: no data_enc`);
|
|
148
|
+
return decrypt(json.data_enc, key);
|
|
149
|
+
}
|
|
150
|
+
// ─── Orchestrateur principal ─────────────────────────────────────────
|
|
151
|
+
export async function processTool(toolName, input) {
|
|
152
|
+
const t = TOOLS[toolName];
|
|
153
|
+
if (!t)
|
|
154
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
155
|
+
const isEncrypted = !!t.crypto;
|
|
156
|
+
const upRes = isEncrypted
|
|
157
|
+
? await uploadEncrypted(t, input)
|
|
158
|
+
: await uploadLegacy(t, input);
|
|
159
|
+
if ('directUrls' in upRes && upRes.directUrls && upRes.directUrls.length > 0) {
|
|
160
|
+
return resolveUrls(t, upRes.directUrls);
|
|
161
|
+
}
|
|
162
|
+
const code = 'code' in upRes ? upRes.code : upRes.code;
|
|
163
|
+
let status;
|
|
164
|
+
for (let i = 0; i < t.maxPolls; i++) {
|
|
165
|
+
if (i > 0)
|
|
166
|
+
await new Promise(r => setTimeout(r, t.pollInterval));
|
|
167
|
+
status = isEncrypted
|
|
168
|
+
? await pollEncrypted(t, code)
|
|
169
|
+
: await pollLegacy(t, code);
|
|
170
|
+
if (status.status === 'success' || hasUrls(status.downloadUrls))
|
|
171
|
+
break;
|
|
172
|
+
if (status.status === 'failed')
|
|
173
|
+
throw new Error(`Status ${t.name} failed: ${status.message || JSON.stringify(status).slice(0, 200)}`);
|
|
174
|
+
if (status.status && !['processing', 'pending', 'queued', 'waiting'].includes(status.status)) {
|
|
175
|
+
throw new Error(`Status ${t.name} unexpected: ${JSON.stringify(status).slice(0, 300)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const urls = status ? normalizeUrls(status.downloadUrls) : [];
|
|
179
|
+
if (!urls.length)
|
|
180
|
+
throw new Error(`Polling ${t.name} timeout: no downloadUrls after ${t.maxPolls} attempts`);
|
|
181
|
+
return resolveUrls(t, urls);
|
|
182
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36";
|
|
2
|
+
export declare const CRYPTO: {
|
|
3
|
+
PBKDF2: {
|
|
4
|
+
iterations: number;
|
|
5
|
+
hash: string;
|
|
6
|
+
keylen: number;
|
|
7
|
+
};
|
|
8
|
+
AES_GCM: {
|
|
9
|
+
ivLen: number;
|
|
10
|
+
tagLen: number;
|
|
11
|
+
};
|
|
12
|
+
password: string;
|
|
13
|
+
};
|
|
14
|
+
export interface ToolConfig {
|
|
15
|
+
name: string;
|
|
16
|
+
label: string;
|
|
17
|
+
host: string;
|
|
18
|
+
uploadPath: string;
|
|
19
|
+
statusPath: string;
|
|
20
|
+
crypto: typeof CRYPTO & {
|
|
21
|
+
salt: string;
|
|
22
|
+
} | null;
|
|
23
|
+
statusType: number;
|
|
24
|
+
statusField: string;
|
|
25
|
+
maxPolls: number;
|
|
26
|
+
pollInterval: number;
|
|
27
|
+
downloadUrlsFormat?: string;
|
|
28
|
+
page: string;
|
|
29
|
+
uploadFields?: Record<string, string>;
|
|
30
|
+
statusBodyBuilder?: (codes: string[]) => Record<string, unknown>;
|
|
31
|
+
selectedModel?: string;
|
|
32
|
+
modelName?: string;
|
|
33
|
+
tool?: string;
|
|
34
|
+
resultBase?: string;
|
|
35
|
+
ratios?: string[];
|
|
36
|
+
targetLongestSides?: Array<{
|
|
37
|
+
label: string;
|
|
38
|
+
value: number;
|
|
39
|
+
}>;
|
|
40
|
+
prompt?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare const TOOLS: Record<string, ToolConfig>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// imgtools/config.ts — endpoints, crypto params, headers des 4 outils
|
|
2
|
+
// Adapté depuis /home/fkomp/Bureau/oracle/dev-serveur/iamges-tools-api/modules/config.js
|
|
3
|
+
export const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/150.0.0.0 Safari/537.36';
|
|
4
|
+
export const CRYPTO = {
|
|
5
|
+
PBKDF2: { iterations: 10000, hash: 'sha256', keylen: 32 },
|
|
6
|
+
AES_GCM: { ivLen: 12, tagLen: 16 },
|
|
7
|
+
password: 'vH33r_2025_AES_GCM_S3cur3_K3y_9X7mP4qR8nT2wE5yU1oI6aS3dF7gH0jK9lZ',
|
|
8
|
+
};
|
|
9
|
+
export const TOOLS = {
|
|
10
|
+
rmbg: {
|
|
11
|
+
name: 'rmbg',
|
|
12
|
+
label: 'Background Removal',
|
|
13
|
+
host: 'bgeraser.com',
|
|
14
|
+
uploadPath: '/api/bgeraser/legacy/upload',
|
|
15
|
+
statusPath: '/api/bgeraser/legacy/status',
|
|
16
|
+
crypto: null,
|
|
17
|
+
statusType: 4,
|
|
18
|
+
statusField: 'codes',
|
|
19
|
+
uploadFields: { type: '4', mattValue: '0' },
|
|
20
|
+
maxPolls: 30,
|
|
21
|
+
pollInterval: 5000,
|
|
22
|
+
downloadUrlsFormat: 'object',
|
|
23
|
+
page: 'https://bgeraser.com',
|
|
24
|
+
},
|
|
25
|
+
ruo: {
|
|
26
|
+
name: 'ruo',
|
|
27
|
+
label: 'Remove Unwanted Objects',
|
|
28
|
+
host: 'objectremover.com',
|
|
29
|
+
uploadPath: '/api/u2/upload',
|
|
30
|
+
statusPath: '/api/u2/status',
|
|
31
|
+
crypto: { ...CRYPTO, salt: 'objectremover-salt-2026' },
|
|
32
|
+
statusType: 7,
|
|
33
|
+
statusField: 'code',
|
|
34
|
+
maxPolls: 40,
|
|
35
|
+
pollInterval: 3000,
|
|
36
|
+
selectedModel: 'flux_klein/edit',
|
|
37
|
+
modelName: 'Flux Klein Edit',
|
|
38
|
+
tool: 'object-remover',
|
|
39
|
+
page: 'https://objectremover.com',
|
|
40
|
+
resultBase: 'https://access.vheer.com/results',
|
|
41
|
+
},
|
|
42
|
+
upscale: {
|
|
43
|
+
name: 'upscale',
|
|
44
|
+
label: 'Image Upscaler',
|
|
45
|
+
host: 'imgupscaler.com',
|
|
46
|
+
uploadPath: '/api/legacy/upload',
|
|
47
|
+
statusPath: '/api/legacy/status',
|
|
48
|
+
crypto: null,
|
|
49
|
+
statusType: 4,
|
|
50
|
+
statusField: 'codes',
|
|
51
|
+
statusBodyBuilder: (codes) => ({ taskId: codes[0] }),
|
|
52
|
+
uploadFields: { type: '4' },
|
|
53
|
+
maxPolls: 30,
|
|
54
|
+
pollInterval: 5000,
|
|
55
|
+
downloadUrlsFormat: 'array',
|
|
56
|
+
page: 'https://imgupscaler.com',
|
|
57
|
+
ratios: ['200', '400'],
|
|
58
|
+
},
|
|
59
|
+
enhance: {
|
|
60
|
+
name: 'enhance',
|
|
61
|
+
label: 'AI Image Enhancer',
|
|
62
|
+
host: 'imgupscaler.com',
|
|
63
|
+
uploadPath: '/api/u2/upload',
|
|
64
|
+
statusPath: '/api/u2/status',
|
|
65
|
+
crypto: { ...CRYPTO, salt: 'imgupscaler-salt-2026' },
|
|
66
|
+
statusType: 7,
|
|
67
|
+
statusField: 'code',
|
|
68
|
+
maxPolls: 40,
|
|
69
|
+
pollInterval: 3000,
|
|
70
|
+
selectedModel: 'hypir',
|
|
71
|
+
modelName: 'Hypir Enhancer',
|
|
72
|
+
page: 'https://imgupscaler.com/enhancer',
|
|
73
|
+
resultBase: 'https://access.vheer.com/results',
|
|
74
|
+
prompt: 'preserve facial identity, preserve facial geometry, restore image clarity, enhance natural details, reduce blur and noise, preserve original colors and structure',
|
|
75
|
+
targetLongestSides: [
|
|
76
|
+
{ label: '1K', value: 1024 },
|
|
77
|
+
{ label: '2K', value: 2048 },
|
|
78
|
+
{ label: '4K', value: 4096 },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function deriveKey(password: string, salt: string): Buffer;
|
|
2
|
+
export declare function encrypt(data: Record<string, unknown>, key: Buffer): string;
|
|
3
|
+
export declare function decrypt(encoded: string, key: Buffer): any;
|
|
4
|
+
export declare function getKey(salt: string): Buffer;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// imgtools/crypto.ts — AES-GCM + PBKDF2 pour ruo/enhance
|
|
2
|
+
// Adapté depuis /home/fkomp/Bureau/oracle/dev-serveur/iamges-tools-api/modules/crypto.js
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
const PBKDF2 = { iterations: 10000, hash: 'sha256', keylen: 32 };
|
|
5
|
+
const AES_GCM = { ivLen: 12, tagLen: 16 };
|
|
6
|
+
export function deriveKey(password, salt) {
|
|
7
|
+
return crypto.pbkdf2Sync(Buffer.from(password, 'utf8'), Buffer.from(salt, 'utf8'), PBKDF2.iterations, PBKDF2.keylen, PBKDF2.hash);
|
|
8
|
+
}
|
|
9
|
+
export function encrypt(data, key) {
|
|
10
|
+
const iv = crypto.randomBytes(AES_GCM.ivLen);
|
|
11
|
+
const plaintext = Buffer.from(JSON.stringify(data), 'utf8');
|
|
12
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
13
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
14
|
+
const tag = cipher.getAuthTag();
|
|
15
|
+
return Buffer.concat([iv, ciphertext, tag]).toString('base64');
|
|
16
|
+
}
|
|
17
|
+
export function decrypt(encoded, key) {
|
|
18
|
+
const buf = Buffer.from(encoded, 'base64');
|
|
19
|
+
const iv = buf.subarray(0, 12);
|
|
20
|
+
const ciphertext = buf.subarray(12, -16);
|
|
21
|
+
const tag = buf.subarray(-16);
|
|
22
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
23
|
+
decipher.setAuthTag(tag);
|
|
24
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
25
|
+
return JSON.parse(plaintext.toString('utf8'));
|
|
26
|
+
}
|
|
27
|
+
const _keyCache = {};
|
|
28
|
+
export function getKey(salt) {
|
|
29
|
+
if (_keyCache[salt])
|
|
30
|
+
return _keyCache[salt];
|
|
31
|
+
const key = deriveKey('vH33r_2025_AES_GCM_S3cur3_K3y_9X7mP4qR8nT2wE5yU1oI6aS3dF7gH0jK9lZ', salt);
|
|
32
|
+
_keyCache[salt] = key;
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
2
|
+
export declare function buildMultipart(fields: Array<{
|
|
3
|
+
name: string;
|
|
4
|
+
value: Buffer | string;
|
|
5
|
+
filename?: string;
|
|
6
|
+
contentType?: string;
|
|
7
|
+
}>): {
|
|
8
|
+
boundary: string;
|
|
9
|
+
body: Buffer;
|
|
10
|
+
};
|
|
11
|
+
export declare function httpsPost(host: string, path: string, headers: Record<string, string>, body: Buffer | string): Promise<{
|
|
12
|
+
status: number;
|
|
13
|
+
body: Buffer;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function getDims(buf: Buffer): {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
} | null;
|
|
20
|
+
export declare function aspectRatio(w: number, h: number): string;
|
|
21
|
+
export declare function dlImage(url: string): Promise<{
|
|
22
|
+
body: Buffer;
|
|
23
|
+
contentType: string;
|
|
24
|
+
}>;
|