트렌딩 딥다이브 · 2026-07-18 · OSSInsight

Forget-C/Jellyfish 딥다이브
— AI가 만든 결과물을 "재사용 가능한 자산"으로 승격시키는 숏드라마 제작 파이프라인

Jellyfish대본 텍스트 한 덩이를 넣으면 샷 분할 → 등장인물·장소·소품 추출 → 일관성 검사 → 프레임 이미지 생성 → 영상 생성까지 이어지는 숏드라마(짧은 세로형 드라마) 제작 워크스페이스다. 이런 프로젝트는 보통 "AI로 영상 만들기" 데모에 그치는데, 이 저장소가 흥미로운 이유는 영상 생성이 아니라 일관성 관리를 1급 문제로 놓고 데이터베이스 스키마 자체를 그쪽에 맞춰 설계했다는 점이다. 캐릭터·장면·소품·의상이 각각 독립 테이블이고, AI가 뽑아낸 후보와 사람이 확정한 자산이 서로 다른 테이블로 분리돼 있다. 게다가 정작 GPU 코드가 한 줄도 없다 — 생성은 전부 외부 API에 위임하고, 이 저장소는 순수하게 오케스트레이션·상태관리·일관성 보증만 담당한다. 그래서 이 문서는 "AI로 영상 뽑는 법"이 아니라 신뢰할 수 없는 LLM 출력을 프로덕션 데이터로 안전하게 승격시키는 설계를 읽는 데 초점을 맞춘다. 비동기 태스크 시스템, LLM JSON 방어 파싱, 벤더 추상화까지 — 영상과 무관한 백엔드 개발자에게도 교보재다. (저장소: Forget-C/Jellyfish · Python + TypeScript · Apache-2.0 · 별 5,510 · 포크 952 · 최신 커밋 2026-04-20 · 소스 약 7.7만 줄)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 주목받는가 — 일관성이라는 진짜 병목
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석 — 13개 에이전트와 단일 태스크 엔트리
  5. 디렉토리 구조 해부
  6. 학습 포인트 — 기술별 배울 것
  7. 시스템 · 실행 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

이 저장소가 정확히 무엇인가
한 문장으로

대본을 넣으면 "촬영 준비가 끝난 샷 목록"이 나오는 드라마 제작 관리 시스템이다. 영상 생성기가 아니라, 영상 생성기를 부리는 제작부(制作部)다.

Jellyfish는 FastAPI(백엔드) + React(프론트) + Celery(비동기 작업)로 만든 웹 애플리케이션이다. 사용자가 프로젝트를 만들고 챕터 대본을 붙여넣으면, 여러 LLM 에이전트가 순서대로 돌면서 대본을 샷으로 쪼개고, 등장인물과 장소와 소품을 뽑아내고, 서로 충돌하는 표기를 병합하고, 각 샷의 첫 프레임·끝 프레임 이미지 생성용 프롬프트를 써준다.

그리고 그 결과를 버리지 않고 데이터베이스에 자산으로 남긴다. 3화에서 등장한 "붉은 코트를 입은 여주인공"은 7화에서도 같은 캐릭터 레코드를 참조한다. 이게 이 프로젝트의 존재 이유 전부다.

비유

영화 촬영장에는 스크립터(script supervisor)라는 직책이 있다. 배우가 어제 왼손에 들었던 잔을 오늘도 왼손에 들고 있는지, 3번 씬에서 찢어진 소매가 5번 씬에도 찢어져 있는지를 집요하게 기록하고 확인하는 사람이다. 관객은 그 존재를 눈치채지 못하지만, 없으면 영화가 무너진다.

Jellyfish는 AI 영상 제작판 스크립터다. 이미지 생성 모델은 매번 새로 그리기 때문에 같은 인물을 두 번 그리면 다른 얼굴이 나온다. 그래서 "이 캐릭터는 이 참조 이미지, 이 설명, 이 의상 타임라인을 쓴다"를 DB에 못 박아두고 매번 그걸 물려준다.

용어
숏드라마 / 마이크로드라마 (短剧, vertical drama)
1편 1~2분짜리 세로형 짧은 드라마. 중국에서 폭발적으로 성장한 포맷으로, 한 작품이 60~100편으로 구성된다. 편수가 많아서 제작 속도가 생명이고, 그래서 AI 생성과 궁합이 좋다 — 동시에 편수가 많으니 일관성 관리가 지옥이 된다. Jellyfish가 겨냥한 지점이 정확히 여기다.
용어
분경 / 분镜 (shot division, 스토리보드 분할)
대본을 "카메라 한 번 돌릴 단위"로 쪼개는 작업. 소설처럼 흐르는 문장을 샷 1: 낮, 골목, 여자가 뛴다 식의 촬영 단위로 변환한다. Jellyfish의 첫 번째 에이전트가 하는 일이 바로 이것이다.

2왜 주목받는가 — 일관성이라는 진짜 병목

트렌딩 이유와 경쟁 제품 대비 차별점

AI 영상의 진짜 어려움은 "생성"이 아니다

Runway, Pika, Kling, Sora 같은 도구는 이미 한 컷은 훌륭하게 만든다. 그런데 60편짜리 드라마를 만들려고 하면 문제가 완전히 달라진다.

어려움단일 생성기(Runway·Pika 등)Jellyfish의 접근
같은 인물 유지매번 프롬프트를 사람이 복붙. 40편쯤 가면 관리 불능Character 테이블 + CharacterImage 참조 이미지를 DB에 저장, 자동 주입
대본 → 샷 변환사람이 수동으로 쪼갬ScriptDividerAgent가 자동 분할 후 shots 테이블에 upsert
샷 간 연결없음. 각 컷이 독립프레임 프롬프트 에이전트가 앞/뒤 샷 정보(continuity_guidance)를 받아 연속성 반영
표기 흔들림"주인공/여자/그녀"가 다 다른 인물로 처리됨EntityMergerAgent가 별칭(aliases)을 병합해 전역 ID 부여
긴 작업 추적탭 닫으면 끝GenerationTask 테이블 + Celery. 진행률·취소·재조회 가능
핵심 통찰
"AI 출력을 자산으로 승격시킨다"

대부분의 AI 툴은 결과물을 일회용으로 다룬다. 만들고, 저장하고, 끝. Jellyfish는 반대로 간다 — AI가 뽑아낸 캐릭터·소품·장면을 프로젝트 전체가 재사용하는 영속 데이터로 만든다.

이게 왜 중요하냐면, 이 구조에서는 AI가 좋아질수록 자산의 가치가 올라가는 게 아니라, 자산이 쌓일수록 AI 출력의 품질이 올라가기 때문이다. 데이터 플라이휠이 프로젝트 안에서 돈다.

