diff --git a/fastAPI_tarot.py b/fastAPI_tarot.py index 3d3f52b..2d90683 100644 --- a/fastAPI_tarot.py +++ b/fastAPI_tarot.py @@ -80,6 +80,33 @@ CLEANUP_CONFIG = { "interval": int(os.getenv("CLEANUP_INTERVAL_SECONDS", "600")) } +# 提示词配置 (Prompt Config) +PROMPTS = { + "translate": "请将以下描述翻译成简洁、精准的英文,用于图像分割模型(SAM)的提示词。直接返回英文,不要包含任何解释或其他文字。\n\n输入: {text}", + "tarot_card_dual": """这是一张塔罗牌的两个方向: +图1:原始方向 +图2:旋转180度后的方向 + +请仔细对比两张图片的牌面内容(文字方向、人物站立方向、图案逻辑): +1. 识别这张牌的名字(中文)。 +2. 判断哪一张图片展示了正确的“正位”(Upright)状态。 + - 如果图1是正位,说明原图就是正位。 + - 如果图2是正位,说明原图是逆位。 + +请以JSON格式返回,包含 'name' 和 'position' 两个字段。 +例如:{'name': '愚者', 'position': '正位'} 或 {'name': '倒吊人', 'position': '逆位'}。 +不要包含Markdown代码块标记。""", + "tarot_card_single": "这是一张塔罗牌。请识别它的名字(中文),并判断它是正位还是逆位。请以JSON格式返回,包含 'name' 和 'position' 两个字段。例如:{'name': '愚者', 'position': '正位'}。不要包含Markdown代码块标记。", + "tarot_spread": "这是一张包含多张塔罗牌的图片。请根据牌的排列方式识别这是什么牌阵(例如:圣三角、凯尔特十字、三张牌等)。如果看不出明显的正规牌阵,请返回“不是正规牌阵”。请以JSON格式返回,包含 'spread_name' 和 'description' 两个字段。例如:{'spread_name': '圣三角', 'description': '常见的时间流占卜法'}。不要包含Markdown代码块标记。", + "face_analysis": """请仔细观察这张图片中的人物头部/面部特写: +1. 识别性别 (Gender):男性/女性 +2. 预估年龄 (Age):请给出一个合理的年龄范围,例如 "25-30岁" +3. 简要描述:发型、发色、是否有眼镜等显著特征。 + +请以 JSON 格式返回,包含 'gender', 'age', 'description' 字段。 +不要包含 Markdown 标记。""" +} + # API Tags (用于文档分类) TAG_GENERAL = "General Segmentation (通用分割)" TAG_TAROT = "Tarot Analysis (塔罗牌分析)" @@ -265,11 +292,12 @@ def translate_to_sam3_prompt(text: str) -> str: """ print(f"正在翻译提示词: {text}") try: + prompt_template = PROMPTS["translate"] messages = [ { "role": "user", "content": [ - {"text": f"请将以下描述翻译成简洁、精准的英文,用于图像分割模型(SAM)的提示词。直接返回英文,不要包含任何解释或其他文字。\n\n输入: {text}"} + {"text": prompt_template.format(text=text)} ] } ] @@ -503,19 +531,7 @@ def recognize_card_with_qwen(image_path: str) -> dict: "content": [ {"image": file_url}, # 图1 (原图) {"image": rotated_file_url}, # 图2 (旋转180度) - {"text": """这是一张塔罗牌的两个方向: -图1:原始方向 -图2:旋转180度后的方向 - -请仔细对比两张图片的牌面内容(文字方向、人物站立方向、图案逻辑): -1. 识别这张牌的名字(中文)。 -2. 判断哪一张图片展示了正确的“正位”(Upright)状态。 - - 如果图1是正位,说明原图就是正位。 - - 如果图2是正位,说明原图是逆位。 - -请以JSON格式返回,包含 'name' 和 'position' 两个字段。 -例如:{'name': '愚者', 'position': '正位'} 或 {'name': '倒吊人', 'position': '逆位'}。 -不要包含Markdown代码块标记。"""} + {"text": PROMPTS["tarot_card_dual"]} ] } ] @@ -527,7 +543,7 @@ def recognize_card_with_qwen(image_path: str) -> dict: "role": "user", "content": [ {"image": file_url}, - {"text": "这是一张塔罗牌。请识别它的名字(中文),并判断它是正位还是逆位。请以JSON格式返回,包含 'name' 和 'position' 两个字段。例如:{'name': '愚者', 'position': '正位'}。不要包含Markdown代码块标记。"} + {"text": PROMPTS["tarot_card_single"]} ] } ] @@ -562,7 +578,7 @@ def recognize_spread_with_qwen(image_path: str) -> dict: "role": "user", "content": [ {"image": file_url}, - {"text": "这是一张包含多张塔罗牌的图片。请根据牌的排列方式识别这是什么牌阵(例如:圣三角、凯尔特十字、三张牌等)。如果看不出明显的正规牌阵,请返回“不是正规牌阵”。请以JSON格式返回,包含 'spread_name' 和 'description' 两个字段。例如:{'spread_name': '圣三角', 'description': '常见的时间流占卜法'}。不要包含Markdown代码块标记。"} + {"text": PROMPTS["tarot_spread"]} ] } ] @@ -1034,7 +1050,8 @@ async def segment_face( image=image, prompt=final_prompt, output_base_dir=RESULT_IMAGE_DIR, - qwen_model=QWEN_MODEL + qwen_model=QWEN_MODEL, + analysis_prompt=PROMPTS["face_analysis"] ) except Exception as e: import traceback @@ -1268,6 +1285,27 @@ async def set_model(model: str = Form(...)): QWEN_MODEL = model return {"status": "success", "message": f"Model switched to {model}", "current_model": QWEN_MODEL} +@app.get("/admin/api/prompts", dependencies=[Depends(verify_admin)]) +async def get_prompts(): + """ + Get all prompts + """ + return PROMPTS + +@app.post("/admin/api/prompts", dependencies=[Depends(verify_admin)]) +async def update_prompts( + key: str = Form(...), + content: str = Form(...) +): + """ + Update a specific prompt + """ + if key not in PROMPTS: + raise HTTPException(status_code=400, detail="Invalid prompt key") + + PROMPTS[key] = content + return {"status": "success", "message": f"Prompt '{key}' updated"} + # ========================================== # 10. Main Entry Point (启动入口) # ========================================== diff --git a/human_analysis_service.py b/human_analysis_service.py index 925252f..88e196b 100644 --- a/human_analysis_service.py +++ b/human_analysis_service.py @@ -95,7 +95,7 @@ def create_highlighted_visualization(image: Image.Image, masks, output_path: str # Save Image.fromarray(result_np).save(output_path) -def analyze_demographics_with_qwen(image_path: str, model_name: str = 'qwen-vl-max') -> dict: +def analyze_demographics_with_qwen(image_path: str, model_name: str = 'qwen-vl-max', prompt_template: str = None) -> dict: """ 调用 Qwen-VL 模型分析人物的年龄和性别 """ @@ -104,19 +104,24 @@ def analyze_demographics_with_qwen(image_path: str, model_name: str = 'qwen-vl-m abs_path = os.path.abspath(image_path) file_url = f"file://{abs_path}" + # 默认 Prompt + default_prompt = """请仔细观察这张图片中的人物头部/面部特写: +1. 识别性别 (Gender):男性/女性 +2. 预估年龄 (Age):请给出一个合理的年龄范围,例如 "25-30岁" +3. 简要描述:发型、发色、是否有眼镜等显著特征。 + +请以 JSON 格式返回,包含 'gender', 'age', 'description' 字段。 +不要包含 Markdown 标记。""" + + final_prompt = prompt_template if prompt_template else default_prompt + # 构造 Prompt messages = [ { "role": "user", "content": [ {"image": file_url}, - {"text": """请仔细观察这张图片中的人物头部/面部特写: -1. 识别性别 (Gender):男性/女性 -2. 预估年龄 (Age):请给出一个合理的年龄范围,例如 "25-30岁" -3. 简要描述:发型、发色、是否有眼镜等显著特征。 - -请以 JSON 格式返回,包含 'gender', 'age', 'description' 字段。 -不要包含 Markdown 标记。"""} + {"text": final_prompt} ] } ] @@ -144,7 +149,8 @@ def process_face_segmentation_and_analysis( image: Image.Image, prompt: str = "head", output_base_dir: str = "static/results", - qwen_model: str = "qwen-vl-max" + qwen_model: str = "qwen-vl-max", + analysis_prompt: str = None ) -> dict: """ 核心处理逻辑: @@ -209,7 +215,7 @@ def process_face_segmentation_and_analysis( cropped_img.save(save_path) # 3. 识别 - analysis = analyze_demographics_with_qwen(save_path, model_name=qwen_model) + analysis = analyze_demographics_with_qwen(save_path, model_name=qwen_model, prompt_template=analysis_prompt) # 构造返回结果 # 注意:URL 生成需要依赖外部的 request context,这里只返回相对路径或文件名 diff --git a/run_monitor.sh b/run_monitor.sh index 808e850..0e73af2 100755 --- a/run_monitor.sh +++ b/run_monitor.sh @@ -13,7 +13,7 @@ SCRIPT_NAME="fastAPI_tarot.py" # Python 启动脚本 LOG_FILE="${PROJECT_DIR}/log/monitor.log" # 监控日志文件 APP_LOG_FILE="${PROJECT_DIR}/log/app.log" # 应用输出日志文件 PORT=55600 # 服务端口 -CHECK_INTERVAL=20 # 检查间隔(秒) +CHECK_INTERVAL=60 # 检查间隔(秒) MAX_FAILURES=3 # 最大连续失败次数,超过则重启 STARTUP_TIMEOUT=300 # 启动超时时间(秒),等待模型加载 PYTHON_CMD="python" # Python 命令,根据环境可能是 python3 diff --git a/static/admin.html b/static/admin.html index aec4f5a..0499538 100644 --- a/static/admin.html +++ b/static/admin.html @@ -47,6 +47,9 @@ 文件管理 + + 提示词管理 + 系统设置 @@ -164,6 +167,26 @@ + +
+

