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:
eust-w 2025-04-11 12:00:32 +08:00
parent 9c2229fa47
commit 487f3af948
21 changed files with 1661 additions and 96 deletions

15
.env
View File

@ -1,2 +1,13 @@
BAIDU_API_KEY=ALTAKmzKDy1OqhqmepD2OeXqbN
BAIDU_SECRET_KEY=b79c5fbc26344868916ec6e9e2ff65f0
BAIDU_API_KEY=cqn5SYRZAKM9WBXgYg8niUdM
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

Binary file not shown.

177
app.py
View File

@ -4,15 +4,20 @@
import os
import json
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 werkzeug.utils import secure_filename
from app.api.baidu_image_search import BaiduImageSearch
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')
CORS(app) # 启用CORS支持跨域请求
# 设置会话密钥
app.secret_key = os.urandom(24)
# 配置上传文件夹
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
if not os.path.exists(UPLOAD_FOLDER):
@ -25,6 +30,16 @@ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
# 初始化百度图像搜索API
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):
"""检查文件扩展名是否允许"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@ -48,14 +63,18 @@ def upload_image():
return jsonify({'error': '不支持的文件类型'}), 400
# 获取表单数据
image_type = request.form.get('type', '')
description = request.form.get('description', '')
name = request.form.get('name', '')
image_id = request.form.get('id', '')
tags = request.form.get('tags', '')
tags = request.form.get('tags', '1') # 默认标签为1
# 创建brief信息
# 将类型和描述信息保存到本地
type_manager.add_type(image_type, description)
# 创建brief信息不包含描述信息
brief = {
'name': name,
'id': image_id
'type': image_type,
'name': name
}
# 保存文件
@ -92,7 +111,8 @@ def search_image():
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')
# 保存文件
@ -105,9 +125,29 @@ def search_image():
result = image_search_api.search_image(
image_path=file_path,
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)
except Exception as e:
return jsonify({'error': str(e)}), 500
@ -141,6 +181,18 @@ def update_image():
brief = data.get('brief')
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:
# 调用API更新图片
result = image_search_api.update_image(
@ -161,5 +213,112 @@ def get_token():
except Exception as e:
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__':
app.run(debug=True, host='0.0.0.0', port=5000)
app.run(debug=True, host='0.0.0.0', port=5001)

Binary file not shown.

Binary file not shown.

Binary file not shown.

166
app/api/azure_openai.py Normal file
View 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}")

View File

@ -45,7 +45,7 @@ class BaiduImageSearch:
image_base64: 图片的base64编码
url: 图片URL
brief: 图片摘要信息最长256B{"name":"图片名称", "id":"123"}
tags: 分类信息最多2个tag"1,2"
tags: 分类信息1 - 65535范围内的整数 tag间以逗号分隔最多2个tag2个tag无层级关系检索时支持逻辑运算样例"100,11" 检索时可圈定分类维度进行检索
Returns:
dict: API返回的结果
@ -80,13 +80,15 @@ class BaiduImageSearch:
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
response = requests.post(request_url, data=params, headers=headers)
print(f"添加图片请求: {params}")
print(f"添加图片响应: {response.text}")
if response.status_code == 200:
return response.json()
else:
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
tags: 分类信息过滤"1,2"
tag_logic: 标签逻辑0表示逻辑and1表示逻辑or
type_filter: 类型过滤用于按图片类型进行筛选
pn: 分页起始位置默认0
rn: 返回结果数量默认300最大1000
@ -128,11 +131,47 @@ class BaiduImageSearch:
if rn is not None:
params['rn'] = rn
# 处理搜索结果的类型过滤
self.type_filter = type_filter
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
# print(f"搜索图片请求: {params}")
response = requests.post(request_url, data=params, headers=headers)
# print(f"搜索图片响应: {response.text}")
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:
raise Exception(f"检索图片失败: {response.text}")
@ -175,6 +214,143 @@ class BaiduImageSearch:
else:
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):
"""
更新图库中图片的摘要和分类信息

116
app/api/type_manager.py Normal file
View 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()

View 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()

View 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
View 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;
}
}

View File

