111 lines
4.5 KiB
Python
111 lines
4.5 KiB
Python
"""썰박스 상수.
|
||
|
||
설정(SsulboxSettings)이 아니라 코드와 함께 고정되는 값들. 시나리오 ↔ 엔진 폴더 매핑,
|
||
생성 잡 stdout 마커 파싱 규칙, 태스크 상태 enum이 여기 있다.
|
||
"""
|
||
|
||
import re
|
||
from enum import Enum
|
||
from typing import Final
|
||
|
||
from config import apikey_settings
|
||
|
||
# =============================================================================
|
||
# 시나리오 ↔ 생성 엔진 폴더
|
||
# =============================================================================
|
||
# 프론트가 보내는 시나리오 코드를 generator/ 하위 엔진 디렉터리명으로 옮긴다.
|
||
# 엔진을 추가하면 여기와 프론트 ssulData.ts 양쪽을 함께 고쳐야 한다.
|
||
SCENARIO_ENGINE: Final[dict[str, str]] = {
|
||
"joseon": "animation",
|
||
"samgukji": "animation_samgukji",
|
||
"greek": "animation_greekroman",
|
||
"odyssey": "animation_odyssey",
|
||
}
|
||
|
||
ENGINE_SCENARIO: Final[dict[str, str]] = {v: k for k, v in SCENARIO_ENGINE.items()}
|
||
|
||
SCENARIOS: Final[tuple[str, ...]] = tuple(SCENARIO_ENGINE)
|
||
|
||
|
||
# =============================================================================
|
||
# 생성 잡 상태
|
||
# =============================================================================
|
||
class SsulTaskStatus(str, Enum):
|
||
"""생성 잡의 권위 상태. 인메모리 진행률(step)과 달리 DB가 진실 원천이다."""
|
||
|
||
QUEUED = "queued"
|
||
RUNNING = "running"
|
||
DONE = "done"
|
||
ERROR = "error"
|
||
|
||
|
||
#: 프로세스 기동 시 고아로 판정해 환불·정리할 상태들
|
||
ORPHAN_STATUSES: Final[tuple[str, ...]] = (
|
||
SsulTaskStatus.QUEUED.value,
|
||
SsulTaskStatus.RUNNING.value,
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# 생성 엔진 stdout 파싱
|
||
# =============================================================================
|
||
#: 엔진이 뱉는 진행 마커. 예) "[2/4] 스토리보드 생성"
|
||
STEP_RE: Final[re.Pattern[str]] = re.compile(r"\[(\d)\s*/\s*4\]")
|
||
|
||
#: 완료 마커. 예) "완성: output/animation/xxx/final.mp4"
|
||
DONE_RE: Final[re.Pattern[str]] = re.compile(r"완성[::]\s*(.+\.mp4)")
|
||
|
||
#: 업장명 마커. 예) "■ 가게: 골목냉면 / 소재 5줄 → 키워드로 사용"
|
||
#: place URL 을 직접 붙여넣어 검색을 거치지 않은 경우, 업장명을 얻을 수 있는
|
||
#: 유일한 경로다(엔진이 네이버 브리핑에서 뽑아 찍는다).
|
||
#: 주소는 찍지 않으므로 이 경로에서는 region 을 채울 수 없다.
|
||
STORE_RE: Final[re.Pattern[str]] = re.compile(r"가게\s*[::]\s*([^/\n]+?)\s*(?:/|$)")
|
||
|
||
#: 작업 폴더 마커. 예) "■ 작업 폴더 : C:\...\output\animation\골목냉면20260729"
|
||
#: 실패 시 정리할 대상을 알아내는 유일한 수단이다 — 실패하면 완료 마커가
|
||
#: 없어 mp4 경로로 폴더를 역산할 수 없다.
|
||
JOB_DIR_RE: Final[re.Pattern[str]] = re.compile(r"작업\s*폴더\s*[::]\s*(.+)")
|
||
|
||
#: step 1~4에 대응하는 사람이 읽는 단계명 (프론트 진행 표시용)
|
||
STEP_NAMES: Final[tuple[str, ...]] = ("대본 생성", "스토리보드", "이미지·음성", "영상 합성")
|
||
|
||
TOTAL_STEPS: Final[int] = len(STEP_NAMES)
|
||
|
||
|
||
# =============================================================================
|
||
# 크레딧 원장 멱등 키
|
||
# =============================================================================
|
||
#: credit_transaction.job_type 값. (job_type, job_ref, type) 유니크로 중복 차감을 막는다.
|
||
JOB_TYPE_SSUL: Final[str] = "ssul"
|
||
JOB_TYPE_VIDEO: Final[str] = "video"
|
||
|
||
|
||
# =============================================================================
|
||
# Gemini API 키 판정
|
||
# =============================================================================
|
||
#: castad config 의 GEMINI_API_KEY 기본값이 플레이스홀더 문자열이라
|
||
#: `if not key` 형태의 가드가 항상 통과해버린다. 반드시 gemini_key() 로 판정할 것.
|
||
_PLACEHOLDER_KEYS: Final[frozenset[str]] = frozenset(
|
||
{
|
||
"",
|
||
"your-gemeni-api-key", # config.py 기본값 (오타 그대로)
|
||
"your-gemini-api-key",
|
||
"none",
|
||
}
|
||
)
|
||
|
||
|
||
def gemini_key() -> str | None:
|
||
"""실제로 사용 가능한 Gemini API 키를 반환한다. 미설정이면 None.
|
||
|
||
Returns:
|
||
설정된 키. 플레이스홀더이거나 비어 있으면 None.
|
||
"""
|
||
key = (apikey_settings.GEMINI_API_KEY or "").strip()
|
||
return None if key.lower() in _PLACEHOLDER_KEYS else key
|
||
|
||
|
||
def is_generation_available() -> bool:
|
||
"""생성 파이프라인을 돌릴 수 있는 상태인지 여부."""
|
||
return gemini_key() is not None
|