Files
modalDeploy/src/cluster/video_apps/cache_submit.py
康宇佳 91ebad57c6 合并分支
* Merge branch 'main' into feature/modal-cluster-gemini
* ADD 增加大模型输出错误时间值的处理
* Merge branch 'main' into feature/modal-cluster-gemini
* FIX 修复推理问题
* Merge branch 'main' into feature/modal-cluster-gemini
* FIX 修复拼接网格图的异步失效问题
* FIX 修复拼接网格图的异步失效问题
* Merge branch 'main' into feature/modal-cluster-gemini
* Merge branch 'main' into feature/modal-cluster-gemini
* FIX 调高合成网格图容器配置
* Merge branch 'main' into feature/modal-cluster-gemini
* FIX 修复带图片模态推理接口
* PERF 优化Gemini Prompt词
* Merge remote-tracking branch 'origin/main' into feature/modal-cluster-gemini
* ADD 增加网格图片创建接口 ADD gemini推理打点增加商品封面模态 PERF gemini打点结果相邻时间线同商品合并
* Merge branch 'main' into feature/modal-cluster-gemini
* FIX 429重试机制
* FIX 修复Gemini推理函数入参
* Merge branch 'main' into feature/modal-cluster-gemini
* PERF gemini修改参数类型 ADD 增加批量删除Gemini文件接口
* PERF gemini修改prompt
* Merge branch 'feature/modal-cluster' into feature/modal-cluster-gemini
* PERF gemini修改prompt
* 录制HLS时 结果复制到S3挂载点增加一个fallback处理
* Merge branch 'feature/modal-cluster' into feature/modal-cluster-gemini
* PERF gemini修改prompt
* PERF gemini增加图像模态
* PERF gemini增加图像模态
* fix : TikHub的Logger换为Loguru
* fix : 修复直播切分视频接口
* Merge branch 'feature/modal-cluster' into feature/modal-cluster-gemini
* PERF tikhub接口增加随机延迟
* Merge remote-tracking branch 'origin/feature/modal-cluster' into feature/modal-cluster
* 合并分支
* fix : hls录制缓存读写问题
* FIX 修改冲突
* ADD 增加获取直播间指定商品数量接口
* Merge branch 'feature/modal-cluster' into feature/modal-cluster-gemini
* PERF 修改tikhub到aws
* fix : 增加s3分片上传接口
* PERF Gemini推理直接从参数列表获取商品列表 ADD Tikhub增加获取置顶|讲解中商品接口
* PERF Gemini推理直接从参数获取商品列表
* Merge branch 'feature/modal-cluster' into feature/modal-cluster-gemini
* fix 处理jpg图片的codec_name应为mjpg
* Merge branch 'feature/modal-cluster' into feature/modal-cluster-gemini
* PERF 优化返回值
* fix 获取媒体metadata的类型报错,modal client升级到1.0.2
* Merge branch 'feature/modal-cluster' into feature/modal-cluster-gemini
* PERF 风控后爬虫直接退出避免cookie失效 PERF 分离tikhub api, 使用更低的保持时间以更换IP FIX 修复Dict键不存在报错的bug
* fix 一些webhook相关的bug,添加了更详细的接口说明
* 更新直播录制为可跳转时间的hls流接口,用于替代掉腾讯VOD+火山云拉流转推,片段保证每片开头为关键帧,时间精度准确到毫秒级
* ADD 增加直播间商品增量缓存触发器
* Merge branch 'feature/modal-cluster' into feature/modal-cluster-gemini
* PERF 调整gemini prompt
* - KVCache类改为可拓展,基于环境变量设置KV space - test 环境配置与CF测试环境对齐
* FIX 修复Gemini推理视频切条的问题 PERF 优化视频切条函数性能
* ADD 基本完成gemini视频推理API
* ADD 添加内联的tikhub
* 对齐预发环境

---------

Merge request URL: https://g-ldyi2063.coding.net/p/dev/d/modalDeploy/git/merge/4802
Co-authored-by: 康宇佳,shuohigh@gmail.com
2025-06-13 17:15:06 +08:00