提示词管理

+ +
+
+
+

+ {{ key }} + {{ getPromptDescription(key) }} +

+ +
+ +
+
+
+

系统设置

@@ -266,6 +289,7 @@ lifetime: 3600, interval: 600 }); + const prompts = ref({}); // 检查登录状态 const checkLogin = () => { @@ -288,6 +312,7 @@ loginError.value = ''; fetchHistory(); fetchSystemInfo(); + fetchPrompts(); } } catch (e) { loginError.value = '密码错误'; @@ -347,6 +372,38 @@ } }; + const fetchPrompts = async () => { + try { + const res = await axios.get('/admin/api/prompts'); + prompts.value = res.data; + } catch (e) { + console.error(e); + } + }; + + const savePrompt = async (key) => { + try { + const formData = new FormData(); + formData.append('key', key); + formData.append('content', prompts.value[key]); + const res = await axios.post('/admin/api/prompts', formData); + alert(res.data.message); + } catch (e) { + alert('保存失败: ' + (e.response?.data?.detail || e.message)); + } + }; + + const getPromptDescription = (key) => { + const map = { + 'translate': 'Prompt 翻译 (中文 -> 英文)', + 'tarot_card_dual': '塔罗牌识别 (正/逆位对比模式)', + 'tarot_card_single': '塔罗牌识别 (单图模式)', + 'tarot_spread': '塔罗牌阵识别', + 'face_analysis': '人脸/属性分析 (Qwen-VL)' + }; + return map[key] || ''; + }; + const updateModel = async () => { try { const formData = new FormData(); @@ -439,6 +496,7 @@ Vue.watch(currentTab, (newTab) => { if (newTab === 'files') fetchFiles(); if (newTab === 'dashboard') fetchHistory(); + if (newTab === 'prompts') fetchPrompts(); }); onMounted(() => { @@ -452,7 +510,8 @@ viewResult, previewImage, isImage, previewUrl, formatDate, getTypeBadgeClass, cleaning, deviceInfo, currentModel, availableModels, updateModel, - cleanupConfig, saveCleanupConfig + cleanupConfig, saveCleanupConfig, + prompts, fetchPrompts, savePrompt, getPromptDescription }; } }).mount('#app');