selinazarzour commited on
Commit
b369881
·
1 Parent(s): 1ef07a0

Deploy AI Virtual Try-On

Browse files

Solved the conflict in README file

README.md CHANGED
@@ -1,3 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Ai Virtual Tryon
3
  emoji: 🌍
 
1
+ # 🤖 AI-Powered Virtual Try-On
2
+
3
+ A portfolio-ready, full-stack AI fashion assistant and virtual try-on system. Combines computer vision, conversational AI, and web automation to deliver style advice, outfit analysis, and shopping recommendations—all in one interactive web app.
4
+
5
+ ## 🚀 Features
6
+ - **Virtual Try-On:** Upload clothing and avatar images to generate realistic try-on results.
7
+ - **AI Fashion Analysis:** Get style advice and outfit analysis using BLIP and CLIP models.
8
+ - **Conversational Assistant:** Chat with a TinyLlama-powered AI for personalized fashion tips.
9
+ - **Automated Shopping:** Find similar items and compare prices across e-commerce sites using Playwright automation.
10
+ - **Modern UI:** Gradio-powered web interface for easy interaction.
11
+
12
+ ## 🏗️ Architecture
13
+ - **Frontend:** Gradio (for Hugging Face Spaces)
14
+ - **Backend:** Flask API server (runs in background)
15
+ - **AI Models:** BLIP, CLIP, TinyLlama (all open-source, loaded on demand)
16
+ - **Automation:** Playwright for web scraping and product search
17
+
18
+ ## 🛠️ Setup & Deployment
19
+ 1. All dependencies are listed in `requirements.txt`.
20
+ 2. The backend Flask server is started automatically by `app.py`.
21
+ 3. The Gradio interface is the entry point for Hugging Face Spaces.
22
+
23
+ ## 💡 Usage
24
+ - Upload a fashion image to get instant AI-powered analysis and advice.
25
+ - Chat with the AI assistant for style tips and outfit suggestions.
26
+ - Use the shopping automation to discover and compare similar products online.
27
+
28
+ ## 🌐 Public Demo
29
+ Deploy this project on [Hugging Face Spaces](https://huggingface.co/spaces) for a live, shareable demo. Free CPU and GPU options available.
30
+
31
+ ## 📄 License
32
+ Open-source, for educational and portfolio use. See project root for details.
33
+
34
+ ---
35
+
36
+ **For more details, see the full documentation and quick start guides in the main project repository.**
37
  ---
38
  title: Ai Virtual Tryon
39
  emoji: 🌍
app.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import subprocess
3
+ import threading
4
+ import time
5
+ import requests
6
+ import base64
7
+ from PIL import Image
8
+ import io
9
+
10
+ # Start Flask backend
11
+ def start_backend():
12
+ subprocess.Popen(["python", "src/main.py"])
13
+
14
+ backend_thread = threading.Thread(target=start_backend)
15
+ backend_thread.daemon = True
16
+ backend_thread.start()
17
+ time.sleep(15) # Wait for models to load
18
+
19
+ def analyze_fashion(image):
20
+ if image is None:
21
+ return "Please upload an image first!"
22
+
23
+ # Convert PIL to base64
24
+ buffered = io.BytesIO()
25
+ image.save(buffered, format="PNG")
26
+ img_str = base64.b64encode(buffered.getvalue()).decode()
27
+
28
+ try:
29
+ response = requests.post(
30
+ "http://localhost:5000/ai_api/analyze-image",
31
+ json={"image": f"data:image/png;base64,{img_str}"},
32
+ timeout=30
33
+ )
34
+ result = response.json()
35
+ return result.get('advice', 'Analysis complete!')
36
+ except:
37
+ return "AI analysis service starting up... Please try again in a moment."
38
+
39
+ # Create Gradio interface
40
+ iface = gr.Interface(
41
+ fn=analyze_fashion,
42
+ inputs=gr.Image(type="pil", label="Upload Fashion Image"),
43
+ outputs=gr.Textbox(label="AI Fashion Analysis"),
44
+ title="🤖 AI-Powered Fashion Advisor",
45
+ description="Upload any fashion image to get AI-powered style advice and recommendations!"
46
+ )
47
+
48
+ if __name__ == "__main__":
49
+ iface.launch()
huggingface.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Space configuration file
2
+ # See https://huggingface.co/docs/hub/spaces-config-reference for all options
3
+
4
+ title: AI-Powered Virtual Try-On
5
+ sdk: gradio
6
+ python_version: 3.10
7
+ app_file: app.py
8
+ description: |
9
+ A full-stack AI fashion assistant and virtual try-on system. Upload images, get style advice, chat with an AI stylist, and discover similar products—all in one interactive web app. Powered by BLIP, CLIP, TinyLlama, and Playwright automation.
10
+
11
+ # Optional: hardware
12
+ # hardware: cpu-basic
13
+ # Uncomment for GPU support if needed:
14
+ # hardware: gpu
requirements.txt ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ anyio==4.10.0
3
+ blinker==1.9.0
4
+ certifi==2025.8.3
5
+ charset-normalizer==3.4.3
6
+ click==8.2.1
7
+ distro==1.9.0
8
+ filelock==3.19.1
9
+ Flask==3.1.1
10
+ flask-cors==6.0.0
11
+ Flask-SQLAlchemy==3.1.1
12
+ fsspec==2025.7.0
13
+ greenlet==3.2.4
14
+ h11==0.16.0
15
+ hf-xet==1.1.8
16
+ httpcore==1.0.9
17
+ httpx==0.28.1
18
+ huggingface-hub==0.34.4
19
+ idna==3.10
20
+ itsdangerous==2.2.0
21
+ Jinja2==3.1.6
22
+ jiter==0.10.0
23
+ MarkupSafe==3.0.2
24
+ mpmath==1.3.0
25
+ numpy==2.2.6
26
+ openai==1.100.2
27
+ packaging==25.0
28
+ pillow==11.3.0
29
+ playwright==1.54.0
30
+ pydantic==2.11.7
31
+ pydantic_core==2.33.2
32
+ pyee==13.0.0
33
+ PyYAML==6.0.2
34
+ regex==2025.7.34
35
+ requests==2.32.5
36
+ safetensors==0.6.2
37
+ sniffio==1.3.1
38
+ SQLAlchemy==2.0.41
39
+ sympy==1.14.0
40
+ tokenizers==0.21.4
41
+ torch==2.8.0
42
+ torchvision==0.23.0
43
+ tqdm==4.67.1
44
+ transformers==4.55.2
45
+ typing-inspection==0.4.1
46
+ typing_extensions==4.14.0
47
+ urllib3==2.5.0
48
+ Werkzeug==3.1.3
src/.DS_Store ADDED
Binary file (6.15 kB). View file
 
src/__init__.py ADDED
File without changes
src/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (161 Bytes). View file
 
src/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (154 Bytes). View file
 
src/database/app.db ADDED
Binary file (16.4 kB). View file
 
src/main.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ # DON'T CHANGE THIS !!!
4
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
5
+
6
+ from flask import Flask, send_from_directory
7
+ from src.models.user import db
8
+ from src.routes.user import user_bp
9
+ from src.routes.ai_agent import ai_agent_bp # Import the new AI agent blueprint
10
+ from src.routes.shopping_automation import shopping_bp # Import shopping automation blueprint
11
+
12
+ app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), 'static'))
13
+ app.config['SECRET_KEY'] = 'asdf#FGSgvasgf$5$WGT'
14
+
15
+ app.register_blueprint(user_bp, url_prefix='/api')
16
+ app.register_blueprint(ai_agent_bp, url_prefix='/ai_api') # Register the AI agent blueprint
17
+ app.register_blueprint(shopping_bp, url_prefix='/shopping_api') # Register shopping automation blueprint
18
+
19
+ # uncomment if you need to use database
20
+ app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(os.path.dirname(__file__), 'database', 'app.db')}"
21
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
22
+ db.init_app(app)
23
+ with app.app_context():
24
+ db.create_all()
25
+
26
+ @app.route('/', defaults={'path': ''})
27
+ @app.route('/<path:path>')
28
+ def serve(path):
29
+ static_folder_path = app.static_folder
30
+ if static_folder_path is None:
31
+ return "Static folder not configured", 404
32
+
33
+ if path != "" and os.path.exists(os.path.join(static_folder_path, path)):
34
+ return send_from_directory(static_folder_path, path)
35
+ else:
36
+ index_path = os.path.join(static_folder_path, 'index.html')
37
+ if os.path.exists(index_path):
38
+ return send_from_directory(static_folder_path, 'index.html')
39
+ else:
40
+ return "index.html not found", 404
41
+
42
+
43
+ if __name__ == '__main__':
44
+ app.run(host='0.0.0.0', port=5000, debug=True)
45
+
46
+
src/models/__pycache__/user.cpython-310.pyc ADDED
Binary file (840 Bytes). View file
 
