@xuda.io/ai_module 1.1.3612 → 1.1.3613

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 +250 -0
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -5689,3 +5689,253 @@ const delete_depended_chats = async function (uid, reference_id) {
5689
5689
  await delete_ai_chat({ conversation_id: doc._id, uid });
5690
5690
  }
5691
5691
  };
5692
+
5693
+ export const create_ai_agent_image = async function (req, job_id, headers) {
5694
+ async function normalizeBase64To1024(base64Image) {
5695
+ const inputBuffer = Buffer.from(base64Image, 'base64');
5696
+
5697
+ const normalizedBuffer = await sharp(inputBuffer)
5698
+ .resize(1024, 1024, {
5699
+ fit: 'contain', // Preserve aspect ratio, fit within 211x211
5700
+ background: { r: 0, g: 0, b: 0, alpha: 0 }, // Transparent background for padding
5701
+ })
5702
+ .png({
5703
+ quality: 100,
5704
+ compressionLevel: 9,
5705
+ force: true,
5706
+ })
5707
+ .toBuffer();
5708
+
5709
+ return normalizedBuffer.toString('base64'); // Return normalized base64 string
5710
+ }
5711
+
5712
+ async function hasTransparentBackground(filePath) {
5713
+ const image = sharp(filePath);
5714
+ const metadata = await image.metadata();
5715
+
5716
+ // If the image doesn't have an alpha channel at all → no transparency possible
5717
+ if (!metadata.hasAlpha) {
5718
+ return false;
5719
+ }
5720
+
5721
+ // For images with alpha channel, check if any pixel actually has alpha < 255
5722
+ const { data, info } = await image.raw().toBuffer({ resolveWithObject: true });
5723
+
5724
+ // Determine position of alpha channel based on number of channels
5725
+ const channels = info.channels; // 2 (grayscale+alpha), 4 (RGBA), etc.
5726
+ const alphaOffset = channels === 2 ? 1 : channels === 4 ? 3 : -1;
5727
+
5728
+ if (alphaOffset === -1) {
5729
+ // Unusual channel count – be conservative and assume it might have transparency
5730
+ return true;
5731
+ }
5732
+
5733
+ for (let i = alphaOffset; i < data.length; i += channels) {
5734
+ if (data[i] < 255) {
5735
+ return true; // Found at least one transparent/semi-transparent pixel
5736
+ }
5737
+ }
5738
+
5739
+ return false; // Has alpha channel but all pixels are fully opaque
5740
+ }
5741
+
5742
+ async function imageToPngBlob(filePath) {
5743
+ const ext = path.extname(filePath).toLowerCase();
5744
+
5745
+ let buffer;
5746
+
5747
+ let is_transparent = false;
5748
+ if (ext === '.png') {
5749
+ buffer = await fs.promises.readFile(filePath);
5750
+ is_transparent = await hasTransparentBackground(filePath);
5751
+ } else {
5752
+ console.log(`Converting ${path.basename(filePath)} to PNG...`);
5753
+ buffer = await sharp(filePath).png({ quality: 100, compressionLevel: 9, force: true }).toBuffer();
5754
+ }
5755
+
5756
+ // Create a Blob with correct MIME type
5757
+
5758
+ return { is_transparent, image_blob: new Blob([buffer], { type: 'image/png' }) };
5759
+ }
5760
+
5761
+ const detect_real_person_in_image = async function (base64) {
5762
+ const ret = await submit_chat_gpt_prompt({
5763
+ model: 'gpt-5-nano',
5764
+ prompt: [
5765
+ {
5766
+ role: 'user',
5767
+ content: [
5768
+ {
5769
+ type: 'input_text',
5770
+ text: `detect if a postrate of real person shown in the picture `,
5771
+ },
5772
+ {
5773
+ type: 'input_image',
5774
+ image_url: `data:image/png;base64,${base64}`,
5775
+ },
5776
+ ],
5777
+ },
5778
+ ],
5779
+ response_format: z.object({
5780
+ is_real_person_in_picture: z.boolean().describe('if a postrate of real person in the picture return true otherwise return false'),
5781
+ }),
5782
+ });
5783
+
5784
+ const res = JSON5.parse(ret.data);
5785
+ return res?.is_real_person_in_picture;
5786
+ };
5787
+
5788
+ const { url, uid, account_obj, prompt } = req;
5789
+
5790
+ try {
5791
+ if (!url) {
5792
+ throw new Error('No URL provided in request');
5793
+ }
5794
+
5795
+ // 3. Download the image to a Buffer
5796
+ const res = await fetch(url);
5797
+ if (!res.ok) {
5798
+ throw new Error(`Failed to fetch image URL: ${res.status} ${res.statusText}`);
5799
+ }
5800
+
5801
+ const arrayBuffer = await res.arrayBuffer();
5802
+ const imageBuffer = Buffer.from(arrayBuffer);
5803
+
5804
+ const tempDir = os.tmpdir();
5805
+ const uniqueId = Date.now() + '_' + Math.random().toString(36).substring(7);
5806
+
5807
+ const inputExt = path.extname(url) || '.jpg';
5808
+ const tempInputPath = path.join(tempDir, `input_${uniqueId}${inputExt}`);
5809
+ const tempOutputPath = path.join(tempDir, `output_${uniqueId}.png`); // Output is always PNG (transparent)
5810
+
5811
+ // 5. Write the downloaded image to disk
5812
+ fs.writeFileSync(tempInputPath, imageBuffer);
5813
+ /////////////////////////////////
5814
+
5815
+ const imageFiles = [tempInputPath];
5816
+
5817
+ const images = await Promise.all(
5818
+ imageFiles.map(async (filePath) => {
5819
+ const img_ret = await imageToPngBlob(filePath);
5820
+ const blob = img_ret.image_blob;
5821
+
5822
+ // OpenAI expects a filename for logging/debugging
5823
+ const filename = path.basename(filePath, path.extname(filePath)) + '.png';
5824
+ blob.name = filename; // Optional but helpful
5825
+ // @ts-ignore – name is allowed in Node 20+
5826
+ blob.lastModified = Date.now();
5827
+
5828
+ return { is_transparent: img_ret.is_transparent, image_blob: blob };
5829
+ }),
5830
+ );
5831
+
5832
+ let imageBase64;
5833
+ let avatar_source = '';
5834
+
5835
+ //////////////////
5836
+ imageBase64 = Buffer.from(await images[0].image_blob.arrayBuffer()).toString('base64');
5837
+ if (account_obj.account_info.account_type === 'business') {
5838
+ // create avatar from logo - BUSINESS
5839
+ const { bio, business_name, country, mainCategory, subCategory } = account_obj.account_info;
5840
+ // const ai_avatar_response = await client.images.edit({
5841
+ // model: 'chatgpt-image-latest',
5842
+ // image: images[0].image_blob, // only one image allowed
5843
+ // prompt: `Generate a realistic fictional portrait wearing a hat. The portrait should be centered, head-and-shoulders only, on a transparent background. Place the provided logo on the hat, and 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.
5844
+ // Context: ${JSON.stringify({ bio, business_name, country, mainCategory, subCategory })}`,
5845
+ // background: 'transparent',
5846
+ // });
5847
+ const ai_avatar_response = await client.images.edit({
5848
+ model: 'chatgpt-image-latest',
5849
+ image: images[0].image_blob, //base photo
5850
+ prompt: `Create a transparent PNG profile image for a business account featuring building branded as ${business_name}.
5851
+ Display the attached logo clearly as prominent exterior signage.
5852
+ The building’s architecture and overall atmosphere should reflect the nature of the business, aligning with ${mainCategory} and ${subCategory}.
5853
+ Use the provided bio (${bio}) to understand the company’s focus and personality, and adapt the architectural style to match the ${country} country’s local design language. Select an appropriate building type for the specific business category, and present a contemporary,
5854
+ creative office environment with a strong, recognizable brand identity. The background should be fully removed, remove trees,clouds,sky or any landscape objects`,
5855
+ background: 'transparent',
5856
+ });
5857
+ imageBase64 = ai_avatar_response.data[0].b64_json;
5858
+ avatar_source = 'fictional';
5859
+ } else {
5860
+ const create_avatar_from_pic = async function () {
5861
+ const { first_name, last_name, bio, country, mainCategory, subCategory } = account_obj.account_info;
5862
+ const ai_avatar_response = await client.images.edit({
5863
+ model: 'chatgpt-image-latest',
5864
+ image: images[0].image_blob,
5865
+
5866
+ 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.
5867
+ 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.
5868
+ Context: ${JSON.stringify({ first_name, last_name, bio, country, mainCategory, subCategory })}`,
5869
+ background: 'transparent',
5870
+ });
5871
+ imageBase64 = ai_avatar_response.data[0].b64_json;
5872
+ avatar_source = 'ai profile';
5873
+ };
5874
+ const create_fictional_avatar = async function () {
5875
+ const { first_name, last_name, bio, country, mainCategory, subCategory } = account_obj.account_info;
5876
+
5877
+ const prompt = `analyze the the person info : ${JSON.stringify({ first_name, last_name, bio, country, mainCategory, subCategory })}
5878
+ 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`;
5879
+
5880
+ imageBase64 = await create_image(prompt, undefined, undefined, 1);
5881
+ avatar_source = 'fictional';
5882
+ };
5883
+ // personal account
5884
+ const is_real_person_in_picture = await detect_real_person_in_image(imageBase64);
5885
+ if (is_real_person_in_picture) {
5886
+ if (images[0].is_transparent) {
5887
+ // keep the original
5888
+ imageBase64 = Buffer.from(await images[0].image_blob.arrayBuffer()).toString('base64');
5889
+ avatar_source = 'authentic profile';
5890
+ } else {
5891
+ // create avatar from profile picture
5892
+ await create_avatar_from_pic();
5893
+ }
5894
+ } else {
5895
+ // create avatar from profile picture
5896
+ await create_fictional_avatar();
5897
+ }
5898
+ }
5899
+
5900
+ console.log('Normalizing final avatar to 1024 with transparent padding...');
5901
+ imageBase64 = await normalizeBase64To1024(imageBase64);
5902
+
5903
+ const outputBuffer = Buffer.from(imageBase64, 'base64');
5904
+ const outputPath = tempOutputPath;
5905
+ await fs.promises.writeFile(outputPath, outputBuffer);
5906
+
5907
+ console.log('Success! Saved →', outputPath);
5908
+
5909
+ const ext = 'png';
5910
+ const originalname = `avatar_${account_obj?.account_info?.email || account_obj._id}.${ext}`;
5911
+
5912
+ const file_obj = {
5913
+ originalname,
5914
+ path: outputPath,
5915
+ mimetype: `image/${ext}`,
5916
+ };
5917
+
5918
+ const drive_ret = await drive_module.upload_drive_file_user({ uid, path: '/Profile Images' }, job_id, headers, file_obj);
5919
+
5920
+ if (drive_ret.code > 0) {
5921
+ let db_doc = await db_module.get_couch_doc_native('xuda_accounts', account_obj._id);
5922
+ db_doc.account_info.profile_avatar = drive_ret.data.file_url;
5923
+ db_doc.account_info.profile_avatar_obj == drive_ret.data;
5924
+ db_doc.account_info.avatar_source = avatar_source;
5925
+ db_doc.image_request_ts = Date.now();
5926
+ const save_ret = await db_module.save_couch_doc_native('xuda_accounts', db_doc);
5927
+ console.log(save_ret);
5928
+ await update_profile_avatar_status(uid, 3);
5929
+ return save_ret;
5930
+ // account_module.update_contacts_with_account_changes(db_doc._id, ['profile_avatar']);
5931
+ } else {
5932
+ throw new Error(drive_ret.data);
5933
+ }
5934
+ } catch (error) {
5935
+ await update_profile_avatar_status(uid, 1);
5936
+ console.error('Error:', error.message);
5937
+ if (error.response?.data) {
5938
+ console.error('OpenAI API response:', error.response.data);
5939
+ }
5940
+ }
5941
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.3612",
3
+ "version": "1.1.3613",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",