Spaces:
Sleeping
Sleeping
testcoder-ui
commited on
Commit
·
d9add37
1
Parent(s):
d1c1f8f
Add S3 support and simplify README
Browse files- README.md +41 -63
- app.py +41 -10
- pollo_service_single.py +24 -15
- requirements.txt +1 -0
- s3_utils.py +179 -0
README.md
CHANGED
|
@@ -7,89 +7,67 @@ sdk: gradio
|
|
| 7 |
sdk_version: 6.2.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
# 🎬 Video Model Evaluator
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
## ✨
|
| 17 |
|
| 18 |
-
- 🔐
|
| 19 |
-
- 📊
|
| 20 |
-
- 🚀
|
| 21 |
-
- 💾
|
| 22 |
-
-
|
| 23 |
-
|
| 24 |
-
## 🛠️ 技术栈
|
| 25 |
-
|
| 26 |
-
- **Gradio**: Web 界面框架
|
| 27 |
-
- **Hugging Face Hub**: 数据存储和用户认证
|
| 28 |
-
- **视频生成 API**: 调用多个视频生成模型
|
| 29 |
-
- **Python**: 后端逻辑
|
| 30 |
|
| 31 |
## 📋 环境变量配置
|
| 32 |
|
| 33 |
在 Space Settings > Variables and secrets 中配置:
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|--------|------|------|
|
| 37 |
-
| `API_KEY` | 视频生成服务的 API 密钥 | ✅ |
|
| 38 |
-
| `HF_TOKEN` | HF Write Token(用于写入 Dataset) | ✅ |
|
| 39 |
-
| `DATASET_REPO_ID` | Private Dataset 名称(格式:`username/dataset-name`) | ✅ |
|
| 40 |
-
| `PUBLIC_DOMAIN` | Space 公网地址(用于图片 URL) | ⚠️ |
|
| 41 |
-
|
| 42 |
-
## 🚀 使用说明
|
| 43 |
-
|
| 44 |
-
1. **登录**: 使用 Hugging Face 账户登录
|
| 45 |
-
2. **上传图片**: 上传输入图片
|
| 46 |
-
3. **输入提示词**: 描述你希望视频展现的内容
|
| 47 |
-
4. **生成视频**: 系统会调用多个模型同时生成视频
|
| 48 |
-
5. **评分**: 对每个模型的生成结果进行评分(0-10分)
|
| 49 |
-
6. **提交**: 提交评分,数据自动保存到 Private Dataset
|
| 50 |
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
| 54 |
-
- **Seedance Pro** - ByteDance Seedance Pro
|
| 55 |
-
- **Veo 3.1** - Google Veo 3.1
|
| 56 |
-
- **Kling 2.6** - Kling AI 2.6
|
| 57 |
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
- **Private Dataset (100GB)**: $0(免费额度足够)
|
| 62 |
-
- **视频生成 API**: 取决于 API 定价
|
| 63 |
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
##
|
| 67 |
|
| 68 |
-
|
| 69 |
-
-
|
| 70 |
-
-
|
| 71 |
-
-
|
| 72 |
-
- 各模型评分
|
| 73 |
-
- 视频 URL
|
| 74 |
-
- 模型结果
|
| 75 |
|
| 76 |
-
##
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
4. 确保已配置所有必需的环境变量
|
| 82 |
|
| 83 |
## 🔧 故障排除
|
| 84 |
|
| 85 |
-
|
| 86 |
-
- 检查
|
| 87 |
-
|
| 88 |
-
### 无法保存评分
|
| 89 |
-
- 检查 `HF_TOKEN` 是否正确
|
| 90 |
-
- 确认 `DATASET_REPO_ID` 格式正确
|
| 91 |
-
- 确认 Token 有 Write 权限
|
| 92 |
-
|
| 93 |
-
### API 调用失败
|
| 94 |
-
- 检查 `API_KEY` 是否正确
|
| 95 |
-
- 查看 Space 日志了解详细错误
|
|
|
|
| 7 |
sdk_version: 6.2.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
hf_oauth: true
|
| 11 |
---
|
| 12 |
|
| 13 |
# 🎬 Video Model Evaluator
|
| 14 |
|
| 15 |
+
视频生成模型评估系统 - 支持 Prompt、模型、视频的评估和评分。
|
| 16 |
|
| 17 |
+
## ✨ 功能
|
| 18 |
|
| 19 |
+
- 🔐 强制登录(HF OAuth)
|
| 20 |
+
- 📊 次数限制(每天 4 次)
|
| 21 |
+
- 🚀 4 个模型同时生成(Sora 2 pro, Seedance Pro, Veo 3.1, Kling 2.6)
|
| 22 |
+
- 💾 评分数据保存到 Private Dataset
|
| 23 |
+
- ☁️ 图片和视频自动上传到 S3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
## 📋 环境变量配置
|
| 26 |
|
| 27 |
在 Space Settings > Variables and secrets 中配置:
|
| 28 |
|
| 29 |
+
### Secrets(敏感信息)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
| 变量名 | 说明 |
|
| 32 |
+
|--------|------|
|
| 33 |
+
| `API_KEY` | 视频生成 API 密钥 |
|
| 34 |
+
| `HF_TOKEN` | HF Write Token |
|
| 35 |
+
| `AWS_ACCESS_KEY_ID` | AWS Access Key ID |
|
| 36 |
+
| `AWS_SECRET_ACCESS_KEY` | AWS Secret Access Key |
|
| 37 |
|
| 38 |
+
### Variables(非敏感信息)
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
| 变量名 | 说明 | 示例 |
|
| 41 |
+
|--------|------|------|
|
| 42 |
+
| `DATASET_REPO_ID` | Private Dataset 名称 | `learnmlf/video-evaluations` |
|
| 43 |
+
| `S3_BUCKET_NAME` | S3 Bucket 名称 | `cuti-agent-assets-dev-699475938168-ap-southeast-2` |
|
| 44 |
+
| `CDN_DOMAIN` | CDN 域名 | `https://cdn-dev.newai.land` |
|
| 45 |
+
| `AWS_REGION` | AWS 区域(可选) | `ap-southeast-2` |
|
| 46 |
|
| 47 |
+
## 🚀 使用流程
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
1. 登录 Hugging Face 账户
|
| 50 |
+
2. 上传输入图片
|
| 51 |
+
3. 输入提示词
|
| 52 |
+
4. 系统调用 4 个模型生成视频
|
| 53 |
+
5. 对每个模型评分(0-10分)
|
| 54 |
+
6. 提交评分,数据保存到 Dataset
|
| 55 |
|
| 56 |
+
## 📊 支持的模型
|
| 57 |
|
| 58 |
+
- Sora 2 pro
|
| 59 |
+
- Seedance Pro
|
| 60 |
+
- Veo 3.1
|
| 61 |
+
- Kling 2.6
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
## 💰 成本
|
| 64 |
|
| 65 |
+
- Space (CPU): $0
|
| 66 |
+
- Private Dataset (100GB): $0
|
| 67 |
+
- S3 存储: 按使用量计费
|
|
|
|
| 68 |
|
| 69 |
## 🔧 故障排除
|
| 70 |
|
| 71 |
+
- **无法登录**: 检查 README.md 中 `hf_oauth: true` 是否设置
|
| 72 |
+
- **无法保存评分**: 检查 `HF_TOKEN` 和 `DATASET_REPO_ID`
|
| 73 |
+
- **S3 上传失败**: 检查 AWS 凭证和 Bucket 名称
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -17,6 +17,9 @@ import time
|
|
| 17 |
# 导入视频生成服务
|
| 18 |
from pollo_service_single import PolloAIService, get_pollo_service
|
| 19 |
|
|
|
|
|
|
|
|
|
|
| 20 |
# 配置日志
|
| 21 |
logging.basicConfig(level=logging.INFO)
|
| 22 |
logger = logging.getLogger(__name__)
|
|
@@ -279,9 +282,19 @@ def generate_videos(prompt: str, input_image: Optional[str], request: gr.Request
|
|
| 279 |
|
| 280 |
# 处理图片上传(如果提供)
|
| 281 |
image_path = None
|
|
|
|
| 282 |
if input_image:
|
| 283 |
# Gradio 返回的是临时文件路径
|
| 284 |
image_path = input_image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
for model_name in models:
|
| 287 |
try:
|
|
@@ -301,10 +314,11 @@ def generate_videos(prompt: str, input_image: Optional[str], request: gr.Request
|
|
| 301 |
status_messages.append(f"❌ {display_name}: 需要上传输入图片")
|
| 302 |
continue
|
| 303 |
|
|
|
|
| 304 |
result = service.generate_video(
|
| 305 |
prompt=prompt,
|
| 306 |
mode="i2v", # 图片生成视频模式
|
| 307 |
-
input_image_path=
|
| 308 |
video_length=5,
|
| 309 |
width=1280,
|
| 310 |
height=720
|
|
@@ -320,15 +334,32 @@ def generate_videos(prompt: str, input_image: Optional[str], request: gr.Request
|
|
| 320 |
poll_result = service.poll_task_result(task_id)
|
| 321 |
|
| 322 |
if poll_result['status'] == 'completed':
|
| 323 |
-
|
| 324 |
-
if
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
break
|
| 333 |
elif poll_result['status'] == 'failed':
|
| 334 |
error_msg = poll_result.get('error_message', '未知错误')
|
|
|
|
| 17 |
# 导入视频生成服务
|
| 18 |
from pollo_service_single import PolloAIService, get_pollo_service
|
| 19 |
|
| 20 |
+
# 导入S3工具
|
| 21 |
+
from s3_utils import s3_utils
|
| 22 |
+
|
| 23 |
# 配置日志
|
| 24 |
logging.basicConfig(level=logging.INFO)
|
| 25 |
logger = logging.getLogger(__name__)
|
|
|
|
| 282 |
|
| 283 |
# 处理图片上传(如果提供)
|
| 284 |
image_path = None
|
| 285 |
+
image_url = None
|
| 286 |
if input_image:
|
| 287 |
# Gradio 返回的是临时文件路径
|
| 288 |
image_path = input_image
|
| 289 |
+
|
| 290 |
+
# 上传图片到S3,获取公网URL(Pollo API需要URL)
|
| 291 |
+
logger.info("上传图片到S3...")
|
| 292 |
+
image_url = s3_utils.upload_image_from_path(image_path)
|
| 293 |
+
|
| 294 |
+
if not image_url:
|
| 295 |
+
return "❌ 图片上传到S3失败,请检查S3配置", {}, {}
|
| 296 |
+
|
| 297 |
+
logger.info(f"图片已上传到S3: {image_url}")
|
| 298 |
|
| 299 |
for model_name in models:
|
| 300 |
try:
|
|
|
|
| 314 |
status_messages.append(f"❌ {display_name}: 需要上传输入图片")
|
| 315 |
continue
|
| 316 |
|
| 317 |
+
# 使用S3 URL而不是本地路径
|
| 318 |
result = service.generate_video(
|
| 319 |
prompt=prompt,
|
| 320 |
mode="i2v", # 图片生成视频模式
|
| 321 |
+
input_image_path=image_url, # 使用S3 URL
|
| 322 |
video_length=5,
|
| 323 |
width=1280,
|
| 324 |
height=720
|
|
|
|
| 334 |
poll_result = service.poll_task_result(task_id)
|
| 335 |
|
| 336 |
if poll_result['status'] == 'completed':
|
| 337 |
+
pollo_video_url = poll_result.get('video_url')
|
| 338 |
+
if pollo_video_url:
|
| 339 |
+
# 下载视频并上传到S3(Pollo的视频只保存一段时间)
|
| 340 |
+
logger.info(f"下载视频并上传到S3: {pollo_video_url}")
|
| 341 |
+
s3_video_url = s3_utils.download_and_upload_video(pollo_video_url)
|
| 342 |
+
|
| 343 |
+
if s3_video_url:
|
| 344 |
+
video_urls[model_name] = s3_video_url
|
| 345 |
+
model_results[model_name] = {
|
| 346 |
+
'status': 'success',
|
| 347 |
+
'task_id': task_id,
|
| 348 |
+
'video_url': s3_video_url,
|
| 349 |
+
'pollo_video_url': pollo_video_url # 保留原始URL
|
| 350 |
+
}
|
| 351 |
+
status_messages.append(f"✅ {display_name}: 生成成功并已保存到S3")
|
| 352 |
+
else:
|
| 353 |
+
# 如果S3上传失败,使用原始URL
|
| 354 |
+
logger.warning(f"S3上传失败,使用原始URL: {pollo_video_url}")
|
| 355 |
+
video_urls[model_name] = pollo_video_url
|
| 356 |
+
model_results[model_name] = {
|
| 357 |
+
'status': 'success',
|
| 358 |
+
'task_id': task_id,
|
| 359 |
+
'video_url': pollo_video_url,
|
| 360 |
+
'warning': 'S3上传失败,使用临时URL'
|
| 361 |
+
}
|
| 362 |
+
status_messages.append(f"✅ {display_name}: 生成成功(S3上传失败)")
|
| 363 |
break
|
| 364 |
elif poll_result['status'] == 'failed':
|
| 365 |
error_msg = poll_result.get('error_message', '未知错误')
|
pollo_service_single.py
CHANGED
|
@@ -181,22 +181,31 @@ class PolloAIService:
|
|
| 181 |
image_data = None
|
| 182 |
image_tail_data = None # Lite版本的结束图片
|
| 183 |
|
| 184 |
-
if input_image_path
|
| 185 |
-
#
|
| 186 |
-
if
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
else:
|
| 196 |
-
raise Exception(
|
| 197 |
-
"Pollo AI只接受图片URL,无法生成公网可访问的图片URL。"
|
| 198 |
-
"请配置PUBLIC_DOMAIN设置或确保图片可通过URL访问。"
|
| 199 |
-
)
|
| 200 |
|
| 201 |
if not image_data and mode != "t2v":
|
| 202 |
raise Exception("Pollo AI需要输入图片URL,但未提供有效的图片路径或无法生成公网URL")
|
|
|
|
| 181 |
image_data = None
|
| 182 |
image_tail_data = None # Lite版本的结束图片
|
| 183 |
|
| 184 |
+
if input_image_path:
|
| 185 |
+
# 检查是否是URL(以http://或https://开头)
|
| 186 |
+
if input_image_path.startswith(('http://', 'https://')):
|
| 187 |
+
# 直接使用URL
|
| 188 |
+
image_data = input_image_path
|
| 189 |
+
logger.info(f"使用图片URL: {image_data}")
|
| 190 |
+
elif os.path.exists(input_image_path):
|
| 191 |
+
# 本地文件路径,需要转换为URL
|
| 192 |
+
# 验证图片宽高比
|
| 193 |
+
if not self._validate_image_aspect_ratio(input_image_path):
|
| 194 |
+
raise Exception("图片宽高比不符合要求(必须小于1:4或4:1)")
|
| 195 |
+
|
| 196 |
+
# 只尝试构建公网可访问的URL,不再使用base64
|
| 197 |
+
public_image_url = self._try_get_public_image_url(input_image_path, symlink_folder)
|
| 198 |
+
|
| 199 |
+
if public_image_url:
|
| 200 |
+
image_data = public_image_url
|
| 201 |
+
logger.info(f"使用公网起始图片URL: {public_image_url}")
|
| 202 |
+
else:
|
| 203 |
+
raise Exception(
|
| 204 |
+
"Pollo AI只接受图片URL,无法生成公网可访问的图片URL。"
|
| 205 |
+
"请配置PUBLIC_DOMAIN设置或确保图片可通过URL访问。"
|
| 206 |
+
)
|
| 207 |
else:
|
| 208 |
+
raise Exception(f"无效的图片路径或URL: {input_image_path}")
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
if not image_data and mode != "t2v":
|
| 211 |
raise Exception("Pollo AI需要输入图片URL,但未提供有效的图片路径或无法生成公网URL")
|
requirements.txt
CHANGED
|
@@ -4,4 +4,5 @@ huggingface-hub>=0.20.0
|
|
| 4 |
requests>=2.31.0
|
| 5 |
Pillow>=10.0.0
|
| 6 |
numpy>=1.24.0
|
|
|
|
| 7 |
|
|
|
|
| 4 |
requests>=2.31.0
|
| 5 |
Pillow>=10.0.0
|
| 6 |
numpy>=1.24.0
|
| 7 |
+
boto3>=1.28.0
|
| 8 |
|
s3_utils.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
S3工具类 - 用于上传图片和视频到S3
|
| 3 |
+
简化版本,适配 Hugging Face Space
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import logging
|
| 8 |
+
import uuid
|
| 9 |
+
import boto3
|
| 10 |
+
import requests
|
| 11 |
+
from typing import Optional
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class S3Utils:
|
| 18 |
+
"""S3工具类,用于处理文件上传到S3"""
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
# 从环境变量读取AWS配置
|
| 22 |
+
aws_access_key_id = os.getenv('AWS_ACCESS_KEY_ID', '')
|
| 23 |
+
aws_secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY', '')
|
| 24 |
+
aws_region = os.getenv('AWS_REGION', 'us-east-1')
|
| 25 |
+
self.bucket_name = os.getenv('S3_BUCKET_NAME', '')
|
| 26 |
+
self.cdn_domain = os.getenv('CDN_DOMAIN', '') # 例如: https://cdn-dev.newai.land
|
| 27 |
+
|
| 28 |
+
if not aws_access_key_id or not aws_secret_access_key:
|
| 29 |
+
logger.warning("AWS凭证未配置,S3功能将不可用")
|
| 30 |
+
self.s3_client = None
|
| 31 |
+
else:
|
| 32 |
+
self.s3_client = boto3.client(
|
| 33 |
+
's3',
|
| 34 |
+
aws_access_key_id=aws_access_key_id,
|
| 35 |
+
aws_secret_access_key=aws_secret_access_key,
|
| 36 |
+
region_name=aws_region
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def upload_image_from_path(self, image_path: str, folder: str = "evaluator/images") -> Optional[str]:
|
| 40 |
+
"""
|
| 41 |
+
从本地路径上传图片到S3
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
image_path: 本地图片路径
|
| 45 |
+
folder: S3文件夹路径
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
str: 图片的CDN URL,失败返回None
|
| 49 |
+
"""
|
| 50 |
+
if not self.s3_client or not self.bucket_name:
|
| 51 |
+
logger.error("S3未配置,无法上传图片")
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
# 读取图片文件
|
| 56 |
+
with open(image_path, 'rb') as f:
|
| 57 |
+
image_data = f.read()
|
| 58 |
+
|
| 59 |
+
# 生成唯一文件名
|
| 60 |
+
file_extension = os.path.splitext(image_path)[1] or '.jpg'
|
| 61 |
+
unique_filename = f"{uuid.uuid4().hex}{file_extension}"
|
| 62 |
+
|
| 63 |
+
# S3文件键
|
| 64 |
+
s3_key = f"{folder}/{datetime.now().strftime('%Y/%m/%d')}/{unique_filename}"
|
| 65 |
+
|
| 66 |
+
# 确定Content-Type
|
| 67 |
+
content_type_map = {
|
| 68 |
+
'.jpg': 'image/jpeg',
|
| 69 |
+
'.jpeg': 'image/jpeg',
|
| 70 |
+
'.png': 'image/png',
|
| 71 |
+
'.webp': 'image/webp',
|
| 72 |
+
'.gif': 'image/gif'
|
| 73 |
+
}
|
| 74 |
+
content_type = content_type_map.get(file_extension.lower(), 'image/jpeg')
|
| 75 |
+
|
| 76 |
+
# 上传到S3
|
| 77 |
+
self.s3_client.put_object(
|
| 78 |
+
Bucket=self.bucket_name,
|
| 79 |
+
Key=s3_key,
|
| 80 |
+
Body=image_data,
|
| 81 |
+
ContentType=content_type
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# 构建CDN URL
|
| 85 |
+
if self.cdn_domain:
|
| 86 |
+
cdn_url = f"{self.cdn_domain.rstrip('/')}/{s3_key}"
|
| 87 |
+
else:
|
| 88 |
+
# 如果没有CDN,使用S3 URL
|
| 89 |
+
cdn_url = f"https://{self.bucket_name}.s3.amazonaws.com/{s3_key}"
|
| 90 |
+
|
| 91 |
+
logger.info(f"图片上传成功: {s3_key} -> {cdn_url}")
|
| 92 |
+
return cdn_url
|
| 93 |
+
|
| 94 |
+
except Exception as e:
|
| 95 |
+
logger.error(f"上传图片到S3失败: {e}")
|
| 96 |
+
return None
|
| 97 |
+
|
| 98 |
+
def download_and_upload_video(self, video_url: str, folder: str = "evaluator/videos") -> Optional[str]:
|
| 99 |
+
"""
|
| 100 |
+
从URL下载视频并上传到S3
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
video_url: 视频URL
|
| 104 |
+
folder: S3文件夹路径
|
| 105 |
+
|
| 106 |
+
Returns:
|
| 107 |
+
str: 视频的CDN URL,失败返回None
|
| 108 |
+
"""
|
| 109 |
+
if not self.s3_client or not self.bucket_name:
|
| 110 |
+
logger.error("S3未配置,无法上传视频")
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
# 下载视频
|
| 115 |
+
logger.info(f"开始下载视频: {video_url}")
|
| 116 |
+
response = requests.get(video_url, stream=True, timeout=300)
|
| 117 |
+
response.raise_for_status()
|
| 118 |
+
|
| 119 |
+
video_data = response.content
|
| 120 |
+
|
| 121 |
+
if not video_data or len(video_data) < 1000:
|
| 122 |
+
logger.error(f"下载的视频数据无效: {len(video_data)} bytes")
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
logger.info(f"视频下载成功: {len(video_data)} bytes")
|
| 126 |
+
|
| 127 |
+
# 生成唯一文件名
|
| 128 |
+
file_extension = '.mp4' # 默认mp4
|
| 129 |
+
if video_url:
|
| 130 |
+
try:
|
| 131 |
+
from urllib.parse import urlparse
|
| 132 |
+
parsed = urlparse(video_url)
|
| 133 |
+
path = parsed.path.lower()
|
| 134 |
+
if path.endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
|
| 135 |
+
file_extension = path[path.rfind('.'):]
|
| 136 |
+
except:
|
| 137 |
+
pass
|
| 138 |
+
|
| 139 |
+
unique_filename = f"{uuid.uuid4().hex}{file_extension}"
|
| 140 |
+
|
| 141 |
+
# S3文件键
|
| 142 |
+
s3_key = f"{folder}/{datetime.now().strftime('%Y/%m/%d')}/{unique_filename}"
|
| 143 |
+
|
| 144 |
+
# 确定Content-Type
|
| 145 |
+
content_type_map = {
|
| 146 |
+
'.mp4': 'video/mp4',
|
| 147 |
+
'.avi': 'video/x-msvideo',
|
| 148 |
+
'.mov': 'video/quicktime',
|
| 149 |
+
'.mkv': 'video/x-matroska',
|
| 150 |
+
'.webm': 'video/webm'
|
| 151 |
+
}
|
| 152 |
+
content_type = content_type_map.get(file_extension.lower(), 'video/mp4')
|
| 153 |
+
|
| 154 |
+
# 上传到S3
|
| 155 |
+
self.s3_client.put_object(
|
| 156 |
+
Bucket=self.bucket_name,
|
| 157 |
+
Key=s3_key,
|
| 158 |
+
Body=video_data,
|
| 159 |
+
ContentType=content_type
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# 构建CDN URL
|
| 163 |
+
if self.cdn_domain:
|
| 164 |
+
cdn_url = f"{self.cdn_domain.rstrip('/')}/{s3_key}"
|
| 165 |
+
else:
|
| 166 |
+
# 如果没有CDN,使用S3 URL
|
| 167 |
+
cdn_url = f"https://{self.bucket_name}.s3.amazonaws.com/{s3_key}"
|
| 168 |
+
|
| 169 |
+
logger.info(f"视频上传成功: {s3_key} -> {cdn_url}")
|
| 170 |
+
return cdn_url
|
| 171 |
+
|
| 172 |
+
except Exception as e:
|
| 173 |
+
logger.error(f"下载并上传视频失败: {e}")
|
| 174 |
+
return None
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# 创建全局实例
|
| 178 |
+
s3_utils = S3Utils()
|
| 179 |
+
|