Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a36235d58e | |||
| 433aa3e751 | |||
| 7bf080b3e7 | |||
| 03c4759895 | |||
| 03ff6605a3 | |||
| 4b3b0f6aa8 | |||
| c798d37fdd | |||
| 43fd576da6 | |||
| 1bbb9c945e | |||
|
|
3ca6b16eaf | ||
|
|
f9d83f6a99 | ||
|
|
496e7ad590 | ||
|
|
a7b101253b | ||
|
|
077ceea219 | ||
| 4fa815158f | |||
|
|
46793ba271 | ||
|
|
773db4fcc3 | ||
|
|
13b99f4dd3 | ||
|
|
9d6fbc92f1 | ||
|
|
e7ec547671 | ||
| 507d8a3e12 | |||
| feb431e9c1 | |||
| 042e6015f0 | |||
| 8ccf899441 | |||
| 7a1496aeb7 | |||
| d8df6b28ea | |||
| 79ec1c5300 | |||
| 1f6ba5fd09 | |||
|
|
54aac900ad | ||
| 884e7966dd | |||
| 2db36e2c1d | |||
|
|
3910c07c40 | ||
|
|
c0c72a9c87 |
40
.gitea/workflows/prod_build_manual.yaml
Normal file
40
.gitea/workflows/prod_build_manual.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
name: 手动 LC python main 分支构建部署
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
scheduled_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
REMOTE_DEPLOY_PATH: /workspace/LC_Workspace/LitServe_Server_Workspace/lc_stylist_agent
|
||||
|
||||
steps:
|
||||
- name: 1.检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'main'
|
||||
|
||||
- name: 2.复制文件到服务器
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
password: ${{ secrets.SERVER_PASSWORD }}
|
||||
source: "."
|
||||
target: ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
|
||||
- name: 3.重启docker-compose
|
||||
uses: appleboy/ssh-action@v0.1.10
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
password: ${{ secrets.SERVER_PASSWORD }}
|
||||
script: |
|
||||
# 进入项目目录
|
||||
cd ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
|
||||
docker-compose down 2>&1
|
||||
docker-compose up -d --build --remove-orphans 2>&1
|
||||
|
||||
# docker image prune -f 2>&1
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ data/
|
||||
.prod_env
|
||||
google_application_credentials.json
|
||||
*.bash
|
||||
test
|
||||
app/google_application_credentials.json
|
||||
63
Dockerfile
63
Dockerfile
@@ -1,46 +1,37 @@
|
||||
# Change CUDA and cuDNN version here
|
||||
FROM ghcr.io/astral-sh/uv:latest AS uv_bin
|
||||
FROM nvidia/cuda:12.4.1-base-ubuntu22.04
|
||||
ARG PYTHON_VERSION=3.10
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
# 1. 基础环境配置
|
||||
ENV UV_LINK_MODE=copy \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
UV_PROJECT_ENVIRONMENT=/app/.venv
|
||||
|
||||
COPY --from=uv_bin /uv /uvx /bin/
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
software-properties-common \
|
||||
wget \
|
||||
&& add-apt-repository ppa:deadsnakes/ppa \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
python$PYTHON_VERSION \
|
||||
python$PYTHON_VERSION-dev \
|
||||
python$PYTHON_VERSION-venv \
|
||||
&& wget https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
||||
&& python$PYTHON_VERSION get-pip.py \
|
||||
&& rm get-pip.py \
|
||||
&& ln -sf /usr/bin/python$PYTHON_VERSION /usr/bin/python \
|
||||
&& ln -sf /usr/local/bin/pip$PYTHON_VERSION /usr/local/bin/pip \
|
||||
&& python --version \
|
||||
&& pip --version \
|
||||
&& apt-get purge -y --auto-remove software-properties-common \
|
||||
libcurl4-openssl-dev \
|
||||
build-essential \
|
||||
libgl1 \
|
||||
libglib2.0-0 \
|
||||
ca-certificates \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
####### Add your own installation commands here #######
|
||||
# RUN pip install some-package
|
||||
# RUN wget https://path/to/some/data/or/weights
|
||||
# RUN apt-get update && apt-get install -y <package-name>
|
||||
RUN apt-get update && \
|
||||
apt-get install -y libcurl4-openssl-dev build-essential
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install libgl1
|
||||
RUN apt-get -y install libglib2.0-0
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
|
||||
# Install litserve and requirements
|
||||
RUN pip install --upgrade pip setuptools wheel
|
||||
RUN pip install --no-cache-dir litserve==0.2.16 -r requirements.txt
|
||||
RUN pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
||||
COPY pyproject.toml uv.lock ./
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=0
|
||||
|
||||
RUN uv sync --frozen --no-dev --no-install-project --python 3.10
|
||||
|
||||
# 4. 拷贝项目文件并安装项目本身
|
||||
COPY . .
|
||||
RUN uv sync --frozen --no-dev --python 3.10
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["python", "-m","app.main"]
|
||||
#CMD ["tail", "-f","/dev/null"]
|
||||
CMD ["uv", "run","-m","app.main"]
|
||||
|
||||
@@ -16,6 +16,12 @@ class Settings(BaseSettings):
|
||||
env_file_encoding='utf-8',
|
||||
extra='ignore' # 忽略环境变量中多余的键
|
||||
)
|
||||
# 启动端口
|
||||
SERVE_PORT: int = Field(default=8000, description='')
|
||||
|
||||
# 换脸模型服务
|
||||
RE_FACE_MODEL_URL: str = Field(default="10.1.1.240:10071", description='')
|
||||
|
||||
# 调试配饰
|
||||
LOCAL: int = Field(default=0, description="是否在本地运行,1表示本地运行,0表示生产环境运行")
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
from pydantic import Field
|
||||
import time
|
||||
|
||||
@@ -15,6 +16,7 @@ from app.server.ChatbotAgent.core.redis_manager import RedisManager
|
||||
from app.server.ChatbotAgent.core.stylist_agent_server import AsyncStylistAgent
|
||||
from app.server.ChatbotAgent.core.prompt import SUMMARY_PROMPT
|
||||
from app.server.ChatbotAgent.core.vector_database import VectorDatabase
|
||||
from app.server.utils.request_post import post_request
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,7 +43,7 @@ class OccasionEnum(str, Enum):
|
||||
|
||||
class StylistResponse(BaseModel):
|
||||
occasions: List[OccasionEnum] = Field(
|
||||
description="A list of **applicable** occasions that are most strongly implied or explicitly requested by the user's conversation history. These occasions are used later in item retrieval for filtering and must strictly match the predefined OccasionEnum list."
|
||||
description="A list of **applicable** occasions that are most strongly implied or explicitly requested by the user's conversation history. The first occasion in this list is the most applicable one. These occasions are used later in item retrieval for filtering and must strictly match the predefined OccasionEnum list."
|
||||
)
|
||||
summary: str = Field(
|
||||
description="A detailed summary of the user's styling requirements, preferences, constraints, and specific item requests."
|
||||
@@ -56,6 +58,8 @@ class AgentRequestModel(BaseModel):
|
||||
batch_sources: List[str]
|
||||
callback_url: str
|
||||
gender: str
|
||||
occasions: Optional[list] = None
|
||||
request_summary: Optional[str] = None
|
||||
|
||||
|
||||
class LCAgent(ls.LitAPI):
|
||||
@@ -105,8 +109,12 @@ class LCAgent(ls.LitAPI):
|
||||
|
||||
async def background_run(self, request: AgentRequestModel, outfit_ids):
|
||||
# 1. 根据用户ID查询对话历史,总结对话内容
|
||||
if request.request_summary and request.occasions:
|
||||
request_summary = request.request_summary
|
||||
occasions = request.occasions
|
||||
else:
|
||||
request_summary, occasions = await self.get_conversation_summary(request.session_id)
|
||||
logger.info(f"request_summary: {request_summary}")
|
||||
logger.info(f"request_summary: {request_summary},occasions : {occasions}")
|
||||
|
||||
# 2.根据对话总结推荐搭配
|
||||
recommendation_results = await self.recommend_outfit(
|
||||
@@ -138,7 +146,7 @@ class LCAgent(ls.LitAPI):
|
||||
history_messages = self.redis.get_history(session_id)
|
||||
if not history_messages:
|
||||
# 处理无历史记录的情况
|
||||
return {"occasions": [], "summary": "User has no history provided."}
|
||||
return "User has no history provided.", []
|
||||
|
||||
input_message = "\n".join([f"{msg.role.value}: {msg.content}" for msg in history_messages])
|
||||
json_schema = StylistResponse.model_json_schema()
|
||||
@@ -187,32 +195,31 @@ class LCAgent(ls.LitAPI):
|
||||
task_map = {}
|
||||
|
||||
stylist_agent_kwages = self.stylist_agent_kwages.copy()
|
||||
if num_outfits == 1:
|
||||
# 通过请求数量判断 num == 1 单个outfit刷新
|
||||
stylist_agent_kwages['outfit_id'] = outfit_ids[0]
|
||||
stylist_agent_kwages['stylist_name'] = stylist_name
|
||||
stylist_agent_kwages['gender'] = gender
|
||||
agent = AsyncStylistAgent(**stylist_agent_kwages)
|
||||
task = agent.run_iterative_styling(
|
||||
request_summary=request_summary,
|
||||
occasions=occasions,
|
||||
start_outfit=start_outfit,
|
||||
batch_sources=batch_sources,
|
||||
user_id=user_id,
|
||||
callback_url=callback_url,
|
||||
)
|
||||
tasks.append(task)
|
||||
task_map[task] = {"outfit_id": outfit_ids[0], "retries": 0}
|
||||
elif num_outfits > 1:
|
||||
# 通过请求数量判断 num > 1 四套搭配推荐 (1快 , num-1慢)
|
||||
tasks_mapping = {}
|
||||
|
||||
# 历史1快3慢推荐逻辑
|
||||
# if num_outfits == 1:
|
||||
# tasks_mapping[outfit_ids[0]] = "fast"
|
||||
# else:
|
||||
# for i in range(num_outfits):
|
||||
# if i == 0:
|
||||
# tasks_mapping[outfit_ids[i]] = "fast"
|
||||
# else:
|
||||
# tasks_mapping[outfit_ids[i]] = "slow"
|
||||
|
||||
for i in range(num_outfits):
|
||||
stylist_agent_kwages['outfit_id'] = outfit_ids[i]
|
||||
tasks_mapping[outfit_ids[i]] = "fast"
|
||||
|
||||
for k, v in tasks_mapping.items():
|
||||
logger.info(f"fast request outfit_id is : {k}")
|
||||
# 通过请求数量判断 num == 1 单个outfit刷新
|
||||
stylist_agent_kwages['outfit_id'] = k
|
||||
stylist_agent_kwages['stylist_name'] = stylist_name
|
||||
stylist_agent_kwages['gender'] = gender
|
||||
stylist_agent_kwages['callback_url'] = callback_url
|
||||
|
||||
agent = AsyncStylistAgent(**stylist_agent_kwages)
|
||||
if i == 0:
|
||||
# 第一套搭配使用快速方法 一次跑出所有单品
|
||||
logger.info(f"fast request outfit_id is : {outfit_ids[i]}")
|
||||
if v == "fast":
|
||||
task = agent.run_quick_batch_styling(
|
||||
request_summary=request_summary,
|
||||
occasions=occasions,
|
||||
@@ -222,7 +229,6 @@ class LCAgent(ls.LitAPI):
|
||||
callback_url=callback_url,
|
||||
)
|
||||
else:
|
||||
# 后续
|
||||
task = agent.run_iterative_styling(
|
||||
request_summary=request_summary,
|
||||
occasions=occasions,
|
||||
@@ -232,7 +238,7 @@ class LCAgent(ls.LitAPI):
|
||||
callback_url=callback_url,
|
||||
)
|
||||
tasks.append(task)
|
||||
task_map[task] = {"outfit_id": outfit_ids[i], "retries": 0}
|
||||
task_map[task] = {"outfit_id": k, "retries": 0}
|
||||
logger.info(f"--- Starting {num_outfits} concurrent outfit generation tasks. ---")
|
||||
|
||||
# 2. 任务执行与重试循环
|
||||
@@ -243,7 +249,8 @@ class LCAgent(ls.LitAPI):
|
||||
retry_limit = 1 # 允许重试一次
|
||||
while tasks_to_run:
|
||||
try:
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
results = await asyncio.gather(*tasks_to_run, return_exceptions=True)
|
||||
|
||||
next_tasks_to_run = []
|
||||
for task, result in zip(tasks_to_run, results):
|
||||
task_info = task_map[task]
|
||||
@@ -255,7 +262,14 @@ class LCAgent(ls.LitAPI):
|
||||
logger.error(f"Outfit {outfit_id} failed with error: {result}. Current retries: {current_retries}.")
|
||||
|
||||
if current_retries < retry_limit:
|
||||
# 尚未达到重试上限,准备重试
|
||||
# 尚未达到重试上限,准备重试 并通知前端
|
||||
object_data = {
|
||||
'outfit_id': outfit_id,
|
||||
"status": "retrying",
|
||||
"path": "",
|
||||
}
|
||||
post_request(url=f'{callback_url}/api/style/callback', data=json.dumps(object_data))
|
||||
|
||||
task_info["retries"] += 1
|
||||
logger.info(f"--- Retrying outfit {outfit_id} (Attempt {current_retries + 1}/{retry_limit}). ---")
|
||||
|
||||
@@ -263,8 +277,20 @@ class LCAgent(ls.LitAPI):
|
||||
stylist_agent_kwages['outfit_id'] = outfit_id
|
||||
stylist_agent_kwages['stylist_name'] = stylist_name
|
||||
stylist_agent_kwages['gender'] = gender
|
||||
stylist_agent_kwages['callback_url'] = callback_url
|
||||
|
||||
agent = AsyncStylistAgent(**stylist_agent_kwages)
|
||||
if tasks_mapping[outfit_id] == "fast":
|
||||
new_task = agent.run_quick_batch_styling(
|
||||
request_summary=request_summary,
|
||||
occasions=occasions,
|
||||
start_outfit=start_outfit,
|
||||
batch_sources=batch_sources,
|
||||
user_id=user_id,
|
||||
callback_url=callback_url,
|
||||
)
|
||||
else:
|
||||
new_task = agent.run_iterative_styling(
|
||||
request_summary=request_summary,
|
||||
occasions=occasions,
|
||||
start_outfit=start_outfit,
|
||||
@@ -279,8 +305,15 @@ class LCAgent(ls.LitAPI):
|
||||
# 清理旧任务(可选,但推荐,以防内存泄漏或混淆)
|
||||
del task_map[task]
|
||||
else:
|
||||
# 达到重试上限,最终失败
|
||||
# 达到重试上限,最终失败 , 并通知前端
|
||||
object_data = {
|
||||
'outfit_id': outfit_id,
|
||||
"status": "retry_failed",
|
||||
"path": "",
|
||||
}
|
||||
response = post_request(url=f'{callback_url}/api/style/callback', data=json.dumps(object_data))
|
||||
failed_outfits.append(f"Outfit {outfit_id} ultimately failed after {retry_limit} retries: {result}")
|
||||
logger.info(f"request data :{json.dumps(object_data, ensure_ascii=False, indent=2)} | JAVA callback info -> status:{response.status_code} | message:{response.text}")
|
||||
del task_map[task]
|
||||
|
||||
else:
|
||||
@@ -320,19 +353,24 @@ if __name__ == "__main__":
|
||||
# 2. 准备请求数据
|
||||
import json
|
||||
stylist_agent_kwages = agent_api.stylist_agent_kwages.copy()
|
||||
with open("/mnt/data/workspace/Code/lc_stylist_agent/data/2025_q4/request_test.json", "r") as f:
|
||||
if settings.LOCAL == 1:
|
||||
request_file_path = "./data/2025_q4/request_test.json"
|
||||
else:
|
||||
request_file_path = "/mnt/data/workspace/Code/lc_stylist_agent/data/2025_q4/request_test.json"
|
||||
|
||||
with open(request_file_path, "r") as f:
|
||||
request_data = json.load(f)
|
||||
|
||||
tasks_with_metadata = []
|
||||
for test_content in request_data[20:25]:
|
||||
for test_content in request_data[6:7]:
|
||||
occasions = test_content['occasions']
|
||||
request_summary = test_content['request_summary']
|
||||
for stylist_name in ["edi", "vera"]:
|
||||
stylist_agent_kwages['outfit_id'] = test_content['test_case_id'] + "_" + "_".join(occasions) + f"_{stylist_name}"
|
||||
for stylist_name in ["crystal", "mini", "vera", "edi"]:
|
||||
stylist_agent_kwages['outfit_id'] = test_content['test_case_id'] + '_' + test_content['occasions'][0].replace('/', '_') + f"_{stylist_name}"
|
||||
stylist_agent_kwages['stylist_name'] = stylist_name
|
||||
stylist_agent_kwages['gender'] = "female"
|
||||
stylist_agent_kwages['callback_url'] = ""
|
||||
agent = AsyncStylistAgent(**stylist_agent_kwages)
|
||||
# coro = agent.run_iterative_styling(
|
||||
coro = agent.run_quick_batch_styling(
|
||||
request_summary=request_summary,
|
||||
occasions=occasions,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import litserve as ls
|
||||
|
||||
from typing import AsyncGenerator
|
||||
from google import genai
|
||||
from pydantic import BaseModel
|
||||
from sympy.core.evalf import rnd
|
||||
|
||||
from app.config import settings
|
||||
from google.genai import types
|
||||
|
||||
@@ -45,10 +49,18 @@ class LCChatBot(ls.LitAPI):
|
||||
user_msg = Message(role=Role.USER, content=user_message)
|
||||
chat_history = self.redis.get_history(session_id)
|
||||
chat_history.append(user_msg)
|
||||
|
||||
turn_count = sum(1 for msg in chat_history if msg.role == Role.USER)
|
||||
|
||||
if request.gender == 'male':
|
||||
prompt = BASIC_PROMPT.format(gender='men')
|
||||
system_instruction = BASIC_PROMPT.format(gender='men')
|
||||
else:
|
||||
prompt = BASIC_PROMPT.format(gender='women')
|
||||
system_instruction = BASIC_PROMPT.format(gender='women')
|
||||
|
||||
if turn_count >= 3:
|
||||
system_instruction += "\n\nCRITICAL: This is the final turn. Do not ask questions. Provide a brief summary and use the mandatory closing phrase."
|
||||
else:
|
||||
system_instruction += f"\n\nCURRENT STATE: This is turn {turn_count} of 3. Keep gathering info efficiently or provide a brief summary and use the mandatory closing phrase if you have sufficient information."
|
||||
|
||||
contents = []
|
||||
|
||||
@@ -64,7 +76,7 @@ class LCChatBot(ls.LitAPI):
|
||||
model='gemini-2.5-flash',
|
||||
contents=contents,
|
||||
config=types.GenerateContentConfig(
|
||||
system_instruction=prompt,
|
||||
system_instruction=system_instruction,
|
||||
# temperature=0.3,
|
||||
)
|
||||
)
|
||||
@@ -95,42 +107,105 @@ class LCChatBot(ls.LitAPI):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# sys.stdout = open('permanent.log', 'w', encoding='utf-8')
|
||||
|
||||
import asyncio
|
||||
async def run_simple_test():
|
||||
|
||||
|
||||
async def run_simple_test(text):
|
||||
"""
|
||||
一个简单的异步测试用例,用于测试 LCChatBot 的流式输出。
|
||||
"""
|
||||
print("\n" + "=" * 50)
|
||||
print("--- 🔬 开始 LCChatBot 简单流式测试 ---")
|
||||
# print("--- 🔬 开始 LCChatBot 简单流式测试 ---")
|
||||
|
||||
# 1. 初始化 LitAPI 和其依赖
|
||||
chatbot_api = LCChatBot()
|
||||
chatbot_api.setup(device="cpu")
|
||||
print("✅ Setup complete. Mock services initialized.")
|
||||
# print("✅ Setup complete. Mock services initialized.")
|
||||
|
||||
# 2. 构造请求数据
|
||||
request_data = PredictRequest(
|
||||
user_id="simple_user",
|
||||
session_id="simple_session",
|
||||
user_message="I want an outfit. I am going to a evening party with friends. Suggest something stylish yet comfortable.",
|
||||
user_message=text,
|
||||
gender="female"
|
||||
)
|
||||
chatbot_api.redis.clear_history(request_data.session_id)
|
||||
|
||||
print(f"-> 正在发送查询: {request_data.user_message}")
|
||||
print(f"user: \n {request_data.user_message}")
|
||||
|
||||
# 3. 调用 predict 方法并处理流
|
||||
response_generator = chatbot_api.predict(request_data)
|
||||
|
||||
print("\n<- 接收流式响应:")
|
||||
print("agent:")
|
||||
|
||||
# 4. 异步迭代生成器,实时打印输出
|
||||
async for chunk in response_generator:
|
||||
print(chunk, end="", flush=True)
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
# 启动异步事件循环
|
||||
try:
|
||||
asyncio.run(run_simple_test())
|
||||
except Exception as e:
|
||||
print(f"\n发生致命错误: {e}")
|
||||
|
||||
# text_list = [
|
||||
# '我要去参加好朋友的婚礼,你能帮我挑一套衣服吗?',
|
||||
# 'I need something to wear for a big presentation at work tomorrow. I want to look powerful but still approachable.',
|
||||
# 'Who do you think is the best world leader right now?',
|
||||
# 'I’m going on a trip to Paris next week and need some outfits.',
|
||||
# 'Help me find a cool outfit for a rock concert. I hate wearing dresses.',
|
||||
# 'I want to look very cool, 或者是那种很有个性的风格 for a gallery opening.',
|
||||
# "I'm going to a gala. Please list 5 different dress styles for me and use bold text for the names.",
|
||||
# "I'm feeling really sad today and just want an outfit that matches my mood."
|
||||
# ]
|
||||
# for text in text_list:
|
||||
# asyncio.run(run_simple_test(text))
|
||||
|
||||
|
||||
async def run_interactive_test():
|
||||
"""
|
||||
手动输入测试:模拟真实用户与 AI 的多轮对话
|
||||
"""
|
||||
# 1. 初始化
|
||||
chatbot_api = LCChatBot()
|
||||
chatbot_api.setup(device="cpu")
|
||||
|
||||
session_id = "manual_test_session"
|
||||
user_id = "test_user"
|
||||
|
||||
print("--- 👗 欢迎进入 AI 造型师测试 (输入 'quit' 退出) ---")
|
||||
|
||||
|
||||
# 清空旧的测试记录,开始新会话
|
||||
chatbot_api.redis.clear_history(session_id)
|
||||
print("✅ 已清空历史记录,开始新会话。")
|
||||
|
||||
|
||||
while True:
|
||||
# 2. 获取手动输入
|
||||
user_input = input("\nUser (你): ")
|
||||
if user_input.lower() in ['quit', 'exit', '退出']:
|
||||
break
|
||||
|
||||
# 3. 构造请求
|
||||
request_data = PredictRequest(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
user_message=user_input,
|
||||
gender="female" # 或者根据需要修改
|
||||
)
|
||||
|
||||
print("Agent (AI): ", end="")
|
||||
|
||||
# 4. 调用并流式打印
|
||||
# 注意:这里的 predict 内部现在会计算 turn_count
|
||||
response_generator = chatbot_api.predict(request_data)
|
||||
|
||||
full_response = ""
|
||||
async for chunk in response_generator:
|
||||
print(chunk, end="", flush=True)
|
||||
full_response += chunk
|
||||
|
||||
# 将 AI 的回复存入 Redis (如果你的 predict 函数里没存的话)
|
||||
# chatbot_api.redis.add_message(session_id, Role.ASSISTANT, full_response)
|
||||
|
||||
print() # 换行
|
||||
|
||||
asyncio.run(run_interactive_test())
|
||||
|
||||
@@ -1,25 +1,45 @@
|
||||
BASIC_PROMPT = """You are a professional, friendly, and insightful AI {gender}'s styling assistant.
|
||||
|
||||
Your primary mission is to engage in a multi-turn conversation with the user to fully understand their dressing intent. You must adopt a professional yet approachable tone.
|
||||
BASIC_PROMPT = """
|
||||
You are a professional, friendly, and insightful AI {gender}'s styling assistant. You are smart, young, and enthusiastic, turning styling into an exciting experience. Your tone is warm, confident, composed, and genuinely curious about the user's context.
|
||||
|
||||
CONVERSATION GOALS:
|
||||
1. **Occasion:** Determine the specific event (e.g., romantic dinner, summer wedding, business meeting).
|
||||
2. **Style:** Pinpoint the desired aesthetic (e.g., classic elegance, edgy, minimalist, bohemian).
|
||||
3. **Vibe/Details:** Gather any mood or specific constraints (e.g., needs to be comfortable, requires light colors, no bare shoulders).
|
||||
4. **Item Preference:** Ask the user if they have any specific preferences for an item type or silhouette (e.g., preference for a dress, skirt, tailored pants, or a particular neckline/length).
|
||||
1. **Occasion:** Determine the specific event (e.g., romantic dinner, summer wedding).
|
||||
2. **Style:** Pinpoint the desired aesthetic (e.g., minimalist, bohemian).
|
||||
3. **Vibe/Details:** Gather mood or constraints (e.g., comfort, specific colors).
|
||||
4. **Item Preference:** Identify preferences for silhouettes or specific items.
|
||||
|
||||
GUIDANCE FOR RESPONSE GENERATION:
|
||||
- After the user's initial request (e.g., "I want a chic outfit for dinner."), immediately reply with a friendly, targeted follow-up question to elicit the most crucial missing information (usually a combination of **Occasion** and **Style**).
|
||||
- Be concise. Ask only 1 to 2 essential questions per turn.
|
||||
- You must gather sufficient, clear intent before proceeding to actual clothing recommendations.
|
||||
CONVERSATION FLOW:
|
||||
- You have a maximum of 3 turns (one turn = one user message and one assistant response) to gather information.
|
||||
- On the 3rd turn, or *once you have sufficient info*, do NOT ask further questions.
|
||||
- MANDATORY CLOSING: You must end your final response using the FINAL TURN CLOSING RULE below.
|
||||
|
||||
FINAL TURN CLOSING RULE:
|
||||
- Once requirements are gathered, do NOT ask any more questions.
|
||||
- End your final response with a creative and friendly variation of the closing phrase.
|
||||
- REQUIRED COMPONENTS: Every variation MUST mention the "**Generate button**" (this phrase must be bounded using double asterisks).
|
||||
|
||||
PERSONALITY AND COMMUNICATION RULES:
|
||||
- Always reply in English only. If the user speaks another language, state in English that you cannot speak that language and steer back to styling.
|
||||
- Keep responses extremely SHORT (maximum 2 sentences).
|
||||
- Use clear, modern, and vivid language. Avoid clichés or corporate jargon.
|
||||
- Be positive and encouraging, never patronizing.
|
||||
- When uncertain, state assumptions and invite correction.
|
||||
- Gather sufficient intent before providing recommendations. Ask only 1 to 2 essential questions per turn.
|
||||
|
||||
IMPORTANT GUARDRAILS:
|
||||
- ONLY help with styling ideas and outfit generation.
|
||||
- If asked about politics, controversial topics, or anything inappropriate, respond with a witty deflection and steer back to styling.
|
||||
|
||||
OUTPUT FORMAT INSTRUCTION:
|
||||
- **DO NOT** use any Markdown formatting whatsoever (e.g., do not use asterisks (*), bold text (**), lists, or code blocks).
|
||||
- **DO NOT** use any Markdown formatting whatsoever (no asterisks, bold text, lists, or code blocks).
|
||||
- **ONLY** output the plain text response spoken by the AI Assistant.
|
||||
|
||||
Example Follow-up (mimicking a conversational flow):
|
||||
User: I want a chic outfit for dinner.
|
||||
Your Response: Hey there! A chic dinner outfit, I love that! To give you the perfect recommendations, tell me: is this a romantic date, business dinner, or celebration with friends? And what's your go-to style vibe: classic elegance or something with more edge?"""
|
||||
EXAMPLE DIALOGUES:
|
||||
User: 我想找件衣服参加婚礼。 Response: I would love to help you find the perfect wedding guest look if you can tell me the dress code or the venue vibe.
|
||||
|
||||
User: I need a chic outfit for dinner. Response: That sounds like a fabulous evening! Is this a romantic date or a casual night out with friends, and do you prefer a sleek dress or tailored separates?
|
||||
|
||||
User: What do you think about the current election? Response: I prefer to stick to the polls of fashion rather than politics! Tell me, what kind of style aesthetic are we aiming for today?
|
||||
"""
|
||||
|
||||
SUMMARY_PROMPT = """
|
||||
You are an expert fashion request analyzer. Analyze the conversation history provided by the user.
|
||||
@@ -31,133 +51,113 @@ Your task is to:
|
||||
Extract this information accurately from the chat history.
|
||||
"""
|
||||
|
||||
from app.taxonomy import FASHION_TAXONOMY, IGNORE_SUBCATEGORY
|
||||
GENERAL_RULES = """## General Rule
|
||||
### **Color:**
|
||||
* Limit your outfit in two-three color shades max.
|
||||
* Pair one accent color item with neutral colors
|
||||
* Don't show neon color items
|
||||
* Pair colors in warm tone/cool tone
|
||||
* Denim and black and white is a neutral color matches with every other color.
|
||||
|
||||
from app.taxonomy import FASHION_TAXONOMY, IGNORE_SUBCATEGORY, ALL_SUBCATEGORY_LIST
|
||||
core_outfit_template = f"""
|
||||
You are a professional fashion stylist Agent, specialized in creating complete, tailored outfits for {{gender}}. Your current task is to recommend items for the **{{current_category}}** stage, strictly **mimicking the style and preference** specified in the following Stylist Guide.
|
||||
|
||||
Your task is to **create a cohesive and complete outfit**, strictly adhering to **BOTH** the user's explicit **Request Summary** and the **Outfit Style Guide**. You must decide the next logical item to add to the outfit based on the current stage and constraints. Descriptions of current outfit combination is listed in user's message.
|
||||
|
||||
---
|
||||
## Request from the User:
|
||||
{{request_summary}}
|
||||
|
||||
## Core Guidance Document: Outfit Style Guide
|
||||
{{stylist_guide}}
|
||||
---
|
||||
|
||||
## Your Workflow and Constraints
|
||||
|
||||
1. **Style Adherence**: You must strictly observe all rules in the Style Guide concerning **color palette, fit, layering principles, pattern restrictions , shoe coordination**.
|
||||
2. **Uniqueness Mandate**: Every item must follow the **absolute no-repeat rule for subcategories** within its stage. Each subcategory from the allowed list can appear **exactly once** in the entire outfit. Furthermore, the categories 'dresses' and 'pants' and 'skirts' are mutually exclusive; they NORMALLY cannot be included in the same outfit.
|
||||
3. **Step Planning**: The styling sequence must follow a logical approach (e.g., top-down, inside-out for clothing). Prioritize unused subcategories from the allowed list to avoid repetition.
|
||||
4. **Structured Output**: Your output MUST be a valid JSON object. The strict JSON structure and field requirements are provided separately via the API schema.
|
||||
|
||||
You must only output one of two actions: "recommend_item" or "stop".
|
||||
4.1. **recommend_item**: Use this action to suggest the next single item.
|
||||
* **subcategory**: Must be strictly no repeats, and drawn from the allowed list.
|
||||
* **description**: This must be an **extremely detailed and precise** description for the vector search. It MUST include: **Color, Fit/Silhouette, Material/Detail, and Role in the Outfit.**
|
||||
You must strictly use the **JSON format** for your output, as follows:
|
||||
```json
|
||||
{{{{
|
||||
"action": "recommend_item",
|
||||
"subcategory": "YOUR_ITEM_SUBCATEGORY",
|
||||
"description": "YOUR_DETAILED_DESCRIPTION",
|
||||
"reason": "YOUR_RECOMMENDATION_REASON"
|
||||
}}}}
|
||||
|
||||
4.2. **stop**: Use this action when the Termination Condition is met.
|
||||
* **reason**: This field is mandatory when stopping, and must clearly state why the outfit is complete.
|
||||
You must strictly use the **JSON format** for your output, as follows:
|
||||
{{{{
|
||||
"action": "stop",
|
||||
"subcategory": "",
|
||||
"description": "",
|
||||
"reason": "CORE_OUTFIT_COMPLETE"
|
||||
}}}}
|
||||
|
||||
5. **Termination Condition**: Terminate when the below condition is fully met:
|
||||
5.1. **CLOTHING Stage**: The core clothing part of the outfit is complete, meaning the combination of items effectively achieves **full body coverage** (e.g., includes both a top/upper garment and a bottom/lower garment, or a single full-body piece like a dress/jumpsuit). Additionally, **all mandatory elements** stipulated in the Style Guide are satisfied. *(Note: Typically, {{max_len}} items are sufficient for this stage.)*
|
||||
5.2. **SHOES Stage**: **Exactly one (1) item** has been successfully recommended, as shoes are a **mandatory component** for any complete outfit.
|
||||
5.3. **BAGS Stage**: **Exactly one (1) item** has been successfully recommended, **OR** the recommendation is skipped if the Style Guide or the User Request **does not mandate** a bag for the specific occasion (i.e., the bag is considered optional).
|
||||
|
||||
6. **Context Dependency**: The user's next input (if not Start) will contain the **image and description of the selected item**. When recommending the next item:
|
||||
a) First verify the subcategories of all already selected items to ensure no duplicates;
|
||||
b) Select an unused subcategory from the allowed list as the priority;
|
||||
c) Ensure the recommended item coordinates with the already selected items and complies with all rules in the Style Guide.
|
||||
Now, please start building an outfit (with strictly unique categories for all items) and output the JSON for the first item.
|
||||
### **Layering:**
|
||||
* Style at least 2-3 layers
|
||||
* Show white t-shirt, shirt hem under pull over cardigan and hoodies; Don't show hem in layerinmg except sweaters, cardigan and hoodies (if capable)
|
||||
* Style hoodie under blazer/ bomber jacker, leathe jacket/ trechcoat to create a street style casual look (if capable)
|
||||
"""
|
||||
|
||||
GENERAL_RULES_DICT = {
|
||||
'clothing': """### **Body items mix & match:**
|
||||
* Loose/oversized pair with fitted bottoms. Wide bottoms pair with fitted top.
|
||||
* With loose clothing, use a belt or a "French tuck" (tucking just the front of the shirt) to define waistline
|
||||
* Pair casual items with dressy items. Wear a blazer with a graphic tee, or a silk skirt with a chunky knit sweater.
|
||||
* Always add an outer on a top and bottom to finish the outfit, mix and match in at least 2 layers.
|
||||
* Mix menswear-inspired pieces (blazers, loafers, trousers) with feminine pieces (lace, silk, florals) for a dynamic look.
|
||||
* one print per outfit""",
|
||||
'bags': """### **Bag matching:**
|
||||
* Large totes are for day/work; clutches and mini bags are for evening. Never bring a giant work tote to a cocktail event.
|
||||
* A smooth satin dress pair with a leather, a beaded or velvet bag ; Heavy wool pair with a sleek leather bag
|
||||
* Match bag and shoes in same color or match bag and coat in same color
|
||||
* Straw and canvas bags for spring/summer. Velvet and faux fur bags for autumn/winter. Leather works year-round.""",
|
||||
'shoes': """### **Shoes matching:**
|
||||
* Dress with heels ; Jeans and trousers with heels or loafers
|
||||
* Match shoes with skin tone or the bottom piece
|
||||
* Chunky winter layers pair with a boot, platform shoe (boot, platform).
|
||||
* Crop pants pair with ankle boots, don't show skin ; Don't show calf length pants ; Always suggest wide leg floor length pants
|
||||
* Wide-leg trousers pair with a heel ; loafer ; heel boots ; or a sneaker.
|
||||
* No socks with sandals.""",
|
||||
'accessories': """### **Accessory matching:**
|
||||
* Acceesory wear in a set of same metal colour (gold earrings with gold belt buckle)
|
||||
* Wear chokers or short necklaces with V-necks; wear long pendants with crew necks or turtlenecks.
|
||||
* Wear a belt in tuck-in outfits
|
||||
* Style silk scarf for the neck; tie it on your bag handle, around your wrist, or use it as a belt for a pop of color.
|
||||
* No watches, No hats, No sunglasses""",
|
||||
}
|
||||
|
||||
accessories_template = f"""
|
||||
You are a professional fashion stylist Agent, specialized in creating complete, tailored outfits for {{gender}}. Your current task is to finalize the look by recommending accessories for the **{{current_category}}** stage, strictly **mimicking the style and preference** specified in the following Accessories Guide.
|
||||
|
||||
Your final task is to **select the perfect set of accessories** to complete the given outfit. You must strictly adhere to **BOTH** the user's **Request Summary** and the **ACCESSORIES Style Guide**. The **full description of the existing outfit** is provided in the user's message.
|
||||
|
||||
---
|
||||
## CONTEXT
|
||||
[User Request]: {{request_summary}}
|
||||
|
||||
[Accessories Style Guide]:
|
||||
{{stylist_guide}}
|
||||
|
||||
---
|
||||
## ACCESSORIES GENERATION RULES
|
||||
|
||||
1. **Batch Recommendation**: You must output the **COMPLETE LIST of accessories** in a single response using the 'recommended_accessories' list defined in the schema. Do not recommend items one by one.
|
||||
2. **Quantity Constraint**: The total number of accessories recommended in the list must not exceed **{{max_len}}** items. Typically, 1 to {{max_len}} distinct items are required to complete a look.
|
||||
3. **Harmony & Guide Compliance**:
|
||||
- Assess the existing outfit (provided in the user's message) and ensure all accessories complement its style, color palette, and occasion.
|
||||
- **Strictly follow the [Accessories Style Guide]** regarding material types (e.g., metals like gold/silver), total numbers allowed, and specific layering requirements (e.g., mandated watch or jewelry layering).
|
||||
4. **Exclusion List**: Subcategories in the following list are strictly excluded from recommendation: ({IGNORE_SUBCATEGORY}).
|
||||
5. **Description Quality**: The 'description' field for each accessory must be **extremely detailed and precise** for high-accuracy vector search, including: **Color, Material/Detail, and the specific Role in the Outfit.**
|
||||
|
||||
Generate the final, complete accessories list now.
|
||||
"""
|
||||
|
||||
all_items_template = f"""
|
||||
You are a professional fashion stylist Agent, specialized in creating complete, tailored outfits for {{gender}}. Your task is to **generate a Complete, Head-to-Toe Outfit** in a **Single Batch**, strictly **mimicking the style and preference** specified in the Stylist Guide.
|
||||
# ROLE: Professional Fashion Stylist Agent
|
||||
You are a professional fashion stylist for {{gender}}. Your goal is to generate a Complete, Head-to-Toe Outfit in a Single Batch.
|
||||
|
||||
You must create a cohesive look that includes **Clothing, Shoes, Bags, and Accessories**. You must strictly adhere to **BOTH** the user's **Request Summary** and the **Combined Style Guide**.
|
||||
## 1. INTEGRATION LOGIC (How to Think)
|
||||
1. **Analyze User Request**: Identify the target occasion, mood, and specific color/item preferences from the [Request Summary].
|
||||
2. **Apply Stylist Filter**: Use the [Stylist Guide] as the primary aesthetic filter. If the user request and Stylist Guide conflict, the user request takes precedence.
|
||||
3. **Synthesis with Material**: Incorporate the [Material Hint] into your item descriptions to ensure the outfit is contextually appropriate for the {{occasion}} and better performance in item retrieval.
|
||||
4. **Final Rule Check**: Validate the outfit against [General Rules] and ensure no items from the [Exclusion List] are included.
|
||||
|
||||
## 2. RULE HIERARCHY
|
||||
1. **USER REQUEST**: (Highest Priority)
|
||||
1. **STYLIST CORE RULES**: (Secondary Priority)
|
||||
2. **GENERAL COORDINATION RULES**: (Standard Professional Logic)
|
||||
|
||||
---
|
||||
## Request from the User:
|
||||
|
||||
## 3. CONTEXT & GUIDANCE
|
||||
### [User Request Summary]
|
||||
{{request_summary}}
|
||||
|
||||
## Core Guidance Document: Combined Style Guide
|
||||
### [Target Occasion]
|
||||
{{occasion}}
|
||||
|
||||
### [Stylist Guide]
|
||||
{{stylist_guide}}
|
||||
|
||||
### [Material Hint]
|
||||
{{material_hint}}
|
||||
|
||||
### [General Rules]
|
||||
{{general_rule}}
|
||||
|
||||
---
|
||||
|
||||
## GENERATION WORKFLOW & RULES
|
||||
## 4. CONSTRAINTS & WORKFLOW
|
||||
1. **Selection Pool (Mandatory)**: You MUST only choose items from the following **[Allowed Subcategories]**. Do not recommend any subcategory outside this list.
|
||||
**ALLOWED**: {{allowed_subcategories}}
|
||||
|
||||
1. **Holistic Styling**: You are NOT recommending items step-by-step. You must visualize the final look and output **ALL** necessary items (Clothing, Shoes, Bags, Accessories) in a **single JSON response** using the `recommended_items` list.
|
||||
2. **Exclusion List**: Strictly FORBIDDEN to recommend: {",".join(IGNORE_SUBCATEGORY)}.
|
||||
|
||||
2. **Outfit Composition Rules (Mandatory)**:
|
||||
* **CLOTHING**: Ensure **full body coverage**. You must include either [Top + Bottom] OR [One-piece (e.g., Dress/Jumpsuit)]. 'Dresses' and 'Skirts/Pants' are mutually exclusive.
|
||||
* **SHOES**: **Exactly one (1) pair** of shoes is MANDATORY.
|
||||
* **BAGS**: Recommend **0 or 1 bag**. Skip the bag only if the occasion or Style Guide explicitly suggests it (e.g., home wear, yoga).
|
||||
* **ACCESSORIES**: Recommend a set of accessories (typically 1-3 items) that complement the clothing. Follow metal/material constraints in the guide.
|
||||
Number of items in outfit must not exceed {{max_len}}.
|
||||
3. **Mandatory Composition**:
|
||||
- **Clothing**: Follow the stylist and general rules for clothing selection.
|
||||
- **Shoes**: Exactly one (1) pair is mandatory.
|
||||
- **Bags & Accessories**: These are **CONDITIONAL**.
|
||||
- If NO subcategories related to Bags or Accessories appear in the **[ALLOWED]** list, DO NOT recommend them.
|
||||
- Simply omit these categories from your output if they are not in the allowed pool for the current {{occasion}}.
|
||||
- Do not invent subcategories that are not explicitly listed in the [ALLOWED] section.
|
||||
|
||||
3. **Uniqueness Mandate**:
|
||||
* Each **subcategory** belonging to CLOTHING (e.g., 't-shirts', 'sweaters', 'jacket') can appear **EXACTLY ONCE** in the entire list.
|
||||
* But **subcategory** belonging to ACCESSORIES can repeat.
|
||||
4. **Uniqueness**: Each **subcategory** (e.g., 'earrings', 't-shirts') can appear **EXACTLY ONCE**. No repeats.
|
||||
|
||||
4. **Exclusion List**:
|
||||
* The following subcategories are **STRICTLY FORBIDDEN**: ({IGNORE_SUBCATEGORY}). Do not include them in your recommendation.
|
||||
5. **Visual Balance**: Explicitly describe the fit (Loose vs. Fitted) to maintain silhouette balance according to General Rules.
|
||||
|
||||
5. **Style Adherence**:
|
||||
* Ensure all items coordinate in **color, fit, and material**.
|
||||
* Strictly observe the layering principles and color palette defined in the Style Guide.
|
||||
6. Max item number is {{max_len}}.
|
||||
|
||||
6. **Description Quality**:
|
||||
* The `description` field for every item must be **extremely detailed and precise** for high-accuracy vector search.
|
||||
* It MUST include: **Color, Fit/Silhouette, Material/Detail, and Role in the Outfit.**
|
||||
---
|
||||
|
||||
## OUTPUT FORMAT
|
||||
Output a valid JSON object matching the provided API schema. The `recommended_items` array must contain all the items for this outfit.
|
||||
## 5. OUTPUT REQUIREMENT
|
||||
- **Format**: Valid JSON matching the provided API schema (`recommended_items` list).
|
||||
- **Description Quality**: Each 'description' field must be a precise string for vector search: **subcategory + Color + Fit/Silhouette + Material/Detail.**
|
||||
- **Reasoning**: In the 'reason' field, explain how the outfit satisfies the User's Request while maintaining the Stylist's DNA.
|
||||
|
||||
Generate the complete outfit list now.
|
||||
Generate the complete outfit list now
|
||||
"""
|
||||
|
||||
|
||||
@@ -182,14 +182,12 @@ def build_iterative_schema(current_category):
|
||||
return schema
|
||||
|
||||
|
||||
def build_batch_schema(specified_category: str=""):
|
||||
assert(specified_category in FASHION_TAXONOMY.keys() or specified_category == "")
|
||||
if not specified_category:
|
||||
category_range_desc = "the complete final outfit (including all categories)"
|
||||
subcategory_list = ALL_SUBCATEGORY_LIST
|
||||
def build_batch_schema(specified_category: str = "", subcategory_list: list = []):
|
||||
assert (specified_category in FASHION_TAXONOMY.keys() or specified_category == "all")
|
||||
if specified_category == "all":
|
||||
category_range_desc = "all categories of the outfit"
|
||||
else:
|
||||
category_range_desc = specified_category
|
||||
subcategory_list = FASHION_TAXONOMY[specified_category]
|
||||
category_range_desc = f"only *{specified_category}* part of the outfit"
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -203,7 +201,7 @@ def build_batch_schema(specified_category: str=""):
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {"type": "string", "description": f"The detailed description for this {specified_category} item."},
|
||||
"description": {"type": "string", "description": f"The detailed description for this recommended item."},
|
||||
"subcategory": {
|
||||
"type": "string",
|
||||
"description": "The subcategory of the recommended item.",
|
||||
|
||||
@@ -15,19 +15,23 @@ from app.server.utils.minio_client import minio_client, oss_upload_image
|
||||
from app.server.utils.request_post import post_request
|
||||
from app.config import settings
|
||||
from app.server.ChatbotAgent.core.prompt import (
|
||||
core_outfit_template,
|
||||
accessories_template,
|
||||
GENERAL_RULES,
|
||||
GENERAL_RULES_DICT,
|
||||
all_items_template,
|
||||
build_iterative_schema,
|
||||
build_batch_schema
|
||||
)
|
||||
from app.taxonomy import FASHION_TAXONOMY, ALL_SUBCATEGORY_LIST
|
||||
from app.taxonomy import FASHION_TAXONOMY, ALL_SUBCATEGORY_LIST, OCCASION_CATEGORY_MAP, OCCASION_MATERIAL_MAP, SUBCATEGORY_MERGE_MAP
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncStylistAgent:
|
||||
def __init__(self, local_db: str, gemini_model_name: str, outfit_id: str, stylist_name: str, gender: str):
|
||||
ONE_PIECE_SUBS = {"dresses", "suits", "jumpsuits", "bodysuits", "swimwear", "underwear", "lingerie", "pajamas"}
|
||||
TOP_SUBS = {"t-shirts", "shirts", "blouses", "polo shirts", "tank tops", "sweaters", "cardigans", "pullovers", "hoodies", "sweatshirts", "vests", "coats", "jackets", "blazers", "bras"}
|
||||
BOTTOM_SUBS = {"trousers", "pants", "jeans", "joggers", "leggings", "shorts", "skirts", "skorts"}
|
||||
|
||||
def __init__(self, local_db: str, gemini_model_name: str, outfit_id: str, stylist_name: str, gender: str, callback_url: str):
|
||||
# self.outfit_items: List[Dict[str, str]] = []
|
||||
self.outfit_id = outfit_id
|
||||
self.stylist_name = stylist_name
|
||||
@@ -38,13 +42,6 @@ class AsyncStylistAgent:
|
||||
self.local_db = local_db
|
||||
self.gemini_model_name = gemini_model_name
|
||||
self.stop_reason = ""
|
||||
self.headers = {
|
||||
'Accept': "*/*",
|
||||
'Accept-Encoding': "gzip, deflate, br",
|
||||
'User-Agent': "PostmanRuntime-ApipostRuntime/1.1.0",
|
||||
'Connection': "keep-alive",
|
||||
'Content-Type': "application/json"
|
||||
}
|
||||
|
||||
# 存储桶配置
|
||||
try:
|
||||
@@ -59,19 +56,56 @@ class AsyncStylistAgent:
|
||||
)
|
||||
self.gcs_bucket = "lc_stylist_agent_outfit_items"
|
||||
self.minio_bucket = "lanecarford"
|
||||
self.callback_url = f'{callback_url}/api/style/callback'
|
||||
|
||||
def _is_outfit_complete(self, occasion: str, first_try: bool = True) -> bool:
|
||||
"""
|
||||
判断当前的 self.outfit_items 是否满足该场合的完整性要求
|
||||
"""
|
||||
if occasion not in OCCASION_CATEGORY_MAP:
|
||||
# 兜底逻辑:如果场合没定义,至少要有衣服和鞋子
|
||||
raise ValueError(f"Provided {occasion} is not defined in OCCASION_CATEGORY_MAP.")
|
||||
|
||||
requirements = OCCASION_CATEGORY_MAP[occasion]
|
||||
current_subs = [item['subcategory'] for item in self.outfit_items]
|
||||
current_cats = [item['category'] for item in self.outfit_items]
|
||||
|
||||
# --- 1. 鞋子检查 ---
|
||||
# 如果该场合允许穿鞋(List不为空),则必须至少有一双鞋
|
||||
if requirements.get("shoes") and "shoes" not in current_cats:
|
||||
return False
|
||||
|
||||
# --- 2. 衣服检查 (最核心) ---
|
||||
# 检查是否有全身单品 (One-piece)
|
||||
has_one_piece = any(sub in self.ONE_PIECE_SUBS for sub in current_subs)
|
||||
|
||||
if not has_one_piece:
|
||||
# 如果没有连体衣,必须同时有上装和下装
|
||||
has_top = any(sub in self.TOP_SUBS for sub in current_subs)
|
||||
has_bottom = any(sub in self.BOTTOM_SUBS for sub in current_subs)
|
||||
if not (has_top and has_bottom):
|
||||
return False
|
||||
|
||||
# --- 3. 包包检查 ---
|
||||
# 如果该场合(如 Evening)明确需要包,且目前还没找到,可以返回 False 继续重试
|
||||
if requirements.get("bags") and "bags" not in current_cats and first_try:
|
||||
return False
|
||||
|
||||
# --- 4. 配饰检查 ---
|
||||
if requirements.get("accessories") and "accessories" not in current_cats and first_try:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _load_style_guide(self, stylist_name: str):
|
||||
"""加载 markdown 风格指南内容。"""
|
||||
guide_path = os.path.join(settings.STYLIST_GUIDE_DIR, f"{stylist_name}_en.md")
|
||||
acc_guide_path = os.path.join(settings.STYLIST_GUIDE_DIR, f"{stylist_name}_acc.md")
|
||||
try:
|
||||
with open(guide_path, 'r', encoding='utf-8') as file:
|
||||
stylist_guide = file.read()
|
||||
with open(acc_guide_path, 'r', encoding='utf-8') as file:
|
||||
accessories_guide = file.read()
|
||||
return stylist_guide, accessories_guide
|
||||
return stylist_guide
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to load style guide from {guide_path}, {acc_guide_path}: {e}")
|
||||
raise Exception(f"Failed to load style guide from {guide_path}: {e}")
|
||||
|
||||
async def _call_gemini(self, user_input: str, user_id: str, file_name: str, output_schema: Dict[str, Any], image_bytes: bytes = None, system_prompt: str = "") -> str:
|
||||
"""
|
||||
@@ -172,23 +206,23 @@ class AsyncStylistAgent:
|
||||
"""
|
||||
# 1. 生成查询嵌入
|
||||
query_embedding = self.local_db.get_clip_embedding(item_description, is_image=False)
|
||||
search_subcategories = SUBCATEGORY_MERGE_MAP.get(subcategory, [subcategory])
|
||||
|
||||
# 特殊逻辑处理:Clutch 和 Crossbody 在同一场合下不应互换(根据你的规则)
|
||||
# 2. 执行查询,并过滤类别
|
||||
try:
|
||||
results = self.local_db.get_matched_item(
|
||||
query_embedding,
|
||||
category,
|
||||
search_subcategories,
|
||||
occasions=occasions,
|
||||
batch_sources=batch_sources,
|
||||
gender=gender,
|
||||
n_results=1
|
||||
)
|
||||
except ValueError as e:
|
||||
print(f"检测到无效参数错误:{e}")
|
||||
results = []
|
||||
|
||||
if not results:
|
||||
print(f"数据库中未找到符合 '{category}' 和描述的单品。")
|
||||
print(f"No matching item found for description: {item_description}, category: {category}, subcategory: {subcategory}")
|
||||
return None
|
||||
|
||||
# 3. 模拟 Agent 审核(实际应用中,你需要将图片发回给 Agent进行审核)
|
||||
@@ -200,20 +234,45 @@ class AsyncStylistAgent:
|
||||
"category": best_meta['category'],
|
||||
'description': best_meta['description'],
|
||||
"subcategory": best_meta['subcategory'],
|
||||
"brand": best_meta['brand'],
|
||||
"gpt_description": item_description,
|
||||
"gpt_subcategory": subcategory,
|
||||
# 假设 'item_path' 存储在 metadata 中,或从 'item_id' 推导
|
||||
# 这里假设 item_id 就是文件名的一部分
|
||||
"image_path": os.path.join(settings.DATA_ROOT, batch_source, 'image_data', f"{item_id}.jpg")
|
||||
}
|
||||
except Exception as e:
|
||||
print((f"Internal error in _get_next_item: {str(e)}"))
|
||||
return None
|
||||
|
||||
def _build_system_prompt(self, template: str, request_summary: str = "", stylist_guide: str = "", current_category: str = "clothing", max_len: int = 4) -> str:
|
||||
def _build_system_prompt(self, template: str, general_rule: str, request_summary: str = "", occasion: str = "", stylist_guide: str = "", current_category: str = "clothing", allowed_subcategories: list = [], max_len: int = 4) -> str:
|
||||
# 1. 材质偏好 (Occasion Material Map)
|
||||
material_hint = OCCASION_MATERIAL_MAP.get(occasion, "")
|
||||
# Insert the style_guide content into the template
|
||||
if occasion == "Bridal / Wedding":
|
||||
request_summary += "IMPORTANT: Strictly only recommend colorful or white items (clothing, shoes, and bags)."
|
||||
elif occasion == "Evening":
|
||||
request_summary += (" **EVENING STYLE MANDATE:** Prioritize high textural contrast. Avoid flat/matte monochrome or total black looks. Ensure visual depth through layering "
|
||||
"(e.g., varying lengths or sheer panels). Use statement accessories to break simple silhouettes.")
|
||||
elif occasion == "Business / workwear":
|
||||
request_summary += (" **WORKWEAR STYLE MANDATE:** Focus on 'Power Dressing' silhouettes—prioritize sharp, oversized blazers paired with floor-length wide-leg trousers. "
|
||||
"Use tonal dressing to create a sophisticated, elongated look. Balance masculine tailoring with polished, feminine textures. "
|
||||
"Select structured, architectural bags and pointed-toe or sleek loafers. MANDATORY: Include a complete gold or silver jewelry set "
|
||||
"(earrings, necklaces, bracelets, and rings).")
|
||||
elif occasion == "Outdoor":
|
||||
request_summary += " **OUTDOOR STYLE MANDATE:** No jeans allowed. Focus on functional yet polished alternatives like chinos, technical fabrics, or tailored shorts."
|
||||
elif occasion == "Festival / Concert":
|
||||
request_summary += " **FESTIVAL STYLE MANDATE:** No maxi dresses or maxi skirts. Prioritize shorter hemlines, sets, or trousers to ensure ease of movement."
|
||||
elif occasion == "Beach / Swim":
|
||||
request_summary += " **BEACH STYLE MANDATE:** No denim allowed. Focus on breathable, lightweight fabrics like linen, silk, or crochet."
|
||||
|
||||
sys_template = template.format(
|
||||
gender=self.gender,
|
||||
current_category=current_category.upper(),
|
||||
general_rule=general_rule,
|
||||
request_summary=request_summary,
|
||||
stylist_guide=stylist_guide,
|
||||
occasion=occasion,
|
||||
material_hint=material_hint,
|
||||
allowed_subcategories=','.join(allowed_subcategories),
|
||||
max_len=max_len
|
||||
)
|
||||
return sys_template.strip()
|
||||
@@ -239,7 +298,7 @@ class AsyncStylistAgent:
|
||||
context += "\nRecommend a **complete, full outfit**, including all items (clothing, shoes, bags, and accessories), strictly following the Request Summary and Style Guide. Output the **complete list** of items in a single JSON response."
|
||||
return context
|
||||
|
||||
def post_operation(self, status: str, message: str, callback_url: str, img_path: str):
|
||||
def post_operation(self, status: str, message: str, callback_url: str, img_path: str, request_summary=None, occasions=None):
|
||||
"""处理完成后的回调操作。"""
|
||||
if settings.LOCAL == 0:
|
||||
# 生产回调请求数据处理
|
||||
@@ -256,109 +315,22 @@ class AsyncStylistAgent:
|
||||
'status': status,
|
||||
# 'message': message,
|
||||
'path': img_path,
|
||||
'outfit_id': self.outfit_id
|
||||
'outfit_id': self.outfit_id,
|
||||
"request_summary": request_summary,
|
||||
"occasions": occasions
|
||||
}
|
||||
response = post_request(url=callback_url, data=json.dumps(response_data), headers=self.headers)
|
||||
|
||||
if status in ['failed']:
|
||||
# 失败直接打印参数 不发送结果
|
||||
response_data['message'] = message
|
||||
logger.error(f"request data :{json.dumps(response_data, ensure_ascii=False, indent=2)}")
|
||||
else:
|
||||
response = post_request(url=callback_url, data=json.dumps(response_data))
|
||||
logger.info(f"request data :{json.dumps(response_data, ensure_ascii=False, indent=2)} | JAVA callback info -> status:{response.status_code} | message:{response.text}")
|
||||
return response_data
|
||||
else:
|
||||
return {}
|
||||
|
||||
async def _execute_iterative_recommendation(
|
||||
self,
|
||||
current_category: str,
|
||||
system_prompt: str,
|
||||
schema: Dict,
|
||||
max_len: int,
|
||||
occasions: List[str],
|
||||
batch_sources: List[str],
|
||||
user_id: str,
|
||||
url: str
|
||||
):
|
||||
recommend_timestep = 0
|
||||
gemini_data = {'action': 'start'}
|
||||
existing_subcategories = []
|
||||
while recommend_timestep < max_len and gemini_data.get('action') != 'stop':
|
||||
recommend_timestep += 1
|
||||
# 1. 准备用户输入(上下文)
|
||||
user_input = self._build_user_input(current_category, ", ".join(existing_subcategories))
|
||||
|
||||
# 2. 把图片组装起来供api调用
|
||||
merged_image_path, image_bytes = await self._merge_images(self.outfit_id, user_id, self.stylist_name)
|
||||
|
||||
# 3. 调用 Gemini Agent
|
||||
gemini_response_text = await self._call_gemini(
|
||||
user_input,
|
||||
user_id,
|
||||
self.outfit_id,
|
||||
schema,
|
||||
image_bytes,
|
||||
system_prompt
|
||||
)
|
||||
gemini_data = self._parse_gemini_response(gemini_response_text)
|
||||
|
||||
if not gemini_data:
|
||||
print("Agent 返回无效响应,终止流程。")
|
||||
self.post_operation(
|
||||
status="failed",
|
||||
message="Agent returned invalid response, terminating process.",
|
||||
callback_url=url,
|
||||
img_path=merged_image_path,
|
||||
)
|
||||
break
|
||||
|
||||
# 处理推荐单品
|
||||
if gemini_data.get('action') == 'recommend_item':
|
||||
subcategory = gemini_data.get('subcategory')
|
||||
description = gemini_data.get('description')
|
||||
|
||||
# 4a. 检查类别是否有效 (重要步骤)
|
||||
if subcategory not in FASHION_TAXONOMY[current_category]:
|
||||
self.post_operation(
|
||||
status="continue",
|
||||
message=f"Invalid subcategory recommended by Agent: {subcategory}. Requesting Agent to re-output.",
|
||||
callback_url=url,
|
||||
img_path=merged_image_path,
|
||||
)
|
||||
continue
|
||||
|
||||
# 4b. 在本地 DB 中查询单品
|
||||
new_item = self._get_next_item(description, current_category, subcategory, occasions, batch_sources, self.gender)
|
||||
if not new_item:
|
||||
self.post_operation(
|
||||
status="continue",
|
||||
message=f"No matching item is found. Ask Gemini to re-output.",
|
||||
callback_url=url,
|
||||
img_path=merged_image_path,
|
||||
)
|
||||
continue
|
||||
elif new_item['subcategory'] in [x['subcategory'] for x in self.outfit_items]:
|
||||
self.post_operation(
|
||||
status="continue",
|
||||
message=f"{new_item['item_id']}'s subcategory {new_item['subcategory']} duplicated. Ask Gemini to re-output.",
|
||||
callback_url=url,
|
||||
img_path=merged_image_path,
|
||||
)
|
||||
continue
|
||||
elif new_item['item_id'] in [x['item_id'] for x in self.outfit_items]:
|
||||
self.post_operation(
|
||||
status="continue",
|
||||
message=f"Item {new_item['item_id']} duplicated. Ask Gemini to re-output.",
|
||||
callback_url=url,
|
||||
img_path=merged_image_path,
|
||||
)
|
||||
continue
|
||||
else:
|
||||
self.outfit_items.append(new_item)
|
||||
existing_subcategories.append(new_item["subcategory"])
|
||||
self.post_operation(
|
||||
status="ok",
|
||||
message=f"Add new item {new_item['item_id']} in category {new_item['category']} successfully.",
|
||||
callback_url=url,
|
||||
img_path=merged_image_path,
|
||||
)
|
||||
print(f"Stage {current_category.upper()}, Step {recommend_timestep}: {gemini_data}, found item: {new_item['item_id']}")
|
||||
|
||||
async def _execute_batch_recommendation(
|
||||
self,
|
||||
current_category: str, # this can be any category or all
|
||||
@@ -385,99 +357,74 @@ class AsyncStylistAgent:
|
||||
gemini_data = self._parse_gemini_response(gemini_response_text)
|
||||
recommended_items = gemini_data.get('recommended_items', [])
|
||||
reason = gemini_data.get('reason', '')
|
||||
failed_found_item_count = 0
|
||||
|
||||
if not recommended_items or not isinstance(recommended_items, List):
|
||||
print("No recommended item from Gemini, terminating process.")
|
||||
self.post_operation(
|
||||
status="failed",
|
||||
message="Agent returned invalid response, terminating process.",
|
||||
callback_url=url,
|
||||
img_path=merged_image_path
|
||||
)
|
||||
raise Exception("No recommended item from Gemini, terminating process.")
|
||||
|
||||
else:
|
||||
for idx, rec_item in enumerate(recommended_items):
|
||||
subcategory = rec_item.get('subcategory')
|
||||
description = rec_item.get('description')
|
||||
|
||||
# 4a. 检查类别是否有效 (重要步骤)
|
||||
if subcategory not in ALL_SUBCATEGORY_LIST:
|
||||
continue
|
||||
|
||||
# 4b. 在本地 DB 中查询单品
|
||||
# we need first determine the category if current category is 'all'
|
||||
if current_category == "all":
|
||||
for category, subcategories_list in FASHION_TAXONOMY.items():
|
||||
# 将子类别列表转换为集合 (set) 可以提高查找效率,
|
||||
# 特别是当列表很长时。
|
||||
if subcategory in subcategories_list:
|
||||
break
|
||||
category = self._identify_category(subcategory)
|
||||
else:
|
||||
category = current_category
|
||||
|
||||
allowed_subcategories = self._get_allowed_subcategories(occasions[0], category)
|
||||
# 4a. 检查类别是否有效 (重要步骤)
|
||||
if subcategory not in allowed_subcategories:
|
||||
failed_found_item_count += 1
|
||||
logger.warning(f"Failed to found Item {idx + 1}: ({subcategory}) {rec_item}, invalid subcategory {subcategory} recommended by Agent.")
|
||||
continue
|
||||
|
||||
# 4b. 如果这个子类别已经被推荐过了,就舍弃
|
||||
if subcategory in [x['subcategory'] for x in self.outfit_items]:
|
||||
logger.warning(f"Subcategory {subcategory} already recommended, skipping.")
|
||||
continue
|
||||
|
||||
# 4c. 在本地 DB 中查询单品
|
||||
new_item = self._get_next_item(description, category, subcategory, occasions, batch_sources, self.gender)
|
||||
if not new_item or new_item['item_id'] in [x['item_id'] for x in self.outfit_items]:
|
||||
if not new_item:
|
||||
failed_found_item_count += 1
|
||||
logger.warning(f"Failed to found Item {idx + 1}: ({subcategory}) {rec_item}, no matching item is found.")
|
||||
continue
|
||||
elif new_item['item_id'] in [x['item_id'] for x in self.outfit_items]:
|
||||
logger.warning(f"Found Item {idx + 1}: ({subcategory}) {rec_item}, found item is duplicated.")
|
||||
continue
|
||||
else:
|
||||
self.outfit_items.append(new_item)
|
||||
print(f"Item {idx + 1}: ({subcategory}) {rec_item}, found item: {new_item}")
|
||||
logger.info(f"Item {idx + 1}: ({subcategory}) {rec_item}, found item: {new_item}")
|
||||
return reason
|
||||
|
||||
async def run_iterative_styling(self, request_summary, occasions, start_outfit: Optional[List] = None, batch_sources: List = [], user_id="test", callback_url=""):
|
||||
start_time = time.monotonic()
|
||||
STAGES = ['clothing', 'shoes', 'bags']
|
||||
# 深拷贝start_outfit 避免实例之间的参数泄漏 确保每个实例都有自己的 start_outfit 副本
|
||||
if start_outfit is None:
|
||||
self.outfit_items = []
|
||||
else:
|
||||
self.outfit_items = deepcopy(start_outfit)
|
||||
stylist_guide, accessories_guide = self._load_style_guide(self.stylist_name)
|
||||
url = f'{callback_url}/api/style/callback'
|
||||
def _get_allowed_subcategories(self, occasion: str, category: str) -> List[str]:
|
||||
"""根据场景和分类获取合法的子类别列表"""
|
||||
# 如果 occasion 不在配置中,回退到全局分类
|
||||
occ_config = OCCASION_CATEGORY_MAP.get(occasion, {})
|
||||
|
||||
"""主流程控制循环。"""
|
||||
print(f"--- Starting Agent (Outfit ID: {self.outfit_id}) ---")
|
||||
for current_category in STAGES:
|
||||
max_len = 4 if current_category == 'clothing' else 1
|
||||
system_prompt = self._build_system_prompt(core_outfit_template, request_summary, stylist_guide, current_category, max_len)
|
||||
if category == "all":
|
||||
# 获取该场景下所有分类的合集
|
||||
all_subs = []
|
||||
for cat_subs in occ_config.values():
|
||||
all_subs.extend(cat_subs)
|
||||
return all_subs if all_subs else ALL_SUBCATEGORY_LIST
|
||||
|
||||
await self._execute_iterative_recommendation(
|
||||
current_category,
|
||||
system_prompt,
|
||||
build_iterative_schema(current_category),
|
||||
max_len,
|
||||
occasions,
|
||||
batch_sources,
|
||||
user_id,
|
||||
url
|
||||
)
|
||||
# 返回特定分类下的子类
|
||||
return occ_config.get(category, FASHION_TAXONOMY.get(category, []))
|
||||
|
||||
# 根据stylist要求增加配饰 3-4个配饰
|
||||
MAX_LEN_ACC = 3
|
||||
acc_system_prompt = self._build_system_prompt(accessories_template, request_summary, accessories_guide, 'accessories', MAX_LEN_ACC)
|
||||
reason = await self._execute_batch_recommendation(
|
||||
'accessories', # can be 'accessories' or 'all'
|
||||
acc_system_prompt,
|
||||
build_batch_schema(current_category),
|
||||
occasions,
|
||||
batch_sources,
|
||||
user_id,
|
||||
url
|
||||
)
|
||||
|
||||
final_image_path, _ = await self._merge_images(self.outfit_id, user_id, self.stylist_name)
|
||||
response_data = self.post_operation(
|
||||
status="stop",
|
||||
message=reason,
|
||||
callback_url=url,
|
||||
img_path=final_image_path
|
||||
)
|
||||
if settings.LOCAL == 1:
|
||||
with open(os.path.join(settings.OUTFIT_OUTPUT_DIR, self.stylist_name, f'{self.outfit_id}.json'), 'w') as f:
|
||||
json.dump({"request_summary": request_summary, "occasions": occasions, "items": self.outfit_items}, f, indent=2)
|
||||
|
||||
end_time = time.monotonic()
|
||||
total_duration = end_time - start_time
|
||||
|
||||
return response_data, total_duration
|
||||
def _identify_category(self, subcategory: str) -> str:
|
||||
"""反向查找:通过子类确定它属于哪个大类"""
|
||||
for cat, subs in FASHION_TAXONOMY.items():
|
||||
if subcategory in subs:
|
||||
return cat
|
||||
return "unknown"
|
||||
|
||||
async def run_quick_batch_styling(self, request_summary, occasions, start_outfit: Optional[List] = None, batch_sources: List = [], user_id="test", callback_url=""):
|
||||
start_time = time.monotonic()
|
||||
@@ -486,36 +433,73 @@ class AsyncStylistAgent:
|
||||
self.outfit_items = []
|
||||
else:
|
||||
self.outfit_items = deepcopy(start_outfit)
|
||||
stylist_guide, accessories_guide = self._load_style_guide(self.stylist_name)
|
||||
stylist_guide = self._load_style_guide(self.stylist_name)
|
||||
url = f'{callback_url}/api/style/callback'
|
||||
|
||||
print(f"--- Starting Agent (Outfit ID: {self.outfit_id}) ---")
|
||||
|
||||
MAX_LEN = 9
|
||||
system_prompt = self._build_system_prompt(all_items_template, request_summary, stylist_guide + accessories_guide, "", MAX_LEN)
|
||||
if not occasions:
|
||||
occasions = ["Casual"]
|
||||
logger.info(f"""--- Starting Agent (Outfit ID: {self.outfit_id}) ---
|
||||
Occasion: {occasions[0]}. Stylist: {self.stylist_name}. User ID: {user_id}. Request Summary: {request_summary}. Batch sources: {batch_sources}""")
|
||||
|
||||
general_rules = "\n".join(GENERAL_RULES_DICT.values()) + '\n' + GENERAL_RULES
|
||||
allowed_subcategories = self._get_allowed_subcategories(occasions[0], "all")
|
||||
system_prompt = self._build_system_prompt(
|
||||
all_items_template,
|
||||
general_rules,
|
||||
request_summary,
|
||||
occasions[0],
|
||||
stylist_guide,
|
||||
"",
|
||||
allowed_subcategories,
|
||||
MAX_LEN
|
||||
)
|
||||
|
||||
max_retries = 2
|
||||
for attempt in range(max_retries):
|
||||
logger.info(f"Batch recommendation attempt {attempt + 1}")
|
||||
reason = await self._execute_batch_recommendation(
|
||||
'all', # can be 'accessories' or 'all'
|
||||
system_prompt,
|
||||
build_batch_schema(),
|
||||
build_batch_schema(specified_category="all", subcategory_list=allowed_subcategories),
|
||||
occasions,
|
||||
batch_sources,
|
||||
user_id,
|
||||
url
|
||||
)
|
||||
if self._is_outfit_complete(occasions[0], first_try=(attempt==0)):
|
||||
logger.info(f"Successfully assembled a complete outfit for {occasions[0]} after {attempt + 1} attempts. Reason: {reason}")
|
||||
break
|
||||
else:
|
||||
self.post_operation(
|
||||
status="failed",
|
||||
message=f"Failed to assemble a complete outfit after {max_retries} attempts for {occasions[0]}. Current items: {self.outfit_items}. Subcategories required by this occasion is: {allowed_subcategories}",
|
||||
callback_url=url,
|
||||
img_path=""
|
||||
)
|
||||
raise Exception(f"Failed to assemble a complete outfit after {max_retries} attempts for {occasions[0]}. Current items: {self.outfit_items}. Subcategories required by this occasion is: {allowed_subcategories}")
|
||||
|
||||
# 推荐即将完成 回调通知前端
|
||||
self.post_operation(
|
||||
status="almost_done",
|
||||
message="Recommendation has been completed and the outfit is being assembled",
|
||||
callback_url=url,
|
||||
img_path="",
|
||||
)
|
||||
final_image_path, _ = await self._merge_images(self.outfit_id, user_id, self.stylist_name)
|
||||
response_data = self.post_operation(
|
||||
status="stop",
|
||||
message=reason,
|
||||
callback_url=url,
|
||||
img_path=final_image_path
|
||||
img_path=final_image_path,
|
||||
request_summary=request_summary,
|
||||
occasions=occasions
|
||||
)
|
||||
if settings.LOCAL == 1:
|
||||
with open(os.path.join(settings.OUTFIT_OUTPUT_DIR, self.stylist_name, f'{self.outfit_id}.json'), 'w') as f:
|
||||
json.dump({"request_summary": request_summary, "occasions": occasions, "items": self.outfit_items}, f, indent=2)
|
||||
|
||||
end_time = time.monotonic()
|
||||
total_duration = end_time - start_time
|
||||
if settings.LOCAL == 1:
|
||||
with open(os.path.join(settings.OUTFIT_OUTPUT_DIR, self.stylist_name, f'{self.outfit_id}.json'), 'w') as f:
|
||||
json.dump({"request_summary": request_summary, "occasions": occasions, "items": self.outfit_items, "total_duration": total_duration}, f, indent=2)
|
||||
|
||||
return response_data, total_duration
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from PIL import Image
|
||||
from typing import List, Dict, Any
|
||||
from transformers import CLIPProcessor, CLIPModel
|
||||
|
||||
from app.taxonomy import OCCASION, CATEGORY_LIST, IGNORE_SUBCATEGORY
|
||||
from app.taxonomy import OCCASION, CATEGORY_LIST, IGNORE_SUBCATEGORY, BRAND_WHITELIST
|
||||
|
||||
|
||||
class VectorDatabase():
|
||||
@@ -46,7 +46,9 @@ class VectorDatabase():
|
||||
|
||||
return features.cpu().numpy().flatten().tolist()
|
||||
|
||||
def get_matched_item(self, embedding: List[float], category: str, occasions: List[str] = [], batch_sources: List[str] = [], gender: str = 'female', n_results: int = 1) -> List[Dict[str, Any]]:
|
||||
def get_matched_item(self, embedding: List[float], category: str, search_subcategories: List[str], occasions: List[str] = [], batch_sources: List[str] = [], gender: str = 'female', n_results: int = 1) -> List[Dict[str, Any]]:
|
||||
if 'ties' in search_subcategories or 'cufflinks' in search_subcategories:
|
||||
gender = "male"
|
||||
if category not in CATEGORY_LIST:
|
||||
raise ValueError(f"Recommended {category} is not valid.")
|
||||
|
||||
@@ -57,18 +59,20 @@ class VectorDatabase():
|
||||
{"gender": gender},
|
||||
{"gender": "unisex"},
|
||||
]},
|
||||
{"subcategory": {"$nin": IGNORE_SUBCATEGORY}}
|
||||
{"subcategory": {"$in": search_subcategories}}
|
||||
]
|
||||
if batch_sources and len(batch_sources) > 0:
|
||||
if len(batch_sources) == 1:
|
||||
and_conditions.append({"batch_source": batch_sources[0]})
|
||||
else:
|
||||
source_conditions = []
|
||||
for source in batch_sources:
|
||||
source_conditions.append({"batch_source": source})
|
||||
|
||||
# 将 Batch Source 的 OR 子句添加到主 AND 条件中
|
||||
and_conditions.append({"$or": source_conditions})
|
||||
# 加了一条限制,但是部署到生产的时候把他设定为False
|
||||
brand_strication = False
|
||||
if brand_strication:
|
||||
and_conditions.append({"brand": {"$in": BRAND_WHITELIST}})
|
||||
|
||||
# 加一条occasion限制,婚礼不能穿黑色
|
||||
if any(o in ["Bridal / Wedding", "Beach / Swim"] for o in occasions):
|
||||
and_conditions.append({"color": {"$nin": ["BLACK", "DARK GREY", "DARK BLUE", "NAVY"]}})
|
||||
|
||||
if batch_sources and len(batch_sources) > 0:
|
||||
and_conditions.append({"batch_source": {"$in": batch_sources}})
|
||||
|
||||
results = self.collection.query(
|
||||
query_embeddings=[embedding],
|
||||
@@ -80,12 +84,17 @@ class VectorDatabase():
|
||||
return []
|
||||
|
||||
metadatas = results['metadatas'][0] # List[Dict[str, Any]]
|
||||
final_scores = []
|
||||
item_rank_list = []
|
||||
all_scores = []
|
||||
for idx, metadata in enumerate(metadatas):
|
||||
dist_img = results['distances'][0][idx]
|
||||
score_vec = 1 - dist_img # cosine similarity range: [-1, 1]
|
||||
score_subcategory = 1.0
|
||||
# if subcategory == metadata['subcategory']:
|
||||
# score_subcategory = 1
|
||||
|
||||
score_occ = 0.0
|
||||
occasions = occasions[0:1] # 目前只考虑第一个场合
|
||||
if occasions:
|
||||
count = 0
|
||||
for occ in occasions:
|
||||
@@ -94,26 +103,40 @@ class VectorDatabase():
|
||||
count += 1
|
||||
status_val = metadata.get(occ, -1)
|
||||
if status_val == 1:
|
||||
score_occ += 1.0
|
||||
score_occ += 5.0
|
||||
elif status_val == 0:
|
||||
score_occ += 0.0
|
||||
else:
|
||||
score_occ -= 100.0
|
||||
score_occ -= 5.0
|
||||
|
||||
score_occ = score_occ / count if count else 0.0
|
||||
|
||||
final_score = 0.6 * score_vec + 0.4 * score_occ
|
||||
final_scores.append(final_score)
|
||||
final_score = 0.5 * score_vec + 0.1 * score_occ + 0.5 * score_subcategory
|
||||
all_scores.append(final_score)
|
||||
item_rank_list.append({
|
||||
"score": final_score,
|
||||
"metadata": metadata
|
||||
})
|
||||
|
||||
scores_arr = np.array(final_scores)
|
||||
temperature = 0.5
|
||||
scores_arr = scores_arr / temperature
|
||||
item_rank_list.sort(key=lambda x: x['score'], reverse=True)
|
||||
candidate_pool = [item for item in item_rank_list if item['score'] > 0.0]
|
||||
|
||||
# Softmax: 将分数转换为概率
|
||||
exp_scores = np.exp(scores_arr - np.max(scores_arr))
|
||||
if not candidate_pool:
|
||||
print(f"Warning: No positive scores found for {search_subcategories}. Falling back to top matches.")
|
||||
return [item['metadata'] for item in item_rank_list[:n_results]]
|
||||
|
||||
# 采取topk截断
|
||||
current_k = min(10, len(candidate_pool))
|
||||
top_candidates = candidate_pool[:current_k]
|
||||
top_scores = np.array([x['score'] for x in top_candidates])
|
||||
|
||||
#降低温度,使得选择稳定
|
||||
temperature = 0.2
|
||||
exp_scores = np.exp((top_scores - np.max(top_scores)) / temperature)
|
||||
probabilities = exp_scores / np.sum(exp_scores)
|
||||
|
||||
# 采样 (或直接取 Top 1)
|
||||
sampled_index = np.random.choice(a=len(results['ids'][0]), p=probabilities, size=n_results, replace=False) # 不重复采样
|
||||
sampled_items = [metadatas[i] for i in sampled_index]
|
||||
sampled_index = np.random.choice(a=current_k, p=probabilities, size=min(n_results, current_k), replace=False) # 不重复采样
|
||||
sampled_items = [top_candidates[i]['metadata'] for i in sampled_index]
|
||||
|
||||
return sampled_items
|
||||
|
||||
@@ -4,6 +4,8 @@ import litserve as ls
|
||||
import requests
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
class PredictRequest(BaseModel):
|
||||
input_image_list: list[str] # 待换脸图片
|
||||
@@ -17,7 +19,7 @@ class ReFace(ls.LitAPI):
|
||||
|
||||
def predict(self, request):
|
||||
# 服务的 URL
|
||||
url = "http://10.1.1.240:10071/predict"
|
||||
url = f"http://{settings.RE_FACE_MODEL_URL}/predict"
|
||||
|
||||
# 请求头
|
||||
headers = {
|
||||
|
||||
@@ -91,7 +91,8 @@ def merge_images_to_square(outfit_items: List[Dict[str, str]], max_len=9, add_te
|
||||
raise ValueError("No valid images were loaded.")
|
||||
|
||||
if num_images > max_len:
|
||||
raise ValueError(f"Valid item number {num_images} exceed max limit {max_len}")
|
||||
valid_images = valid_images[:max_len]
|
||||
print(f"Valid item number {num_images} exceed max limit {max_len}, only first {max_len} items will be processed.")
|
||||
|
||||
# Get the correct list of target areas based on the number of valid images
|
||||
target_areas = quadrants.get(num_images, [])
|
||||
@@ -103,6 +104,7 @@ def merge_images_to_square(outfit_items: List[Dict[str, str]], max_len=9, add_te
|
||||
for i, (img, item) in enumerate(zip(valid_images, outfit_items)):
|
||||
item_id = item['item_id']
|
||||
category = item['category']
|
||||
subcategory = item['subcategory']
|
||||
if i >= len(target_areas):
|
||||
# This should not happen if num_images <= 4
|
||||
break
|
||||
@@ -155,7 +157,7 @@ def merge_images_to_square(outfit_items: List[Dict[str, str]], max_len=9, add_te
|
||||
|
||||
# --- 文本居中与定位 ---
|
||||
|
||||
full_text = f"ID: {item_id}, Category: {category}"
|
||||
full_text = f"ID: {item_id}, Subcategory: {subcategory}"
|
||||
if add_text:
|
||||
try:
|
||||
# 推荐使用:计算文本的实际尺寸 (width, height)
|
||||
|
||||
@@ -4,18 +4,24 @@ import time
|
||||
import requests
|
||||
|
||||
|
||||
def post_request(url, data=None, json_data=None, headers=None, auth=None, timeout=5):
|
||||
def post_request(url, data=None, json_data=None, auth=None, timeout=5):
|
||||
"""
|
||||
发送POST请求的封装函数
|
||||
|
||||
:param url: 接口的URL地址
|
||||
:param data: 要发送的数据(字典形式,用于表单数据等,会自动编码)
|
||||
:param json_data: 要发送的JSON数据(字典形式,会自动转换为JSON字符串)
|
||||
:param headers: 请求头字典
|
||||
:param auth: 认证信息(如 ('username', 'password') 形式用于基本认证)
|
||||
:param timeout: 超时时间,单位为秒
|
||||
:return: 返回接口的响应对象
|
||||
"""
|
||||
headers = {
|
||||
'Accept': "*/*",
|
||||
'Accept-Encoding': "gzip, deflate, br",
|
||||
'User-Agent': "PostmanRuntime-ApipostRuntime/1.1.0",
|
||||
'Connection': "keep-alive",
|
||||
'Content-Type': "application/json"
|
||||
}
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
@@ -52,6 +58,6 @@ if __name__ == '__main__':
|
||||
'Content-Type': "application/json"
|
||||
}
|
||||
start_time = time.time()
|
||||
X = post_request(url=url, data=json.dumps(object_data), headers=headers)
|
||||
X = post_request(url=url, data=json.dumps(object_data))
|
||||
print(time.time() - start_time)
|
||||
print(X)
|
||||
|
||||
178
app/taxonomy.py
178
app/taxonomy.py
@@ -15,7 +15,6 @@ FASHION_TAXONOMY = {
|
||||
'blouses', # 女式衬衫
|
||||
'polo shirts', # Polo衫
|
||||
'tank tops', # 背心/坎肩
|
||||
'camisoles', # 吊带背心
|
||||
# --- Knits/Sweaters ---
|
||||
'sweaters', # 毛衣 (泛指)
|
||||
'cardigans', # 开衫
|
||||
@@ -91,4 +90,179 @@ FASHION_TAXONOMY = {
|
||||
CATEGORY_LIST = list(FASHION_TAXONOMY.keys())
|
||||
ALL_SUBCATEGORY_LIST = sum(FASHION_TAXONOMY.values(), [])
|
||||
|
||||
IGNORE_SUBCATEGORY = ['socks']
|
||||
IGNORE_SUBCATEGORY = ['socks', 'watches', 'hats', 'eyewear']
|
||||
|
||||
BRAND_WHITELIST = [
|
||||
"ALAÏA",
|
||||
"BALENCIAGA",
|
||||
"BALMAIN",
|
||||
"BARBOUR",
|
||||
"BOTTEGA VENETA",
|
||||
"BRUNELLO CUCINELLI",
|
||||
"ALEXANDERWANG",
|
||||
"CHLOÉ",
|
||||
"DIOR",
|
||||
"FEAR OF GOD",
|
||||
"FENDI",
|
||||
"GIA STUDIOS",
|
||||
"GUCCI",
|
||||
"HELMUT LANG",
|
||||
"JACQUEMUS",
|
||||
"JIMMY CHOO",
|
||||
"JW ANDERSON",
|
||||
"KHAITE",
|
||||
"LEMAIRE",
|
||||
"LOEWE",
|
||||
"MAISON MARGIELA",
|
||||
"MIU MIU",
|
||||
"MM6 MAISON MARGIELA",
|
||||
"MONCLER",
|
||||
"PEDDER RED",
|
||||
"PETER DO",
|
||||
"PHOEBE PHILO",
|
||||
"PRADA",
|
||||
"RICK OWENS",
|
||||
"SACAI",
|
||||
"SKIMS",
|
||||
"THE ROW",
|
||||
"THEORY",
|
||||
"THOM BROWNE",
|
||||
"THE FRANKIE SHOP",
|
||||
"TOTEME",
|
||||
]
|
||||
|
||||
OCCASION_CATEGORY_MAP = {
|
||||
"Casual": {
|
||||
"clothing": ["trousers", "pants", "jeans", "t-shirts", "tank tops", "polo shirts", "hoodies", "leggings", "shorts", "skirts"],
|
||||
"shoes": ["sneakers", "flats", "boots"],
|
||||
"bags": ["shoulder bags", "crossbody"],
|
||||
"accessories": ["earrings", "necklaces", "bracelets", "rings"]
|
||||
},
|
||||
"Formal": {
|
||||
"clothing": ["suits", "trousers", "shirts", "blazers", "skirts", "dresses", "coats"],
|
||||
"shoes": ["formal shoes"],
|
||||
"bags": [],
|
||||
"accessories": ["ties", "earrings", "necklaces", "bracelets", "rings"]
|
||||
},
|
||||
"Activewear": {
|
||||
"clothing": ["leggings", "tank tops", "pants", "joggers", "hoodies", "jackets"],
|
||||
"shoes": ["sneakers"],
|
||||
"bags": ["travel bags"],
|
||||
"accessories": []
|
||||
},
|
||||
"Resort": {
|
||||
"clothing": ["dresses", "shorts", "tank tops", "swimwear"],
|
||||
"shoes": ["sandals"],
|
||||
"bags": ["tote bags"],
|
||||
"accessories": ["earrings", "necklaces", "bracelets", "rings", "scarves"]
|
||||
},
|
||||
"Evening": {
|
||||
"clothing": ["dresses", "blazers", "coats"],
|
||||
"shoes": ["heels"],
|
||||
"bags": ["clutch bags"],
|
||||
"accessories": ["earrings", "necklaces", "bracelets", "rings"]
|
||||
},
|
||||
"Outdoor": {
|
||||
"clothing": ["jackets", "sweaters", "pants", "joggers", "leggings", "shorts"],
|
||||
"shoes": ["boots"],
|
||||
"bags": ["backpacks", "travel bags"],
|
||||
"accessories": []
|
||||
},
|
||||
"Business / workwear": {
|
||||
"clothing": ["trousers", "blouses", "blazers", "skirts"],
|
||||
"shoes": ["formal shoes", "heels", "flats"],
|
||||
"bags": ["tote bags", "shoulder bags"],
|
||||
"accessories": []
|
||||
},
|
||||
"Cocktail / Semi-Formal": {
|
||||
"clothing": ["dresses", "jumpsuits", "skirts", "blouses", "blazers", "coats"],
|
||||
"shoes": ["heels"],
|
||||
"bags": ["clutch bags", "shoulder bags"],
|
||||
"accessories": ["earrings", "necklaces", "bracelets", "rings", "scarves"]
|
||||
},
|
||||
"Black Tie / White Tie": {
|
||||
"clothing": ["dresses", "suits"],
|
||||
"shoes": ["formal shoes"],
|
||||
"bags": ["clutch bags"],
|
||||
"accessories": ["earrings", "necklaces", "bracelets", "rings"]
|
||||
},
|
||||
"Bridal / Wedding": {
|
||||
"clothing": ["dresses", "suits"],
|
||||
"shoes": ["heels", "formal shoes"],
|
||||
"bags": ["clutch bags"],
|
||||
"accessories": ["earrings", "necklaces", "bracelets", "rings"]
|
||||
},
|
||||
"Festival / Concert": {
|
||||
"clothing": ["jackets", "shorts", "tank tops", "skirts"],
|
||||
"shoes": ["boots"],
|
||||
"bags": ["crossbody", "shoulder bags"],
|
||||
"accessories": ["earrings", "necklaces", "bracelets", "rings"]
|
||||
},
|
||||
"Party / Clubbing": {
|
||||
"clothing": ["jackets", "dresses", "skirts", "bodysuits"],
|
||||
"shoes": ["heels", "boots"],
|
||||
"bags": ["clutch bags"],
|
||||
"accessories": []
|
||||
},
|
||||
"Travel / Transit": {
|
||||
"clothing": ["joggers", "sweatshirts", "t-shirts", "hoodies", "sweaters", "jackets"],
|
||||
"shoes": ["sneakers"],
|
||||
"bags": ["backpacks", "travel bags", "tote bags"],
|
||||
"accessories": []
|
||||
},
|
||||
"Athleisure": {
|
||||
"clothing": ["leggings", "hoodies", "tank tops", "joggers", "jackets"],
|
||||
"shoes": ["sneakers"],
|
||||
"bags": ["travel bags", "tote bags"],
|
||||
"accessories": ["earrings", "bracelets"]
|
||||
},
|
||||
"Beach / Swim": {
|
||||
"clothing": ["swimwear", "shorts", "dresses"],
|
||||
"shoes": ["sandals"],
|
||||
"bags": ["tote bags"],
|
||||
"accessories": ["earrings", "necklaces", "bracelets", "rings"]
|
||||
},
|
||||
"Ski / Snow / Mountain": {
|
||||
"clothing": ["jackets", "coats", "sweaters", "hoodies", "pants"],
|
||||
"shoes": ["boots"],
|
||||
"bags": [],
|
||||
"accessories": ["gloves"]
|
||||
},
|
||||
"Garden Party / Daytime Event": {
|
||||
"clothing": ["dresses", "skirts", "blouses", "cardigans"],
|
||||
"shoes": ["sandals"],
|
||||
"bags": ["shoulder bags", "crossbody"],
|
||||
"accessories": ["earrings", "necklaces", "bracelets", "rings"]
|
||||
}
|
||||
}
|
||||
|
||||
OCCASION_MATERIAL_MAP = {
|
||||
"Casual": "Cotton, denim, jersey, fleece, relaxed fits, distressed textures.",
|
||||
"Formal": "Wool, silk, crisp cotton, structured tailoring, cufflinks, leather soles.",
|
||||
"Activewear": "Spandex, moisture-wicking synthetics, mesh, breathable fabrics, compression.",
|
||||
"Resort": "Linen, crochet, chiffon, straw/raffia, tropical prints, breezy silhouettes.",
|
||||
"Evening": "Silk, satin, velvet, sequins, lace, sheer panels, metallic finishes.",
|
||||
"Outdoor": "Gore-Tex, fleece, down, wool, waterproof leather, corduroy, flannel.",
|
||||
"Business / workwear": "Gabardine, crepe, silk blends, modest cuts, structured leather bags.",
|
||||
"Cocktail / Semi-Formal": "Satin, crepe, lace details, ruffles, asymmetric hems, polished hardware.",
|
||||
"Black Tie / White Tie": "Tulle, silk taffeta, fine wool, crystals, pearls, opera gloves.",
|
||||
"Bridal / Wedding": "Chiffon, lace, silk, floral prints (for guests), pastel tones.",
|
||||
"Festival / Concert": "Denim, fringe, leather, crochet, sequins, bold prints, band tees.",
|
||||
"Party / Clubbing": "Sequins, leather/pleather, mesh, cut-outs, bodycon fits, latex.",
|
||||
"Travel / Transit": "Cotton jersey, cashmere blends, stretch denim, soft knits, layers.",
|
||||
"Athleisure": "Scuba fabric, tech fleece, rib-knit, clean lines, logo details.",
|
||||
"Beach / Swim": "Lycra, terry cloth, linen, straw, quick-dry fabrics, sheer voile.",
|
||||
"Ski / Snow / Mountain": "Down feathers, wool, thermal tech, faux fur, waterproof shells.",
|
||||
"Garden Party / Daytime Event": "Floral prints, linen, eyelet lace, cotton poplin, gingham, straw accessories."
|
||||
}
|
||||
|
||||
SUBCATEGORY_MERGE_MAP = {
|
||||
"clutch bags": ["clutch bags", "shoulder bags"],
|
||||
"shoulder bags": ["shoulder bags", "clutch bags"],
|
||||
"crossbody": ["crossbody", "shoulder bags"],
|
||||
"luggage": ["luggage", "backpacks", "travel bags"],
|
||||
"jumpsuits": ["jumpsuits", "dresses"],
|
||||
"bodysuits": ["bodysuits", "dresses"],
|
||||
"suits": ["suits", "dresses"],
|
||||
"pullovers": ["pullovers", "sweaters"],
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
services:
|
||||
lc_agent_server:
|
||||
container_name: LC_Agent_Server
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
@@ -9,12 +10,12 @@ services:
|
||||
DEBUG: 0
|
||||
volumes:
|
||||
- ./app:/app/app
|
||||
- ./.prod_env:/app/.env
|
||||
- ./.env:/app/.env
|
||||
- ./data:/data
|
||||
- ./google_application_credentials.json:/google_application_credentials.json
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "10070:8000"
|
||||
- "${SERVE_PORT}:8000"
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
@@ -23,3 +24,9 @@ services:
|
||||
- driver: nvidia
|
||||
device_ids: [ '0' ]
|
||||
capabilities: [ gpu ]
|
||||
networks:
|
||||
- lc_app_net
|
||||
networks:
|
||||
lc_app_net:
|
||||
external: true
|
||||
name: lc_app_net
|
||||
BIN
docs/2025_q4 stat.xlsx
Normal file
BIN
docs/2025_q4 stat.xlsx
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
docs/Styling AI rules.xlsx
Normal file
BIN
docs/Styling AI rules.xlsx
Normal file
Binary file not shown.
22
pyproject.toml
Executable file
22
pyproject.toml
Executable file
@@ -0,0 +1,22 @@
|
||||
[project]
|
||||
name = "lc-stylist-agent"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
requires-python = "==3.10.*"
|
||||
dependencies = [
|
||||
"chromadb==1.1.1",
|
||||
"google-cloud-storage==2.19.0",
|
||||
"google-genai==1.45.0",
|
||||
"litserve>=0.2.16",
|
||||
"minio==7.2.18",
|
||||
"numpy==1.24.4",
|
||||
"open-clip-torch==2.24.0",
|
||||
"opencv-python==4.9.0.80",
|
||||
"pydantic-settings==2.11.0",
|
||||
"pytorch-fid==0.3.0",
|
||||
"redis==6.4.0",
|
||||
"setuptools==80.9.0",
|
||||
"torch-fidelity==0.3.0",
|
||||
"torchmetrics==1.4.0.post0",
|
||||
"transformers==4.41.1",
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# Crystal's Accessory Guide: Pure Balance
|
||||
This guide strictly outlines accessory selection, emphasizing Gold Tones and pure, solid colors to stabilize the outfit's primary focus on bold pattern clashing.
|
||||
|
||||
## I. Color, Pattern, and Material Constraints
|
||||
Color & Tone: All jewelry must be in Gold Tones. Vector-style accessories are prohibited.
|
||||
|
||||
Accessory Pattern: Bags and shoes must be Solid Color only. Patterned or printed bags and shoes are strictly prohibited.
|
||||
|
||||
|
||||
## II. Mandatory & Stacking Requirements
|
||||
The use of accessories is essential to complete the look, focusing on stacking and specific shapes:
|
||||
|
||||
Mandatory Jewelry: A Necklace is mandatory (minimum one piece). A Watch must be included as part of the wrist stack.
|
||||
|
||||
Earrings: Must be Hoop Earrings.
|
||||
|
||||
Stacking: Encourage stacking of Bracelets and Rings alongside the mandatory watch.
|
||||
@@ -37,3 +37,18 @@ This stylist's style prioritizes visual impact through **clashing prints** rathe
|
||||
5. **Shoe Principle**: Shoes should provide height (low heel, platform, boots).
|
||||
6. **Overall Balance**: If clothing patterns are complex, shoes must be **simple and pure** to ground the look. Style leans toward **mixed-casual and energetic**, avoiding blandness.
|
||||
8. **Scene Adaptability**: Add **cargo pants/low heels** for casual settings; **yoga sets** for sportier looks; use **denim** to balance out heavy prints.
|
||||
|
||||
---
|
||||
|
||||
## Accessory Guide: Pure Balance
|
||||
This section strictly outlines accessory selection, emphasizing Gold Tones and pure, solid colors to stabilize the outfit's primary focus on bold pattern clashing.
|
||||
|
||||
### I. Color, Pattern, and Material Constraints
|
||||
Color & Tone: All jewelry must be in Gold Tones. Vector-style accessories are prohibited.
|
||||
Accessory Pattern: Bags and shoes must be Solid Color only. Patterned or printed bags and shoes are strictly prohibited.
|
||||
|
||||
### II. Mandatory & Stacking Requirements
|
||||
The use of accessories is essential to complete the look, focusing on stacking and specific shapes:
|
||||
Mandatory Jewelry: A Necklace is mandatory (minimum one piece). A Watch must be included as part of the wrist stack.
|
||||
Earrings: Must be Hoop Earrings.
|
||||
Stacking: Encourage stacking of Bracelets and Rings alongside the mandatory watch.
|
||||
@@ -1,10 +0,0 @@
|
||||
# Accessory Style Guide
|
||||
This guide strictly outlines accessory selection, emphasizing a flexible approach to metal tones and highlighting the aesthetic of long-worn items.
|
||||
|
||||
# I. Color and Material Constraints
|
||||
Metal Tones: Both Gold and Silver metals are preferred and should be mixed and matched together.
|
||||
|
||||
Aesthetic Preference: Items that show wear, such as Silver items that have changed color over time, are acceptable as they present a unique personal preference.
|
||||
|
||||
# II. Mandatory & Stacking Requirements
|
||||
Mandatory Items: No specific jewelry piece is listed as mandatory, but the style encourages mixing both gold and silver jewelry.
|
||||
@@ -18,3 +18,13 @@ Color Mixing: All color tones are acceptable for mixing together, including high
|
||||
|
||||
## II. Styling Pattern: Functional and Harmonious
|
||||
This stylist prioritizes practical, comfortable items (functional bag, sneakers) while embracing complex color and print compositions that lean on Black as a foundational element. The style aims for a unique, harmonious look achieved through flexible mixing.
|
||||
|
||||
## Accessory Style Guide
|
||||
This section strictly outlines accessory selection, emphasizing a flexible approach to metal tones and highlighting the aesthetic of long-worn items.
|
||||
|
||||
### I. Color and Material Constraints
|
||||
Metal Tones: Both Gold and Silver metals are preferred and should be mixed and matched together.
|
||||
Aesthetic Preference: Items that show wear, such as Silver items that have changed color over time, are acceptable as they present a unique personal preference.
|
||||
|
||||
### II. Mandatory & Stacking Requirements
|
||||
Mandatory Items: No specific jewelry piece is listed as mandatory, but the style encourages mixing both gold and silver jewelry.
|
||||
@@ -1,12 +0,0 @@
|
||||
Stylist Accessories Guide
|
||||
I. Gold Jewelry
|
||||
Preference: Necklace, Bracelet, and Earrings must all be present and layered.
|
||||
Prohibition: Avoid Large/Bulky Earrings or Vector-style accessories.
|
||||
|
||||
II. Watch
|
||||
Preference: Mandatory item.
|
||||
Prohibition: N/A.
|
||||
|
||||
III. Accent Colors
|
||||
Preference: Added via accessories (e.g., scarves); Max 2 accent colors in total.
|
||||
Prohibition: Avoid Bright/Vivid colors dominating the outfit.
|
||||
@@ -35,3 +35,18 @@ This stylist's outfits emphasize **comfort** and **layering** (creating depth).
|
||||
4. **Pattern Restriction**: Only **plaid/checkered or stripes** are acceptable as subtle accents; **strictly no florals**, unless an extremely minimal exception is made.
|
||||
5. **Shoe/Bag Coordination**: Footwear must be **flat and casual** (white sneakers are preferred).
|
||||
6. **Overall Balance**: When the upper body is complex (layered), the lower body should remain **simple**; the overall style is **neutral and polished**, avoiding highly feminine heels or boots.
|
||||
|
||||
---
|
||||
|
||||
## Stylist Accessories Guide
|
||||
### I. Gold Jewelry
|
||||
Preference: Necklace, Bracelet, and Earrings must all be present and layered.
|
||||
Prohibition: Avoid Large/Bulky Earrings or Vector-style accessories.
|
||||
|
||||
### II. Watch
|
||||
Preference: Mandatory item.
|
||||
Prohibition: N/A.
|
||||
|
||||
### III. Accent Colors
|
||||
Preference: Added via accessories (e.g., scarves); Max 2 accent colors in total.
|
||||
Prohibition: Avoid Bright/Vivid colors dominating the outfit.
|
||||
@@ -1,9 +0,0 @@
|
||||
# Accessory Style Guide
|
||||
This guide outlines accessory selection based on the desired overall aesthetic, emphasizing a balanced approach to metal tones.
|
||||
|
||||
# I. Metal Tone and Aesthetic Constraints
|
||||
Jewelry Tone: Prefers Gold for a vintage and nostalgic feel.
|
||||
|
||||
Jewelry Tone: Prefers Silver for grungier looks.
|
||||
|
||||
Wear Preference: Wears both Gold and Silver, depending on the outfit's desired aesthetic.
|
||||
@@ -1,7 +1,7 @@
|
||||
# Outfit Style Guide
|
||||
This guide summarizes the preferred styling logic, colors, patterns, and structure for Vera's outfits, emphasizing harmony in color mixing and the use of statement bags.
|
||||
|
||||
# I. Core Preferences and Prohibitions
|
||||
## I. Core Preferences and Prohibitions
|
||||
Primary Colors: Most often wears Khakis, Black, Creams, and sometimes Burgundies.
|
||||
|
||||
Dominant Colors: Maximum of two dominant colors per outfit, handled with care.
|
||||
@@ -20,5 +20,16 @@ Bags: Prefers Bigger bags and bags that make a statement. Specific examples incl
|
||||
|
||||
Prohibited Bags: Not a fan of crossbody or micro mini bags.
|
||||
|
||||
# II. Styling Pattern: Harmonious Statement
|
||||
## II. Styling Pattern: Harmonious Statement
|
||||
This stylist prioritizes a core color palette of neutrals and deep tones (Khakis, Black, Creams, Burgundies) and uses print mixing (e.g., different sized polka dots or florals) only when colors and shapes are harmonized. The overall look is anchored by comfortable shoes (loafers) and a large, functional, statement bag.
|
||||
|
||||
|
||||
## Accessory Style Guide
|
||||
This section outlines accessory selection based on the desired overall aesthetic, emphasizing a balanced approach to metal tones.
|
||||
|
||||
### I. Metal Tone and Aesthetic Constraints
|
||||
Jewelry Tone: Prefers Gold for a vintage and nostalgic feel.
|
||||
|
||||
Jewelry Tone: Prefers Silver for grungier looks.
|
||||
|
||||
Wear Preference: Wears both Gold and Silver, depending on the outfit's desired aesthetic.
|
||||
|
||||
Reference in New Issue
Block a user