From 487f3af948b4e5767414abfa446d52e59dba4706 Mon Sep 17 00:00:00 2001 From: eust-w Date: Fri, 11 Apr 2025 12:00:32 +0800 Subject: [PATCH] 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 --- .env | 15 +- __pycache__/mock_baidu_api.cpython-39.pyc | Bin 0 -> 3757 bytes app.py | 177 ++++++++- .../__pycache__/azure_openai.cpython-39.pyc | Bin 0 -> 4339 bytes .../baidu_image_search.cpython-39.pyc | Bin 6090 -> 9783 bytes .../__pycache__/type_manager.cpython-39.pyc | Bin 0 -> 3349 bytes .../type_manager_mongo.cpython-39.pyc | Bin 0 -> 3145 bytes app/api/azure_openai.py | 166 +++++++++ app/api/baidu_image_search.py | 182 ++++++++- app/api/type_manager.py | 116 ++++++ app/api/type_manager_mongo.py | 115 ++++++ app/data/type_descriptions.json | 5 + app/static/css/chat.css | 101 +++++ app/static/css/style.css | 50 ++- app/static/js/chat.js | 220 +++++++++++ app/static/js/main.js | 346 +++++++++++++++--- app/templates/chat.html | 72 ++++ app/templates/index.html | 137 +++++-- migrate_types_to_mongo.py | 54 +++ requirements.txt | 1 + run.sh | 0 21 files changed, 1661 insertions(+), 96 deletions(-) create mode 100644 __pycache__/mock_baidu_api.cpython-39.pyc create mode 100644 app/api/__pycache__/azure_openai.cpython-39.pyc create mode 100644 app/api/__pycache__/type_manager.cpython-39.pyc create mode 100644 app/api/__pycache__/type_manager_mongo.cpython-39.pyc create mode 100644 app/api/azure_openai.py create mode 100644 app/api/type_manager.py create mode 100644 app/api/type_manager_mongo.py create mode 100644 app/data/type_descriptions.json create mode 100644 app/static/css/chat.css create mode 100644 app/static/js/chat.js create mode 100644 app/templates/chat.html create mode 100644 migrate_types_to_mongo.py mode change 100644 => 100755 run.sh 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 0000000000000000000000000000000000000000..84696e3560b9839cf0107a5396cea5074d302345 GIT binary patch literal 3757 zcmd5<-ESOM6~A|8c6N5WaX$J%i`vxf17=ZUuN^08EE5_U6(UgsNn50aYBZTU-b}M! zxieE*Yb+7jBo0m~rA?ILCG4V7VySJ~sv%L5I)4HD0rS8Ecz5j=ctCv#t>BzHyOXuq z5i{o7L0!IXe#$jY}uje*T-*FXo!FKW?17ymED6_2%Mg{Y>-n!s?~7x&^A& zjU=odhw1JEViT}|z5WSdw&NNG#~ z%eRGhKUKDlecg1Jf3Rrg?ZdXot)u=RsBT*>F4bDRx^m+NT&;EbRD3ls8SZ6KknIfQ zC74gZbngSgXh00&5TMkPz-N?_w$HY~c4PjRjo&Ub&YX*W47`#1QV|R?Ov|!e*YL{6 z?2<7ux^ryT?mds~&1UyLwrBUQv7MtM!ooA%8H34;9HzT9hN^>?b;Cr3J0&ke zL&dcVQ%i)aVAQR>!_T-jcZb;yUOrOx9LpX4VVNI0Trn+s*eT{+;Ut?63saAv^9-}% z3{_qVHN$X9j%OHY^Z;rjl1f!N{a`OY4|Sv`Eg_2HFf^D6n8J$~Wkg(o%%~m>)S6T$ zvvejIC_y5SD~|-U@(7ce!V*;_AkWJ|a)K~5NWz&U)2bNdl>bc#j6TpFk*}Lqt~Y*C zi$dC%e-B}M?X_4YX7=;EJ07o4gK(WuF}j-!3sm^Be)$^-womjb9Z=SfcPRcXE+JVOW zNdRV#?O67Z?fJa4;bP3OJh+lbSU}?H&DR@?7ZBvvUu#~d#%I?cAY{@<_(rnb*Pp&? zG&{O$M|NyScJIK*$appjpNtYFg#Dp{28J@+GpCu18J<(LoqK-&{bZPQeF1rpRSV_3 z;jl1el}n!CI{8wl+nkrVVU?L3rXs%R=0R5ReUOx0kLMD|j&Uql8I2=;Lj88E!gRz( zzUK%}j+Kx1A;HV=%}5Z35y3hWrqD`p5=BHxlg%1UD-xBcPNh%M8glUW$@KP~5RVn` z6lhp`6p&~kz&itfA9v7h_X3bSnb82qq(R66LI>XmgO@#6C&r+{-vax&Put}|g*LWu1|m|-su$aDQOG@$e31@bI`fJ=-V zCQEcupnpjUrJ-yngEy9FuAN8dx$fMU$lE3R#R}i|6zKaKOfo&zGY6uE5#`a)M4@b% z1$W<2eEb%D^uMu{-w(eS+4Du@9*mcmx_mp3?;`UMko=SwfBBk7`iv}+JxsV(ncL!# z^ZwC4jz1}Kly8Cq8I||LS5D`)>lZwu02CatrE**Cwnu=-n{kcyeiQ;#ohI^2Rl7!VDOvM-srAjH3cq!M{ zHlyG`sl!6miHTF1Dn~vL*B3=A-R*A?@UOrLvQ?3{NeWF%Yq1~a$wnhJ^t=4%ud7rj+%aX~*81u-km8&l|-Z>BN(Mz&`=xq>oH8kuf0>?`< z>WE4NH__v6_q`3>F6~#C>Df`DGEm`SVv)2i321z(CKcLGVan0^Gl0^q#lphV8OX;# zK}>!IPy|H2ieP{j`8F7m{+n|%`|`t2ZS|K9!G=QD{(9GRr}imdp8FaRE= z@O8ap7Hz`_bpt9I^9#tQ4db|P7TP;W!(e60FgU)oaBLlX2a-`FSY4v>v;Gzw{}aMH oyI9sVsz{ouKdg`F4~d;gaKIisiekOahuTDB&OXG8D}a;uCx;|jWB>pF literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1469b20cbe9974a895b77148da698345982ea0e0 GIT binary patch literal 4339 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 bLVb~ZYfp#Ee+K2)*aVj)@wP8da)kaDD2QwF literal 0 HcmV?d00001 diff --git a/app/api/__pycache__/baidu_image_search.cpython-39.pyc b/app/api/__pycache__/baidu_image_search.cpython-39.pyc index eeafe83b418411a4817613b5c1459052164ce27e..029623ee93c2cb04b72d543d4b5cc20815ea051c 100644 GIT binary patch literal 9783 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 VUzL`xp zyiExZi4kM!GhPUxrO<>62A9YQj~+b8g$tATCnks|NQfS|@%zmRThTo@oB8bf{(duW zet+J3H%EWikSx2dCBgOeZ-0%C=4O)TEFIJva0h#4F6Rf*RHw!n@}5S!s7)Oh>2wKo zX%a>TT}o5bgON$KF{zN5d3?o-+?_l2j=Wvkad2;G-|j<^xJHf;x0#X!A(og|)@TMy z&P;JzDfZ&0BXjqOia!-rkJlnK^iPC^1ar`?)Y&BSM30*FTzn-)5=XWeQIE)vB(b3W zxSrb+4o!V1v!A&=tISDbQs`0-tjY7z1^F~NMW_zJrR#z;ZzTqaNc0{RkMxUVx%k33 zE3>rNZ?0UCVsR{1HK^8tuw1M7kxHwTkY&Ufb8B}Oes&3BDPkGo1wg@Jj`+#!d22aZ zdJwAtkV6QG;vCRIK45utuSKjt{4=S@EK^#l9xIRg_((7)_FF~Ku!{SzUQaJf zAN4M{gFgY3aA`uqOdCd3YM7j`cS6Dml~2o`_cW|fo>jOttIkPtq+yE(`P{V4O{xw{ zwLUI0%H_XRmMe^L1ylljMwp-)){d*}W3GZ#AC`E6Ds{rX;t9|OXf4(YTmx-_)?@uG z*Fjsu5-yl3X*i)ap>uA?u0>;(6V2-J8Q-*vuFye_Oq5L&@!)nmo%5T zn@RJ_q)n!CknVw25#wsF0^O>%8J5ROlgG!al}MhdMKY_gUUc>$`VnbB9b5PN{K@+_ zF5Nr--ThC_-n)GLj~iEp@>iX>`K*Xf4j={*8xZReardX&?a!dGy#|u;dSx%6bt57U z57zi=w3H&mv9Br+MLlW$X06!slj;xB3l@-6F9kDap-Q3dk zz3eQmj?gpjde2ut)H2=gv+XcJ!T!&HU&UztUxd`3^h3WD;in>(9c|9%i^|aASa8yK z=3yAU$+}TL+b92OlQ8>o)3_5mH{(V077%e8*5Rn*_H;XE+79BmoCoqaW#_ikr)c?U z(%(qpPT%3YegH2<@Wm~Ff{MDJA>rQ=WBr4E3cWVsWyBETHN@+P_G!>Q=M-?D3gT@~ lRN2rC&)NjPwG4d)y|rB9LTETkmw4E}__%D%B{FMPKLM(0GZg>; 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 0000000000000000000000000000000000000000..274f0d362fd09dabbb38c834c6f8f67a72d3c3c7 GIT binary patch literal 3349 zcmcIm-EZ4e6u;MY5;y7RnD)U2jIjp-tnH>fKvl*NP|ySc(LuFplI7;Qozv6~*Y-xM z3;`?9eRQp0lznugk)~}$`zQ#pYGqX7Kj77`)M@*~BhsEY$8NkN?WBQ(BOf2%d(O?h zzjMy-Sgosz6SxM?{M~n?mXJU3rTh}%Wgpzub08RDG)uaJC+(*GyU;DXNtnRIVZy}Y zLbu3hCrL`nU{6wMl;u~r-hLR>2(HmBFbZBxOk~n9?Uq=CMTbdugvFQ)&nT;5ad^g9 zg4M!PW_7F{o;9q2HNrE_1o*#oZsNdTPJ2f+)INT_x0)G*x1ti%))%3_}OiLfmB*3+kXUf8SdBM zwtfeZr;Iv;x7#E`rvzV$wp5@6dMtPo@&f3hKNHD|2vkAL6D4Xy^Aw*j6AQfaBoi%A z1^|o-l=x$EFlPN=*PzCt2+F89w*8o7$1@3?tc-MX?cdsofnTF|(~hM*}-v(jA8 z*}7?1%^Axy!bMZAgl0n<*0wv4q^_)etXsBX_Pg}x{fJ}t?tF0?M~$?}bfd3h8RIuC zxV4&LIb2gzE3NBpR5zGr*vTeWFfCWgsrF$v#&m9|1DYFCdn|uMR{YZ_iYsMI-EeEd z1G@5n+OL6w3&zr zf=otS$j`q2kisX~Lg|`TMW5d|+wZYGFIM*<;#z+G;s! z^8U`8n$}wMfj&!9dHS%%TM%V6rw1Sp1f|iO8+74Vx}n>OQU}Xf8$ghlEUXu5;f{)% z#Cp1pwu*6aXnk2tHJ6;y6dhJB1h)omzaV-*8QNz2-dCO4YeL|boiq%N4%3$OZV;t z`4$d_%KMt_aHBd;^|al#!VOhLzj$HTyZN2>#ca5OOF56iAMzN8R5HpDFh5d!BWM^w zmAMW3JKs&cu0hgiZ*XpMx2Xf_AZ6MereiRFXjb2ubNJSfnZfJ1_znAf1c^bmjBw3SGoXvGD1ltvnJe21v^eZa73@ zVg8bL^IR3u969j;Kph;@f#We!$w(Ded=t(+j^bGql^{U9Sn3g=1E`mmVA_&E5NQ<( zp-~#UN$Em8L}4$yzVyV)uY7Y^^vub}-R#VUh6c#XED5 zXvL9<;>Dkf6EhEQ7|4U6*J_INi40jL%)jx`#cMO6hfkvq_z;M+x+9?lUZKmHDc)n6 zS#bVlbm87)@!V~1{AzJz+&lSw=#@G!E%GPeh(SD(pe02=L4l-@m3=BqlyP%)xMY4Z!Ll>MsU@+Y6(wRHnSg9pl z7+w>U(D=Wh9=flIr)BfXSU@}Tu6|y;JQ<|I!t5y227Fd{Bj=aq&w3XxFHYYDCRx03 zbz%0WaBUY(<^%4EeVScjgy-R?)#x_Be0gVl4{R(%jl_qxl(RyW-71vBqWBPe{i0}x zqJVFY3_U{l?w~kcerR!26pQE=%Ui38MRF!_V?ko#H^kKP`6~f1;b5rzAK`g~bnoE! z4a-|mR1r^tYXc@Q4<_>=n780l7>*MpvrHT5(8hAa%d9Ji7#kx-$j@nHz#yj!(7KY; z`2?3NAr$0u@x-Ua$rF#rC2OW7`!}7elzLLy%nc@K(2c5~sKazpvsoVtYXJ5fBwcK1 zb2$VRR!N#lCh)5QDFZ&1;)*h0GAE0At)hJ7sM(d7n4&N#eB9JQK6C)e@d>CV{)ThD>reRO-dZUYW&vBx1ffjYc1x=?ZGbh OA`e;E;$`42S@;J{eyOJb literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cc15438b93f16ea10097bda47876e3faf41d4382 GIT binary patch literal 3145 zcmcIm?QaxC7@wKl+itH{I1mvGA&2pUmnetC1k)IUP()2y5?&Is(X7{PrHVYQOlw#Q$Kw+P$Nn{Aglg;`7Y*HrE$EXyPU_JF`2_ zJkR`o&&!m%yR!tI-!A?+xIIV6?>K3H6qsy>=Di3VC!9LOz&CSfl^KlUSaFo9YN(Xh z>M=seZo*ZbIZk-y7&9`Q?juEQ4x~wckw#s8Zf(%r=~V2Rk5k7jbCbKiU3=HUHSG_B z$#!VoYtY%mpqvedStt&=%61-~~`H`_oMG+)X*}-O;==*&P2kzVvhR)XCr=Zcz|yJqVQwZ6`GEP3U|wK{@4Yl8sO~R=5gd=ChC56B^ehDJ6af?+Do?a)lj(8Q0+moxxhzJ|4Xi-}p2>ePQ0_ zhcj0jXRjHZeS7+M?=cVTeSP@JDuT@idP*h7waU&R*YkU}Zrk$wwvy4EEbHB2_P^4% z%jl|92R*witV6aiR`~#s$!*UPmAYSXYhI+{r(g@Ho!3@q1F?{r65l!97#;s(`gCGU zJUUhAFX|CMRvSpXrj#ety z>(DACdBSSq1E$<}bNy(fo1nSko8}7G)WeP_Ihs?l6#E<-Uf*_V3B=}>goX6C9(02Y zH0cTmT1bT+GB1bG0OXP(aEp*uCl{%PEM5Pve! z7`@QA^h4vqRN|6}Kc;Ka!7F0W>q%$0gFG$b)l-=F=J7Ml8&eC`w9(yKwMfCM0?#z8 zKJD3l)YZ0re~}tmPzMHpEP)Bd$Od(eM7lNG$d#?bHTONo=7Tmbssi#1i8@o8L>}jw zu)VMz~%W%??vC!9A&w}uL7QTkm(_4H% zph`X_=?Dv%cqU{MkYhNV1TJI-Q-fbYBn9l@*?aNWh5v(wRK;G~4@7Mdk)AIWk^`X@ zU;n6a^>Sj%?EN$G?JwfXH{;QZ4`^y*xiq0w?htKdu=93I2 z38+&Q->xpkuN$4N>^KrYZ$C&S2zID+1zk&rH!eYNfxrSEMf_3>H^YQu%AgZOBDmo# zHlmC`EuWyF5<;~XIY5zZBO~kxi~vO?_yvU0^UXdu`^LUK{f+ZejWc)RyVLQBYyUQ@ z40&$Ngk)RpDafrRrsL_cL;o0hP&SZaZ@qhXl;Wer9LLPr<*mb=4M3Z0q-XmWhw`?=3=%LLWCBe}Zt7n#5? z1KSjDO*6`xaOdEGgX5fO9tq0M z{GSdJZo`&oij}aRgi}0$qjlJAz^;HD>TLs2T7~sepdDS%WT}Kg$@ILwMqdqmPS4A~ qMNMe1!04zSmG_?c3t4gp7g#0bvgLM<^p_H|Uq&@DhIDx|(%3)m=6{F) literal 0 HcmV?d00001 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 += '
上传成功的图片:
    '; + results.forEach((result, index) => { + resultHTML += `
  • 图片${index+1} - 签名: ${result.cont_sign}
  • `; + }); + 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