Modal worker统一python代码框架
This commit is contained in:
13
.env
Normal file
13
.env
Normal file
@@ -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=kfAqoOmIiyiywi25xaAkJUQbZ/EKDnzvI6NRCW1l
|
||||
|
||||
CF_KV_API_TOKEN=hAiXy3jB-Lt5q-XiIV588lyvyH1c8FJaG3oieqeT
|
||||
CF_KV_NAMESPACE_ID=f24d396e0daa418e89a1d7074b435c24
|
||||
CF_ACCOUNT_ID=67720b647ff2b55cf37ba3ef9e677083
|
||||
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="MySQL" uuid="0c96f6ba-5b9d-4b2d-ac3f-629fdfb28b12">
|
||||
<driver-ref>mysql.8</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:mysql://root:MrYRE^u9aU$n@sh-cynosdbmysql-grp-hofmpgvu.sql.tencentcdb.com:23314/local_merge</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -3,5 +3,5 @@
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.9 (pythonProject)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="ComfyUI-3.10" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (modalDeploy)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
2
.idea/modalDeploy.iml
generated
2
.idea/modalDeploy.iml
generated
@@ -4,7 +4,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="ComfyUI-3.10" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.11 (modalDeploy)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.11
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
```bash
|
||||
modal deploy -e dev ./src/cluster/app.py
|
||||
```
|
||||
80
deploy.py
80
deploy.py
@@ -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)
|
||||
12
heygem.py
12
heygem.py
@@ -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()
|
||||
|
||||
|
||||
|
||||
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
@@ -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",
|
||||
]
|
||||
@@ -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)}
|
||||
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/cluster/__init__.py
Normal file
0
src/cluster/__init__.py
Normal file
13
src/cluster/app.py
Normal file
13
src/cluster/app.py
Normal file
@@ -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)
|
||||
25
src/cluster/config.py
Normal file
25
src/cluster/config.py
Normal file
@@ -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"
|
||||
)
|
||||
12
src/cluster/ffmpeg_worker/Utils/PathUtils.py
Normal file
12
src/cluster/ffmpeg_worker/Utils/PathUtils.py
Normal file
@@ -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)
|
||||
16
src/cluster/ffmpeg_worker/Utils/TimeUtils.py
Normal file
16
src/cluster/ffmpeg_worker/Utils/TimeUtils.py
Normal file
@@ -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]
|
||||
0
src/cluster/ffmpeg_worker/Utils/__init__.py
Normal file
0
src/cluster/ffmpeg_worker/Utils/__init__.py
Normal file
0
src/cluster/ffmpeg_worker/__init__.py
Normal file
0
src/cluster/ffmpeg_worker/__init__.py
Normal file
65
src/cluster/ffmpeg_worker/model.py
Normal file
65
src/cluster/ffmpeg_worker/model.py
Normal file
@@ -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
|
||||
}
|
||||
132
src/cluster/ffmpeg_worker/worker.py
Normal file
132
src/cluster/ffmpeg_worker/worker.py
Normal file
@@ -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
|
||||
0
src/cluster/video_downloader/__init__.py
Normal file
0
src/cluster/video_downloader/__init__.py
Normal file
103
src/cluster/video_downloader/model.py
Normal file
103
src/cluster/video_downloader/model.py
Normal file
@@ -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")
|
||||
301
src/cluster/video_downloader/worker.py
Normal file
301
src/cluster/video_downloader/worker.py
Normal file
@@ -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
|
||||
0
src/cluster/web/__init__.py
Normal file
0
src/cluster/web/__init__.py
Normal file
49
src/cluster/web/model.py
Normal file
49
src/cluster/web/model.py
Normal file
@@ -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="任务运行结果")
|
||||
281
src/cluster/web/worker.py
Normal file
281
src/cluster/web/worker.py
Normal file
@@ -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
|
||||
5
src/deploy.py
Normal file
5
src/deploy.py
Normal file
@@ -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)
|
||||
@@ -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")
|
||||
571
uv.lock
generated
Normal file
571
uv.lock
generated
Normal file
@@ -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 },
|
||||
]
|
||||
Reference in New Issue
Block a user