almeira-web/dripzone-app/src/lib/gemini-parser.ts

888 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { GoogleGenAI } from '@google/genai';
import { ExtractedProductData } from '@/types';
import { calculateRetailPrice } from './pricing-engine';
import { getProductSocialProof } from './social-proof';
import { generateAttractiveProductCopy } from './attractive-copy';
import { sanitizePublicationTitle } from './sanitizer';
/**
* Catálogo maestro de marcas y sus firmas de modelos y palabras clave exclusivas
*/
export interface BrandSignature {
brand: string;
aliases: RegExp[];
models: { name: string; pattern: RegExp }[];
}
export const BRAND_SIGNATURES: BrandSignature[] = [
{
brand: 'Adidas',
aliases: [/\badidas\b/i, /\btrefoil\b/i, /\btr[eé]bol\b/i, /3\s*rayas|tres\s*rayas/i],
models: [
{ name: 'Adidas Samba OG', pattern: /\bsamba\b/i },
{ name: 'Adidas Gazelle Indoor', pattern: /\bgazelle\b/i },
{ name: 'Adidas Campus 00s', pattern: /\bcampus\b/i },
{ name: 'Adidas Handball Spezial', pattern: /\bspezial\b/i },
{ name: 'Adidas Forum Low', pattern: /\bforum\b/i },
{ name: 'Adidas Bad Bunny', pattern: /\bbad\s*bunny\b/i },
{ name: 'Adidas Stan Smith', pattern: /\bstan\s*smith\b/i },
{ name: 'Adidas Superstar', pattern: /\bsuperstar\b/i },
{ name: 'Adidas SL 72', pattern: /\bsl\s*72\b|\bsl72\b/i },
{ name: 'Adidas Response CL', pattern: /\bresponse\s*cl\b/i },
{ name: 'Adidas Yeezy Boost 350', pattern: /\byeezy\b|\b350\s*v2\b|\bfoam\s*runner\b/i },
{ name: 'Adidas Astir', pattern: /\bastir\b/i },
{ name: 'Adidas Ozelia', pattern: /\bozelia\b/i },
{ name: 'Adidas Adizero', pattern: /\badizero\b/i },
{ name: 'Adidas Country OG', pattern: /\bcountry\s*og\b/i },
{ name: 'Adidas NMD R1', pattern: /\bnmd\b/i },
{ name: 'Adidas Ultraboost', pattern: /\bultra\s*boost\b|\bultraboost\b/i },
],
},
{
brand: 'Jordan',
aliases: [/\bjordan\b/i, /\bjumpman\b/i, /\bair\s*jordan\b/i, /\baj\d+\b/i],
models: [
{ name: 'Jordan 4 Retro', pattern: /\b(?:retro\s*4|aj4|jordan\s*4|retro\s*iv)\b/i },
{ name: 'Jordan 1 Retro High OG', pattern: /\b(?:retro\s*1|aj1|jordan\s*1|retro\s*i)\b/i },
{ name: 'Jordan 3 Retro', pattern: /\b(?:retro\s*3|aj3|jordan\s*3|retro\s*iii)\b/i },
{ name: 'Jordan 11 Retro', pattern: /\b(?:retro\s*11|aj11|jordan\s*11|retro\s*xi)\b/i },
{ name: 'Jordan 1 Low Travis Scott', pattern: /\btravis\s*scott\b|\bcactus\s*jack\b/i },
{ name: 'Jordan Spizike Low', pattern: /\bspizike\b/i },
{ name: 'Jordan 5 Retro', pattern: /\b(?:retro\s*5|aj5|jordan\s*5)\b/i },
{ name: 'Jordan 6 Retro', pattern: /\b(?:retro\s*6|aj6|jordan\s*6)\b/i },
],
},
{
brand: 'New Balance',
aliases: [/\bnew\s*balance\b/i, /\bnb\b/i],
models: [
{ name: 'New Balance 9060', pattern: /\b9060\b/i },
{ name: 'New Balance 550', pattern: /\b550\b/i },
{ name: 'New Balance 530', pattern: /\b530\b/i },
{ name: 'New Balance 1906R', pattern: /\b1906\b|\b1906r\b|\b1906d\b/i },
{ name: 'New Balance 2002R', pattern: /\b2002\b|\b2002r\b|\bprotection\s*pack\b/i },
{ name: 'New Balance 327', pattern: /\b327\b/i },
{ name: 'New Balance 574', pattern: /\b574\b/i },
{ name: 'New Balance 990v6', pattern: /\b990\b|\b990v\d\b/i },
{ name: 'New Balance 991', pattern: /\b991\b/i },
{ name: 'New Balance 992', pattern: /\b992\b/i },
{ name: 'New Balance 993', pattern: /\b993\b/i },
{ name: 'New Balance 610', pattern: /\b610\b/i },
{ name: 'New Balance CT302', pattern: /\bct302\b/i },
],
},
{
brand: 'On Running',
aliases: [/\bon\s*running\b/i, /\bon\s*cloud\b/i, /\bcloud\b/i, /\bclould\b/i],
models: [
{ name: 'On Cloudmonster', pattern: /\bcloudmonster\b|\bcloud\s*monster\b/i },
{ name: 'On Cloudtilt Loewe', pattern: /\bcloudtilt\b|\bcloud\s*tilt\b|\bloewe\b/i },
{ name: 'On Cloud 5', pattern: /\bcloud\s*5\b/i },
{ name: 'On Cloud X3', pattern: /\bcloud\s*x\b|\bcloud\s*x3\b/i },
{ name: 'On Cloudsurfer', pattern: /\bcloudsurfer\b|\bcloud\s*surfer\b/i },
{ name: 'On Cloudstratus', pattern: /\bcloudstratus\b|\bcloud\s*stratus\b/i },
{ name: 'On Cloudrunner', pattern: /\bcloudrunner\b|\bcloud\s*runner\b/i },
{ name: 'On The Roger Centre Court', pattern: /\broger\b|\bthe\s*roger\b/i },
],
},
{
brand: 'Puma',
aliases: [/\bpuma\b/i],
models: [
{ name: 'Puma Palermo Special', pattern: /\bpalermo\b/i },
{ name: 'Puma Speedcat OG', pattern: /\bspeedcat\b|\bspeed\s*cat\b/i },
{ name: 'Puma Suede Classic', pattern: /\bsuede\b/i },
{ name: 'Puma Caven 2.0', pattern: /\bcaven\b/i },
{ name: 'Puma Slipstream', pattern: /\bslipstream\b/i },
{ name: 'Puma Mayze Wedge', pattern: /\bmayze\b/i },
{ name: 'Puma Easy Rider', pattern: /\beasy\s*rider\b/i },
{ name: 'Puma RS-X Efekt', pattern: /\brs-x\b|\brsx\b/i },
{ name: 'Puma CA Pro Classic', pattern: /\bca\s*pro\b/i },
],
},
{
brand: 'Asics',
aliases: [/\basics\b/i],
models: [
{ name: 'Asics Gel-Kayano 14', pattern: /\bgel[\s-]*kayano\b|\bkayano\b/i },
{ name: 'Asics Gel-NYC', pattern: /\bgel[\s-]*nyc\b|\bnyc\b/i },
{ name: 'Asics Gel-1130', pattern: /\bgel[\s-]*1130\b|\b1130\b/i },
{ name: 'Asics GT-2160', pattern: /\bgt[\s-]*2160\b|\b2160\b/i },
{ name: 'Asics Gel-Quantum 360', pattern: /\bgel[\s-]*quantum\b|\bquantum\b/i },
{ name: 'Asics Gel-Nimbus 9', pattern: /\bgel[\s-]*nimbus\b|\bnimbus\b/i },
{ name: 'Asics Gel-Lyte III', pattern: /\bgel[\s-]*lyte\b/i },
{ name: 'Asics Novablast 4', pattern: /\bnovablast\b/i },
],
},
{
brand: 'Vans',
aliases: [/\bvans\b/i, /\boff\s*the\s*wall\b/i],
models: [
{ name: 'Vans Knu Skool Chunky', pattern: /\bknu\s*skool\b|\bknuskool\b|\bknu\b/i },
{ name: 'Vans Old Skool Classic', pattern: /\bold\s*skool\b|\boldskool\b/i },
{ name: 'Vans Sk8-Hi Reissue', pattern: /\bsk8\b|\bsk8-hi\b|\bsk8\s*hi\b/i },
{ name: 'Vans Authentic Pro', pattern: /\bauthentic\b/i },
{ name: 'Vans Era Classic', pattern: /\bera\b/i },
{ name: 'Vans Slip-On Checkerboard', pattern: /\bslip[\s-]*on\b|\bcheckerboard\b/i },
{ name: 'Vans UltraRange EXO', pattern: /\bultrarange\b|\bultra\s*range\b/i },
],
},
{
brand: 'Converse',
aliases: [/\bconverse\b/i, /\ball\s*star\b/i],
models: [
{ name: 'Converse Chuck 70 Vintage', pattern: /\bchuck\s*70\b|\bchuck70\b/i },
{ name: 'Converse Chuck Taylor All Star', pattern: /\bchuck\s*taylor\b|\ball\s*star\b/i },
{ name: 'Converse Run Star Hike', pattern: /\brun\s*star\s*hike\b|\brun\s*star\b/i },
{ name: 'Converse Run Star Motion', pattern: /\brun\s*star\s*motion\b/i },
{ name: 'Converse Weapon Ox', pattern: /\bweapon\b/i },
{ name: 'Converse One Star Pro', pattern: /\bone\s*star\b/i },
],
},
{
brand: 'Salomon',
aliases: [/\bsalomon\b/i],
models: [
{ name: 'Salomon XT-6 Gore-Tex', pattern: /\bxt-6\b|\bxt6\b/i },
{ name: 'Salomon XT-4 OG', pattern: /\bxt-4\b|\bxt4\b/i },
{ name: 'Salomon Speedcross 6', pattern: /\bspeedcross\b/i },
{ name: 'Salomon ACS Pro Advanced', pattern: /\bacs\s*pro\b/i },
{ name: 'Salomon XA Pro 3D', pattern: /\bxa\s*pro\b/i },
],
},
{
brand: 'Hoka',
aliases: [/\bhoka\b/i, /\bhoka\s*one\s*one\b/i],
models: [
{ name: 'Hoka Clifton 9', pattern: /\bclifton\b/i },
{ name: 'Hoka Bondi 8', pattern: /\bbondi\b/i },
{ name: 'Hoka Tor Ultra Low', pattern: /\btor\s*ultra\b/i },
{ name: 'Hoka Speedgoat 5', pattern: /\bspeedgoat\b/i },
{ name: 'Hoka Hopara Sandal', pattern: /\bhopara\b/i },
{ name: 'Hoka Transport', pattern: /\btransport\b/i },
],
},
{
brand: 'Balenciaga',
aliases: [/\bbalenciaga\b/i],
models: [
{ name: 'Balenciaga Track 3.0', pattern: /\btrack\b|\btrack\s*2\b|\btrack\s*3\b/i },
{ name: 'Balenciaga Triple S Clear Sole', pattern: /\btriple\s*s\b|\btriples\b/i },
{ name: 'Balenciaga 3XL Sneaker', pattern: /\b3xl\b/i },
{ name: 'Balenciaga Defender Chunky', pattern: /\bdefender\b/i },
{ name: 'Balenciaga Speed Trainer Knit', pattern: /\bspeed\s*trainer\b/i },
],
},
{
brand: 'Alexander McQueen',
aliases: [/\bmcq|mcqueen|alexander\s*mcqueen\b/i],
models: [
{ name: 'Alexander McQueen Oversized', pattern: /\boversized\b|\bchunky\s*sole\b/i },
{ name: 'Alexander McQueen Court Sneaker', pattern: /\bcourt\b/i },
{ name: 'Alexander McQueen Tread Slick', pattern: /\btread\s*slick\b/i },
],
},
{
brand: 'Golden Goose',
aliases: [/\bgolden\s*goose\b/i, /\bggdb\b/i],
models: [
{ name: 'Golden Goose Super-Star Classic', pattern: /\bsuper[\s-]*star\b|\bball[\s-]*star\b/i },
{ name: 'Golden Goose Mid Star', pattern: /\bmid\s*star\b/i },
{ name: 'Golden Goose Purestar', pattern: /\bpurestar\b/i },
],
},
{
brand: 'Crocs',
aliases: [/\bcrocs\b/i],
models: [
{ name: 'Crocs Pollex Clog Salehe Bembury', pattern: /\bpollex\b|\bsalehe\b/i },
{ name: 'Crocs Echo Clog', pattern: /\becho\s*clog\b|\becho\b/i },
{ name: 'Crocs Mega Crush Clog', pattern: /\bmega\s*crush\b|\bcrush\b/i },
{ name: 'Crocs Classic Clog', pattern: /\bclassic\s*clog\b|\bclog\b/i },
],
},
{
brand: 'Under Armour',
aliases: [/\bunder\s*armour\b/i, /\bua\b/i],
models: [
{ name: 'Under Armour Curry Flow', pattern: /\bcurry\b/i },
{ name: 'Under Armour Project Rock', pattern: /\bproject\s*rock\b/i },
{ name: 'Under Armour HOVR Phantom', pattern: /\bhovr\b|\bphantom\b/i },
],
},
{
brand: 'Lacoste',
aliases: [/\blacoste\b/i, /\bcocodrilo\b/i],
models: [
{ name: 'Lacoste Carnaby Pro', pattern: /\bcarnaby\b/i },
{ name: 'Lacoste Europa Classic', pattern: /\beuropa\b/i },
{ name: 'Lacoste T-Clip Urban', pattern: /\bt[\s-]*clip\b/i },
{ name: 'Lacoste L001 Sneaker', pattern: /\bl001\b/i },
],
},
{
brand: 'Timberland',
aliases: [/\btimberland\b/i, /\btimbs\b/i],
models: [
{ name: 'Timberland 6-Inch Premium Boot', pattern: /\b6[\s-]*inch\b|\byellow\s*boot\b|\bbotas?\b/i },
{ name: 'Timberland Euro Trekker', pattern: /\beuro\s*trekker\b/i },
],
},
{
brand: 'Diesel',
aliases: [/\bdiesel\b/i],
models: [
{ name: 'Diesel 1DR Sneaker', pattern: /\b1dr\b/i },
{ name: 'Diesel S-Serendipity', pattern: /\bserendipity\b/i },
{ name: 'Diesel S-Prototype', pattern: /\bprototype\b/i },
],
},
{
brand: 'Oakley',
aliases: [/\boakley\b/i],
models: [
{ name: 'Oakley Chop Saw Mule', pattern: /\bchop\s*saw\b/i },
{ name: 'Oakley Flesh Slip-On', pattern: /\bflesh\b/i },
{ name: 'Oakley Teeth Sneaker', pattern: /\bteeth\b/i },
],
},
{
brand: 'Fila',
aliases: [/\bfila\b/i],
models: [
{ name: 'Fila Disruptor II Chunky', pattern: /\bdisruptor\b/i },
{ name: 'Fila Ray Tracer', pattern: /\bray\s*tracer\b/i },
],
},
{
brand: 'Reebok',
aliases: [/\breebok\b/i],
models: [
{ name: 'Reebok Club C 85 Vintage', pattern: /\bclub\s*c\b|\bclubc\b/i },
{ name: 'Reebok Classic Leather', pattern: /\bclassic\s*leather\b/i },
{ name: 'Reebok BB 4000 II', pattern: /\bbb\s*4000\b/i },
],
},
{
brand: 'Bape',
aliases: [/\bbape\b/i, /\ba\s*bathing\s*ape\b/i],
models: [
{ name: 'Bape Sta Low Classic', pattern: /\bbape\s*sta\b|\bbapesta\b/i },
{ name: 'Bape Sk8 Sta', pattern: /\bsk8\s*sta\b/i },
],
},
{
brand: 'Louis Vuitton',
aliases: [/\blouis\s*vuitton\b/i, /\blv\b/i],
models: [
{ name: 'Louis Vuitton LV Skate Sneaker', pattern: /\blv\s*skate\b|\bskate\b/i },
{ name: 'Louis Vuitton LV Trainer', pattern: /\blv\s*trainer\b|\btrainer\b/i },
],
},
{
brand: 'Dior',
aliases: [/\bdior\b/i],
models: [
{ name: 'Dior B22 Chunky Runner', pattern: /\bb22\b/i },
{ name: 'Dior B30 Technical Mesh', pattern: /\bb30\b/i },
{ name: 'Dior B23 High Top Oblique', pattern: /\bb23\b|\boblique\b/i },
],
},
{
brand: 'Gucci',
aliases: [/\bgucci\b/i],
models: [
{ name: 'Gucci Rhyton Leather Chunky', pattern: /\brhyton\b/i },
{ name: 'Gucci Ace Embroidered Sneaker', pattern: /\bace\b/i },
{ name: 'Gucci Screener Vintage', pattern: /\bscreener\b/i },
],
},
{
brand: 'Prada',
aliases: [/\bprada\b/i],
models: [
{ name: "Prada America's Cup", pattern: /\bamericas?\s*cup\b|\bcup\b/i },
{ name: 'Prada Cloudbust Thunder', pattern: /\bcloudbust\b/i },
{ name: 'Prada Downtown Leather', pattern: /\bdowntown\b/i },
],
},
{
brand: 'Autry',
aliases: [/\bautry\b/i],
models: [{ name: 'Autry Medalist Low Vintage', pattern: /\bmedalist\b|\bclc\b/i }],
},
{
brand: 'Veja',
aliases: [/\bveja\b/i],
models: [
{ name: 'Veja Campo Extra White', pattern: /\bcampo\b/i },
{ name: 'Veja V-10 Leather', pattern: /\bv-10\b|\bv10\b/i },
{ name: 'Veja V-12 B-Mesh', pattern: /\bv-12\b|\bv12\b/i },
],
},
{
brand: 'Nike',
aliases: [/\bnike\b/i, /\bswoosh\b/i],
models: [
{ name: 'Nike Air Force 1 07', pattern: /\bair\s*force\b|\baf1\b|\baf-1\b|\bforce\s*1\b/i },
{ name: 'Nike Dunk Low Retro Panda', pattern: /\bdunk\b|\bpanda\b|\bsb\s*dunk\b/i },
{ name: 'Nike Air Max 90', pattern: /\bair\s*max\s*90\b|\bam90\b|\bmax\s*90\b/i },
{ name: 'Nike Air Max 97', pattern: /\bair\s*max\s*97\b|\bam97\b|\bmax\s*97\b/i },
{ name: 'Nike Air Max 270', pattern: /\bair\s*max\s*270\b|\bmax\s*270\b/i },
{ name: 'Nike Air Max Plus TN', pattern: /\btn\b|\bair\s*max\s*plus\b|\btuned\b/i },
{ name: 'Nike Vomero 5', pattern: /\bvomero\b|\bvomero\s*5\b/i },
{ name: 'Nike P-6000 Retro', pattern: /\bp-6000\b|\bp6000\b/i },
{ name: 'Nike Shox TL', pattern: /\bshox\b|\bshox\s*tl\b|\br4\b/i },
{ name: 'Nike Cortez Classic', pattern: /\bcortez\b/i },
{ name: 'Nike Blazer Mid 77', pattern: /\bblazer\b/i },
{ name: 'Nike TC 7900 Chunky', pattern: /\btc\s*7900\b|\btc7900\b/i },
{ name: 'Nike Gamma Force', pattern: /\bgamma\s*force\b/i },
{ name: 'Nike Air VaporMax Plus', pattern: /\bvapormax\b|\bvapor\s*max\b/i },
{ name: 'Nike Pegasus 40', pattern: /\bpegasus\b/i },
{ name: 'Nike Killshot 2', pattern: /\bkillshot\b/i },
],
},
];
/**
* Detector determinístico y de alta precisión de marca y modelo desde texto
*/
export function detectBrandFromText(text: string): {
brand: string;
modelName?: string;
confidence: number;
} {
const normalized = text.toLowerCase();
// 1. Prioridad: Buscar coincidencia de modelo exacto
for (const item of BRAND_SIGNATURES) {
for (const model of item.models) {
if (model.pattern.test(normalized)) {
return {
brand: item.brand,
modelName: model.name,
confidence: 0.96,
};
}
}
}
// 2. Buscar marca por alias explícito
for (const item of BRAND_SIGNATURES) {
for (const alias of item.aliases) {
if (alias.test(normalized)) {
return {
brand: item.brand,
confidence: 0.88,
};
}
}
}
// 3. Si no hay coincidencia, NO asignar Nike arbitrariamente
return {
brand: 'Urbano / Variado',
confidence: 0.4,
};
}
/**
* Extractor robusto de precios mayoristas en COP desde mensajes de WhatsApp
*/
export function extractSupplierPrice(text: string): number {
const explicitPattern =
/(?:💸|💰|💵|💲|precio|valor|costo|mayorista|mayor|mayoristas|\$)+[\s:=]*\$?\s*(\d{2,3}(?:\.\d{3})*|\d{2,3}\s*k|\d{2,3}\s*mil|\d{5,6})/i;
const explicitMatch = text.match(explicitPattern);
if (explicitMatch) {
const raw = explicitMatch[1].toLowerCase().replace(/\s+/g, '').replace(/\./g, '');
if (raw.endsWith('k') || raw.endsWith('mil')) {
const num = parseInt(raw.replace('mil', '').replace('k', ''), 10);
if (!isNaN(num) && num > 0) return num < 1000 ? num * 1000 : num;
}
const num = parseInt(raw, 10);
if (!isNaN(num)) {
if (num >= 40 && num <= 350) return num * 1000;
if (num >= 40000 && num <= 500000) return num;
}
}
const dotThousands = text.match(/\b([4-9]\d|1\d{2}|2\d{2})\.(000|500)\b/);
if (dotThousands) {
const num = parseInt(dotThousands[0].replace(/\./g, ''), 10);
if (!isNaN(num) && num >= 40000 && num <= 500000) return num;
}
const kMatch = text.match(/\b([4-9]\d|1\d{2}|2\d{2})\s*(?:k|mil)\b/i);
if (kMatch) {
const num = parseInt(kMatch[1], 10);
if (!isNaN(num)) return num * 1000;
}
const rawNumbers = text.match(/\b(4[5-9]\d{3}|[5-9]\d{4}|1\d{5}|2\d{5})\b/g);
if (rawNumbers && rawNumbers.length > 0) {
const num = parseInt(rawNumbers[0], 10);
if (!isNaN(num)) return num;
}
return 75000;
}
/**
* Detección y Categorización Inteligente Multi-categoría
* Identifica con precisión: Zapatillas, Chanclas, Bolsos, Maletines, Carrieles, Gorras, Ropa, Accesorios
*/
export interface CategoryDetection {
category: string;
categoryKey: 'zapatillas' | 'chanclas' | 'bolsos' | 'maletines' | 'carrieles' | 'gorras' | 'ropa' | 'accesorios';
icon: string;
confidence: number;
}
export const OFFICIAL_CATEGORIES: { id: CategoryDetection['categoryKey']; label: string; icon: string }[] = [
{ id: 'zapatillas', label: 'Zapatillas', icon: '👟' },
{ id: 'chanclas', label: 'Chanclas / Sandalias', icon: '🩴' },
{ id: 'bolsos', label: 'Bolsos / Carteras', icon: '👜' },
{ id: 'maletines', label: 'Maletines / Morrales', icon: '🎒' },
{ id: 'carrieles', label: 'Carrieles / Bandoleras', icon: '👝' },
{ id: 'gorras', label: 'Gorras / Caps', icon: '🧢' },
{ id: 'ropa', label: 'Ropa / Streetwear', icon: '👕' },
{ id: 'accesorios', label: 'Accesorios', icon: '🕶️' },
];
export function detectCategory(text: string, title?: string): CategoryDetection {
const full = `${title || ''} ${text || ''}`.toLowerCase();
// 1. Chanclas / Sandalias / Slides / Crocs
if (
/\b(chancl[as]|sandali[as]|slide[s]?|croc[s]?|pantufl[as]|ojot[as]|adilette|chola[s]|chinela[s]|huarache[s]|alparagatas?|flip[\s-]*flops?|chancleta[s]?|chancletero)\b/i.test(
full
)
) {
return { category: 'Chanclas', categoryKey: 'chanclas', icon: '🩴', confidence: 0.98 };
}
// 2. Carrieles / Bandoleras / Riñoneras / Crossbody / Pecheras / Canguros
if (
/\b(carriel(es)?|bandolera[s]?|ri[ñn]onera[s]?|pechera[s]?|crossbody|waist[\s-]*bag|fanny[\s-]*pack|chest[\s-]*bag|canguro[s]?|mariconera[s]?|shoulder[\s-]*bag|koala[s]?|manbag)\b/i.test(
full
)
) {
return { category: 'Carrieles', categoryKey: 'carrieles', icon: '👝', confidence: 0.98 };
}
// 3. Maletines / Morrales / Mochilas / Backpacks / Tulas
if (
/\b(malet[ií]n(es)?|morral(es)?|mochila[s]?|backpack[s]?|tula[s]?|maleta[s]?|bolso\s*deportivo|duffle|gym[\s-]*bag|equipaje)\b/i.test(
full
)
) {
return { category: 'Maletines', categoryKey: 'maletines', icon: '🎒', confidence: 0.98 };
}
// 4. Bolsos / Carteras / Tote
if (
/\b(bolso[s]?|cartera[s]?|tote([\s-]*bag)?|clutch|satchel|baguette|monedero[s]?|shopping[\s-]*bag|handbag[s]?|bolsa[s]?\s*de\s*mano)\b/i.test(
full
)
) {
return { category: 'Bolsos', categoryKey: 'bolsos', icon: '👜', confidence: 0.97 };
}
// 5. Gorras / Sombreros / Accesorios para la cabeza
if (
/\b(gorra[s]?|gorro[s]?|cap[s]?|snapback|bucket[\s-]*hat|beanie|sombrero[s]?|visera[s]?)\b/i.test(
full
)
) {
return { category: 'Gorras', categoryKey: 'gorras', icon: '🧢', confidence: 0.98 };
}
// 6. Ropa / Textil
if (
/\b(camiset[as]|camibuso[s]?|buzo[s]?|hoodie[s]?|chaqueta[s]?|sudadera[s]?|pantalon(es)?|short[s]?|jogger[s]?|polo[s]?|conjunto[s]?|bermuda[s]?|overol[es]?)\b/i.test(
full
)
) {
return { category: 'Ropa', categoryKey: 'ropa', icon: '👕', confidence: 0.97 };
}
// 7. Accesorios varios
if (
/\b(billetera[s]?|cintur[oó]n(es)?|correa[s]?|reloj(es)?|gafa[s]?|medias?|calcet[ií]n(es)?)\b/i.test(
full
)
) {
return { category: 'Accesorios', categoryKey: 'accesorios', icon: '🕶️', confidence: 0.95 };
}
// 8. Por defecto: Zapatillas
return { category: 'Zapatillas', categoryKey: 'zapatillas', icon: '👟', confidence: 0.92 };
}
/**
* Extractor de tallas con normalización Unicode y distinción por categoría
*/
export function extractSizes(rawText: string, category?: string): string[] {
const normCat = (category || '').toLowerCase();
const isAccessory =
normCat.includes('bolso') ||
normCat.includes('malet') ||
normCat.includes('carriel') ||
normCat.includes('gorra') ||
normCat.includes('accesorio');
// Si es un carriel, bolso, maletín o gorra, buscar si dice Talla Única o estándar
if (isAccessory) {
if (/\b(talla\s*[\u00fa]nica|talla\s*unica|unitalla|ajustable|est[aá]ndar|standard)\b/i.test(rawText)) {
return ['Talla Única'];
}
}
// Si es Ropa, buscar tallas S, M, L, XL, XXL
if (normCat.includes('ropa')) {
const textSizes = rawText.match(/\b(xs|s|m|l|xl|xxl|2xl|3xl)\b/gi);
if (textSizes && textSizes.length > 0) {
return Array.from(new Set(textSizes.map((s) => s.toUpperCase())));
}
}
const normalized = rawText
.normalize('NFKD')
.replace(/[ᴛ]/g, 't')
.replace(/[ᴀ]/g, 'a')
.replace(/[ʟ]/g, 'l')
.replace(/[s]/g, 's')
.replace(/[ᴅ]/g, 'd')
.replace(/[ɪ]/g, 'i')
.replace(/[ᴘ]/g, 'p')
.replace(/[ᴏ]/g, 'o')
.replace(/[ɴ]/g, 'n')
.replace(/[ʙ]/g, 'b')
.replace(/[ᴇ]/g, 'e');
const sizes: string[] = [];
const rangeMatch = normalized.match(
/(?:tallas?|eur|talla|disponibles?)?[^\d\n]*\b(3[4-9]|4[0-6])\s*(?:a|al|-)\s*(3[4-9]|4[0-6])\b/i
);
if (rangeMatch) {
const start = parseInt(rangeMatch[1], 10);
const end = parseInt(rangeMatch[2], 10);
if (start <= end && start >= 34 && end <= 46) {
for (let s = start; s <= end; s++) sizes.push(s.toString());
return sizes;
}
}
const tallaSection = normalized.match(/(?:tallas?|eur|disponibles?|talla)[^\n]*/i);
const textToScan = tallaSection ? tallaSection[0] : normalized;
const matches = textToScan.match(/(?:^|[^\d])(3[4-9]|4[0-6])(?=[^\d]|$)/g);
if (matches) {
matches.forEach((m) => {
const num = m.replace(/[^\d]/g, '');
if (num && !sizes.includes(num)) {
sizes.push(num);
}
});
}
if (sizes.length === 0) {
if (isAccessory) {
return ['Talla Única'];
}
if (normCat.includes('ropa')) {
return ['S', 'M', 'L', 'XL'];
}
return ['37', '38', '39', '40', '41', '42', '43'];
}
return sizes.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
}
/**
* Limpia y formatea el nombre del producto garantizando que NUNCA se incluyan nombres
* de proveedores, bodegas, canales o grupos de WhatsApp.
*/
export function cleanProductName(
text: string,
detectedBrand: string,
extraForbidden: (string | undefined | null)[] = []
): string {
const lines = text.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
for (const line of lines) {
const lower = line.toLowerCase();
if (
lower.includes('difusion jovic') ||
lower.includes('jovic') ||
lower.includes('cc mío') ||
lower.includes('cc mio') ||
lower.includes('bodega central') ||
lower.includes('bodega mayorista') ||
lower.includes('proveedor') ||
lower.includes('canal mayorista') ||
lower.includes('canal de difusión') ||
lower.includes('canal de difusion') ||
lower.includes('grupo mayorista') ||
lower.includes('grupo whatsapp') ||
lower.includes('team emporio') ||
lower.includes('notificaciones') ||
lower.includes('medios y prensa') ||
lower.includes('calzado mayorista') ||
lower.includes('san andresito')
) {
continue;
}
const clean = line
.replace(/🔥|🔴|‼️|✨|🌸|👑|💎|🛍️|🌟|💸|💰|💵|💲|\*/g, '')
.replace(/dama\s*y\s*caballero|caballero|dama|unisex|recarga|nueva\s*mercanc[ií]a|[uú]ltima\s*unidad/gi, '')
.replace(/c[oó]digo\s*:\s*[\d-]+/gi, '')
.replace(/\b\d{2,3}[\.,]\d{3}\b|\b\d{2,3}k\b|\$\s*\d+/gi, '')
.replace(/tallas?:.*|precio:.*|eur\s*[\d\/-]+/gi, '')
.replace(/\s+/g, ' ')
.trim();
const sanitized = sanitizePublicationTitle(clean, extraForbidden, detectedBrand);
if (sanitized.length >= 3 && !/^\d+$/.test(sanitized) && !/^\d+[\.,]\d+$/.test(sanitized)) {
return sanitized;
}
}
const catDetect = detectCategory(text);
let defaultGeneric = `${detectedBrand} Urban Edition`;
if (detectedBrand === 'Urbano / Variado') {
if (catDetect.category === 'Chanclas') defaultGeneric = 'Sandalia / Slide Confort';
else if (catDetect.category === 'Carrieles') defaultGeneric = 'Carriel / Bandolera Urbana';
else if (catDetect.category === 'Maletines') defaultGeneric = 'Morral / Maletín Deportivo';
else if (catDetect.category === 'Bolsos') defaultGeneric = 'Bolso Elegante Casual';
else if (catDetect.category === 'Gorras') defaultGeneric = 'Gorra Urbana Clásica';
else if (catDetect.category === 'Ropa') defaultGeneric = 'Prenda Streetwear Urbana';
else if (catDetect.category === 'Accesorios') defaultGeneric = 'Accesorio Urbano Prémium';
else defaultGeneric = 'Zapatilla Urbana Nacional';
} else {
if (catDetect.category === 'Chanclas') defaultGeneric = `${detectedBrand} Slide Confort`;
else if (catDetect.category === 'Carrieles') defaultGeneric = `${detectedBrand} Carriel / Bandolera`;
else if (catDetect.category === 'Maletines') defaultGeneric = `${detectedBrand} Morral Deportivo`;
else if (catDetect.category === 'Bolsos') defaultGeneric = `${detectedBrand} Bolso Casual`;
else if (catDetect.category === 'Gorras') defaultGeneric = `${detectedBrand} Gorra Clásica`;
else if (catDetect.category === 'Ropa') defaultGeneric = `${detectedBrand} Colección Streetwear`;
else defaultGeneric = `${detectedBrand} Urban Edition`;
}
return sanitizePublicationTitle(defaultGeneric, extraForbidden, detectedBrand);
}
/**
* Helper para extraer base64 y mimeType de una imagen en cualquier formato
*/
function extractBase64Payload(imageInput?: string): { mimeType: string; data: string } | null {
if (!imageInput || typeof imageInput !== 'string') return null;
const dataUriMatch = imageInput.match(/^data:([^;]+);base64,(.+)$/);
if (dataUriMatch) {
return {
mimeType: dataUriMatch[1] || 'image/jpeg',
data: dataUriMatch[2],
};
}
if (imageInput.length > 100 && !imageInput.startsWith('http') && !imageInput.includes(' ')) {
return {
mimeType: 'image/jpeg',
data: imageInput,
};
}
return null;
}
/**
* Parser Inteligente de Mensajes y Visión Multimodal con Gemini y Categorización Automática
* Reconoce zapatillas, chanclas, bolsos, maletines, carrieles, gorras, ropa y accesorios.
*
* REGLA ESTRICTA: NI en el título NI en el texto de la publicación se debe incluir el nombre
* del grupo de WhatsApp ni el proveedor del producto ni el costo.
*/
export async function parseSupplierMessage(
text: string,
imageBase64OrUrl?: string,
extraForbidden: (string | undefined | null)[] = []
): Promise<ExtractedProductData> {
const apiKey = process.env.GEMINI_API_KEY;
const categoryDetection = detectCategory(text);
// Fallback determinístico con catálogo maestro de marcas y categorías
const fallbackExtract = (): ExtractedProductData => {
const price = extractSupplierPrice(text);
const sizes = extractSizes(text, categoryDetection.category);
const brandDetection = detectBrandFromText(text);
const brand = brandDetection.brand;
const rawClean = brandDetection.modelName || cleanProductName(text, brand, extraForbidden);
const cleanName = sanitizePublicationTitle(rawClean, extraForbidden, brand);
const proof = getProductSocialProof({ name: cleanName, brand });
return {
name: cleanName,
brand,
sizes,
supplierPrice: price,
suggestedRetailPrice: calculateRetailPrice(price, 35, 30000),
category: categoryDetection.category,
confidence: brandDetection.confidence,
rating: proof.rating,
reviewsCount: proof.reviewsCount,
salesCount: proof.salesCount,
originBadge: proof.badge,
};
};
if (!apiKey) {
return fallbackExtract();
}
try {
const ai = new GoogleGenAI({ apiKey });
const imagePayload = extractBase64Payload(imageBase64OrUrl);
const forbiddenTermsStr = extraForbidden.filter(Boolean).join(', ');
const forbiddenClause = forbiddenTermsStr
? `Términos específicos PROHIBIDOS a omitir absolutamente: "${forbiddenTermsStr}".`
: '';
const systemPrompt = `Eres un experto mundial en reconocimiento y categorización de calzado, marroquinería y artículos urbanos para e-commerce en Colombia.
Tu misión es identificar con MÁXIMA PRECISIÓN la marca, el modelo exacto y la CATEGORÍA del producto.
CATEGORÍAS OFICIALES OBLIGATORIAS:
Debes clasificar el producto en EXACTAMENTE una de las siguientes categorías en el campo "category":
- "Zapatillas": Zapatillas deportivas, tenis urbanos, sneakers, calzado deportivo o casual.
- "Chanclas": Chanclas, sandalias, slides, crocs, zuecos, pantuflas, ojotas.
- "Bolsos": Bolsos para dama, carteras, tote bags, carteras de mano o de hombro.
- "Maletines": Maletines ejecutivos, mochilas, morrales, backpacks, tulas deportivas.
- "Carrieles": Carrieles tradicionales o modernos, bandoleras, riñoneras, pecheras, canguros, crossbody.
- "Gorras": Gorras, gorros, caps, sombreros.
- "Ropa": Camisetas, buzos, hoodies, chaquetas, sudaderas, shorts, pantalones.
- "Accesorios": Billeteras, cinturones, gafas, relojes.
⚠️ REGLA CRÍTICA DE CERO PROVEEDORES / CERO GRUPOS / CERO COSTOS (ESTRICTO):
- NI EN EL TÍTULO ("name") NI EN NINGÚN TEXTO COMERCIAL DE LA PUBLICACIÓN SE DEBE INCLUIR EL NOMBRE DEL GRUPO DE WHATSAPP, CANAL, PROVEEDOR, BODEGA MAYORISTA, DISTRIBUIDOR, NI EL COSTO DEL PRODUCTO (PRECIO DE COMPRA / MAYORISTA).
- El costo del producto SOLO debe extraerse en el campo numérico interno "supplierPrice" para el cálculo de margen, pero JAMÁS debe figurar en el título ("name") ni en ninguna descripción de la publicación.
- El campo "name" debe ser ÚNICAMENTE el modelo limpio y comercial del producto (ej: "Adidas Samba OG Blanco Negro", "Nike Air Force 1 07 All White", "Yeezy Slide Onyx", "Carriel de Cuero Clásico Jericó", "Morral Deportivo Nike Brasilia", "Bolso Tote Monograma", "Gorra New Era NY Yankees").
- Si el mensaje contiene nombres de grupos o proveedores (${forbiddenClause} o palabras como "Bodega", "Mayorista", "Distribuidora", "Proveedor", "Jovic", "CC Mío", "Team Emporio", etc.), o menciones de costo/precios de bodega, DEBES OMITIRLOS Y ELIMINARLOS POR COMPLETO DEL TÍTULO Y DE TODO TEXTO.
${
imagePayload
? `🔴 INSTRUCCIÓN DE VISIÓN CRÍTICA:
Observa con atención la IMAGEN del producto y detecta los logos visuales de la marca y el tipo de artículo:
- CALZADO / ZAPATILLAS: Zapatillas deportivas, sneakers (Nike Swoosh, Adidas 3 Rayas, Jordan Jumpman, NB, Puma, etc.).
- CHANCLAS / SANDALIAS: Chanclas abiertas, slides de una pieza, zuecos Crocs, Yeezy slides.
- CARRIELES / BANDOLERAS: Bolsos pequeños cruzados al pecho o cintura (crossbody, riñonera, carriel de cuero).
- MALETINES / MORRALES: Mochilas de dos cargaderas para espalda, maletines para portátil o tulas.
- BOLSOS: Bolsos de mano o de hombro para dama o unisex.
- GORRAS: Gorras con visera plana o curva, sombreros.
`
: `Analiza el texto y las palabras clave del mensaje mayorista para identificar la categoría, la marca y el modelo exacto.`
}
Extrae los siguientes campos en formato JSON estricto:
- "brand": La marca ("Adidas", "Nike", "Jordan", "New Balance", "On Running", "Puma", "Asics", "Vans", "Converse", "Crocs", "Tommy Hilfiger", "Lacoste", "Gucci", "Louis Vuitton", "Totto", "Vélez", etc.). Si no puedes identificar ninguna marca con certeza, responde "Urbano / Variado".
- "name": Nombre comercial completo y limpio del modelo. ¡CERO NOMBRES DE GRUPOS O PROVEEDORES!
- "category": Exactamente una de: "Zapatillas" | "Chanclas" | "Bolsos" | "Maletines" | "Carrieles" | "Gorras" | "Ropa" | "Accesorios".
- "colorName": Nombre descriptivo del color o colorway.
- "sizes": Array de tallas disponibles (para calzado números ej: ["37","38"], para accesorios ["Talla Única"], para ropa ["S","M","L"]).
- "supplierPrice": Precio de compra mayorista como número entero en COP. Analiza símbolos como 💸, 💰, $, 75k, 85.000, etc.
Texto del mensaje de WhatsApp:
"""
${text}
"""
Responde ÚNICAMENTE con el objeto JSON sin bloques de código markdown ni texto adicional.`;
const contents: any[] = [];
if (imagePayload) {
contents.push({
inlineData: {
mimeType: imagePayload.mimeType,
data: imagePayload.data,
},
});
}
contents.push(systemPrompt);
// Ejecutar llamada a Gemini 3.5 Flash (con fallback a gemini-3.6-flash si es necesario)
let outputText = '';
try {
const response = await ai.models.generateContent({
model: 'gemini-3.5-flash',
contents,
});
outputText = response.text || '';
} catch (primaryErr: any) {
console.warn('Reintentando con gemini-3.6-flash:', primaryErr?.message);
const fallbackResponse = await ai.models.generateContent({
model: 'gemini-3.6-flash',
contents,
});
outputText = fallbackResponse.text || '';
}
const cleanJson = outputText.replace(/```json|```/gi, '').trim();
const parsed = JSON.parse(cleanJson);
// Validación y categorización unificada
const catDetection = detectCategory(`${parsed.name || ''} ${text}`, parsed.category);
const finalCategory = catDetection.category;
// Validación y saneamiento del precio
const supplierPrice =
typeof parsed.supplierPrice === 'number' && parsed.supplierPrice >= 20000
? parsed.supplierPrice
: extractSupplierPrice(text);
// Validación de tallas dependiente de categoría
const sizes =
Array.isArray(parsed.sizes) && parsed.sizes.length > 0
? parsed.sizes.map(String)
: extractSizes(text, finalCategory);
// Validación de marca: Si la IA no devolvió marca o vino vacía, usamos el detector maestro
let brand = parsed.brand?.trim();
if (!brand || brand.toLowerCase() === 'desconocida' || brand.toLowerCase() === 'generica') {
const fallbackBrand = detectBrandFromText(parsed.name || text);
brand = fallbackBrand.brand;
}
const secondaryCheck = detectBrandFromText(`${parsed.name || ''} ${text}`);
if (secondaryCheck.confidence >= 0.95 && secondaryCheck.brand !== 'Urbano / Variado') {
brand = secondaryCheck.brand;
}
const rawExtractedName =
parsed.name ||
secondaryCheck.modelName ||
cleanProductName(text, brand, extraForbidden);
const cleanName = sanitizePublicationTitle(rawExtractedName, extraForbidden, brand);
const proof = getProductSocialProof({ name: cleanName, brand });
return {
name: cleanName,
brand,
colorName: parsed.colorName,
sizes,
supplierPrice,
suggestedRetailPrice: calculateRetailPrice(supplierPrice, 35, 30000),
category: finalCategory,
confidence: imagePayload ? 0.99 : secondaryCheck.confidence || 0.94,
rating: proof.rating,
reviewsCount: proof.reviewsCount,
salesCount: proof.salesCount,
originBadge: proof.badge,
};
} catch (err) {
console.warn('Error en llamada Gemini, utilizando extractor maestro de alta precisión:', err);
return fallbackExtract();
}
}