비디오 스튜디오 (video-studio)
한 줄 요약: 동영상 = 대본 + 오디오 실제 재생 시간 + 확정적 프레임 함수 + ffmpeg 인코딩 + 자동 품질 검사. 화면과 사운드는 모두 코드로 생성되며,素材 없음, 재현 가능, 중단 및 재개 가능, 스크립트화된 검증 가능.
0. 불변의 법칙 (위반 시 완성본에 반드시 문제 발생)
- 오디오를 먼저, 화면은 나중에. 타임라인은 TTS 실제 재생 시간으로 역산해야 하며, 절대 글자 수로 추정하지 마세요. 글자 수로 만든 타임라인은 20번째 문장부터 눈에 띄게 어긋납니다.
- 모든 프레임은 순수 함수:
draw(t, frame) -> Image.random/time()은 금지, 무작위성은filmkit.hash2/hash3/vnoise를 사용하세요. 동일한 프레임 번호는 항상 동일한 이미지를 그려야 합니다. - 자막과 큰 글자는 안전 영역 안에 있어야 함: 본문
y ∈ [58, 974], 자막 줄y ∈ [872, 1030], 절대 1030을 초과하지 않음. - 원본 프레임 직접 인코딩: PIL 프레임 →
rawvideo파이프 →libx264. 중간 파일로cv2.VideoWriter를 사용하지 마세요 (2배 느리고 이차 손실 발생). - 오디오 믹싱은 numpy 샘플 수준 오버레이 사용, 수백 개의 입력을 ffmpeg 명령 하나에 전달하지 마세요 (Windows에서 비확정적 무음 발생).
- 배송 전 반드시 품질 검사 스크립트 실행 + 3장의 스틸 프레임 직접 확인 (
read_image사용). "이미지가 나온다"고 "문제없다"는 아닙니다. - 사운드는 화면을 따라감: 소리가 나야 하는 지점은 장면 코드에 큐 지점을 등록 (
K.CueBus/scenes.py의cues()훅), 사후에 수동으로 시간을 맞추지 마세요. 화면이 변경되면(체류 창 변경, 리듬 조정), 큐 지점도 자동으로 변경됩니다. - 잘 안 보이면 체류 시간을 추가하고, 전체 영상을 느리게 하지 마세요:
film.json의hold(전역) 또는lines[].hold(개별 문장)를 음성이 끝난 후에 추가하고, 배경 음악은bpm을 사용하여 마디를 정렬하세요.
0.5 【불변의 법칙】 먼저 제안하고, 그 다음에 제작 (사용자가 요구하지 않더라도)
사용자가 요구하지 않아도 반드시 먼저 방안을 제시해야 합니다. 이 스킬에서 가장 중요한 제품 동작이며, 권장 사항이 아닙니다. 이유: 외형이 프로그램에 의해 사용자를 대신해 선택되면, 사용자는 처음부터 끝까지 의사 결정에 참여하지 못하게 되고, 결과는 "모든 동영상이 비슷해 보인다"는 것입니다. 따라서 프로세스는 고정되어 있습니다: 현장에서 여러 방안 샘플링 → 사용자가 하나 선택 → 디자인 확정 → 그 다음 제작.
고정 동작 (새로운 완성본 제작 작업의 첫 번째 일):
python propose.py --brief "<주제/대상/시간/톤>" --script film.json --n 3
이 명령은 N개의 후보 방안을 생성하고 (각 방안 = 디자인 1개 + 스틸 프레임 4장 + 비용 + 다른 후보와의 디자인 거리),
work/proposal.json에 저장하며, "하나를 확인하세요 / 모두 마음에 안 들면 --revise"라는 메시지를 출력합니다. 전체 과정 오프라인, 1초 미만.
| 사용자 반응 | 수행할 작업 |
|---|---|
| "이걸로" / 하나 선택 | python propose.py --confirm --candidate B를 실행한 후에만 렌더링 허용 |
| "너무 차갑다/너무 화려하다/다른 목소리/템포가 더 빠르게" | python propose.py --revise "<원문>" —— 새 개정, 이전 개정은 diff 가능하도록 유지 |
| "다 안 돼, 다시" | 위와 동일, 요구 사항을 구체적으로 작성 (--revise 텍스트가 proposal.feedback에 저장됨) |
| 명확히 "네가 정해" | 그래도 먼저 방안 제시, 당신이 추천하는 하나를 선택하고 이유를 설명하여 사용자 동의 받기 |
엄격한 제약 조건:
propose.py --confirm전에,render.py의frames/all은 제안 게이트에 의해 차단됩니다 (기본 경고,--require-proposal시 실패).- 후보는 두 개 이상의 디자인 차원에서 서로 달라야 함 (색상 계열/모티프/전환/이징/타이포그래피/배경 음악/음성). 색상만 바꾸는 것은 다중 방안이 아닙니다,
diversity.vs[].n < 2는 플래그 지정됩니다. - 각 후보는 하나의 중국어 입안 (pitch) 이 있어야 하며, "이 방안이 달성하려는 비전"을 설명해야 함 —— "방안 A/방안 B"가 아닙니다.
- 내장 템플릿 없음:
design.json의 모든 필드는 개방되어 있습니다 (35개 필드 테마 + 5개 시각적 언어 시그니처 + 사운드/배경 음악/리듬), 색상은 연속 색상 공간의 임의의 한 점이며, 기성 외형 세트 중에서 고르는 것이 아닙니다. 각 제안은 현장에서 생성됩니다 – 동일한 brief를 연속 두 번 실행하면 다른 디자인을 얻습니다 (재현하려면--seed전달). - 작품 간 중복 제거 원장 (기본 활성화):
--confirm은 확정된 디자인을~/.dsh/video-studio/design-ledger.json에 기록하고 (최대 500개), 다음 제안 시 최근에 배송된 외형을 적극적으로 회피합니다 – 후보와 원장에 있는 어떤 영화와의 디자인 거리가 < 2차원이면 자동으로 nonce를 교체하여 재샘플링하고 경고합니다. 이는 "다른 영화는 반복하지 않는다"를 "무작위 의존"에서 "기억 있음"으로 바꾸는 메커니즘입니다.--show-ledger로 원장 조회,--no-ledger로 이번 실행 무시, 환경 변수VIDEO_STUDIO_LEDGER로 경로 변경 또는off설정으로 비활성화. 원장 읽기/쓰기 실패 시 항상 빈 테이블로 처리되며, 출시를 절대 차단하지 않음.
제안을 건너뛸 수 있는 유일한 경우: 순수 수리 / 순수 재렌더링 (내용 변경 없음, 디자인 변경 없음, 오디오/비디오 싱크 불일치만 수정하거나 재출력).
이 경우 --require-proposal 외의 방식으로 실행하고, 응답에 "이번에는 다음 이유로 제안하지 않았습니다..."라고 설명.
0.5.1 외형은 어디서 오는가: 두 가지 방법뿐
'내장 스타일'은 선택할 수 없습니다. film.json에 style 값을 쓰는 것 (auto / legacy / 특정 id)은
명확히 거부되고 두 단계 안내가 제공됩니다 – 대본이 외형을 결정하지 않으며, 외형은 work/design.json이 결정합니다.
마찬가지로, 스킬에는 THEMES/STYLES/VISUAL_LANGUAGES 테이블이 없고, presets/도 없습니다.
배경 음악은 musicgen.py가 매개변수에 따라 현장에서 편곡하고, scenes.draw_frame에도 레거시 브랜치가 없습니다.
특정 외형을 원한다면 두 가지 방법뿐입니다:
# ① 자연어로 요청 (must_have/avoid의 키워드 인식)
python propose.py --brief "기술 발표회, 다크 블루, 시네마스코프 비율" --script film.json
# ② 직접 design.json 작성 (모든 필드 개방, 색상은 임의의 RGB)
python render.py --script film.json --stage all # 전제 조건: work/design.json 이미 존재
상세 계약은 references/proposal.md 및 schemas/design.schema.json 참조.
1. 5단계 시작하기 (제로 카피: 스크립트, 템플릿 복사 불필요)
이 경로에는 '복사' 동작이 전혀 없습니다. 스킬 스크립트는 스킬 디렉토리 내에서 직접 실행됩니다. 프로젝트 디렉토리에는
film.json(및 실행 중 생성되는work/및out/)만 있으면 됩니다.
<SKILL_DIR>은 **스킬 로드 시skill도구가 반환한resourceBase.path**를 의미합니다. 플러그인으로 설치되면 이 플러그인 패키지 내skills/video-studio/입니다 (프로필의node_modules/@dsh-gis/dsh-video-studio-skill은 링크). 찾을 수 없으면 이름으로 위치 확인:find ~ -path '*/skills/video-studio/scripts/filmkit.py'.
<REPO>는 패키지 루트 (저장소 루트, 또는 설치 후 프로필의node_modules/@dsh-gis/dsh-video-studio-skill/)를 의미하며, 즉<SKILL_DIR>의 두 레벨 위입니다.tools/및schemas/는<REPO>아래에 있으며,<SKILL_DIR>아래에 있지 않습니다.
# 1) film.json 하나만 작성 (프로젝트 디렉토리에 수동으로 작성해야 하는 유일한 파일)
mkdir -p myfilm && cd myfilm
cat > film.json <<'JSON'
{
"title": "내 영화",
"lines": [
{"id": "l01", "kind": "hook", "chapter": "01 · 시작",
"text": "이것은 코드로 처음부터 생성된 화면과 소리입니다.", "sub": "素材 없음, 전체 코드 생성", "big": "素材 없음, 전체 코드 생성"},
{"id": "l02", "kind": "body", "chapter": "02 · 본문", "kicker": "PIPELINE",
"text": "전체 파이프라인은 다섯 단계뿐입니다: 시나리오 작성, 내레이션 생성, 타임라인 구성, 프레임별 렌더링, 오디오 믹싱 합성.",
"sub": "5단계 완성, 각 단계 저장", "big": "5단계 완성",
"items": [{"text": "시나리오 film.json"}, {"text": "내레이션"}, {"text": "타임라인"}, {"text": "프레임별 렌더링"}, {"text": "오디오 믹싱"}]},
{"id": "l03", "kind": "outro", "chapter": "99 · 마무리",
"text": "먼저 대본을 정하고, 다음으로 사운드를 정하고, 마지막으로 화면을 정합니다.", "sub": "순서가 노력보다 중요", "big": "순서가 노력보다 중요"}
]
}
JSON
# lines[].text = 낭독 텍스트 (자연스러운 구어체); sub = 화면 자막 (짧게); kind = hook/body/quote/outro/emphasis/calm
# 그림 스타일/배경 음악/음성/리듬은 모두 design이 결정합니다. film.json에는 **작성할 수 있는 style 필드가 없습니다**.
# 계약 검증 (선택 사항, 몇 초):
python <SKILL_DIR>/scripts/filmcheck.py film.json
# 2) 먼저 환경 자체 점검 (선택 사항; 69개 회귀 가드, 약 10–20초, 오프라인; 선택 모듈 누락 시 SKIP 표시)
python <SKILL_DIR>/scripts/selfcheck.py
# 3) 【불변의 법칙】 먼저 방안을 제시한 후, 그 다음 제작 – 동일한 brief라도 매번 다르므로 사용자가 반드시 확인해야 함
python <SKILL_DIR>/scripts/propose.py \
--brief "AI 대형 언어 모델의 3년, 30초 과학, 다크 블루" \
--script film.json --work work
# 3개의 후보 생성: 각각 디자인 1개 + 스틸 프레임 접촉 시트 (work/proposals/rev1/_compare.png) + 디자인 거리,
# 약 2초 결과, 전체 오프라인. 나란히 놓인 그림을 보고 하나 선택.
# 4) 후보 확인, 디자인 확정 (work/design.json) – 이후 렌더링은 이 디자인만 인식
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work
# 마음에 안 들면 다른 것으로: --revise "좀 더 차갑게, 장식 줄이고, 여성 음성으로 변경" (새 개정, 이전 개정은 diff용 유지)
# 특정 실행 재현: 첫 번째 제안 시 --seed 42 추가 (seed 미전달 = 매번 새 nonce, 이것이 '매 영화 다름'의 기원)
# 5) 스틸 프레임 먼저 확인 후 전체 영화 제작; --scenes를 작성하지 않으면 스킬 내장 렌더러 사용
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills \
--stills "l01:3,l02:5,l03:2.5"
# → work/stills/_sheet.png: read_image로 직접 확인, 건너뛰지 마세요
python <SKILL_DIR>/scripts/render.py --script film.json --work work \
--out out/film.mp4 --stage all
# TTS→레이아웃→배경 음악→음향 효과→오디오 믹싱→프레임별→인코딩→품질 검사 올인원; 중간 산출물 전부 work/에, 중단 및 재개 가능.
--scenes에 관하여 (제로 카피의 핵심): render.py에 --scenes를 작성하지 않을 때의 위치 순서는
**① 시나리오와 동일한 디렉토리의 scenes.py → ② 스킬 내장 assets/scenes.py**입니다. 따라서 기본적으로 아무것도 작성하거나
복사할 필요 없이, 내장 렌더러가 직접 영화를 제작합니다. 화면을 변경하고 싶을 때만 시나리오와 같은 디렉토리에 자신만의 scenes.py를 넣으세요
(아래 draw_frame 및 선택적 cues() 구현) – 자동으로 발견됩니다. 명시적으로 --scenes my_scenes.py를 사용할 수도 있습니다.
내장 렌더러 소스 코드가 가장 좋은 예시입니다: <SKILL_DIR>/assets/scenes.py.
# 실제 시그니처 (6개 매개변수, 하나라도 빠지면 오류): `scenes.draw_frame`
def draw_frame(scene, t_local, dur, t_global, script, frame):
# scene : 타임라인에 있는 이 세그먼트의 딕셔너리 (id / kind / scene_type / sub / big / items / stats ...)
# t_local : 세그먼트 내 시간 (초, 0..dur) – 입장/지속 효과 모두 이에 따라 세그먼트 내에서 진행
# dur : 이 세그먼트의 총 지속 시간 (초)
# t_global : 전체 영화 시간 (초) – 세그먼트를 가로지르는 지속 효과 (느린 줌, 브리싱)에 사용
# script : 전체 시나리오/계획 딕셔너리 (plan, 테마 색상, words 등; 캐시 있음, 원본 수정 금지)
# frame : 절대 프레임 번호 (정수) – 확정적 노이즈/위상이 필요할 때 사용, time() 사용 금지
return img # PIL.Image, 반드시 1920x1080 RGB (K.new_frame()으로 시작하는 것이 가장 안정적)
사용자 정의
scenes.py작성 시 먼저<SKILL_DIR>/assets/scenes.py의 드로어 하나를 복사하세요 (_draw_hero등). 순수 함수, 캐시된 레이아웃, 사전 베이킹된 베이스 플레이트 등의 함정을 이미 우회한 코드입니다.
왜 먼저 제안하는가: '내장 스타일'을 선택할 수 없으며, 외형은 design.json이 결정합니다.
동일한 brief를 연속 두 번 실행하면 다른 디자인을 얻습니다 (색상/모티프/전환/배경 음악/음성이 모두 다를 수 있음).
따라서 프로세스는 "방안 생성 → 사람 확인 → 확정 → 제작"이어야 합니다. 확인 없이 렌더링하면 render.py가 제안 경고를 표시합니다
(--require-proposal 추가 시 직접 실패). 디자인 계약은 references/proposal.md 참조.
단계별 실행 (각 stage는 자신의 작업만 수행하며, 저장 후 개별적으로 재실행 가능):
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage style # 확인된 디자인 파싱, 테마/시각적 언어/타이포그래피 순환/자막 스타일 출력
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage tts # 내레이션만 합성 + 실제 재생 시간 (입력 검증 포함 캐시)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage layout # 타임라인만 구성 + 구도 선택, timeline.json / sub.srt / storyboard.json 저장
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage cues # 큐 포인트만 생성 -> work/cues.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage score # 배경 음악만 생성 -> work/music.wav (musicgen 현장 편곡)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage sfx # 음향 효과 트랙만 생성 (sfxkit, cues.json 기반)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage audio # 오디오 믹싱만 (내레이션+배경 음악+음향 효과, 내레이션 우선)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames # 프레임만 렌더링 (--engine pil|web, --jobs N 분할 병렬)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage mux # 합성만 (비디오 스트림 복사, 2패스 loudnorm)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage qa # 품질 검사만 (읽기 전용 입력, work/qa/에만 쓰기)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills # 마스터 스틸 프레임 + 접촉 시트만 렌더링
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate stills # 수동 서명 게이트 (work/signoff.json에 쓰기)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage explain # "어떤 단계가 구식이고, 이유와 실행해야 할 것"만 보고, 어떤 stage도 실행하지 않음
--stage all의 전체 순서: style → tts → layout → cues → score → sfx → audio → frames → mux → qa.
어느 단계에서든 문제가 발생하면 해당 단계에서 중단되며, 잘못된 입력을 가지고 계속 실행되지 않습니다.
렌더링 시작 전 비용 계산 (선택 사항; estimate.py는 저장소 도구, <REPO>로 호출):
python <REPO>/tools/estimate.py --script film.json --jobs 4
이는 프레임 수/각 단계 소요 시간/디스크/분할 권장 사항을 제공하며 (--json으로 기계 판독 가능), 구간 상한이지 약속이 아닙니다.
work/timeline.json이 있으면 실제 지속 시간을 사용하고, 없으면 글자 수로 대략 추정합니다. 전체 영화 VMAF를 생략하려면 film.json에
"qa": {"vmaf": {"mode": "off"}}를 작성하세요 (estimate와 render 모두 이 필드를 읽고, G11은 skipped로 기록).
배송 증명서 확인 후 서명 검증:
cat out/QC-REPORT.md # 게이트 매트릭스 pass / fail / skipped 세 등급; skipped = 이번에 실행되지 않음, 통과 의미 아님
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate release
서명 검증은 콘텐츠 지문에 바인딩됩니다: 대본 / scenes.py / 디자인을 변경하면 이전 서명은 자동으로 무효화되며, 다시 보고 다시 서명해야 합니다.
배송 전 강제 (skipped도 실패로 간주): --strict-gates 추가.
장편 렌더링 (분할 병렬 + 중단 및 재개):
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4 --resume
분할은
work/segments/에 저장되며,seg_<시작_프레임>_<끝_프레임>.mp4형식으로 명명됩니다. 연결 전에 갭을 명시적으로 감지하고, 누락된 블록은 직접 오류를 발생시키며, "일부 세그먼트가 없어도 영화를 만들 수" 없습니다. 기본 청크는총_프레임_수 / jobs / 4를 정수 초로 반올림하여 결정됩니다 (jobs=4, 900프레임일 때 약 8초 분할).film.json의chunk_frames로 재정의 가능.대사 / scenes.py / 디자인 / 인코딩 매개변수 변경 후,
--resume은 이전 분할의 재사용을 거부합니다 (work/segments/_plan.json에 계획 지문 저장), 그리고rm -rf work/segments를提示합니다 – 이는 의도적인 것입니다: 이전 분할과 새 타임라인을 결합하면 "화면과 내레이션이 맞지 않는" 영화가 만들어지며, 지속 시간 게이트로 감지할 수 없습니다.
2. 표준 8단계 (P0–P7)
| 단계 | 산출물 | 주요 동작 | 일반적인 오류 |
|---|---|---|---|
| P0.5 디자인 확정 | work/design.json + work/style.json | propose.py로 방안 생성 → --confirm으로 디자인 확정; --stage style는 확인된 디자인만 파싱하고 테마/시각적 언어/타이포그래피 순환/자막 출력 | 건너뛰기 → render.py 오류 (design 없으면 외형 없음); 확인 없이 렌더링 시 제안 경고 |
| P0 기준 설정 | CONVENTIONS.md | 해상도/프레임레이트/지속 시간/BPM→프레임 변환을 한 번에 계산하여 고정, 전체 프로젝트에서 동일한 참조 사용 | 각 파일이 150과 같은 매직 넘버를 자체적으로 작성 → 장면 경계 정렬 불일치 |
| P1 대본 작성 | film.json | 각 문장을 text(낭독)와 sub(자막)로 분리; 각 단락에 kind; 선택적 lines[].type로 구도 고정 | 하나의 필드를 두 용도로 사용 → 자막이 너무 길거나 읽기에 어색함; style 작성 금지 (더 이상 화면에 영향을 미치지 않음, 외형은 work/design.json이 결정) |
| P2 사실 확인 | FACTS.md | 화면에 표시되는 모든 숫자/연도는 출처 번호가 있어야 함; "표시 금지" 블랙리스트 작성 | 이 단계 건너뛰기 → 화면과 내레이션 숫자가 충돌, 댓글 첫 번째가 당신이 틀렸다고 지적 |
| P3 TTS | work/tts/*.mp3 + tts.json | edge-tts로 문장별 합성, ffmpeg -f f32le로 실제 재생 시간 측정, 디스크 캐시 | 글자 수로 시간 추정 → 자막 어긋남 |
| P4 타임라인 구성 | work/timeline.json + sub.srt | dur = lead + 실제 음성 + tail, 문장은 반드시 해당 문단 내에 있어야 함 | 문단이 너무 짧음 → 내레이션이 다음 문단에서 잘림 |
| P5 오디오 믹싱 | work/audio.wav | numpy 버스: 내레이션 + 배경 음악 + 음향 효과를 샘플 단위로 오버레이 | ffmpeg 다중 입력 믹싱 → 비확정적 무음 |
| P6 마스터 | 3장의 스틸 프레임 | 먼저 3프레임을 렌더링하여 구도/색상/글자 두께 확인, 결정 후 전체 영화 렌더링 | 전체 영화 직접 렌더링 → 36000프레임 후에야 폰트가 나쁘다는 것을 알게 됨 |
| P7 완성본 | out/film.mp4 | rawvideo→libx264 CRF18 + 2패스 loudnorm으로 -16 LUFS | 비트레이트过低/음량 -25 LUFS → "초안 같음" |
참고:
filmkit/render.py의 기본값은crf=18입니다.assets/film.json은 테스트 픽스처일 뿐입니다 (전체 체인과 골드 프레임/게이트 회귀 테스트용, 복사할 템플릿이 아님), 17로 설정되어 있습니다. 직접 대본을 작성할 때crf를 작성하지 않으면 18입니다. 더 선명한 텍스트를 원하면 17을 작성할 수 있습니다 (볼륨 약 +30%).
3. 렌더링 엔진 선택 방법
| 시나리오 | 선택 | 실제 속도 | 적합 |
|---|---|---|---|
| 그림/텍스트/데이터/과학/UI 스타일 | PIL (numpy+cv2 보조) — filmkit 기본 | 40–80 fps @1080p30 (예시 75) | 대부분의 중국어 과학 영화, 보고서를 비디오로 변환 |
| 부드러운 벡터/입자/3D/종이 질감 필요 | 헤드리스 브라우저 Canvas2D/WebGL + CDP 스크린샷 → image2pipe (webengine.py) | 80–130 ms/프레임 (내장 그래픽, PNG 스크린샷) | 다큐멘터리 수준 장편, 복잡한 시각적 언어 필요 |
| 순수 데이터 애니메이션 | manim (설치된 경우) | 시나리오에 따라 다름 | 수학/공식 유도 |
위 표 속도는 단일 시스템 유휴 실제 측정 기준입니다 (시스템 변경, 무거운 화면 변경 시 달라짐; §12 참조). 구간을 보장으로 간주하지 마세요.
디자인은 PIL 엔진만 다룹니다 (
engine: pil, 기본값):work/design.json의 색상/폰트/레이아웃/모티프는assets/scenes.py가 읽습니다.--engine web의 페이지는 직접 작성해야 합니다 (assets/web/는 실행 가능한 예시 페이지일 뿐, 제작 템플릿이 아님), 화면은 디자인을 따르지 않습니다. 하지만 배경 음악과 내레이션은 그대로design.json을 따릅니다 (렌더링 엔진에 의존하지 않음).
기본 PIL: 의존성 적음 (numpy+Pillow만 있으면 됨), 시작 빠름, 확정성 강함, 디버깅 비용 낮음. "원샷 원킬 입자 변형 / 실시간 셰이더 질감"이 필요한 경우에만 브라우저 방식 사용:
cp -r <SKILL_DIR>/assets/web ./web # 페이지 템플릿 (네이티브 Canvas2D, 제로 서드파티 라이브러리)
pip install playwright && python -m playwright install chromium # 웹 엔진 사용 시에만 필요
python render.py ... --engine web --stage cues # 페이지에서 큐 포인트 내보내기
python render.py ... --engine web --stage frames --jobs 3 --resume
python webengine.py textqa ./web/index.html # 텍스트 중복/프레임 이탈/번쩍임/한 번도 나타나지 않음 검사
두 경로의 선택 (먼저 명확히 생각하고 선택하세요, 엔진 변경 시 화면을 다시 만들어야 함):
PIL (engine: pil) | 브라우저 (engine: web) | |
|---|---|---|
| 의존성 | numpy + Pillow (+cv2 선명도 비교) | + playwright + Chromium/Edge |
| 속도 | 40–80 프레임/초 | 8–12 프레임/초 (스크린샷+인코딩, 한 자릿수 느림) |
| 화면 언어 | 비트맵 합성, 그라데이션, 기하학, 텍스트 | Canvas2D/WebGL: 입자, 변형, 셰이더, 벡터 애니메이션 |
| 확정성 | 자연스러움 (순수 함수 그림) | 타임라인을 정확히 f/FPS로 seek하여 보장 (window.renderFrame) |
| 디버깅 | 프레임 하나 수정 후 실행 | 브라우저에서 index.html?dev=1&t=초로 직접 확인, 버그 발견 쉬움 |
| 적합 | 그림/텍스트/데이터/과학/홍보 영상 | 다큐멘터리 수준 장편, 원샷 원킬 시각적 언어 필요 |
브라우저 엔진의 세 가지 엄격한 제약 조건 (webengine.py에서 이미 구현됨, 직접 페이지를 작성할 때도 따라야 함):
- 모든 트위닝은 일시 정지된 하나의 메인 타임라인에 배치되며, f번째 프레임 렌더링은
f / FPS초로 seek하는 것임; 재생에 의존 금지. - 루프/입자/파도와 같은 프로그래밍 방식 콘텐츠는 "시간 기준 직접 계산" 업데이터 (
update(fn))를 사용하며,setInterval누적 금지. - 페이지는 반드시
window.ready / DURATION / renderFrame(f) / getCues()를 노출해야 함; CDN 연결 금지, 네트워크 요청 금지.
브라우저 세션 간 바이트 수준 재현성 (충돌 후 재시작, 여러 번 렌더링하여 diff, 품질 검사에서 두 이미지 비교)이 필요할 때 확정성 모드 활성화:
--deterministic 또는 환경 변수 WEBENGINE_DETERMINISTIC=1 (GPU 래스터화 비활성화, 8/8 세션 일치 실제 확인,
비용 약 +22%/프레임). 기본 비활성화: 동일 세션 내에서는 원래 일치하며, 세션 간에는 ≤4 픽셀, ±1 레벨의 앤티앨리어싱 차이만 있습니다.
4. 화질 체크리스트 (이 항목들이 "전문가 수준 영화처럼 보이는지" 결정)
- 밝기와 계조: 어두운 배경 (
#0a1420) + 상단 밝은 악센트; 순수 흑백 사용 금지, 사용자 화면이 그라데이션을 먹어버림. - 글자 두께와 크기: 큰 글자 84–96px, 부제목 34–38px, 자막 46px; 중국어는
msyhbd.ttc사용 (Microsoft YaHei Bold). - 자막: ≤ 2줄, 한 줄 약 24자, 반투명 배경 포함,
y ≥ 872. 실제 줄바꿈은 픽셀 너비를 기준으로 합니다 (fit_sub_lines가max_w=1560 px기준으로 자동 줄바꿈). 24자는 "대략적인 상한"일 뿐 하드 검증이 아닙니다 ——SUB_MAX_CHARS=24라는 상수는 현재 어떤 코드에서도 참조되지 않으며, 이를 막아줄 것이라고 기대하지 마세요. - 리듬: 3–6초마다 시각적 변화가 있어야 함 (새 카드 / 이동 / 숫자 스크롤); 6초 이상 정적이면 시청자가 이탈.
- 카메라: 3–15% 느린 줌만 사용, 큰 확대/축소 금지.
- 색보정: 전체 영화에 하나의 주색 + 하나의 강조색, 4개 이상의 색상 사용 금지.
- 오디오: 마스터 -16 LUFS / 실제 피크 ≤ -1 dBTP; 내레이션이 간격보다 12 dB 이상 큼; AAC 전에 1.4 dB 여유 확보.
5. 사운드: 배경 음악 + 음향 효과 + 큐 포인트
이 섹션의 아이디어는 실제 완성본 (
ai-history-film)의 회고에서 비롯되었습니다: 사운드가 좋지 않은 것은 "믹싱이 충분히 크지 않아서"가 아니라 "사운드가 화면을 인식하지 못하기 때문" 입니다. 해결 순서는 항상: 먼저 사운드가 화면에서 무슨 일이 일어나는지 알게 한 다음 (큐 포인트), 음색에 대해 논하고, 마지막으로 마스터링입니다.
5.1 사운드의 세 층
| 층 | 담당자 | 저장 | 설명 |
|---|---|---|---|
| 내레이션 | filmkit.tts (edge-tts) | work/tts/*.mp3 | 유일한 지속 시간 권위자: 타임라인이 이를 통해 역산됨 |
| 배경 음악 | musicgen.py (매개변수화 편곡) | work/music.wav | 디자인의 bpm/key/scale/mood/seed + 편곡 매개변수에 따라 현장 생성; 기성 음악 스타일 선택 불가 |
| 음향 효과 | sfxkit.py (물리 모델링) | work/sfx.wav | cues.json에 등록된 큐 포인트에 따라 개별 합성 |
세 버스는 --stage audio에서 합성됩니다: 음향 효과가 울리면 배경 음악이 자동으로 양보 (duck_db 기본 4 dB,
시네마/에픽 5.5 dB – 3 dB의 양보량은 단락 전환 시 배경 음악이 음향 효과와 거의 동일한 레벨이 되어 청감이 뭉개짐),
마지막으로 ffmpeg 2패스 loudnorm에 맡겨 -16 LUFS / 실제 피크 ≤ -1 dBTP로 만듦. 직접 numpy에서 최종 음량 표준화를 수행하지 마세요 –
당신은 loudnorm을 가지고 있으며, 이는 수동 정규화보다 훨씬 정확하고, 실제 피크는 게이트 통과 조건입니다.
영화 내 분리: 배경 음악은 단락 경계에서 원래 전환음이 있습니다 (편곡기가 작성한 swell / riser / sub_boom), sfx 트랙은 동일한 경계에서 whoosh/riser를 반복 재생하지 않습니다 –
--stage sfx는 [경계 - 2.2 s, 경계] 내에 있는 전환 유형 cue에 대해 자동으로 -3 dB를 적용합니다 (sfx.duck_transitions=
…
[truncated for size; full content provided in the translation above]