src/models/__pycache__/user.cpython-311.pyc ADDED
Binary file (1.3 kB). View file
 
src/models/user.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+
3
+ db = SQLAlchemy()
4
+
5
+ class User(db.Model):
6
+ id = db.Column(db.Integer, primary_key=True)
7
+ username = db.Column(db.String(80), unique=True, nullable=False)
8
+ email = db.Column(db.String(120), unique=True, nullable=False)
9
+
10
+ def __repr__(self):
11
+ return f'<User {self.username}>'
12
+
13
+ def to_dict(self):
14
+ return {
15
+ 'id': self.id,
16
+ 'username': self.username,
17
+ 'email': self.email
18
+ }
src/routes/__pycache__/ai_agent.cpython-310.pyc ADDED
Binary file (6.78 kB). View file
 
src/routes/__pycache__/ai_agent.cpython-311.pyc ADDED
Binary file (13 kB). View file
 
src/routes/__pycache__/shopping_automation.cpython-310.pyc ADDED
Binary file (7.98 kB). View file
 
src/routes/__pycache__/shopping_automation.cpython-311.pyc ADDED
Binary file (16.4 kB). View file
 
src/routes/__pycache__/user.cpython-310.pyc ADDED
Binary file (1.68 kB). View file
 
src/routes/__pycache__/user.cpython-311.pyc ADDED
Binary file (3.4 kB). View file
 
