Compare commits

..

No commits in common. "robosen" and "master" have entirely different histories.

17 changed files with 342 additions and 1692 deletions

View File

@ -1,17 +1,18 @@
# Drone CI/CD 配置文件 # Drone CI/CD 配置文件
kind: pipeline kind: pipeline
type: docker # 使用 Docker 作为执行环境 type: docker # 使用 Docker 作为执行环境
name: default name: imgsearcher-pipeline
steps: steps:
# 步骤1: 构建 Python 应用 # 步骤1: 构建 Python 应用
- name: build_imgsearcher - name: build_imgsearcher
image: python:3.9-slim image: ccr-29eug8s3-vpc.cnc.bj.baidubce.com/service/python:3.9-slim
commands: commands:
- pip install -r requirements.txt - pip install poetry
- poetry config virtualenvs.create false
- poetry install --no-dev
- mkdir -p build/imgsearcher - mkdir -p build/imgsearcher
- cp -r app.py app requirements.txt Dockerfile build/imgsearcher/ - cp -r app.py app pyproject.toml poetry.lock Dockerfile build/imgsearcher/
- pip install -r requirements.txt
when: when:
branch: branch:
- dev - dev
@ -50,8 +51,8 @@ steps:
from_secret: kubernetes_cert_dev from_secret: kubernetes_cert_dev
namespace: namespace:
from_secret: kubernetes_namespace_dev from_secret: kubernetes_namespace_dev
deployment: imgsearcher-api deployment: imgsearcher
container: imgsearcher-api container: imgsearcher
repo: ccr-29eug8s3-vpc.cnc.bj.baidubce.com/service/imgsearcher repo: ccr-29eug8s3-vpc.cnc.bj.baidubce.com/service/imgsearcher
tag: ${DRONE_COMMIT_SHA} tag: ${DRONE_COMMIT_SHA}
when: when:

View File

@ -2,11 +2,20 @@ FROM python:3.9-slim
WORKDIR /app WORKDIR /app
# 复制应用程序代码 # 安装Poetry
COPY . . RUN pip install --no-cache-dir poetry
# 配置Poetry不创建虚拟环境
RUN poetry config virtualenvs.create false
# 复制Poetry配置文件
COPY pyproject.toml poetry.lock* ./
# 安装依赖 # 安装依赖
RUN pip install --no-cache-dir -r requirements.txt RUN poetry install --no-dev --no-interaction --no-ansi
# 复制应用程序代码
COPY . .
# 创建上传目录 # 创建上传目录
RUN mkdir -p uploads RUN mkdir -p uploads

306
app.py
View File

