fix(P0): 素材库视频上传时生成缩略图 #588
@@ -38,53 +38,36 @@ def extract_first_frame(
|
||||
"""
|
||||
from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_duration, run_ffmpeg
|
||||
|
||||
_is_temp_output = False
|
||||
if output_path is None:
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
|
||||
tmp.close()
|
||||
output_path = tmp.name
|
||||
|
||||
# 计算抽帧时间点:取视频时长 * seek_ratio,最少 min_seek_seconds 秒
|
||||
try:
|
||||
duration = probe_duration(video_path)
|
||||
seek_time = max(min_seek_seconds, duration * seek_ratio)
|
||||
except Exception:
|
||||
# probe 失败时 fallback 到第1秒
|
||||
seek_time = min_seek_seconds
|
||||
|
||||
# 格式化为 HH:MM:SS.xx
|
||||
seek_str = _format_seek_time(seek_time)
|
||||
|
||||
# -ss 放在 -i 前面(input seeking,更快但精度稍低,缩略图够用)
|
||||
# -vframes 1 只取一帧
|
||||
# -q:v 2 jpeg 高质量
|
||||
scale_filter = f"scale={width}:{height}:force_original_aspect_ratio=decrease"
|
||||
cmd = [
|
||||
FFMPEG_BIN,
|
||||
"-y",
|
||||
"-ss",
|
||||
seek_str,
|
||||
"-i",
|
||||
video_path,
|
||||
"-vframes",
|
||||
"1",
|
||||
"-vf",
|
||||
scale_filter,
|
||||
"-q:v",
|
||||
"2",
|
||||
output_path,
|
||||
]
|
||||
_is_temp_output = True
|
||||
|
||||
try:
|
||||
run_ffmpeg(cmd, capture_output=True, timeout=timeout)
|
||||
except Exception:
|
||||
# 失败时退回到第0帧兜底
|
||||
cmd2 = [
|
||||
# 计算抽帧时间点:取视频时长 * seek_ratio,最少 min_seek_seconds 秒
|
||||
try:
|
||||
duration = probe_duration(video_path)
|
||||
seek_time = max(min_seek_seconds, duration * seek_ratio)
|
||||
except Exception:
|
||||
# probe 失败时 fallback 到第1秒
|
||||
seek_time = min_seek_seconds
|
||||
|
||||
# 格式化为 HH:MM:SS.xx
|
||||
seek_str = _format_seek_time(seek_time)
|
||||
|
||||
# -ss 放在 -i 前面(input seeking,更快但精度稍低,缩略图够用)
|
||||
# -vframes 1 只取一帧
|
||||
# -q:v 2 jpeg 高质量
|
||||
scale_filter = f"scale={width}:{height}:force_original_aspect_ratio=decrease"
|
||||
cmd = [
|
||||
FFMPEG_BIN,
|
||||
"-y",
|
||||
"-ss",
|
||||
seek_str,
|
||||
"-i",
|
||||
video_path,
|
||||
"-ss",
|
||||
"00:00:00",
|
||||
"-vframes",
|
||||
"1",
|
||||
"-vf",
|
||||
@@ -93,12 +76,40 @@ def extract_first_frame(
|
||||
"2",
|
||||
output_path,
|
||||
]
|
||||
run_ffmpeg(cmd2, capture_output=True, timeout=timeout)
|
||||
|
||||
if not Path(output_path).exists() or Path(output_path).stat().st_size == 0:
|
||||
raise RuntimeError(f"Thumbnail generation failed: {output_path}")
|
||||
try:
|
||||
run_ffmpeg(cmd, capture_output=True, timeout=timeout)
|
||||
except Exception:
|
||||
# 失败时退回到第0帧兜底
|
||||
cmd2 = [
|
||||
FFMPEG_BIN,
|
||||
"-y",
|
||||
"-i",
|
||||
video_path,
|
||||
"-ss",
|
||||
"00:00:00",
|
||||
"-vframes",
|
||||
"1",
|
||||
"-vf",
|
||||
scale_filter,
|
||||
"-q:v",
|
||||
"2",
|
||||
output_path,
|
||||
]
|
||||
run_ffmpeg(cmd2, capture_output=True, timeout=timeout)
|
||||
|
||||
return output_path
|
||||
if not Path(output_path).exists() or Path(output_path).stat().st_size == 0:
|
||||
raise RuntimeError(f"Thumbnail generation failed: {output_path}")
|
||||
|
||||
return output_path
|
||||
except Exception:
|
||||
# 失败时清理自己创建的临时文件
|
||||
if _is_temp_output and output_path:
|
||||
try:
|
||||
Path(output_path).unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def _format_seek_time(seconds: float) -> str:
|
||||
|
||||
@@ -280,6 +280,7 @@ def ingest_asset(job_id: str) -> dict:
|
||||
# 先从 OSS 下载文件到本地临时目录,再提取元数据
|
||||
# (storage_key 是 OSS 内部路径,不能直接传给 ffprobe/Pillow)
|
||||
local_file = None
|
||||
thumbnail_url = None
|
||||
try:
|
||||
suffix = Path(job.storage_key).suffix or ".bin"
|
||||
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
||||
@@ -291,6 +292,27 @@ def ingest_asset(job_id: str) -> dict:
|
||||
metadata, extract_success = {}, False
|
||||
else:
|
||||
metadata, extract_success = extract_media_metadata(str(local_file), media_type)
|
||||
|
||||
# 视频类型:生成缩略图(文件还在的时候生成)
|
||||
thumbnail_url = None
|
||||
if media_type == "video" and extract_success:
|
||||
try:
|
||||
from video_processing.thumbnail_generator import generate_and_upload_thumbnail
|
||||
|
||||
thumb_storage_key = f"assets/{job.project_id}/thumbnails/{job_id}.jpg"
|
||||
thumbnail_url = generate_and_upload_thumbnail(str(local_file), thumb_storage_key)
|
||||
if thumbnail_url:
|
||||
logger.info(
|
||||
"素材缩略图生成成功: job_id=%s url=%s",
|
||||
job_id,
|
||||
thumbnail_url[:80],
|
||||
)
|
||||
except Exception as thumb_err:
|
||||
logger.warning(
|
||||
"素材缩略图生成失败(不影响主流程): job_id=%s error=%s",
|
||||
job_id,
|
||||
thumb_err,
|
||||
)
|
||||
finally:
|
||||
if local_file and local_file.exists():
|
||||
try:
|
||||
@@ -359,6 +381,7 @@ def ingest_asset(job_id: str) -> dict:
|
||||
codec=metadata.get("codec") or None,
|
||||
status=AssetStatus.READY,
|
||||
file_hash=job.file_hash,
|
||||
thumbnail_url=thumbnail_url,
|
||||
)
|
||||
asset_repo.create(asset)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user