From 8e5cb27f323089603c020394e427a23fc608246a Mon Sep 17 00:00:00 2001 From: "shuohigh@gmail.com" Date: Tue, 29 Apr 2025 10:31:25 +0800 Subject: [PATCH] =?UTF-8?q?Modal=20worker=E7=BB=9F=E4=B8=80python=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 13 + .idea/dataSources.xml | 12 + .idea/misc.xml | 2 +- .idea/modalDeploy.iml | 2 +- .python-version | 1 + AutoDL/AutoDL_pure_heygem.py | 270 --------- .../autodl_scheduling/entity/instance_pool.py | 146 ----- AutoDL/autodl_scheduling/entity/result_map.py | 36 -- .../autodl_scheduling/entity/running_pool.py | 82 --- .../autodl_scheduling/entity/waiting_queue.py | 21 - AutoDL/autodl_scheduling/server.py | 125 ---- AutoDL/autodl_scheduling/util/audodl_sdk.py | 305 ---------- AutoDL/heygem.py | 12 - README.md | 3 + deploy.py | 80 --- heygem.py | 12 - pyproject.toml | 23 + server_pure_heygem.py | 298 --------- server_with_s3.py | 258 -------- server_with_s3_auth.py | 261 -------- server_with_s3_auth_heygem.py | 320 ---------- src/__init__.py | 0 src/cluster/__init__.py | 0 src/cluster/app.py | 13 + src/cluster/config.py | 25 + src/cluster/ffmpeg_worker/Utils/PathUtils.py | 12 + src/cluster/ffmpeg_worker/Utils/TimeUtils.py | 16 + src/cluster/ffmpeg_worker/Utils/__init__.py | 0 src/cluster/ffmpeg_worker/__init__.py | 0 src/cluster/ffmpeg_worker/model.py | 65 ++ src/cluster/ffmpeg_worker/worker.py | 132 ++++ src/cluster/video_downloader/__init__.py | 0 src/cluster/video_downloader/model.py | 103 ++++ src/cluster/video_downloader/worker.py | 301 +++++++++ src/cluster/web/__init__.py | 0 src/cluster/web/model.py | 49 ++ src/cluster/web/worker.py | 281 +++++++++ src/deploy.py | 5 + upload.py | 8 - uv.lock | 571 ++++++++++++++++++ 40 files changed, 1627 insertions(+), 2236 deletions(-) create mode 100644 .env create mode 100644 .idea/dataSources.xml create mode 100644 .python-version delete mode 100644 AutoDL/AutoDL_pure_heygem.py delete mode 100644 AutoDL/autodl_scheduling/entity/instance_pool.py delete mode 100644 AutoDL/autodl_scheduling/entity/result_map.py delete mode 100644 AutoDL/autodl_scheduling/entity/running_pool.py delete mode 100644 AutoDL/autodl_scheduling/entity/waiting_queue.py delete mode 100644 AutoDL/autodl_scheduling/server.py delete mode 100644 AutoDL/autodl_scheduling/util/audodl_sdk.py delete mode 100644 AutoDL/heygem.py create mode 100644 README.md delete mode 100644 deploy.py delete mode 100644 heygem.py create mode 100644 pyproject.toml delete mode 100644 server_pure_heygem.py delete mode 100644 server_with_s3.py delete mode 100644 server_with_s3_auth.py delete mode 100644 server_with_s3_auth_heygem.py create mode 100644 src/__init__.py create mode 100644 src/cluster/__init__.py create mode 100644 src/cluster/app.py create mode 100644 src/cluster/config.py create mode 100644 src/cluster/ffmpeg_worker/Utils/PathUtils.py create mode 100644 src/cluster/ffmpeg_worker/Utils/TimeUtils.py create mode 100644 src/cluster/ffmpeg_worker/Utils/__init__.py create mode 100644 src/cluster/ffmpeg_worker/__init__.py create mode 100644 src/cluster/ffmpeg_worker/model.py create mode 100644 src/cluster/ffmpeg_worker/worker.py create mode 100644 src/cluster/video_downloader/__init__.py create mode 100644 src/cluster/video_downloader/model.py create mode 100644 src/cluster/video_downloader/worker.py create mode 100644 src/cluster/web/__init__.py create mode 100644 src/cluster/web/model.py create mode 100644 src/cluster/web/worker.py create mode 100644 src/deploy.py delete mode 100644 upload.py create mode 100644 uv.lock diff --git a/.env b/.env new file mode 100644 index 0000000..9ac7c27 --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +R2_API_TOKEN=_AmmE60blmX2wLIu6cuBWDhOuNpHOE4xQH6n5fYH +R2_ACCESS_KEY_ID=79cde91614e6e43eeb6ee9d63836b64e +R2_SECRET_ACCESS_KEY=7a712d6026e9f96fbde2b2efc2411f3dc5e4f9bed6e7d190e0f437a7aa77f9a0 + +VOD_SECRET_ID=AKIDsrihIyjZOBsjimt8TsN8yvv1AMh5dB44 +VOD_SECRET_KEY=CPZcxdk6W39Jd4cGY95wvupoyMd0YFqW + +AWS_ACCESS_KEY_ID=AKIAYRH5NGRSWHN2L4M6 +AWS_SECRET_ACCESS_KEY=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 + +CF_KV_API_TOKEN=hAiXy3jB-Lt5q-XiIV588lyvyH1c8FJaG3oieqeT +CF_KV_NAMESPACE_ID=f24d396e0daa418e89a1d7074b435c24 +CF_ACCOUNT_ID=67720b647ff2b55cf37ba3ef9e677083 diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..f44cd4f --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + mysql.8 + true + com.mysql.cj.jdbc.Driver + jdbc:mysql://root:MrYRE^u9aU$n@sh-cynosdbmysql-grp-hofmpgvu.sql.tencentcdb.com:23314/local_merge + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index c7eed4e..0fa8607 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/.idea/modalDeploy.iml b/.idea/modalDeploy.iml index a661ebe..851df58 100644 --- a/.idea/modalDeploy.iml +++ b/.idea/modalDeploy.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/AutoDL/AutoDL_pure_heygem.py b/AutoDL/AutoDL_pure_heygem.py deleted file mode 100644 index 660c454..0000000 --- a/AutoDL/AutoDL_pure_heygem.py +++ /dev/null @@ -1,270 +0,0 @@ -import json -import os -import shutil -import socket -import subprocess -import time -import traceback -import uuid -from concurrent.futures.thread import ThreadPoolExecutor -from typing import Any, Optional, Union - -import httpx -import loguru -import requests -import starlette.datastructures -import uvicorn - -from fastapi import UploadFile, HTTPException, Depends, FastAPI -from fastapi.routing import APIRoute -from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from starlette import status - - -class HeyGem: - def __init__(self): - def check_port_in_use(port, host='127.0.0.1'): - s = None - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(1) - s.connect((host, int(port))) - return True - except socket.error: - return False - finally: - if s: - s.close() - - uid = str(uuid.uuid4()) - cmd = "/bin/bash -c \"echo client_id: %s && source /root/.bashrc && nohup python /root/heygem.py >> /root/logs/%s.log 2>&1 &\"" % (uid, uid) - subprocess.run(cmd, shell=True, check=True) - timeout = 30 - while timeout > 0: - check = check_port_in_use(8383) - if check: - loguru.logger.success("HeyGem Server started on port 8383") - break - else: - timeout -= 1 - time.sleep(2) - if timeout == 0: - raise TimeoutError("HeyGem Server Start timed out") - self.result = {} - self.executor = ThreadPoolExecutor(max_workers=1) - self.server = FastAPI() - self.server.routes.append(APIRoute(path='/', endpoint=self.api, methods=['POST'])) - self.server.routes.append(APIRoute(path='/{code}', endpoint=self.get_result, methods=['GET'])) - - - - def infer(self, code, video_path, audio_path) -> str: - def task_submit(uid, video, audio, heygem_url): - """Submit a task to the API""" - task_submit_api = f'{heygem_url}/easy/submit' - result_json = { - 'status': False, 'data': {}, 'msg': '' - } - try: - data = { - "code": uid, - "video_url": video, - "audio_url": audio, - "chaofen": 1, - "watermark_switch": 0, - "pn": 1 - } - loguru.logger.info(f'data={json.dumps(data, ensure_ascii=False)}') - with httpx.Client() as client: - resp = client.post(task_submit_api, json=data) - resp_dict = resp.json() - loguru.logger.info(f'submit data: {json.dumps(resp_dict, ensure_ascii=False)}') - if resp_dict['code'] != 10000: - result_json['status'] = False - result_json['msg'] = result_json['msg'] - else: - result_json['status'] = True - result_json['data'] = uid - result_json['msg'] = '任务提交成功' - return result_json - except Exception as e: - loguru.logger.info(f'submit task fail case by:{str(e)}') - raise RuntimeError(str(e)) - - def query_task_progress(heygem_url, heygem_temp_path, task_id: str, interval: int = 10, timeout: int = 60 * 35): - """Query task progress and wait for completion""" - result_json = {'status': False, 'data': {}, 'msg': ''} - - def query_result(t_id: str): - tmp_dict = {'status': True, 'data': dict(), 'msg': ''} - try: - query_task_url = f'{heygem_url}/easy/query' - params = { - 'code': t_id - } - with httpx.Client() as client: - resp = client.get(query_task_url, params=params) - resp_dict = resp.json() - loguru.logger.info(f'query task data: {json.dumps(resp_dict, ensure_ascii=False)}') - status_code = resp_dict['code'] - if status_code in (9999, 10002, 10003, 10001): - tmp_dict['status'] = False - tmp_dict['msg'] = resp_dict['msg'] - elif status_code == 10000: - status_code = resp_dict['data'].get('status', 1) - if status_code == 3: - tmp_dict['status'] = False - tmp_dict['msg'] = resp_dict['data']['msg'] - else: - process = resp_dict['data'].get('progress', 20) - if status_code == 2: - process = 100 - else: - process = process - result = resp_dict['data'].get('result', '') - tmp_dict['data'] = {'progress': process, - 'path': result, - } - else: - pass - except Exception as e: - loguru.logger.info(f'query task fail case by:{str(e)}') - raise RuntimeError(str(e)) - return tmp_dict - - end = time.time() + timeout - while time.time() < end: - tmp_result = query_result(task_id) - if not tmp_result['status'] or tmp_result['data'].__eq__({}): - result_json['status'] = False - result_json['msg'] = tmp_result['msg'] - break - else: - process = tmp_result['data']['progress'] - loguru.logger.info(f'query task progress :{process}') - if tmp_result['data']['progress'] < 100: - time.sleep(interval) - loguru.logger.info(f'wait next interval:{interval}') - else: - p = tmp_result['data']['path'] - p = p.replace('/', '').replace('\\', '') - result_json['data'] = "%s/%s" % (heygem_temp_path, p) - result_json['status'] = True - break - return result_json - - try: - heygem_url = "http://127.0.0.1:8383" - heygem_temp_path = "/code/data/temp" - submit_result = task_submit(code, video_path, audio_path, heygem_url) - if not submit_result['status']: - raise Exception(f"Task submission failed: {submit_result['msg']}") - - task_id = submit_result['data'] - loguru.logger.info(f'Submitted task: {task_id}') - - # Query task progress - progress_result = query_task_progress(heygem_url, heygem_temp_path, task_id, interval=5) - - if not progress_result['status']: - raise RuntimeError(f"Task processing failed: {progress_result['msg']}") - - # Return the file for download - file_path = progress_result['data'] - if not os.path.exists(file_path): - raise FileNotFoundError(f"Output file not found at {file_path}") - return file_path - except Exception as e: - loguru.logger.error(f"Error processing request || {str(e)}") - raise Exception(str(e)) - finally: - try: - os.remove(video_path) - os.remove(audio_path) - except: - pass - - def download_file(self, url, path): - with requests.get(url, stream=True) as r: - with open(path, 'wb') as f: - shutil.copyfileobj(r.raw, f) - - async def get_result(self, code:str, token: HTTPAuthorizationCredentials = Depends(HTTPBearer())): - if token.credentials != os.environ["AUTH_TOKEN"]: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect bearer token", - headers={"WWW-Authenticate": "Bearer"}, - ) - if code in self.result: - return self.result[code] - else: - return {"status": "waiting", "msg":""} - - async def api(self, video_file: Optional[Union[UploadFile, str]], audio_file: Optional[Union[UploadFile, str]], token: HTTPAuthorizationCredentials = Depends(HTTPBearer())): - if token.credentials != os.environ["AUTH_TOKEN"]: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect bearer token", - headers={"WWW-Authenticate": "Bearer"}, - ) - code = str(uuid.uuid4()) - video_path=None - audio_path=None - try: - file_id = str(uuid.uuid4()) - if isinstance(video_file, UploadFile) or isinstance(video_file, starlette.datastructures.UploadFile): - video_path = "/root/%s.%s" % (file_id, video_file.filename.split(".")[-1]) - with open(video_path, "wb") as f: - data = await video_file.read() - f.write(data) - elif isinstance(video_file, str) and video_file.startswith("http"): - video_path = "/root/%s.%s" % (file_id, video_file.split("?")[0].split("/")[-1].split(".")[-1]) - self.download_file(video_file, video_path) - else: - self.result[code] = {"status": "fail", "msg": "video file save failed: Not a valid input"} - - if isinstance(audio_file, UploadFile) or isinstance(audio_file, starlette.datastructures.UploadFile): - audio_path = "/root/%s.%s" % (file_id, audio_file.filename.split(".")[-1]) - with open(audio_path, "wb") as f: - data = await audio_file.read() - f.write(data) - elif isinstance(audio_file, str) and audio_file.startswith("http"): - audio_path = "/root/%s.%s" % (file_id, audio_file.split("?")[0].split("/")[-1].split(".")[-1]) - self.download_file(audio_file, audio_path) - else: - self.result[code] = {"status": "fail", "msg": "audio file save failed: Not a valid input"} - except Exception as e: - self.result[code] = {"status": "fail", "msg": f"video or audio file save failed: {str(e)}"} - # Submit - if video_path and audio_path: - self.executor.submit(self._api, video_path=video_path, audio_path=audio_path, code=code) - return {"status": "success", "code": code} - else: - return self.result[code] - - def _api(self, video_path: str, audio_path: str, code): - loguru.logger.info(f"Enter Inference Module") - try: - file_path = self.infer(code, video_path, audio_path) - try: - loguru.logger.info("Try move file to S3 manually") - # S3 Fallback - import boto3 - import yaml - with open("/root/config.yaml", encoding="utf-8", - mode="r+") as config: - yaml_config = yaml.load(config, Loader=yaml.FullLoader) - awss3 = boto3.resource('s3', aws_access_key_id=yaml_config["aws_key_id"], - aws_secret_access_key=yaml_config["aws_access_key"]) - awss3.meta.client.upload_file(file_path, "bw-heygem-output", file_path.split(os.path.sep)[-1]) - except Exception as e: - self.result[code] = {"status": "fail", "msg": "Failed to move file to S3 manually: " + str(e)} - self.result[code] = {"status": "success", "msg":f"{file_path.split(os.path.sep)[-1]}"} - except Exception as e: - traceback.print_exc() - self.result[code] = {"status": "fail", "msg": "Inference module failed: "+str(e)} - -if __name__ == "__main__": - heygem = HeyGem() - uvicorn.run(app=heygem.server, host="0.0.0.0", port=6006) \ No newline at end of file diff --git a/AutoDL/autodl_scheduling/entity/instance_pool.py b/AutoDL/autodl_scheduling/entity/instance_pool.py deleted file mode 100644 index 6689f06..0000000 --- a/AutoDL/autodl_scheduling/entity/instance_pool.py +++ /dev/null @@ -1,146 +0,0 @@ -import copy -import os -import random -import time -from concurrent.futures.thread import ThreadPoolExecutor -from typing import List - -import loguru - -from AutoDL.autodl_scheduling.util.audodl_sdk import instance_operate, get_autodl_machines, payg - -RETRY_LIMIT = 20 # 创建实例重试次数 - - -class Instance: - def __init__(self, uuid:str, active:bool=False, last_active_time:float=-1., domain:str=""): - self.uuid:str=uuid - self.active:bool=active - self.last_active_time:float=last_active_time - self.domain:str=domain - - def __str__(self): - return "uuid:%s active:%s last_active_time:%s domain:%s" % (self.uuid, self.active, self.last_active_time, self.domain) - - -class InstancePool: - def __init__(self, min_instance=0, max_instance=100, scaledown_window=120, buffer_instance=0, timeout=1200): - self.min_instance = min_instance - self.max_instance = max_instance - self.scaledown_window = scaledown_window - self.buffer_instance = buffer_instance - self.timeout = timeout - self.instances:List[Instance] = [] - self.executor = ThreadPoolExecutor(max_workers=os.cpu_count()*2) - self.threads = [] - self.intro_threads = [] - - def scale_instance(self, target_instance, disable_shrink=True): - if target_instance + self.buffer_instance < self.min_instance: - return self._scale(self.min_instance) - if target_instance + self.buffer_instance > self.max_instance: - return self._scale(self.max_instance) - if target_instance + self.buffer_instance == len(self.instances): - return True - return self._scale(target_instance + self.buffer_instance, disable_shrink) - - def remove_instance(self, instance:Instance): - if instance_operate(instance.uuid, "power_off"): - if instance_operate(instance.uuid, "release"): - for i in self.instances: - if i.uuid == instance.uuid: - self.instances.remove(i) - else: - loguru.logger.error("Instance {} failed to release".format(instance.uuid)) - else: - loguru.logger.error("Instance {} failed to power off".format(instance.uuid)) - - def _add_instance(self): - lim = RETRY_LIMIT - while lim > 0: - machines = get_autodl_machines() - if len(machines) > 0: - m = random.choice(machines) - result = payg(m["region_name"], m["machine_id"]) - if result: - self.instances.append( - Instance(uuid=result[0], active=False, last_active_time=time.time(), domain="https://"+result[1])) - break - else: - time.sleep(1) - lim -= 1 - if lim <= 0: - loguru.logger.error("Fail to Scale[Add] Instance") - - def introspection(self): - # 停止超时实例(运行超时和无任务超时) - timeout = self.timeout//6 - while len(self.intro_threads) > 0 and timeout > 0: - time.sleep(1) - timeout -= 1 - if timeout == 0: - loguru.logger.error("Fail to Introspect Instances: Timeout") - return - try: - before=len(self.instances) - instance_copy = copy.deepcopy(self.instances) - flag = [] - for instance in instance_copy: - if instance.active: - if (time.time() - instance.last_active_time) > self.timeout: - flag.append(instance.uuid) - self.intro_threads.append(self.executor.submit(self.remove_instance, instance=instance)) - else: - if (time.time() - instance.last_active_time) > self.scaledown_window: - flag.append(instance.uuid) - self.intro_threads.append(self.executor.submit(self.remove_instance, instance=instance)) - while len(self.intro_threads) > 0: - for t in self.intro_threads: - t.result(timeout=self.timeout//2) - self.intro_threads.remove(t) - after = len(self.instances) - for instance in self.instances: - if instance.uuid in flag: - raise Exception("Instance[%s] Remove Failed" % instance.uuid) - if len(flag) > 0: - loguru.logger.info("Instance Num Before Introspecting %d After Introspecting %d" % (before, after)) - except Exception as e: - loguru.logger.error("Fail to Introspect Instances: %s" % e) - - def _scale(self, target_instance:int, disable_shrink=True): - try: - self.introspection() - # 调整实例数量 - instance_copy = copy.deepcopy(self.instances) - dest = target_instance - len(instance_copy) - if (disable_shrink and dest < 0) or dest == 0: - return True - loguru.logger.info("Instance Num Before Scaling %d ; Target %d" % (len(self.instances), target_instance)) - if dest < 0: - dest = abs(dest) - for instance in instance_copy: - if not instance.active and dest > 0: - self.threads.append(self.executor.submit(self.remove_instance, instance=instance)) - dest -= 1 - elif dest > 0: - for i in range(dest): - self.threads.append(self.executor.submit(self._add_instance)) - while len(self.threads) > 0: - for t in self.threads: - t.result(timeout=self.timeout//2) - self.threads.remove(t) - loguru.logger.info("Instance Num After Scaling %d ; Target %d" % (len(self.instances), target_instance)) - if len(self.instances) == target_instance: - return True - else: - return False - except Exception as e: - loguru.logger.error("Fail to Scale: %s" % e) - return False - - -if __name__ == "__main__": - ip = InstancePool() - print(ip.scale_instance(5)) - time.sleep(5) - print(ip.scale_instance(0)) \ No newline at end of file diff --git a/AutoDL/autodl_scheduling/entity/result_map.py b/AutoDL/autodl_scheduling/entity/result_map.py deleted file mode 100644 index 3aafec1..0000000 --- a/AutoDL/autodl_scheduling/entity/result_map.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Union - - -class ResultMap: - def __init__(self, max_size:int=1000): - self.map = dict() - self.max_size = max_size - - def get(self, uid:str) -> Union[dict,None]: - if uid in self.map: - if self.map[uid]: - r = self.map[uid] - # self.remove(uid) - return r - else: - return None - else: - raise KeyError(uid) - - def set(self, uid:str, result:dict=None) -> None: - if uid in self.map: - raise RuntimeError(f"Key {uid} already exists") - else: - if len(self.map) + 1 > self.max_size: - raise RuntimeError(f"ResultMap exceeds max size {self.max_size}") - self.map[uid] = result - - def update(self, uid:str, result:dict) -> None: - if uid in self.map: - self.map[uid] = result - else: - raise KeyError(uid) - - def remove(self, uid:str) -> None: - if uid in self.map: - del self.map[uid] \ No newline at end of file diff --git a/AutoDL/autodl_scheduling/entity/running_pool.py b/AutoDL/autodl_scheduling/entity/running_pool.py deleted file mode 100644 index c87df8c..0000000 --- a/AutoDL/autodl_scheduling/entity/running_pool.py +++ /dev/null @@ -1,82 +0,0 @@ -import time -import traceback -from typing import Union - -import loguru -import requests -from fastapi import UploadFile -SERVER_TIME_OUT = 60 * 35 - -class RunningPool: - def __init__(self): - self.tasks = {} - self._headers = { - "Authorization": "Bearer 7468B79CA2CF53436C06B29A6F16451C" - } - - def run(self, instance_id, uid, base_url:str, video_file:Union[UploadFile,str], audio_file:Union[UploadFile,str]) -> Union[bool, None]: - try: - code = self._req(base_url, video_file, audio_file) - if not code: - raise Exception("[Code] is None") - except Exception as e: - loguru.logger.error("%s Task Submit Failed: %s" % (uid, e)) - return None - self.tasks[uid] = {"instance_id": instance_id, "base_url":base_url, "code": code, "status": False, "start_time": time.time()} - return True - - def result(self, uid:str) -> Union[dict, None]: - if uid in self.tasks: - try: - resp = requests.get(self.tasks[uid]["base_url"] + "/" + self.tasks[uid]["code"], headers=self._headers, timeout=10) - if resp.status_code != 200: - raise Exception("%s Get Result Failed: %s" % (uid, resp.status_code)) - else: - if resp.json()["status"] == "waiting": - if self.tasks[uid]["start_time"] + SERVER_TIME_OUT + 10 < time.time(): - self.tasks[uid]["status"] = True - return {"status": "fail", "msg": "Instance Timeout", "instance_id": self.tasks[uid]["instance_id"]} - return None - else: - self.tasks[uid]["status"] = True - return {"status": resp.json()['status'], "msg": resp.json()['msg'], - "instance_id": self.tasks[uid]["instance_id"]} - except Exception as e: - loguru.logger.error("%s Get Result Failed: %s" % (uid, e)) - return None - else: - raise KeyError(uid) - - def get_running_size(self): - return len([i for i in self.tasks.values() if not i["status"]]) - - def _req(self, base_url, video_file, audio_file): - data = { - "video_file": video_file, - "audio_file": audio_file - } - resp = requests.post(base_url, data, headers=self._headers, allow_redirects=True, stream=True, timeout=60) - loguru.logger.info("Submit Response: %s" % resp.text) - if resp.status_code == 200: - if resp.json()["status"] == "success": - code = resp.json()["code"] - return code - else: - return None - else: - return None - -if __name__ == "__main__": - rp = RunningPool() - id = rp.run("https://u336391-b31a-07132bc4.westx.seetacloud.com:8443", - "https://sucai-1324682537.cos.ap-shanghai.myqcloud.com/tiktok/video/1111.mp4", - "https://sucai-1324682537.cos.ap-shanghai.myqcloud.com/tiktok/video/XBR037ruAZsA.mp3") - while True: - r = rp.result(id) - if r: - loguru.logger.success(r) - break - else: - loguru.logger.info("waiting...") - time.sleep(5) - diff --git a/AutoDL/autodl_scheduling/entity/waiting_queue.py b/AutoDL/autodl_scheduling/entity/waiting_queue.py deleted file mode 100644 index 3809afa..0000000 --- a/AutoDL/autodl_scheduling/entity/waiting_queue.py +++ /dev/null @@ -1,21 +0,0 @@ -from queue import Queue - - -class WaitingQueue: - def __init__(self): - self.queue = Queue(maxsize=500) - - def enqueue(self,uid, video_path,audio_path): - data = { - "uid": uid, - "video_path": video_path, - "audio_path": audio_path - } - self.queue.put(data,timeout=10) - - def dequeue(self): - data = self.queue.get(timeout=10) - return data["uid"], data["video_path"], data["audio_path"] - - def get_size(self): - return self.queue.qsize() diff --git a/AutoDL/autodl_scheduling/server.py b/AutoDL/autodl_scheduling/server.py deleted file mode 100644 index efcd698..0000000 --- a/AutoDL/autodl_scheduling/server.py +++ /dev/null @@ -1,125 +0,0 @@ -import time -import traceback -import uuid -from concurrent.futures.thread import ThreadPoolExecutor - -import loguru -import uvicorn -from fastapi import FastAPI, Depends, HTTPException -from fastapi.routing import APIRoute -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from starlette import status - -from AutoDL.autodl_scheduling.entity.instance_pool import InstancePool, Instance -from AutoDL.autodl_scheduling.entity.result_map import ResultMap -from AutoDL.autodl_scheduling.entity.running_pool import RunningPool -from AutoDL.autodl_scheduling.entity.waiting_queue import WaitingQueue - - -class Server: - def __init__(self): - self.app = FastAPI() - self.waiting_queue = WaitingQueue() - self.running_pool = RunningPool() - #账号限制max_instance不能超过30 - self.instance_pool = InstancePool(max_instance=2) - self.result_map = ResultMap() - self.executor = ThreadPoolExecutor(max_workers=2) - self.worker_1 = self.executor.submit(self.scaling_worker) - self.worker_2 = self.executor.submit(self.introspect_instance) - self.app.routes.append(APIRoute("/", endpoint=self.submit, methods=['POST'])) - self.app.routes.append(APIRoute("/{uid}", endpoint=self.get_result, methods=['GET'])) - self.app.routes.append(APIRoute("/", endpoint=self.get_all_result, methods=['GET'])) - - # async def submit(self, video_url, audio_url, token: HTTPAuthorizationCredentials = Depends(HTTPBearer())): - async def submit(self, video_url, audio_url): - # if token.credentials != "7468B79CA2CF53436C06B29A6F16451C": - # raise HTTPException( - # status_code=status.HTTP_401_UNAUTHORIZED, - # detail="Incorrect bearer token", - # headers={"WWW-Authenticate": "Bearer"}, - # ) - uid = str(uuid.uuid4()) - try: - self.waiting_queue.enqueue(uid, video_url, audio_url) - return {"uid": uid} - except: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - - async def get_result(self, uid): - try: - return self.result_map.get(uid) - except: - l = list(self.waiting_queue.queue.queue) - for i in l: - if i["uid"] == uid: - return {"status": "queuing", "msg":""} - return {"status": "not found", "msg":""} - - async def get_all_result(self): - try: - return self.result_map.map - except: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) - - def introspect_instance(self): - loguru.logger.info("Introspecting worker started || Scaledown Window: %ds" % self.instance_pool.scaledown_window) - while True: - try: - self.instance_pool.introspection() - time.sleep(1) - except: - traceback.print_exc() - - def scaling_worker(self): - loguru.logger.info("Scaling worker started") - while True: - try: - # 提交任务 - self.instance_pool.scale_instance(self.waiting_queue.get_size()+self.running_pool.get_running_size(), disable_shrink=True) - for instance in self.instance_pool.instances: - if not instance.active and self.waiting_queue.get_size() > 0: - # 从等待队列取出任务 - uid, video_path, audio_path = self.waiting_queue.dequeue() - loguru.logger.info("Task[%s] Submitting to Instance[%s]" % (uid, instance.uuid)) - # 提交任务到运行池 - if self.running_pool.run(instance.uuid, uid, instance.domain, video_path, audio_path): - # 更新实例池状态 - instance.active = True - instance.last_active_time = time.time() - loguru.logger.info("Task[%s] Submitted" % uid) - self.result_map.set(uid, {"status": "running", "msg": "Task Submitted, Waiting for result"}) - else: - loguru.logger.error("Submit Task[%s] Failed" % uid) - self.result_map.set(uid, {"status":"fail", "msg":"Submit Task Failed"}) - - #查询结果放到结果缓存里 - for uid, task in list(self.running_pool.tasks.items()): - result = self.running_pool.result(uid) - # 任务运行完成或失败 - if result: - self.result_map.update(uid, result) - self.running_pool.tasks.pop(uid) - if result["status"] == "fail": - #删除失败任务的实例 - self.instance_pool.remove_instance(Instance(uuid=task["instance_id"])) - loguru.logger.info("Instance[%s] Removed Due to Task[%s] Failure" % (task["instance_id"], uid)) - else: - #更新成功任务实例的状态 - for instance in self.instance_pool.instances: - if instance.uuid == task["instance_id"]: - loguru.logger.info("Instance[%s] Task[%s] Finished" % (instance.uuid, uid)) - instance.active = False - instance.last_active_time = time.time() - time.sleep(0.5) - except: - traceback.print_exc() - - -if __name__=="__main__": - server = Server() - uvicorn.run(server.app, host="127.0.0.1", port=8888) - - - - diff --git a/AutoDL/autodl_scheduling/util/audodl_sdk.py b/AutoDL/autodl_scheduling/util/audodl_sdk.py deleted file mode 100644 index 6be7855..0000000 --- a/AutoDL/autodl_scheduling/util/audodl_sdk.py +++ /dev/null @@ -1,305 +0,0 @@ -import time -from typing import Literal, Union, Any - -import logger -import loguru -import paramiko -import requests - -token = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjMzNjM5MSwidXVpZCI6ImU2MDU0ZGI4LTlhN2UtNDllNC1hNDQ1LWI4N2M1NGViMjU4ZCIsImlzX2FkbWluIjpmYWxzZSwiYmFja3N0YWdlX3JvbGUiOiIiLCJpc19zdXBlcl9hZG1pbiI6ZmFsc2UsInN1Yl9uYW1lIjoid3F5QGI4N2M1NGViMjU4ZCIsInRlbmFudCI6IiIsInVwayI6IiJ9.4MV4P1feiUmrrzFbtTQpNQjvYyezPdaLxRJ79y0VyRAxR0aS5NQJGJPxa-6wuqsgzY-E1rvf5S8FCY92ZnViFQ" -req_instance_page_size = 1500 -chk_instance_page_size = 1500 -instances = {} -LIM = 200 # 等待状态时间LIM*5s -START_LIM = 20 #Heygem脚本启动等待超时时间-10 -DEBUG = False -gpu_list = ["V100-SXM2-32GB", - "vGPU-32GB", - "RTX 4090", - "RTX 4090D", - "RTX 3090", - "RTX 3080", - "RTX 3080x2", - "RTX 3080 Ti", - "RTX 3060", - "RTX A4000", - "RTX 2080 Ti", - "RTX 2080 Ti x2", - "GTX 1080 Ti"] -area_list = ["nm-B1", - "nm-B2", - "west-B", - "west-C", - "west-X", - "bj-B1", - "beijing-A", - "beijing-B", - "beijing-D", - "beijing-E"] - -def ssh_try(host,port,pwd,uid): - # 建立连接 - trans = paramiko.Transport((host, int(port))) - trans.connect(username="root", password=pwd) - - # 将sshclient的对象的transport指定为以上的trans - ssh = paramiko.SSHClient() - ssh._transport = trans - - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command("bash -ic \"source /root/.bashrc && sleep 10 && ps -ef|grep heygem|grep -v grep && lsof -i:6006|wc -l\"", get_pty=True) - out = ssh_stdout.read().decode() - start_lim = START_LIM - if len(out.split("\n")[-2]) > 2: - trans.close() - raise RuntimeError(out.split("\n")[-2]) - loguru.logger.info(f"[{uid}]Waiting for HeyGem server ready...") - while int(out.split("\n")[-2]) <= 1 and start_lim > 0: - ssh_stdin, ssh_stdout, ssh_stderr = ssh.exec_command("bash -ic \"sleep 2 && lsof -i:6006|wc -l\"") - out = ssh_stdout.read().decode() - start_lim -= 1 - if start_lim <= 0: - loguru.logger.error("HeyGem Server Start Timeout, Please check!") - else: - loguru.logger.success("HeyGem Server Start Success") - # 关闭连接 - trans.close() - -def get_autodl_machines() -> Union[list, None]: - machines = list() - headers = { - "Authorization": token, - "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", - "Host": "www.autodl.com" - } - index = 1 - payload = { - "charge_type":"payg", - "region_sign":"", - "gpu_type_name": gpu_list, - "machine_tag_name":[], - "gpu_idle_num":1, - "mount_net_disk":False, - "instance_disk_size_order":"", - "date_range":"", - "date_from":"", - "date_to":"", - "page_index":index, - "page_size":req_instance_page_size, - "pay_price_order":"", - "gpu_idle_type":"", - "default_order":False, - "region_sign_list": area_list, - "cpu_arch":["x86"], - "chip_corp":["nvidia"], - "machine_id":"" - } - if DEBUG: - loguru.logger.info("Req Machine index {}".format(index)) - rsp = requests.post("https://www.autodl.com/api/v1/sub_user/user/machine/list", json=payload, headers=headers) - - if rsp.status_code == 200: - machine_list = rsp.json() - if DEBUG: - loguru.logger.info("Machine Result Total {}".format(machine_list["data"]["result_total"])) - while index < machine_list["data"]["max_page"]: - index += 1 - if DEBUG: - loguru.logger.info("Req Machine index {}/{}".format(index, machine_list["data"]["max_page"])) - payload["page_index"] = index - rsp = requests.post("https://www.autodl.com/api/v1/sub_user/user/machine/list", json=payload, headers=headers) - if rsp.status_code == 200: - machine_list["data"]["list"].extend(rsp.json()["data"]["list"]) - else: - loguru.logger.error("Get Machines Req Error") - return None - else: - loguru.logger.error("Get Machines Req Error") - return None - i = 0 - for machine in machine_list["data"]["list"]: - if machine["health_status"] == 0 \ - and machine["gpu_order_num"] > 0 \ - and float(machine["highest_cuda_version"])>12. \ - and machine["payg"] == True \ - and machine["rent_mode"] == "" \ - and not machine["user_visible_limit"]: - i += 1 - machines.append({ - "machine_id": machine["machine_id"], - "region_name": machine["region_name"], - "machine_alias": machine["machine_alias"], - "gpu_name": machine["gpu_name"], - "gpu_order_num": machine["gpu_order_num"], - "gpu_number": machine["gpu_number"], - "region_sign": machine["region_sign"], - }) - return sorted(machines, key=lambda machine: machine["gpu_order_num"], reverse=True) - -def payg(region_name:str, machine_id:str) -> tuple[Any, Any] | None: - region_image = { - "西北": ["hub.kce.ksyun.com/autodl-image/miniconda:cuda11.8-cudnn8-devel-ubuntu20.04-py38","image-0d3ab01d64"], - "内蒙": ["hub.kce.ksyun.com/autodl-image/miniconda:cuda11.8-cudnn8-devel-ubuntu20.04-py38","image-bfff5e82ae"], - "北京": ["hub.kce.ksyun.com/autodl-image/miniconda:cuda11.8-cudnn8-devel-ubuntu20.04-py38","image-537b5da0c5"] - } - headers = { - "Authorization": token, - "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", - "Host": "www.autodl.com" - } - payload = { - "instance_info":{ - "machine_id":machine_id, - "charge_type":"payg", - "req_gpu_amount":1, - "image":region_image[region_name[:2]][0], - "private_image_uuid":region_image[region_name[:2]][1], - "reproduction_uuid":"", - "instance_name":"", - "expand_data_disk":0, - "reproduction_id":0 - }, - "price_info":{ - "coupon_id_list":[], - "machine_id":machine_id, - "charge_type":"payg", - "duration":1, - "num":1, - "expand_data_disk":0 - } - } - loguru.logger.info("Try Create Payg Container on Machine {}/{}".format(region_name, machine_id)) - rsp = requests.post("https://www.autodl.com/api/v1/sub_user/order/instance/create/payg", json=payload, headers=headers) - - if rsp.status_code == 200: - j = rsp.json() - if j["code"] == "Success": - lim = LIM - while lim>0: - time.sleep(3) - status, host, port, pwd, domain = check_status(j['data']) - if status == "running": - ssh_try(host, port, pwd, j['data']) - break - elif status == "shutdown_by_starting_error": - loguru.logger.error("Create Payg Instance Error: Start Error, Try Remove Instance[%s]" % j['data']) - instance_operate(j['data'], "release") - return None - else: - lim = lim-1 - if lim > 0: - loguru.logger.success("Create Payg Instance Success: %s" % j['data']) - return j['data'],domain - else: - logger.logger.error("Create Payg Instance Error: Wait for Created Timeout, Please Check!!! instance_uuid(%s)" % j['data']) - return None - else: - loguru.logger.error("Create Payg Instance Error: %s" % j['msg']) - return None - else: - loguru.logger.error("Create Payg Instance Error: Status Code[%s]" % rsp.status_code) - return None - -def instance_operate(instance_uuid:str, operation: Literal["power_off","power_on","release"]) -> bool: - dest_dict={ - "power_off":"shutdown", - "power_on":"running", - } - headers = { - "Authorization": token, - "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", - "Host": "www.autodl.com" - } - payload = {"instance_uuid":instance_uuid} - rsp = requests.post("https://www.autodl.com/api/v1/sub_user/instance/%s" % operation, json=payload, headers=headers) - if rsp.status_code == 200: - j = rsp.json() - if j["code"] == "Success": - lim = LIM - if operation in dest_dict.keys(): - while lim>0: - time.sleep(5) - status = check_status(instance_uuid) - if status and status[0] == dest_dict[operation]: - break - else: - lim = lim-1 - if lim > 0: - loguru.logger.success("Operate[%s] Instance[%s] Success" % (operation, instance_uuid)) - return True - else: - loguru.logger.error("Operate[%s] Instance[%s] Error: Timeout, Please Check!!!" % (operation, instance_uuid)) - return False - else: - loguru.logger.error("Operate[%s] Instance[%s] Error: %s" % (operation, instance_uuid, j['msg'])) - return False - else: - loguru.logger.error("Operate[%s] Instance[%s] Error: Status Code[%s]" % (operation, instance_uuid, rsp.status_code)) - return False - -def check_status(instance_uuid:str) -> tuple[Any, Any, Any, Any, Any] | None: - headers = { - "Authorization": token, - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36", - "Host": "www.autodl.com" - } - index = 1 - payload = { - "date_from":"", - "date_to":"", - "page_index": index, - "page_size":chk_instance_page_size, - "status":[], - "charge_type":[] - } - if DEBUG: - loguru.logger.info("Req Instance index {}".format(index)) - lim = 3 - while lim > 0: - rsp = None - try: - rsp = requests.post("https://www.autodl.com/api/v1/sub_user/instance", json=payload, headers=headers) - except: - lim -= 1 - if rsp: - break - if lim <= 0: - loguru.logger.error("Get Instance Req Error") - return None - if rsp.status_code == 200: - instance_list = rsp.json() - if DEBUG: - loguru.logger.info("Instance Result Total {}".format(instance_list["data"]["result_total"])) - while index < instance_list["data"]["max_page"]: - if DEBUG: - loguru.logger.info("Req Instance index {}/{}".format(index, instance_list["data"]["max_page"])) - payload["page_index"] = index - rsp = requests.post("https://www.autodl.com/api/v1/sub_user/instance", json=payload, headers=headers) - if rsp.status_code == 200: - instance_list["data"]["list"].extend(rsp.json()["data"]["list"]) - else: - loguru.logger.error("Get Instance Req Error") - return None - for l in instance_list["data"]["list"]: - if l["uuid"] == instance_uuid: - if DEBUG: - loguru.logger.info("Instance {} Status {}".format(instance_uuid, l["status"])) - return l["status"], l["proxy_host"], l["ssh_port"], l["root_password"], l["tensorboard_domain"] - loguru.logger.warning("Instance {} Not Found".format(instance_uuid)) - return None - else: - loguru.logger.error("Get Instance Req Error") - return None - - - - -if __name__=="__main__": - machines = get_autodl_machines() - for m in machines: - instance_uuid, domain = payg(m["region_name"], m["machine_id"]) - loguru.logger.success("instance_id:%s server_domain: https://%s" % (instance_uuid, domain)) - # if instance_uuid: - # instance_operate(instance_uuid, "power_off") - # instance_operate(instance_uuid, "release") - break \ No newline at end of file diff --git a/AutoDL/heygem.py b/AutoDL/heygem.py deleted file mode 100644 index a4a850b..0000000 --- a/AutoDL/heygem.py +++ /dev/null @@ -1,12 +0,0 @@ -import site -import subprocess -import sys -pkg_path = site.getsitepackages()[0] -sys.path.append(f"{pkg_path}/heygem/code") - -p = subprocess.Popen(f"cd {pkg_path}/heygem/code && " - "python3.8 app_local.py", shell=True) -p.communicate() - - - diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7fa7fa --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +```bash +modal deploy -e dev ./src/cluster/app.py +``` \ No newline at end of file diff --git a/deploy.py b/deploy.py deleted file mode 100644 index d080353..0000000 --- a/deploy.py +++ /dev/null @@ -1,80 +0,0 @@ -import json -import subprocess -import uuid -from pathlib import Path -from typing import Dict - -import modal - -image = ( # build up a Modal Image to run ComfyUI, step by step - modal.Image.debian_slim( # start from basic Linux with Python - python_version="3.10" - ) - .apt_install("git") # install git to clone ComfyUI - .pip_install("fastapi[standard]==0.115.4") # install web dependencies - .pip_install("comfy-cli==1.3.5") # install comfy-cli - .pip_install("cos-python-sdk-v5") - .pip_install("sqlalchemy") - .pip_install("ultralytics") - .pip_install("tencentcloud-sdk-python") - .pip_install("pymysql") - .pip_install("Pillow") - .pip_install("ffmpy") - .pip_install("opencv-python") - .run_commands( # use comfy-cli to install ComfyUI and its dependencies - "comfy --skip-prompt install --nvidia --version 0.3.10" - ).add_local_file("snapshot.json","/root/snapshot.json", copy=True) -) - -image = ( - image.run_commands("comfy node install https://github.com/M1kep/ComfyLiterals") - .run_commands("comfy node install https://github.com/evanspearman/ComfyMath") - .run_commands("comfy node install https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-Bowong.git") - .run_commands("comfy node install https://github.com/crystian/ComfyUI-Crystools") - .run_commands("comfy node install https://github.com/pythongosssss/ComfyUI-Custom-Scripts") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-CustomNode.git") - .run_commands("comfy node install https://github.com/BennyKok/comfyui-deploy") - .run_commands("comfy node install https://github.com/yolain/ComfyUI-Easy-Use") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-LatentSync-Node.git") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-VideoHelperSuite.git") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/CosyVoice-ComfyUI.git") - .run_commands("comfy node install https://github.com/jags111/efficiency-nodes-comfyui") - .run_commands("comfy node install https://github.com/WASasquatch/was-node-suite-comfyui") - .run_commands("comfy node install https://github.com/rgthree/rgthree-comfy") - .run_commands("comfy node install https://github.com/cubiq/ComfyUI_essentials") - .run_commands("comfy node install https://github.com/melMass/comfy_mtb") - .run_commands( - "mkdir -p /root/comfy/ComfyUI/models/ComfyUI-CustomNode/model && rm -rf /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/model && ln -s /root/comfy/ComfyUI/models/ComfyUI-CustomNode/model /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/model" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/ComfyUI-LatentSync-Node/checkpoints && rm -rf /root/comfy/ComfyUI/custom_nodes/ComfyUI-LatentSync-Node/checkpoints && ln -s /root/comfy/ComfyUI/models/ComfyUI-LatentSync-Node/checkpoints /root/comfy/ComfyUI/custom_nodes/ComfyUI-LatentSync-Node/checkpoints" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M-SFT && mkdir -p /root/comfy/ComfyUI/custom_nodes/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M-SFT && rm -rf /root/comfy/ComfyUI/custom_nodes/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M-SFT && ln -s /root/comfy/ComfyUI/models/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M-SFT /root/comfy/ComfyUI/custom_nodes/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M-SFT" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/.cache/torch/hub/checkpoints && mkdir -p /root/.cache/torch/hub/checkpoints && rm -rf /root/.cache/torch/hub/checkpoints && ln -s /root/comfy/ComfyUI/models/.cache/torch/hub/checkpoints /root/.cache/torch/hub/checkpoints" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/stabilityai && ln -s /root/comfy/ComfyUI/models/stabilityai /root/comfy/ComfyUI/stabilityai" - ).run_commands( - "rm -rf /root/comfy/ComfyUI/models" - ).run_commands( - "apt update && apt install -y ffmpeg && ffmpeg -version" - ).pip_install("av") - # Add .run_commands(...) calls for any other custom nodes you want to download -) - -image = image.add_local_file( - "highlight_v0.113_api.json", "/root/workflow_api.json" -) - -app = modal.App(name="highlight-comfyui", image=image) -vol = modal.Volume.from_name("comfyui-model", create_if_missing=True) - -@app.function( - allow_concurrent_inputs=10, # required for UI startup process which runs several API calls concurrently - concurrency_limit=1, # limit interactive session to 1 container - gpu="T4", # good starter GPU for inference - volumes={"/root/comfy/ComfyUI/models": vol}, # mounts our cached models -) -@modal.web_server(8000, startup_timeout=60) -def ui(): - subprocess.Popen("comfy launch -- --listen 0.0.0.0 --port 8000", shell=True) diff --git a/heygem.py b/heygem.py deleted file mode 100644 index a4a850b..0000000 --- a/heygem.py +++ /dev/null @@ -1,12 +0,0 @@ -import site -import subprocess -import sys -pkg_path = site.getsitepackages()[0] -sys.path.append(f"{pkg_path}/heygem/code") - -p = subprocess.Popen(f"cd {pkg_path}/heygem/code && " - "python3.8 app_local.py", shell=True) -p.communicate() - - - diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0f96c7f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "modaldeploy" +version = "0.2.0" +description = "管理Modal worker的工程" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "boto3>=1.37.37", + "cos-python-sdk-v5>=1.9.36", + "crcmod>=1.7", + "httpx>=0.28.1", + "loguru>=0.7.3", + "pydantic>=2.11.3", + "pydantic-settings>=2.9.1", + "python-ffmpeg>=2.0.12", + "requests>=2.32.3", + "scalar-fastapi>=1.0.3", + "sentry-sdk[loguru]>=2.26.1", + "tencentcloud-sdk-python-common>=3.0.1363", + "tencentcloud-sdk-python-vod>=3.0.1363", + "tqdm>=4.67.1", + "webvtt-py>=0.5.1", +] diff --git a/server_pure_heygem.py b/server_pure_heygem.py deleted file mode 100644 index 8f3ab4c..0000000 --- a/server_pure_heygem.py +++ /dev/null @@ -1,298 +0,0 @@ -# HeyGem模板 -import json -import os -import shutil -import socket -import subprocess -import time -import traceback -import uuid -from typing import Any, Optional, Union - -import httpx -import loguru -import modal -import requests -from fastapi import Depends, HTTPException, status, UploadFile -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -image = ( - modal.Image.debian_slim( # start from basic Linux with Python - python_version="3.10" - ).apt_install("git") - .apt_install("gcc") - .pip_install("loguru") - .pip_install("fastapi[standard]") - .run_commands( - "apt update && apt install -y ffmpeg && ffmpeg -version" - ) - # 添加Python3.8 HeyGem - .run_commands("apt update && apt install -y curl build-essential libssl-dev zlib1g-dev libncurses5-dev libncursesw5-dev libreadline-dev libsqlite3-dev libgdbm-dev libdb5.3-dev libbz2-dev libexpat1-dev lzma liblzma-dev tk-dev libffi-dev") - .run_commands("curl -O https://www.python.org/ftp/python/3.8.12/Python-3.8.12.tar.xz&&tar -xf Python-3.8.12.tar.xz") - .run_commands("cd Python-3.8.12 && ./configure --enable-optimizations && make -j 10 && make altinstall") - .add_local_file("heygem-1.0-py3-none-any.whl","/root/heygem-1.0-py3-none-any.whl", copy=True) - .shell(["/bin/bash", "-c"]) - .run_commands("python3.8 -m pip install /root/heygem-1.0-py3-none-any.whl") - .env({"LD_LIBRARY_PATH":"/usr/local/lib/python3.8/site-packages/nvidia/cuda_nvrtc/lib"}) - .run_commands("ln -s /usr/local/lib/python3.8/site-packages/nvidia/cuda_nvrtc/lib/libnvrtc.so.11.2 /usr/local/lib/python3.8/site-packages/nvidia/cuda_nvrtc/lib/libnvrtc.so") - .run_commands("python3.8 -m pip install https://github.com/pydata/numexpr/releases/download/v2.8.6/numexpr-2.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl") - .add_local_file("heygem.py", "/root/heygem.py", copy=True) - .add_local_file("config.yaml", "/root/config.yaml", copy=True) - .workdir("/root").pip_install("boto3").pip_install("PyYAML").pip_install("requests") -) - -app = modal.App(name="heygem", image=image) -secret = modal.Secret.from_name("aws-s3-secret") -auth_scheme = HTTPBearer() -bucket_output = "bw-heygem-output" - -@app.cls( - allow_concurrent_inputs=1, # required for UI startup process which runs several API calls concurrently - max_containers=25, # limit interactive session to 1 container - gpu="L40S", # good starter GPU for inference - cpu=(2,32), - memory=(6144, 32768), - timeout=2160, - scaledown_window=240, - secrets=[secret, modal.Secret.from_name("web_auth_token")], - volumes={ - "/code/data/final": modal.CloudBucketMount( - bucket_name=bucket_output, - # bucket_endpoint_url="https://s3.%s.amazonaws.com" % os.environ["AWS_REGION"], - secret=secret, - key_prefix="/" - ), - "/root/logs": modal.CloudBucketMount( - bucket_name=bucket_output, - # bucket_endpoint_url="https://s3.%s.amazonaws.com" % os.environ["AWS_REGION"], - secret=secret, - key_prefix="/logs/" - ) - }, # mounts our cached models -) -class HeyGem: - @modal.enter() - def start(self): - def check_port_in_use(port, host='127.0.0.1'): - s = None - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(1) - s.connect((host, int(port))) - return True - except socket.error: - return False - finally: - if s: - s.close() - uid = str(uuid.uuid4()) - cmd = "echo client_id: %s && nohup python3.8 /root/heygem.py >> /root/logs/%s.log 2>&1 &" % (uid, uid) - subprocess.run(cmd, shell=True, check=True) - timeout = 30 - while timeout > 0: - check = check_port_in_use(8383) - if check: - loguru.logger.success("HeyGem Server started on port 8383") - break - else: - timeout -= 1 - time.sleep(2) - if timeout == 0: - raise TimeoutError("HeyGem Server Start timed out") - - - @modal.method() - def infer(self, code, video_path, audio_path) -> str: - def task_submit(uid, video, audio, heygem_url): - """Submit a task to the API""" - task_submit_api = f'{heygem_url}/easy/submit' - result_json = { - 'status': False, 'data': {}, 'msg': '' - } - try: - data = { - "code": uid, - "video_url": video, - "audio_url": audio, - "chaofen": 1, - "watermark_switch": 0, - "pn": 1 - } - loguru.logger.info(f'data={json.dumps(data, ensure_ascii=False)}') - with httpx.Client() as client: - resp = client.post(task_submit_api, json=data) - resp_dict = resp.json() - loguru.logger.info(f'submit data: {json.dumps(resp_dict, ensure_ascii=False)}') - if resp_dict['code'] != 10000: - result_json['status'] = False - result_json['msg'] = result_json['msg'] - else: - result_json['status'] = True - result_json['data'] = uid - result_json['msg'] = '任务提交成功' - return result_json - except Exception as e: - loguru.logger.info(f'submit task fail case by:{str(e)}') - raise RuntimeError(str(e)) - - def query_task_progress(heygem_url, heygem_temp_path, task_id: str, interval: int = 10, timeout: int = 60 * 35): - """Query task progress and wait for completion""" - result_json = {'status': False, 'data': {}, 'msg': ''} - - def query_result(t_id: str): - tmp_dict = {'status': True, 'data': dict(), 'msg': ''} - try: - query_task_url = f'{heygem_url}/easy/query' - params = { - 'code': t_id - } - with httpx.Client() as client: - resp = client.get(query_task_url, params=params) - resp_dict = resp.json() - loguru.logger.info(f'query task data: {json.dumps(resp_dict, ensure_ascii=False)}') - status_code = resp_dict['code'] - if status_code in (9999, 10002, 10003, 10001): - tmp_dict['status'] = False - tmp_dict['msg'] = resp_dict['msg'] - elif status_code == 10000: - status_code = resp_dict['data'].get('status', 1) - if status_code == 3: - tmp_dict['status'] = False - tmp_dict['msg'] = resp_dict['data']['msg'] - else: - process = resp_dict['data'].get('progress', 20) - if status_code == 2: - process = 100 - else: - process = process - result = resp_dict['data'].get('result', '') - tmp_dict['data'] = {'progress': process, - 'path': result, - } - else: - pass - except Exception as e: - loguru.logger.info(f'query task fail case by:{str(e)}') - raise RuntimeError(str(e)) - return tmp_dict - - end = time.time() + timeout - while time.time() < end: - tmp_result = query_result(task_id) - if not tmp_result['status'] or tmp_result['data'].__eq__({}): - result_json['status'] = False - result_json['msg'] = tmp_result['msg'] - break - else: - process = tmp_result['data']['progress'] - loguru.logger.info(f'query task progress :{process}') - if tmp_result['data']['progress'] < 100: - time.sleep(interval) - loguru.logger.info(f'wait next interval:{interval}') - else: - p = tmp_result['data']['path'] - p = p.replace('/', '').replace('\\', '') - result_json['data'] = "%s/%s" % (heygem_temp_path, p) - result_json['status'] = True - break - return result_json - - try: - heygem_url = "http://127.0.0.1:8383" - heygem_temp_path = "/code/data/temp" - submit_result = task_submit(code, video_path, audio_path, heygem_url) - if not submit_result['status']: - raise Exception(f"Task submission failed: {submit_result['msg']}") - - task_id = submit_result['data'] - loguru.logger.info(f'Submitted task: {task_id}') - - # Query task progress - progress_result = query_task_progress(heygem_url, heygem_temp_path, task_id, interval=5) - - if not progress_result['status']: - raise RuntimeError(f"Task processing failed: {progress_result['msg']}") - - # Return the file for download - file_path = progress_result['data'] - if not os.path.exists(file_path): - raise FileNotFoundError(f"Output file not found at {file_path}") - return file_path - except Exception as e: - loguru.logger.error(f"Error processing request || {str(e)}") - raise Exception(str(e)) - finally: - try: - os.remove(video_path) - os.remove(audio_path) - except: - pass - - def download_file(self, url, path): - with requests.get(url, stream=True) as r: - with open(path, 'wb') as f: - shutil.copyfileobj(r.raw, f) - - @modal.fastapi_endpoint(method="POST") - async def api(self, video_file: Optional[Union[UploadFile, str]], audio_file: Optional[Union[UploadFile, str]], token: HTTPAuthorizationCredentials = Depends(auth_scheme)): - if token.credentials != os.environ["AUTH_TOKEN"]: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect bearer token", - headers={"WWW-Authenticate": "Bearer"}, - ) - code = str(uuid.uuid4()) - try: - if isinstance(video_file, UploadFile): - video_path = "/root/%s" % video_file.filename - with open(video_path, "wb") as f: - data = await video_file.read() - f.write(data) - elif isinstance(video_file, str) and video_file.startswith("http"): - video_path = "/root/%s" % video_file.split("?")[0].split("/")[-1] - self.download_file(video_file, video_path) - else: - return {"status": "fail", "msg": "video file save failed: Not a valid input"} - - if isinstance(audio_file, UploadFile): - audio_path = "/root/%s" % audio_file.filename - with open(audio_path, "wb") as f: - data = await audio_file.read() - f.write(data) - elif isinstance(audio_file, str) and audio_file.startswith("http"): - audio_path = "/root/%s" % audio_file.split("?")[0].split("/")[-1] - self.download_file(audio_file, audio_path) - else: - return {"status": "fail", "msg": "audio file save failed: Not a valid input"} - except Exception as e: - return {"status": "fail", "msg": f"video or audio file save failed: {str(e)}"} - - loguru.logger.info(f"Enter Inference Module") - try: - file_path = self.infer.local(code, video_path, audio_path) - try: - shutil.copy(file_path, os.path.join("/code/data/final",file_path.split(os.path.sep)[-1])) - except Exception as e: - loguru.logger.info(f"Moving {file_path} to S3 Failed: {str(e)}") - try: - print("Try move file to S3 manually") - # S3 Fallback - import boto3 - import yaml - with open("/root/config.yaml", encoding="utf-8", - mode="r+") as config: - yaml_config = yaml.load(config, Loader=yaml.FullLoader) - awss3 = boto3.resource('s3', aws_access_key_id=yaml_config["aws_key_id"], - aws_secret_access_key=yaml_config["aws_access_key"]) - awss3.meta.client.upload_file(file_path, bucket_output, file_path.split(os.path.sep)[-1]) - except: - return {"status": "fail", "msg": "Failed to move file to S3 manually: " + str(e)} - # resp = FileResponse(path=file_path, filename=Path(file_path).name, status_code=200, headers={ - # "Content-Type": "application/octet-stream", - # "Content-Disposition": f"attachment; filename={Path(file_path).name}", - # }) - return {"status": "success", "msg":f"{file_path.split(os.path.sep)[-1]}"} - except Exception as e: - traceback.print_exc() - return {"status": "fail", "msg": "Inference module failed: "+str(e)} - - diff --git a/server_with_s3.py b/server_with_s3.py deleted file mode 100644 index 17a3eff..0000000 --- a/server_with_s3.py +++ /dev/null @@ -1,258 +0,0 @@ -# ComfyUI模板--Base 停止维护!! -import json -import os -import shutil -import subprocess -import uuid -from typing import Dict - -import loguru -import modal - -image = ( # build up a Modal Image to run ComfyUI, step by step - modal.Image.debian_slim( # start from basic Linux with Python - python_version="3.10" - ) - .apt_install("git") # install git to clone ComfyUI - .apt_install("gcc") - .apt_install("libportaudio2") - .pip_install("fastapi[standard]==0.115.4") # install web dependencies - .pip_install("comfy-cli==1.3.5") # install comfy-cli - .pip_install("cos-python-sdk-v5") - .pip_install("sqlalchemy") - .pip_install("ultralytics") - .pip_install("tencentcloud-sdk-python") - .pip_install("pymysql") - .pip_install("Pillow") - .pip_install("ffmpy") - .pip_install("opencv-python") - .pip_install("av") - .pip_install("imageio") - .pip_install("loguru") - .pip_install("conformer==0.3.2",extra_options="--no-dependencies") - .pip_install("einops>0.6.1",extra_options="--no-dependencies") - .pip_install("openai-whisper") - .run_commands( # use comfy-cli to install ComfyUI and its dependencies - "comfy --skip-prompt install --nvidia --version 0.3.10" - ) -) - -image = ( - image.run_commands("comfy node install https://github.com/M1kep/ComfyLiterals") - .run_commands("comfy node install https://github.com/evanspearman/ComfyMath") - .run_commands("comfy node install https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-Bowong.git") - .run_commands("comfy node install https://github.com/crystian/ComfyUI-Crystools") - .run_commands("comfy node install https://github.com/pythongosssss/ComfyUI-Custom-Scripts") - .run_commands("comfy node install https://github.com/BennyKok/comfyui-deploy") - .run_commands("comfy node install https://github.com/yolain/ComfyUI-Easy-Use") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-VideoHelperSuite.git") - .run_commands("comfy node install https://github.com/jags111/efficiency-nodes-comfyui") - .run_commands("comfy node install https://github.com/WASasquatch/was-node-suite-comfyui") - .run_commands("comfy node install https://github.com/rgthree/rgthree-comfy") - .run_commands("comfy node install https://github.com/cubiq/ComfyUI_essentials") - .run_commands("comfy node install https://github.com/melMass/comfy_mtb") - .run_commands("echo 3 && comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI_SparkTTS.git") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-CustomNode.git") - .run_commands("echo 04111 && comfy node install https://e.coding.net/g-ldyi2063/dev/cosyvoice_comfyui.git") - .run_commands("echo 4 && comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-LatentSync-Node.git") - .run_commands( - "mkdir -p /root/comfy/ComfyUI/models/ComfyUI-CustomNode/model && rm -rf /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/model && ln -s /root/comfy/ComfyUI/models/ComfyUI-CustomNode/model /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/model" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/ComfyUI-LatentSync-Node/checkpoints && rm -rf /root/comfy/ComfyUI/custom_nodes/ComfyUI-LatentSync-Node/checkpoints && ln -s /root/comfy/ComfyUI/models/ComfyUI-LatentSync-Node/checkpoints /root/comfy/ComfyUI/custom_nodes/ComfyUI-LatentSync-Node/checkpoints" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/.cache/torch/hub/checkpoints && mkdir -p /root/.cache/torch/hub/checkpoints && rm -rf /root/.cache/torch/hub/checkpoints && ln -s /root/comfy/ComfyUI/models/.cache/torch/hub/checkpoints /root/.cache/torch/hub/checkpoints" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/stabilityai && ln -s /root/comfy/ComfyUI/models/stabilityai /root/comfy/ComfyUI/stabilityai" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M && mkdir -p /root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/CosyVoice-300M && rm -rf /root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/CosyVoice-300M && ln -s /root/comfy/ComfyUI/models/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M /root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/CosyVoice-300M" - ).run_commands( - "rm -rf /root/comfy/ComfyUI/models" - ).run_commands( - "apt update && apt install -y ffmpeg && ffmpeg -version" - ).add_local_file("config.yaml", "/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml", copy=True - ).add_local_file("config.py", "/root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/tools/config.py", copy=True - ).workdir("/root/comfy") - - # Add .run_commands(...) calls for any other custom nodes you want to download -) - -app = modal.App(name="highlight-comfyui-s3", image=image) -vol = modal.Volume.from_name("comfyui-model", create_if_missing=True) -bucket_dict = modal.Dict.from_name("aws_s3_bucket", create_if_missing=False) -bucket_input = str(bucket_dict.get("INPUT")) -bucket_output = str(bucket_dict.get("OUTPUT")) -secret = modal.Secret.from_name("aws-s3-secret") -# completed workflows write output images to this directory -output_dir = "/root/comfy/ComfyUI/output" - - -@app.cls( - allow_concurrent_inputs=1, # allow 10 concurrent API calls - max_containers=200, - min_containers=0, - buffer_containers=0, - scaledown_window=120, - # 5 minute container keep alive after it processes an input; increasing this value is a great way to reduce ComfyUI cold start times - timeout=900, - gpu=["L4", "T4"], - cpu=(2,16), - # memory=(32768, 32768), # (内存预留量, 内存使用上限) - memory=(20480,40960), - enable_memory_snapshot=False, - secrets=[secret], - volumes={ - "/root/comfy/ComfyUI/models": vol, - "/root/comfy/ComfyUI/input_s3": modal.CloudBucketMount( - bucket_name=bucket_input, - # bucket_endpoint_url="https://s3.%s.amazonaws.com" % os.environ["AWS_REGION"], - secret=secret, - key_prefix="/" - ), - "/root/comfy/ComfyUI/output_s3": modal.CloudBucketMount( - bucket_name=bucket_output, - # bucket_endpoint_url="https://s3.%s.amazonaws.com" % os.environ["AWS_REGION"], - secret=secret, - key_prefix="/" - ), - }, -) -class ComfyUI: - @modal.enter() - def launch_comfy_background(self): - # starts the ComfyUI server in the background exactly once when the first input is received - self.session_id = str(uuid.uuid4()) - cmd = "echo client_uuid: {}&&rm -rf /root/comfy/ComfyUI/user&&mkdir -p /root/comfy/ComfyUI/output_s3/logs/{}&&ln -s /root/comfy/ComfyUI/output_s3/logs/{} /root/comfy/ComfyUI/user && comfy launch --background".format(self.session_id,self.session_id, self.session_id) - subprocess.run(cmd, shell=True, check=True) - - @modal.method() - def infer(self, workflow_json: str = ""): - self.poll_server_health() - self.prompt_uuid = str(uuid.uuid4()) - # runs the comfy run --workflow command as a subprocess - workflow = json.loads(workflow_json) - print("Workflow JSON:") - print(json.dumps(workflow, indent=4, ensure_ascii=False)) - for node in workflow.values(): - if node.get("class_type") == "ComfyUIDeployExternalText": - if node.get("inputs").get("input_id") == "file_path": - file_to_move = node.get("inputs").get("default_value") - if file_to_move: - os.makedirs(os.path.dirname(file_to_move.replace("input_s3", "input")), exist_ok=True) - try: - shutil.copy(file_to_move, file_to_move.replace("input_s3", "input")) - except: - try: - print("Try download file to S3 manually") - # S3 Fallback - import boto3 - import yaml - with open("/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml", - encoding="utf-8", mode="r+") as config: - yaml_config = yaml.load(config, Loader=yaml.FullLoader) - awss3 = boto3.resource('s3', aws_access_key_id=yaml_config["aws_key_id"], - aws_secret_access_key=yaml_config["aws_access_key"]) - awss3.meta.client.download_file(bucket_input, file_to_move.split("input_s3/")[1], - file_to_move.replace("input_s3", "input")) - except: - raise Exception("Failed to download file from S3 manually") - node["inputs"]["default_value"] = node["inputs"]["default_value"].replace("input_s3", "input") - with open(f"/root/{self.prompt_uuid}.json", "w", encoding="utf-8") as f: - f.write(json.dumps(workflow, ensure_ascii=False)) - cmd = f"comfy run --workflow /root/{self.prompt_uuid}.json --wait --timeout 890 --verbose" - subprocess.run(cmd, shell=True, check=True, timeout=895) - - # looks up the name of the output image file based on the workflow - file_prefix = [ - node.get("inputs") - for node in workflow.values() - if node.get("class_type") == "VHS_VideoCombine" - ][0]["filename_prefix"] - - # returns the image as bytes - file_list = os.listdir(output_dir) - # 获取按照文件时间创建排序的列表,默认是按时间升序 - new_file_list = sorted(file_list, key=lambda file: os.path.getctime(os.path.join(output_dir, file)), - reverse=True) - # print("file_list", new_file_list) - for f in new_file_list: - if f.startswith(file_prefix): - os.makedirs(os.path.dirname(os.path.join(output_dir.replace("output", "output_s3"), f)), exist_ok=True) - try: - shutil.copy(os.path.join(output_dir, f), os.path.join(output_dir.replace("output", "output_s3"), f)) - except: - try: - print("Try move file to S3 manually") - # S3 Fallback - import boto3 - import yaml - with open("/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml", encoding="utf-8", - mode="r+") as config: - yaml_config = yaml.load(config, Loader=yaml.FullLoader) - awss3 = boto3.resource('s3', aws_access_key_id=yaml_config["aws_key_id"], - aws_secret_access_key=yaml_config["aws_access_key"]) - awss3.meta.client.upload_file(os.path.join(output_dir, f), bucket_output, f) - except: - raise Exception("Failed to move file to S3 manually") - return f - - @modal.fastapi_endpoint(method="POST") - def api(self, item: Dict): - try: - # save this updated workflow to a new file - new_workflow_file = item["prompt"] - # run inference on the currently running container - fname = self.infer.local(new_workflow_file) - if fname is None: - raise RuntimeError("Output File not found") - j = {"file_name": fname} - loguru.logger.success(j) - return j - except Exception as e: - raise RuntimeError(str(e)) - finally: - print("Purge Garbage By Restart Comfy") - cmd = "comfy stop" - try: - subprocess.run(cmd, shell=True, check=True) - except: - pass - cmd = "comfy launch --background" - try: - subprocess.run(cmd, shell=True, check=True) - except: - pass - - - def poll_server_health(self): - import socket - import urllib - - try: - # dummy request to check if the server is healthy - req = urllib.request.Request("http://127.0.0.1:8188/system_stats") - urllib.request.urlopen(req, timeout=5) - print("ComfyUI server is healthy") - except (socket.timeout, urllib.error.URLError) as e: - # if no response in 5 seconds, stop the container; Modal will schedule queued inputs on a new container - print(f"Server health check failed: {str(e)} restarting ComfyUI...") - try: - cmd = "comfy stop" - try: - subprocess.run(cmd, shell=True, check=True) - except: - pass - cmd = "comfy launch --background" - try: - subprocess.run(cmd, shell=True, check=True) - except: - raise Exception("Failed to launch ComfyUI") - except: - modal.experimental.stop_fetching_inputs() - raise Exception("ComfyUI server is not healthy, restart failed, stopping container") - - # @modal.web_endpoint(method="POST", label="tk") - # def tk_api(self, item: Dict): - # pass - # - diff --git a/server_with_s3_auth.py b/server_with_s3_auth.py deleted file mode 100644 index 682d8a2..0000000 --- a/server_with_s3_auth.py +++ /dev/null @@ -1,261 +0,0 @@ -# ComfyUI模板--Base Auth -import json -import os -import shutil -import subprocess -import uuid -from typing import Dict - -import loguru -import modal -from fastapi import Depends, HTTPException, status -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials - -image = ( # build up a Modal Image to run ComfyUI, step by step - modal.Image.debian_slim( # start from basic Linux with Python - python_version="3.10" - ) - .apt_install("git") # install git to clone ComfyUI - .apt_install("gcc") - .apt_install("libportaudio2") - .pip_install("fastapi[standard]==0.115.4") # install web dependencies - .pip_install("comfy-cli==1.3.5") # install comfy-cli - .pip_install("cos-python-sdk-v5") - .pip_install("sqlalchemy") - .pip_install("ultralytics") - .pip_install("tencentcloud-sdk-python") - .pip_install("pymysql") - .pip_install("Pillow") - .pip_install("ffmpy") - .pip_install("opencv-python") - .pip_install("av") - .pip_install("imageio") - .pip_install("loguru") - .pip_install("conformer==0.3.2",extra_options="--no-dependencies") - .pip_install("einops>0.6.1",extra_options="--no-dependencies") - .pip_install("openai-whisper") - .run_commands( # use comfy-cli to install ComfyUI and its dependencies - "comfy --skip-prompt install --nvidia --version 0.3.10" - ) -) - -image = ( - image.run_commands("comfy node install https://github.com/M1kep/ComfyLiterals") - .run_commands("comfy node install https://github.com/evanspearman/ComfyMath") - .run_commands("comfy node install https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-Bowong.git") - .run_commands("comfy node install https://github.com/crystian/ComfyUI-Crystools") - .run_commands("comfy node install https://github.com/pythongosssss/ComfyUI-Custom-Scripts") - .run_commands("comfy node install https://github.com/BennyKok/comfyui-deploy") - .run_commands("comfy node install https://github.com/yolain/ComfyUI-Easy-Use") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-VideoHelperSuite.git") - .run_commands("comfy node install https://github.com/jags111/efficiency-nodes-comfyui") - .run_commands("comfy node install https://github.com/WASasquatch/was-node-suite-comfyui") - .run_commands("comfy node install https://github.com/rgthree/rgthree-comfy") - .run_commands("comfy node install https://github.com/cubiq/ComfyUI_essentials") - .run_commands("comfy node install https://github.com/melMass/comfy_mtb") - .run_commands("echo 3 && comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI_SparkTTS.git") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-CustomNode.git") - .run_commands("echo 04111 && comfy node install https://e.coding.net/g-ldyi2063/dev/cosyvoice_comfyui.git") - .run_commands("echo 4 && comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-LatentSync-Node.git") - .run_commands( - "mkdir -p /root/comfy/ComfyUI/models/ComfyUI-CustomNode/model && rm -rf /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/model && ln -s /root/comfy/ComfyUI/models/ComfyUI-CustomNode/model /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/model" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/ComfyUI-LatentSync-Node/checkpoints && rm -rf /root/comfy/ComfyUI/custom_nodes/ComfyUI-LatentSync-Node/checkpoints && ln -s /root/comfy/ComfyUI/models/ComfyUI-LatentSync-Node/checkpoints /root/comfy/ComfyUI/custom_nodes/ComfyUI-LatentSync-Node/checkpoints" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/.cache/torch/hub/checkpoints && mkdir -p /root/.cache/torch/hub/checkpoints && rm -rf /root/.cache/torch/hub/checkpoints && ln -s /root/comfy/ComfyUI/models/.cache/torch/hub/checkpoints /root/.cache/torch/hub/checkpoints" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/stabilityai && ln -s /root/comfy/ComfyUI/models/stabilityai /root/comfy/ComfyUI/stabilityai" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M && mkdir -p /root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/CosyVoice-300M && rm -rf /root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/CosyVoice-300M && ln -s /root/comfy/ComfyUI/models/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M /root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/CosyVoice-300M" - ).run_commands( - "rm -rf /root/comfy/ComfyUI/models" - ).run_commands( - "apt update && apt install -y ffmpeg && ffmpeg -version" - ).add_local_file("config.yaml", "/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml", copy=True - ).add_local_file("config.py", "/root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/tools/config.py", copy=True - ).workdir("/root/comfy") - - # Add .run_commands(...) calls for any other custom nodes you want to download -) - -app = modal.App(name="highlight-comfyui-s3", image=image) -vol = modal.Volume.from_name("comfyui-model", create_if_missing=True) -bucket_dict = modal.Dict.from_name("aws_s3_bucket", create_if_missing=False) -bucket_input = str(bucket_dict.get("INPUT")) -bucket_output = str(bucket_dict.get("OUTPUT")) -secret = modal.Secret.from_name("aws-s3-secret") -# completed workflows write output images to this directory -output_dir = "/root/comfy/ComfyUI/output" -auth_scheme = HTTPBearer() - - -@app.cls( - allow_concurrent_inputs=1, # allow 10 concurrent API calls - max_containers=200, - min_containers=0, - buffer_containers=0, - scaledown_window=120, - # 5 minute container keep alive after it processes an input; increasing this value is a great way to reduce ComfyUI cold start times - timeout=900, - gpu=["L4", "T4"], - cpu=(2,16), - # memory=(32768, 32768), # (内存预留量, 内存使用上限) - memory=(32768,131072), - enable_memory_snapshot=False, - secrets=[secret, modal.Secret.from_name("web_auth_token")], - volumes={ - "/root/comfy/ComfyUI/models": vol, - "/root/comfy/ComfyUI/input_s3": modal.CloudBucketMount( - bucket_name=bucket_input, - # bucket_endpoint_url="https://s3.%s.amazonaws.com" % os.environ["AWS_REGION"], - secret=secret, - key_prefix="/" - ), - "/root/comfy/ComfyUI/output_s3": modal.CloudBucketMount( - bucket_name=bucket_output, - # bucket_endpoint_url="https://s3.%s.amazonaws.com" % os.environ["AWS_REGION"], - secret=secret, - key_prefix="/" - ), - }, -) -class ComfyUI: - @modal.enter() - def launch_comfy_background(self): - # starts the ComfyUI server in the background exactly once when the first input is received - self.session_id = str(uuid.uuid4()) - cmd = "echo client_uuid: {}&&rm -rf /root/comfy/ComfyUI/user&&mkdir -p /root/comfy/ComfyUI/output_s3/logs/{}&&ln -s /root/comfy/ComfyUI/output_s3/logs/{} /root/comfy/ComfyUI/user && comfy launch --background".format(self.session_id,self.session_id, self.session_id) - subprocess.run(cmd, shell=True, check=True) - - @modal.method() - def infer(self, workflow_json: str = ""): - self.poll_server_health() - self.prompt_uuid = str(uuid.uuid4()) - # runs the comfy run --workflow command as a subprocess - workflow = json.loads(workflow_json) - print("Workflow JSON:") - print(json.dumps(workflow, indent=4, ensure_ascii=False)) - for node in workflow.values(): - if node.get("class_type") == "ComfyUIDeployExternalText": - if node.get("inputs").get("input_id") == "file_path": - file_to_move = node.get("inputs").get("default_value") - if file_to_move: - os.makedirs(os.path.dirname(file_to_move.replace("input_s3", "input")), exist_ok=True) - try: - shutil.copy(file_to_move, file_to_move.replace("input_s3", "input")) - except: - try: - print("Try download file to S3 manually") - # S3 Fallback - import boto3 - import yaml - with open("/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml",encoding="utf-8",mode="r+") as config: - yaml_config = yaml.load(config, Loader=yaml.FullLoader) - awss3 = boto3.resource('s3', aws_access_key_id=yaml_config["aws_key_id"], - aws_secret_access_key=yaml_config["aws_access_key"]) - awss3.meta.client.download_file(bucket_input, file_to_move.split("input_s3/")[1], file_to_move.replace("input_s3", "input")) - except: - raise Exception("Failed to download file from S3 manually") - node["inputs"]["default_value"] = node["inputs"]["default_value"].replace("input_s3", "input") - with open(f"/root/{self.prompt_uuid}.json", "w", encoding="utf-8") as fi: - fi.write(json.dumps(workflow, ensure_ascii=False)) - cmd = f"comfy run --workflow /root/{self.prompt_uuid}.json --wait --timeout 890 --verbose" - subprocess.run(cmd, shell=True, check=True, timeout=895) - - # looks up the name of the output image file based on the workflow - file_prefix = [ - node.get("inputs") - for node in workflow.values() - if node.get("class_type") == "VHS_VideoCombine" - ][0]["filename_prefix"] - - # returns the image as bytes - file_list = os.listdir(output_dir) - # 获取按照文件时间创建排序的列表,默认是按时间升序 - new_file_list = sorted(file_list, key=lambda file: os.path.getctime(os.path.join(output_dir, file)), - reverse=True) - # print("file_list", new_file_list) - for f in new_file_list: - if f.startswith(file_prefix): - os.makedirs(os.path.dirname(os.path.join(output_dir.replace("output", "output_s3"), f)), exist_ok=True) - try: - shutil.copy(os.path.join(output_dir, f), os.path.join(output_dir.replace("output", "output_s3"), f)) - except: - try: - print("Try move file to S3 manually") - # S3 Fallback - import boto3 - import yaml - with open("/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml", encoding="utf-8",mode="r+") as config: - yaml_config = yaml.load(config, Loader=yaml.FullLoader) - awss3 = boto3.resource('s3', aws_access_key_id=yaml_config["aws_key_id"], - aws_secret_access_key=yaml_config["aws_access_key"]) - awss3.meta.client.upload_file(os.path.join(output_dir, f), bucket_output, f) - except: - raise Exception("Failed to move file to S3 manually") - return f - - @modal.fastapi_endpoint(method="POST") - def api(self, item: Dict, token: HTTPAuthorizationCredentials = Depends(auth_scheme)): - if token.credentials != os.environ["AUTH_TOKEN"]: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect bearer token", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: - # save this updated workflow to a new file - new_workflow_file = item["prompt"] - # run inference on the currently running container - fname = self.infer.local(new_workflow_file) - if fname is None: - raise RuntimeError("Output File not found") - j = {"status":"success", "file_name": fname} - loguru.logger.success(j) - return j - except Exception as e: - j = {"status":"fail", "msg": str(e)} - loguru.logger.error(j) - return j - finally: - print("Purge Garbage By Restart Comfy") - cmd = "comfy stop" - try: - subprocess.run(cmd, shell=True, check=True) - except: - pass - cmd = "comfy launch --background" - try: - subprocess.run(cmd, shell=True, check=True) - except: - pass - - def poll_server_health(self): - import socket - import urllib - - try: - # dummy request to check if the server is healthy - req = urllib.request.Request("http://127.0.0.1:8188/system_stats") - urllib.request.urlopen(req, timeout=5) - print("ComfyUI server is healthy") - except (socket.timeout, urllib.error.URLError) as e: - # if no response in 5 seconds, stop the container; Modal will schedule queued inputs on a new container - print(f"Server health check failed: {str(e)} restarting ComfyUI...") - try: - cmd = "comfy stop" - try: - subprocess.run(cmd, shell=True, check=True) - except: - pass - cmd = "comfy launch --background" - try: - subprocess.run(cmd, shell=True, check=True) - except: - raise Exception("Failed to launch ComfyUI") - except: - modal.experimental.stop_fetching_inputs() - raise Exception("ComfyUI server is not healthy, restart failed, stopping container") - - diff --git a/server_with_s3_auth_heygem.py b/server_with_s3_auth_heygem.py deleted file mode 100644 index 9967487..0000000 --- a/server_with_s3_auth_heygem.py +++ /dev/null @@ -1,320 +0,0 @@ -# ComfyUI模板--Base Auth Heygem -import json -import os -import shutil -import subprocess -import uuid -from typing import Dict - -import loguru -import modal -from fastapi import Depends, HTTPException, status -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials - -image = ( # build up a Modal Image to run ComfyUI, step by step - modal.Image.debian_slim( # start from basic Linux with Python - python_version="3.10" - ) - .apt_install("git") # install git to clone ComfyUI - .apt_install("gcc") - .apt_install("libportaudio2") - .pip_install("fastapi[standard]==0.115.4") # install web dependencies - .pip_install("comfy-cli==1.3.5") # install comfy-cli - .pip_install("cos-python-sdk-v5") - .pip_install("sqlalchemy") - .pip_install("ultralytics") - .pip_install("tencentcloud-sdk-python") - .pip_install("pymysql") - .pip_install("Pillow") - .pip_install("ffmpy") - .pip_install("opencv-python") - .pip_install("av") - .pip_install("imageio") - .pip_install("loguru") - .pip_install("conformer==0.3.2",extra_options="--no-dependencies") - .pip_install("einops>0.6.1",extra_options="--no-dependencies") - .pip_install("openai-whisper") - .run_commands( # use comfy-cli to install ComfyUI and its dependencies - "comfy --skip-prompt install --nvidia --version 0.3.10" - ) -) - -image = ( - image.run_commands("comfy node install https://github.com/M1kep/ComfyLiterals") - .run_commands("comfy node install https://github.com/evanspearman/ComfyMath") - .run_commands("comfy node install https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-Bowong.git") - .run_commands("comfy node install https://github.com/crystian/ComfyUI-Crystools") - .run_commands("comfy node install https://github.com/pythongosssss/ComfyUI-Custom-Scripts") - .run_commands("comfy node install https://github.com/BennyKok/comfyui-deploy") - .run_commands("comfy node install https://github.com/yolain/ComfyUI-Easy-Use") - .run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-VideoHelperSuite.git") - .run_commands("comfy node install https://github.com/jags111/efficiency-nodes-comfyui") - .run_commands("comfy node install https://github.com/WASasquatch/was-node-suite-comfyui") - .run_commands("comfy node install https://github.com/rgthree/rgthree-comfy") - .run_commands("comfy node install https://github.com/cubiq/ComfyUI_essentials") - .run_commands("comfy node install https://github.com/melMass/comfy_mtb") - .run_commands("echo 3 && comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI_SparkTTS.git") - .run_commands("echo 4 && comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-CustomNode.git") - .run_commands("echo 04111 && comfy node install https://e.coding.net/g-ldyi2063/dev/cosyvoice_comfyui.git") - .run_commands("echo 4 && comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-LatentSync-Node.git") - .run_commands( - "mkdir -p /root/comfy/ComfyUI/models/ComfyUI-CustomNode/model && rm -rf /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/model && ln -s /root/comfy/ComfyUI/models/ComfyUI-CustomNode/model /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/model" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/ComfyUI-LatentSync-Node/checkpoints && rm -rf /root/comfy/ComfyUI/custom_nodes/ComfyUI-LatentSync-Node/checkpoints && ln -s /root/comfy/ComfyUI/models/ComfyUI-LatentSync-Node/checkpoints /root/comfy/ComfyUI/custom_nodes/ComfyUI-LatentSync-Node/checkpoints" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/.cache/torch/hub/checkpoints && mkdir -p /root/.cache/torch/hub/checkpoints && rm -rf /root/.cache/torch/hub/checkpoints && ln -s /root/comfy/ComfyUI/models/.cache/torch/hub/checkpoints /root/.cache/torch/hub/checkpoints" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/stabilityai && ln -s /root/comfy/ComfyUI/models/stabilityai /root/comfy/ComfyUI/stabilityai" - ).run_commands( - "mkdir -p /root/comfy/ComfyUI/models/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M && mkdir -p /root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/CosyVoice-300M && rm -rf /root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/CosyVoice-300M && ln -s /root/comfy/ComfyUI/models/CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M /root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/CosyVoice-300M" - ).run_commands( - "rm -rf /root/comfy/ComfyUI/models" - ).run_commands( - "apt update && apt install -y ffmpeg && ffmpeg -version" - ).add_local_file("config.yaml", "/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml", copy=True - ).add_local_file("config.py", "/root/comfy/ComfyUI/custom_nodes/cosyvoice_comfyui/pretrained_models/tools/config.py", copy=True - ) # 添加Python3.8 HeyGem - .run_commands("apt update && apt install -y curl build-essential libssl-dev zlib1g-dev libncurses5-dev libncursesw5-dev libreadline-dev libsqlite3-dev libgdbm-dev libdb5.3-dev libbz2-dev libexpat1-dev lzma liblzma-dev tk-dev libffi-dev") - .run_commands("curl -O https://www.python.org/ftp/python/3.8.12/Python-3.8.12.tar.xz&&tar -xf Python-3.8.12.tar.xz") - .run_commands("cd Python-3.8.12 && ./configure --enable-optimizations && make -j 10 && make altinstall") - .add_local_file("heygem-1.0-py3-none-any.whl","/root/comfy/heygem-1.0-py3-none-any.whl", copy=True) - .shell(["/bin/bash", "-c"]) - .run_commands("python3.8 -m pip install /root/comfy/heygem-1.0-py3-none-any.whl") - .env({"LD_LIBRARY_PATH":"/usr/local/lib/python3.8/site-packages/nvidia/cuda_nvrtc/lib"}) - .run_commands("ln -s /usr/local/lib/python3.8/site-packages/nvidia/cuda_nvrtc/lib/libnvrtc.so.11.2 /usr/local/lib/python3.8/site-packages/nvidia/cuda_nvrtc/lib/libnvrtc.so") - .run_commands("python3.8 -m pip install https://github.com/pydata/numexpr/releases/download/v2.8.6/numexpr-2.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl") - .add_local_file("heygem.py", "/root/comfy/heygem.py", copy=True) - .workdir("/root/comfy") - - # Add .run_commands(...) calls for any other custom nodes you want to download -) - -app = modal.App(name="highlight-comfyui-s3", image=image) -vol = modal.Volume.from_name("comfyui-model", create_if_missing=True) -bucket_dict = modal.Dict.from_name("aws_s3_bucket", create_if_missing=False) -bucket_input = str(bucket_dict.get("INPUT")) -bucket_output = str(bucket_dict.get("OUTPUT")) -secret = modal.Secret.from_name("aws-s3-secret") -# completed workflows write output images to this directory -output_dir = "/root/comfy/ComfyUI/output" -auth_scheme = HTTPBearer() - - -@app.cls( - allow_concurrent_inputs=1, # allow 10 concurrent API calls - max_containers=200, - min_containers=0, - buffer_containers=0, - scaledown_window=120, - # 5 minute container keep alive after it processes an input; increasing this value is a great way to reduce ComfyUI cold start times - timeout=1200, - gpu=["L4", "T4"], - cpu=(2,16), - # memory=(32768, 32768), # (内存预留量, 内存使用上限) - memory=(20480,81920), - enable_memory_snapshot=False, - secrets=[secret, modal.Secret.from_name("web_auth_token")], - volumes={ - "/root/comfy/ComfyUI/models": vol, - "/root/comfy/ComfyUI/input_s3": modal.CloudBucketMount( - bucket_name=bucket_input, - # bucket_endpoint_url="https://s3.%s.amazonaws.com" % os.environ["AWS_REGION"], - secret=secret, - key_prefix="/" - ), - "/root/comfy/ComfyUI/output_s3": modal.CloudBucketMount( - bucket_name=bucket_output, - # bucket_endpoint_url="https://s3.%s.amazonaws.com" % os.environ["AWS_REGION"], - secret=secret, - key_prefix="/" - ), - }, -) -class ComfyUI: - @modal.enter() - def launch_comfy_background(self): - # starts the ComfyUI server in the background exactly once when the first input is received - self.session_id = str(uuid.uuid4()) - cmd = ("echo client_uuid: {}" - " && rm -rf /root/comfy/ComfyUI/user" - " && mkdir -p /root/comfy/ComfyUI/output_s3/logs/{}" - " && ln -s /root/comfy/ComfyUI/output_s3/logs/{} /root/comfy/ComfyUI/user" - " && nohup python3.8 /root/comfy/heygem.py >> /root/comfy/ComfyUI/output_s3/logs/{}/heygem.log 2>&1 &" - ).format(self.session_id,self.session_id, self.session_id, self.session_id) - subprocess.run(cmd, shell=True, check=True) - cmd = "comfy launch --background" - subprocess.run(cmd, shell=True, check=True) - - @modal.method() - def infer(self, workflow_json: str = ""): - self.poll_server_health() - self.prompt_uuid = str(uuid.uuid4()) - # runs the comfy run --workflow command as a subprocess - workflow = json.loads(workflow_json) - print("Workflow JSON:") - print(json.dumps(workflow, indent=4, ensure_ascii=False)) - for node in workflow.values(): - if node.get("class_type") == "ComfyUIDeployExternalText": - if node.get("inputs").get("input_id") == "file_path": - file_to_move = node.get("inputs").get("default_value") - if file_to_move: - os.makedirs(os.path.dirname(file_to_move.replace("input_s3", "input")), exist_ok=True) - try: - shutil.copy(file_to_move, file_to_move.replace("input_s3", "input")) - except: - try: - print("Try download file to S3 manually") - # S3 Fallback - import boto3 - import yaml - with open("/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml",encoding="utf-8",mode="r+") as config: - yaml_config = yaml.load(config, Loader=yaml.FullLoader) - awss3 = boto3.resource('s3', aws_access_key_id=yaml_config["aws_key_id"], - aws_secret_access_key=yaml_config["aws_access_key"]) - awss3.meta.client.download_file(bucket_input, file_to_move.split("input_s3/")[1], file_to_move.replace("input_s3", "input")) - except: - raise Exception("Failed to download file from S3 manually") - node["inputs"]["default_value"] = node["inputs"]["default_value"].replace("input_s3", "input") - with open(f"/root/{self.prompt_uuid}.json", "w", encoding="utf-8") as fi: - fi.write(json.dumps(workflow, ensure_ascii=False)) - cmd = f"comfy run --workflow /root/{self.prompt_uuid}.json --wait --timeout 1190 --verbose" - subprocess.run(cmd, shell=True, check=True, timeout=1195) - - # looks up the name of the output image file based on the workflow - if "VHS_VideoCombine" in workflow_json: - file_prefix = [ - node.get("inputs") - for node in workflow.values() - if node.get("class_type") == "VHS_VideoCombine" - ][0]["filename_prefix"] - - # returns the image as bytes - file_list = os.listdir(output_dir) - # 获取按照文件时间创建排序的列表,默认是按时间升序 - new_file_list = sorted(file_list, key=lambda file: os.path.getctime(os.path.join(output_dir, file)), - reverse=True) - # print("file_list", new_file_list) - for f in new_file_list: - if f.startswith(file_prefix): - os.makedirs(os.path.dirname(os.path.join(output_dir.replace("output", "output_s3"), f)), exist_ok=True) - try: - shutil.copy(os.path.join(output_dir, f), os.path.join(output_dir.replace("output", "output_s3"), f)) - except: - try: - print("Try move file to S3 manually") - # S3 Fallback - import boto3 - import yaml - with open("/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml", encoding="utf-8",mode="r+") as config: - yaml_config = yaml.load(config, Loader=yaml.FullLoader) - awss3 = boto3.resource('s3', aws_access_key_id=yaml_config["aws_key_id"], - aws_secret_access_key=yaml_config["aws_access_key"]) - awss3.meta.client.upload_file(os.path.join(output_dir, f), bucket_output, f) - except: - raise Exception("Failed to move file to S3 manually") - return f - elif "HeyGemF2F" in workflow_json or "HeyGemF2FFromFile" in workflow_json: - heygem_dir = [ - node.get("inputs") - for node in workflow.values() - if node.get("class_type") == "HeyGemF2F" or node.get("class_type") == "HeyGemF2FFromFile" - ][0]["heygem_temp_path"] - # returns the image as bytes - file_list = os.listdir(heygem_dir) - # 获取按照文件时间创建排序的列表,默认是按时间升序 - new_file_list = sorted(file_list, key=lambda file: os.path.getctime(os.path.join(heygem_dir, file)), - reverse=True) - if len(new_file_list) > 0: - try: - shutil.copy(os.path.join(heygem_dir, new_file_list[0]), os.path.join(output_dir.replace("output", "output_s3"), new_file_list[0])) - except: - try: - print("Try move file to S3 manually") - # S3 Fallback - import boto3 - import yaml - with open("/root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/config.yaml", encoding="utf-8", - mode="r+") as config: - yaml_config = yaml.load(config, Loader=yaml.FullLoader) - awss3 = boto3.resource('s3', aws_access_key_id=yaml_config["aws_key_id"], - aws_secret_access_key=yaml_config["aws_access_key"]) - awss3.meta.client.upload_file(os.path.join(heygem_dir, new_file_list[0]), bucket_output, new_file_list[0]) - except: - raise Exception("Failed to move HeyGem file to S3 manually") - return new_file_list[0] - else: - raise FileNotFoundError("HeyGem generation file not found") - else: - raise Exception("Workflow does not contain VHS_VideoCombine/HeygemF2F node, cannot find output file") - - - - - @modal.fastapi_endpoint(method="POST") - def api(self, item: Dict, token: HTTPAuthorizationCredentials = Depends(auth_scheme)): - if token.credentials != os.environ["AUTH_TOKEN"]: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect bearer token", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: - # save this updated workflow to a new file - new_workflow_file = item["prompt"] - # run inference on the currently running container - fname = self.infer.local(new_workflow_file) - if fname is None: - raise RuntimeError("Output File not found") - j = {"status": "success", "file_name": fname} - loguru.logger.success(j) - return j - except Exception as e: - j = {"status":"fail", "msg": str(e)} - loguru.logger.error(j) - return j - finally: - print("Purge Garbage By Restart Comfy") - cmd = "comfy stop" - try: - subprocess.run(cmd, shell=True, check=True) - except: - pass - cmd = "comfy launch --background" - try: - subprocess.run(cmd, shell=True, check=True) - except: - pass - - def poll_server_health(self): - import socket - import urllib - - try: - # dummy request to check if the server is healthy - req = urllib.request.Request("http://127.0.0.1:8188/system_stats") - urllib.request.urlopen(req, timeout=5) - print("ComfyUI server is healthy") - except (socket.timeout, urllib.error.URLError) as e: - # if no response in 5 seconds, stop the container; Modal will schedule queued inputs on a new container - print(f"Server health check failed: {str(e)} restarting ComfyUI...") - try: - cmd = "comfy stop" - try: - subprocess.run(cmd, shell=True, check=True) - except: - pass - cmd = "comfy launch --background" - try: - subprocess.run(cmd, shell=True, check=True) - except: - raise Exception("Failed to launch ComfyUI") - except: - modal.experimental.stop_fetching_inputs() - raise Exception("ComfyUI server is not healthy, restart failed, stopping container") - - # @modal.web_endpoint(method="POST", label="tk") - # def tk_api(self, item: Dict): - # pass - # - diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cluster/__init__.py b/src/cluster/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cluster/app.py b/src/cluster/app.py new file mode 100644 index 0000000..ac3254e --- /dev/null +++ b/src/cluster/app.py @@ -0,0 +1,13 @@ +import modal +from config import config +from video_downloader.worker import worker_app +from web.worker import fastapi_app +from ffmpeg_worker.worker import ffmpeg_worker_app + +app = modal.App('video-downloader', + secrets=[modal.Secret.from_name("cf-kv-secret", + environment_name=config.environment)]) + +app.include(fastapi_app) +app.include(worker_app) +app.include(ffmpeg_worker_app) diff --git a/src/cluster/config.py b/src/cluster/config.py new file mode 100644 index 0000000..834b8c5 --- /dev/null +++ b/src/cluster/config.py @@ -0,0 +1,25 @@ +from typing import Optional, Any +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class WorkerConfig(BaseSettings): + video_downloader_concurrency: Optional[int] = Field(default=10, description="处理缓存任务的并行数") + ffmpeg_worker_concurrency: Optional[int] = Field(default=10, description="处理视频合成任务的并行数") + + modal_kv_name: Optional[str] = Field(default='media-cache', description="Modal视频缓存KV库") + s3_bucket_name: Optional[str] = Field(default='modal-media-cache', description="集群挂载的S3存储桶") + cdn_endpoint: Optional[str] = Field(default="https://d2nj71io21vkj2.cloudfront.net", + description="集群挂载S3存储桶的对应AWS Cloudfront CDN") + environment: Optional[str] = Field(default="dev", description="Modal worker运行环境") + + modal_config: Any = SettingsConfigDict() + + +config = WorkerConfig( + video_downloader_concurrency=10, + ffmpeg_worker_concurrency=10, + modal_kv_name="media-cache", + s3_bucket_name="modal-media-cache", + environment="dev" +) \ No newline at end of file diff --git a/src/cluster/ffmpeg_worker/Utils/PathUtils.py b/src/cluster/ffmpeg_worker/Utils/PathUtils.py new file mode 100644 index 0000000..95ecf2b --- /dev/null +++ b/src/cluster/ffmpeg_worker/Utils/PathUtils.py @@ -0,0 +1,12 @@ +import os + + +class FileUtils: + @staticmethod + def file_path_extend(media_path: str, extend: str) -> str: + media_filename = os.path.basename(media_path) + media_dir = os.path.dirname(media_path) + '/' + filenames = media_filename.split('.') + filenames[0] = f"{filenames[0]}_{extend}" + extend_filename = '.'.join(filenames) + return os.path.join(media_dir, extend_filename) diff --git a/src/cluster/ffmpeg_worker/Utils/TimeUtils.py b/src/cluster/ffmpeg_worker/Utils/TimeUtils.py new file mode 100644 index 0000000..7c347a4 --- /dev/null +++ b/src/cluster/ffmpeg_worker/Utils/TimeUtils.py @@ -0,0 +1,16 @@ +from datetime import timedelta, datetime + + +class TimeDelta(timedelta): + @classmethod + def from_timedelta(cls, delta: timedelta) -> "TimeDelta": + return cls(days=delta.days, seconds=delta.seconds, microseconds=delta.microseconds) + + @classmethod + def from_format_string(cls, format_string: str) -> "TimeDelta": + formated_time = datetime.strptime(format_string, "%H:%M:%S.%f") + return cls(hours=formated_time.hour, minutes=formated_time.minute, seconds=formated_time.second, + microseconds=formated_time.microsecond) + + def toFormatStr(self) -> str: + return (datetime(year=2000, month=1, day=1) + self).strftime("%H:%M:%S.%f")[:-3] diff --git a/src/cluster/ffmpeg_worker/Utils/__init__.py b/src/cluster/ffmpeg_worker/Utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cluster/ffmpeg_worker/__init__.py b/src/cluster/ffmpeg_worker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cluster/ffmpeg_worker/model.py b/src/cluster/ffmpeg_worker/model.py new file mode 100644 index 0000000..f53705d --- /dev/null +++ b/src/cluster/ffmpeg_worker/model.py @@ -0,0 +1,65 @@ +from typing import Union, Any + +from pydantic import BaseModel, Field, computed_field, field_validator +from pydantic.json_schema import JsonSchemaValue + +from src.cluster.ffmpeg_worker.Utils.TimeUtils import TimeDelta + + +class FFMpegSliceSegment(BaseModel): + start: TimeDelta = Field(description="FFMPEG segment start time from 0") + end: TimeDelta = Field(description="FFMPEG segment end time from 0") + + @computed_field + @property + def duration(self) -> TimeDelta: + return self.end - self.start + + @field_validator('start', mode='before') + @classmethod + def parse_start(cls, v: Union[float, TimeDelta]): + if isinstance(v, float): + return TimeDelta(seconds=v) + elif isinstance(v, int): + return TimeDelta(seconds=v) + elif isinstance(v, TimeDelta): + return v + else: + raise TypeError(v) + + @field_validator('end', mode='before') + @classmethod + def parse_end(cls, v: Union[float, TimeDelta]): + if isinstance(v, float): + return TimeDelta(seconds=v) + elif isinstance(v, int): + return TimeDelta(seconds=v) + elif isinstance(v, TimeDelta): + return v + else: + raise TypeError(v) + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: Any, handler: Any) -> JsonSchemaValue: + # Override the schema to represent it as a string + return { + "type": "object", + "properties": { + "start": { + "type": "number", + "examples": [5, 10.5] + }, + "end": { + "type": "number", + "examples": [8, 12.5] + } + }, + "required": [ + "start", + "end" + ] + } + + model_config = { + "arbitrary_types_allowed": True + } diff --git a/src/cluster/ffmpeg_worker/worker.py b/src/cluster/ffmpeg_worker/worker.py new file mode 100644 index 0000000..4f6aaac --- /dev/null +++ b/src/cluster/ffmpeg_worker/worker.py @@ -0,0 +1,132 @@ +import os.path + +import modal +from src.cluster.config import config + +ffmpeg_worker_image = ( + modal.Image.debian_slim(python_version="3.11") + .apt_install('ffmpeg') + .pip_install('sentry-sdk', 'loguru', 'pydantic', 'pydantic_settings', 'httpx', 'python-ffmpeg') + .add_local_python_source("src.cluster.ffmpeg_worker.model", copy=True) + .add_local_python_source("src.cluster.video_downloader.model", copy=True) +) + +ffmpeg_worker_app = modal.App("ffmpeg_worker_app", image=ffmpeg_worker_image, ) +ffmpeg_worker_app.set_description("FFMPEG worker app") + +with ffmpeg_worker_image.imports(): + from typing import List + from loguru import logger + import sentry_sdk + from src.cluster.video_downloader.model import MediaSource, MediaCache + from src.cluster.ffmpeg_worker.model import FFMpegSliceSegment, TimeDelta + + sentry_sdk.init( + dsn="https://75cca970bfcc3d45d24361e7c0f1833c@sentry.bowongai.com/4", + send_default_pii=True, + add_full_stack=True, + shutdown_timeout=2, + traces_sample_rate=1.0, + environment=config.environment, + ) + + modal_kv = modal.Dict.from_name(config.modal_kv_name, + environment_name=config.environment, + create_if_missing=True) + + + @sentry_sdk.trace + def get_cache_filepath(media: MediaSource) -> MediaCache: + cache_data_json = modal_kv.get(media.cache_key) + if cache_data_json: + cache_data = MediaCache.model_validate_json(cache_data_json) + return cache_data + raise FileNotFoundError(f"{media.cache_key} cache not found") + + + @ffmpeg_worker_app.function( + cpu=6, timeout=1800, memory=(2048, 6144), + # cloud="aws", + # region='ap-northeast', + max_containers=config.ffmpeg_worker_concurrency, + volumes={ + "/mntS3": modal.CloudBucketMount( + bucket_name=config.s3_bucket_name, + secret=modal.Secret.from_name("aws-s3-secret", environment_name=config.environment), + ), + }, ) + @modal.concurrent(max_inputs=1) + def ffmpeg_concat_medias(): + return {} + + + @ffmpeg_worker_app.function( + cpu=12, timeout=900, memory=(2048, 4096), + cloud="aws", + max_containers=config.ffmpeg_worker_concurrency, + volumes={ + "/mntS3": modal.CloudBucketMount( + bucket_name=config.s3_bucket_name, + secret=modal.Secret.from_name("aws-s3-secret", environment_name=config.environment), + ), + }, + ) + @modal.concurrent(max_inputs=1) + async def ffmpeg_slice_media(media: MediaSource, markers: List[FFMpegSliceSegment], ): + from src.cluster.ffmpeg_worker.Utils.PathUtils import FileUtils + from ffmpeg.asyncio import FFmpeg + + cache = get_cache_filepath(media) + logger.info(cache.cache_filepath) + outputs: List[str] = [] + ffmpeg_cmd = FFmpeg().option('y').option('hide_banner').input(cache.cache_filepath) + filter_complex: List[str] = [] + for index, marker in enumerate(markers): + segment_out_index = f"cut{index}" + filter_complex.append( + f"[0]trim=start={marker.start.total_seconds()}:end={marker.end.total_seconds()},setpts=PTS-STARTPTS[{segment_out_index}]") + ffmpeg_cmd.option('filter_complex', ';'.join(filter_complex)) + for i, marker in enumerate(markers): + output_filepath = FileUtils.file_path_extend(cache.cache_filepath, str(i)) + # f"{marker.start.toFormatStr()}_{marker.end.toFormatStr()}") + workdir = os.path.dirname(output_filepath) + os.makedirs(workdir, exist_ok=True) + outputs.append(output_filepath) + ffmpeg_cmd.output(output_filepath, + map=f"[cut{i}]", + reset_timestamps="1", + sc_threshold="0", + g="1", + force_key_frames="expr:gte(t, n_forced * 1)", + vcodec="libx264", + crf=16, + r=30, ) + + @ffmpeg_cmd.on("start") + def on_start(arguments: list[str]): + try: + filter_index = arguments.index("-filter_complex") + filter_content = arguments[filter_index + 1] + arguments[filter_index + 1] = f'"{filter_content}"' + args = " ".join(arguments) + logger.info(f"FFmpeg command:{args}") + arguments[filter_index + 1] = filter_content + except ValueError: + args = " ".join(arguments) + logger.info(f"FFmpeg command:{args}") + + @ffmpeg_cmd.on("progress") + def on_progress(progress): + logger.info(f"处理进度: {progress}") + + @ffmpeg_cmd.on("completed") + def on_completed(): + logger.info(f"FFMpeg task completed.") + + @ffmpeg_cmd.on("stderr") + def on_stderr(line): + logger.warning(line) + + await ffmpeg_cmd.execute() + + return outputs diff --git a/src/cluster/video_downloader/__init__.py b/src/cluster/video_downloader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cluster/video_downloader/model.py b/src/cluster/video_downloader/model.py new file mode 100644 index 0000000..e23e427 --- /dev/null +++ b/src/cluster/video_downloader/model.py @@ -0,0 +1,103 @@ +from datetime import datetime +from enum import Enum +from typing import List, Union, Optional, Any, Dict +from pydantic import BaseModel, Field, field_validator, ValidationError, field_serializer, SerializationInfo +from pydantic.json_schema import JsonSchemaValue + + +class MediaProtocol(str, Enum): + http = "http" + s3 = "s3" + vod = "vod" + cos = "cos" + + +class MediaCacheStatus(str, Enum): + downloading = "downloading" + failed = "failed" + ready = "ready" + deleted = "deleted" + missing = "missing" + + +class MediaSource(BaseModel): + url: str = Field() + protocol: MediaProtocol = Field() + endpoint: Optional[str] = Field() + bucket: Optional[str] = Field() + cache_key: Optional[str] = Field() + + @classmethod + def from_str(cls, mediaUrl: str) -> 'MediaSource': + if mediaUrl.startswith('http://') or mediaUrl.startswith('https://'): + return MediaSource(url=mediaUrl, protocol=MediaProtocol.http, cache_key=mediaUrl) + elif mediaUrl.startswith('s3://'): # s3://{endpoint}/{bucket}/{url} + paths = mediaUrl[5:].split('/') + return MediaSource(url='/'.join(paths[2:]), protocol=MediaProtocol.s3, endpoint=paths[0], bucket=paths[1], + cache_key=mediaUrl) + elif mediaUrl.startswith('vod://'): # vod://{endpoint}/{subAppId}/{fileId} + paths = mediaUrl[6:].split('/') + return MediaSource(url=paths[2], protocol=MediaProtocol.vod, bucket=paths[1], endpoint=paths[0], + cache_key=mediaUrl) + elif mediaUrl.startswith('cos://'): # cos://{endpoint}/{bucket}/{url} + paths = mediaUrl[6:].split('/') + return MediaSource(url='/'.join(paths[2:]), protocol=MediaProtocol.cos, endpoint=paths[0], bucket=paths[1], + cache_key=mediaUrl) + else: + raise ValidationError("mediaUrl必须以http[s]、s3或vod协议开头") + + @classmethod + def __get_pydantic_json_schema__(cls, core_schema: Any, handler: Any) -> JsonSchemaValue: + # Override the schema to represent it as a string + return { + "type": "string", + "examples": [ + "vod://ap-shanghai/subAppId/fileId", + "http://example.com/video.mp4", + "s3://endpoint/bucket/path/to/file", + "cos://endpoint/bucket/path/to/file" + ] + } + + +class MediaSources(BaseModel): + inputs: List[MediaSource] = Field(examples=[ + ["vod://ap-shanghai/1500034234/1397757910405340824", "vod://ap-shanghai/1500034234/1397757910403699452"]]) + + @field_validator('inputs', mode='before') + @classmethod + def parse_inputs(cls, v: Union[str, MediaSource]) -> List[MediaSource]: + if not v: + return v + result = [] + for item in v: + if isinstance(item, str): + result.append(MediaSource.from_str(item)) + elif isinstance(item, MediaSource): + result.append(item) + else: + raise ValidationError("inputs元素类型错误: 必须是字符串") + return result + + model_config = { + "arbitrary_types_allowed": True + } + + +class MediaCache(BaseModel): + status: MediaCacheStatus = Field(description="缓存被处理的状态") + progress: float = Field(description="缓存下载进度0-1", default=0) + expired_at: Optional[datetime] = Field(description="缓存过期时间点", default=None) + downloader_id: Optional[str] = Field(description="正在处理下载的Downloader ID", default=None) + cache_filepath: Optional[str] = Field(description="缓存的文件地址", default=None) + + @field_serializer('expired_at') + def serialize_datetime(self, value: Optional[datetime], info: SerializationInfo) -> Optional[str]: + if value: + return value.isoformat() + else: + return None + + +class CacheResult(BaseModel): + caches: Dict[str, MediaCache] = Field(description="Cache ID") diff --git a/src/cluster/video_downloader/worker.py b/src/cluster/video_downloader/worker.py new file mode 100644 index 0000000..eb26a87 --- /dev/null +++ b/src/cluster/video_downloader/worker.py @@ -0,0 +1,301 @@ +import modal +from src.cluster.config import config + +downloader_image = ( + modal.Image + .debian_slim(python_version="3.11") + .pip_install('httpx', 'cos-python-sdk-v5', 'loguru', 'pydantic', 'pydantic_settings', 'sentry-sdk[loguru]', + 'tqdm', 'crcmod', 'tencentcloud-sdk-python-common', 'tencentcloud-sdk-python-vod') + .add_local_python_source("src.cluster.video_downloader.model", copy=True) + .add_local_python_source("src.cluster.web.model", copy=True) + .add_local_python_source("src.cluster.config", copy=True) +) + +worker_app = modal.App("video-downloader-worker", image=downloader_image, secrets=[ + modal.Secret.from_name("cf-kv-secret", environment_name=config.environment), +]) + +with downloader_image.imports(): + import os, httpx, crcmod + from tqdm import tqdm + from typing import Tuple, Dict + from loguru import logger + from modal import current_function_call_id + from src.cluster.video_downloader.model import MediaSource, MediaCacheStatus, MediaCache, MediaProtocol + from src.cluster.web.model import SentryTransactionInfo + from datetime import datetime, UTC, timedelta + from tencentcloud.common.credential import Credential + from tencentcloud.vod.v20180717.vod_client import VodClient + from tencentcloud.vod.v20180717 import models as vod_request_models + import sentry_sdk + + sentry_sdk.init(dsn="https://85632fdcd62f699c2f88af6ca489e9ec@sentry.bowongai.com/3", + send_default_pii=True, + traces_sample_rate=1.0, + profiles_sample_rate=1.0, + add_full_stack=True, + shutdown_timeout=2, + environment=config.environment, + ) + + cf_account_id = os.environ.get("CF_ACCOUNT_ID") + cf_kv_api_token = os.environ.get("CF_KV_API_TOKEN") + cf_kv_namespace_id = os.environ.get("CF_KV_NAMESPACE_ID") + + + @sentry_sdk.trace + def batch_update_cloudflare_kv(caches: Dict[str, MediaCache]): + with httpx.Client() as client: + try: + response = client.put( + f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/storage/kv/namespaces/{cf_kv_namespace_id}/bulk", + headers={"Authorization": f"Bearer {cf_kv_api_token}"}, + json=[ + { + "based64": False, + "key": mediaKey, + "value": cache_data.model_dump_json(), + } + for (mediaKey, cache_data) in caches.items() + ] + ) + response.raise_for_status() + except httpx.RequestError as e: + logger.error(f"An error occurred while put kv to cloudflare") + raise e + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error occurred while get kv from cloudflare {str(e)}") + raise e + except Exception as e: + logger.error(f"An unexpected error occurred: {str(e)}") + raise e + + + @sentry_sdk.trace + def batch_remove_cloudflare_kv(caches: Dict[str, MediaCache]): + with httpx.Client() as client: + try: + response = client.post( + f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/storage/kv/namespaces/{cf_kv_namespace_id}/bulk/delete", + headers={"Authorization": f"Bearer {cf_kv_api_token}"}, + json=[mediaKey for (mediaKey, cache_data) in caches.items()] + ) + response.raise_for_status() + except httpx.RequestError as e: + logger.error(f"An error occurred while put kv to cloudflare") + raise e + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error occurred while get kv from cloudflare {str(e)}") + raise e + except Exception as e: + logger.error(f"An unexpected error occurred: {str(e)}") + raise e + + + @worker_app.function(cpu=1, timeout=1800, + cloud="aws", + # region='ap-northeast', + max_containers=config.video_downloader_concurrency, + volumes={ + "/mntS3": modal.CloudBucketMount( + bucket_name=config.s3_bucket_name, + secret=modal.Secret.from_name("aws-s3-secret", environment_name=config.environment), + ), + }, + secrets=[modal.Secret.from_name("tencent-cloud-secret", environment_name=config.environment)]) + @modal.concurrent(max_inputs=10) + async def cache_submit(media: MediaSource, sentry_trace: SentryTransactionInfo) -> MediaCache: + def vod_init(): + tencent_secret_id = os.environ["VOD_SECRET_ID"] + tencent_secret_key = os.environ["VOD_SECRET_KEY"] + cred = Credential(secret_id=tencent_secret_id, secret_key=tencent_secret_key) + return VodClient(credential=cred, region='ap-shanghai') + + def vod_info(media: MediaSource) -> Tuple[str, str, str]: + logger.info(f"Downloading media {media}") + request = vod_request_models.DescribeMediaInfosRequest() + request.SubAppId = int(media.bucket) + request.FileIds = [media.url] + response = vod_client.DescribeMediaInfos(request) + if len(response.MediaInfoSet) > 0: + media_info = response.MediaInfoSet[0].BasicInfo + logger.info(f"VOD info = {media_info}") + file_extension = media_info.Type + cache_dir = f"/mntS3/{media.protocol.value}/{media.endpoint}/{media.bucket}" + cache_file = f"{media.url}.{file_extension}" + return (cache_dir, cache_file, media_info.MediaUrl) + else: + raise FileNotFoundError( + f"FileId : {media.url} not found in SubAppId: {media.bucket} at {media.endpoint}") + + def vod_download(media: MediaSource, on_progress_update: callable(float) = None) -> str: + cache_dir, cache_file, url = vod_info(media) + local_cache_filepath = os.path.join(cache_dir, cache_file) + download_large_file(url=url, output_path=local_cache_filepath, + on_progress_callback=on_progress_update) + return local_cache_filepath + + def download_large_file(url: str, output_path: str, on_progress_callback: callable(float) = None) -> None: + # 配置日志 + logger.info(f"Starting download from {url}") + try: + # 使用 httpx 发送 HEAD 请求获取文件大小 + with httpx.Client() as client: + head_response = client.head(url) + file_size = int(head_response.headers.get('content-length', 0)) + remote_crc64 = int(head_response.headers.get('X-Cos-Hash-Crc64ecma', 0)) + logger.info(f"File size: {file_size / (1024 * 1024 * 1024):.2f} GB") + # 设置请求头,支持断点续传 + headers = {'Range': 'bytes=0-'} + if os.path.exists(output_path): + local_file_size = os.path.getsize(output_path) + logger.info(f"File size match check {local_file_size} = {file_size}") + if local_file_size == file_size: + logger.info(f"Check file CRC64...") + # CRC64使用ECMA-182标准校验 ref: https://cloud.tencent.com/document/product/436/40334#python-sdk + c64 = crcmod.mkCrcFun(0x142F0E1EBA9EA3693, initCrc=0, xorOut=0xffffffffffffffff, rev=True) + with open(output_path, "rb") as local_file: + local_crc64 = c64(local_file.read()) + logger.info(f"File crc64 check {local_crc64} = {remote_crc64}") + if local_crc64 == remote_crc64: + logger.success("File size verification passed!") + return + # 发起流式请求 + with client.stream('GET', url, headers=headers) as response: + response.raise_for_status() + # 设置进度条 + progress_bar = tqdm( + total=file_size, + unit='iB', + unit_scale=True, + desc='Downloading' + ) + # 以二进制写模式打开文件 + with open(output_path, 'wb') as file: + # 分块下载,每次读取1MB + chunk_size = 1024 * 1024 # 1MB + downloaded_size = 0 + for chunk in response.iter_bytes(chunk_size=chunk_size): + if chunk: + file.write(chunk) + downloaded_size += len(chunk) + if on_progress_callback: + on_progress_callback(downloaded_size / file_size) + progress_bar.update(len(chunk)) + # 每下载100MB记录一次日志 + if downloaded_size % (100 * 1024 * 1024) == 0: + logger.info(f"Downloaded: {downloaded_size / (1024 * 1024 * 1024):.2f} GB") + + progress_bar.close() + + # 验证下载是否完成 + if os.path.exists(output_path): + final_size = os.path.getsize(output_path) + logger.info(f"Download completed successfully!") + logger.info(f"Final file size: {final_size / (1024 * 1024 * 1024):.2f} GB") + logger.info(f"File saved to: {os.path.abspath(output_path)}") + # 验证文件大小是否匹配 + if final_size == file_size: + logger.info("File size verification passed!") + else: + logger.warning(f"File size mismatch! Expected: {file_size}, Got: {final_size}") + except httpx.RequestError as e: + logger.error(f"An error occurred while requesting {url}: {str(e)}") + raise e + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error occurred: {str(e)}") + raise e + except Exception as e: + logger.error(f"An unexpected error occurred: {str(e)}") + raise e + finally: + if 'progress_bar' in locals(): + progress_bar.close() + + vod_client = vod_init() + modal_kv = modal.Dict.from_name(config.modal_kv_name, environment_name=config.environment, + create_if_missing=True) + fn_id = current_function_call_id() + + with sentry_sdk.continue_trace(environ_or_headers={"sentry-trace": sentry_trace.trace_id, + "baggage": sentry_trace.baggage, }) as transaction: + transaction.set_context("runtime_environment", { + "MODAL_CLOUD_PROVIDER": os.environ.get('MODAL_CLOUD_PROVIDER', 'unknown'), + "MODAL_ENVIRONMENT": os.environ.get('MODAL_ENVIRONMENT', 'unknown'), + "MODAL_IMAGE_ID": os.environ.get('MODAL_IMAGE_ID', 'unknown'), + "MODAL_IS_REMOTE": os.environ.get('MODAL_IS_REMOTE', 'unknown'), + "MODAL_REGION": os.environ.get('MODAL_REGION', 'unknown'), + "MODAL_TASK_ID": os.environ.get('MODAL_TASK_ID', 'unknown'), + "MODAL_IDENTITY_TOKEN": os.environ.get('MODAL_IDENTITY_TOKEN', 'unknown'), + }) + with transaction.start_child(name="收到缓存视频任务", op="queue.receive") as receive_span: + receive_span.set_data("messaging.message.id", fn_id) + receive_span.set_data("messaging.destination.name", "video-downloader.cache_submit") + receive_span.set_data("messaging.message.retry.count", 0) + receive_span.set_data("cache.key", media.cache_key) + with receive_span.start_child(name="处理缓存视频任务", op="queue.process") as process_span: + process_span.set_data("messaging.message.id", fn_id) + process_span.set_data("messaging.destination.name", "video-downloader.cache_submit") + process_span.set_data("messaging.message.retry.count", 0) + process_span.set_data("cache.key", media.cache_key) + volume_cache_path = None + match media.protocol: + case MediaProtocol.vod: + media_cache_downloading = MediaCache(status=MediaCacheStatus.downloading, + downloader_id=fn_id) + + def on_progress_callback(progress: float): + media_cache_downloading.progress = progress + caches_data = {media.cache_key: media_cache_downloading} + modal_kv.put(media.cache_key, media_cache_downloading.model_dump_json()) + batch_update_cloudflare_kv(caches_data) + + try: + volume_cache_path = vod_download(media) + process_span.set_status("success") + except Exception as e: + logger.error(f"An unexpected error occurred: {str(e)}") + media_cache_downloading.status = MediaCacheStatus.failed + caches = {f"{media.cache_key}": media_cache_downloading} + modal_kv.put(media.cache_key, media_cache_downloading) + batch_update_cloudflare_kv(caches) + process_span.set_status("failed") + case _: + process_span.set_status("failed") + raise NotImplementedError("protocol not yet supported") + media_cache_ready = MediaCache( + downloader_id=fn_id, + cache_filepath=volume_cache_path, + status=MediaCacheStatus.ready if volume_cache_path else MediaCacheStatus.failed, + progress=1 if volume_cache_path else 0, + expired_at=datetime.now(UTC) + timedelta(days=7) if volume_cache_path else None, ) + modal_kv.put(media.cache_key, media_cache_ready.model_dump_json()) + batch_update_cloudflare_kv({media.cache_key: media_cache_ready}) + return media_cache_ready + + + @worker_app.function(cpu=1, timeout=300, + max_containers=config.video_downloader_concurrency, + volumes={ + "/mntS3": modal.CloudBucketMount( + bucket_name=config.s3_bucket_name, + secret=modal.Secret.from_name("aws-s3-secret", environment_name=config.environment), + ), + }) + @modal.concurrent(max_inputs=10) + async def cache_delete(cache: MediaCache) -> MediaCache: + sentry_sdk.add_breadcrumb({ + "MODAL_CLOUD_PROVIDER": os.environ.get('MODAL_CLOUD_PROVIDER', 'unknown'), + "MODAL_ENVIRONMENT": os.environ.get('MODAL_ENVIRONMENT', 'unknown'), + "MODAL_IMAGE_ID": os.environ.get('MODAL_IMAGE_ID', 'unknown'), + "MODAL_IS_REMOTE": os.environ.get('MODAL_IS_REMOTE', 'unknown'), + "MODAL_REGION": os.environ.get('MODAL_REGION', 'unknown'), + "MODAL_TASK_ID": os.environ.get('MODAL_TASK_ID', 'unknown'), + "MODAL_IDENTITY_TOKEN": os.environ.get('MODAL_IDENTITY_TOKEN', 'unknown'), + }) + if os.path.exists(cache.cache_filepath): + os.remove(cache.cache_filepath) + cache.status = MediaCacheStatus.deleted + else: + cache.status = MediaCacheStatus.missing + return cache diff --git a/src/cluster/web/__init__.py b/src/cluster/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cluster/web/model.py b/src/cluster/web/model.py new file mode 100644 index 0000000..727d632 --- /dev/null +++ b/src/cluster/web/model.py @@ -0,0 +1,49 @@ +from enum import Enum +from typing import List, Union, Any, Optional + +from pydantic import BaseModel, Field, field_validator + +from src.cluster.ffmpeg_worker.model import FFMpegSliceSegment +from src.cluster.video_downloader.model import MediaSource + + +class SentryTransactionInfo(BaseModel): + trace_id: str = Field(description="Sentry Transaction ID") + baggage: str = Field(description="Sentry Transaction baggage") + + +class FFMPEGSliceRequest(BaseModel): + media: MediaSource = Field(description="FFMPEG Slice Media Source") + markers: List[FFMpegSliceSegment] = Field(description="FFMPEG Slice Marker List") + + @field_validator('media', mode='before') + @classmethod + def parse_inputs(cls, v: Union[str, MediaSource]): + if isinstance(v, str): + return MediaSource.from_str(v) + elif isinstance(v, MediaSource): + return v + else: + raise TypeError(v) + + +class FFMPEGSliceResponse(BaseModel): + success: bool = Field(description="任务创建成功与否") + taskId: str = Field(description="任务Id") + + +class FFMPEGSliceTaskStatusRequest(BaseModel): + taskId: str = Field(description="任务Id") + + +class TaskStatus(str, Enum): + running = "running" + failed = "failed" + success = "success" + expired = "expired" + + +class FFMPEGSliceTaskStatusResponse(BaseModel): + taskId: str = Field(description="任务Id") + status: TaskStatus = Field(description="任务运行状态") + result: Optional[List[str]] = Field(default=None, description="任务运行结果") diff --git a/src/cluster/web/worker.py b/src/cluster/web/worker.py new file mode 100644 index 0000000..0ebb15d --- /dev/null +++ b/src/cluster/web/worker.py @@ -0,0 +1,281 @@ +import modal +from src.cluster.config import config + +fastapi_image = ( + modal.Image + .debian_slim(python_version="3.11") + .pip_install("fastapi[standard]", "sentry-sdk[fastapi]", + 'loguru', 'pydantic', 'pydantic_settings', 'scalar-fastapi') + .add_local_python_source("src.cluster.video_downloader.model", copy=True) + .add_local_python_source("src.cluster.web.model", copy=True) + .add_local_python_source("src.cluster.config", copy=True) +) + +fastapi_app = modal.App("video-downloader-web", image=fastapi_image) + +with fastapi_image.imports(): + @fastapi_app.function(scaledown_window=60, + secrets=[ + modal.Secret.from_name("cf-kv-secret", environment_name=config.environment), + ]) + @modal.concurrent(max_inputs=100) + @modal.asgi_app() + def fastapi_webapp(): + import os + import httpx + from typing import Dict, List + from modal import current_function_call_id + from loguru import logger + from fastapi import FastAPI, Request, Depends + from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + from fastapi.responses import JSONResponse + from fastapi.exceptions import HTTPException + from fastapi.encoders import jsonable_encoder + from starlette import status + from scalar_fastapi import get_scalar_api_reference + import sentry_sdk + from sentry_sdk.integrations.loguru import LoguruIntegration + from sentry_sdk.integrations.loguru import LoggingLevels + from sentry_sdk.integrations.fastapi import FastApiIntegration + from src.cluster.video_downloader.model import MediaSources, MediaCacheStatus, MediaCache, CacheResult + from src.cluster.web.model import SentryTransactionInfo, FFMPEGSliceRequest, FFMPEGSliceResponse, \ + FFMPEGSliceTaskStatusRequest, FFMPEGSliceTaskStatusResponse, TaskStatus + + bearer_scheme = HTTPBearer() + + web_app = FastAPI(title="Modal worker API", + summary="Modal Worker的API, 包括缓存视频, 发起生产任务等", + servers=[ + {'url': 'https://bowongai-dev--video-downloader-fastapi-webapp.modal.run', + 'description': 'modal dev环境测试服务'}]) + + async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)): + token = credentials.credentials + # 在这里实现你的token验证逻辑 + if not is_valid_token(token): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return token + + def is_valid_token(token: str) -> bool: + # 这里实现具体的token验证逻辑 + # 例如:验证JWT token,检查数据库中的token等 + return token == "bowong7777" + + sentry_sdk.init(dsn="https://dab7b7ae652216282c89f029a76bb10a@sentry.bowongai.com/2", + send_default_pii=True, + traces_sample_rate=1.0, + profiles_sample_rate=1.0, + add_full_stack=True, + environment=config.environment, + integrations=[ + LoguruIntegration(level=LoggingLevels.INFO.value, event_level=LoggingLevels.ERROR.value), + FastApiIntegration() + ] + ) + media_cache_kv = modal.Dict.from_name('media-cache', environment_name=config.environment, + create_if_missing=True) + cf_account_id = os.environ.get("CF_ACCOUNT_ID") + cf_kv_api_token = os.environ.get("CF_KV_API_TOKEN") + cf_kv_namespace_id = os.environ.get("CF_KV_NAMESPACE_ID") + + @sentry_sdk.trace + def batch_update_cloudflare_kv(caches: Dict[str, MediaCache]): + with httpx.Client() as client: + try: + response = client.put( + f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/storage/kv/namespaces/{cf_kv_namespace_id}/bulk", + headers={"Authorization": f"Bearer {cf_kv_api_token}"}, + json=[ + { + "based64": False, + "key": mediaKey, + "value": cache_data.model_dump_json(), + } + for (mediaKey, cache_data) in caches.items() + ] + ) + response.raise_for_status() + except httpx.RequestError as e: + logger.error(f"An error occurred while put kv to cloudflare") + raise e + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error occurred while get kv from cloudflare {str(e)}") + raise e + except Exception as e: + logger.error(f"An unexpected error occurred: {str(e)}") + raise e + + @sentry_sdk.trace + def batch_remove_cloudflare_kv(keys: List[str]): + with httpx.Client() as client: + try: + response = client.post( + f"https://api.cloudflare.com/client/v4/accounts/{cf_account_id}/storage/kv/namespaces/{cf_kv_namespace_id}/bulk/delete", + headers={"Authorization": f"Bearer {cf_kv_api_token}"}, + json=keys + ) + response.raise_for_status() + except httpx.RequestError as e: + logger.error(f"An error occurred while put kv to cloudflare") + raise e + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error occurred while get kv from cloudflare {str(e)}") + raise e + except Exception as e: + logger.error(f"An unexpected error occurred: {str(e)}") + raise e + + @web_app.get("/scalar", include_in_schema=False) + async def scalar(): + return get_scalar_api_reference(openapi_url=web_app.openapi_schema, title="Modal worker web endpoint") + + @web_app.post("/cache", + summary="缓存视频文件", + description="异步缓存视频文件到S3存储桶和Modal Dict(KV)", + dependencies=[Depends(verify_token)]) + @sentry_sdk.trace + async def cache(medias: MediaSources) -> CacheResult: + fn_id = current_function_call_id() + caches: Dict[str, MediaCache] = {} + parent = sentry_sdk.get_current_span() + sentry_trace = SentryTransactionInfo(trace_id=sentry_sdk.get_traceparent(), + baggage=sentry_sdk.get_baggage()) + for media in medias.inputs: + with parent.start_child(name="同步视频缓存", op="cache.get") as cache_span: + cache_span.set_data("runner_id", fn_id) + cache_span.set_data("cache.key", [media.cache_key]) + video_cache_status_json = media_cache_kv.get(media.cache_key) + video_cache: MediaCache + cache_hit: bool = False + + if not video_cache_status_json: + # start new download task + with cache_span.start_child(name="视频缓存任务入队", + op="queue.publish") as queue_publish_span: + fn = modal.Function.from_name('video-downloader', 'cache_submit') + fn_task = fn.spawn(media, sentry_trace) + queue_publish_span.set_data("cache.key", media.cache_key) + queue_publish_span.set_data("messaging.message.id", fn_task.object_id) + queue_publish_span.set_data("messaging.destination.name", + "video-downloader.cache_submit") + queue_publish_span.set_data("messaging.message.body.size", 0) + video_cache = MediaCache(status=MediaCacheStatus.downloading, + downloader_id=fn_task.object_id) + video_cache_status_json = video_cache.model_dump_json() + media_cache_kv.put(media.cache_key, video_cache_status_json) + + video_cache = MediaCache.model_validate_json(video_cache_status_json) + match video_cache.status: + case MediaCacheStatus.ready: + cache_hit = True + case MediaCacheStatus.downloading: # 下载任务已经在进行 + cache_hit = True + case _: + # start new download task + with cache_span.start_child(name="视频缓存任务入队", + op="queue.publish") as queue_publish_span: + fn = modal.Function.from_name('video-downloader', 'cache_submit') + fn_task = fn.spawn(media, sentry_trace) + queue_publish_span.set_data("cache.key", media.cache_key) + queue_publish_span.set_data("messaging.message.id", fn_task.object_id) + queue_publish_span.set_data("messaging.destination.name", + "video-downloader.cache_submit") + queue_publish_span.set_data("messaging.message.body.size", 0) + video_cache = MediaCache(status=MediaCacheStatus.downloading, + downloader_id=fn_task.object_id) + video_cache_status_json = video_cache.model_dump_json() + media_cache_kv.put(media.cache_key, video_cache_status_json) + + caches[media.cache_key] = video_cache + logger.info(f"Media cache hit ? {cache_hit}") + cache_span.set_data("cache.hit", cache_hit) + batch_update_cloudflare_kv(caches) + return CacheResult(caches=caches) + # return JSONResponse(content={"caches": jsonable_encoder(caches)}) + + @web_app.delete("/cache/kv", + summary="清除KV记录", + description="清除当前环境下KV缓存过的所有数据(S3存储桶内的文件会保留)", + dependencies=[Depends(verify_token)]) + async def purge_kv_all(): + parent = sentry_sdk.get_current_span() + span = parent.start_child(name="清除缓存KV", op="purge.flush") + media_cache_kv.clear() + span.set_data("cache.success", True) + span.finish() + return JSONResponse(content={"success": True}) + + @web_app.post("/cache/kv", + summary="删除对应的KV记录", + description="删除请求中对应的视频缓存记录", + dependencies=[Depends(verify_token)]) + async def purge_kv(medias: MediaSources): + try: + for media in medias.inputs: + media_cache_kv.pop(media.cache_key) + keys = [media.cache_key for media in medias.inputs] + batch_remove_cloudflare_kv(keys) + return JSONResponse(content={"success": True, "keys": keys}) + except Exception as e: + return JSONResponse(content={"success": False, "error": str(e)}) + + @web_app.post("/cache/media", + summary="清除指定的所有缓存", + description="清除指定的所有缓存(包括KV记录和S3存储文件)", + dependencies=[Depends(verify_token)]) + async def purge_media(medias: MediaSources): + caches: Dict[str, MediaCache] = {} + for media in medias.inputs: + try: + cache_data_json = media_cache_kv.pop(media.cache_key) + cache_data = MediaCache.model_validate_json(cache_data_json) + fn = modal.Function.from_name("video-downloader", "cache_delete", + environment_name=config.environment) + deleted_cache = await fn.remote.aio(cache_data) + caches[media.cache_key] = deleted_cache + except KeyError: + logger.warning("cache key not found") + caches[media.cache_key] = MediaCache(status=MediaCacheStatus.missing) + continue + keys = [key for key in caches.keys()] + batch_remove_cloudflare_kv(keys) + return JSONResponse(content={"success": True, "keys": keys}) + + @web_app.post("/merge", + summary="发起合成任务", + description="开发中", + dependencies=[Depends(verify_token)]) + async def merge_media(request: Request): + body_json = await request.json() + return JSONResponse(content={"success": False, "message": "Not Implemented"}) + + @web_app.post("/ffmpeg/slice", summary="发起切割任务", description="依据打点信息切出多个片段", + dependencies=[Depends(verify_token)]) + async def slice_media(request: FFMPEGSliceRequest): + fn = modal.Function.from_name("video-downloader", "ffmpeg_slice_media", environment_name=config.environment) + fn_call = fn.spawn(request.media, request.markers) + return FFMPEGSliceResponse(success=True, taskId=fn_call.object_id) + + @web_app.get("/ffmpeg/slice/{task_id}", summary="查询切割任务状态/结果", description="根据任务Id查询运行状态", + dependencies=[Depends(verify_token)]) + async def slice_media(task_id: str): + try: + fn_task = modal.FunctionCall.from_id(task_id) + try: + results: List[str] = fn_task.get(timeout=2) + return FFMPEGSliceTaskStatusResponse(taskId=task_id, status=TaskStatus.success, result=results) + except modal.exception.OutputExpiredError: + return FFMPEGSliceTaskStatusResponse(taskId=task_id, status=TaskStatus.expired) + except TimeoutError: + return FFMPEGSliceTaskStatusResponse(taskId=task_id, status=TaskStatus.running) + except Exception as e: + logger.error(e) + return FFMPEGSliceTaskStatusResponse(taskId=task_id, status=TaskStatus.failed) + except Exception as e: + return JSONResponse(content={"success": False, "message": "任务Id不存在"}, status_code=400) + + return web_app diff --git a/src/deploy.py b/src/deploy.py new file mode 100644 index 0000000..75a9da1 --- /dev/null +++ b/src/deploy.py @@ -0,0 +1,5 @@ +import modal.cli.run +from cluster.config import config + +if __name__ == '__main__': + modal.cli.run.deploy(app_ref='.\\src\\cluster\\app.py', env=config.environment, use_module_mode=False) diff --git a/upload.py b/upload.py deleted file mode 100644 index 6674c45..0000000 --- a/upload.py +++ /dev/null @@ -1,8 +0,0 @@ -import modal - -vol = modal.Volume.from_name("comfyui-model") - -with vol.batch_upload() as batch: - # batch.put_directory("D:\ComfyUI-aki-v1.6\ComfyUI\custom_nodes\ComfyUI-LatentSync-Node\checkpoints", "ComfyUI-LatentSync-Node/checkpoints") - # batch.put_directory("D:\ComfyUI-aki-v1.6\ComfyUI\custom_nodes\ComfyUI-CustomNode\model", "ComfyUI-CustomNode/model") - batch.put_directory(r"D:\ComfyUI-aki-v1.6\ComfyUI\custom_nodes\CosyVoice-ComfyUI\pretrained_models\CosyVoice-300M-SFT","CosyVoice-ComfyUI/pretrained_models/CosyVoice-300M-SFT") \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fd0dcff --- /dev/null +++ b/uv.lock @@ -0,0 +1,571 @@ +version = 1 +revision = 1 +requires-python = ">=3.11" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "boto3" +version = "1.37.37" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/8c/2ca661db6c9e591d9dc46149b43a91385283c852436ccba62e199643e196/boto3-1.37.37.tar.gz", hash = "sha256:752d31105a45e3e01c8c68471db14ae439990b75a35e72b591ca528e2575b28f", size = 111666 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/5f/032d93e74949222ffbfbc3270f29a3ee423fe648de8a31c49cce0cbb0a09/boto3-1.37.37-py3-none-any.whl", hash = "sha256:d125cb11e22817f7a2581bade4bf7b75247b401888890239ceb5d3e902ccaf38", size = 139917 }, +] + +[[package]] +name = "botocore" +version = "1.37.37" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d0/70969515e3ae8ff0fcccf22827d5d131bc7b8729331127415cf8f2861d63/botocore-1.37.37.tar.gz", hash = "sha256:3eadde6fed95c4cb469cc39d1c3558528b7fa76d23e7e16d4bddc77250431a64", size = 13828530 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/17/602915b29cb695e1e66f65e33b1026f1534e49975d99ea4e32e58d963542/botocore-1.37.37-py3-none-any.whl", hash = "sha256:eb730ff978f47c02f0c8ed07bccdc0db6d8fa098ed32ac31bee1da0e9be480d1", size = 13495584 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cos-python-sdk-v5" +version = "1.9.36" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "crcmod" }, + { name = "pycryptodome" }, + { name = "requests" }, + { name = "six" }, + { name = "xmltodict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/29/9ff39438a3f749f291ddc41bc34a3932184665efe4f3f96c69f5743b9d97/cos_python_sdk_v5-1.9.36.tar.gz", hash = "sha256:789070f54be11b211a5ba9c0b00c88ec959abd678297dd0e5ffcd6d4d15eb472", size = 96589 } + +[[package]] +name = "crcmod" +version = "1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670 } + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, +] + +[[package]] +name = "modaldeploy" +version = "0.2.0" +source = { virtual = "." } +dependencies = [ + { name = "boto3" }, + { name = "cos-python-sdk-v5" }, + { name = "crcmod" }, + { name = "httpx" }, + { name = "loguru" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-ffmpeg" }, + { name = "requests" }, + { name = "scalar-fastapi" }, + { name = "sentry-sdk", extra = ["loguru"] }, + { name = "tencentcloud-sdk-python-common" }, + { name = "tencentcloud-sdk-python-vod" }, + { name = "tqdm" }, + { name = "webvtt-py" }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", specifier = ">=1.37.37" }, + { name = "cos-python-sdk-v5", specifier = ">=1.9.36" }, + { name = "crcmod", specifier = ">=1.7" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "loguru", specifier = ">=0.7.3" }, + { name = "pydantic", specifier = ">=2.11.3" }, + { name = "pydantic-settings", specifier = ">=2.9.1" }, + { name = "python-ffmpeg", specifier = ">=2.0.12" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "scalar-fastapi", specifier = ">=1.0.3" }, + { name = "sentry-sdk", extras = ["loguru"], specifier = ">=2.26.1" }, + { name = "tencentcloud-sdk-python-common", specifier = ">=3.0.1363" }, + { name = "tencentcloud-sdk-python-vod", specifier = ">=3.0.1363" }, + { name = "tqdm", specifier = ">=4.67.1" }, + { name = "webvtt-py", specifier = ">=0.5.1" }, +] + +[[package]] +name = "pycryptodome" +version = "3.22.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/e6/099310419df5ada522ff34ffc2f1a48a11b37fc6a76f51a6854c182dbd3e/pycryptodome-3.22.0.tar.gz", hash = "sha256:fd7ab568b3ad7b77c908d7c3f7e167ec5a8f035c64ff74f10d47a4edd043d723", size = 4917300 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/65/a05831c3e4bcd1bf6c2a034e399f74b3d6f30bb4e37e36b9c310c09dc8c0/pycryptodome-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:009e1c80eea42401a5bd5983c4bab8d516aef22e014a4705622e24e6d9d703c6", size = 2490637 }, + { url = "https://files.pythonhosted.org/packages/5c/76/ff3c2e7a60d17c080c4c6120ebaf60f38717cd387e77f84da4dcf7f64ff0/pycryptodome-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b76fa80daeff9519d7e9f6d9e40708f2fce36b9295a847f00624a08293f4f00", size = 1635372 }, + { url = "https://files.pythonhosted.org/packages/cc/7f/cc5d6da0dbc36acd978d80a72b228e33aadaec9c4f91c93221166d8bdc05/pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a31fa5914b255ab62aac9265654292ce0404f6b66540a065f538466474baedbc", size = 2177456 }, + { url = "https://files.pythonhosted.org/packages/92/65/35f5063e68790602d892ad36e35ac723147232a9084d1999630045c34593/pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0092fd476701eeeb04df5cc509d8b739fa381583cda6a46ff0a60639b7cd70d", size = 2263744 }, + { url = "https://files.pythonhosted.org/packages/cc/67/46acdd35b1081c3dbc72dc466b1b95b80d2f64cad3520f994a9b6c5c7d00/pycryptodome-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d5b0ddc7cf69231736d778bd3ae2b3efb681ae33b64b0c92fb4626bb48bb89", size = 2303356 }, + { url = "https://files.pythonhosted.org/packages/3d/f9/a4f8a83384626098e3f55664519bec113002b9ef751887086ae63a53135a/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f6cf6aa36fcf463e622d2165a5ad9963b2762bebae2f632d719dfb8544903cf5", size = 2176714 }, + { url = "https://files.pythonhosted.org/packages/88/65/e5f8c3a885f70a6e05c84844cd5542120576f4369158946e8cfc623a464d/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec7b40a7ea5af7c40f8837adf20a137d5e11a6eb202cde7e588a48fb2d871a8", size = 2337329 }, + { url = "https://files.pythonhosted.org/packages/b8/2a/25e0be2b509c28375c7f75c7e8d8d060773f2cce4856a1654276e3202339/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772", size = 2262255 }, + { url = "https://files.pythonhosted.org/packages/41/58/60917bc4bbd91712e53ce04daf237a74a0ad731383a01288130672994328/pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb", size = 1763403 }, + { url = "https://files.pythonhosted.org/packages/55/f4/244c621afcf7867e23f63cfd7a9630f14cfe946c9be7e566af6c3915bcde/pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627", size = 1794568 }, +] + +[[package]] +name = "pydantic" +version = "2.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, + { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, + { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, + { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, + { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, + { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, + { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, + { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, + { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, + { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, + { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, + { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, + { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, + { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, + { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, + { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, + { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, + { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, + { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, + { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, + { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, + { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, + { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, + { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, + { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, + { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, + { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, + { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, + { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, + { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, + { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, + { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, + { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, + { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, + { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, + { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, + { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, + { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, + { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, + { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, +] + +[[package]] +name = "pyee" +version = "13.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730 }, +] + +[[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 } +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 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, +] + +[[package]] +name = "python-ffmpeg" +version = "2.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyee" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/4d/7ecffb341d646e016be76e36f5a42cb32f409c9ca21a57b68f067fad3fc7/python_ffmpeg-2.0.12.tar.gz", hash = "sha256:19ac80af5a064a2f53c245af1a909b2d7648ea045500d96d3bcd507b88d43dc7", size = 14126292 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/6d/02e817aec661defe148cb9eb0c4eca2444846305f625c2243fb9f92a9045/python_ffmpeg-2.0.12-py3-none-any.whl", hash = "sha256:d86697da8dfb39335183e336d31baf42fb217468adf5ac97fd743898240faae3", size = 14411 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "s3transfer" +version = "0.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/2b/5c9562795c2eb2b5f63536961754760c25bf0f34af93d36aa28dea2fb303/s3transfer-0.11.5.tar.gz", hash = "sha256:8c8aad92784779ab8688a61aefff3e28e9ebdce43142808eaa3f0b0f402f68b7", size = 149107 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/39/13402e323666d17850eca87e4cd6ecfcf9fd7809cac9efdcce10272fc29d/s3transfer-0.11.5-py3-none-any.whl", hash = "sha256:757af0f2ac150d3c75bc4177a32355c3862a98d20447b69a0161812992fe0bd4", size = 84782 }, +] + +[[package]] +name = "scalar-fastapi" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/04/08ae9e86f24cd9b43bc5b2068fc5d1dcd3e7efa6db860290734d7d742512/scalar_fastapi-1.0.3.tar.gz", hash = "sha256:9e9cb8398e298cd435a0171eebe1675b8899eb21e47c238db0d48783143f0ffb", size = 4107 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/9e/ca0ffc3fc4788c6e1367b96a70f2d6ee33f91d580664a8142ba7ee292ef2/scalar_fastapi-1.0.3-py3-none-any.whl", hash = "sha256:4a47a140795097ad034518ce0e32940f2c54f0f4bc60e4c3289ca30a7e6f954d", size = 4579 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/26/099631caa51abffb1fd9e08c2138bc6681d3f288a5936c2fc4e054729611/sentry_sdk-2.26.1.tar.gz", hash = "sha256:759e019c41551a21519a95e6cef6d91fb4af1054761923dadaee2e6eca9c02c7", size = 323099 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/32/0a30b4fafdb3d26d133f99bb566aaa6000004ee7f2c4b72aafea9237ab7e/sentry_sdk-2.26.1-py2.py3-none-any.whl", hash = "sha256:e99390e3f217d13ddcbaeaed08789f1ca614d663b345b9da42e35ad6b60d696a", size = 340558 }, +] + +[package.optional-dependencies] +loguru = [ + { name = "loguru" }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "tencentcloud-sdk-python-common" +version = "3.0.1363" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/05ad068ea5422b0ab11a08a2c2c7ed3e35c36ef1130aa01dfd53beca7ad8/tencentcloud-sdk-python-common-3.0.1363.tar.gz", hash = "sha256:c93df1475ea52ae16fb66c8b8eb5111145587e37dd81117def345b8545f7bc5f", size = 16906 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/67/f146b46fcc5e5fb5e609bd1053cbcdc60cf4327ab15c405f8ef15ba5afc3/tencentcloud_sdk_python_common-3.0.1363-py2.py3-none-any.whl", hash = "sha256:36e9c409b6fb0d8ca1323f3db0e97a8b97d1676eebf5f49e3b81cadec1732d46", size = 24710 }, +] + +[[package]] +name = "tencentcloud-sdk-python-vod" +version = "3.0.1363" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tencentcloud-sdk-python-common" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/60/4f80b7746da039d8d0bcaf52112c3ed22cef59b5fa20e7e5ca2d62f47d26/tencentcloud-sdk-python-vod-3.0.1363.tar.gz", hash = "sha256:f21af928e6bbc19b340a1f17ebe1f4dc0b80909bfe9735cf7fd58b6f13615f56", size = 291862 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/98/56239e5c4154de2ebaa1b44e26e48395506fe2cfad90e19659ef4bab1721/tencentcloud_sdk_python_vod-3.0.1363-py2.py3-none-any.whl", hash = "sha256:ab8a8c62ae1c332fc1b427c1354980d3c8afcfd63186a6d1afe3216b51dcfe1f", size = 298441 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + +[[package]] +name = "webvtt-py" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f6/7c9c964681fb148e0293e6860108d378e09ccab2218f9063fd3eb87f840a/webvtt-py-0.5.1.tar.gz", hash = "sha256:2040dd325277ddadc1e0c6cc66cbc4a1d9b6b49b24c57a0c3364374c3e8a3dc1", size = 55128 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/ed/aad7e0f5a462d679f7b4d2e0d8502c3096740c883b5bbed5103146480937/webvtt_py-0.5.1-py3-none-any.whl", hash = "sha256:9d517d286cfe7fc7825e9d4e2079647ce32f5678eb58e39ef544ffbb932610b7", size = 19802 }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, +] + +[[package]] +name = "xmltodict" +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/05/51dcca9a9bf5e1bce52582683ce50980bcadbc4fa5143b9f2b19ab99958f/xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", size = 51942 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/45/fc303eb433e8a2a271739c98e953728422fa61a3c1f36077a49e395c972e/xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac", size = 9981 }, +]