Spaces:
Runtime error
Runtime error
Commit
·
b369881
1
Parent(s):
1ef07a0
Deploy AI Virtual Try-On
Browse filesSolved the conflict in README file
- README.md +36 -0
- app.py +49 -0
- huggingface.yaml +14 -0
- requirements.txt +48 -0
- src/.DS_Store +0 -0
- src/__init__.py +0 -0
- src/__pycache__/__init__.cpython-310.pyc +0 -0
- src/__pycache__/__init__.cpython-311.pyc +0 -0
- src/database/app.db +0 -0
- src/main.py +46 -0
- src/models/__pycache__/user.cpython-310.pyc +0 -0
- src/models/__pycache__/user.cpython-311.pyc +0 -0
- src/models/user.py +18 -0
- src/routes/__pycache__/ai_agent.cpython-310.pyc +0 -0
- src/routes/__pycache__/ai_agent.cpython-311.pyc +0 -0
- src/routes/__pycache__/shopping_automation.cpython-310.pyc +0 -0
- src/routes/__pycache__/shopping_automation.cpython-311.pyc +0 -0
- src/routes/__pycache__/user.cpython-310.pyc +0 -0
- src/routes/__pycache__/user.cpython-311.pyc +0 -0
- src/routes/ai_agent.py +215 -0
- src/routes/shopping_automation.py +353 -0
- src/routes/user.py +39 -0
- src/static/favicon.ico +0 -0
- src/static/index.html +207 -0
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/<id>)</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/<id>)</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/<id>)</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>
|