src/routes/ai_agent.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ from flask_cors import cross_origin
3
+ import base64
4
+ import io
5
+ from PIL import Image
6
+ import torch
7
+ from transformers import (
8
+ BlipProcessor,
9
+ BlipForConditionalGeneration,
10
+ CLIPProcessor,
11
+ CLIPModel,
12
+ AutoTokenizer,
13
+ AutoModelForCausalLM,
14
+ )
15
+ import os
16
+ import json
17
+
18
+ ai_agent_bp = Blueprint('ai_agent', __name__)
19
+
20
+ # Global state
21
+ blip_model = None
22
+ blip_processor = None
23
+ clip_model = None
24
+ clip_processor = None
25
+ llm_tokenizer = None
26
+ llm_model = None
27
+
28
+ def device():
29
+ return 'cuda' if torch.cuda.is_available() else 'cpu'
30
+
31
+ def load_models():
32
+ global blip_model, blip_processor, clip_model, clip_processor, llm_model, llm_tokenizer
33
+
34
+ if blip_model is None:
35
+ print('Loading BLIP image captioning model...')
36
+ blip_processor = BlipProcessor.from_pretrained('Salesforce/blip-image-captioning-base')
37
+ blip_model = BlipForConditionalGeneration.from_pretrained('Salesforce/blip-image-captioning-base').to(device())
38
+ blip_model.eval()
39
+
40
+ if clip_model is None:
41
+ print('Loading CLIP model...')
42
+ clip_processor = CLIPProcessor.from_pretrained('openai/clip-vit-base-patch32')
43
+ clip_model = CLIPModel.from_pretrained('openai/clip-vit-base-patch32').to(device())
44
+ clip_model.eval()
45
+
46
+ # Optional lightweight local LLM for chat (no paid APIs)
47
+ if os.environ.get('USE_TINY_LLAMA', '1') == '1' and llm_model is None:
48
+ try:
49
+ print('Loading TinyLlama chat model (CPU)...')
50
+ llm_tokenizer = AutoTokenizer.from_pretrained('TinyLlama/TinyLlama-1.1B-Chat-v1.0')
51
+ llm_model = AutoModelForCausalLM.from_pretrained('TinyLlama/TinyLlama-1.1B-Chat-v1.0').to('cpu')
52
+ llm_model.eval()
53
+ except Exception as e:
54
+ print(f'Failed to load TinyLlama: {e}')
55
+
56
+
57
+ def analyze_fashion_image(pil_image: Image.Image):
58
+ load_models()
59
+
60
+ # BLIP caption
61
+ inputs = blip_processor(pil_image, return_tensors='pt').to(device())
62
+ with torch.no_grad():
63
+ out = blip_model.generate(**inputs, max_length=64)
64
+ caption = blip_processor.decode(out[0], skip_special_tokens=True)
65
+
66
+ # Lightweight fashion prompts via BLIP conditional generation
67
+ fashion_prompts = [
68
+ 'Describe the clothing type and key attributes.',
69
+ 'List the dominant colors.',
70
+ 'What style or vibe does this outfit convey?',
71
+ 'Suggest an occasion where this outfit fits well.'
72
+ ]
73
+ analysis = {}
74
+ for prompt in fashion_prompts:
75
+ try:
76
+ cond_inputs = blip_processor(pil_image, prompt, return_tensors='pt').to(device())
77
+ with torch.no_grad():
78
+ out2 = blip_model.generate(**cond_inputs, max_length=48)
79
+ resp = blip_processor.decode(out2[0], skip_special_tokens=True)
80
+ analysis[prompt] = resp
81
+ except Exception:
82
+ analysis[prompt] = ''
83
+
84
+ return {
85
+ 'caption': caption,
86
+ 'fashion_analysis': analysis,
87
+ }
88
+
89
+
90
+ def simple_rule_based_advice(text: str) -> str:
91
+ t = (text or '').lower()
92
+ advice = []
93
+ if 'black' in t:
94
+ advice.append('Black is versatile; add a contrasting accessory (silver, gold, or a bold color) to elevate the look.')
95
+ if 'white' in t:
96
+ advice.append('White gives a clean aesthetic; consider layering with textured fabrics or a light jacket for depth.')
97
+ if 'blue' in t:
98
+ advice.append('Blue pairs well with neutrals (tan, white, grey). Try denim-on-denim or navy with beige for a classic combo.')
99
+ if 'dress' in t:
100
+ advice.append('Consider heel height and bag size to match the dress formality. A belt can define the silhouette if needed.')
101
+ if 'shirt' in t or 'top' in t:
102
+ advice.append('Balance the silhouette: pair fitted tops with relaxed bottoms or vice versa. Tuck/half-tuck for shape.')
103
+ if not advice:
104
+ advice.append('Consider color harmony, silhouette balance, and occasion. Add one statement accessory to complete the look.')
105
+ return ' '.join(advice)
106
+
107
+
108
+ def llm_advise(system_prompt: str, user_prompt: str) -> str:
109
+ # Try TinyLlama, else fallback to rule-based
110
+ if llm_model is None or llm_tokenizer is None:
111
+ return simple_rule_based_advice(user_prompt)
112
+ prompt = (
113
+ f"<|system|>\n{system_prompt}\n<|user|>\n{user_prompt}\n<|assistant|>"
114
+ )
115
+ inputs = llm_tokenizer(prompt, return_tensors='pt')
116
+ with torch.no_grad():
117
+ out = llm_model.generate(
118
+ **inputs,
119
+ max_new_tokens=220,
120
+ do_sample=True,
121
+ temperature=0.7,
122
+ top_p=0.9,
123
+ eos_token_id=llm_tokenizer.eos_token_id,
124
+ )
125
+ text = llm_tokenizer.decode(out[0], skip_special_tokens=True)
126
+ # Extract assistant part
127
+ if '<|assistant|>' in text:
128
+ text = text.split('<|assistant|>')[-1].strip()
129
+ return text
130
+
131
+
132
+ @ai_agent_bp.route('/analyze-image', methods=['POST'])
133
+ @cross_origin()
134
+ def analyze_image():
135
+ try:
136
+ data = request.get_json(force=True)
137
+ if not data or 'image' not in data:
138
+ return jsonify({'error': 'No image provided'}), 400
139
+ b64 = data['image']
140
+ if ',' in b64:
141
+ b64 = b64.split(',')[1]
142
+ image_bytes = base64.b64decode(b64)
143
+ pil = Image.open(io.BytesIO(image_bytes)).convert('RGB')
144
+
145
+ analysis = analyze_fashion_image(pil)
146
+ sys_prompt = (
147
+ 'You are a professional fashion stylist. Provide constructive, specific, and friendly advice about fit, colors, style, and accessories.'
148
+ )
149
+ user_msg = data.get('message') or f"Analyze and advise based on: {json.dumps(analysis)}"
150
+ advice = llm_advise(sys_prompt, user_msg)
151
+ return jsonify({'success': True, 'analysis': analysis, 'advice': advice})
152
+ except Exception as e:
153
+ return jsonify({'error': str(e)}), 500
154
+
155
+
156
+ @ai_agent_bp.route('/chat', methods=['POST'])
157
+ @cross_origin()
158
+ def chat():
159
+ try:
160
+ data = request.get_json(force=True)
161
+ message = (data or {}).get('message', '').strip()
162
+ if not message:
163
+ return jsonify({'error': 'No message provided'}), 400
164
+ sys_prompt = 'You are a helpful, up-to-date fashion stylist assistant.'
165
+ reply = llm_advise(sys_prompt, message)
166
+ return jsonify({'success': True, 'reply': reply})
167
+ except Exception as e:
168
+ return jsonify({'error': str(e)}), 500
169
+
170
+
171
+ @ai_agent_bp.route('/recommend-similar', methods=['POST'])
172
+ @cross_origin()
173
+ def recommend_similar():
174
+ try:
175
+ data = request.get_json(force=True)
176
+ if 'image' not in data:
177
+ return jsonify({'error': 'No image provided'}), 400
178
+ b64 = data['image']
179
+ if ',' in b64:
180
+ b64 = b64.split(',')[1]
181
+ image_bytes = base64.b64decode(b64)
182
+ pil = Image.open(io.BytesIO(image_bytes)).convert('RGB')
183
+
184
+ load_models()
185
+ inputs = clip_processor(images=pil, return_tensors='pt').to(device())
186
+ with torch.no_grad():
187
+ _ = clip_model.get_image_features(**inputs)
188
+ # Placeholder recommendations (no external API)
189
+ recs = [
190
+ {
191
+ 'title': 'Similar silhouette',
192
+ 'description': 'Find items with a matching cut and color palette.',
193
+ 'stores': ['Zara', 'H&M', 'ASOS']
194
+ },
195
+ {
196
+ 'title': 'Complementary accessories',
197
+ 'description': 'Belts, bags, and shoes that elevate this outfit.',
198
+ 'stores': ['Uniqlo', 'Mango', 'Amazon']
199
+ },
200
+ {
201
+ 'title': 'Alternative colors',
202
+ 'description': 'Same style in seasonal colorways to suit your palette.',
203
+ 'stores': ['Nordstrom', "Macy's", 'Urban Outfitters']
204
+ }
205
+ ]
206
+ return jsonify({'success': True, 'recommendations': recs})
207
+ except Exception as e:
208
+ return jsonify({'error': str(e)}), 500
209
+
210
+
211
+ @ai_agent_bp.route('/health', methods=['GET'])
212
+ @cross_origin()
213
+ def health():
214
+ return jsonify({'status': 'ok'})
215
+
src/routes/shopping_automation.py ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, request, jsonify
2
+ from flask_cors import cross_origin
3
+ import asyncio
4
+ from playwright.async_api import async_playwright
5
+ import json
6
+ import re
7
+ from urllib.parse import quote_plus
8
+ import time
9
+
10
+ shopping_bp = Blueprint('shopping', __name__)
11
+
12
+ # Popular fashion e-commerce sites for product search
13
+ FASHION_SITES = {
14
+ 'zara': {
15
+ 'url': 'https://www.zara.com/us/en/search',
16
+ 'search_param': 'searchTerm',
17
+ 'selectors': {
18
+ 'products': '.product-item',
19
+ 'title': '.product-link',
20
+ 'price': '.price',
21
+ 'image': '.media-image img',
22
+ 'link': '.product-link'
23
+ }
24
+ },
25
+ 'hm': {
26
+ 'url': 'https://www2.hm.com/en_us/search-results.html',
27
+ 'search_param': 'q',
28
+ 'selectors': {
29
+ 'products': '.item-link',
30
+ 'title': '.item-heading',
31
+ 'price': '.item-price',
32
+ 'image': '.item-image img',
33
+ 'link': '.item-link'
34
+ }
35
+ },
36
+ 'asos': {
37
+ 'url': 'https://www.asos.com/us/search/',
38
+ 'search_param': 'q',
39
+ 'selectors': {
40
+ 'products': '[data-testid="product-tile"]',
41
+ 'title': '[data-testid="product-title"]',
42
+ 'price': '[data-testid="current-price"]',
43
+ 'image': 'img',
44
+ 'link': 'a'
45
+ }
46
+ }
47
+ }
48
+
49
+ async def search_fashion_items(query, max_results=5):
50
+ """Search for fashion items across multiple e-commerce sites"""
51
+ results = []
52
+
53
+ async with async_playwright() as p:
54
+ browser = await p.chromium.launch(headless=True)
55
+ context = await browser.new_context(
56
+ user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
57
+ )
58
+
59
+ for site_name, site_config in FASHION_SITES.items():
60
+ try:
61
+ page = await context.new_page()
62
+
63
+ # Construct search URL
64
+ search_url = f"{site_config['url']}?{site_config['search_param']}={quote_plus(query)}"
65
+
66
+ # Navigate to search page
67
+ await page.goto(search_url, wait_until='networkidle', timeout=10000)
68
+ await page.wait_for_timeout(2000) # Wait for dynamic content
69
+
70
+ # Extract product information
71
+ products = await page.query_selector_all(site_config['selectors']['products'])
72
+
73
+ site_results = []
74
+ for product in products[:max_results]:
75
+ try:
76
+ # Extract product details
77
+ title_elem = await product.query_selector(site_config['selectors']['title'])
78
+ price_elem = await product.query_selector(site_config['selectors']['price'])
79
+ image_elem = await product.query_selector(site_config['selectors']['image'])
80
+ link_elem = await product.query_selector(site_config['selectors']['link'])
81
+
82
+ title = await title_elem.inner_text() if title_elem else 'N/A'
83
+ price = await price_elem.inner_text() if price_elem else 'N/A'
84
+ image_src = await image_elem.get_attribute('src') if image_elem else ''
85
+ link_href = await link_elem.get_attribute('href') if link_elem else ''
86
+
87
+ # Clean up the data
88
+ title = title.strip()[:100] # Limit title length
89
+ price = re.sub(r'[^\d.,\$€£]', '', price) if price != 'N/A' else 'N/A'
90
+
91
+ # Make sure link is absolute
92
+ if link_href and not link_href.startswith('http'):
93
+ base_url = f"https://{site_name}.com" if site_name != 'hm' else 'https://www2.hm.com'
94
+ link_href = base_url + link_href
95
+
96
+ site_results.append({
97
+ 'title': title,
98
+ 'price': price,
99
+ 'image': image_src,
100
+ 'link': link_href,
101
+ 'store': site_name.upper(),
102
+ 'query': query
103
+ })
104
+
105
+ except Exception as e:
106
+ print(f"Error extracting product from {site_name}: {e}")
107
+ continue
108
+
109
+ results.extend(site_results)
110
+ await page.close()
111
+
112
+ except Exception as e:
113
+ print(f"Error searching {site_name}: {e}")
114
+ continue
115
+
116
+ await browser.close()
117
+
118
+ return results
119
+
120
+ def generate_search_queries(description):
121
+ """Generate relevant search queries from fashion description"""
122
+ # Extract key fashion terms
123
+ fashion_keywords = [
124
+ 'dress', 'shirt', 'blouse', 'top', 'pants', 'jeans', 'skirt',
125
+ 'jacket', 'coat', 'sweater', 'cardigan', 'blazer', 'suit',
126
+ 'black', 'white', 'blue', 'red', 'green', 'navy', 'gray',
127
+ 'casual', 'formal', 'business', 'party', 'summer', 'winter'
128
+ ]
129
+
130
+ description_lower = description.lower()
131
+ found_keywords = [kw for kw in fashion_keywords if kw in description_lower]
132
+
133
+ # Generate search queries
134
+ queries = []
135
+ if found_keywords:
136
+ # Primary query with main keywords
137
+ queries.append(' '.join(found_keywords[:3]))
138
+
139
+ # Secondary queries with individual keywords
140
+ for keyword in found_keywords[:2]:
141
+ queries.append(keyword)
142
+ else:
143
+ # Fallback generic queries
144
+ queries = ['women dress', 'fashion top', 'casual wear']
145
+
146
+ return queries[:3] # Limit to 3 queries
147
+
148
+ @shopping_bp.route('/search-products', methods=['POST'])
149
+ @cross_origin()
150
+ def search_products():
151
+ """Search for fashion products based on description"""
152
+ try:
153
+ data = request.get_json()
154
+ description = data.get('description', '').strip()
155
+
156
+ if not description:
157
+ return jsonify({'error': 'No description provided'}), 400
158
+
159
+ # Generate search queries from description
160
+ queries = generate_search_queries(description)
161
+
162
+ # Run async search
163
+ loop = asyncio.new_event_loop()
164
+ asyncio.set_event_loop(loop)
165
+
166
+ all_results = []
167
+ for query in queries:
168
+ try:
169
+ results = loop.run_until_complete(search_fashion_items(query, max_results=3))
170
+ all_results.extend(results)
171
+ except Exception as e:
172
+ print(f"Error searching for '{query}': {e}")
173
+ continue
174
+
175
+ loop.close()
176
+
177
+ # Remove duplicates and limit results
178
+ seen_titles = set()
179
+ unique_results = []
180
+ for result in all_results:
181
+ if result['title'] not in seen_titles and len(unique_results) < 10:
182
+ seen_titles.add(result['title'])
183
+ unique_results.append(result)
184
+
185
+ return jsonify({
186
+ 'success': True,
187
+ 'results': unique_results,
188
+ 'queries_used': queries,
189
+ 'total_found': len(unique_results)
190
+ })
191
+
192
+ except Exception as e:
193
+ print(f"Error in search_products: {e}")
194
+ return jsonify({'error': str(e)}), 500
195
+
196
+ @shopping_bp.route('/get-product-details', methods=['POST'])
197
+ @cross_origin()
198
+ def get_product_details():
199
+ """Get detailed information about a specific product"""
200
+ try:
201
+ data = request.get_json()
202
+ product_url = data.get('url', '').strip()
203
+
204
+ if not product_url:
205
+ return jsonify({'error': 'No product URL provided'}), 400
206
+
207
+ # Run async product detail extraction
208
+ loop = asyncio.new_event_loop()
209
+ asyncio.set_event_loop(loop)
210
+
211
+ async def extract_product_details(url):
212
+ async with async_playwright() as p:
213
+ browser = await p.chromium.launch(headless=True)
214
+ context = await browser.new_context()
215
+ page = await context.new_page()
216
+
217
+ try:
218
+ await page.goto(url, wait_until='networkidle', timeout=15000)
219
+ await page.wait_for_timeout(2000)
220
+
221
+ # Extract basic product information
222
+ title = await page.title()
223
+
224
+ # Try to find price (common selectors)
225
+ price_selectors = [
226
+ '.price', '.current-price', '.product-price',
227
+ '[data-testid="current-price"]', '.price-current',
228
+ '.sale-price', '.regular-price'
229
+ ]
230
+
231
+ price = 'N/A'
232
+ for selector in price_selectors:
233
+ try:
234
+ price_elem = await page.query_selector(selector)
235
+ if price_elem:
236
+ price = await price_elem.inner_text()
237
+ break
238
+ except:
239
+ continue
240
+
241
+ # Try to find description
242
+ desc_selectors = [
243
+ '.product-description', '.description', '.product-details',
244
+ '[data-testid="product-description"]', '.product-info'
245
+ ]
246
+
247
+ description = 'N/A'
248
+ for selector in desc_selectors:
249
+ try:
250
+ desc_elem = await page.query_selector(selector)
251
+ if desc_elem:
252
+ description = await desc_elem.inner_text()
253
+ description = description[:500] # Limit length
254
+ break
255
+ except:
256
+ continue
257
+
258
+ return {
259
+ 'title': title,
260
+ 'price': price,
261
+ 'description': description,
262
+ 'url': url,
263
+ 'available': True
264
+ }
265
+
266
+ except Exception as e:
267
+ print(f"Error extracting product details: {e}")
268
+ return {
269
+ 'title': 'Product Details Unavailable',
270
+ 'price': 'N/A',
271
+ 'description': 'Could not extract product details',
272
+ 'url': url,
273
+ 'available': False
274
+ }
275
+ finally:
276
+ await browser.close()
277
+
278
+ result = loop.run_until_complete(extract_product_details(product_url))
279
+ loop.close()
280
+
281
+ return jsonify({
282
+ 'success': True,
283
+ 'product': result
284
+ })
285
+
286
+ except Exception as e:
287
+ print(f"Error in get_product_details: {e}")
288
+ return jsonify({'error': str(e)}), 500
289
+
290
+ @shopping_bp.route('/compare-prices', methods=['POST'])
291
+ @cross_origin()
292
+ def compare_prices():
293
+ """Compare prices for similar items across different stores"""
294
+ try:
295
+ data = request.get_json()
296
+ item_name = data.get('item_name', '').strip()
297
+
298
+ if not item_name:
299
+ return jsonify({'error': 'No item name provided'}), 400
300
+
301
+ # Search across multiple sites for price comparison
302
+ loop = asyncio.new_event_loop()
303
+ asyncio.set_event_loop(loop)
304
+
305
+ results = loop.run_until_complete(search_fashion_items(item_name, max_results=3))
306
+ loop.close()
307
+
308
+ # Group by store and extract price information
309
+ price_comparison = {}
310
+ for result in results:
311
+ store = result['store']
312
+ if store not in price_comparison:
313
+ price_comparison[store] = []
314
+
315
+ # Extract numeric price for comparison
316
+ price_text = result['price']
317
+ price_numeric = None
318
+ if price_text != 'N/A':
319
+ price_match = re.search(r'[\d.,]+', price_text)
320
+ if price_match:
321
+ try:
322
+ price_numeric = float(price_match.group().replace(',', ''))
323
+ except:
324
+ pass
325
+
326
+ price_comparison[store].append({
327
+ 'title': result['title'],
328
+ 'price_text': price_text,
329
+ 'price_numeric': price_numeric,
330
+ 'link': result['link'],
331
+ 'image': result['image']
332
+ })
333
+
334
+ return jsonify({
335
+ 'success': True,
336
+ 'comparison': price_comparison,
337
+ 'item_searched': item_name
338
+ })
339
+
340
+ except Exception as e:
341
+ print(f"Error in compare_prices: {e}")
342
+ return jsonify({'error': str(e)}), 500
343
+
344
+ @shopping_bp.route('/health', methods=['GET'])
345
+ @cross_origin()
346
+ def health_check():
347
+ """Health check for shopping automation service"""
348
+ return jsonify({
349
+ 'status': 'ok',
350
+ 'service': 'shopping_automation',
351
+ 'playwright_available': True
352
+ })
353
+
src/routes/user.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, jsonify, request
2
+ from src.models.user import User, db
3
+
4
+ user_bp = Blueprint('user', __name__)
5
+
6
+ @user_bp.route('/users', methods=['GET'])
7
+ def get_users():
8
+ users = User.query.all()
9
+ return jsonify([user.to_dict() for user in users])
10
+
11
+ @user_bp.route('/users', methods=['POST'])
12
+ def create_user():
13
+
14
+ data = request.json
15
+ user = User(username=data['username'], email=data['email'])
16
+ db.session.add(user)
17
+ db.session.commit()
18
+ return jsonify(user.to_dict()), 201
19
+
20
+ @user_bp.route('/users/<int:user_id>', methods=['GET'])
21
+ def get_user(user_id):
22
+ user = User.query.get_or_404(user_id)
23
+ return jsonify(user.to_dict())
24
+
25
+ @user_bp.route('/users/<int:user_id>', methods=['PUT'])
26
+ def update_user(user_id):
27
+ user = User.query.get_or_404(user_id)
28
+ data = request.json
29
+ user.username = data.get('username', user.username)
30
+ user.email = data.get('email', user.email)
31
+ db.session.commit()
32
+ return jsonify(user.to_dict())
33
+
34
+ @user_bp.route('/users/<int:user_id>', methods=['DELETE'])
35
+ def delete_user(user_id):
36
+ user = User.query.get_or_404(user_id)
37
+ db.session.delete(user)
38
+ db.session.commit()
39
+ return '', 204
src/static/favicon.ico ADDED
src/static/index.html ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>virtual-tryon-ai</title>
8
+ <style>
9
+ body { font-family: sans-serif; margin: 20px; }
10
+ .section { margin-bottom: 20px; padding: 15px; border: 1px solid #ccc; border-radius: 5px; }
11
+ h2 { margin-top: 0; }
12
+ label { display: inline-block; width: 80px; margin-bottom: 5px; }
13
+ input[type="text"], input[type="email"], input[type="number"] { margin-bottom: 10px; padding: 5px; width: 200px; }
14
+ button { padding: 8px 15px; margin-right: 10px; cursor: pointer; }
15
+ pre { background-color: #f4f4f4; padding: 10px; border: 1px solid #ddd; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <h1>User API Test</h1>
20
+
21
+ <!-- Get All Users -->
22
+ <div class="section">
23
+ <h2>Get All Users (GET /users)</h2>
24
+ <button onclick="getUsers()">Get Users</button>
25
+ <pre id="get-users-result"></pre>
26
+ </div>
27
+
28
+ <!-- Create User -->
29
+ <div class="section">
30
+ <h2>Create User (POST /users)</h2>
31
+ <label for="create-username">Username:</label>
32
+ <input type="text" id="create-username" name="username"><br>
33
+ <label for="create-email">Email:</label>
34
+ <input type="email" id="create-email" name="email"><br>
35
+ <button onclick="createUser()">Create User</button>
36
+ <pre id="create-user-result"></pre>
37
+ </div>
38
+
39
+ <!-- Get Single User -->
40
+ <div class="section">
41
+ <h2>Get Single User (GET /users/&lt;id&gt;)</h2>
42
+ <label for="get-user-id">User ID:</label>
43
+ <input type="number" id="get-user-id" name="user_id"><br>
44
+ <button onclick="getUser()">Get User</button>
45
+ <pre id="get-user-result"></pre>
46
+ </div>
47
+
48
+ <!-- Update User -->
49
+ <div class="section">
50
+ <h2>Update User (PUT /users/&lt;id&gt;)</h2>
51
+ <label for="update-user-id">User ID:</label>
52
+ <input type="number" id="update-user-id" name="user_id"><br>
53
+ <label for="update-username">New Username:</label>
54
+ <input type="text" id="update-username" name="username"><br>
55
+ <label for="update-email">New Email:</label>
56
+ <input type="email" id="update-email" name="email"><br>
57
+ <button onclick="updateUser()">Update User</button>
58
+ <pre id="update-user-result"></pre>
59
+ </div>
60
+
61
+ <!-- Delete User -->
62
+ <div class="section">
63
+ <h2>Delete User (DELETE /users/&lt;id&gt;)</h2>
64
+ <label for="delete-user-id">User ID:</label>
65
+ <input type="number" id="delete-user-id" name="user_id"><br>
66
+ <button onclick="deleteUser()">Delete User</button>
67
+ <pre id="delete-user-result"></pre>
68
+ </div>
69
+
70
+ <script>
71
+ const API_BASE_URL = '/api/users';
72
+
73
+ // Helper function to display results
74
+ function displayResult(elementId, data) {
75
+ document.getElementById(elementId).textContent = JSON.stringify(data, null, 2);
76
+ }
77
+
78
+ // Helper function to display errors
79
+ function displayError(elementId, error) {
80
+ document.getElementById(elementId).textContent = `Error: ${error.message || error}`;
81
+ }
82
+
83
+ // GET /users
84
+ async function getUsers() {
85
+ const resultElementId = 'get-users-result';
86
+ try {
87
+ const response = await fetch(API_BASE_URL);
88
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
89
+ const data = await response.json();
90
+ displayResult(resultElementId, data);
91
+ } catch (error) {
92
+ displayError(resultElementId, error);
93
+ }
94
+ }
95
+
96
+ // POST /users
97
+ async function createUser() {
98
+ const resultElementId = 'create-user-result';
99
+ const username = document.getElementById('create-username').value;
100
+ const email = document.getElementById('create-email').value;
101
+ if (!username || !email) {
102
+ displayError(resultElementId, 'Username and email cannot be empty');
103
+ return;
104
+ }
105
+ try {
106
+ const response = await fetch(API_BASE_URL, {
107
+ method: 'POST',
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: JSON.stringify({ username, email })
110
+ });
111
+ const data = await response.json();
112
+ if (!response.ok) throw new Error(data.message || `HTTP error! status: ${response.status}`);
113
+ displayResult(resultElementId, data);
114
+ // 清空输入框
115
+ document.getElementById('create-username').value = '';
116
+ document.getElementById('create-email').value = '';
117
+ } catch (error) {
118
+ displayError(resultElementId, error);
119
+ }
120
+ }
121
+
122
+ // GET /users/<id>
123
+ async function getUser() {
124
+ const resultElementId = 'get-user-result';
125
+ const userId = document.getElementById('get-user-id').value;
126
+ if (!userId) {
127
+ displayError(resultElementId, 'User ID cannot be empty');
128
+ return;
129
+ }
130
+ try {
131
+ const response = await fetch(`${API_BASE_URL}/${userId}`);
132
+ if (response.status === 404) throw new Error('User not found');
133
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
134
+ const data = await response.json();
135
+ displayResult(resultElementId, data);
136
+ } catch (error) {
137
+ displayError(resultElementId, error);
138
+ }
139
+ }
140
+
141
+ // PUT /users/<id>
142
+ async function updateUser() {
143
+ const resultElementId = 'update-user-result';
144
+ const userId = document.getElementById('update-user-id').value;
145
+ const username = document.getElementById('update-username').value;
146
+ const email = document.getElementById('update-email').value;
147
+ if (!userId) {
148
+ displayError(resultElementId, 'User ID cannot be empty');
149
+ return;
150
+ }
151
+ const updateData = {};
152
+ if (username) updateData.username = username;
153
+ if (email) updateData.email = email;
154
+ if (Object.keys(updateData).length === 0) {
155
+ displayError(resultElementId, 'Please enter a username or email to update');
156
+ return;
157
+ }
158
+
159
+ try {
160
+ const response = await fetch(`${API_BASE_URL}/${userId}`, {
161
+ method: 'PUT',
162
+ headers: { 'Content-Type': 'application/json' },
163
+ body: JSON.stringify(updateData)
164
+ });
165
+ if (response.status === 404) throw new Error('User not found');
166
+ const data = await response.json();
167
+ if (!response.ok) throw new Error(data.message || `HTTP error! status: ${response.status}`);
168
+ displayResult(resultElementId, data);
169
+ // Clear input fields
170
+ document.getElementById('update-username').value = '';
171
+ document.getElementById('update-email').value = '';
172
+ } catch (error) {
173
+ displayError(resultElementId, error);
174
+ }
175
+ }
176
+
177
+ // DELETE /users/<id>
178
+ async function deleteUser() {
179
+ const resultElementId = 'delete-user-result';
180
+ const userId = document.getElementById('delete-user-id').value;
181
+ if (!userId) {
182
+ displayError(resultElementId, 'User ID cannot be empty');
183
+ return;
184
+ }
185
+ try {
186
+ const response = await fetch(`${API_BASE_URL}/${userId}`, {
187
+ method: 'DELETE'
188
+ });
189
+ if (response.status === 404) throw new Error('User not found');
190
+ if (!response.ok && response.status !== 204) throw new Error(`HTTP error! status: ${response.status}`); // Allow 204
191
+ // 204 No Content indicates success
192
+ if (response.status === 204) {
193
+ displayResult(resultElementId, { message: `User ID ${userId} has been successfully deleted` });
194
+ } else {
195
+ // Try to read potential error message even on success-like status if not 204
196
+ const data = await response.text();
197
+ displayResult(resultElementId, data || { message: `Deletion successful, status code: ${response.status}` });
198
+ }
199
+ // Clear input field
200
+ document.getElementById('delete-user-id').value = '';
201
+ } catch (error) {
202
+ displayError(resultElementId, error);
203
+ }
204
+ }
205
+ </script>
206
+ </body>
207
+ </html>