벤더 락인을 피한 설계

Jellyfish는 특정 AI 회사에 묶여 있지 않다. 텍스트 LLM은 전부 OpenAI 호환 프로토콜로 통일했고(langchain_openai.ChatOpenAI 하나만 씀), 이미지·영상은 core/contracts/에 벤더 중립 계약을 정의한 뒤 OpenAI와 Volcengine(중국 바이트댄스 계열) 어댑터를 각각 구현했다.

더 중요한 건 Provider와 Model이 코드가 아니라 DB 테이블이라는 점이다. 관리자 화면에서 API 키와 base_url을 넣으면 새 공급자가 등록된다. 자체 호스팅 LLM을 꽂아도 되고, 로컬 vLLM을 붙여도 된다.

냉정하게 볼 점
최신 커밋이 2026-04-20 — 약 3개월째 멈춰 있다

별 5,510개에 포크 952개인데도 업데이트가 정체된 상태다. 실무 도입을 검토한다면 유지보수 리스크를 감안해야 한다. 다만 공부 목적으로는 오히려 안정적이다 — 구조가 바뀌지 않으니 읽은 내용이 유효하다.

또 하나: pyproject.tomllanggraph가 의존성으로 들어 있지만, 실제 코드에서 StateGraph 같은 그래프 구성은 발견되지 않았다. 파이프라인은 그래프가 아니라 Celery 태스크 단위로 순차 호출되는 독립 에이전트들이다. README나 의존성 목록만 보고 "LangGraph 프로젝트"라고 판단하면 틀린다.

읽는 방법 제안

이 저장소를 "영상 프로젝트"로만 보면 관심 범위가 좁아진다. "외부 API에 오래 걸리는 작업을 위임하고, 그 결과를 신뢰할 수 없는 상태로 받아서, 사람 검수를 거쳐 정식 데이터로 만드는 시스템"으로 읽으면 — 이건 결제 정산, 문서 OCR 파이프라인, 데이터 라벨링 툴에 그대로 적용되는 패턴이다.

3기술 스택 전체 지도

백엔드 · 프론트엔드 · 인프라 · AI 연동을 버전까지

백엔드 — Python 3.11+, uv로 관리

backend/pyproject.toml을 그대로 읽어 각 의존성이 코드 어디서 실제로 쓰이는지까지 정리했다. 의존성 목록만 보면 알 수 없는 부분이 많다.

