Files
video-gen/video-gen-api/app/services/video_gen.py
T
2026-05-25 17:08:18 +08:00

218 lines
7.4 KiB
Python

import base64
import json
import logging
import mimetypes
import os
from datetime import datetime, timezone
import httpx
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from volcenginesdkarkruntime import AsyncArk
from app.config import settings
from app.models.video_engine import VideoEngine
from app.models.generation_record import GenerationRecord
from app.services.log_config import is_enabled, LOG_DIR, LOG_DATE_FORMAT, encrypt_data
from app.services.error_codes import extract_error_message
logger = logging.getLogger("videogen")
def _log_video_request(engine, record_id: str, request_data: dict):
"""Log video generation request to log/AiModel/YYYY-MM-DD.log"""
if not is_enabled():
return
try:
os.makedirs(LOG_DIR, exist_ok=True)
today = datetime.now().strftime(LOG_DATE_FORMAT)
log_file = os.path.join(LOG_DIR, f"{today}.log")
request_str = json.dumps(request_data, ensure_ascii=False)
request_encrypted = encrypt_data(request_data)
entry = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"type": "video_gen_request",
"engine": engine.name,
"model": engine.model_name,
"record_id": record_id,
"request": request_encrypted,
"request_length": len(request_str),
}
with open(log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
except Exception:
pass
def _log_video_response(record_id: str, response_data: dict, error: str | None = None):
"""Log video generation response to log/AiModel/YYYY-MM-DD.log"""
if not is_enabled():
return
try:
os.makedirs(LOG_DIR, exist_ok=True)
today = datetime.now().strftime(LOG_DATE_FORMAT)
log_file = os.path.join(LOG_DIR, f"{today}.log")
response_encrypted = encrypt_data(response_data) if response_data else ""
entry = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"type": "video_gen_response",
"record_id": record_id,
"response": response_encrypted,
"error": error,
}
with open(log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
except Exception:
pass
async def get_active_engine(db: AsyncSession) -> VideoEngine:
"""Get the active video engine with highest priority."""
result = await db.execute(
select(VideoEngine)
.where(VideoEngine.is_active == True)
.order_by(VideoEngine.priority.desc())
.limit(1)
)
engine = result.scalar_one_or_none()
if not engine:
raise ValueError("没有可用的视频引擎,请联系管理员配置")
return engine
def _resolve_url(url: str) -> str:
"""Convert local path to base64 data URI, pass through remote URLs."""
# if url.startswith("http"):
# return url
# # Local file: read and encode as base64 data URI
# file_path = os.path.join(settings.UPLOAD_LOCAL_PATH, url.replace("/uploads/", ""))
# if not os.path.exists(file_path):
# return url
# mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
# with open(file_path, "rb") as f:
# b64 = base64.b64encode(f.read()).decode()
# return f"data:{mime};base64,{b64}"
return settings.BASE_URL + url
async def submit_video_task(
db: AsyncSession,
engine: VideoEngine,
record: GenerationRecord,
) -> str:
"""Submit a video generation task via Ark SDK. Returns task_id."""
client = AsyncArk(
base_url=engine.api_base,
api_key=engine.api_key,
)
content = [{"type": "text", "text": record.optimized_prompt}]
# Add reference images/videos from media_references
if record.media_references:
try:
refs = json.loads(record.media_references)
for ref in refs:
ref_type = ref.get("type")
ref_url = ref.get("url", "")
if ref_type == "image" and ref_url:
resolved = _resolve_url(ref_url)
content.append({"type": "image_url", "image_url": {"url": resolved},"role":"reference_image"})
elif ref_type == "video" and ref_url:
resolved = _resolve_url(ref_url)
content.append({"type": "video_url", "video_url": {"url": resolved},"role":"reference_video"})
except (json.JSONDecodeError, TypeError):
pass
request_payload = {
"model": engine.model_name,
"content": content,
"ratio": record.aspect_ratio,
"duration": record.duration,
"resolution": record.resolution,
"generate_audio": True,
"watermark": False,
}
# Log request to AiModel log
_log_video_request(engine, record.id, request_payload)
try:
result = await client.content_generation.tasks.create(**request_payload)
task_id = result.id
_log_video_response(record.id, {"task_id": task_id})
except Exception as e:
_log_video_response(record.id, {}, str(e))
raise
finally:
await client.close()
return task_id
async def poll_task_status(engine: VideoEngine, task_id: str) -> dict:
"""Query task status via Ark SDK. Returns {status, video_url, response_data}."""
client = AsyncArk(
base_url=engine.api_base,
api_key=engine.api_key,
)
result = await client.content_generation.tasks.get(task_id=task_id)
await client.close()
# Serialize response
response_dict = {
"id": result.id,
"model": result.model,
"status": result.status,
"created_at": result.created_at,
"updated_at": result.updated_at,
}
video_url = None
video_tokens = 0
if result.status == "succeeded" and result.content:
video_url = getattr(result.content, "video_url", None)
response_dict["video_url"] = video_url
response_dict["duration"] = getattr(result, "duration", None)
response_dict["ratio"] = getattr(result, "ratio", None)
response_dict["resolution"] = getattr(result, "resolution", None)
# Extract usage info if present
usage = getattr(result, "usage", None)
if usage:
response_dict["usage"] = {
"input_tokens": getattr(usage, "input_tokens", 0),
"output_tokens": getattr(usage, "output_tokens", 0),
"total_tokens": getattr(usage, "total_tokens", 0),
}
video_tokens = getattr(usage, "total_tokens", 0)
elif result.status == "failed":
response_dict["error"] = str(getattr(result, "error", "视频生成失败"))
return {
"status": result.status,
"video_url": video_url,
"video_tokens": video_tokens,
"response_data": json.dumps(response_dict, ensure_ascii=False, default=str),
"error": response_dict.get("error"),
}
async def download_video(video_url: str, dest_path: str) -> str:
"""Download video to local storage."""
import os
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
async with httpx.AsyncClient(timeout=300) as client:
async with client.stream("GET", video_url) as response:
response.raise_for_status()
with open(dest_path, "wb") as f:
async for chunk in response.aiter_bytes(chunk_size=8192):
f.write(chunk)
return dest_path