@xuda.io/ai_module 1.1.5606 → 1.1.5608
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/index.mjs +336 -47
- package/package.json +15 -15
package/index.mjs
CHANGED
|
@@ -7,6 +7,10 @@ import { pathToFileURL } from 'node:url';
|
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
|
|
9
9
|
import sharp from 'sharp';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { removeBackground as imglyRemoveBackground } from '@imgly/background-removal-node';
|
|
12
|
+
|
|
13
|
+
const imglyPublicPath = pathToFileURL(path.dirname(createRequire(import.meta.url).resolve('@imgly/background-removal-node')) + path.sep).href;
|
|
10
14
|
|
|
11
15
|
import puppeteer from 'puppeteer-extra';
|
|
12
16
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
@@ -111,6 +115,26 @@ const parse_codex_jsonl = function (stdout) {
|
|
|
111
115
|
return events;
|
|
112
116
|
};
|
|
113
117
|
|
|
118
|
+
const get_number_value = function (...values) {
|
|
119
|
+
for (const value of values) {
|
|
120
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
121
|
+
if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) return Number(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return 0;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const normalize_codex_usage = function (usage) {
|
|
128
|
+
if (!usage || typeof usage !== 'object') {
|
|
129
|
+
return { input_tokens: 0, output_tokens: 0 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const input_tokens = get_number_value(usage.input_tokens, usage.inputTokens, usage.prompt_tokens, usage.promptTokens, usage.total_input_tokens, usage.totalInputTokens, usage.total_token_usage?.input_tokens, usage.totalTokenUsage?.inputTokens);
|
|
133
|
+
const output_tokens = get_number_value(usage.output_tokens, usage.outputTokens, usage.completion_tokens, usage.completionTokens, usage.total_output_tokens, usage.totalOutputTokens, usage.total_token_usage?.output_tokens, usage.totalTokenUsage?.outputTokens);
|
|
134
|
+
|
|
135
|
+
return { input_tokens, output_tokens };
|
|
136
|
+
};
|
|
137
|
+
|
|
114
138
|
const create_codex_jsonl_stream_parser = function (onEvent) {
|
|
115
139
|
let buffer = '';
|
|
116
140
|
|
|
@@ -137,11 +161,24 @@ const get_openai_codex_cli_command = function () {
|
|
|
137
161
|
return _conf.openai_codex_command || 'codex';
|
|
138
162
|
};
|
|
139
163
|
|
|
140
|
-
const
|
|
164
|
+
const get_openai_codex_model = function () {
|
|
165
|
+
return _conf.openai_codex_model || _conf.ai_models?.openai_codex_model?.model || null;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const get_openai_codex_usage_model = function (codex_model, requested_codex_model) {
|
|
169
|
+
if (requested_codex_model) return codex_model;
|
|
170
|
+
return _conf.ai_models?.openai_codex_model ? 'openai_codex_model' : codex_model || _conf.openai_codex_command || 'openai_codex_cli';
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const get_openai_codex_exec_args = function (codex_model) {
|
|
141
174
|
const args = ['exec', '--json'];
|
|
142
175
|
const bypass_approvals_and_sandbox = typeof _conf.openai_codex_bypass_approvals_and_sandbox === 'undefined' ? true : _conf.openai_codex_bypass_approvals_and_sandbox;
|
|
143
176
|
const sandbox = _conf.openai_codex_sandbox;
|
|
144
177
|
|
|
178
|
+
if (codex_model) {
|
|
179
|
+
args.push('--model', codex_model);
|
|
180
|
+
}
|
|
181
|
+
|
|
145
182
|
if (bypass_approvals_and_sandbox) {
|
|
146
183
|
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
147
184
|
} else if (sandbox) {
|
|
@@ -1216,11 +1253,14 @@ export const execute_codex_request = async function (req_or_ip, prompt_arg, atta
|
|
|
1216
1253
|
const prompt = is_req_call ? req_or_ip.prompt : prompt_arg;
|
|
1217
1254
|
let attachments = is_req_call ? req_or_ip.attachments || [] : attachments_arg;
|
|
1218
1255
|
const uid = is_req_call ? req_or_ip.uid : undefined;
|
|
1256
|
+
const account_profile_info = is_req_call && uid ? req_or_ip.account_profile_info || (await get_active_account_profile_info(uid, req_or_ip.profile_id)) : undefined;
|
|
1257
|
+
const requested_codex_model = is_req_call ? req_or_ip.codex_model || req_or_ip.model : null;
|
|
1258
|
+
const codex_model = requested_codex_model || get_openai_codex_model();
|
|
1219
1259
|
const conversation_id = is_req_call ? req_or_ip.conversation_id || req_or_ip.conversation_doc?._id : undefined;
|
|
1220
1260
|
const job_id = is_req_call ? prompt_arg || req_or_ip.job_id : undefined;
|
|
1221
1261
|
stream_enabled = is_req_call ? req_or_ip.stream !== false && req_or_ip.stream !== 'false' : false;
|
|
1222
|
-
const prompt_conversation_item_id = is_req_call ? await _common.xuda_get_uuid('chat_conversation_item') : undefined;
|
|
1223
|
-
const response_conversation_item_id = is_req_call ? await _common.xuda_get_uuid('chat_conversation_item') : undefined;
|
|
1262
|
+
const prompt_conversation_item_id = is_req_call && uid ? await _common.xuda_get_uuid('chat_conversation_item') : undefined;
|
|
1263
|
+
const response_conversation_item_id = is_req_call && uid ? await _common.xuda_get_uuid('chat_conversation_item') : undefined;
|
|
1224
1264
|
|
|
1225
1265
|
let stream_delta_seq = 0;
|
|
1226
1266
|
let stream_delta_text = '';
|
|
@@ -1286,6 +1326,7 @@ export const execute_codex_request = async function (req_or_ip, prompt_arg, atta
|
|
|
1286
1326
|
}
|
|
1287
1327
|
|
|
1288
1328
|
const reasoning = [];
|
|
1329
|
+
let codex_usage = { input_tokens: 0, output_tokens: 0 };
|
|
1289
1330
|
let streamed_agent_messages = new Set();
|
|
1290
1331
|
const pushReasoning = function (type, text, metadata = {}) {
|
|
1291
1332
|
if (!text) return;
|
|
@@ -1345,6 +1386,7 @@ export const execute_codex_request = async function (req_or_ip, prompt_arg, atta
|
|
|
1345
1386
|
emitToDashboard('stream_phase', event.message, { update: true, error: true });
|
|
1346
1387
|
break;
|
|
1347
1388
|
case 'turn.completed':
|
|
1389
|
+
codex_usage = normalize_codex_usage(event.usage);
|
|
1348
1390
|
emitToDashboard('stream_phase', 'Codex request completed', { update: true, usage: event.usage });
|
|
1349
1391
|
break;
|
|
1350
1392
|
case 'turn.failed':
|
|
@@ -1401,7 +1443,7 @@ ${prompt}`;
|
|
|
1401
1443
|
}
|
|
1402
1444
|
|
|
1403
1445
|
const openai_codex_command = get_openai_codex_cli_command();
|
|
1404
|
-
const codex_args = get_openai_codex_exec_args();
|
|
1446
|
+
const codex_args = get_openai_codex_exec_args(codex_model);
|
|
1405
1447
|
for (const image_path of image_paths) {
|
|
1406
1448
|
codex_args.push('--image', image_path);
|
|
1407
1449
|
}
|
|
@@ -1415,6 +1457,29 @@ ${prompt}`;
|
|
|
1415
1457
|
onStdout: create_codex_jsonl_stream_parser(handleCodexEvent),
|
|
1416
1458
|
});
|
|
1417
1459
|
const events = parse_codex_jsonl(ret.stdout);
|
|
1460
|
+
if (!codex_usage.input_tokens && !codex_usage.output_tokens && Array.isArray(events)) {
|
|
1461
|
+
const completed_event_with_usage = events.findLast((event) => event?.type === 'turn.completed' && event.usage);
|
|
1462
|
+
codex_usage = normalize_codex_usage(completed_event_with_usage?.usage);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (uid && account_profile_info && (codex_usage.input_tokens || codex_usage.output_tokens)) {
|
|
1466
|
+
account_msa.record_ai_usage(
|
|
1467
|
+
uid,
|
|
1468
|
+
codex_usage.input_tokens,
|
|
1469
|
+
codex_usage.output_tokens,
|
|
1470
|
+
'execute codex request',
|
|
1471
|
+
prompt,
|
|
1472
|
+
get_openai_codex_usage_model(codex_model, requested_codex_model),
|
|
1473
|
+
{
|
|
1474
|
+
conversation_id,
|
|
1475
|
+
job_id,
|
|
1476
|
+
remote_host: get_codex_remote_host(ip),
|
|
1477
|
+
exit_code: ret.exit_code,
|
|
1478
|
+
attachments_count: attachments.length,
|
|
1479
|
+
},
|
|
1480
|
+
account_profile_info,
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1418
1483
|
|
|
1419
1484
|
if (ret.exit_code !== 0) {
|
|
1420
1485
|
emitToDashboard('stream_phase', 'Codex request failed', { update: true });
|
|
@@ -1432,6 +1497,7 @@ ${prompt}`;
|
|
|
1432
1497
|
stderr: ret.stderr,
|
|
1433
1498
|
events,
|
|
1434
1499
|
reasoning,
|
|
1500
|
+
usage: codex_usage,
|
|
1435
1501
|
},
|
|
1436
1502
|
};
|
|
1437
1503
|
}
|
|
@@ -1448,6 +1514,7 @@ ${prompt}`;
|
|
|
1448
1514
|
stderr: ret.stderr,
|
|
1449
1515
|
events,
|
|
1450
1516
|
reasoning,
|
|
1517
|
+
usage: codex_usage,
|
|
1451
1518
|
},
|
|
1452
1519
|
};
|
|
1453
1520
|
} catch (err) {
|
|
@@ -7202,7 +7269,7 @@ export const get_profile_avatar = async function (profile_picture, uid, prompt,
|
|
|
7202
7269
|
const tempOutputPath = path.join(tempDir, `output_${uniqueId}.png`);
|
|
7203
7270
|
const { is_user } = metadata;
|
|
7204
7271
|
let filename = email || _id || name;
|
|
7205
|
-
const
|
|
7272
|
+
const inspect_person_in_image = async function (base64) {
|
|
7206
7273
|
const ret = await submit_chat_gpt_prompt({
|
|
7207
7274
|
uid,
|
|
7208
7275
|
model: _conf.default_ai_model,
|
|
@@ -7212,7 +7279,7 @@ export const get_profile_avatar = async function (profile_picture, uid, prompt,
|
|
|
7212
7279
|
content: [
|
|
7213
7280
|
{
|
|
7214
7281
|
type: 'input_text',
|
|
7215
|
-
text: `
|
|
7282
|
+
text: `Inspect this profile image. Return whether it contains a real human portrait suitable for an authentic account avatar. Mark it unsuitable if the face is not visible, side-facing, heavily cropped, very blurry, tiny, or not a real person.`,
|
|
7216
7283
|
},
|
|
7217
7284
|
{
|
|
7218
7285
|
type: 'input_image',
|
|
@@ -7222,14 +7289,66 @@ export const get_profile_avatar = async function (profile_picture, uid, prompt,
|
|
|
7222
7289
|
},
|
|
7223
7290
|
],
|
|
7224
7291
|
response_format: z.object({
|
|
7225
|
-
is_real_person_in_picture: z.boolean().describe('if a
|
|
7292
|
+
is_real_person_in_picture: z.boolean().describe('true if a real human portrait is visible'),
|
|
7293
|
+
is_front_facing: z.boolean().describe('true if the face is mostly front-facing or only slightly angled'),
|
|
7294
|
+
is_face_too_cropped: z.boolean().describe('true if important face/head parts are cut off'),
|
|
7295
|
+
is_too_blurry: z.boolean().describe('true if the face is too blurry for an authentic avatar'),
|
|
7296
|
+
is_too_small: z.boolean().describe('true if the portrait is too small or low-detail'),
|
|
7297
|
+
needs_restoration: z.boolean().describe('true if the photo is old, scanned, scratched, faded, grainy, low-detail, or otherwise degraded such that face-restoration would noticeably improve it'),
|
|
7226
7298
|
}),
|
|
7227
7299
|
metadata: { _id, func: 'detect_real_person_in_image' },
|
|
7228
7300
|
account_profile_info,
|
|
7229
7301
|
});
|
|
7230
7302
|
|
|
7231
7303
|
const res = JSON5.parse(ret.data);
|
|
7232
|
-
return
|
|
7304
|
+
return {
|
|
7305
|
+
is_real_person_in_picture: Boolean(res?.is_real_person_in_picture),
|
|
7306
|
+
is_front_facing: Boolean(res?.is_front_facing),
|
|
7307
|
+
is_face_too_cropped: Boolean(res?.is_face_too_cropped),
|
|
7308
|
+
is_too_blurry: Boolean(res?.is_too_blurry),
|
|
7309
|
+
is_too_small: Boolean(res?.is_too_small),
|
|
7310
|
+
needs_restoration: Boolean(res?.needs_restoration),
|
|
7311
|
+
};
|
|
7312
|
+
};
|
|
7313
|
+
|
|
7314
|
+
const detect_face_box = async function (base64) {
|
|
7315
|
+
try {
|
|
7316
|
+
const ret = await submit_chat_gpt_prompt({
|
|
7317
|
+
uid,
|
|
7318
|
+
model: _conf.default_ai_model,
|
|
7319
|
+
prompt: [
|
|
7320
|
+
{
|
|
7321
|
+
role: 'user',
|
|
7322
|
+
content: [
|
|
7323
|
+
{
|
|
7324
|
+
type: 'input_text',
|
|
7325
|
+
text: `Return the bounding box of the visible face in this portrait, from the top of the head (including hair) to just below the chin, ear to ear. Use normalized coordinates in [0,1] where (0,0) is the top-left corner and (1,1) is the bottom-right corner of the image.`,
|
|
7326
|
+
},
|
|
7327
|
+
{ type: 'input_image', image_url: `data:image/png;base64,${base64}` },
|
|
7328
|
+
],
|
|
7329
|
+
},
|
|
7330
|
+
],
|
|
7331
|
+
response_format: z.object({
|
|
7332
|
+
face_top: z.number().min(0).max(1).describe('Top edge of face bbox, normalized'),
|
|
7333
|
+
face_left: z.number().min(0).max(1).describe('Left edge of face bbox, normalized'),
|
|
7334
|
+
face_width: z.number().min(0).max(1).describe('Width of face bbox, normalized'),
|
|
7335
|
+
face_height: z.number().min(0).max(1).describe('Height of face bbox, normalized'),
|
|
7336
|
+
}),
|
|
7337
|
+
metadata: { _id, func: 'detect_face_box' },
|
|
7338
|
+
account_profile_info,
|
|
7339
|
+
});
|
|
7340
|
+
const res = JSON5.parse(ret.data);
|
|
7341
|
+
if (!res || typeof res.face_height !== 'number' || res.face_height <= 0) return null;
|
|
7342
|
+
return {
|
|
7343
|
+
face_top: Number(res.face_top) || 0,
|
|
7344
|
+
face_left: Number(res.face_left) || 0,
|
|
7345
|
+
face_width: Number(res.face_width) || 0,
|
|
7346
|
+
face_height: Number(res.face_height) || 0,
|
|
7347
|
+
};
|
|
7348
|
+
} catch (err) {
|
|
7349
|
+
console.error('detect_face_box failed:', err.message);
|
|
7350
|
+
return null;
|
|
7351
|
+
}
|
|
7233
7352
|
};
|
|
7234
7353
|
|
|
7235
7354
|
try {
|
|
@@ -7419,57 +7538,48 @@ export const get_profile_avatar = async function (profile_picture, uid, prompt,
|
|
|
7419
7538
|
}
|
|
7420
7539
|
} else {
|
|
7421
7540
|
/// PERSONAL
|
|
7422
|
-
const
|
|
7423
|
-
const { bio, country, mainCategory, subCategory } = metadata;
|
|
7424
|
-
|
|
7425
|
-
const prompt = `Completely remove the background, enhance and color-correct the subject, and return only a centered head-and-shoulders portrait facing the camera. Add extra top margin, include no labels or text, and export the result as a clean transparent PNG.
|
|
7426
|
-
Ensure the subject is large enough to fill most of the frame. Use the supplied name, country, and context to guide the character’s appearance.
|
|
7427
|
-
Context: ${JSON.stringify({ name, bio, country, mainCategory, subCategory })}`;
|
|
7541
|
+
const source_quality = await inspectAvatarSourceQuality(imageBase64);
|
|
7428
7542
|
|
|
7429
|
-
// const { first_name, last_name, bio, country, mainCategory, subCategory } = account_obj.account_info;
|
|
7430
|
-
// const model = 'gpt-image-1-mini';
|
|
7431
|
-
const model = 'chatgpt-image-latest';
|
|
7432
|
-
let ai_avatar_response;
|
|
7433
|
-
try {
|
|
7434
|
-
ai_avatar_response = await client.images.edit({
|
|
7435
|
-
model,
|
|
7436
|
-
image: image_blob_ret.image_blob,
|
|
7437
|
-
|
|
7438
|
-
prompt,
|
|
7439
|
-
background: 'transparent',
|
|
7440
|
-
});
|
|
7441
|
-
report_ai_status(model);
|
|
7442
|
-
} catch (err) {
|
|
7443
|
-
report_ai_status(model, err);
|
|
7444
|
-
throw err;
|
|
7445
|
-
}
|
|
7446
|
-
account_msa.record_ai_usage(uid, ai_avatar_response.usage.input_tokens, ai_avatar_response.usage.output_tokens, 'profile avatar', prompt, model, { profile_picture }, account_profile_info);
|
|
7447
|
-
imageBase64 = ai_avatar_response.data[0].b64_json;
|
|
7448
|
-
avatar_source = 'ai profile';
|
|
7449
|
-
};
|
|
7450
7543
|
const create_fictional_avatar = async function () {
|
|
7451
7544
|
// const { name, bio, country, mainCategory, subCategory } = account_obj.account_info;
|
|
7452
7545
|
|
|
7453
|
-
const prompt = `analyze the the person info : ${JSON.stringify(metadata)}
|
|
7546
|
+
const prompt = `analyze the the person info : ${JSON.stringify(metadata)}
|
|
7454
7547
|
and create realistic fictional profile picture transparent background ,person in the center of the picture return only a centered head-and-shoulders portrait facing the camera. include no labels or text add top margin the person should cover the whole picture`;
|
|
7455
7548
|
|
|
7456
7549
|
imageBase64 = await create_image(uid, prompt, undefined, undefined, 1, undefined, undefined, { url: profile_picture }, account_profile_info);
|
|
7457
7550
|
avatar_source = 'fictional';
|
|
7458
7551
|
};
|
|
7459
|
-
|
|
7460
7552
|
// personal account
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7553
|
+
let person_inspection = await inspect_person_in_image(imageBase64);
|
|
7554
|
+
const wants_restoration = person_inspection.needs_restoration || person_inspection.is_too_blurry;
|
|
7555
|
+
|
|
7556
|
+
if (wants_restoration && person_inspection.is_real_person_in_picture && !person_inspection.is_too_small) {
|
|
7557
|
+
console.log('Source photo flagged for restoration, calling gpt-image-1...');
|
|
7558
|
+
try {
|
|
7559
|
+
const restored_base64 = await restoreFaceWithOpenAI(imageBase64, { uid, account_profile_info, profile_picture });
|
|
7560
|
+
const verify = await inspect_person_in_image(restored_base64);
|
|
7561
|
+
if (verify.is_real_person_in_picture && !verify.is_too_blurry && !verify.is_face_too_cropped) {
|
|
7562
|
+
imageBase64 = restored_base64;
|
|
7563
|
+
person_inspection = verify;
|
|
7564
|
+
console.log('Face restoration applied');
|
|
7565
|
+
} else {
|
|
7566
|
+
console.log('Restoration verify failed, using original photo');
|
|
7567
|
+
}
|
|
7568
|
+
} catch (err) {
|
|
7569
|
+
console.error('Face restoration failed, using original photo:', err.message);
|
|
7470
7570
|
}
|
|
7571
|
+
}
|
|
7572
|
+
|
|
7573
|
+
const can_create_authentic_avatar = source_quality.is_usable && person_inspection.is_real_person_in_picture && !person_inspection.is_too_blurry && !person_inspection.is_too_small;
|
|
7574
|
+
|
|
7575
|
+
if (can_create_authentic_avatar) {
|
|
7576
|
+
const face_box = await detect_face_box(imageBase64);
|
|
7577
|
+
imageBase64 = await normalizeAuthenticProfileAvatar(imageBase64, {
|
|
7578
|
+
remove_background: !image_blob_ret.is_transparent,
|
|
7579
|
+
face_box,
|
|
7580
|
+
});
|
|
7581
|
+
avatar_source = 'authentic profile';
|
|
7471
7582
|
} else {
|
|
7472
|
-
// create avatar from profile picture
|
|
7473
7583
|
await create_fictional_avatar();
|
|
7474
7584
|
}
|
|
7475
7585
|
}
|
|
@@ -8555,6 +8665,185 @@ function normalizeFilename(name) {
|
|
|
8555
8665
|
.replace(/^_+|_+$/g, ''); // trim leading/trailing underscores
|
|
8556
8666
|
}
|
|
8557
8667
|
|
|
8668
|
+
async function inspectAvatarSourceQuality(base64Image) {
|
|
8669
|
+
const inputBuffer = Buffer.from(base64Image, 'base64');
|
|
8670
|
+
const metadata = await sharp(inputBuffer).metadata();
|
|
8671
|
+
const width = metadata.width || 0;
|
|
8672
|
+
const height = metadata.height || 0;
|
|
8673
|
+
const shortest_side = Math.min(width, height);
|
|
8674
|
+
const longest_side = Math.max(width, height);
|
|
8675
|
+
const aspect_ratio = shortest_side ? longest_side / shortest_side : 0;
|
|
8676
|
+
|
|
8677
|
+
return {
|
|
8678
|
+
width,
|
|
8679
|
+
height,
|
|
8680
|
+
shortest_side,
|
|
8681
|
+
aspect_ratio,
|
|
8682
|
+
has_alpha: Boolean(metadata.hasAlpha),
|
|
8683
|
+
is_usable: shortest_side >= 180 && aspect_ratio <= 3,
|
|
8684
|
+
};
|
|
8685
|
+
}
|
|
8686
|
+
|
|
8687
|
+
async function normalizeAuthenticProfileAvatar(base64Image, options = {}) {
|
|
8688
|
+
const { remove_background = true, face_box = null } = options;
|
|
8689
|
+
const inputBuffer = Buffer.from(base64Image, 'base64');
|
|
8690
|
+
const orientedBuffer = await sharp(inputBuffer).rotate().png({ force: true }).toBuffer();
|
|
8691
|
+
const orientedMeta = await sharp(orientedBuffer).metadata();
|
|
8692
|
+
const segmentedBuffer = remove_background ? await removePortraitBackground(orientedBuffer) : orientedBuffer;
|
|
8693
|
+
|
|
8694
|
+
const passportBuffer = face_box ? await cropToPassportFrame(segmentedBuffer, face_box, orientedMeta) : await cropToOpaqueBounds(segmentedBuffer);
|
|
8695
|
+
|
|
8696
|
+
const subjectBuffer = await sharp(passportBuffer).modulate({ brightness: 1.02, saturation: 1.04 }).sharpen({ sigma: 0.35, m1: 0.4, m2: 0.2 }).png({ quality: 98, compressionLevel: 8, force: true }).toBuffer();
|
|
8697
|
+
|
|
8698
|
+
return subjectBuffer.toString('base64');
|
|
8699
|
+
}
|
|
8700
|
+
|
|
8701
|
+
async function cropToPassportFrame(segmentedBuffer, face_box, sourceMeta) {
|
|
8702
|
+
const segmentedMeta = await sharp(segmentedBuffer).metadata();
|
|
8703
|
+
const sourceW = segmentedMeta.width || sourceMeta.width || 0;
|
|
8704
|
+
const sourceH = segmentedMeta.height || sourceMeta.height || 0;
|
|
8705
|
+
if (!sourceW || !sourceH) return cropToOpaqueBounds(segmentedBuffer);
|
|
8706
|
+
|
|
8707
|
+
const faceCenterX = (face_box.face_left + face_box.face_width / 2) * sourceW;
|
|
8708
|
+
const faceCenterY = (face_box.face_top + face_box.face_height / 2) * sourceH;
|
|
8709
|
+
const faceHeightPx = face_box.face_height * sourceH;
|
|
8710
|
+
if (!(faceHeightPx > 4)) return cropToOpaqueBounds(segmentedBuffer);
|
|
8711
|
+
|
|
8712
|
+
const faceFraction = 0.62;
|
|
8713
|
+
const frameSize = Math.max(8, Math.round(faceHeightPx / faceFraction));
|
|
8714
|
+
const verticalAnchor = 0.42;
|
|
8715
|
+
const cropLeft = Math.round(faceCenterX - frameSize / 2);
|
|
8716
|
+
const cropTop = Math.round(faceCenterY - frameSize * verticalAnchor);
|
|
8717
|
+
|
|
8718
|
+
const padLeft = Math.max(0, -cropLeft);
|
|
8719
|
+
const padTop = Math.max(0, -cropTop);
|
|
8720
|
+
const padRight = Math.max(0, cropLeft + frameSize - sourceW);
|
|
8721
|
+
const padBottom = Math.max(0, cropTop + frameSize - sourceH);
|
|
8722
|
+
|
|
8723
|
+
let workingBuffer = segmentedBuffer;
|
|
8724
|
+
if (padLeft || padTop || padRight || padBottom) {
|
|
8725
|
+
workingBuffer = await sharp(segmentedBuffer)
|
|
8726
|
+
.ensureAlpha()
|
|
8727
|
+
.extend({
|
|
8728
|
+
left: padLeft,
|
|
8729
|
+
top: padTop,
|
|
8730
|
+
right: padRight,
|
|
8731
|
+
bottom: padBottom,
|
|
8732
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
8733
|
+
})
|
|
8734
|
+
.png({ force: true })
|
|
8735
|
+
.toBuffer();
|
|
8736
|
+
}
|
|
8737
|
+
|
|
8738
|
+
const workingMeta = await sharp(workingBuffer).metadata();
|
|
8739
|
+
const wW = workingMeta.width || 0;
|
|
8740
|
+
const wH = workingMeta.height || 0;
|
|
8741
|
+
|
|
8742
|
+
const wantLeft = cropLeft + padLeft;
|
|
8743
|
+
const wantTop = cropTop + padTop;
|
|
8744
|
+
const extractLeft = Math.max(0, Math.min(Math.max(0, wW - 1), wantLeft));
|
|
8745
|
+
const extractTop = Math.max(0, Math.min(Math.max(0, wH - 1), wantTop));
|
|
8746
|
+
const extractWidth = Math.max(1, Math.min(wW - extractLeft, frameSize));
|
|
8747
|
+
const extractHeight = Math.max(1, Math.min(wH - extractTop, frameSize));
|
|
8748
|
+
|
|
8749
|
+
if (extractWidth <= 0 || extractHeight <= 0) {
|
|
8750
|
+
console.warn('cropToPassportFrame: degenerate extract region, falling back', { sourceW, sourceH, frameSize, cropLeft, cropTop, wW, wH });
|
|
8751
|
+
return cropToOpaqueBounds(segmentedBuffer);
|
|
8752
|
+
}
|
|
8753
|
+
|
|
8754
|
+
const framed = await sharp(workingBuffer).extract({ left: extractLeft, top: extractTop, width: extractWidth, height: extractHeight }).png({ force: true }).toBuffer();
|
|
8755
|
+
|
|
8756
|
+
return sharp(framed)
|
|
8757
|
+
.resize(1024, 1024, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
8758
|
+
.png({ force: true })
|
|
8759
|
+
.toBuffer();
|
|
8760
|
+
}
|
|
8761
|
+
|
|
8762
|
+
async function removePortraitBackground(inputBuffer) {
|
|
8763
|
+
const orientedBuffer = await sharp(inputBuffer).rotate().png({ force: true }).toBuffer();
|
|
8764
|
+
const inputBlob = new Blob([orientedBuffer], { type: 'image/png' });
|
|
8765
|
+
|
|
8766
|
+
const outputBlob = await imglyRemoveBackground(inputBlob, {
|
|
8767
|
+
model: 'medium',
|
|
8768
|
+
publicPath: imglyPublicPath,
|
|
8769
|
+
output: { format: 'image/png' },
|
|
8770
|
+
});
|
|
8771
|
+
|
|
8772
|
+
return Buffer.from(await outputBlob.arrayBuffer());
|
|
8773
|
+
}
|
|
8774
|
+
|
|
8775
|
+
async function cropToOpaqueBounds(inputBuffer) {
|
|
8776
|
+
const image = sharp(inputBuffer).ensureAlpha();
|
|
8777
|
+
const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
|
|
8778
|
+
const { width, height, channels } = info;
|
|
8779
|
+
const alphaThreshold = 8;
|
|
8780
|
+
|
|
8781
|
+
let minX = width;
|
|
8782
|
+
let minY = height;
|
|
8783
|
+
let maxX = -1;
|
|
8784
|
+
let maxY = -1;
|
|
8785
|
+
|
|
8786
|
+
for (let y = 0; y < height; y++) {
|
|
8787
|
+
for (let x = 0; x < width; x++) {
|
|
8788
|
+
const alpha = data[(y * width + x) * channels + (channels - 1)];
|
|
8789
|
+
if (alpha > alphaThreshold) {
|
|
8790
|
+
if (x < minX) minX = x;
|
|
8791
|
+
if (x > maxX) maxX = x;
|
|
8792
|
+
if (y < minY) minY = y;
|
|
8793
|
+
if (y > maxY) maxY = y;
|
|
8794
|
+
}
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
8797
|
+
|
|
8798
|
+
if (maxX < 0 || maxY < 0) {
|
|
8799
|
+
return sharp(inputBuffer).png({ force: true }).toBuffer();
|
|
8800
|
+
}
|
|
8801
|
+
|
|
8802
|
+
const padX = Math.round((maxX - minX + 1) * 0.04);
|
|
8803
|
+
const padY = Math.round((maxY - minY + 1) * 0.04);
|
|
8804
|
+
const cropLeft = Math.max(0, minX - padX);
|
|
8805
|
+
const cropTop = Math.max(0, minY - padY);
|
|
8806
|
+
const cropWidth = Math.min(width - cropLeft, maxX - minX + 1 + padX * 2);
|
|
8807
|
+
const cropHeight = Math.min(height - cropTop, maxY - minY + 1 + padY * 2);
|
|
8808
|
+
|
|
8809
|
+
return sharp(inputBuffer).ensureAlpha().extract({ left: cropLeft, top: cropTop, width: cropWidth, height: cropHeight }).png({ force: true }).toBuffer();
|
|
8810
|
+
}
|
|
8811
|
+
|
|
8812
|
+
async function restoreFaceWithOpenAI(base64Image, ctx = {}) {
|
|
8813
|
+
const { uid, account_profile_info, profile_picture } = ctx;
|
|
8814
|
+
const inputBuffer = Buffer.from(base64Image, 'base64');
|
|
8815
|
+
const oriented = await sharp(inputBuffer).rotate().png({ force: true }).toBuffer();
|
|
8816
|
+
const meta = await sharp(oriented).metadata();
|
|
8817
|
+
const maxDim = Math.max(meta.width || 0, meta.height || 0);
|
|
8818
|
+
const sourceBuffer = maxDim > 1536 ? await sharp(oriented).resize({ width: 1536, height: 1536, fit: 'inside' }).png({ force: true }).toBuffer() : oriented;
|
|
8819
|
+
|
|
8820
|
+
const imageFile = await toFile(sourceBuffer, 'portrait.png', { type: 'image/png' });
|
|
8821
|
+
const model = 'gpt-image-1';
|
|
8822
|
+
let response;
|
|
8823
|
+
try {
|
|
8824
|
+
response = await client.images.edit({
|
|
8825
|
+
model,
|
|
8826
|
+
image: imageFile,
|
|
8827
|
+
input_fidelity: 'high',
|
|
8828
|
+
quality: 'high',
|
|
8829
|
+
size: 'auto',
|
|
8830
|
+
prompt:
|
|
8831
|
+
'Restore this portrait photograph. Remove scratches, dust, grain, noise, faded color, and any age-related damage. Recover natural skin texture, sharpen face details, and balance lighting and color. Preserve the exact same person completely unchanged: same face shape, same eye shape and color, same nose, same mouth, same hairline and hair, same skin tone, same age, same gender, same facial expression, and same clothing. Do not alter, beautify, age, or de-age the person. Output a clean, naturally lit photograph of the same person.',
|
|
8832
|
+
});
|
|
8833
|
+
report_ai_status(model);
|
|
8834
|
+
} catch (err) {
|
|
8835
|
+
report_ai_status(model, err);
|
|
8836
|
+
throw err;
|
|
8837
|
+
}
|
|
8838
|
+
|
|
8839
|
+
const outBase64 = response.data?.[0]?.b64_json;
|
|
8840
|
+
if (!outBase64) throw new Error('OpenAI returned no restored image');
|
|
8841
|
+
if (response.usage && account_msa?.record_ai_usage) {
|
|
8842
|
+
account_msa.record_ai_usage(uid, response.usage.input_tokens, response.usage.output_tokens, 'avatar face restoration', 'restore portrait', model, { profile_picture }, account_profile_info);
|
|
8843
|
+
}
|
|
8844
|
+
return outBase64;
|
|
8845
|
+
}
|
|
8846
|
+
|
|
8558
8847
|
async function normalizeBase64To1024(
|
|
8559
8848
|
base64Image,
|
|
8560
8849
|
resize = {
|
package/package.json
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xuda.io/ai_module",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5608",
|
|
4
4
|
"description": "Xuda AI Module",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"
|
|
8
|
+
"@imgly/background-removal-node": "^1.4.5",
|
|
9
|
+
"@openai/agents": "^0.3.4",
|
|
10
|
+
"amqplib": "^0.10.9",
|
|
9
11
|
"dotenv": "^16.0.3",
|
|
10
|
-
"
|
|
12
|
+
"firebase-admin": "^9.6.0",
|
|
11
13
|
"fs-extra": "^11.2.0",
|
|
12
|
-
"utf8": "^3.0.0",
|
|
13
|
-
"@openai/agents": "^0.3.4",
|
|
14
|
-
"uglify-js": "^3.19.3",
|
|
15
|
-
"json5": "^2.2.3",
|
|
16
|
-
"zod": "^3.25.67",
|
|
17
|
-
"zod-to-json-schema": "^3.24.6",
|
|
18
|
-
"sharp": "^0.34.4",
|
|
19
14
|
"https-proxy-agent": "^7.0.6",
|
|
15
|
+
"json5": "^2.2.3",
|
|
16
|
+
"lodash": "^4.17.21",
|
|
17
|
+
"md-to-pdf": "^5.2.5",
|
|
18
|
+
"openai": "^6.7.0",
|
|
20
19
|
"puppeteer-extra": "^3.3.6",
|
|
21
20
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
21
|
+
"sharp": "^0.34.4",
|
|
22
|
+
"uglify-js": "^3.19.3",
|
|
23
|
+
"utf8": "^3.0.0",
|
|
24
24
|
"vm2": "^3.10.0",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
25
|
+
"youtube-dl-exec": "^2.1.3",
|
|
26
|
+
"zod": "^3.25.67",
|
|
27
|
+
"zod-to-json-schema": "^3.24.6"
|
|
27
28
|
},
|
|
28
|
-
"devDependencies": {},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"pub": "npm version patch --force && npm publish",
|
|
31
31
|
"test:codex": "node tests/execute-codex-request.mjs"
|