testcoder-ui commited on
Commit
d9add37
·
1 Parent(s): d1c1f8f

Add S3 support and simplify README

Browse files
Files changed (5) hide show
  1. README.md +41 -63
  2. app.py +41 -10
  3. pollo_service_single.py +24 -15
  4. requirements.txt +1 -0
  5. 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
- 基于 Hugging Face Spaces 的视频生成模型评估系统,支持 Prompt、模型、视频的评估和评分。
15
 
16
- ## ✨ 功能特性
17
 
18
- - 🔐 **强制登录**: 使用 Hugging Face OAuth 强制用户登录
19
- - 📊 **次数限制**: 每个用户每天最多调用 4
20
- - 🚀 **多模型支持**: 同时调用多个视频生成模型(Sora 2 pro, Seedance Pro, Veo 3.1, Kling 2.6)
21
- - 💾 **数据持久化**: 评分数据自动保存到 Private Dataset
22
- - 💰 **零成本运行**: 完全使用 CPU,无需 GPU,免费运行
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
- - **Sora 2 pro** - OpenAI Sora 2 Pro
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
- - **Hugging Face Space (CPU)**: $0(免费)
61
- - **Private Dataset (100GB)**: $0(免费额度足够)
62
- - **视频生成 API**: 取决于 API 定价
63
 
64
- 如果视频生成 API 免费,整个流程完全免费!
 
 
 
 
 
65
 
66
- ## 📝 数据格式
67
 
68
- 评分数据保存为 JSONL 格式,包含:
69
- - 时间戳
70
- - 用户名
71
- - 提示词(Prompt)
72
- - 各模型评分
73
- - 视频 URL
74
- - 模型结果
75
 
76
- ## ⚠️ 注意事项
77
 
78
- 1. 需要图片输入才能生成视频
79
- 2. 每个用户每天最多调用 4
80
- 3. 视频生成可能需要几分钟时间
81
- 4. 确保已配置所有必需的环境变量
82
 
83
  ## 🔧 故障排除
84
 
85
- ### 无法登录
86
- - 检查 Space Settings 中是否启用了 OAuth
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=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
- video_url = poll_result.get('video_url')
324
- if video_url:
325
- video_urls[model_name] = video_url
326
- model_results[model_name] = {
327
- 'status': 'success',
328
- 'task_id': task_id,
329
- 'video_url': video_url
330
- }
331
- status_messages.append(f"✅ {display_name}: 生成成功")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 and os.path.exists(input_image_path):
185
- # 验证图片宽高比
186
- if not self._validate_image_aspect_ratio(input_image_path):
187
- raise Exception("图片宽高比不符合要求(必须小于1:4或4:1)")
188
-
189
- # 只尝试构建公网可访问的URL,不再使用base64
190
- public_image_url = self._try_get_public_image_url(input_image_path, symlink_folder)
191
-
192
- if public_image_url:
193
- image_data = public_image_url
194
- logger.info(f"使用公网起始图片URL: {public_image_url}")
 
 
 
 
 
 
 
 
 
 
 
 
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
+