diff --git a/.env b/.env index c4c61a4..cc47590 100644 --- a/.env +++ b/.env @@ -1,2 +1,13 @@ -BAIDU_API_KEY=ALTAKmzKDy1OqhqmepD2OeXqbN -BAIDU_SECRET_KEY=b79c5fbc26344868916ec6e9e2ff65f0 \ No newline at end of file +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 \ No newline at end of file diff --git a/__pycache__/mock_baidu_api.cpython-39.pyc b/__pycache__/mock_baidu_api.cpython-39.pyc new file mode 100644 index 0000000..84696e3 Binary files /dev/null and b/__pycache__/mock_baidu_api.cpython-39.pyc differ diff --git a/app.py b/app.py index 7d59e81..51b6b16 100644 --- a/app.py +++ b/app.py @@ -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) diff --git a/app/api/__pycache__/azure_openai.cpython-39.pyc b/app/api/__pycache__/azure_openai.cpython-39.pyc new file mode 100644 index 0000000..1469b20 Binary files /dev/null and b/app/api/__pycache__/azure_openai.cpython-39.pyc differ diff --git a/app/api/__pycache__/baidu_image_search.cpython-39.pyc b/app/api/__pycache__/baidu_image_search.cpython-39.pyc index eeafe83..029623e 100644 Binary files a/app/api/__pycache__/baidu_image_search.cpython-39.pyc and b/app/api/__pycache__/baidu_image_search.cpython-39.pyc differ diff --git a/app/api/__pycache__/type_manager.cpython-39.pyc b/app/api/__pycache__/type_manager.cpython-39.pyc new file mode 100644 index 0000000..274f0d3 Binary files /dev/null and b/app/api/__pycache__/type_manager.cpython-39.pyc differ diff --git a/app/api/__pycache__/type_manager_mongo.cpython-39.pyc b/app/api/__pycache__/type_manager_mongo.cpython-39.pyc new file mode 100644 index 0000000..cc15438 Binary files /dev/null and b/app/api/__pycache__/type_manager_mongo.cpython-39.pyc differ diff --git a/app/api/azure_openai.py b/app/api/azure_openai.py new file mode 100644 index 0000000..f96a1b4 --- /dev/null +++ b/app/api/azure_openai.py @@ -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}") diff --git a/app/api/baidu_image_search.py b/app/api/baidu_image_search.py index 054107c..3261d0a 100644 --- a/app/api/baidu_image_search.py +++ b/app/api/baidu_image_search.py @@ -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个tag,2个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表示逻辑and,1表示逻辑or + type_filter: 类型过滤,用于按图片类型进行筛选 pn: 分页起始位置,默认0 rn: 返回结果数量,默认300,最大1000 @@ -127,12 +130,48 @@ class BaiduImageSearch: params['pn'] = pn 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): """ 更新图库中图片的摘要和分类信息 diff --git a/app/api/type_manager.py b/app/api/type_manager.py new file mode 100644 index 0000000..71fe240 --- /dev/null +++ b/app/api/type_manager.py @@ -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() diff --git a/app/api/type_manager_mongo.py b/app/api/type_manager_mongo.py new file mode 100644 index 0000000..79c1dc8 --- /dev/null +++ b/app/api/type_manager_mongo.py @@ -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() diff --git a/app/data/type_descriptions.json b/app/data/type_descriptions.json new file mode 100644 index 0000000..d1c06ef --- /dev/null +++ b/app/data/type_descriptions.json @@ -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" +} \ No newline at end of file diff --git a/app/static/css/chat.css b/app/static/css/chat.css new file mode 100644 index 0000000..706ecc3 --- /dev/null +++ b/app/static/css/chat.css @@ -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; + } +} diff --git a/app/static/css/style.css b/app/static/css/style.css index 594fd97..aaef1d2 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -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 { diff --git a/app/static/js/chat.js b/app/static/js/chat.js new file mode 100644 index 0000000..ad9daee --- /dev/null +++ b/app/static/js/chat.js @@ -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 = ' 上传中...'; + + 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 = ` +
${welcomeText}
+
${getCurrentTime()}
+ `; + + 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 = ''; + 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 = ` +
${formatMessage(content)}
+
${getCurrentTime()}
+ `; + + chatMessages.appendChild(messageElement); + + // 滚动到底部 + chatMessages.scrollTop = chatMessages.scrollHeight; +} + +// 格式化消息内容,支持简单的markdown +function formatMessage(content) { + // 转义HTML特殊字符 + let formatted = content + .replace(/&/g, '&') + .replace(//g, '>'); + + // 支持加粗 + formatted = formatted.replace(/\*\*(.*?)\*\*/g, '$1'); + + // 支持斜体 + formatted = formatted.replace(/\*(.*?)\*/g, '$1'); + + // 支持代码 + formatted = formatted.replace(/`(.*?)`/g, '$1'); + + // 支持换行 + formatted = formatted.replace(/\n/g, '
'); + + 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}`; +} diff --git a/app/static/js/main.js b/app/static/js/main.js index bc00172..904ed3c 100644 --- a/app/static/js/main.js +++ b/app/static/js/main.js @@ -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]) { - const reader = new FileReader(); - - reader.onload = function(e) { - preview.src = e.target.result; - preview.classList.remove('d-none'); - }; - - reader.readAsDataURL(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) { + previewImg.src = e.target.result; + }; + + 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 = '
正在上传,请稍候...
'; - fetch('/upload', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.error) { - resultDiv.innerHTML = `
${data.error}
`; - } else { - resultDiv.innerHTML = ` -
-
上传成功!
-

图片签名: ${data.cont_sign}

-

日志ID: ${data.log_id}

-
- `; - // 重置表单 - form.reset(); - document.getElementById('uploadPreview').classList.add('d-none'); + 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 + }); + + const data = await response.json(); + + if (data.error) { + errorCount++; + } else { + successCount++; + results.push(data); + } + } catch (error) { + errorCount++; } - }) - .catch(error => { - resultDiv.innerHTML = `
上传失败: ${error.message}
`; - }); + + // 更新进度信息 + resultDiv.innerHTML = ` +
+
+
${Math.round((i+1) / files.length * 100)}%
+
+

正在上传: ${i+1}/${files.length}

+

成功: ${successCount}, 失败: ${errorCount}

+
+ `; + } + + // 显示最终结果 + if (successCount > 0) { + let resultHTML = ` +
+
上传完成!
+

成功上传: ${successCount}张图片

+

上传失败: ${errorCount}张图片

+
+ `; + + if (results.length > 0) { + resultHTML += '
上传成功的图片:
'; + } + + resultDiv.innerHTML = resultHTML; + + // 重置表单 + form.reset(); + document.getElementById('uploadPreviewContainer').innerHTML = ''; + document.getElementById('imageTags').value = '1'; // 重置后设置默认值为1 + } else { + resultDiv.innerHTML = `
所有图片上传失败
`; + } }); + + // 设置默认标签值 + 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 = '
正在搜索,请稍候...
'; 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 = `
+
+ +
${briefInfo.name || '图片'}
${briefInfo.name || '未命名图片'}
-

ID: ${briefInfo.id || '无ID'}

+

类型: ${briefInfo.type || '未指定'}

+

描述: ${briefInfo.description ? (briefInfo.description.length > 30 ? briefInfo.description.substring(0, 30) + '...' : briefInfo.description) : '无描述'}

相似度: ${(item.score * 100).toFixed(2)}%

+ +
+
+ 预览图 +
+
+
+
+
+
+
+
+ +
+
+
与图片对话
+
+
+
+
+
+
+ + +
+
+
+
+
+
+ + + + + + + diff --git a/app/templates/index.html b/app/templates/index.html index 07580bb..a3a9627 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -3,59 +3,65 @@ - 相似图片搜索系统 + 地瓜机器人相似图片搜索及分类识别系统
-

相似图片搜索系统

-

基于百度AI开放平台的相似图片搜索API

+

地瓜机器人相似图片搜索及分类识别demo

+

基于图片相似度进行分类的demo演示

- +
-
图片入库
+
角色录入
- - + +
支持JPG、PNG、BMP格式,最短边至少50px,最长边最大4096px
- + + +
+
+ + +
+
+
- - -
用于关联至本地图库的标识
-
-
- - + +
最多2个标签,用逗号分隔
-
- 预览图 +
+
@@ -77,11 +83,11 @@
- -
可选,用于按标签筛选结果
+ +
可选,用于按标签筛选结果(默认是1,后面可以用来区分机器人,类似对象存储分桶)
- +
+
+ + +
可选,用于按类型筛选结果
+
预览图 @@ -104,7 +115,67 @@
-
搜索结果:0 个匹配项
+
+
+
搜索结果:0 个匹配项
+
+ + +
+
+
+ + +
+
+
+ +
+
+
+
最可信类型分析(这些方法我写的都比较简单,后面可以再优化)
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
分析方法结果类型说明
方法一-基于最高相似度分数选择类型
方法二-基于加权投票选择类型
方法三-基于阈值过滤和加权投票选择类型
方法四-基于多数投票选择类型
方法五-基于加权多数投票选择类型
+
+
+
@@ -119,10 +190,10 @@
图库管理

此功能需要先搜索图片,然后可以对搜索结果进行管理操作。

- 提示:百度相似图片搜索API不提供直接查看所有图库内容的功能,需要通过搜索相似图片来获取图库内容。 + 提示:因为百度相似图片搜索API不提供直接查看所有图库内容的功能,需要通过搜索相似图片来获取图库内容,目前先这样,后面看效果好的话再考虑管理相关的功能。
-

请先在"图片搜索"标签页进行搜索,然后可以对搜索结果进行管理。

+

因为我们用的是百度的api做为支撑的,所以先这样,包括搜索界面不展示图片也是

@@ -144,16 +215,20 @@
- + + +
+
+ + +
+
+
-
- - -
- +
最多2个标签,用逗号分隔
diff --git a/migrate_types_to_mongo.py b/migrate_types_to_mongo.py new file mode 100644 index 0000000..cde1741 --- /dev/null +++ b/migrate_types_to_mongo.py @@ -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() diff --git a/requirements.txt b/requirements.txt index c2b1ee1..d3280d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/run.sh b/run.sh old mode 100644 new mode 100755