Spaces:
Sleeping
Sleeping
| import os | |
| import logging | |
| import anthropic | |
| logger = logging.getLogger(__name__) | |
| ANALYZE_IMAGE_PROMPT = """Analyze this image carefully and return ONLY a valid JSON object. No explanation, no markdown. | |
| First determine if this is a FASHION product or a FOOD item. | |
| If FASHION, return: | |
| { | |
| "type": "fashion", | |
| "fashionAttributes": { | |
| "category": "e.g. Jacket, T-shirt, Dress, Jeans, Shoes, Bag, Sunglasses, Watch, Belt, Scarf", | |
| "color": "primary color", | |
| "secondaryColor": "secondary color or null", | |
| "pattern": "e.g. Solid, Striped, Floral, Graphic", | |
| "style": "e.g. Casual, Formal, Streetwear, Sportswear", | |
| "sleeveLength": "e.g. Sleeveless, Short, Long or null", | |
| "neckline": "e.g. Crew, V-neck, Collar or null", | |
| "fit": "e.g. Slim, Regular, Oversized", | |
| "gender": "Men, Women or Unisex", | |
| "season": "e.g. Summer, Winter, All-season", | |
| "fabric": "e.g. Cotton, Denim, Polyester or null" | |
| } | |
| } | |
| If FOOD, return: | |
| { | |
| "type": "food", | |
| "foodAnalysis": { | |
| "dishName": "name of the dish", | |
| "confidence": 0.95, | |
| "ingredients": ["ingredient1", "ingredient2", "..."], | |
| "nutrition": { | |
| "calories": "estimated calories", | |
| "protein": "estimated protein", | |
| "carbs": "estimated carbs", | |
| "fat": "estimated fat" | |
| }, | |
| "recipes": [ | |
| { | |
| "name": "Recipe name", | |
| "difficulty": "Easy/Medium/Hard", | |
| "cookTime": "e.g. 30 mins", | |
| "steps": ["Step 1...", "Step 2...", "Step 3..."] | |
| }, | |
| { | |
| "name": "Recipe name 2", | |
| "difficulty": "Easy/Medium/Hard", | |
| "cookTime": "e.g. 45 mins", | |
| "steps": ["Step 1...", "Step 2...", "Step 3..."] | |
| } | |
| ] | |
| } | |
| } | |
| If NEITHER food nor fashion, return: | |
| { | |
| "type": "unknown" | |
| } | |
| Return ONLY the JSON. No explanation, no markdown backticks.""" | |
| ANALYZE_FASHION_FOR_SEARCH_PROMPT = """Analyze this fashion item image and return ONLY a valid JSON object. You MUST pick values EXACTLY from the allowed lists below. Do NOT invent, paraphrase, or guess values outside these lists. Return null only if you are genuinely unable to determine the value. | |
| { | |
| "graphical_appearance": one of: "All over pattern" | "Application/3D" | "Argyle" | "Chambray" | "Check" | "Colour blocking" | "Contrast" | "Denim" | "Dot" | "Embroidery" | "Front print" | "Glittering/Metallic" | "Jacquard" | "Lace" | "Melange" | "Mesh" | "Metallic" | "Mixed solid/pattern" | "Neps" | "Other pattern" | "Other structure" | "Placement print" | "Sequin" | "Slub" | "Solid" | "Stripe" | "Transparent" | "Treatment" | "Unknown" | null, | |
| "product_group": one of: "Garment Lower body" | "Garment Upper body" | "Accessories" | "Shoes" | "Socks & Tights" | "Nightwear" | "Swimwear" | "Underwear" | null, | |
| "index_group": one of: "Baby/Children" | "Divided" | "Ladieswear" | "Menswear" | "Sport" | null | |
| } | |
| Rules: | |
| - graphical_appearance: the surface pattern or texture. Use "Solid" for plain single-colour items, "Stripe" for stripes, "Denim" for denim texture, "Check" for checked/plaid, etc. | |
| - product_group: use the EXACT value β "Garment Upper body" for tops, shirts, jackets, coats, dresses. "Garment Lower body" for trousers, jeans, shorts, skirts. "Accessories" for bags, belts, watches, jewellery, scarves, hats, sunglasses, gloves, hair accessories, wallets, umbrellas, etc. "Shoes" for any footwear (sneakers, boots, sandals, heels, etc.). "Socks & Tights" for socks, tights, stockings. "Nightwear" for pyjamas, nightgowns. "Swimwear" for swimsuits, bikinis. "Underwear" for underwear, bras. | |
| - index_group: the target demographic. "Menswear" for men's clothing, "Ladieswear" for women's, "Divided" for young/unisex fashion, "Sport" for sportswear, "Baby/Children" for kids. | |
| Return ONLY the JSON. No explanation, no markdown.""" | |
| def get_outfit_ideas_from_description_prompt(product_description: str) -> str: | |
| return f"""You are a fashion stylist. The user has selected this product: "{product_description}". | |
| Suggest a complete outfit that pairs well with it. | |
| STRICT RULES: | |
| - Include clothing items (tops, bottoms, jackets, coats, dresses, knitwear, trousers, shorts, skirts) AND accessories (bags, belts, watches, jewellery, scarves, hats, sunglasses, gloves) AND shoes where appropriate. | |
| - Include at most ONE item per category β e.g. one top, one bottom, one outer layer, one accessory, one shoe. | |
| - Suggest 2-4 items total (excluding the selected product). | |
| - For each item, you MUST assign EXACTLY one value from each of the three allowed lists below. Do NOT invent or paraphrase values. | |
| ALLOWED graphical_appearance values (pick exactly one): | |
| All over pattern | Application/3D | Argyle | Chambray | Check | Colour blocking | Contrast | Denim | Dot | Embroidery | Front print | Glittering/Metallic | Jacquard | Lace | Melange | Mesh | Metallic | Mixed solid/pattern | Neps | Other pattern | Other structure | Placement print | Sequin | Slub | Solid | Stripe | Transparent | Treatment | Unknown | |
| ALLOWED product_group values (pick exactly one): | |
| Garment Lower body | Garment Upper body | Accessories | Shoes | Socks & Tights | Nightwear | Swimwear | Underwear | |
| ALLOWED index_group values (pick exactly one): | |
| Baby/Children | Divided | Ladieswear | Menswear | Sport | |
| Return ONLY a valid JSON object in this exact shape. No explanation, no markdown: | |
| {{ | |
| "detectedItem": "short name of the selected product", | |
| "outfit": {{ | |
| "title": "outfit name (e.g. Smart Casual Day Look)", | |
| "description": "1-2 sentences describing the overall vibe", | |
| "occasion": "one of: Casual, Smart Casual, Formal, Streetwear, Sporty", | |
| "items": [ | |
| {{ | |
| "item": "clothing item name (e.g. Slim-fit Chinos, White T-shirt)", | |
| "color": "recommended color", | |
| "graphical_appearance": "exact value from the allowed list above", | |
| "product_group": "exact value from the allowed list above", | |
| "index_group": "exact value from the allowed list above", | |
| "reason": "why it pairs well with the selected product" | |
| }} | |
| ], | |
| "stylingTips": ["tip 1", "tip 2"] | |
| }} | |
| }} | |
| Return ONLY the JSON.""" | |
| def _detect_mime_type(base64_data: str) -> str: | |
| import base64 as b64 | |
| raw = b64.b64decode(base64_data[:24]) | |
| if len(raw) >= 2 and raw[0] == 0xFF and raw[1] == 0xD8: | |
| return "image/jpeg" | |
| if len(raw) >= 4 and raw[:4] == b'\x89PNG': | |
| return "image/png" | |
| if len(raw) >= 3 and raw[:3] == b'GIF': | |
| return "image/gif" | |
| if len(raw) >= 12 and raw[8:12] == b'WEBP': | |
| return "image/webp" | |
| return "image/jpeg" | |
| class VisionService: | |
| def __init__(self): | |
| api_key = os.environ.get("CLAUDE_API_KEY") | |
| self.client = anthropic.Anthropic(api_key=api_key) | |
| self.model = "claude-sonnet-4-20250514" | |
| logger.info(f"VisionService initialized with model: {self.model}") | |
| def _strip_prefix(self, image_base64: str) -> str: | |
| return image_base64.split(",")[1] if "," in image_base64 else image_base64 | |
| def _parse_json(self, raw: str) -> dict: | |
| import json | |
| cleaned = raw.replace("```json", "").replace("```", "").strip() | |
| return json.loads(cleaned) | |
| def analyze_image(self, image_base64: str, mime_type: str) -> dict: | |
| logger.info("Claude β analyze_image: classifying image (fashion / food / unknown)") | |
| base64_data = self._strip_prefix(image_base64) | |
| detected_mime = _detect_mime_type(base64_data) | |
| response = self.client.messages.create( | |
| model=self.model, | |
| max_tokens=2048, | |
| messages=[{ | |
| "role": "user", | |
| "content": [ | |
| { | |
| "type": "image", | |
| "source": { | |
| "type": "base64", | |
| "media_type": detected_mime, | |
| "data": base64_data, | |
| }, | |
| }, | |
| {"type": "text", "text": ANALYZE_IMAGE_PROMPT}, | |
| ], | |
| }], | |
| ) | |
| raw = response.content[0].text if response.content[0].type == "text" else "{}" | |
| result = {"success": True, **self._parse_json(raw)} | |
| logger.info(f"Claude β analyze_image result type: {result.get('type')}") | |
| if result.get("type") == "food": | |
| logger.info(f"Claude β food detected: {result.get('foodAnalysis', {}).get('dishName')}") | |
| elif result.get("type") == "fashion": | |
| attrs = result.get("fashionAttributes", {}) | |
| logger.info(f"Claude β fashion detected: category={attrs.get('category')}, color={attrs.get('color')}") | |
| return result | |
| def analyze_fashion_for_search(self, image_base64: str, mime_type: str) -> dict: | |
| logger.info("Claude β analyze_fashion_for_search: extracting searchable fashion attributes") | |
| base64_data = self._strip_prefix(image_base64) | |
| detected_mime = _detect_mime_type(base64_data) | |
| response = self.client.messages.create( | |
| model=self.model, | |
| max_tokens=512, | |
| messages=[{ | |
| "role": "user", | |
| "content": [ | |
| { | |
| "type": "image", | |
| "source": { | |
| "type": "base64", | |
| "media_type": detected_mime, | |
| "data": base64_data, | |
| }, | |
| }, | |
| {"type": "text", "text": ANALYZE_FASHION_FOR_SEARCH_PROMPT}, | |
| ], | |
| }], | |
| ) | |
| raw = response.content[0].text if response.content[0].type == "text" else "{}" | |
| result = self._parse_json(raw) | |
| logger.info(f"Claude β fashion attributes: {result}") | |
| return result | |
| def get_outfit_ideas_from_description(self, product_description: str) -> dict: | |
| logger.info(f"Claude β get_outfit_ideas_from_description: \"{product_description}\"") | |
| prompt = get_outfit_ideas_from_description_prompt(product_description) | |
| response = self.client.messages.create( | |
| model=self.model, | |
| max_tokens=1024, | |
| system="You are a fashion stylist. You MUST respond with valid JSON only. No explanations, no preamble, no markdown.", | |
| messages=[ | |
| {"role": "user", "content": prompt}, | |
| {"role": "assistant", "content": "{"}, | |
| ], | |
| ) | |
| raw = response.content[0].text if response.content[0].type == "text" else "{}" | |
| full_raw = "{" + raw | |
| result = {"success": True, **self._parse_json(full_raw)} | |
| outfit = result.get("outfit", {}) | |
| logger.info(f"Claude β outfit: \"{outfit.get('title')}\" with {len(outfit.get('items', []))} item(s)") | |
| return result | |