218 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import modal
from ..video import downloader_image, app, config, batch_update_cloudflare_kv, modal_kv_cache
with downloader_image.imports():
import os, httpx, crcmod
import sentry_sdk
from tqdm import tqdm
from typing import Tuple
from loguru import logger
from datetime import datetime, UTC, timedelta
from modal import current_function_call_id
from tencentcloud.common.credential import Credential
from tencentcloud.vod.v20180717.vod_client import VodClient
from tencentcloud.vod.v20180717 import models as vod_request_models
from BowongModalFunctions.models.media_model import MediaSource, MediaCacheStatus, MediaProtocol
from BowongModalFunctions.models.web_model import SentryTransactionInfo
@app.function(cpu=1, timeout=1800,
cloud="aws",
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.modal_environment),
),
},
secrets=[modal.Secret.from_name("tencent-cloud-secret", environment_name=config.modal_environment)])
@modal.concurrent(max_inputs=10)
async def cache_submit(media: MediaSource, sentry_trace: SentryTransactionInfo) -> MediaSource:
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)
# 兼容fileId带文件类型的格式和不带文件类型的格式
request.FileIds = [media.path.split('.')[0] if '.' in media.path else media.path]
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"{config.S3_mount_dir}/{media.protocol.value}/{media.endpoint}/{media.bucket}"
cache_file = media.path if '.' in media.path else f"{media.path}.{file_extension}"
return cache_dir, cache_file, media_info.MediaUrl
else:
raise FileNotFoundError(
f"FileId : {media.path} 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, protocol: MediaProtocol = MediaProtocol.vod,
on_progress_callback: callable(float) = None) -> None:
# 配置日志
logger.info(f"Starting download from {url}")
try:
# 使用 httpx 发送 HEAD 请求获取文件大小
# 设置请求头,支持断点续传
headers = {'Range': 'bytes=0-'}
with httpx.Client() as client:
match protocol:
case MediaProtocol.vod:
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")
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
os.makedirs(os.path.dirname(output_path), exist_ok=True)
logger.info(f"Downloading {url}...")
# 发起流式请求
with client.stream('GET', url, headers=headers) as response:
response.raise_for_status()
file_size = int(response.headers.get('content-length', 0))
# 设置进度条
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_kv_cache
fn_id = current_function_call_id()
with sentry_sdk.continue_trace(environ_or_headers={"sentry-trace": sentry_trace.x_trace_id,
"baggage": sentry_trace.x_baggage, }) as transaction:
transaction.set_context("runtime_environment", {
"MODAL_CLOUD_PROVIDER": os.environ.get('MODAL_CLOUD_PROVIDER', 'unknown'),
"MODAL_ENVIRONMENT": config.modal_environment,
"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.urn)
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.urn)
volume_cache_path = None
match media.protocol:
case MediaProtocol.vod:
try:
volume_cache_path = vod_download(media)
process_span.set_status("success")
except Exception as e:
logger.exception(e)
media.status = MediaCacheStatus.failed
modal_kv.set_cache(media)
batch_update_cloudflare_kv([media])
process_span.set_status("failed")
raise e
case MediaProtocol.http:
try:
cache_filepath = f"{config.S3_mount_dir}/{media.protocol.value}/{media.cache_filepath}"
os.makedirs(os.path.dirname(cache_filepath), exist_ok=True)
download_large_file(url=media.__str__(), output_path=cache_filepath)
except Exception as e:
logger.exception(e)
media.status = MediaCacheStatus.failed
modal_kv.set_cache(media)
batch_update_cloudflare_kv([media])
process_span.set_status("failed")
raise e
case MediaProtocol.s3:
# 本地挂载缓存
if media.protocol == MediaProtocol.s3 and media.endpoint == config.S3_region and media.bucket == config.S3_bucket_name:
volume_cache_path = f"{config.S3_mount_dir}/{media.cache_filepath}"
else:
logger.error("protocol not yet supported")
case _:
process_span.set_status("failed")
raise NotImplementedError("protocol not yet supported")
media.downloader_id = fn_id
media.status = MediaCacheStatus.ready if volume_cache_path else MediaCacheStatus.failed
media.progress = 1 if volume_cache_path else 0
media.expired_at = datetime.now(UTC) + timedelta(days=7) if volume_cache_path else None
modal_kv.set_cache(media)
batch_update_cloudflare_kv([media])
return media