Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- .gitattributes +2 -0
- app.py +982 -0
- fashion_key.json +13 -0
- final_prod_data.csv +3 -0
- final_review_df.csv +3 -0
- prod_emb_data.pkl +3 -0
- requirements.txt +16 -0
- size_sorted.csv +0 -0
- style_key_data.csv +49 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
final_prod_data.csv filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
final_review_df.csv filter=lfs diff=lfs merge=lfs -text
|
app.py
ADDED
|
@@ -0,0 +1,982 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""app.ipynb
|
| 3 |
+
|
| 4 |
+
Automatically generated by Colab.
|
| 5 |
+
|
| 6 |
+
Original file is located at
|
| 7 |
+
https://colab.research.google.com/drive/1hl-apQjNm2ijxBoDj-PV0ua7-4taVJo2
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
|
| 13 |
+
__import__('pysqlite3')
|
| 14 |
+
import sys
|
| 15 |
+
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
|
| 16 |
+
|
| 17 |
+
import shutil
|
| 18 |
+
import torch
|
| 19 |
+
import re
|
| 20 |
+
import chromadb
|
| 21 |
+
from langchain_chroma import Chroma
|
| 22 |
+
import pandas as pd
|
| 23 |
+
import gradio as gr
|
| 24 |
+
import ast
|
| 25 |
+
import numpy as np
|
| 26 |
+
from PIL import Image
|
| 27 |
+
from collections import Counter, defaultdict
|
| 28 |
+
from transformers import CLIPProcessor, CLIPModel
|
| 29 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 30 |
+
from langchain_openai import ChatOpenAI
|
| 31 |
+
from langchain_core.prompts import PromptTemplate
|
| 32 |
+
from sentence_transformers import util
|
| 33 |
+
|
| 34 |
+
import sys
|
| 35 |
+
import gspread
|
| 36 |
+
from oauth2client.service_account import ServiceAccountCredentials
|
| 37 |
+
import datetime
|
| 38 |
+
import json
|
| 39 |
+
|
| 40 |
+
# [1] 구글 시트 연결 설정 함수
|
| 41 |
+
def get_google_sheet():
|
| 42 |
+
# 1. 인증 범위 설정
|
| 43 |
+
scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
|
| 44 |
+
# 2. 다운받은 키 파일 이름 (같은 폴더에 있어야 함)
|
| 45 |
+
creds = ServiceAccountCredentials.from_json_keyfile_name('fashion_key.json', scope)
|
| 46 |
+
client = gspread.authorize(creds)
|
| 47 |
+
|
| 48 |
+
# 3. 스프레드시트 열기 (공유한 파일 이름 정확히 입력!)
|
| 49 |
+
sh = client.open('패션추천시스템_사용자정보')
|
| 50 |
+
return sh
|
| 51 |
+
|
| 52 |
+
# [2] 검색 데이터 저장 함수 (시트1에 저장)
|
| 53 |
+
def log_search_to_sheet(bg, cat, min_p, max_p, style, txt, check, persona):
|
| 54 |
+
try:
|
| 55 |
+
sh = get_google_sheet()
|
| 56 |
+
worksheet = sh.get_worksheet(0) # 첫 번째 시트 선택
|
| 57 |
+
|
| 58 |
+
# 헤더가 없으면 추가 (옵션)
|
| 59 |
+
if not worksheet.row_values(1):
|
| 60 |
+
worksheet.append_row(['시간', '브랜드', '카테고리', '최소가격', '최대가격', '스타일', '텍스트', '성별교차', '결과페르소나'])
|
| 61 |
+
|
| 62 |
+
# 데이터 추가
|
| 63 |
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 64 |
+
worksheet.append_row([timestamp, bg, cat, min_p, max_p, style, txt, "O" if check else "X", persona])
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"구글 시트 저장 실패: {e}")
|
| 67 |
+
|
| 68 |
+
# [3] 사이즈 추천 데이터 저장 함수 (시트2에 저장)
|
| 69 |
+
def log_size_to_sheet(sel_product, u_info, rec_size, reason):
|
| 70 |
+
try:
|
| 71 |
+
sh = get_google_sheet()
|
| 72 |
+
# 두 번째 시트가 없으면 생성, 있으면 가져오기
|
| 73 |
+
try:
|
| 74 |
+
worksheet = sh.get_worksheet(1)
|
| 75 |
+
except:
|
| 76 |
+
worksheet = sh.add_worksheet(title="사이즈추천", rows="100", cols="20")
|
| 77 |
+
|
| 78 |
+
if not worksheet.row_values(1):
|
| 79 |
+
worksheet.append_row(['시간', '선택상품', '유저정보', '추천사이즈', '추천이유'])
|
| 80 |
+
|
| 81 |
+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 82 |
+
user_info_str = json.dumps(u_info, ensure_ascii=False)
|
| 83 |
+
worksheet.append_row([timestamp, sel_product, user_info_str, rec_size, reason])
|
| 84 |
+
except Exception as e:
|
| 85 |
+
print(f"구글 시트 저장 실패: {e}")
|
| 86 |
+
|
| 87 |
+
os.environ["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY")
|
| 88 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 89 |
+
|
| 90 |
+
base_path = "."
|
| 91 |
+
|
| 92 |
+
product_db_path = os.path.join(base_path, "product_db")
|
| 93 |
+
review_db_path = os.path.join(base_path, "review_db")
|
| 94 |
+
style_db_path = os.path.join(base_path, "style_db")
|
| 95 |
+
|
| 96 |
+
product_csv_path = os.path.join(base_path, "final_prod_data.csv")
|
| 97 |
+
size_csv_path = os.path.join(base_path, "size_sorted.csv")
|
| 98 |
+
review_csv_path = os.path.join(base_path, "final_review_df.csv")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
prod_data = pd.read_csv(product_csv_path)
|
| 103 |
+
prod_data['primary_id'] = prod_data['primary_id'].astype(str)
|
| 104 |
+
except:
|
| 105 |
+
prod_data = pd.DataFrame()
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
prod_size_df = pd.read_csv(size_csv_path)
|
| 109 |
+
prod_size_df = prod_size_df.replace('-', np.nan)
|
| 110 |
+
prod_size_df['primary_id'] = prod_size_df['primary_id'].astype(str)
|
| 111 |
+
except:
|
| 112 |
+
prod_size_df = pd.DataFrame()
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
review_df = pd.read_csv(review_csv_path)
|
| 116 |
+
review_df['primary_id'] = review_df['primary_id'].astype(int)
|
| 117 |
+
review_ids = set(review_df['primary_id'].astype(str))
|
| 118 |
+
except:
|
| 119 |
+
print(f"리뷰 데이터 로드 실패: {e}")
|
| 120 |
+
review_ids = set()
|
| 121 |
+
|
| 122 |
+
clip_model = CLIPModel.from_pretrained("patrickjohncyh/fashion-clip").to(device)
|
| 123 |
+
clip_processor = CLIPProcessor.from_pretrained("patrickjohncyh/fashion-clip")
|
| 124 |
+
|
| 125 |
+
review_emb_func = HuggingFaceEmbeddings(
|
| 126 |
+
model_name='jhgan/ko-sroberta-multitask',
|
| 127 |
+
model_kwargs={'device': device},
|
| 128 |
+
encode_kwargs={'normalize_embeddings': True}
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
llm = ChatOpenAI(model = "gpt-4o-mini", temperature=0.7) # 결과값 확인 후 temperature 값 조정 필요
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
product_client = chromadb.PersistentClient(path=product_db_path)
|
| 135 |
+
product_collection = product_client.get_collection("product_vectorstore")
|
| 136 |
+
except Exception as e:
|
| 137 |
+
print(f"{e} DB 연결 실패")
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
style_client = chromadb.PersistentClient(path=style_db_path)
|
| 141 |
+
style_collection = style_client.get_collection("style_vectorstore")
|
| 142 |
+
except Exception as e:
|
| 143 |
+
print(f"{e} DB 연결 실패")
|
| 144 |
+
|
| 145 |
+
try:
|
| 146 |
+
review_db = Chroma(
|
| 147 |
+
persist_directory=review_db_path,
|
| 148 |
+
embedding_function=review_emb_func,
|
| 149 |
+
collection_name="review_vectorstore"
|
| 150 |
+
)
|
| 151 |
+
except Exception as e:
|
| 152 |
+
print(f"{e} DB 연결 실패")
|
| 153 |
+
|
| 154 |
+
def get_style_options():
|
| 155 |
+
try:
|
| 156 |
+
data = style_collection.get(include=["metadatas"], limit=9999)
|
| 157 |
+
names = [m.get("display_name") for m in data["metadatas"] if m.get("display_name")]
|
| 158 |
+
names = sorted(set(names))
|
| 159 |
+
|
| 160 |
+
return ["선택 안 함"] + names
|
| 161 |
+
|
| 162 |
+
except Exception as e:
|
| 163 |
+
print("ERROR in get_style_options:", e)
|
| 164 |
+
return ["선택 안 함"]
|
| 165 |
+
|
| 166 |
+
style_key_options = get_style_options()
|
| 167 |
+
|
| 168 |
+
'''
|
| 169 |
+
추천 상품의 사이즈와 유사도 점수 가장 높은 상품의 추천 사이즈 간 유클리디언 거리 측정
|
| 170 |
+
(단, 추천 상품의 사이즈 데이터가 하나 밖에 없을 경우 상세 사이즈 수치만 제시)
|
| 171 |
+
'''
|
| 172 |
+
def set_fin_size(id, size_label):
|
| 173 |
+
'''
|
| 174 |
+
[1단계] 유사도 점수가 가장 높은 상품 사이즈의 row 가져오기
|
| 175 |
+
'''
|
| 176 |
+
if prod_size_df.empty:
|
| 177 |
+
return None
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
r = prod_size_df[
|
| 181 |
+
(prod_size_df['primary_id'] == str(id)) &
|
| 182 |
+
(prod_size_df['size_cleaned'].astype(str) == str(size_label))
|
| 183 |
+
]
|
| 184 |
+
if r.empty:
|
| 185 |
+
return None
|
| 186 |
+
|
| 187 |
+
numeric_cols = r.select_dtypes(include=[np.number]).columns.tolist()
|
| 188 |
+
numeric_cols = [c for c in numeric_cols if c not in ['primary_id']]
|
| 189 |
+
|
| 190 |
+
return r.iloc[0][numeric_cols].to_dict()
|
| 191 |
+
except:
|
| 192 |
+
return None
|
| 193 |
+
|
| 194 |
+
def find_closest_size(target_id, ideal_spec):
|
| 195 |
+
'''
|
| 196 |
+
[2단계] 추천 상품의 사이즈 중 ideal_spec에 가장 가까운 사이즈 찾기
|
| 197 |
+
'''
|
| 198 |
+
if prod_size_df.empty or not ideal_spec:
|
| 199 |
+
return None
|
| 200 |
+
|
| 201 |
+
target_r = prod_size_df[prod_size_df['primary_id'] == str(target_id)]
|
| 202 |
+
if target_r.empty:
|
| 203 |
+
return None
|
| 204 |
+
|
| 205 |
+
if len(target_r) == 1:
|
| 206 |
+
only_size = target_r.iloc[0]['size_cleaned']
|
| 207 |
+
return only_size
|
| 208 |
+
|
| 209 |
+
if not ideal_spec:
|
| 210 |
+
return None
|
| 211 |
+
|
| 212 |
+
min_dist = float('inf')
|
| 213 |
+
best_size = None
|
| 214 |
+
common_keys = [k for k in ideal_spec.keys() if k in target_r.columns]
|
| 215 |
+
|
| 216 |
+
if not common_keys:
|
| 217 |
+
return None
|
| 218 |
+
|
| 219 |
+
for _, row in target_r.iterrows():
|
| 220 |
+
try:
|
| 221 |
+
c_specs = [float(ideal_spec[k]) for k in common_keys if pd.notna(row[k])]
|
| 222 |
+
t_specs = [float(row[k]) for k in common_keys if pd.notna(row[k])]
|
| 223 |
+
|
| 224 |
+
if not c_specs or len(c_specs) != len(t_specs):
|
| 225 |
+
continue
|
| 226 |
+
|
| 227 |
+
dist = np.linalg.norm(np.array(c_specs) - np.array(t_specs))
|
| 228 |
+
if dist < min_dist:
|
| 229 |
+
min_dist = dist
|
| 230 |
+
best_size = row['size_cleaned']
|
| 231 |
+
except:
|
| 232 |
+
continue
|
| 233 |
+
|
| 234 |
+
return best_size
|
| 235 |
+
|
| 236 |
+
def search_recom_products(brand_lb, cat,
|
| 237 |
+
sel_key, txt, img, user_info,
|
| 238 |
+
min_price, max_price, check_option):
|
| 239 |
+
#=================================================================================
|
| 240 |
+
# 추천 1단계: 프롬프트에 입력된 스타일 값을 바탕으로 벡터 유사도 계산 후 top-5개 추천
|
| 241 |
+
#================================================================================
|
| 242 |
+
query_vec, source = get_search_vector(sel_key, txt, img)
|
| 243 |
+
if query_vec is None:
|
| 244 |
+
return [], "검색 조건을 입력하세요", {}, "", []
|
| 245 |
+
|
| 246 |
+
gender_option = [user_info['gender'], 'all'] # 남성: male, 여성: female, 공용: all nan: all로 바꾸기
|
| 247 |
+
|
| 248 |
+
filter = {
|
| 249 |
+
"$and": [
|
| 250 |
+
{"brand_label": {"$eq": brand_lb}},
|
| 251 |
+
{"sub_category": {"$eq": cat}}
|
| 252 |
+
]
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
if min_price is not None and max_price is not None:
|
| 256 |
+
if min_price > max_price:
|
| 257 |
+
min_price, max_price = max_price, min_price
|
| 258 |
+
filter["$and"].append({"price": {"$gte": min_price}})
|
| 259 |
+
filter["$and"].append({"price": {"$lte": max_price}})
|
| 260 |
+
|
| 261 |
+
if not check_option:
|
| 262 |
+
filter["$and"].append({"gender": {"$in": gender_option}})
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
candidates = product_collection.query(
|
| 266 |
+
query_embeddings=[query_vec],
|
| 267 |
+
n_results=20,
|
| 268 |
+
where=filter,
|
| 269 |
+
include=["metadatas", "embeddings", "distances"]
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
if not candidates['ids'][0]:
|
| 273 |
+
return [], "조건에 맞는 상품이 없습니다.", {}, "", []
|
| 274 |
+
|
| 275 |
+
candidate_list_text = ""
|
| 276 |
+
candidate_map = {}
|
| 277 |
+
|
| 278 |
+
for i, meta in enumerate(candidates['metadatas'][0]):
|
| 279 |
+
p_id = meta.get('primary_id')
|
| 280 |
+
name = meta.get('product_name')
|
| 281 |
+
brand = meta.get('brand')
|
| 282 |
+
price = meta.get('price')
|
| 283 |
+
dist = candidates['distances'][0][i]
|
| 284 |
+
sim = 1 / (1 + dist) # 유사도 점수로 변환
|
| 285 |
+
|
| 286 |
+
candidate_list_text += f"[{i+1}] ID: {p_id} | 브랜드: {brand} | 제품명: {name} | 스타일유사도: {sim:.4f}\n"
|
| 287 |
+
|
| 288 |
+
candidate_map[str(i+1)] = {
|
| 289 |
+
"id": p_id, "name": name, "brand": brand, "price": price,
|
| 290 |
+
"cat": meta.get('sub_category'), "brand_lb": meta.get('brand_label'),
|
| 291 |
+
"idx": i
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
print(candidate_map)
|
| 295 |
+
style_requests = []
|
| 296 |
+
|
| 297 |
+
if txt:
|
| 298 |
+
style_requests.append(f"텍스트 요청: '{txt}'")
|
| 299 |
+
|
| 300 |
+
if sel_key and sel_key != '선택 안 함':
|
| 301 |
+
style_key_data = style_collection.get(
|
| 302 |
+
where={"display_name": {"$eq": sel_key}},
|
| 303 |
+
include=["metadatas"]
|
| 304 |
+
)
|
| 305 |
+
if style_key_data["metadatas"]:
|
| 306 |
+
style_desc = style_key_data["metadatas"][0].get("description")
|
| 307 |
+
if style_desc:
|
| 308 |
+
style_requests.append(f"선택 스타일 키워드 설명: '{style_desc}'")
|
| 309 |
+
|
| 310 |
+
if img is not None:
|
| 311 |
+
style_requests.append("이미지 기반 스타일 검색 포함")
|
| 312 |
+
|
| 313 |
+
if style_requests:
|
| 314 |
+
user_input_str = ", ".join(style_requests)
|
| 315 |
+
|
| 316 |
+
# 브랜드 라벨도 추가해서 특정 무드의 브랜드들을 선호하는 소비자의 성향 반영 필
|
| 317 |
+
selection_persona_prompt = PromptTemplate.from_template("""
|
| 318 |
+
당신은 전문 패션 MD입니다.
|
| 319 |
+
사용자 프로필과 스타일 유사도 데이터를 바탕으로 선정된 후보 목록 20개를 분석하여, 가장 적합한 **제품 5개를 선정**하고 **패션 페르소나**를 정의하세요.
|
| 320 |
+
|
| 321 |
+
[사용자 프로필]
|
| 322 |
+
- 성별/체형: {g} / {h}cm / {w}kg
|
| 323 |
+
- 선호 스타일 요청: "{s}"
|
| 324 |
+
|
| 325 |
+
[후보 제품 목록 (유사도 순)]
|
| 326 |
+
{c_list}
|
| 327 |
+
|
| 328 |
+
[사고 과정 (Chain of thought)]
|
| 329 |
+
1. 사용자 분석: 사용자 프로필 정보를 고려하여 사용자의 성별/체형에서 사용자의 선호 스타일을 표현하기 위해서는 어떤 실루엣, 소재, 디자인이 가장 적합한지를 기준을 세우세요.
|
| 330 |
+
2. 추천 제품 선정: 위에서 세운 기준에 근거하여 사용자에게 가장 적합한 제품 5개를 선정하세요. (단순 유사도 순이 아닌, 스타일 적합성 우선)
|
| 331 |
+
3. 페르소나 도출: 선별된 5개 제품과 사용자의 특징을 종합하여, 이 사용자가 추구하는 패션 아이덴티티(페르소나)를 구체적인 코디 예시를 들어 묘사합니다.
|
| 332 |
+
|
| 333 |
+
[출력 형식 및 제약 사]
|
| 334 |
+
1. 사고 과정(설명)은 출력하지 말고, 오직 **결과 데이터**만 아래 형식으로 출력하세요.
|
| 335 |
+
2. 제품번호는 후보 목록에 표시된 **순번(숫자)**만 적으세요.
|
| 336 |
+
|
| 337 |
+
SELECTED_IDS = [순번1, 순번2, 순번3, 순번4, 순번5]
|
| 338 |
+
|||
|
| 339 |
+
PERSONA = (여기에 3줄 내외의 페르소나 정의 작성)
|
| 340 |
+
""")
|
| 341 |
+
|
| 342 |
+
print("LLM이 제품을 선별 및 페르소나 생성 중...")
|
| 343 |
+
selection_response = llm.invoke(selection_persona_prompt.format(
|
| 344 |
+
g=user_info['gender'], h=user_info['height'], w=user_info['weight'],
|
| 345 |
+
s=user_input_str, c_list=candidate_list_text
|
| 346 |
+
)).content
|
| 347 |
+
|
| 348 |
+
try:
|
| 349 |
+
parts = selection_response.split("|||")
|
| 350 |
+
|
| 351 |
+
ids_text = parts[0]
|
| 352 |
+
selected_idx = re.findall(r'\d+', ids_text)
|
| 353 |
+
selected_idx = [idx for idx in selected_idx if idx in candidate_map]
|
| 354 |
+
|
| 355 |
+
persona_text = parts[1].replace("PERSONA =", "").strip()
|
| 356 |
+
|
| 357 |
+
except Exception as e:
|
| 358 |
+
print(f"파싱 오류: {e}")
|
| 359 |
+
|
| 360 |
+
if not selected_idx:
|
| 361 |
+
selected_idx = list(candidate_map.keys())[:5]
|
| 362 |
+
|
| 363 |
+
print("LLM 선택 응답:", selection_response)
|
| 364 |
+
print("candidate_map keys:", list(candidate_map.keys()))
|
| 365 |
+
print("selected_idx after filtering:", selected_idx)
|
| 366 |
+
|
| 367 |
+
user_info['p'] = persona_text
|
| 368 |
+
|
| 369 |
+
final_recom_item = []
|
| 370 |
+
p_map = {}
|
| 371 |
+
|
| 372 |
+
for idx_str in selected_idx[:5]:
|
| 373 |
+
item_data = candidate_map[idx_str]
|
| 374 |
+
img_url = get_img_url(item_data['id'])
|
| 375 |
+
label = f"[{item_data['brand']}]_{item_data['name']}] - {item_data['price']}원"
|
| 376 |
+
|
| 377 |
+
final_recom_item.append((img_url, label))
|
| 378 |
+
final_recom_item_gr = [[url if url else "", str(label)] for url, label in final_recom_item]
|
| 379 |
+
# Gradio Gallery, Image에 데이터를 넣을 때는 list 형태로 들어가야 됨
|
| 380 |
+
p_map[label] = item_data
|
| 381 |
+
|
| 382 |
+
print("Gallery 데이터:", final_recom_item_gr)
|
| 383 |
+
return final_recom_item_gr, user_info['p'], p_map, user_info
|
| 384 |
+
|
| 385 |
+
def get_sim_products(target_id, category, brand_label=None, limit=5):
|
| 386 |
+
#========================================================================================
|
| 387 |
+
# 추천 2단계: 사이즈 추천 로직
|
| 388 |
+
# (추천 제품과 유사도 높은 제품 구하는 로직: 추천 제품의 리뷰가 임계치 이하일 경우 로직 작동)
|
| 389 |
+
#========================================================================================
|
| 390 |
+
target_data = product_collection.get(
|
| 391 |
+
ids=[target_id], include=['embeddings', 'metadatas'])
|
| 392 |
+
|
| 393 |
+
embs = target_data.get('embeddings')
|
| 394 |
+
if embs is None or len(embs) == 0:
|
| 395 |
+
return {}
|
| 396 |
+
|
| 397 |
+
target_img = torch.tensor(embs[0], dtype=torch.float32)
|
| 398 |
+
try:
|
| 399 |
+
meta_size = target_data['metadatas'][0].get('size_vec', '[]') #오 뭐야 왜 size_vec이 없냐
|
| 400 |
+
target_size = torch.tensor(ast.literal_eval(meta_size), dtype=torch.float32)
|
| 401 |
+
except:
|
| 402 |
+
return {}
|
| 403 |
+
|
| 404 |
+
review_ids_list = list(review_ids)
|
| 405 |
+
filter = {"$and": [{"sub_category": {"$eq": category}},
|
| 406 |
+
{"primary_id": {"$in": review_ids_list}}, # primary_key save_type 문제 flaot타입으로 저장되어서 맞는게 없음
|
| 407 |
+
{"review_count": { "$gt": 0 }}]}
|
| 408 |
+
|
| 409 |
+
if brand_label is not None:
|
| 410 |
+
filter["$and"].append({"brand_label": {"$eq": brand_label}})
|
| 411 |
+
|
| 412 |
+
candidates = product_collection.get(
|
| 413 |
+
where=filter, include=['embeddings', 'metadatas'])
|
| 414 |
+
|
| 415 |
+
cand_ids = candidates.get('ids')
|
| 416 |
+
if cand_ids is None or len(cand_ids) == 0:
|
| 417 |
+
return {}
|
| 418 |
+
|
| 419 |
+
valid_idx, candi_img, candi_size = [], [], []
|
| 420 |
+
for i, p_id in enumerate(cand_ids):
|
| 421 |
+
try:
|
| 422 |
+
v_str = candidates['metadatas'][i].get('size_vec')
|
| 423 |
+
if v_str:
|
| 424 |
+
v = ast.literal_eval(v_str)
|
| 425 |
+
if v:
|
| 426 |
+
candi_size.append(v)
|
| 427 |
+
candi_img.append(candidates['embeddings'][i])
|
| 428 |
+
valid_idx.append(i)
|
| 429 |
+
except:
|
| 430 |
+
continue
|
| 431 |
+
|
| 432 |
+
if not valid_idx:
|
| 433 |
+
return {}
|
| 434 |
+
|
| 435 |
+
cand_img_vec = torch.tensor(np.array(candi_img), dtype=torch.float32)
|
| 436 |
+
cand_size_vec = torch.tensor(np.array(candi_size), dtype=torch.float32)
|
| 437 |
+
|
| 438 |
+
scores = (util.cos_sim(target_img, cand_img_vec)[0] * 0.6) + \
|
| 439 |
+
(util.cos_sim(target_size, cand_size_vec)[0] * 0.4)
|
| 440 |
+
|
| 441 |
+
top_k = torch.argsort(scores, descending=True)[:limit].tolist()
|
| 442 |
+
|
| 443 |
+
result_map = {}
|
| 444 |
+
for i in top_k:
|
| 445 |
+
real_idx = valid_idx[i]
|
| 446 |
+
p_id = cand_ids[real_idx]
|
| 447 |
+
score = float(scores[i])
|
| 448 |
+
result_map[p_id] = score
|
| 449 |
+
|
| 450 |
+
print(result_map)
|
| 451 |
+
|
| 452 |
+
return result_map
|
| 453 |
+
|
| 454 |
+
def recom_size(label, p_map, user_info):
|
| 455 |
+
'''
|
| 456 |
+
추천 2단계: 사이즈 추천 로직
|
| 457 |
+
'''
|
| 458 |
+
# 유저가 상품 선택 후 선택 상품의 한해 사이즈 추천 진행
|
| 459 |
+
if not label:
|
| 460 |
+
return "상품을 먼저 선택해주세요"
|
| 461 |
+
if label not in p_map:
|
| 462 |
+
return f"오류: 선택한 제품 '{label}'의 데이터를 찾을 수 없습니다.\n(검색을 다시 하거나 다른 제품을 선택해보세요)", ""
|
| 463 |
+
|
| 464 |
+
if user_info is None:
|
| 465 |
+
user_info = {}
|
| 466 |
+
else:
|
| 467 |
+
user_info = dict(user_info)
|
| 468 |
+
|
| 469 |
+
target_id, cat, bl = p_map[label]['id'], p_map[label]['cat'], p_map[label]['brand_lb']
|
| 470 |
+
|
| 471 |
+
# 유저 페르소나의 리뷰와 유사도 계산
|
| 472 |
+
hyde_prompt = PromptTemplate.from_template("""
|
| 473 |
+
당신은 지금부터 아래 프로필을 가진 사용자가 되어, 방금 구매한 옷에 대해 **만족한 후기**를 작성해야 합니다.
|
| 474 |
+
|
| 475 |
+
[사용자 프로필]
|
| 476 |
+
- 성별: {g}
|
| 477 |
+
- 신체 스펙: {h}cm / {w}kg
|
| 478 |
+
- 사용자 페르소나: {p}
|
| 479 |
+
|
| 480 |
+
[구매한 상품 정보]
|
| 481 |
+
- 제품 기본 정보: {p_info}
|
| 482 |
+
- 카테고리: {cat} (예: 후드티, 와이드 슬랙스)
|
| 483 |
+
|
| 484 |
+
[작성 지침]
|
| 485 |
+
1. 당신의 신체 스펙과 선호 스타일을 고려하여 사용자가 제품 핏에 만족한다는 가정 하에 리뷰를 작성하세요.
|
| 486 |
+
2. **주의:** S, M, L 같은 **구체적인 사이즈(옵션명)는 절대 언급하지 마세요.** 오직 '핏감(Fit)'과 '착용감'에 대해서만 이야기하세요.
|
| 487 |
+
3. 리뷰는 구매한 상품 정보에 기반하여 작성하세요.
|
| 488 |
+
4. 말투는 자연스러운 한국어 구어체(리뷰톤)를 사용하며 3줄 이내로 작성하세요.
|
| 489 |
+
""")
|
| 490 |
+
|
| 491 |
+
hyde_txt = llm.invoke(hyde_prompt.format(
|
| 492 |
+
h=user_info.get('height',''), w=user_info.get('weight',''),
|
| 493 |
+
g=user_info['gender'], p=user_info.get('p',''),
|
| 494 |
+
p_info=label, cat=cat)).content
|
| 495 |
+
hyde_vec = review_emb_func.embed_query(hyde_txt)
|
| 496 |
+
print(hyde_txt)
|
| 497 |
+
|
| 498 |
+
product_score_map = {str(target_id): 1.0} # 상품 유사도 저장 (해당 추천 상품은 유사도 1)
|
| 499 |
+
|
| 500 |
+
search_pool_ids = [str(target_id)]
|
| 501 |
+
method_log = f"해당 추천 상품"
|
| 502 |
+
|
| 503 |
+
sim_ids_2 = get_sim_products(target_id, cat, brand_label=bl, limit=5)
|
| 504 |
+
if sim_ids_2:
|
| 505 |
+
product_score_map.update(sim_ids_2)
|
| 506 |
+
search_pool_ids.extend(sim_ids_2.keys())
|
| 507 |
+
method_log += f" + 같은 브랜드 그룹 내 유사도 높은 상품 {len(sim_ids_2)}개"
|
| 508 |
+
|
| 509 |
+
if len(search_pool_ids) < 3:
|
| 510 |
+
sim_ids_3 = get_sim_products(target_id, cat, brand_label=None, limit=5)
|
| 511 |
+
|
| 512 |
+
# 중복 제거
|
| 513 |
+
for id, score in sim_ids_3.items():
|
| 514 |
+
if id not in product_score_map:
|
| 515 |
+
product_score_map[id] = score
|
| 516 |
+
search_pool_ids.extend(sim_ids_2.keys())
|
| 517 |
+
method_log = f" + 같은 카테고리 내 유사도 높은 상품 {len(sim_ids_3)}개"
|
| 518 |
+
|
| 519 |
+
gender = user_info.get('gender', 'male')
|
| 520 |
+
height = int(user_info.get('height'))
|
| 521 |
+
weight = int(user_info.get('weight'))
|
| 522 |
+
|
| 523 |
+
min_h, max_h = height - 3, height + 3
|
| 524 |
+
min_w, max_w = weight - 3, weight + 3
|
| 525 |
+
|
| 526 |
+
final_docs_raw = review_db.similarity_search_by_vector(
|
| 527 |
+
hyde_vec,
|
| 528 |
+
k=30,
|
| 529 |
+
filter={
|
| 530 |
+
"$and": [
|
| 531 |
+
{"product_id": {"$in": search_pool_ids}}, # 리뷰 데이터 primary_id vectorDB 저장 시 float --> int로 타입 변환 후 str 씌우기!!!!!!!!
|
| 532 |
+
{"gender": {"$eq": gender}},
|
| 533 |
+
{"height": {"$gte": min_h}},
|
| 534 |
+
{"height": {"$lte": max_h}},
|
| 535 |
+
{"weight": {"$gte": min_w}},
|
| 536 |
+
{"weight": {"$lte": max_w}}
|
| 537 |
+
]
|
| 538 |
+
})
|
| 539 |
+
|
| 540 |
+
if not final_docs_raw:
|
| 541 |
+
relaxed_filter = {
|
| 542 |
+
"$and": [
|
| 543 |
+
{"product_id": {"$in": search_pool_ids}},
|
| 544 |
+
{"gender": {"$eq": gender}}
|
| 545 |
+
]
|
| 546 |
+
}
|
| 547 |
+
final_docs_raw = review_db.similarity_search_by_vector(hyde_vec, k=30, filter=relaxed_filter) # 리스트 타입 반환 [doc1, doc2, doc3...]
|
| 548 |
+
|
| 549 |
+
if not final_docs_raw:
|
| 550 |
+
return "유사 리뷰 데이터 부족", method_log
|
| 551 |
+
|
| 552 |
+
print(final_docs_raw)
|
| 553 |
+
best_doc, max_score, weight_docs = None, -1e9 , []
|
| 554 |
+
size_score = defaultdict(float)
|
| 555 |
+
size_count = defaultdict(int)
|
| 556 |
+
|
| 557 |
+
valid_doc_cnt = 0
|
| 558 |
+
|
| 559 |
+
for doc in final_docs_raw:
|
| 560 |
+
meta = getattr(doc, 'metadata', {}) or {}
|
| 561 |
+
size = meta.get('size')
|
| 562 |
+
if size:
|
| 563 |
+
size_count[size] += 1
|
| 564 |
+
valid_doc_cnt += 1
|
| 565 |
+
else:
|
| 566 |
+
continue
|
| 567 |
+
if valid_doc_cnt == 0:
|
| 568 |
+
return "best_doc is not found", method_log
|
| 569 |
+
|
| 570 |
+
for doc in final_docs_raw:
|
| 571 |
+
meta = getattr(doc, 'metadata', {}) or {}
|
| 572 |
+
prod_id = meta.get('product_id')
|
| 573 |
+
size = meta.get('size')
|
| 574 |
+
#print("DOC META:", meta) # 디버깅용 코드
|
| 575 |
+
#print("prod_id:", prod_id, 'size:', size)
|
| 576 |
+
|
| 577 |
+
if not prod_id or not size:
|
| 578 |
+
continue
|
| 579 |
+
|
| 580 |
+
dis = meta.get("score", 0.0) # 상품 유사도
|
| 581 |
+
review_sim = max(0.0, 1.0 - dis) # 리뷰 유사도
|
| 582 |
+
s_cnt = size_count[size]
|
| 583 |
+
size_ratio_score =(1 + s_cnt / valid_doc_cnt)
|
| 584 |
+
|
| 585 |
+
prod_id = str(prod_id)
|
| 586 |
+
prod_sim = float(product_score_map.get(prod_id, 0.5)) # 상품 유사
|
| 587 |
+
w_score = prod_sim * review_sim * size_ratio_score # 가중 점수에 (추천 리뷰 사이즈 개수 / 전체 사이즈 개수)까지 곱해주기
|
| 588 |
+
|
| 589 |
+
weight_docs.append({
|
| 590 |
+
"doc": doc,
|
| 591 |
+
"id": prod_id,
|
| 592 |
+
"weight": w_score,
|
| 593 |
+
"size": size
|
| 594 |
+
})
|
| 595 |
+
|
| 596 |
+
size_score[size] += w_score
|
| 597 |
+
|
| 598 |
+
if w_score > max_score:
|
| 599 |
+
max_score = w_score
|
| 600 |
+
best_doc = {
|
| 601 |
+
"product_id": prod_id,
|
| 602 |
+
"size": size
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
if not best_doc:
|
| 606 |
+
return "best_doc is not found", method_log
|
| 607 |
+
|
| 608 |
+
ref_p, ref_sz = best_doc['product_id'], best_doc['size']
|
| 609 |
+
ideal_spec = set_fin_size(ref_p, ref_sz) # 가장 유사한 제품 사이즈 정보 가져오기
|
| 610 |
+
|
| 611 |
+
final_sz, info = ref_sz, "(유사 제품 사이즈 추천)"
|
| 612 |
+
|
| 613 |
+
# 추천 상품 사이즈 옵션 개수 확인
|
| 614 |
+
target_r = prod_size_df[prod_size_df['primary_id'] == str(target_id)]
|
| 615 |
+
size_cnt = len(target_r)
|
| 616 |
+
|
| 617 |
+
if size_cnt == 1:
|
| 618 |
+
final_sz = target_r.iloc[0]['size_cleaned']
|
| 619 |
+
info = "(원사이즈 제품)"
|
| 620 |
+
elif ideal_spec:
|
| 621 |
+
actual_sz = find_closest_size(target_id, ideal_spec)
|
| 622 |
+
if actual_sz:
|
| 623 |
+
final_sz = actual_sz
|
| 624 |
+
info = f"(유사 제품 {ref_p} [{ref_sz}] 실측 기반 매핑)"
|
| 625 |
+
|
| 626 |
+
weight_docs.sort(key=lambda x: x['weight'], reverse=True)
|
| 627 |
+
|
| 628 |
+
evidence_txt = '\n'.join([
|
| 629 |
+
f"-[{doc['size']}] {doc['doc'].page_content[:80]}...({doc['weight']:.2f})"
|
| 630 |
+
for doc in weight_docs[:5]
|
| 631 |
+
])
|
| 632 |
+
|
| 633 |
+
detail_log = "\n".join([
|
| 634 |
+
f"[{i+1}위] ID: {d['id']} ({d['size']}) --> {d['weight']:.2f}점"
|
| 635 |
+
for i, d in enumerate(weight_docs[:10])
|
| 636 |
+
])
|
| 637 |
+
|
| 638 |
+
score_dist = ", ".join([
|
| 639 |
+
f"{k}: {v:.1f}" for k, v in sorted(size_score.items(), key=lambda x:x[1], reverse=True)
|
| 640 |
+
])
|
| 641 |
+
|
| 642 |
+
# 추천 근거 생성 LLM (상세 사이즈 정보 추가)
|
| 643 |
+
prompt = "패션 상품 구매를 고려하고 있는 사용자에게 '{s}' 사이즈 추천. 근거:\n{e}\n페르소나: {p}\n(로직: {m}, {i})"
|
| 644 |
+
msg = llm.invoke(prompt.format(
|
| 645 |
+
s=final_sz, e=evidence_txt, p=user_info.get('p',''), m=method_log, i=info)).content
|
| 646 |
+
|
| 647 |
+
return final_sz, msg
|
| 648 |
+
|
| 649 |
+
# 이미지 url 가져오기
|
| 650 |
+
def get_img_url(id):
|
| 651 |
+
if prod_data.empty:
|
| 652 |
+
return None
|
| 653 |
+
|
| 654 |
+
try:
|
| 655 |
+
r = prod_data[prod_data['primary_id'] == str(id).strip()]
|
| 656 |
+
if not r.empty:
|
| 657 |
+
url = r.iloc[0]['thumbnail_img_url']
|
| 658 |
+
if pd.notna(url):
|
| 659 |
+
return str(url)
|
| 660 |
+
except:
|
| 661 |
+
pass
|
| 662 |
+
return None
|
| 663 |
+
|
| 664 |
+
# 검색 벡터 생성
|
| 665 |
+
def get_search_vector(selected_key, user_txt, img_path):
|
| 666 |
+
vector_list = [] # input_prompt 요소들의 embedding_vector 리스트
|
| 667 |
+
message_list = []
|
| 668 |
+
|
| 669 |
+
if img_path:
|
| 670 |
+
image = Image.open(img_path)
|
| 671 |
+
inputs = clip_processor(images=image, return_tensors="pt", padding=True).to(device)
|
| 672 |
+
with torch.no_grad():
|
| 673 |
+
img_vec = clip_model.get_image_features(**inputs).cpu().numpy().tolist()[0]
|
| 674 |
+
vector_list.append(img_vec)
|
| 675 |
+
message_list.append("이미지 기반 검색")
|
| 676 |
+
|
| 677 |
+
if user_txt:
|
| 678 |
+
inputs = clip_processor(text=[user_txt], return_tensors="pt", padding=True).to(device)
|
| 679 |
+
with torch.no_grad():
|
| 680 |
+
txt_vec = clip_model.get_text_features(**inputs).cpu().numpy().tolist()[0]
|
| 681 |
+
vector_list.append(txt_vec)
|
| 682 |
+
message_list.append(f"{user_txt}에 대한 검색 중...")
|
| 683 |
+
|
| 684 |
+
if selected_key and selected_key != '선택 안 함':
|
| 685 |
+
items = style_collection.get(include=["metadatas", "embeddings"])
|
| 686 |
+
query_vec = None
|
| 687 |
+
|
| 688 |
+
for m, e in zip(items["metadatas"], items["embeddings"]):
|
| 689 |
+
if m.get("display_name") == selected_key:
|
| 690 |
+
vector_list.append(e)
|
| 691 |
+
message_list.append(f"{selected_key}에 대한 검색 중...")
|
| 692 |
+
break
|
| 693 |
+
|
| 694 |
+
if not vector_list:
|
| 695 |
+
return None, "검색 조건을 입력하세요"
|
| 696 |
+
else:
|
| 697 |
+
mean_vector = torch.mean(torch.tensor(vector_list), dim=0).tolist() # 멀티모달 사용 시 벡터 concat 후 512차원으로 projection
|
| 698 |
+
return mean_vector, ", ".join(message_list)
|
| 699 |
+
|
| 700 |
+
def on_gallery_click(p_map, evt: gr.SelectData):
|
| 701 |
+
"""
|
| 702 |
+
갤러리 클릭 시 해당 인덱스의 제품 라벨(Key)을 찾아 반환하는 함수
|
| 703 |
+
"""
|
| 704 |
+
if not p_map:
|
| 705 |
+
return ""
|
| 706 |
+
|
| 707 |
+
keys_list = list(p_map.keys())
|
| 708 |
+
|
| 709 |
+
if evt.index < len(keys_list):
|
| 710 |
+
selected_label = keys_list[evt.index]
|
| 711 |
+
return selected_label # 텍스트 박스에 들어갈 라벨 반환
|
| 712 |
+
|
| 713 |
+
return ""
|
| 714 |
+
|
| 715 |
+
# 브랜드 label 선택 시 category range 변경
|
| 716 |
+
def update_cat(brand_label):
|
| 717 |
+
if not brand_label:
|
| 718 |
+
return gr.Dropdown(choices=[], value=None)
|
| 719 |
+
|
| 720 |
+
try:
|
| 721 |
+
filtered_df = prod_data[prod_data['brand_label'] == brand_label]
|
| 722 |
+
new_choices = sorted(filtered_df['category_final'].dropna().unique().tolist())
|
| 723 |
+
|
| 724 |
+
new_value = new_choices[0] if new_choices else None
|
| 725 |
+
return gr.Dropdown(choices=new_choices, value=new_value, label="카테고리 (서브)")
|
| 726 |
+
|
| 727 |
+
except Exception as e:
|
| 728 |
+
print(f" 카테고리 업데이트 실패: {e}")
|
| 729 |
+
return gr.Dropdown(choices=[], value=None)
|
| 730 |
+
|
| 731 |
+
# 카테고리 선택 시 가격 range 변경 함수
|
| 732 |
+
def update_price(brand_label, cat):
|
| 733 |
+
min_price = int(prod_data['sale_price'].dropna().min())
|
| 734 |
+
max_price = int(prod_data['sale_price'].dropna().max())
|
| 735 |
+
|
| 736 |
+
if not brand_label or not cat:
|
| 737 |
+
return (f"가격범위: {min_price:,}원 ~ {max_price:,}원",
|
| 738 |
+
min_price, max_price,
|
| 739 |
+
gr.update(visible=False)
|
| 740 |
+
)
|
| 741 |
+
|
| 742 |
+
try:
|
| 743 |
+
selected_prod_df = prod_data[(prod_data['brand_label'] == brand_label) & (prod_data['category_final'] == cat)]
|
| 744 |
+
update_min_price = int(selected_prod_df['sale_price'].min())
|
| 745 |
+
update_max_price = int(selected_prod_df['sale_price'].max())
|
| 746 |
+
|
| 747 |
+
price_text_info = f"가격범위: {update_min_price:,}원 ~ {update_max_price:,}원"
|
| 748 |
+
|
| 749 |
+
return price_text_info, update_min_price, update_max_price, gr.update(visible=False)
|
| 750 |
+
|
| 751 |
+
except Exception as e:
|
| 752 |
+
print(f"가격 업데이트 실패: {e}")
|
| 753 |
+
return min_price, max_price
|
| 754 |
+
|
| 755 |
+
brand_description = {
|
| 756 |
+
"미니멀리즘, 높은 퀄리티, 편안함, 타임리스, 웨어러블":
|
| 757 |
+
"미니멀하고 타임리스한 디자인을 통해 높은 품질의 편안한 의류를 제공하며, 실용성과 웨어러블함을 중요시합니다.",
|
| 758 |
+
"편안함, 스타일, 개성, 실용성, 자유로움":
|
| 759 |
+
"이 클러스터의 브랜드들은 편안한 실루엣과 실용적인 디자인을 통해 개인의 개성을 표현하며, 일상에서 자유로운 스타일을 추구합니다.",
|
| 760 |
+
"편안함, 일상, 유연함, 트렌디, 개인성":
|
| 761 |
+
"일상 속에서 편안하게 착용할 수 있는 트렌디한 디자인을 추구하며, 개인의 개성과 다양한 라이프스타일을 반영하는 브랜드들이 모여 있습니다.",
|
| 762 |
+
"편안함, 유니크함, 지속 가능성, 반항적 디자인, 일상성과 특별함":
|
| 763 |
+
"이 클러스터의 브랜드들은 일상 속에서 편안함과 유니크함을 추구하며, 반항적이고 독창적인 디자인으로 새로운 흐름을 제안하는 동시에 지속 가능한 삶을 지향합니다.",
|
| 764 |
+
"편안함, 개성, 자연스러움, 균형, 다채로움":
|
| 765 |
+
"이 클러스터의 브랜드들은 편안함과 개성을 중시하며, 자연스러운 스타일과 균형 잡힌 라이프스타일을 통해 다양성을 존중하는 독창적인 패션을 제안합니다.",
|
| 766 |
+
"아웃도어, 기능성, 일상, 스포츠, 혁신":
|
| 767 |
+
"스포츠와 아웃도어 활동을 일상 속에서 즐길 수 있도록 기능성과 스타일을 겸비한 다양한 제품을 제공하는 브랜드들이 모인 클러스터입니다.",
|
| 768 |
+
"여성, 클래식, 트렌디, 웨어러블, 심플, 감성":
|
| 769 |
+
"이 클러스터의 브랜드들은 클래식함과 트렌디함을 조화롭게 담아내며, 편안한 착용감을 제공하는 웨어러블한 디자인과 감성적인 스타일을 강조합니다."
|
| 770 |
+
|
| 771 |
+
}
|
| 772 |
+
def load_stye_collection():
|
| 773 |
+
data = style_collection.get(include=["metadatas"], limit=9999)
|
| 774 |
+
|
| 775 |
+
desc_dict = {}
|
| 776 |
+
for meta in data['metadatas']:
|
| 777 |
+
style_key = meta.get('display_name')
|
| 778 |
+
style_desc = meta.get('description')
|
| 779 |
+
|
| 780 |
+
if style_key and style_desc and style_key not in desc_dict:
|
| 781 |
+
desc_dict[style_key] = style_desc
|
| 782 |
+
|
| 783 |
+
return desc_dict
|
| 784 |
+
|
| 785 |
+
style_description = load_stye_collection()
|
| 786 |
+
|
| 787 |
+
brand_label_choice, subcat_choice = [], []
|
| 788 |
+
min_price, max_price = 0, 100
|
| 789 |
+
|
| 790 |
+
if not prod_data.empty:
|
| 791 |
+
brand_label_choice = sorted(prod_data['brand_label'].dropna().unique().tolist())
|
| 792 |
+
subcat_choice = sorted(prod_data['category_final'].dropna().unique().tolist())
|
| 793 |
+
min_price = int(prod_data['sale_price'].dropna().min())
|
| 794 |
+
max_price = int(prod_data['sale_price'].dropna().max())
|
| 795 |
+
|
| 796 |
+
# 데이터에 맞는 변수명으로 변경 작업
|
| 797 |
+
def get_brand_desc(brand_label):
|
| 798 |
+
return brand_description.get(brand_label, "")
|
| 799 |
+
|
| 800 |
+
def get_style_desc(style):
|
| 801 |
+
return style_description.get(style, "")
|
| 802 |
+
|
| 803 |
+
# [1] 텍스트를 예쁜 HTML 박스로 감싸주는 함수
|
| 804 |
+
def format_info_box(text):
|
| 805 |
+
if not text:
|
| 806 |
+
return ""
|
| 807 |
+
# 여기에 style을 직접 적으면 절대 무시당하지 않습니다.
|
| 808 |
+
return f"""
|
| 809 |
+
<div style="
|
| 810 |
+
background-color: #f0f9ff;
|
| 811 |
+
color: #333333;
|
| 812 |
+
border: 1px solid #bae6fd;
|
| 813 |
+
border-radius: 8px;
|
| 814 |
+
padding: 12px;
|
| 815 |
+
text-align: center;
|
| 816 |
+
font-weight: 600;
|
| 817 |
+
font-size: 0.95em;
|
| 818 |
+
margin-top: 5px;
|
| 819 |
+
">
|
| 820 |
+
{text}
|
| 821 |
+
</div>
|
| 822 |
+
"""
|
| 823 |
+
|
| 824 |
+
with gr.Blocks(theme=gr.themes.Soft()) as app:
|
| 825 |
+
state_pmap = gr.State({}) # 제품 메타데이터 저장
|
| 826 |
+
state_results = gr.State({})
|
| 827 |
+
|
| 828 |
+
with gr.Group(visible=True) as page_input:
|
| 829 |
+
gr.Markdown("### 1. 스타일 조건을 입력해주세요")
|
| 830 |
+
with gr.Column():
|
| 831 |
+
in_g = gr.Radio(["male", "female"], value="male", label="성별")
|
| 832 |
+
check = gr.Checkbox(label="다른 성별의 제품도 볼래요", value=False)
|
| 833 |
+
|
| 834 |
+
with gr.Row():
|
| 835 |
+
in_h, in_w = gr.Number(175, label="키"), gr.Number(70, label="몸무게")
|
| 836 |
+
|
| 837 |
+
with gr.Row():
|
| 838 |
+
with gr.Column():
|
| 839 |
+
in_bg = gr.Dropdown(brand_label_choice, value=brand_label_choice[0] if brand_label_choice else None,
|
| 840 |
+
label="브랜드 그룹", info="선택 시 브랜드 설명이 아래에 나타납니다.")
|
| 841 |
+
initial_bg_text = get_brand_desc(brand_label_choice[0] if brand_label_choice else "")
|
| 842 |
+
bg_desc = gr.HTML(value=format_info_box(initial_bg_text), visible=True)
|
| 843 |
+
|
| 844 |
+
in_cat = gr.Dropdown(subcat_choice, value=subcat_choice[0] if subcat_choice else None, label="카테고리")
|
| 845 |
+
|
| 846 |
+
price_info = gr.Markdown(value=f"가격범위: {min_price:,}원 ~ {max_price:,}원")
|
| 847 |
+
with gr.Row():
|
| 848 |
+
min_num = gr.Number(value=min_price, label="최소 가격 입력")
|
| 849 |
+
max_num = gr.Number(value=max_price, label="최대 가격 입력")
|
| 850 |
+
price_error = gr.Markdown("", visible=False)
|
| 851 |
+
|
| 852 |
+
with gr.Column():
|
| 853 |
+
in_sel = gr.Dropdown(choices = style_key_options, value="선택 안 함",
|
| 854 |
+
label="스타일", info="원하는 스타일 무드를 선택해보세요.")
|
| 855 |
+
style_desc = gr.HTML(value=format_info_box(""), visible=True)
|
| 856 |
+
|
| 857 |
+
in_txt = gr.Textbox(label="텍스트 (패션 추구미를 직접 입력해보세요)")
|
| 858 |
+
in_img = gr.Image(type="filepath", label="이미지를 넣으면 더 정밀한 추천을 받을 수 있어요 (선택 사항)")
|
| 859 |
+
|
| 860 |
+
btn_s = gr.Button("🔍 검색", variant='primary', size='lg')
|
| 861 |
+
|
| 862 |
+
# 검색 결과 페이지
|
| 863 |
+
with gr.Group(visible=False) as page_result:
|
| 864 |
+
gr.Markdown("### 2. 추천 결과")
|
| 865 |
+
out_p = gr.Textbox(label="나만의 패션 페르소나", lines=3)
|
| 866 |
+
|
| 867 |
+
with gr.Row():
|
| 868 |
+
with gr.Column(scale=1):
|
| 869 |
+
gal = gr.Gallery(label="검색 결과", columns=3)
|
| 870 |
+
|
| 871 |
+
with gr.Column(scale=2):
|
| 872 |
+
sel = gr.Textbox(label="사이즈 추천을 원하는 상품")
|
| 873 |
+
btn_r = gr.Button("사이즈 추천")
|
| 874 |
+
f_sz = gr.Textbox(label="추천 사이즈", lines=1)
|
| 875 |
+
out_m = gr.Textbox(label="추천 이유 설명", lines=5, max_lines=10)
|
| 876 |
+
|
| 877 |
+
with gr.Row():
|
| 878 |
+
btn_back = gr.Button("검색 조건 다시 입력하기", variant='secondary')
|
| 879 |
+
|
| 880 |
+
def validate_price(min_num, max_num):
|
| 881 |
+
if min_num < min_price:
|
| 882 |
+
return gr.update(value=f"최소 가격은 {min_price:,}원 이상이어야 합니다.", visible=True)
|
| 883 |
+
if max_num > max_price:
|
| 884 |
+
return gr.update(value=f"최대 가격은 {max_price:,}원 이하이어야 합니다.", visible=True)
|
| 885 |
+
if min_num > max_num:
|
| 886 |
+
return gr.update(value="최소 가격이 최대 가격보다 클 수 없습니다.", visible=True)
|
| 887 |
+
|
| 888 |
+
return gr.update(value="", visible=False)
|
| 889 |
+
|
| 890 |
+
min_num.change(fn=validate_price, inputs=[min_num, max_num], outputs=[price_error])
|
| 891 |
+
max_num.change(fn=validate_price, inputs=[min_num, max_num], outputs=[price_error])
|
| 892 |
+
|
| 893 |
+
in_bg.change(fn=lambda x: format_info_box(get_brand_desc(x)),
|
| 894 |
+
inputs=[in_bg],
|
| 895 |
+
outputs=[bg_desc])
|
| 896 |
+
in_sel.change(fn=lambda x: format_info_box(get_style_desc(x)),
|
| 897 |
+
inputs=[in_sel],
|
| 898 |
+
outputs=[style_desc])
|
| 899 |
+
|
| 900 |
+
in_bg.change(fn=update_cat, inputs=[in_bg], outputs=[in_cat])
|
| 901 |
+
in_cat.change(fn=update_price, inputs=[in_bg, in_cat], outputs=[price_info, min_num, max_num, price_error])
|
| 902 |
+
|
| 903 |
+
# 페이지 전환 함수
|
| 904 |
+
def move_to_result():
|
| 905 |
+
return gr.update(visible=False), gr.update(visible=True)
|
| 906 |
+
|
| 907 |
+
def move_to_input():
|
| 908 |
+
return gr.update(visible=True), gr.update(visible=False)
|
| 909 |
+
|
| 910 |
+
def search_and_log(bg, c, sl, tx, im, g, h, w, min_p, max_p, check):
|
| 911 |
+
try:
|
| 912 |
+
gr.Info("🚀 사용자 데이터 분석을 시작합니다...")
|
| 913 |
+
|
| 914 |
+
if not bg or not c:
|
| 915 |
+
raise gr.Error("브랜드그룹과 카테고리를 반드시 선택하세요")
|
| 916 |
+
|
| 917 |
+
# 1. 기존 검색 로직 수행
|
| 918 |
+
user_info = {'gender': g, 'height': h, 'weight': w}
|
| 919 |
+
|
| 920 |
+
results = search_recom_products(bg, c, sl, tx, im, user_info, min_p, max_p, check)
|
| 921 |
+
|
| 922 |
+
gr.Info("✅ 분석 완료! 결과 페이지로 이동합니다.")
|
| 923 |
+
persona_result = results[1]
|
| 924 |
+
|
| 925 |
+
try:
|
| 926 |
+
# 2. DB에 입력값과 결과 저장
|
| 927 |
+
log_search_to_sheet(bg, c, min_p, max_p, sl, tx, check, persona_result)
|
| 928 |
+
except Exception as log_e:
|
| 929 |
+
print(f"로그 저장 실패: {log_e}")
|
| 930 |
+
|
| 931 |
+
return results
|
| 932 |
+
|
| 933 |
+
except gr.Error as ge:
|
| 934 |
+
raise ge
|
| 935 |
+
|
| 936 |
+
except Exception as e:
|
| 937 |
+
print(f"시스템 에러: {e}")
|
| 938 |
+
raise gr.Error(f"시스템 오류가 발생했습니다: {str(e)}")
|
| 939 |
+
|
| 940 |
+
def size_recom_and_log(sel_product, p_map, u_info):
|
| 941 |
+
# 1. 기존 사이즈 추천 로직 수행
|
| 942 |
+
# recom_size 함수는 2개의 값을 반환한다고 가정 (f_sz, out_m)
|
| 943 |
+
rec_size, reason = recom_size(sel_product, p_map, u_info)
|
| 944 |
+
|
| 945 |
+
# 2. DB에 저장
|
| 946 |
+
log_size_to_sheet(sel_product, u_info, rec_size, reason)
|
| 947 |
+
|
| 948 |
+
return rec_size, reason
|
| 949 |
+
|
| 950 |
+
# 1. 검색 버튼 연결
|
| 951 |
+
btn_s.click(
|
| 952 |
+
fn=search_and_log,
|
| 953 |
+
inputs=[in_bg,in_cat,in_sel,in_txt,in_img,in_g,in_h,in_w, min_num, max_num, check],
|
| 954 |
+
outputs=[gal, out_p, state_pmap, state_results]
|
| 955 |
+
).then(
|
| 956 |
+
fn=move_to_result,
|
| 957 |
+
inputs=None,
|
| 958 |
+
outputs=[page_input, page_result]
|
| 959 |
+
) # 버튼 클릭 시 페이지 이동
|
| 960 |
+
|
| 961 |
+
btn_back.click( # 버튼 클릭 시 페이지 이동
|
| 962 |
+
fn=move_to_input,
|
| 963 |
+
inputs=None,
|
| 964 |
+
outputs=[page_input, page_result]
|
| 965 |
+
)
|
| 966 |
+
|
| 967 |
+
gal.select(fn=on_gallery_click, inputs=[state_pmap], outputs=sel)
|
| 968 |
+
|
| 969 |
+
# 3. 사이즈 추천 버튼 연결
|
| 970 |
+
# inputs에 state_persona를 넣어서 recommend_size로 전달
|
| 971 |
+
btn_r.click(
|
| 972 |
+
fn=size_recom_and_log,
|
| 973 |
+
inputs=[sel, state_pmap, state_results], # -> state_persona 값을 꺼내서 씀
|
| 974 |
+
outputs=[f_sz, out_m]
|
| 975 |
+
)
|
| 976 |
+
|
| 977 |
+
try:
|
| 978 |
+
app.close()
|
| 979 |
+
except:
|
| 980 |
+
pass
|
| 981 |
+
|
| 982 |
+
app.queue().launch(share=True)
|
fashion_key.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "service_account",
|
| 3 |
+
"project_id": "fluent-music-480402-m3",
|
| 4 |
+
"private_key_id": "204a19a89bde0b461b35ddd5149b6d34e0e955c6",
|
| 5 |
+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDuBlEcoMBWT62A\nPX7cX6e/vYYF2Z3F/Bh+TmM5B8l3yHUU/fAiDwV2abb/TYAPPcUXu/ElVwkL8GBl\n9uBRV6zc3GsndR9Vu7PQ1j4pt4BKSNhwQPWcmxHhiqET52G5+EDET0SyXD6xjgq8\nAbMFuzkqbtIMUDIo27MND0I47bCerxSCjIyBSKe7faJ69WX7z+xGoZXyuND5Ln4D\n2jyt+vCV9nrX1MkaHMkN9SfwvSh/SlZT7VkbekgjtGK2lQW8VR9QpqcZAGxq/BVt\n7CUghQdB/0efO5DQqgYzPIYiBoSxazH7t13igDZcNeSODcDOxPmAsSRnjK1gtH8n\nB/01HZETAgMBAAECggEAAR0ywElnJCrxrEd3eq/VQt5Vxnlh55anu7rd5fLOa0Kp\nTJcJ5vyJYe89xUlR3wLC7Z5TNFI0Cgs6pLhrdWVp9AHxbr707JbDzHgidcG/X+V8\nRr3IR1Nw/4oP04j8TOz5WO0hm+wadT0aqRdZ1XN6XMvQbyA+5Cb0RzclzvDsFSYx\nSMEICHJ4+q6gi4y42J6b7uw/9kPHhNuVJjBZ9aVDChdLqHvRfIRFxOS/15FlfMyL\n+10gu3iWX1jK975+/wBOQ2fe6RcZ+uLiE0hQ4HGzSZTQLtaf++Vx7K/l0+qMdOMJ\n6sVkuNe40BEij7MOV/2zzC7dQFAIAmyfTsPAEUyBkQKBgQD9wfSrg6rDokdnlJee\nq3p/MVb7DA21G6BEGaq64NN7RWOh98fT+gfHFKI3B7qCGWiSU3Boa5xAylplDHv/\n1ZbKYN8Oj0EA1XHSwYwVMBVfcpDKaCr1kCfvLcvNRQxoQMVc9LIZxPQymGhBn9U3\n1ZTyzfO6l12TDpjq8I2vafy9hwKBgQDwIMU3ewAL77/es3svGqId+OLvWf6fsS0Z\nlnbZGExJxs34xgc7sUDJHV2UGX7FVKrF/jOqBUDTSfmI/kAn85ETF0uBO7hIeuqz\nuqNwDfAIugT45nTPzCS0Xl0BGf0zOnsvUibjC462mm1KM4g/dcep4I2aeR7yMJFy\nmW94gGcTFQKBgDL4fpjk/awGMKmRo1Lvxs89recHaMl0HyEOtsiK2G/uZDDogG8H\nzgAjGOJM3x261NJviCixVNV+z+F0PIQK9xr3klZuV/Q+63oV/nlTCzf83zCI8/gX\nWK4mWMWGlRNrzzj4vb2HXjW7f1WPMVjhweVzkP1t4hHj4LtwTkVp2KaJAoGASp4U\nSqo5GHx2xfBF7hw7lk65ziDMA7mdxx6/bbSkOCTD6nM22jC5bgVnB9doUk9+Bap5\nSXL6cu3A0fYX35JLWYBaoRz6peM0PYdYtEBQf5W2Z9XNJKlOGpJcGjSGPr4Ee27u\n0IzN2yZfobgjtXyW/83ckszideXrvI27WtmjrgUCgYEAiyg/gtAki5HAWG1bsa9p\ndB4yXdW9a7izkWL3QHCodkpF9syQzvvZDK4aP28T4mF0/SxS0aZEj8ZXo7S2en41\nvAvk+iC3qObkQelJzLbLb5imLxoA9n9nHUjQG6ENJtym7eUwrsHjW+a8RKatmR2x\n36GyilDuARXOVLi6zE6x9vA=\n-----END PRIVATE KEY-----\n",
|
| 6 |
+
"client_email": "fashion-recom@fluent-music-480402-m3.iam.gserviceaccount.com",
|
| 7 |
+
"client_id": "107965914755558240659",
|
| 8 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
| 9 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
| 10 |
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
| 11 |
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/fashion-recom%40fluent-music-480402-m3.iam.gserviceaccount.com",
|
| 12 |
+
"universe_domain": "googleapis.com"
|
| 13 |
+
}
|
final_prod_data.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cf0b50de02abf738157423ea2bf1cd5de772e10a53993886d464b1f5224cb7fa
|
| 3 |
+
size 17593097
|
final_review_df.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:05bd48bfd2fdb8e083d69fdba498ca9b957835410867c777c05f86b6e48371d5
|
| 3 |
+
size 21556328
|
prod_emb_data.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ec053f98d4d23f2769d1ef7f47edfa658426ea87bf831ed84fd105b2a033db4c
|
| 3 |
+
size 160038091
|
requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
pandas
|
| 3 |
+
numpy
|
| 4 |
+
torch
|
| 5 |
+
chromadb
|
| 6 |
+
pysqlite3-binary
|
| 7 |
+
transformers
|
| 8 |
+
langchain
|
| 9 |
+
langchain-community
|
| 10 |
+
langchain-huggingface
|
| 11 |
+
langchain-openai
|
| 12 |
+
openai
|
| 13 |
+
sentence-transformers
|
| 14 |
+
gspread
|
| 15 |
+
oauth2client
|
| 16 |
+
pillow
|
size_sorted.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
style_key_data.csv
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
style_key,display_name,synonyms,description,embedding_queries_en,embedding_queries_ko
|
| 2 |
+
clean_girl_aesthetic,클린걸 에스테틱,"['clean girl aesthetic', '클린걸룩', '클린룩', '꾸안꾸 미니멀']","깔끔하고 미니멀하며 자연스러운 아름다움을 강조하는 스타일. 뉴트럴 톤, 심플한 실루엣, 정돈된 헤어와 메이크업이 특징. (예: 화이트 티셔츠 + 스트레이트 데님 + 골드 주얼리)","['clean girl aesthetic', 'minimalist natural look', 'effortless chic fashion', 'neutral tones outfit', 'simple silhouette style']","['클린걸 에스테틱', '꾸안꾸 미니멀룩', '자연스러운 스타일', '뉴트럴톤 코디', '심플한 실루엣']"
|
| 3 |
+
vanilla_girl_aesthetic,바닐라걸 에스테틱,"['vanilla girl aesthetic', '바닐라걸룩', '소프트걸', '크림룩']","부드럽고 포근한 느낌을 주는 스타일. 크림, 베이지, 화이트 등 밝고 따뜻한 색감과 니트, 코튼 등 부드러운 소재를 주로 사용. (예: 아이보리 니트 스웨터 + 새틴 스커트)","['vanilla girl aesthetic', 'soft cozy fashion style', 'cream and beige outfits', 'light academia soft', 'warm and comfortable look']","['바닐라걸 에스테틱', '부드럽고 포근한 룩', '크림색 베이지색 코디', '소프트걸 스타일', '따뜻한 느낌의 패션']"
|
| 4 |
+
dark_romantic,다크 로맨틱,"['dark romanticism', 'gothic romantic', '다크 로맨틱룩']","로맨틱한 실루엣에 어둡고 고딕적인 요소를 결합한 스타일. 레이스, 프릴, 코르셋 디테일과 블랙, 버건디 등 깊은 색상을 사용. (예: 블랙 레이스 드레스 + 벨벳 초커)","['dark romantic fashion', 'gothic romantic style', 'victorian goth look', 'lace and velvet dark aesthetic', 'moody romantic outfit']","['다크 로맨틱 패션', '고딕 로맨틱 스타일', '빅토리안 고스룩', '레이스 벨벳 어두운 코디', '분위기 있는 로맨틱룩']"
|
| 5 |
+
indie_sleaze,인디 슬리즈,"['indie sleaze aesthetic', '2000s indie', '인디 슬리즈룩']","2000년대 후반~2010년대 초반 인디 씬의 자유분방하고 다소 정돈되지 않은 스타일. 빈티지, 그런지, 일렉트로 팝 문화를 혼합. (예: 찢어진 스키니진 + 밴드 티셔츠 + 가죽 재킷)","['indie sleaze fashion', '2000s indie aesthetic', 'grunge party look', 'messy chic style', 'vintage band shirt outfit']","['인디 슬리즈 패션', '2000년대 인디 스타일', '그런지 파티룩', '자유분방한 빈티지', '밴드 티셔츠 코디']"
|
| 6 |
+
genderless,젠더리스,"['genderless fashion', 'unisex fashion', '젠더리스룩', '유니섹스']","성별의 경계를 허무는 스타일. 남성복과 여성복의 구분이 없는 오버사이즈 핏, 중성적인 디자인의 아이템을 활용. (예: 오버사이즈 셔츠 + 와이드 슬랙스)","['genderless fashion style', 'unisex clothing', 'androgynous look', 'breaking gender norms in fashion', 'oversized neutral outfit']","['젠더리스 패션', '유니섹스 의류', '중성적인 룩', '성별 구분 없는 스타일', '오버사이즈 중성적인 코디']"
|
| 7 |
+
deconstructed_fashion,해체주의 패션,"['deconstructed clothing', 'deconstructionism', '해체주의룩']","의복의 기존 구조를 의도적으로 해체하고 재조립하여 새로운 형태를 만드는 아방가르드한 스타일. 비대칭, 노출된 시접, 미완성된 듯한 디테일이 특징. (예: 비대칭 헴라인 스커트 + 재구성된 셔츠)","['deconstructed fashion', 'deconstructionism in clothing', 'asymmetrical garment design', 'raw edges unfinished look', 'avant-garde reconstruction']","['해체주의 패션', '의류 해체주의', '비대칭 디자인', '노출된 시접 마감', '아방가르드 재구성']"
|
| 8 |
+
avant_garde,아방가르드,"['avant-garde fashion', 'experimental fashion', '전위적인 패션']","전통적인 패션의 관습과 규범을 깨는 실험적이고 혁신적인 스타일. 비정형적인 실루엣, 독특한 소재, 예술적인 표현을 중시. (예: 과장된 볼륨의 구조적인 드레스)","['avant-garde fashion style', 'experimental clothing', 'unconventional silhouettes', 'artistic fashion', 'pushing fashion boundaries']","['아방가르드 패션', '실험적인 의상', '비정형적 실루엣', '예술적인 패션', '관습을 깨는 디자인']"
|
| 9 |
+
futuristic_fashion,퓨처리스틱,"['futuristic style', 'sci-fi fashion', '미래지향적 패션', '사이버펑크']","미래나 과학 소설(SF)에서 영감을 받은 스타일. 메탈릭 소재, 기하학적 패턴, 구조적인 실루엣, 테크웨어 요소를 특징으로 함. (예: 실버 메탈릭 패딩 점퍼 + 테크 팬츠)","['futuristic fashion', 'sci-fi inspired clothing', 'metallic outfits', 'cyberpunk style', 'techwear aesthetic']","['퓨처리스틱 패션', 'SF 영화 패션', '메탈릭 소재 코디', '사이버펑크 스타일', '테크웨어']"
|
| 10 |
+
goth,고스,"['goth style', 'gothic fashion', '고스룩', '고딕']","어둡고 신비로우며 종종 으스스한 분위기를 연���하는 스타일. 주로 블랙 컬러를 사용하며, 레이스, 가죽, 벨벳 소재와 코르셋, 플랫폼 부츠 등을 활용. (예: 올블랙 코디 + 가죽 부츠 + 실버 액세서리)","['goth fashion style', 'all black gothic look', 'dark aesthetic clothing', 'lace and leather goth', 'platform boots outfit']","['고스 패션 스타일', '올블랙 고딕룩', '어두운 패션', '레이스 가죽 고스', '플랫폼 부츠 코디']"
|
| 11 |
+
emo,이모,"['emo style', 'emo fashion', '이모룩', '이모코어']","이모(Emo) 펑크 음악 하위문화에서 파생된 스타일. 감정적인 표현을 중시하며, 스키니 진, 밴드 티셔츠, 스터드 벨트, 짙은 아이라이너가 특징. (예: 블랙 밴드 티셔츠 + 블랙 스키니진 + 컨버스)","['emo fashion style', 'emotional hardcore fashion', 'skinny jeans and band shirt', 'studded belt aesthetic', '2000s emo look']","['이모 패션 스타일', '이모코어 패션', '스키니진 밴드 티셔츠', '스터드 벨트', '2000년대 이모룩']"
|
| 12 |
+
mod_style,모드,"['mod fashion', '1960s mod', '모드룩', '60년대 스타일']","1960년대 영국 런던에서 시작된 모던(Modern) 스타일. 깔끔하고 대담한 실루엣, 기하학적 패턴, 미니 스커트, A라인 드레스가 특징. (예: A라인 미니 드레스 + 앵클 부츠)","['mod fashion style', '1960s London look', 'A-line mini dress', 'geometric patterns clothing', 'Twiggy style']","['모드 패션 스타일', '1960년대 런던 패션', 'A라인 미니 드레스', '기하학적 패턴', '트위기 스타일']"
|
| 13 |
+
gorpcore,고프코어,"['gorpcore', '고프코어룩', '아웃도어룩', '바람막이 코디']","아웃도어 활동을 위한 기능성 의류(바람막이, 등산화, 카고 팬츠)를 일상복으로 활용하는 스타일. 'Gorp'는 등산객이 먹는 견과류 믹스를 의미.","['gorpcore aesthetic', 'outdoor functional wear as daily fashion', 'man wearing a windbreaker and cargo pants', 'hiking boots street style', 'arcteryx style jacket']","['고프코어 스타일', '기능성 아웃도어 데일리룩', '바람막이와 카고 팬츠 코디', '등산화 스트릿 패션', '아크테릭스 스타일 자켓']"
|
| 14 |
+
balletcore,발레코어,"['balletcore', '발레코어룩', '발레리나 스타일', '리본 패션']","발레리나의 연습복에서 영감을 받은 스타일. 발레 플랫 슈즈, 레그 워머, 튤 스커트(샤 스커트), 리본, 랩 가디건 등이 핵심 아이템.","['balletcore aesthetic', 'ballerina inspired fashion', 'wearing ballet flats and leg warmers', 'tulle skirt with wrap cardigan', 'outfit with pink ribbons']","['발레코어 스타일', '발레리나 패션', '발레 플랫 슈즈와 레그 워머 코디', '튤 스커트와 랩 가디건', '핑크 리본 디테일 아웃핏']"
|
| 15 |
+
blokecore,블록코어,"['blokecore', '블록코어룩', '축구 유니폼 코디', '사커 저지']","영국 남성(Bloke)들이 축구 유니폼(저지)을 일상복처럼 입는 스타일. 축구 유니폼 상의에 데님 팬츠나 트랙 팬츠, 스니커즈(주로 아디다스 삼바)를 매치.","['blokecore aesthetic', 'soccer jersey street style', 'wearing football shirt with jeans', 'adidas samba sneakers outfit', 'sports jersey fashion']","['블록코어 스타일', '축구 유니폼 스트릿 패션', '사커 저지와 청바지 코디', '아디다스 삼바 스니커즈 매치', '스포츠 저지 패션']"
|
| 16 |
+
girlcore,걸코어,"['girlcore', '걸코어룩', '걸리시룩', '하이틴 스타일']","소녀스러운 감성을 극대화한 스타일. 핑크 컬러, 리본, 레이스, 프릴, 크롭탑, 미니 스커트 등을 활용하여 귀엽고 발랄한 무드를 연출.","['girlcore aesthetic', 'girly fashion style', 'pink ribbon and lace details', 'cute crop top and mini skirt', 'k-pop idol style outfit']","['걸코어 스타일', '소녀 감성 패션', '핑크 리본과 레이스 디테일', '귀여운 크롭탑과 미니 스커트', '하이틴 영화 스타일']"
|
| 17 |
+
y2k,Y2K,"['Y2K fashion', 'Y2K 스타일', '2000년대 패션', '세기말 패션']","2000년대 초반(Year 2000) 유행했던 스타일. 로우라이즈 팬츠, 크롭탑, 벨벳 트레이닝 수트, 카고 팬츠, 화려한 그래픽과 컬러가 특징.","['Y2K fashion aesthetic', 'early 2000s style', 'low-rise jeans and crop top', 'velour tracksuit outfit', 'colorful graphic t-shirt']","['Y2K 패션', '2000년대 초반 스타일', '로우라이즈 팬츠와 크롭탑 코디', '벨벳 트레이닝 수트', '화려한 그래픽 티셔츠']"
|
| 18 |
+
copenhagen_core,코펜하겐 코어,"['copenhagen core', '코펜하겐 스타일', '덴마크 패션', '스칸디나비아 스타일']","덴마크 코펜하겐 여성들의 편안하면서도 컬러풀하고 실용적인 스트릿 스타일. 비비드한 컬러의 니트, 와이드 데님, 편안한 스니커즈 등이 특징.","['copenhagen street style', 'scandinavian fashion', 'colorful knit vest and wide-leg denim', 'vibrant color patterns', 'practical layered look with sneakers']","['코펜하겐 스트릿 스타일', '���칸디나비아 패션', '컬러풀한 니트 조끼와 와이드 데님', '생동감 있는 컬러 패턴', '스니커즈를 매치한 실용적인 레이어드 룩']"
|
| 19 |
+
old_money_look,올드머니룩,"['old money aesthetic', 'quiet luxury', '올드머니', '조용한 럭셔리']","과시하지 않는 상류층의 클래식하고 고급스러운 스타일. 로고 플레이를 배제하고, 린넨, 캐시미어 등 고급 소재와 뉴트럴 톤(베이지, 화이트, 네이비)을 주로 사용.","['quiet luxury aesthetic', 'old money style', 'linen shirt and beige slacks', 'minimalist luxury fashion without logos', 'classic preppy style with cashmere sweater']","['조용한 럭셔리 패션', '로고 없는 고급스러운 스타일', '린넨 셔츠와 베이지색 슬랙스 코디', '캐시미어 스웨터를 활용한 클래식 룩', '뉴트럴 톤의 우아한 아웃핏']"
|
| 20 |
+
running_core,러닝코어,"['running core', '러닝코어룩', '러닝화 코디', '스포츠웨어 믹스']","러닝(달리기)을 위한 기능성 의류를 일상복과 결합한 스타일. 고프코어와 유사하나, 러닝화, 바람막이, 레깅스, 스포츠 쇼츠 등 '러닝'에 더 집중.","['running core aesthetic', 'fashion with running shoes', 'windbreaker jacket and shorts', 'sportswear as daily outfit', 'technical running apparel']","['러닝코어 스타일', '러닝화 패션 코디', '바람막이 자켓과 쇼츠', '스포츠웨어 데일리룩', '기능성 러닝 의류']"
|
| 21 |
+
rock_chic,락시크,"['rock chic', '락시크룩', '락스타일', '글램락']","락(Rock) 음악 문화에서 영감을 받은 시크한 스타일. 가죽 재킷, 스키니진(주로 블랙), 밴드 티셔츠, 워커 부츠가 핵심 아이템.","['rock chic style', 'leather jacket and skinny jeans', 'band t-shirt with biker jacket', 'dark aesthetic with boots', 'glam rock fashion']","['락시크 스타일', '가죽 자켓과 스키니진', '밴드 티셔츠와 바이커 자켓', '부츠를 활용한 다크한 패션', '글램락 무드']"
|
| 22 |
+
grunge,그런지룩,"['grunge', '그런지', '너바나 스타일', 'nirvana style']","1990년대 락 밴드 '너바나'에서 유래한, 지저분하고 낡아 보이는 스타일. 찢어진 데님, 체크무늬 플란넬 셔츠, 빈티지 티셔츠, 워커 등을 레이어드.","['90s grunge aesthetic', 'kurt cobain style fashion', 'ripped jeans with flannel check shirt', 'vintage band t-shirt and worn-out boots', 'layered messy look']","['90년대 그런지 스타일', '커트 코베인 패션', '찢어진 청바지와 체크 플란넬 셔츠', '빈티지 밴드 티셔츠와 낡은 부츠', '지저분하게 레이어드한 룩']"
|
| 23 |
+
punk,펑크룩,"['punk', '펑크 스타일', '펑크 패션']","1970년대 펑크 락 문화에 기반한 반항적인 스타일. 스터드(징) 박힌 가죽 재킷, 찢어진 옷, 타탄 체크(플래드) 팬츠, 안전핀, 닥터마틴 워커가 상징적.","['punk fashion aesthetic', 'studded leather jacket', 'tartan plaid pants', 'ripped t-shirt with safety pins', 'dr. martens boots style']","['펑크 패션 스타일', '스터드 가죽 자켓', '타탄 체크 바지', '안전핀으로 고정한 찢어진 티셔츠', '닥터마틴 워커 코디']"
|
| 24 |
+
boho_chic,보호 시크,"['boho chic', '보호 시크룩', '페스티벌룩']","보헤미안(Bohemian) 스타일을 현대적이고 시크하게 재해석한 룩. 주로 페스티벌 룩으로 인기. 프린지(술 장식), 스웨이드, 로브 가디건, 에스닉 패턴이 특징.","['boho chic style', 'modern bohemian fashion', 'music festival outfit', 'suede jacket with fringe', 'ethnic pattern dress']","['보호 시크 스타일', '현대적인 보헤미안 패션', '뮤직 페스티벌 코디', '프린지 디테일의 스웨이드 자켓', '에스닉 패턴 원피스']"
|
| 25 |
+
bohemian,보헤미안,"['bohemian style', '보헤미안룩', '집시 스타일']","사회 관습에 얽매이지 않는 집시(Gypsy)들의 자유로운 스타일. 에스닉하고 이국적인 패턴, 루즈핏의 맥시 드레스, 자수, 천연 소재가 특징.","['bohemian aesthetic', 'gypsy style fashion', 'ethnic print maxi dress', 'loose fit natural fabric', 'embroidered tunic']","['보헤미안 스타일', '집시 패션', '에스닉 프린트 맥시 원피스', '루즈핏 천연 소재 의류', '자수가 놓인 튜닉']"
|
| 26 |
+
hippie_chic,히피 시크,"['hippie chic', '히피룩', '70년대 스타일']","1970년대 '히피' 문화에서 영감을 받은 스타일. 플라워 패턴, 플레어 팬츠(나팔바지), 크로셰(코바늘 뜨개) 니트, 헤어밴드, 평화의 상징 등이 특징.","['70s hippie style', 'hippie chic fashion', 'flare pants and flower pattern shirt', 'crochet knit vest', 'peace sign accessories']","['70년대 히피 스타일', '히피 시크 패션', '나팔바지와 플라워 패턴 셔츠', '크로셰 니트 조끼', '평화 상징 액세서리']"
|
| 27 |
+
new_romanticism,뉴로맨티시즘,"['new romanticism', '뉴로맨틱룩', '80년대 패션']","1980년대 초 영국 팝 문화(듀란듀란 등)에서 유래한 화려하고 젠더리스한 스타일. 프릴이나 러플이 달린 블라우스, 화려한 메이크업, 과장된 실루엣이 특징.","['80s new romantic fashion', 'duran duran style', 'frilly blouse with ruffles', 'flamboyant genderless look', 'dandy pirate aesthetic']","['80년대 뉴로맨틱 패션', '듀란듀란 스타일', '프릴과 러플이 달린 블라우스', '화려한 젠더리스 룩', '과장된 실루엣']"
|
| 28 |
+
mori_girl,모리걸,"['mori girl', '모리걸룩', '숲속의 소녀']","일본에서 유래한 '숲속의 소녀'를 뜻하는 스타일. 천연 소재(린넨, 코튼), 아스톤(Earth-tone) 컬러, 레이스, 자수 등을 활용해 여러 겹 레이어드하는 것이 특징.","['japanese mori girl style', 'forest girl aesthetic', 'layered natural fabrics dress', 'earth tone clothing with lace', 'vintage inspired loose fit']","['일본 모리걸 스타일', '숲속의 소녀 패션', '천연 소재 레이어드 원피스', '아스톤과 레이스 디테일', '빈티지 감성 루즈핏']"
|
| 29 |
+
natural,내추럴,"['natural style', '내추럴룩', '편안한 스타일', '린넨 코디']","자연스럽고 편안한 무드를 추구하는 스타일. 린넨, 코튼 등 천연 소재와 베이지, 아이보리, 브라운 등 뉴트럴 컬러, 루즈핏 실루엣이 중심.","['natural aesthetic fashion', 'comfortable loose fit', 'linen shirt and cotton pants', 'neutral color palette', 'simple effortless look']","['내추럴 스타일 패션', '편안한 루즈핏', '린넨 셔츠와 코튼 팬츠', '뉴트럴 컬러 코디', '꾸밈없는 편안한 룩']"
|
| 30 |
+
french_chic,프렌치시크,"['french chic', '프렌치시크룩', '파리지앵 스타일']","프랑스 파리 여성들의 무심한 듯 시크한 스타일. 스트라이프 티셔츠(브레통 셔츠), 트렌치 코트, 발레리나 플랫, 레드 립 등이 상징적.","['french chic aesthetic', 'parisian style fashion', 'breton stripe shirt', 'effortless chic with trench coat', 'jeanne damas style']","['프렌치시크 스타일', '파리지앵 패션', '스트라이프 티셔츠 (브레통 셔츠)', '트렌치 코트를 활용한 무심한 시크함', '제인 버킨 스타일']"
|
| 31 |
+
western_look,웨스턴룩,"['western look', '웨스턴 스타일', '카우보이 스타일']","미국 서부 카우보이 스타일에서 영감을 받은 룩. 데님(청청 패션), 웨스턴 부츠, 프린지 장식, 웨스턴 셔츠, 볼로 타이 등이 특징.","['western fashion style', 'cowboy aesthetic', 'denim on denim with cowboy boots', 'western shirt with fringe jacket', 'bolo tie accessory']","['웨스턴 패션 스타일', '카우보이 스타일', '청청 패션과 웨스턴 부츠', '웨스턴 셔츠와 프린지 자켓', '볼로 타이 액세서리']"
|
| 32 |
+
minimal,미니멀,"['minimalism', '미니멀리즘', '미니멀룩']","장식적인 요소를 최소화하고 단순함을 추구하는 스타일. 깔끔한 실루엣, 무채색(블랙, 화이트, 그레이) 및 뉴트럴 톤, 로고가 없는 디자인이 특징.","['minimalist fashion', 'clean silhouette outfit', 'monotone clothing', 'simple and modern look', 'unembellished design']","['미니멀 패션', '깔끔한 실루엣의 아웃핏', '무채색 코디', '심플하고 모던한 룩', '장식 없는 디자인']"
|
| 33 |
+
simple_basic,심플 베이직,"['simple basic', '심플 베이직룩', '기본템 코디']","미니멀과 유사하나, '기본 아이템'에 더 초점을 맞춘 스타일. 흰 티셔츠, 기본 셔츠, 청바지, 슬랙스 등 유행을 타지 않는 기본 아이템 위주의 조합.","['simple basic style', 'wardrobe essentials', 'white t-shirt and blue jeans', 'classic button-down shirt', 'timeless basic items']","['심플 베이직 스타일', '기본 아이템 코디', '흰 티셔츠와 청바지', '클래식 버튼다운 셔츠', '유행 안 타는 기본템']"
|
| 34 |
+
modern_chic,모던시크,"['modern chic', '모던시크룩', '도시적인 스타일']","현대적이고(Modern) 세련된(Chic) 도시적인 무드의 스타일. 미니멀을 기반으로 하되, 구조적인 실루엣(블레이저, 와이드 슬랙스)이나 포인트 컬러를 사용해 세련미를 강조.","['modern chic aesthetic', 'urban sophisticated style', 'structured blazer outfit', 'wide-leg slacks', 'minimal but sharp look']","['모던시크 스타일', '도시적이고 세련된 룩', '각 잡힌 블레이저 코디', '와이드 슬랙스', '미니멀하지만 날카로운 룩']"
|
| 35 |
+
modern_classic,모던 클래식,"['modern classic', '모던 클래식룩']","전통적인 클래식 아이템(트렌치 코트, 테일러드 수트, 로퍼)을 현대적인 실루엣이나 소재와 결합하여 재해석한 스타일.","['modern classic style', 'timeless fashion with modern twist', 'classic trench coat', 'tailored suit', 'loafers with slacks']","['모던 클래식 스타일', '현대적으로 재해석한 클래식 패션', '클래식 트렌치 코트', '테일러드 수트', '로퍼와 슬랙스 코디']"
|
| 36 |
+
dandy,댄디,"['dandy style', '댄디룩', '깔끔한 남성복']","멋을 내는 남성(Dandy)이라는 뜻에서 유래한, 깔끔하고 클래식한 남성 스타일. 주로 테일러드 재킷, 셔츠, 슬랙스, 니트, 로퍼 등을 활용.","['dandy style for men', ""clean and neat men's fashion"", 'tailored jacket with slacks', 'knitwear over shirt', 'classic loafers']","['댄디 스타일', '깔끔한 남자 패션', '테일러드 자켓과 슬랙스', '셔츠 위 니트 레이어드', '클래식 로퍼 코디']"
|
| 37 |
+
vintage,빈티지,"['vintage style', '빈티지룩', '구제 패션', '레트로']","과거 특정 시대(주로 20세기)의 패션을 재현하거나 실제 중고(구제) 의류를 활용하는 스타일. 바랜 색감, 레트로 그래픽, 고유한 패턴이 특징.","['vintage fashion aesthetic', 'retro style clothing', 'secondhand clothing look', '80s or 90s inspired outfit', 'faded colors and retro graphics']","['빈티지 패션 스타일', '레트로 스타일 의류', '구제 옷 코디', '80년대 또는 90년대 감성 룩', '빛 바랜 색감과 레트로 그래픽']"
|
| 38 |
+
street_fashion,스트릿 패션,"['streetwear', '스트릿룩', '스트리트 패션']","(대분류) 스케이트보드, 힙합, 서핑 등 도시의 길거리(Street) 하위문화에서 발생한 패션. 루즈핏, 그래픽 티셔츠, 후드티, 스니커즈가 중심.","['streetwear fashion', 'urban street style', 'graphic hoodie and baggy jeans', 'sneakers culture', 'supreme style']","['스트릿웨어 패션', '도시 스트릿 스타일', '그래픽 후드와 배기진', '스니커즈 문화', '슈프림 스타일']"
|
| 39 |
+
hip_hop,힙합,"['hip hop fashion', '힙합 스타일', '스웩']","힙합 문화에서 파생된 스타일. 매우 넉넉한 오버사이징(배기 팬츠, 큰 후드), 스냅백, 볼드한 액세서리(체인), 농구화 등이 특징.","['hip hop fashion style', '90s hip hop look', 'oversized hoodie and baggy pants', 'snapback cap and gold chain', 'basketball sneakers']","['힙합 패션 스타일', '90년대 힙합 룩', '오버사이즈 후드와 배기 팬츠', '스냅백과 골드 체인', '농구화 코디']"
|
| 40 |
+
skater_look,스케이터룩,"['skater style', '스케이터 스타일', '보드룩']","스케이트보더들의 스타일. 활동성을 위한 루즈핏 티셔츠, 반스(Vans)나 컨버스(Converse) 같은 스케이트 슈즈, 디키즈(Dickies) 팬츠, 볼캡이 상징적.","['skater style aesthetic', 'skateboarder fashion', 'loose fit graphic t-shirt', 'vans old skool sneakers', 'dickies pants and beanie']","['스케이터 스타일', '스케이트보더 패션', '루즈핏 그래픽 티셔츠', '반스 올드스쿨 스니커즈', '디키즈 팬츠와 비니']"
|
| 41 |
+
techwear,테크웨어,"['techwear', '테크웨어룩', '아크로님 스타일']","고프코어와 유사하나, 더 도시적이고 전술적(Tactical)인 무드를 가짐. 기능성 방수/방풍 소재, 많은 포켓, 스트랩, 버클 디테일, 어두운 컬러가 특징.","['techwear aesthetic', 'urban functional fashion', 'jacket with many pockets and straps', 'waterproof cargo pants', 'acronym style']","['테크웨어 스타일', '도시형 기능성 패션', '포켓과 스트랩이 많은 자켓', '방수 카고 팬츠', '아크로님 스타일']"
|
| 42 |
+
workwear,워크웨어,"['workwear', '워크웨어룩', '아메카지 기반']","19-20세기 미국 노동자들의 작업복에서 유래한 스타일. 튼튼한 데님(진), 덕 캔버스 소재(칼하트), 워커 부츠, 오버롤(멜빵바지) 등이 특징.","['workwear style', 'american casual fashion', 'carhartt duck jacket', 'denim overalls', 'timberland boots']","['워크웨어 스타일', '아메리칸 캐주얼 패션', '칼하트 덕 자켓', '데님 오버롤 (멜빵바지)', '팀버랜드 워커']"
|
| 43 |
+
military_look,밀리터리룩,"['military look', '밀리터리 스타일', '군복 패션']","군복(Military uniform)에서 영감을 받은 스타일. 카모플라주(위장) 패턴, 카고 팬츠(건빵바지), 야상 점퍼(M-65), 항공 점퍼(MA-1)가 대표 아이템.","['military fashion style', 'camouflage pattern', 'cargo pants', 'M-65 field jacket', 'MA-1 bomber jacket']","['밀리터리 패션 스타일', '카모플라주 패턴 (군복 무늬)', '카고 팬츠 (건빵바지)', '야상 점퍼', '항공 점퍼']"
|
| 44 |
+
city_boy,시티보이룩,"['city boy', '시티보이', '씨티보이', '뽀빠이 매거진 스타일']","일본 '뽀빠이(Popeye)' 매거진에서 유래한 스타일. 넉넉하고 여유 있는 실루엣이 특징. 오버핏 셔츠, 치노 팬츠, 볼캡, 스니커즈 등을 주로 매치.","['japanese city boy aesthetic', 'oversized button-down shirt with wide chino pants', 'skater style mixed with preppy look', 'loose fit casual outfit with baseball cap', ""style from 'popeye' magazine""]","['일본 시티보이 스타일', '오버사이즈 셔츠와 와이드 치노 팬츠', '스케이터와 프레피가 섞인 룩', '볼캡을 매치한 루즈핏 코디', '뽀빠이 매거진 스타일']"
|
| 45 |
+
kku_an_kku,꾸안꾸,"['꾸안꾸룩', 'effortless chic', 'korean natural style']","'꾸민 듯 안 꾸민 듯'의 줄임말. 편안해 보이지만(스웻셔츠, 루즈핏 니트) 실루엣이나 소재, 액세서리 등으로 은근히 멋을 낸 자연스러운 스타일.","[""korean 'kku-an-kku' style"", 'effortless chic', 'natural and simple but stylish', 'oversized sweater with simple accessories', 'casual but fashionable']","['꾸안꾸 스타일', '무심한 듯 시크한 룩', '자연스럽지만 멋스러운 코디', '오버사이즈 스웨터와 심플한 액세서리', '편안하지만 패셔너블한']"
|
| 46 |
+
campus_look,캠퍼스룩,"['campus look', '대학생 코디', '프레피 캐주얼']","대학생들의 편안하면서도 단정한 스타일. 후드티, 맨투맨, 바시티 재킷(과잠), 백팩, 스니커즈, 청바지 등이 주요 아이템.","['campus look aesthetic', 'college student style', 'varsity jacket with hoodie', 'backpack and sneakers outfit', 'preppy casual look']","['캠퍼스룩 스타일', '대학생 코디', '바시티 자켓(과잠)과 후드티', '백팩과 스니커즈 코디', '프레피 캐주얼 룩']"
|
| 47 |
+
one_mile_wear,원마일웨어,"['one-mile wear', '원마일웨어룩', '라운지웨어', '집 근처 패션']","집 근처 1마일(약 1.6km) 반경 내에서 입기 좋은 편안하고 실용적인 옷. 주로 스웻 셋업(트레이닝복), 후드 집업, 조거 팬츠, 레깅스 등.","['one-mile wear', 'loungewear', 'comfortable sweat suit set', 'jogger pants and hoodie zip-up', 'athleisure look']","['원마일웨어', '라운지웨어', '편안한 스웻 셋업 (트레이닝복 세트)', '조거 팬츠와 후드 집업', '애슬레저 룩']"
|
| 48 |
+
mix_match,믹스매치,"['mix and match', '믹스매치룩', '스타일 믹스']","서로 다른 스타일, 소재, 패턴의 아이템을 의도적으로 혼합하여 새로운 조화를 이끌어내는 코디 방식. (예: 포멀한 재킷 + 캐주얼한 조거 팬츠)","['mix and match fashion style', 'mixing different styles', 'formal jacket with casual pants', 'pattern mixing outfit', 'unexpected combination']","['믹스매치 패션 스타일', '서로 다른 스타일 조합', '포멀한 자켓과 캐주얼한 바지', '패턴 믹스 코디', '의외의 조합']"
|
| 49 |
+
sports_mix,스포츠믹스,"['sports mix', '스포츠믹스룩', '애슬레저']","믹스매치의 일종으로, 스포츠웨어(트랙 팬츠, 저지, 아노락)를 일상복(재킷, 셔츠, 코트)과 조합하는 스타일.","['sports mix style', 'athleisure fashion', 'track pants with formal jacket', 'sports jersey with coat', 'sporty and casual mix']","['스포츠믹스 스타일', '애슬레저 패션', '트랙 팬츠(추리닝)와 포멀한 자켓', '스포츠 저지와 코트 조합', '스포티 캐주얼 믹스']"
|