feat(video): 集成可灵AI多图参考生视频生成服务

- 替换视频生成服务为可灵AI多图参考生视频API,支持1-4张多视角图片输入
- 调整图片拼接逻辑,生成横向长图传入即梦API备用
- 实现基于JWT认证的可灵API请求和轮询机制,支持高品质1:1正方形视频生成
- 在设计详情页新增视频展示区域及生成、重新生成和下载视频操作
- 更新后台系统配置,支持配置可灵AI Access Key和Secret Key
- 删除即梦视频相关配置及逻辑,所有视频生成功能切换到可灵AI实现
- 优化视频生成提示词,提升视频质感和展示效果
- 增加视频文件本地存储和路径管理,保证视频可访问和下载
- 前端增加视频生成状态管理和用户界面交互提示
- 后端添加PyJWT依赖,支持JWT认证流程
This commit is contained in:
2026-03-28 00:20:48 +08:00
parent 8f5a86418e
commit 1d94ec114a
9 changed files with 596 additions and 95 deletions

View File

@@ -13,7 +13,7 @@ from ..models import User, Design, DesignImage
from ..schemas import DesignCreate, DesignResponse, DesignListResponse, DesignImageResponse
from ..utils.deps import get_current_user
from ..services import design_service
from ..services import ai_video_generator
from ..services import ai_video_generator_kling
from ..services import ai_3d_generator
logger = logging.getLogger(__name__)
@@ -255,7 +255,7 @@ async def generate_video(
):
"""
为设计生成 360 度旋转展示视频
取设计的多视角图片,通过火山引擎即梦 3.0 Pro 生成视频
取设计的多视角图片,通过可灵 AI 多图参考生视频 API 生成视频
"""
design = design_service.get_design_by_id(db=db, design_id=design_id, user_id=current_user.id)
if not design:
@@ -285,7 +285,7 @@ async def generate_video(
logger.info(f"设计 {design_id} 生成视频,共收集到 {len(image_urls)} 张图片")
try:
video_url = await ai_video_generator.generate_video(image_urls)
video_url = await ai_video_generator_kling.generate_video(image_urls)
design.video_url = video_url
db.commit()
db.refresh(design)

View File

@@ -152,12 +152,11 @@ def _get_volc_keys() -> tuple:
async def _merge_images_to_base64(image_urls: List[str]) -> str:
"""
下载多张图片并拼接为一张网格图,返回 base64 编码
下载多张图片并横向拼接为一张图,返回 base64 编码
拼接策略:
- 1张: 直接使用
- 2张: 1×2 横向拼接
- 3~4张: 2×2 网格拼接
- 张: 横向一字排开拼接(展示各角度全貌)
"""
# 下载所有图片
images = []
@@ -179,36 +178,43 @@ async def _merge_images_to_base64(image_urls: List[str]) -> str:
if len(images) == 1:
merged = images[0]
else:
# 统一尺寸到最大尺寸
max_w = max(img.width for img in images)
max_h = max(img.height for img in images)
# 统一高度,横向拼接
target_h = max(img.height for img in images)
resized = []
for img in images:
if img.height != target_h:
ratio = target_h / img.height
new_w = int(img.width * ratio)
img = img.resize((new_w, target_h), Image.LANCZOS)
resized.append(img)
# 计算网格布局
n = len(images)
if n == 2:
cols, rows = 2, 1
else:
cols = 2
rows = math.ceil(n / cols)
total_w = sum(img.width for img in resized)
merged = Image.new("RGB", (total_w, target_h), (255, 255, 255))
x_offset = 0
for img in resized:
merged.paste(img, (x_offset, 0))
x_offset += img.width
# 创建拼接画布(白色背景)
canvas_w = cols * max_w
canvas_h = rows * max_h
merged = Image.new("RGB", (canvas_w, canvas_h), (255, 255, 255))
logger.info(f"图片横向拼接完成: {len(resized)}张 -> 尺寸={merged.size}")
for idx, img in enumerate(images):
r = idx // cols
c = idx % cols
# 居中放置
x = c * max_w + (max_w - img.width) // 2
y = r * max_h + (max_h - img.height) // 2
merged.paste(img, (x, y))
# 压缩图片尺寸即梦API对请求体大小有限制
max_width = 1920
if merged.width > max_width:
ratio = max_width / merged.width
new_h = int(merged.height * ratio)
merged = merged.resize((max_width, new_h), Image.LANCZOS)
logger.info(f"拼接图已压缩至: {merged.size}")
logger.info(f"图片拼接完成: {n}张 -> {cols}x{rows}网格, 尺寸={merged.size}")
# 转 base64
# 转 base64控制质量确保 base64 不会过大
buf = io.BytesIO()
merged.save(buf, format="JPEG", quality=90)
quality = 75
merged.save(buf, format="JPEG", quality=quality)
# 如果超过 5MB进一步压缩
while buf.tell() > 5 * 1024 * 1024 and quality > 30:
buf = io.BytesIO()
quality -= 10
merged.save(buf, format="JPEG", quality=quality)
logger.info(f"图片超过 5MB降低质量到 {quality},大小={buf.tell()} bytes")
b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
logger.info(f"拼接图 base64 长度: {len(b64)}")
return b64
@@ -222,44 +228,56 @@ async def generate_video(
"""
调用即梦3.0 Pro 生成 360 度旋转展示视频
流程:
1. 将多视角图片横向拼接成一张长图
2. 以 base64 方式传入即梦API
3. 使用强化提示词描述单品展示
Args:
image_urls: 多视角图片 URL 列表(取第一张作为首帧)
image_urls: 多视角图片 URL 列表
prompt: 视频生成提示词
duration_seconds: 预留参数(即梦目前固定帧数)
duration_seconds: 预留参数
Returns:
生成的视频远程 URL
Raises:
RuntimeError: 视频生成失败
生成的视频本地 URL
"""
access_key, secret_key = _get_volc_keys()
logger.info(f"传入视频生成的图片数量: {len(image_urls)}")
# 即梦API只支持单张图片输入取第一张正面效果图作为基准
first_url = image_urls[0]
logger.info(f"使用第一张图片生成视频: {first_url[:80]}...")
# Step 0: 拼接多视角图片为横向长图,转 base64
merged_b64 = await _merge_images_to_base64(image_urls)
logger.info(f"多视角图片已拼接为长图base64长度: {len(merged_b64)}")
# 从配置读取默认 prompt
# 从配置读取默认 prompt,如果没有则使用强化提示词
if not prompt:
prompt = get_config_value("VIDEO_PROMPT", "")
if not prompt:
prompt = (
"玉雕作品在摄影棚内缓慢旋转360度展示全貌"
"专业珠宝摄影灯光,纯白色背景,平稳旋转,"
"展示正面、侧面、背面各个角度,电影级画质"
"参考图展示的是同一件精美玉雕工艺品的多个角度"
"请生成这一件玉雕作品在专业珠宝摄影棚内的展示视频。"
"纯白色背景,柔和的珠宝摄影灯光,"
"这一件玉石作品放在旋转展台上缓慢平稳地旋转360度"
"展现其温润的质感、细腻的雕刻纹理和通透的光泽,"
"电影级画质,微距的细节感,平稳流畅的转台旋转,"
"画面中只有一件玉雕作品"
)
# Step 1: 提交任务(只传第一张图片URL
task_id = await _submit_video_task(access_key, secret_key, first_url, prompt)
logger.info(f"即梦视频生成任务已提交: task_id={task_id}")
# Step 1: 提交任务(用 base64 拼接长图,失败则降级为第一张图片 URL
try:
task_id = await _submit_video_task(access_key, secret_key, None, prompt, merged_b64)
logger.info(f"即梦视频生成任务已提交(base64拼接图): task_id={task_id}")
except Exception as e:
logger.warning(f"base64 拼接图提交失败降级为第一张图片URL: {e}")
first_url = image_urls[0]
task_id = await _submit_video_task(access_key, secret_key, first_url, prompt)
logger.info(f"即梦视频生成任务已提交(单图URL): task_id={task_id}")
# Step 2: 轮询等待结果
remote_video_url = await _poll_video_result(access_key, secret_key, task_id)
logger.info(f"即梦视频生成完成: {remote_video_url[:80]}...")
# Step 3: 下载视频到本地存储即梦URL有效期约 1 小时,必须保存到本地)
# Step 3: 下载视频到本地存储
local_path = await _download_video_to_local(remote_video_url)
logger.info(f"视频已保存到本地: {local_path}")
@@ -269,31 +287,43 @@ async def generate_video(
async def _submit_video_task(
access_key: str,
secret_key: str,
image_url: str,
image_url: Optional[str],
prompt: str,
image_base64: Optional[str] = None,
) -> str:
"""提交图生视频任务到即梦3.0 Pro使用单张图片URL"""
"""提交图生视频任务到即梦3.0 Pro支持 URL 或 base64 输入"""
action = "CVSync2AsyncSubmitTask"
logger.info(f"提交即梦视频任务图片URL: {image_url[:80]}...")
payload = {
"req_key": REQ_KEY_I2V,
"prompt": prompt,
"image_urls": [image_url],
"seed": -1,
"frames": int(get_config_value("VIDEO_FRAMES", "121")),
"aspect_ratio": "1:1", # 玉雕展示用正方形
"aspect_ratio": "1:1",
}
# 优先使用 base64拼接长图否则用 URL
if image_base64:
payload["binary_data_base64"] = [image_base64]
logger.info(f"使用 base64 拼接长图提交即梦视频任务base64长度={len(image_base64)}")
elif image_url:
payload["image_urls"] = [image_url]
logger.info(f"使用图片URL提交即梦视频任务: {image_url[:80]}...")
else:
raise RuntimeError("未提供图片输入")
body = json.dumps(payload, ensure_ascii=False)
headers = _build_signed_headers(access_key, secret_key, action, body)
url = f"{VISUAL_API_URL}?Action={action}&Version={API_VERSION}"
async with httpx.AsyncClient(timeout=SUBMIT_TIMEOUT) as client:
# base64 数据量大,需要更长的超时时间
timeout = 120 if image_base64 else SUBMIT_TIMEOUT
async with httpx.AsyncClient(timeout=timeout) as client:
resp = await client.post(url, content=body, headers=headers)
if resp.status_code != 200:
logger.error(f"即梦视频任务提交失败: status={resp.status_code}, body={resp.text[:500]}")
error_body = resp.text[:1000]
logger.error(f"即梦视频任务提交失败: status={resp.status_code}, body={error_body}")
resp.raise_for_status()
data = resp.json()

View File

@@ -0,0 +1,276 @@
"""
AI 视频生成服务 - 可灵Kling多图参考生视频
使用可灵 AI 的多图参考生视频 API原生支持传入 1-4 张参考图片
AI 会理解为同一物体的多角度参考,生成单品旋转展示视频
API 文档: https://app.klingai.com/cn/dev/document-api/apiReference/model/multiImageToVideo
认证方式: JWT (Access Key + Secret Key)
API 端点: https://api.klingai.com
"""
import asyncio
import json
import logging
import time
import uuid
from pathlib import Path
from typing import Optional, List
import httpx
import jwt
from .config_service import get_config_value
# 视频本地存储目录
VIDEO_UPLOAD_DIR = Path(__file__).resolve().parent.parent.parent / "uploads" / "videos"
VIDEO_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
logger = logging.getLogger(__name__)
# 可灵 API 配置(中国区域名)
KLING_API_BASE = "https://api-beijing.klingai.com"
# 超时与轮询配置
SUBMIT_TIMEOUT = 30
POLL_TIMEOUT = 15
MAX_POLL_ATTEMPTS = 120 # 约 10 分钟
POLL_INTERVAL = 5
# ============================================================
# JWT 认证
# ============================================================
def _generate_jwt_token(access_key: str, secret_key: str) -> str:
"""
使用 Access Key 和 Secret Key 生成 JWT Token
可灵 API 使用 JWT 认证token 有效期 30 分钟
"""
now = int(time.time())
headers = {
"alg": "HS256",
"typ": "JWT"
}
payload = {
"iss": access_key,
"exp": now + 1800, # 30 分钟过期
"nbf": now - 5, # 允许 5 秒时钟偏差
"iat": now, # 签发时间
}
token = jwt.encode(payload, secret_key, algorithm="HS256", headers=headers)
return token
def _get_kling_keys() -> tuple:
"""获取可灵 Access Key 和 Secret Key"""
access_key = get_config_value("KLING_ACCESS_KEY", "")
secret_key = get_config_value("KLING_SECRET_KEY", "")
if not access_key or not secret_key:
raise RuntimeError(
"未配置 KLING_ACCESS_KEY 或 KLING_SECRET_KEY无法使用可灵视频生成。"
"请在管理后台 系统配置 中添加可灵 AI 的 Access Key 和 Secret Key。"
)
return access_key, secret_key
def _build_headers(access_key: str, secret_key: str) -> dict:
"""构建带 JWT 认证的请求头"""
token = _generate_jwt_token(access_key, secret_key)
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
}
# ============================================================
# 视频生成核心逻辑
# ============================================================
async def generate_video(
image_urls: List[str],
prompt: str = "",
duration_seconds: int = 5,
) -> str:
"""
调用可灵多图参考生视频 API生成 360 度旋转展示视频
核心优势原生支持传入多张参考图1-4张
AI 理解为同一物体的多角度参考,生成单品视频。
Args:
image_urls: 多视角图片 URL 列表最多4张
prompt: 视频生成提示词
duration_seconds: 视频时长5 或 10 秒)
Returns:
生成的视频本地 URL
"""
access_key, secret_key = _get_kling_keys()
logger.info(f"可灵视频生成,传入图片数量: {len(image_urls)}")
# 可灵最多支持 4 张参考图
if len(image_urls) > 4:
image_urls = image_urls[:4]
logger.info("图片数量超过4张截取前4张")
# 构建提示词
if not prompt:
prompt = get_config_value("VIDEO_PROMPT", "")
if not prompt:
prompt = (
"精美玉雕工艺品在专业珠宝摄影棚内展示,"
"纯白色背景,柔和的珠宝摄影灯光,"
"玉石作品放在旋转展台上缓慢平稳地旋转360度"
"展示正面、侧面、背面全貌,"
"展现玉石温润的质感、细腻的雕刻纹理和通透的光泽,"
"电影级画质,微距细节感,平稳流畅的转台旋转"
)
# 视频时长,仅支持 5 或 10
duration = str(duration_seconds) if duration_seconds in (5, 10) else "5"
# Step 1: 提交任务
task_id = await _submit_video_task(
access_key, secret_key, image_urls, prompt, duration
)
logger.info(f"可灵视频生成任务已提交: task_id={task_id}")
# Step 2: 轮询等待结果
remote_video_url = await _poll_video_result(access_key, secret_key, task_id)
logger.info(f"可灵视频生成完成: {remote_video_url[:80]}...")
# Step 3: 下载视频到本地存储
local_path = await _download_video_to_local(remote_video_url)
logger.info(f"视频已保存到本地: {local_path}")
return local_path
async def _submit_video_task(
access_key: str,
secret_key: str,
image_urls: List[str],
prompt: str,
duration: str = "5",
) -> str:
"""提交多图参考生视频任务到可灵 API"""
url = f"{KLING_API_BASE}/v1/videos/multi-image2video"
# 构建 image_list每个元素是 {"image": "url"}
image_list = [{"image": img_url} for img_url in image_urls]
payload = {
"model_name": "kling-v1-6",
"image_list": image_list,
"prompt": prompt,
"mode": "pro", # 高品质模式
"duration": duration, # 视频时长
"aspect_ratio": "1:1", # 1:1 正方形
}
headers = _build_headers(access_key, secret_key)
body = json.dumps(payload, ensure_ascii=False)
logger.info(f"提交可灵视频任务: {len(image_urls)}张参考图, 时长={duration}s, 模式=pro")
async with httpx.AsyncClient(timeout=SUBMIT_TIMEOUT) as client:
resp = await client.post(url, content=body, headers=headers)
if resp.status_code not in (200, 201):
error_body = resp.text[:1000]
logger.error(f"可灵视频任务提交失败: status={resp.status_code}, body={error_body}")
resp.raise_for_status()
data = resp.json()
# 检查响应
code = data.get("code", -1)
if code != 0:
msg = data.get("message", "未知错误")
raise RuntimeError(f"可灵视频任务提交失败 (code={code}): {msg}")
task_id = data.get("data", {}).get("task_id")
if not task_id:
raise RuntimeError(f"可灵响应中未找到 task_id: {data}")
return task_id
async def _poll_video_result(
access_key: str,
secret_key: str,
task_id: str,
) -> str:
"""轮询可灵视频生成结果"""
url = f"{KLING_API_BASE}/v1/videos/multi-image2video/{task_id}"
for attempt in range(1, MAX_POLL_ATTEMPTS + 1):
await asyncio.sleep(POLL_INTERVAL)
# 每次轮询重新生成 JWT避免过期
headers = _build_headers(access_key, secret_key)
try:
async with httpx.AsyncClient(timeout=POLL_TIMEOUT) as client:
resp = await client.get(url, headers=headers)
if resp.status_code != 200:
logger.warning(
f"轮询可灵视频结果失败 (attempt={attempt}): "
f"status={resp.status_code}, body={resp.text[:300]}"
)
continue
data = resp.json()
except Exception as e:
logger.warning(f"轮询可灵视频异常 (attempt={attempt}): {e}")
continue
code = data.get("code", -1)
if code != 0:
msg = data.get("message", "未知错误")
logger.warning(f"轮询可灵视频返回错误 (attempt={attempt}): code={code}, msg={msg}")
continue
task_data = data.get("data", {})
task_status = task_data.get("task_status", "")
if task_status == "succeed":
# 从 task_result.videos 中提取视频 URL
task_result = task_data.get("task_result", {})
videos = task_result.get("videos", [])
if videos and videos[0].get("url"):
return videos[0]["url"]
raise RuntimeError(f"可灵视频生成完成但未找到视频URL: {data}")
elif task_status == "failed":
fail_msg = task_data.get("task_status_msg", "未知原因")
raise RuntimeError(f"可灵视频生成失败: {fail_msg}")
else:
# submitted / processing
if attempt % 6 == 0:
logger.info(
f"可灵视频生成中... (attempt={attempt}, status={task_status})"
)
raise RuntimeError(f"可灵视频生成超时: 轮询 {MAX_POLL_ATTEMPTS} 次后仍未完成")
async def _download_video_to_local(remote_url: str) -> str:
"""
下载远程视频到本地 uploads/videos/ 目录
Returns:
本地视频的 URL 路径,如 /uploads/videos/xxx.mp4
"""
filename = f"{uuid.uuid4().hex}.mp4"
local_file = VIDEO_UPLOAD_DIR / filename
try:
async with httpx.AsyncClient(timeout=120, follow_redirects=True) as client:
resp = await client.get(remote_url)
resp.raise_for_status()
local_file.write_bytes(resp.content)
logger.info(f"视频下载完成: {len(resp.content)} 字节 -> {local_file}")
except Exception as e:
logger.error(f"视频下载失败: {e}")
raise RuntimeError(f"视频下载失败: {e}")
return f"/uploads/videos/{filename}"

View File

@@ -13,3 +13,4 @@ Pillow==10.2.0
pydantic[email]==2.6.1
pydantic-settings==2.1.0
httpx==0.27.0
PyJWT==2.8.0

71
backend/uvicorn.log Normal file
View File

@@ -0,0 +1,71 @@
INFO: Will watch for changes in these directories: ['/Users/changyoutongxue/开发-qoder/YuShiSheJi/backend']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [63566] using StatReload
INFO: Started server process [63686]
INFO: Waiting for application startup.
INFO: Application startup complete.
✅ 上传目录已准备: uploads
INFO: 127.0.0.1:62589 - "POST /api/auth/login HTTP/1.1" 200 OK
INFO: 127.0.0.1:62593 - "GET /api/auth/me HTTP/1.1" 200 OK
INFO: 127.0.0.1:62599 - "GET /api/categories HTTP/1.1" 200 OK
INFO: 127.0.0.1:62605 - "GET /api/admin/dashboard HTTP/1.1" 200 OK
INFO: 127.0.0.1:62608 - "GET /api/admin/configs HTTP/1.1" 200 OK
INFO: 127.0.0.1:62786 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:62786 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:62835 - "GET /api/auth/me HTTP/1.1" 200 OK
INFO: 127.0.0.1:62838 - "GET /api/admin/configs HTTP/1.1" 200 OK
INFO: 127.0.0.1:62913 - "GET /api/admin/configs HTTP/1.1" 200 OK
INFO: 127.0.0.1:62982 - "GET /api/admin/configs HTTP/1.1" 200 OK
INFO: 127.0.0.1:63026 - "GET /api/auth/me HTTP/1.1" 200 OK
INFO: 127.0.0.1:63030 - "GET /api/admin/configs HTTP/1.1" 200 OK
INFO: 127.0.0.1:63052 - "PUT /api/admin/configs HTTP/1.1" 200 OK
INFO: 127.0.0.1:63055 - "GET /api/admin/configs HTTP/1.1" 200 OK
INFO: 127.0.0.1:63063 - "GET /api/admin/users?page=1&page_size=20 HTTP/1.1" 200 OK
INFO: 127.0.0.1:63072 - "GET /api/auth/me HTTP/1.1" 200 OK
INFO: 127.0.0.1:63075 - "GET /api/categories HTTP/1.1" 200 OK
INFO: 127.0.0.1:63079 - "GET /api/designs?page=1&page_size=20 HTTP/1.1" 200 OK
INFO: 127.0.0.1:63081 - "GET /uploads/designs/26.png HTTP/1.1" 304 Not Modified
INFO: 127.0.0.1:63086 - "GET /api/designs/27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:63088 - "GET /uploads/videos/f2e6c3eac03b485f8115e72a1d33b491.mp4 HTTP/1.1" 304 Not Modified
INFO: 127.0.0.1:63091 - "GET /uploads/models/506909359d6c45fe9e43108ee7765a9a.glb HTTP/1.1" 304 Not Modified
可灵视频任务提交失败: status=401, body={"code":1002,"message":"Auth failed","request_id":"CjyhVGikJMQAAAAAGOrdCQ","data":null}
视频生成失败: Client error '401 ' for url 'https://api.klingai.com/v1/videos/multi-image2video'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
INFO: 127.0.0.1:63096 - "POST /api/designs/27/generate-video?force=true HTTP/1.1" 500 Internal Server Error
WARNING: StatReload detected changes in 'app/services/ai_video_generator_kling.py'. Reloading...
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [63686]
👋 应用已关闭
INFO: Started server process [64167]
INFO: Waiting for application startup.
INFO: Application startup complete.
✅ 上传目录已准备: uploads
INFO: 127.0.0.1:63400 - "GET /api/auth/me HTTP/1.1" 200 OK
INFO: 127.0.0.1:63405 - "GET /api/categories HTTP/1.1" 200 OK
INFO: 127.0.0.1:63407 - "GET /api/designs/27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:63409 - "GET /uploads/videos/f2e6c3eac03b485f8115e72a1d33b491.mp4 HTTP/1.1" 404 Not Found
可灵视频任务提交失败: status=401, body={"code":1002,"message":"Auth failed","request_id":"CjMkpGikJkcAAAAAGOrbdA","data":null}
视频生成失败: Client error '401 ' for url 'https://api.klingai.com/v1/videos/multi-image2video'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
INFO: 127.0.0.1:63412 - "POST /api/designs/27/generate-video?force=true HTTP/1.1" 500 Internal Server Error
INFO: 127.0.0.1:63418 - "GET /uploads/videos/f2e6c3eac03b485f8115e72a1d33b491.mp4 HTTP/1.1" 404 Not Found
WARNING: StatReload detected changes in 'app/services/ai_video_generator_kling.py'. Reloading...
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [64167]
👋 应用已关闭
INFO: Started server process [64861]
INFO: Waiting for application startup.
INFO: Application startup complete.
✅ 上传目录已准备: uploads
INFO: 127.0.0.1:63556 - "GET /api/auth/me HTTP/1.1" 200 OK
INFO: 127.0.0.1:63563 - "GET /api/categories HTTP/1.1" 200 OK
INFO: 127.0.0.1:63565 - "GET /api/designs/27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:63567 - "GET /uploads/videos/f2e6c3eac03b485f8115e72a1d33b491.mp4 HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:63569 - "GET /uploads/models/506909359d6c45fe9e43108ee7765a9a.glb HTTP/1.1" 304 Not Modified
INFO: 127.0.0.1:63575 - "POST /api/designs/27/generate-video?force=true HTTP/1.1" 200 OK
INFO: 127.0.0.1:63785 - "GET /uploads/videos/6ed3f33421a44876b303c7671c1597d5.mp4 HTTP/1.1" 200 OK
INFO: 127.0.0.1:63846 - "GET /uploads/videos/6ed3f33421a44876b303c7671c1597d5.mp4 HTTP/1.1" 200 OK