Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
| 機能 | 説明 |
| :--- | :--- |
| **スマートアップロード** | 衣類の写真をアップロードすると `rembg` で背景を自動除去し、AI ビジョンモデルでカテゴリ・色・スタイルを分析 |
| **天気連動スタイリング** | QWeather API と連携し、リアルタイムの天気に基づいたコーディネートを提案 |
| **天気連動スタイリング** | Open-Meteo の無料グローバル天気 API を利用し、リアルタイム天気に基づいたコーディネートを提案 |
| **デジタルワードローブ** | 構造化されたビューで衣類を閲覧・検索・管理 |
| **AI レコメンデーション** | Gemini や OpenAI 互換プロバイダーによるパーソナライズされたコーディネート生成 |
| **レスポンシブ UI** | Tailwind CSS によるモダンなインターフェースで、デスクトップ・タブレット・モバイルに対応 |
Expand Down Expand Up @@ -60,7 +60,6 @@

- **Node.js** `v20+`  |  **Python** `v3.10+`
- [Google Gemini API Key](https://aistudio.google.com/app/apikey) または OpenAI 互換 API キー
- [QWeather API Key](https://console.qweather.com)

### 1. クローンと設定

Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Upload clothing photos, remove backgrounds automatically, classify garments with
| Feature | Description |
| :--- | :--- |
| **Smart Upload** | Upload clothing photos, auto-remove backgrounds with `rembg`, analyze category, color, and style via AI vision models |
| **Weather-Based Styling** | Integrates QWeather API to generate outfit suggestions based on real-time conditions |
| **Weather-Based Styling** | Integrates free global weather data (Open-Meteo) to generate outfit suggestions based on real-time conditions |
| **Digital Wardrobe** | Browse, search, and manage your clothing in a structured wardrobe view |
| **AI Recommendations** | Supports Gemini and OpenAI-compatible providers for personalized outfit generation |
| **Responsive UI** | Optimized for desktop, tablet, and mobile with a modern Tailwind CSS interface |
Expand Down Expand Up @@ -60,7 +60,6 @@ Upload clothing photos, remove backgrounds automatically, classify garments with

- **Node.js** `v20+`  |  **Python** `v3.10+`
- [Google Gemini API Key](https://aistudio.google.com/app/apikey) or an OpenAI-compatible API key
- [QWeather API Key](https://console.qweather.com)

### 1. Clone & Configure

Expand Down
3 changes: 1 addition & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
| 特性 | 描述 |
| :--- | :--- |
| **智能上传** | 上传衣服照片后,使用 `rembg` 自动去背景,并通过视觉模型识别类别、颜色和风格 |
| **天气穿搭** | 集成和风天气 API,根据实时天气生成更合适的穿搭建议 |
| **天气穿搭** | 集成 Open-Meteo 免费全球天气接口,根据实时天气生成更合适的穿搭建议 |
| **虚拟衣柜** | 以结构化方式浏览、搜索和管理所有衣物 |
| **AI 推荐** | 支持 Gemini 和 OpenAI 风格接口,用于生成个性化穿搭方案 |
| **响应式界面** | 基于 Tailwind CSS,适配桌面、平板和手机 |
Expand Down Expand Up @@ -60,7 +60,6 @@

- **Node.js** `v20+`  |  **Python** `v3.10+`
- [Google Gemini API Key](https://aistudio.google.com/app/apikey) 或 OpenAI 兼容接口 Key
- [和风天气 API Key](https://console.qweather.com)

### 1. 克隆与配置

Expand Down
Binary file modified backend/api/__pycache__/upload.cpython-314.pyc
Binary file not shown.
5 changes: 3 additions & 2 deletions backend/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ async def set_config(config_update: LLMConfigUpdate):
model=config_update.model,
removebg_api_key=config_update.removebg_api_key,
bg_removal_method=config_update.bg_removal_method,
qweather_api_key=config_update.qweather_api_key,
qweather_api_host=config_update.qweather_api_host,
weather_location=config_update.weather_location,
zodiac_sign=config_update.zodiac_sign
)
return {
"success": True,
"message": "配置已更新",
"config": get_masked_config()
}
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

Expand Down
35 changes: 29 additions & 6 deletions backend/api/horoscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pydantic import BaseModel

from services.horoscope import get_daily_horoscope
from services.weather import get_weather
from services.weather import get_weather, normalize_location_request, DEFAULT_LOCATION_QUERY

router = APIRouter()

Expand All @@ -23,24 +23,47 @@ class HoroscopeResponse(BaseModel):
lucky_color: str
lucky_number: int
suggestion: str
source_provider: str
llm_status: str
llm_reasoning: str


@router.get("/horoscope/daily", response_model=HoroscopeResponse)
async def get_today_horoscope(
location: str = Query(
default="101020100",
description="LocationID 或 经纬度坐标"
default=DEFAULT_LOCATION_QUERY,
description="城市名 或 经纬度坐标"
),
city: Optional[str] = Query(default=None, description="城市(结构化查询参数)"),
state: Optional[str] = Query(default=None, description="省/州(结构化查询参数)"),
country: Optional[str] = Query(default=None, description="国家(结构化查询参数)"),
zodiac_sign: Optional[str] = Query(
default=None,
description="可选,若传入会覆盖设置中的星座"
)
),
include_inference: bool = Query(
default=True,
description="是否执行并返回 LLM 推理结果。false 时仅返回并缓存基础运势。"
),
):
"""
获取今日星座运势
"""
weather = await get_weather(location)
normalized_location, validation_error = normalize_location_request(
location=location,
city=city,
state=state,
country=country,
)
if validation_error:
raise HTTPException(status_code=422, detail=validation_error)

weather = await get_weather(normalized_location)
if not weather:
raise HTTPException(status_code=500, detail="获取天气信息失败")

return await get_daily_horoscope(weather=weather, zodiac_sign=zodiac_sign)
return await get_daily_horoscope(
weather=weather,
zodiac_sign=zodiac_sign,
include_inference=include_inference,
)
47 changes: 31 additions & 16 deletions backend/api/recommendation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,66 @@
"""
from fastapi import APIRouter, HTTPException, Query
from typing import Optional
from services.weather import get_weather
from services.weather import get_weather, normalize_location_request, DEFAULT_LOCATION_QUERY
from services.recommendation import get_ai_recommendation
from pydantic import BaseModel
from pydantic import BaseModel, Field

router = APIRouter()


class RecommendationResponse(BaseModel):
"""推荐响应"""
weather: dict
horoscope: Optional[dict] = None
temperature_rule: Optional[dict] = None
recommendation_text: str
outfit_summary: Optional[str] = None
selection_reasons: Optional[dict] = None
suggested_top: Optional[dict] = None
suggested_bottom: Optional[dict] = None
suggested_shoes: Optional[dict] = None
suggested_accessories: list[dict] = Field(default_factory=list)
purchase_suggestions: list[dict] = Field(default_factory=list)


@router.get("/recommendation", response_model=RecommendationResponse)
async def get_outfit_recommendation(
location: str = Query(
default="101020100",
description="LocationID 或 经纬度坐标(如 '116.41,39.92')"
)
default=DEFAULT_LOCATION_QUERY,
description="城市名 或 经纬度坐标(如 '31.23,121.47' 或 '121.47,31.23')"
),
city: Optional[str] = Query(default=None, description="城市(结构化查询参数)"),
state: Optional[str] = Query(default=None, description="省/州(结构化查询参数)"),
country: Optional[str] = Query(default=None, description="国家(结构化查询参数)"),
zodiac_sign: Optional[str] = Query(
default=None,
description="可选,临时指定星座(会覆盖设置中的星座)"
),
):
"""
获取AI穿搭推荐

参数:
location: LocationID(如 101010100=北京)或 经纬度坐标(如 116.41,39.92)

location: 城市名(如 上海、Tokyo)或 经纬度坐标(如 31.23,121.47)
返回:
天气信息 + AI推荐文本 + 推荐的衣服和裤子

常用城市 LocationID:
- 101010100: 北京
- 101020100: 上海
- 101280101: 广州
- 101280601: 深圳
- 101210101: 杭州
"""
normalized_location, validation_error = normalize_location_request(
location=location,
city=city,
state=state,
country=country,
)
if validation_error:
raise HTTPException(status_code=422, detail=validation_error)

# 获取天气信息
weather = await get_weather(location)
weather = await get_weather(normalized_location)

if not weather:
raise HTTPException(status_code=500, detail="获取天气信息失败")

# 获取AI推荐
recommendation = await get_ai_recommendation(weather)
recommendation = await get_ai_recommendation(weather, zodiac_sign=zodiac_sign)

return recommendation
10 changes: 8 additions & 2 deletions backend/api/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from services.removebg import remove_background_api
from services.openai_compatible import analyze_clothes_openai
from storage.config_store import load_config
from domain.clothes import ClothesSemantics, ClothesCreate, ClothesItem
from domain.clothes import ClothesSemantics, ClothesCreate, ClothesItem, normalize_category_value
from storage.db import add_clothes, get_clothes_by_id

router = APIRouter()
Expand All @@ -18,6 +18,8 @@
UPLOAD_DIR = Path(__file__).parent.parent / "uploads"
UPLOAD_DIR.mkdir(exist_ok=True)

ALLOWED_CATEGORIES = {"top", "bottom", "shoes", "accessory"}


@router.post("/upload", response_model=ClothesItem)
async def upload_image(file: UploadFile = File(...)):
Expand Down Expand Up @@ -68,9 +70,13 @@ async def upload_image(file: UploadFile = File(...)):
with open(filepath, "wb") as f:
f.write(processed_bytes)

normalized_category = normalize_category_value(semantics.category)
if normalized_category not in ALLOWED_CATEGORIES:
normalized_category = "accessory"

# 保存到数据库
clothes_data = ClothesCreate(
category=semantics.category,
category=normalized_category,
item=semantics.item,
style_semantics=semantics.style_semantics,
season_semantics=semantics.season_semantics,
Expand Down
25 changes: 14 additions & 11 deletions backend/api/wardrobe.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
衣柜 API - 获取和管理衣物
"""
from fastapi import APIRouter, HTTPException
from typing import Optional

from domain.clothes import ClothesItem, WardrobeResponse, ClothesCreate
from domain.clothes import normalize_category_value
from storage.db import (
get_all_clothes,
get_clothes_by_category,
get_clothes_by_id,
delete_clothes,
update_clothes
Expand All @@ -21,18 +20,20 @@ async def get_wardrobe():
"""
获取整个衣柜

按 top/bottom/shoes 三类返回所有衣物
按 top/bottom/shoes/accessory 四类返回所有衣物
"""
all_clothes = await get_all_clothes()

tops = [c for c in all_clothes if c.category == "top"]
bottoms = [c for c in all_clothes if c.category == "bottom"]
shoes = [c for c in all_clothes if c.category == "shoes"]
tops = [c for c in all_clothes if normalize_category_value(c.category) == "top"]
bottoms = [c for c in all_clothes if normalize_category_value(c.category) == "bottom"]
shoes = [c for c in all_clothes if normalize_category_value(c.category) == "shoes"]
accessories = [c for c in all_clothes if normalize_category_value(c.category) == "accessory"]

return WardrobeResponse(
tops=tops,
bottoms=bottoms,
shoes=shoes
shoes=shoes,
accessories=accessories
)


Expand All @@ -42,15 +43,17 @@ async def get_wardrobe_category(category: str):
按类别获取衣物

Args:
category: top, bottom, shoes
category: top, bottom, shoes, accessory
"""
if category not in ["top", "bottom", "shoes"]:
category = normalize_category_value(category)
if category not in ["top", "bottom", "shoes", "accessory"]:
raise HTTPException(
status_code=400,
detail="类别必须是 top, bottom 或 shoes"
detail="类别必须是 top, bottom, shoes 或 accessory"
)

return await get_clothes_by_category(category)
all_clothes = await get_all_clothes()
return [item for item in all_clothes if normalize_category_value(item.category) == category]


@router.get("/clothes/{clothes_id}", response_model=ClothesItem)
Expand Down
Loading
Loading