"""Sora 2 video generation — OpenAI Videos API. POST /v1/videos → poll GET /v1/videos/{id} → GET /v1/videos/{id}/content download. ⚠️ Deprecated, shutdown 2026-09-24. Spec 진행은 박재오 결정 따름. """ from __future__ import annotations import logging import os import time from typing import Optional import requests from nas_client import webhook_update_task logger = logging.getLogger(__name__) SORA_BASE_URL = "https://api.openai.com/v1" VIDEO_MEDIA_ROOT = os.getenv("VIDEO_MEDIA_ROOT", "/mnt/nas/webpage/data/video") VIDEO_MEDIA_URL_PREFIX = os.getenv("VIDEO_MEDIA_URL_PREFIX", "/media/video") POLL_INTERVAL = 15 # OpenAI 권장: 10~20초 POLL_MAX_ATTEMPTS = 40 # 최대 ~10분 DEFAULT_MODEL = "sora-2" def _headers() -> dict: api_key = os.getenv("OPENAI_API_KEY", "") return { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } def run_sora_generation(task_id: str, params: dict) -> None: """Sora 2로 영상 생성 → mp4 → NAS SMB 저장 → webhook.""" try: if not os.getenv("OPENAI_API_KEY"): webhook_update_task(task_id, "failed", 0, "", error="OPENAI_API_KEY 미설정 (Windows .env)") return webhook_update_task(task_id, "processing", 5, "Sora API 호출 중...") payload = { "model": params.get("model") or DEFAULT_MODEL, "prompt": params["prompt"][:5000], } if params.get("duration"): payload["seconds"] = params["duration"] if params.get("size"): payload["size"] = params["size"] elif params.get("aspect_ratio") == "9:16": payload["size"] = "1080x1920" elif params.get("aspect_ratio") == "16:9": payload["size"] = "1920x1080" resp = requests.post(f"{SORA_BASE_URL}/videos", headers=_headers(), json=payload, timeout=30) if resp.status_code not in (200, 201): webhook_update_task(task_id, "failed", 0, "", error=f"Sora API 오류: {resp.status_code} {resp.text[:300]}") return body = resp.json() video_id = body.get("id", "") if not video_id: webhook_update_task(task_id, "failed", 0, "", error="Sora 응답에 video id 없음") return webhook_update_task(task_id, "processing", 15, f"Sora 작업 생성됨 (id={video_id[:16]})") # 폴링 for attempt in range(POLL_MAX_ATTEMPTS): time.sleep(POLL_INTERVAL) sr = requests.get(f"{SORA_BASE_URL}/videos/{video_id}", headers=_headers(), timeout=30) if sr.status_code != 200: continue sd = sr.json() status = sd.get("status", "") progress = sd.get("progress", 0) scaled = min(15 + int(progress * 0.65), 79) webhook_update_task(task_id, "processing", scaled, f"Sora 생성 중... {progress}%") if status == "completed": break elif status == "failed": err = sd.get("error", {}).get("message", "Sora 작업 실패") webhook_update_task(task_id, "failed", 0, "", error=err) return else: webhook_update_task(task_id, "failed", 0, "", error="Sora 폴링 timeout (10분)") return # 다운로드 webhook_update_task(task_id, "processing", 80, "Sora 결과 다운로드 중...") filename = f"{task_id}.mp4" os.makedirs(VIDEO_MEDIA_ROOT, exist_ok=True) file_path = os.path.join(VIDEO_MEDIA_ROOT, filename) dl = requests.get( f"{SORA_BASE_URL}/videos/{video_id}/content", headers=_headers(), params={"variant": "video"}, stream=True, timeout=300, ) dl.raise_for_status() with open(file_path, "wb") as f: for chunk in dl.iter_content(chunk_size=8192): f.write(chunk) local_url = f"{VIDEO_MEDIA_URL_PREFIX}/{filename}" webhook_update_task(task_id, "succeeded", 100, "Sora 생성 완료", video_url=local_url) except requests.Timeout: webhook_update_task(task_id, "failed", 0, "", error="Sora API 타임아웃") except Exception as e: logger.exception("Sora generation error task=%s", task_id) webhook_update_task(task_id, "failed", 0, "", error=str(e))