code init
This commit is contained in:
commit
9c2229fa47
2
.env
Normal file
2
.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
BAIDU_API_KEY=ALTAKmzKDy1OqhqmepD2OeXqbN
|
||||||
|
BAIDU_SECRET_KEY=b79c5fbc26344868916ec6e9e2ff65f0
|
||||||
99
README.md
Normal file
99
README.md
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# 相似图片搜索系统
|
||||||
|
|
||||||
|
这是一个基于百度AI开放平台的相似图片搜索API的封装系统,提供了图片入库、检索、删除和更新等功能,并配有简洁美观的Web界面。
|
||||||
|
|
||||||
|
## 功能特点
|
||||||
|
|
||||||
|
- **图片入库**:将图片添加到百度相似图片搜索库中,支持添加名称、ID和标签
|
||||||
|
- **图片检索**:上传图片搜索相似的图片,支持标签过滤
|
||||||
|
- **图库管理**:更新和删除图库中的图片信息
|
||||||
|
- **直观界面**:简洁美观的Web界面,操作便捷
|
||||||
|
|
||||||
|
## 安装与使用
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- Python 3.9+
|
||||||
|
- Conda
|
||||||
|
- Poetry
|
||||||
|
|
||||||
|
### 安装步骤
|
||||||
|
|
||||||
|
1. 克隆或下载本项目到本地
|
||||||
|
|
||||||
|
2. 使用Conda创建虚拟环境:
|
||||||
|
```bash
|
||||||
|
conda env create -f environment.yml
|
||||||
|
conda activate imgsearcher
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 使用Poetry安装依赖:
|
||||||
|
```bash
|
||||||
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 配置API密钥:
|
||||||
|
在项目根目录创建`.env`文件,填入百度AI平台的API密钥:
|
||||||
|
```
|
||||||
|
BAIDU_API_KEY=你的API_KEY
|
||||||
|
BAIDU_SECRET_KEY=你的SECRET_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 创建上传目录:
|
||||||
|
```bash
|
||||||
|
mkdir uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
6. 运行应用:
|
||||||
|
```bash
|
||||||
|
poetry run python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
7. 在浏览器中访问:
|
||||||
|
```
|
||||||
|
http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 图片入库
|
||||||
|
|
||||||
|
1. 在"图片入库"标签页中,选择要上传的图片
|
||||||
|
2. 填写图片名称和ID(用于后续识别)
|
||||||
|
3. 可选择添加标签(最多2个,用逗号分隔)
|
||||||
|
4. 点击"上传入库"按钮
|
||||||
|
|
||||||
|
### 图片搜索
|
||||||
|
|
||||||
|
1. 在"图片搜索"标签页中,选择要搜索的图片
|
||||||
|
2. 可选择添加标签过滤条件
|
||||||
|
3. 选择标签逻辑(AND或OR)
|
||||||
|
4. 点击"搜索"按钮
|
||||||
|
5. 查看搜索结果
|
||||||
|
|
||||||
|
### 图库管理
|
||||||
|
|
||||||
|
1. 先在"图片搜索"标签页进行搜索
|
||||||
|
2. 在搜索结果中,可以对图片进行编辑或删除操作
|
||||||
|
|
||||||
|
## API说明
|
||||||
|
|
||||||
|
本系统封装了百度相似图片搜索API的以下功能:
|
||||||
|
|
||||||
|
- 图片入库:`/upload` (POST)
|
||||||
|
- 图片检索:`/search` (POST)
|
||||||
|
- 图片删除:`/delete` (POST)
|
||||||
|
- 图片更新:`/update` (POST)
|
||||||
|
- 获取Token:`/api/token` (GET)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 上传的图片最短边至少50px,最长边最大4096px
|
||||||
|
- 支持JPG、PNG、BMP格式的图片
|
||||||
|
- 每个图片可以添加最多2个标签
|
||||||
|
- 检索接口不返回原图,只返回入库时填写的brief信息
|
||||||
|
- 图片删除可能会有延时生效(百度API特性)
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
165
app.py
Normal file
165
app.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
||||||
|
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
|
||||||
|
|
||||||
|
app = Flask(__name__, template_folder='app/templates', static_folder='app/static')
|
||||||
|
CORS(app) # 启用CORS支持跨域请求
|
||||||
|
|
||||||
|
# 配置上传文件夹
|
||||||
|
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
||||||
|
if not os.path.exists(UPLOAD_FOLDER):
|
||||||
|
os.makedirs(UPLOAD_FOLDER)
|
||||||
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||||
|
|
||||||
|
# 允许的文件扩展名
|
||||||
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
|
|
||||||
|
# 初始化百度图像搜索API
|
||||||
|
image_search_api = BaiduImageSearch()
|
||||||
|
|
||||||
|
def allowed_file(filename):
|
||||||
|
"""检查文件扩展名是否允许"""
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""首页"""
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
@app.route('/upload', methods=['POST'])
|
||||||
|
def upload_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
|
||||||
|
|
||||||
|
# 获取表单数据
|
||||||
|
name = request.form.get('name', '')
|
||||||
|
image_id = request.form.get('id', '')
|
||||||
|
tags = request.form.get('tags', '')
|
||||||
|
|
||||||
|
# 创建brief信息
|
||||||
|
brief = {
|
||||||
|
'name': name,
|
||||||
|
'id': image_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用API添加图片
|
||||||
|
result = image_search_api.add_image(
|
||||||
|
image_path=file_path,
|
||||||
|
brief=brief,
|
||||||
|
tags=tags
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加本地存储路径到结果
|
||||||
|
result['file_path'] = file_path
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/search', methods=['POST'])
|
||||||
|
def search_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
|
||||||
|
|
||||||
|
# 获取表单数据
|
||||||
|
tags = request.form.get('tags', '')
|
||||||
|
tag_logic = request.form.get('tag_logic', '0')
|
||||||
|
|
||||||
|
# 保存文件
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用API搜索图片
|
||||||
|
result = image_search_api.search_image(
|
||||||
|
image_path=file_path,
|
||||||
|
tags=tags,
|
||||||
|
tag_logic=tag_logic
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/delete', methods=['POST'])
|
||||||
|
def delete_image():
|
||||||
|
"""删除图库中的图片"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data or 'cont_sign' not in data:
|
||||||
|
return jsonify({'error': '缺少必要参数'}), 400
|
||||||
|
|
||||||
|
cont_sign = data['cont_sign']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用API删除图片
|
||||||
|
result = image_search_api.delete_image(cont_sign=cont_sign)
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/update', methods=['POST'])
|
||||||
|
def update_image():
|
||||||
|
"""更新图库中的图片信息"""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data or 'cont_sign' not in data:
|
||||||
|
return jsonify({'error': '缺少必要参数'}), 400
|
||||||
|
|
||||||
|
cont_sign = data['cont_sign']
|
||||||
|
brief = data.get('brief')
|
||||||
|
tags = data.get('tags')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用API更新图片
|
||||||
|
result = image_search_api.update_image(
|
||||||
|
cont_sign=cont_sign,
|
||||||
|
brief=brief,
|
||||||
|
tags=tags
|
||||||
|
)
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/token')
|
||||||
|
def get_token():
|
||||||
|
"""获取API访问令牌"""
|
||||||
|
try:
|
||||||
|
token = image_search_api.get_access_token()
|
||||||
|
return jsonify({'access_token': token})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# 初始化应用包
|
||||||
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/baidu_image_search.cpython-39.pyc
Normal file
BIN
app/api/__pycache__/baidu_image_search.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/image_utils.cpython-39.pyc
Normal file
BIN
app/api/__pycache__/image_utils.cpython-39.pyc
Normal file
Binary file not shown.
226
app/api/baidu_image_search.py
Normal file
226
app/api/baidu_image_search.py
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class BaiduImageSearch:
|
||||||
|
"""百度相似图片搜索API封装类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化,获取API密钥"""
|
||||||
|
self.api_key = os.getenv('BAIDU_API_KEY')
|
||||||
|
self.secret_key = os.getenv('BAIDU_SECRET_KEY')
|
||||||
|
self.access_token = None
|
||||||
|
self.get_access_token()
|
||||||
|
|
||||||
|
def get_access_token(self):
|
||||||
|
"""获取百度API的access_token"""
|
||||||
|
url = "https://aip.baidubce.com/oauth/2.0/token"
|
||||||
|
params = {
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": self.api_key,
|
||||||
|
"client_secret": self.secret_key
|
||||||
|
}
|
||||||
|
response = requests.post(url, params=params)
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
self.access_token = result.get('access_token')
|
||||||
|
return self.access_token
|
||||||
|
else:
|
||||||
|
raise Exception(f"获取access_token失败: {response.text}")
|
||||||
|
|
||||||
|
def add_image(self, image_path=None, image_base64=None, url=None, brief=None, tags=None):
|
||||||
|
"""
|
||||||
|
添加图片到图库
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 本地图片路径
|
||||||
|
image_base64: 图片的base64编码
|
||||||
|
url: 图片URL
|
||||||
|
brief: 图片摘要信息,最长256B,如:{"name":"图片名称", "id":"123"}
|
||||||
|
tags: 分类信息,最多2个tag,如:"1,2"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: API返回的结果
|
||||||
|
"""
|
||||||
|
request_url = f"https://aip.baidubce.com/rest/2.0/image-classify/v1/realtime_search/similar/add?access_token={self.access_token}"
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
# 设置brief信息(必选)
|
||||||
|
if brief:
|
||||||
|
if isinstance(brief, dict):
|
||||||
|
brief = json.dumps(brief, ensure_ascii=False)
|
||||||
|
params['brief'] = brief
|
||||||
|
else:
|
||||||
|
raise ValueError("brief参数是必须的")
|
||||||
|
|
||||||
|
# 设置图片信息(三选一)
|
||||||
|
if image_path:
|
||||||
|
with open(image_path, 'rb') as f:
|
||||||
|
image = base64.b64encode(f.read())
|
||||||
|
params['image'] = image
|
||||||
|
elif image_base64:
|
||||||
|
params['image'] = image_base64
|
||||||
|
elif url:
|
||||||
|
params['url'] = url
|
||||||
|
else:
|
||||||
|
raise ValueError("必须提供image_path、image_base64或url其中之一")
|
||||||
|
|
||||||
|
# 设置可选的tags
|
||||||
|
if tags:
|
||||||
|
params['tags'] = tags
|
||||||
|
|
||||||
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||||
|
response = requests.post(request_url, data=params, headers=headers)
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
检索相似图片
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 本地图片路径
|
||||||
|
image_base64: 图片的base64编码
|
||||||
|
url: 图片URL
|
||||||
|
tags: 分类信息过滤,如:"1,2"
|
||||||
|
tag_logic: 标签逻辑,0表示逻辑and,1表示逻辑or
|
||||||
|
pn: 分页起始位置,默认0
|
||||||
|
rn: 返回结果数量,默认300,最大1000
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: API返回的结果
|
||||||
|
"""
|
||||||
|
request_url = f"https://aip.baidubce.com/rest/2.0/image-classify/v1/realtime_search/similar/search?access_token={self.access_token}"
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
# 设置图片信息(三选一)
|
||||||
|
if image_path:
|
||||||
|
with open(image_path, 'rb') as f:
|
||||||
|
image = base64.b64encode(f.read())
|
||||||
|
params['image'] = image
|
||||||
|
elif image_base64:
|
||||||
|
params['image'] = image_base64
|
||||||
|
elif url:
|
||||||
|
params['url'] = url
|
||||||
|
else:
|
||||||
|
raise ValueError("必须提供image_path、image_base64或url其中之一")
|
||||||
|
|
||||||
|
# 设置可选参数
|
||||||
|
if tags:
|
||||||
|
params['tags'] = tags
|
||||||
|
if tag_logic is not None:
|
||||||
|
params['tag_logic'] = tag_logic
|
||||||
|
if pn is not None:
|
||||||
|
params['pn'] = pn
|
||||||
|
if rn is not None:
|
||||||
|
params['rn'] = rn
|
||||||
|
|
||||||
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||||
|
response = requests.post(request_url, data=params, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"检索图片失败: {response.text}")
|
||||||
|
|
||||||
|
def delete_image(self, image_path=None, image_base64=None, url=None, cont_sign=None):
|
||||||
|
"""
|
||||||
|
删除图库中的图片
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 本地图片路径
|
||||||
|
image_base64: 图片的base64编码
|
||||||
|
url: 图片URL
|
||||||
|
cont_sign: 图片签名,支持批量删除,格式如:"932301884,1068006219;316336521,553141152"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: API返回的结果
|
||||||
|
"""
|
||||||
|
request_url = f"https://aip.baidubce.com/rest/2.0/image-classify/v1/realtime_search/similar/delete?access_token={self.access_token}"
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
# 设置图片信息(三选一)
|
||||||
|
if image_path:
|
||||||
|
with open(image_path, 'rb') as f:
|
||||||
|
image = base64.b64encode(f.read())
|
||||||
|
params['image'] = image
|
||||||
|
elif image_base64:
|
||||||
|
params['image'] = image_base64
|
||||||
|
elif url:
|
||||||
|
params['url'] = url
|
||||||
|
elif cont_sign:
|
||||||
|
params['cont_sign'] = cont_sign
|
||||||
|
else:
|
||||||
|
raise ValueError("必须提供image_path、image_base64、url或cont_sign其中之一")
|
||||||
|
|
||||||
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||||
|
response = requests.post(request_url, data=params, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"删除图片失败: {response.text}")
|
||||||
|
|
||||||
|
def update_image(self, image_path=None, image_base64=None, url=None, cont_sign=None, brief=None, tags=None):
|
||||||
|
"""
|
||||||
|
更新图库中图片的摘要和分类信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 本地图片路径
|
||||||
|
image_base64: 图片的base64编码
|
||||||
|
url: 图片URL
|
||||||
|
cont_sign: 图片签名
|
||||||
|
brief: 更新的摘要信息,最长256B
|
||||||
|
tags: 更新的分类信息,最多2个tag,如:"1,2"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: API返回的结果
|
||||||
|
"""
|
||||||
|
request_url = f"https://aip.baidubce.com/rest/2.0/image-classify/v1/realtime_search/similar/update?access_token={self.access_token}"
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
# 设置图片信息(三选一)
|
||||||
|
if image_path:
|
||||||
|
with open(image_path, 'rb') as f:
|
||||||
|
image = base64.b64encode(f.read())
|
||||||
|
params['image'] = image
|
||||||
|
elif image_base64:
|
||||||
|
params['image'] = image_base64
|
||||||
|
elif url:
|
||||||
|
params['url'] = url
|
||||||
|
elif cont_sign:
|
||||||
|
params['cont_sign'] = cont_sign
|
||||||
|
else:
|
||||||
|
raise ValueError("必须提供image_path、image_base64、url或cont_sign其中之一")
|
||||||
|
|
||||||
|
# 设置更新信息
|
||||||
|
if brief:
|
||||||
|
if isinstance(brief, dict):
|
||||||
|
brief = json.dumps(brief, ensure_ascii=False)
|
||||||
|
params['brief'] = brief
|
||||||
|
|
||||||
|
if tags:
|
||||||
|
params['tags'] = tags
|
||||||
|
|
||||||
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||||
|
response = requests.post(request_url, data=params, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise Exception(f"更新图片失败: {response.text}")
|
||||||
74
app/api/image_utils.py
Normal file
74
app/api/image_utils.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
class ImageUtils:
|
||||||
|
"""图片处理工具类"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resize_image(image_path, max_size=(1024, 1024)):
|
||||||
|
"""
|
||||||
|
调整图片大小,确保不超过最大尺寸
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 图片路径
|
||||||
|
max_size: 最大尺寸 (宽, 高)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image: 调整大小后的图片对象
|
||||||
|
"""
|
||||||
|
img = Image.open(image_path)
|
||||||
|
img.thumbnail(max_size, Image.LANCZOS)
|
||||||
|
return img
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def convert_to_base64(image, format='JPEG'):
|
||||||
|
"""
|
||||||
|
将PIL图片对象转换为base64编码
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: PIL图片对象
|
||||||
|
format: 图片格式,如'JPEG', 'PNG'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: base64编码的图片数据
|
||||||
|
"""
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
image.save(buffer, format=format)
|
||||||
|
img_str = base64.b64encode(buffer.getvalue())
|
||||||
|
return img_str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def image_to_base64(image_path, max_size=(1024, 1024), format='JPEG'):
|
||||||
|
"""
|
||||||
|
将图片文件转换为base64编码,并可选调整大小
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 图片路径
|
||||||
|
max_size: 最大尺寸 (宽, 高)
|
||||||
|
format: 图片格式,如'JPEG', 'PNG'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: base64编码的图片数据
|
||||||
|
"""
|
||||||
|
img = ImageUtils.resize_image(image_path, max_size)
|
||||||
|
return ImageUtils.convert_to_base64(img, format)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def base64_to_image(base64_str):
|
||||||
|
"""
|
||||||
|
将base64编码转换为PIL图片对象
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base64_str: base64编码的图片数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image: 图片对象
|
||||||
|
"""
|
||||||
|
img_data = base64.b64decode(base64_str)
|
||||||
|
buffer = io.BytesIO(img_data)
|
||||||
|
img = Image.open(buffer)
|
||||||
|
return img
|
||||||
160
app/static/css/style.css
Normal file
160
app/static/css/style.css
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/* 全局样式 */
|
||||||
|
body {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题样式 */
|
||||||
|
header h1 {
|
||||||
|
color: #343a40;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .lead {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 */
|
||||||
|
.card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: #343a40;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单样式 */
|
||||||
|
.form-control {
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.btn {
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
border-color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #0b5ed7;
|
||||||
|
border-color: #0a58ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片预览 */
|
||||||
|
.img-preview {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-preview-container,
|
||||||
|
.search-preview-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索结果样式 */
|
||||||
|
.result-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-img-container {
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-info {
|
||||||
|
padding: 15px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-score {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签页样式 */
|
||||||
|
.nav-tabs .nav-link {
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs .nav-link.active {
|
||||||
|
color: #0d6efd;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.spinner-border {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.result-item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-img-container {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
app/static/img/placeholder.png
Normal file
1
app/static/img/placeholder.png
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
282
app/static/js/main.js
Normal file
282
app/static/js/main.js
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
// 页面加载完成后执行
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// 初始化图片预览功能
|
||||||
|
initImagePreview('uploadFile', 'uploadPreview');
|
||||||
|
initImagePreview('searchFile', 'searchPreview');
|
||||||
|
|
||||||
|
// 初始化表单提交事件
|
||||||
|
initUploadForm();
|
||||||
|
initSearchForm();
|
||||||
|
initUpdateForm();
|
||||||
|
initDeleteForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化图片预览功能
|
||||||
|
function initImagePreview(inputId, previewId) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const preview = document.getElementById(previewId);
|
||||||
|
|
||||||
|
if (input && preview) {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化上传表单
|
||||||
|
function initUploadForm() {
|
||||||
|
const form = document.getElementById('uploadForm');
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const resultDiv = document.getElementById('uploadResult');
|
||||||
|
|
||||||
|
resultDiv.innerHTML = '<div class="alert alert-info"><div class="spinner-border text-primary" role="status"></div>正在上传,请稍候...</div>';
|
||||||
|
|
||||||
|
fetch('/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<h5>上传成功!</h5>
|
||||||
|
<p>图片签名: ${data.cont_sign}</p>
|
||||||
|
<p>日志ID: ${data.log_id}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
// 重置表单
|
||||||
|
form.reset();
|
||||||
|
document.getElementById('uploadPreview').classList.add('d-none');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger">上传失败: ${error.message}</div>`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化搜索表单
|
||||||
|
function initSearchForm() {
|
||||||
|
const form = document.getElementById('searchForm');
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const resultDiv = document.getElementById('searchResult');
|
||||||
|
const resultList = document.getElementById('searchResultList');
|
||||||
|
const resultStats = document.getElementById('searchStats');
|
||||||
|
const resultCount = document.getElementById('resultCount');
|
||||||
|
|
||||||
|
resultList.innerHTML = '<div class="col-12 text-center"><div class="spinner-border text-primary" role="status"></div> 正在搜索,请稍候...</div>';
|
||||||
|
resultStats.classList.add('d-none');
|
||||||
|
|
||||||
|
fetch('/search', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
resultList.innerHTML = `<div class="col-12"><div class="alert alert-danger">${data.error}</div></div>`;
|
||||||
|
} else {
|
||||||
|
// 显示搜索结果统计
|
||||||
|
resultCount.textContent = data.result_num || 0;
|
||||||
|
resultStats.classList.remove('d-none');
|
||||||
|
|
||||||
|
// 清空结果列表
|
||||||
|
resultList.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.result && data.result.length > 0) {
|
||||||
|
// 显示搜索结果
|
||||||
|
data.result.forEach(item => {
|
||||||
|
// 解析brief信息
|
||||||
|
let briefInfo = {};
|
||||||
|
try {
|
||||||
|
briefInfo = JSON.parse(item.brief);
|
||||||
|
} catch (e) {
|
||||||
|
briefInfo = { name: '未知', id: '未知' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultItem = document.createElement('div');
|
||||||
|
resultItem.className = 'col-md-4 col-sm-6 result-item';
|
||||||
|
resultItem.innerHTML = `
|
||||||
|
<div class="card result-card">
|
||||||
|
<div class="result-img-container">
|
||||||
|
<img class="result-img" src="/static/img/placeholder.png" alt="${briefInfo.name || '图片'}">
|
||||||
|
</div>
|
||||||
|
<div class="result-info">
|
||||||
|
<h5>${briefInfo.name || '未命名图片'}</h5>
|
||||||
|
<p>ID: ${briefInfo.id || '无ID'}</p>
|
||||||
|
<p>相似度: <span class="result-score">${(item.score * 100).toFixed(2)}%</span></p>
|
||||||
|
<div class="result-actions">
|
||||||
|
<button class="btn btn-sm btn-outline-primary update-btn"
|
||||||
|
data-cont-sign="${item.cont_sign}"
|
||||||
|
data-name="${briefInfo.name || ''}"
|
||||||
|
data-id="${briefInfo.id || ''}"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#updateModal">
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger delete-btn"
|
||||||
|
data-cont-sign="${item.cont_sign}"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteModal">
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
resultList.appendChild(resultItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化编辑和删除按钮事件
|
||||||
|
initResultButtons();
|
||||||
|
} else {
|
||||||
|
resultList.innerHTML = '<div class="col-12"><div class="alert alert-warning">没有找到匹配的图片</div></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
resultList.innerHTML = `<div class="col-12"><div class="alert alert-danger">搜索失败: ${error.message}</div></div>`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化搜索结果中的按钮事件
|
||||||
|
function initResultButtons() {
|
||||||
|
// 更新按钮点击事件
|
||||||
|
document.querySelectorAll('.update-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const contSign = this.getAttribute('data-cont-sign');
|
||||||
|
const name = this.getAttribute('data-name');
|
||||||
|
const id = this.getAttribute('data-id');
|
||||||
|
|
||||||
|
document.getElementById('updateContSign').value = contSign;
|
||||||
|
document.getElementById('updateName').value = name;
|
||||||
|
document.getElementById('updateId').value = id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除按钮点击事件
|
||||||
|
document.querySelectorAll('.delete-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const contSign = this.getAttribute('data-cont-sign');
|
||||||
|
document.getElementById('deleteContSign').value = contSign;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化更新表单
|
||||||
|
function initUpdateForm() {
|
||||||
|
const updateButton = document.getElementById('updateSubmit');
|
||||||
|
|
||||||
|
if (updateButton) {
|
||||||
|
updateButton.addEventListener('click', function() {
|
||||||
|
const contSign = document.getElementById('updateContSign').value;
|
||||||
|
const name = document.getElementById('updateName').value;
|
||||||
|
const id = document.getElementById('updateId').value;
|
||||||
|
const tags = document.getElementById('updateTags').value;
|
||||||
|
|
||||||
|
// 创建brief信息
|
||||||
|
const brief = {
|
||||||
|
name: name,
|
||||||
|
id: id
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送更新请求
|
||||||
|
fetch('/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
cont_sign: contSign,
|
||||||
|
brief: brief,
|
||||||
|
tags: tags
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// 关闭模态框
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('updateModal'));
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert(`更新失败: ${data.error}`);
|
||||||
|
} else {
|
||||||
|
alert('更新成功!');
|
||||||
|
// 如果当前在搜索标签页,刷新搜索结果
|
||||||
|
if (document.getElementById('search-tab').classList.contains('active')) {
|
||||||
|
document.getElementById('searchForm').dispatchEvent(new Event('submit'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(`更新失败: ${error.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化删除表单
|
||||||
|
function initDeleteForm() {
|
||||||
|
const deleteButton = document.getElementById('deleteSubmit');
|
||||||
|
|
||||||
|
if (deleteButton) {
|
||||||
|
deleteButton.addEventListener('click', function() {
|
||||||
|
const contSign = document.getElementById('deleteContSign').value;
|
||||||
|
|
||||||
|
// 发送删除请求
|
||||||
|
fetch('/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
cont_sign: contSign
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// 关闭模态框
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteModal'));
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert(`删除失败: ${data.error}`);
|
||||||
|
} else {
|
||||||
|
alert('删除成功!');
|
||||||
|
// 如果当前在搜索标签页,刷新搜索结果
|
||||||
|
if (document.getElementById('search-tab').classList.contains('active')) {
|
||||||
|
document.getElementById('searchForm').dispatchEvent(new Event('submit'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(`删除失败: ${error.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
192
app/templates/index.html
Normal file
192
app/templates/index.html
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>相似图片搜索系统</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="text-center my-5">
|
||||||
|
<h1>相似图片搜索系统</h1>
|
||||||
|
<p class="lead">基于百度AI开放平台的相似图片搜索API</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-4">
|
||||||
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button" role="tab" aria-controls="upload" aria-selected="true">图片入库</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="search-tab" data-bs-toggle="tab" data-bs-target="#search" type="button" role="tab" aria-controls="search" aria-selected="false">图片搜索</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage" type="button" role="tab" aria-controls="manage" aria-selected="false">图库管理</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content" id="myTabContent">
|
||||||
|
<!-- 图片入库 -->
|
||||||
|
<div class="tab-pane fade show active" id="upload" role="tabpanel" aria-labelledby="upload-tab">
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">图片入库</h5>
|
||||||
|
<form id="uploadForm" enctype="multipart/form-data">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="uploadFile" class="form-label">选择图片</label>
|
||||||
|
<input class="form-control" type="file" id="uploadFile" name="file" accept="image/*" required>
|
||||||
|
<div class="form-text">支持JPG、PNG、BMP格式,最短边至少50px,最长边最大4096px</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="imageName" class="form-label">图片名称</label>
|
||||||
|
<input type="text" class="form-control" id="imageName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="imageId" class="form-label">图片ID</label>
|
||||||
|
<input type="text" class="form-control" id="imageId" name="id" required>
|
||||||
|
<div class="form-text">用于关联至本地图库的标识</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="imageTags" class="form-label">标签</label>
|
||||||
|
<input type="text" class="form-control" id="imageTags" name="tags" placeholder="例如:1,2">
|
||||||
|
<div class="form-text">最多2个标签,用逗号分隔</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="upload-preview-container">
|
||||||
|
<img id="uploadPreview" class="img-preview d-none" src="#" alt="预览图">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">上传入库</button>
|
||||||
|
</form>
|
||||||
|
<div id="uploadResult" class="mt-3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片搜索 -->
|
||||||
|
<div class="tab-pane fade" id="search" role="tabpanel" aria-labelledby="search-tab">
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">图片搜索</h5>
|
||||||
|
<form id="searchForm" enctype="multipart/form-data">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="searchFile" class="form-label">选择图片</label>
|
||||||
|
<input class="form-control" type="file" id="searchFile" name="file" accept="image/*" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="searchTags" class="form-label">标签过滤</label>
|
||||||
|
<input type="text" class="form-control" id="searchTags" name="tags" placeholder="例如:1,2">
|
||||||
|
<div class="form-text">可选,用于按标签筛选结果</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">标签逻辑</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="tag_logic" id="tagLogicAnd" value="0" checked>
|
||||||
|
<label class="form-check-label" for="tagLogicAnd">
|
||||||
|
AND(同时满足所有标签)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="tag_logic" id="tagLogicOr" value="1">
|
||||||
|
<label class="form-check-label" for="tagLogicOr">
|
||||||
|
OR(满足任一标签)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="search-preview-container">
|
||||||
|
<img id="searchPreview" class="img-preview d-none" src="#" alt="预览图">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">搜索</button>
|
||||||
|
</form>
|
||||||
|
<div id="searchResult" class="mt-3">
|
||||||
|
<div id="searchStats" class="mb-3 d-none">
|
||||||
|
<h6>搜索结果:<span id="resultCount">0</span> 个匹配项</h6>
|
||||||
|
</div>
|
||||||
|
<div id="searchResultList" class="row"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图库管理 -->
|
||||||
|
<div class="tab-pane fade" id="manage" role="tabpanel" aria-labelledby="manage-tab">
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">图库管理</h5>
|
||||||
|
<p>此功能需要先搜索图片,然后可以对搜索结果进行管理操作。</p>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
提示:百度相似图片搜索API不提供直接查看所有图库内容的功能,需要通过搜索相似图片来获取图库内容。
|
||||||
|
</div>
|
||||||
|
<div id="manageInfo" class="mt-3">
|
||||||
|
<p>请先在"图片搜索"标签页进行搜索,然后可以对搜索结果进行管理。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 更新图片信息模态框 -->
|
||||||
|
<div class="modal fade" id="updateModal" tabindex="-1" aria-labelledby="updateModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="updateModalLabel">更新图片信息</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="updateForm">
|
||||||
|
<input type="hidden" id="updateContSign" name="cont_sign">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="updateName" class="form-label">图片名称</label>
|
||||||
|
<input type="text" class="form-control" id="updateName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="updateId" class="form-label">图片ID</label>
|
||||||
|
<input type="text" class="form-control" id="updateId" name="id" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="updateTags" class="form-label">标签</label>
|
||||||
|
<input type="text" class="form-control" id="updateTags" name="tags" placeholder="例如:1,2">
|
||||||
|
<div class="form-text">最多2个标签,用逗号分隔</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="updateSubmit">保存更改</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认删除模态框 -->
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="deleteModalLabel">确认删除</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>确定要删除这张图片吗?此操作不可撤销。</p>
|
||||||
|
<input type="hidden" id="deleteContSign">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="deleteSubmit">删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
environment.yml
Normal file
9
environment.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
name: imgsearcher
|
||||||
|
channels:
|
||||||
|
- defaults
|
||||||
|
- conda-forge
|
||||||
|
dependencies:
|
||||||
|
- python=3.9
|
||||||
|
- pip
|
||||||
|
- pip:
|
||||||
|
- poetry
|
||||||
155
mock_baidu_api.py
Normal file
155
mock_baidu_api.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
百度图像搜索API模拟类
|
||||||
|
用于在没有有效API密钥的情况下进行测试
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MockBaiduImageSearch:
|
||||||
|
"""模拟百度相似图片搜索API的类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化模拟API"""
|
||||||
|
self.access_token = "mock_access_token_12345678900987654321"
|
||||||
|
self.images = {} # 存储模拟图片数据
|
||||||
|
print("已初始化模拟百度图像搜索API")
|
||||||
|
|
||||||
|
def add_image(self, image_path=None, image_base64=None, url=None, brief=None, tags=None):
|
||||||
|
"""
|
||||||
|
模拟添加图片到图库
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 本地图片路径
|
||||||
|
image_base64: 图片的base64编码
|
||||||
|
url: 图片URL
|
||||||
|
brief: 图片摘要信息
|
||||||
|
tags: 分类信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 模拟API返回的结果
|
||||||
|
"""
|
||||||
|
# 生成一个唯一的图片ID
|
||||||
|
import uuid
|
||||||
|
cont_sign = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 存储图片信息
|
||||||
|
self.images[cont_sign] = {
|
||||||
|
'brief': brief,
|
||||||
|
'tags': tags,
|
||||||
|
'url': url or image_path or "模拟图片路径",
|
||||||
|
'add_time': '2025-04-09 11:00:00'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'log_id': 123456789,
|
||||||
|
'cont_sign': cont_sign,
|
||||||
|
'error_code': 0,
|
||||||
|
'error_msg': 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
def search_image(self, image_path=None, image_base64=None, url=None, tags=None, tag_logic=None, pn=0, rn=300):
|
||||||
|
"""
|
||||||
|
模拟搜索相似图片
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_path: 本地图片路径
|
||||||
|
image_base64: 图片的base64编码
|
||||||
|
url: 图片URL
|
||||||
|
tags: 分类信息
|
||||||
|
tag_logic: 标签逻辑,'AND'或'OR'
|
||||||
|
pn: 分页起始位置
|
||||||
|
rn: 返回结果数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 模拟API返回的结果
|
||||||
|
"""
|
||||||
|
# 模拟搜索结果
|
||||||
|
result_list = []
|
||||||
|
|
||||||
|
# 如果有图片,则返回它们作为搜索结果
|
||||||
|
for cont_sign, image_info in self.images.items():
|
||||||
|
# 如果指定了标签,则进行标签过滤
|
||||||
|
if tags:
|
||||||
|
image_tags = image_info.get('tags', '')
|
||||||
|
if tag_logic == 'AND':
|
||||||
|
# 所有标签都必须匹配
|
||||||
|
if not all(tag in image_tags for tag in tags.split(',')):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# 任意标签匹配即可
|
||||||
|
if not any(tag in image_tags for tag in tags.split(',')):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 添加到结果列表
|
||||||
|
result_list.append({
|
||||||
|
'cont_sign': cont_sign,
|
||||||
|
'score': 0.95, # 模拟相似度分数
|
||||||
|
'brief': image_info.get('brief', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'log_id': 987654321,
|
||||||
|
'result_num': len(result_list),
|
||||||
|
'result': result_list,
|
||||||
|
'has_more': False,
|
||||||
|
'error_code': 0,
|
||||||
|
'error_msg': 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_image(self, cont_sign, brief=None, tags=None):
|
||||||
|
"""
|
||||||
|
模拟更新图片信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cont_sign: 图片ID
|
||||||
|
brief: 新的图片摘要信息
|
||||||
|
tags: 新的分类信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 模拟API返回的结果
|
||||||
|
"""
|
||||||
|
if cont_sign not in self.images:
|
||||||
|
return {
|
||||||
|
'log_id': 135792468,
|
||||||
|
'error_code': 222202,
|
||||||
|
'error_msg': '图片不存在'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 更新图片信息
|
||||||
|
if brief:
|
||||||
|
self.images[cont_sign]['brief'] = brief
|
||||||
|
if tags:
|
||||||
|
self.images[cont_sign]['tags'] = tags
|
||||||
|
|
||||||
|
return {
|
||||||
|
'log_id': 135792468,
|
||||||
|
'error_code': 0,
|
||||||
|
'error_msg': 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
def delete_image(self, cont_sign):
|
||||||
|
"""
|
||||||
|
模拟删除图片
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cont_sign: 图片ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 模拟API返回的结果
|
||||||
|
"""
|
||||||
|
if cont_sign not in self.images:
|
||||||
|
return {
|
||||||
|
'log_id': 246813579,
|
||||||
|
'error_code': 222202,
|
||||||
|
'error_msg': '图片不存在'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 删除图片
|
||||||
|
del self.images[cont_sign]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'log_id': 246813579,
|
||||||
|
'error_code': 0,
|
||||||
|
'error_msg': 'success'
|
||||||
|
}
|
||||||
334
poetry.lock
generated
Normal file
334
poetry.lock
generated
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2023.5.7"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"},
|
||||||
|
{file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "2.0.12"
|
||||||
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5.0"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
||||||
|
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
unicode-backport = ["unicodedata2"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.3"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||||
|
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "platform_system == \"Windows\""
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask"
|
||||||
|
version = "2.0.1"
|
||||||
|
description = "A simple framework for building complex web applications."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "Flask-2.0.1-py3-none-any.whl", hash = "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"},
|
||||||
|
{file = "Flask-2.0.1.tar.gz", hash = "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=7.1.2"
|
||||||
|
itsdangerous = ">=2.0"
|
||||||
|
Jinja2 = ">=3.0"
|
||||||
|
Werkzeug = ">=2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
async = ["asgiref (>=3.2)"]
|
||||||
|
dotenv = ["python-dotenv"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask-cors"
|
||||||
|
version = "3.0.10"
|
||||||
|
description = "A Flask extension adding a decorator for CORS support"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"},
|
||||||
|
{file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Flask = ">=0.9"
|
||||||
|
Six = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.4"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||||
|
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itsdangerous"
|
||||||
|
version = "2.1.2"
|
||||||
|
description = "Safely pass data to untrusted environments and back."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
|
||||||
|
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.2"
|
||||||
|
description = "A very fast and expressive template engine."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
|
||||||
|
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
i18n = ["Babel (>=2.7)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "2.1.2"
|
||||||
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
|
||||||
|
{file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
|
||||||
|
{file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "8.3.1"
|
||||||
|
description = "Python Imaging Library (Fork)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"},
|
||||||
|
{file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"},
|
||||||
|
{file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"},
|
||||||
|
{file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"},
|
||||||
|
{file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"},
|
||||||
|
{file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"},
|
||||||
|
{file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"},
|
||||||
|
{file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"},
|
||||||
|
{file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37"},
|
||||||
|
{file = "Pillow-8.3.1-cp36-cp36m-win32.whl", hash = "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229"},
|
||||||
|
{file = "Pillow-8.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de"},
|
||||||
|
{file = "Pillow-8.3.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14"},
|
||||||
|
{file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab"},
|
||||||
|
{file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf"},
|
||||||
|
{file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0"},
|
||||||
|
{file = "Pillow-8.3.1-cp37-cp37m-win32.whl", hash = "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba"},
|
||||||
|
{file = "Pillow-8.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500"},
|
||||||
|
{file = "Pillow-8.3.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4"},
|
||||||
|
{file = "Pillow-8.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e"},
|
||||||
|
{file = "Pillow-8.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79"},
|
||||||
|
{file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae"},
|
||||||
|
{file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844"},
|
||||||
|
{file = "Pillow-8.3.1-cp38-cp38-win32.whl", hash = "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367"},
|
||||||
|
{file = "Pillow-8.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8"},
|
||||||
|
{file = "Pillow-8.3.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e"},
|
||||||
|
{file = "Pillow-8.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856"},
|
||||||
|
{file = "Pillow-8.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82"},
|
||||||
|
{file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc"},
|
||||||
|
{file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83"},
|
||||||
|
{file = "Pillow-8.3.1-cp39-cp39-win32.whl", hash = "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d"},
|
||||||
|
{file = "Pillow-8.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8"},
|
||||||
|
{file = "Pillow-8.3.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf"},
|
||||||
|
{file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63"},
|
||||||
|
{file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636"},
|
||||||
|
{file = "Pillow-8.3.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d"},
|
||||||
|
{file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77"},
|
||||||
|
{file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8"},
|
||||||
|
{file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"},
|
||||||
|
{file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "0.19.0"
|
||||||
|
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"},
|
||||||
|
{file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cli = ["click (>=5.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.26.0"
|
||||||
|
description = "Python HTTP for Humans."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||||
|
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = ">=2017.4.17"
|
||||||
|
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||||
|
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
|
||||||
|
urllib3 = ">=1.21.1,<1.27"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton ; sys_platform == \"win32\" and python_version == \"2.7\""]
|
||||||
|
use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.16.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "1.26.16"
|
||||||
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"},
|
||||||
|
{file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli (>=1.0.9) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""]
|
||||||
|
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkzeug"
|
||||||
|
version = "2.2.3"
|
||||||
|
description = "The comprehensive WSGI web application library."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"},
|
||||||
|
{file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=2.1.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
watchdog = ["watchdog"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.1"
|
||||||
|
python-versions = "^3.9"
|
||||||
|
content-hash = "c80c5641fe75055f2a80acf7a46ab694e2c875a0bc58f86bb52205c2dd34e5af"
|
||||||
19
pyproject.toml
Normal file
19
pyproject.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "imgsearcher"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "基于百度AI开放平台的相似图片搜索系统"
|
||||||
|
authors = ["Your Name <your.email@example.com>"]
|
||||||
|
readme = "README.md"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.9"
|
||||||
|
flask = "^2.0.1"
|
||||||
|
requests = "^2.26.0"
|
||||||
|
python-dotenv = "^0.19.0"
|
||||||
|
pillow = "^8.3.1"
|
||||||
|
flask-cors = "^3.0.10"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
flask==2.0.1
|
||||||
|
requests==2.26.0
|
||||||
|
python-dotenv==0.19.0
|
||||||
|
Pillow==8.3.1
|
||||||
|
flask-cors==3.0.10
|
||||||
8
run.sh
Normal file
8
run.sh
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 激活conda环境
|
||||||
|
eval "$(conda shell.bash hook)"
|
||||||
|
conda activate imgsearcher
|
||||||
|
|
||||||
|
# 运行应用
|
||||||
|
poetry run python app.py
|
||||||
187
test_baidu_api.py
Normal file
187
test_baidu_api.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
百度图像搜索API测试脚本
|
||||||
|
简单测试API功能,打印结果
|
||||||
|
支持实际API和模拟模式
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from app.api.baidu_image_search import BaiduImageSearch
|
||||||
|
from app.api.image_utils import ImageUtils
|
||||||
|
|
||||||
|
# 导入模拟类
|
||||||
|
from mock_baidu_api import MockBaiduImageSearch
|
||||||
|
|
||||||
|
# 全局变量,用于存储测试过程中的图片ID
|
||||||
|
test_cont_sign = None
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
"""解析命令行参数"""
|
||||||
|
parser = argparse.ArgumentParser(description='百度图像搜索API测试脚本')
|
||||||
|
parser.add_argument('--mock', action='store_true', help='使用模拟模式,不连接实际API')
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
def check_api_keys():
|
||||||
|
"""检查API密钥是否已配置"""
|
||||||
|
api_key = os.getenv('BAIDU_API_KEY')
|
||||||
|
secret_key = os.getenv('BAIDU_SECRET_KEY')
|
||||||
|
|
||||||
|
if not api_key or not secret_key:
|
||||||
|
print("\n\033[91m错误: API密钥未配置\033[0m")
|
||||||
|
print("\n请在.env文件中添加以下内容:")
|
||||||
|
print("BAIDU_API_KEY=你的API密钥")
|
||||||
|
print("BAIDU_SECRET_KEY=你的密钥")
|
||||||
|
print("\n您可以从百度AI开放平台获取密钥: https://ai.baidu.com/")
|
||||||
|
print("\n或者使用 --mock 参数运行模拟模式: python test_baidu_api.py --mock")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"API密钥: {api_key[:4]}...")
|
||||||
|
print(f"Secret密钥: {secret_key[:4]}...")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 解析命令行参数
|
||||||
|
args = parse_arguments()
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
print("加载环境变量...")
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# 判断是否使用模拟模式
|
||||||
|
if args.mock:
|
||||||
|
print("\n\033[93m注意: 使用模拟模式,不连接实际API\033[0m")
|
||||||
|
api = MockBaiduImageSearch()
|
||||||
|
|
||||||
|
# 测试添加图片
|
||||||
|
test_add_image(api)
|
||||||
|
|
||||||
|
# 测试搜索图片
|
||||||
|
test_search_image(api)
|
||||||
|
|
||||||
|
# 测试更新图片
|
||||||
|
test_update_image(api)
|
||||||
|
|
||||||
|
# 测试删除图片
|
||||||
|
test_delete_image(api)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 如果不是模拟模式,检查API密钥
|
||||||
|
if not check_api_keys():
|
||||||
|
return
|
||||||
|
|
||||||
|
# 初始化实际API
|
||||||
|
print("\n初始化百度图像搜索API...")
|
||||||
|
try:
|
||||||
|
api = BaiduImageSearch()
|
||||||
|
if api.access_token:
|
||||||
|
print(f"获取到的Access Token: {api.access_token[:10]}...(已截断)")
|
||||||
|
|
||||||
|
# 测试添加图片
|
||||||
|
test_add_image(api)
|
||||||
|
|
||||||
|
# 测试搜索图片
|
||||||
|
test_search_image(api)
|
||||||
|
|
||||||
|
# 测试更新图片
|
||||||
|
test_update_image(api)
|
||||||
|
|
||||||
|
# 测试删除图片
|
||||||
|
test_delete_image(api)
|
||||||
|
else:
|
||||||
|
print("\033[91m错误: 无法获取Access Token\033[0m")
|
||||||
|
print("\n尝试使用模拟模式: python test_baidu_api.py --mock")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\033[91m错误: {e}\033[0m")
|
||||||
|
print("\n尝试使用模拟模式: python test_baidu_api.py --mock")
|
||||||
|
|
||||||
|
def test_add_image(api):
|
||||||
|
"""测试添加图片功能"""
|
||||||
|
print("\n===== 测试添加图片 =====")
|
||||||
|
|
||||||
|
# 使用URL方式添加图片
|
||||||
|
test_image_url = "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png" # 百度logo
|
||||||
|
brief = json.dumps({"name": "测试图片", "id": "test001"}, ensure_ascii=False)
|
||||||
|
tags = "测试,图片"
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = api.add_image(
|
||||||
|
url=test_image_url,
|
||||||
|
brief=brief,
|
||||||
|
tags=tags
|
||||||
|
)
|
||||||
|
print("添加图片结果:")
|
||||||
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
# 保存contSign用于后续测试
|
||||||
|
if result.get("cont_sign"):
|
||||||
|
global test_cont_sign
|
||||||
|
test_cont_sign = result.get("cont_sign")
|
||||||
|
print(f"已保存图片ID: {test_cont_sign} 用于后续测试")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"添加图片失败: {e}")
|
||||||
|
|
||||||
|
def test_search_image(api):
|
||||||
|
"""测试搜索图片功能"""
|
||||||
|
print("\n===== 测试搜索图片 =====")
|
||||||
|
|
||||||
|
# 使用URL方式搜索图片
|
||||||
|
test_image_url = "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png" # 百度logo
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = api.search_image(
|
||||||
|
url=test_image_url,
|
||||||
|
tags="测试"
|
||||||
|
)
|
||||||
|
print("搜索图片结果:")
|
||||||
|
print(f"找到 {len(result.get('result', []))} 个匹配项")
|
||||||
|
|
||||||
|
# 只打印前2个结果
|
||||||
|
for i, item in enumerate(result.get("result", [])[:2]):
|
||||||
|
print(f"结果 {i+1}:")
|
||||||
|
print(f" 相似度: {item.get('score', 0)}")
|
||||||
|
print(f" 简介: {item.get('brief', '')}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"搜索图片失败: {e}")
|
||||||
|
|
||||||
|
def test_update_image(api):
|
||||||
|
"""测试更新图片功能"""
|
||||||
|
print("\n===== 测试更新图片 =====")
|
||||||
|
|
||||||
|
global test_cont_sign
|
||||||
|
if not test_cont_sign:
|
||||||
|
print("没有可用的图片ID,跳过更新图片测试")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_brief = json.dumps({"name": "更新后的测试图片", "id": "test001"}, ensure_ascii=False)
|
||||||
|
result = api.update_image(test_cont_sign, new_brief)
|
||||||
|
print("更新图片结果:")
|
||||||
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"更新图片失败: {e}")
|
||||||
|
|
||||||
|
def test_delete_image(api):
|
||||||
|
"""测试删除图片功能"""
|
||||||
|
print("\n===== 测试删除图片 =====")
|
||||||
|
|
||||||
|
global test_cont_sign
|
||||||
|
if not test_cont_sign:
|
||||||
|
print("没有可用的图片ID,跳过删除图片测试")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = api.delete_image(test_cont_sign)
|
||||||
|
print("删除图片结果:")
|
||||||
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||||
|
# 清除已删除的图片ID
|
||||||
|
test_cont_sign = None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"删除图片失败: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user