@ -71,17 +71,40 @@ header .lead {
/* 图片预览 */
.img-preview {
max-width: 100%;
max-height: 300px;
max-height: 200px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 10px;
}
.upload-preview-container,
.search-preview-container {
text-align: center;
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-start;
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 {
margin-bottom: 20px;
@ -106,6 +129,7 @@ header .lead {
justify-content: center;
background-color: #f8f9fa;
border-radius: 5px;
position: relative;
}
.result-img {
@ -148,6 +172,28 @@ header .lead {
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) {
.result-item {

220
app/static/js/chat.js Normal file
View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 支持加粗
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}`;
}

View File

@ -1,7 +1,7 @@
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 初始化图片预览功能
initImagePreview('uploadFile', 'uploadPreview');
initImagePreview('uploadFile', 'uploadPreviewContainer');
initImagePreview('searchFile', 'searchPreview');
// 初始化表单提交事件
@ -9,24 +9,39 @@ document.addEventListener('DOMContentLoaded', function() {
initSearchForm();
initUpdateForm();
initDeleteForm();
initBatchDeleteButtons();
});
// 初始化图片预览功能
function initImagePreview(inputId, previewId) {
function initImagePreview(inputId, containerId) {
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() {
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 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) {
preview.src = e.target.result;
preview.classList.remove('d-none');
previewImg.src = e.target.result;
};
reader.readAsDataURL(this.files[0]);
reader.readAsDataURL(file);
});
}
});
}
@ -37,39 +52,106 @@ function initUploadForm() {
const form = document.getElementById('uploadForm');
if (form) {
form.addEventListener('submit', function(e) {
form.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const resultDiv = document.getElementById('uploadResult');
const files = document.getElementById('uploadFile').files;
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>';
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',
body: formData
})
.then(response => response.json())
.then(data => {
});
const data = await response.json();
if (data.error) {
resultDiv.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
errorCount++;
} else {
successCount++;
results.push(data);
}
} catch (error) {
errorCount++;
}
// 更新进度信息
resultDiv.innerHTML = `
<div class="alert alert-success">
<h5>上传成功</h5>
<p>图片签名: ${data.cont_sign}</p>
<p>日志ID: ${data.log_id}</p>
<div class="alert alert-info">
<div class="progress mb-2">
<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>
</div>
<p>正在上传: ${i+1}/${files.length}</p>
<p>成功: ${successCount}, 失败: ${errorCount}</p>
</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();
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');
if (form) {
// 设置默认标签值
const tagsInput = document.getElementById('searchTags');
if (tagsInput && !tagsInput.value) {
tagsInput.value = '1';
}
form.addEventListener('submit', function(e) {
e.preventDefault();
@ -86,9 +174,13 @@ function initSearchForm() {
const resultList = document.getElementById('searchResultList');
const resultStats = document.getElementById('searchStats');
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>';
resultStats.classList.add('d-none');
batchActions.classList.add('d-none');
reliableTypesInfo.classList.add('d-none');
fetch('/search', {
method: 'POST',
@ -103,6 +195,18 @@ function initSearchForm() {
resultCount.textContent = data.result_num || 0;
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 = '';
@ -114,7 +218,7 @@ function initSearchForm() {
try {
briefInfo = JSON.parse(item.brief);
} catch (e) {
briefInfo = { name: '未知', id: '未知' };
briefInfo = { type: '未知', description: '未知', name: '未知' };
}
const resultItem = document.createElement('div');
@ -122,17 +226,22 @@ function initSearchForm() {
resultItem.innerHTML = `
<div class="card result-card">
<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 || '图片'}">
</div>
<div class="result-info">
<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>
<div class="result-actions">
<button class="btn btn-sm btn-outline-primary update-btn"
data-cont-sign="${item.cont_sign}"
data-type="${briefInfo.type || ''}"
data-description="${briefInfo.description || ''}"
data-name="${briefInfo.name || ''}"
data-id="${briefInfo.id || ''}"
data-bs-toggle="modal"
data-bs-target="#updateModal">
编辑
@ -152,6 +261,8 @@ function initSearchForm() {
// 初始化编辑和删除按钮事件
initResultButtons();
// 初始化批量删除功能
initBatchDelete();
} else {
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 => {
button.addEventListener('click', function() {
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 id = this.getAttribute('data-id');
document.getElementById('updateContSign').value = contSign;
document.getElementById('updateType').value = type;
document.getElementById('updateDescription').value = description;
document.getElementById('updateName').value = name;
document.getElementById('updateId').value = id;
});
});
@ -195,14 +308,16 @@ function initUpdateForm() {
if (updateButton) {
updateButton.addEventListener('click', function() {
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 id = document.getElementById('updateId').value;
const tags = document.getElementById('updateTags').value;
const tags = document.getElementById('updateTags').value || '1';
// 创建brief信息
const brief = {
name: name,
id: id
type: type,
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
View 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>

View File

@ -3,59 +3,65 @@
<head>
<meta charset="UTF-8">
<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="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div class="container">
<header class="text-center my-5">
<h1>相似图片搜索系统</h1>
<p class="lead">基于百度AI开放平台的相似图片搜索API</p>
<h1>地瓜机器人相似图片搜索及分类识别demo</h1>
<p class="lead">基于图片相似度进行分类的demo演示</p>
</header>
<div class="row">
<div class="col-md-12 mb-4">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<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 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>
</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">
<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>
</ul>
<div class="tab-content" id="myTabContent">
<!-- 图片入库 -->
<!-- 角色录入 -->
<div class="tab-pane fade show active" id="upload" role="tabpanel" aria-labelledby="upload-tab">
<div class="card mt-3">
<div class="card-body">
<h5 class="card-title">图片入库</h5>
<h5 class="card-title">角色录入</h5>
<form id="uploadForm" enctype="multipart/form-data">
<div class="mb-3">
<label for="uploadFile" class="form-label">选择图片</label>
<input class="form-control" type="file" id="uploadFile" name="file" accept="image/*" required>
<label for="uploadFile" class="form-label">选择图片(这里可以多选的,也就是批量入库)</label>
<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>
<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>
</div>
<div class="mb-3">
<label for="imageId" class="form-label">图片ID</label>
<input type="text" class="form-control" id="imageId" name="id" required>
<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">
<label for="imageTags" class="form-label">标签(后面可以用来区分给哪个机器人用的)</label>
<input type="text" class="form-control" id="imageTags" name="tags" placeholder="例如1,2" value="1">
<div class="form-text">最多2个标签用逗号分隔</div>
</div>
<div class="mb-3">
<div class="upload-preview-container">
<img id="uploadPreview" class="img-preview d-none" src="#" alt="预览图">
<div class="upload-preview-container" id="uploadPreviewContainer">
<!-- 预览图将在这里动态添加 -->
</div>
</div>
<button type="submit" class="btn btn-primary">上传入库</button>
@ -77,11 +83,11 @@
</div>
<div class="mb-3">
<label for="searchTags" class="form-label">标签过滤</label>
<input type="text" class="form-control" id="searchTags" name="tags" placeholder="例如1,2">
<div class="form-text">可选,用于按标签筛选结果</div>
<input type="text" class="form-control" id="searchTags" name="tags" placeholder="例如1,2" value="1">
<div class="form-text">可选,用于按标签筛选结果默认是1后面可以用来区分机器人类似对象存储分桶</div>
</div>
<div class="mb-3">
<label class="form-label">标签逻辑</label>
<label class="form-label">标签逻辑(现在不用考虑)</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="tag_logic" id="tagLogicAnd" value="0" checked>
<label class="form-check-label" for="tagLogicAnd">
@ -95,6 +101,11 @@
</label>
</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="search-preview-container">
<img id="searchPreview" class="img-preview d-none" src="#" alt="预览图">
@ -104,7 +115,67 @@
</form>
<div id="searchResult" class="mt-3">
<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>
<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 id="searchResultList" class="row"></div>
</div>
@ -119,10 +190,10 @@
<h5 class="card-title">图库管理</h5>
<p>此功能需要先搜索图片,然后可以对搜索结果进行管理操作。</p>
<div class="alert alert-info">
提示百度相似图片搜索API不提供直接查看所有图库内容的功能需要通过搜索相似图片来获取图库内容。
提示:因为百度相似图片搜索API不提供直接查看所有图库内容的功能需要通过搜索相似图片来获取图库内容,目前先这样,后面看效果好的话再考虑管理相关的功能
</div>
<div id="manageInfo" class="mt-3">
<p>请先在"图片搜索"标签页进行搜索,然后可以对搜索结果进行管理。</p>
<p>因为我们用的是百度的api做为支撑的所以先这样包括搜索界面不展示图片也是</p>
</div>
</div>
</div>
@ -144,16 +215,20 @@
<form id="updateForm">
<input type="hidden" id="updateContSign" name="cont_sign">
<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>
</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">
<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>
</form>

54
migrate_types_to_mongo.py Normal file
View 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()

View File

@ -3,3 +3,4 @@ requests==2.26.0
python-dotenv==0.19.0
Pillow==8.3.1
flask-cors==3.0.10
pymongo==4.5.0

0
run.sh Normal file → Executable file
View File