패키지버전실제 사용처
fastapi≥0.115app/main.py 엔트리, api/v1/routes/* 전체
uvicorn[standard]ASGI 서버. Dockerfile의 실행 명령
celery[redis]≥5.4core/celery_app.py (브로커=Redis), tasks/execute_task.py
langchain / langchain-core≥0.3chains/agents/base.pycreate_agent + ToolStrategy 구조화 출력
langgraph≥0.2그래프 구성 코드 미발견. create_agent의 내부 의존이거나 확장 예약용으로 추정
langchain-openai(dev)services/llm/{resolver,runtime}.py — 텍스트 LLM 전부 ChatOpenAI로 통일
httpx≥0.27core/integrations/{openai,volcengine}/*.py — 이미지·영상은 SDK 안 쓰고 raw HTTP
sqlalchemy[asyncio]≥2.0models/*.py ORM 전체. 비동기 세션
aiosqlite로컬 개발 기본 DB (sqlite+aiosqlite:///./jellyfish.db)
aiomysql운영 배포 (mysql+aiomysql://...)
boto3core/storage.py — S3 호환 스토리지. anyio로 스레드풀 위임해 이벤트루프 블로킹 방지
pydantic / pydantic-settings≥2.0스키마 검증 전체, app/config.py 설정 로딩
cryptographyAPI 키 등 민감정보 처리용으로 추정 (직접 사용처 미확인)
jinja2프롬프트 템플릿 렌더링
용어
uv
Rust로 만든 초고속 Python 패키지 관리자. pip + venv + pip-tools를 하나로 합친 도구로, uv sync --frozenuv.lock에 잠긴 버전 그대로 설치한다. Jellyfish의 Dockerfile이 이걸 쓴다.

프론트엔드 — Vite + React + Ant Design

항목선택비고
빌드Vite 5 + TypeScript 5.2tsc && vite build
UIantd ^5.10관리자형 화면. 커스텀 디자인 시스템 없음
상태관리zustand ^5.0.11src/store/useAppStore.ts 단일 스토어
라우팅react-router-dom ^6.30
i18ni18next + react-i18nextlocales/{en-US,zh-CN}
HTTPaxios + openapi-typescript-codegen아래 참고 — 이게 핵심
드래그앤드롭react-beautiful-dnd샷 순서 편집용
모킹msw개발·테스트용 Mock Service Worker
눈여겨볼 것
API 클라이언트를 손으로 안 쓴다

pnpm run openapi:updatecurl http://127.0.0.1:8000/openapi.json으로 FastAPI의 OpenAPI 스펙을 받아와 front/src/services/generated/TypeScript 클라이언트를 통째로 생성한다.

그리고 저장소의 AGENTS.md가 규칙으로 못 박았다 — "API를 바꾸면 반드시 이 커맨드를 실행할 것. 프론트는 수기 서비스 래퍼 대신 generated client만 쓸 것." 백엔드 스키마와 프론트 타입이 어긋날 수 없는 구조다.

인프라 — docker-compose 8개 컨테이너

docker-compose 구성 (deploy/compose/docker-compose.yml) ┌──────────────┐ ┌──────────────┐ │ front │ │ backend │ │ nginx:1.27 │──API──▶│ FastAPI │ │ :7788 │ │ :8000 │ └──────────────┘ └──────┬───────┘ ▲ │ │ window.__ENV 런타임 주입 │ 태스크 enqueue │ (docker-entrypoint.d/) ▼ │ ┌──────────────┐ │ │ redis:7 │ Celery 브로커 │ │ │ (result backend 미사용) │ └──────┬───────┘ │ │ │ ┌──────▼───────┐ │ │ celery-worker│ 동일 이미지, worker 모드 │ └──────┬───────┘ │ │ │ ┌──────────────┼──────────────┐ │ ▼ ▼ │ ┌──────────────┐ ┌──────────────┐ └──│ rustfs │ │ mysql:9.0 │ │ S3 호환 스토리지 │ │ │ │ :9000/:9001 │ └──────▲───────┘ └──────────────┘ │ ┌──────────┴──────────┐ │ backend-init-db │ 1회성 │ mysql-init-sql │ 1회성 └─────────────────────┘
용어
RustFS
Rust로 작성된 S3 호환 오브젝트 스토리지. MinIO가 라이선스를 AGPL로 바꾼 뒤 대안으로 주목받는 프로젝트다. Jellyfish는 이걸 기본으로 쓰지만 boto3 기반이라 실제 AWS S3나 MinIO로 바꿔 꽂아도 그대로 동작한다.
특이점
DB 마이그레이션에 Alembic을 안 쓴다

Python 생태계 표준인 Alembic 대신, backend/sql/001-*.sql ~ 008-*.sql 순번 SQL 파일을 mysql-init-sql 컨테이너가 순서대로 적용하고, init_db.py가 SQLAlchemy create_all로 테이블을 만든다.

장점은 단순함이고, 단점은 다운그레이드(롤백)가 없고 이미 적용된 마이그레이션 추적 테이블이 없다는 것이다. 개인·소규모 프로젝트에서는 흔한 선택이지만, 팀 규모가 커지면 반드시 문제가 되는 지점이라 "왜 Alembic을 쓰는가"를 역으로 이해하기 좋은 반례다.

CI/CD — GitHub Actions 6개 워크플로

워크플로역할
ghcr-images.ymlmain/태그 push 시 backend·front 이미지를 GHCR에 빌드·푸시. QEMU + Buildx 멀티플랫폼, GHA 캐시
backend-pylint.yml백엔드 린트
commit-messages.yml커밋 메시지 규칙 검사[docs], [feat] 같은 프리픽스 강제
deploy-site.ymlsite/ 문서 사이트 배포
issues-stale.yml오래된 이슈 자동 정리
tag-release.yml릴리스 태깅

AI 모델 연동 — 텍스트 / 이미지 / 영상 3계층

종류구현지원 공급자
텍스트 LLMlangchain_openai.ChatOpenAI 하나로 통일OpenAI 호환이면 무엇이든. 내장 등록: OpenAI, 알리바바 百炼(DashScope 호환 엔드포인트)
이미지core/integrations/*/images.py, httpx 직접 호출OpenAI /images/generations·/images/edits (예: gpt-image-1.5) / Volcengine 火山方舟 (예: doubao-seedream-*)
영상core/integrations/*/video.py, 생성 후 폴링OpenAI /videos (Sora 계열 추정) / Volcengine /contents/generations/tasks (Seedance 계열 추정)

이미지·영상 어댑터는 둘 다 "생성 요청 → task_id 반환 → GET으로 폴링"이라는 동일한 패턴을 쓰고, core/contracts/image_generation.pyImageGenerationInput/ImageGenerationResult라는 공통 계약으로 추상화된다. 새 공급자를 추가할 때 계약만 지키면 나머지 시스템은 손댈 필요가 없다.

4아키텍처 심화 분석

13개 에이전트, 단일 Celery 엔트리, 그리고 일관성 관리의 실체

4-1. 요청 하나가 흘러가는 전체 경로

가장 대표적인 흐름 — "대본을 샷으로 쪼개줘"를 따라가 보자.

[1] 프론트 POST /api/v1/script-processing/divide-async { chapter_id, script_text } │ ▼ [2] 라우트 api/v1/routes/script_processing.py services/script_processing_tasks.py ├─ create_divide_task() → GenerationTask 레코드 INSERT │ task_kind="script_divide" │ mode=async_polling, progress=0 └─ spawn_divide_task() → enqueue_task_execution(task_id) │ ▼ Celery에 "task.execute" 디스패치 (인자는 task_id 하나뿐!) [3] Celery worker tasks/execute_task.py::run_task_celery DB에서 task_kind 읽음 → 레지스트리 조회 services/worker/task_registry.py │ ▼ [4] 실행기 DivideTaskExecutor.execute() script_processing_worker.py └─ DivideResultGenerator.generate_with_llm() │ ▼ [5] 에이전트 ScriptDividerAgent(llm).divide_script() chains/agents/script_divider_agent.py └─ AgentBase.extract() ├─ 1차: create_agent + ToolStrategy(구조화 출력) └─ 실패 시: run() 원시 텍스트 → 5단계 JSON 복구 파서 │ ▼ [6] 결과 반영 write_to_db=true 이면 services/studio/script_division.py::apply_division_result └─ ShotDivisionResult → shots 테이블 UPSERT └─ GenerationTask.result(JSON 컬럼)에 저장, progress=100 │ ▼ [7] 프론트 상태 폴링(task-status)으로 결과 회수 ※ SSE/WebSocket 아님
설계 포인트
Celery 태스크가 딱 하나뿐이다

보통은 작업 종류마다 Celery 태스크를 만든다 — divide_script, generate_image, merge_entities... 그러면 태스크가 늘어날 때마다 Celery 라우팅 설정을 건드려야 한다.

Jellyfish는 Celery 태스크를 task.execute 하나만 등록하고, 인자로 task_id만 넘긴다. 워커는 DB에서 task_kind를 읽어 task_registry에서 실행기를 동적으로 찾는다. 새 작업을 추가할 때 실행기 클래스 하나만 등록하면 끝이다.

Registry + Strategy 패턴의 아주 깔끔한 실전 적용 사례다.

4-2. 13개 LLM 에이전트 전수 조사

backend/app/chains/agents/ 안의 에이전트를 전부 정리했다. 각각 입력과 출력(Pydantic 모델)이 명확하게 정의돼 있다.

에이전트하는 일출력
AgentBase공통 베이스 — 프롬프트 렌더링 + LLM 호출 + JSON 복구 파싱(추상)
ScriptDividerAgent전체 대본 → 샷 단위 분할ScriptDivisionResult (shots[]: index/start_line/shot_name/time_of_day)
ElementExtractorAgent분할 결과 → 캐릭터·장면·소품·의상·대사 전체 초안StudioScriptExtractionDraft
EntityMergerAgent여러 샷의 추출 결과를 교차 병합, 전역 ID 부여, 충돌 식별EntityMergeResult (merged_library, conflicts[])
VariantAnalyzerAgent의상·외형 변이 타임라인 분석VariantAnalysisResult
ConsistencyCheckerAgent원문에서 "같은 인물이 다른 정체성으로 뒤섞이는" 혼동 탐지 (동명이인, 대명사 오귀속)ScriptConsistencyCheckResult (issues[])
ScriptOptimizerAgent발견된 일관성 이슈 겨냥해 대본 수정ScriptOptimizationResult
ScriptSimplifierAgent스토리 유지하며 분량 압축ScriptSimplificationResult
CharacterPortraitAnalysisAgent캐릭터 초상 설명 → 구조화 스펙*AnalysisResult
CostumeInfoAnalysisAgent의상 설명 → 구조화 스펙동일
PropInfoAnalysisAgent소품 설명 → 구조화 스펙동일
SceneInfoAnalysisAgent장면 설명 → 구조화 스펙동일
ShotFirstFrame / LastFrame / KeyFramePromptAgent샷 메타 30여 필드 → 첫/끝/키 프레임 이미지 생성 프롬프트 작성ShotFramePromptResult(prompt)
특히 인상적인 것

프레임 프롬프트 에이전트의 "연출 규칙"

첫 프레임과 끝 프레임은 그냥 "처음"과 "끝"이 아니다. 영상 생성 모델은 두 프레임 사이를 보간하기 때문에, 첫 프레임이 이미 동작을 완료한 상태로 그려지면 영상에 움직임이 없어진다.

shot_frame_prompt_agents.py는 이걸 프롬프트에 명시적으로 박아뒀다:

# chains/agents/shot_frame_prompt_agents.py
_FRAME_FOCUS = {
    "首帧": "...只表现触发瞬间或最初反应,
             不要直接写成后续完成动作、最终姿态或情绪爆发完成态。",
    #  첫 프레임: 촉발 순간이나 최초 반응만 표현. 
    #  완료된 동작·최종 자세·감정 폭발 완료태로 쓰지 말 것.

    "尾帧": "...强调动作收束、人物结束姿态、视线落点或情绪余韵。",
    #  끝 프레임: 동작의 수렴, 인물의 종료 자세, 시선의 착지점, 감정의 여운.

    "关键帧": "...不必平均描述整个过程。",
    #  키 프레임: 전 과정을 균등하게 서술할 필요 없음.
}

이건 프롬프트 엔지니어링이 아니라 영상 문법에 대한 도메인 지식이 코드에 녹아든 사례다. 이런 게 오픈소스를 읽을 때 진짜 얻는 것이다.

4-3. LLM은 완벽한 JSON을 주지 않는다 — 5단계 방어

chains/agents/base.py는 이 프로젝트에서 가장 배울 게 많은 파일이다. LLM에게 구조화된 출력을 요구했는데 이상한 게 왔을 때 포기하지 않는 5단계 폴백 체인을 구현했다.

LLM 응답 파싱 폴백 체인 (base.py) ① create_agent + ToolStrategy(output_model) └ 구조화 출력 성공? → 끝 │ 실패 ▼ ② _extract_json_from_text() └ ```json ... ``` 마크다운 펜스 벗겨내기 │ 여전히 실패 ▼ ③ _repair_json_like() ├ 유니코드 따옴표(" ") → ASCII 따옴표 ├ trailing comma 제거 {a:1,} → {a:1} └ _quote_unquoted_object_keys() {a:1} → {"a":1} │ 여전히 실패 ▼ ④ ast.literal_eval() └ 파이썬 리터럴로 해석 시도 (True/None 등도 처리) │ 여전히 실패 ▼ ⑤ _parse_python_call_kwargs() └ LLM이 Foo(a=1, b='x') 함수호출 스타일로 답한 경우까지 파싱 │ ▼ 전부 실패해야 비로소 에러
# chains/agents/base.py — 마지막 방어선
call_kwargs = _parse_python_call_kwargs(python_like)
if call_kwargs is not None:
    return call_kwargs
실무 교훈
"LLM은 규격을 지킬 것이다"라고 가정하지 마라

구조화 출력(structured output) 기능이 있다고 해서 100% 신뢰할 수 없다. 모델 버전이 바뀌거나, 프롬프트가 길거나, 토큰이 잘리면 형식이 무너진다.

Jellyfish의 접근은 "실패를 예외가 아니라 정상 경로의 일부로 설계"하는 것이다. 이건 LLM을 쓰는 모든 프로덕션 시스템에 그대로 필요한 태도다.

4-4. "일관성 관리"의 실체 — 벡터DB가 아니다

기대와 다른 부분
임베딩·벡터 검색이 아니라 문자열 매칭 + 사람 확인이다

"AI 일관성 관리"라고 하면 보통 임베딩 유사도나 벡터DB를 떠올린다. Jellyfish는 그렇지 않다. services/studio/entity_existence.pycheck_names_existence는 SQL ILIKE '%q%' — 그냥 대소문자 무시 부분일치다.

퍼지 매칭도, 임베딩도 없다. 대신 LLM이 병합 단계에서 이름 사전을 만들고, 이후 모든 샷이 그 사전의 정확한 문자열만 참조하도록 프롬프트로 강제한다.

그럼 어떻게 일관성이 유지되는가? 세 가지 장치의 조합이다.

① 엔티티 스키마에 별칭을 1급으로 넣는다

# schemas/skills/script_processing.py — EntityEntry
id                : str            # 전역 고유 ID
name              : str            # 정식 이름 (이후 모든 참조가 이걸 씀)
type              : str            # character / scene / prop / costume
aliases           : list[str]      # "그녀", "여주인공", "민서" 전부 여기로
normalized_name   : str
confidence        : float          # LLM의 확신도
first_appearance  : EvidenceSpan   # 대본 어디서 처음 나왔는지 (증거)
appearances       : list[...]
variants          : list[...]      # 의상·외형 변이

first_appearance증거(evidence)를 담는다는 점이 좋다. AI가 "이 인물이 있다"고 주장할 때 대본 몇 번째 줄이 근거인지를 함께 내놓게 만든 것이다. 사람이 검수할 때 이게 결정적이다.

② AI 후보와 확정 자산을 다른 테이블로 분리한다

Draft → Candidate → Confirm 워크플로 LLM 추출 결과 │ ▼ ┌─────────────────────────────┐ │ ShotExtractedCandidate │ AI가 뽑은 "후보" │ candidate_status: │ ← 아직 진짜 데이터 아님 │ pending / linked / ignored│ └──────────┬──────────────────┘ │ 사람이 화면에서 확인 │ "이건 3화의 그 캐릭터 맞음" → linked │ "이건 오탐" → ignored ▼ ┌─────────────────────────────┐ │ Character / Scene / Prop / │ 확정된 프로덕션 자산 │ Costume (+ *Image 참조이미지) │ ← 이제부터 재사용 └─────────────────────────────┘
이 패턴의 이름
Human-in-the-loop 게이팅

AI 출력을 곧바로 정식 테이블에 쓰지 않고, 중간 상태 테이블을 하나 두고 사람이 승격시키는 구조. 오탐(같은 이름 다른 인물, 지문을 인물로 착각 등)이 프로덕션 데이터를 오염시키는 걸 원천 차단한다.

이건 영상과 아무 상관 없이 모든 AI 데이터 파이프라인에 그대로 쓸 수 있는 패턴이다. 문서 OCR, 이력서 파싱, 인보이스 추출 — 전부 같은 문제를 가진다.

③ 병합은 재시도를 전제로 설계됐다

EntityMergerAgent의 입력을 보면 특이한 필드가 있다.

all_extractions_json      # 모든 샷의 추출 결과
historical_library_json   # 기존에 확정된 자산 라이브러리
script_division_json      # 샷 분할 정보
previous_merge_json       # ← 직전 병합 결과
conflict_resolutions_json # ← 사람이 내린 충돌 판정

마지막 두 개가 핵심이다. 병합이 충돌을 냈을 때 사람이 "A와 B는 같은 인물이다"라고 판정을 내려주면, 그 판정을 프롬프트에 넣어 다시 돌린다. Celery의 자동 재시도가 아니라 사람이 개입한 뒤의 수동 재시도다 — 그리고 이 도메인에서는 그게 맞다.

4-5. DB 스키마 핵심 구조

Project └─ Chapter (1:N) └─ Shot (1:N, chapter_id+index 유니크) ├─ ShotDetail (1:1, PK 공유) ├─ ShotFrameImage (frame_type 유니크: first/last/key) ├─ ShotDialogLine ├─ ShotCharacterLink (N:M → Character) └─ ShotExtractedCandidate (AI 후보, pending/linked/ignored) 엔티티 (프로젝트 횡단 재사용) Character ─ CharacterImage ─┐ Scene ─ SceneImage ─┤ Prop ─ PropImage ─┼─ FileItem (S3 실제 파일) Costume ─ CostumeImage ─┤ Actor ──────────────── ┘ └─ Project*Link (프로젝트 소속 연결) 태스크 GenerationTask 공용 비동기 작업 ├─ task_kind (실행기 결정) ├─ status / progress(0-100) / elapsed_ms ├─ cancel_requested / cancelled_at / cancel_reason └─ result (JSON) GenerationTaskLink 태스크 ↔ project/chapter/shot 다형 연결 모델 설정 (코드가 아니라 DB!) Provider ─ Model ─ ModelSettings(단일행, 전역 기본 모델)

4-6. 상태(status)를 셋으로 쪼갠 이유

AGENTS.md 규약이 스키마를 규정한 사례

저장소 루트의 AGENTS.md(약 8KB짜리 내부 규약 문서)는 상태의 의미를 세 가지로 명확히 분리하라고 못 박았고, 코드가 실제로 그걸 지킨다.

상태어디에의미
정보 확인 상태Shot.status (pending / ready)"이 샷의 정보 추출이 끝났는가". generating 값은 의도적으로 폐기됨
런타임 태스크 상태GenerationTask.status"지금 무슨 작업이 돌고 있는가". 진행률·취소 포함
영상 준비도shot_video_readiness.py"지금 영상 생성을 시작해도 되는가". DB에 저장 안 하고 매번 재계산
왜 이렇게 하나

상태 필드 하나에 모든 걸 욱여넣으면 반드시 터진다. Shot.status = "generating"으로 두면, 서버가 죽었을 때 그 샷은 영원히 generating에 갇힌다. 아무도 그걸 되돌려주지 않기 때문이다.

Jellyfish는 영속 상태(shot)는 사실만 담고, 휘발성 상태(task)는 별도 테이블에, 파생 상태(readiness)는 저장하지 않고 계산한다. 상태 관리 설계의 교과서적 분리다.

4-7. Celery + asyncio의 함정을 정면 대응

# core/celery_app.py
@worker_process_init.connect
def _reset_async_db_runtime(**_: object) -> None:
    reset_db_runtime()
용어
prefork fork 문제
Celery는 기본적으로 프로세스를 fork()해서 워커를 만든다. 그런데 부모 프로세스가 이미 만든 asyncio 이벤트루프와 DB 커넥션 풀은 fork 후 자식에서 깨진다. 여러 자식이 같은 소켓을 공유하면서 응답이 뒤섞이거나 데드락이 난다. 매우 흔하면서 디버깅이 지옥인 버그다.

해결책은 worker_process_init 시그널로 자식 프로세스가 시작될 때마다 DB 런타임을 새로 만드는 것이다. 세 줄짜리 코드지만, 이걸 몰라서 며칠을 태우는 사람이 많다.

추가로 task_ignore_result=True 설정이 눈에 띈다. Celery의 result backend를 아예 안 쓴다는 뜻이다 — 상태의 진실원(single source of truth)은 오직 GenerationTask 테이블 하나다. 상태가 두 군데 저장돼 어긋나는 문제를 원천 제거한 선택이다.

5디렉토리 구조 해부

어디부터 읽어야 하는가
Jellyfish/ ├── AGENTS.md ★ 내부 코딩·문서·상태 규약 (8KB, 먼저 읽을 것) ├── backend/ │ ├── app/ │ │ ├── main.py FastAPI 엔트리 │ │ ├── config.py pydantic-settings 기반 환경설정 │ │ ├── api/v1/routes/ HTTP 라우트 │ │ │ ├── studio.py 프로젝트/챕터/샷/엔티티 CRUD │ │ │ ├── film.py 영상·프레임 생성 │ │ │ ├── script_processing.py 대본 처리 (분할/추출/병합) │ │ │ ├── llm.py Provider/Model 관리 │ │ │ └── health.py │ │ ├── chains/agents/ ★ 13개 LLM 에이전트 (핵심) │ │ │ └── base.py ★ JSON 복구 파서 — 최고의 학습 자료 │ │ ├── core/ │ │ │ ├── celery_app.py ★ Celery 설정 + fork 대응 │ │ │ ├── db.py / db_sync.py 비동기/동기 세션 │ │ │ ├── storage.py S3 (boto3 + anyio 스레드풀) │ │ │ ├── contracts/ 벤더 중립 입출력 계약 │ │ │ │ ├── image_generation.py │ │ │ │ ├── video_generation.py │ │ │ │ └── provider.py │ │ │ ├── integrations/ 벤더별 HTTP 어댑터 │ │ │ │ ├── openai/{images,video}.py │ │ │ │ └── volcengine/{images,video}.py │ │ │ ├── task_manager/ 범용 태스크 추상 계층 │ │ │ │ ├── manager.py / strategies.py / stores.py / types.py │ │ │ └── tasks/registry.py provider별 어댑터 레지스트리 │ │ ├── models/ SQLAlchemy ORM │ │ │ ├── studio_projects.py / studio_shots.py / studio_assets.py │ │ │ ├── task.py ★ GenerationTask │ │ │ └── llm.py Provider / Model / ModelSettings │ │ ├── schemas/ Pydantic API 계약 │ │ │ └── skills/script_processing.py ★ EntityEntry 정의 │ │ ├── services/ │ │ │ ├── llm/ Provider 레지스트리·리졸버·런타임 │ │ │ ├── studio/ ★ 도메인 서비스 (가장 큼) │ │ │ │ ├── entity_existence.py 이름 매칭 │ │ │ │ ├── shot_extracted_candidates.py 후보 승격 │ │ │ │ ├── shot_video_readiness.py 준비도 재계산 │ │ │ │ └── generation/{asset_image,frame,video}/ │ │ │ ├── worker/ task_kind → 실행기 레지스트리 │ │ │ ├── film/ 프레임 프롬프트·영상 태스크 │ │ │ ├── script_processing_tasks.py 태스크 생성/디스패치 │ │ │ └── script_processing_worker.py 실행기 구현 │ │ └── tasks/execute_task.py ★ Celery 유일 진입점 │ ├── sql/ 001~008 순번 마이그레이션 │ ├── init_db.py create_all │ └── pyproject.toml / pytest.ini ├── front/ │ └── src/ │ ├── pages/aiStudio/ agents/assets/chapter/editor/files/ │ │ models/project/prompts/shots │ ├── services/generated/ ★ OpenAPI 자동생성 (직접 수정 금지) │ ├── store/useAppStore.ts zustand 단일 스토어 │ └── locales/{en-US,zh-CN} ├── deploy/ │ ├── compose/docker-compose.yml │ └── docker/{backend,front}.Dockerfile, nginx.conf, │ docker-entrypoint.d/10-generate-env-js.sh ├── site/ 문서 사이트 (guide/architecture/plans/reference/blog) └── .github/workflows/ 6개 CI 워크플로
읽는 순서 추천
이 순서로 4시간이면 전체 구조가 잡힌다

1시간차AGENTS.mdmodels/task.pytasks/execute_task.pyservices/worker/task_registry.py. 태스크 시스템의 뼈대가 보인다.

2시간차chains/agents/base.py 정독. 그다음 script_divider_agent.py 하나만 읽으면 나머지 12개는 같은 패턴이다.

3시간차schemas/skills/script_processing.pyEntityEntrymodels/studio_assets.pyservices/studio/shot_extracted_candidates.py. 일관성 관리의 데이터 흐름이 보인다.

4시간차core/contracts/ + core/integrations/ 비교. 추상화 계약이 어떻게 벤더 차이를 흡수하는지.

6학습 포인트 — 기술별 배울 것

이 저장소에서 실제로 가져갈 수 있는 것

① LLM 출력 방어 파싱 난이도 ★★☆

어디: chains/agents/base.py

구조화 출력 → 마크다운 펜스 제거 → 따옴표 정규화 → ast.literal_eval → 파이썬 함수호출 파싱. 5단계 폴백. LLM을 프로덕션에 쓰는 사람이라면 이 파일 하나만 읽어도 본전이다.

가져갈 것: "실패를 예외가 아니라 정상 경로의 일부로 설계한다"는 사고방식.

② Registry + Strategy로 확장점 만들기 난이도 ★★☆

어디: tasks/execute_task.py, services/worker/task_registry.py, core/tasks/registry.py

같은 패턴이 두 곳에서 재사용된다 — 태스크 실행기 레지스트리(task_kind → executor)와 벤더 어댑터 레지스트리(task_kind + provider_key → adapter). 새 기능을 추가할 때 기존 코드를 수정하지 않고 등록만 하면 되는 구조를 어떻게 만드는지 보여준다.

③ 벤더 중립 계약(contract) 설계 난이도 ★★☆

어디: core/contracts/image_generation.py vs core/integrations/{openai,volcengine}/images.py

두 벤더의 API는 엔드포인트도, 파라미터명도, 응답 형태도 다르다. 그런데 시스템의 나머지 부분은 ImageGenerationInput/Result만 안다. 인터페이스를 어느 수준에서 잘라야 하는지에 대한 좋은 실례다.

④ 상태 의미론의 분리 난이도 ★★★

어디: AGENTS.md + models/studio_shots.py + services/studio/shot_video_readiness.py

영속 상태 / 휘발성 상태 / 파생 상태를 나눈 것. 특히 파생 상태를 DB에 저장하지 않고 매번 계산하는 판단이 좋다. 캐시하면 빨라지지만 반드시 어긋난다.

가져갈 것: 상태 필드를 추가하기 전에 "이건 사실인가, 진행 중인 일인가, 다른 것으로부터 계산되는가"를 먼저 묻는 습관.

⑤ Human-in-the-loop 데이터 승격 난이도 ★★★

어디: ShotExtractedCandidate 모델 + services/studio/shot_extracted_candidates.py

AI 결과를 담는 후보 테이블과 확정 자산 테이블을 물리적으로 분리한 것. 이 하나가 "AI 툴"과 "AI를 쓰는 프로덕션 시스템"의 차이다.

⑥ 스키마 우선 개발과 코드 생성 난이도 ★★☆

어디: front/package.jsonopenapi:update + AGENTS.md 규칙

FastAPI가 자동 생성한 OpenAPI 스펙 → TypeScript 클라이언트 자동 생성. 백엔드 타입을 바꾸면 프론트가 컴파일 에러로 알려준다. 규약을 문서에만 쓰지 않고 도구로 강제한 점이 핵심이다.

⑦ Celery × asyncio 실전 대응 난이도 ★★★

어디: core/celery_app.pyworker_process_init 핸들러, task_ignore_result=True

fork 후 이벤트루프 재구축, result backend 미사용으로 상태 진실원 단일화. 둘 다 겪어본 사람만 아는 함정이다.

⑧ 규약 파일을 1급 산출물로 난이도 ★☆☆

어디: AGENTS.md

단순 코딩 컨벤션이 아니라 문서 카테고리 분류, 릴리스 노트 필수 섹션, 상태의 의미론까지 정의했다. AI 코딩 에이전트와 협업하는 저장소에서 규약 자체가 산출물이 되는 최근 흐름을 잘 보여준다.

실습 아이디어 (섹션 8에서 자세히)

7시스템 · 실행 요구사항

돌려보려면 무엇이 필요한가
중요
GPU가 필요 없다

백엔드에 로컬 추론 코드가 한 줄도 없다. 이미지·영상 생성은 전부 외부 API 호출이다. 즉 일반 노트북이나 소형 VM에서 전체 스택이 돈다. 대신 API 사용료가 든다.

항목요구사항
Python3.11 이상 (Docker는 3.12-slim)
Node20 이상 (프론트 빌드)
DBMySQL 9.0 (운영) / SQLite (로컬 개발 기본값)
메시지 브로커Redis 7 — Celery 브로커 전용, result backend 미사용
오브젝트 스토리지RustFS (기본) — S3·MinIO로 교체 가능
GPU불필요
API 키최소 텍스트 LLM 1개(OpenAI 호환). 이미지·영상 쓰려면 OpenAI 또는 Volcengine 키 추가
포트7788(프론트), 8000(API), 3306(MySQL), 6379(Redis), 9000/9001(RustFS)

docker-compose로 뜨는 컨테이너

컨테이너역할수명
mysqlMySQL 9.0상시
redisCelery 브로커상시
rustfsS3 호환 스토리지 + 콘솔상시
backend-init-dbinit_db.py로 테이블 생성1회성
mysql-init-sqlsql/001~008 순서 적용1회성
backendFastAPI (:8000)상시
celery-worker동일 이미지, worker 모드상시
frontnginx (:7788)상시

로컬 개발 실행

# 백엔드
cd backend
uv sync
uvicorn app.main:app --reload

# Celery 워커 (별도 터미널)
celery -A app.core.celery_app worker -l info

# 프론트 (별도 터미널)
cd front
pnpm install
pnpm dev

# API 바꿨으면 반드시 — AGENTS.md 규칙
pnpm run openapi:update
주의
비용이 조용히 새어 나갈 수 있다

영상 생성 API는 호출당 단가가 높다. 60편짜리 드라마의 모든 샷에 영상 생성을 돌리면 수백~수천 건의 API 호출이 발생한다. 실습할 때는 챕터 1개, 샷 2~3개로 제한하고, 공급자 대시보드에서 사용량 한도를 먼저 걸어두는 걸 권한다.

텍스트 처리(분할·추출·병합)까지만 돌려도 이 프로젝트의 학습 가치는 대부분 얻을 수 있다.

8직접 해볼 수 있는 실습 과제

난이도별 5개 — 위에서 아래로
과제 1 ★☆☆ · 2~3시간

JSON 복구 파서를 떼어내 라이브러리로 만들기

chains/agents/base.py에서 _extract_json_from_text, _repair_json_like, _quote_unquoted_object_keys, _parse_python_call_kwargs를 추출해 독립 모듈로 만든다.

그다음이 진짜 과제다 — 일부러 망가진 LLM 응답 20종(마크다운 펜스, trailing comma, 유니코드 따옴표, 키에 따옴표 없음, 중간에 잘린 JSON, 설명문이 앞에 붙은 경우…)을 만들어 pytest로 각 단계가 어디서 구제하는지 검증한다.

배우는 것: 폴백 체인의 각 단계가 실제로 어떤 실패를 잡는지 몸으로 이해하게 된다.

과제 2 ★★☆ · 반나절

텍스트 파이프라인만 로컬에서 완주하기

SQLite + 로컬 Redis로 백엔드를 띄우고, 이미지·영상 없이 대본 → 분할 → 추출 → 병합까지만 돌려본다. LLM은 저렴한 모델(또는 Ollama 같은 OpenAI 호환 로컬 서버)을 Provider로 등록한다.

짧은 한국어 대본(A4 1장, 등장인물 3명, 장면 4개)을 직접 써서 넣어보고, GenerationTask 테이블을 직접 SELECT해서 각 단계 결과 JSON을 눈으로 확인한다.

확인할 것: 한국어 대본에서 별칭 병합(EntityMergerAgent)이 얼마나 잘 되는가? 중국어 프롬프트로 작성된 에이전트가 한국어를 어떻게 다루는가?

과제 3 ★★☆ · 하루

새 이미지 공급자 어댑터 추가하기

core/contracts/image_generation.py의 계약을 읽고, 새 공급자(예: Stability AI, Replicate, 또는 로컬 ComfyUI HTTP API) 어댑터를 core/integrations/에 추가한다. register_task_adapter로 등록만 하면 기존 코드는 손대지 않아야 한다.

성공 기준: 기존 파일을 하나도 수정하지 않고 새 공급자가 동작하면 성공. 수정이 필요했다면 그 지점이 곧 추상화가 새는 곳이다 — 왜 샜는지 분석하는 게 이 과제의 진짜 목적이다.

과제 4 ★★★ · 2~3일

Candidate → Confirm 패턴을 다른 도메인에 이식

영상과 전혀 무관한 도메인을 하나 골라 같은 구조를 만든다. 추천: 이력서 PDF에서 경력·기술스택 추출 또는 영수증 이미지에서 항목 추출.

구현할 것:

① 원본 테이블 + ExtractedCandidate 테이블 (pending/linked/ignored)
② LLM 추출 시 first_appearance 같은 증거 필드 포함
③ 후보를 확정 엔티티로 승격시키는 서비스 함수
④ 같은 대상이 여러 번 추출됐을 때의 병합(별칭 관리)

배우는 것: Jellyfish의 설계가 영상 도메인 특수적인지, 일반적인 패턴인지 직접 검증하게 된다. 필자 생각엔 후자다.

과제 5 ★★★ · 3~5일

폴링을 SSE 스트리밍으로 바꾸기

현재 프론트는 태스크 상태를 폴링한다. 그런데 core/task_manager/strategies.py에는 이미 StreamingDeliveryStrategyAsyncPollingDeliveryStrategy와 나란히 준비돼 있다.

이 확장점을 실제로 살려서, 태스크 진행률을 SSE(Server-Sent Events)로 밀어주도록 만든다. 백엔드는 StreamingResponse, 프론트는 EventSource.

어려운 지점: Celery 워커는 API 서버와 다른 프로세스다. 워커의 진행률 변화를 API 프로세스가 어떻게 알 것인가? (힌트: Redis Pub/Sub. 이미 Redis가 떠 있다.)

배우는 것: 설계자가 남겨둔 확장점(인터페이스)이 실제로 확장 가능한지 검증하는 경험. 대부분의 "확장 가능한 설계"는 막상 확장하려면 안 된다.

9관련 기술 심화 학습 로드맵

6주 · 주차별 목표와 산출물
1주차

FastAPI + 비동기 SQLAlchemy 2.0

목표: async def 라우트에서 비동기 세션을 안전하게 쓰는 법. 세션 수명 관리(dependency injection), N+1 문제, selectinload.

Jellyfish에서 볼 곳: core/db.py, api/v1/routes/studio.py

산출물: 프로젝트→챕터→샷 3계층 CRUD API를 직접 처음부터 작성

2주차

Celery 비동기 태스크 시스템

목표: 브로커/워커/result backend의 역할 구분, prefork vs gevent, 태스크 취소와 타임아웃, 진행률 보고.

Jellyfish에서 볼 곳: core/celery_app.py, tasks/execute_task.py

산출물: 30초 걸리는 가짜 작업을 만들고 진행률 폴링 + 취소 기능까지 구현

함정 체험: 일부러 worker_process_init 핸들러 없이 비동기 DB를 써보고 무슨 일이 일어나는지 관찰

3주차

LLM 구조화 출력과 방어적 파싱

목표: Pydantic 모델을 LLM 출력 스키마로 쓰는 법, function calling / JSON mode / tool strategy의 차이, 그리고 전부 실패했을 때의 대응.

Jellyfish에서 볼 곳: chains/agents/base.py (이 주차의 핵심)

산출물: 과제 1(파서 라이브러리화) 완료

4주차

도메인 모델링 — 엔티티 · 별칭 · 버전

목표: "같은 것을 가리키는 여러 표현"을 어떻게 데이터로 다룰 것인가. 별칭 테이블, 정규화, 병합·분할(merge/split) 연산, 감사 로그.

Jellyfish에서 볼 곳: schemas/skills/script_processing.pyEntityEntry, models/studio_assets.py

확장 학습: Entity Resolution(개체 해소)이라는 정식 분야가 있다. Jellyfish는 LLM으로 이걸 우회했지만, 전통적 방법(레벤슈타인 거리, blocking, Fellegi-Sunter 모델)도 함께 보면 왜 LLM 방식을 택했는지 이해된다.

산출물: 과제 4(다른 도메인 이식) 착수

5주차

외부 API 오케스트레이션과 장애 대응

목표: 느리고 불안정한 외부 API를 다루는 법. 폴링 백오프, 타임아웃 계층화, 멱등성(idempotency) 키, 부분 실패 처리, 비용 추적.

Jellyfish에서 볼 곳: core/integrations/*/video.py의 폴링 루프, core/contracts/

