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 = ` +
+ + `; + + 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 = ` + + + `; + + 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, '图片签名: ${data.cont_sign}
-日志ID: ${data.log_id}
-正在上传: ${i+1}/${files.length}
+成功: ${successCount}, 失败: ${errorCount}
+成功上传: ${successCount}张图片
+上传失败: ${errorCount}张图片
+
ID: ${briefInfo.id || '无ID'}
+类型: ${briefInfo.type || '未指定'}
+描述: ${briefInfo.description ? (briefInfo.description.length > 30 ? briefInfo.description.substring(0, 30) + '...' : briefInfo.description) : '无描述'}
相似度: ${(item.score * 100).toFixed(2)}%