@xuda.io/ai_module 1.1.3611 → 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.
- package/index.mjs +250 -17
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -5512,23 +5512,6 @@ export const convert_profile_image_to_avatar = async function (req, job_id, head
|
|
|
5512
5512
|
await create_fictional_avatar();
|
|
5513
5513
|
}
|
|
5514
5514
|
}
|
|
5515
|
-
//////////////
|
|
5516
|
-
// if (images[0].is_transparent && account_obj.account_info.account_type === 'personal') {
|
|
5517
|
-
// imageBase64 = Buffer.from(await images[0].image_blob.arrayBuffer()).toString('base64');
|
|
5518
|
-
// } else {
|
|
5519
|
-
// const { first_name, last_name, bio, business_name, country, mainCategory, subCategory } = account_obj.account_info;
|
|
5520
|
-
// const ai_avatar_response = await client.images.edit({
|
|
5521
|
-
// model: 'chatgpt-image-latest', // 'gpt-image-1',
|
|
5522
|
-
// image: images[0].image_blob, // only one image allowed
|
|
5523
|
-
|
|
5524
|
-
// prompt: `If a real person is detected in the image, 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.
|
|
5525
|
-
// If no person is detected, 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.
|
|
5526
|
-
// Context: ${JSON.stringify({ first_name, last_name, bio, business_name, country, mainCategory, subCategory })}`,
|
|
5527
|
-
// background: 'transparent',
|
|
5528
|
-
// });
|
|
5529
|
-
// imageBase64 = ai_avatar_response.data[0].b64_json;
|
|
5530
|
-
// }
|
|
5531
|
-
///////////////
|
|
5532
5515
|
}
|
|
5533
5516
|
console.log('Normalizing final avatar to 1024 with transparent padding...');
|
|
5534
5517
|
imageBase64 = await normalizeBase64To1024(imageBase64);
|
|
@@ -5706,3 +5689,253 @@ const delete_depended_chats = async function (uid, reference_id) {
|
|
|
5706
5689
|
await delete_ai_chat({ conversation_id: doc._id, uid });
|
|
5707
5690
|
}
|
|
5708
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
|
+
};
|