"""썰박스 상수. 설정(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