@ -4,15 +4,13 @@
import os import os
import json import json
import base64 import base64
import requests from flask import Flask, render_template, request, jsonify, redirect, url_for, session
from flask import Flask, render_template, request, jsonify, redirect, url_for, session, Response, stream_with_context
from flask_cors import CORS from flask_cors import CORS
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from app.api.baidu_image_search import BaiduImageSearch from app.api.baidu_image_search import BaiduImageSearch
from app.api.image_utils import ImageUtils from app.api.image_utils import ImageUtils
from app.api.azure_openai import AzureOpenAI from app.api.azure_openai import AzureOpenAI
from app.api.type_manager_mongo import TypeManagerMongo from app.api.type_manager_mongo import TypeManagerMongo
from app.api.robot_manager import RobotManager
app = Flask(__name__, template_folder='app/templates', static_folder='app/static') app = Flask(__name__, template_folder='app/templates', static_folder='app/static')
CORS(app) # 启用CORS支持跨域请求 CORS(app) # 启用CORS支持跨域请求
@ -42,19 +40,16 @@ except ValueError as e:
# 初始化类型管理器 # 初始化类型管理器
type_manager = TypeManagerMongo() type_manager = TypeManagerMongo()
# 初始化机器人角色管理器
robot_manager = RobotManager()
def allowed_file(filename): def allowed_file(filename):
"""检查文件扩展名是否允许""" """检查文件扩展名是否允许"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/imgsearcherApi/') @app.route('/')
def index(): def index():
"""首页""" """首页"""
return render_template('index.html') return render_template('index.html')
@app.route('/imgsearcherApi/upload', methods=['POST']) @app.route('/upload', methods=['POST'])
def upload_image(): def upload_image():
"""上传图片到图库""" """上传图片到图库"""
if 'file' not in request.files: if 'file' not in request.files:
@ -102,7 +97,7 @@ def upload_image():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/search', methods=['POST']) @app.route('/search', methods=['POST'])
def search_image(): def search_image():
"""搜索相似图片""" """搜索相似图片"""
if 'file' not in request.files: if 'file' not in request.files:
@ -157,7 +152,7 @@ def search_image():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/delete', methods=['POST']) @app.route('/delete', methods=['POST'])
def delete_image(): def delete_image():
"""删除图库中的图片""" """删除图库中的图片"""
data = request.get_json() data = request.get_json()
@ -174,7 +169,7 @@ def delete_image():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/update', methods=['POST']) @app.route('/update', methods=['POST'])
def update_image(): def update_image():
"""更新图库中的图片信息""" """更新图库中的图片信息"""
data = request.get_json() data = request.get_json()
@ -209,7 +204,7 @@ def update_image():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/token') @app.route('/api/token')
def get_token(): def get_token():
"""获取API访问令牌""" """获取API访问令牌"""
try: try:
@ -218,12 +213,12 @@ def get_token():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/chat-with-image', methods=['GET']) @app.route('/chat-with-image', methods=['GET'])
def chat_with_image_page(): def chat_with_image_page():
"""图片对话页面""" """图片对话页面"""
return render_template('chat.html') return render_template('chat.html')
@app.route('/imgsearcherApi/api/upload-chat-image', methods=['POST']) @app.route('/api/upload-chat-image', methods=['POST'])
def upload_chat_image(): def upload_chat_image():
"""上传图片用于对话""" """上传图片用于对话"""
if 'file' not in request.files: if 'file' not in request.files:
@ -236,9 +231,6 @@ def upload_chat_image():
if not allowed_file(file.filename): if not allowed_file(file.filename):
return jsonify({'error': '不支持的文件类型'}), 400 return jsonify({'error': '不支持的文件类型'}), 400
# 获取选择的机器人角色ID
robot_id = request.form.get('robot_id', '')
# 保存文件 # 保存文件
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
@ -247,9 +239,6 @@ def upload_chat_image():
# 存储图片路径到会话 # 存储图片路径到会话
session['chat_image_path'] = file_path session['chat_image_path'] = file_path
# 存储机器人角色ID到会话
session['robot_id'] = robot_id
# 清空对话历史 # 清空对话历史
session['conversation_history'] = [] session['conversation_history'] = []
@ -280,27 +269,16 @@ def upload_chat_image():
except: except:
continue continue
# 获取机器人角色信息
robot_info = None
if robot_id:
robot = robot_manager.get_robot(robot_id)
if robot:
robot_info = {
'name': robot.get('name', ''),
'background': robot.get('background', '')
}
return jsonify({ return jsonify({
'success': True, 'success': True,
'image_path': file_path, 'image_path': file_path,
'image_type': image_type, 'image_type': image_type,
'description': description, 'description': description
'robot': robot_info
}) })
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/chat', methods=['POST']) @app.route('/api/chat', methods=['POST'])
def chat(): def chat():
"""与图片进行对话""" """与图片进行对话"""
if not azure_openai_api: if not azure_openai_api:
@ -312,7 +290,6 @@ def chat():
message = data['message'] message = data['message']
image_path = session.get('chat_image_path') image_path = session.get('chat_image_path')
robot_id = session.get('robot_id', '')
if not image_path or not os.path.exists(image_path): if not image_path or not os.path.exists(image_path):
return jsonify({'error': '没有上传图片或图片已失效'}), 400 return jsonify({'error': '没有上传图片或图片已失效'}), 400
@ -320,23 +297,12 @@ def chat():
# 获取对话历史 # 获取对话历史
conversation_history = session.get('conversation_history', []) conversation_history = session.get('conversation_history', [])
# 获取机器人角色信息
robot_info = None
if robot_id:
robot = robot_manager.get_robot(robot_id)
if robot:
robot_info = {
'name': robot.get('name', ''),
'background': robot.get('background', '')
}
try: try:
# 调用Azure OpenAI API进行对话 # 调用Azure OpenAI API进行对话
response = azure_openai_api.chat_with_image( response = azure_openai_api.chat_with_image(
image_path=image_path, image_path=image_path,
message=message, message=message,
conversation_history=conversation_history, conversation_history=conversation_history
robot_info=robot_info
) )
# 提取回复内容 # 提取回复内容
@ -354,253 +320,5 @@ def chat():
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/robots', methods=['GET'])
def robot_page():
"""机器人角色管理页面"""
return render_template('robots.html')
@app.route('/imgsearcherApi/api/robots', methods=['GET'])
def get_robots():
"""获取所有机器人角色"""
try:
robots = robot_manager.get_all_robots()
return jsonify({
'success': True,
'robots': robots
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/robots', methods=['POST'])
def add_robot():
"""添加新的机器人角色"""
try:
name = request.form.get('name')
background = request.form.get('background')
if not name or not background:
return jsonify({'error': '机器人名称和背景故事不能为空'}), 400
avatar_file = None
if 'avatar' in request.files:
avatar_file = request.files['avatar']
if avatar_file.filename == '':
avatar_file = None
elif not allowed_file(avatar_file.filename):
return jsonify({'error': '不支持的文件类型'}), 400
result = robot_manager.add_robot(name, background, avatar_file)
if 'error' in result:
return jsonify(result), 400
return jsonify({
'success': True,
'robot': result
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/robots/<robot_id>', methods=['GET'])
def get_robot(robot_id):
"""获取指定机器人角色"""
try:
robot = robot_manager.get_robot(robot_id)
if not robot:
return jsonify({'error': '找不到该机器人'}), 404
return jsonify({
'success': True,
'robot': robot
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/robots/<robot_id>', methods=['PUT'])
def update_robot(robot_id):
"""更新机器人角色"""
try:
name = request.form.get('name')
background = request.form.get('background')
avatar_file = None
if 'avatar' in request.files:
avatar_file = request.files['avatar']
if avatar_file.filename == '':
avatar_file = None
elif not allowed_file(avatar_file.filename):
return jsonify({'error': '不支持的文件类型'}), 400
result = robot_manager.update_robot(robot_id, name, background, avatar_file)
if 'error' in result:
return jsonify(result), 400
return jsonify({
'success': True,
'robot': result
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/robots/<robot_id>', methods=['DELETE'])
def delete_robot(robot_id):
"""删除机器人角色"""
try:
success = robot_manager.delete_robot(robot_id)
if not success:
return jsonify({'error': '找不到该机器人或删除失败'}), 404
return jsonify({
'success': True
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# ==================== Chat Stream ====================
@app.route('/imgsearcherApi/api/chat-stream', methods=['POST'])
def chat_stream():
"""流式返回文本增量 + 最终语音 (SSE)"""
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')
robot_id = session.get('robot_id', '')
if not image_path or not os.path.exists(image_path):
return jsonify({'error': '没有上传图片或图片已失效'}), 400
conversation_history = session.get('conversation_history', [])
robot_info = None
if robot_id:
robot = robot_manager.get_robot(robot_id)
if robot:
robot_info = {'name': robot.get('name', ''), 'background': robot.get('background', '')}
# 语音音色选择
voice_wav = data.get('voice') or 'zh-CN-XiaoXiao-Assistant-Audio.wav'
# 与Azure流式对话
openai_resp = azure_openai_api.chat_with_image_stream(
image_path=image_path,
message=message,
conversation_history=conversation_history,
robot_info=robot_info
)
def event_stream():
accumulated_text = ""
pending = ""
punctuation = "。!?.!?"
def send_tts(sentence):
if not sentence.strip():
return
tts_params = {
'tts_text': sentence,
'prompt_wav': voice_wav,
'text_split_method': 'cut5'
}
try:
tts_resp = requests.get(
"https://cloud.infini-ai.com/AIStudio/v1/inference/api/te-c7zfd4hrdoj2vqmw/tts/CosyVoice/v1/zero_shot",
params=tts_params,
headers={'Authorization': f'Bearer {os.getenv("INFINI_API_KEY", "sk-daooolufaf7ienn6")}'},
stream=True,
timeout=30
)
if tts_resp.status_code == 200:
audio_bytes = bytearray()
for chunk in tts_resp.iter_content(chunk_size=8192):
if chunk:
audio_bytes.extend(chunk)
if audio_bytes:
b64_audio = base64.b64encode(audio_bytes).decode()
yield f"data: {{\"type\": \"audio\", \"content\": \"{b64_audio}\" }}\n\n"
except Exception as e:
print("TTS 调用失败", e)
# 读取 GPT 流
for line in openai_resp.iter_lines():
if not line:
continue
try:
if line.strip() == b'data: [DONE]':
break
if line.startswith(b'data:'):
content_json = json.loads(line[5:].strip())
choices = content_json.get('choices', [])
if not choices:
continue
delta = choices[0].get('delta', {}).get('content', '')
if not delta:
continue
accumulated_text += delta
pending += delta
# 立即发送文本增量
yield f"data: {{\"type\": \"text\", \"content\": {json.dumps(delta, ensure_ascii=False)} }}\n\n"
# 检测标点
while True:
idx = -1
for i, ch in enumerate(pending):
if ch in punctuation:
idx = i
break
if idx == -1:
break
sentence = pending[:idx+1]
pending = pending[idx+1:]
# 调用 TTS 并流式发送
for audio_evt in send_tts(sentence):
yield audio_evt
except Exception as e:
print("解析OpenAI流错误", e)
continue
# 处理剩余 pending
if pending.strip():
for audio_evt in send_tts(pending):
yield audio_evt
pending = ""
# 更新会话历史
conversation_history.append({"role": "user", "content": message})
conversation_history.append({"role": "assistant", "content": accumulated_text})
session['conversation_history'] = conversation_history
yield "event: end\ndata: {}\n\n"
# 保持请求上下文,避免在生成器中操作 session 报错
return Response(stream_with_context(event_stream()), content_type='text/event-stream')
# ==================== TTS ====================
@app.route('/imgsearcherApi/api/tts', methods=['POST'])
def tts():
"""文本转语音接口,返回音频数据流"""
data = request.get_json()
if not data or 'text' not in data:
return jsonify({'error': '缺少text参数'}), 400
text = data['text']
prompt_text = data.get('prompt_text') or "我是威震天,我只代表月亮消灭你"
prompt_wav = data.get('voice') or data.get('prompt_wav') or "zh-CN-XiaoXiao-Assistant-Audio.wav"
tts_base_url = "https://cloud.infini-ai.com/AIStudio/v1/inference/api/te-c7zfd4hrdoj2vqmw/tts/CosyVoice/v1/zero_shot"
api_key = os.getenv('INFINI_API_KEY', 'sk-daooolufaf7ienn6')
headers = {'Authorization': f'Bearer {api_key}'}
params = {
'tts_text': text,
'prompt_wav': prompt_wav,
'text_split_method': 'cut5'
}
try:
r = requests.get(tts_base_url, params=params, headers=headers, timeout=20)
if r.status_code == 200:
return Response(r.content, content_type=r.headers.get('Content-Type', 'audio/wav'))
else:
print(f"TTS remote error {r.status_code}: {r.text}")
return jsonify({'error': f'TTS服务错误: {r.text}'}), 502
except Exception as e:
return jsonify({'error': f'TTS请求失败: {str(e)}'}), 500
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5001) app.run(debug=True, host='0.0.0.0', port=5001)

View File

@ -85,7 +85,7 @@ class AzureOpenAI:
print(f"获取图片类型和描述失败: {str(e)}") print(f"获取图片类型和描述失败: {str(e)}")
return "", "" return "", ""
def chat_with_image(self, image_path, message, conversation_history=None, robot_info=None): def chat_with_image(self, image_path, message, conversation_history=None):
""" """
使用图片和消息与GPT-4o多模态模型进行对话 使用图片和消息与GPT-4o多模态模型进行对话
@ -93,7 +93,6 @@ class AzureOpenAI:
image_path: 图片路径 image_path: 图片路径
message: 用户消息 message: 用户消息
conversation_history: 对话历史记录 conversation_history: 对话历史记录
robot_info: 机器人角色信息包含name和background
Returns: Returns:
dict: 模型响应 dict: 模型响应
@ -108,14 +107,7 @@ class AzureOpenAI:
base64_image = self._encode_image(image_path) base64_image = self._encode_image(image_path)
# 构建系统提示 # 构建系统提示
if robot_info and robot_info.get('name') and robot_info.get('background'): system_message = "你是一个智能助手,能够分析图片并回答问题。"
# 如果有机器人角色信息,使用机器人角色
system_message = f"你是{robot_info['name']},一个能够分析图片并回答问题的角色。\n\n你的背景故事:{robot_info['background']}\n\n在对话中,你应该始终保持这个角色的身份和特点,用第一人称回答问题。"
else:
# 默认智能助手
system_message = "你是一个智能助手,能够分析图片并回答问题。"
# 添加图片类型和描述信息
if image_type and description: if image_type and description:
system_message += f"\n\n这是一张{image_type}的图片。\n描述信息:{description}\n\n请基于这些信息和图片内容回答用户的问题。" system_message += f"\n\n这是一张{image_type}的图片。\n描述信息:{description}\n\n请基于这些信息和图片内容回答用户的问题。"
@ -172,39 +164,3 @@ class AzureOpenAI:
return response.json() return response.json()
else: else:
raise Exception(f"Azure OpenAI API请求失败: {response.text}") raise Exception(f"Azure OpenAI API请求失败: {response.text}")
def chat_with_image_stream(self, image_path, message, conversation_history=None, robot_info=None):
"""流式输出 GPT-4o 回复,返回 requests.Response 对象 (stream=True)"""
if conversation_history is None:
conversation_history = []
image_type, description = self._get_image_type_description(image_path)
base64_image = self._encode_image(image_path)
if robot_info and robot_info.get('name') and robot_info.get('background'):
system_message = f"你是{robot_info['name']},一个能够分析图片并回答问题的角色。\n\n你的背景故事:{robot_info['background']}\n\n在对话中,你应该始终保持这个角色的身份和特点,用第一人称回答问题。"
else:
system_message = "你是一个智能助手,能够分析图片并回答问题。"
if image_type and description:
system_message += f"\n\n这是一张{image_type}的图片。\n描述信息:{description}\n\n请基于这些信息和图片内容回答用户的问题。"
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}"}}
]
})
headers = {"Content-Type": "application/json", "api-key": self.api_key}
payload = {
"messages": messages,
"max_tokens": 2000,
"temperature": 0.7,
"top_p": 0.95,
"stream": True
}
url = f"{self.endpoint}/openai/deployments/{self.deployment_name}/chat/completions?api-version={self.api_version}"
return requests.post(url, headers=headers, json=payload, stream=True)

View File

@ -1,217 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import datetime
from pymongo import MongoClient
from dotenv import load_dotenv
import base64
import uuid
# 加载环境变量
load_dotenv()
class RobotManager:
"""管理机器人角色的类"""
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['robot_characters']
# 确保有索引以提高查询性能
self.collection.create_index('robot_id')
self.collection.create_index('name')
# 上传文件夹路径
self.upload_folder = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'uploads')
if not os.path.exists(self.upload_folder):
os.makedirs(self.upload_folder)
print(f"MongoDB RobotManager 初始化完成,连接到 {db_name}")
def add_robot(self, name, background, avatar_file=None):
"""
添加新的机器人角色
Args:
name: 机器人名称
background: 机器人背景故事
avatar_file: 机器人头像文件可选
Returns:
dict: 包含新添加的机器人信息
"""
if not name or not background:
return {"error": "机器人名称和背景故事不能为空"}
# 检查是否已存在同名机器人
if self.collection.find_one({"name": name}):
return {"error": f"已存在名为 '{name}' 的机器人"}
# 生成唯一ID
robot_id = str(uuid.uuid4())
# 处理头像文件
avatar_path = None
if avatar_file:
# 保存头像文件
filename = f"robot_avatar_{robot_id}.{avatar_file.filename.split('.')[-1]}"
file_path = os.path.join(self.upload_folder, filename)
avatar_file.save(file_path)
avatar_path = file_path
# 创建机器人记录
robot = {
"robot_id": robot_id,
"name": name,
"background": background,
"avatar_path": avatar_path,
"created_at": datetime.datetime.now()
}
# 保存到数据库
self.collection.insert_one(robot)
# 返回新添加的机器人信息不包含MongoDB的_id字段
robot.pop("_id", None)
return robot
def get_robot(self, robot_id):
"""
获取指定ID的机器人信息
Args:
robot_id: 机器人ID
Returns:
dict: 机器人信息如果不存在则返回None
"""
robot = self.collection.find_one({"robot_id": robot_id})
if robot:
# 转换MongoDB的_id为字符串
robot["_id"] = str(robot["_id"])
# 如果有头像添加base64编码的头像数据
if robot.get("avatar_path") and os.path.exists(robot["avatar_path"]):
with open(robot["avatar_path"], "rb") as f:
avatar_data = base64.b64encode(f.read()).decode('utf-8')
robot["avatar_data"] = avatar_data
return robot
return None
def update_robot(self, robot_id, name=None, background=None, avatar_file=None):
"""
更新机器人信息
Args:
robot_id: 机器人ID
name: 新的机器人名称可选
background: 新的背景故事可选
avatar_file: 新的头像文件可选
Returns:
dict: 更新后的机器人信息
"""
# 获取现有机器人
robot = self.collection.find_one({"robot_id": robot_id})
if not robot:
return {"error": f"找不到ID为 '{robot_id}' 的机器人"}
# 准备更新数据
update_data = {}
if name:
# 检查新名称是否与其他机器人冲突
existing = self.collection.find_one({"name": name, "robot_id": {"$ne": robot_id}})
if existing:
return {"error": f"已存在名为 '{name}' 的机器人"}
update_data["name"] = name
if background:
update_data["background"] = background
# 处理新头像
if avatar_file:
# 删除旧头像
old_avatar_path = robot.get("avatar_path")
if old_avatar_path and os.path.exists(old_avatar_path):
try:
os.remove(old_avatar_path)
except:
pass
# 保存新头像
filename = f"robot_avatar_{robot_id}.{avatar_file.filename.split('.')[-1]}"
file_path = os.path.join(self.upload_folder, filename)
avatar_file.save(file_path)
update_data["avatar_path"] = file_path
# 更新数据库
if update_data:
update_data["updated_at"] = datetime.datetime.now()
self.collection.update_one({"robot_id": robot_id}, {"$set": update_data})
# 返回更新后的机器人信息
return self.get_robot(robot_id)
def delete_robot(self, robot_id):
"""
删除机器人
Args:
robot_id: 机器人ID
Returns:
bool: 是否成功删除
"""
# 获取机器人信息
robot = self.collection.find_one({"robot_id": robot_id})
if not robot:
return False
# 删除头像文件
avatar_path = robot.get("avatar_path")
if avatar_path and os.path.exists(avatar_path):
try:
os.remove(avatar_path)
except:
pass
# 从数据库中删除
result = self.collection.delete_one({"robot_id": robot_id})
return result.deleted_count > 0
def get_all_robots(self):
"""
获取所有机器人列表
Returns:
list: 机器人信息列表
"""
robots = []
for robot in self.collection.find():
# 转换MongoDB的_id为字符串
robot["_id"] = str(robot["_id"])
# 如果有头像添加base64编码的头像数据
if robot.get("avatar_path") and os.path.exists(robot["avatar_path"]):
with open(robot["avatar_path"], "rb") as f:
avatar_data = base64.b64encode(f.read()).decode('utf-8')
robot["avatar_data"] = avatar_data
robots.append(robot)
return robots
def close(self):
"""关闭MongoDB连接"""
if self.client:
self.client.close()

View File

@ -1,35 +1,5 @@
// 页面加载完成后执行 // 页面加载完成后执行
// ====== 音频队列处理 ======
const audioQueue = [];
let audioPlaying = false;
function enqueueAudio(blob) {
audioQueue.push(blob);
playNextAudio();
}
function playNextAudio() {
if (audioPlaying || audioQueue.length === 0) return;
const blob = audioQueue.shift();
const audio = new Audio(URL.createObjectURL(blob));
audioPlaying = true;
audio.addEventListener('ended', () => {
audioPlaying = false;
playNextAudio();
});
audio.play();
}
// 顺序播放一组音频片段(用于重放)
function playAudioSegments(segments) {
segments.forEach(seg => enqueueAudio(seg));
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// 加载机器人角色列表
loadRobotCharacters();
// 初始化图片上传表单 // 初始化图片上传表单
initUploadImageForm(); initUploadImageForm();
@ -37,28 +7,6 @@ document.addEventListener('DOMContentLoaded', function() {
initChatForm(); initChatForm();
}); });
// 加载机器人角色列表
async function loadRobotCharacters() {
const robotSelect = document.getElementById('robotSelect');
try {
const response = await fetch('/imgsearcherApi/api/robots');
const data = await response.json();
if (data.robots && data.robots.length > 0) {
// 添加机器人选项
data.robots.forEach(robot => {
const option = document.createElement('option');
option.value = robot.robot_id;
option.textContent = robot.name;
robotSelect.appendChild(option);
});
}
} catch (error) {
console.error('加载机器人角色失败:', error);
}
}
// 初始化图片上传表单 // 初始化图片上传表单
function initUploadImageForm() { function initUploadImageForm() {
const form = document.getElementById('uploadImageForm'); const form = document.getElementById('uploadImageForm');
@ -96,7 +44,7 @@ function initUploadImageForm() {
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 上传中...'; submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 上传中...';
try { try {
const response = await fetch('/imgsearcherApi/api/upload-chat-image', { const response = await fetch('/api/upload-chat-image', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
@ -126,31 +74,14 @@ function initUploadImageForm() {
// 显示聊天界面 // 显示聊天界面
chatContainer.classList.remove('d-none'); chatContainer.classList.remove('d-none');
// 显示欢迎消息 // 添加欢迎消息
const chatMessages = document.getElementById('chatMessages'); const chatMessages = document.getElementById('chatMessages');
chatMessages.innerHTML = ''; chatMessages.innerHTML = '';
const welcomeMessage = document.createElement('div'); const welcomeMessage = document.createElement('div');
welcomeMessage.className = 'message message-assistant'; welcomeMessage.className = 'message message-assistant';
let welcomeText = ''; let welcomeText = '你好我是AI助手可以帮你分析这张图片并回答问题。';
// 如果选择了机器人角色,使用机器人的自我介绍
if (data.robot) {
welcomeText = `你好!我是${data.robot.name}`;
// 存储机器人信息到页面数据中
document.getElementById('chatContainer').dataset.robotName = data.robot.name;
// 显示机器人信息在界面上
const robotInfoElement = document.createElement('div');
robotInfoElement.className = 'alert alert-info mt-2 mb-3';
robotInfoElement.innerHTML = `<strong>当前角色:${data.robot.name}</strong><br><small>${data.robot.background}</small>`;
document.getElementById('chatContainer').prepend(robotInfoElement);
} else {
welcomeText = '你好我是AI助手';
}
welcomeText += '可以帮你分析这张图片并回答问题。';
if (data.image_type && data.description) { if (data.image_type && data.description) {
welcomeText += `我看到这是一张${data.image_type}的图片,${data.description}。你有什么想问的吗?`; welcomeText += `我看到这是一张${data.image_type}的图片,${data.description}。你有什么想问的吗?`;
} else { } else {
@ -205,87 +136,28 @@ function initChatForm() {
chatMessages.scrollTop = chatMessages.scrollHeight; chatMessages.scrollTop = chatMessages.scrollHeight;
try { try {
// 使用流式接口 const response = await fetch('/api/chat', {
const response = await fetch('/imgsearcherApi/api/chat-stream', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
message: message, message: message
voice: document.getElementById('voiceSelect') ? document.getElementById('voiceSelect').value : ''
}) })
}); });
if (!response.ok) { const data = await response.json();
chatMessages.removeChild(typingIndicator);
alert('对话失败'); // 移除正在输入指示器
chatMessages.removeChild(typingIndicator);
if (data.error) {
alert(`对话失败: ${data.error}`);
return; return;
} }
// 创建助手消息占位 // 添加AI回复到聊天界面
const assistantElement = document.createElement('div'); addMessage('assistant', data.reply);
assistantElement.className = 'message message-assistant';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
assistantElement.appendChild(contentDiv);
// 存储该消息对应的音频片段集合
const audioSegments = [];
const timeDiv = document.createElement('div');
timeDiv.className = 'message-time';
timeDiv.textContent = getCurrentTime();
assistantElement.appendChild(timeDiv);
chatMessages.appendChild(assistantElement);
chatMessages.scrollTop = chatMessages.scrollHeight;
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buf = '';
let streamEnded = false;
while (!streamEnded) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let pos;
while ((pos = buf.indexOf('\n\n')) !== -1) {
const raw = buf.slice(0, pos).trim();
buf = buf.slice(pos + 2);
if (!raw) continue;
if (raw.startsWith('event: end')) {
chatMessages.removeChild(typingIndicator);
streamEnded = true;
// 为该消息添加重放按钮
if (audioSegments.length > 0) {
const replayBtn = document.createElement('button');
replayBtn.className = 'btn btn-sm btn-outline-secondary ms-2 replay-btn';
replayBtn.innerHTML = '<i class="bi bi-play-circle"></i> 重放音频';
replayBtn.addEventListener('click', () => { playAudioSegments(audioSegments); });
assistantElement.appendChild(replayBtn);
}
}
if (!raw.startsWith('data:')) continue;
const payload = raw.slice(5).trim();
if (!payload) continue;
try {
const obj = JSON.parse(payload);
if (obj.type === 'text') {
contentDiv.innerHTML += formatMessage(obj.content);
chatMessages.scrollTop = chatMessages.scrollHeight;
} else if (obj.type === 'audio') {
const audioBytes = Uint8Array.from(atob(obj.content), c => c.charCodeAt(0));
const audioBlob = new Blob([audioBytes], { type: 'audio/wav' });
audioSegments.push(audioBlob);
enqueueAudio(audioBlob);
}
} catch (e) {
console.error('解析事件失败', e, payload);
}
}
}
} catch (error) { } catch (error) {
// 移除正在输入指示器 // 移除正在输入指示器
@ -299,7 +171,7 @@ function initChatForm() {
} }
// 添加消息到聊天界面 // 添加消息到聊天界面
async function addMessage(role, content) { function addMessage(role, content) {
const chatMessages = document.getElementById('chatMessages'); const chatMessages = document.getElementById('chatMessages');
const messageElement = document.createElement('div'); const messageElement = document.createElement('div');
@ -312,9 +184,6 @@ async function addMessage(role, content) {
chatMessages.appendChild(messageElement); chatMessages.appendChild(messageElement);
// 滚动到底部 // 滚动到底部
chatMessages.scrollTop = chatMessages.scrollHeight; chatMessages.scrollTop = chatMessages.scrollHeight;
} }

View File

@ -89,7 +89,7 @@ function initUploadForm() {
formData.append('tags', tags); formData.append('tags', tags);
try { try {
const response = await fetch('/imgsearcherApi/upload', { const response = await fetch('/upload', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
@ -182,7 +182,7 @@ function initSearchForm() {
batchActions.classList.add('d-none'); batchActions.classList.add('d-none');
reliableTypesInfo.classList.add('d-none'); reliableTypesInfo.classList.add('d-none');
fetch('/imgsearcherApi/search', { fetch('/search', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
@ -321,7 +321,7 @@ function initUpdateForm() {
}; };
// 发送更新请求 // 发送更新请求
fetch('/imgsearcherApi/update', { fetch('/update', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -364,7 +364,7 @@ function initDeleteForm() {
const contSign = document.getElementById('deleteContSign').value; const contSign = document.getElementById('deleteContSign').value;
// 发送删除请求 // 发送删除请求
fetch('/imgsearcherApi/delete', { fetch('/delete', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -490,7 +490,7 @@ async function batchDelete(selectedItems) {
const contSign = selectedItems[i].getAttribute('data-cont-sign'); const contSign = selectedItems[i].getAttribute('data-cont-sign');
try { try {
const response = await fetch('/imgsearcherApi/delete', { const response = await fetch('/delete', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'

View File

@ -1,297 +0,0 @@
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 初始化机器人表单
initRobotForm();
// 加载机器人列表
loadRobots();
// 初始化编辑和删除功能
initEditRobotModal();
initDeleteRobotModal();
});
// 初始化机器人表单
function initRobotForm() {
const form = document.getElementById('robotForm');
const avatarInput = document.getElementById('robotAvatar');
const avatarPreview = document.getElementById('avatarPreview');
const avatarPreviewContainer = document.querySelector('#robotForm .avatar-preview');
// 头像预览
avatarInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
avatarPreview.src = e.target.result;
avatarPreviewContainer.classList.remove('d-none');
};
reader.readAsDataURL(this.files[0]);
} else {
avatarPreviewContainer.classList.add('d-none');
}
});
// 表单提交
form.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
// 显示提交中状态
const submitButton = form.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 添加中...';
try {
const response = await fetch('/imgsearcherApi/api/robots', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
alert(`添加失败: ${data.error}`);
return;
}
// 重置表单
form.reset();
avatarPreviewContainer.classList.add('d-none');
// 刷新机器人列表
loadRobots();
// 显示成功消息
alert('机器人角色添加成功!');
} catch (error) {
alert(`添加失败: ${error.message}`);
} finally {
// 恢复按钮状态
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
}
});
}
// 加载机器人列表
async function loadRobots() {
const robotsList = document.getElementById('robotsList');
const loadingIndicator = document.getElementById('loadingIndicator');
const noRobotsMessage = document.getElementById('noRobotsMessage');
// 显示加载指示器
loadingIndicator.classList.remove('d-none');
noRobotsMessage.classList.add('d-none');
// 清除现有的机器人卡片(保留标题和加载指示器)
const existingCards = robotsList.querySelectorAll('.robot-card-container');
existingCards.forEach(card => card.remove());
try {
const response = await fetch('/imgsearcherApi/api/robots');
const data = await response.json();
// 隐藏加载指示器
loadingIndicator.classList.add('d-none');
if (!data.robots || data.robots.length === 0) {
noRobotsMessage.classList.remove('d-none');
return;
}
// 渲染机器人卡片
data.robots.forEach(robot => {
const robotCard = createRobotCard(robot);
robotsList.appendChild(robotCard);
});
} catch (error) {
console.error('加载机器人列表失败:', error);
loadingIndicator.classList.add('d-none');
alert(`加载机器人列表失败: ${error.message}`);
}
}
// 创建机器人卡片
function createRobotCard(robot) {
const cardContainer = document.createElement('div');
cardContainer.className = 'col-md-4 mb-4 robot-card-container';
const avatarHtml = robot.avatar_data
? `<img src="data:image/jpeg;base64,${robot.avatar_data}" alt="${robot.name}" class="robot-avatar">`
: `<div class="robot-avatar-placeholder"><i class="bi bi-person"></i></div>`;
cardContainer.innerHTML = `
<div class="card robot-card">
<div class="card-body text-center">
${avatarHtml}
<h5 class="card-title">${robot.name}</h5>
<div class="robot-background text-start mb-3">
<small class="text-muted">${robot.background}</small>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary edit-robot" data-robot-id="${robot.robot_id}">
<i class="bi bi-pencil"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger delete-robot" data-robot-id="${robot.robot_id}">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
`;
// 添加编辑按钮事件
cardContainer.querySelector('.edit-robot').addEventListener('click', function() {
openEditRobotModal(robot);
});
// 添加删除按钮事件
cardContainer.querySelector('.delete-robot').addEventListener('click', function() {
openDeleteRobotModal(robot.robot_id, robot.name);
});
return cardContainer;
}
// 初始化编辑机器人模态框
function initEditRobotModal() {
const editRobotForm = document.getElementById('editRobotForm');
const saveChangesButton = document.getElementById('saveRobotChanges');
const editRobotAvatar = document.getElementById('editRobotAvatar');
const editAvatarPreview = document.getElementById('editAvatarPreview');
// 头像预览
editRobotAvatar.addEventListener('change', function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
editAvatarPreview.src = e.target.result;
};
reader.readAsDataURL(this.files[0]);
}
});
// 保存更改按钮点击事件
saveChangesButton.addEventListener('click', async function() {
const robotId = document.getElementById('editRobotId').value;
const formData = new FormData(editRobotForm);
// 显示保存中状态
const originalButtonText = saveChangesButton.innerHTML;
saveChangesButton.disabled = true;
saveChangesButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 保存中...';
try {
const response = await fetch(`/imgsearcherApi/api/robots/${robotId}`, {
method: 'PUT',
body: formData
});
const data = await response.json();
if (data.error) {
alert(`更新失败: ${data.error}`);
return;
}
// 关闭模态框
const editModal = bootstrap.Modal.getInstance(document.getElementById('editRobotModal'));
editModal.hide();
// 刷新机器人列表
loadRobots();
// 显示成功消息
alert('机器人角色更新成功!');
} catch (error) {
alert(`更新失败: ${error.message}`);
} finally {
// 恢复按钮状态
saveChangesButton.disabled = false;
saveChangesButton.innerHTML = originalButtonText;
}
});
}
// 打开编辑机器人模态框
function openEditRobotModal(robot) {
document.getElementById('editRobotId').value = robot.robot_id;
document.getElementById('editRobotName').value = robot.name;
document.getElementById('editRobotBackground').value = robot.background;
// 设置头像预览
if (robot.avatar_data) {
document.getElementById('editAvatarPreview').src = `data:image/jpeg;base64,${robot.avatar_data}`;
} else {
document.getElementById('editAvatarPreview').src = '';
}
// 打开模态框
const editModal = new bootstrap.Modal(document.getElementById('editRobotModal'));
editModal.show();
}
// 初始化删除机器人模态框
function initDeleteRobotModal() {
const confirmDeleteButton = document.getElementById('confirmDeleteRobot');
confirmDeleteButton.addEventListener('click', async function() {
const robotId = document.getElementById('deleteRobotId').value;
// 显示删除中状态
const originalButtonText = confirmDeleteButton.innerHTML;
confirmDeleteButton.disabled = true;
confirmDeleteButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 删除中...';
try {
const response = await fetch(`/imgsearcherApi/api/robots/${robotId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.error) {
alert(`删除失败: ${data.error}`);
return;
}
// 关闭模态框
const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteRobotModal'));
deleteModal.hide();
// 刷新机器人列表
loadRobots();
// 显示成功消息
alert('机器人角色删除成功!');
} catch (error) {
alert(`删除失败: ${error.message}`);
} finally {
// 恢复按钮状态
confirmDeleteButton.disabled = false;
confirmDeleteButton.innerHTML = originalButtonText;
}
});
}
// 打开删除机器人模态框
function openDeleteRobotModal(robotId, robotName) {
document.getElementById('deleteRobotId').value = robotId;
document.getElementById('deleteRobotModalLabel').textContent = `确认删除 "${robotName}"`;
document.querySelector('#deleteRobotModal .modal-body p').textContent = `确定要删除机器人角色 "${robotName}" 吗?此操作不可撤销。`;
// 打开模态框
const deleteModal = new bootstrap.Modal(document.getElementById('deleteRobotModal'));
deleteModal.show();
}

View File

@ -15,7 +15,7 @@
<h1>图片智能对话系统</h1> <h1>图片智能对话系统</h1>
<p class="lead">上传图片与AI进行多轮对话</p> <p class="lead">上传图片与AI进行多轮对话</p>
<nav class="mt-3"> <nav class="mt-3">
<a href="/imgsearcherApi" class="btn btn-outline-secondary">返回首页</a> <a href="/" class="btn btn-outline-secondary">返回首页</a>
</nav> </nav>
</header> </header>
@ -25,44 +25,12 @@
<div class="card-body"> <div class="card-body">
<h5 class="card-title">上传图片</h5> <h5 class="card-title">上传图片</h5>
<form id="uploadImageForm" enctype="multipart/form-data"> <form id="uploadImageForm" enctype="multipart/form-data">
<div class="row"> <div class="mb-3">
<div class="col-md-6"> <label for="chatImageFile" class="form-label">选择图片</label>
<div class="mb-3"> <input class="form-control" type="file" id="chatImageFile" name="file" accept="image/*" required>
<label for="chatImageFile" class="form-label">选择图片</label> <div class="form-text">支持JPG、PNG、JPEG、GIF格式</div>
<input class="form-control" type="file" id="chatImageFile" name="file" accept="image/*" required>
<div class="form-text">支持JPG、PNG、JPEG、GIF格式</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="robotSelect" class="form-label">选择机器人角色(可选)</label>
<select class="form-select" id="robotSelect" name="robot_id">
<option value="">默认AI助手</option>
<!-- 机器人选项将在这里动态添加 -->
</select>
<div class="form-text">选择一个机器人角色进行对话</div>
</div>
</div>
</div>
<!-- 语音音色选择 -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="voiceSelect" class="form-label">选择音色</label>
<select class="form-select" id="voiceSelect" name="voice">
<option value="zh-CN-XiaoXiao-Assistant-Audio.wav" selected>小小助手</option>
<option value="zh-CN-XiaoXiao-Chat-Audio.wav">小小聊天</option>
<option value="zh-CN-YunYang-CustomerService-Audio.wav">云扬客服</option>
<option value="zh-CN-Yunxi-Boy-Default-Audio.wav">云希男孩</option>
<option value="zh-CN-Yunxi-Narrator-Narrative-Audio.wav">云希旁白</option>
</select>
</div>
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">上传并开始对话</button>
<a href="/imgsearcherApi/robots" class="btn btn-outline-secondary">管理机器人角色</a>
</div> </div>
<button type="submit" class="btn btn-primary">上传并开始对话</button>
</form> </form>
<div id="uploadPreview" class="mt-3 text-center d-none"> <div id="uploadPreview" class="mt-3 text-center d-none">
<div class="preview-container"> <div class="preview-container">

View File

@ -3,22 +3,22 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>地瓜&乐森变形金刚demo</title> <title>地瓜机器人相似图片搜索及分类识别系统</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="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') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header class="text-center my-5"> <header class="text-center my-5">
<h1>地瓜&乐森变形金刚demo</h1> <h1>地瓜机器人相似图片搜索及分类识别demo</h1>
<p class="lead">基于图片相似度的demo演示</p> <p class="lead">基于图片相似度进行分类的demo演示</p>
</header> </header>
<div class="row"> <div class="row">
<div class="col-md-12 mb-4"> <div class="col-md-12 mb-4">
<ul class="nav nav-tabs" id="myTab" role="tablist"> <ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button" role="tab" aria-controls="upload" aria-selected="true">待识别角色录入</button> <button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button" role="tab" aria-controls="upload" aria-selected="true">角色录入</button>
</li> </li>
<li class="nav-item" role="presentation"> <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> <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>
@ -27,18 +27,15 @@
<!-- <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> --> <!-- <button class="nav-link" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage" type="button" role="tab" aria-controls="manage" aria-selected="false">图库管理</button> -->
<!-- </li> --> <!-- </li> -->
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<a href="/imgsearcherApi/chat-with-image" class="nav-link" style="color: #0d6efd; font-weight: bold;">图片智能对话</a> <a href="/chat-with-image" class="nav-link" style="color: #0d6efd; font-weight: bold;">图片智能对话</a>
</li>
<li class="nav-item" role="presentation">
<a href="/imgsearcherApi/robots" class="nav-link" style="color: #198754; font-weight: bold;">机器人角色管理</a>
</li> </li>
</ul> </ul>
<div class="tab-content" id="myTabContent"> <div class="tab-content" id="myTabContent">
<!-- 待识别角色录入 --> <!-- 角色录入 -->
<div class="tab-pane fade show active" id="upload" role="tabpanel" aria-labelledby="upload-tab"> <div class="tab-pane fade show active" id="upload" role="tabpanel" aria-labelledby="upload-tab">
<div class="card mt-3"> <div class="card mt-3">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">待识别角色录入</h5> <h5 class="card-title">角色录入</h5>
<form id="uploadForm" enctype="multipart/form-data"> <form id="uploadForm" enctype="multipart/form-data">
<div class="mb-3"> <div class="mb-3">
<label for="uploadFile" class="form-label">选择图片(这里可以多选的,也就是批量入库)</label> <label for="uploadFile" class="form-label">选择图片(这里可以多选的,也就是批量入库)</label>
@ -46,17 +43,15 @@
<div class="form-text">支持JPG、PNG、BMP格式最短边至少50px最长边最大4096px</div> <div class="form-text">支持JPG、PNG、BMP格式最短边至少50px最长边最大4096px</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="imageType" class="form-label">角色名称</label> <label for="imageType" class="form-label">类型这个实际就是宠物名类似小狗pet、小猫pussy之类的</label>
<input type="text" class="form-control" id="imageType" name="type" required> <input type="text" class="form-control" id="imageType" name="type" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="imageDescription" class="form-label">描述(角色卡信息)</label> <label for="imageDescription" class="form-label">描述(目前demo只支持250个字符后面正式版可以扩充这个实际是角色卡信息,也就是宠物的描述,类似小狗是金毛之类的</label>
<textarea class="form-control" id="imageDescription" name="description" rows="2" required></textarea> <textarea class="form-control" id="imageDescription" name="description" rows="2" required></textarea>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="imageName" class="form-label">图片名称 <label for="imageName" class="form-label">名称(这个实际是图片名称,直接和宠物名重名即可,我会给加个名字)</label>
</label>
<input type="text" class="form-control" id="imageName" name="name" required> <input type="text" class="form-control" id="imageName" name="name" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View File

@ -1,186 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>机器人角色管理 - 地瓜机器人</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
.robot-card {
height: 100%;
transition: transform 0.3s;
}
.robot-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.robot-avatar {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
margin-bottom: 15px;
}
.robot-avatar-placeholder {
width: 100px;
height: 100px;
border-radius: 50%;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
font-size: 2rem;
color: #6c757d;
}
.robot-background {
max-height: 150px;
overflow-y: auto;
}
#robotForm .avatar-preview {
width: 100px;
height: 100px;
border-radius: 50%;
overflow: hidden;
margin-top: 10px;
}
#robotForm .avatar-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
</head>
<body>
<div class="container">
<header class="text-center my-4">
<h1>机器人角色管理</h1>
<p class="lead">创建和管理您的机器人角色</p>
<nav class="mt-3">
<a href="/imgsearcherApi" class="btn btn-outline-secondary me-2">返回首页</a>
<a href="/imgsearcherApi/chat-with-image" class="btn btn-outline-primary">图片智能对话</a>
</nav>
</header>
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">添加新机器人角色</h5>
<form id="robotForm" enctype="multipart/form-data">
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="robotName" class="form-label">机器人名称</label>
<input type="text" class="form-control" id="robotName" name="name" required>
</div>
<div class="mb-3">
<label for="robotAvatar" class="form-label">头像(可选)</label>
<input class="form-control" type="file" id="robotAvatar" name="avatar" accept="image/*">
<div class="avatar-preview d-none mt-2">
<img id="avatarPreview" src="" alt="头像预览">
</div>
</div>
</div>
<div class="col-md-8">
<div class="mb-3">
<label for="robotBackground" class="form-label">背景故事</label>
<textarea class="form-control" id="robotBackground" name="background" rows="5" required></textarea>
<div class="form-text">描述机器人的性格、背景和特点,这些信息将用于图片对话中</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> 添加机器人
</button>
</form>
</div>
</div>
</div>
</div>
<div class="row" id="robotsList">
<div class="col-12 mb-4">
<h3>现有机器人角色</h3>
<div class="text-center py-5 d-none" id="loadingIndicator">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">加载机器人角色...</p>
</div>
<div class="alert alert-info d-none" id="noRobotsMessage">
暂无机器人角色,请添加一个新的机器人角色。
</div>
</div>
<!-- 机器人卡片将在这里动态生成 -->
</div>
</div>
<!-- 编辑机器人模态框 -->
<div class="modal fade" id="editRobotModal" tabindex="-1" aria-labelledby="editRobotModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editRobotModalLabel">编辑机器人角色</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editRobotForm" enctype="multipart/form-data">
<input type="hidden" id="editRobotId">
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="editRobotName" class="form-label">机器人名称</label>
<input type="text" class="form-control" id="editRobotName" name="name" required>
</div>
<div class="mb-3">
<label for="editRobotAvatar" class="form-label">头像(可选)</label>
<input class="form-control" type="file" id="editRobotAvatar" name="avatar" accept="image/*">
<div class="avatar-preview mt-2">
<img id="editAvatarPreview" src="" alt="头像预览">
</div>
</div>
</div>
<div class="col-md-8">
<div class="mb-3">
<label for="editRobotBackground" class="form-label">背景故事</label>
<textarea class="form-control" id="editRobotBackground" name="background" rows="5" required></textarea>
</div>
</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="saveRobotChanges">保存更改</button>
</div>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div class="modal fade" id="deleteRobotModal" tabindex="-1" aria-labelledby="deleteRobotModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteRobotModalLabel">确认删除</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="deleteRobotId">
</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="confirmDeleteRobot">删除</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/robots.js') }}"></script>
</body>
</html>

View File

@ -1,43 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: imgsearcher-api
namespace: bj2-dcloud
labels:
app: imgsearcher-api
spec:
replicas: 1
selector:
matchLabels:
app: imgsearcher-api
template:
metadata:
labels:
app: imgsearcher-api
spec:
imagePullSecrets:
- name: ccr
containers:
- name: imgsearcher-api
image: ccr-29eug8s3-vpc.cnc.bj.baidubce.com/service/imgsearcher:${DRONE_COMMIT_SHA}
ports:
- containerPort: 5001
env:
- name: BAIDU_API_KEY
value: "cqn5SYRZAKM9WBXgYg8niUdM"
- name: BAIDU_SECRET_KEY
value: "vG1VNzQPsz7G8sogjMm9vCDU7LUawRnD"
- name: APP_ID
value: "6691505"
- name: AZURE_OPENAI_API_KEY
value: "AMbAvhIHJIyKdNUeSMYBkM9wf4ISBoYrNUW7g0L5lwCSiETBGCpsJQQJ99AKACHYHv6XJ3w3AAAAACOGS84A"
- name: AZURE_OPENAI_ENDPOINT
value: "https://admin-m3wql277-eastus2.cognitiveservices.azure.com"
- name: AZURE_OPENAI_API_VERSION
value: "2023-12-01-preview"
- name: AZURE_OPENAI_DEPLOYMENT_NAME
value: "gpt-4o-drobotics"
- name: MONGO_URI
value: "mongodb://root:12345678a!@wIBH1rV4a.mongodb.bj.baidubce.com/imgsearcher?authSource=admin"
- name: MONGO_DB_NAME
value: "imgsearcher"

View File

@ -1,29 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
# Ingress的名称
name: imgsearcher-ingress
# 部署的命名空间
namespace: bj2-dcloud
# Ingress的具体配置
spec:
# 指定使用nginx作为Ingress控制器
ingressClassName: nginx
# 定义路由规则
rules:
# 指定域名规则
- host: imagechat.d-robotircs.cc # 访问域名,根据项目
# HTTP规则配置
http:
# 路径配置列表
paths:
# 单个路径规则配置
- path: /imgsearcherApi/ # URL路径前缀
pathType: Prefix # 路径匹配类型Prefix表示前缀匹配
# 后端服务配置
backend:
# 服务配置
service:
name: imgsearcher-api-service # 后端Service名称
port:
number: 80 # 后端Service端口

View File

@ -1,13 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: imgsearcher-api-service
namespace: bj2-dcloud
spec:
selector:
app: imgsearcher-api
ports:
- protocol: TCP
port: 80
targetPort: 5001
type: ClusterIP

113
k8s-deployment.yaml Normal file
View File

@ -0,0 +1,113 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: imgsearcher
namespace: imgsearcher
labels:
app: imgsearcher
spec:
replicas: 2
selector:
matchLabels:
app: imgsearcher
template:
metadata:
labels:
app: imgsearcher
spec:
containers:
- name: imgsearcher
image: ${DRONE_REPO_OWNER}/imgsearcher:${DRONE_COMMIT_SHA:0:8}
ports:
- containerPort: 5001
env:
- name: MONGO_URI
valueFrom:
secretKeyRef:
name: imgsearcher-secrets
key: mongo-uri
- name: BAIDU_API_KEY
valueFrom:
secretKeyRef:
name: imgsearcher-secrets
key: baidu-api-key
- name: BAIDU_SECRET_KEY
valueFrom:
secretKeyRef:
name: imgsearcher-secrets
key: baidu-secret-key
- name: AZURE_API_KEY
valueFrom:
secretKeyRef:
name: imgsearcher-secrets
key: azure-api-key
- name: AZURE_API_ENDPOINT
valueFrom:
secretKeyRef:
name: imgsearcher-secrets
key: azure-api-endpoint
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "200m"
memory: "256Mi"
volumeMounts:
- name: uploads
mountPath: /app/uploads
volumes:
- name: uploads
persistentVolumeClaim:
claimName: imgsearcher-uploads-pvc
---
apiVersion: v1
kind: Service
metadata:
name: imgsearcher
namespace: imgsearcher
spec:
selector:
app: imgsearcher
ports:
- port: 80
targetPort: 5001
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: imgsearcher
namespace: imgsearcher
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
rules:
- host: imgsearcher.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: imgsearcher
port:
number: 80
tls:
- hosts:
- imgsearcher.example.com
secretName: imgsearcher-tls
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: imgsearcher-uploads-pvc
namespace: imgsearcher
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: standard

468
poetry.lock generated
View File

@ -1,141 +1,42 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "blinker"
version = "1.9.0"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"},
{file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"},
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2025.1.31" version = "2023.5.7"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"},
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"},
] ]
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.1" version = "2.0.12"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.5.0"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
{file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
{file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
{file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
{file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
{file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
{file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
{file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
{file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
{file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
{file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
{file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
{file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
{file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
{file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
{file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
{file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
{file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
{file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
{file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
{file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
{file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
{file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
{file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
{file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
{file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
{file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
{file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
{file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
{file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
{file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
{file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
] ]
[package.extras]
unicode-backport = ["unicodedata2"]
[[package]] [[package]]
name = "click" name = "click"
version = "8.1.8" version = "8.1.3"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
] ]
[package.dependencies] [package.dependencies]
@ -156,23 +57,21 @@ files = [
[[package]] [[package]]
name = "flask" name = "flask"
version = "2.3.3" version = "2.0.1"
description = "A simple framework for building complex web applications." description = "A simple framework for building complex web applications."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.6"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"}, {file = "Flask-2.0.1-py3-none-any.whl", hash = "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"},
{file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"}, {file = "Flask-2.0.1.tar.gz", hash = "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55"},
] ]
[package.dependencies] [package.dependencies]
blinker = ">=1.6.2" click = ">=7.1.2"
click = ">=8.1.3" itsdangerous = ">=2.0"
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0"
itsdangerous = ">=2.1.2" Werkzeug = ">=2.0"
Jinja2 = ">=3.1.2"
Werkzeug = ">=2.3.7"
[package.extras] [package.extras]
async = ["asgiref (>=3.2)"] async = ["asgiref (>=3.2)"]
@ -196,66 +95,38 @@ Six = "*"
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.5"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
] ]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "importlib-metadata"
version = "8.6.1"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version == \"3.9\""
files = [
{file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"},
{file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"},
]
[package.dependencies]
zipp = ">=3.20"
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
perf = ["ipython"]
test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
type = ["pytest-mypy"]
[[package]] [[package]]
name = "itsdangerous" name = "itsdangerous"
version = "2.2.0" version = "2.1.2"
description = "Safely pass data to untrusted environments and back." description = "Safely pass data to untrusted environments and back."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
] ]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.6" version = "3.1.2"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
] ]
[package.dependencies] [package.dependencies]
@ -266,136 +137,123 @@ i18n = ["Babel (>=2.7)"]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "3.0.2" version = "2.1.2"
description = "Safely add untrusted strings to HTML/XML markup." description = "Safely add untrusted strings to HTML/XML markup."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, {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-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
{file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
{file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, {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-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
{file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
{file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, {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-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
{file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
{file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, {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-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
{file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
{file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, {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-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
{file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
{file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
{file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
] ]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "8.4.0" version = "8.3.1"
description = "Python Imaging Library (Fork)" description = "Python Imaging Library (Fork)"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"},
{file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"}, {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"},
{file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"}, {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"},
{file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"}, {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"},
{file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"}, {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"},
{file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"}, {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"},
{file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"},
{file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"},
{file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37"},
{file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"}, {file = "Pillow-8.3.1-cp36-cp36m-win32.whl", hash = "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229"},
{file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"}, {file = "Pillow-8.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de"},
{file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"}, {file = "Pillow-8.3.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14"},
{file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"}, {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab"},
{file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"}, {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf"},
{file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"}, {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0"},
{file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"}, {file = "Pillow-8.3.1-cp37-cp37m-win32.whl", hash = "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba"},
{file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"}, {file = "Pillow-8.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500"},
{file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"}, {file = "Pillow-8.3.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4"},
{file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"}, {file = "Pillow-8.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e"},
{file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"}, {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79"},
{file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"}, {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae"},
{file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"}, {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844"},
{file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"}, {file = "Pillow-8.3.1-cp38-cp38-win32.whl", hash = "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367"},
{file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"}, {file = "Pillow-8.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8"},
{file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"}, {file = "Pillow-8.3.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e"},
{file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"}, {file = "Pillow-8.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856"},
{file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"}, {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82"},
{file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"}, {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc"},
{file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"}, {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83"},
{file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"}, {file = "Pillow-8.3.1-cp39-cp39-win32.whl", hash = "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d"},
{file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"}, {file = "Pillow-8.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8"},
{file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"}, {file = "Pillow-8.3.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf"},
{file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"}, {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63"},
{file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"}, {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636"},
{file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"}, {file = "Pillow-8.3.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d"},
{file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"}, {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77"},
{file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"}, {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8"},
{file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"}, {file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"},
{file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"}, {file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"},
{file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"},
{file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"},
] ]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "0.19.2" version = "0.19.0"
description = "Read key-value pairs from a .env file and set them as environment variables" description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"}, {file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"},
{file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"}, {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"},
] ]
[package.extras] [package.extras]
@ -403,94 +261,72 @@ cli = ["click (>=5.0)"]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.26.0"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
] ]
[package.dependencies] [package.dependencies]
certifi = ">=2017.4.17" certifi = ">=2017.4.17"
charset-normalizer = ">=2,<4" charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
idna = ">=2.5,<4" idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
urllib3 = ">=1.21.1,<3" urllib3 = ">=1.21.1,<1.27"
[package.extras] [package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"] 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,<6)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"]
[[package]] [[package]]
name = "six" name = "six"
version = "1.17.0" version = "1.16.0"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
] ]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.4.0" version = "1.26.16"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"},
{file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"},
] ]
[package.extras] [package.extras]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 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\""]
h2 = ["h2 (>=4,<5)"] 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)"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "werkzeug" name = "werkzeug"
version = "3.1.3" version = "2.2.3"
description = "The comprehensive WSGI web application library." description = "The comprehensive WSGI web application library."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, {file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"},
{file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, {file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"},
] ]
[package.dependencies] [package.dependencies]
MarkupSafe = ">=2.1.1" MarkupSafe = ">=2.1.1"
[package.extras] [package.extras]
watchdog = ["watchdog (>=2.3)"] watchdog = ["watchdog"]
[[package]]
name = "zipp"
version = "3.21.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version == \"3.9\""
files = [
{file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
{file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
type = ["pytest-mypy"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"

View File

@ -1,36 +1,6 @@
# 核心Web框架 flask==2.0.1
Flask==2.3.3 requests==2.26.0
Flask-Cors==3.0.10 python-dotenv==0.19.0
Werkzeug==3.1.3 Pillow==8.3.1
flask-cors==3.0.10
# 图像处理
Pillow==8.4.0
# 数据库
pymongo==4.5.0 pymongo==4.5.0
dnspython==2.7.0
# HTTP请求
requests==2.32.3
# 环境变量管理
python-dotenv==0.19.2
# 其他工具
click==8.1.8
blinker==1.9.0
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
six==1.17.0
sniffio==1.3.1
tomli==2.2.1
tomlkit==0.13.2
trove-classifiers==2025.3.19.19
typing_extensions==4.13.1
urllib3==2.4.0
virtualenv==20.30.0
Werkzeug==3.1.3
xattr==1.1.4
zipp==3.21.0
zstandard==0.23.0