commit d09923d1922f64e5bbf4b386de3b2aeec1d4a42c Author: kyj@bowong.ai Date: Wed Jul 30 11:47:17 2025 +0800 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6bd04b3 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# GUI小工具集 + +- video_gen.py 调用n8n生成视频小工具 +- tts.py 调用Minimax生成语音 +- comfyui_gui.py 调用自定义comfyui执行换装任务 + +## 打包指令 + nuitka --standalone --onefile --windows-console-mode=disable --plugin-enable=pyside6 --windows-icon-from-ico=./static/video.ico --include-data-file=static/SourceHanSansCN-Medium.ttf=SourceHanSansCN-Medium.ttf video_gen.py + nuitka --standalone --onefile --windows-console-mode=disable --plugin-enable=pyside6 --windows-icon-from-ico=./static/video.ico tts.py + nuitka --standalone --onefile --windows-console-mode=disable --plugin-enable=pyside6 --windows-icon-from-ico=./static/comfyui_hz.ico --include-data-file=static/comfyui_hz.ico=comfyui_hz.ico comfyui_gui.py \ No newline at end of file diff --git a/comfyui_gui.py b/comfyui_gui.py new file mode 100644 index 0000000..775e6c7 --- /dev/null +++ b/comfyui_gui.py @@ -0,0 +1,572 @@ +import sys +import os +import json +import time +import requests +import copy +import uuid +import configparser +from pathlib import Path +from urllib.parse import quote + +from PySide6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLineEdit, QPushButton, QComboBox, QPlainTextEdit, QGroupBox, + QFormLayout, QFileDialog, QMessageBox, QStatusBar, QProgressBar, + QDialog, QLabel, QDialogButtonBox, QStyle, QListWidget, QListWidgetItem +) +from PySide6.QtCore import Qt, QThread, Signal, QObject, QSettings +from PySide6.QtGui import QIcon, QPixmap + + +# --- 配置管理类 --- +class ConfigManager: + def __init__(self, filename='config.ini'): + self.filename, self.config = filename, configparser.ConfigParser() + self.config.read(self.filename) + + def get(self, section, key, fallback=None): return self.config.get(section, key, fallback=fallback) + + def set(self, section, key, value): + if not self.config.has_section(section): self.config.add_section(section) + self.config.set(section, key, str(value)) + + def save(self): + with open(self.filename, 'w') as configfile: self.config.write(configfile) + + +# --- 辅助函数:将工作流图谱格式转换为API提示格式 --- +def convert_graph_to_prompt(workflow_graph): + if 'nodes' not in workflow_graph or 'links' not in workflow_graph: return None + prompt, nodes_by_id = {}, {str(node['id']): node for node in workflow_graph['nodes']} + for node_id, node_data in nodes_by_id.items(): + inputs, widget_values, widget_index = {}, node_data.get('widgets_values', []), 0 + if 'inputs' in node_data: + for an_input in node_data['inputs']: + if an_input.get('widget') and widget_index < len(widget_values): + inputs[an_input['name']] = widget_values[widget_index] + widget_index += 1 + for link_info in workflow_graph['links']: + target_node_id_str, source_node_id_str = str(link_info[3]), str(link_info[1]) + if target_node_id_str == node_id: + source_slot_idx, target_slot_idx = link_info[2], link_info[4] + if 'inputs' in node_data and target_slot_idx < len(node_data['inputs']): + inputs[node_data['inputs'][target_slot_idx]['name']] = [source_node_id_str, source_slot_idx] + prompt[node_id] = {"class_type": node_data['type'], "inputs": inputs} + return prompt + + +# --- API通信类 --- +class ComfyAPI: + def __init__(self, host, port): + self.base_url, self.client_id = f"http://{host}:{port}", str(uuid.uuid4()) + + def _request(self, method, endpoint, **kwargs): + try: + response = requests.request(method, f"{self.base_url}{endpoint}", timeout=10, **kwargs) + response.raise_for_status() + return response.json(), None + except requests.exceptions.RequestException as e: + return None, str(e) + + def get_workflows(self): + return self._request("get", "/api/userdata?dir=workflows&recurse=true&split=false&full_info=true") + + def get_workflow_details(self, workflow_name): + return self._request("get", f"/api/userdata/workflows%2F{quote(workflow_name)}") + + def get_queue(self): + return self._request("get", "/api/queue") + + def queue_prompt(self, prompt, workflow_graph): + payload = {"prompt": prompt, "extra_data": {"extra_pnginfo": {"workflow": workflow_graph}}, + "client_id": self.client_id} + return self._request("post", "/api/prompt", json=payload) + + +# --- 后台执行线程 --- +class ExecutionWorker(QObject): + log, progress, finished = Signal(str), Signal(int, int), Signal(bool, str) + status_update = Signal(str) # FIX 2: New signal for status bar + + def __init__(self, api, workflow_prompt, workflow_graph, model_image_path, model_node_id, + clothing_dir, clothing_node_id, output_dir, output_node_id): + super().__init__() + self.api, self.workflow_prompt, self.workflow_graph = api, workflow_prompt, workflow_graph + self.model_image_path, self.model_node_id = Path(model_image_path), model_node_id + self.clothing_dir, self.clothing_node_id = Path(clothing_dir), clothing_node_id + self.output_dir, self.output_node_id = Path(output_dir), output_node_id + self._is_running = True + + def stop(self): + self._is_running = False; self.log.emit("执行停止请求已发送...") + + def run(self): + image_extensions = ['.png', '.jpg', '.jpeg', '.webp', '.bmp'] + try: + clothing_files = [f for f in self.clothing_dir.iterdir() if + f.is_file() and f.suffix.lower() in image_extensions] + except FileNotFoundError: + return self.finished.emit(False, f"错误:找不到衣物文件夹 {self.clothing_dir}") + if not clothing_files: return self.finished.emit(False, f"在 {self.clothing_dir} 中没有找到有效的图片文件。") + total_files = len(clothing_files); + self.log.emit(f"找到 {total_files} 件衣物图片待处理。"); + self.progress.emit(0, total_files) + for i, clothing_path in enumerate(clothing_files): + if not self._is_running: break + self.log.emit( + f"--- 开始处理: {self.model_image_path.name} + {clothing_path.name} ({i + 1}/{total_files}) ---") + prompt = copy.deepcopy(self.workflow_prompt) + if self.model_node_id in prompt: + prompt[self.model_node_id]["inputs"]["image"] = str(self.model_image_path) + else: + return self.finished.emit(False, f"错误:模特节点ID '{self.model_node_id}' 在工作流中不存在!") + if self.clothing_node_id in prompt: + prompt[self.clothing_node_id]["inputs"]["image"] = str(clothing_path) + else: + return self.finished.emit(False, f"错误:衣物节点ID '{self.clothing_node_id}' 在工作流中不存在!") + if self.output_node_id in prompt: + prompt[self.output_node_id]["inputs"]["output_dir"] = str(self.output_dir) + prompt[self.output_node_id]["inputs"][ + "filename_prefix"] = f"{self.model_image_path.stem}_{clothing_path.stem}" + else: + return self.finished.emit(False, f"错误:输出节点ID '{self.output_node_id}' 在工作流中不存在!") + response, error = self.api.queue_prompt(prompt, self.workflow_graph) + if error: self.log.emit(f"错误: 任务排队失败 - {error}"); continue + prompt_id = response['prompt_id']; + self.log.emit(f"任务已入队,ID: {prompt_id}") + + # FIX 2: New wait logic + logged_wait_for_task = False + while self._is_running: + queue_info, error = self.api.get_queue() + if error: self.status_update.emit(f"无法获取队列状态: {error}"); time.sleep(2); continue + + queue_running = queue_info.get('queue_running', []) + queue_pending = queue_info.get('queue_pending', []) + + is_running = any(item[1] == prompt_id for item in queue_running) + is_pending = any(item[1] == prompt_id for item in queue_pending) + + if not is_running and not is_pending: + self.status_update.emit(f"任务 {prompt_id} 已完成") + self.log.emit(f"任务 {prompt_id} 处理完成。") + break + + if not logged_wait_for_task: + self.log.emit(f"等待任务 {prompt_id} 完成...") + logged_wait_for_task = True + + if is_running: + self.status_update.emit(f"正在执行任务: {prompt_id}") + elif is_pending: + try: + # Find position in the pending queue + pending_ids = [item[1] for item in queue_pending] + position = pending_ids.index(prompt_id) + 1 + self.status_update.emit(f"队列等候中 (位置 {position}/{len(pending_ids)})") + except ValueError: + self.status_update.emit("正在更新队列状态...") + time.sleep(1) + + self.progress.emit(i + 1, total_files) + if self._is_running: + self.finished.emit(True, "全部任务执行完毕。") + else: + self.finished.emit(False, "执行被用户中止。") + + +# --- UI 组件类 --- +class ImagePreviewDialog(QDialog): + def __init__(self, image_path, parent=None): + super().__init__(parent); + self.setWindowTitle("模特预览") + self.image_label = QLabel(self); + self.image_label.setAlignment(Qt.AlignCenter) + pixmap = QPixmap(image_path); + scaled_pixmap = pixmap.scaled(800, 800, Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.image_label.setPixmap(scaled_pixmap); + layout = QVBoxLayout(self); + layout.addWidget(self.image_label) + button_box = QDialogButtonBox(QDialogButtonBox.Ok); + button_box.accepted.connect(self.accept) + layout.addWidget(button_box); + self.setLayout(layout); + self.adjustSize() + + +class SettingsDialog(QDialog): + def __init__(self, workflow_prompt, current_settings, parent=None): + super().__init__(parent); + self.setWindowTitle("工作流输入输出设置"); + self.setMinimumWidth(450) + self.workflow_prompt = workflow_prompt; + layout = QFormLayout(self) + self.model_node_combo = QComboBox(); + self.clothing_node_combo = QComboBox(); + self.output_node_combo = QComboBox() + for node_id, node_info in self.workflow_prompt.items(): + text = f"ID: {node_id} (类型: {node_info['class_type']})" + if "LoadImage" in node_info['class_type']: self.model_node_combo.addItem(text, + userData=node_id); self.clothing_node_combo.addItem( + text, userData=node_id) + if "Save" in node_info['class_type']: self.output_node_combo.addItem(text, userData=node_id) + if current_settings.get('model_node_id'): self.model_node_combo.setCurrentIndex( + self.model_node_combo.findData(current_settings['model_node_id'])) + if current_settings.get('clothing_node_id'): self.clothing_node_combo.setCurrentIndex( + self.clothing_node_combo.findData(current_settings['clothing_node_id'])) + if current_settings.get('output_node_id'): self.output_node_combo.setCurrentIndex( + self.output_node_combo.findData(current_settings['output_node_id'])) + layout.addRow("1. 模特加载节点 (LoadImage):", self.model_node_combo); + layout.addRow("2. 衣物加载节点 (LoadImage):", self.clothing_node_combo); + layout.addRow("3. 图像保存节点 (SaveImageAnywhere):", self.output_node_combo) + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel); + button_box.accepted.connect(self.accept); + button_box.rejected.connect(self.reject); + layout.addRow(button_box) + + def get_settings(self): + return {"model_node_id": self.model_node_combo.currentData(), + "clothing_node_id": self.clothing_node_combo.currentData(), + "output_node_id": self.output_node_combo.currentData()} + + +# --- 主窗口 --- +class ComfyUIController(QMainWindow): + def __init__(self): + super().__init__(); + self.setWindowTitle("ComfyUI 智能试衣控制器"); + self.setWindowIcon(QIcon(os.path.join(os.path.dirname(__file__), "comfyui_hz.ico"))); + self.setGeometry(100, 100, 1000, 700) + self.config = ConfigManager(os.path.join(os.path.dirname(os.path.dirname(__file__)), "config_hz.ini")); + self.api, self.workflow_settings = None, {} + self.current_workflow_prompt, self.current_workflow_graph = None, None + self.execution_thread, self.execution_worker = None, None + self._create_widgets(); + self._create_layouts(); + self._connect_signals(); + self._load_settings(); + self._load_models(); + self._update_ui_state() + + def _create_widgets(self): + self.model_group = QGroupBox("模特库"); + self.model_list_widget = QListWidget(); + self.add_model_button = QPushButton("✚ 添加模特"); + self.delete_model_button = QPushButton("✖ 删除选中") + self.server_group = QGroupBox("① 服务器连接"); + self.host_input = QLineEdit(); + self.port_input = QLineEdit(); + self.connect_button = QPushButton("连接") + self.workflow_group = QGroupBox("② 工作流选择"); + self.workflow_combo = QComboBox(); + self.refresh_button = QPushButton("🔄"); + self.settings_button = QPushButton("⚙️"); + self.workflow_tip_label = QLabel("提示:请选择一个包含两个LoadImage节点和1个SaveImageAnywhere节点的换装工作流。"); + self.workflow_tip_label.setStyleSheet("color: #e67e22;") + self.exec_group = QGroupBox("③ 执行配置"); + self.clothing_dir_input = QLineEdit(); + self.clothing_dir_button = QPushButton("浏览...") + self.output_group = QGroupBox("④ 输出设置"); + self.output_dir_input = QLineEdit(); + self.output_dir_button = QPushButton("浏览...") + self.action_group = QGroupBox("⑤ 开始执行"); + self.execute_button = QPushButton("▶️ 开始批量换装"); + self.stop_button = QPushButton("⏹️ 停止执行") + self.log_group = QGroupBox("执行日志"); + self.log_box = QPlainTextEdit(); + self.log_box.setReadOnly(True) + self.status_bar = QStatusBar(); + self.setStatusBar(self.status_bar); + self.progress_bar = QProgressBar(); + self.status_bar.addPermanentWidget(self.progress_bar) + + def _create_layouts(self): + model_layout, model_buttons_layout = QVBoxLayout(), QHBoxLayout(); + model_buttons_layout.addWidget(self.add_model_button); + model_buttons_layout.addWidget(self.delete_model_button); + model_layout.addWidget(self.model_list_widget); + model_layout.addLayout(model_buttons_layout); + self.model_group.setLayout(model_layout) + right_v_layout = QVBoxLayout() + server_h_layout = QHBoxLayout(); + server_h_layout.addWidget(QLabel("主机:")); + server_h_layout.addWidget(self.host_input, 1); + server_h_layout.addWidget(QLabel("端口:")); + server_h_layout.addWidget(self.port_input); + server_h_layout.addWidget(self.connect_button); + self.server_group.setLayout(server_h_layout) + workflow_layout, workflow_controls_layout = QVBoxLayout(), QHBoxLayout(); + workflow_controls_layout.addWidget(self.workflow_combo, 1); + workflow_controls_layout.addWidget(self.refresh_button); + workflow_controls_layout.addWidget(self.settings_button); + workflow_layout.addLayout(workflow_controls_layout); + workflow_layout.addWidget(self.workflow_tip_label); + self.workflow_group.setLayout(workflow_layout) + exec_form_layout = QFormLayout(); + clothing_layout = QHBoxLayout(); + clothing_layout.addWidget(self.clothing_dir_input, 1); + clothing_layout.addWidget(self.clothing_dir_button); + exec_form_layout.addRow("衣物文件夹:", clothing_layout); + self.exec_group.setLayout(exec_form_layout) + output_form_layout = QFormLayout(); + output_layout = QHBoxLayout(); + output_layout.addWidget(self.output_dir_input, 1); + output_layout.addWidget(self.output_dir_button); + output_form_layout.addRow("输出文件夹:", output_layout); + self.output_group.setLayout(output_form_layout) + action_layout = QHBoxLayout(); + action_layout.addStretch(); + action_layout.addWidget(self.execute_button); + action_layout.addWidget(self.stop_button); + self.action_group.setLayout(action_layout) + log_layout = QVBoxLayout(); + log_layout.addWidget(self.log_box); + self.log_group.setLayout(log_layout) + right_v_layout.addWidget(self.server_group); + right_v_layout.addWidget(self.workflow_group); + right_v_layout.addWidget(self.exec_group); + right_v_layout.addWidget(self.output_group); + right_v_layout.addWidget(self.action_group); + right_v_layout.addWidget(self.log_group, 1) + main_h_layout = QHBoxLayout(); + main_h_layout.setSpacing(15); + main_h_layout.addWidget(self.model_group, 1); + main_h_layout.addLayout(right_v_layout, 3) + central_widget = QWidget(); + central_widget.setLayout(main_h_layout); + self.setCentralWidget(central_widget) + + def _connect_signals(self): + self.add_model_button.clicked.connect(self._add_model); + self.delete_model_button.clicked.connect(self._delete_model); + self.model_list_widget.itemDoubleClicked.connect(self.show_model_preview) + self.connect_button.clicked.connect(self.connect_and_fetch_workflows); + self.refresh_button.clicked.connect(self.fetch_workflows); + self.workflow_combo.currentTextChanged.connect(self.fetch_workflow_details) + self.settings_button.clicked.connect(self.open_settings_dialog) + self.clothing_dir_button.clicked.connect( + lambda: self._select_directory(self.clothing_dir_input, "选择衣物文件夹")) + self.output_dir_button.clicked.connect(lambda: self._select_directory(self.output_dir_input, "选择输出文件夹")) + self.execute_button.clicked.connect(self.start_execution); + self.stop_button.clicked.connect(self.stop_execution) + + def _update_ui_state(self, is_running=False): + # FIX 1: Finer control over UI elements + self.server_group.setEnabled(not is_running) + self.workflow_group.setEnabled(not is_running) + self.exec_group.setEnabled(not is_running) + self.output_group.setEnabled(not is_running) + self.add_model_button.setEnabled(not is_running) + self.delete_model_button.setEnabled(not is_running) + self.model_list_widget.setEnabled(True) # Always enabled for preview + + self.execute_button.setEnabled(not is_running and self.api is not None) + self.stop_button.setEnabled(is_running) + + def _load_settings(self): + self.host_input.setText(self.config.get("server", "host", "127.0.0.1")); + self.port_input.setText(self.config.get("server", "port", "8188")); + self.clothing_dir_input.setText(self.config.get("paths", "clothing", "")); + self.output_dir_input.setText(self.config.get("paths", "output", "")); + self.log_message("欢迎使用ComfyUI智能试衣控制器!配置已从 config.ini 加载。") + + def _save_settings(self): + self.config.set("server", "host", self.host_input.text()); + self.config.set("server", "port", self.port_input.text()); + self.config.set("paths", "clothing", self.clothing_dir_input.text()); + self.config.set("paths", "output", self.output_dir_input.text()); + self._save_models() + self.config.save() + + def closeEvent(self, event): + reply = QMessageBox.question(self, '确认退出', '您确定要退出程序吗?\n所有配置将自动保存。', + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + self._save_settings(); self.log_message("配置已保存,程序退出。"); event.accept() + else: + event.ignore() + + def log_message(self, message): + self.log_box.appendPlainText(f"[{time.strftime('%H:%M:%S')}] {message}"); self.status_bar.showMessage( + message.split('\n')[-1], 5000) + + # FIX 2: Slot for status bar updates + def _update_status_bar(self, message): + self.status_bar.showMessage(message, 0) # 0 means persistent message + + def show_model_preview(self, item): + image_path = item.data(Qt.UserRole) + if image_path and os.path.exists(image_path): + ImagePreviewDialog(image_path, self).exec() + else: + self.show_error("文件错误", f"找不到图片文件:\n{image_path}") + + def _add_model(self): + file_path, _ = QFileDialog.getOpenFileName(self, "选择模特图片", self.config.get("paths", "last_used", ""), + "图片文件 (*.png *.jpg *.jpeg *.webp)"); + if file_path: item = QListWidgetItem(QIcon(file_path), os.path.basename(file_path)); item.setData(Qt.UserRole, + file_path); self.model_list_widget.addItem( + item); self.log_message(f"已添加模特: {os.path.basename(file_path)}") + + def _delete_model(self): + selected_items = self.model_list_widget.selectedItems() + if not selected_items: return self.show_error("操作无效", "请先在左侧列表中选择一个要删除的模特。") + for item in selected_items: self.model_list_widget.takeItem(self.model_list_widget.row(item)); self.log_message( + f"已删除模特: {item.text()}") + + def _load_models(self): + models_str = self.config.get("models", "list", ""); + if models_str: + models = models_str.splitlines() + for path in models: + if path and os.path.exists(path): item = QListWidgetItem(QIcon(path), + os.path.basename(path)); item.setData( + Qt.UserRole, path); self.model_list_widget.addItem(item) + self.log_message(f"已加载 {len(models)} 个模特。") + + def _save_models(self): + models = [self.model_list_widget.item(i).data(Qt.UserRole) for i in range(self.model_list_widget.count())] + self.config.set("models", "list", "\n".join(models)) + + def connect_and_fetch_workflows(self): + host, port = self.host_input.text(), self.port_input.text(); + self.log_message(f"正在连接到 http://{host}:{port}..."); + self.api = ComfyAPI(host, port); + self.fetch_workflows() + + def fetch_workflows(self): + if not self.api: return + self.log_message("正在获取工作流列表...") + workflows_data, error = self.api.get_workflows(); + self.workflow_combo.clear() + if error: + self.show_error("连接错误", f"无法获取工作流列表。\n原因: {error}"); self.api = None + elif workflows_data: + workflows = [item['path'] for item in workflows_data if item.get('path', '').endswith('.json')] + if workflows: + self.workflow_combo.addItems(workflows); self.log_message(f"成功找到 {len(workflows)} 个工作流。") + else: + self.log_message("连接成功,但在workflows文件夹中未找到.json文件。") + self._update_ui_state() + + def fetch_workflow_details(self, workflow_name): + self.current_workflow_prompt, self.current_workflow_graph = None, None + if not workflow_name or not self.api: self._update_ui_state(); return + self.log_message(f"正在加载工作流: {workflow_name}") + workflow_graph, error = self.api.get_workflow_details(workflow_name) + if error: self._update_ui_state(); return self.show_error("加载失败", + f"无法获取工作流 '{workflow_name}' 的详细信息。\n\n原因: {error}") + if workflow_graph: + self.current_workflow_graph = copy.deepcopy(workflow_graph) + original_node_count = len(workflow_graph['nodes']) + workflow_graph['nodes'] = [node for node in workflow_graph['nodes'] if node.get('type') != 'Note'] + removed_count = original_node_count - len(workflow_graph['nodes']) + if removed_count > 0: self.log_message(f"已自动移除 {removed_count} 个 'Note' 节点。") + self.log_message("正在转换工作流为可执行格式...") + self.current_workflow_prompt = convert_graph_to_prompt(workflow_graph) + if self.current_workflow_prompt: + self.log_message("转换成功,工作流已就绪。"); + settings_key = f"workflow/{workflow_name.replace('/', '_').replace('.', '_')}" + self.workflow_settings = json.loads(self.config.get("workflows", settings_key, "{}")) + else: + self.show_error("转换失败", "无法将工作流转换为API格式。") + self._update_ui_state() + + def _select_directory(self, line_edit, title="选择文件夹"): + start_dir = line_edit.text() or self.config.get("paths", "last_used", "") + directory = QFileDialog.getExistingDirectory(self, title, start_dir); + if directory: line_edit.setText(directory); self.config.set("paths", "last_used", directory) + + def open_settings_dialog(self): + if not self.current_workflow_prompt: return self.show_error("未加载工作流", "请先选择一个工作流。") + dialog = SettingsDialog(self.current_workflow_prompt, self.workflow_settings, self) + if dialog.exec(): + self.workflow_settings = dialog.get_settings(); + workflow_name = self.workflow_combo.currentText() + settings_key = f"workflow/{workflow_name.replace('/', '_').replace('.', '_')}"; + self.config.set("workflows", settings_key, json.dumps(self.workflow_settings)) + self.log_message(f"已为'{workflow_name}'保存节点设置。") + + def start_execution(self): + if self.model_list_widget.currentItem() is None: return self.show_error("缺少输入", + "请在左侧模特库中选择一位模特。") + if not self.clothing_dir_input.text(): return self.show_error("缺少输入", "请选择包含衣物图片的文件夹。") + if not self.output_dir_input.text(): return self.show_error("缺少输入", "请选择输出文件夹。") + if not self.current_workflow_prompt: return self.show_error("缺少工作流", "请选择一个有效的工作流。") + ws = self.workflow_settings + if not all( + [ws.get('model_node_id'), ws.get('clothing_node_id'), ws.get('output_node_id')]): return self.show_error( + "配置不完整", "请点击设置按钮(⚙️)来指定模特、衣物和输出节点。") + + self._update_ui_state(is_running=True) + self.execution_thread = QThread() + self.execution_worker = ExecutionWorker(api=self.api, workflow_prompt=self.current_workflow_prompt, + workflow_graph=self.current_workflow_graph, + model_image_path=self.model_list_widget.currentItem().data(Qt.UserRole), + model_node_id=ws['model_node_id'], + clothing_dir=self.clothing_dir_input.text(), + clothing_node_id=ws['clothing_node_id'], + output_dir=self.output_dir_input.text(), + output_node_id=ws['output_node_id']) + self.execution_worker.moveToThread(self.execution_thread); + self.execution_worker.log.connect(self.log_message) + self.execution_worker.progress.connect(self.update_progress) + self.execution_worker.finished.connect(self.execution_finished) + self.execution_worker.status_update.connect(self._update_status_bar) # FIX 2 + self.execution_thread.started.connect(self.execution_worker.run) + self.execution_thread.start(); + self.log_message("=== 开始批量换装任务 ===") + + def stop_execution(self): + if self.execution_worker: + self.execution_worker.stop() + else: + self.log_message("没有正在执行的任务可停止。") + + def update_progress(self, current, total): + self.progress_bar.setMaximum(total); self.progress_bar.setValue(current) + + def execution_finished(self, success, message): + self.log_message(f"=== {message} ==="); + if not success and "中止" not in message: self.show_error("执行失败", message) + self.progress_bar.setValue(0); + self.status_bar.clearMessage() # Clear status bar on finish + if self.execution_thread: + if self.execution_thread.isRunning(): self.execution_thread.quit() + self.execution_thread.wait(1000) + self.execution_thread, self.execution_worker = None, None + self._update_ui_state(is_running=False) + + def show_error(self, title, message): + self.log_message(f"错误: {title} - {message}"); + QMessageBox.critical(self, title, message) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + LIGHT_STYLE_SHEET = """ + QMainWindow, QDialog { background-color: #f4f6f8; color: #34495e; font-family: "Microsoft YaHei", "Segoe UI", "Roboto", sans-serif; } + QGroupBox { background-color: #ffffff; border: 1px solid #dcdfe6; border-radius: 8px; margin-top: 10px; padding: 15px; } + QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 5px 15px; background-color: #5dade2; color: white; border-top-left-radius: 8px; border-bottom-right-radius: 8px; font-weight: bold; } + QLabel { font-size: 13px; font-weight: bold; color: #34495e; padding: 5px 0; } + QLineEdit, QPlainTextEdit, QComboBox { background-color: #ffffff; border: 1px solid #dcdfe6; border-radius: 4px; padding: 6px; color: #34495e; font-size: 13px; } + QLineEdit:focus, QPlainTextEdit:focus, QComboBox:focus { border-color: #5dade2; } + QPushButton { background-color: #5dade2; color: white; border: none; border-radius: 4px; padding: 8px 16px; font-size: 13px; font-weight: bold; } + QPushButton:hover { background-color: #85c1e9; } + QPushButton:pressed { background-color: #2e86c1; } + QPushButton:disabled { background-color: #bdc3c7; color: #7f8c8d; } + QComboBox::drop-down { border: none; } + QComboBox QAbstractItemView { background-color: #ffffff; border: 1px solid #dcdfe6; selection-background-color: #5dade2; selection-color: white; } + QListWidget { background-color: #ffffff; border: 1px solid #dcdfe6; border-radius: 4px; } + QListWidget::item { padding: 8px; border-bottom: 1px solid #f4f6f8; } + QListWidget::item:hover { background-color: #ecf5ff; } + QListWidget::item:selected { background-color: #5dade2; color: white; } + QProgressBar { border: 1px solid #dcdfe6; border-radius: 4px; text-align: center; background-color: #e9ecef; color: #495057; } + QProgressBar::chunk { background-color: #28a745; border-radius: 3px; } + QStatusBar { background-color: #ffffff; border-top: 1px solid #dcdfe6; } + """ + app.setStyleSheet(LIGHT_STYLE_SHEET) + window = ComfyUIController() + window.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8fa23ad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "gui-dify-video-gen" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "boto3>=1.39.8", + "nuitka>=2.7.12", + "pyside6>=6.9.1", + "requests>=2.32.4", +] diff --git a/release/comfyui_gui.exe b/release/comfyui_gui.exe new file mode 100644 index 0000000..abc5f63 Binary files /dev/null and b/release/comfyui_gui.exe differ diff --git a/release/dify_video_gen_v4.1.exe b/release/dify_video_gen_v4.1.exe new file mode 100644 index 0000000..ba54f27 Binary files /dev/null and b/release/dify_video_gen_v4.1.exe differ diff --git a/release/dify_video_gen_v4.exe b/release/dify_video_gen_v4.exe new file mode 100644 index 0000000..7c357ce Binary files /dev/null and b/release/dify_video_gen_v4.exe differ diff --git a/release/n8n_video_gen_v2.exe b/release/n8n_video_gen_v2.exe new file mode 100644 index 0000000..c1fd23e Binary files /dev/null and b/release/n8n_video_gen_v2.exe differ diff --git a/release/n8n_video_gen_v3.1.exe b/release/n8n_video_gen_v3.1.exe new file mode 100644 index 0000000..babe572 Binary files /dev/null and b/release/n8n_video_gen_v3.1.exe differ diff --git a/release/n8n_video_gen_v3.exe b/release/n8n_video_gen_v3.exe new file mode 100644 index 0000000..5cc468e Binary files /dev/null and b/release/n8n_video_gen_v3.exe differ diff --git a/release/n8n_video_gen_v4.exe b/release/n8n_video_gen_v4.exe new file mode 100644 index 0000000..e599745 Binary files /dev/null and b/release/n8n_video_gen_v4.exe differ diff --git a/release/tts.exe b/release/tts.exe new file mode 100644 index 0000000..469a836 Binary files /dev/null and b/release/tts.exe differ diff --git a/static/SourceHanSansCN-Medium.ttf b/static/SourceHanSansCN-Medium.ttf new file mode 100644 index 0000000..0d03448 Binary files /dev/null and b/static/SourceHanSansCN-Medium.ttf differ diff --git a/static/comfyui_hz.ico b/static/comfyui_hz.ico new file mode 100644 index 0000000..50b0820 Binary files /dev/null and b/static/comfyui_hz.ico differ diff --git a/static/video.ico b/static/video.ico new file mode 100644 index 0000000..fe59df2 Binary files /dev/null and b/static/video.ico differ diff --git a/tts.py b/tts.py new file mode 100644 index 0000000..7ac94a9 --- /dev/null +++ b/tts.py @@ -0,0 +1,580 @@ +#!/usr/bin/env python3 +""" +PySide6 文本转 MP3 GUI 脚本 (仅支持 Minimax API v2) +支持配置记忆功能,将语音设置、保存目录和文件名格式保存到 config.ini。 +""" + +import sys +import http.client +import json +import base64 +import os +import random +import string +import configparser +from pathlib import Path + +from PySide6.QtGui import QFont +from PySide6.QtWidgets import ( + QApplication, + QWidget, + QVBoxLayout, + QHBoxLayout, + QTextEdit, + QLineEdit, + QPushButton, + QComboBox, + QSlider, + QLabel, + QFileDialog, + QMessageBox, + QGroupBox, + QGridLayout, + QInputDialog, +) +from PySide6.QtCore import Qt, Signal, QObject, QThread, QDir + +# --- 配置管理 --- +CONFIG_FILE = "config.ini" + + +class ConfigManager: + def __init__(self, config_file=CONFIG_FILE): + self.config_file = config_file + self.config = configparser.ConfigParser() + self.load_config() + + def load_config(self): + """加载配置文件,如果不存在则创建默认配置。""" + if not os.path.exists(self.config_file): + self.create_default_config() + else: + try: + # 确保读取时使用 utf-8 编码 + with open(self.config_file, 'r', encoding='utf-8') as f: + self.config.read_file(f) + except Exception as e: + print(f"❌ 加载配置文件 '{self.config_file}' 失败: {e}") + self.create_default_config() + + def create_default_config(self): + """创建包含默认值的配置文件。""" + print(f"ℹ️ 配置文件 '{self.config_file}' 不存在,正在创建默认配置。") + self.config['API'] = {'api_key': ''} + self.config['VoiceSettings'] = { + 'voice_id': 'iheytang-jasmine', + 'speed': '1.0', + 'vol': '100', + 'pitch': '0', + 'emotion': 'happy', + } + self.config['OutputSettings'] = { + 'save_directory': os.path.expanduser('~'), + 'filename_format': '(voice_id)_(random_8)', + } + self.save_config() + + def save_config(self): + """保存当前配置到文件。""" + try: + with open(self.config_file, 'w', encoding='utf-8') as configfile: + self.config.write(configfile) + print(f"✅ 配置已保存到 '{self.config_file}'") + except Exception as e: + print(f"❌ 保存配置文件 '{self.config_file}' 失败: {e}") + + def get_api_key(self): + """获取 API Key。""" + # 使用 get() 的 fallback 参数来提供默认值 + return self.config.get('API', 'api_key', fallback='') + + def set_api_key(self, api_key): + """设置 API Key。""" + if 'API' not in self.config: + self.config['API'] = {} + self.config['API']['api_key'] = api_key + self.save_config() + + def get_voice_settings(self): + """获取语音设置。""" + # 修正: 使用 config.items() 获取特定 section 的所有键值对 + if 'VoiceSettings' in self.config: + return dict(self.config.items('VoiceSettings')) + else: + return {} # 如果 Section 不存在,返回空字典 + + def set_voice_settings(self, settings): + """设置语音设置。""" + if 'VoiceSettings' not in self.config: + self.config['VoiceSettings'] = {} + for key, value in settings.items(): + self.config['VoiceSettings'][key] = str(value) # 确保值是字符串 + self.save_config() + + def get_output_settings(self): + """获取输出设置。""" + # 修正: 使用 config.items() 获取特定 section 的所有键值对 + if 'OutputSettings' in self.config: + return dict(self.config.items('OutputSettings')) + else: + return {} # 如果 Section 不存在,返回空字典 + + def set_output_settings(self, settings): + """设置输出设置。""" + if 'OutputSettings' not in self.config: + self.config['OutputSettings'] = {} + for key, value in settings.items(): + self.config['OutputSettings'][key] = str(value) # 确保值是字符串 + self.save_config() + + +# --- Minimax API v2 相关函数 --- +DEFAULT_VOICE_ID_V2 = 'jasmine_52025' # 默认的v2语音ID + + +def call_minimax_api_v2( + text, voice_id=DEFAULT_VOICE_ID_V2, speed=1.0, vol=1.0, pitch=0, emotion="happy", api_key="" +): + """ + 调用 Minimax API v2 (新方式) 将文本转换为音频。 + """ + if not api_key: + print("❌ Minimax API Key 未设置。") + return None + + conn = http.client.HTTPSConnection("api.minimaxi.com") + payload = json.dumps( + { + "model": "speech-02-hd", + "text": text, + "stream": False, + "language_boost": "auto", + "output_format": "hex", + "voice_setting": { + "voice_id": voice_id, + "speed": speed, + "vol": vol, + "pitch": pitch, + "emotion": emotion, + }, + "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "mp3"}, + } + ) + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + try: + print(f"🔄 正在调用 Minimax API v2...") + conn.request("POST", "/v1/t2a_v2", payload, headers) + res = conn.getresponse() + data = res.read() + + if res.status != 200: + print(f"❌ API v2 调用失败: {res.status} {res.reason}") + try: + error_details = json.loads(data.decode('utf-8')) + print(f"响应内容: {error_details}") + except json.JSONDecodeError: + print(f"响应内容: {data.decode('utf-8', errors='ignore')}") + return None + + response_data = json.loads(data.decode("utf-8")) + + if "data" in response_data and "audio" in response_data["data"]: + return response_data["data"]["audio"] + else: + print(f"❌ API v2 响应中没有音频数据。") + print(f"响应内容: {response_data}") + return None + + except Exception as e: + print(f"❌ API v2 调用异常: {str(e)}") + return None + finally: + conn.close() + + +# --- 音频数据处理函数 --- +def fix_base64_padding(b64_string): + """自动补齐 base64 字符串的填充。""" + b64_string = b64_string.rstrip("=") + return b64_string + "=" * ((4 - len(b64_string) % 4) % 4) + + +def validate_mp3_format(audio_data): + """验证音频数据是否为有效的 MP3 格式(简化版)。""" + if len(audio_data) < 10: return False + return audio_data.startswith(b"ID3") or audio_data.startswith(b"\xff\xfb") + + +def clean_base64_data(base64_string): + """清理 base64 数据,移除可能的 JSON 包装和其他非 base64 字符。""" + if base64_string.startswith('"') and base64_string.endswith('"'): + base64_string = base64_string[1:-1] + base64_string = base64_string.replace("\n", "").replace("\r", "").replace(" ", "") + base64_string = (base64_string.replace("\\n", "").replace("\\r", "").replace("\\t", "")) + return base64_string + + +def is_hex_data(data_string): + """检查数据是否为十六进制格式。""" + if not data_string: return False + hex_chars = set("0123456789abcdefABCDEF") + return all(c in hex_chars for c in data_string) + + +def hex_to_bytes(hex_string): + """将十六进制字符串转换为字节。""" + try: + return bytes.fromhex(hex_string) + except ValueError: + raise ValueError("无效的十六进制格式") + + +def decode_audio_data(data_string): + """解码音频数据,支持 base64 和十六进制格式。""" + cleaned_data = clean_base64_data(data_string) + if is_hex_data(cleaned_data): + print("📝 检测到十六进制格式数据") + return hex_to_bytes(cleaned_data) + try: + print("📝 尝试 base64 解码") + fixed_base64 = fix_base64_padding(cleaned_data) + return base64.b64decode(fixed_base64) + except Exception: + raise ValueError("无法解码音频数据,请检查数据格式") + + +def save_mp3_file(audio_data, output_path): + """保存音频数据为 MP3 文件。""" + try: + if not validate_mp3_format(audio_data): + print("⚠️ 警告: 解码的数据可能不是有效的 MP3 格式") + with open(output_path, "wb") as f: + f.write(audio_data) + return True + except Exception as e: + print(f"❌ 保存文件失败: {str(e)}") + return False + + +# --- GUI 组件 --- +class Worker(QObject): + finished = Signal(bool, str) # success, message + progress = Signal(str) # status message + + def __init__(self, text, output_path, voice_id, speed, vol, pitch, emotion, api_key): + super().__init__() + self.text = text + self.output_path = output_path + self.voice_id = voice_id + self.speed = speed + self.vol = vol + self.pitch = pitch + self.emotion = emotion + self.api_key = api_key + + def run(self): + """执行文本转 MP3 的整个流程。""" + try: + self.progress.emit("🔄 正在调用 Minimax API v2...") + api_response = call_minimax_api_v2( + self.text, self.voice_id, self.speed, self.vol, self.pitch, self.emotion, self.api_key + ) + if api_response is None: + self.finished.emit(False, "API 调用失败,请检查日志信息或 API Key。") + return + + self.progress.emit("🔄 正在解码音频数据...") + audio_data = decode_audio_data(api_response) + + self.progress.emit(f"💾 正在保存音频到: {self.output_path}") + if save_mp3_file(audio_data, self.output_path): + self.finished.emit(True, f"✅ 成功生成 MP3 文件: {self.output_path}") + else: + self.finished.emit(False, "保存 MP3 文件失败,请检查文件权限和路径。") + + except ValueError as ve: + self.finished.emit(False, f"数据处理错误: {ve}") + except Exception as e: + self.finished.emit(False, f"发生未知错误: {str(e)}") + + +class TextToMP3_GUI(QWidget): + def __init__(self, config_manager): + super().__init__() + self.config_manager = config_manager + self.api_key = self.config_manager.get_api_key() + + self.setWindowTitle("文本转 MP3 (Minimax v2)") + self.setGeometry(100, 100, 700, 550) + self.setMinimumSize(600, 450) + + self.init_ui() + self.load_settings_from_config() + + def init_ui(self): + """初始化用户界面。""" + main_layout = QVBoxLayout(self) + + # --- 输入区域 --- + input_group = QGroupBox("输入文本") + input_layout = QVBoxLayout() + self.text_edit = QTextEdit() + self.text_edit.setPlaceholderText("在此输入您想要转换为语音的文本...") + self.text_edit.setFont(QFont("Arial", 11)) + self.text_edit.setFixedHeight(150) + input_layout.addWidget(self.text_edit) + input_group.setLayout(input_layout) + main_layout.addWidget(input_group) + + # --- 语音参数区域 --- + params_group = QGroupBox("语音设置 (Minimax v2)") + params_layout = QGridLayout() + + params_layout.addWidget(QLabel("语音 ID:"), 0, 0, 1, 1, Qt.AlignRight) + self.voice_id_edit = QLineEdit() + self.voice_id_edit.setPlaceholderText("如: jasmine_52025") + params_layout.addWidget(self.voice_id_edit, 0, 1, 1, 2) + + params_layout.addWidget(QLabel("语速:"), 1, 0, 1, 1, Qt.AlignRight) + self.speed_slider = QSlider(Qt.Horizontal) + self.speed_slider.setRange(5, 20) # 对应 0.5x 到 2.0x + self.speed_label = QLabel("1.0") + self.speed_slider.valueChanged.connect(lambda value: self.speed_label.setText(f"{value / 10.0:.1f}")) + speed_h_layout = QHBoxLayout() + speed_h_layout.addWidget(self.speed_slider) + speed_h_layout.addWidget(self.speed_label) + params_layout.addLayout(speed_h_layout, 1, 1, 1, 2) + + params_layout.addWidget(QLabel("音量:"), 2, 0, 1, 1, Qt.AlignRight) + self.vol_slider = QSlider(Qt.Horizontal) + self.vol_slider.setRange(0, 100) + self.vol_label = QLabel("100%") + self.vol_slider.valueChanged.connect(lambda value: self.vol_label.setText(f"{value}%")) + vol_h_layout = QHBoxLayout() + vol_h_layout.addWidget(self.vol_slider) + vol_h_layout.addWidget(self.vol_label) + params_layout.addLayout(vol_h_layout, 2, 1, 1, 2) + + params_layout.addWidget(QLabel("音调:"), 3, 0, 1, 1, Qt.AlignRight) + self.pitch_slider = QSlider(Qt.Horizontal) + self.pitch_slider.setRange(-10, 10) + self.pitch_label = QLabel("0") + self.pitch_slider.valueChanged.connect(lambda value: self.pitch_label.setText(f"{value}")) + pitch_h_layout = QHBoxLayout() + pitch_h_layout.addWidget(self.pitch_slider) + pitch_h_layout.addWidget(self.pitch_label) + params_layout.addLayout(pitch_h_layout, 3, 1, 1, 2) + + params_layout.addWidget(QLabel("情感:"), 4, 0, 1, 1, Qt.AlignRight) + self.emotion_combo = QComboBox() + self.emotion_combo.addItems(["happy", "sad", "angry", "fearful", "surprised", "neutral"]) + params_layout.addWidget(self.emotion_combo, 4, 1, 1, 2) + + params_group.setLayout(params_layout) + main_layout.addWidget(params_group) + + # --- 文件保存区域 --- + output_group = QGroupBox("输出设置") + output_layout = QVBoxLayout() + + output_h_layout = QHBoxLayout() + self.output_dir_label = QLabel("保存目录:") + self.output_dir_path_edit = QLineEdit() + self.output_dir_path_edit.setPlaceholderText("选择保存 MP3 文件的目录") + self.browse_dir_button = QPushButton("浏览...") + self.browse_dir_button.clicked.connect(self.browse_directory) + output_h_layout.addWidget(self.output_dir_label) + output_h_layout.addWidget(self.output_dir_path_edit) + output_h_layout.addWidget(self.browse_dir_button) + output_layout.addLayout(output_h_layout) + + filename_layout = QHBoxLayout() + filename_layout.addWidget(QLabel("文件名格式:"), alignment=Qt.AlignRight) + self.filename_format_edit = QLineEdit() + self.filename_format_edit.setPlaceholderText("如: (voice_id)_(random_8)") + filename_layout.addWidget(self.filename_format_edit) + output_layout.addLayout(filename_layout) + + output_group.setLayout(output_layout) + main_layout.addWidget(output_group) + + # --- 状态与操作按钮 --- + status_layout = QHBoxLayout() + self.status_label = QLabel("等待输入...") + self.status_label.setFont(QFont("Arial", 10)) + self.status_label.setStyleSheet("color: gray;") + status_layout.addWidget(self.status_label, 1) + self.convert_button = QPushButton("开始转换") + self.convert_button.setFont(QFont("Arial", 12, QFont.Bold)) + self.convert_button.setStyleSheet( + "QPushButton { background-color: #4CAF50; color: white; } QPushButton:pressed { background-color: #45a049; }") + self.convert_button.clicked.connect(self.start_conversion) + status_layout.addWidget(self.convert_button) + main_layout.addLayout(status_layout) + + self.worker_thread = None + self.worker = None + + def load_settings_from_config(self): + """从配置文件加载设置并填充到 UI 控件。""" + config_voice = self.config_manager.get_voice_settings() + self.voice_id_edit.setText(config_voice.get('voice_id', DEFAULT_VOICE_ID_V2)) + self.speed_slider.setValue(int(float(config_voice.get('speed', '1.0')) * 10)) + self.vol_slider.setValue(int(config_voice.get('vol', '100'))) + self.pitch_slider.setValue(int(config_voice.get('pitch', '0'))) + # 找到情感在 ComboBox 中的索引,如果值不存在则使用默认值 + emotion_index = self.emotion_combo.findText(config_voice.get('emotion', 'happy')) + if emotion_index != -1: + self.emotion_combo.setCurrentIndex(emotion_index) + else: + self.emotion_combo.setCurrentText('happy') # 找不到则设为happy + + config_output = self.config_manager.get_output_settings() + self.output_dir_path_edit.setText(config_output.get('save_directory', os.path.expanduser('~'))) + self.filename_format_edit.setText(config_output.get('filename_format', '(voice_id)_(random_8)')) + + def save_settings_to_config(self): + """保存当前 UI 控件中的设置到配置文件。""" + settings = { + 'voice_id': self.voice_id_edit.text(), + 'speed': f"{self.speed_slider.value() / 10.0:.1f}", + 'vol': str(self.vol_slider.value()), + 'pitch': str(self.pitch_slider.value()), + 'emotion': self.emotion_combo.currentText(), + } + self.config_manager.set_voice_settings(settings) + + output_settings = { + 'save_directory': self.output_dir_path_edit.text(), + 'filename_format': self.filename_format_edit.text(), + } + self.config_manager.set_output_settings(output_settings) + + def browse_directory(self): + """打开目录选择对话框,并更新保存目录的 QLineEdit。""" + directory = QFileDialog.getExistingDirectory(self, "选择保存目录", + self.output_dir_path_edit.text() or QDir.homePath()) + if directory: + self.output_dir_path_edit.setText(directory) + # 实时保存目录设置 + self.save_settings_to_config() + + def generate_random_string(self, length=8): + """生成指定长度的随机字符串。""" + characters = string.ascii_lowercase + string.digits + return ''.join(random.choice(characters) for _ in range(length)) + + def get_output_filename(self): + """根据用户设置的文件名格式生成完整的文件名。""" + base_filename_template = self.filename_format_edit.text() + voice_id = self.voice_id_edit.text() if self.voice_id_edit.text() else "default_voice" + random_part = self.generate_random_string() + + filename = base_filename_template.replace("(voice_id)", voice_id).replace("(random_8)", random_part) + if not filename.lower().endswith(".mp3"): + filename += ".mp3" + return filename + + def start_conversion(self): + """开始文本转 MP3 的转换过程。""" + text_to_convert = self.text_edit.toPlainText().strip() + if not text_to_convert: + QMessageBox.warning(self, "输入错误", "请输入要转换的文本!") + return + + output_dir = self.output_dir_path_edit.text().strip() + if not output_dir: + QMessageBox.warning(self, "输出错误", "请选择 MP3 文件的保存目录!") + return + + output_filename = self.get_output_filename() + output_path = Path(output_dir) / output_filename + + if output_path.exists(): + reply = QMessageBox.question( + self, "文件已存在", f"文件 '{output_path.name}' 已存在。是否覆盖?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if reply == QMessageBox.No: + return + + voice_id = self.voice_id_edit.text() + speed = self.speed_slider.value() / 10.0 + vol = self.vol_slider.value() + pitch = self.pitch_slider.value() + emotion = self.emotion_combo.currentText() + + if not voice_id: + QMessageBox.warning(self, "输入错误", "请提供语音 ID!") + return + if not self.api_key: + QMessageBox.critical(self, "API Key 错误", "Minimax API Key 未设置,请在程序启动时配置。") + return + + # 保存当前UI控件中的值到配置 + self.save_settings_to_config() + + self.convert_button.setEnabled(False) + self.browse_dir_button.setEnabled(False) + self.status_label.setText("🔄 准备中...") + self.status_label.setStyleSheet("color: orange;") + + self.worker = Worker( + text_to_convert, str(output_path), voice_id, speed, vol, pitch, emotion, self.api_key + ) + self.worker.progress.connect(self.update_status) + self.worker.finished.connect(self.on_conversion_finished) + + self.worker_thread = QThread() + self.worker.moveToThread(self.worker_thread) + self.worker_thread.started.connect(self.worker.run) + self.worker_thread.start() + + def update_status(self, message): + """更新状态标签显示信息。""" + self.status_label.setText(message) + if "🔄" in message: + self.status_label.setStyleSheet("color: orange;") + elif "✅" in message: + self.status_label.setStyleSheet("color: green;") + elif "❌" in message: + self.status_label.setStyleSheet("color: red;") + + def on_conversion_finished(self, success, message): + """当转换完成时,更新 UI 状态并显示结果。""" + self.update_status(message) + if success: + QMessageBox.information(self, "转换成功", message) + else: + QMessageBox.critical(self, "转换失败", message) + + self.convert_button.setEnabled(True) + self.browse_dir_button.setEnabled(True) + + if self.worker_thread: + self.worker_thread.quit() + self.worker_thread.wait() + self.worker_thread = None + self.worker = None + + +def main(): + # 检查并加载配置 + config_manager = ConfigManager() + + # 获取 API Key + minimax_api_key = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiJpSGV5VGFuZyIsIlVzZXJOYW1lIjoiaUhleVRhbmciLCJBY2NvdW50IjoiIiwiU3ViamVjdElEIjoiMTkzMDY1MjkzNjIwODEyNjY2NCIsIlBob25lIjoiMTM1ODgwMTQ3NzkiLCJHcm91cElEIjoiMTkzMDY1MjkzNjE5OTczODA1NiIsIlBhZ2VOYW1lIjoiIiwiTWFpbCI6IiIsIkNyZWF0ZVRpbWUiOiIyMDI1LTA3LTI1IDE4OjE0OjM3IiwiVG9rZW5UeXBlIjoxLCJpc3MiOiJtaW5pbWF4In0.JCm0sjxZLBLB7mrTrQOdtzUZZ00T97S30jFjHCuWuAvjPwUiIDGkgsm6SHLa8VVh0rcP7ZACr3Xlwy1lplvZpMUFVJg_orJh73Z5WYOZKmwIg2NbRTw5O7VbAcWivJVqj2n5q3OhYm6zAAdrxS5gbXmMMMkI7G1jsBk-v_OfPRJovoilU7nb7mMTdtl35bLMDjQgbV-TymGwDDekPoz1Vb-YYa0HRYzZPAZLR4XIzkpNnGEioYqEsECt-WrLWWkl-5n3I82JrP7HiGnyI7zP1Q5aB3SyHlwxMKDP-C6_2WLwHaHr0fcngMgrEgDvYq1_SLnq9BFhL26bviM1cz-3jQ" + config_manager.set_api_key(minimax_api_key) + app = QApplication(sys.argv) + window = TextToMP3_GUI(config_manager) + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..c905760 --- /dev/null +++ b/uv.lock @@ -0,0 +1,415 @@ +version = 1 +revision = 2 +requires-python = ">=3.10" + +[[package]] +name = "boto3" +version = "1.39.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/ef/f8dbe6482bdf9eb0230f2639483cdd40ef5aaa89c2fb651f2edeee9c248a/boto3-1.39.8.tar.gz", hash = "sha256:456ea6baef037eb6205d64e012259d14f0c9300c9b30603890746c1a0882fa01", size = 111829, upload-time = "2025-07-17T19:19:14.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/f0/f3701472b2e6192e62d80e703186ae9c789b3d607ba22943702c500897d2/boto3-1.39.8-py3-none-any.whl", hash = "sha256:dcea5270ccced0b4b962eb5874cb71b6232ccfc6203e05bf834a314442e4a79c", size = 139886, upload-time = "2025-07-17T19:19:12.634Z" }, +] + +[[package]] +name = "botocore" +version = "1.39.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/57/16d3d21963975b9be180e96695abfb146695ae7db57f9a2d47e92d33ce9d/botocore-1.39.8.tar.gz", hash = "sha256:3848bd9057ea8dbc059e7764eda63bda575727ad1101dbd03636ab4a6f283fa5", size = 14205898, upload-time = "2025-07-17T19:19:03.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/ac/51462dd35fc60d11cdce93ba82ccf1635a161ceadc646d89f67d666fff31/botocore-1.39.8-py3-none-any.whl", hash = "sha256:ab43f79c6893271934faba7ae1987a313d59576361c544c70a5391ade560891d", size = 13866818, upload-time = "2025-07-17T19:18:58.521Z" }, +] + +[[package]] +name = "certifi" +version = "2025.7.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "gui-dify-video-gen" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "boto3" }, + { name = "nuitka" }, + { name = "pyside6" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", specifier = ">=1.39.8" }, + { name = "nuitka", specifier = ">=2.7.12" }, + { name = "pyside6", specifier = ">=6.9.1" }, + { name = "requests", specifier = ">=2.32.4" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + +[[package]] +name = "nuitka" +version = "2.7.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ordered-set" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/9c/f51b7c7dae304f2381b5af20a5ac6ccb56f0a773d16470b269146f5909db/Nuitka-2.7.12.tar.gz", hash = "sha256:ee28e5699005904e83250ad1b3192cf2c5b46bebbf4ae12f6fc5efa4a0368c16", size = 3887702, upload-time = "2025-07-08T16:04:10.371Z" } + +[[package]] +name = "ordered-set" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/ca/bfac8bc689799bcca4157e0e0ced07e70ce125193fc2e166d2e685b7e2fe/ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8", size = 12826, upload-time = "2022-01-26T14:38:56.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pyside6" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-addons" }, + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/91/8e9c7f7e90431297de9856e90a156ade9420977e26d87996909c63f30bd2/PySide6-6.9.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:f843ef39970a2f79757810fffd7b8e93ac42a3de9ea62f2a03648cde57648aed", size = 558097, upload-time = "2025-06-03T13:20:03.739Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ff/04d1b6b30edd24d761cc30d964860f997bdf37d06620694bf9aab35eec3a/PySide6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:db44ac08b8f7ac1b421bc1c6a44200d03f08d80dc7b3f68dfdb1684f30f41c17", size = 558239, upload-time = "2025-06-03T13:20:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b4/ca076c55c11a8e473363e05aa82c5c03dd7ba8f17b77cc9311ce17213193/PySide6-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:531a6e67c429b045674d57fe9864b711eb59e4cded753c2640982e368fd468d1", size = 558239, upload-time = "2025-06-03T13:20:08.257Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/95c941f53b0faebc27dbe361d8e971b77f504b9cf36f8f5d750fd82cd6fc/PySide6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:c82dbb7d32bbdd465e01059174f71bddc97de152ab71bded3f1907c40f9a5f16", size = 564571, upload-time = "2025-06-03T13:20:10.321Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ef/0aa5e910fa4e9770db6b45c23e360a52313922e0ca71fc060a57db613de1/PySide6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:1525d63dc6dc425b8c2dc5bc01a8cb1d67530401449f3a3490c09a14c095b9f9", size = 401793, upload-time = "2025-06-03T13:20:12.108Z" }, +] + +[[package]] +name = "pyside6-addons" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyside6-essentials" }, + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e2/39b9e04335d7ac782b6459bf7abec90c36b8efaac5a88ef818e972c59387/PySide6_Addons-6.9.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:7be0708fa89715c282541fca47e2ba97c0c8d2886e0236ef994b2dd8f52aacdd", size = 316212438, upload-time = "2025-06-03T13:06:15.027Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6f/691d7039a6f7943522a770b713ecd85fa169688dfdd65ddd4db1699d01b6/PySide6_Addons-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:da7869b02e3599d26546fad582db4656060786bc5ec8ece5ec9ee8aa8b42371c", size = 166690468, upload-time = "2025-06-03T13:06:34.962Z" }, + { url = "https://files.pythonhosted.org/packages/9d/08/a264db09ad35819643d910cd4c73a86f72f23b7092f8ebc7e51dcca53a86/PySide6_Addons-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:53fd08c8152b6ba8c435458afd189835ba905793a5077a2bb0b1b11222b375d4", size = 162466096, upload-time = "2025-06-03T13:08:58.065Z" }, + { url = "https://files.pythonhosted.org/packages/84/be/a849402f7e73d137b5ae8b4370a49b0cf0e0c02f028b845782cb743e4995/PySide6_Addons-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:cd93a3a5e3886cd958f3a5acc7c061c24f10a394ce9f4ce657ac394544ca7ec2", size = 143150906, upload-time = "2025-06-03T13:09:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f1/1bb6b5859aff4e2b3f5ef789b9cee200811a9f469f04d9aa7425e816622b/PySide6_Addons-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:4f589631bdceb518080ae9c9fa288e64f092cd5bebe25adc8ad89e8eadd4db29", size = 26938762, upload-time = "2025-06-03T13:09:20.009Z" }, +] + +[[package]] +name = "pyside6-essentials" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "shiboken6" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/59/714874db9ef3bbbbda654fd3223248969bea02ec1a5bfdd1c941c4e97749/PySide6_Essentials-6.9.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:ed43435a70e018e1c22efcaf34a9430b83cfcad716dba661b03de21c13322fab", size = 132957077, upload-time = "2025-06-03T13:11:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/ea0db68d40a1c487fd255634896f4e37b6560e3ef1f57ca5139bf6509b1f/PySide6_Essentials-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e5da48883f006c6206ef85874db74ddebcdf69b0281bd4f1642b1c5ac1d54aea", size = 96416183, upload-time = "2025-06-03T13:12:48.945Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2f/4243630d1733522638c4967d36018c38719d8b84f5246bf3d4c010e0aa9d/PySide6_Essentials-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:e46a2801c9c6098025515fd0af6c594b9e9c951842f68b8f6f3da9858b9b26c2", size = 94171343, upload-time = "2025-06-03T13:12:59.426Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a9/a8e0209ba9116f2c2db990cfb79f2edbd5a3a428013be2df1f1cddd660a9/PySide6_Essentials-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:ad1ac94011492dba33051bc33db1c76a7d6f815a81c01422cb6220273b369145", size = 72435676, upload-time = "2025-06-03T13:13:08.805Z" }, + { url = "https://files.pythonhosted.org/packages/d0/e4/23268c57e775a1a4d2843d288a9583a47f2e4b3977a9ae93cb9ded1a4ea5/PySide6_Essentials-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:35c2c2bb4a88db74d11e638cf917524ff35785883f10b439ead07960a5733aa4", size = 49483707, upload-time = "2025-06-03T13:13:16.399Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/5d/9dcc100abc6711e8247af5aa561fc07c4a046f72f659c3adea9a449e191a/s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177", size = 150232, upload-time = "2025-05-22T19:24:50.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/17/22bf8155aa0ea2305eefa3a6402e040df7ebe512d1310165eda1e233c3f8/s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be", size = 85152, upload-time = "2025-05-22T19:24:48.703Z" }, +] + +[[package]] +name = "shiboken6" +version = "6.9.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/98/34d4d25b79055959b171420d47fcc10121aefcbb261c91d5491252830e31/shiboken6-6.9.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:40e92afc88da06b5100c56b761e59837ff282166e9531268f3d910b6128e621e", size = 406159, upload-time = "2025-06-03T13:16:45.104Z" }, + { url = "https://files.pythonhosted.org/packages/5a/07/53b2532ecd42ff925feb06b7bb16917f5f99f9c3470f0815c256789d818b/shiboken6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efcdfa8655d34aaf8d7a0c7724def3440bd46db02f5ad3b1785db5f6ccb0a8ff", size = 206756, upload-time = "2025-06-03T13:16:46.528Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b0/75b86ee3f7b044e6a87fbe7abefd1948ca4ae5fcde8321f4986a1d9eaa5e/shiboken6-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:efcf75d48a29ae072d0bf54b3cd5a59ae91bb6b3ab7459e17c769355486c2e0b", size = 203233, upload-time = "2025-06-03T13:16:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/30/56/00af281275aab4c79e22e0ea65feede0a5c6da3b84e86b21a4a0071e0744/shiboken6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:209ccf02c135bd70321143dcbc5023ae0c056aa4850a845955dd2f9b2ff280a9", size = 1153587, upload-time = "2025-06-03T13:16:50.454Z" }, + { url = "https://files.pythonhosted.org/packages/de/ce/6ccd382fbe1a96926c5514afa6f2c42da3a9a8482e61f8dfc6068a9ca64f/shiboken6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:2a39997ce275ced7853defc89d3a1f19a11c90991ac6eef3435a69bb0b7ff1de", size = 1831623, upload-time = "2025-06-03T13:16:52.468Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "zstandard" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/55/bd0487e86679db1823fc9ee0d8c9c78ae2413d34c0b461193b5f4c31d22f/zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9", size = 788701, upload-time = "2024-07-15T00:13:27.351Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8a/ccb516b684f3ad987dfee27570d635822e3038645b1a950c5e8022df1145/zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880", size = 633678, upload-time = "2024-07-15T00:13:30.24Z" }, + { url = "https://files.pythonhosted.org/packages/12/89/75e633d0611c028e0d9af6df199423bf43f54bea5007e6718ab7132e234c/zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc", size = 4941098, upload-time = "2024-07-15T00:13:32.526Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7a/bd7f6a21802de358b63f1ee636ab823711c25ce043a3e9f043b4fcb5ba32/zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573", size = 5308798, upload-time = "2024-07-15T00:13:34.925Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/775f851a4a65013e88ca559c8ae42ac1352db6fcd96b028d0df4d7d1d7b4/zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391", size = 5341840, upload-time = "2024-07-15T00:13:37.376Z" }, + { url = "https://files.pythonhosted.org/packages/09/4f/0cc49570141dd72d4d95dd6fcf09328d1b702c47a6ec12fbed3b8aed18a5/zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e", size = 5440337, upload-time = "2024-07-15T00:13:39.772Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7c/aaa7cd27148bae2dc095191529c0570d16058c54c4597a7d118de4b21676/zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd", size = 4861182, upload-time = "2024-07-15T00:13:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/ac/eb/4b58b5c071d177f7dc027129d20bd2a44161faca6592a67f8fcb0b88b3ae/zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4", size = 4932936, upload-time = "2024-07-15T00:13:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/44/f9/21a5fb9bb7c9a274b05ad700a82ad22ce82f7ef0f485980a1e98ed6e8c5f/zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea", size = 5464705, upload-time = "2024-07-15T00:13:46.822Z" }, + { url = "https://files.pythonhosted.org/packages/49/74/b7b3e61db3f88632776b78b1db597af3f44c91ce17d533e14a25ce6a2816/zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2", size = 4857882, upload-time = "2024-07-15T00:13:49.297Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/d8eb1cb123d8e4c541d4465167080bec88481ab54cd0b31eb4013ba04b95/zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9", size = 4697672, upload-time = "2024-07-15T00:13:51.447Z" }, + { url = "https://files.pythonhosted.org/packages/5e/05/f7dccdf3d121309b60342da454d3e706453a31073e2c4dac8e1581861e44/zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a", size = 5206043, upload-time = "2024-07-15T00:13:53.587Z" }, + { url = "https://files.pythonhosted.org/packages/86/9d/3677a02e172dccd8dd3a941307621c0cbd7691d77cb435ac3c75ab6a3105/zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0", size = 5667390, upload-time = "2024-07-15T00:13:56.137Z" }, + { url = "https://files.pythonhosted.org/packages/41/7e/0012a02458e74a7ba122cd9cafe491facc602c9a17f590367da369929498/zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c", size = 5198901, upload-time = "2024-07-15T00:13:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/65/3a/8f715b97bd7bcfc7342d8adcd99a026cb2fb550e44866a3b6c348e1b0f02/zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813", size = 430596, upload-time = "2024-07-15T00:14:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/19/b7/b2b9eca5e5a01111e4fe8a8ffb56bdcdf56b12448a24effe6cfe4a252034/zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4", size = 495498, upload-time = "2024-07-15T00:14:02.741Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699, upload-time = "2024-07-15T00:14:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681, upload-time = "2024-07-15T00:14:13.99Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328, upload-time = "2024-07-15T00:14:16.588Z" }, + { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955, upload-time = "2024-07-15T00:14:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944, upload-time = "2024-07-15T00:14:22.173Z" }, + { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927, upload-time = "2024-07-15T00:14:24.825Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910, upload-time = "2024-07-15T00:14:26.982Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544, upload-time = "2024-07-15T00:14:29.582Z" }, + { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094, upload-time = "2024-07-15T00:14:40.126Z" }, + { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440, upload-time = "2024-07-15T00:14:42.786Z" }, + { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091, upload-time = "2024-07-15T00:14:45.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682, upload-time = "2024-07-15T00:14:47.407Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707, upload-time = "2024-07-15T00:15:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792, upload-time = "2024-07-15T00:15:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586, upload-time = "2024-07-15T00:15:32.26Z" }, + { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420, upload-time = "2024-07-15T00:15:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" }, + { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975, upload-time = "2024-07-15T00:16:16.005Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448, upload-time = "2024-07-15T00:16:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269, upload-time = "2024-07-15T00:16:20.136Z" }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228, upload-time = "2024-07-15T00:16:23.398Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891, upload-time = "2024-07-15T00:16:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310, upload-time = "2024-07-15T00:16:29.018Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912, upload-time = "2024-07-15T00:16:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946, upload-time = "2024-07-15T00:16:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994, upload-time = "2024-07-15T00:16:36.887Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681, upload-time = "2024-07-15T00:16:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239, upload-time = "2024-07-15T00:16:41.83Z" }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149, upload-time = "2024-07-15T00:16:44.287Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392, upload-time = "2024-07-15T00:16:46.423Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299, upload-time = "2024-07-15T00:16:49.053Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862, upload-time = "2024-07-15T00:16:51.003Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578, upload-time = "2024-07-15T00:16:53.135Z" }, +] diff --git a/video_gen.py b/video_gen.py new file mode 100644 index 0000000..e035f5a --- /dev/null +++ b/video_gen.py @@ -0,0 +1,1032 @@ +import base64 +import configparser +import json +import logging +import mimetypes +import os +import re +import sys +import threading +import time +import uuid +from dataclasses import dataclass, field +from enum import Enum, auto + +import boto3 +import httpx +import requests +from PySide6.QtCore import Qt, QSize, QObject, Signal +from PySide6.QtGui import (QPixmap, QIcon, QColor, QFontDatabase, QFont, + QPainter, QAction) +from PySide6.QtSvg import QSvgRenderer +from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QFileDialog, QMessageBox, QTextEdit, + QSpinBox, QGraphicsDropShadowEffect, QGridLayout, QSizePolicy, + QAbstractSpinBox, QListWidget, QListWidgetItem, QProgressBar, QDialog, + QDialogButtonBox, QInputDialog, QMenu, QFormLayout) + +# --- 全局配置 --- +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# --- 默认参数 --- +DEFAULT_PRODUCT = "超短牛仔裙(白色紧身蕾丝短袖)" +DEFAULT_SCENE = "室内可爱简约的女性卧室" +DEFAULT_MODEL = "单马尾充满媚态,对自己的性感妩媚十分自信,眼神勾人" +DEFAULT_DUPLICATE = 1 +DEFAULT_AVAILABLE_TEMPLATES = ["妩媚眼神", "商品展示", "切镜展示"] +DEFAULT_SELECTED_TEMPLATES = ["妩媚眼神"] +DEFAULT_OUTPUT = "" + +# --- API & S3 配置 --- +HOST = "http://192.168.0.148:5678" +AWS_ACCESS_KEY_ID = "AKIAYRH5NGRSWHN2L4M6" +AWS_SECRET_ACCESS_KEY = "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" +S3_BUCKET = "modal-media-cache" + +# --- LLM 配置 --- +LLM_API_URL = "https://gateway.bowong.cc/chat/completions" +LLM_PROVIDER = "gpt-4o-1120" +DEFAULT_LLM_PROMPT_DATA = { + "product": { + "desc": "模特身上的服饰,格式为“下装(上装)”", + "example": "蓝色牛仔裤(白色T恤)" + }, + "scene": { + "desc": "图中的场景", + "example": "阳光下的海滩" + }, + "model": { + "desc": "模特的样貌,包含肤色、发型、发色、神态等", + "example": "黑色长发皮肤白皙的亚洲女性,正在微笑" + } +} + + +# --- 辅助对话框 --- + +class PromptConfigDialog(QDialog): + """用于结构化配置LLM Prompt的对话框""" + + def __init__(self, prompt_data, parent=None): + super().__init__(parent) + self.setWindowTitle("配置智能参数Prompt") + self.setMinimumSize(650, 350) + self.layout = QVBoxLayout(self) + + info_label = QLabel("自定义用于“智能获取参数”的说明和示例,核心指令将保持不变以确保格式正确。") + self.layout.addWidget(info_label) + + form_layout = QFormLayout() + form_layout.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapAllRows) + form_layout.setLabelAlignment(Qt.AlignRight) + form_layout.setSpacing(10) + + self.editors = {} + for key, values in prompt_data.items(): + field_label = QLabel(f"{key.capitalize()} 字段:") + field_label.setStyleSheet("margin-top: 10px;") + form_layout.addRow(field_label) + + desc_edit = QLineEdit(values.get("desc", "")) + example_edit = QLineEdit(values.get("example", "")) + form_layout.addRow(" 说明:", desc_edit) + form_layout.addRow(" 示例:", example_edit) + self.editors[key] = {"desc": desc_edit, "example": example_edit} + + self.layout.addLayout(form_layout) + + self.button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel, self) + self.button_box.button(QDialogButtonBox.Save).setText("保存") + self.button_box.button(QDialogButtonBox.Cancel).setText("取消") + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + self.layout.addWidget(self.button_box) + + def get_prompt_data(self): + """从UI控件收集数据并返回字典""" + new_data = {} + for key, editor_group in self.editors.items(): + new_data[key] = { + "desc": editor_group["desc"].text(), + "example": editor_group["example"].text() + } + return new_data + + +class ImagePreviewDialog(QDialog): + """用于显示大图预览的对话框""" + + def __init__(self, pixmap, parent=None): + super().__init__(parent) + self.setWindowTitle("图片预览") + self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint) + self.label = QLabel(self) + self.label.setAlignment(Qt.AlignCenter) + screen_geometry = QApplication.primaryScreen().availableGeometry() + max_size = QSize(int(screen_geometry.width() * 0.8), int(screen_geometry.height() * 0.8)) + scaled_pixmap = pixmap.scaled(max_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.label.setPixmap(scaled_pixmap) + layout = QVBoxLayout(self) + layout.addWidget(self.label) + self.setLayout(layout) + self.resize(scaled_pixmap.size()) + + +class TemplateManagerDialog(QDialog): + """用于管理可用模板列表的对话框""" + + def __init__(self, templates, parent=None): + super().__init__(parent) + self.setWindowTitle("管理模板库") + self.setMinimumSize(400, 300) + + self.list_widget = QListWidget(self) + self.list_widget.addItems(templates) + self.list_widget.setAlternatingRowColors(True) + + add_button = QPushButton("添加新模板") + edit_button = QPushButton("编辑选中") + delete_button = QPushButton("删除选中") + + button_layout = QHBoxLayout() + button_layout.addWidget(add_button) + button_layout.addWidget(edit_button) + button_layout.addWidget(delete_button) + button_layout.addStretch() + + self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + self.button_box.button(QDialogButtonBox.Ok).setText("完成") + self.button_box.button(QDialogButtonBox.Cancel).setText("取消") + + main_layout = QVBoxLayout(self) + main_layout.addLayout(button_layout) + main_layout.addWidget(self.list_widget) + main_layout.addWidget(self.button_box) + + add_button.clicked.connect(self.add_template) + edit_button.clicked.connect(self.edit_template) + delete_button.clicked.connect(self.delete_template) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + self.list_widget.itemDoubleClicked.connect(self.edit_template) + + def add_template(self): + text, ok = QInputDialog.getText(self, "添加模板", "请输入新的模板名称:") + if ok and text and text.strip(): + self.list_widget.addItem(text.strip()) + + def edit_template(self): + current_item = self.list_widget.currentItem() + if not current_item: return + text, ok = QInputDialog.getText(self, "编辑模板", "请修改模板名称:", QLineEdit.Normal, current_item.text()) + if ok and text and text.strip(): + current_item.setText(text.strip()) + + def delete_template(self): + current_item = self.list_widget.currentItem() + if not current_item: return + reply = QMessageBox.question(self, "确认删除", f"确定要删除模板 '{current_item.text()}' 吗?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + self.list_widget.takeItem(self.list_widget.row(current_item)) + + def get_templates(self): + return [self.list_widget.item(i).text() for i in range(self.list_widget.count())] + + +# --- 辅助类与数据结构 --- + +class ParamStatus(Enum): + IDLE, PROCESSING, DONE, FAILED = auto(), auto(), auto(), auto() + + +@dataclass +class ImageConfig: + path: str + product: str = DEFAULT_PRODUCT + scene: str = DEFAULT_SCENE + model: str = DEFAULT_MODEL + uid: str = field(default_factory=lambda: str(uuid.uuid4())) + param_status: ParamStatus = ParamStatus.IDLE + + +class SvgIcon: + _renderer_cache = {} + + @classmethod + def get_icon(cls, svg_content: str, color: QColor = QColor("#000000"), size: QSize = QSize(20, 20)) -> QIcon: + cache_key = (svg_content, color.rgba(), size.width(), size.height()) + if cache_key in cls._renderer_cache: return cls._renderer_cache[cache_key] + renderer = QSvgRenderer(svg_content.encode('utf-8')) + pixmap = QPixmap(size); + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap); + renderer.render(painter) + painter.setCompositionMode(QPainter.CompositionMode_SourceIn) + painter.fillRect(pixmap.rect(), color); + painter.end() + icon = QIcon(pixmap); + cls._renderer_cache[cache_key] = icon + return icon + + +class FeatherIcons: + FOLDER = '' + PLAY = '' + UP_ARROW = "" + DOWN_ARROW = "" + ICON = '' + WAND = '' + GEAR = '' + + +class AspectRatioLabel(QLabel): + clicked = Signal() + + def __init__(self, text="", parent=None, aspect_ratio=9.0 / 16.0): + super().__init__(text, parent) + self.aspect_ratio = aspect_ratio + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding); + self.setMinimumSize(1, 1) + self._pixmap = QPixmap(); + self.setCursor(Qt.PointingHandCursor) + + def setPixmap(self, pixmap): + self._pixmap = pixmap if isinstance(pixmap, QPixmap) and not pixmap.isNull() else QPixmap() + self.update() + + def heightForWidth(self, width): + return int(width / self.aspect_ratio) + + def hasHeightForWidth(self): + return True + + def paintEvent(self, event): + painter = QPainter(self) + if self._pixmap.isNull(): + painter.setPen(QColor("#A0AEC0")); + painter.drawText(self.rect(), Qt.AlignCenter, self.text()) + else: + size = self.size(); + scaled_pixmap = self._pixmap.scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + x = (size.width() - scaled_pixmap.width()) / 2; + y = (size.height() - scaled_pixmap.height()) / 2 + painter.drawPixmap(int(x), int(y), scaled_pixmap) + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton and not self._pixmap.isNull(): self.clicked.emit() + super().mousePressEvent(event) + + +class StyledSpinBox(QSpinBox): + def __init__(self, parent=None): + super().__init__(parent) + self.setButtonSymbols(QAbstractSpinBox.NoButtons) + self.up_button = QPushButton(self); + self.down_button = QPushButton(self) + self.up_button.setIcon(SvgIcon.get_icon(FeatherIcons.UP_ARROW, size=QSize(12, 12))) + self.down_button.setIcon(SvgIcon.get_icon(FeatherIcons.DOWN_ARROW, size=QSize(12, 12))) + self.up_button.setCursor(Qt.PointingHandCursor); + self.down_button.setCursor(Qt.PointingHandCursor) + self.up_button.clicked.connect(self.stepUp); + self.down_button.clicked.connect(self.stepDown) + self.up_button.setObjectName("SpinBoxUpButton"); + self.down_button.setObjectName("SpinBoxDownButton") + + def resizeEvent(self, event): + super().resizeEvent(event); + button_width = 20 + self.up_button.setGeometry(self.width() - button_width, 0, button_width, self.height() // 2) + self.down_button.setGeometry(self.width() - button_width, self.height() // 2, button_width, self.height() // 2) + + +class Communications(QObject): + log_signal = Signal(str, int); + status_signal = Signal(str); + generation_done_signal = Signal() + preview_signal = Signal(str, str); + params_received_signal = Signal(str, dict) + param_fetch_failed_signal = Signal(str); + single_param_op_finished_signal = Signal() + all_params_op_finished_signal = Signal(); + progress_update_signal = Signal(int, int, str) + + +class ProgressDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("处理中..."); + self.setModal(True); + self.setFixedSize(400, 120) + self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint) + layout = QVBoxLayout(self); + self.status_label = QLabel("正在初始化...", self) + self.status_label.setAlignment(Qt.AlignCenter) + self.progress_bar = QProgressBar(self); + self.progress_bar.setTextVisible(True) + layout.addWidget(self.status_label); + layout.addWidget(self.progress_bar) + + def update_progress(self, current, total, text): + self.status_label.setText(text); + self.progress_bar.setValue(int((current / total) * 100)) + self.progress_bar.setFormat(f"%p% ({current}/{total})") + + def reject(self): pass + + +# --- 主窗口 --- +class VideoGeneratorGUI(QWidget): + def __init__(self): + super().__init__() + self.is_generating = False + self.is_getting_single_param = False + self.comm = Communications() + self.image_configs = [] + self._is_updating_ui = False + self.current_config_uid = None + self.config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.ini") if getattr(sys, + 'frozen', + False) else os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.ini") + # -- 初始化可配置参数 -- + self.llm_prompt_data = DEFAULT_LLM_PROMPT_DATA.copy() + self.available_templates = DEFAULT_AVAILABLE_TEMPLATES[:] + self.selected_templates = DEFAULT_SELECTED_TEMPLATES[:] + + self.init_resources() + self.initUI() + self.connect_signals() + + def init_resources(self): + font_dir = os.path.dirname(__file__) if getattr(sys, 'frozen', False) else os.path.dirname( + os.path.abspath(__file__)) + font_path = os.path.join(font_dir, 'SourceHanSansCN-Medium.ttf') + if os.path.exists(font_path): + font_id = QFontDatabase.addApplicationFont(font_path) + if font_id != -1: + self.app_font = QFont(QFontDatabase.applicationFontFamilies(font_id)[0], 10) + QApplication.instance().setFont(self.app_font); + return + logger.warning("字体加载失败,使用默认字体。") + self.app_font = QFont("Arial", 10) + + def initUI(self): + self.setWindowTitle("视频物料生成工具") + self.setGeometry(100, 100, 1200, 900) + self.setMinimumSize(1100, 900) + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(20, 20, 20, 20) + main_layout.setSpacing(20) + + content_layout = QHBoxLayout(); + content_layout.setSpacing(20) + params_card = self.create_params_and_preview_card() + content_layout.addWidget(params_card, 1) + list_card = self.create_task_list_card() + content_layout.addWidget(list_card, 1) + main_layout.addLayout(content_layout, 1) + + log_card = self.create_log_card() + main_layout.addWidget(log_card, 1) + + bottom_layout = QHBoxLayout() + self.status_bar = QLabel("就绪"); + self.status_bar.setObjectName("StatusBar") + self.generate_button = QPushButton("开始生成") + self.generate_button.setObjectName("GenerateButton") + self.generate_button.setIcon(SvgIcon.get_icon(FeatherIcons.PLAY, QColor("#FFFFFF"))) + bottom_layout.addWidget(self.status_bar); + bottom_layout.addStretch() + bottom_layout.addWidget(self.generate_button) + main_layout.addLayout(bottom_layout) + + self.progress_dialog = ProgressDialog(self) + self.apply_stylesheet() + self.setWindowIcon(SvgIcon.get_icon(FeatherIcons.ICON, QColor("#75C82B"))) + self.toggle_param_view_state(False) + + def showEvent(self, event): + super().showEvent(event) + if not hasattr(self, '_initial_load_done'): self.load_config(); self._initial_load_done = True + + def create_card(self, title, with_settings_button=False): + card = QWidget(); + card.setObjectName("Card") + layout = QVBoxLayout(card); + layout.setContentsMargins(20, 15, 20, 20); + layout.setSpacing(15) + if title: + title_layout = QHBoxLayout(); + card_title = QLabel(title); + card_title.setObjectName("CardTitle") + title_layout.addWidget(card_title); + title_layout.addStretch() + if with_settings_button: + self.prompt_config_button = QPushButton() + self.prompt_config_button.setIcon(SvgIcon.get_icon(FeatherIcons.GEAR, QColor("#666"))) + self.prompt_config_button.setObjectName("SettingsButton"); + self.prompt_config_button.setToolTip("配置智能获取参数的Prompt") + title_layout.addWidget(self.prompt_config_button) + layout.addLayout(title_layout) + shadow = QGraphicsDropShadowEffect(self); + shadow.setBlurRadius(25); + shadow.setXOffset(0); + shadow.setYOffset(5); + shadow.setColor(QColor(0, 0, 0, 30)); + card.setGraphicsEffect(shadow) + return card, layout + + def create_params_and_preview_card(self): + card, layout = self.create_card("图片参数配置", with_settings_button=True) + preview_container = QWidget(); + preview_container_layout = QHBoxLayout(preview_container) + self.preview_label = AspectRatioLabel("请从右侧列表选择图片"); + self.preview_label.setObjectName("PreviewLabel"); + self.preview_label.setFixedSize(135, 240) + preview_container_layout.addStretch(); + preview_container_layout.addWidget(self.preview_label); + preview_container_layout.addStretch() + layout.addWidget(preview_container) + grid_layout = QGridLayout(); + grid_layout.setSpacing(15); + grid_layout.setColumnStretch(1, 1) + self.product_input = self.create_input_field(grid_layout, 0, "产品:", "", "例如:夏季连衣裙") + self.scene_input = self.create_input_field(grid_layout, 1, "场景:", "", "例如:海滩、街拍") + self.model_input = self.create_input_field(grid_layout, 2, "模特:", "", "例如:亚洲女性、金发模特") + layout.addLayout(grid_layout) + param_buttons_layout = QHBoxLayout(); + self.get_params_button = QPushButton("智能获取参数"); + self.get_params_button.setIcon(SvgIcon.get_icon(FeatherIcons.WAND, QColor("#333"))) + self.get_all_params_button = QPushButton("一键获取全部"); + self.get_all_params_button.setIcon(SvgIcon.get_icon(FeatherIcons.WAND, QColor("#333"))) + param_buttons_layout.addWidget(self.get_params_button); + param_buttons_layout.addWidget(self.get_all_params_button) + layout.addLayout(param_buttons_layout) + self.param_widgets = [self.product_input, self.scene_input, self.model_input]; + self.param_buttons = [self.get_params_button, self.get_all_params_button] + layout.addStretch() + return card + + def create_task_list_card(self): + card, layout = self.create_card("任务列表 & 全局配置") + button_layout = QHBoxLayout(); + self.add_button = QPushButton("添加图片"); + self.remove_button = QPushButton("移除选中") + button_layout.addWidget(self.add_button); + button_layout.addWidget(self.remove_button); + button_layout.addStretch(); + layout.addLayout(button_layout) + self.image_list_widget = QListWidget(); + self.image_list_widget.setObjectName("ImageList"); + layout.addWidget(self.image_list_widget, 1) + global_params_label = QLabel("全局参数"); + global_params_label.setObjectName("CardSubTitle"); + layout.addWidget(global_params_label) + template_grid = QGridLayout(); + template_grid.setSpacing(10) + self.selected_templates_display = QLineEdit(); + self.selected_templates_display.setReadOnly(True); + self.selected_templates_display.setPlaceholderText("请选择模板") + self.select_templates_button = QPushButton("选择模板"); + self.manage_templates_button = QPushButton() + self.manage_templates_button.setIcon(SvgIcon.get_icon(FeatherIcons.GEAR, QColor("#333"))); + self.manage_templates_button.setToolTip("管理模板库") + self.manage_templates_button.setObjectName("PathButton") # Re-use style + template_layout = QHBoxLayout(); + template_layout.setSpacing(5); + template_layout.addWidget(self.selected_templates_display, 1); + template_layout.addWidget(self.select_templates_button); + template_layout.addWidget(self.manage_templates_button) + template_grid.addWidget(QLabel("模板:"), 0, 0); + template_grid.addLayout(template_layout, 0, 1) + self.global_duplicate_input = StyledSpinBox(); + self.global_duplicate_input.setMinimum(1); + self.global_duplicate_input.setValue(DEFAULT_DUPLICATE); + self.global_duplicate_input.setObjectName("SpinBox") + template_grid.addWidget(QLabel("重复:"), 1, 0); + template_grid.addWidget(self.global_duplicate_input, 1, 1); + layout.addLayout(template_grid) + output_label = QLabel("视频输出文件夹:"); + output_label.setObjectName("CardSubTitle") + self.output_input = QLineEdit(DEFAULT_OUTPUT); + self.output_input.setPlaceholderText("选择视频输出文件夹") + self.output_button = QPushButton(); + self.output_button.setIcon(SvgIcon.get_icon(FeatherIcons.FOLDER, QColor("#333"))); + self.output_button.setObjectName("PathButton") + path_layout = QHBoxLayout(); + path_layout.addWidget(self.output_input); + path_layout.addWidget(self.output_button) + layout.addWidget(output_label); + layout.addLayout(path_layout) + return card + + def create_input_field(self, layout, row, label_text, value, placeholder): + label = QLabel(label_text); + line_edit = QLineEdit(value); + line_edit.setPlaceholderText(placeholder) + layout.addWidget(label, row, 0); + layout.addWidget(line_edit, row, 1); + return line_edit + + def create_log_card(self): + card, layout = self.create_card("运行日志"); + self.log_text = QTextEdit(); + self.log_text.setReadOnly(True); + self.log_text.setObjectName("LogText"); + layout.addWidget(self.log_text); + return card + + def apply_stylesheet(self): + style = f""" + QWidget {{ font-family: '{self.app_font.family()}'; }} + VideoGeneratorGUI {{ background-color: #F4F6F8; }} + #Card {{ background-color: #FFFFFF; border-radius: 12px; }} + #CardTitle {{ font-size: 16px; font-weight: bold; color: #2D3748; padding-bottom: 5px; border-bottom: 1px solid #E2E8F0; }} + #CardSubTitle {{ font-size: 14px; font-weight: bold; color: #4A5568; margin-top: 10px; }} + QLabel {{ font-size: 14px; color: #4A5568; }} + QLineEdit, #SpinBox {{ background-color: #F7FAFC; border: 1px solid #E2E8F0; border-radius: 8px; padding: 10px; font-size: 14px; color: #2D3748; }} + QLineEdit:focus, #SpinBox:focus {{ border-color: #4299E1; background-color: #FFFFFF; }} + QLineEdit:disabled, #SpinBox:disabled, QPushButton:disabled, QListWidget:disabled {{ background-color: #EDF2F7; color: #A0AEC0; border-color: #E2E8F0; }} + QLineEdit[readOnly=true] {{ background-color: #EDF2F7; }} + #SpinBox {{ padding-right: 20px; }} + #ImageList {{ border: 1px solid #E2E8F0; border-radius: 8px; }} + #ImageList::item {{ padding: 8px; }} #ImageList::item:selected {{ background-color: #EBF8FF; border-left: 3px solid #4299E1; color: #2C5282; }} + #PreviewLabel {{ border: 2px dashed #CBD5E0; border-radius: 8px; color: #A0AEC0; }} + #LogText {{ background-color: #1A202C; color: #F7FAFC; border: none; border-radius: 8px; font-family: 'Monaco', 'Courier New', 'monospace'; font-size: 13px; padding: 10px; }} + QPushButton {{ background-color: #FFFFFF; border: 1px solid #D1D5DB; border-radius: 6px; padding: 8px 15px; font-size: 14px; }} + QPushButton:hover {{ background-color: #F9FAFB; }} + #GenerateButton {{ background-color: #4299E1; color: white; font-weight: bold; border: none; padding: 12px 25px; }} + #GenerateButton:hover {{ background-color: #3182CE; }} #GenerateButton:disabled {{ background-color: #A0AEC0; }} + #PathButton, #SettingsButton {{ min-width: 36px; max-width: 36px; padding: 8px; }} + """ + self.setStyleSheet(style) + + def connect_signals(self): + self.generate_button.clicked.connect(self.start_generation); + self.add_button.clicked.connect(self.add_images) + self.remove_button.clicked.connect(self.remove_selected_image); + self.output_button.clicked.connect(self.select_output_path) + self.image_list_widget.currentItemChanged.connect(self.on_current_image_changed) + self.product_input.editingFinished.connect(self.save_current_parameters) + self.scene_input.editingFinished.connect(self.save_current_parameters); + self.model_input.editingFinished.connect(self.save_current_parameters) + self.preview_label.clicked.connect(self.show_large_preview); + self.prompt_config_button.clicked.connect(self.open_prompt_config) + self.select_templates_button.clicked.connect(self.show_template_selection_menu); + self.manage_templates_button.clicked.connect(self.open_template_manager) + self.get_params_button.clicked.connect(self.get_intelligent_params); + self.get_all_params_button.clicked.connect(self.get_all_intelligent_params) + self.comm.log_signal.connect(self.log); + self.comm.status_signal.connect(self.update_status) + self.comm.generation_done_signal.connect(self.on_generation_finished); + self.comm.params_received_signal.connect(self.on_params_received) + self.comm.param_fetch_failed_signal.connect(self.on_param_fetch_failed); + self.comm.single_param_op_finished_signal.connect(self.on_single_param_op_finished) + self.comm.all_params_op_finished_signal.connect(self.on_all_params_op_finished); + self.comm.progress_update_signal.connect(self.progress_dialog.update_progress) + self.comm.preview_signal.connect(self.show_preview_box) + + def toggle_batch_ui_lock(self, locked: bool): + widgets_to_lock = [ + self.image_list_widget, self.add_button, self.remove_button, self.output_input, self.output_button, + self.select_templates_button, self.manage_templates_button, self.global_duplicate_input, + self.prompt_config_button, + self.get_params_button, self.get_all_params_button, self.generate_button, *self.param_widgets] + for widget in widgets_to_lock: widget.setDisabled(locked) + if not locked and self.image_list_widget.currentItem(): self.toggle_param_view_state(True) + + def open_prompt_config(self): + dialog = PromptConfigDialog(self.llm_prompt_data, self) + if dialog.exec(): + self.llm_prompt_data = dialog.get_prompt_data() + self.log("智能参数Prompt已更新。", logging.INFO) + + def show_large_preview(self): + if self.preview_label._pixmap.isNull(): return + ImagePreviewDialog(self.preview_label._pixmap, self).exec() + + def open_template_manager(self): + dialog = TemplateManagerDialog(self.available_templates, self) + if dialog.exec(): + self.available_templates = dialog.get_templates() + self.selected_templates = [t for t in self.selected_templates if t in self.available_templates] + self.update_selected_templates_display(); + self.log("模板库已更新。", logging.INFO) + + def show_template_selection_menu(self): + menu = QMenu(self) + for template in self.available_templates: + action = QAction(template, self); + action.setCheckable(True) + if template in self.selected_templates: action.setChecked(True) + action.triggered.connect(lambda checked, t=template: self.on_template_selected(t, checked)) + menu.addAction(action) + menu.exec(self.select_templates_button.mapToGlobal(self.select_templates_button.rect().bottomLeft())) + + def on_template_selected(self, template, is_checked): + if is_checked and template not in self.selected_templates: + self.selected_templates.append(template) + elif not is_checked and template in self.selected_templates: + self.selected_templates.remove(template) + self.update_selected_templates_display() + + def update_selected_templates_display(self): + self.selected_templates_display.setText(", ".join(self.selected_templates)) + + def add_images(self): + file_paths, _ = QFileDialog.getOpenFileNames(self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg)") + if not file_paths: return + last_config = self.image_configs[-1] if self.image_configs else None + for path in file_paths: + new_config = ImageConfig(path=path) + if last_config: new_config.product, new_config.scene, new_config.model = last_config.product, last_config.scene, last_config.model + self.image_configs.append(new_config) + item = QListWidgetItem(os.path.basename(path)); + item.setData(Qt.UserRole, new_config.uid) + self.image_list_widget.addItem(item) + self.log(f"添加了 {len(file_paths)} 张新图片。", logging.INFO) + if self.image_list_widget.count() > 0: self.image_list_widget.setCurrentRow( + self.image_list_widget.count() - len(file_paths)) + + def remove_selected_image(self): + current_item = self.image_list_widget.currentItem() + if not current_item: return + uid_to_remove = current_item.data(Qt.UserRole) + self.image_configs = [cfg for cfg in self.image_configs if cfg.uid != uid_to_remove] + self.image_list_widget.takeItem(self.image_list_widget.row(current_item)) + self.log(f"移除了图片: {current_item.text()}", logging.INFO) + + def on_current_image_changed(self, current_item, _): + if self._is_updating_ui: return + if not current_item: + self.clear_param_view(); + self.toggle_param_view_state(False); + self.current_config_uid = None; + return + uid = current_item.data(Qt.UserRole) + if config := self.get_config_by_uid(uid): + self.current_config_uid = uid; + self.populate_param_view(config); + self.toggle_param_view_state(True) + + def populate_param_view(self, config: ImageConfig): + self._is_updating_ui = True + if (pixmap := QPixmap(config.path)).isNull(): + self.preview_label.setText("预览失败"); + self.preview_label.setPixmap(QPixmap()) + else: + self.preview_label.setPixmap(pixmap) + if config.param_status == ParamStatus.PROCESSING: + for widget in self.param_widgets: widget.setText(""); widget.setPlaceholderText("正在智能获取中...") + else: + self.product_input.setText(config.product); + self.product_input.setPlaceholderText("例如:夏季连衣裙") + self.scene_input.setText(config.scene); + self.scene_input.setPlaceholderText("例如:海滩、街拍") + self.model_input.setText(config.model); + self.model_input.setPlaceholderText("例如:亚洲女性、金发模特") + self._is_updating_ui = False + + def save_current_parameters(self): + if self._is_updating_ui or not self.current_config_uid: return + if ( + config := self.get_config_by_uid( + self.current_config_uid)) and config.param_status != ParamStatus.PROCESSING: + config.product, config.scene, config.model = self.product_input.text(), self.scene_input.text(), self.model_input.text() + + def get_config_by_uid(self, uid: str) -> ImageConfig | None: + return next((config for config in self.image_configs if config.uid == uid), None) + + def clear_param_view(self): + self._is_updating_ui = True; + [w.clear() for w in self.param_widgets] + self.preview_label.setText("请从右侧列表选择图片"); + self.preview_label.setPixmap(QPixmap()); + self._is_updating_ui = False + + def toggle_param_view_state(self, enabled: bool): + if not enabled: [w.setDisabled(True) for w in self.param_widgets + self.param_buttons]; return + config = self.get_config_by_uid(self.current_config_uid) + is_processing = config and config.param_status == ParamStatus.PROCESSING + [w.setDisabled(is_processing) for w in self.param_widgets] + [b.setDisabled(self.is_getting_single_param or self.is_generating) for b in self.param_buttons] + self.prompt_config_button.setDisabled(self.is_generating or self.is_getting_single_param) + + def load_config(self): + if not os.path.exists(self.config_path): + self.log("未找到配置文件,将使用默认设置。", logging.INFO); + self.update_selected_templates_display(); + return + self.log("正在加载配置...", logging.INFO) + config = configparser.ConfigParser(); + config.read(self.config_path, "utf-8") + if 'General' in config: + gen_conf = config['General']; + self.output_input.setText(gen_conf.get('OUTPUT', DEFAULT_OUTPUT)) + self.global_duplicate_input.setValue(gen_conf.getint('DUPLICATE', DEFAULT_DUPLICATE)) + self.available_templates = [t.strip() for t in gen_conf.get('AvailableTemplates', "").split(',') if + t.strip()] or DEFAULT_AVAILABLE_TEMPLATES[:] + self.selected_templates = [t.strip() for t in gen_conf.get('SelectedTemplates', "").split(',') if + t.strip()] or DEFAULT_SELECTED_TEMPLATES[:] + self.update_selected_templates_display() + if 'LLM' in config and (prompt_data_str := config['LLM'].get('PromptData')): + try: + self.llm_prompt_data = json.loads(prompt_data_str) + except json.JSONDecodeError: + self.log("加载Prompt配置失败,使用默认值。", logging.WARNING) + image_sections = sorted([s for s in config.sections() if s.startswith('Image_')]) + for section_name in image_sections: + section = config[section_name]; + path = section.get('path') + if not path or not os.path.exists(path): self.log(f"配置文件图片路径无效: {path}", + logging.WARNING); continue + cfg = ImageConfig(path=path, product=section.get('product', DEFAULT_PRODUCT), + scene=section.get('scene', DEFAULT_SCENE), model=section.get('model', DEFAULT_MODEL)) + self.image_configs.append(cfg); + item = QListWidgetItem(os.path.basename(path)); + item.setData(Qt.UserRole, cfg.uid); + self.image_list_widget.addItem(item) + if self.image_list_widget.count() > 0: self.image_list_widget.setCurrentRow(0) + self.log(f"配置加载完成,共 {len(self.image_configs)} 个任务。", logging.INFO) + + def closeEvent(self, event): + self.save_current_parameters() + if QMessageBox.question(self, '确认退出', '确定要保存当前配置并退出吗?', QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes) == QMessageBox.Yes: + self.save_config_to_file(); + event.accept() + else: + event.ignore() + + def save_config_to_file(self): + self.log("正在保存配置...", logging.INFO); + config = configparser.ConfigParser() + config['General'] = {'OUTPUT': self.output_input.text(), 'DUPLICATE': str(self.global_duplicate_input.value()), + 'AvailableTemplates': ",".join(self.available_templates), + 'SelectedTemplates': ",".join(self.selected_templates)} + config['LLM'] = {'PromptData': json.dumps(self.llm_prompt_data, ensure_ascii=False, indent=2)} + for i, cfg in enumerate(self.image_configs): config[f'Image_{i}'] = {'path': cfg.path, 'product': cfg.product, + 'scene': cfg.scene, 'model': cfg.model} + try: + with open(self.config_path, "w", encoding="utf-8") as f: + config.write(f) + self.log("配置已成功保存。", logging.INFO) + except Exception as e: + self.log(f"保存配置失败: {e}", logging.ERROR) + + def start_generation(self): + if self.is_generating or self.is_getting_single_param: return + if not self.image_configs: QMessageBox.critical(self, "错误", "请至少添加一张图片到任务列表。"); return + if not self.output_input.text() or not os.path.isdir(self.output_input.text()): QMessageBox.critical(self, + "错误", + "请选择一个有效的视频输出文件夹。"); return + if not self.selected_templates: QMessageBox.critical(self, "错误", "请至少选择一个生成模板。"); return + self.is_generating = True; + self.generate_button.setDisabled(True); + self.generate_button.setText("生成中...") + self.update_status("开始生成任务..."); + self.log(f"--- 任务开始,共 {len(self.image_configs)} 张图片 ---", logging.INFO) + threading.Thread(target=self.generate_video_thread, + args=(self.image_configs[:], self.global_duplicate_input.value(), self.selected_templates[:]), + daemon=True).start() + + def on_generation_finished(self): + self.is_generating = False; + self.generate_button.setDisabled(False); + self.generate_button.setText("开始生成") + self.update_status("所有任务完成"); + self.log("--- 所有任务完成 ---", logging.INFO) + + def generate_video_thread(self, configs_to_process, duplicate, templates): + try: + total_images = len(configs_to_process) + if not templates: self.comm.log_signal.emit("全局模板为空,任务中止。", logging.ERROR); return + for idx, config in enumerate(configs_to_process): + image_basename = os.path.basename(config.path) + self.comm.log_signal.emit(f"--- ({idx + 1}/{total_images}) 开始处理: {image_basename} ---", + logging.INFO) + image_url = self.upload_to_s3(config.path) + if not image_url: self.comm.log_signal.emit(f"图片 {image_basename} 上传失败,跳过。", + logging.ERROR); continue + self.comm.status_signal.emit(f"正在为 {image_basename} 生成视频...") + for template in templates: + self.comm.log_signal.emit(f"处理模板: {template} (图片: {image_basename})", logging.INFO) + if video_urls := self.generate_video_from_config(image_url, template, config, duplicate): + for url in video_urls: + if url and isinstance(url, str): + self.download_video(url, template, config.path) + else: + self.comm.log_signal.emit(f"收到无效视频URL '{url}',跳过下载。", logging.WARNING) + else: + self.comm.log_signal.emit(f"模板 {template} 生成失败或未返回URL。", logging.ERROR) + except Exception as e: + self.comm.log_signal.emit(f"线程发生未知错误: {e}", logging.ERROR); + logger.exception( + "Error in generation thread") + finally: + self.comm.generation_done_signal.emit() + + def _build_llm_prompt(self): + """从结构化数据动态构建LLM Prompt字符串""" + parts = ["请详细描述图中的信息,并以json格式返回。包含以下key:"] + for key, data in self.llm_prompt_data.items(): + parts.append(f'- {key}:{data["desc"]},例如:“{data["example"]}”。') + return "\n".join(parts) + + def _get_params_worker_logic(self, config: ImageConfig): + try: + with open(config.path, "rb") as image_file: + image_data = image_file.read() + base64_encoded_data = base64.b64encode(image_data).decode('utf-8') + mime_type, _ = mimetypes.guess_type(config.path); + mime_type = mime_type or 'image/jpeg' + payload = {"model": LLM_PROVIDER, "messages": [{"role": "user", "content": [ + {"type": "text", "text": self._build_llm_prompt()}, + {"type": "image_url", "image_url": {"url": f"data:{mime_type};base64,{base64_encoded_data}"}} + ]}], "temperature": 0.2, "max_tokens": 500} + with httpx.Client(timeout=httpx.Timeout(120.0, connect=15)) as session: + resp = session.post(LLM_API_URL, + headers={"Content-Type": "application/json", "Accept": "application/json", + "Authorization": "Bearer auth-bowong7777"}, json=payload) + resp.raise_for_status() + content_str = re.sub(r'```json\s*|\s*```', '', resp.json()['choices'][0]['message']['content']).strip() + params = json.loads(content_str) + self.comm.params_received_signal.emit(config.uid, params) + except Exception as e: + self.comm.log_signal.emit(f"智能获取参数失败 ({os.path.basename(config.path)}): {e}", logging.ERROR) + self.comm.param_fetch_failed_signal.emit(config.uid); + logger.exception("Error in LLM worker") + + def _get_params_worker(self, config: ImageConfig): + try: + self.comm.log_signal.emit(f"正在读取图片并请求LLM: {os.path.basename(config.path)}", logging.INFO) + self._get_params_worker_logic(config) + finally: + self.comm.single_param_op_finished_signal.emit() + + def _get_all_params_worker(self, configs_to_process): + total = len(configs_to_process) + for i, config in enumerate(configs_to_process): + filename = os.path.basename(config.path) + self.comm.progress_update_signal.emit(i, total, f"正在处理: {filename}") + config.param_status = ParamStatus.PROCESSING + self._get_params_worker_logic(config) + self.comm.progress_update_signal.emit(i + 1, total, f"已完成: {filename}") + time.sleep(0.5) + self.comm.all_params_op_finished_signal.emit() + + def log(self, message, level=logging.INFO): + color_map = {logging.ERROR: "#F56565", logging.WARNING: "#ECC94B", logging.INFO: "#48BB78", + logging.DEBUG: "#A0AEC0"} + formatted_message = f'{message}' + self.log_text.append(formatted_message); + logger.log(level, message) + + def generate_video_from_config(self, image_url: str, template: str, config: ImageConfig, duplicate: int): + url = f"{HOST}/webhook/glam-video-generation"; + headers = {"Content-Type": "application/json"} + data = {"product": config.product, "scene": config.scene, "model": config.model, "image": image_url, + "duplicate": duplicate, "template": template} + for attempt in range(3): + try: + self.comm.log_signal.emit(f"调用API (模板: {template}, 尝试: {attempt + 1}/3)...", logging.INFO) + response = requests.post(url, headers=headers, json=data, timeout=600); + response.raise_for_status() + result = response.json() + if isinstance(result, list) and all("data" in item for item in result): + return [i["data"] for i in result] + else: + self.comm.log_signal.emit(f"API返回错误: {result.get('error', '未知API错误')}", + logging.ERROR); + return None + except requests.exceptions.RequestException as e: + self.comm.log_signal.emit(f"API请求失败 (尝试 {attempt + 1}): {e}", logging.WARNING) + if attempt < 2: + time.sleep(5) + else: + self.comm.log_signal.emit(f"API请求最终失败 (模板: {template})", logging.ERROR); + return None + return None + + def get_intelligent_params(self): + if self.is_getting_single_param or self.is_generating: return + if not (current_item := self.image_list_widget.currentItem()): QMessageBox.information(self, "提示", + "请先在右侧列表中选择一张图片。"); return + if not (config := self.get_config_by_uid(current_item.data(Qt.UserRole))): return + self.is_getting_single_param = True; + self.add_button.setDisabled(True); + self.remove_button.setDisabled(True); + [b.setDisabled(True) for b in self.param_buttons] + config.param_status = ParamStatus.PROCESSING; + self.populate_param_view(config); + self.toggle_param_view_state(True) + self.update_status(f"正在为 {os.path.basename(config.path)} 智能获取参数..."); + threading.Thread(target=self._get_params_worker, args=(config,), daemon=True).start() + + def on_single_param_op_finished(self): + self.is_getting_single_param = False; + self.add_button.setDisabled(False); + self.remove_button.setDisabled(False) + self.toggle_param_view_state(True); + self.update_status("就绪") + + def get_all_intelligent_params(self): + if self.is_getting_single_param or self.is_generating: return + if not self.image_configs: QMessageBox.information(self, "提示", "任务列表为空,请先添加图片。"); return + self.toggle_batch_ui_lock(True); + self.progress_dialog.show(); + self.log("--- 开始一键获取所有图片参数 ---", logging.INFO) + threading.Thread(target=self._get_all_params_worker, args=(self.image_configs[:],), daemon=True).start() + + def on_all_params_op_finished(self): + self.toggle_batch_ui_lock(False); + self.progress_dialog.hide(); + self.log("--- 所有图片参数获取完成 ---", logging.INFO) + if self.image_list_widget.currentItem(): self.on_current_image_changed(self.image_list_widget.currentItem(), + None) + + def on_params_received(self, uid: str, params: dict): + if config := self.get_config_by_uid(uid): + config.product = params.get("product", config.product); + config.scene = params.get("scene", config.scene); + config.model = params.get("model", config.model) + config.param_status = ParamStatus.DONE + if self.current_config_uid == uid: self.populate_param_view(config); self.toggle_param_view_state(True) + self.log(f"图片 '{os.path.basename(config.path)}' 的智能参数已更新。", logging.INFO) + + def on_param_fetch_failed(self, uid: str): + if config := self.get_config_by_uid(uid): + config.param_status = ParamStatus.FAILED + if self.current_config_uid == uid: self.populate_param_view(config); self.toggle_param_view_state(True) + self.log(f"图片 '{os.path.basename(config.path)}' 的参数获取失败。", logging.WARNING) + + def update_status(self, message): + self.status_bar.setText(message) + + def select_output_path(self): + if dir_path := QFileDialog.getExistingDirectory(self, "选择输出文件夹"): self.output_input.setText( + dir_path); self.log(f"已选择输出路径: {dir_path}", logging.INFO) + + def upload_to_s3(self, file_path): + self.comm.log_signal.emit(f"上传图片 {os.path.basename(file_path)}...", logging.INFO) + if not all([AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET]): self.comm.log_signal.emit( + "S3配置不完整,无法上传。", logging.ERROR); return None + try: + client = boto3.client("s3", aws_access_key_id=AWS_ACCESS_KEY_ID, + aws_secret_access_key=AWS_SECRET_ACCESS_KEY) + dest_key = f"upload/{uuid.uuid4()}{os.path.splitext(file_path)[1]}" + client.upload_file(file_path, S3_BUCKET, dest_key); + s3_url = f"https://cdn.roasmax.cn/{dest_key}" + self.comm.log_signal.emit(f"图片上传成功: {s3_url}", logging.INFO); + return s3_url + except Exception as e: + self.comm.log_signal.emit(f"图片上传失败: {e}", logging.ERROR); + return None + + def download_video(self, video_url, template, source_image_path): + image_basename = os.path.basename(source_image_path); + self.comm.status_signal.emit(f"下载视频 (模板: {template}, 源: {image_basename})...") + try: + response = requests.get(video_url, stream=True, timeout=300) + response.raise_for_status() + base_name, output_dir = os.path.splitext(image_basename)[0], self.output_input.text() + num = 1 + safe_template_name = re.sub(r'[\\/*?:"<>|]', '_', template) + while True: + output_path = "/".join([output_dir, f"{base_name}_{safe_template_name}_{num}.mp4"]) + if not os.path.exists(output_path): break + num += 1 + with open(output_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): f.write(chunk) + self.comm.log_signal.emit(f"视频下载完成: {output_path}", logging.INFO) + # self.comm.preview_signal.emit(output_path, image_basename) + except Exception as e: + self.comm.log_signal.emit(f"视频下载失败 ({video_url}): {e}", logging.ERROR) + + def show_preview_box(self, output_path, source_image_basename): + msg_box = QMessageBox(self) + msg_box.setWindowTitle("下载完成") + msg_box.setText(f"源于 {source_image_basename} 的视频已保存到:\n{output_path}") + msg_box.setStandardButtons(QMessageBox.Open | QMessageBox.Ok) + open_button = msg_box.button(QMessageBox.Open) + open_button.setText("打开文件夹") + if msg_box.exec() == QMessageBox.Open: + output_dir = os.path.dirname(output_path) + try: + if sys.platform == "win32": + os.startfile(output_dir) + elif sys.platform == "darwin": + os.system(f'open "{output_dir}"') + else: + os.system(f'xdg-open "{output_dir}"') + except Exception as e: + self.log(f"无法打开文件夹 {output_dir}: {e}", logging.ERROR) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = VideoGeneratorGUI() + window.show() + sys.exit(app.exec())