비판적으로 볼 것: Jellyfish에는 지수 백오프나 서킷 브레이커가 명시적으로 없다. 있다면 어디에 넣어야 할까? 이 질문에 답할 수 있으면 이 주차는 성공이다.

산출물: 과제 3(새 공급자 어댑터) 완료

6주차

스키마 우선 개발과 프론트 자동화

목표: OpenAPI 스펙을 단일 진실원으로 삼는 개발 흐름. 코드 생성, 타입 안정성, CI에서 스펙 변경 감지.

Jellyfish에서 볼 곳: front/package.jsonopenapi:update, AGENTS.md

산출물: 1주차에 만든 CRUD API에 대해 TypeScript 클라이언트를 자동 생성하고, CI에서 "생성물이 최신인지" 검사하는 워크플로까지 작성

6주 뒤에 남는 것

이 로드맵을 다 돌면 "AI 영상 만드는 법"은 거의 안 배운다. 대신 느리고 불확실한 외부 시스템을 붙여서, 신뢰할 수 없는 출력을 받아, 사람 검수를 거쳐 정식 데이터로 만드는 백엔드를 설계할 수 있게 된다.

그게 지금 대부분의 "AI 제품"이 실제로 하는 일이다.

10핵심 키워드 사전

이 문서에 나온 용어 정리
용어
숏드라마 (短剧)1~2분짜리 세로형 짧은 드라마. 한 작품이 60~100편. 편수가 많아 일관성 관리가 핵심 난제.
분경 (分镜)대본을 카메라 단위(샷)로 쪼개는 작업. 스토리보드 분할.
Shot / ShotDetail촬영 단위. chapter_id + index로 유니크. 상세 정보는 1:1 테이블에 분리.
ShotFrameImage샷의 첫/끝/키 프레임 이미지. frame_type에 유니크 제약.
EntityEntry캐릭터·장면·소품·의상을 표현하는 스키마. aliases, first_appearance(증거), variants를 포함.
ShotExtractedCandidateAI가 뽑은 후보. pending/linked/ignored 상태. 확정 자산 테이블과 물리적으로 분리.
GenerationTask모든 비동기 작업의 공용 테이블. task_kind가 실행기를 결정. 상태의 단일 진실원.
task_kind작업 종류 문자열. Celery 태스크를 늘리지 않고 이 값으로 실행기를 동적 조회.
task registrytask_kind → 실행기 클래스 매핑. 새 작업 추가 시 등록만 하면 됨.
video readiness"지금 영상 생성을 시작해도 되는가"의 파생 상태. DB에 저장하지 않고 매번 재계산.
DeliveryStrategy결과 전달 방식 추상화. AsyncPolling(현재 사용) / Streaming(준비만 됨).
ToolStrategyLangChain에서 툴 호출 형태로 구조화 출력을 강제하는 전략.
JSON 복구 파서LLM의 깨진 출력을 5단계로 구제하는 폴백 체인 (base.py).
prefork fork 문제Celery가 fork()한 뒤 부모의 asyncio 이벤트루프·DB 풀이 자식에서 깨지는 현상. worker_process_init으로 대응.
task_ignore_resultCelery result backend를 쓰지 않는 설정. 상태를 DB 한 곳에만 두기 위한 선택.
Provider / ModelAI 공급자와 모델. 코드가 아니라 DB 테이블이라 관리 화면에서 추가 가능.
ModelSettings단일 행 테이블. 전역 기본 텍스트/이미지/영상 모델을 지정.
contract (계약)ImageGenerationInput/Result 같은 벤더 중립 입출력 정의. 어댑터가 이걸 구현.
RustFSRust로 만든 S3 호환 오브젝트 스토리지. MinIO 대안.
uvRust로 만든 초고속 Python 패키지 관리자. uv sync --frozen으로 락파일 그대로 설치.
openapi-typescript-codegenOpenAPI 스펙에서 TS 클라이언트를 생성하는 도구. 프론트는 이 생성물만 사용(규약).
window.__ENV 주입nginx 컨테이너 시작 시 환경변수를 JS로 만들어 주입하는 패턴. 이미지 재빌드 없이 설정 변경.
AGENTS.md저장소 루트의 내부 규약 문서. 코딩·문서·상태 의미론까지 정의. 스키마가 실제로 이걸 따름.
Human-in-the-loop 게이팅AI 출력을 중간 상태로 받고, 사람이 승인해야 정식 데이터가 되는 구조.
Entity Resolution"같은 것을 가리키는 여러 표현"을 하나로 묶는 문제. Jellyfish는 LLM 병합으로 접근.

11참고 링크

더 파고들 곳

공식

저장소 안에서 꼭 볼 파일

배경 지식