feat: modifications based on team suggestions
- Add MongoDB type manager implementation (TypeManagerMongo) - Update environment variables configuration to support MongoDB connection - Add chat functionality - Integrate Azure OpenAI API support - Update dependencies and startup script
This commit is contained in:
parent
9c2229fa47
commit
487f3af948
15
.env
15
.env
@ -1,2 +1,13 @@
|
|||||||
BAIDU_API_KEY=ALTAKmzKDy1OqhqmepD2OeXqbN
|
BAIDU_API_KEY=cqn5SYRZAKM9WBXgYg8niUdM
|
||||||
BAIDU_SECRET_KEY=b79c5fbc26344868916ec6e9e2ff65f0
|
BAIDU_SECRET_KEY=vG1VNzQPsz7G8sogjMm9vCDU7LUawRnD
|
||||||
|
APP_ID=6691505
|
||||||
|
|
||||||
|
# Azure OpenAI配置
|
||||||
|
AZURE_OPENAI_API_KEY=AMbAvhIHJIyKdNUeSMYBkM9wf4ISBoYrNUW7g0L5lwCSiETBGCpsJQQJ99AKACHYHv6XJ3w3AAAAACOGS84A
|
||||||
|
AZURE_OPENAI_ENDPOINT=https://admin-m3wql277-eastus2.cognitiveservices.azure.com
|
||||||
|
AZURE_OPENAI_API_VERSION=2023-12-01-preview
|
||||||
|
AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o-drobotics
|
||||||
|
|
||||||
|
# MongoDB配置
|
||||||
|
MONGO_URI=mongodb://localhost:27017/
|
||||||
|
MONGO_DB_NAME=imgsearcher
|
||||||
BIN
__pycache__/mock_baidu_api.cpython-39.pyc
Normal file
BIN
__pycache__/mock_baidu_api.cpython-39.pyc
Normal file
Binary file not shown.
177
app.py
177
app.py
@ -4,15 +4,20 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
from flask import Flask, render_template, request, jsonify, redirect, url_for, session
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from app.api.baidu_image_search import BaiduImageSearch
|
from app.api.baidu_image_search import BaiduImageSearch
|
||||||
from app.api.image_utils import ImageUtils
|
from app.api.image_utils import ImageUtils
|
||||||
|
from app.api.azure_openai import AzureOpenAI
|
||||||
|
from app.api.type_manager_mongo import TypeManagerMongo
|
||||||
|
|
||||||
app = Flask(__name__, template_folder='app/templates', static_folder='app/static')
|
app = Flask(__name__, template_folder='app/templates', static_folder='app/static')
|
||||||
CORS(app) # 启用CORS支持跨域请求
|
CORS(app) # 启用CORS支持跨域请求
|
||||||
|
|
||||||
|
# 设置会话密钥
|
||||||
|
app.secret_key = os.urandom(24)
|
||||||
|
|
||||||
# 配置上传文件夹
|
# 配置上传文件夹
|
||||||
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
||||||
if not os.path.exists(UPLOAD_FOLDER):
|
if not os.path.exists(UPLOAD_FOLDER):
|
||||||
@ -25,6 +30,16 @@ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
|||||||
# 初始化百度图像搜索API
|
# 初始化百度图像搜索API
|
||||||
image_search_api = BaiduImageSearch()
|
image_search_api = BaiduImageSearch()
|
||||||
|
|
||||||
|
# 初始化Azure OpenAI API
|
||||||
|
try:
|
||||||
|
azure_openai_api = AzureOpenAI()
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"警告: {str(e)}")
|
||||||
|
azure_openai_api = None
|
||||||
|
|
||||||
|
# 初始化类型管理器
|
||||||
|
type_manager = TypeManagerMongo()
|
||||||
|
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
"""检查文件扩展名是否允许"""
|
"""检查文件扩展名是否允许"""
|
||||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
@ -48,14 +63,18 @@ def upload_image():
|
|||||||
return jsonify({'error': '不支持的文件类型'}), 400
|
return jsonify({'error': '不支持的文件类型'}), 400
|
||||||
|
|
||||||
# 获取表单数据
|
# 获取表单数据
|
||||||
|
image_type = request.form.get('type', '')
|
||||||
|
description = request.form.get('description', '')
|
||||||
name = request.form.get('name', '')
|
name = request.form.get('name', '')
|
||||||
image_id = request.form.get('id', '')
|
tags = request.form.get('tags', '1') # 默认标签为1
|
||||||
tags = request.form.get('tags', '')
|
|
||||||
|
|
||||||
# 创建brief信息
|
# 将类型和描述信息保存到本地
|
||||||
|
type_manager.add_type(image_type, description)
|
||||||
|
|
||||||
|
# 创建brief信息(不包含描述信息)
|
||||||
brief = {
|
brief = {
|
||||||
'name': name,
|
'type': image_type,
|
||||||
'id': image_id
|
'name': name
|
||||||
}
|
}
|
||||||
|
|
||||||
# 保存文件
|
# 保存文件
|
||||||
@ -92,7 +111,8 @@ def search_image():
|
|||||||
return jsonify({'error': '不支持的文件类型'}), 400
|
return jsonify({'error': '不支持的文件类型'}), 400
|
||||||
|
|
||||||
# 获取表单数据
|
# 获取表单数据
|
||||||
tags = request.form.get('tags', '')
|
image_type = request.form.get('type', '')
|
||||||
|
tags = request.form.get('tags', '1') # 默认标签为1
|
||||||
tag_logic = request.form.get('tag_logic', '0')
|
tag_logic = request.form.get('tag_logic', '0')
|
||||||
|
|
||||||
# 保存文件
|
# 保存文件
|
||||||
@ -105,9 +125,29 @@ def search_image():
|
|||||||
result = image_search_api.search_image(
|
result = image_search_api.search_image(
|
||||||
image_path=file_path,
|
image_path=file_path,
|
||||||
tags=tags,
|
tags=tags,
|
||||||
tag_logic=tag_logic
|
tag_logic=tag_logic,
|
||||||
|
type_filter=image_type
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 从本地获取类型描述信息,并添加到结果中
|
||||||
|
if 'result' in result and result['result']:
|
||||||
|
for item in result['result']:
|
||||||
|
try:
|
||||||
|
brief = item.get('brief', '{}')
|
||||||
|
if isinstance(brief, str):
|
||||||
|
brief_dict = json.loads(brief)
|
||||||
|
item_type = brief_dict.get('type', '')
|
||||||
|
|
||||||
|
# 从本地获取描述信息
|
||||||
|
description = type_manager.get_description(item_type)
|
||||||
|
|
||||||
|
# 将描述信息添加到brief中
|
||||||
|
brief_dict['description'] = description
|
||||||
|
item['brief'] = json.dumps(brief_dict, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"处理搜索结果项出错: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
@ -141,6 +181,18 @@ def update_image():
|
|||||||
brief = data.get('brief')
|
brief = data.get('brief')
|
||||||
tags = data.get('tags')
|
tags = data.get('tags')
|
||||||
|
|
||||||
|
# 如果提供了brief信息,将类型和描述信息保存到本地
|
||||||
|
if brief and isinstance(brief, dict):
|
||||||
|
image_type = brief.get('type', '')
|
||||||
|
description = brief.get('description', '')
|
||||||
|
|
||||||
|
# 如果有描述信息,则更新到本地
|
||||||
|
if image_type and description:
|
||||||
|
type_manager.add_type(image_type, description)
|
||||||
|
|
||||||
|
# 从要上传的brief中移除描述信息
|
||||||
|
brief.pop('description', None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 调用API更新图片
|
# 调用API更新图片
|
||||||
result = image_search_api.update_image(
|
result = image_search_api.update_image(
|
||||||
@ -161,5 +213,112 @@ def get_token():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/chat-with-image', methods=['GET'])
|
||||||
|
def chat_with_image_page():
|
||||||
|
"""图片对话页面"""
|
||||||
|
return render_template('chat.html')
|
||||||
|
|
||||||
|
@app.route('/api/upload-chat-image', methods=['POST'])
|
||||||
|
def upload_chat_image():
|
||||||
|
"""上传图片用于对话"""
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return jsonify({'error': '没有文件上传'}), 400
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
|
if file.filename == '':
|
||||||
|
return jsonify({'error': '没有选择文件'}), 400
|
||||||
|
|
||||||
|
if not allowed_file(file.filename):
|
||||||
|
return jsonify({'error': '不支持的文件类型'}), 400
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
# 存储图片路径到会话
|
||||||
|
session['chat_image_path'] = file_path
|
||||||
|
|
||||||
|
# 清空对话历史
|
||||||
|
session['conversation_history'] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用百度图像搜索API搜索图片
|
||||||
|
search_result = image_search_api.search_image(image_path=file_path)
|
||||||
|
|
||||||
|
# 使用方法三确定最可信的类型
|
||||||
|
reliable_types = image_search_api._determine_most_reliable_types(search_result.get('result', []))
|
||||||
|
image_type = reliable_types.get('method3', '')
|
||||||
|
|
||||||
|
# 从本地获取描述信息
|
||||||
|
description = type_manager.get_description(image_type)
|
||||||
|
|
||||||
|
# 如果本地没有描述信息,尝试从搜索结果中获取
|
||||||
|
if not description:
|
||||||
|
for item in search_result.get('result', []):
|
||||||
|
brief = item.get('brief', '{}')
|
||||||
|
if isinstance(brief, str):
|
||||||
|
try:
|
||||||
|
brief_dict = json.loads(brief)
|
||||||
|
if brief_dict.get('type') == image_type:
|
||||||
|
description = brief_dict.get('description', '')
|
||||||
|
if description:
|
||||||
|
# 将获取到的描述信息保存到本地
|
||||||
|
type_manager.add_type(image_type, description)
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'image_path': file_path,
|
||||||
|
'image_type': image_type,
|
||||||
|
'description': description
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/chat', methods=['POST'])
|
||||||
|
def chat():
|
||||||
|
"""与图片进行对话"""
|
||||||
|
if not azure_openai_api:
|
||||||
|
return jsonify({'error': 'Azure OpenAI API未正确配置'}), 500
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or 'message' not in data:
|
||||||
|
return jsonify({'error': '缺少消息内容'}), 400
|
||||||
|
|
||||||
|
message = data['message']
|
||||||
|
image_path = session.get('chat_image_path')
|
||||||
|
|
||||||
|
if not image_path or not os.path.exists(image_path):
|
||||||
|
return jsonify({'error': '没有上传图片或图片已失效'}), 400
|
||||||
|
|
||||||
|
# 获取对话历史
|
||||||
|
conversation_history = session.get('conversation_history', [])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用Azure OpenAI API进行对话
|
||||||
|
response = azure_openai_api.chat_with_image(
|
||||||
|
image_path=image_path,
|
||||||
|
message=message,
|
||||||
|
conversation_history=conversation_history
|
||||||
|
)
|
||||||
|
|
||||||
|
# 提取回复内容
|
||||||
|
reply = response['choices'][0]['message']['content']
|
||||||
|
|
||||||
|
# 更新对话历史
|
||||||
|
conversation_history.append({"role": "user", "content": message})
|
||||||
|
conversation_history.append({"role": "assistant", "content": reply})
|
||||||
|
session['conversation_history'] = conversation_history
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'reply': reply,
|
||||||
|
'conversation_history': conversation_history
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
app.run(debug=True, host='0.0.0.0', port=5001)
|
||||||
|
|||||||
BIN
app/api/__pycache__/azure_openai.cpython-39.pyc
Normal file
BIN
app/api/__pycache__/azure_openai.cpython-39.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/__pycache__/type_manager.cpython-39.pyc
Normal file
BIN
app/api/__pycache__/type_manager.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/type_manager_mongo.cpython-39.pyc
Normal file
BIN
app/api/__pycache__/type_manager_mongo.cpython-39.pyc
Normal file
Binary file not shown.
166
app/api/azure_openai.py
Normal file
166
app/api/azure_openai.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from app.api.baidu_image_search import BaiduImageSearch
|
||||||
|
from app.api.image_utils import ImageUtils
|
||||||
|
from app.api.type_manager_mongo import TypeManagerMongo
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class AzureOpenAI:
|
||||||
|
"""Azure OpenAI API封装类,用于与GPT-4o多模态模型交互"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化,获取API密钥和端点"""
|
||||||
|
self.api_key = os.getenv('AZURE_OPENAI_API_KEY')
|
||||||
|
self.endpoint = os.getenv('AZURE_OPENAI_ENDPOINT')
|
||||||
|
self.api_version = os.getenv('AZURE_OPENAI_API_VERSION')
|
||||||
|
self.deployment_name = os.getenv('AZURE_OPENAI_DEPLOYMENT_NAME')
|
||||||
|
self.baidu_image_search = BaiduImageSearch()
|
||||||
|
self.type_manager = TypeManagerMongo()
|
||||||
|
|
||||||
|
# 检查配置是否存在
|
||||||
|
if not self.api_key or not self.endpoint or not self.api_version or not self.deployment_name:
|
||||||
|
raise ValueError("Azure OpenAI配置不完整,请检查.env文件")
|
||||||
|
|
||||||
|
def _encode_image(self, image_path):
|
||||||
|
"""
|
||||||
|
将图片编码为base64格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 图片路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: base64编码的图片数据
|
||||||
|
"""
|
||||||
|
with open(image_path, "rb") as image_file:
|
||||||
|
return base64.b64encode(image_file.read()).decode('utf-8')
|
||||||
|
|
||||||
|
def _get_image_type_description(self, image_path):
|
||||||
|
"""
|
||||||
|
获取图片类型和描述
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 图片路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (类型, 描述)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用百度图像搜索API搜索图片
|
||||||
|
search_result = self.baidu_image_search.search_image(image_path=image_path)
|
||||||
|
|
||||||
|
# 使用方法三确定最可信的类型
|
||||||
|
reliable_types = self.baidu_image_search._determine_most_reliable_types(search_result.get('result', []))
|
||||||
|
image_type = reliable_types.get('method3', '')
|
||||||
|
|
||||||
|
# 首先从本地类型管理器获取描述信息
|
||||||
|
description = self.type_manager.get_description(image_type)
|
||||||
|
|
||||||
|
# 如果本地没有描述信息,尝试从搜索结果中获取
|
||||||
|
if not description:
|
||||||
|
for item in search_result.get('result', []):
|
||||||
|
brief = item.get('brief', '{}')
|
||||||
|
if isinstance(brief, str):
|
||||||
|
try:
|
||||||
|
brief_dict = json.loads(brief)
|
||||||
|
if brief_dict.get('type') == image_type:
|
||||||
|
description = brief_dict.get('description', '')
|
||||||
|
if description:
|
||||||
|
# 将获取到的描述信息保存到本地
|
||||||
|
self.type_manager.add_type(image_type, description)
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"获取到的图片类型: {image_type}, 描述长度: {len(description)}")
|
||||||
|
return image_type, description
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取图片类型和描述失败: {str(e)}")
|
||||||
|
return "", ""
|
||||||
|
|
||||||
|
def chat_with_image(self, image_path, message, conversation_history=None):
|
||||||
|
"""
|
||||||
|
使用图片和消息与GPT-4o多模态模型进行对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 图片路径
|
||||||
|
message: 用户消息
|
||||||
|
conversation_history: 对话历史记录
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 模型响应
|
||||||
|
"""
|
||||||
|
if conversation_history is None:
|
||||||
|
conversation_history = []
|
||||||
|
|
||||||
|
# 获取图片类型和描述
|
||||||
|
image_type, description = self._get_image_type_description(image_path)
|
||||||
|
|
||||||
|
# 编码图片
|
||||||
|
base64_image = self._encode_image(image_path)
|
||||||
|
|
||||||
|
# 构建系统提示
|
||||||
|
system_message = "你是一个智能助手,能够分析图片并回答问题。"
|
||||||
|
if image_type and description:
|
||||||
|
system_message += f"\n\n这是一张{image_type}的图片。\n描述信息:{description}\n\n请基于这些信息和图片内容回答用户的问题。"
|
||||||
|
|
||||||
|
# 打印系统提示
|
||||||
|
print("\nimage_type and description:")
|
||||||
|
print(f"{image_type} - {description}")
|
||||||
|
print(f"系统提示: {system_message}...(总长度:{len(system_message)})")
|
||||||
|
|
||||||
|
# 构建消息历史
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": system_message}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 添加对话历史
|
||||||
|
for msg in conversation_history:
|
||||||
|
messages.append(msg)
|
||||||
|
|
||||||
|
# 添加当前消息和图片
|
||||||
|
messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": message
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:image/jpeg;base64,{base64_image}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 构建API请求
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"api-key": self.api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"messages": messages,
|
||||||
|
"max_tokens": 2000,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
url = f"{self.endpoint}/openai/deployments/{self.deployment_name}/chat/completions?api-version={self.api_version}"
|
||||||
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Azure OpenAI API请求失败: {response.text}")
|
||||||
@ -45,7 +45,7 @@ class BaiduImageSearch:
|
|||||||
image_base64: 图片的base64编码
|
image_base64: 图片的base64编码
|
||||||
url: 图片URL
|
url: 图片URL
|
||||||
brief: 图片摘要信息,最长256B,如:{"name":"图片名称", "id":"123"}
|
brief: 图片摘要信息,最长256B,如:{"name":"图片名称", "id":"123"}
|
||||||
tags: 分类信息,最多2个tag,如:"1,2"
|
tags: 分类信息,1 - 65535范围内的整数 tag间以逗号分隔,最多2个tag,2个tag无层级关系,检索时支持逻辑运算。样例:"100,11" ;检索时可圈定分类维度进行检索
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: API返回的结果
|
dict: API返回的结果
|
||||||
@ -80,13 +80,15 @@ class BaiduImageSearch:
|
|||||||
|
|
||||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||||
response = requests.post(request_url, data=params, headers=headers)
|
response = requests.post(request_url, data=params, headers=headers)
|
||||||
|
print(f"添加图片请求: {params}")
|
||||||
|
print(f"添加图片响应: {response.text}")
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.json()
|
return response.json()
|
||||||
else:
|
else:
|
||||||
raise Exception(f"添加图片失败: {response.text}")
|
raise Exception(f"添加图片失败: {response.text}")
|
||||||
|
|
||||||
def search_image(self, image_path=None, image_base64=None, url=None, tags=None, tag_logic=None, pn=0, rn=300):
|
def search_image(self, image_path=None, image_base64=None, url=None, tags=None, tag_logic=None, type_filter=None, pn=0, rn=300):
|
||||||
"""
|
"""
|
||||||
检索相似图片
|
检索相似图片
|
||||||
|
|
||||||
@ -96,6 +98,7 @@ class BaiduImageSearch:
|
|||||||
url: 图片URL
|
url: 图片URL
|
||||||
tags: 分类信息过滤,如:"1,2"
|
tags: 分类信息过滤,如:"1,2"
|
||||||
tag_logic: 标签逻辑,0表示逻辑and,1表示逻辑or
|
tag_logic: 标签逻辑,0表示逻辑and,1表示逻辑or
|
||||||
|
type_filter: 类型过滤,用于按图片类型进行筛选
|
||||||
pn: 分页起始位置,默认0
|
pn: 分页起始位置,默认0
|
||||||
rn: 返回结果数量,默认300,最大1000
|
rn: 返回结果数量,默认300,最大1000
|
||||||
|
|
||||||
@ -128,11 +131,47 @@ class BaiduImageSearch:
|
|||||||
if rn is not None:
|
if rn is not None:
|
||||||
params['rn'] = rn
|
params['rn'] = rn
|
||||||
|
|
||||||
|
# 处理搜索结果的类型过滤
|
||||||
|
self.type_filter = type_filter
|
||||||
|
|
||||||
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||||
|
# print(f"搜索图片请求: {params}")
|
||||||
response = requests.post(request_url, data=params, headers=headers)
|
response = requests.post(request_url, data=params, headers=headers)
|
||||||
|
# print(f"搜索图片响应: {response.text}")
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.json()
|
result = response.json()
|
||||||
|
|
||||||
|
# 如果有类型过滤,对结果进行过滤
|
||||||
|
if self.type_filter and 'result' in result and result['result']:
|
||||||
|
filtered_results = []
|
||||||
|
for item in result['result']:
|
||||||
|
try:
|
||||||
|
brief = item.get('brief', '{"type":""}')
|
||||||
|
|
||||||
|
# 确保brief是字符串
|
||||||
|
if not isinstance(brief, str):
|
||||||
|
brief = str(brief)
|
||||||
|
|
||||||
|
try:
|
||||||
|
brief_info = json.loads(brief)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"JSON解析错误: {e}, brief: {brief}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if brief_info.get('type', '') == self.type_filter:
|
||||||
|
filtered_results.append(item)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"处理搜索结果项出错: {e}")
|
||||||
|
continue
|
||||||
|
result['result'] = filtered_results
|
||||||
|
result['result_num'] = len(filtered_results)
|
||||||
|
|
||||||
|
# 分析结果,确定最可信的类型
|
||||||
|
if 'result' in result and result['result']:
|
||||||
|
result['most_reliable_types'] = self._determine_most_reliable_types(result['result'])
|
||||||
|
|
||||||
|
return result
|
||||||
else:
|
else:
|
||||||
raise Exception(f"检索图片失败: {response.text}")
|
raise Exception(f"检索图片失败: {response.text}")
|
||||||
|
|
||||||
@ -175,6 +214,143 @@ class BaiduImageSearch:
|
|||||||
else:
|
else:
|
||||||
raise Exception(f"删除图片失败: {response.text}")
|
raise Exception(f"删除图片失败: {response.text}")
|
||||||
|
|
||||||
|
def _determine_most_reliable_types(self, results):
|
||||||
|
"""
|
||||||
|
使用多种方法确定最可信的类型
|
||||||
|
只使用相似度前十的结果进行计算
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results: 搜索结果列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含不同方法确定的最可信类型
|
||||||
|
"""
|
||||||
|
# 防止空结果
|
||||||
|
if not results:
|
||||||
|
return {
|
||||||
|
"method1": "",
|
||||||
|
"method2": "",
|
||||||
|
"method3": "",
|
||||||
|
"method4": "",
|
||||||
|
"method5": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 只使用相似度前十的结果
|
||||||
|
# 按相似度降序排序并取前10个
|
||||||
|
sorted_results = sorted(results, key=lambda x: x.get('score', 0), reverse=True)
|
||||||
|
top_results = sorted_results[:min(10, len(sorted_results))]
|
||||||
|
|
||||||
|
print(f"使用前{len(top_results)}个结果进行最可信类型分析")
|
||||||
|
# 提取前十个结果的类型和分数
|
||||||
|
type_scores = []
|
||||||
|
for item in top_results:
|
||||||
|
try:
|
||||||
|
score = item.get('score', 0)
|
||||||
|
brief = item.get('brief', '{"type":""}')
|
||||||
|
|
||||||
|
# 确保brief是字符串
|
||||||
|
if not isinstance(brief, str):
|
||||||
|
brief = str(brief)
|
||||||
|
|
||||||
|
try:
|
||||||
|
brief_info = json.loads(brief)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"JSON解析错误: {e}, brief: {brief}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
item_type = brief_info.get('type', '')
|
||||||
|
if item_type: # 只考虑有类型的结果
|
||||||
|
type_scores.append((item_type, score))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"处理搜索结果项出错: {e}, item: {item}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not type_scores:
|
||||||
|
return {
|
||||||
|
"method1": "",
|
||||||
|
"method2": "",
|
||||||
|
"method3": "",
|
||||||
|
"method4": "",
|
||||||
|
"method5": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 方法一:基于最高相似度分数选择类型
|
||||||
|
method1_type = ""
|
||||||
|
max_score = -1
|
||||||
|
for t, s in type_scores:
|
||||||
|
if s > max_score:
|
||||||
|
max_score = s
|
||||||
|
method1_type = t
|
||||||
|
|
||||||
|
# 方法二:基于加权投票选择类型
|
||||||
|
type_weight = {}
|
||||||
|
for t, s in type_scores:
|
||||||
|
if t not in type_weight:
|
||||||
|
type_weight[t] = 0
|
||||||
|
type_weight[t] += s
|
||||||
|
|
||||||
|
try:
|
||||||
|
method2_type = max(type_weight.items(), key=lambda x: x[1])[0] if type_weight else ""
|
||||||
|
except ValueError:
|
||||||
|
method2_type = ""
|
||||||
|
|
||||||
|
# 方法三:基于阈值过滤和加权投票选择类型
|
||||||
|
# 只考虑相似度大于0.6的结果
|
||||||
|
threshold = 0.6
|
||||||
|
filtered_type_weight = {}
|
||||||
|
for t, s in type_scores:
|
||||||
|
if s >= threshold:
|
||||||
|
if t not in filtered_type_weight:
|
||||||
|
filtered_type_weight[t] = 0
|
||||||
|
filtered_type_weight[t] += s
|
||||||
|
|
||||||
|
try:
|
||||||
|
method3_type = max(filtered_type_weight.items(), key=lambda x: x[1])[0] if filtered_type_weight else ""
|
||||||
|
except ValueError:
|
||||||
|
method3_type = ""
|
||||||
|
|
||||||
|
# 方法四:基于多数投票选择类型
|
||||||
|
type_count = {}
|
||||||
|
for t, _ in type_scores:
|
||||||
|
if t not in type_count:
|
||||||
|
type_count[t] = 0
|
||||||
|
type_count[t] += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
method4_type = max(type_count.items(), key=lambda x: x[1])[0] if type_count else ""
|
||||||
|
except ValueError:
|
||||||
|
method4_type = ""
|
||||||
|
|
||||||
|
# 方法五:基于加权多数投票选择类型
|
||||||
|
# 权重 = 计数 * 平均分数
|
||||||
|
type_weighted_count = {}
|
||||||
|
type_total_score = {}
|
||||||
|
|
||||||
|
# 初始化类型计数和总分数
|
||||||
|
for t, s in type_scores:
|
||||||
|
if t not in type_total_score:
|
||||||
|
type_total_score[t] = 0
|
||||||
|
type_total_score[t] += s
|
||||||
|
|
||||||
|
# 计算加权分数
|
||||||
|
for t in type_count.keys():
|
||||||
|
if type_count[t] > 0:
|
||||||
|
avg_score = type_total_score.get(t, 0) / type_count[t]
|
||||||
|
type_weighted_count[t] = type_count[t] * avg_score
|
||||||
|
|
||||||
|
try:
|
||||||
|
method5_type = max(type_weighted_count.items(), key=lambda x: x[1])[0] if type_weighted_count else ""
|
||||||
|
except ValueError:
|
||||||
|
method5_type = ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"method1": method1_type, # 最高分数法
|
||||||
|
"method2": method2_type, # 加权投票法
|
||||||
|
"method3": method3_type, # 阈值过滤加权投票法
|
||||||
|
"method4": method4_type, # 多数投票法
|
||||||
|
"method5": method5_type # 加权多数投票法
|
||||||
|
}
|
||||||
|
|
||||||
def update_image(self, image_path=None, image_base64=None, url=None, cont_sign=None, brief=None, tags=None):
|
def update_image(self, image_path=None, image_base64=None, url=None, cont_sign=None, brief=None, tags=None):
|
||||||
"""
|
"""
|
||||||
更新图库中图片的摘要和分类信息
|
更新图库中图片的摘要和分类信息
|
||||||
|
|||||||
116
app/api/type_manager.py
Normal file
116
app/api/type_manager.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class TypeManager:
|
||||||
|
"""
|
||||||
|
类型管理器,用于在本地管理类型和描述信息
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data_file=None):
|
||||||
|
"""
|
||||||
|
初始化类型管理器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_file: 数据文件路径,默认为app/data/type_descriptions.json
|
||||||
|
"""
|
||||||
|
if data_file is None:
|
||||||
|
# 获取当前文件所在目录
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
# 获取app目录
|
||||||
|
app_dir = os.path.dirname(current_dir)
|
||||||
|
# 数据文件路径
|
||||||
|
self.data_file = os.path.join(app_dir, 'data', 'type_descriptions.json')
|
||||||
|
else:
|
||||||
|
self.data_file = data_file
|
||||||
|
|
||||||
|
# 确保数据目录存在
|
||||||
|
os.makedirs(os.path.dirname(self.data_file), exist_ok=True)
|
||||||
|
|
||||||
|
# 确保数据文件存在
|
||||||
|
if not os.path.exists(self.data_file):
|
||||||
|
with open(self.data_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump({}, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# 加载数据
|
||||||
|
self.types = self._load_data()
|
||||||
|
|
||||||
|
# 线程锁,用于防止并发写入
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def _load_data(self):
|
||||||
|
"""
|
||||||
|
加载数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 类型和描述信息的字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(self.data_file, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, FileNotFoundError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_data(self):
|
||||||
|
"""
|
||||||
|
保存数据
|
||||||
|
"""
|
||||||
|
with open(self.data_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self.types, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
def add_type(self, type_name, description):
|
||||||
|
"""
|
||||||
|
添加或更新类型和描述信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_name: 类型名称
|
||||||
|
description: 描述信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 操作是否成功
|
||||||
|
"""
|
||||||
|
with self.lock:
|
||||||
|
self.types[type_name] = description
|
||||||
|
self._save_data()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_description(self, type_name):
|
||||||
|
"""
|
||||||
|
获取类型的描述信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_name: 类型名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 描述信息,如果类型不存在则返回空字符串
|
||||||
|
"""
|
||||||
|
return self.types.get(type_name, "")
|
||||||
|
|
||||||
|
def remove_type(self, type_name):
|
||||||
|
"""
|
||||||
|
删除类型
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_name: 类型名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 操作是否成功
|
||||||
|
"""
|
||||||
|
with self.lock:
|
||||||
|
if type_name in self.types:
|
||||||
|
del self.types[type_name]
|
||||||
|
self._save_data()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_all_types(self):
|
||||||
|
"""
|
||||||
|
获取所有类型和描述信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 类型和描述信息的字典
|
||||||
|
"""
|
||||||
|
return self.types.copy()
|
||||||
115
app/api/type_manager_mongo.py
Normal file
115
app/api/type_manager_mongo.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pymongo import MongoClient
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class TypeManagerMongo:
|
||||||
|
"""使用MongoDB管理图片类型和描述信息的类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化MongoDB连接"""
|
||||||
|
# 从环境变量获取MongoDB连接信息
|
||||||
|
mongo_uri = os.getenv('MONGO_URI', 'mongodb://localhost:27017/')
|
||||||
|
db_name = os.getenv('MONGO_DB_NAME', 'imgsearcher')
|
||||||
|
|
||||||
|
# 连接MongoDB
|
||||||
|
self.client = MongoClient(mongo_uri)
|
||||||
|
self.db = self.client[db_name]
|
||||||
|
self.collection = self.db['type_descriptions']
|
||||||
|
|
||||||
|
# 确保有索引以提高查询性能
|
||||||
|
self.collection.create_index('type')
|
||||||
|
|
||||||
|
print(f"MongoDB TypeManager 初始化完成,连接到 {db_name}")
|
||||||
|
|
||||||
|
def add_type(self, type_name, description):
|
||||||
|
"""
|
||||||
|
添加或更新类型描述
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_name: 类型名称
|
||||||
|
description: 类型描述
|
||||||
|
"""
|
||||||
|
if not type_name or not description:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 使用upsert=True确保如果类型不存在则创建,存在则更新
|
||||||
|
result = self.collection.update_one(
|
||||||
|
{'type': type_name},
|
||||||
|
{'$set': {'description': description}},
|
||||||
|
upsert=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return result.acknowledged
|
||||||
|
|
||||||
|
def get_description(self, type_name):
|
||||||
|
"""
|
||||||
|
获取类型描述
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_name: 类型名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 类型描述,如果类型不存在则返回空字符串
|
||||||
|
"""
|
||||||
|
if not type_name:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
result = self.collection.find_one({'type': type_name})
|
||||||
|
if result:
|
||||||
|
return result.get('description', "")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def remove_type(self, type_name):
|
||||||
|
"""
|
||||||
|
删除类型
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type_name: 类型名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功删除
|
||||||
|
"""
|
||||||
|
if not type_name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = self.collection.delete_one({'type': type_name})
|
||||||
|
return result.deleted_count > 0
|
||||||
|
|
||||||
|
def get_all_types(self):
|
||||||
|
"""
|
||||||
|
获取所有类型
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 类型名称和描述的字典
|
||||||
|
"""
|
||||||
|
types = {}
|
||||||
|
for item in self.collection.find():
|
||||||
|
types[item['type']] = item.get('description', "")
|
||||||
|
return types
|
||||||
|
|
||||||
|
def import_from_json(self, json_data):
|
||||||
|
"""
|
||||||
|
从JSON数据导入类型描述
|
||||||
|
|
||||||
|
Args:
|
||||||
|
json_data: JSON格式的类型描述数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: 导入的类型数量
|
||||||
|
"""
|
||||||
|
count = 0
|
||||||
|
for type_name, description in json_data.items():
|
||||||
|
if self.add_type(type_name, description):
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""关闭MongoDB连接"""
|
||||||
|
if self.client:
|
||||||
|
self.client.close()
|
||||||
5
app/data/type_descriptions.json
Normal file
5
app/data/type_descriptions.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"小猫Tom": "角色信息:Tom(汤姆)\r\n基本信息\r\n名字:Tom(汤姆)\r\n性别:雄性\r\n年龄:3岁(相当于人类的25岁)\r\n品种:虎斑猫(Tabby Cat)\r\n毛色:灰褐色虎斑纹,带有白色胸毛和白色“袜子”脚掌\r\n眼睛:明亮的绿色眼睛,充满好奇和智慧\r\n性格特点\r\n好奇心旺盛:Tom对周围的一切都充满兴趣,无论是洗衣机里的衣服、镜子里的自己,还是主人手中的猕猴桃,它总是第一个去探索。\r\n幽默风趣:Tom喜欢用自己的搞怪表情和动作逗主人开心,比如假装用电脑工作,或者摆出“充电中”的姿势。\r\n聪明伶俐:Tom非常擅长观察和模仿,它甚至学会了如何“假装”使用笔记本电脑,俨然一副“职场猫”的模样。\r\n爱冒险:Tom喜欢钻进各种狭小的空间,比如洗衣机,或者跳到高处观察世界,总是充满活力。\r\n温柔体贴:虽然Tom很调皮,但它也很懂得陪伴主人,喜欢静静地依偎在主人身边,用温暖的毛毛安慰主人。\r\n背景故事\r\nTom是一只生活在现代都市的虎斑猫,出生在一个温暖的小家庭中。它的主人是一位自由职业者,经常在家工作,Tom因此成了主人最忠实的“同事”。Tom从小就展现出超乎寻常的智慧和好奇心,喜欢观察主人的一举一动,甚至学会了一些“人类行为”,比如假装用电脑、戴耳机“开会”,或者对着镜子“整理毛发”。\r\n\r\nTom最喜欢的事情之一是探索家里的每一个角落。它曾经钻进洗衣机里“体验旋转”,结果被主人发现后拍下了搞笑的照片。Tom还对水果特别感兴趣,尤其是猕猴桃,它总是盯着猕猴桃看,好像在研究它的“内在秘密”。主人经常开玩笑说,Tom可能是世界上第一只想当“水果研究员”的猫。\r\n\r\n特殊技能\r\n伪装大师:Tom擅长模仿人类的行为,比如假装用电脑、戴耳机,甚至“假装充电”,让主人忍俊不禁。\r\n探险家:Tom能钻进任何狭小的空间,无论是洗衣机、抽屉还是纸箱,它都能轻松应对。\r\n表情帝:Tom有一张非常有表现力的脸,无论是惊讶、开心还是“假装严肃”,它的表情总能让人捧腹大笑。\r\n治愈系:Tom的陪伴能让主人感到放松和温暖,尤其是在主人工作压力大时,Tom会静静地趴在旁边,用自己的方式“鼓励”主人。\r\n日常生活\r\n早晨:Tom喜欢在主人起床前跳到床上,用小爪子轻轻拍主人的脸,提醒主人“该起床啦”。\r\n白天:Tom会“监督”主人工作,偶尔跳到键盘上“帮忙”打字(虽然通常是乱按一通)。它还喜欢盯着电脑屏幕,假装自己也在“开会”。\r\n晚上:Tom最喜欢和主人一起窝在沙发上看电视,或者钻进主人的被子里,享受温暖的夜晚。\r\n座右铭\r\n“生活就是一场冒险,我要用我的小爪子探索整个世界!”\r\n\r\n趣闻\r\nTom曾经试图“偷吃”猕猴桃,结果被主人抓了个正着。从那以后,它对猕猴桃有了一种特别的执念,总是盯着看,但不敢再下嘴。\r\nTom最喜欢洗衣机,因为它觉得那是一个“神秘的旋转乐园”,但主人每次都要小心检查,以免Tom真的被“洗”了。",
|
||||||
|
"Tom(汤姆)": "### 角色信息:Tom(汤姆)\r\n\r\n#### 基本信息\r\n\r\n- **名字**:Tom(汤姆)\r\n- **性别**:雄性\r\n- **年龄**:3岁(相当于人类的25岁)\r\n- **品种**:虎斑猫(Tabby Cat)\r\n- **毛色**:灰褐色虎斑纹,带有白色胸毛和白色“袜子”脚掌\r\n- **眼睛**:明亮的绿色眼睛,充满好奇和智慧\r\n\r\n#### 性格特点\r\n\r\n- **好奇心旺盛**:Tom对周围的一切都充满兴趣,无论是洗衣机里的衣服、镜子里的自己,还是主人手中的猕猴桃,它总是第一个去探索。\r\n- **幽默风趣**:Tom喜欢用自己的搞怪表情和动作逗主人开心,比如假装用电脑工作,或者摆出“充电中”的姿势。\r\n- **聪明伶俐**:Tom非常擅长观察和模仿,它甚至学会了如何“假装”使用笔记本电脑,俨然一副“职场猫”的模样。\r\n- **爱冒险**:Tom喜欢钻进各种狭小的空间,比如洗衣机,或者跳到高处观察世界,总是充满活力。\r\n- **温柔体贴**:虽然Tom很调皮,但它也很懂得陪伴主人,喜欢静静地依偎在主人身边,用温暖的毛毛安慰主人。\r\n\r\n#### 背景故事\r\n\r\nTom是一只生活在现代都市的虎斑猫,出生在一个温暖的小家庭中。它的主人是一位自由职业者,经常在家工作,Tom因此成了主人最忠实的“同事”。Tom从小就展现出超乎寻常的智慧和好奇心,喜欢观察主人的一举一动,甚至学会了一些“人类行为”,比如假装用电脑、戴耳机“开会”,或者对着镜子“整理毛发”。\r\n\r\nTom最喜欢的事情之一是探索家里的每一个角落。它曾经钻进洗衣机里“体验旋转”,结果被主人发现后拍下了搞笑的照片。Tom还对水果特别感兴趣,尤其是猕猴桃,它总是盯着猕猴桃看,好像在研究它的“内在秘密”。主人经常开玩笑说,Tom可能是世界上第一只想当“水果研究员”的猫。\r\n\r\n#### 特殊技能\r\n\r\n- **伪装大师**:Tom擅长模仿人类的行为,比如假装用电脑、戴耳机,甚至“假装充电”,让主人忍俊不禁。\r\n- **探险家**:Tom能钻进任何狭小的空间,无论是洗衣机、抽屉还是纸箱,它都能轻松应对。\r\n- **表情帝**:Tom有一张非常有表现力的脸,无论是惊讶、开心还是“假装严肃”,它的表情总能让人捧腹大笑。\r\n- **治愈系**:Tom的陪伴能让主人感到放松和温暖,尤其是在主人工作压力大时,Tom会静静地趴在旁边,用自己的方式“鼓励”主人。\r\n\r\n#### 日常生活\r\n\r\n- **早晨**:Tom喜欢在主人起床前跳到床上,用小爪子轻轻拍主人的脸,提醒主人“该起床啦”。\r\n- **白天**:Tom会“监督”主人工作,偶尔跳到键盘上“帮忙”打字(虽然通常是乱按一通)。它还喜欢盯着电脑屏幕,假装自己也在“开会”。\r\n- **晚上**:Tom最喜欢和主人一起窝在沙发上看电视,或者钻进主人的被子里,享受温暖的夜晚。\r\n\r\n#### 座右铭\r\n\r\n“生活就是一场冒险,我要用我的小爪子探索整个世界!”\r\n\r\n#### 趣闻\r\n\r\n- Tom曾经试图“偷吃”猕猴桃,结果被主人抓了个正着。从那以后,它对猕猴桃有了一种特别的执念,总是盯着看,但不敢再下嘴。\r\n- Tom最喜欢洗衣机,因为它觉得那是一个“神秘的旋转乐园”,但主人每次都要小心检查,以免Tom真的被“洗”了。",
|
||||||
|
"alice": "活泼的猫猫alice"
|
||||||
|
}
|
||||||
101
app/static/css/chat.css
Normal file
101
app/static/css/chat.css
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/* 聊天界面样式 */
|
||||||
|
.chat-messages {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
max-width: 80%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-user {
|
||||||
|
background-color: #dcf8c6;
|
||||||
|
margin-left: auto;
|
||||||
|
text-align: right;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-assistant {
|
||||||
|
background-color: #ffffff;
|
||||||
|
margin-right: auto;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewImage {
|
||||||
|
max-height: 300px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #888;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 3px;
|
||||||
|
animation: typing 1s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-5px); }
|
||||||
|
100% { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-container {
|
||||||
|
position: relative;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.message {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
max-height: 350px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -71,17 +71,40 @@ header .lead {
|
|||||||
/* 图片预览 */
|
/* 图片预览 */
|
||||||
.img-preview {
|
.img-preview {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 300px;
|
max-height: 200px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-preview-container,
|
.upload-preview-container,
|
||||||
.search-preview-container {
|
.search-preview-container {
|
||||||
text-align: center;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-start;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
/* 搜索结果样式 */
|
/* 搜索结果样式 */
|
||||||
.result-item {
|
.result-item {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
@ -106,6 +129,7 @@ header .lead {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-img {
|
.result-img {
|
||||||
@ -148,6 +172,28 @@ header .lead {
|
|||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 复选框样式 */
|
||||||
|
.select-checkbox {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-checkbox input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式调整 */
|
/* 响应式调整 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.result-item {
|
.result-item {
|
||||||
|
|||||||
220
app/static/js/chat.js
Normal file
220
app/static/js/chat.js
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
// 页面加载完成后执行
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 初始化图片上传表单
|
||||||
|
initUploadImageForm();
|
||||||
|
|
||||||
|
// 初始化聊天表单
|
||||||
|
initChatForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化图片上传表单
|
||||||
|
function initUploadImageForm() {
|
||||||
|
const form = document.getElementById('uploadImageForm');
|
||||||
|
const fileInput = document.getElementById('chatImageFile');
|
||||||
|
const previewContainer = document.getElementById('uploadPreview');
|
||||||
|
const previewImage = document.getElementById('previewImage');
|
||||||
|
const chatContainer = document.getElementById('chatContainer');
|
||||||
|
const imageTypeElement = document.getElementById('imageType');
|
||||||
|
const imageDescriptionElement = document.getElementById('imageDescription');
|
||||||
|
|
||||||
|
// 图片选择预览
|
||||||
|
fileInput.addEventListener('change', function() {
|
||||||
|
if (this.files && this.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function(e) {
|
||||||
|
previewImage.src = e.target.result;
|
||||||
|
previewContainer.classList.remove('d-none');
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(this.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单提交
|
||||||
|
form.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
|
||||||
|
// 显示上传中状态
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
const originalButtonText = submitButton.innerHTML;
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 上传中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/upload-chat-image', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert(`上传失败: ${data.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示图片信息
|
||||||
|
if (data.image_type) {
|
||||||
|
imageTypeElement.textContent = data.image_type;
|
||||||
|
imageTypeElement.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
imageTypeElement.classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.description) {
|
||||||
|
imageDescriptionElement.textContent = data.description;
|
||||||
|
imageDescriptionElement.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
imageDescriptionElement.classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示聊天界面
|
||||||
|
chatContainer.classList.remove('d-none');
|
||||||
|
|
||||||
|
// 添加欢迎消息
|
||||||
|
const chatMessages = document.getElementById('chatMessages');
|
||||||
|
chatMessages.innerHTML = '';
|
||||||
|
|
||||||
|
const welcomeMessage = document.createElement('div');
|
||||||
|
welcomeMessage.className = 'message message-assistant';
|
||||||
|
|
||||||
|
let welcomeText = '你好!我是AI助手,可以帮你分析这张图片并回答问题。';
|
||||||
|
if (data.image_type && data.description) {
|
||||||
|
welcomeText += `我看到这是一张${data.image_type}的图片,${data.description}。你有什么想问的吗?`;
|
||||||
|
} else {
|
||||||
|
welcomeText += '你有什么想问的吗?';
|
||||||
|
}
|
||||||
|
|
||||||
|
welcomeMessage.innerHTML = `
|
||||||
|
<div class="message-content">${welcomeText}</div>
|
||||||
|
<div class="message-time">${getCurrentTime()}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
chatMessages.appendChild(welcomeMessage);
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert(`上传失败: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
// 恢复按钮状态
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.innerHTML = originalButtonText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化聊天表单
|
||||||
|
function initChatForm() {
|
||||||
|
const form = document.getElementById('chatForm');
|
||||||
|
const messageInput = document.getElementById('messageInput');
|
||||||
|
const chatMessages = document.getElementById('chatMessages');
|
||||||
|
|
||||||
|
form.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const message = messageInput.value.trim();
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
// 添加用户消息到聊天界面
|
||||||
|
addMessage('user', message);
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
messageInput.value = '';
|
||||||
|
|
||||||
|
// 显示正在输入指示器
|
||||||
|
const typingIndicator = document.createElement('div');
|
||||||
|
typingIndicator.className = 'typing-indicator';
|
||||||
|
typingIndicator.innerHTML = '<span></span><span></span><span></span>';
|
||||||
|
chatMessages.appendChild(typingIndicator);
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 移除正在输入指示器
|
||||||
|
chatMessages.removeChild(typingIndicator);
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert(`对话失败: ${data.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加AI回复到聊天界面
|
||||||
|
addMessage('assistant', data.reply);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 移除正在输入指示器
|
||||||
|
if (typingIndicator.parentNode === chatMessages) {
|
||||||
|
chatMessages.removeChild(typingIndicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`对话失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加消息到聊天界面
|
||||||
|
function addMessage(role, content) {
|
||||||
|
const chatMessages = document.getElementById('chatMessages');
|
||||||
|
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
messageElement.className = `message message-${role}`;
|
||||||
|
|
||||||
|
messageElement.innerHTML = `
|
||||||
|
<div class="message-content">${formatMessage(content)}</div>
|
||||||
|
<div class="message-time">${getCurrentTime()}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
chatMessages.appendChild(messageElement);
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化消息内容,支持简单的markdown
|
||||||
|
function formatMessage(content) {
|
||||||
|
// 转义HTML特殊字符
|
||||||
|
let formatted = content
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
|
||||||
|
// 支持加粗
|
||||||
|
formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
|
||||||
|
// 支持斜体
|
||||||
|
formatted = formatted.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||||
|
|
||||||
|
// 支持代码
|
||||||
|
formatted = formatted.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||||
|
|
||||||
|
// 支持换行
|
||||||
|
formatted = formatted.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前时间
|
||||||
|
function getCurrentTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const hours = now.getHours().toString().padStart(2, '0');
|
||||||
|
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// 页面加载完成后执行
|
// 页面加载完成后执行
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// 初始化图片预览功能
|
// 初始化图片预览功能
|
||||||
initImagePreview('uploadFile', 'uploadPreview');
|
initImagePreview('uploadFile', 'uploadPreviewContainer');
|
||||||
initImagePreview('searchFile', 'searchPreview');
|
initImagePreview('searchFile', 'searchPreview');
|
||||||
|
|
||||||
// 初始化表单提交事件
|
// 初始化表单提交事件
|
||||||
@ -9,24 +9,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
initSearchForm();
|
initSearchForm();
|
||||||
initUpdateForm();
|
initUpdateForm();
|
||||||
initDeleteForm();
|
initDeleteForm();
|
||||||
|
initBatchDeleteButtons();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化图片预览功能
|
// 初始化图片预览功能
|
||||||
function initImagePreview(inputId, previewId) {
|
function initImagePreview(inputId, containerId) {
|
||||||
const input = document.getElementById(inputId);
|
const input = document.getElementById(inputId);
|
||||||
const preview = document.getElementById(previewId);
|
const container = document.getElementById(containerId || 'uploadPreviewContainer');
|
||||||
|
|
||||||
if (input && preview) {
|
if (input && container) {
|
||||||
input.addEventListener('change', function() {
|
input.addEventListener('change', function() {
|
||||||
if (this.files && this.files[0]) {
|
// 清空预览容器
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
if (this.files && this.files.length > 0) {
|
||||||
|
// 遍历所有选择的文件
|
||||||
|
Array.from(this.files).forEach((file, index) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
const previewDiv = document.createElement('div');
|
||||||
|
previewDiv.className = 'preview-item';
|
||||||
|
|
||||||
|
const previewImg = document.createElement('img');
|
||||||
|
previewImg.className = 'img-preview';
|
||||||
|
previewImg.alt = `预览图 ${index + 1}`;
|
||||||
|
|
||||||
|
previewDiv.appendChild(previewImg);
|
||||||
|
container.appendChild(previewDiv);
|
||||||
|
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
preview.src = e.target.result;
|
previewImg.src = e.target.result;
|
||||||
preview.classList.remove('d-none');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(this.files[0]);
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -37,39 +52,106 @@ function initUploadForm() {
|
|||||||
const form = document.getElementById('uploadForm');
|
const form = document.getElementById('uploadForm');
|
||||||
|
|
||||||
if (form) {
|
if (form) {
|
||||||
form.addEventListener('submit', function(e) {
|
form.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const formData = new FormData(this);
|
const files = document.getElementById('uploadFile').files;
|
||||||
const resultDiv = document.getElementById('uploadResult');
|
if (!files || files.length === 0) {
|
||||||
|
alert('请选择至少一个文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultDiv = document.getElementById('uploadResult');
|
||||||
resultDiv.innerHTML = '<div class="alert alert-info"><div class="spinner-border text-primary" role="status"></div>正在上传,请稍候...</div>';
|
resultDiv.innerHTML = '<div class="alert alert-info"><div class="spinner-border text-primary" role="status"></div>正在上传,请稍候...</div>';
|
||||||
|
|
||||||
fetch('/upload', {
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
// 获取表单基本数据
|
||||||
|
const type = document.getElementById('imageType').value;
|
||||||
|
const description = document.getElementById('imageDescription').value;
|
||||||
|
const name = document.getElementById('imageName').value;
|
||||||
|
const tags = document.getElementById('imageTags').value || '1'; // 默认为1
|
||||||
|
|
||||||
|
// 依次上传每个文件
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// 添加当前文件
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
// 添加其他表单数据
|
||||||
|
formData.append('type', type);
|
||||||
|
formData.append('description', description);
|
||||||
|
formData.append('name', `${name}_${i+1}`);
|
||||||
|
formData.append('tags', tags);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
})
|
});
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
resultDiv.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
|
errorCount++;
|
||||||
} else {
|
} else {
|
||||||
|
successCount++;
|
||||||
|
results.push(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度信息
|
||||||
resultDiv.innerHTML = `
|
resultDiv.innerHTML = `
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-info">
|
||||||
<h5>上传成功!</h5>
|
<div class="progress mb-2">
|
||||||
<p>图片签名: ${data.cont_sign}</p>
|
<div class="progress-bar" role="progressbar" style="width: ${Math.round((i+1) / files.length * 100)}%" aria-valuenow="${i+1}" aria-valuemin="0" aria-valuemax="${files.length}">${Math.round((i+1) / files.length * 100)}%</div>
|
||||||
<p>日志ID: ${data.log_id}</p>
|
</div>
|
||||||
|
<p>正在上传: ${i+1}/${files.length}</p>
|
||||||
|
<p>成功: ${successCount}, 失败: ${errorCount}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示最终结果
|
||||||
|
if (successCount > 0) {
|
||||||
|
let resultHTML = `
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<h5>上传完成!</h5>
|
||||||
|
<p>成功上传: ${successCount}张图片</p>
|
||||||
|
<p>上传失败: ${errorCount}张图片</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
resultHTML += '<div class="mt-3"><h6>上传成功的图片:</h6><ul>';
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
resultHTML += `<li>图片${index+1} - 签名: ${result.cont_sign}</li>`;
|
||||||
|
});
|
||||||
|
resultHTML += '</ul></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
resultDiv.innerHTML = resultHTML;
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
form.reset();
|
form.reset();
|
||||||
document.getElementById('uploadPreview').classList.add('d-none');
|
document.getElementById('uploadPreviewContainer').innerHTML = '';
|
||||||
|
document.getElementById('imageTags').value = '1'; // 重置后设置默认值为1
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger">所有图片上传失败</div>`;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
resultDiv.innerHTML = `<div class="alert alert-danger">上传失败: ${error.message}</div>`;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 设置默认标签值
|
||||||
|
const tagsInput = document.getElementById('imageTags');
|
||||||
|
if (tagsInput && !tagsInput.value) {
|
||||||
|
tagsInput.value = '1';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +160,12 @@ function initSearchForm() {
|
|||||||
const form = document.getElementById('searchForm');
|
const form = document.getElementById('searchForm');
|
||||||
|
|
||||||
if (form) {
|
if (form) {
|
||||||
|
// 设置默认标签值
|
||||||
|
const tagsInput = document.getElementById('searchTags');
|
||||||
|
if (tagsInput && !tagsInput.value) {
|
||||||
|
tagsInput.value = '1';
|
||||||
|
}
|
||||||
|
|
||||||
form.addEventListener('submit', function(e) {
|
form.addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -86,9 +174,13 @@ function initSearchForm() {
|
|||||||
const resultList = document.getElementById('searchResultList');
|
const resultList = document.getElementById('searchResultList');
|
||||||
const resultStats = document.getElementById('searchStats');
|
const resultStats = document.getElementById('searchStats');
|
||||||
const resultCount = document.getElementById('resultCount');
|
const resultCount = document.getElementById('resultCount');
|
||||||
|
const batchActions = document.querySelector('.batch-actions');
|
||||||
|
const reliableTypesInfo = document.getElementById('reliableTypesInfo');
|
||||||
|
|
||||||
resultList.innerHTML = '<div class="col-12 text-center"><div class="spinner-border text-primary" role="status"></div> 正在搜索,请稍候...</div>';
|
resultList.innerHTML = '<div class="col-12 text-center"><div class="spinner-border text-primary" role="status"></div> 正在搜索,请稍候...</div>';
|
||||||
resultStats.classList.add('d-none');
|
resultStats.classList.add('d-none');
|
||||||
|
batchActions.classList.add('d-none');
|
||||||
|
reliableTypesInfo.classList.add('d-none');
|
||||||
|
|
||||||
fetch('/search', {
|
fetch('/search', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -103,6 +195,18 @@ function initSearchForm() {
|
|||||||
resultCount.textContent = data.result_num || 0;
|
resultCount.textContent = data.result_num || 0;
|
||||||
resultStats.classList.remove('d-none');
|
resultStats.classList.remove('d-none');
|
||||||
|
|
||||||
|
// 显示最可信类型分析结果
|
||||||
|
if (data.most_reliable_types) {
|
||||||
|
reliableTypesInfo.classList.remove('d-none');
|
||||||
|
document.getElementById('method1Type').textContent = data.most_reliable_types.method1 || '-';
|
||||||
|
document.getElementById('method2Type').textContent = data.most_reliable_types.method2 || '-';
|
||||||
|
document.getElementById('method3Type').textContent = data.most_reliable_types.method3 || '-';
|
||||||
|
document.getElementById('method4Type').textContent = data.most_reliable_types.method4 || '-';
|
||||||
|
document.getElementById('method5Type').textContent = data.most_reliable_types.method5 || '-';
|
||||||
|
} else {
|
||||||
|
reliableTypesInfo.classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
// 清空结果列表
|
// 清空结果列表
|
||||||
resultList.innerHTML = '';
|
resultList.innerHTML = '';
|
||||||
|
|
||||||
@ -114,7 +218,7 @@ function initSearchForm() {
|
|||||||
try {
|
try {
|
||||||
briefInfo = JSON.parse(item.brief);
|
briefInfo = JSON.parse(item.brief);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
briefInfo = { name: '未知', id: '未知' };
|
briefInfo = { type: '未知', description: '未知', name: '未知' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultItem = document.createElement('div');
|
const resultItem = document.createElement('div');
|
||||||
@ -122,17 +226,22 @@ function initSearchForm() {
|
|||||||
resultItem.innerHTML = `
|
resultItem.innerHTML = `
|
||||||
<div class="card result-card">
|
<div class="card result-card">
|
||||||
<div class="result-img-container">
|
<div class="result-img-container">
|
||||||
|
<div class="select-checkbox">
|
||||||
|
<input type="checkbox" class="item-checkbox" data-cont-sign="${item.cont_sign}">
|
||||||
|
</div>
|
||||||
<img class="result-img" src="/static/img/placeholder.png" alt="${briefInfo.name || '图片'}">
|
<img class="result-img" src="/static/img/placeholder.png" alt="${briefInfo.name || '图片'}">
|
||||||
</div>
|
</div>
|
||||||
<div class="result-info">
|
<div class="result-info">
|
||||||
<h5>${briefInfo.name || '未命名图片'}</h5>
|
<h5>${briefInfo.name || '未命名图片'}</h5>
|
||||||
<p>ID: ${briefInfo.id || '无ID'}</p>
|
<p>类型: ${briefInfo.type || '未指定'}</p>
|
||||||
|
<p>描述: ${briefInfo.description ? (briefInfo.description.length > 30 ? briefInfo.description.substring(0, 30) + '...' : briefInfo.description) : '无描述'}</p>
|
||||||
<p>相似度: <span class="result-score">${(item.score * 100).toFixed(2)}%</span></p>
|
<p>相似度: <span class="result-score">${(item.score * 100).toFixed(2)}%</span></p>
|
||||||
<div class="result-actions">
|
<div class="result-actions">
|
||||||
<button class="btn btn-sm btn-outline-primary update-btn"
|
<button class="btn btn-sm btn-outline-primary update-btn"
|
||||||
data-cont-sign="${item.cont_sign}"
|
data-cont-sign="${item.cont_sign}"
|
||||||
|
data-type="${briefInfo.type || ''}"
|
||||||
|
data-description="${briefInfo.description || ''}"
|
||||||
data-name="${briefInfo.name || ''}"
|
data-name="${briefInfo.name || ''}"
|
||||||
data-id="${briefInfo.id || ''}"
|
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#updateModal">
|
data-bs-target="#updateModal">
|
||||||
编辑
|
编辑
|
||||||
@ -152,6 +261,8 @@ function initSearchForm() {
|
|||||||
|
|
||||||
// 初始化编辑和删除按钮事件
|
// 初始化编辑和删除按钮事件
|
||||||
initResultButtons();
|
initResultButtons();
|
||||||
|
// 初始化批量删除功能
|
||||||
|
initBatchDelete();
|
||||||
} else {
|
} else {
|
||||||
resultList.innerHTML = '<div class="col-12"><div class="alert alert-warning">没有找到匹配的图片</div></div>';
|
resultList.innerHTML = '<div class="col-12"><div class="alert alert-warning">没有找到匹配的图片</div></div>';
|
||||||
}
|
}
|
||||||
@ -170,12 +281,14 @@ function initResultButtons() {
|
|||||||
document.querySelectorAll('.update-btn').forEach(button => {
|
document.querySelectorAll('.update-btn').forEach(button => {
|
||||||
button.addEventListener('click', function() {
|
button.addEventListener('click', function() {
|
||||||
const contSign = this.getAttribute('data-cont-sign');
|
const contSign = this.getAttribute('data-cont-sign');
|
||||||
|
const type = this.getAttribute('data-type');
|
||||||
|
const description = this.getAttribute('data-description');
|
||||||
const name = this.getAttribute('data-name');
|
const name = this.getAttribute('data-name');
|
||||||
const id = this.getAttribute('data-id');
|
|
||||||
|
|
||||||
document.getElementById('updateContSign').value = contSign;
|
document.getElementById('updateContSign').value = contSign;
|
||||||
|
document.getElementById('updateType').value = type;
|
||||||
|
document.getElementById('updateDescription').value = description;
|
||||||
document.getElementById('updateName').value = name;
|
document.getElementById('updateName').value = name;
|
||||||
document.getElementById('updateId').value = id;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -195,14 +308,16 @@ function initUpdateForm() {
|
|||||||
if (updateButton) {
|
if (updateButton) {
|
||||||
updateButton.addEventListener('click', function() {
|
updateButton.addEventListener('click', function() {
|
||||||
const contSign = document.getElementById('updateContSign').value;
|
const contSign = document.getElementById('updateContSign').value;
|
||||||
|
const type = document.getElementById('updateType').value;
|
||||||
|
const description = document.getElementById('updateDescription').value;
|
||||||
const name = document.getElementById('updateName').value;
|
const name = document.getElementById('updateName').value;
|
||||||
const id = document.getElementById('updateId').value;
|
const tags = document.getElementById('updateTags').value || '1';
|
||||||
const tags = document.getElementById('updateTags').value;
|
|
||||||
|
|
||||||
// 创建brief信息
|
// 创建brief信息
|
||||||
const brief = {
|
const brief = {
|
||||||
name: name,
|
type: type,
|
||||||
id: id
|
description: description,
|
||||||
|
name: name
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送更新请求
|
// 发送更新请求
|
||||||
@ -280,3 +395,136 @@ function initDeleteForm() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化批量删除按钮
|
||||||
|
function initBatchDeleteButtons() {
|
||||||
|
const batchDeleteBtn = document.getElementById('batchDeleteBtn');
|
||||||
|
const cancelSelectionBtn = document.getElementById('cancelSelectionBtn');
|
||||||
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
|
|
||||||
|
if (batchDeleteBtn) {
|
||||||
|
batchDeleteBtn.addEventListener('click', function() {
|
||||||
|
const selectedItems = document.querySelectorAll('.item-checkbox:checked');
|
||||||
|
|
||||||
|
if (selectedItems.length === 0) {
|
||||||
|
alert('请选择要删除的图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirm(`确定要删除选中的 ${selectedItems.length} 张图片吗?此操作不可撤销。`)) {
|
||||||
|
batchDelete(selectedItems);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelSelectionBtn) {
|
||||||
|
cancelSelectionBtn.addEventListener('click', function() {
|
||||||
|
document.querySelectorAll('.item-checkbox').forEach(checkbox => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
}
|
||||||
|
document.querySelector('.batch-actions').classList.add('d-none');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全选功能
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.addEventListener('change', function() {
|
||||||
|
const checkboxes = document.querySelectorAll('.item-checkbox');
|
||||||
|
const batchActions = document.querySelector('.batch-actions');
|
||||||
|
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = this.checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.checked && checkboxes.length > 0) {
|
||||||
|
batchActions.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
batchActions.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化批量删除功能
|
||||||
|
function initBatchDelete() {
|
||||||
|
const checkboxes = document.querySelectorAll('.item-checkbox');
|
||||||
|
const batchActions = document.querySelector('.batch-actions');
|
||||||
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
|
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', function() {
|
||||||
|
const checkedBoxes = document.querySelectorAll('.item-checkbox:checked');
|
||||||
|
const anyChecked = checkedBoxes.length > 0;
|
||||||
|
const allChecked = checkedBoxes.length === checkboxes.length;
|
||||||
|
|
||||||
|
// 更新全选复选框状态
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.checked = allChecked && checkboxes.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyChecked) {
|
||||||
|
batchActions.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
batchActions.classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除选中的图片
|
||||||
|
async function batchDelete(selectedItems) {
|
||||||
|
const resultList = document.getElementById('searchResultList');
|
||||||
|
const loadingDiv = document.createElement('div');
|
||||||
|
loadingDiv.className = 'col-12 text-center batch-delete-progress';
|
||||||
|
loadingDiv.innerHTML = '<div class="alert alert-info"><div class="spinner-border text-primary" role="status"></div> 正在删除选中图片,请稍候...</div>';
|
||||||
|
|
||||||
|
resultList.appendChild(loadingDiv);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < selectedItems.length; i++) {
|
||||||
|
const contSign = selectedItems[i].getAttribute('data-cont-sign');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
cont_sign: contSign
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
errorCount++;
|
||||||
|
} else {
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
loadingDiv.innerHTML = `
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<div class="progress mb-2">
|
||||||
|
<div class="progress-bar" role="progressbar" style="width: ${Math.round((i+1) / selectedItems.length * 100)}%" aria-valuenow="${i+1}" aria-valuemin="0" aria-valuemax="${selectedItems.length}">${Math.round((i+1) / selectedItems.length * 100)}%</div>
|
||||||
|
</div>
|
||||||
|
<p>正在删除: ${i+1}/${selectedItems.length}</p>
|
||||||
|
<p>成功: ${successCount}, 失败: ${errorCount}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除完成后刷新搜索结果
|
||||||
|
alert(`批量删除完成\n成功: ${successCount} 张\n失败: ${errorCount} 张`);
|
||||||
|
document.getElementById('searchForm').dispatchEvent(new Event('submit'));
|
||||||
|
}
|
||||||
|
|||||||
72
app/templates/chat.html
Normal file
72
app/templates/chat.html
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>图片智能对话系统 - 地瓜机器人</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="text-center my-4">
|
||||||
|
<h1>图片智能对话系统</h1>
|
||||||
|
<p class="lead">上传图片,与AI进行多轮对话</p>
|
||||||
|
<nav class="mt-3">
|
||||||
|
<a href="/" class="btn btn-outline-secondary">返回首页</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">上传图片</h5>
|
||||||
|
<form id="uploadImageForm" enctype="multipart/form-data">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="chatImageFile" class="form-label">选择图片</label>
|
||||||
|
<input class="form-control" type="file" id="chatImageFile" name="file" accept="image/*" required>
|
||||||
|
<div class="form-text">支持JPG、PNG、JPEG、GIF格式</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">上传并开始对话</button>
|
||||||
|
</form>
|
||||||
|
<div id="uploadPreview" class="mt-3 text-center d-none">
|
||||||
|
<div class="preview-container">
|
||||||
|
<img id="previewImage" class="img-fluid rounded" alt="预览图">
|
||||||
|
</div>
|
||||||
|
<div id="imageInfo" class="mt-2">
|
||||||
|
<div id="imageType" class="badge bg-info"></div>
|
||||||
|
<div id="imageDescription" class="text-muted small mt-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chatContainer" class="card d-none">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">与图片对话</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="chatMessages" class="chat-messages mb-3"></div>
|
||||||
|
<div class="chat-input-container">
|
||||||
|
<form id="chatForm">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="messageInput" class="form-control" placeholder="输入您的问题..." required>
|
||||||
|
<button class="btn btn-primary" type="submit">
|
||||||
|
<i class="bi bi-send"></i> 发送
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/chat.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -3,59 +3,65 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>相似图片搜索系统</title>
|
<title>地瓜机器人相似图片搜索及分类识别系统</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="text-center my-5">
|
<header class="text-center my-5">
|
||||||
<h1>相似图片搜索系统</h1>
|
<h1>地瓜机器人相似图片搜索及分类识别demo</h1>
|
||||||
<p class="lead">基于百度AI开放平台的相似图片搜索API</p>
|
<p class="lead">基于图片相似度进行分类的demo演示</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12 mb-4">
|
<div class="col-md-12 mb-4">
|
||||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button" role="tab" aria-controls="upload" aria-selected="true">图片入库</button>
|
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button" role="tab" aria-controls="upload" aria-selected="true">角色录入</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="search-tab" data-bs-toggle="tab" data-bs-target="#search" type="button" role="tab" aria-controls="search" aria-selected="false">图片搜索</button>
|
<button class="nav-link" id="search-tab" data-bs-toggle="tab" data-bs-target="#search" type="button" role="tab" aria-controls="search" aria-selected="false">图片搜索</button>
|
||||||
</li>
|
</li>
|
||||||
|
<!-- <li class="nav-item" role="presentation"> -->
|
||||||
|
<!-- <button class="nav-link" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage" type="button" role="tab" aria-controls="manage" aria-selected="false">图库管理</button> -->
|
||||||
|
<!-- </li> -->
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage" type="button" role="tab" aria-controls="manage" aria-selected="false">图库管理</button>
|
<a href="/chat-with-image" class="nav-link" style="color: #0d6efd; font-weight: bold;">图片智能对话</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content" id="myTabContent">
|
<div class="tab-content" id="myTabContent">
|
||||||
<!-- 图片入库 -->
|
<!-- 角色录入 -->
|
||||||
<div class="tab-pane fade show active" id="upload" role="tabpanel" aria-labelledby="upload-tab">
|
<div class="tab-pane fade show active" id="upload" role="tabpanel" aria-labelledby="upload-tab">
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">图片入库</h5>
|
<h5 class="card-title">角色录入</h5>
|
||||||
<form id="uploadForm" enctype="multipart/form-data">
|
<form id="uploadForm" enctype="multipart/form-data">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="uploadFile" class="form-label">选择图片</label>
|
<label for="uploadFile" class="form-label">选择图片(这里可以多选的,也就是批量入库)</label>
|
||||||
<input class="form-control" type="file" id="uploadFile" name="file" accept="image/*" required>
|
<input class="form-control" type="file" id="uploadFile" name="file" accept="image/*" multiple required>
|
||||||
<div class="form-text">支持JPG、PNG、BMP格式,最短边至少50px,最长边最大4096px</div>
|
<div class="form-text">支持JPG、PNG、BMP格式,最短边至少50px,最长边最大4096px</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="imageName" class="form-label">图片名称</label>
|
<label for="imageType" class="form-label">类型(这个实际就是宠物名,类似小狗pet、小猫pussy之类的)</label>
|
||||||
|
<input type="text" class="form-control" id="imageType" name="type" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="imageDescription" class="form-label">描述(目前demo只支持250个字符,后面正式版可以扩充,这个实际是角色卡信息,也就是宠物的描述,类似小狗是金毛之类的)</label>
|
||||||
|
<textarea class="form-control" id="imageDescription" name="description" rows="2" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="imageName" class="form-label">名称(这个实际是图片名称,直接和宠物名重名即可,我会给加个名字)</label>
|
||||||
<input type="text" class="form-control" id="imageName" name="name" required>
|
<input type="text" class="form-control" id="imageName" name="name" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="imageId" class="form-label">图片ID</label>
|
<label for="imageTags" class="form-label">标签(后面可以用来区分给哪个机器人用的)</label>
|
||||||
<input type="text" class="form-control" id="imageId" name="id" required>
|
<input type="text" class="form-control" id="imageTags" name="tags" placeholder="例如:1,2" value="1">
|
||||||
<div class="form-text">用于关联至本地图库的标识</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="imageTags" class="form-label">标签</label>
|
|
||||||
<input type="text" class="form-control" id="imageTags" name="tags" placeholder="例如:1,2">
|
|
||||||
<div class="form-text">最多2个标签,用逗号分隔</div>
|
<div class="form-text">最多2个标签,用逗号分隔</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="upload-preview-container">
|
<div class="upload-preview-container" id="uploadPreviewContainer">
|
||||||
<img id="uploadPreview" class="img-preview d-none" src="#" alt="预览图">
|
<!-- 预览图将在这里动态添加 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">上传入库</button>
|
<button type="submit" class="btn btn-primary">上传入库</button>
|
||||||
@ -77,11 +83,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="searchTags" class="form-label">标签过滤</label>
|
<label for="searchTags" class="form-label">标签过滤</label>
|
||||||
<input type="text" class="form-control" id="searchTags" name="tags" placeholder="例如:1,2">
|
<input type="text" class="form-control" id="searchTags" name="tags" placeholder="例如:1,2" value="1">
|
||||||
<div class="form-text">可选,用于按标签筛选结果</div>
|
<div class="form-text">可选,用于按标签筛选结果(默认是1,后面可以用来区分机器人,类似对象存储分桶)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">标签逻辑</label>
|
<label class="form-label">标签逻辑(现在不用考虑)</label>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="radio" name="tag_logic" id="tagLogicAnd" value="0" checked>
|
<input class="form-check-input" type="radio" name="tag_logic" id="tagLogicAnd" value="0" checked>
|
||||||
<label class="form-check-label" for="tagLogicAnd">
|
<label class="form-check-label" for="tagLogicAnd">
|
||||||
@ -95,6 +101,11 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="searchType" class="form-label">类型过滤(现在不用考虑,后面可以用来做验证之类的用)</label>
|
||||||
|
<input type="text" class="form-control" id="searchType" name="type" placeholder="可选的类型过滤">
|
||||||
|
<div class="form-text">可选,用于按类型筛选结果</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="search-preview-container">
|
<div class="search-preview-container">
|
||||||
<img id="searchPreview" class="img-preview d-none" src="#" alt="预览图">
|
<img id="searchPreview" class="img-preview d-none" src="#" alt="预览图">
|
||||||
@ -104,7 +115,67 @@
|
|||||||
</form>
|
</form>
|
||||||
<div id="searchResult" class="mt-3">
|
<div id="searchResult" class="mt-3">
|
||||||
<div id="searchStats" class="mb-3 d-none">
|
<div id="searchStats" class="mb-3 d-none">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
<h6>搜索结果:<span id="resultCount">0</span> 个匹配项</h6>
|
<h6>搜索结果:<span id="resultCount">0</span> 个匹配项</h6>
|
||||||
|
<div class="form-check mt-1">
|
||||||
|
<input class="form-check-input" type="checkbox" id="selectAllCheckbox">
|
||||||
|
<label class="form-check-label" for="selectAllCheckbox">全选</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="batch-actions d-none">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" id="batchDeleteBtn">批量删除</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary ms-2" id="cancelSelectionBtn">取消选择</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 最可信类型分析区域 -->
|
||||||
|
<div id="reliableTypesInfo" class="mb-4 d-none">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">最可信类型分析(这些方法我写的都比较简单,后面可以再优化)</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>分析方法</th>
|
||||||
|
<th>结果类型</th>
|
||||||
|
<th>说明</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td title="该方法直接选择相似度最高的图片的类型作为结果。这是最直接的方法,适用于高质量图片库。">方法一</td>
|
||||||
|
<td id="method1Type">-</td>
|
||||||
|
<td title="该方法假设相似度最高的图片最可能提供正确的类型信息。然而,如果最高分数的图片是异常值,这种方法可能会导致错误。">基于最高相似度分数选择类型</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td title="该方法考虑所有结果的类型,但根据其相似度分数进行加权。相似度越高,权重越大。">方法二</td>
|
||||||
|
<td id="method2Type">-</td>
|
||||||
|
<td title="该方法将每个类型的所有分数相加,选择总分最高的类型。这种方法在有多个相似度高的结果时比方法一更可靠。">基于加权投票选择类型</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td title="该方法首先过滤掉相似度低于阈值的结果,然后对剩下的结果使用加权投票。">方法三</td>
|
||||||
|
<td id="method3Type">-</td>
|
||||||
|
<td title="该方法只考虑相似度大于0.6的结果,对这些结果进行加权投票。这种方法可以有效过滤掉低质量的匹配,提高准确性。">基于阈值过滤和加权投票选择类型</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td title="该方法统计每种类型出现的次数,选择出现次数最多的类型。">方法四</td>
|
||||||
|
<td id="method4Type">-</td>
|
||||||
|
<td title="该方法仅考虑类型出现的频率,不考虑相似度分数。这种方法在所有结果的相似度相近时效果最好。">基于多数投票选择类型</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td title="该方法结合了多数投票和加权投票的优点,同时考虑类型出现的频率和其平均相似度。">方法五</td>
|
||||||
|
<td id="method5Type">-</td>
|
||||||
|
<td title="该方法将每种类型的出现次数与其平均相似度相乘,选择加权后分数最高的类型。这种方法在大多数情况下提供最平衡的结果。">基于加权多数投票选择类型</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="searchResultList" class="row"></div>
|
<div id="searchResultList" class="row"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -119,10 +190,10 @@
|
|||||||
<h5 class="card-title">图库管理</h5>
|
<h5 class="card-title">图库管理</h5>
|
||||||
<p>此功能需要先搜索图片,然后可以对搜索结果进行管理操作。</p>
|
<p>此功能需要先搜索图片,然后可以对搜索结果进行管理操作。</p>
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
提示:百度相似图片搜索API不提供直接查看所有图库内容的功能,需要通过搜索相似图片来获取图库内容。
|
提示:因为百度相似图片搜索API不提供直接查看所有图库内容的功能,需要通过搜索相似图片来获取图库内容,目前先这样,后面看效果好的话再考虑管理相关的功能。
|
||||||
</div>
|
</div>
|
||||||
<div id="manageInfo" class="mt-3">
|
<div id="manageInfo" class="mt-3">
|
||||||
<p>请先在"图片搜索"标签页进行搜索,然后可以对搜索结果进行管理。</p>
|
<p>因为我们用的是百度的api做为支撑的,所以先这样,包括搜索界面不展示图片也是</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -144,16 +215,20 @@
|
|||||||
<form id="updateForm">
|
<form id="updateForm">
|
||||||
<input type="hidden" id="updateContSign" name="cont_sign">
|
<input type="hidden" id="updateContSign" name="cont_sign">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="updateName" class="form-label">图片名称</label>
|
<label for="updateType" class="form-label">类型</label>
|
||||||
|
<input type="text" class="form-control" id="updateType" name="type" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="updateDescription" class="form-label">描述</label>
|
||||||
|
<textarea class="form-control" id="updateDescription" name="description" rows="2" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="updateName" class="form-label">名称</label>
|
||||||
<input type="text" class="form-control" id="updateName" name="name" required>
|
<input type="text" class="form-control" id="updateName" name="name" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="updateId" class="form-label">图片ID</label>
|
|
||||||
<input type="text" class="form-control" id="updateId" name="id" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="updateTags" class="form-label">标签</label>
|
<label for="updateTags" class="form-label">标签</label>
|
||||||
<input type="text" class="form-control" id="updateTags" name="tags" placeholder="例如:1,2">
|
<input type="text" class="form-control" id="updateTags" name="tags" placeholder="例如:1,2" value="1">
|
||||||
<div class="form-text">最多2个标签,用逗号分隔</div>
|
<div class="form-text">最多2个标签,用逗号分隔</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
54
migrate_types_to_mongo.py
Normal file
54
migrate_types_to_mongo.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
迁移脚本:将类型描述从JSON文件迁移到MongoDB
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from app.api.type_manager_mongo import TypeManagerMongo
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
def migrate_types_to_mongo():
|
||||||
|
"""将类型描述从JSON文件迁移到MongoDB"""
|
||||||
|
# 创建MongoDB类型管理器
|
||||||
|
mongo_manager = TypeManagerMongo()
|
||||||
|
|
||||||
|
# 读取JSON文件
|
||||||
|
json_file_path = os.path.join('app', 'data', 'type_descriptions.json')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(json_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
type_data = json.load(f)
|
||||||
|
|
||||||
|
# 导入数据到MongoDB
|
||||||
|
imported_count = mongo_manager.import_from_json(type_data)
|
||||||
|
print(f"成功导入 {imported_count} 个类型描述到MongoDB")
|
||||||
|
|
||||||
|
# 验证导入
|
||||||
|
all_types = mongo_manager.get_all_types()
|
||||||
|
print(f"MongoDB中现有 {len(all_types)} 个类型描述")
|
||||||
|
|
||||||
|
# 打印部分类型作为验证
|
||||||
|
print("\n部分类型描述示例:")
|
||||||
|
count = 0
|
||||||
|
for type_name, description in all_types.items():
|
||||||
|
print(f"类型: {type_name}")
|
||||||
|
print(f"描述: {description[:100]}..." if len(description) > 100 else f"描述: {description}")
|
||||||
|
print("-" * 50)
|
||||||
|
count += 1
|
||||||
|
if count >= 3: # 只显示前3个类型
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"迁移过程中出错: {str(e)}")
|
||||||
|
finally:
|
||||||
|
# 关闭MongoDB连接
|
||||||
|
mongo_manager.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate_types_to_mongo()
|
||||||
@ -3,3 +3,4 @@ requests==2.26.0
|
|||||||
python-dotenv==0.19.0
|
python-dotenv==0.19.0
|
||||||
Pillow==8.3.1
|
Pillow==8.3.1
|
||||||
flask-cors==3.0.10
|
flask-cors==3.0.10
|
||||||
|
pymongo==4.5.0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user