opencode-pollinations-plugin 6.4.6 → 6.4.8
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/README.de.md +3 -0
- package/README.es.md +3 -0
- package/README.fr.md +3 -0
- package/README.it.md +3 -0
- package/README.md +4 -1
- package/README.zh.md +3 -0
- package/dist/locales/de.json +17 -2
- package/dist/locales/en.json +17 -2
- package/dist/locales/es.json +17 -2
- package/dist/locales/fr.json +17 -2
- package/dist/locales/it.json +17 -2
- package/dist/locales/zh.json +17 -2
- 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
|
@@ -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
|
+
}>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// imgtools/helpers.ts — HTTP, multipart, image dimensions
|
|
2
|
+
// Adapté depuis /home/fkomp/Bureau/oracle/dev-serveur/iamges-tools-api/modules/helpers.js
|
|
3
|
+
import * as https from 'https';
|
|
4
|
+
import * as crypto from 'crypto';
|
|
5
|
+
export function sleep(ms) {
|
|
6
|
+
return new Promise(r => setTimeout(r, ms));
|
|
7
|
+
}
|
|
8
|
+
export function buildMultipart(fields) {
|
|
9
|
+
const boundary = '----WebKitFormBoundary' + crypto.randomBytes(16).toString('hex');
|
|
10
|
+
const parts = [];
|
|
11
|
+
for (const f of fields) {
|
|
12
|
+
let h = `--${boundary}\r\nContent-Disposition: form-data; name="${f.name}"`;
|
|
13
|
+
if (f.filename) {
|
|
14
|
+
h += `; filename="${f.filename}"\r\nContent-Type: ${f.contentType || 'application/octet-stream'}\r\n\r\n`;
|
|
15
|
+
parts.push(Buffer.from(h));
|
|
16
|
+
parts.push(typeof f.value === 'string' ? Buffer.from(f.value) : f.value);
|
|
17
|
+
parts.push(Buffer.from('\r\n'));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
h += `\r\n\r\n${f.value}\r\n`;
|
|
21
|
+
parts.push(Buffer.from(h));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
parts.push(Buffer.from(`--${boundary}--\r\n`));
|
|
25
|
+
return { boundary, body: Buffer.concat(parts) };
|
|
26
|
+
}
|
|
27
|
+
export function httpsPost(host, path, headers, body) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const req = https.request({
|
|
30
|
+
hostname: host,
|
|
31
|
+
path,
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Length': Buffer.byteLength(body),
|
|
35
|
+
...headers,
|
|
36
|
+
},
|
|
37
|
+
timeout: 120000,
|
|
38
|
+
}, (res) => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
res.on('data', c => chunks.push(c));
|
|
41
|
+
res.on('end', () => {
|
|
42
|
+
resolve({
|
|
43
|
+
status: res.statusCode || 0,
|
|
44
|
+
body: Buffer.concat(chunks),
|
|
45
|
+
headers: res.headers,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
req.on('error', reject);
|
|
50
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
|
51
|
+
if (typeof body === 'string')
|
|
52
|
+
req.write(body);
|
|
53
|
+
else
|
|
54
|
+
req.write(body);
|
|
55
|
+
req.end();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function getDims(buf) {
|
|
59
|
+
try {
|
|
60
|
+
// PNG
|
|
61
|
+
if (buf[0] === 0x89 && buf[1] === 0x50) {
|
|
62
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
63
|
+
}
|
|
64
|
+
// JPEG
|
|
65
|
+
if (buf[0] === 0xff && buf[1] === 0xd8) {
|
|
66
|
+
let offset = 2;
|
|
67
|
+
while (offset < buf.length) {
|
|
68
|
+
if (buf[offset] !== 0xff)
|
|
69
|
+
break;
|
|
70
|
+
const marker = buf[offset + 1];
|
|
71
|
+
if (marker === 0xc0 || marker === 0xc2) {
|
|
72
|
+
return { width: buf.readUInt16BE(offset + 7), height: buf.readUInt16BE(offset + 5) };
|
|
73
|
+
}
|
|
74
|
+
offset += 2 + buf.readUInt16BE(offset + 2);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function aspectRatio(w, h) {
|
|
84
|
+
if (w === 0 || h === 0)
|
|
85
|
+
return '1:1';
|
|
86
|
+
const r = w / h;
|
|
87
|
+
if (r > 1.7)
|
|
88
|
+
return '16:9';
|
|
89
|
+
if (r > 1.2)
|
|
90
|
+
return '4:3';
|
|
91
|
+
if (r > 0.8)
|
|
92
|
+
return '1:1';
|
|
93
|
+
if (r > 0.55)
|
|
94
|
+
return '3:4';
|
|
95
|
+
return '9:16';
|
|
96
|
+
}
|
|
97
|
+
export async function dlImage(url) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
https.get(url, { timeout: 15000 }, (res) => {
|
|
100
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
101
|
+
return reject(new Error(`Download failed: ${res.statusCode}`));
|
|
102
|
+
}
|
|
103
|
+
const chunks = [];
|
|
104
|
+
res.on('data', c => chunks.push(c));
|
|
105
|
+
res.on('end', () => resolve({
|
|
106
|
+
body: Buffer.concat(chunks),
|
|
107
|
+
contentType: res.headers['content-type'] || 'image/png',
|
|
108
|
+
}));
|
|
109
|
+
}).on('error', reject);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// object_remover — Suppression d'objets par prompt via objectremover.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 objectRemoverTool = tool({
|
|
11
|
+
description: `Supprime un objet d'une image via un prompt (gratuit, appel direct).
|
|
12
|
+
Exemples : "remove the person", "erase the text", "delete the car".
|
|
13
|
+
Temps de traitement : 30-120s. Pas de clé requise.`,
|
|
14
|
+
args: {
|
|
15
|
+
file: tool.schema.string().describe('Chemin local de l\'image à traiter'),
|
|
16
|
+
prompt: tool.schema.string().describe('Description de l\'objet à supprimer (ex: "remove the red car")'),
|
|
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.object_remover.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 prompt = args.prompt || 'remove unwanted objects';
|
|
29
|
+
context.metadata({ title: '🧹 object_remover', metadata: { type: 'info', message: `Suppression de "${prompt}"...` } });
|
|
30
|
+
try {
|
|
31
|
+
const result = await processTool('ruo', {
|
|
32
|
+
data: imageData,
|
|
33
|
+
contentType: mimeType,
|
|
34
|
+
filename: path.basename(imagePath),
|
|
35
|
+
options: { prompt },
|
|
36
|
+
});
|
|
37
|
+
if (!result.imageUrl) {
|
|
38
|
+
return t('tools.object_remover.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('object_remover');
|
|
42
|
+
const outputFilename = (args.filename ? sanitizeFilename(args.filename) : generateFilename('ruo', 'object-remover', 'png'));
|
|
43
|
+
const filePath = path.join(outputDir, outputFilename.includes('.') ? outputFilename : `${outputFilename}.png`);
|
|
44
|
+
ensureDir(outputDir);
|
|
45
|
+
fs.writeFileSync(filePath, dl.data);
|
|
46
|
+
emitStatusToast("success", "🧹 Objet supprimé", "object_remover", { filePath, freeTool: true });
|
|
47
|
+
emitStatusToast('success', '🧹 Objet supprimé', 'object_remover', { filePath, freeTool: true });
|
|
48
|
+
const fileSize = fs.statSync(filePath).size;
|
|
49
|
+
const lines = [];
|
|
50
|
+
lines.push('🧹 **Objet Supprimé**');
|
|
51
|
+
lines.push('━━━━━━━━━━━━━━━━━━');
|
|
52
|
+
lines.push(`Fichier : \`${filePath}\``);
|
|
53
|
+
lines.push(`Taille : ${formatFileSize(fileSize)}`);
|
|
54
|
+
lines.push(`Prompt : ${prompt}`);
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
emitStatusToast("warning", "❌ " + (err.message?.substring(0, 80) || ""), "object_remover", { freeTool: true });
|
|
59
|
+
return "❌ Erreur : " + err.message;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
});
|