@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.
Files changed (2) hide show
  1. package/index.mjs +336 -47
  2. 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 get_openai_codex_exec_args = function () {
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 detect_real_person_in_image = async function (base64) {
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: `detect if a postrate of real person shown in the picture `,
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 postrate of real person in the picture return true otherwise return false'),
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 res?.is_real_person_in_picture;
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 create_avatar_from_pic = async function () {
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
- const is_real_person_in_picture = await detect_real_person_in_image(imageBase64);
7462
- if (is_real_person_in_picture) {
7463
- if (image_blob_ret.is_transparent) {
7464
- // keep the original
7465
- imageBase64 = Buffer.from(await image_blob_ret.image_blob.arrayBuffer()).toString('base64');
7466
- avatar_source = 'authentic profile';
7467
- } else {
7468
- // create avatar from profile picture
7469
- await create_avatar_from_pic();
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.5606",
3
+ "version": "1.1.5608",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",
7
7
  "dependencies": {
8
- "lodash": "^4.17.21",
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
- "openai": "^6.7.0",
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
- "youtube-dl-exec": "^2.1.3",
23
- "md-to-pdf": "^5.2.5",
21
+ "sharp": "^0.34.4",
22
+ "uglify-js": "^3.19.3",
23
+ "utf8": "^3.0.0",
24
24
  "vm2": "^3.10.0",
25
- "amqplib": "^0.10.9",
26
- "firebase-admin": "^9.6.0"
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"